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>
This commit is contained in:
2026-03-08 19:05:03 +01:00
parent 934728da77
commit ee6eb34b4c
34 changed files with 1274 additions and 511 deletions
+63 -8
View File
@@ -24,13 +24,22 @@ import api from '../../api/client'
export interface ThreeDViewerProps {
cadFileId: string
onClose: () => void
/** URL for the geometry-only GLB (from STL export) */
/** URL for the geometry-only GLB (from OCC export) */
geometryGltfUrl?: string
/** URL for the production-quality GLB (from asset library render) */
/** URL for the production-quality GLB (Blender + PBR materials) */
productionGltfUrl?: string
/** Download URLs for GLB and .blend assets */
/** Whether a geometry GLB exists (for hint display) */
hasGeometryGlb?: boolean
/** Whether a production GLB exists (for hint display) */
hasProductionGlb?: boolean
/** Called when the user clicks "Generate Geometry GLB" from the hint banner */
onGenerateGeometry?: () => void
/** Whether a geometry GLB generation is in progress */
isGeneratingGeometry?: boolean
/** Download URLs for assets */
downloadUrls?: {
glb?: string
production?: string
blend?: string
}
}
@@ -217,9 +226,15 @@ export default function ThreeDViewer({
onClose,
geometryGltfUrl,
productionGltfUrl,
hasGeometryGlb,
hasProductionGlb,
onGenerateGeometry,
isGeneratingGeometry,
downloadUrls,
}: ThreeDViewerProps) {
const [mode, setMode] = useState<ViewMode>('geometry')
// Default to production mode if only production GLB is available
const initialMode: ViewMode = productionGltfUrl && !geometryGltfUrl ? 'production' : 'geometry'
const [mode, setMode] = useState<ViewMode>(initialMode)
const [wireframe, setWireframe] = useState(false)
const [envPreset, setEnvPreset] = useState<EnvPreset>('city')
const [capturing, setCapturing] = useState(false)
@@ -232,11 +247,11 @@ export default function ThreeDViewer({
staleTime: 60_000,
})
// Resolve the active model URL based on mode
// Resolve the active model URL: prefer selected mode, fall back to whichever URL exists
const activeUrl =
mode === 'production' && productionGltfUrl
? productionGltfUrl
: geometryGltfUrl
: geometryGltfUrl ?? productionGltfUrl
const handleModelReady = useCallback(() => setModelReady(true), [])
const handleError = useCallback((msg: string) => setLoadError(msg), [])
@@ -312,11 +327,20 @@ export default function ThreeDViewer({
{/* Download buttons */}
{downloadUrls?.glb && (
<button
onClick={() => handleDownload(downloadUrls.glb!, `${cadFileId}.glb`)}
onClick={() => handleDownload(downloadUrls.glb!, `${cadFileId}_geometry.glb`)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-gray-700 hover:bg-gray-600 text-white text-xs font-medium transition-colors"
>
<Download size={12} />
GLB
Geometry GLB
</button>
)}
{downloadUrls?.production && (
<button
onClick={() => handleDownload(downloadUrls.production!, `${cadFileId}_production.glb`)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-gray-700 hover:bg-gray-600 text-white text-xs font-medium transition-colors"
>
<Download size={12} />
Production GLB
</button>
)}
{downloadUrls?.blend && (
@@ -350,6 +374,37 @@ export default function ThreeDViewer({
</div>
</div>
{/* Hint banners */}
{!hasProductionGlb && (
<div className="bg-amber-900/60 border-b border-amber-700/50 px-4 py-2 flex items-center gap-2 text-amber-200 text-xs shrink-0">
<Cpu size={13} className="shrink-0" />
<span>
<strong>No Production GLB yet.</strong> Go to the product page and click "Generate Production GLB" to create a high-quality version with PBR materials and proper mesh smoothing.
</span>
</div>
)}
{!hasGeometryGlb && hasProductionGlb && onGenerateGeometry && (
<div className="bg-blue-900/50 border-b border-blue-700/50 px-4 py-2 flex items-center gap-3 text-blue-200 text-xs shrink-0">
<Box size={13} className="shrink-0" />
<span>
<strong>Showing Production GLB.</strong> Generate a Geometry GLB to enable the mode toggle and compare geometry vs. production quality.
</span>
{isGeneratingGeometry ? (
<span className="flex items-center gap-1 text-blue-300 ml-auto shrink-0">
<Loader2 size={11} className="animate-spin" />
Generating
</span>
) : (
<button
onClick={onGenerateGeometry}
className="ml-auto shrink-0 px-3 py-1 rounded bg-blue-700 hover:bg-blue-600 text-white text-xs font-medium transition-colors"
>
Generate Geometry GLB
</button>
)}
</div>
)}
{/* Viewport */}
<div className="relative flex-1">
{loadError && (