Skip to content

Fillet

1. Identity

A fillet is a parametric feature that rounds the corner where two adjacent primitives (segment-segment, segment-arc, or arc-arc) meet at a shared node, owning a derived arc and two trimmed parent stubs while the original parents persist as construction-locked geometry.

2. When to use it

  • Soften a stator-slot corner so flux doesn't concentrate at a sharp re-entrant.
  • Round the lamination outer profile before exporting DXF.
  • Build a tear-drop slot opening: fillet the two corners where the opening lips meet the slot body.
  • Parametrically sweep radius (e.g. via MotorScript variable) and watch the corner re-fit.

3. Inputs

  • Tool button: CanvasToolbar.tsx:563, FilletIcon slot in the primary toolbar.
  • Keyboard shortcut: f (Canvas/index.tsx:2761).
  • MotorScript builtin: fillet(corner: Point, radius: number)executor/builtins/primitives.ts:296. Throws on radius <= 0 or non-finite; throws if no node sits at corner.
  • Command Palette: not yet wired.
  • Context menu: not yet wired.

4. State machine

  1. Action: activate Fillet tool (toolbar / f). Cursor enters fillet-targeting mode; non-primary mouse buttons are ignored (Canvas/index.tsx:1648, K04 fix).
    • Preview: no preview yet; tool waits for a corner click.
    • Cancel: Esc clears tool state and filletPending (Canvas/index.tsx:2897); selecting another tool also clears (Canvas/index.tsx:468).
  2. Click 1: primary-button click on a node where exactly two segment/arc primitives meet.
    • On a node hit: setFilletPending({ nodeId }) (Canvas/index.tsx:1652).
    • On any other hit (segment body, empty space, spline-CP): surfaceError info-level toast — "Fillet expects a corner node — click where two segments meet" (Canvas/index.tsx:1658). No state change.
    • Preview after click: the radius dialog opens anchored on the chosen corner.
    • Cancel: dialog dismiss / Esc clears filletPending.
  3. Enter radius in the radius dialog (positive finite number).
    • Preview: dialog-modal; no live canvas preview of the arc (radius is committed-or-cancelled).
  4. Finalize: confirm dialog → calls applyFillet(scene, cornerId, r) (fillet.ts:798). On success a FilletFeature lands in scene.geometry.fillets. On failure (radius too large, unsupported adjacency, wrong arity) the scene is returned unchanged and a console.warn is emitted; modify-paths additionally raise a CONSTRAINT_DEGENERATE toast (fillet.ts:892-913).

5. Committed state

After finalize the following is added (all owned by the new FilletFeaturescene-model.ts:335-381):

  • 1 × FilletFeature in scene.geometry.fillets with { id, sourceCornerId, radius, parents:[FilletParent,FilletParent], snapshot }.
  • 2 × Segment in feature.derivedSegments — the truncated parent stubs from each tangent point out to the far-end (not added to scene.geometry.segments; merged on demand via evaluatedScene, fillet.ts:143).
  • 1 × ArcSegment in feature.derivedArcs — the rounded corner.
  • 2 × Node in feature.derivedNodes — the tangent points. Each carries virtualRef: { kind: 'fillet-tangent', filletId, side } (scene-model.ts:67, set in fillet.ts:1079). IDs are stored in feature.tangentNodeIds: [string, string] and remain stable across re-evaluations so promoted nodes survive radius edits.
  • Original parent segment/arc IDs are pushed into feature.constructionSegIds / constructionArcIds; their isConstruction flag is set true and locked for the feature's lifetime (lockedConstructionTargets, fillet.ts:2055). They render dashed.
  • The source corner node remains in scene.geometry.nodes but is flagged isConstruction (dashed), preserved as the parametric anchor.
  • No constraints are auto-emitted. The tangency is structural (re-derived every eval), not solver-mediated.
  • MotorScript codegen: codegenFillet writes fillet(<pName>, <radius>) — side-effect statement, no binding (codegen.ts:151-166).

6. Constraints / interactions

