123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- # ##### 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
- 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 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,
- )
|