diff --git a/src/areas/generate/components/Viewer3D.tsx b/src/areas/generate/components/Viewer3D.tsx index 5122c86..2054903 100644 --- a/src/areas/generate/components/Viewer3D.tsx +++ b/src/areas/generate/components/Viewer3D.tsx @@ -1,4 +1,5 @@ -import { Suspense, useEffect, useRef, useState } from 'react' +import { Component, Suspense, useEffect, useRef, useState } from 'react' +import type { ReactNode, ErrorInfo } from 'react' import { Canvas, useThree } from '@react-three/fiber' import { OrbitControls, useGLTF } from '@react-three/drei' import * as THREE from 'three' @@ -59,6 +60,55 @@ function CanvasCapture({ return null } +// --------------------------------------------------------------------------- +// ModelErrorBoundary — catches useGLTF load failures (e.g. 404) +// --------------------------------------------------------------------------- + +interface ErrorBoundaryProps { + children: ReactNode + fallback: ReactNode + resetKey?: string | null +} + +interface ErrorBoundaryState { + hasError: boolean +} + +class ModelErrorBoundary extends Component { + state: ErrorBoundaryState = { hasError: false } + + static getDerivedStateFromError(): ErrorBoundaryState { + return { hasError: true } + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + console.warn('[Viewer3D] Failed to load model:', error.message, info.componentStack) + } + + componentDidUpdate(prevProps: ErrorBoundaryProps): void { + if (prevProps.resetKey !== this.props.resetKey && this.state.hasError) { + this.setState({ hasError: false }) + } + } + + render(): ReactNode { + return this.state.hasError ? this.props.fallback : this.props.children + } +} + +function ModelLoadError(): JSX.Element { + return ( +
+ + + + + +

Model file not found

+
+ ) +} + // --------------------------------------------------------------------------- // MeshModel // --------------------------------------------------------------------------- @@ -230,75 +280,77 @@ export default function Viewer3D(): JSX.Element { } return ( -
- {!modelUrl && } - - - - - - - - {modelUrl && currentJob && ( - - - - - - + }> +
+ {!modelUrl && } + + + + + + + + {modelUrl && currentJob && ( + + + + + + + )} + + + + + {/* Left toolbar — visible only when a model is loaded */} + {modelUrl && ( + setAutoRotate((v) => !v)} + onScreenshot={handleScreenshot} + /> )} - - - - {/* Left toolbar — visible only when a model is loaded */} - {modelUrl && ( - setAutoRotate((v) => !v)} - onScreenshot={handleScreenshot} - /> - )} - - {/* Bottom-left stats overlay */} - {meshStats && ( -
-

- {meshStats.triangles.toLocaleString()} tri • {meshStats.vertices.toLocaleString()} verts -

-
- )} - - {/* Bottom-right hint */} - {modelUrl && ( -
-

Drag to rotate • Scroll to zoom

-
- )} -
+ {/* Bottom-left stats overlay */} + {meshStats && ( +
+

+ {meshStats.triangles.toLocaleString()} tri • {meshStats.vertices.toLocaleString()} verts +

+
+ )} + + {/* Bottom-right hint */} + {modelUrl && ( +
+

Drag to rotate • Scroll to zoom

+
+ )} +
+ ) } diff --git a/src/areas/generate/components/WorkspacePanel.tsx b/src/areas/generate/components/WorkspacePanel.tsx index 0e8d5c6..dc7ebb4 100644 --- a/src/areas/generate/components/WorkspacePanel.tsx +++ b/src/areas/generate/components/WorkspacePanel.tsx @@ -98,7 +98,10 @@ export default function WorkspacePanel(): JSX.Element { const jobs = activeCollection?.jobs ?? [] const handleDeleteConfirm = () => { - if (pendingDeleteId) removeFromWorkspace(pendingDeleteId) + if (pendingDeleteId) { + if (currentJob?.id === pendingDeleteId) setCurrentJob(null) + removeFromWorkspace(pendingDeleteId) + } setPendingDeleteId(null) } diff --git a/src/areas/workspace/WorkspacePage.tsx b/src/areas/workspace/WorkspacePage.tsx index be9cd79..2a789b3 100644 --- a/src/areas/workspace/WorkspacePage.tsx +++ b/src/areas/workspace/WorkspacePage.tsx @@ -28,7 +28,10 @@ export default function WorkspacePage(): JSX.Element { } const handleDeleteJobConfirm = () => { - if (pendingDeleteId) removeFromWorkspace(pendingDeleteId) + if (pendingDeleteId) { + if (currentJob?.id === pendingDeleteId) setCurrentJob(null) + removeFromWorkspace(pendingDeleteId) + } setPendingDeleteId(null) }