Appearance
Arc
1. Identity
An Arc is a circular curve segment between two scene nodes (n0, n1) on a host circle whose centre, radius, and start angle are cached on the entity (cx, cy, radius, startAngleRad) alongside the -style signed sweep (arcLength, degrees, CCW positive).
2. When to use it
- Filleted corners, bearing seats, slot bottoms — anywhere the boundary is a circular fillet between two points.
- Pole-piece curvature on a stator/rotor profile, where the chord is known but the curve passes through a third reference point (3pt mode).
- Continuing a tangent run-off from an existing segment without breaking C1 continuity (arcTangent mode).
- As sub-arcs decomposed from a Circle when a segment crosses the circle (see
decomposedFromCircleId).
3. Inputs
- Tool button: Sketch toolbar, Arc slot. Long-press exposes the three modes (
arc3pt,arcCenterEnds,arcTangent); the legacy 2-clickarc(centre + drag radius) goes throughArcAngleDialogand is not yet wired toplanArcToolAction. - Keyboard shortcut:
Acycles the active arc mode (cross-checkCanvas/index.tsxtool dispatch around line 2716). - MotorScript builtin:
arc(p1, p2, angleDeg)andarcCtr(centre, p1, p2). Seeapp/src/lib/fea2d/executor/builtins/primitives.ts:161(arc) and:203(arcCtr).arcrejects non-finite inputs, zero sweep, and zero chord;arcCtrrejects degenerate (start ≡ end on the host circle). - Command Palette (Cmd+K): "Arc — 3 points", "Arc — centre & ends", "Arc — tangent". Not yet wired for arcTangent (
tangentSourcehint requires a canvas hit-test). - Context menu: right-click a Segment endpoint → "Start tangent arc from here" pre-selects arcTangent mode with the segment as the tangent source.
4. State machine
All three modes are dispatched by planArcToolAction in app/src/lib/fea2d/tools/arc-tool.ts:168. The function is pure — it returns an ArcToolPlan of addNode / reuseNode / addArc / addConstraint steps that the store action addArcWithMode replays. Cancel options are uniform across modes: Esc resets the click buffer; right-click aborts; selecting any other tool discards the in-flight state.
4a. Mode arc3pt — circumcircle through three clicks
1•start 3•end
╲ ╱
╲ 2 ╱
╲ •mid ╱
╲ ╱
╲_________╱ (preview after click 3: cached cx/cy/r)- Click 1 (start): free point or snap to a node. Preview after step 1: a dot at the start; rubber-band line from start to cursor.
- Click 2 (mid): free point or snap. The mid is geometry-only — it is not committed as a scene node, only used to pick the signed sweep. Preview after step 2: rubber-band arc from start through mid to cursor, recomputed every mouse move via
arcThroughThreePoints. - Click 3 (end) = Finalize: free point or snap. Plan emits
addArcwith cached{cx, cy, radius}and signedangleDeg.
Failure: collinear start/mid/end → arcThroughThreePoints returns null → noOp('arc3pt: collinear points'), friendly toast "These three points are on a straight line — pick a non-collinear middle point".
4b. Mode arcCenterEnds — centre, start, projected end
1•centre (preview after click 1: crosshair)
╲
╲ r
╲
2•start ────arc───► (preview after click 2: full circle of radius r)
╲
3•end (projected onto circle if snapped off-radius)- Click 1 (centre): free point only. The centre is NOT a scene node — it lives only in the cached
cx, cyof the resulting arc. Preview after step 1: crosshair at centre. - Click 2 (start): free or snapped node, becomes arc
n0. Radius =dist(centre, start). Preview after step 2: full preview circle of that radius and a rubber-band radial line from centre to cursor. - Click 3 (end) = Finalize: free or snapped node, becomes arc
n1. If snapped to an existing node off the (centre, start) circle, the end is projected onto the circle for the cached geometry, AND apoint-on-circleconstraint step is emitted so the solver later pins the reused node to the radius (R03 / Q-A-3 — seearc-tool.ts:267-291).
Failure: start == centre (zero radius) → noOp('arcCenterEnds: zero-radius (start == centre)').
4c. Mode arcTangent — tangent to a host edge, two clicks
────A•──────────────•B──── (existing segment AB; canvas hit-test
║ derives tangent unit vector (dx,dy))
1•anchor (on AB)
╲ tangent
╲
╲ (preview after click 1: rubber-band arc whose
╲ tangent at anchor matches AB direction)
2•end = Finalize- Click 1 (anchor): must lie on an existing segment / arc / circle. The canvas computes the tangent direction
(dx, dy)at the hit point and passes it viaArcToolHintsWithTangent.anchorTangentDx/Dy.tangentSourceN0/tangentSourceN1carry the host edge's endpoint node ids. Preview after step 1: anchor dot plus a faint tangent direction tick. - Click 2 (end) = Finalize: free or snapped.
arcTangentDirection(anchor, {dx,dy}, end)returns the unique arc throughanchorandendwhose tangent atanchormatches the source direction. Plan emitsaddArcfollowed by anaddConstraint{kind:'tangent', arcVar:'arc', lineN0, lineN1}.
Failure: end lies on the tangent line (parallel-degenerate) → noOp('arcTangent: degenerate (start == end or parallel to tangent)').
5. Committed state
After planArcToolAction returns and the store replays the plan, the scene gains:
- 0–2 new
Nodeentries (each non-snapped click resolves toaddNode; snapped clicks becomereuseNode). For arcCenterEnds the centre click adds no node. - 1 new
ArcSegment(scene-model.ts:152) with fields:id,n0,n1, signedarcLength(deg), cachedcx,cy,radius,startAngleRad,maxDegSeg(default 1), and inheritedisConstructionfrom the active layer. - For
arcTangent: onetangentConstraintrecord withentityA = lineN0,entityB = lineN1,entityC = newArcId(slvsC_ARC_LINE_TANGENT, seeconstraint-schema.ts:104,slvs-translate.ts:469-477). - For
arcCenterEndswith off-circle snapped end: oneonCircle(point-on-circle)Constraintpinningn1to the new arc's host circle. - Reverse codegen:
codegenArc(3pt and tangent →arc(p1, p2, angleDeg)) orcodegenArcCtr(centre-ends →arcCtr(centre, p1, p2)) emits one MotorScript statement. Seecodegen.ts.
6. Constraints / interactions
| Constraint | Behaviour on an arc |
|---|---|
radius / diameter | Locks radius (and arcLength-derived chord) of the host circle. See dimensional. |
equal (arc–arc) | Equates radii of two arcs. Cross-ref geometric. |
tangent (arc–line) | Slvs C_ARC_LINE_TANGENT. Emitted automatically by arcTangent mode. verified the WASM path enforces it under endpoint drag. |
tangent (arc–arc) | Slvs C_CURVE_CURVE_TANGENT. See §8 — TS fallback is failure-only. |
onCircle (point on arc's host circle) | Pins another node to radius. Emitted automatically by arcCenterEnds when the end click is snapped off-radius. |
coincident (node–node) | Standard endpoint merge; preserves cached geometry of the surviving arc. |
parallel (arc chord to segment) | Treats the arc's chord n0→n1 as the direction reference. Cross-ref geometric. |
Cached-geometry invariant: cx, cy, radius, startAngleRad are derived fields recomputed on every endpoint move (drag, solve, constraint enforcement). The cache exists so downstream ops (rendering, hit-test, intersection, region detection) read one source of truth rather than re-deriving from the chord. This is what eliminated the "180° drift" (cos(π/2) ≈ 6e-17 leaked ulp-scale error into the centre); see the ArcSegment doc-comment at scene-model.ts:159-178.
Sub-arc decomposition: when a Circle is crossed by a segment or another arc, the circle is exploded into arcs that inherit the circle's exact cx, cy, radius. Each resulting arc carries decomposedFromCircleId back-linking to the source circle so that radius / equal / tangent constraints originally written against the circle's slvs entity still resolve via the arc's slvs entity .
Interactions: arc as fillet target → see fillet. Arc crossing a segment or another arc → see crossing; the intersection materialises a virtualNodeId and a CrossingFeature in commands.ts:addArc:869-922. Full sweeps (|angle| ≥ 360° - eps) are rejected by addArc — full circles must go through addCircle (GEO-01 / GEO-24, commands.ts:805-811).
7. Failure modes
- Collinear 3pt (arc3pt):
arcThroughThreePointsreturns null → no-op plan, toast "These three points are on a straight line — pick a non-collinear middle point". No scene mutation. - Zero-radius / centre ≡ start (arcCenterEnds): no-op plan, toast "Arc would have zero radius — move the centre or the start point".
- End projected onto zero-radius circle (arcCenterEnds): caught earlier by the zero-radius guard —
endDist > TOLS.GEOMETRICis required before projection (arc-tool.ts:285). If the start equals the centre, projection is skipped and the zero-radius no-op fires. - Missing tangent source / direction (arcTangent): no-op plan, toast "Tangent arc needs a host segment — start the first click on a segment endpoint".
- Tangent-degenerate (arcTangent end on the tangent line): no-op plan, toast "Tangent arc is degenerate — the end point is on the tangent line".
- Non-finite click / tangent direction: rejected at the top of
planArcToolActionbefore any geometry math, toast "Arc rejected — one or more coordinates are not finite numbers". - Snap target node missing from scene: defensive no-op when a snapped node id resolves to nothing (race against deletion), toast "Snapped node no longer exists — try again without snapping".
- Zero chord at
addArclevel (|x1-x2|<TOL && |y1-y2|<TOL): silently returns the unchanged scene (commands.ts:801). The tool-plan layer rejects earlier with a toast; this is the last-line guard for programmatic / script callers. - |angle| ≥ 360° - eps: silent no-op at
addArc(commands.ts:811). MotorScriptarcdoes not pre-guard for this — callers writing scripts should usecirclefor full sweeps.
All failure paths are no-ops on scene.geometry; nothing is partially committed.
8. Figures
Figure 1: arc3pt mode — three clicks p1/p2/p3 produce the circumcircle arc; p2 is geometry-only and is not committed as a scene node.
Figure 2: arcCenterEnds mode — centre + start define the radius; an off-radius end click is projected onto the host circle and a violet onCircle glyph marks the auto-emitted constraint.
Figure 3: arcTangent mode — arc tangent to the existing horizontal segment AB at the contact point; violet T glyph marks the auto-emitted tangent(arc, line) constraint.
Figure 4: arc3pt collinear failure — three near-collinear clicks yield a no-op plan and the red ✗ indicator; nothing is committed to scene.geometry.
ASCII diagrams are included in §4a / §4b / §4c above for the three modes' click order.
9. Known bugs
**B- caveat ** — in the TS-fallback solver path,
tangent(arc, arc)is failure-only: the residual can flag a violation but cannot actively solve, because arc centres are not present in the TS solver's DOF state. The WASM path solves it correctly. arcTangent emitstangent(arc, line)exclusively, which both paths handle. See audit .
No arc-specific entries in beyond the shared crossing/solver surfaces above as of .
10. Class API
In the current model, arcs are class instances rather than plain objects. The class wraps the centre-based cache (cx, cy, radius, startAngleRad) plus the -parity chord-and-sweep form (n0, n1, arcLength).
- Class file:
app/src/lib/fea2d/model/ArcSegment.ts - Constructor:
new ArcSegment(id: ArcId, n0: NodeId, n1: NodeId, arcLength: number, cache: ArcCache, opts?: ArcSegmentOptions)— validates endpoints distinct, finitearcLength,|arcLength| ∈ (TOL.GEOMETRIC, 360 − TOL.GEOMETRIC), finite cache,radius > 0. - Snapshot type:
ArcSegmentSnapshot(app/src/lib/fea2d/model/snapshots/Arc.ts). - Lifecycle methods:
toJSON: ArcSegmentSnapshot— required fields plus optionalisConstruction/anchored/decomposedFromCircleIdwhen departing from defaults.static fromJSON(snap): ArcSegment— re-hydrates through the same multi-arg constructor.clone: ArcSegment—ArcSegment.fromJSON(this.toJSON)structural copy with same id.equals(other: SceneEntity): boolean— class-and-id equality.
- Geometry methods:
center: { x, y }— cached centre as a plain point.sweepAngle: number— signed sweep in radians (CCW+). The class stores degrees for parity; this is the conversion seam.pointAt(t: number): { x, y }— point on the arc at parametert(t = 0isn0,t = 1isn1). No clamping.midpoint: { x, y }— point on the arc att = 0.5(mid-sweep, lies ON the arc — distinct from the chord midpoint).
id, n0, n1, arcLength are readonly: changing any of them implies new cached geometry, so the in-tree mutators implement edits as delete + re-add through pushArc. The cache fields (cx, cy, radius, startAngleRad) are public mutable so the solver write-back path can update them in place.