433 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			433 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # ##### BEGIN GPL LICENSE BLOCK #####
 | |
| 
 | |
| #
 | |
| #  This program is free software; you can redistribute it and/or
 | |
| #  modify it under the terms of the GNU General Public License
 | |
| #  as published by the Free Software Foundation; either version 2
 | |
| #  of the License, or (at your option) any later version.
 | |
| #
 | |
| #  This program is distributed in the hope that it will be useful,
 | |
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| #  GNU General Public License for more details.
 | |
| #
 | |
| #  You should have received a copy of the GNU General Public License
 | |
| #  along with this program; if not, write to the Free Software Foundation,
 | |
| #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 | |
| #
 | |
| # ##### END GPL LICENSE BLOCK #####
 | |
| 
 | |
| # <pep8-80 compliant>
 | |
| 
 | |
| import bpy
 | |
| 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
 | |
| 
 | |
|     me = obj.to_mesh(depsgraph=depsgraph, apply_modifiers=True)
 | |
|     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
 | |
| 
 | |
|     bpy.data.meshes.remove(me)
 | |
| 
 | |
|     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.depsgraph
 | |
|     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).
 | |
|     scene.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 calculation of the "
 | |
|             "bounding box for perfect results on complex "
 | |
|             "shape meshes with rotation/scale (Slow)"
 | |
|         ),
 | |
|         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,
 | |
| )
 |