Appearance
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,FilletIconslot in the primary toolbar. - Keyboard shortcut:
f(Canvas/index.tsx:2761). - MotorScript builtin:
fillet(corner: Point, radius: number)—executor/builtins/primitives.ts:296. Throws onradius <= 0or non-finite; throws if no node sits atcorner. - Command Palette: not yet wired.
- Context menu: not yet wired.
4. State machine
- 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).
- 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):
surfaceErrorinfo-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.
- On a node hit:
- Enter radius in the radius dialog (positive finite number).
- Preview: dialog-modal; no live canvas preview of the arc (radius is committed-or-cancelled).
- Finalize: confirm dialog → calls
applyFillet(scene, cornerId, r)(fillet.ts:798). On success aFilletFeaturelands inscene.geometry.fillets. On failure (radius too large, unsupported adjacency, wrong arity) the scene is returned unchanged and aconsole.warnis emitted; modify-paths additionally raise aCONSTRAINT_DEGENERATEtoast (fillet.ts:892-913).
5. Committed state
After finalize the following is added (all owned by the new FilletFeature — scene-model.ts:335-381):
- 1 ×
FilletFeatureinscene.geometry.filletswith{ id, sourceCornerId, radius, parents:[FilletParent,FilletParent], snapshot }. - 2 ×
Segmentinfeature.derivedSegments— the truncated parent stubs from each tangent point out to the far-end (not added toscene.geometry.segments; merged on demand viaevaluatedScene,fillet.ts:143). - 1 ×
ArcSegmentinfeature.derivedArcs— the rounded corner. - 2 ×
Nodeinfeature.derivedNodes— the tangent points. Each carriesvirtualRef: { kind: 'fillet-tangent', filletId, side }(scene-model.ts:67, set infillet.ts:1079). IDs are stored infeature.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; theirisConstructionflag 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.nodesbut is flaggedisConstruction(dashed), preserved as the parametric anchor. - No constraints are auto-emitted. The tangency is structural (re-derived every eval), not solver-mediated.
- MotorScript codegen:
codegenFilletwritesfillet(<pName>, <radius>)— side-effect statement, no binding (codegen.ts:151-166).
6. Constraints / interactions
| Constraint | Behavior on a fillet |
|---|---|
| any geometric constraint on the derived arc / sub-segments | rejected — 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 parents | rejected — lockedConstructionTargets blocks dimensional + geometric attachment while the fillet exists |
| constraint on the source corner node | allowed — 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 node | allowed — far-end moves trigger reEvaluateFillets (fillet.ts:1275) |
Interactions with other features:
- Drag of corner / far-end:
endDragTransactionruns the deferredreEvaluateFillets(Canvas/index.tsx:1977). Stale-detection comparessnapshot.corner/farEnd1/farEnd2against current positions; mismatched features tear down derived data, un-flag the corner, re-emit the parent stubs at moved positions, and re-runapplyFillet(fillet.ts:2071-2108). Tangent node IDs are preserved across re-eval — any node promoted toscene.nodeswith matchingvirtualRef.filletIdis 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 whosesourceCornerId ∈ exp.nodeIds, locates the cloned corner viaTOL_POINTproximity match, and callsapplyFilleton the copy with the source radius. This is the reference pattern for 'srect/polygonreplication. A partial selection (only one parent cloned) cleanly degrades to no fillet becauseapplyFilletreturns the scene unchanged whenprimAtCornerdoesn'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 viafilletHighlight(Canvas/index.tsx:327-345).
7. Failure modes
- Radius too large (does not fit between adjacent primitives):
findFilletSolutionreturns null.applyFilletno-ops withconsole.warnlisting arm lengths (fillet.ts:875-886). In the MODIFY path (corner already filleted, orexistingFeatureIdpassed) the previous fillet stays in place and aCONSTRAINT_DEGENERATEtoast 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); directapplyFilletcall 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.warnnaming 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:
findFilletSolutionfails (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.Figure 1: BEFORE — sharp corner at node C where segments A and B meet at a right angle.
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' }.
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.
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)— validatesradius > 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 concreteSegmentParent/ArcParentsubclasses. - Lifecycle methods:
toJSON: FilletFeatureSnapshot— collects all owned derived primitives' snapshots.static fromJSON(snap, scene?): FilletFeature— rehydrates parents throughFilletParent.fromJSON.clone: FilletFeatureequals(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 storedsnapshot: FilletEvalSnapshot. (Stubbedfalsein phases 3–6; lights it up alongsideScene.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.