Files
HartOMat/frontend/src/pages/CadPreview.tsx
T
Hartmut ee6eb34b4c feat: GPU rendering + material matching + perf improvements
- 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>
2026-03-08 19:05:03 +01:00

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,
}}
/>
)
}