import bpy import mathutils def resolve_to_mesh_objects(obj): """Recursively resolve collection instances and linked instances to their source mesh objects.""" if obj.type == 'MESH': return [obj] elif obj.type == 'EMPTY' and obj.instance_collection: # Collection instance — walk linked objects result = [] for linked_obj in obj.instance_collection.objects: result.extend(resolve_to_mesh_objects(linked_obj)) return result elif obj.type == 'MESH' or obj.instance_type != 'NONE': # Linked instance or other instance type — use its data if obj.data and obj.data.type == 'MESH': return [obj] elif hasattr(obj, 'instance_object') and obj.instance_object: return resolve_to_mesh_objects(obj.instance_object) return [] def calculate_global_bounds(selected_objects): if not selected_objects: return None min_x, min_y, min_z = float('inf'), float('inf'), float('inf') max_x, max_y, max_z = float('-inf'), float('-inf'), float('-inf') found_any_mesh = False for obj in selected_objects: mesh_objects = resolve_to_mesh_objects(obj) if not mesh_objects: continue for source_obj in mesh_objects: found_any_mesh = True if hasattr(source_obj, 'bound_box_world') and source_obj.bound_box_world: bbw = source_obj.bound_box_world for corner in bbw: min_x = min(min_x, corner.x) min_y = min(min_y, corner.y) min_z = min(min_z, corner.z) max_x = max(max_x, corner.x) max_y = max(max_y, corner.y) max_z = max(max_z, corner.z) else: matrix = source_obj.matrix_world for corner in source_obj.bound_box: world_corner = matrix @ mathutils.Vector(corner) min_x = min(min_x, world_corner.x) min_y = min(min_y, world_corner.y) min_z = min(min_z, world_corner.z) max_x = max(max_x, world_corner.x) max_y = max(max_y, world_corner.y) max_z = max(max_z, world_corner.z) if not found_any_mesh: return None return { 'min': mathutils.Vector((min_x, min_y, min_z)), 'max': mathutils.Vector((max_x, max_y, max_z)), 'size': mathutils.Vector((max_x - min_x, max_y - min_y, max_z - min_z)) } def create_aligned_boundary_cube(bounds, parent=None, mode=None): if not bounds: return None min_v = bounds['min'] max_v = bounds['max'] size = bounds['size'] mesh = bpy.data.meshes.new("Stocker_Boundary") obj = bpy.data.objects.new("Stocker_Boundary", mesh) bpy.context.collection.objects.link(obj) if mode == 'GRID': # Origin at top-front-left: (min_x, max_y, min_z) — cube extends +X, -Y, +Z obj.location = (min_v.x, max_v.y, min_v.z) verts = [ (0, 0, 0), (size.x, 0, 0), (size.x, -size.y, 0), (0, -size.y, 0), (0, 0, size.z), (size.x, 0, size.z), (size.x, -size.y, size.z), (0, -size.y, size.z) ] else: # CIRCLE mode: origin at center X,Y, min Z — cube extends equally in all X/Y directions center_x = (min_v.x + max_v.x) / 2 center_y = (min_v.y + max_v.y) / 2 obj.location = (center_x, center_y, min_v.z) verts = [ (-size.x / 2, -size.y / 2, 0), (size.x / 2, -size.y / 2, 0), (size.x / 2, size.y / 2, 0), (-size.x / 2, size.y / 2, 0), (-size.x / 2, -size.y / 2, size.z), (size.x / 2, -size.y / 2, size.z), (size.x / 2, size.y / 2, size.z), (-size.x / 2, size.y / 2, size.z) ] faces = [ (0, 1, 2, 3), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7) ] mesh.from_pydata(verts, [], faces) mesh.update() # Parent to selected object so boundary follows instances if parent: obj.parent = parent return obj if __name__ == "__main__": print("Phase 2: Geometry (Origin Alignment) logic loaded.")