feat(localize): zoom the object editor canvas at the pointer - #378
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-originto 50/50 on everynotch, which threw the
Zobject framing away: pressZ, wheel once, and theview 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 —
transformOriginandpanOffset— and anchoring needs a single one to solve for.Zframed theobject 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
Zconversion are pure numbers — and only the pointer anchor needsto know how big the image actually is.
With
Othe image centre,zthe scale andtthe pan fraction:That last one is the algebraic equivalent of the old origin-based framing, so
Zframes the object exactly where it did before — asserted in
stageViewUtils.test.tsby projecting points through both transforms.What changed
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, readthrough 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.TransformConfigis gone;StageViewreplaces it.DetectionAnnotationCanvas,DrawingOverlay,AddObjectOverlay— thetransformOriginprop is gone andtranslaterenders in percent.Wheel steps are multiplicative now (~15% a notch, so a step feels the same at 1x
and 6x, where
+0.2was a 20% jump at 1x and a 5% nudge at 4x), scaled by thedelta's magnitude so a trackpad's stream of small deltas zooms smoothly and
ctrl+wheelpinch works. The ceiling goes 4 → 8, matching the grid'sMAX_SCALE,so a small distant plume can fill the frame.
Z's own ceiling stays 3.Wheeling inside crop view leaves the
Ztoggle pressed: it's a mode, not asnapshot, and stepping to the next frame re-frames the object as before.
Two consequences worth flagging
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
grabbingcursor promises. Pinned by a test.
Zcrop view could drag blank space into view. The oldconstrainPanbound was exact only for a centred origin and too permissivefor an off-centre one. The clamp is now exact everywhere.
Testing
npm run qualityclean; 1465 tests pass (baseline 1445).New
stageViewUtils.test.ts(16 tests) carries the math: anchor invarianceacross zoom in, zoom out, and an 8-step burst; the clamp overriding the anchor
rather than showing a blank edge;
cropToPanequivalence; delta-modenormalization. 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