Show 3D hall loading overlay during canvas and texture boot.
Display Loading gallery and Loading paintings in the canvas area while the WebGL scene initializes and wall textures resolve, and document the behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
21e3e41e48
commit
aa6635803d
@@ -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 `<Canvas>` 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.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 && (
|
||||
<GalleryLoadingMarker overlay message="Loading paintings…" />
|
||||
{active && (gallerySceneLoading || glLost) && (
|
||||
<GalleryLoadingMarker
|
||||
overlay
|
||||
message={glLost ? 'Restoring gallery…' : galleryLoadingMessage}
|
||||
/>
|
||||
)}
|
||||
{syncStatus && texturesPending === 0 && (
|
||||
{syncStatus && !gallerySceneLoading && !glLost && (
|
||||
<div className="gallery-loading-overlay gallery-sync-badge">
|
||||
<p>{syncStatus}</p>
|
||||
</div>
|
||||
@@ -2080,9 +2089,6 @@ export default function VirtualGallery(props: Props) {
|
||||
{!showExitNav && (
|
||||
<div className="gallery-exit-hint">{exitHint}</div>
|
||||
)}
|
||||
{glLost && (
|
||||
<GalleryLoadingMarker overlay message="Restoring gallery…" />
|
||||
)}
|
||||
<Canvas
|
||||
key={`${hallKey}-${glEpoch}`}
|
||||
shadows
|
||||
|
||||
Reference in New Issue
Block a user