Skip to content

Mirror

1. Identity

A mirror is a modify-op that reflects a selection across a user-specified axis line, either in place (the originals move to their reflected positions) or as a clone (the originals stay and reflected copies are emitted).

2. When to use it

  • Build a symmetric stator/rotor half and reflect it across the d-axis to get the other half (clone mode).
  • Flip the chirality of a sketch — e.g. an N-pole shoe drawn CW becomes the S-pole drawn CCW (in-place mode).
  • Reflect a single asymmetric coil-end profile to the opposite slot without re-dimensioning (clone mode + retained constraints via RectFeature/PolygonFeature replication).
  • Repair a "drawn on the wrong side" mistake on a node-anchor-locked sketch — anchors and frameId follow the reflection .

3. Inputs

  • Tool button: Modify toolbar → Mirror icon. Sets activeTool = "mirror" (see Canvas/index.tsx:461,507,1296,2902,4524).
  • Keyboard shortcut: not yet wired.
  • MotorScript builtin: mirror(entity, axis)executor/builtins/transforms.ts:132-170. Lines only per the audit: throws mirror: currently supports only line entities (got <kind>) for any non-line EntityRef. Throws mirror: degenerate axis when both axis endpoints resolve to the same point (transforms.ts:155-159).
  • Command Palette: not yet wired.
  • Context menu: right-click on a non-empty selection → "Mirror…" (entry that calls onStartMirror, Canvas/index.tsx:4524).
  • Dialog: shared TransformDialog with mode="mirror" — two-point axis (A, B), with an axisPreset of x-axis / y-axis / custom (TransformDialog.tsx:17,59). The dialog's confirm dispatches to mirrorSelected({x:ax,y:ay},{x:bx,y:by}) for in-place and to the clone variant when "Keep originals" is set.

4. State machine

  1. Click 1 / Action: with active-tool = mirror, click the first axis point A (snap targets honoured).
    • Preview after step 1: rubber-band line from A to cursor; selection rendered in its current position with a faint reflection ghost across the live A→cursor line.
    • Cancel options: Esc clears the in-progress axis and returns to select; selecting any other tool aborts.
  2. Click 2 / Action: click the second axis point B. If the picked B coincides with A (within TOL.GEOMETRIC), the click is ignored — axis is still pending.
    • Preview after step 2: TransformDialog opens with the axis prefilled and a "Keep originals (clone)" checkbox. The reflected ghost is frozen as a preview of the committed result.
    • Cancel options: dialog Cancel clears the pending axis and resets activeTool to select (Canvas/index.tsx:4811-4814).
  3. Finalize: dialog Confirm:
    • In-place: transformMirror(scene, selection, A, B) (transforms.ts:359).
    • Clone: transformCopyMirror(scene, selection, A, B)applyCloneMirror (transforms.ts:620-628, 1114).
    • activeTool resets to select; transformMode cleared.

5. Committed state

In-place (transformMirror, transforms.ts:359-460):

  • Node.x/y for every node in expandSelection(...).nodeIds is replaced with its mirrored position — gated by isComputedNode (virtualRef + crossing virtualNodeId) and by applyAnchor (anchor xy refuses; anchor x/y slides along the free axis).
  • Arc: both endpoints in selection → cached centre is mirrored, arcLength flips sign (chirality), startAngleRad re-derives from new chord (transforms.ts:397-414). Partial selection → host circle re-derives via arcGeometry(newN0, newN1, arcLength); sweep sign NOT flipped (transforms.ts:416-428).
  • Circle: centre mirrored, radius invariant (transforms.ts:430-435).
  • Spline: every controls[] entry mirrored; tangents[] are reflected as free vectors via head-minus-tail through mirrorPoint (transforms.ts:439-454).
  • BlockLabel: position mirrored; properties unchanged (transforms.ts:456-458).
  • No MotorScript statement is reverse-codegened for the in-place case — it's a UI-only mutation; the script path is the mirror(entity, axis) builtin.

Clone (applyCloneMirror, transforms.ts:1114-1239): emits cloned segments/arcs/circles/splines/labels/lone-nodes through addSegment/addArc/addCircle/addSpline/addBlockLabel/addNode so auto-split with existing geometry fires, plus the patch chain:

  • anchor (patchClonedNodeAnchor, transforms.ts:650-685): copies anchor/anchored from each source endpoint onto the cloned node delta. The pre/post node-id snapshot guards against an unrelated pre-existing node being stomped when addSegment dedupes.
  • spline tangent locks (transforms.ts:1184-1196): tangentLockStart/tangentLockEnd propagated verbatim. Mirror does NOT swap start/end — addSpline preserves [e0, e1] order, so the source n0's lock follows to the cloned start endpoint.
  • RectFeature/PolygonFeature (replicateRects, replicatePolygons, transforms.ts:871-1077): when all backing nodes of a rect/polygon are in the selection, the feature record is re-emitted at the mirrored corners with a fresh parallel/perpendicular (rect) or equal + distance(centre→vertex) (polygon) constraint set. Partial selection cleanly degrades to "no feature" — cloned segments persist as loose primitives. The model this emulates is replicateFillets.
  • frameId (transforms.ts:681): cloned nodes inherit frameId verbatim. The mirror axis is world-frame, so a node tagged with frameId stays in that same local frame; mirror does NOT reassign frames.
  • Fillet replication (replicateFillets, transforms.ts:1079-1112): any FilletFeature whose sourceCornerId ∈ nodeIds is re-created on the cloned corner via applyFillet at the same radius. Pre-this was the lone parametric-replication helper; modelled rect/polygon replication on it.

