From 7d1f05902d2c5e22a37672f086a21efaea4c42b0 Mon Sep 17 00:00:00 2001
From: hypercross
Date: Sat, 8 Aug 2026 12:58:28 +0800
Subject: [PATCH] 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.
---
apps/web/src/components/ErrorBoundary.tsx | 54 +++++++++++++++++++++++
apps/web/src/pages/ModPage.tsx | 21 +++++----
2 files changed, 66 insertions(+), 9 deletions(-)
create mode 100644 apps/web/src/components/ErrorBoundary.tsx
diff --git a/apps/web/src/components/ErrorBoundary.tsx b/apps/web/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..12a813a
--- /dev/null
+++ b/apps/web/src/components/ErrorBoundary.tsx
@@ -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 {
+ 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)
+ : ;
+ }
+ return this.props.children;
+ }
+}
+
+function DefaultFallback({ error }: { error: Error }) {
+ return (
+
+
Couldn't render this object
+
+ {error.message}
+
+ {error.stack && (
+
+ {error.stack}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/pages/ModPage.tsx b/apps/web/src/pages/ModPage.tsx
index 9433506..3f0caf6 100644
--- a/apps/web/src/pages/ModPage.tsx
+++ b/apps/web/src/pages/ModPage.tsx
@@ -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() {
{Viewer && (
-
- Loading viewer…
-
- }
- >
-
-
+
+
+ Loading viewer…
+
+ }
+ >
+
+
+
)}
>
) : (