feat: allow 3D Cursor as align source in legacy align operator.

- allow 3D Cursor as align source in legacy align operator. this feature is requested by Zzq.
- add icon for legacy align.
This commit is contained in:
2025-08-01 14:02:26 +08:00
parent a2b8f41a21
commit 1383e87104

View File

@ -9,11 +9,11 @@ class AlignMode(enum.IntEnum):
BBoxCenter = enum.auto() BBoxCenter = enum.auto()
AxisCenter = enum.auto() AxisCenter = enum.auto()
Max = enum.auto() Max = enum.auto()
_g_AlignModeDesc: dict[AlignMode, tuple[str, str]] = { _g_AlignModeDesc: dict[AlignMode, tuple[str, str, str]] = {
AlignMode.Min: ("Min", "The min value in specified axis."), AlignMode.Min: ("Min", "The min value in specified axis.", "REMOVE"),
AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis."), AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis.", "SHADING_BBOX"),
AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis."), AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis.", "OBJECT_ORIGIN"),
AlignMode.Max: ("Max", "The max value in specified axis."), AlignMode.Max: ("Max", "The max value in specified axis.", "ADD"),
} }
_g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper( _g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper(
AlignMode, AlignMode,
@ -21,7 +21,23 @@ _g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper(
lambda x: AlignMode(int(x)), lambda x: AlignMode(int(x)),
lambda x: _g_AlignModeDesc[x][0], lambda x: _g_AlignModeDesc[x][0],
lambda x: _g_AlignModeDesc[x][1], lambda x: _g_AlignModeDesc[x][1],
lambda _: '' lambda x: _g_AlignModeDesc[x][2]
)
class CurrentInstance(enum.IntEnum):
ActiveObject = enum.auto()
Cursor = enum.auto()
_g_CurrentInstanceDesc: dict[CurrentInstance, tuple[str, str, str]] = {
CurrentInstance.ActiveObject: ("Active Object", "Use Active Object as Current Object", "OBJECT_DATA"),
CurrentInstance.Cursor: ("3D Cursor", "Use 3D Cursor as Current Object", "CURSOR"),
}
_g_EnumHelper_CurrentInstance = UTIL_functions.EnumPropHelper(
CurrentInstance,
lambda x: str(x.value),
lambda x: CurrentInstance(int(x)),
lambda x: _g_CurrentInstanceDesc[x][0],
lambda x: _g_CurrentInstanceDesc[x][1],
lambda x: _g_CurrentInstanceDesc[x][2]
) )
#endregion #endregion
@ -55,14 +71,23 @@ class BBP_PG_legacy_align_history(bpy.types.PropertyGroup):
default = False, default = False,
translation_context = 'BBP_PG_legacy_align_history/property' translation_context = 'BBP_PG_legacy_align_history/property'
) # type: ignore ) # type: ignore
current_instance: bpy.props.EnumProperty(
name = "Current Instance",
description = "Decide which instance should be used as Current Object",
items = _g_EnumHelper_CurrentInstance.generate_items(),
default = _g_EnumHelper_CurrentInstance.to_selection(CurrentInstance.ActiveObject),
translation_context = 'BBP_PG_legacy_align_history/property'
) # type: ignore
current_align_mode: bpy.props.EnumProperty( current_align_mode: bpy.props.EnumProperty(
name = "Current Object (Active Object)", name = "Current Object",
description = "The align mode applied to Current Object",
items = _g_EnumHelper_AlignMode.generate_items(), items = _g_EnumHelper_AlignMode.generate_items(),
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter), default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
translation_context = 'BBP_PG_legacy_align_history/property' translation_context = 'BBP_PG_legacy_align_history/property'
) # type: ignore ) # type: ignore
target_align_mode: bpy.props.EnumProperty( target_align_mode: bpy.props.EnumProperty(
name = "Target Objects (Selected Objects)", name = "Target Objects",
description = "The align mode applied to Target Objects (selected objects except active object if Current Instance is active object)",
items = _g_EnumHelper_AlignMode.generate_items(), items = _g_EnumHelper_AlignMode.generate_items(),
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter), default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
translation_context = 'BBP_PG_legacy_align_history/property' translation_context = 'BBP_PG_legacy_align_history/property'
@ -148,7 +173,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
def execute(self, context): def execute(self, context):
# get processed objects # get processed objects
(current_obj, target_objs) = _prepare_objects() (current_obj, current_cursor, target_objs) = _prepare_objects()
# YYC MARK: # YYC MARK:
# This statement is VERY IMPORTANT. # This statement is VERY IMPORTANT.
# If this statement is not presented, Blender will return identity matrix # If this statement is not presented, Blender will return identity matrix
@ -162,7 +187,8 @@ class BBP_OT_legacy_align(bpy.types.Operator):
histories = UTIL_functions.CollectionVisitor(self.align_history) histories = UTIL_functions.CollectionVisitor(self.align_history)
for entry in histories: for entry in histories:
_align_objects( _align_objects(
current_obj, target_objs, _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance),
current_obj, current_cursor, target_objs,
entry.align_x, entry.align_y, entry.align_z, entry.align_x, entry.align_y, entry.align_z,
_g_EnumHelper_AlignMode.get_selection(entry.current_align_mode), _g_EnumHelper_AlignMode.get_selection(entry.current_align_mode),
_g_EnumHelper_AlignMode.get_selection(entry.target_align_mode) _g_EnumHelper_AlignMode.get_selection(entry.target_align_mode)
@ -185,10 +211,21 @@ class BBP_OT_legacy_align(bpy.types.Operator):
row.prop(entry, "align_y", toggle = 1) row.prop(entry, "align_y", toggle = 1)
row.prop(entry, "align_z", toggle = 1) row.prop(entry, "align_z", toggle = 1)
# show mode # show current instance
col.separator() col.separator()
col.label(text='Current Object (Active Object)', text_ctxt='BBP_OT_legacy_align/draw') col.label(text='Current Instance', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "current_align_mode", expand = True) # it should be shown in horizon so we create a new sublayout
row = col.row()
row.prop(entry, 'current_instance', expand=True)
# show instance and mode
col.separator()
# only show current object mode if current instance is active object,
# because there is no mode for 3d cursor.
current_instnce = _g_EnumHelper_CurrentInstance.get_selection(entry.current_instance)
if current_instnce == CurrentInstance.ActiveObject:
col.label(text='Current Object (Active Object)', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "current_align_mode", expand = True)
col.label(text='Target Objects (Selected Objects)', text_ctxt='BBP_OT_legacy_align/draw') col.label(text='Target Objects (Selected Objects)', text_ctxt='BBP_OT_legacy_align/draw')
col.prop(entry, "target_align_mode", expand = True) col.prop(entry, "target_align_mode", expand = True)
@ -206,44 +243,66 @@ class BBP_OT_legacy_align(bpy.types.Operator):
#region Core Functions #region Core Functions
def _check_align_requirement() -> bool: def _check_align_requirement() -> bool:
# if we are not in object mode, do not do legacy align # If we are not in object mode, do not do legacy align
if not UTIL_functions.is_in_object_mode(): if not UTIL_functions.is_in_object_mode():
return False return False
# check current obj # YYC MARK:
# We still need to check active object (as current object)
# although we can choose align with active object or 3d cursor.
# Because we can not make any promise that user will
# select Active Object or 3D Cursor as current object before executing this operator.
if bpy.context.active_object is None: if bpy.context.active_object is None:
return False return False
# check target obj with filter of current obj
length = len(bpy.context.selected_objects) # YYC MARK:
if bpy.context.active_object in bpy.context.selected_objects: # Roughly check selected objects.
length -= 1 # We do not need exclude active object from selected objects,
return length != 0 # because active object may be moved when 3D Cursor is current object.
if len(bpy.context.selected_objects) == 0:
return False
return True
def _prepare_objects() -> tuple[bpy.types.Object, set[bpy.types.Object]]: def _prepare_objects() -> tuple[bpy.types.Object, mathutils.Vector, list[bpy.types.Object]]:
# get current object # Fetch current object
current_obj: bpy.types.Object = bpy.context.active_object current_obj = typing.cast(bpy.types.Object, bpy.context.active_object)
# get target objects # Fetch 3d cursor location
target_objs: set[bpy.types.Object] = set(bpy.context.selected_objects) current_cursor: mathutils.Vector = bpy.context.scene.cursor.location
# remove active one
if current_obj in target_objs: # YYC MARK:
target_objs.remove(current_obj) # Fetch target objects and do NOT remove active object from it.
# because active object will be moved when current instance is 3D Cursor.
target_objs: list[bpy.types.Object] = bpy.context.selected_objects[:]
# return value # return value
return (current_obj, target_objs) return (current_obj, current_cursor, target_objs)
def _align_objects( def _align_objects(
current_obj: bpy.types.Object, target_objs: set[bpy.types.Object], current_instance: CurrentInstance,
current_obj: bpy.types.Object, current_cursor: mathutils.Vector, target_objs: list[bpy.types.Object],
align_x: bool, align_y: bool, align_z: bool, current_mode: AlignMode, target_mode: AlignMode) -> None: align_x: bool, align_y: bool, align_z: bool, current_mode: AlignMode, target_mode: AlignMode) -> None:
# if no align, skip # if no align, skip
if not (align_x or align_y or align_z): if not (align_x or align_y or align_z):
return return
# calc current object data # calc current object data
current_obj_ref: mathutils.Vector = _get_object_ref_point(current_obj, current_mode) current_obj_ref: mathutils.Vector
match current_instance:
case CurrentInstance.ActiveObject:
current_obj_ref = _get_object_ref_point(current_obj, current_mode)
case CurrentInstance.Cursor:
current_obj_ref = current_cursor
# process each target obj # process each target obj
for target_obj in target_objs: for target_obj in target_objs:
# YYC MARK:
# If we use active object as current instance, we need exclude it from target objects,
# because there is no pre-exclude considering the scenario that 3D Cursor is current instance.
if current_instance == CurrentInstance.ActiveObject and current_obj == target_obj:
continue
# calc target object data # calc target object data
target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode) target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode)
# build translation transform # build translation transform
@ -256,21 +315,21 @@ def _align_objects(
# apply translation transform to left side (add into original matrix) # apply translation transform to left side (add into original matrix)
target_obj.matrix_world = target_obj_translation_matrix @ target_obj.matrix_world target_obj.matrix_world = target_obj_translation_matrix @ target_obj.matrix_world
bpy.context.scene.update_tag
def _get_object_ref_point(obj: bpy.types.Object, mode: AlignMode) -> mathutils.Vector: def _get_object_ref_point(obj: bpy.types.Object, mode: AlignMode) -> mathutils.Vector:
ref_pos: mathutils.Vector = mathutils.Vector((0, 0, 0)) ref_pos = mathutils.Vector((0, 0, 0))
# calc bounding box data # calc bounding box data
corners: tuple[mathutils.Vector] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box) corners: tuple[mathutils.Vector, ...] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box)
bbox_min_corner: mathutils.Vector = mathutils.Vector((0, 0, 0)) bbox_min_corner = mathutils.Vector((
bbox_min_corner.x = min((vec.x for vec in corners)) min((vec.x for vec in corners)),
bbox_min_corner.y = min((vec.y for vec in corners)) min((vec.y for vec in corners)),
bbox_min_corner.z = min((vec.z for vec in corners)) min((vec.z for vec in corners)),
bbox_max_corner: mathutils.Vector = mathutils.Vector((0, 0, 0)) ))
bbox_max_corner.x = max((vec.x for vec in corners)) bbox_max_corner = mathutils.Vector((
bbox_max_corner.y = max((vec.y for vec in corners)) max((vec.x for vec in corners)),
bbox_max_corner.z = max((vec.z for vec in corners)) max((vec.y for vec in corners)),
max((vec.z for vec in corners)),
))
# return value by given align mode # return value by given align mode
match(mode): match(mode):