Sitelet https://github.com/shift-editor/shift/pull/503
Skip to content

refactor(editor): size drawing through the tracked canvas transform - #503

Merged
kostyafarber merged 7 commits into
mainfrom
refactor/canvas-transform-cleanup
Oct 3, 2026
Merged

kostyafarber merged 7 commits into
mainfrom
refactor/canvas-transform-cleanup

Conversation

@kostyafarber

@kostyafarber kostyafarber commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #502.

Summary

  • Canvas tracks the transform from its current drawing units to logical screen pixels, through withTransform, save/restore, translate, scale and rotate.
  • Nothing converts pixels to drawing units any more (pxToUpm is gone). Geometry is drawn in the current units; widths, dashes and radii are screen pixels applied in screen space.
    • Canvas.stroke() strokes the current path after resetting to pixels.
    • strokePath maps the Path2D to the screen with addPath(path, transform).
    • Circles, CPU handles, snap crosses and the read-only lock icon project their anchors with toScreen and draw in pixels.
    • Strokes and chrome therefore stay the right size inside any node transform, including scaled text runs.
  • Pixel-sized drawing is enforced by type. withScreenSpace((screen, project) => …) hands its callback a branded ScreenCanvas and a projection from the outer units, and the screen-only helpers (handle and anchor drawing, the bounding-box handles) require a ScreenCanvas and ScreenPoints. Calling them from node units no longer compiles. That mistake had made anchor diamonds scale with zoom.
  • Handles read the units-to-screen matrix and the culling bounds from the canvas (Canvas.visibleBounds). This removes the Editor.screenTransform and Editor.visibleLocalBounds plumbing, plus the unused Editor.screenToSceneDistance.
  • The selection bounding box draws its rect and corner handles directly in screen space (Canvas.withScreenSpace), so it no longer offsets a scene rect by pixel distances converted at the current zoom.
  • The handle marker shader derives each marker's screen rotation from the matrix, replacing the hard-coded -rotation that assumed a Y flip.
  • Handle cull padding is cached per theme instead of rebuilt every frame.

Walking the node tree for drawing is left to the text-run work: sceneTransform already places child nodes correctly, and per-pass child dispatch belongs with the run's design.

Issue

Refs #129
Refs #458

Testing

  • pnpm check (lint, typecheck, browser check, strict dead code, unit tests) passes, plus the pre-commit suite on each commit.
  • pnpm format:check passes.
  • Not yet checked in the app. Needs a visual pass on handle markers (direction, first and last shapes, GPU and CPU fallback), segment hover/selection strokes, metric guides and the read-only lock, snap crosses, the selection box and its component corner handles, and handle culling at the canvas edges.
  • E2E not run locally. Relying on PR CI, especially the canvas snapshot specs in glyph-rendering and tools.

🤖 Generated with Claude Code

@kostyafarber
kostyafarber force-pushed the refactor/canvas-transform-cleanup branch from 6ce5b85 to c0bb6cb Compare October 3, 2026 14:49
Pixel-sized strokes and markers divided by camera zoom, which is only
right while every node draws at unit scale. Canvas now tracks the
transform from its current drawing units to screen pixels, so
pxToUnits, visible-bounds culling and the marker matrix follow
whatever node transform is active, and the selection bounding box
draws its chrome directly in screen space.
Widths, dashes and radii are screen pixels, so Canvas now applies them in
screen space: strokes reset to pixels after the path is built, Path2D
strokes are mapped to the screen, and handles, snap crosses and the lock
icon project their anchors and draw in pixels. Nothing converts pixels to
drawing units any more, so pxToUnits is gone.
… copies

Mapping each outline Path2D into screen space copied the whole path on
every stroke, and the CPU handle fallback built an array of projected
handles each frame. Retained paths now stroke in place with the pixel
width scaled into the current units, and handles project inside the
draw loop.
Anchors called the screen-space handle drawing while the canvas was still
in glyph units, so their size scaled with zoom. Project anchors and draw
them in screen space like the other handles.
Screen-space helpers took a plain Canvas and Point2D, so calling them from
node units compiled and drew at the wrong size, as anchors did.
withScreenSpace now hands its callback a branded ScreenCanvas and a
projection from the outer units, and those helpers require both.
@kostyafarber
kostyafarber force-pushed the refactor/canvas-transform-cleanup branch from c0bb6cb to 1acee6a Compare October 3, 2026 15:42
@kostyafarber
kostyafarber changed the base branch from refactor/camera-view-matrix to main October 3, 2026 15:42
@kostyafarber kostyafarber reopened this Oct 3, 2026
@kostyafarber kostyafarber added the release: nightly Run a Nightly build after this pull request merges label Oct 3, 2026
@kostyafarber
kostyafarber added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit f561c6e Oct 3, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release: nightly Run a Nightly build after this pull request merges

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant