cc3071297b
- export_step_to_usd.py: accept --material_map CLI arg, write
schaeffler:canonicalMaterialName as customData on each Mesh prim,
fix geometry transform (strip shape Location before face exploration,
apply both face_loc and shape_loc sequentially)
- import_usd.py: after Blender USD import, use pxr to read customData
directly from the USD file — builds {part_key: material_name} lookup
(Blender ignores STRING primvars and customData, but pxr reads both)
- _blender_materials.py: add apply_material_library_direct() for exact
dict-based material assignment without name-matching heuristics
- _blender_scene_setup.py: prefer direct USD lookup, fall back to
name-matching for legacy USD files without material metadata
- export_glb.py (generate_usd_master_task): resolve material_map via
material_service.resolve_material_map() and pass to subprocess;
include material hash in cache key for invalidation
- ROADMAP.md: update P5 status, add M5-M7 milestones
Tested: 3/3 parts matched (ans_lfs120), 172/175 parts matched
(F-802007.TR4-D1-H122AG). Previous: 0/25 matched.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.9 KiB
Python
100 lines
3.9 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) -> tuple[list, dict]:
|
|
"""Import USD stage into current Blender scene — delegates to import_usd module.
|
|
|
|
Returns (parts, material_lookup) where material_lookup maps
|
|
blender_object_name → canonical SCHAEFFLER material name (from USD primvars).
|
|
"""
|
|
from import_usd import import_usd_file as _impl
|
|
result = _impl(usd_path)
|
|
# Backward compat: old import_usd returned just a list
|
|
if isinstance(result, tuple):
|
|
return result
|
|
return result, {}
|