feat(M5-M7): embed canonical material names in USD via customData + pxr direct read

- 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>
This commit is contained in:
2026-03-12 23:04:26 +01:00
parent 1321ef2bd4
commit cc3071297b
15 changed files with 488 additions and 246 deletions
+39 -3
View File
@@ -15,10 +15,14 @@ import bmesh # type: ignore[import]
from mathutils import Vector # type: ignore[import]
def import_usd_file(usd_path: str) -> list:
def import_usd_file(usd_path: str) -> list | tuple:
"""Import USD stage into current Blender scene.
Returns list of imported mesh objects, centred at world origin.
Returns a tuple of (parts, material_lookup) where:
- parts: list of imported mesh objects, centred at world origin
- material_lookup: dict mapping blender_object_name → canonical_material_name
(populated from schaeffler:canonicalMaterialName primvars, empty dict if absent)
USD stage is mm Y-up with metersPerUnit=0.001 — Blender scales to metres.
"""
bpy.ops.object.select_all(action='DESELECT')
@@ -43,6 +47,38 @@ def import_usd_file(usd_path: str) -> list:
if restored:
print(f"[import_usd] restored seam/sharp on {restored} mesh(es)", flush=True)
# Extract material lookup via pxr direct read of the USD file.
# Blender's USD importer does NOT expose STRING primvars or customData as
# Python-accessible properties — but the pxr module (available in render-worker)
# can read them perfectly from the same file.
material_lookup: dict[str, str] = {}
try:
from pxr import Usd, UsdGeom # type: ignore[import]
stage = Usd.Stage.Open(usd_path)
for prim in stage.Traverse():
if prim.GetTypeName() != "Mesh":
continue
part_key = prim.GetCustomDataByKey("schaeffler:partKey") or ""
mat_name = prim.GetCustomDataByKey("schaeffler:canonicalMaterialName") or ""
if not part_key or not mat_name:
# Also check parent Xform prim (metadata may be on container)
parent = prim.GetParent()
if parent:
part_key = part_key or (parent.GetCustomDataByKey("schaeffler:partKey") or "")
mat_name = mat_name or (parent.GetCustomDataByKey("schaeffler:canonicalMaterialName") or "")
if part_key and mat_name:
# Blender object name = mesh prim leaf name (part_key)
material_lookup[part_key] = mat_name
except Exception as exc:
print(f"[import_usd] WARNING: pxr material lookup failed: {exc}", flush=True)
if material_lookup:
print(f"[import_usd] pxr material lookup: {len(material_lookup)}/{len(parts)} parts",
flush=True)
else:
print("[import_usd] no schaeffler:canonicalMaterialName metadata found (legacy USD)",
flush=True)
# Centre combined bbox at world origin (same as import_glb convention)
all_corners = []
for p in parts:
@@ -60,7 +96,7 @@ def import_usd_file(usd_path: str) -> list:
for obj in root_objects:
obj.location -= center
return parts
return parts, material_lookup
def _rename_usd_objects(parts: list) -> None: