Skip to content

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 / onCircle targets 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.tsx tool dispatch.
  • MotorScript builtin: circle(ctr: Point, radius: number) → CircleRef (executor/builtins/primitives.ts:246-275). Throws on non-finite center or radius <= 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

  1. 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.
  2. 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.
  3. Finalize: auto on click 2. addCircle(scene, cx, cy, r) runs; if r > 1e-6 the circle is added (or lazy-decomposed on crossings); otherwise a CONSTRAINT_DEGENERATE warning 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? } in scene.geometry.circles. Zero new Nodes — 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 carries decomposedFromCircleId = <newCircleId> (commands.ts:372, scene-model.ts:202).
    • Any previously-standalone Circle that the new ring crosses is itself woken up into arcs the same way.
    • Crossed Segment / Arc primitives are split at the crossing nodes via addNode.
  • 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 transversalPoints vs tangentPoints accounting in commands.ts:1029-1059.
  • MotorScript reverse-codegen: circle(point(cx, cy), r) statement appended (codegen.ts, function emitting CircleRef).

6. Constraints / interactions

ConstraintBehavior on this primitive
radiusLocks radius directly. Accepts circle or any of its decomposed arcs (solver resolves arc → host circle via decomposedFromCircleId, slvs-translate.ts:772-779).
diameterSame target resolution as radius; locks 2·radius.
equalPairs this circle with another circle or arc; ties their radius.
tangentAccepted 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.
onCircleOther Nodes constrained to lie on this circle (`
horizontal / verticalN/A — no endpoints.
parallel / perpendicularN/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; the Crossing feature 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 decomposedFromCircleId back-link semantics.

7. Failure modes

  • Zero-radius (click 2 == click 1, r < 1e-6): addCircle is not called. surfaceError emits a CONSTRAINT_DEGENERATE warning 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: circle builtin throws "radius must be a positive finite number" (primitives.ts:255) before addCircle runs.
  • Duplicate circle (same center + radius on the same layer, within TOL_POINT): addCircle returns the scene unchanged (commands.ts:999-1018). The MotorScript builtin transparently returns the existing CircleRef so downstream constraints still bind (primitives.ts:259-273).
  • Radius below TOL_POINT through addCircle directly: 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 + tangent triples. 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:

centercursorr = |cursor − center|

Figure 1: After click 1 (center placed), the preview ring expands to the live cursor position; radius equals hypot(cursor − center).

(cx, cy)Circle { cx, cy, radius } — zero graph nodes

Figure 2: Committed standalone Circle — clean ring, no endpoint nodes (only the construction-cross marks the centre).

arc 1arc 2n_x0n_x1decomposedFromCircleId = <origId>

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 finite cx / cy, finite radius >= 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: CircleCircle.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.

motordevs studio — geometry editor specification