# RFC 0001: Canonical STEP to USD Workflow for Visualization and Rendering

- Status: Proposed
- Author: Codex
- Date: 2026-03-11

## Summary

This RFC proposes replacing the current dual-GLB CAD visualization pipeline with a canonical USD-based workflow:

- `STEP -> USD` becomes the primary geometry and scene-authoring path.
- One canonical USD stage becomes the source of truth for preview, rendering, and downstream conversions.
- Part metadata is preserved directly on USD prims instead of being tunneled through GLB extras or Blender object names.
- Material assignment and replacement remain supported through USD material bindings plus Blender-time shader realization.
- The current browser-side part picking, isolation, hide/ghost, and manual material assignment workflow is preserved through a derived interactive preview asset keyed by canonical USD `partKey`.
- Admin/backend concepts are simplified around one canonical scene plus optional derived preview artifacts instead of a geometry-GLB vs production-GLB split.
- STEPper-like seam, sharp-edge, and UV-supporting topology data is computed once during tessellation/export and carried forward as USD-authored mesh metadata.

The target state is not "USD everywhere on day one". The target state is "USD is the canonical persisted scene asset", with derived formats generated only where a consumer still requires them.

## Motivation

The current pipeline is split between:

- a geometry GLB exported directly from STEP via OCC
- a production GLB exported by re-importing geometry into Blender, rebuilding topology cues, replacing materials, and exporting another GLB

That split introduces avoidable duplication, fragility, and impedance mismatches:

- STEP is tessellated multiple times for different outputs.
- Metadata preservation depends on ad hoc transport mechanisms.
- Material replacement depends on object-name matching after format round-trips.
- Sharp edges and seams are re-derived late instead of authored once near the source geometry.
- The frontend and media model are forced to treat "geometry" and "production" as separate assets even though they describe the same product.

The code confirms this architecture:

- Geometry export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L16)
- Production export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L176)
- Blender production export script: [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_gltf.py#L106)
- OCC GLB exporter with XCAF name/color preservation and sharp-edge extras: [render-worker/scripts/export_step_to_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_step_to_gltf.py#L301)
- Media asset model: [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/media/models.py#L11)
- Frontend viewer contract: [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L40)

## Goals

- Establish one canonical persisted scene asset for the visualization pipeline.
- Preserve STEP/XCAF hierarchy, names, colors, and relevant product metadata.
- Support material assignment and replacement without duplicating geometry assets.
- Preserve the current browser-side 3D material assignment workflow, including click-to-select, isolate, hide/ghost other geometry, and assignment of missing materials from Blender asset-library material names.
- Preserve or improve current render quality.
- Preserve or improve UV-unwrapping support via seam and sharp-edge data.
- Reduce format round-trips and late-stage topology repair.
- Allow phased migration without breaking current browser preview or render outputs.
- Simplify admin/backend settings, APIs, and operational actions around one canonical scene model.

## Non-Goals

- Replacing Blender as the final shader/render engine.
- Solving browser-native USD visualization immediately.
- Designing a full custom USD schema package in the first phase.
- Eliminating all derived preview/export assets on day one.

## Current State

### Export Pipeline

The current backend has two first-class CAD outputs:

- `gltf_geometry`
- `gltf_production`

The geometry task exports a GLB directly from STEP using OCC tessellation and optional per-part color mapping. The production task re-exports the STEP file at a higher tessellation quality, then runs Blender headless to:

- import the GLB
- clear OCC-authored custom normals
- mark seams and sharp edges
- append materials from a Blender asset library
- export another GLB

This means the "production" asset is effectively a post-processed derivative of geometry data that already existed earlier in the pipeline.

### Metadata Handling

The OCC exporter already preserves meaningful data:

- XCAF part names
- embedded colors
- sharp edge segment pairs injected into GLB extras

However, this data is not represented in a durable scene data model. Some of it is embedded in GLB extras and consumed indirectly by Blender, while some downstream logic still relies on object-name matching.

### Material Assignment

Material mapping already exists conceptually in the domain model:

- product-level `cad_part_materials`
- canonical material/alias resolution in [backend/app/domains/materials/service.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/materials/service.py#L32)

The weak point is the last mile: materials are currently assigned in Blender by matching imported object names from a GLB round-trip in [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_gltf.py#L192).

### Admin and Settings Surface

The admin/backend model still mirrors the dual-GLB architecture:

- separate preview and production tessellation settings in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/admin.py#L24)
- a bulk action specifically for missing geometry GLBs in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/admin.py#L536)
- an admin UI that exposes preview-vs-production GLB tessellation controls in [frontend/src/pages/Admin.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/pages/Admin.tsx#L1400)
- product detail logic that queries both `gltf_geometry` and `gltf_production` assets in [frontend/src/pages/ProductDetail.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/pages/ProductDetail.tsx#L182)

That duplication is operationally expensive and should be reduced as part of the refactor, not carried forward under new names.

### Seam and Unwrap Support

Blender currently recreates seams by angle selection and then supplements them with OCC-derived sharp-edge segment pairs.

STEPper shows a stronger approach:

- edge seams derived from triangle batch/material boundaries
- sharpness derived from normal discontinuity across shared topology
- UV data preserved from OCC triangulation

Relevant references:

- [STEPper/trimesh.py](/home/hartmut/.config/blender/5.0/scripts/addons/STEPper/trimesh.py#L251)
- [STEPper/main.py](/home/hartmut/.config/blender/5.0/scripts/addons/STEPper/main.py#L297)
- [STEPper/importer.py](/home/hartmut/.config/blender/5.0/scripts/addons/STEPper/importer.py#L620)

## Problems With the Current Design

### 1. Duplicate canonical assets

The system treats preview geometry and production geometry as separate first-class artifacts even though they represent the same assembly. This forces duplicated media types, viewer logic, and task orchestration.

### 2. Late topology repair

Sharp edges, seams, and related unwrap hints are reconstructed in Blender after import instead of being preserved as part of the authored scene.

### 3. Name-based material matching

Material replacement currently depends on imported object naming conventions. That is brittle across exporters, instancing, suffix normalization, and hierarchy flattening.

### 4. Format-driven workflow instead of scene-driven workflow

GLB is used as both transport and canonical source. That works for browser preview, but it is not a strong scene interchange format for preserving richer metadata, overrides, and layered authoring decisions.

### 5. Frontend/backend coupling to an implementation detail

The frontend API assumes two separate GLB assets. That is a product of today’s implementation, not a business requirement.

### 6. Admin coupling to the legacy artifact split

Admin settings, API vocabulary, and repair actions are more complex than necessary because they expose the current implementation detail of two first-class GLB outputs.

## Proposal

## Overview

Introduce a canonical `usd_master` asset authored directly from STEP/XCAF tessellation output. This USD stage becomes the single source of truth for:

- geometry
- hierarchy
- source metadata
- default display bindings
- seam/sharp/unwrap support data

Derived outputs become optional compatibility products:

- browser preview GLB, if needed
- Blender render session imports
- downstream packaged deliveries

Important constraint:

- the browser viewer must retain today’s interactive material-assignment workflow

Therefore this RFC does not assume that the browser renders the canonical USD directly. The canonical scene is USD, but the browser may continue to consume a derived interactive preview mesh package as long as it preserves canonical part identity.

The existing dual-GLB split is replaced conceptually by:

- one canonical authored scene
- zero or more derived consumer-specific outputs

## Canonical USD Stage Structure

Recommended stage layout:

```text
/Root
/Root/Assembly
/Root/Assembly/<AssemblyNode>
/Root/Assembly/<AssemblyNode>/<PartKey>
/Root/Assembly/<AssemblyNode>/<PartKey>/Mesh
/Root/Looks
/Root/Looks/<MaterialName>
```

### Prim identity

Each part prim must have a stable identity independent of display name. Use a normalized part key derived from:

- XCAF label path or stable assembly path
- original part name
- fallback deterministic hash where required

This key becomes the system’s canonical join key for:

- product material mapping
- render overrides
- frontend selection
- audit/debug output

Object names imported into Blender must no longer be the primary identity mechanism.

## USD Authored Data

Each part prim should carry:

- `schaeffler:partKey`
- `schaeffler:sourceName`
- `schaeffler:sourceAssemblyPath`
- `schaeffler:sourceColor`
- `schaeffler:rawMaterialName`
- `schaeffler:canonicalMaterialName`
- `schaeffler:tessellation:linearDeflectionMm`
- `schaeffler:tessellation:angularDeflectionRad`
- `schaeffler:cadFileId`
- `schaeffler:productId` when available
- `schaeffler:mesh:topologyHash`

Each mesh prim should carry:

- points
- face vertex counts / indices
- normals
- UV set(s)
- display color where useful
- seam/sharp topology payload

### Seam and sharp-edge payload

USD does not have a first-class standard seam concept for Blender UV editing, so this RFC proposes storing authored topology support data as custom primvars or custom attributes on the mesh prim:

- `primvars:schaeffler:seamEdgeVertexPairs`
- `primvars:schaeffler:sharpEdgeVertexPairs`
- `primvars:schaeffler:faceBatchIds`
- `primvars:schaeffler:sourceUv`

The exact encoding can evolve, but the initial implementation should optimize for deterministic Blender reconstruction rather than elegance.

Recommended first-phase encoding:

- edge vertex index pairs in local mesh topology space
- optional fallback world-space segment pairs only if index-space authoring is not yet practical

Index-space is preferred because it survives transforms cleanly and avoids current KD-tree matching workarounds.

## Material Assignment and Replacement

### Design requirements

Material assignment must remain possible at:

- product level
- order-line/render level
- interactive replacement level

without duplicating or re-authoring geometry each time.

### Proposed model

Use USD material bindings as the durable scene-level material assignment mechanism.

The canonical USD contains:

- geometry
- default display materials or placeholder bindings
- canonical material identifiers attached to prims

Render-time or session-time replacement happens through:

- material binding overrides
- optional lightweight USD override layers
- Blender-time shader realization against the `.blend` material library

### Browser-side assignment model

Browser-side assignment must continue to work even when Excel names and tessellated part identities do not match cleanly.

The important design change is this:

- Excel names are an input source for proposed assignments.
- Tessellated USD part keys are the canonical targets that users can actually assign in the browser.

In other words, browser assignment should not depend on "fixing" the Excel name mismatch perfectly. The browser should operate on the actual tessellated scene graph that the user sees, and save overrides against canonical `partKey` values from the USD stage.

Recommended browser workflow:

1. Export USD with stable `partKey` metadata on every tessellated part.
2. Run a reconciliation pass that tries to match Excel rows to those `partKey` targets.
3. Store the result as:
   - proposed auto-matches
   - unmatched Excel rows
   - unmatched tessellated parts
   - user-authored overrides
4. Let the browser assign or repair materials by clicking actual scene parts, not by editing raw Excel names.
5. Persist browser-authored overrides by `partKey`, with provenance indicating they came from manual user assignment.

This preserves the current capability to assign missing materials in the browser, but moves the identity model from fragile name matching to stable part-targeted overrides.

### Preserve current 3D viewer behavior

The current viewer supports several behaviors that must not regress:

- clicking an individual visible geometry in the 3D scene
- pinning that selection
- hiding or ghosting other geometry
- highlighting unassigned parts
- assigning a target material from Blender asset-library material names
- fixing missing assignments interactively when automatic matching fails

Those behaviors currently operate on GLB mesh objects in:

- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L488)
- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L553)
- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L675)
- [frontend/src/components/cad/MaterialPanel.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/MaterialPanel.tsx#L123)

The USD refactor must preserve these capabilities. The replacement is not "browser renders USD directly". The replacement is:

- USD remains the canonical scene and material-binding source of truth
- the browser consumes a derived interactive preview asset
- every preview mesh carries the canonical USD `partKey`
- browser interactions save overrides against `partKey`

This means the browser continues to provide the same UX even if the underlying canonical asset is USD.

### Viewer package design

To preserve current functionality, the system should produce a browser-facing viewer package derived from the canonical USD scene:

- `usd_master`
- `preview.glb` or `preview.gltf`
- `scene_manifest.json`

The preview mesh is not a second canonical asset. It is an interaction/render surrogate for the browser.

The manifest or preview-mesh metadata should carry, per selectable mesh:

- `partKey`
- `sourceName`
- `primPath`
- `effectiveMaterial`
- `assignmentProvenance`
- `isUnassigned`

This allows the browser to continue doing:

- per-part picking
- isolation and ghost/hide of other geometry
- assigned/unassigned highlighting
- library material assignment by visible part

without relying on exporter-specific name normalization.

### Reconciliation model for Excel mismatches

The current system needs normalization and prefix heuristics because the Excel/imported names do not always match the tessellated output exactly. That problem does not disappear with USD, so the workflow needs an explicit reconciliation layer.

Recommended persistence shape:

- `source_material_assignments`
  - raw imported Excel assignments keyed by source name
- `resolved_material_assignments`
  - auto-matched assignments keyed by canonical `partKey`
- `manual_material_overrides`
  - user-authored browser assignments keyed by canonical `partKey`
- `unmatched_source_rows`
  - Excel rows that could not be mapped
- `unassigned_parts`
  - tessellated parts with no resolved material

Resolution priority should be:

1. manual browser override by `partKey`
2. resolved auto-match by `partKey`
3. source color/default display material
4. explicit "unassigned" state in the UI

That makes missing assignments visible instead of silently failing.

### Material assignment data model cleanup

The current backend already has the right conceptual split, but the naming is misleading:

- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/products/models.py#L62) `Product.cad_part_materials` behaves like imported or product-authored source material rows
- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/products/models.py#L34) `CadFile.part_materials` behaves like viewer-side manual assignments or overrides
- [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/cad.py#L395) still presents those overrides as a part-name keyed map
- [backend/app/api/routers/products.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/products.py#L433) performs Excel reconciliation directly into the product-side list

The USD refactor should formalize this into three explicit layers:

- `source_material_assignments`
  - imported Excel or product-authored rows keyed by source-side names
- `resolved_material_assignments`
  - auto-matched canonical assignments keyed by `partKey`
- `manual_material_overrides`
  - browser-authored overrides keyed by `partKey`

`effective_material_assignments` should be computed from those layers rather than stored as a fourth conflicting source of truth.

This should be reflected in API semantics as well:

- product/admin APIs manage source assignments and reconciliation results
- viewer APIs manage manual overrides by canonical `partKey`
- scene/preview responses expose effective assignment plus provenance

The immediate migration does not require renaming every DB column on day one, but the RFC recommends moving the API contract and service-layer vocabulary to these meanings first and treating legacy field names as internal compatibility details until a later migration.

### Browser UI implications

The browser should expose a material-assignment mode based on the canonical tessellated part list:

- click a visible part in the viewer
- inspect its `partKey`, source name, current resolved material, and assignment provenance
- assign a library material or color directly to that part
- optionally bulk-apply to similar/unassigned parts after user confirmation

The browser should also show a reconciliation panel with:

- unmatched Excel material rows
- unassigned tessellated parts
- confidence or match reason for auto-resolved assignments

This is preferable to treating the viewer as a thin visual shell around a name-based import table.

### Asset-library material names in the browser

The browser must continue to present assignable materials using the existing Blender asset-library naming model. That means the user-facing picker still offers canonical library material names, and browser assignments continue to save one of those names as the selected target.

The change is only in the assignment target:

- today: assignment is effectively keyed by normalized GLB mesh name
- target state: assignment is keyed by canonical USD `partKey`

This preserves the current operational workflow for users and avoids a regression in missing-material recovery.

### Material identity

The material service remains relevant:

- raw material names from Excel or upstream systems are resolved to canonical names
- canonical names are stored on USD prims or material-binding metadata
- Blender maps canonical names to appended material assets from the existing library

The key change is that matching is done by `partKey` and binding metadata, not by imported object names.

### USD representation of browser assignments

Browser-authored assignments should be serializable as material-binding overrides against the canonical USD stage.

Internally, the system should keep:

- canonical USD geometry and metadata stable
- user overrides as `partKey -> canonicalMaterialName`

Those overrides can then be:

- applied live in the browser preview layer
- written to a USD override layer for render/export
- flattened into a single USD when a single-file artifact is required

This preserves interactive browser editing without requiring the browser to directly edit Blender assets or depend on exporter-specific naming quirks.

### Single USD versus override layers

The user requirement is one single USD for the whole workflow. There are two practical ways to satisfy that:

#### Option A: Flat single-file publish

- Material bindings are rewritten directly into the canonical root layer.
- Every material change produces a newly published single USD file.

Pros:

- simplest mental model
- easiest artifact handling

Cons:

- mutates the canonical authored stage
- harder to track override intent separately

#### Option B: Canonical USD plus override layer, flattened for delivery

- Canonical geometry and metadata stay stable.
- Material replacements are authored into a lightweight override layer.
- For consumers that require "one file", the stage is flattened on publish/export.

Pros:

- best authoring model
- clean separation between geometry truth and per-context material overrides

Cons:

- slightly more implementation work

Recommendation:

- Use Option B internally.
- Publish a flattened single USD when a single-file artifact is required externally.

This satisfies the "one single USD" requirement at delivery/runtime boundaries without sacrificing maintainability.

## STEPper-Inspired Seam and UV Integration

### What should be reused

STEPper already demonstrates three behaviors worth preserving:

1. UVs are derived from OCC triangulation nodes.
2. Seams can be inferred from face/batch boundaries.
3. Sharpness can be inferred from discontinuity across shared edge topology.

That logic currently exists inside Blender addon code, but the underlying idea belongs earlier in the pipeline.

### Proposed integration

Port or reimplement the relevant topology derivation into the exporter stage that authors USD:

- tessellate once from STEP
- generate UV coordinates directly from OCC triangulation
- compute seam candidates from batch/material/face boundaries
- compute sharp edges from discontinuity tests
- write those results to USD mesh metadata

Then add a Blender import helper that:

- imports the USD stage
- reads the authored seam/sharp payload
- marks `edge.seam` and sharp flags on imported meshes
- preserves authored UVs for unwrapping and texture workflows

### Why this is better

This moves seam and unwrap support from "best-effort Blender reconstruction" to "authored mesh semantics". Blender remains a consumer of that data, not the only place where the data exists.

## System Design Changes

## 1. Export Layer

Add a new exporter script:

- `render-worker/scripts/export_step_to_usd.py`

Responsibilities:

- read STEP with XCAF
- tessellate once
- preserve hierarchy, names, and colors
- author canonical USD stage
- attach metadata and seam/sharp payload
- optionally emit compatibility GLB derived from USD or from the same tessellation pass

The existing `export_step_to_gltf.py` can remain temporarily for migration and fallback.

## 2. Media Asset Model

Extend [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/media/models.py#L11) with at least:

- `usd_master`

Optional later additions:

- `usd_preview`
- `usd_render_package`

The important change is that `usd_master` becomes the canonical CAD scene artifact associated with a `cad_file`.

## 3. Pipeline Tasks

Replace the current dual-export mental model in [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L16) with:

- `generate_usd_master_task`
- optional `generate_preview_glb_task`
- optional `generate_delivery_usd_task` if flattening/packaging is separated

The production GLB task should be retired once Blender can render from USD and browser preview is handled separately.

## 4. Render Service

The render service in [backend/app/services/render_blender.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/services/render_blender.py#L18) currently converts `STEP -> GLB -> Blender render`.

Target flow:

- `USD -> Blender render`

The render subprocess should:

- import USD
- read canonical part metadata
- apply material overrides by `partKey`
- restore seam/sharp marks from authored USD mesh metadata when needed
- render stills and turntables

This removes the need for a production GLB as an intermediate render artifact.

## 5. Frontend

The current viewer API in [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L40) assumes:

- one geometry GLB
- one production GLB

Target contract:

- one canonical scene asset reference
- one derived interactive preview artifact reference for browser consumption
- one scene-manifest payload carrying canonical per-part identity and assignment state

Short term:

- keep browser rendering on GLB or glTF
- derive the preview asset from the canonical USD workflow
- preserve today’s select/isolate/hide/material-assign interactions in the browser

Long term:

- evaluate a USD-capable viewer only if it is clearly worth the operational complexity

The frontend should stop encoding the architectural distinction between geometry and production assets, but it must retain the current interactive viewer workflow.

## 6. Viewer Assignment API

The current viewer override endpoint in [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/cad.py#L395) should evolve from a raw part-name keyed map into a canonical scene-assignment endpoint.

Target behavior:

- `GET /cad/{id}/part-materials` returns manual overrides keyed by `partKey` plus effective assignment metadata
- `PUT /cad/{id}/part-materials` accepts a full override map keyed by `partKey`
- the API response includes assignment provenance so the browser can distinguish auto-match from manual repair
- the browser does not need to know whether the preview asset came from GLB, glTF, or another future format

This preserves the current operational workflow while removing exporter-specific identity assumptions.

## 7. Admin and Backend Simplification

### Settings

The current admin settings model exposes four tessellation knobs:

- `gltf_preview_linear_deflection`
- `gltf_preview_angular_deflection`
- `gltf_production_linear_deflection`
- `gltf_production_angular_deflection`

That made sense when both outputs were first-class. In the USD model, the settings should be simplified to reflect the actual scene pipeline:

- canonical scene tessellation profile for `usd_master`
- optional preview-derivation profile if the browser preview truly needs a lower-cost surrogate
- optional render-import or delivery profile only if direct USD consumption proves insufficient

Recommendation:

- collapse the current preview/production naming into `scene_*` and `preview_*` concepts
- do not expose a separate production-GLB quality preset in the target design
- keep the number of admin-visible tessellation profiles to the minimum required operationally

### Bulk actions and repair flows

Admin actions should also move from artifact-specific wording to scene pipeline wording.

Examples:

- replace "generate missing geometry GLBs" with "generate missing canonical scenes" and, if needed, "regenerate missing viewer previews"
- keep "reextract metadata" but scope it to canonical scene metadata, not GLB extras
- ensure "re-process STEP" means rebuild canonical USD, manifest, and preview derivatives together

### Frontend/admin simplification

The admin and product-detail UI should stop presenting `gltf_geometry` and `gltf_production` as equal first-class business objects.

Target UI model:

- one canonical scene status
- one preview availability status
- one effective material-assignment status, including unmatched and manually overridden counts

This should remove a class of user confusion where two 3D artifacts exist for the same product but only one is actually canonical.

## Proposed Data Contract

## Canonical scene metadata

For a `cad_file`, API responses should eventually expose:

- canonical scene asset id
- canonical scene asset type
- preview asset url if browser preview uses GLB or glTF
- scene manifest for canonical part identity and effective material state
- list of part identities and metadata extracted from the canonical scene

Example shape:

```json
{
  "cad_file_id": "uuid",
  "scene_asset": {
    "asset_type": "usd_master",
    "url": "/media/..."
  },
  "preview_asset": {
    "asset_type": "gltf_preview",
    "url": "/media/..."
  },
  "scene_manifest": {
    "parts": [
      {
        "part_key": "ring_outer",
        "source_name": "RingOuter_AF0",
        "prim_path": "/Root/Assembly/Bearing/RingOuter",
        "effective_material": "SCHAEFFLER_010102_Steel-Polished",
        "assignment_provenance": "manual",
        "is_unassigned": false
      }
    ]
  },
  "parts": [
    {
      "part_key": "ring_outer",
      "source_name": "RingOuter_AF0",
      "canonical_material_name": "Steel_Polished"
    }
  ]
}
```

## Migration Plan

### Phase 1: Dual-write USD beside GLB

- add `export_step_to_usd.py`
- add `usd_master` media asset type
- export USD in parallel with existing GLB path
- validate hierarchy, metadata, part identity, and tessellation parity

Exit criteria:

- USD contains all required part metadata
- part count and hierarchy are stable versus current outputs

### Phase 2: Move material identity to canonical part keys

- stop relying on Blender object-name matching
- resolve material mappings to canonical part keys
- persist canonical material identity on USD prims

Exit criteria:

- material replacement works without name heuristics

### Phase 3: Move seam/sharp/UV data into authored scene output

- port STEPper-inspired topology logic into exporter
- write seam/sharp payload to USD
- create Blender-side reconstruction helper for import

Exit criteria:

- Blender unwrap workflow can use authored seams
- current seam quality is matched or improved

### Phase 4: Switch Blender render from GLB to USD

- update Blender render scripts and backend service entrypoints
- import USD directly
- bind materials from canonical material metadata

Exit criteria:

- still and turntable outputs match current production quality
- production GLB is no longer required for rendering

### Phase 5: Decouple frontend from dual-GLB model

- update API/viewer contract to use one canonical scene reference
- retain a derived interactive preview asset
- introduce a manifest-driven browser identity model based on `partKey`
- preserve current isolate/hide/material-assignment behavior in the 3D viewer

Exit criteria:

- frontend no longer depends on `geometryGltfUrl` plus `productionGltfUrl`
- frontend still supports current browser-side material assignment workflow without regression

### Phase 6: Retire legacy GLB pipeline split

- remove `gltf_geometry` / `gltf_production` as canonical workflow states
- keep preview GLB only as a derived compatibility artifact if still needed

### Phase 7: Simplify admin and product-facing operational surfaces

- collapse admin settings and labels to canonical scene plus preview concepts
- align CAD/product APIs with source, resolved, and manual material assignment semantics
- remove product-detail assumptions that two GLB assets are first-class workflow outputs

Exit criteria:

- admin no longer exposes preview-vs-production GLB tessellation as the primary mental model
- product and CAD APIs expose canonical part-keyed assignment semantics
- operators can regenerate canonical scenes and viewer previews without reasoning about legacy GLB pipeline stages

## Non-Regression Requirements

The USD refactor is unacceptable if it removes the existing browser-assisted material repair workflow. The following capabilities are mandatory:

- a user can click a visible part in the browser preview and the selection resolves to a stable canonical `partKey`
- a user can pin a selection and isolate, hide, or ghost non-selected geometry
- the viewer can visually identify unassigned parts
- a user can assign a Blender asset-library material name to the selected part
- the assignment is persisted as a manual override keyed by `partKey`
- reloading the viewer restores the same effective assignment and unassigned-state visualization
- render/export consumers use the same effective assignment state that the browser shows
- the workflow still functions when Excel reconciliation fails for some rows

Acceptance criteria:

1. For a CAD file with mismatched Excel names, the system still produces a canonical scene, a selectable preview asset, a list of unmatched source rows, and a list of unassigned parts.
2. In the 3D viewer, selecting a part and assigning a library material updates the effective assignment immediately without depending on raw GLB mesh-name heuristics.
3. After refresh, the same part remains assigned through the persisted `partKey -> canonicalMaterialName` override mapping.
4. A subsequent Blender render or export consumes that same override and produces matching material output.
5. The preview asset exposes canonical `partKey` identity for every selectable mesh.
6. The migrated workflow does not require `geometryGltfUrl` plus `productionGltfUrl` as separate first-class viewer inputs.

## Risks

### 1. Browser preview remains GLB-bound

USD is a strong canonical scene format, but web runtime support is still weaker than GLB. The likely near-term result is a canonical USD plus a derived browser preview asset. This is acceptable and explicitly part of the design, provided the preview asset is treated as non-canonical and keeps stable `partKey` metadata for interaction.

### 2. Blender USD import behavior may not preserve custom topology semantics automatically

Seam and sharp-edge reconstruction will likely require explicit Blender-side helper code. This is expected and acceptable, but it should be treated as a planned integration task, not assumed to work out of the box.

### 3. Material library remains Blender-native

If the authoritative shaders stay in `.blend` assets, then USD carries material identity and binding intent, while final shader realization still happens in Blender. That is acceptable, but it should be acknowledged explicitly.

### 4. Single-file requirement can conflict with layered authoring

A strict "always exactly one USD file at every intermediate step" requirement would push the system toward root-layer mutation. That is possible, but it is worse than using override layers internally and flattening when needed.

## Alternatives Considered

### Keep the current GLB split and improve it incrementally

Rejected because it preserves the wrong abstraction boundary. It can reduce pain locally, but it does not solve the duplication between canonical geometry, render geometry, and metadata transport.

### Use Blender as the canonical scene authoring tool

Rejected because STEP/XCAF metadata and topology are better preserved at the OCC/export layer, not after import into Blender.

### Move directly to USD-only everywhere

Rejected for the first implementation because browser preview and some tooling still benefit from GLB compatibility outputs.

## Open Questions

- Which USD authoring library will be used in the exporter environment?
- Should seam/sharp payload use custom attributes, primvars, or a lightweight in-house schema from the start?
- Do any downstream consumers require per-face material subsets instead of per-part bindings?
- Is a flattened single-file USD required for all persisted states, or only for delivery and render handoff?
- Should preview GLB be derived from USD, or should both USD and GLB be authored from the same tessellation pass during migration?

## Recommendation

Adopt USD as the canonical persisted scene asset and migrate in phases.

The implementation stance should be:

- author geometry, hierarchy, metadata, and seam/sharp payload once from STEP
- store that in `usd_master`
- drive material replacement via part-keyed USD bindings and overrides
- keep Blender as a renderer/material-realization consumer
- keep GLB only as a compatibility derivative where needed

This is the cleanest path to a single-scene workflow without sacrificing material replacement, metadata fidelity, or unwrap support.

## Initial Work Breakdown

1. Add `usd_master` media asset type and migration.
2. Implement `export_step_to_usd.py` with hierarchy, names, colors, and part keys.
3. Add canonical part-key generation and persistence.
4. Port STEPper-inspired UV and seam/sharp derivation into exporter output.
5. Add Blender USD import helper for seam/sharp restoration and material rebinding.
6. Introduce explicit source, resolved, and manual material-assignment service semantics keyed by `partKey`.
7. Change frontend/API contract to a canonical scene asset plus optional preview asset.
8. Preserve the current browser viewer workflow through preview-manifest driven part selection and assignment.
9. Simplify admin settings and repair actions around canonical scenes and derived previews.
10. Switch still/turntable render path to USD input.
