# SPDX-License-Identifier: GPL-2.0-or-later from bpy.types import Operator from mathutils import Vector def worldspace_bounds_from_object_bounds(bb_world): # Initialize the variables with the 8th vertex left, right, front, back, down, up = ( bb_world[7][0], bb_world[7][0], bb_world[7][1], bb_world[7][1], bb_world[7][2], bb_world[7][2], ) # Test against the other 7 verts for i in range(7): # X Range val = bb_world[i][0] if val < left: left = val if val > right: right = val # Y Range val = bb_world[i][1] if val < front: front = val if val > back: back = val # Z Range val = bb_world[i][2] if val < down: down = val if val > up: up = val return (Vector((left, front, up)), Vector((right, back, down))) def worldspace_bounds_from_object_data(depsgraph, obj): matrix_world = obj.matrix_world.copy() # Initialize the variables with the last vertex ob_eval = obj.evaluated_get(depsgraph) me = ob_eval.to_mesh() verts = me.vertices val = matrix_world @ (verts[-1].co if verts else Vector((0.0, 0.0, 0.0))) left, right, front, back, down, up = ( val[0], val[0], val[1], val[1], val[2], val[2], ) # Test against all other verts for v in verts: vco = matrix_world @ v.co # X Range val = vco[0] if val < left: left = val if val > right: right = val # Y Range val = vco[1] if val < front: front = val if val > back: back = val # Z Range val = vco[2] if val < down: down = val if val > up: up = val ob_eval.to_mesh_clear() return Vector((left, front, up)), Vector((right, back, down)) def align_objects(context, align_x, align_y, align_z, align_mode, relative_to, bb_quality): depsgraph = context.evaluated_depsgraph_get() scene = context.scene cursor = scene.cursor.location # We are accessing runtime data such as evaluated bounding box, so we need to # be sure it is properly updated and valid (bounding box might be lost on operator # redo). context.view_layer.update() Left_Front_Up_SEL = [0.0, 0.0, 0.0] Right_Back_Down_SEL = [0.0, 0.0, 0.0] flag_first = True objects = [] for obj in context.selected_objects: matrix_world = obj.matrix_world.copy() bb_world = [matrix_world @ Vector(v) for v in obj.bound_box] objects.append((obj, bb_world)) if not objects: return False for obj, bb_world in objects: if bb_quality and obj.type == 'MESH': GBB = worldspace_bounds_from_object_data(depsgraph, obj) else: GBB = worldspace_bounds_from_object_bounds(bb_world) Left_Front_Up = GBB[0] Right_Back_Down = GBB[1] # Active Center if obj == context.active_object: center_active_x = (Left_Front_Up[0] + Right_Back_Down[0]) / 2.0 center_active_y = (Left_Front_Up[1] + Right_Back_Down[1]) / 2.0 center_active_z = (Left_Front_Up[2] + Right_Back_Down[2]) / 2.0 size_active_x = (Right_Back_Down[0] - Left_Front_Up[0]) / 2.0 size_active_y = (Right_Back_Down[1] - Left_Front_Up[1]) / 2.0 size_active_z = (Left_Front_Up[2] - Right_Back_Down[2]) / 2.0 # Selection Center if flag_first: flag_first = False Left_Front_Up_SEL[0] = Left_Front_Up[0] Left_Front_Up_SEL[1] = Left_Front_Up[1] Left_Front_Up_SEL[2] = Left_Front_Up[2] Right_Back_Down_SEL[0] = Right_Back_Down[0] Right_Back_Down_SEL[1] = Right_Back_Down[1] Right_Back_Down_SEL[2] = Right_Back_Down[2] else: # X axis if Left_Front_Up[0] < Left_Front_Up_SEL[0]: Left_Front_Up_SEL[0] = Left_Front_Up[0] # Y axis if Left_Front_Up[1] < Left_Front_Up_SEL[1]: Left_Front_Up_SEL[1] = Left_Front_Up[1] # Z axis if Left_Front_Up[2] > Left_Front_Up_SEL[2]: Left_Front_Up_SEL[2] = Left_Front_Up[2] # X axis if Right_Back_Down[0] > Right_Back_Down_SEL[0]: Right_Back_Down_SEL[0] = Right_Back_Down[0] # Y axis if Right_Back_Down[1] > Right_Back_Down_SEL[1]: Right_Back_Down_SEL[1] = Right_Back_Down[1] # Z axis if Right_Back_Down[2] < Right_Back_Down_SEL[2]: Right_Back_Down_SEL[2] = Right_Back_Down[2] center_sel_x = (Left_Front_Up_SEL[0] + Right_Back_Down_SEL[0]) / 2.0 center_sel_y = (Left_Front_Up_SEL[1] + Right_Back_Down_SEL[1]) / 2.0 center_sel_z = (Left_Front_Up_SEL[2] + Right_Back_Down_SEL[2]) / 2.0 # Main Loop for obj, bb_world in objects: matrix_world = obj.matrix_world.copy() bb_world = [matrix_world @ Vector(v[:]) for v in obj.bound_box] if bb_quality and obj.type == 'MESH': GBB = worldspace_bounds_from_object_data(depsgraph, obj) else: GBB = worldspace_bounds_from_object_bounds(bb_world) Left_Front_Up = GBB[0] Right_Back_Down = GBB[1] center_x = (Left_Front_Up[0] + Right_Back_Down[0]) / 2.0 center_y = (Left_Front_Up[1] + Right_Back_Down[1]) / 2.0 center_z = (Left_Front_Up[2] + Right_Back_Down[2]) / 2.0 positive_x = Right_Back_Down[0] positive_y = Right_Back_Down[1] positive_z = Left_Front_Up[2] negative_x = Left_Front_Up[0] negative_y = Left_Front_Up[1] negative_z = Right_Back_Down[2] obj_loc = obj.location if align_x: # Align Mode if relative_to == 'OPT_4': # Active relative if align_mode == 'OPT_1': obj_x = obj_loc[0] - negative_x - size_active_x elif align_mode == 'OPT_3': obj_x = obj_loc[0] - positive_x + size_active_x else: # Everything else relative if align_mode == 'OPT_1': obj_x = obj_loc[0] - negative_x elif align_mode == 'OPT_3': obj_x = obj_loc[0] - positive_x if align_mode == 'OPT_2': # All relative obj_x = obj_loc[0] - center_x # Relative To if relative_to == 'OPT_1': loc_x = obj_x elif relative_to == 'OPT_2': loc_x = obj_x + cursor[0] elif relative_to == 'OPT_3': loc_x = obj_x + center_sel_x elif relative_to == 'OPT_4': loc_x = obj_x + center_active_x obj.location[0] = loc_x if align_y: # Align Mode if relative_to == 'OPT_4': # Active relative if align_mode == 'OPT_1': obj_y = obj_loc[1] - negative_y - size_active_y elif align_mode == 'OPT_3': obj_y = obj_loc[1] - positive_y + size_active_y else: # Everything else relative if align_mode == 'OPT_1': obj_y = obj_loc[1] - negative_y elif align_mode == 'OPT_3': obj_y = obj_loc[1] - positive_y if align_mode == 'OPT_2': # All relative obj_y = obj_loc[1] - center_y # Relative To if relative_to == 'OPT_1': loc_y = obj_y elif relative_to == 'OPT_2': loc_y = obj_y + cursor[1] elif relative_to == 'OPT_3': loc_y = obj_y + center_sel_y elif relative_to == 'OPT_4': loc_y = obj_y + center_active_y obj.location[1] = loc_y if align_z: # Align Mode if relative_to == 'OPT_4': # Active relative if align_mode == 'OPT_1': obj_z = obj_loc[2] - negative_z - size_active_z elif align_mode == 'OPT_3': obj_z = obj_loc[2] - positive_z + size_active_z else: # Everything else relative if align_mode == 'OPT_1': obj_z = obj_loc[2] - negative_z elif align_mode == 'OPT_3': obj_z = obj_loc[2] - positive_z if align_mode == 'OPT_2': # All relative obj_z = obj_loc[2] - center_z # Relative To if relative_to == 'OPT_1': loc_z = obj_z elif relative_to == 'OPT_2': loc_z = obj_z + cursor[2] elif relative_to == 'OPT_3': loc_z = obj_z + center_sel_z elif relative_to == 'OPT_4': loc_z = obj_z + center_active_z obj.location[2] = loc_z return True from bpy.props import ( BoolProperty, EnumProperty, ) class AlignObjects(Operator): """Align objects""" bl_idname = "object.align" bl_label = "Align Objects" bl_options = {'REGISTER', 'UNDO'} bb_quality: BoolProperty( name="High Quality", description=( "Enables high quality but slow calculation of the " "bounding box for perfect results on complex " "shape meshes with rotation/scale" ), default=True, ) align_mode: EnumProperty( name="Align Mode", description="Side of object to use for alignment", items=( ('OPT_1', "Negative Sides", ""), ('OPT_2', "Centers", ""), ('OPT_3', "Positive Sides", ""), ), default='OPT_2', ) relative_to: EnumProperty( name="Relative To", description="Reference location to align to", items=( ('OPT_1', "Scene Origin", "Use the scene origin as the position for the selected objects to align to"), ('OPT_2', "3D Cursor", "Use the 3D cursor as the position for the selected objects to align to"), ('OPT_3', "Selection", "Use the selected objects as the position for the selected objects to align to"), ('OPT_4', "Active", "Use the active object as the position for the selected objects to align to"), ), default='OPT_4', ) align_axis: EnumProperty( name="Align", description="Align to axis", items=( ('X', "X", ""), ('Y', "Y", ""), ('Z', "Z", ""), ), options={'ENUM_FLAG'}, ) @classmethod def poll(cls, context): return context.mode == 'OBJECT' def execute(self, context): align_axis = self.align_axis ret = align_objects( context, 'X' in align_axis, 'Y' in align_axis, 'Z' in align_axis, self.align_mode, self.relative_to, self.bb_quality, ) if not ret: self.report({'WARNING'}, "No objects with bound-box selected") return {'CANCELLED'} else: return {'FINISHED'} classes = ( AlignObjects, )