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:
@@ -108,10 +108,10 @@ export default function AdminPage() {
|
||||
gltf_material_quality: string
|
||||
gltf_pbr_roughness: number
|
||||
gltf_pbr_metallic: number
|
||||
gltf_preview_linear_deflection: number
|
||||
gltf_preview_angular_deflection: number
|
||||
gltf_production_linear_deflection: number
|
||||
gltf_production_angular_deflection: number
|
||||
scene_linear_deflection: number
|
||||
scene_angular_deflection: number
|
||||
render_linear_deflection: number
|
||||
render_angular_deflection: number
|
||||
tessellation_engine: string
|
||||
}
|
||||
|
||||
@@ -224,6 +224,18 @@ export default function AdminPage() {
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'),
|
||||
})
|
||||
|
||||
const generateMissingUsdMastersMut = useMutation({
|
||||
mutationFn: () => api.post('/admin/settings/generate-missing-usd-masters'),
|
||||
onSuccess: (res) => toast.success(res.data.message || 'USD master export queued'),
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'),
|
||||
})
|
||||
|
||||
const generateMissingCanonicalScenesMut = useMutation({
|
||||
mutationFn: () => api.post('/admin/settings/generate-missing-canonical-scenes'),
|
||||
onSuccess: (res) => toast.success(res.data.message || 'Canonical scene export queued'),
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'),
|
||||
})
|
||||
|
||||
const [smtpDraft, setSmtpDraft] = useState<Partial<Settings>>({})
|
||||
const smtp = { ...settings, ...smtpDraft } as Settings
|
||||
|
||||
@@ -921,6 +933,30 @@ export default function AdminPage() {
|
||||
</div>
|
||||
<p className="text-xs text-content-muted">Re-renders thumbnails for all completed CAD files.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => generateMissingUsdMastersMut.mutate()}
|
||||
disabled={generateMissingUsdMastersMut.isPending}
|
||||
className="btn-secondary text-sm w-full justify-start"
|
||||
title="Queue USD master export for all completed CAD files without a USD master asset"
|
||||
>
|
||||
<RefreshCw size={14} className={generateMissingUsdMastersMut.isPending ? 'animate-spin' : ''} />
|
||||
{generateMissingUsdMastersMut.isPending ? 'Queueing…' : 'Generate Missing USD Masters'}
|
||||
</button>
|
||||
<p className="text-xs text-content-muted">Exports USD canonical scene for all completed CAD files missing one.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => generateMissingCanonicalScenesMut.mutate()}
|
||||
disabled={generateMissingCanonicalScenesMut.isPending}
|
||||
className="btn-secondary text-sm w-full justify-start"
|
||||
title="Queue geometry GLB + USD master export for all completed CAD files without a geometry GLB"
|
||||
>
|
||||
<RefreshCw size={14} className={generateMissingCanonicalScenesMut.isPending ? 'animate-spin' : ''} />
|
||||
{generateMissingCanonicalScenesMut.isPending ? 'Queueing…' : 'Generate Missing Canonical Scenes'}
|
||||
</button>
|
||||
<p className="text-xs text-content-muted">Queues geometry GLB + USD master for all completed CAD files missing a canonical scene.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => importMediaAssetsMut.mutate()}
|
||||
@@ -947,11 +983,12 @@ export default function AdminPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (window.confirm('Delete all orphaned STEP files (not linked to any product)? This cannot be undone.')) {
|
||||
cleanupOrphanedCadMut.mutate()
|
||||
}
|
||||
}}
|
||||
onClick={() => setConfirmState({
|
||||
open: true,
|
||||
title: 'Delete Orphaned STEP Files',
|
||||
message: 'Delete all orphaned STEP files (not linked to any product)? This cannot be undone.',
|
||||
onConfirm: () => { cleanupOrphanedCadMut.mutate(); setConfirmState(s => ({ ...s, open: false })) },
|
||||
})}
|
||||
disabled={cleanupOrphanedCadMut.isPending}
|
||||
className="btn-secondary text-sm w-full justify-start"
|
||||
title="Delete STEP files and thumbnails that are no longer linked to any product"
|
||||
@@ -1416,26 +1453,26 @@ export default function AdminPage() {
|
||||
label: 'Draft',
|
||||
description: 'Fast export, visible faceting on large curves',
|
||||
color: 'border-amber-400 text-amber-700',
|
||||
values: { gltf_preview_linear_deflection: 0.2, gltf_preview_angular_deflection: 0.3, gltf_production_linear_deflection: 0.05, gltf_production_angular_deflection: 0.1 },
|
||||
values: { scene_linear_deflection: 0.2, scene_angular_deflection: 0.3, render_linear_deflection: 0.05, render_angular_deflection: 0.1 },
|
||||
},
|
||||
{
|
||||
label: 'Standard',
|
||||
description: 'Smooth curves, no fan artifacts — recommended',
|
||||
color: 'border-blue-400 text-blue-700',
|
||||
values: { gltf_preview_linear_deflection: 0.1, gltf_preview_angular_deflection: 0.1, gltf_production_linear_deflection: 0.03, gltf_production_angular_deflection: 0.05 },
|
||||
values: { scene_linear_deflection: 0.1, scene_angular_deflection: 0.1, render_linear_deflection: 0.03, render_angular_deflection: 0.05 },
|
||||
},
|
||||
{
|
||||
label: 'Fine',
|
||||
description: 'Maximum quality, very large files, slow export',
|
||||
color: 'border-emerald-400 text-emerald-700',
|
||||
values: { gltf_preview_linear_deflection: 0.05, gltf_preview_angular_deflection: 0.05, gltf_production_linear_deflection: 0.01, gltf_production_angular_deflection: 0.02 },
|
||||
values: { scene_linear_deflection: 0.05, scene_angular_deflection: 0.05, render_linear_deflection: 0.01, render_angular_deflection: 0.02 },
|
||||
},
|
||||
]
|
||||
const isActive = (preset: typeof PRESETS[0]) =>
|
||||
tess.gltf_preview_linear_deflection === preset.values.gltf_preview_linear_deflection &&
|
||||
tess.gltf_preview_angular_deflection === preset.values.gltf_preview_angular_deflection &&
|
||||
tess.gltf_production_linear_deflection === preset.values.gltf_production_linear_deflection &&
|
||||
tess.gltf_production_angular_deflection === preset.values.gltf_production_angular_deflection
|
||||
tess.scene_linear_deflection === preset.values.scene_linear_deflection &&
|
||||
tess.scene_angular_deflection === preset.values.scene_angular_deflection &&
|
||||
tess.render_linear_deflection === preset.values.render_linear_deflection &&
|
||||
tess.render_angular_deflection === preset.values.render_angular_deflection
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide mb-2">Presets</p>
|
||||
@@ -1450,8 +1487,8 @@ export default function AdminPage() {
|
||||
<div className="font-semibold text-sm">{preset.label}</div>
|
||||
<div className="text-xs text-content-muted mt-0.5">{preset.description}</div>
|
||||
<div className="text-xs font-mono text-content-secondary mt-1 space-y-0.5">
|
||||
<div>preview: {preset.values.gltf_preview_angular_deflection} rad / {preset.values.gltf_preview_linear_deflection} mm</div>
|
||||
<div>prod: {preset.values.gltf_production_angular_deflection} rad / {preset.values.gltf_production_linear_deflection} mm</div>
|
||||
<div>scene: {preset.values.scene_angular_deflection} rad / {preset.values.scene_linear_deflection} mm</div>
|
||||
<div>render: {preset.values.render_angular_deflection} rad / {preset.values.render_linear_deflection} mm</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -1491,7 +1528,7 @@ export default function AdminPage() {
|
||||
{/* Manual inputs */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Preview (Geometry GLB)</p>
|
||||
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Scene / Viewer</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-sm text-content-secondary w-36 shrink-0">Linear deflection</label>
|
||||
<input
|
||||
@@ -1499,8 +1536,8 @@ export default function AdminPage() {
|
||||
step="0.01"
|
||||
min="0.001"
|
||||
max="10"
|
||||
value={tess.gltf_preview_linear_deflection ?? 0.1}
|
||||
onChange={e => setTessellationDraft(d => ({ ...d, gltf_preview_linear_deflection: parseFloat(e.target.value) }))}
|
||||
value={tess.scene_linear_deflection ?? 0.1}
|
||||
onChange={e => setTessellationDraft(d => ({ ...d, scene_linear_deflection: parseFloat(e.target.value) }))}
|
||||
className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400"
|
||||
/>
|
||||
<span className="text-sm text-content-muted">mm</span>
|
||||
@@ -1512,16 +1549,16 @@ export default function AdminPage() {
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
max="1.5"
|
||||
value={tess.gltf_preview_angular_deflection ?? 0.1}
|
||||
onChange={e => setTessellationDraft(d => ({ ...d, gltf_preview_angular_deflection: parseFloat(e.target.value) }))}
|
||||
value={tess.scene_angular_deflection ?? 0.1}
|
||||
onChange={e => setTessellationDraft(d => ({ ...d, scene_angular_deflection: parseFloat(e.target.value) }))}
|
||||
className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400"
|
||||
/>
|
||||
<span className="text-sm text-content-muted">rad</span>
|
||||
</div>
|
||||
<p className="text-xs text-content-muted">Used when clicking "Generate Geometry GLB".</p>
|
||||
<p className="text-xs text-content-muted">Used for the 3D viewer (canonical scene). Smaller = smoother surfaces.</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Production (Production GLB)</p>
|
||||
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Render output</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-sm text-content-secondary w-36 shrink-0">Linear deflection</label>
|
||||
<input
|
||||
@@ -1529,8 +1566,8 @@ export default function AdminPage() {
|
||||
step="0.005"
|
||||
min="0.001"
|
||||
max="10"
|
||||
value={tess.gltf_production_linear_deflection ?? 0.03}
|
||||
onChange={e => setTessellationDraft(d => ({ ...d, gltf_production_linear_deflection: parseFloat(e.target.value) }))}
|
||||
value={tess.render_linear_deflection ?? 0.03}
|
||||
onChange={e => setTessellationDraft(d => ({ ...d, render_linear_deflection: parseFloat(e.target.value) }))}
|
||||
className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400"
|
||||
/>
|
||||
<span className="text-sm text-content-muted">mm</span>
|
||||
@@ -1542,13 +1579,13 @@ export default function AdminPage() {
|
||||
step="0.005"
|
||||
min="0.005"
|
||||
max="1.5"
|
||||
value={tess.gltf_production_angular_deflection ?? 0.05}
|
||||
onChange={e => setTessellationDraft(d => ({ ...d, gltf_production_angular_deflection: parseFloat(e.target.value) }))}
|
||||
value={tess.render_angular_deflection ?? 0.05}
|
||||
onChange={e => setTessellationDraft(d => ({ ...d, render_angular_deflection: parseFloat(e.target.value) }))}
|
||||
className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400"
|
||||
/>
|
||||
<span className="text-sm text-content-muted">rad</span>
|
||||
</div>
|
||||
<p className="text-xs text-content-muted">Used when clicking "Generate Production GLB". Smaller = smoother surfaces.</p>
|
||||
<p className="text-xs text-content-muted">Used for final render output. Smaller = smoother surfaces, larger file sizes.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -1621,7 +1658,7 @@ export default function AdminPage() {
|
||||
</button>
|
||||
{gpuProbeResult && (
|
||||
<span className="text-xs text-content-muted">
|
||||
Last checked: {new Date(gpuProbeResult.timestamp).toLocaleString()}
|
||||
Last checked: {gpuProbeResult.timestamp ? new Date(gpuProbeResult.timestamp).toLocaleString() : '—'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user