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
+45 -7
View File
@@ -33,13 +33,16 @@ Verified against the repository on `2026-03-11`.
| Priority | Status | Re-evaluated state |
|---|---|---|
| 1. Pipeline Cleanup Foundation | In progress | `step_tasks.py` decomposition is done; dead-code cleanup and `blender_render.py` decomposition are still open |
| 2. USD Foundation Without Viewer Regression | Not started in code | Decisions are documented, but there is no `export_step_to_usd.py`, `usd_master`, `scene-manifest`, or `partKey` implementation yet |
| 3. Tessellation and Topology Quality | Not started in code | No `gmsh` install/wiring, no `tessellation_engine` setting, no Admin dropdown yet |
| 7. Render Job Tracking and Structured Logging | Done | `RenderJobDocument`, migration `048`, `PipelineLogger`, and revoke-by-real-task-id are present |
| 8. Tenant Isolation Completion | In progress | HTTP-side RLS context is wired; Celery task-side `set_tenant_context()` propagation still needs to be added |
| 9. Hash-Based Scene Conversion Caching | Partial foundation | Existing `step_file_hash` and STL-cache utilities should be extended, not rebuilt from scratch |
| 10. UI/UX Polish | Partial | Admin help tooltips, mobile nav, and some empty states exist; notification batching and remaining polish items are still open |
| 1. Pipeline Cleanup Foundation | **Done** | All P1 gates pass: dead code absent, `blender_render.py` split into 8 submodules (68 lines), `step_tasks.py` is 24 lines, legacy dirs deleted |
| 2. USD Foundation Without Viewer Regression | **Done** | `export_step_to_usd.py`, `import_usd.py`, `usd_master` MediaAsset, `scene-manifest`, `partKey`, `part_key_service.py` all implemented; migrations 060-062 applied |
| 3. Tessellation and Topology Quality | **Done** | GMSH 4.15.1 installed, `_tessellate_with_gmsh()` implemented, `tessellation_engine` wired admin → pipeline → CLI |
| 4. Viewer Migration to Canonical Part Identity | **Done** | GLB node `extras.partKey` injected; `userData.partKey` stamped in viewer; hover tooltip shows slug; scene manifest verified |
| 5. Canonical USD Export and Render Migration | **Done (M1M3), M5M7 open** | `import_usd.py` complete; `--usd-path` wired in all render scripts; `render_order_line_task` looks up `usd_master` and passes it through; M4 (deprecation log on production GLB endpoint) added — material metadata and hierarchy fixes required (0/25 parts matched in USD renders) |
| 7. Render Job Tracking and Structured Logging | **Done** | `RenderJobDocument`, migration `048`, `PipelineLogger`, and revoke-by-real-task-id are present |
| 8. Tenant Isolation Completion | **Done (Celery side)** | `set_tenant_context_sync()` called at start of all pipeline tasks; `require_admin``require_global_admin` in all 17 admin router functions |
| 6. Admin and Product Surface Simplification | **Done** | Settings renamed `scene_*`/`render_*`, migration applied, Admin progressive disclosure, ProductDetail single canonical scene, MediaAssetType deprecated values commented |
| 9. Hash-Based Scene Conversion Caching | **Done** | Composite cache key (hash + deflection + engine) in both geometry and USD tasks; disk existence check; `render_config` stored; `step_hash` in API |
| 10. UI/UX Polish | **Done (M1M3,M5)** | Empty states, Admin help text, notification batching (all IDs marked read), per-line reject in OrderDetail with portal modal; kanban drag-to-reject deferred |
---
@@ -211,6 +214,34 @@ This priority combines dead-code deletion and task decomposition because both ar
- M2: Still render from USD matches current production GLB render quality (side-by-side comparison)
- M3: Turntable render from USD works end-to-end
- M4: `generate_production_glb_task` bypassed; renders consume `usd_master` directly
- M5: Material metadata as USD primvars — canonical material names baked into the USD asset
- M6: Geometry transform fix for assembly hierarchy — correct transforms for multi-level assemblies
- M7: Material map resolution at USD export time — USD becomes self-contained with canonical material names
**M5: Material metadata as USD primvars**
The current USD render path matches 0/25 parts for material assignment because Blender has no way to resolve canonical material names from the imported USD prims. This milestone embeds that metadata directly into the USD file.
- Pass resolved `material_map` to `export_step_to_usd.py` via `--material_map` CLI arg (JSON string)
- Write `schaeffler:canonicalMaterialName` as a STRING primvar on each Mesh prim during USD export
- `import_usd.py` reads the primvar after import and performs direct material lookup (no name-matching heuristics)
- Acceptance: Blender log shows `25/25 parts matched` for material assignment from USD
**M6: Geometry transform fix for assembly hierarchy**
Multi-level assemblies (3+ nesting levels) produce wrong geometry in USD because `_extract_mesh()` does not account for shape Location before face exploration and does not accumulate parent transforms through `_traverse_xcaf` recursion.
- `_extract_mesh()` must strip shape Location before face exploration, then apply both transforms sequentially
- For deeply nested assemblies (3+ levels): accumulate parent transforms through `_traverse_xcaf` recursion
- Acceptance: USD render matches STEP import geometry side-by-side (no displaced or rotated parts)
**M7: Material map resolution at USD export time**
Currently `generate_usd_master_task` does not resolve the material map before passing it to the export subprocess. This means the USD file has no material metadata, making it impossible for Blender to assign materials without external context.
- `generate_usd_master_task` must resolve `material_map` via `material_service.resolve_material_map()` and pass to subprocess
- This makes the USD file self-contained: canonical material names baked into the asset
- Acceptance: USD file inspected via `pxr` shows `schaeffler:canonicalMaterialName` on every mesh prim
**File targets:**
@@ -222,12 +253,19 @@ This priority combines dead-code deletion and task decomposition because both ar
| MODIFY | `backend/app/services/render_blender.py` — pass `usd_master` asset path when available |
| MODIFY | `backend/app/domains/pipeline/tasks/export_glb.py` — retire `generate_gltf_production_task` once USD path validated |
| KEEP (compat) | `render-worker/scripts/export_gltf.py` — retained as fallback until USD path confirmed stable |
| MODIFY (M5) | `render-worker/scripts/export_step_to_usd.py` — accept `--material_map` CLI arg, write `schaeffler:canonicalMaterialName` primvar on each Mesh prim |
| MODIFY (M5) | `render-worker/scripts/import_usd.py` — read `schaeffler:canonicalMaterialName` primvar after import, use for direct material lookup |
| MODIFY (M6) | `render-worker/scripts/export_step_to_usd.py` — fix `_extract_mesh()` to strip shape Location; accumulate parent transforms in `_traverse_xcaf` |
| MODIFY (M7) | `backend/app/domains/pipeline/tasks/export_glb.py` (or USD task) — call `resolve_material_map()` and pass result to export subprocess |
**Acceptance gates:**
- `render_still --usd_path usd_master.usd` → PNG output visually identical to current production GLB render (diff tolerance < 5% SSIM)
- Blender log shows `[USD_IMPORT] 25 parts imported, 5044 seam/sharp edges restored` (not reconstructed by angle)
- `GET /api/media?asset_type=gltf_production` returns 0 new entries after switch (old records preserved)
- Turntable MP4 plays without texture or material pop artifacts
- (M5) Blender log shows `[USD_IMPORT] 25/25 parts matched` for material assignment (not 0/25)
- (M6) USD render geometry matches STEP import geometry side-by-side for multi-level assemblies (no displaced/rotated parts)
- (M7) `python3 -c "from pxr import Usd; stage=Usd.Stage.Open('usd_master.usd'); [print(p.GetAttribute('primvars:schaeffler:canonicalMaterialName').Get()) for p in stage.Traverse()]"` → prints canonical material name for every mesh prim
### Priority 6 — Admin and Product Surface Simplification