Add error boundary around object viewers

Wrap the resolved viewer in an error boundary so a render failure (e.g.
WebGL context loss or a model/texture load error) is contained to the
inspector pane instead of crashing the page. The fallback shows the
error message and stack trace.
This commit is contained in:
2026-08-08 12:58:28 +08:00
parent 001d5eeb54
commit 7d1f05902d
2 changed files with 66 additions and 9 deletions
+54
View File
@@ -0,0 +1,54 @@
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
/** Optional fallback rendered in place of `children` when an error is caught. */
fallback?: (error: Error) => ReactNode;
}
interface State {
error: Error | null;
}
/**
* Catches render errors in its subtree and renders a fallback instead of
* letting them crash the whole page. Used to isolate failures in object
* viewers (e.g. WebGL context loss, model/texture load errors).
*/
export default class ErrorBoundary extends Component<Props, State> {
override state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
override componentDidCatch(error: Error, info: ErrorInfo) {
// Surface the error for debugging without breaking the UI.
console.error('Viewer error:', error, info.componentStack);
}
override render() {
if (this.state.error) {
return this.props.fallback
? this.props.fallback(this.state.error)
: <DefaultFallback error={this.state.error} />;
}
return this.props.children;
}
}
function DefaultFallback({ error }: { error: Error }) {
return (
<div className="flex h-80 flex-col items-center justify-center gap-2 overflow-auto rounded-lg border border-zinc-800 bg-zinc-900 p-4 text-center">
<p className="text-sm font-medium text-zinc-200">Couldn't render this object</p>
<p className="max-w-sm wrap-break-word font-mono text-xs text-zinc-400">
{error.message}
</p>
{error.stack && (
<pre className="mt-2 max-h-40 w-full overflow-auto whitespace-pre-wrap text-left font-mono text-[10px] leading-relaxed text-zinc-600">
{error.stack}
</pre>
)}
</div>
);
}
+12 -9
View File
@@ -6,6 +6,7 @@ import { useModStore } from '../stores/modStore';
import { useSearchStore } from '../stores/searchStore';
import { modFileUrl } from '../api';
import ObjectTree from '../components/ObjectTree';
import ErrorBoundary from '../components/ErrorBoundary';
import { resolveViewer } from '../components/viewers';
import { iconsForObject } from '../components/objectIcons';
// Register the 3D viewers (side effect) so `resolveViewer` can find them.
@@ -78,15 +79,17 @@ export default function ModPage() {
</p>
</header>
{Viewer && (
<Suspense
fallback={
<div className="flex h-80 items-center justify-center text-sm text-zinc-500">
Loading viewer
</div>
}
>
<Viewer object={selected.object} />
</Suspense>
<ErrorBoundary>
<Suspense
fallback={
<div className="flex h-80 items-center justify-center text-sm text-zinc-500">
Loading viewer
</div>
}
>
<Viewer object={selected.object} />
</Suspense>
</ErrorBoundary>
)}
</>
) : (