Skip to content

feat(localize): zoom the object editor canvas at the pointer - #378

Merged
Chouffe merged 5 commits into
mainfrom
worktree-localize-pointer-zoom
Aug 12, 2026
Merged

feat(localize): zoom the object editor canvas at the pointer#378
Chouffe merged 5 commits into
mainfrom
worktree-localize-pointer-zoom

Conversation

@Chouffe

@Chouffe Chouffe commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

On the object editor's canvas the wheel zoomed about the image's centre,
wherever the pointer was — so magnifying a plume in a corner meant zoom, pan,
zoom, pan. Worse, the same handler reset transform-origin to 50/50 on every
notch, which threw the Z object framing away: press Z, wheel once, and the
view snapped back to the middle of the scene.

The wheel now zooms at the pointer, and refines the object framing instead of
discarding it.

Why it needed more than a new wheel handler

The stage's transform had two positional knobs — transformOrigin and
panOffset — and anchoring needs a single one to solve for. Z framed the
object by moving the origin; the wheel could only fight that by resetting it.

So the view collapses to one knob: the origin is pinned at the image centre and
all framing lives in the pan, expressed as a fraction of the image's rendered
size rather than layout pixels. That keeps the framing math free of layout — the
clamp and the Z conversion are pure numbers — and only the pointer anchor needs
to know how big the image actually is.

With O the image centre, z the scale and t the pan fraction:

s(p) = O + z * ((p - O) + t * W)        the translate applies inside the scale

t' = (z*t + (z - z')*(c - 0.5)) / z'    anchor the cursor's point c across a zoom
|t| <= (z - 1) / (2z)                   the pan that still covers the frame
t  = (1 - z)(c - 0.5) / z               a computeCellCrop framing, as a pan

That last one is the algebraic equivalent of the old origin-based framing, so Z
frames the object exactly where it did before — asserted in
stageViewUtils.test.ts by projecting points through both transforms.

What changed

  • New src/utils/annotation/stageViewUtils.ts — anchor, clamps, crop→pan,
    wheel-delta normalization, and the one definition of the transform CSS every
    layer shares.
  • useBoxDrawingStage — one { scale, pan } state instead of three pieces, read
    through a ref so the once-attached wheel listener and the plain-function
    coordinate converters can never hold a stale view.
  • screenToImageCoordinates — inverted about the centre, pan read as a fraction.
    TransformConfig is gone; StageView replaces it.
  • DetectionAnnotationCanvas, DrawingOverlay, AddObjectOverlay — the
    transformOrigin prop is gone and translate renders in percent.

Wheel steps are multiplicative now (~15% a notch, so a step feels the same at 1x
and 6x, where +0.2 was a 20% jump at 1x and a 5% nudge at 4x), scaled by the
delta's magnitude so a trackpad's stream of small deltas zooms smoothly and
ctrl+wheel pinch works. The ceiling goes 4 → 8, matching the grid's MAX_SCALE,
so a small distant plume can fill the frame. Z's own ceiling stays 3.

Wheeling inside crop view leaves the Z toggle pressed: it's a mode, not a
snapshot, and stepping to the next frame re-frames the object as before.

Two consequences worth flagging

  • The pan drag is now 1:1 with the cursor. It used to move the image scale×
    faster than the hand, because the pan was in layout pixels but applies inside
    the scale. Converting the units forced a choice; 1:1 is what the grabbing
    cursor promises. Pinned by a test.
  • Panning in Z crop view could drag blank space into view. The old
    constrainPan bound was exact only for a centred origin and too permissive
    for an off-centre one. The clamp is now exact everywhere.

Testing

npm run quality clean; 1465 tests pass (baseline 1445).

New stageViewUtils.test.ts (16 tests) carries the math: anchor invariance
across zoom in, zoom out, and an 8-step burst; the clamp overriding the anchor
rather than showing a blank edge; cropToPan equivalence; delta-mode
normalization. In the editor, a wheel test proves the anchor end-to-end and one
proves the framing survives a notch. The coordinate test that read "the math is
complex, let's just verify it produces reasonable coordinates"
is now a real
screen → image → screen round trip.

Verified in the browser on /localize/971/object/971/16860.

Spec: docs/specs/2026-08-12-localize-pointer-zoom-design.md

@Chouffe
Chouffe merged commit ce2d5fe into main Aug 12, 2026
3 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