Appearance
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/PolygonFeaturereplication). - 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"(seeCanvas/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: throwsmirror: currently supports only line entities (got <kind>)for any non-lineEntityRef. Throwsmirror: degenerate axiswhen 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
TransformDialogwithmode="mirror"— two-point axis (A, B), with anaxisPresetofx-axis/y-axis/custom(TransformDialog.tsx:17,59). The dialog's confirm dispatches tomirrorSelected({x:ax,y:ay},{x:bx,y:by})for in-place and to the clone variant when "Keep originals" is set.
4. State machine
- 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:
Escclears the in-progress axis and returns toselect; selecting any other tool aborts.
- 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:
TransformDialogopens 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
Cancelclears the pending axis and resetsactiveTooltoselect(Canvas/index.tsx:4811-4814).
- Preview after step 2:
- 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). activeToolresets toselect;transformModecleared.
- In-place:
5. Committed state
In-place (transformMirror, transforms.ts:359-460):
Node.x/yfor every node inexpandSelection(...).nodeIdsis replaced with its mirrored position — gated byisComputedNode(virtualRef + crossing virtualNodeId) and byapplyAnchor(anchorxyrefuses; anchorx/yslides along the free axis).Arc: both endpoints in selection → cached centre is mirrored,arcLengthflips sign (chirality),startAngleRadre-derives from new chord (transforms.ts:397-414). Partial selection → host circle re-derives viaarcGeometry(newN0, newN1, arcLength); sweep sign NOT flipped (transforms.ts:416-428).Circle: centre mirrored,radiusinvariant (transforms.ts:430-435).Spline: everycontrols[]entry mirrored;tangents[]are reflected as free vectors via head-minus-tail throughmirrorPoint(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): copiesanchor/anchoredfrom each source endpoint onto the cloned node delta. The pre/post node-id snapshot guards against an unrelated pre-existing node being stomped whenaddSegmentdedupes. - spline tangent locks (
transforms.ts:1184-1196):tangentLockStart/tangentLockEndpropagated verbatim. Mirror does NOT swap start/end —addSplinepreserves[e0, e1]order, so the sourcen0'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 freshparallel/perpendicular(rect) orequal+distance(centre→vertex)(polygon) constraint set. Partial selection cleanly degrades to "no feature" — cloned segments persist as loose primitives. The model this emulates isreplicateFillets. - frameId (
transforms.ts:681): cloned nodes inheritframeIdverbatim. The mirror axis is world-frame, so a node tagged withframeIdstays in that same local frame; mirror does NOT reassign frames. - Fillet replication (
replicateFillets,transforms.ts:1079-1112): anyFilletFeaturewhosesourceCornerId ∈ nodeIdsis re-created on the cloned corner viaapplyFilletat the same radius. Pre-this was the lone parametric-replication helper; modelled rect/polygon replication on it.
6. Constraints / interactions
| Aspect | Behavior |
|---|---|
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:true | Node refuses move; arc/spline reshape works against the pinned endpoint via partial-selection re-derivation. |
virtualRef node | Skipped by isComputedNode guard (transforms.ts:90); the upstream fillet/crossing re-evaluates and the virtual moves with its parents. |
| crossing virtualNode | Same — included in crossingNodeIds via buildCrossingNodeIds; the parent segments mirror and the crossing re-derives. |
RectFeature | Clone 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. |
PolygonFeature | Clone replicates feature + (sides-1) equal + sides distance constraints (replicatePolygons). Centre construction node materialised at mirrored centre. |
FilletFeature | Clone replicates via replicateFillets → applyFillet at copied corner with same radius. In-place mirror moves the source corner; the fillet re-evaluates parametrically from sourceCornerId. |
frameId | Preserved verbatim on clone ; in-place mirror does not touch frame assignment. |
spline tangentLockStart/End | Propagated on clone ; start/end mapping preserved (no swap). |
7. Failure modes
- Degenerate axis (A ≈ B,
len2 < 1e-12):mirrorPointreturnspunchanged (transforms.ts:226), so the UI op is a no-op. The MotorScript path throwsmirror: degenerate axis — both endpoints resolve to the same point(transforms.ts:155-159in executor). The dialog should validate B ≠ A before submit; today it does not. - Empty selection:
expandSelectionreturns empty sets;transformMirrorproduces 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 attransforms.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.
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.
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-405as 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.