Skip to content

Move

1. Identity

A move is an in-place rigid translation of the current selection by a vector (dx, dy); the graph topology is preserved (same node ids, same edge endpoints) and only node coordinates change.

2. When to use it

  • Reposition a sub-assembly (e.g. a slot tooth) without re-drawing it.
  • Slide one or both endpoints of a segment by a known delta to match a parametric dimension.
  • Translate a free-standing block label or construction node onto a snap target.
  • Move a filleted corner — the fillet re-evaluates parametrically from its moved sourceCornerId.

3. Inputs

  • Tool button: Modify toolbar, "Move" slot.
  • Keyboard shortcut: not yet wired (no k === 'm' branch in Canvas/index.tsx shortcut dispatch around line 2716).
  • MotorScript builtin: move(selection, dx, dy) — see executor/builtins/transforms.ts.
  • Command Palette: "Modify: Move selection".
  • Context menu: right-click on a selection → "Move…" opens TransformDialog in mode='move'.

The tool is gated on selection.length > 0; activating it with an empty selection falls through to no-op (see Canvas/index.tsx:1296-1298).

4. State machine

Two parallel entry flows. The canvas-click flow is the default once the tool is active.

Canvas-click flow (planTransformAction in tools/transform-tool.ts:86-94, dispatch at Canvas/index.tsx:1296-1312):

  1. Click 1 / Reference point: pick any point in world space (snap-aware). This is the "from" handle.
    • Preview after click 1: ghost outline of the selection follows the cursor; tool-hint strip shows "Click target point — selection moves by delta".
  2. Click 2 / Destination point: the translation vector is clicks[1] − clicks[0].
    • Preview after click 2: none — applyTransformWithCanvas dispatches and the tool reverts to select.
  3. Finalize: automatic on the second click. setTransformCanvasClicks([]) clears the buffer, setActiveTool("select") exits.
  • Cancel options: Esc (clears the click buffer via the activeTool reset effect at Canvas/index.tsx:461-466), or switch to any non-transform tool.

Dialog-driven exact-value flow (TransformDialog.tsx mode='move', lines 79-83):

  1. Action: invoke "Move…" from context menu or palette → dialog opens pre-filled with defaults.ax / defaults.ay (typically the last cursor delta or (10, 0)).
  2. Enter Δx and Δy: locale-aware numeric inputs (parseDialogNumeric). Both must be finite, else submit is a no-op.
  3. Finalize: click Apply (or press Enter) → onConfirm({ ax, ay }) calls transformMove(scene, selection, dx, dy).
  • Cancel options: Esc, click outside, or Cancel button → onCancel.

5. Committed state

After finalize, transformMove returns a new SceneModelV2 patched via patchGeometry (transforms.ts:241-250, then applyInPlace at :462-545):

  • Nodes: every node id in the expanded selection closure has its (x, y) rewritten — subject to applyAnchor and the computed-node guard isComputedNode (transforms.ts:90-92).
  • Arcs: arcs whose endpoints moved have their cached cx / cy / radius / startAngleRad re-derived; if both endpoints moved under the same rigid map the centre is translated directly (no chord re-derivation drift on 180° arcs).
  • Splines: each interior CP in controls[] is translated; endpoint CPs follow via the node map.
  • Circles: (cx, cy) translated; radius invariant.
  • BlockLabels: (x, y) translated.
  • MotorScript codegen: emits one move(selectionRef, dx, dy) statement — see codegen.ts transform codegen path.
  • Constraints: no new constraints emitted. Existing constraints (parallel / equal / distance) survive untouched since node ids are stable.

A bit-exact zero delta (|dx| < 1e-12 ∧ |dy| < 1e-12) is a no-op and returns the input scene unchanged (transforms.ts:248).

6. Constraints / interactions

FeatureBehavior under move
Anchored node (anchor:'xy' or legacy anchored:true)Refuses outright — applyAnchor returns null, node skipped (transforms.ts:120). Confirmed by ; both representations honoured.
Axis-anchored node (anchor:'x')Only y translates; x clamped to original (transforms.ts:121-122). Same for 'y'.
Computed node (virtualRef set, or bare crossing point)Skipped — isComputedNode guard at transforms.ts:476. Re-evaluated downstream from its parents.
Fillet (selected directly, or sourceCornerId in selection)Source corner translates; applyFillet re-evaluates the tangent arc + isConstruction parents at the new corner location.
CrossingAuto-promotion / drift on partial moves is **** territory — reEvaluateCrossings must run after the move to re-intersect parents.
Rect / Polygon featureAll four (or N) backing nodes must be in the selection closure for the feature to translate coherently; partial selection breaks the shape lock (the feature record stays, but the parallel/perpendicular/equal/distance constraints will solver-error on next dispatch).
Selected segment / arc / splineExpansion (transforms.ts:140-198) pulls in endpoint nodes + interior CPs automatically — see Selection.
Constraints (horizontal, vertical, distance, parallel, etc.)Survive — node ids are stable across in-place transforms. Distance constraints stay satisfied because the rigid translation preserves all pairwise distances.

7. Failure modes

  • Empty selection: tool button refuses activation; if forced, canvas-click dispatch never fires (gated at Canvas/index.tsx:1298). No toast — the Modify toolbar buttons are visually disabled in selection-empty state.
  • All-anchored selection (every node has anchor:'xy' or legacy anchored:true): transformMove runs, applyInPlace walks every node and skips them all → returned scene is identity-equal to input. No toast. Treated as a silent no-op (mirrors moveNode semantics).
  • Zero-vector dialog input (dx = dy = 0): canvas dispatch is a bit-exact no-op (transforms.ts:248). Dialog doesn't reject — Apply closes the dialog with no scene change. N.B. unlike Rotate's surfaceError('rotate:zero-angle') toast (TransformDialog.tsx:107-113), Move has no equivalent zero-delta guard at the dialog layer.
  • Partial rect/polygon selection: feature record's constraint set becomes geometrically inconsistent on the next solve — surfaces as a red constraint pill in the palette. Not a Move-specific failure; the violation is logged against the offending parallel/equal/distance constraint.
  • Computed-node-only selection (e.g. only a crossing virtualNode picked): every node is skipped → silent no-op.

8. Figures

   selection ghost                          committed
   ┌────────┐         click 1 (ref)       ┌────────┐
   │  A──B  │  ────────●──────────→ ●     │  A──B  │  →  A'──B'
   │   /    │            click 2          │   /    │       /
   │  C     │            (target)         │  C     │      C'
   └────────┘                              └────────┘

Figure 1: Two-click flow. Click 1 sets the reference; click 2 sets the destination; the translation vector is (click2 − click1).

A (ref)BCΔ=(80,40)

Figure 2 (before): selection of 3 nodes and 2 segments at the original position (dashed), with the translation vector Δ drawn as a construction arrow from the reference node.

A'B'C'

Figure 3 (after): same shape translated by Δx=80, Δy=−40 (solid blue); arrow from original reference node A to its new position A'.

9. Known bugs

No move-specific entries beyond the cross-cutting crossing issue as of .

10. Class API

In the current model, the selection-aware pipeline still lives as the free function transformMove(scene, dx, dy, selection) in app/src/lib/fea2d/transforms.ts — the transforms.ts dissolution into class methods is deferred to . The class layer provides two cheaper handles for callers that already hold a node instance:

  • Node.withMove(dx, dy): Node (app/src/lib/fea2d/model/Node.ts) — returns a new prototype-preserving copy shifted by (dx, dy). Use this when you want a fresh node record for an immutable patch path.
  • In-place mutation: write n.x += dx; n.y += dy directly on the instance. The drag pipeline uses this hot path; Scene.bumpRevision invalidates phase-7 derived caches.

Scene exposes index lookups (scene.node(id), scene.segment(id), etc.) so a move op can pull the actual instances cheaply without a linear scan. The anchored / virtualRef / splineCpRef guards from Node's constructor invariants and the snapshot semantics described in §5 carry forward unchanged.

motordevs studio — geometry editor specification