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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { useModStore } from '../stores/modStore';
|
|||||||
import { useSearchStore } from '../stores/searchStore';
|
import { useSearchStore } from '../stores/searchStore';
|
||||||
import { modFileUrl } from '../api';
|
import { modFileUrl } from '../api';
|
||||||
import ObjectTree from '../components/ObjectTree';
|
import ObjectTree from '../components/ObjectTree';
|
||||||
|
import ErrorBoundary from '../components/ErrorBoundary';
|
||||||
import { resolveViewer } from '../components/viewers';
|
import { resolveViewer } from '../components/viewers';
|
||||||
import { iconsForObject } from '../components/objectIcons';
|
import { iconsForObject } from '../components/objectIcons';
|
||||||
// Register the 3D viewers (side effect) so `resolveViewer` can find them.
|
// Register the 3D viewers (side effect) so `resolveViewer` can find them.
|
||||||
@@ -78,15 +79,17 @@ export default function ModPage() {
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
{Viewer && (
|
{Viewer && (
|
||||||
<Suspense
|
<ErrorBoundary>
|
||||||
fallback={
|
<Suspense
|
||||||
<div className="flex h-80 items-center justify-center text-sm text-zinc-500">
|
fallback={
|
||||||
Loading viewer…
|
<div className="flex h-80 items-center justify-center text-sm text-zinc-500">
|
||||||
</div>
|
Loading viewer…
|
||||||
}
|
</div>
|
||||||
>
|
}
|
||||||
<Viewer object={selected.object} />
|
>
|
||||||
</Suspense>
|
<Viewer object={selected.object} />
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user