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:
@ -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
|
||||
|
||||
def _prepare_objects() -> tuple[bpy.types.Object, set[bpy.types.Object]]:
|
||||
# get current object
|
||||
current_obj: bpy.types.Object = bpy.context.active_object
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
return True
|
||||
|
||||
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)
|
||||
|
||||
# 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):
|
||||
|
Reference in New Issue
Block a user