Tools
Tools are the modules that interpret pointer and keyboard input on the canvas. Each one is a self-registered ToolModule (see Extension architecture) discovered from packages/. This page documents the five built-in tools and the event-routing contract every tool implements against.
Built-in tools
| Tool | Shortcut | Cursor | Summary |
|---|---|---|---|
| Select | V | default | Move, resize (8 handles), and edit path anchors/handles of the selected layer. |
| Pen | P | crosshair | Draw bezier paths anchor by anchor; close into a filled shape or finish as an open stroke. |
| Text | T | text | Click the canvas to drop a new text layer, then hand off to Select. |
| Pan | H | grab | Drag to pan the camera. Also reached transiently by holding Space in any other tool. |
| Zoom | Z | zoom-in | Click to zoom in anchored at the pointer; Alt-click to zoom out. |
Select (V)
tools/ is the reference tool — it exercises every part of the tool contract and is the file to copy when building a new one. On pointer down it, in order:
If a layer is already selected, tries to grab a path node/bezier handle on it (
tryGrabNode) — within 7px for anchors, 6px for handles.Otherwise tries to grab one of the 8 resize handles on the selection's bounding box (
tryGrabResizeHandle) — only offered forshape/imagelayers that aren't rotated (a rotated bbox isn't axis-resizable).Otherwise hit-tests the whole document top-down (
topmostHit) and selects whatever it hits, or deselects if it hits nothing.
Dragging streams the change via ctx.replace() and only opens an undo entry (ctx.beginGesture()) once the drag has moved past the 0.1mm snap grid — a pure click, or a sub-snap jitter that nets zero effective change, leaves history untouched. pointerUp/onDeactivate both clear the drag state so a tool switch mid-drag can never leave a stale gesture open.
Pen (P)
tools/ builds a bezier path one anchor at a time:
Click appends a grid-snapped corner anchor.
Click-drag on the anchor just placed pulls out mirrored
hout/hinbezier handles, turning it into a smooth anchor.Click near the first anchor (path needs ≥3 anchors) closes the path into a filled shape (
fill: gold, no stroke).Enter finishes the path open instead (a stroke, no fill) — needs ≥2 anchors.
Esc cancels the in-progress draft.
The whole multi-click gesture never touches ctx.doc until the path is finished — unlike Select's live-dragging, nothing is mid-flight in the document, so finishing collapses into exactly one ctx.commit() (one undo entry). A floating hint bar (its own self-mounted React tree, since the tool contract has no chrome slot) shows the available actions and their enabled state. The in-progress path is drawn by the tool's renderDraft hook, not by the main renderer.
Text (T)
tools/ is intentionally the smallest tool: a pointer-down creates a TextLayer at the click point (default content "TEXT", size 6mm, color white — the silkscreen layer), commits it, selects it, and immediately switches the active tool back to select so the freshly placed text is draggable/resizable/editable right away.
Pan (H / Space-drag)
tools/ drags the camera's screen-px offset. It's also reached without switching tools: holding Space while any other tool is active makes the Editor route pointer events to pan instead (see Space-drag override below), so a user can pan mid-edit without losing their place in, say, an in-progress pen path.
Zoom (Z)
tools/ zooms by a fixed factor (1.5× in, 1/1.5× out) anchored at the click point via camera.zoomAt — the mm point under the pointer stays stationary. Alt-click zooms out instead of in.
Discoverability: tooltips & the Help panel
Two surfaces make a tool's purpose legible without opening the docs — both fed straight from the ToolModule data:
Toolbar tooltips. Each toolbar button (
components/→toolbar. tsx ChromeButton) shows a hover tooltip built from the tool'slabelandshortcut— e.g.Select (V),Pen (P). The tooltip text is also the button'saria-label, so it is the button's accessible name too. It appears on hover after a short delay and, because it keys off:focus-visible(not plain focus), a mouse click that leaves the button focused doesn't leave the tooltip stuck open. Add-action buttons get the same treatment from theirlabel(they have no shortcut).The sidebar Help panel. The pinned Help footer (
components/) shows the currently active tool's name, a shortcut badge, and its longerhelp- panel. tsx description— a 2–4 sentence explanation of what the tool does, its key pointer interactions, and its shortcut. Each built-in tool supplies one via the optionalToolModule.descriptionfield (see Extension architecture → Tool descriptions); a tool that omits it falls back to a generic "No description available" line. Because the panel followsactiveToolId, holding Space for a transient pan does not swap the shown description.
The pointer/keyboard event contract
The Editor shell (Editor.tsx) owns exactly one rule for routing input: the active tool gets first refusal, then app-level fallbacks run.
Pointer events
Every canvas pointer event is normalized into a ToolPointerEvent (screen px relative to the canvas, the corresponding document mm, button/buttons, modifier keys, pointerId) and handed to onPointerDown / onPointerMove / onPointerUp / onDoubleClick on whichever tool is currently effective.
Space-drag override
The effective tool is normally activeToolId, but while Space is held it is forced to 'pan' regardless of what's active — so pan is available as a momentary override from any tool without changing activeToolId itself. Releasing Space (or a buttons === 0 move, which catches Space being released mid-drag) restores the previously active tool.
Keyboard events
On keydown, the shell first calls the active tool's onKeyDown(e, ctx). If — and only if — that returns true, routing stops there: the tool has claimed the key and no app-level fallback runs. This is how Pen owns Enter/Esc for finishing/cancelling its draft without the shell needing a shared keyboard switch that every tool would otherwise have to edit.
When the tool does not return true, the shell runs its fallbacks in this order:
⌘/Ctrl+Z(undo) /⌘/Ctrl+Shift+Z(redo).A single-key tool shortcut (
V/P/T/H/Z, case-insensitive) — matched only when no modifier key is held, so it never fires ahead of the undo/redo check.Esc— deselect.Delete/Backspace— delete the selected layer.Arrow keys — nudge the selected layer by 0.1mm, or 1mm with Shift held. Pattern layers (which cover the whole panel) are not nudgeable; path layers translate their whole point set.
Keyboard input is ignored globally while an <input>/<textarea>/<select> is focused (e.g. the layer-rename field, an inspector's number field), so typing "V" into a text box never switches tools.
Tip
When building a new tool, onKeyDown returning true is the only way to intercept a key before the shell's fallbacks — there is no separate priority list to edit. See Adding a tool.