47b5d42bb5
M1 — dead code removed: - Delete blender-renderer/ and threejs-renderer/ source files - Remove PIL/Pillow fallback block from step_processor.py (_generate_thumbnail_placeholder, _finalise_image JPG path) - Remove stl_quality param from render_blender.py, render_still_task, render_turntable_task (was always "low"; hardcode deflection values) - render_turntable_task now reads scene_linear/angular_deflection from system_settings (consistent with export_glb.py pipeline) M3 — blender_render.py split from 263 → 68 lines: - _blender_args.py: parse_args() — all 25 positional + named args - _blender_scene_setup.py: setup_scene() — MODE A/B including USD import - _blender_render_config.py: configure_and_render() — engine + output Post-review fixes: - _db_engine.dispose() after settings read in render_turntable_task - _finalise_image() fmt param removed (always PNG; PIL never installed) - _blender_import.py committed together with new submodules to satisfy import_usd_file dependency Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
3.6 KiB
Python
92 lines
3.6 KiB
Python
"""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)
|