# How to calculate bounds of all selected objects BUT follow the Active Object's Rotation at the same time?

[ Working code at the bottom, just paste in Blender's text editor. ]

There are similar Bounding Box questions out there but this is different, please don't close.

I made a script which iterates through the bound_box of each selected object to find the smallest and largest world space Vector coordinates, which are used to create a Lattice that is sized to fit all of those objects perfectly (a single huge bounding box).

## HOWEVER

What I would actually like is for that Lattice to be aligned to the rotation of the Active Object specifically, but still fit tightly around all the other selected objects. It's not a matter of simply making a huge lattice, the tight fit is important. Example images below:

## IMG 2 (what I'm hoping to get):

### Note:

• The Lattice has the exact same rotation as the Active Object (in all axis).
• The lattice represents an accurate bounding box of the selected objects if we pretend the Active Object's rotation is the world's orientation.
• Average direction the objects are placed in does not matter, only the Active Object's rotation matters.

Anyone have any idea how to accomplish this? If I just rotate the lattice with my current code the dimensions will be wrong because I'm currently calculating the bounds in world space, instead of using the Active objec's rotation as the world orientation.

## CODE:

Pastebin for easy copypasta: https://pastebin.com/gFnCCs3d

import bpy
from bpy.props import IntProperty, BoolProperty, FloatVectorProperty

from mathutils import Vector

class ARMORED_OT_lattice(bpy.types.Operator):
'''
Creates a lattice that matches the dimensions and transforms of the selected objects.
'''

bl_idname = 'object.armored_lattice'
bl_label  = 'ARMORED Lattice'
bl_options = {'REGISTER', 'UNDO'}

resolution: IntProperty(
name='Resolution', default=2, min=2)

scale_offset: FloatVectorProperty(
name='Scale Offset',
#step=0.1,
description='Makes the lattice larger than the object')

rotate_to_active: BoolProperty(
name='Rotate to Active', default=False,
description='Rotate the Lattice to match the Active Object')

@classmethod
def poll(cls, context):
return context.selected_objects

def execute(self, context):
self.selected = context.selected_objects
self.active = context.active_object

if context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')

self.lattice = self._create_lattice()
self._set_lattice_resolution()
self._set_lattice_transforms(context)

return {'FINISHED'}

def _create_lattice(self) -> bpy.types.Lattice:
data = bpy.data.lattices.new('Lattice')
lattice = bpy.data.objects.new('Lattice', data)

return lattice

def _set_lattice_resolution(self):
data = self.lattice.data
data.points_w = self.resolution
data.points_u = self.resolution
data.points_v = self.resolution

def _set_lattice_transforms(self, context):
bb_min, bb_max = self._selection_min_max_bounds(context)

self.lattice.location = self._centroid([bb_min, bb_max])
self.lattice.dimensions = self._vector_to_dimensions(vec1=bb_min, vec2=bb_max) + Vector(self.scale_offset)

# Does NOT work well with multiple selections (dimensions will be incorrect).
if self.active in self.selected and self.rotate_to_active:
self.lattice.rotation_euler = self.active.rotation_euler

def _centroid(self, coords: list[Vector]) -> Vector:
'''Return the average of a list of coordinates.'''

return sum((Vector(vec) for vec in coords), Vector()) / len(coords)

def _vector_to_dimensions(self, vec1: Vector, vec2: Vector) -> Vector:
'''Returns the world x, y, z dimensions of an imaginary bounding box formed by two 3d coordinates (opposite corners).'''

return Vector((abs(vec1.x - vec2.x), abs(vec1.y - vec2.y), abs(vec1.z - vec2.z)))

def _selection_min_max_bounds(self, context) -> tuple[Vector, Vector]:
'''Returns the Min and Max Vector corners (world space) of the imaginary bounding box surrounding the group of selected objects.'''

bbox_coords = [obj.matrix_world @ Vector(point) for obj in context.selected_objects for point in obj.bound_box]

return Vector(map(min, *bbox_coords)), Vector(map(max, *bbox_coords))

classes = (
ARMORED_OT_lattice,
)

def register():
for cls in classes:
bpy.utils.register_class(cls)

def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)

if __name__ == "__main__":
register()

# test call
bpy.ops.object.armored_lattice()


• Here is a algorithm to get smallest Bounding Box: blender.stackexchange.com/questions/261049/…
– X Y
Commented Sep 9, 2022 at 3:13
• Thanks, but I can already do it with 1 object. It's the multiple objects + the rotation of the active object that I can't figure out. I have a feeling it's a simple solution but I'm not good at math. Commented Sep 9, 2022 at 5:34

#### Create a smallest box outside the selected objects and rotation follow the active object

import bpy, bmesh
import numpy as np
from mathutils import Vector

def get_cube_by_selected_objects():
verts = []
for obj in bpy.context.selected_objects:
if obj.type != 'MESH': continue
verts += [obj.matrix_world @ v.co for v in obj.data.vertices]
if not verts: return None

points = np.asarray(verts)
means = np.mean(points, axis=1)

cov = np.cov(points, y = None,rowvar = 0,bias = 1)

v, vect = np.linalg.eig(cov)

tvect = np.transpose(vect)
points_r = np.dot(points, np.linalg.inv(tvect))

co_min = np.min(points_r, axis=0)
co_max = np.max(points_r, axis=0)

xmin, xmax = co_min[0], co_max[0]
ymin, ymax = co_min[1], co_max[1]
zmin, zmax = co_min[2], co_max[2]

