
Without this it's impractical to subscribe to any change to the cursor. Fixes T61969 by having gizmos update on any change to the cursor.
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,
|
|
)
|