Appearance
Segment
1. Identity
A Segment is a straight line bounded by two endpoint Nodes (n0, n1) — the planar-graph edge that carries mesh sides and boundary markers into the FEA solver.
2. When to use it
- Drawing stator/rotor outline edges that aren't arcs.
- Reference centerlines, symmetry axes, sketch scaffolds (use the
isConstructionflag). - Defining a single boundary edge between two known nodes — reach for the line tool.
- Chaining multiple connected edges in one go (e.g., a stepped pole shoe) — reach for the polyline tool.
3. Inputs
- Tool button — toolbar slot "Line" and "Polyline" (two distinct slots).
- Keyboard shortcut —
Lfor line (Canvas/index.tsx:2757). Polyline has no shortcut as of this writing — not yet wired. - MotorScript builtin —
line(p1, p2)(executor/builtins/primitives.ts:134); polyline is sugar over repeatedlinecalls — no dedicated builtin. - Command Palette — not yet wired.
- Context menu — on a committed segment: "Toggle construction", "Anchor", "Delete", "Set max side length". Construction toggle flips
isConstructionand is also driven en bloc by the HardHat toolbar button.
4. State machine
Line tool (activeTool === "line")
- Click 1 — pick
n0. Snaps to existing node / feature-derived point viaensureNodeWithPromotion(commands.ts:646). Stored indraft[0].- Preview after step 1: ghost dot at
n0; rubber-band line fromn0to live cursor; readout showsdx, dy, length. - Cancel:
Esc, right-click, or switching to another tool — clearsdraft.
- Preview after step 1: ghost dot at
- Click 2 — pick
n1. Same snap rules. Routes throughaddLineWithSnap(Canvas/index.tsx:2263) which forwards toaddSegment(commands.ts:621).- Preview after step 2: none — segment is committed;
draftclears.
- Preview after step 2: none — segment is committed;
- Finalize — implicit on click 2. Tool stays active for the next segment (independent start).
Polyline tool (activeTool === "polyline")
- Click 1 — pick first vertex. Stored as
draft[0];polylineLastNodeIdRefset to that node.- Preview after step 1: rubber-band from
draft[0]to cursor.
- Preview after step 1: rubber-band from
- Click N (N ≥ 2) — pick next vertex. Emits a segment from
draft[N-2]→draft[N-1]viaaddPolylineWithSnap(Canvas/index.tsx:2319).draftgrows;polylineLastNodeIdRefupdates so the next vertex re-uses the just-created node (no duplicate free nodes).- Preview after step N: committed segments visible; rubber-band from last vertex to cursor.
- Cancel mid-chain:
Esc, right-click, or tool switch —draftclears, already-committed segments are kept.
- Finalize —
Enter(Canvas/index.tsx:2871), right-click, orEsc. Chain closes; tool stays active for a new chain.
5. Committed state
After finalize, scene.geometry gains:
- One or more
Segment(scene-model.ts:130-150) with freshid, the resolvedn0/n1,maxSideLength: -1(auto),hidden: false,inGroup: 0,boundaryMarker: null,isConstruction: trueiff the active layer was construction. - Zero or more
Nodeentries created for endpoints that didn't pre-exist (viaensureNodeWithPromotion). - Zero or more
Node+CrossingFeaturepairs at each intersection with an existing same-layer segment / arc / woken-up circle (commands.ts:680-740). See Crossing. - Reverse-codegen:
codegenSegment(codegen.ts:61) writesPolyline emits onep17 = point(0, 0) p18 = point(10, 0) ln5 = line(p17, p18)line(...)per segment, re-using the prior endpoint name where possible.
The new segment is appended whole — split-at-interior-nodes is deferred to the read-time evaluator, not materialised in storage.
6. Constraints / interactions
| Constraint | Behavior on this primitive |
|---|---|
| horizontal | locks y(n0) = y(n1). |
| vertical | locks x(n0) = x(n1). |
| parallel | between two segments; locks direction vectors collinear. |
| perpendicular | between two segments; direction vectors orthogonal. |
| equal-length | between two segments; chord lengths equal. |
| distance | length of this segment fixed to a value (dimensional). |
| distanceH / distanceV | x- or y-projection of n0n1 fixed. |
| onLine | a third node forced collinear with this segment. |
| midpoint | a third node forced to (n0+n1)/2. |
| symmetric | two nodes mirrored about this segment (used as the axis). |
See geometric constraints for the full residual / Jacobian table.
Other interactions:
- Crossing: any new segment whose line crosses an existing same-layer segment / arc / live circle emits a Crossing feature at each intersection (
commands.ts:680-740). Cross-layer crossings are intentionally invisible. - Fillet: a segment can be one of the two inputs to a Fillet; the segment is trimmed back to the fillet tangent point.
- Trim: construction segments are excluded from trim candidate lists .
- maxSideLength: mesh hint per
-1(default) means "use the global default". Editing in Properties drop-down sends the value down throughSegment.maxSideLength; the mesher reads it during triangulation. No solver participation. - isConstruction: rendered faded (theme-defined dashed grey). Excluded from solver passes and trim . Toggled per-segment via context menu or en bloc via HardHat. Crossings respect layer isolation — a construction segment never crosses an active one (
commands.ts:681, 688).
7. Failure modes
- Zero-length (
|p1 − p2| < TOL_POINT):addSegmentearly-returns the unchanged scene (commands.ts:636-638). No toast, no segment. The MotorScript builtinline(p1, p1)instead throws (primitives.ts:151). - Self-snap during draw (click 2 snaps back to
n0):a.id === b.idshort-circuits before any push (commands.ts:650). Draft clears silently; no segment added. - Snap to feature-derived virtual node:
ensureNodeWithPromotionpromotes the virtual node to a real endpoint, severing itsvirtualRef. The originating Crossing/Fillet feature is invalidated and re-evaluated. - Polyline mid-chain undo:
Ctrl+Zrolls back the last committed segment but does NOT popdraft; subsequent click will dangle. Treat the chain as opaque to undo — finalize first. - Solver divergence: a degenerate constraint applied to a near-zero-length segment (e.g.,
distance = 0) returnssolverStatus.kind = 'error'with the constraint id infailedConstraintIds; the segment is rendered with a red conflict pill.
8. Figures
Figure 1: Line tool after click 1 — anchor at A and a dashed-preview rubber-band to the live cursor.
Figure 2: Line tool after click 2 — segment AB committed with both endpoint nodes appended via ensureNodeWithPromotion.
Figure 3: Auto-crossing — AB drawn across existing DE emits a virtual crossing node X at the intersection; both source segments remain unsplit in storage.
Figure 4: Active layer (solid blue) vs construction layer (faded dashed grey) — construction segments are excluded from the mesher, solver, and trim candidates.
click 1 click 2 (cursor at C)
A• A•─ ─ ─ ─ ─ ─•C (rubber-band, ghost)
cursor
committed with crossing against existing DE
A•──────────────•B A•─────•X──────•B
╲
D•──•X──•E
(X = CrossingFeature.virtualNode,
AB and DE remain whole — split at read time)9. Known bugs
10. Class API
In the current model, segments are class instances rather than plain objects.
- Class file:
app/src/lib/fea2d/model/Segment.ts - Constructor:
new Segment(snap: SegmentSnapshot)— validatesn0 !== n1(the zero-length-edge invariant), then copies fields. Node existence is not validated at the class boundary; that is a Scene-level invariant. - Snapshot type:
SegmentSnapshot(app/src/lib/fea2d/model/snapshots/Segment.ts). - Lifecycle methods:
toJSON: SegmentSnapshot— required fields always; optionalisConstruction/anchoredemitted only when truthy. Matches today'spushSegmentwriter exactly.static fromJSON(snap): Segment— round-trip rehydration; stray properties are silently dropped.clone: Segment—withPatch(this, {})structural copy.equals(other: SceneEntity): boolean— class-and-id equality.
- Geometry methods — all take a
Scenereference because the class storesn0/n1as ids, never resolved instances :length(scene: Scene): number— Euclidean length; delegates toNode.distanceTosoMath.hypotlives in exactly one place.midpoint(scene: Scene): { x, y }— chord midpoint in node-local coordinates.direction(scene: Scene): { dx, dy }— unnormalised vector fromn0ton1. Cheap (no sqrt) — use for sign / parallel / perpendicular tests.unitDirection(scene: Scene): { dx, dy }— unit direction; returns{ 0, 0 }for a coordinate-coincident pair (legal mid-drag).
n0, n1, and id are readonly because re-endpointing means delete + re-add through Scene.addSegment. Tag fields (maxSideLength, hidden, inGroup, boundaryMarker, isConstruction, anchored) are public mutable so drag / inspector write-back can edit in place.