diff --git a/src/hooks/useDwgViewer.ts b/src/hooks/useDwgViewer.ts
index cad2d86..d4bf336 100644
--- a/src/hooks/useDwgViewer.ts
+++ b/src/hooks/useDwgViewer.ts
@@ -78,8 +78,14 @@ export function useDwgViewer() {
// mount Viewer2D once
useEffect(() => {
- if (!containerRef.current) return;
- const viewer = new Viewer2D(containerRef.current);
+ const container = containerRef.current;
+ if (!container) return;
+ // Viewer2D has no dispose/teardown API (permanent window/canvas
+ // listeners, its own rAF loop) — defensively clear any leftover canvas
+ // from a prior construction (e.g. a dev Fast Refresh remount) so it
+ // never stacks two live WebGL contexts in the same container.
+ container.replaceChildren();
+ const viewer = new Viewer2D(container);
viewerRef.current = viewer;
viewer.setTheme(true);
viewer.onSelect((entity) => setSelectedEntity(entity));
diff --git a/src/main.tsx b/src/main.tsx
index dfacde0..2bf1b15 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,10 @@
-import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-);
+// No : Viewer2D (ported unchanged from the vanilla app) has no
+// dispose/teardown API and attaches permanent window/canvas listeners in its
+// constructor. StrictMode's dev-only mount→unmount→remount double-invoke
+// would construct it twice into the same container with no way to tear the
+// first instance down, leaving two overlapping WebGL canvases.
+createRoot(document.getElementById('root')!).render();