- Mark USD library question as decided: usd-core>=24.11 (pxr module) - Add Task 1.0 to USD implementation plan: Dockerfile install step - Add usd-core to Priority 2 file targets in ROADMAP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
22 KiB
Schaeffler Automat — Master Roadmap
Consolidated: 2026-03-11 Branch:
refactor/v2Sources merged:PLAN.md(Phases A–F),PLAN_REFACTOR.md(Phases 1–8),plan.md(GMSH),docs/rfcs/0001,visual-audit-report.md
✅ What Is Done
| Area | Detail |
|---|---|
| Phase A | Flamenco removed, blender-renderer → render-worker Celery container, threejs-renderer removed, MinIO added |
| Phase B | Domain-driven project structure, Tenant model + RLS migrations 035/036, Tenant management UI |
| Phase C | WorkflowDefinition model, standard workflows seeded, React Flow Workflow Editor |
| Phase D | OCC mesh attributes extraction, Blender integration |
| Phase E | MediaAsset catalog (model + API + frontend) |
| Sharp Edges V02 | GCPnts curve sampling → 17,129 segment pairs in GLB extras → Blender KD-tree marks sharp+seam |
| Tessellation presets | Draft/Standard/Fine preset buttons in Admin UI, default deflections updated |
| Media cache-bust | ?v={file_size_bytes} in download URLs + Cache-Control: no-cache headers |
| GPU activation order | Fix: _activate_gpu() called before AND after open_mainfile to survive engine reset |
| Material system | Aliases-first lookup, get_material_library_path() via AssetLibrary |
🗺 Open Work — Reconciled Priorities
This roadmap now treats the USD refactor as an implementation workstream, not as a blocked strategic idea.
The key architectural clarification from docs/rfcs/0001-step-to-usd-workflow.md is:
- USD becomes the canonical persisted scene asset
- the browser does not need to render USD directly
- the current 3D viewer workflow is preserved through a derived preview asset plus canonical
partKey
That removes the old assumption that USD work must wait for a Three.js USD loader.
Priority 1 — Pipeline Cleanup Foundation
Goal: Reduce refactor risk by simplifying the current pipeline before introducing canonical scene concepts.
This priority combines dead-code deletion and task decomposition because both are prerequisites for a controlled cut-over to USD.
Milestones:
- M1: Dead code deleted — Pillow block, STL settings, orphaned directories
- M2:
step_tasks.pydecomposed intobackend/app/tasks/pipeline/submodules - M3:
blender_render.pydecomposed intorender-worker/scripts/_blender_*.pysubmodules
File targets:
| Action | Path |
|---|---|
| DELETE lines 798–851 | render-worker/scripts/blender_render.py (Pillow overlay, else: branch never runs) |
| DELETE | blender-renderer/ directory |
| DELETE | threejs-renderer/ directory |
| DELETE | flamenco/ directory |
| DELETE | renderproblems_tmp/ |
| DELETE lines 705–1050 | backend/app/tasks/step_tasks.py (render_order_line_task — duplicates rendering/tasks) |
| REMOVE settings | admin.py: VALID_STL_QUALITIES, stl_quality, generate-missing-stls endpoint |
| REMOVE endpoint | cad.py: POST /cad/{id}/generate-stl/{quality} |
| CREATE | backend/app/tasks/pipeline/extract.py — metadata extraction (OCC parsing, < 2s) |
| CREATE | backend/app/tasks/pipeline/thumbnail.py — Blender thumbnail render |
| CREATE | backend/app/tasks/pipeline/stills.py — still render task |
| CREATE | backend/app/tasks/pipeline/turntable.py — turntable render task |
| THIN (< 80 lines) | backend/app/tasks/step_tasks.py — dispatch only |
| CREATE | render-worker/scripts/_blender_gpu.py |
| CREATE | render-worker/scripts/_blender_import.py |
| CREATE | render-worker/scripts/_blender_materials.py |
| CREATE | render-worker/scripts/_blender_camera.py |
| CREATE | render-worker/scripts/_blender_scene.py |
| THIN (< 80 lines) | render-worker/scripts/blender_render.py — entry point only |
Acceptance gates:
grep -r "VALID_STL_QUALITIES\|stl_quality\|from PIL\|from Pillow" backend/ render-worker/→ 0 matcheswc -l backend/app/tasks/step_tasks.py→ < 100 lineswc -l render-worker/scripts/blender_render.py→ < 80 lines- Upload
81113-l_cut.stp, trigger thumbnail → still renders correctly (no regression) ls blender-renderer/ threejs-renderer/ flamenco/→ all return "No such file"
Priority 2 — USD Foundation Without Viewer Regression
Goal: Introduce canonical part identity and the three-layer material assignment model while keeping the current GLB-based browser UX working end-to-end.
Milestones:
- M1:
export_step_to_usd.pyproduces valid USD with part hierarchy andschaeffler:partKeyon every prim - M2:
usd_masterMediaAsset type exists in DB and is stored after each export - M3:
GET /api/cad/{id}/scene-manifestreturns partKey list with effective assignments - M4:
PUT /api/cad/{id}/part-materialsaccepts{partKey → materialName}map and persists it - M5: Browser ThreeDViewer saves material overrides keyed by
partKey, survives page reload
File targets:
| Action | Path |
|---|---|
| ADD pip install | render-worker/Dockerfile — usd-core>=24.11 (provides pxr module) ✅ decided |
| CREATE | render-worker/scripts/export_step_to_usd.py — XCAF → USD, hierarchy + metadata + partKey |
| ADD enum value | backend/app/domains/media/models.py — usd_master to MediaAssetType |
| CREATE migration | backend/alembic/versions/060_usd_master_asset_type.py |
| ADD JSONB columns | backend/app/domains/products/models.py — CadFile.source_material_assignments, resolved_material_assignments, manual_material_overrides |
| CREATE migration | backend/alembic/versions/061_material_assignment_layers.py |
| CREATE | backend/app/services/part_key_service.py — generate_part_key(xcaf_label), build_scene_manifest() |
| CREATE | backend/app/domains/products/schemas.py — SceneManifest, PartEntry Pydantic models |
| ADD endpoint | backend/app/api/routers/cad.py — GET /cad/{id}/scene-manifest |
| MODIFY endpoint | backend/app/api/routers/cad.py — GET/PUT /cad/{id}/part-materials → partKey-keyed |
| ADD task | backend/app/domains/pipeline/tasks/export_glb.py — generate_usd_master_task (dual-writes beside GLB) |
| CREATE | frontend/src/api/sceneManifest.ts — SceneManifest interface, fetchSceneManifest() |
Open questions to decide before M1:
- USD authoring library:
pxr(OpenUSD full Python SDK) vs. plain USDA text templating vs.usd-corepip package - seam/sharp payload encoding: custom primvars (
primvars:schaeffler:seamEdgeVertexPairs) or a separate JSON sidecar
Acceptance gates:
python3 export_step_to_usd.py --step_path 81113-l_cut.stp→ valid.usdfile, 25 part prims, each hasschaeffler:partKeyattributeGET /api/cad/{id}/scene-manifestreturnsparts[]array withpart_key,source_name,effective_material,is_unassigned- Click part in ThreeDViewer → assign material → reload page → material still assigned (persisted via
partKey, not mesh name) - CAD file with mismatched Excel names: UI shows
unmatched_source_rowscount > 0 and unassigned parts highlighted - No regression: existing click/select/isolate/ghost/hide still works in browser
References:
- RFC:
docs/rfcs/0001-step-to-usd-workflow.md - Execution checklist:
docs/plans/0001-step-to-usd-implementation.md
Priority 3 — Tessellation and Topology Quality
Goal: Eliminate fan triangles on cylindrical surfaces (rings, bearings) and produce clean seams for UV unwrap.
Milestones:
- M1: GMSH 4.15+ installed in render-worker container
- M2:
export_step_to_gltf.py --tessellation_engine gmshproduces fan-free GLB - M3:
tessellation_enginesystem setting wired through CLI → Admin UI dropdown
File targets:
| Action | Path |
|---|---|
| ADD pip install | render-worker/Dockerfile — gmsh>=4.15.0 |
| ADD arg + function | render-worker/scripts/export_step_to_gltf.py — --tessellation_engine, _tessellate_with_gmsh() |
| ADD setting | backend/app/api/routers/admin.py — tessellation_engine in SETTINGS_DEFAULTS + SettingsOut |
| MODIFY task | backend/app/domains/pipeline/tasks/export_glb.py — read setting, pass to CLI |
| ADD UI | frontend/src/pages/Admin.tsx — dropdown: OCC vs. GMSH |
(Full task breakdown in plan.md)
Acceptance gates:
docker compose exec render-worker python3 -c "import gmsh; print(gmsh.__version__)"→4.15.xpython3 export_step_to_gltf.py --step_path 81113-l_cut.stp --tessellation_engine gmsh→ no vertex with valence > 10 at cylinder seam edges (inspect via Blender Mesh Analysis overlay)- Standard preset output size with GMSH ≤ 2× size with OCC at same deflection
- Sharp edge pairs still extracted and injected into GLB extras after GMSH tessellation
Note: Better tessellation directly benefits Priority 2 (USD seam/sharp payload) and Priority UV-unwrap work.
Priority 4 — Viewer Migration to Canonical Part Identity
Goal: Move browser interactions from raw GLB mesh-name matching to canonical partKey without any UX regression.
Milestones:
- M1: Preview GLB derivation embeds
partKeyas meshextras.partKeyon every selectable object - M2:
ThreeDViewerreadspartKeyfrom scene manifest + mesh extras on click, no longer uses rawmesh.name - M3:
MaterialPanelshowspartKey, source name, assignment provenance; saves overrides bypartKey - M4: Unmatched source rows and unassigned parts surfaced in
MaterialPanelreconciliation section
File targets:
| Action | Path |
|---|---|
| CREATE | frontend/src/api/sceneManifest.ts — SceneManifest + PartEntry interfaces |
| MODIFY | frontend/src/components/cad/ThreeDViewer.tsx — use partKey from scene manifest for selection, isolation, ghost |
| MODIFY | frontend/src/components/cad/MaterialPanel.tsx — show provenance, unmatched/unassigned sections |
| MODIFY | frontend/src/api/cad.ts — update PartMaterialsMap interface to { [partKey: string]: string } |
| MODIFY | backend/app/api/routers/cad.py — GET/PUT /cad/{id}/part-materials keyed by partKey with provenance |
| ADD util | render-worker/scripts/export_step_to_gltf.py — embed partKey into mesh extras during GLB export |
Acceptance gates:
mesh.userData.partKeyexists on every mesh object in the Three.js scene after GLB load- Select a part → DevTools shows the assignment payload contains
part_key: "ring_outer", notmesh_name: "RingOuter_AF0" - Upload file with 3 unmatched Excel rows → MaterialPanel shows "3 unmatched source rows"
- After full page reload: all manual material assignments are restored correctly
- Isolation, hide, and ghost still work as before (no regression)
Priority 5 — Canonical USD Export and Render Migration
Goal: Switch Blender still/turntable renders to consume the canonical USD stage, retiring the production GLB as an intermediate render artifact.
Milestones:
- M1:
render-worker/scripts/import_usd.py— Blender can import USD + restore seam/sharp from primvars - 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_taskbypassed; renders consumeusd_masterdirectly
File targets:
| Action | Path |
|---|---|
| CREATE | render-worker/scripts/export_step_to_usd.py — STEP→USD exporter (seam/sharp payload on mesh prims) |
| CREATE | render-worker/scripts/import_usd.py — Blender USD import helper: reads primvars:schaeffler:seamEdgeVertexPairs, marks seam+sharp |
| MODIFY | render-worker/scripts/blender_render.py — accept --usd_path flag alongside --glb_path |
| 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 |
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_productionreturns 0 new entries after switch (old records preserved)- Turntable MP4 plays without texture or material pop artifacts
Priority 6 — Admin and Product Surface Simplification
Goal: Remove the geometry-GLB vs production-GLB mental model from admin, product detail, and repair flows.
Milestones:
- M1: Admin tessellation settings collapsed from 4 knobs to
scene_*+preview_*(2 or 3 knobs max) - M2: Bulk actions renamed from
generate-missing-geometry-glbs→generate-missing-canonical-scenes - M3: ProductDetail shows single canonical scene status card, not dual-GLB
File targets:
| Action | Path |
|---|---|
| MODIFY | backend/app/api/routers/admin.py — rename/collapse tessellation settings; new bulk action labels |
| CREATE migration | backend/alembic/versions/06x_rename_tessellation_settings.py — UPDATE system_settings SET key = 'scene_linear_deflection' WHERE key = 'gltf_production_linear_deflection' |
| MODIFY | frontend/src/pages/Admin.tsx — simplified tessellation panel: scene quality + optional preview override |
| MODIFY | frontend/src/pages/ProductDetail.tsx — single canonical scene card (status, regenerate button) |
| MODIFY | backend/app/domains/media/models.py — deprecate gltf_geometry / gltf_production (keep values, add deprecated=True metadata) |
Acceptance gates:
- Admin Settings page has max 3 tessellation quality fields (down from 4)
- ProductDetail page: one "Canonical Scene" status section, no separate geometry/production rows
GET /api/admin/settingsno longer exposesgltf_preview_linear_deflectionkey (replaced byscene_linear_deflection)
Priority 7 — Render Job Tracking and Structured Logging
Goal: Fix broken render job cancellation (synthetic render-{line_id} ID never matches real Celery task ID) and establish structured per-step logging.
Milestones:
- M1:
RenderJobDocumentschema + migration; tasks write realself.request.idto DB - M2: Cancel endpoint reads
celery_task_idfrom job doc and callsrevoke()— actually stops task - M3:
PipelineLoggerintegrated in all task files; every step emits[STEP] start/done/errorwith duration
File targets:
| Action | Path |
|---|---|
| CREATE | backend/app/domains/rendering/job_document.py — RenderJobDocument Pydantic model, update_step(), set_state() |
| CREATE | backend/app/core/pipeline_logger.py — PipelineLogger(step_start/done/error) writing to logging + Redis SSE |
| CREATE migration | backend/alembic/versions/062_render_job_document.py — add render_job_doc JSONB to order_lines |
| MODIFY | backend/app/domains/pipeline/tasks/render_order_line.py — write celery_task_id + step events to job doc |
| MODIFY | backend/app/domains/pipeline/tasks/render_thumbnail.py — same |
| MODIFY | backend/app/api/routers/orders.py — cancel reads render_job_doc.celery_task_id, calls celery.control.revoke() |
Acceptance gates:
- Start a 60s render task → click Cancel →
celery inspect activeshows task is gone within 15s GET /api/orders/{id}/lines/{line_id}response includesrender_job_doc.steps[]with per-stepduration_s- Worker log shows
[THUMBNAIL] done in 34.2sformat (not bare f-strings)
Priority 8 — Tenant Isolation Completion
Goal: Make PostgreSQL RLS enforcement real. Currently build_tenant_db_dep() yields db without calling SET LOCAL app.current_tenant_id, making all tenant isolation a silent no-op.
Milestones:
- M1:
TenantContextMiddlewareregistered; all HTTP requests set RLS context from JWT - M2: All Celery tasks call
set_tenant_context(db, tenant_id)at task start - M3:
global_admin+tenant_adminroles in DB;require_admin()→require_global_admin()
File targets:
| Action | Path |
|---|---|
| CREATE | backend/app/core/middleware.py — TenantContextMiddleware(BaseHTTPMiddleware) |
| MODIFY | backend/app/main.py — app.add_middleware(TenantContextMiddleware) |
| MODIFY | backend/app/utils/auth.py — create_access_token() embeds tenant_id in JWT claims |
| MODIFY | backend/app/tasks/pipeline/thumbnail.py, extract.py, stills.py, turntable.py — set_tenant_context() at start |
| CREATE migration | backend/alembic/versions/063_role_hierarchy.py — rename admin → global_admin, add tenant_admin |
| MODIFY | All routers using require_admin() → require_global_admin() |
Acceptance gates:
- Login as tenant A user →
GET /api/productsreturns 0 results when tenant A has no products, even if tenant B has 50 - Verify via:
SELECT count(*) FROM products WHERE tenant_id != '<tenantA_id>'returns 0 from within tenant A session - Celery task logs show
[TENANT] context set: tenant_id=<uuid>at start GET /api/admin/usersreturns 403 fortenant_adminrole (onlyglobal_admincan list all users)
Priority 9 — Hash-Based Scene Conversion Caching
Goal: Skip re-tessellation when the STEP file has not changed. Cache canonical scene + preview derivatives by SHA256(step_file).
Milestones:
- M1:
cad_files.step_hashcolumn in DB; hash computed and stored on each export - M2: Export task checks hash before processing — returns cached asset UUID on hit
- M3: Hash invalidated correctly when admin forces reprocess or deflection settings change
File targets:
| Action | Path |
|---|---|
| ADD column | backend/app/domains/products/models.py — CadFile.step_hash: str | None |
| CREATE migration | backend/alembic/versions/064_step_hash.py — ADD COLUMN step_hash VARCHAR(64) |
| MODIFY | backend/app/domains/pipeline/tasks/export_glb.py (or future USD task) — hash check before subprocess call |
| ADD util | backend/app/services/step_processor.py — compute_step_hash(file_path) -> str |
Acceptance gates:
- Upload same STEP file twice → second task completes in < 2s (cache hit logged:
[CACHE] hash match, skipping tessellation) - Change deflection setting → force reprocess → new export runs fresh (hash same but settings changed, cache bypassed)
GET /api/cad/{id}response includesstep_hashfield
Priority 10 — UI/UX Polish
Goal: Address independent UI items from visual-audit-report.md that require no backend changes.
Milestones:
- M1: Tooltip/help text on every Admin settings input
- M2: Empty state messages in MediaBrowser, ProductLibrary, Orders
- M3: Notification batching — group per-render noise into job summaries
- M4: Mobile navigation — hamburger menu at < 768px
- M5: Kanban rejection flow — drag-to-reject with reason field
File targets:
| Action | Path |
|---|---|
| MODIFY | frontend/src/pages/Admin.tsx — title attributes on all inputs; help text below complex settings |
| MODIFY | frontend/src/pages/MediaBrowser.tsx — empty state: "No assets yet — upload a STEP file to get started" |
| MODIFY | frontend/src/pages/ProductLibrary.tsx — empty state |
| MODIFY | frontend/src/pages/Orders.tsx — empty state |
| MODIFY | frontend/src/components/layout/Layout.tsx — hamburger + slide-in nav for mobile |
| MODIFY | frontend/src/pages/OrderDetail.tsx — reject button with reason modal |
| CREATE | frontend/src/components/shared/Tooltip.tsx — reusable tooltip wrapper |
Acceptance gates:
- All Admin settings inputs have visible help text or tooltip (manual check: no input label without explanation)
- Mobile viewport (375px): no horizontal scroll, nav accessible via hamburger
- Submit a render → NotificationCenter shows one "Render complete (3 files)" summary, not 3 individual toasts
Dependency Graph
Priority 1 (Pipeline Cleanup Foundation)
└── Priority 2 (USD Foundation Without Viewer Regression)
└── Priority 4 (Viewer Migration to Canonical Part Identity)
└── Priority 5 (Canonical USD Export and Render Migration)
└── Priority 6 (Admin and Product Surface Simplification)
└── Priority 9 (Hash-Based Scene Conversion Caching)
Priority 3 (Tessellation and Topology Quality)
└── Priority 5 (Canonical USD Export and Render Migration)
Priority 7 (Render Job Tracking and Structured Logging) — can run in parallel
Priority 8 (Tenant Isolation Completion) — can run in parallel
Priority 10 (UI/UX Polish) — independent
What To Do Next
Recommended execution path:
- Do Priority 1 first: clean up and split the current pipeline.
- Start Priority 2 immediately after: add
partKey, assignment-layer semantics, and scene manifest without changing the browser UX. - Run Priority 3 in parallel or immediately after, depending on whether current tessellation quality blocks scene-authoring confidence.
- Use the implementation plan in
docs/plans/0001-step-to-usd-implementation.mdas the execution checklist for the USD workstream.
Parallel sprint option (2 agents):
- Agent 1: Priority 1 (pipeline cleanup foundation)
- Agent 2: Priority 3 (tessellation and topology quality)
Parallel sprint option (3 agents):
- Agent 1: Priority 1
- Agent 2: Priority 3
- Agent 3: Priority 7 or Priority 8
Do not defer anymore:
- canonical
partKey - part-keyed browser material overrides
- scene manifest / preview contract
These are now considered implementation prerequisites for the long-term refactor, not optional strategy work.
Archive
Old planning files are kept for reference but superseded by this document:
PLAN.md— original Phase A–F plan (Phases A–E complete, Phase F = Priority 9 here)PLAN_REFACTOR.md— 1,173-line architectural plan (Phases 1–8 mapped to Priorities 2–8 above)plan.md— active GMSH implementation plan (Priority 1)docs/rfcs/0001-step-to-usd-workflow.md— USD RFC (Priority 10)docs/plans/0001-step-to-usd-implementation.md— actionable USD implementation planreview-report.md— latest code review resultsvisual-audit-report.md— UX audit results