Draw handles in screen space (constant size when zooming)#537
Merged
Conversation
Handles were drawn inside the zoomed view transform, so every length they emitted (grab dots, gizmo reach, stroke width) was multiplied by the view scale: zooming in made the handles grow instead of just repositioning them. Refactor handles to operate in screen space. They now receive the full view transform via setViewTransform and project the document coordinates they operate on through toScreen(). Decorations are drawn at a constant pixel size; only genuine document measurements (the four-point bounding box, the circle radius, the snap grid spacing) are scaled by the view. Hit-testing projects both the handle and the cursor to screen space and compares with a constant pixel tolerance; drag value math stays in document space and is unchanged. The Viewer draws the handle layer outside the view transform and sizes the handle canvas to the viewport (Canvas.draw clips to its bounds, which was clipping handles near the document edges at high zoom). Also widen the translate handle's hit regions: the whole axis line is now draggable (with padding), not just the arrow tip. Adds unit tests for the screen-space projection/hit-testing, the zoom- independent scale-handle drag response, and the translate axis-line regions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Problem
Handles were drawn inside the zoomed view transform, so every length they emitted — grab dots, gizmo reach (arrows/circle/box), and stroke width — got multiplied by the view scale. Zooming in made the handles grow instead of just repositioning them; at high zoom the grab dots became huge and strokes thickened.
Approach
Refactor handles to operate in screen space, the way every vector editor draws its overlay layer:
setViewTransform(viewX, viewY, viewScale)and project the document coordinates they operate on throughtoScreen().The result:
viewScalecollapses from "defensively divide every size everywhere" to a single seam (toScreen) plus the handful of real measurements that multiply by it. No/ viewScaleanywhere.Viewer
Canvas.drawclips to its bounds, and the old 1000×1000 canvas was clipping handles near the document edges at high zoom.setViewPositionpath that doesn't fireonViewTransformChanged).Bonus: translate handle affordance
The whole axis line is now draggable (with a padded grab band), not just the arrow tip — drag a line to constrain movement to that axis.
Tests
AbstractHandleTest— screen-space projection and grab-area constancy across 0.1×–10× and under pan.ScaleHandleTest— drag response is identical for equal on-screen drags at any zoom.TranslateHandleTest— axis-line grab regions constrain to the right axis; center moves both; off-axis starts no drag.Full suite: 322 tests, 0 failures. Verified all built-in nodes' handles by hand, plus a Jython smoke test for the
ReflectHandleprojection path.🤖 Generated with Claude Code