ConstraintBehavior on a fillet
any geometric constraint on the derived arc / sub-segmentsrejected — these primitives are not in scene.geometry.{segments,arcs} and have no IDs in solver scope
any constraint on a tangent node (virtualRef.kind === 'fillet-tangent')rejected per — the feature owns the tangent point; user-added constraints would race with re-evaluation
any constraint on the locked construction parentsrejected — lockedConstructionTargets blocks dimensional + geometric attachment while the fillet exists
constraint on the source corner nodeallowed — the corner is the parametric input; constraining it (coincident, distance, smart-dim) re-shapes the fillet via downstream re-evaluation
constraint on the parent's far-end nodeallowed — far-end moves trigger reEvaluateFillets (fillet.ts:1275)

Interactions with other features:

  • Drag of corner / far-end: endDragTransaction runs the deferred reEvaluateFillets (Canvas/index.tsx:1977). Stale-detection compares snapshot.corner / farEnd1 / farEnd2 against current positions; mismatched features tear down derived data, un-flag the corner, re-emit the parent stubs at moved positions, and re-run applyFillet (fillet.ts:2071-2108). Tangent node IDs are preserved across re-eval — any node promoted to scene.nodes with matching virtualRef.filletId is updated in-place instead of replaced (fillet.ts:1025, fillet.ts:813).
  • Re-fillet at the same corner: corner-collision detection (fillet.ts:846-851) rewrites the second call into a modify on the existing feature — no stacked duplicates.
  • Copy / Mirror / Array : replicateFillets (transforms.ts:1079) walks source fillets whose sourceCornerId ∈ exp.nodeIds, locates the cloned corner via TOL_POINT proximity match, and calls applyFillet on the copy with the source radius. This is the reference pattern for 's rect / polygon replication. A partial selection (only one parent cloned) cleanly degrades to no fillet because applyFillet returns the scene unchanged when primAtCorner doesn't find two adjacencies.
  • Delete: deleteFillet (fillet.ts:2202) restores the parents to active layer (un-locks construction flag), tears down derived data, freezes orphaned tangent virtuals to their last position (freezeOrphanedVirtuals, fillet.ts:1991).
  • Hit-test / selection: a fillet appears as { kind: 'fillet', id } in hit candidates anchored on the source corner (Canvas/index.tsx:847-856); Alt-click cycles to it. Selection highlights the derived arc + sub-segments via filletHighlight (Canvas/index.tsx:327-345).

7. Failure modes

  • Radius too large (does not fit between adjacent primitives): findFilletSolution returns null. applyFillet no-ops with console.warn listing arm lengths (fillet.ts:875-886). In the MODIFY path (corner already filleted, or existingFeatureId passed) the previous fillet stays in place and a CONSTRAINT_DEGENERATE toast surfaces — "Fillet radius too large — keeping the previous radius at this corner." (fillet.ts:897-913).
  • Zero / negative / non-finite radius: tool path is gated by the dialog; MotorScript builtin throws fillet: radius must be a positive finite number (primitives.ts:303); direct applyFillet call returns scene unchanged (fillet.ts:819).
  • Wrong adjacency count (1 or 3+ primitives at corner): console.warn "expected exactly 2 adjacent primitives" (fillet.ts:867-871). No-op.
  • Unsupported adjacency kind (spline / circle at the corner): console.warn naming each offending primitive (fillet.ts:859-865). No-op.
  • Interior spline CP node : rejected with explicit warn — interior CPs aren't graph nodes (fillet.ts:827-834).
  • Click misses corner node (segment body / empty canvas): tool surfaces an info toast and remains armed (Canvas/index.tsx:1658-1663).
  • Co-linear arms: findFilletSolution fails (degenerate centre); same code path as radius-too-large.
  • Re-eval cycle: fixed-point loop bounded at fillets.length + 1; if it doesn't settle, console.warn "did not settle in N passes; possible cycle" (fillet.ts:1312-1316).

