diff --git a/Documentation/basics.md b/Documentation/basics.md index 2dd21e4..7921151 100644 --- a/Documentation/basics.md +++ b/Documentation/basics.md @@ -169,6 +169,9 @@ Pan, zoom, and era/event click-to-zoom only update **local** `viewStart` / `view | Overlay **“Loading art history…”** | Until the first catalog fetch (`bounds` + `timeline` + `artists`) completes | | Bottom banner **“Loading portraits…”** | While artist portrait thumbnails are still downloading on the movement flow (timeline stays interactive) | | Overlay **“Opening artist/movement gallery…”** | Between clicking a portrait/movement and the 3D hall data being ready | +| Overlay **“Loading gallery…”** | While the 3D canvas initializes after the hall opens (center area; header and controls stay visible) | +| Overlay **“Loading paintings…”** | While wall textures are still downloading in the 3D hall | +| Overlay **“Restoring gallery…”** | Briefly after WebGL context loss while the canvas remounts | View updates are **batched to one commit per animation frame** via `createViewChangeScheduler()` in `timelineView.ts` (`HomePage.tsx` → `handleViewChange`), so rapid scroll-wheel events do not flood React with separate renders. @@ -311,7 +314,7 @@ Movement galleries do **not** use the predecessor/successor influence picker — ### Shared 3D behaviour -**3D images** use locally cached files only (`galleryImageUrl` in `client/src/api/client.ts`). Remote fetches are too slow for realtime WebGL textures; the client calls `POST /api/artists/:id/preload-images` automatically when entering an **artist** hall (public route — links disk files only). Movement galleries load painting lists from the API without a separate preload step. While a texture is loading, the frame shows the canvas cover instead of a white placeholder, and a **“Loading paintings…”** overlay is shown until every wall texture has resolved (tracked through `GalleryTextureLoadContext`). The 3D hall stays mounted while painting detail or bio overlays are open; returning remounts the canvas when the hall becomes active again. +**3D images** use locally cached files only (`galleryImageUrl` in `client/src/api/client.ts`). Remote fetches are too slow for realtime WebGL textures; the client calls `POST /api/artists/:id/preload-images` automatically when entering an **artist** hall (public route — links disk files only). Movement galleries load painting lists from the API without a separate preload step. While a texture is loading, the frame shows the canvas cover instead of a white placeholder. The center of the hall shows **“Loading gallery…”** until the WebGL canvas is ready, then **“Loading paintings…”** until every wall texture has resolved (tracked through `GalleryTextureLoadContext`). The 3D hall stays mounted while painting detail or bio overlays are open; returning remounts the canvas when the hall becomes active again. **WebGL context-loss recovery:** on some GPUs/drivers (notably certain Chrome setups) the browser can drop the WebGL context right after entering a hall, which would otherwise leave a permanent dark window. `VirtualGallery.tsx` listens for `webglcontextlost` / `webglcontextrestored`, calls `preventDefault()` so the browser can restore the context, and remounts the `` with a fresh context (a **“Restoring gallery…”** overlay shows briefly). The network-loaded HDR `Environment` map is wrapped in an error boundary so, if it fails to load, the hall still renders without reflections instead of unmounting the whole scene. diff --git a/Documentation/setup.md b/Documentation/setup.md index a9dbb27..a79a3dc 100644 --- a/Documentation/setup.md +++ b/Documentation/setup.md @@ -271,4 +271,5 @@ After clone: copy `.env.example` → `.env`, install dependencies, run [one-time | Debug / Checkup returns **401** | Not signed in as curator | **Curator login** (top-right); session cookie `gallery.sid` must be sent (`credentials: include`) | | Debug works in UI but API rejects | Stale server without auth middleware | Restart `npm run dev:web` or `npm run dev:server` after pulling auth changes | | **Empty screen** entering 3D hall (header missing) | Stale client before gallery-session fix | Hard-refresh; pull latest client — hall renders from `view` state, not only `gallerySession` | +| **Dark center** entering 3D hall (header visible, no spinner) | Stale client before gallery loading overlay fix | Hard-refresh; latest client shows **Loading gallery…** / **Loading paintings…** in the canvas area until ready | | 3D hall black after returning from painting detail | WebGL context lost while hall was hidden | Hard-refresh; latest client remounts canvas when hall becomes active again | diff --git a/client/src/components/VirtualGallery.tsx b/client/src/components/VirtualGallery.tsx index 967b1e6..3762bb3 100644 --- a/client/src/components/VirtualGallery.tsx +++ b/client/src/components/VirtualGallery.tsx @@ -1711,6 +1711,7 @@ export default function VirtualGallery(props: Props) { const [nearPassage, setNearPassage] = useState(false); const [isLooking, setIsLooking] = useState(false); const [texturesPending, setTexturesPending] = useState(0); + const [canvasReady, setCanvasReady] = useState(false); const [glEpoch, setGlEpoch] = useState(0); const [glLost, setGlLost] = useState(false); @@ -1722,15 +1723,12 @@ export default function VirtualGallery(props: Props) { [] ); - useEffect(() => { - setTexturesPending(0); - }, [hallKey, glEpoch]); - const handleCanvasCreated = useCallback((state: { gl: THREE.WebGLRenderer }) => { const canvas = state.gl.domElement; // A freshly created canvas has a healthy context, so clear any lingering // "restoring" state from a previous loss/remount. setGlLost(false); + setCanvasReady(true); const onLost = (event: Event) => { // Prevent the default so the browser can restore the context, and // force a clean remount to obtain a fresh WebGL context if it does not. @@ -1798,6 +1796,14 @@ export default function VirtualGallery(props: Props) { return buildHallLayout(paintings, periods); }, [isMovement, movementHalls, hallIndex, paintings, periods]); + const gallerySceneLoading = active && !glLost && (!canvasReady || texturesPending > 0); + + const galleryLoadingMessage = !canvasReady + ? 'Loading gallery…' + : texturesPending > 0 + ? 'Loading paintings…' + : 'Loading gallery…'; + const computedWindows = useMemo(() => { if (!isMovement || !interiorStyle || !('hallIndex' in layout)) return undefined; return computeSideWallWindows(layout as MovementHallLayout, interiorStyle); @@ -2069,10 +2075,13 @@ export default function VirtualGallery(props: Props) { onPointerLeave={endCanvasDrag} onPointerCancel={endCanvasDrag} > - {active && texturesPending > 0 && ( - + {active && (gallerySceneLoading || glLost) && ( + )} - {syncStatus && texturesPending === 0 && ( + {syncStatus && !gallerySceneLoading && !glLost && (

{syncStatus}

@@ -2080,9 +2089,6 @@ export default function VirtualGallery(props: Props) { {!showExitNav && (
{exitHint}
)} - {glLost && ( - - )}