Skip to content

Draw handles in screen space (constant size when zooming)#537

Merged
fdb merged 1 commit into
masterfrom
screen-sized-drag-handles
Jun 15, 2026
Merged

Draw handles in screen space (constant size when zooming)#537
fdb merged 1 commit into
masterfrom
screen-sized-drag-handles

Conversation

@fdb

@fdb fdb commented Jun 15, 2026

Copy link
Copy Markdown
Member

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.

100% zoomed in (before)
small, crisp handles fat dots, thick strokes, oversized gizmos

Approach

Refactor handles to operate in screen space, the way every vector editor draws its overlay layer:

  • Handles receive the full view transform via setViewTransform(viewX, viewY, viewScale) and project the document coordinates they operate on through toScreen().
  • Decorations (dots, knobs, arrows, strokes) are drawn at a constant pixel size — no scale math.
  • Only genuine document measurements scale with the view: the four-point bounding box, the circle radius, the snap grid spacing.
  • Hit-testing projects both the handle position and the cursor to screen space and compares with a constant pixel tolerance.
  • Drag value math stays in document space and is unchanged — a point dragged to document coord X is still X.

The result: viewScale collapses from "defensively divide every size everywhere" to a single seam (toScreen) plus the handful of real measurements that multiply by it. No / viewScale anywhere.

Viewer

  • Draws the handle layer outside the view transform (screen space).
  • Sizes the handle canvas to the viewport — Canvas.draw clips to its bounds, and the old 1000×1000 canvas was clipping handles near the document edges at high zoom.
  • Refreshes the handle transform at paint time (covers the first-paint setViewPosition path that doesn't fire onViewTransformChanged).

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 ReflectHandle projection path.

🤖 Generated with Claude Code

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>
@fdb fdb merged commit f8deadb into master Jun 15, 2026
2 checks passed
@fdb fdb deleted the screen-sized-drag-handles branch June 15, 2026 14:03
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