feat(P2): USD Foundation — canonical part identity + material overrides
M1 — USD exporter:
- render-worker/scripts/export_step_to_usd.py (631 lines)
Full XCAF traversal, one UsdGeom.Mesh per leaf part,
schaeffler:partKey on every prim, index-space sharpEdgeVertexPairs
- render-worker/Dockerfile: usd-core>=24.11 installed (USD 0.26.3)
M2 — usd_master MediaAsset + pipeline auto-chain:
- migrations 060 (usd_master enum), 061 (3 JSONB columns),
062 (rename tessellation settings keys)
- generate_usd_master_task: runs export_step_to_usd.py, upserts
usd_master MediaAsset, writes resolved_material_assignments to CadFile
- Auto-chained from generate_gltf_geometry_task after every GLB export
- step_tasks.py shim re-exports generate_usd_master_task
M3 — scene-manifest API:
- part_key_service.py: build_scene_manifest(), generate_part_key(),
four-layer material priority resolution with provenance
- SceneManifest / PartEntry Pydantic models in products/schemas.py
- GET /api/cad/{id}/scene-manifest endpoint (graceful fallback to
parsed_objects when USD not yet generated)
- POST /api/cad/{id}/generate-usd-master endpoint
- frontend/src/api/sceneManifest.ts: fetchSceneManifest(),
triggerUsdMasterGeneration()
M4 — manual-material-overrides API:
- GET/PUT /api/cad/{id}/manual-material-overrides endpoints
- CadFile.manual_material_overrides JSONB column (migration 061)
- getManualOverrides() / saveManualOverrides() in cad.ts
M5 — ThreeDViewer partKey integration:
- export_step_to_gltf.py injects partKeyMap into GLB extras
- ThreeDViewer: partKeyMap extraction, resolvePartKey(), effectiveMaterials
merges legacy partMaterials + new manualOverrides (server-side persistence)
- MaterialPanel: dual-path save (partKey vs legacy), provenance badge,
reconciliation panel for unmatched/unassigned parts
Also:
- Admin.tsx: generate-missing-usd-masters + canonical scenes bulk actions
- ProductDetail.tsx: usd_master row in asset table
- vite-env.d.ts: fix ImportMeta.env TypeScript error
- GPUProbeResult: add timestamp/devices/render_time_s fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -434,3 +434,110 @@ async def save_part_materials(
|
||||
cad_file_id=str(cad.id),
|
||||
part_materials=cad.part_materials,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
# Manual material overrides schemas (partKey-keyed)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ManualMaterialOverridesIn(BaseModel):
|
||||
overrides: dict[str, str] # { partKey: materialName }
|
||||
|
||||
|
||||
class ManualMaterialOverridesOut(BaseModel):
|
||||
cad_file_id: str
|
||||
manual_material_overrides: dict[str, str] | None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# USD master endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/{id}/scene-manifest")
|
||||
async def get_scene_manifest(
|
||||
id: uuid.UUID,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Return scene manifest for a CAD file (part keys, material assignments)."""
|
||||
from app.domains.products.schemas import SceneManifest
|
||||
from app.services.part_key_service import build_scene_manifest
|
||||
from app.domains.media.models import MediaAsset, MediaAssetType
|
||||
|
||||
cad = await _get_cad_file(id, db)
|
||||
|
||||
usd_result = await db.execute(
|
||||
select(MediaAsset).where(
|
||||
MediaAsset.cad_file_id == id,
|
||||
MediaAsset.asset_type == MediaAssetType.usd_master,
|
||||
)
|
||||
)
|
||||
usd_asset = usd_result.scalars().first()
|
||||
|
||||
if not usd_asset and not cad.parsed_objects:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scene manifest not yet available — run generate-usd-master first",
|
||||
)
|
||||
|
||||
manifest_dict = build_scene_manifest(cad, usd_asset)
|
||||
return SceneManifest(**manifest_dict)
|
||||
|
||||
|
||||
@router.post("/{id}/generate-usd-master", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def generate_usd_master(
|
||||
id: uuid.UUID,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Queue a USD master export for a CAD file."""
|
||||
if not is_privileged(user):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
cad = await _get_cad_file(id, db)
|
||||
if not cad.stored_path:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No STEP file stored")
|
||||
|
||||
from app.tasks.step_tasks import generate_usd_master_task
|
||||
task = generate_usd_master_task.delay(str(id))
|
||||
return {"status": "queued", "task_id": task.id, "cad_file_id": str(id)}
|
||||
|
||||
|
||||
@router.get("/{id}/manual-material-overrides", response_model=ManualMaterialOverridesOut)
|
||||
async def get_manual_material_overrides(
|
||||
id: uuid.UUID,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Return manual material overrides (partKey → materialName) for a CAD file."""
|
||||
cad = await _get_cad_file(id, db)
|
||||
return ManualMaterialOverridesOut(
|
||||
cad_file_id=str(id),
|
||||
manual_material_overrides=cad.manual_material_overrides,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{id}/manual-material-overrides", response_model=ManualMaterialOverridesOut)
|
||||
async def save_manual_material_overrides(
|
||||
id: uuid.UUID,
|
||||
body: ManualMaterialOverridesIn,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Save manual material overrides keyed by partKey.
|
||||
|
||||
Writes to CadFile.manual_material_overrides (JSONB).
|
||||
Takes priority over auto-resolved and source-matched materials in build_scene_manifest().
|
||||
"""
|
||||
if not is_privileged(user):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
cad = await _get_cad_file(id, db)
|
||||
cad.manual_material_overrides = body.overrides
|
||||
await db.commit()
|
||||
await db.refresh(cad)
|
||||
return ManualMaterialOverridesOut(
|
||||
cad_file_id=str(id),
|
||||
manual_material_overrides=cad.manual_material_overrides,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user