Skip to content

fix(viewer): keep zoom viewer mounted + CSS-toggle (restore context reuse/state, follow-up to #509)#510

Merged
Zheaoli merged 1 commit into
mainfrom
fix/zoom-viewer-reuse-context
May 31, 2026
Merged

fix(viewer): keep zoom viewer mounted + CSS-toggle (restore context reuse/state, follow-up to #509)#510
Zheaoli merged 1 commit into
mainfrom
fix/zoom-viewer-reuse-context

Conversation

@Zheaoli
Copy link
Copy Markdown
Collaborator

@Zheaoli Zheaoli commented May 31, 2026

Context

Follow-up to #509 (already merged). @Zheaoli pointed out that #509's approach (conditionally mount/unmount the full-screen viewer) fixed the 2nd-open crash but regressed UX: every open rebuilt the WebGL engine (re-creation cost) and lost the zoom/pan state. The original design deliberately kept one engine + context alive across open/close within the modal — preserving zoom/pan and avoiding rebuilds.

Root cause (recap)

The viewer was toggled with <Activity>, which runs the child's effect cleanup on hide → fired FU-13's destroy()/loseContext() on every close → the 2nd open landed on a permanently-lost context → crash. The destroy was bound to the wrong lifecycle (zoom-close instead of detail-unmount).

Fix C

Keep the viewer mounted once opened and toggle it with CSS visibility instead of <Activity> / conditional unmount:

  • Staying mounted → the effect cleanup never runs on close → one engine + WebGL context stays alive across open/close → context reused, zoom/pan state preserved, zero rebuild cost (restores original UX).
  • destroy()/loseContext() now runs only when ProgressiveImage unmounts (leaving the detail page), via the WebGLImageViewer's own unmount cleanup → FU-13 leak fix fully preserved (one context created+released per detail visit, created==lost).
  • visibility:hidden (not display:none) keeps the canvas sized, so re-showing doesn't drive the engine through a 0×0 resize that would reset zoom (the engine's getFitToScreenScale clamps to 1 on a 0×0 canvas; visibility avoids that path). pointer-events:none while hidden so the invisible overlay never blocks the page.
  • hasOpenedFullScreen latches on first open → engine still built lazily (not before the user zooms).

The WebGL engine and FU-13's destroy()/loseContext() are unchanged.

Verification (post-deploy, devtools)

Modal-internal open→close→open→close ≥3 rounds:

  • same context throughout (isContextLost() stays false, no new context created)
  • zoom/pan state preserved across open/close
  • zero throw, image renders every time

Leaving the detail page → destroy() runs (created==lost, no leak). Will run the harness after deploy; to be independently confirmed via the isContextLost() + scale-preservation hook.

Supersedes #509's approach.

🤖 Generated with Claude Code

…t/state

Follow-up to #509. #509 fixed the 2nd-open crash by conditionally mounting/
unmounting the full-screen viewer, but that regressed UX: every open rebuilt the
WebGL engine (re-creation cost) and lost the zoom/pan state. The original design
deliberately kept one engine + context alive across open/close within the modal,
preserving zoom/pan and avoiding rebuilds.

Root cause of the crash was that the viewer was toggled with `<Activity>`, which
runs the child's effect cleanup on hide — firing FU-13's destroy()/loseContext()
on every close, so the 2nd open landed on a permanently-lost context (crash).
The destroy was bound to the wrong lifecycle event (zoom close instead of detail
unmount).

Fix C: keep the viewer MOUNTED once opened and toggle it with CSS
`visibility` instead of `<Activity>` / conditional unmount:
- Staying mounted means the effect cleanup never runs on close → one engine +
  WebGL context stays alive across open/close → context reused, zoom/pan state
  preserved, zero rebuild cost (restores the original UX).
- destroy()/loseContext() now only runs when ProgressiveImage unmounts (leaving
  the detail page) — via the WebGLImageViewer's own unmount cleanup — so the
  FU-13 leak fix is fully preserved (one context created+released per detail).
- `visibility:hidden` (not display:none) keeps the canvas sized, so re-showing
  doesn't drive the engine through a 0×0 resize that would reset the zoom;
  pointer-events are disabled while hidden so the invisible overlay never blocks
  the page underneath.
- `hasOpenedFullScreen` latches on first open so the engine is still built lazily
  (not before the user zooms).

The WebGL engine and FU-13's destroy()/loseContext() are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
picimpact Ready Ready Preview, Comment May 31, 2026 12:04pm

@Zheaoli Zheaoli merged commit 5b4e4e7 into main May 31, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant