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, ) align_y: bpy.props.BoolProperty( name = "Y Position", default = False, ) align_z: bpy.props.BoolProperty( name = "Z Position", default = False, ) 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), ) 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), ) #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, ) 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, ) align_history : bpy.props.CollectionProperty( name = "Historys", description = "Align history.", type = BBP_PG_legacy_align_history, ) @classmethod def poll(self, 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() # 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: # 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_bbox: tuple[mathutils.Vector] = tuple(current_obj.matrix_world @ mathutils.Vector(corner) for corner in current_obj.bound_box) current_obj_ref: mathutils.Vector = _get_object_ref_point(current_obj, current_obj_bbox, current_mode) # process each target obj for target_obj in target_objs: # calc target object data target_obj_bbox: tuple[mathutils.Vector] = tuple(target_obj.matrix_world @ mathutils.Vector(corner) for corner in target_obj.bound_box) target_obj_ref: mathutils.Vector = _get_object_ref_point(target_obj, target_obj_bbox, target_mode) # do align if align_x: target_obj.location.x += current_obj_ref.x - target_obj_ref.x if align_y: target_obj.location.y += current_obj_ref.y - target_obj_ref.y if align_z: target_obj.location.z += current_obj_ref.z - target_obj_ref.z def _get_object_ref_point(obj: bpy.types.Object, corners: tuple[mathutils.Vector], mode: AlignMode) -> mathutils.Vector: ref_pos: mathutils.Vector = mathutils.Vector((0, 0, 0)) match(mode): case AlignMode.Min: ref_pos.x = min((vec.x for vec in corners)) ref_pos.y = min((vec.y for vec in corners)) ref_pos.z = min((vec.z for vec in corners)) case AlignMode.Max: ref_pos.x = max((vec.x for vec in corners)) ref_pos.y = max((vec.y for vec in corners)) ref_pos.z = max((vec.z for vec in corners)) case AlignMode.BBoxCenter: max_vec_cache: mathutils.Vector = mathutils.Vector((0, 0, 0)) min_vec_cache: mathutils.Vector = mathutils.Vector((0, 0, 0)) min_vec_cache.x = min((vec.x for vec in corners)) min_vec_cache.y = min((vec.y for vec in corners)) min_vec_cache.z = min((vec.z for vec in corners)) max_vec_cache.x = max((vec.x for vec in corners)) max_vec_cache.y = max((vec.y for vec in corners)) max_vec_cache.z = max((vec.z for vec in corners)) ref_pos.x = (max_vec_cache.x + min_vec_cache.x) / 2 ref_pos.y = (max_vec_cache.y + min_vec_cache.y) / 2 ref_pos.z = (max_vec_cache.z + min_vec_cache.z) / 2 case AlignMode.AxisCenter: ref_pos.x = obj.location.x ref_pos.y = obj.location.y ref_pos.z = obj.location.z case _: raise UTIL_functions.BBPException('inpossible align mode.') return ref_pos #endregion def register(): bpy.utils.register_class(BBP_PG_legacy_align_history) bpy.utils.register_class(BBP_OT_legacy_align) def unregister(): bpy.utils.unregister_class(BBP_OT_legacy_align) bpy.utils.unregister_class(BBP_PG_legacy_align_history)