ee6eb34b4c
- GPU: fix Cycles device activation order — set compute_device_type BEFORE engine init, re-set AFTER open_mainfile wipes preferences - GPU: remove _mark_sharp_and_seams edit-mode loop (redundant with Blender 5.0 shade_smooth_by_angle), saves ~200s/render on 175 parts - Material: fix _AFN suffix mismatch — build AF-stripped mat_map keys and add prefix fallback in _apply_material_library (blender_render.py) - Material: production GLB now uses get_material_library_path() which checks active AssetLibrary instead of empty legacy system setting - Admin: RenderTemplateTable multi-select output types (M2M frontend) - Admin: MaterialLibraryPanel replaced with link to Asset Libraries - UX: move Toaster to top-left to avoid dispatch button overlap - SQLAlchemy: add .unique() to all RenderTemplate M2M collection queries - Logging: flush=True on all Blender progress prints, stdout reconfigure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
import { useQuery, useMutation } from '@tanstack/react-query'
|
|
import { Box, Loader2, X } from 'lucide-react'
|
|
import ThreeDViewer from '../components/cad/ThreeDViewer'
|
|
import { getMediaAssets } from '../api/media'
|
|
import { generateGltfGeometry } from '../api/cad'
|
|
|
|
/**
|
|
* Route: /cad/:id
|
|
*
|
|
* Full-screen 3D viewer for a CAD file.
|
|
* If no geometry GLB exists yet, offers to generate one on demand.
|
|
*/
|
|
export default function CadPreviewPage() {
|
|
const { id } = useParams<{ id: string }>()
|
|
const navigate = useNavigate()
|
|
const [generating, setGenerating] = useState(false)
|
|
|
|
// Load any geometry GLB that was generated for this CAD file
|
|
// Poll every 3s while generating so it appears automatically
|
|
const { data: gltfAssets, isLoading: gltfLoading } = useQuery({
|
|
queryKey: ['media-assets', id, 'gltf_geometry'],
|
|
queryFn: () => getMediaAssets({ cad_file_id: id!, asset_types: ['gltf_geometry'] }),
|
|
enabled: !!id,
|
|
staleTime: 5_000,
|
|
refetchInterval: generating ? 3_000 : false,
|
|
})
|
|
|
|
// Load production GLB if available
|
|
const { data: productionAssets } = useQuery({
|
|
queryKey: ['media-assets', id, 'gltf_production'],
|
|
queryFn: () => getMediaAssets({ cad_file_id: id!, asset_types: ['gltf_production'] }),
|
|
enabled: !!id,
|
|
staleTime: 30_000,
|
|
})
|
|
|
|
// Load blend assets for download
|
|
const { data: blendAssets } = useQuery({
|
|
queryKey: ['media-assets', id, 'blend_production'],
|
|
queryFn: () => getMediaAssets({ cad_file_id: id!, asset_types: ['blend_production'] }),
|
|
enabled: !!id,
|
|
staleTime: 30_000,
|
|
})
|
|
|
|
const generateMutation = useMutation({
|
|
mutationFn: () => generateGltfGeometry(id!),
|
|
onSuccess: () => {
|
|
setGenerating(true)
|
|
},
|
|
})
|
|
|
|
// Stop polling once asset appears
|
|
useEffect(() => {
|
|
if (generating && gltfAssets && gltfAssets.length > 0) {
|
|
setGenerating(false)
|
|
}
|
|
}, [generating, gltfAssets])
|
|
|
|
if (!id) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full text-content-muted p-8">
|
|
<p>No CAD file ID provided.</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const latestGltf = gltfAssets?.[0]
|
|
const latestProduction = productionAssets?.[0]
|
|
const latestBlend = blendAssets?.[0]
|
|
|
|
// While checking for assets, show a neutral loading screen (don't attempt to render ThreeDViewer)
|
|
if (gltfLoading) {
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-gray-950 gap-3">
|
|
<Loader2 size={36} className="animate-spin text-gray-400" />
|
|
<p className="text-gray-400 text-sm">Checking for 3D model…</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// No GLB at all — show generate prompt
|
|
if (!latestGltf && !latestProduction) {
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex flex-col bg-gray-950">
|
|
<div className="flex items-center justify-between px-5 py-3 bg-gray-900 border-b border-gray-800">
|
|
<span className="text-white font-semibold tracking-wide">3D Viewer</span>
|
|
<button
|
|
onClick={() => navigate(-1)}
|
|
className="p-1.5 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
<div className="flex-1 flex flex-col items-center justify-center gap-4 text-center px-8">
|
|
<Box size={48} className="text-gray-600" />
|
|
<p className="text-white text-lg font-semibold">No 3D model available yet</p>
|
|
<p className="text-gray-400 text-sm max-w-sm">
|
|
Generate a geometry GLB from the STEP file to enable the 3D viewer.
|
|
Process the STEP file first to make it available.
|
|
</p>
|
|
{generating ? (
|
|
<div className="flex items-center gap-2 text-gray-300 text-sm">
|
|
<Loader2 size={16} className="animate-spin" />
|
|
Generating… checking every 3s
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => generateMutation.mutate()}
|
|
disabled={generateMutation.isPending}
|
|
className="px-5 py-2 rounded-md bg-accent hover:bg-accent-hover disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-medium transition-colors flex items-center gap-2"
|
|
>
|
|
{generateMutation.isPending && <Loader2 size={14} className="animate-spin" />}
|
|
Generate 3D Model
|
|
</button>
|
|
)}
|
|
{generateMutation.isError && (
|
|
<p className="text-red-400 text-sm">
|
|
Failed to start generation. Make sure the STEP file has been processed.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<ThreeDViewer
|
|
cadFileId={id}
|
|
onClose={() => navigate(-1)}
|
|
geometryGltfUrl={latestGltf?.download_url ?? undefined}
|
|
productionGltfUrl={latestProduction?.download_url ?? undefined}
|
|
hasGeometryGlb={!!latestGltf}
|
|
hasProductionGlb={!!latestProduction}
|
|
isGeneratingGeometry={generating}
|
|
onGenerateGeometry={() => generateMutation.mutate()}
|
|
downloadUrls={{
|
|
glb: latestGltf?.download_url ?? undefined,
|
|
production: latestProduction?.download_url ?? undefined,
|
|
blend: latestBlend?.download_url ?? undefined,
|
|
}}
|
|
/>
|
|
)
|
|
}
|