393e4b92a7
M1 dead code removal: - admin.py: remove VALID_STL_QUALITIES + stl_quality (7 locations) - frontend: remove stl_quality from 6 files (api/orders.ts, api/worker.ts, WorkerActivity.tsx, RenderInfoModal.tsx, helpTexts.ts, mocks/handlers.ts) - blender_render.py: delete _mark_sharp_and_seams() — dead, never called (62 lines) - step_processor.py: delete _render_via_service() + 2 elif renderer=="threejs" branches - renderproblems_tmp/: remove 3 orphaned debug images M3 blender_render.py decomposition (858 → 248 lines): - _blender_gpu.py: activate_gpu(), configure_engine() - _blender_import.py: import_glb(), apply_rotation() - _blender_materials.py: FAILED_MATERIAL_NAME, assign_failed_material(), build_mat_map_lower(), apply_material_library() - _blender_camera.py: setup_auto_camera(), setup_auto_lights() - _blender_scene.py: ensure_collection(), apply_smooth_batch(), apply_sharp_edges_from_occ(), setup_shadow_catcher() - Entry-point: sys.path.insert for submodule discovery; arg-parse + Mode A/B orchestration only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
3.4 KiB
Python
86 lines
3.4 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")
|