Appearance
Circle
1. Identity
A Circle is a standalone closed-loop primitive stored as (cx, cy, radius) with no graph nodes, until another primitive crosses it and forces lazy decomposition into arcs (see scene-model.ts:288-314).
2. When to use it
- Stator/rotor bore outlines, shaft outlines, magnet housings — the canonical closed circular loop.
- Construction circles used as
tangent/onCircletargets for downstream geometry. - Bolt-hole / cooling-channel boundaries where the loop is referenced by
boundaryMarker. - Any case where a clean ring with zero ghost nodes is preferred over a 2-arc workaround.
3. Inputs
- Tool button: Sketch toolbar, "Circle" slot.
- Keyboard shortcut: not yet wired in
Canvas/index.tsxtool dispatch. - MotorScript builtin:
circle(ctr: Point, radius: number) → CircleRef(executor/builtins/primitives.ts:246-275). Throws on non-finite center orradius <= 0. - Command Palette: not yet wired.
- Context menu: not yet wired.
- Exact-coords dialog: right-click during the tool opens the unified four-field circle dialog (cx, cy, r, mesh) —
Canvas/index.tsx:2809-2823.
4. State machine
- Click 1 — center: place
(cx, cy). Snap targets honoured (node, midpoint, intersection).- Preview after step 1: cursor-following ring of radius
hypot(cursor − center)rendered as a ghost circle. No node placed yet. - Cancel options: Esc clears draft; switching tools clears draft.
- Preview after step 1: cursor-following ring of radius
- Click 2 — radius point: commits radius as the distance from click 1 to click 2 (
Canvas/index.tsx:1551-1577).- Preview after step 2: none — committed.
- Cancel options: right-click before click 2 opens the exact-coords dialog instead of finalising by click.
- Finalize: auto on click 2.
addCircle(scene, cx, cy, r)runs; ifr > 1e-6the circle is added (or lazy-decomposed on crossings); otherwise aCONSTRAINT_DEGENERATEwarning is surfaced.
(preview)
. - - - .
' '
; •────────× cursor
; click1 ;
' '
' - - - '5. Committed state
- No crossings: one new
Circle { id, cx, cy, radius, maxSideLength, maxDegSeg, hidden, inGroup, boundaryMarker, isConstruction?, frameId?, anchored? }inscene.geometry.circles. Zero newNodes — this is the defining invariant. - With crossings: lazy decomposition (
commands.ts:974-1212):- Each crossing point becomes a
Node. - The new circle is laid down as
Arc[]joining those nodes around(cx, cy); each carriesdecomposedFromCircleId = <newCircleId>(commands.ts:372,scene-model.ts:202). - Any previously-standalone
Circlethat the new ring crosses is itself woken up into arcs the same way. - Crossed
Segment/Arcprimitives are split at the crossing nodes viaaddNode.
- Each crossing point becomes a
- Tangent contact only (single point with one other primitive, no transversal pair): a node is added at the tangent point but the ring is not split — see
transversalPointsvstangentPointsaccounting incommands.ts:1029-1059. - MotorScript reverse-codegen:
circle(point(cx, cy), r)statement appended (codegen.ts, function emitting CircleRef).
6. Constraints / interactions
| Constraint | Behavior on this primitive |
|---|---|
radius | Locks radius directly. Accepts circle or any of its decomposed arcs (solver resolves arc → host circle via decomposedFromCircleId, slvs-translate.ts:772-779). |
diameter | Same target resolution as radius; locks 2·radius. |
equal | Pairs this circle with another circle or arc; ties their radius. |
tangent | Accepted vs Segment, Arc, Circle. Tangency creates a single contact point — see Arc for how the contact node is materialised on each side. |
coincident (center) | The center (cx, cy) is addressable as a virtual point for coincident against a Node; pins the circle's center. |
onCircle | Other Nodes constrained to lie on this circle (` |
horizontal / vertical | N/A — no endpoints. |
parallel / perpendicular | N/A — no direction. |
Interactions:
- Fillet: a circle is not a fillet input directly; its decomposed arcs are. Reaching for a fillet against an untouched circle forces the user to first introduce a crossing.
- Crossing: handled inline by
addCircle; theCrossingfeature is not used for circle-vs-X intersections — decomposition is permanent and structural, not virtual. - Trim: trims operate on arcs, so a trim against a circle first triggers decomposition, then trims the resulting arc.
- See Arc for the post-decomposition primitive and the
decomposedFromCircleIdback-link semantics.
7. Failure modes
- Zero-radius (click 2 == click 1,
r < 1e-6):addCircleis not called.surfaceErroremits aCONSTRAINT_DEGENERATEwarning toast: "Circle requires a non-zero radius — move the cursor before clicking" (Canvas/index.tsx:1564-1572). Draft cleared; no scene mutation. - Negative / non-finite radius via MotorScript:
circlebuiltin throws"radius must be a positive finite number"(primitives.ts:255) beforeaddCircleruns. - Duplicate circle (same center + radius on the same layer, within
TOL_POINT):addCirclereturns the scene unchanged (commands.ts:999-1018). The MotorScript builtin transparently returns the existingCircleRefso downstream constraints still bind (primitives.ts:259-273). - Radius below
TOL_POINTthroughaddCircledirectly: silent no-op (commands.ts:991). User-facing surfaces (tool + dialog) wrap this with a warning; programmatic callers should pre-check. - Solver divergence with circles present: typical pattern is over-constrained
radius + equal + tangenttriples. Failing constraints highlight as red pills on the circle; the circle itself is not torn down.
8. Figures
ASCII diagram for click order (above in §4). Additional capture targets:
Figure 1: After click 1 (center placed), the preview ring expands to the live cursor position; radius equals hypot(cursor − center).
Figure 2: Committed standalone Circle — clean ring, no endpoint nodes (only the construction-cross marks the centre).
Figure 3: Lazy decomposition — a segment crossing the circle introduces two virtual nodes at the crossings, and the original circle becomes two arcs sharing those nodes.
9. Known bugs
None known. ## 10. Class API
In the current model, circles are class instances rather than plain objects.
- Class file:
app/src/lib/fea2d/model/Circle.ts - Constructor:
new Circle(id: CircleId, cx: number, cy: number, radius: number, opts?: CircleOptions)— validates finitecx/cy, finiteradius >= TOL.GEOMETRIC. - Snapshot type:
CircleSnapshot(app/src/lib/fea2d/model/snapshots/Circle.ts). - Lifecycle methods:
toJSON: CircleSnapshot— required fields plus optional flags only when departing from defaults.static fromJSON(snap): Circle— re-hydrates through the constructor.clone: Circle—Circle.fromJSON(this.toJSON)structural copy with same id.equals(other: SceneEntity): boolean— class-and-id equality.
- Geometry methods:
pointAt(angle: number): { x, y }— point on the circumference at an angular position (radians, CCW from +x).
Note: the "circle disappears when crossed" semantics (lazy decomposition into arcs) is a Scene-level state machine, not a method on this class — the class is intentionally dumb about its own mortality . Scene owns the index of decomposed-arc back-pointers via each ArcSegment.decomposedFromCircleId.
id is readonly (identity rule). Every other field is public mutable so drag / move / inspector write-back can edit in place.