"""GLB import and geometry helpers for Blender headless renders.""" from __future__ import annotations import math import sys def import_glb(glb_file: str) -> list: """Import OCC-generated GLB into Blender. OCC exports one mesh object per STEP part, already in metres. Blender's native GLTF importer preserves part names. Returns list of Blender mesh objects, centred at world origin. """ import bpy # type: ignore[import] from mathutils import Vector # type: ignore[import] bpy.ops.object.select_all(action='DESELECT') bpy.ops.import_scene.gltf(filepath=glb_file) parts = [o for o in bpy.context.selected_objects if o.type == 'MESH'] if not parts: print(f"ERROR: No mesh objects imported from {glb_file}") sys.exit(1) print(f"[blender_render] imported {len(parts)} part(s) from GLB: " f"{[p.name for p in parts[:5]]}") # Remove OCC-baked custom normals so shade_smooth_by_angle can recompute # normals from scratch (respecting our sharp edge marks). cleared = 0 for p in parts: if "custom_normal" in p.data.attributes: p.data.attributes.remove(p.data.attributes["custom_normal"]) cleared += 1 if cleared: print(f"[blender_render] cleared OCC custom_normal from {cleared} mesh objects") # Centre combined bbox at world origin all_corners = [] for p in parts: all_corners.extend(p.matrix_world @ Vector(c) for c in p.bound_box) if all_corners: mins = Vector((min(v.x for v in all_corners), min(v.y for v in all_corners), min(v.z for v in all_corners))) maxs = Vector((max(v.x for v in all_corners), max(v.y for v in all_corners), max(v.z for v in all_corners))) center = (mins + maxs) * 0.5 # Move root objects (parentless) to centre. Adjusting a child's local # .location by a world-space vector gives wrong results when the GLB has # Empty parent nodes (OCC assembly hierarchy). Shifting the root moves # the entire hierarchy correctly. all_imported = list(bpy.context.selected_objects) root_objects = [o for o in all_imported if o.parent is None] for obj in root_objects: obj.location -= center return parts def apply_rotation(parts: list, rx: float, ry: float, rz: float) -> None: """Apply Euler rotation (degrees, XYZ order) to all parts around world origin. After import_glb the combined bbox center is at world origin, so rotating around origin is equivalent to rotating around the assembly center. """ if not parts or (rx == 0.0 and ry == 0.0 and rz == 0.0): return import bpy # type: ignore[import] from mathutils import Euler # type: ignore[import] rot_mat = Euler((math.radians(rx), math.radians(ry), math.radians(rz)), 'XYZ').to_matrix().to_4x4() for p in parts: p.matrix_world = rot_mat @ p.matrix_world # Bake rotation into mesh data so camera bbox calculations see the rotated geometry bpy.ops.object.select_all(action='DESELECT') for p in parts: p.select_set(True) bpy.context.view_layer.objects.active = parts[0] bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) print(f"[blender_render] applied rotation ({rx}°, {ry}°, {rz}°) to {len(parts)} parts") def import_usd_file(usd_path: str) -> list: """Import USD stage into current Blender scene — delegates to import_usd module.""" from import_usd import import_usd_file as _impl return _impl(usd_path)