xdif = (xmax - xmin) * 0.5
ydif = (ymax - ymin) * 0.5
zdif = (zmax - zmin) * 0.5

cx = xmin + xdif
cy = ymin + ydif
cz = zmin + zdif

corners = np.array([
[cx - xdif, cy - ydif, cz - zdif],
[cx - xdif, cy + ydif, cz - zdif],
[cx - xdif, cy + ydif, cz + zdif],
[cx - xdif, cy - ydif, cz + zdif],
[cx + xdif, cy + ydif, cz + zdif],
[cx + xdif, cy + ydif, cz - zdif],
[cx + xdif, cy - ydif, cz + zdif],
[cx + xdif, cy - ydif, cz - zdif],
])

corners = np.dot(corners, tvect)
# center = np.dot([cx, cy, cz], tvect)
mat = bpy.context.object.matrix_world

bpy.ops.mesh.primitive_cube_add(enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
cube = bpy.context.object
verts = cube.data.vertices
verts[0].co = corners[0]
verts[2].co = corners[1]
verts[6].co = corners[2]
verts[4].co = corners[3]
verts[5].co = corners[6]
verts[1].co = corners[7]
verts[3].co = corners[5]
verts[7].co = corners[4]

for obj in bpy.context.selected_objects:
obj.select_set(False)
bpy.context.view_layer.objects.active = obj
cube.select_set(True)
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
cube.display_type = 'WIRE'
return cube

def copy_rot(tar, source):
for obj in bpy.context.selected_objects:
obj.select_set(False)
tar.select_set(True)
bpy.context.view_layer.objects.active = tar

mat = source.matrix_world
loc, rot, sca = mat.decompose()
old_mode = tar.rotation_mode
tar.rotation_mode = "QUATERNION"
tar.rotation_quaternion = rot.inverted()
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
tar.rotation_quaternion = rot
tar.rotation_mode = old_mode

def create_lattice_by_cube(cube, del_org=True):
"TODO"
# you need find a way to change points position of the lattice

obj = bpy.context.object
cube = get_cube_by_selected_objects()
if cube is None:
print("fail")
else:
copy_rot(cube, obj)
create_lattice_by_cube(cube)


#### Create a special face normal box outside the selected objects and rotation follow the active object

import bpy
# import numpy as np
from mathutils import Vector

def get_box_by_selected_objects_with_vec_quaternion(q):
q_invert = q.inverted()
verts = []
for obj in bpy.context.selected_objects:
obj_mat = obj.matrix_world
if obj.type != 'MESH': continue
verts += [q_invert @ (obj_mat @ v.co) for v in obj.data.vertices]
if not verts: return None

v_z = [co.z for co in verts]
v_y = [co.y for co in verts]
v_x = [co.x for co in verts]

z_max = max(v_z)
z_min = min(v_z)
y_max = max(v_y)
y_min = min(v_y)
x_max = max(v_x)
x_min = min(v_x)

box_verts = (
(x_min, y_min, z_min),
(x_min, y_max, z_min),
(x_max, y_max, z_min),
(x_max, y_min, z_min),
(x_max, y_min, z_max),
(x_min, y_min, z_max),
(x_min, y_max, z_max),
(x_max, y_max, z_max),
)

box_verts = [q @ Vector(v) for v in box_verts]

bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
cube = bpy.context.object
verts = cube.data.vertices

verts[0].co = box_verts[0]
verts[2].co = box_verts[1]
verts[6].co = box_verts[2]
verts[4].co = box_verts[3]
verts[5].co = box_verts[4]
verts[1].co = box_verts[5]
verts[3].co = box_verts[6]
verts[7].co = box_verts[7]

cube.display_type = 'WIRE'
return cube

def copy_rot(tar, source):
for obj in bpy.context.selected_objects:
obj.select_set(False)
tar.select_set(True)
bpy.context.view_layer.objects.active = tar

mat = source.matrix_world
loc, rot, sca = mat.decompose()
old_mode = tar.rotation_mode
tar.rotation_mode = "QUATERNION"
tar.rotation_quaternion = rot.inverted()
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
tar.rotation_quaternion = rot
tar.rotation_mode = old_mode

act_obj = bpy.context.object
old_mode = act_obj.rotation_mode
act_obj.rotation_mode = "QUATERNION"

cube = get_box_by_selected_objects_with_vec_quaternion(bpy.context.object.rotation_quaternion)
if cube is not None:
copy_rot(cube, act_obj)

act_obj.rotation_mode = old_mode

• Thanks for your knowledge. Your solution is very close but the rotation seems to be based on the average direction the selected objects are placed in, instead of strictly following the Active object's rotation. Here are a couple of screenshots I took with the code you provided so you can see what I mean. Test 1: i.imgur.com/kUW8w2H.jpeg Test 2: i.imgur.com/4LrvXNJ.jpeg Commented Sep 10, 2022 at 0:01
• The rotation of the active object and the box are same. See the updated image in the answer.
– X Y
Commented Sep 10, 2022 at 7:31
• It's actually fine that the first example is technically cheating, because it's still a really cool algorithm that I will probably use at some point. The second code example works exactly as I hoped (not just having the same rotation value but actually being aligned the same way as the Active). I should of been more specific about that and Include better examples. Thank you so much for taking the time to code both of these scripts, you're amazing! And they work great! Commented Sep 10, 2022 at 15:19