Skip to content

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 isConstruction flag).
  • 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 shortcutL for line (Canvas/index.tsx:2757). Polyline has no shortcut as of this writing — not yet wired.
  • MotorScript builtinline(p1, p2) (executor/builtins/primitives.ts:134); polyline is sugar over repeated line calls — no dedicated builtin.
  • Command Palettenot yet wired.
  • Context menu — on a committed segment: "Toggle construction", "Anchor", "Delete", "Set max side length". Construction toggle flips isConstruction and is also driven en bloc by the HardHat toolbar button.

4. State machine

Line tool (activeTool === "line")

  1. Click 1 — pick n0. Snaps to existing node / feature-derived point via ensureNodeWithPromotion (commands.ts:646). Stored in draft[0].
    • Preview after step 1: ghost dot at n0; rubber-band line from n0 to live cursor; readout shows dx, dy, length.
    • Cancel: Esc, right-click, or switching to another tool — clears draft.
  2. Click 2 — pick n1. Same snap rules. Routes through addLineWithSnap (Canvas/index.tsx:2263) which forwards to addSegment (commands.ts:621).
    • Preview after step 2: none — segment is committed; draft clears.
  3. Finalize — implicit on click 2. Tool stays active for the next segment (independent start).

Polyline tool (activeTool === "polyline")

  1. Click 1 — pick first vertex. Stored as draft[0]; polylineLastNodeIdRef set to that node.
    • Preview after step 1: rubber-band from draft[0] to cursor.
  2. Click N (N ≥ 2) — pick next vertex. Emits a segment from draft[N-2]draft[N-1] via addPolylineWithSnap (Canvas/index.tsx:2319). draft grows; polylineLastNodeIdRef updates 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 — draft clears, already-committed segments are kept.
  3. FinalizeEnter (Canvas/index.tsx:2871), right-click, or Esc. 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 fresh id, the resolved n0/n1, maxSideLength: -1 (auto), hidden: false, inGroup: 0, boundaryMarker: null, isConstruction: true iff the active layer was construction.
  • Zero or more Node entries created for endpoints that didn't pre-exist (via ensureNodeWithPromotion).
  • Zero or more Node + CrossingFeature pairs 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) writes
    p17 = point(0, 0)
    p18 = point(10, 0)
    ln5 = line(p17, p18)
    Polyline emits one 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

ConstraintBehavior on this primitive
horizontallocks y(n0) = y(n1).
verticallocks x(n0) = x(n1).
parallelbetween two segments; locks direction vectors collinear.
perpendicularbetween two segments; direction vectors orthogonal.
equal-lengthbetween two segments; chord lengths equal.
distancelength of this segment fixed to a value (dimensional).
distanceH / distanceVx- or y-projection of n0n1 fixed.
onLinea third node forced collinear with this segment.
midpointa third node forced to (n0+n1)/2.
symmetrictwo 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 through Segment.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): addSegment early-returns the unchanged scene (commands.ts:636-638). No toast, no segment. The MotorScript builtin line(p1, p1) instead throws (primitives.ts:151).
  • Self-snap during draw (click 2 snaps back to n0): a.id === b.id short-circuits before any push (commands.ts:650). Draft clears silently; no segment added.
  • Snap to feature-derived virtual node: ensureNodeWithPromotion promotes the virtual node to a real endpoint, severing its virtualRef. The originating Crossing/Fillet feature is invalidated and re-evaluated.
  • Polyline mid-chain undo: Ctrl+Z rolls back the last committed segment but does NOT pop draft; 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) returns solverStatus.kind = 'error' with the constraint id in failedConstraintIds; the segment is rendered with a red conflict pill.

8. Figures

Acursorclick 1 → preview rubber-band

Figure 1: Line tool after click 1 — anchor at A and a dashed-preview rubber-band to the live cursor.

ABclick 2 → segment committed, both endpoints persisted

Figure 2: Line tool after click 2 — segment AB committed with both endpoint nodes appended via ensureNodeWithPromotion.

DEABX (virtualRef)crossing emits a virtual node X; AB and DE stay whole

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.

activeisConstruction

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) — validates n0 !== 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; optional isConstruction / anchored emitted only when truthy. Matches today's pushSegment writer exactly.
    • static fromJSON(snap): Segment — round-trip rehydration; stray properties are silently dropped.
    • clone: SegmentwithPatch(this, {}) structural copy.
    • equals(other: SceneEntity): boolean — class-and-id equality.
  • Geometry methods — all take a Scene reference because the class stores n0 / n1 as ids, never resolved instances :
    • length(scene: Scene): number — Euclidean length; delegates to Node.distanceTo so Math.hypot lives in exactly one place.
    • midpoint(scene: Scene): { x, y } — chord midpoint in node-local coordinates.
    • direction(scene: Scene): { dx, dy } — unnormalised vector from n0 to n1. 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.

motordevs studio — geometry editor specification