8. Figures

   Before fillet (sharp corner)         After fillet (rounded, derived arc)

           B (far-end 2)                        B
           •                                    •
            \                                    \
             \  parent[1]                         \ derived sub-segment 1
              \  (segment)                         \  (B → t2)
               \                                    \
                \                                  t2•─╮
                 \                                      ╲
                  •C  source corner                      ╲ derived arc
                 /   (post-fillet: isConstruction,        ╲  (centre = filletCentre,
                /     virtual-locked)                      ╲   r = feature.radius)
               /                                            ╲
              /  parent[0]                                   •t1
             /   (segment)                                   /
            /                                               / derived sub-segment 0
           •                                               /  (t1 → A)
           A (far-end 1)                                  •A

   Originals (A—C, C—B) persist as              t1, t2 carry
   isConstruction=true (dashed, locked).        virtualRef:{ kind:'fillet-tangent',
   The source corner C stays in scene.nodes      filletId, side }
   as the parametric anchor.
AC (corner)Bsegment Asegment B

Figure 1: BEFORE — sharp corner at node C where segments A and B meet at a right angle.

ABtangent point t1tangent point t2arc

Figure 2: AFTER fillet applied. Segments A and B are truncated; a derived arc joins them with two tangent points (amber) carrying virtualRef: { kind: 'fillet-tangent' }.

ABC (locked, dashed)parent (construction)parent (construction)derived (solid)

Figure 3: Parent flag — the original A and B segments persist as construction-locked geometry (dashed, dimmed); only the derived sub-segments and arc are drawn solid on the active layer.

A'(old A)Bre-fitted arc

Figure 4: Re-eval on drag — after A's endpoint moves from (0,0) to (−5,0), reEvaluateFillets re-fits the arc; tangent node IDs are preserved, the arc adapts in radius/position.

9. Known bugs

None known. > Indirectly relevant: the virtualRef promotion-on-drag pathology surfaced on crossings shares code shape with fillet tangent-node re-evaluation; if a similar regression appears for fillets it would manifest as a tangent node losing its virtualRef after a corner drag.

10. Class API

In the current model, fillets are class instances. Unlike RectFeature / PolygonFeature, FilletFeature owns its derived primitives directly — the trim stubs and the fillet arc are NOT in scene.segments / scene.arcs, they live on the feature record and are merged into the evaluated view at read time.

  • Class file: app/src/lib/fea2d/model/FilletFeature.ts
  • Constructor: new FilletFeature(id, sourceCornerId, radius, parents: [FilletParent, FilletParent], derived: FilletDerived, tangentNodeIds: [NodeId, NodeId], constructionSegIds, constructionArcIds, snapshot: FilletEvalSnapshot) — validates radius > 0.
  • Snapshot type: FilletFeatureSnapshot (app/src/lib/fea2d/model/snapshots/Fillet.ts).
  • Class layer used: FilletParent (app/src/lib/fea2d/model/FilletParent.ts) — the abstract parent ref with concrete SegmentParent / ArcParent subclasses.
  • Lifecycle methods:
    • toJSON: FilletFeatureSnapshot — collects all owned derived primitives' snapshots.
    • static fromJSON(snap, scene?): FilletFeature — rehydrates parents through FilletParent.fromJSON.
    • clone: FilletFeature
    • equals(other: SceneEntity): boolean
  • Polymorphic surface (SceneFeature):
    • referencesEntity(id: string): boolean — checks the source corner id, parent primitive ids, tangent node ids, construction-locked ids, and every owned derived primitive id.
    • isStale(_scene: unknown): boolean — phase-7 implementation walks the tracked corner + far-end positions and compares to the stored snapshot: FilletEvalSnapshot. (Stubbed false in phases 3–6; lights it up alongside Scene.computeFillets.)

id, sourceCornerId, tangentNodeIds, constructionSegIds, constructionArcIds are readonly. radius is mutable so the inspector can edit it in place; snapshot is mutable because phase-7's reEvaluate swaps it atomically.

motordevs studio — geometry editor specification