Appearance
Selection, Snapping & Constraint Palette
1. Identity
A selection is an ordered list of { kind, id } references into scene.geometry; snapping is the pointer-position quantization that biases the next click toward an existing geometric feature; the Constraint Palette is the popover surface that applies a geometric or dimensional constraint to the current selection.
2. When to use it
- Picking one or more entities to feed into a modify op (move, trim, mirror) or constraint.
- Keyboard-only review of a scene without the mouse (a11y path, screen-reader use).
- Drawing onto existing geometry without manually typing coordinates (snap on nodes / midpoints).
- Applying a constraint by clicking a palette button instead of editing MotorScript.
3. Inputs
- Tool button: Select tool (V) in the left toolbar; Constraint Palette popover trigger in
CanvasToolbar.tsx. - Keyboard shortcut:
VSelect,Escclear,ArrowUp/Down/Left/Rightcycle entities (Canvas/index.tsx:3263),F3toggle snap (Solvespace precedent — not yet wired). - MotorScript builtin: none — selection is a UI-only concept; constraints emit via
addConstraint(kind, refs). - Command Palette: "Select All" (Cmd+A), "Clear Selection" (Esc) — palette open shortcut not yet wired.
- Context menu: right-click on an entity offers Delete, Add Constraint (subset compatible with selection).
4. State machine
- Click in empty space: hit-test returns
null→ starts a marquee rubber-band; preview is a dashed rectangle from press-point to cursor. Esc cancels marquee only, preserving prior selection (Canvas/index.tsx:2888-2890). - Click on entity:
hitTestAll(worldP, pointerType)returns ordered hits (Canvas/index.tsx:755-869); first hit becomes single selection. Shift-click toggles add/remove. Alt-click cycles through stacked hits at the same point. - Marquee release: left-to-right drag = entities fully contained; right-to-left = entities crossing or touching (Solvespace convention,
Canvas/index.tsx:888-895). - Arrow-key nav (no modifier): cycles
orderedEntityList(id-sorted across nodes, segments, arcs, circles, splines;Canvas/index.tsx:3245-3256); wraps modulo length; sets single-selection. - Palette open: Tab into trigger button in
CanvasToolbar.tsx, Space/Enter opens popover; ArrowUp/Down cycle palette items; disabled items skipped from focus order; Enter applies viaapplyConstraintToSelection. - Finalize: constraint emitted, popover closes, selection preserved so the user can chain another constraint.
- Cancel options: Esc closes popover; Esc with palette closed clears selection; tool change preserves selection (except
setScript, which re-evaluates scene from source and discards selection — intentional, see §7).
5. Committed state
- No geometry mutated by selection or snapping alone.
- On palette apply: a new
Constraintentry is appended toscene.constraintsviaaddConstraint, and MotorScript codegen re-emits the correspondingconstrain*(...)statement on next round-trip. - Snap-to-node during a draw tool resolves the click world-point to the existing
node.idinstead of creating a fresh node (dedup path inaddNode/ tool click handlers).
6. Constraints / interactions
Selection model:
ts
Selection: { kind: 'node'|'segment'|'arc'|'circle'|'spline'|'fillet'|'crossing'|'rect'|'polygon', id: string }[]| Surface | Selection behavior |
|---|---|
| Single click | replaces selection with one hit |
| Shift+click | toggles target in/out of selection |
| Marquee L→R | contained-only |
| Marquee R→L | crossing/touching |
| ArrowKey | id-sorted single-selection cycle |
| Esc | clears selection (palette closed) |
| Tool change | preserves selection |
setScript | clears selection (re-evaluation; see §9) |
Hit-test precedence in hitTestAll (top-of-list wins for hitTest): labels → nodes → arcs → circles → segments → splines → fillets (Canvas/index.tsx:765-858). Note: labels are appended first but are short-circuited only when the cursor is inside their flag bbox; nodes are the practical top of the geometric stack.
Hit radius: hitRadiusFor(pointerType) from HIT_PX_BY_POINTER = { mouse: 6, touch: 18, pen: 8 } (Canvas/Surface.tsx:15), divided by viewport.scale to get world tolerance. Candidates are pre-filtered by the uniform-grid spatial index (spatial-index.ts) before the linear exact-distance test.
Snap targets (priority high→low while a draw tool is active):
- Existing node within hit radius.
- Segment midpoint.
- Segment endpoint (redundant with node, but reached via segment hit path).
- Perpendicular foot from cursor onto the nearest segment.
- Free world point (no snap).
F3 toggles snap on/off globally (Solvespace precedent; wiring pending). When snap is on, the snap radius reuses the pointer-aware hit radius — touch input gets 18 px snap halo, mouse 6 px.
Constraint Palette: shows 9 of 12 geometric kinds; symmetric, onLine, onCircle are excluded from the popover and reachable only via MotorScript / context menu — cross-ref geometric.md. Keyboard reachability: Tab → trigger; Space/Enter opens; ArrowDown/Up cycle; Enter applies; Esc closes (CanvasToolbar.tsx).
Viewport culling: ConstraintOverlay.tsx:117-128 computes a visible world rect with a 10% bleed margin and skips badges outside it. The spatial index is not reused for render culling — it's hit-test only.
7. Failure modes
- Empty selection on tool needing selection (e.g., palette button pressed with
selection.length === 0): action is a no-op and a toast surfaces "Select an entity first." Palette renders incompatible items asdisabledrather than firing a toast on click. - Marquee touched nothing: rubber-band closes silently, prior selection unchanged, no toast.
- Palette with no compatible selection: items are disabled in the popover (e.g.,
horizontalrequiressegment; greyed out when only nodes selected). - Alt-click cycle exhausted: returns top-of-stack hit so user is never stranded with
nullselection (Canvas/index.tsx:880-883). setScriptdiscards selection: intentional — script re-evaluation rebuilds geometry and ids may rotate; downstream draft state inEditableFieldis lost.- Snap target deleted mid-tool: tool falls back to free-world coordinates at next pointermove.
8. Figures
snap halo on hover near a node
┌─────────────────────────┐
│ │
│ ╭─╮ │
│ ╱ ╲ ← 6 px halo │
│ │ ● │ (mouse) │
│ ╲ ╱ cursor inside
│ ╰─╯ → snap fires
│ │
└─────────────────────────┘ marquee box covering 3 entities (L→R, contained-only)
┌─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┐
╷ •A───────•B ╷
╷ ╷
╷ •C ◯circle ╷ ← rectangle, segment AB,
╷ ╷ circle: all fully inside.
└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘ Segment leaving the box on
╲ R→L drag would also select
cursor end-point under crossing semantics.Figure 1 (snap halo): cursor near a node fires a green snap halo and locks the next click to the node id rather than creating a fresh one; a floating "snap to node" hint annotates the affordance.
Figure 2 (marquee select): dashed green marquee rectangle; 3 contained entities are highlighted green as selected, an entity outside the box is not.
9. Known bugs
setScriptround-trip clearsEditableFielddrafts because selection is dropped on scene re-evaluation; the user loses in-flight numeric edits in the Properties drop-down.