Skip to content

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: V Select, Esc clear, ArrowUp/Down/Left/Right cycle entities (Canvas/index.tsx:3263), F3 toggle 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

  1. 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).
  2. 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.
  3. Marquee release: left-to-right drag = entities fully contained; right-to-left = entities crossing or touching (Solvespace convention, Canvas/index.tsx:888-895).
  4. 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.
  5. 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 via applyConstraintToSelection.
  6. 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 Constraint entry is appended to scene.constraints via addConstraint, and MotorScript codegen re-emits the corresponding constrain*(...) statement on next round-trip.
  • Snap-to-node during a draw tool resolves the click world-point to the existing node.id instead of creating a fresh node (dedup path in addNode / tool click handlers).

6. Constraints / interactions

Selection model:

ts
Selection: { kind: 'node'|'segment'|'arc'|'circle'|'spline'|'fillet'|'crossing'|'rect'|'polygon', id: string }[]
SurfaceSelection behavior
Single clickreplaces selection with one hit
Shift+clicktoggles target in/out of selection
Marquee L→Rcontained-only
Marquee R→Lcrossing/touching
ArrowKeyid-sorted single-selection cycle
Escclears selection (palette closed)
Tool changepreserves selection
setScriptclears 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):

  1. Existing node within hit radius.
  2. Segment midpoint.
  3. Segment endpoint (redundant with node, but reached via segment hit path).
  4. Perpendicular foot from cursor onto the nearest segment.
  5. 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 as disabled rather 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., horizontal requires segment; greyed out when only nodes selected).
  • Alt-click cycle exhausted: returns top-of-stack hit so user is never stranded with null selection (Canvas/index.tsx:880-883).
  • setScript discards selection: intentional — script re-evaluation rebuilds geometry and ids may rotate; downstream draft state in EditableField is 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.
snap to node

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.

marquee (L→R contained)

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

  • setScript round-trip clears EditableField drafts because selection is dropped on scene re-evaluation; the user loses in-flight numeric edits in the Properties drop-down.

motordevs studio — geometry editor specification