BallanceBlenderHelper/bbp_ng/OP_OBJECT_legacy_align.py
yyc12345 f5c50ae079 fix: fix legacy align issue.
- fix issus that legacy align can not align object with non-1 scale factors.
- refactor some functions in legacy align to have better looking.
- fix issus that legacy fail to align objects since second executing by forcely updating view layer.
2024-09-07 20:34:22 +08:00

281 lines
11 KiB
Python

import bpy, mathutils
import enum, typing
from . import UTIL_functions
#region Align Mode
class AlignMode(enum.IntEnum):
Min = enum.auto()
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_EnumHelper_AlignMode: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelper(
AlignMode,
lambda x: str(x.value),
lambda x: AlignMode(int(x)),
lambda x: _g_AlignModeDesc[x][0],
lambda x: _g_AlignModeDesc[x][1],
lambda _: ''
)
#endregion
#region Align Cache Implement
## As we known, 3ds Max's align window have a Apply button which can apply current align to scene,
# and user call set next align settings after clicking Apply. It will not affect previous set align settings.
# But Blender have no vanilla Apply function for operator. The only possible way is re-run this operator.
# However the experience is pretty shit. Because the window still locate at the left-bottom corner.
# User can't keep up to change it.
#
# We use a dirty way to implement Apply function. The solution is pretty like BME struct adder.
# We use a CollectionProperty to store all align steps.
# And use a BoolProperty with update function to implement Apply button. Once its value changed,
# reset its value (order a recursive hinder), and add a new settings.
class BBP_PG_legacy_align_history(bpy.types.PropertyGroup):
align_x: bpy.props.BoolProperty(
name = "X Position",
default = False,
) # type: ignore
align_y: bpy.props.BoolProperty(
name = "Y Position",
default = False,
) # type: ignore
align_z: bpy.props.BoolProperty(
name = "Z Position",
default = False,
) # type: ignore
current_align_mode: bpy.props.EnumProperty(
name = "Current Object (Active Object)",
items = _g_EnumHelper_AlignMode.generate_items(),
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
) # type: ignore
target_align_mode: bpy.props.EnumProperty(
name = "Target Objects (Other Objects)",
items = _g_EnumHelper_AlignMode.generate_items(),
default = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter),
) # type: ignore
#endregion
class BBP_OT_legacy_align(bpy.types.Operator):
"""Align Objects with 3ds Max Style"""
bl_idname = "bbp.legacy_align"
bl_label = "3ds Max Align"
bl_options = {'REGISTER', 'UNDO'}
# the updator for apply flag value
def apply_flag_updated(self, context):
# check hinder and set hinder first
if self.recursive_hinder: return
self.recursive_hinder = True
# reset apply button value (default is True)
# due to the hinder, no recursive calling will happend
if self.apply_flag == True: return
self.apply_flag = True
# check whether add new entry
# if no selected axis, this alignment is invalid
entry: BBP_PG_legacy_align_history = self.align_history[-1]
if entry.align_x == True or entry.align_y == True or entry.align_z == True:
# valid one
# add a new entry in history
self.align_history.add()
else:
# invalid one
# reset all data to default
entry.align_x = False
entry.align_y = False
entry.align_z = False
entry.current_align_mode = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter)
entry.target_align_mode = _g_EnumHelper_AlignMode.to_selection(AlignMode.AxisCenter)
# reset hinder
self.recursive_hinder = False
# blender required
return None
apply_flag: bpy.props.BoolProperty(
name = "Apply Flag",
description = "Internal flag.",
options = {'HIDDEN', 'SKIP_SAVE'},
default = True, # default True value to make it as a "light" button, not a grey one.
update = apply_flag_updated,
) # type: ignore
recursive_hinder: bpy.props.BoolProperty(
name = "Recursive Hinder",
description = "An internal flag to prevent the loop calling to apply_flags's updator.",
options = {'HIDDEN', 'SKIP_SAVE'},
default = False,
) # type: ignore
align_history : bpy.props.CollectionProperty(
name = "Historys",
description = "Align history.",
type = BBP_PG_legacy_align_history,
) # type: ignore
@classmethod
def poll(cls, context):
return _check_align_requirement()
def invoke(self, context, event):
# clear history and add 1 entry for following functions
self.align_history.clear()
self.align_history.add()
# run execute() function
return self.execute(context)
def execute(self, context):
# get processed objects
(current_obj, target_objs) = _prepare_objects()
# INFO: YYC MARK:
# This statement is VERY IMPORTANT.
# If this statement is not presented, Blender will return identity matrix
# when getting world matrix from Object since the second execution of this function.
# It seems that Blender fail to read restored value from a new execution.
# Additionally, this statement only can be placed in there.
# If you place it at the end of this function, it doesn't work.
context.view_layer.update()
# iterate history to align objects
entry: BBP_PG_legacy_align_history
for entry in self.align_history:
_align_objects(
current_obj, 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)
)
return {'FINISHED'}
def draw(self, context):
# get last entry in history to show
entry: BBP_PG_legacy_align_history = self.align_history[-1]
layout = self.layout
col = layout.column()
# show axis
col.label(text="Align Axis (Multi-selection)")
row = col.row()
row.prop(entry, "align_x", toggle = 1)
row.prop(entry, "align_y", toggle = 1)
row.prop(entry, "align_z", toggle = 1)
# show mode
col.separator()
col.label(text = 'Current Object (Active Object)')
col.prop(entry, "current_align_mode", expand = True)
col.label(text = 'Target Objects (Selected Objects)')
col.prop(entry, "target_align_mode", expand = True)
# show apply button
col.separator()
conditional_disable_area = col.column()
# only allow Apply when there is a selected axis
conditional_disable_area.enabled = entry.align_x == True or entry.align_y == True or entry.align_z == True
# show apply and counter
conditional_disable_area.prop(self, 'apply_flag', text = 'Apply', icon = 'CHECKMARK', toggle = 1)
conditional_disable_area.label(text = f'Total {len(self.align_history) - 1} applied alignments')
#region Core Functions
def _check_align_requirement() -> bool:
# 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
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
# 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 value
return (current_obj, target_objs)
def _align_objects(
current_obj: bpy.types.Object, target_objs: set[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)
# process each target obj
for target_obj in target_objs:
# calc target object data
target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_mode)
# build translation transform
target_obj_translation: mathutils.Vector = current_obj_ref - target_obj_ref
if not align_x: target_obj_translation.x = 0
if not align_y: target_obj_translation.y = 0
if not align_z: target_obj_translation.z = 0
# target_obj.location += target_obj_translation
target_obj_translation_matrix: mathutils.Matrix = mathutils.Matrix.Translation(target_obj_translation)
# 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))
# 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))
# return value by given align mode
match(mode):
case AlignMode.Min:
ref_pos = bbox_min_corner
case AlignMode.Max:
ref_pos = bbox_max_corner
case AlignMode.BBoxCenter:
ref_pos = (bbox_max_corner + bbox_min_corner) / 2
case AlignMode.AxisCenter:
ref_pos = obj.matrix_world.translation
case _:
raise UTIL_functions.BBPException('impossible align mode.')
return ref_pos
#endregion
def register() -> None:
bpy.utils.register_class(BBP_PG_legacy_align_history)
bpy.utils.register_class(BBP_OT_legacy_align)
def unregister() -> None:
bpy.utils.unregister_class(BBP_OT_legacy_align)
bpy.utils.unregister_class(BBP_PG_legacy_align_history)