Appearance
Copy and Array (Linear / Radial)
1. Identity
A copy is a modify-op that emits cloned copies of a selection — translated (linear array), rotated about a centre (radial array), or reflected across an axis (mirror copy, covered in mirror.md) — leaving the originals in place and routing each cloned primitive through the standard adders so auto-split fires.
2. When to use it
- Replicate a stator slot N times around the bore centre (radial,
count = slotCount). - Stamp out a row of rotor magnets along the airgap (linear,
dx/dyset by pole pitch). - Duplicate a parametric
RectFeatureorPolygonFeatureand keep its shape constraints intact on every copy (replication). - Mirror-copy a coil-end half across the d-axis to fill both phases (see mirror.md §3 clone mode).
3. Inputs
- Tool button: Modify toolbar → Copy icon (linear and radial share the same toolbar entry with a kind preset). Sets
activeTool = "copy"andtransformMode = "copy"(Canvas/index.tsx:461,507,1296,4525,4529,4530,4797). - Keyboard shortcut: not yet wired.
- MotorScript builtins (
executor/builtins/transforms.ts):radial(entity, count, center)—:69-130. Lines only; throws on non-integer/<1/non-line/missing-PointRef/count > MAX_PATTERN_COUNT(5000) / cumulative scene cap.linearArray(entity, dx, dy, count)—:172-210. Lines only; same guards oncount.- Caps: per-call
MAX_PATTERN_COUNT = 5000plus cumulativeMAX_SCRIPT_ENTITIES_LOCAL = 5000projected check before any allocation.
- Command Palette: not yet wired.
- Context menu: right-click on a non-empty selection → "Copy…", "Linear Array…", "Radial Array…" (entries calling
onStartCopy/onStartLinearArray/onStartCopyRadial,Canvas/index.tsx:4525-4530). The two array entries pre-settransformCopyKindPresetso the shared dialog opens on the right tab. - Dialog: shared
TransformDialogwithmode="copy";arrayKindof'linear'or'radial', pluscount,(dx,dy)for linear,(cx,cy)+angleDegfor radial (TransformDialog.tsx:19-32,69-70). Confirm dispatches tocopySelectedLinearorcopySelectedRadial(Canvas/index.tsx:4797-4802).
4. State machine
- Click 1 / Action: pick the reference point. Linear → first point of the (dx,dy) vector. Radial → centre of rotation (snap to a node for "rotate around this node" intent).
- Preview after step 1: live-ghost of the selection at one-step offset/rotation following the cursor.
- Cancel options:
Escaborts; switching tools aborts.
- Click 2 / Action: linear → second point fixing (dx,dy); radial → second point fixing the angle from centre (or the dialog can override).
- Preview after step 2: dialog opens with
count,arrayKind, and (dx,dy) or (cx,cy,angleDeg) prefilled. - Cancel options: dialog
Cancel→setTransformMode(null),setActiveTool("select")(Canvas/index.tsx:4811-4814).
- Preview after step 2: dialog opens with
- Finalize: dialog
Confirmdispatches:- Linear:
transformCopyArray(scene, selection, dx, dy, count)(transforms.ts:560-586). - Radial:
transformCopyRadial(scene, selection, cx, cy, angleDeg, count)(transforms.ts:592-615). - Both reset
transformModeandactiveTooltoselect.
- Linear:
5. Committed state
For each i = 1..count the cloner emits via applyClone (transforms.ts:687-826):
- New
Segments viaaddSegment(which auto-splits against existing geometry); new endpointNodes are created or deduped atTOL_POINT. - New
Arcs viaaddArcpreserving the sourcearcLength(sign unchanged for translate/rotate; flipped only in mirror, see mirror.md). - New
Circles viaaddCircle(radius invariant). - New
Splines viaaddSplinewith materialised CP nodes carryingsplineCpRefback-pointers. - New
BlockLabels viaaddBlockLabel, withblockType/inCircuit/turns/magDir/fillFactor/inGroup/maxAreapatched in viaapplyLabelProps(transforms.ts:1251-1269). - Lone selected
Nodes (not endpoints of any cloned segment/arc, notvirtualRef) cloned viainlineAddNode.
Plus the patch-preservation chain — applied in this order at every clone step:
- anchor (
patchClonedNodeAnchor,transforms.ts:650-685): snapshotspreExistingIdsbefore eachaddSegment/addArc/addSpline/inlineAddNode, then copiesanchorandanchoredfrom the source node onto the cloned node located at the transformed position. The snapshot guard prevents stomping whenaddSegmentdedupes to a pre-existing node. - spline tangent locks (
transforms.ts:762-773):tangentLockStart/tangentLockEndpropagated onto the just-added spline (identified as the one whose id isn't inpreSplineIds). - RectFeature/PolygonFeature replication (
replicateRectstransforms.ts:871-959,replicatePolygons:961-1077): when all backing nodes are in the selection, a fresh feature record is emitted with cloned segment/node ids, plus the rectangle'sparallel+parallel+perpendicularconstraint trio (encoding) or the polygon's(sides-1)equal+sidesdistance(centre,vertex)constraints. Polygon centre construction node is materialised viaaddNode(..., 'construction')if owned. Partial selection cleanly degrades to "loose primitives, no feature". - frameId (
transforms.ts:681): cloned nodes inheritframeIdverbatim — every copy in a linear/radial pattern stays in the same local frame as the source. - Fillet replication (
replicateFillets,transforms.ts:1079-1112): anyFilletFeaturewhosesourceCornerId ∈ nodeIdsis re-created at the cloned corner viaapplyFillet(scene, copiedCorner.id, f.radius). The rect / polygon replicators above follow the same pattern.
MotorScript reverse-codegen is not yet wired for the UI clone path; scripts must call linearArray / radial directly.
6. Constraints / interactions
| Aspect | Behavior |
|---|---|
node.anchor | Source-anchor flags copy to clone via patchClonedNodeAnchor. Clone path does NOT consult applyAnchor to gate the move — the clone is at a new world position by definition. |
virtualRef source node | Lone-node loop skips it (transforms.ts:798, 1213); the upstream feature owns the position and the cloned fillet/crossing materialises from cloned parents. |
frameId | Preserved verbatim across linear/radial/mirror clone (transforms.ts:681). |
RectFeature | All 4 backing nodes selected → cloned with fresh parallel/parallel/perpendicular constraint trio (replicateRects). |
PolygonFeature | All N vertices selected → cloned with (sides-1) equal-length + sides distance(centre→vertex) constraints (replicatePolygons); centre node materialised. |
FilletFeature | sourceCornerId ∈ nodeIds → re-emitted at copied corner with same radius (replicateFillets). Partial parent selection at copied corner → applyFillet bails, no fillet on clone. |
Crossing | Not in selection language (transforms.ts:34); ignored by expandSelection. Cloned segments that intersect existing geometry re-derive crossings via addSegment's auto-split. |
| Segment auto-pull | A segment whose n0 AND n1 are in nodeIds is cloned even when not explicitly selected (transforms.ts:706-710) — matches user intuition for "select two corners, copy the connecting edge". |
| Cumulative cap (script) | Each call projects (count-1)*3 entities and fast-fails if existing + projected > 5000 (builtins/transforms.ts:104-112, 191-198). |
7. Failure modes
count < 1: UI path returns scene unchanged (transforms.ts:567, 600). Script path throwslinearArray: count must be a positive integer (got <n>)/radial: count must be a positive integer(builtins/transforms.ts:75-77, 176-178).count > MAX_PATTERN_COUNT (5000): UI path throwstransformCopyArray: count <n> exceeds the per-pattern limit of 5000(transforms.ts:573-577, 603-607). Script path throws the equivalentlinearArray/radialmessage and additionally fast-fails on cumulative cap (builtins/transforms.ts:93-112, 182-198).(dx,dy) ≈ (0,0)(linear) or|angleDeg| < TOL.SOLVER(radial): UI path returns scene unchanged (transforms.ts:579, 608).- Non-integer / NaN / Infinity count: script path throws (
builtins/transforms.ts:75-77, 176-178); UI path'scountfield usesparseDialogNumericand silently rounds — BACKLOG to surface a dialog-side validator. - Non-line MotorScript entity: throws
radial: currently supports only line entities/linearArrayreturns[](note asymmetry —linearArraysilently returns empty for non-line;radialthrows). BACKLOG. - Radial centre missing/invalid in script:
radial: center must be a PointRef. - Empty selection:
expandSelectionreturns empty sets; every loop is a no-op; no toast.
8. Figure
•───• (source)
│
step 1:•───• (clone i=1, rotated by 120°)
│
step 2:•───• (clone i=2, rotated by 240°)
│
centre ✱ 3-element radial pattern about ✱ (count=3, angleDeg=120)ASCII: 3-element radial array of a segment around centre ✱; source plus two clones positioned at angleDegi for i=1..2. For count=3 the executor emits count-1 = 2 clones (builtins/transforms.ts:116), while the UI transformCopyRadial emits count clones (transforms.ts:611) — see "source-code surprises" in the audit notes.*
Figure 1 (single copy): original (dashed) and a single clone (solid) offset by Δx/Δy.
Figure 2 (linear array N=4): original (dashed) plus 3 clones translated along a vector.
Figure 3 (radial array N=6): source (dashed) plus 5 clones placed at 60° intervals around the centre pivot.
9. Known bugs
UI/script clone-count asymmetry — UI
transformCopyRadialloopsi=1..count(emitscountclones) while scriptradialloopsi=1..count-1(emitscount-1clones, treating source as i=0). Not yet catalog-numbered.
linearArraysilent empty on non-line entity —builtins/transforms.ts:200returns[]instead of throwing the domain-specific error its siblingradialthrows. Not yet catalog-numbered.
No other bugs known.
10. Class API
In the current model, the selection-aware copy pipelines still live as the free functions transformCopyArray(...), transformCopyRadial(...), transformCopyMirror(...) in app/src/lib/fea2d/transforms.ts — the transforms.ts dissolution into class methods is deferred to .
Cloning machinery available on the class layer:
Node.clone / Segment.clone / ArcSegment.clone / Circle.clone / Spline.clone / BlockLabel.clone / Frame.clone— structural copies that preserve the prototype (soinstanceof <Class>survives) but keep the same id. The array path remaps to a fresh id by reading the snapshot viatoJSON, allocating a new id fromScene.mintXxxId, and constructing viafromJSON({ ...snap, id: newId }).Scene.addNode / addSegment / ...— duplicate-id-rejecting adders so the array path can't accidentally clobber an existing entity.
The virtualRef rewire (each cloned VirtualPoint must reference the COPIED upstream feature, not the original) is unchanged behaviour from pre-refactor transforms.ts; preserves the exact rewire semantics when the op moves onto class methods.