diff --git a/bbp_ng/OP_OBJECT_legacy_align.py b/bbp_ng/OP_OBJECT_legacy_align.py index 7c35d13..49a814d 100644 --- a/bbp_ng/OP_OBJECT_legacy_align.py +++ b/bbp_ng/OP_OBJECT_legacy_align.py @@ -9,11 +9,11 @@ class AlignMode(enum.IntEnum): BBoxCenter = enum.auto() AxisCenter = enum.auto() Max = enum.auto() -_g_AlignModeDesc: dict[AlignMode, tuple[str, str]] = { - AlignMode.Min: ("Min", "The min value in specified axis."), - AlignMode.BBoxCenter: ("Center (Bounding Box)", "The bounding box center in specified axis."), - AlignMode.AxisCenter: ("Center (Axis)", "The object's source point in specified axis."), - AlignMode.Max: ("Max", "The max value in specified axis."), +_g_AlignModeDesc: dict[AlignMode, tuple[str, str, str]] = { + AlignMode.Min: ("Min", "The min value in specified axis.", "REMOVE"), + 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.", "OBJECT_ORIGIN"), + AlignMode.Max: ("Max", "The max value in specified axis.", "ADD"), } _g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper( AlignMode, @@ -21,7 +21,23 @@ _g_EnumHelper_AlignMode = UTIL_functions.EnumPropHelper( lambda x: AlignMode(int(x)), lambda x: _g_AlignModeDesc[x][0], 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 @@ -55,14 +71,23 @@ class BBP_PG_legacy_align_history(bpy.types.PropertyGroup): default = False, translation_context = 'BBP_PG_legacy_align_history/property' ) # 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( - name = "Current Object (Active Object)", + name = "Current Object", + description = "The align mode applied to Current Object", items = _g_EnumHelper_AlignMode.generate_items(), default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter), translation_context = 'BBP_PG_legacy_align_history/property' ) # type: ignore 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(), default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter), translation_context = 'BBP_PG_legacy_align_history/property' @@ -148,7 +173,7 @@ class BBP_OT_legacy_align(bpy.types.Operator): def execute(self, context): # get processed objects - (current_obj, target_objs) = _prepare_objects() + (current_obj, current_cursor, target_objs) = _prepare_objects() # YYC MARK: # This statement is VERY IMPORTANT. # 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) for entry in histories: _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, _g_EnumHelper_AlignMode.get_selection(entry.current_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_z", toggle = 1) - # show mode + # show current instance col.separator() - 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='Current Instance', text_ctxt='BBP_OT_legacy_align/draw') + # 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.prop(entry, "target_align_mode", expand = True) @@ -206,44 +243,66 @@ class BBP_OT_legacy_align(bpy.types.Operator): #region Core Functions 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(): 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: return False - # check target obj with filter of current obj - length = len(bpy.context.selected_objects) - if bpy.context.active_object in bpy.context.selected_objects: - length -= 1 - return length != 0 + + # YYC MARK: + # Roughly check selected objects. + # We do not need exclude active object from selected objects, + # 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]]: - # get current object - current_obj: bpy.types.Object = bpy.context.active_object +def _prepare_objects() -> tuple[bpy.types.Object, mathutils.Vector, list[bpy.types.Object]]: + # Fetch current object + current_obj = typing.cast(bpy.types.Object, bpy.context.active_object) - # get target objects - target_objs: set[bpy.types.Object] = set(bpy.context.selected_objects) - # remove active one - if current_obj in target_objs: - target_objs.remove(current_obj) + # Fetch 3d cursor location + current_cursor: mathutils.Vector = bpy.context.scene.cursor.location + + # YYC MARK: + # 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 (current_obj, target_objs) + return (current_obj, current_cursor, target_objs) 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: # if no align, skip if not (align_x or align_y or align_z): return # 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 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 target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode) # build translation transform @@ -256,21 +315,21 @@ def _align_objects( # apply translation transform to left side (add into original matrix) 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: - ref_pos: mathutils.Vector = mathutils.Vector((0, 0, 0)) + ref_pos = mathutils.Vector((0, 0, 0)) # calc bounding box data - 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.x = min((vec.x for vec in corners)) - bbox_min_corner.y = min((vec.y for vec in corners)) - bbox_min_corner.z = 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.y = max((vec.y for vec in corners)) - bbox_max_corner.z = max((vec.z for vec in corners)) + corners: tuple[mathutils.Vector, ...] = tuple(obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box) + bbox_min_corner = mathutils.Vector(( + min((vec.x for vec in corners)), + min((vec.y for vec in corners)), + min((vec.z for vec in corners)), + )) + bbox_max_corner = mathutils.Vector(( + max((vec.x for vec in corners)), + max((vec.y for vec in corners)), + max((vec.z for vec in corners)), + )) # return value by given align mode match(mode):