Listing the "Blender Foundation" as copyright holder implied the Blender Foundation holds copyright to files which may include work from many developers. While keeping copyright on headers makes sense for isolated libraries, Blender's own code may be refactored or moved between files in a way that makes the per file copyright holders less meaningful. Copyright references to the "Blender Foundation" have been replaced with "Blender Authors", with the exception of `./extern/` since these this contains libraries which are more isolated, any changed to license headers there can be handled on a case-by-case basis. Some directories in `./intern/` have also been excluded: - `./intern/cycles/` it's own `AUTHORS` file is planned. - `./intern/opensubdiv/`. An "AUTHORS" file has been added, using the chromium projects authors file as a template. Design task: #110784 Ref !110783.
415 lines
11 KiB
Python
415 lines
11 KiB
Python
# SPDX-FileCopyrightText: 2010-2023 Blender Authors
|
|
#
|
|
# 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,
|
|
)
|