Skip to content

Fix lock_view: deck's camera controller must live on the view, not the DeckGL prop - #4

Merged
sminot merged 2 commits into
mainfrom
fix-lock-view-controller
Sep 21, 2026
Merged

sminot merged 2 commits into
mainfrom
fix-lock-view-controller

Conversation

@sminot

@sminot sminot commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

lock_view did nothing when it was turned on while a canvas was already live — reported against a Cirro dashboard tile, where the inspector's "Lock view (no zoom or pan)" left the camera fully pannable and zoomable.

deck.gl only copies its controller prop onto a view when that prop is truthy, and it mutates the view instance in place (@deck.gl/core/dist/lib/deck.js):

_getViews() {
    const normalizedViews = Array.isArray(views) ? views : ...;
    if (normalizedViews.length && this.props.controller) {
        // Backward compatibility: support controller prop
        normalizedViews[0].props.controller = this.props.controller;
    }
    return normalizedViews;
}

Both canvases memoize their view on something unrelated to the camera — [invertX, invertY] in SpatialCanvas, [is_3d] in EmbeddingCanvas — so the instance outlives the render that wrote controller: true, and switching to controller={false} never took it back. The lock was the only setting this reached: every other branch ({ dragPan: false, … }) is a truthy options object, which the shim copies happily.

Changes

  • Both canvases declare the controller on the view they pass to <DeckGL views={…}> and drop the controller prop. The view memos moved below the state they now depend on. viewState is controlled in both, so re-minting the view leaves the camera where the user put it.
  • e2e/lock-view.spec.ts guards it through the embed protocol — apply-display toggles lock_view on a live canvas and display-changed reports camera moves.
  • Version bumped to 1.0.1 in all three manifests, per the release rule in CLAUDE.md. Note they read 0.1.10 while v1.0.0 sits on the same commit, so this also closes that drift.

Verification

Measured in a Cirro dashboard tile against a Xenium checkpoint — mean absolute pixel difference across the canvas for a wheel-zoom plus drag, against an idle no-gesture noise floor:

idle locked gesture unlocked gesture
released 1.0.0 0.011 53.947 (71.9% of pixels) 48.999 (75.4%)
this branch 0.011 0.405 (1.1%) 53.798 (71.8%)

On 1.0.0 a locked gesture moves the camera as much as an unlocked one. Here it drops to the noise floor; the 1.1% residual is the hover highlight, not the camera.

e2e/lock-view.spec.ts was checked in both directions. Against the previous canvases it fails with a display-changed carrying lock_view: true and a moved viewport; against these it passes. Two controls keep it from passing vacuously: the wheel must move an unlocked camera, and unlocking must give it back. A pre-locked share link is not sufficient — loaded locked, controller is false from the first render, there is no stale true to survive, and such a test passes against the broken code.

Also: viewer typecheck clean, npm run test -w spatial-data-studio-frontend 51/51, full npm run build clean. eslint is not installed in my checkout, so lint did not run.

Other:

  • DEVELOPMENT.md and serverless-share.spec.ts both claimed synthetic wheel events never reach deck's controller. They do — the zoom buttons are simply the easier lever there — and that claim is what would stop the next person writing this test.
  • Unrelated, noticed while running the suite: serverless-share.spec.ts (both tests) fails on a clean checkout because it points at docs-site/viewer-data/fluorescence-section.sdata.zarr.zip, which is neither on disk nor tracked by git. Not touched here.

This needs a v1.0.1 tag from main after merge for the built asset to reach anything; the Cirro-portal side is then a one-line bump of the @cirrobio/spatial-viewer tarball URL.

🤖 Generated with Claude Code

sminot and others added 2 commits September 21, 2026 14:47
deck.gl copies its `controller` prop onto the first view only when that prop is truthy
(`Deck._getViews`, "Backward compatibility: support controller prop"), and it writes it
onto the view instance in place. Both canvases memoize their view on something unrelated
to the camera — `[invertX, invertY]` and `[is_3d]` — so the instance outlives the render
that set `controller: true`, and switching to `controller={false}` never took it back.

Lock view was the only setting this reached: the other branches are truthy options
objects, which the shim happily copies. Measured against a Xenium checkpoint in a Cirro
dashboard tile, a wheel-zoom plus drag with the lock ON moved the canvas as much as with
it off (mean |pixel diff| 53.9 vs 49.0 against an idle noise floor of 0.011); with the
controller declared on the view it drops to 0.4, which is the hover highlight rather than
the camera.

`viewState` is controlled in both canvases, so re-minting the view when the controller
changes leaves the camera where the user put it.

Co-Authored-By: Claude Opus 5 <[email protected]>
The regression only appears when the lock is turned on while the canvas is already
live, which is what a host's inspector does — loaded pre-locked, `controller` is false
from the first render and there is no stale `true` to survive, so a share-link test
passes against the broken code. Embed mode is the one place this repo can drive that
sequence: `apply-display` toggles `lock_view` at runtime, and `display-changed` reports
camera moves, so the spec asserts the wheel stops producing them.

Verified both ways: against the previous canvases the spec fails with a
`display-changed` carrying `lock_view: true` and a moved viewport; against these it
passes. Two controls keep it from passing vacuously — the wheel must move an unlocked
camera, and unlocking must give it back.

Other:
- `DEVELOPMENT.md` and `serverless-share.spec.ts` claimed synthetic wheel events never
  reach deck's controller. They do; the zoom buttons are simply the easier lever there.

Co-Authored-By: Claude Opus 5 <[email protected]>
@sminot
sminot merged commit fc9b5a6 into main Sep 21, 2026
5 checks passed
@sminot
sminot deleted the fix-lock-view-controller branch September 21, 2026 22:25
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