6. Constraints / interactions

AspectBehavior
node.anchor='x'Mirror about a non-x-aligned axis ignores the proposed y move on the anchored axis (slides along free axis only); see in-place mirror NOTE at transforms.ts:402-405 — this can leave the cached arc centre geometrically inconsistent. Documented BACKLOG limitation.
node.anchor='xy' / anchored:trueNode refuses move; arc/spline reshape works against the pinned endpoint via partial-selection re-derivation.
virtualRef nodeSkipped by isComputedNode guard (transforms.ts:90); the upstream fillet/crossing re-evaluates and the virtual moves with its parents.
crossing virtualNodeSame — included in crossingNodeIds via buildCrossingNodeIds; the parent segments mirror and the crossing re-derives.
RectFeatureClone replicates the feature record + 3 fresh constraints (replicateRects). In-place mirror does not touch the feature record — it stays bound to the same node ids whose positions changed.
PolygonFeatureClone replicates feature + (sides-1) equal + sides distance constraints (replicatePolygons). Centre construction node materialised at mirrored centre.
FilletFeatureClone replicates via replicateFilletsapplyFillet at copied corner with same radius. In-place mirror moves the source corner; the fillet re-evaluates parametrically from sourceCornerId.
frameIdPreserved verbatim on clone ; in-place mirror does not touch frame assignment.
spline tangentLockStart/EndPropagated on clone ; start/end mapping preserved (no swap).

7. Failure modes

  • Degenerate axis (A ≈ B, len2 < 1e-12): mirrorPoint returns p unchanged (transforms.ts:226), so the UI op is a no-op. The MotorScript path throws mirror: degenerate axis — both endpoints resolve to the same point (transforms.ts:155-159 in executor). The dialog should validate B ≠ A before submit; today it does not.
  • Empty selection: expandSelection returns empty sets; transformMirror produces a no-op patch. No toast.
  • Selection contains only virtualRef nodes: every node is skipped by isComputedNode; arcs whose endpoints are all virtual also skip → silent no-op.
  • Mixed anchor + both-endpoints-mirrored arc: an anchor:'x' endpoint stays put while the cached centre mirrors, producing a geometrically inconsistent arc. Documented at transforms.ts:402-405. BACKLOG.
  • Non-line MotorScript entity: throws mirror: currently supports only line entities.
  • Missing axis arg / invalid axis ref: throws mirror: axis must be a LineRef.

8. Figure

              axis

   A•──────•B  │  B'•──────•A'        (clone mirror across vertical axis)
       (sel)   │   (clone)

   ─•──•──•──•─┼─•──•──•──•─
   axis endpoint at top, axis endpoint at bottom

ASCII: clone-mirror of segment A–B across the vertical axis line; A'–B' is the reflected clone. In in-place mode the original A–B disappears and only A'–B' remains.

axis

Figure 1 (in-place mirror): original (dashed) and mirrored result (solid) share the scene; the vertical axis is drawn as a construction line. In-place mode discards the dashed source on commit.

axisclone

Figure 2 (clone mirror): original (blue) is retained; the reflected clone (green) is emitted on the other side of the axis with replicated anchors/features.

9. Known bugs

Anchor + both-endpoints-mirrored arc — documented inline at transforms.ts:402-405 as a BACKLOG limitation; not yet catalog-numbered.

No other bugs known.

10. Class API

In the current model, the selection-aware mirror pipeline still lives as the free function transformMirror(scene, axisP1, axisP2, mode, selection) in app/src/lib/fea2d/transforms.ts — the transforms.ts dissolution into class methods is deferred to .

The clone-mode path calls clone on each affected primitive instance (so the prototype is preserved) and then mints fresh ids via Scene.mintNodeId / mintSegmentId / … before adding the copies through Scene.addNode / addSegment / …. The id-only cross-reference rule makes the rewire trivial: the cloned primitive's endpoint ids are remapped to the cloned node ids before insertion.

The virtualRef rewire on copy (so duplicated VirtualPoints reference the COPIED upstream feature, not the original) is unchanged behaviour from pre-refactor transforms.ts.

motordevs studio — geometry editor specification