diff --git a/okf/decisions/concave-edge-classifier-can-select-wrong-edges.md b/okf/decisions/concave-edge-classifier-can-select-wrong-edges.md
new file mode 100644
index 0000000..004ef7b
--- /dev/null
+++ b/okf/decisions/concave-edge-classifier-can-select-wrong-edges.md
@@ -0,0 +1,121 @@
+---
+type: decision
+title: Shape.concaveEdges() can return the wrong edges entirely, not just a threshold quirk
+description: On OCCTSwift 1.x, concaveEdges() on an extruded L-profile returns two top-cap boundary edges rather than the one true reentrant edge, and classifies that edge convex. Fixed in the 2.0.0 line. Select fillet/chamfer edges geometrically while on 1.x.
+resource: https://github.com/SecondMouseAU/OCCTSwiftScripts/issues/105
+tags: [decision, occtswift, fillet, topology, recipes, edge-selection]
+timestamp: 2026-08-05
+---
+
+# Decision
+
+On the OCCTSwift 1.x line, do not trust `Shape.concaveEdges()` / `Shape.convexEdges()` to
+find the edge you mean to fillet or chamfer, even when you can name the one edge you expect
+geometrically. Verify by
+inspecting the returned edges' actual positions, and prefer `Shape.edges(where:)` with an
+explicit geometric predicate once you know what you are looking for.
+
+# Version scope: 1.x only, fixed in 2.0.0
+
+This is an **OCCTSwift 1.x defect**. It is already fixed in the 2.0.0 line, verified against the
+published `v2.0.0-kernel.1` prerelease with the same repro:
+
+```
+1.17.0 L-prism concave=2 (expected 1) insideCorner inConcave=false MISMATCH
+2.0.0-kernel.1 L-prism concave=1 (expected 1) insideCorner inConcave=true OK
+```
+
+A T-prism with two reentrant edges reports 3 on 1.17.0 and 2 on 2.0.0-kernel.1. A box, having no
+reentrant edges, is correct on both.
+
+The fix was too involved to backport to the 1.x line, so it is carried by the 2.0.0 refactor.
+Raised upstream as [OCCTSwift#695](https://github.com/SecondMouseAU/OCCTSwift/issues/695).
+
+**So the geometric selection below is a 1.x workaround with a known end date.** When this repo
+moves to the 2.0.0 line, `concaveEdges()` becomes usable for this case again, and recipe 01 could
+return to it. That would be a legitimate simplification rather than a regression. Re-run the
+repro above before relying on it, rather than assuming the migration carried the fix.
+
+# Why
+
+Recipe 01's L-bracket profile has exactly one reentrant vertex, at `(thickness, thickness)`.
+Extruded along Z, that vertex produces exactly one concave edge: a line parallel to the
+extrusion axis, running its full length.
+
+`prism.concaveEdges()` does not return that edge. It returns two different edges instead,
+each length `legLength - thickness` (45 mm on the recipe's own parameters), lying in the
+*end cap* plane (`z = width`) rather than running the extrusion axis:
+
+```
+edge[7] (the true reentrant edge) isLine, len=40 (=width), bounds x:[5,5] y:[5,5] z:[0,40] -> classified CONVEX
+edge[9] (concaveEdges() result #1) isLine, len=45, bounds x:[5,50] y:[5,5] z:[40,40] -> classified CONCAVE
+edge[12] (concaveEdges() result #2) isLine, len=45, bounds x:[5,5] y:[5,50] z:[40,40] -> classified CONCAVE
+```
+
+`edge[7]` is exactly where the reentrant vertex's profile predicts a concave edge should be.
+It is classified **convex**. The two edges classified concave are boundary segments of the
+top cap, at the far end of the part from where the reentrant corner's own edge runs.
+
+This is not a rounding or threshold problem (`concaveEdges(angle:)`'s default tolerance is
+irrelevant here): the classifier is naming structurally different edges than the geometric
+feature it is supposed to describe.
+
+## Consequence: the wrong edges have the wrong feasible radius
+
+The two edges `concaveEdges()` returns are bounded by the 5 mm leg thickness (they sit on
+the boundary between the end cap and a wall that is only 5 mm across), so filleting them
+fails above roughly that radius. The true edge (`edge[7]`) is bounded only by
+`legLength - thickness` (45 mm here), since it runs along the full unconstrained length of
+each wall. A radius that is completely reasonable for the real feature (8 mm, comfortably
+under 45 mm) reads as infeasible when applied to the wrong edges (over the 5 mm limit), and
+`prism.filleted(edges:radius:)` returns `nil`. That nil, hidden behind a `?? prism`
+fallback, is what recipe 01 shipped for as long as it trusted `concaveEdges()`
+([OCCTSwiftScripts#105](https://github.com/SecondMouseAU/OCCTSwiftScripts/issues/105)).
+
+## A concave fillet adds material; verify the sign, not just a nonzero delta
+
+Filleting the true reentrant edge **increases** the bracket's volume: it fills part of the
+sharp inside corner with a rounded blend. `prism.filleted(edges: [edge7], radius: r).volume`
+is `prism.volume + r² · (1 - π/4) · length`, not minus. Filleting the two edges
+`concaveEdges()` actually returns *removes* material instead, because those two are
+ordinary 90-degree corners from the fillet's point of view (the same
+`r² · (1 - π/4) · length` formula, sign flipped): measured 176.42 mm3 removed at r=3 mm
+across both wrong edges (2 × 45 mm), against 77.26 mm3 that a single correct fillet at r=3
+mm over the 40 mm extrusion length would add. A volume check that only asserts "some
+material moved" would have passed on the wrong edges; the sign and the magnitude both have
+to match the specific edge you intended.
+
+# How to select instead
+
+Once you know the geometric feature you want (here: a line parallel to the extrusion axis,
+positioned at the profile's reentrant vertex), select it directly rather than filtering a
+classifier's output:
+
+```swift
+let insideCorner = prism.edges { edge in
+ guard edge.isLine else { return false }
+ let b = edge.bounds
+ let runsFullWidth = abs((b.max.z - b.min.z) - width) < 1e-6
+ && abs(b.max.x - b.min.x) < 1e-6 && abs(b.max.y - b.min.y) < 1e-6
+ guard runsFullWidth else { return false }
+ return abs(b.min.x - thickness) < 1e-6 && abs(b.min.y - thickness) < 1e-6
+}
+```
+
+# Related
+
+[Revolve seams cannot be chamfered](revolve-seams-cannot-be-chamfered.md) already noted, on
+the pipe flange (#103/#104), that `convexEdges()` / `concaveEdges()` disagreed with what
+chamfering the edges actually did volumetrically. This is the same finding on a second,
+unrelated shape, which is why it is worth its own entry rather than a footnote: two
+independent recipes have now hit a classifier/reality mismatch on `concaveEdges()` and
+`convexEdges()`. Treat both as a hint, to be checked against the shape's actual geometry and
+the operation's actual volumetric effect, not as ground truth.
+
+This is also the third instance of a broader pattern tracked across #100, #103, and #105: an
+optional-returning geometry operation degrades silently via a `?? fallback`, the emitted
+output stays volumetrically plausible, and the docs keep describing the intended behaviour.
+A fourth, dormant instance (`recipes/06-fan-blade`'s `blade.union(hub) ?? blade`, which has
+never actually failed) was fixed alongside #105 once the audit turned it up. Any new
+`Shape`-returning call in a recipe that can return `nil` should fail loudly (force-unwrap or
+an explicit `guard ... else { fatalError(...) }`), never degrade through `??`.
diff --git a/okf/decisions/index.md b/okf/decisions/index.md
index 0e4b881..dbba3b4 100644
--- a/okf/decisions/index.md
+++ b/okf/decisions/index.md
@@ -17,3 +17,6 @@ that need standalone rationale.
called as `f || status=1` must `return 1` explicitly or its checks are decorative.
* [Revolve seams cannot be chamfered](revolve-seams-cannot-be-chamfered.md): the all-edge
`chamfered(distance:)` always fails on a full revolve; select edges explicitly.
+* [Concave edge classifier can select wrong edges](concave-edge-classifier-can-select-wrong-edges.md):
+ `concaveEdges()` returned two unrelated edges instead of an L-bracket's one true reentrant
+ edge; verify a classifier's output geometrically before trusting it.
diff --git a/okf/log.md b/okf/log.md
index 4ef82e8..0624f5c 100644
--- a/okf/log.md
+++ b/okf/log.md
@@ -1,5 +1,19 @@
# Knowledge Log
+## 2026-08-05 (fix/105-bracket-fillet)
+
+* **Update**: Fixed recipe 01's inside-corner fillet, which had never applied (#105).
+ `prism.concaveEdges()` returns the wrong two edges on this shape (top-cap boundary
+ segments bounded by the 5 mm leg thickness), not the one true reentrant edge (bounded
+ only by `legLength - thickness`, 45 mm). `filletRadius = 8` was infeasible for the wrong
+ edges and a `?? prism` fallback hid the resulting `nil`. Now selects the true edge
+ geometrically with `Shape.edges(where:)`; the same `filletRadius = 8` now applies,
+ adding 549.38 mm3 (matches the analytic `r² · (1 - pi/4) · width` prediction exactly).
+ Also fixed `recipes/06-fan-blade`'s `blade.union(hub) ?? blade`, the same pattern found
+ dormant during the `??`-fallback audit #105 requested (the union has never actually
+ failed; behaviour is unchanged).
+* **Creation**: Recorded the concave-edge-classifier-can-select-wrong-edges decision.
+
## 2026-08-05
* **Update**: Fixed two cookbook recipes that emitted shells while documenting themselves as
diff --git a/recipes/01-mounting-bracket/README.md b/recipes/01-mounting-bracket/README.md
index a32747d..abe2dec 100644
--- a/recipes/01-mounting-bracket/README.md
+++ b/recipes/01-mounting-bracket/README.md
@@ -18,28 +18,45 @@ An L-shaped mounting bracket with a rounded inside corner and four through-holes
## Algorithm
The L cross-section is built as a closed polygon in the XY plane and extruded along Z into
-a prism. The inside corner is then rounded by filleting the solid's **concave edge**,
-located geometrically with `Shape.concaveEdges()`: no fragile edge-index bookkeeping, and
-it tracks the corner as parameters change. The fillet is applied *before* drilling so
-`concaveEdges()` returns only the reentrant corner. Finally four holes are cut: two through
-the base leg (drilled along Y) and two through the upright leg (drilled along X). Each drill
-starts 1 mm outside the entry face and over-runs the exit by 1 mm so the resulting cut faces
-are clean and coincident-face artifacts are avoided.
+a prism. The reentrant vertex at `(thickness, thickness)` extrudes to exactly one concave
+edge: a straight line parallel to the extrusion axis. That is the edge this recipe fillets,
+adding a rounded blend that fills part of the sharp inside corner. It is selected
+**geometrically**, with `Shape.edges(where:)`, not with `Shape.concaveEdges()`: on this
+shape that call returns a different pair of edges (see Gotchas), so the selector checks
+directly for a line edge parallel to Z sitting at `(thickness, thickness)`. This tracks the
+corner as parameters change without a fragile edge index. The fillet is applied *before*
+drilling so the selector only ever sees the corner edge, not a drilled hole's rim. Finally
+four holes are cut: two through the base leg (drilled along Y) and two through the upright
+leg (drilled along X). Each drill starts 1 mm outside the entry face and over-runs the exit
+by 1 mm so the resulting cut faces are clean and coincident-face artifacts are avoided.
## OCCTSwift APIs used
- `Wire.polygon(_:closed:)`: L-shaped cross-section
- `Shape.extrude(profile:direction:length:)`: profile → prism
-- `Shape.concaveEdges()`: find the reentrant inside-corner edge (OCCTSwift v1.3.1)
+- `Shape.edges(where:)`: select the inside-corner edge geometrically (OCCTSwift v1.2.1)
- `Shape.filleted(edges:radius:)`: round that edge
- `Shape.drilled(at:direction:radius:depth:)`: the four through-holes
-- `Shape.volume`: sanity print
+- `Shape.volume`: sanity print, and the check that the fillet actually ran
## Gotchas
-- Fillet **before** drilling: `concaveEdges()` classifies *every* concave edge, and a
- drilled hole's rim can read as concave. Filleting first keeps the selection to just the
- inside corner. (Tighten `concaveEdges(angle:)` if a near-flat junction sneaks in.)
+- **`Shape.concaveEdges()` picks the wrong edges on this shape.** It returns two edges
+ instead of one: the top-cap boundary segments at `z = width` where each wall meets the
+ end face (each running the leg length, not the extrusion width), rather than the true
+ reentrant edge. Those two are bounded by the 5 mm leg thickness, so a fillet on them fails
+ above roughly that radius; that mismatch is what let `filletRadius = 8` silently no-op
+ behind a `?? prism` fallback for as long as this recipe used `concaveEdges()`
+ (OCCTSwiftScripts #105). The true inside-corner edge has no such limit (its bound is
+ `legLength − thickness`, 45 mm here), which is why the same `filletRadius = 8` works fine
+ once the correct edge is selected.
+- **A concave fillet adds material, it does not remove it.** Rounding the inside corner
+ fills part of the sharp reentrant point with a blend, so `bracket.volume` after the
+ fillet is *larger* than the prism's, by `filletRadius² · (1 − π/4) · width`. Do not expect
+ a volume decrease as evidence the fillet ran; check the increase against that formula
+ instead.
+- Fillet **before** drilling: a drilled hole's rim sitting near the corner could otherwise
+ confuse a looser selector. Filleting first keeps the selection to just the inside corner.
- Drill start points sit *outside* the part and `depth` over-runs the thickness so the
hole punches fully through; drilling exactly on a face can leave a sliver.
- The bracket is a single solid emitted as `body-0` (the reference `output.brep`).
diff --git a/recipes/01-mounting-bracket/main.swift b/recipes/01-mounting-bracket/main.swift
index 08d5027..ebf3cbc 100644
--- a/recipes/01-mounting-bracket/main.swift
+++ b/recipes/01-mounting-bracket/main.swift
@@ -3,10 +3,21 @@
// Inputs: none (edit the parameter block below)
// Outputs: one solid body: an L-bracket with a filleted inside corner and four
// through-holes (two per leg).
-// Notes: The inside corner is rounded by filleting the solid's concave edge, found
-// geometrically with Shape.concaveEdges() (OCCTSwift v1.3.1, #171) rather than
-// by a fragile edge index. Fillet before drilling so concaveEdges() returns only
-// the reentrant corner. Holes are drilled through the leg thickness with a small
+// Notes: The reentrant corner at (thickness, thickness) extrudes to exactly one
+// concave edge, a straight line parallel to the extrusion axis. That is the
+// edge this recipe fillets. It is NOT the edge `Shape.concaveEdges()` finds:
+// on this shape that call returns two different edges instead, the top-cap
+// boundary segments at z = width where each wall meets the end face, each
+// running the leg length rather than the extrusion width (OCCTSwiftScripts
+// #105). Those two are bounded by the 5 mm leg thickness and a fillet there
+// fails above roughly that radius, which is why `filletRadius = 8` used to
+// silently no-op behind a `?? prism` fallback. The true inside-corner edge has
+// no such limit (its bound is legLength − thickness, 45 mm here), so this
+// recipe selects it explicitly with `Shape.edges(where:)`: a line parallel to
+// the extrusion axis positioned at (thickness, thickness), the same
+// geometric-selection approach recipe 03 uses for the pipe flange (#103).
+// Fillet before drilling so the selector only ever sees the corner edge, not a
+// drilled hole's rim. Holes are drilled through the leg thickness with a small
// overshoot so the cut faces stay clean.
//
// Run: swift run occtkit run recipes/01-mounting-bracket/main.swift --format brep
@@ -18,7 +29,8 @@ import ScriptHarness
let legLength: Double = 50 // length of each leg, measured from the heel (mm)
let thickness: Double = 5 // material thickness of each leg (mm)
let width: Double = 40 // bracket width (extrusion depth, mm)
-let filletRadius: Double = 8 // inside-corner radius (mm)
+let filletRadius: Double = 8 // inside-corner radius (mm); fits comfortably under the
+ // legLength − thickness = 45 mm geometric limit (see below)
let holeRadius: Double = 3.5 // mounting-hole radius (mm)
let ctx = ScriptContext(metadata: ManifestMetadata(
@@ -37,7 +49,29 @@ let lProfile = Wire.polygon([
// ── Extrude to a solid prism, then round the concave (inside-corner) edge ─────
let prism = Shape.extrude(profile: lProfile, direction: SIMD3(0, 0, 1), length: width)!
-var bracket = prism.filleted(edges: prism.concaveEdges(), radius: filletRadius) ?? prism
+
+// The inside corner is the one straight edge parallel to the extrusion axis (Z) that
+// sits at (thickness, thickness): select it geometrically rather than trusting
+// concaveEdges(), which picks the wrong edges on this shape (see the header note).
+// That classifier defect is OCCTSwift 1.x only: it is fixed in the 2.0.0 line
+// (verified on v2.0.0-kernel.1, upstream OCCTSwift#695). This geometric selection is
+// therefore a 1.x workaround, and this recipe could return to concaveEdges() once the
+// package moves to 2.0.0. Re-run the check in the OKF entry before doing so.
+let insideCornerEdges = prism.edges { edge in
+ guard edge.isLine else { return false }
+ let b = edge.bounds
+ let runsFullWidth = abs((b.max.z - b.min.z) - width) < 1e-6
+ && abs(b.max.x - b.min.x) < 1e-6 && abs(b.max.y - b.min.y) < 1e-6
+ guard runsFullWidth else { return false }
+ return abs(b.min.x - thickness) < 1e-6 && abs(b.min.y - thickness) < 1e-6
+}
+// Guard the selector separately from the fillet. `filleted(edges: [], radius:)` does
+// return nil today (checked on both 1.17.0 and 2.0.0-kernel.1), so the force-unwrap
+// below would catch an empty match, but only as an anonymous nil-unwrap crash. This
+// names the actual fault, and avoids depending on undocumented nil-on-empty behaviour
+// if a parameter change or an upstream tweak ever silently breaks the predicate.
+guard !insideCornerEdges.isEmpty else { fatalError("inside-corner edge selector matched nothing") }
+var bracket = prism.filleted(edges: insideCornerEdges, radius: filletRadius)!
// ── Four through-holes: two in the base leg (drill along Y), two in the upright
// leg (drill along X). Start just outside the entry face and over-run the exit.
diff --git a/recipes/01-mounting-bracket/output.brep b/recipes/01-mounting-bracket/output.brep
index 44253a0..2a15ead 100644
--- a/recipes/01-mounting-bracket/output.brep
+++ b/recipes/01-mounting-bracket/output.brep
@@ -7,136 +7,163 @@ Locations 2
0 1 0 0
0 0 1 40
2 1 -1 0
-Curve2ds 40
-1 0 0 0 -1
-1 50 0 0 -1
-1 0 0 1 0
+Curve2ds 46
+1 0 -0 1 0
+1 0 0 0 1
+1 37 0 0 -1
+1 0 -0 0 -1
1 0 -40 1 0
-1 50 0 0 -1
-2 27.500000000000004 -28 0 -1 1 0 3.5
-1 0 1 1 0
-2 27.500000000000004 -12 0 -1 1 0 3.5
-1 0 1 1 0
-1 0 0 0 -1
+2 22.499999999999996 -28 0 -1 -1 -0 3.5
+1 0 6 1 0
+2 22.499999999999996 -12 0 -1 -1 -0 3.5
+1 0 6 1 0
+1 0 0 1 0
+2 -1.8750000000000018 -1.8750000000000018 0 -1 -1 -0 8
+1 0 0 1 0
1 0 0 1 0
-1 0 -40 1 0
-2 22.499999999999996 -28 0 -1 1 0 3.5
-1 0 1 1 0
-2 22.499999999999996 -12 0 -1 1 0 3.5
-1 0 1 1 0
-1 0 -0 1 0
1 0 -40 1 0
1 0 0 1 0
1 0 -40 1 0
-1 0 -0 0 -1
-2 22.499999999999996 -28 0 -1 -1 -0 3.5
-1 0 6 1 0
+1 1.5707963267948966 0 0 1
+1 7.9999999999999982 0 0 -1
+1 0 40 1 0
+2 -1.8750000000000018 -1.8750000000000018 0 -1 -1 -0 8
+1 50 0 0 -1
+1 0 -40 1 0
1 6.2831853071795862 -0 0 1
1 0 -0 0 1
-2 22.499999999999996 -12 0 -1 -1 -0 3.5
-1 0 6 1 0
+2 27.500000000000004 -28 0 -1 1 0 3.5
+1 0 1 1 0
1 6.2831853071795862 -0 0 1
1 0 -0 0 1
+2 27.500000000000004 -12 0 -1 1 0 3.5
+1 0 1 1 0
1 45 0 0 -1
2 22.500000000000004 -28 0 -1 -1 -0 3.5
1 0 6 1 0
-1 6.2831853071795862 -0 0 1
-1 0 -0 0 1
2 22.500000000000004 -12 0 -1 -1 -0 3.5
1 0 6 1 0
+1 0 0 0 -1
+1 50 0 0 -1
+1 0 0 0 -1
+2 22.499999999999996 -28 0 -1 1 0 3.5
+1 0 1 1 0
+2 22.499999999999996 -12 0 -1 1 0 3.5
+1 0 1 1 0
1 6.2831853071795862 -0 0 1
1 0 -0 0 1
-1 45 0 0 -1
-1 0 0 0 -1
-Curves 24
-1 0 0 0 0 0 1
-1 0 0 0 1 0 0
-1 50 0 0 0 0 1
-2 27.500000000000004 0 28 0 1 0 0 -0 1 1 0 -0 3.5
-2 27.500000000000004 0 12 0 1 0 0 -0 1 1 0 -0 3.5
-1 0 50 0 0 0 1
-1 0 50 0 0 -1 0
-2 0 27.500000000000004 28 1 0 0 -0 0 1 0 -1 0 3.5
-2 0 27.500000000000004 12 1 0 0 -0 0 1 0 -1 0 3.5
-1 50 0 0 0 1 0
+1 6.2831853071795862 -0 0 1
+1 0 -0 0 1
+Curves 27
1 50 5 0 -1 0 0
-1 5 5 0 0 1 0
-1 5 50 0 -1 0 0
+1 12.999999999999998 4.9999999999999982 0 0 0 1
1 50 5 0 0 0 1
2 27.500000000000004 5 28 0 1 0 0 -0 1 1 0 -0 3.5
-1 27.500000000000004 -1 31.5 0 1 0
2 27.500000000000004 5 12 0 1 0 0 -0 1 1 0 -0 3.5
+8 0 1.5707963267948966
+2 12.999999999999998 12.999999999999998 0 -0 -0 -1 0 -1 0 -1 0 0 8
+1 50 0 0 0 1 0
+1 5 5 0 0 1 0
+1 0 0 0 1 0 0
+1 5 50 0 -1 0 0
+1 0 50 0 0 -1 0
+1 4.9999999999999982 12.999999999999998 0 0 0 1
+8 0 1.5707963267948966
+2 12.999999999999998 12.999999999999998 40 -0 -0 -1 0 -1 0 -1 0 0 8
+1 50 0 0 0 0 1
+1 27.500000000000004 -1 31.5 0 1 0
+2 27.500000000000004 0 28 0 1 0 0 -0 1 1 0 -0 3.5
1 27.500000000000004 -1 15.5 0 1 0
+2 27.500000000000004 0 12 0 1 0 0 -0 1 1 0 -0 3.5
1 5 50 0 0 0 1
2 5 27.500000000000004 28 1 0 0 -0 0 1 0 -1 0 3.5
-1 -1 27.500000000000004 31.5 1 0 0
2 5 27.500000000000004 12 1 0 0 -0 0 1 0 -1 0 3.5
+1 0 0 0 0 0 1
+1 0 50 0 0 0 1
+2 0 27.500000000000004 28 1 0 0 -0 0 1 0 -1 0 3.5
+2 0 27.500000000000004 12 1 0 0 -0 0 1 0 -1 0 3.5
+1 -1 27.500000000000004 31.5 1 0 0
1 -1 27.500000000000004 15.5 1 0 0
-1 5 5 0 0 0 1
Polygon3D 0
PolygonOnTriangulations 0
-Surfaces 11
-1 0 0 0 0 1 0 1 0 0 0 0 -1
-1 0 50 0 1 0 -0 0 -1 0 0 -0 -1
+Surfaces 12
+1 50 5 0 0 -1 0 -1 0 0 -0 -0 -1
+2 12.999999999999998 12.999999999999998 0 0 0 1 0 -1 0 -1 -0 0 8
2 27.500000000000004 -1 28 0 1 0 0 -0 1 1 0 -0 3.5
2 27.500000000000004 -1 12 0 1 0 0 -0 1 1 0 -0 3.5
-2 -1 27.500000000000004 28 1 0 0 -0 0 1 0 -1 0 3.5
-2 -1 27.500000000000004 12 1 0 0 -0 0 1 0 -1 0 3.5
1 14.875 14.875 0 0 0 1 1 0 -0 -0 1 0
-1 50 5 0 0 -1 0 -1 0 0 -0 -0 -1
1 5 5 0 -1 0 0 0 1 0 0 0 -1
+1 0 0 0 0 1 0 1 0 0 0 0 -1
+1 0 50 0 1 0 -0 0 -1 0 0 -0 -1
1 50 0 0 -1 0 0 0 1 0 0 0 -1
+2 -1 27.500000000000004 28 1 0 0 -0 0 1 0 -1 0 3.5
+2 -1 27.500000000000004 12 1 0 0 -0 0 1 0 -1 0 3.5
1 5 50 0 0 -1 0 -1 0 0 -0 -0 -1
Triangulations 0
-TShapes 71
+TShapes 83
Ve
1e-07
-0 0 0
+13 5 0
+0 0
+
+0101101
+*
+Ve
+1e-07
+50 5 0
0 0
0101101
*
Ed
1e-07 1 1 0
-1 1 0 0 40
-2 1 1 0 0 40
-2 2 2 0 0 40
+1 1 0 0 37
+2 1 1 0 0 37
0
0101000
-+71 0 -71 1 *
+-83 0 +82 0 *
Ve
1e-07
-50 0 0
+13 5 40
0 0
0101101
*
Ed
1e-07 1 1 0
-1 2 0 0 50
-2 3 1 0 0 50
-2 4 1 2 0 50
+1 2 0 -0 40
+2 2 2 0 -0 40
+2 3 1 0 -0 40
+4 G1 2 0 1 0
0
0101000
-+71 0 -69 0 *
++83 0 -80 0 *
Ed
1e-07 1 1 0
1 3 0 0 40
-2 5 1 0 0 40
+2 4 1 0 0 40
0
0101000
-+69 0 -69 1 *
++82 0 -82 1 *
+Ed
+ 1e-07 1 1 0
+1 1 0 0 37
+2 5 1 2 0 37
+0
+
+0101000
+-80 2 +82 0 *
Wi
0101100
-+70 0 -68 0 +68 1 -67 0 *
+-81 0 -79 0 +78 0 +77 1 *
Ve
1e-07
-27.5 0 31.5
+27.5 5 31.5
0 0
0101101
@@ -149,14 +176,14 @@ Ed
0
0101100
-+65 0 -65 0 *
++75 0 -75 0 *
Wi
0101100
--64 0 *
++74 0 *
Ve
1e-07
-27.5 0 15.5
+27.5 5 15.5
0 0
0101101
@@ -169,251 +196,256 @@ Ed
0
0101100
-+62 0 -62 0 *
++72 0 -72 0 *
Wi
0101100
--61 0 *
++71 0 *
Fa
0 1e-07 1 0
0101000
-+66 0 +63 0 +60 0 *
++76 0 +73 0 +70 0 *
Ve
1e-07
-0 50 0
+5 13 0
0 0
0101101
*
Ed
1e-07 1 1 0
-1 6 0 0 40
-2 10 2 0 0 40
-0
-
-0101000
-+58 0 -58 1 *
-Ed
- 1e-07 1 1 0
-1 7 0 0 50
-2 11 2 0 0 50
-2 12 2 2 0 50
+1 6 0 0 1.5707963267949
+2 10 2 0 0 1.5707963267949
+2 11 5 0 0 1.5707963267949
0
0101000
-+58 0 -71 0 *
-Wi
-
-0101100
-+57 0 -56 0 +56 1 -70 0 *
++83 0 -68 0 *
Ve
1e-07
-0 27.5 31.5
+50 0 0
0 0
0101101
*
Ed
1e-07 1 1 0
-1 8 0 0 6.28318530717959
-2 13 2 0 0 6.28318530717959
-2 14 5 0 0 6.28318530717959
+1 7 0 0 5
0
-0101100
-+54 0 -54 0 *
-Wi
-
-0101100
--53 0 *
+0101000
++66 0 -82 0 *
Ve
1e-07
-0 27.5 15.5
+5 50 0
0 0
0101101
*
Ed
1e-07 1 1 0
-1 9 0 0 6.28318530717959
-2 15 2 0 0 6.28318530717959
-2 16 6 0 0 6.28318530717959
+1 8 0 8 45
+2 12 6 0 8 45
0
-0101100
-+51 0 -51 0 *
-Wi
-
-0101100
--50 0 *
-Fa
-0 1e-07 2 0
-
0101000
-+55 0 +52 0 +49 0 *
++68 0 -64 0 *
Ve
1e-07
-50 5 0
+0 0 0
0 0
0101101
*
Ed
1e-07 1 1 0
-1 10 0 0 5
+1 9 0 0 50
+2 13 7 0 0 50
+2 14 7 2 0 50
0
0101000
-+69 0 -47 0 *
++62 0 -66 0 *
Ve
1e-07
-5 5 0
+0 50 0
0 0
0101101
*
Ed
1e-07 1 1 0
-1 11 0 0 45
-2 17 8 0 0 45
-2 18 8 2 0 45
+1 10 0 0 5
+0
+
+0101000
++64 0 -60 0 *
+Ed
+ 1e-07 1 1 0
+1 11 0 0 50
+2 15 8 0 0 50
+2 16 8 2 0 50
0
0101000
-+47 0 -45 0 *
++60 0 -62 0 *
+Wi
+
+0101100
++81 0 +67 0 +65 0 +63 0 +61 0 +59 0 +58 0 *
+Fa
+0 1e-07 5 0
+
+0101000
++57 0 *
Ve
1e-07
-5 50 0
+5 13 40
0 0
0101101
*
Ed
1e-07 1 1 0
-1 12 0 0 45
-2 19 9 0 0 45
-2 20 9 2 0 45
+1 12 0 -0 40
+2 17 2 0 -0 40
+2 18 6 0 -0 40
+4 G1 2 0 6 0
0
0101000
-+45 0 -43 0 *
++68 0 -55 0 *
Ed
1e-07 1 1 0
-1 13 0 0 5
+1 13 0 0 1.5707963267949
+2 19 2 0 0 1.5707963267949
+2 20 5 1 0 1.5707963267949
0
0101000
-+43 0 -58 0 *
++80 0 -55 0 *
Wi
0101100
-+68 0 +46 0 +44 0 +42 0 +41 0 +56 0 *
++67 0 -79 0 +54 0 -53 0 *
Fa
-0 1e-07 7 0
+0 0 2 0
0101000
-+40 0 *
++52 0 *
Ed
1e-07 1 1 0
1 14 0 0 40
-2 21 8 0 0 40
+2 21 7 0 0 40
0
0101000
-+47 0 -47 1 *
++66 0 -66 1 *
Wi
0101100
-+67 0 -38 0 -46 0 +46 1 *
++50 0 -78 0 -65 0 +65 1 *
Fa
-0 1e-07 10 0
+0 1e-07 9 0
+
+0101000
++49 0 *
+Ed
+ 1e-07 1 1 0
+1 8 0 8 45
+2 22 6 2 8 45
+0
+
+0101000
++55 2 -64 0 *
+Wi
+
+0101100
++77 1 +53 0 +65 1 +47 1 +61 1 +59 1 +58 1 *
+Fa
+0 1e-07 5 0
0101000
-+37 0 *
++46 2 *
Ve
1e-07
-27.5 5 31.5
+27.5 0 31.5
0 0
0101101
*
Ed
1e-07 1 1 0
-1 15 0 0 6.28318530717959
-2 22 8 0 0 6.28318530717959
-2 23 3 0 0 6.28318530717959
+1 15 0 1 6
+3 23 24CN 3 0 1 6
0
-0101100
-+35 0 -35 0 *
+0101000
++44 0 -75 0 *
Ed
1e-07 1 1 0
-1 16 0 1 6
-3 24 25CN 3 0 1 6
+1 16 0 0 6.28318530717959
+2 25 7 0 0 6.28318530717959
+2 26 3 0 0 6.28318530717959
0
-0101000
-+65 0 -35 0 *
+0101100
++44 0 -44 0 *
Wi
0101100
--34 0 +33 0 +64 0 -33 0 *
+-74 0 +43 0 +42 0 -43 0 *
Fa
0 1e-07 3 0
0101000
-+32 0 *
++41 0 *
Ve
1e-07
-27.5 5 15.5
+27.5 0 15.5
0 0
0101101
*
Ed
1e-07 1 1 0
-1 17 0 0 6.28318530717959
-2 26 8 0 0 6.28318530717959
-2 27 4 0 0 6.28318530717959
+1 17 0 1 6
+3 27 28CN 4 0 1 6
0
-0101100
-+30 0 -30 0 *
+0101000
++39 0 -72 0 *
Ed
1e-07 1 1 0
-1 18 0 1 6
-3 28 29CN 4 0 1 6
+1 18 0 0 6.28318530717959
+2 29 7 0 0 6.28318530717959
+2 30 4 0 0 6.28318530717959
0
-0101000
-+62 0 -30 0 *
+0101100
++39 0 -39 0 *
Wi
0101100
--29 0 +28 0 +61 0 -28 0 *
+-71 0 +38 0 +37 0 -38 0 *
Fa
0 1e-07 4 0
0101000
-+27 0 *
++36 0 *
Ed
1e-07 1 1 0
1 19 0 0 40
-2 30 9 0 0 40
+2 31 6 0 0 40
0
0101000
-+43 0 -43 1 *
++64 0 -64 1 *
Wi
0101100
-+25 0 -57 0 -41 0 +41 1 *
-Fa
-0 1e-07 11 0
-
-0101000
-+24 0 *
+-63 0 +54 0 -34 0 +47 1 *
Ve
1e-07
5 27.5 31.5
@@ -424,29 +456,16 @@ Ve
Ed
1e-07 1 1 0
1 20 0 0 6.28318530717959
-2 31 9 0 0 6.28318530717959
-2 32 5 0 0 6.28318530717959
+2 32 6 0 0 6.28318530717959
+2 33 10 0 0 6.28318530717959
0
0101100
-+22 0 -22 0 *
-Ed
- 1e-07 1 1 0
-1 21 0 1 6
-3 33 34CN 5 0 1 6
-0
-
-0101000
-+54 0 -22 0 *
++32 0 -32 0 *
Wi
0101100
--21 0 +20 0 +53 0 -20 0 *
-Fa
-0 1e-07 5 0
-
-0101000
-+19 0 *
++31 0 *
Ve
1e-07
5 27.5 15.5
@@ -456,78 +475,153 @@ Ve
*
Ed
1e-07 1 1 0
-1 22 0 0 6.28318530717959
-2 35 9 0 0 6.28318530717959
-2 36 6 0 0 6.28318530717959
+1 21 0 0 6.28318530717959
+2 34 6 0 0 6.28318530717959
+2 35 11 0 0 6.28318530717959
0
0101100
-+17 0 -17 0 *
++29 0 -29 0 *
+Wi
+
+0101100
++28 0 *
+Fa
+0 1e-07 6 0
+
+0101000
++33 0 +30 0 +27 0 *
Ed
1e-07 1 1 0
-1 23 0 1 6
-3 37 38CN 6 0 1 6
+1 22 0 0 40
+2 36 7 0 0 40
+2 37 8 0 0 40
0
0101000
-+51 0 -17 0 *
++62 0 -62 1 *
+Wi
+
+0101100
++25 0 -61 0 +61 1 -50 0 *
+Wi
+
+0101100
+-42 0 *
Wi
0101100
--16 0 +15 0 +50 0 -15 0 *
+-37 0 *
Fa
-0 1e-07 6 0
+0 1e-07 7 0
0101000
-+14 0 *
++24 0 +23 0 +22 0 *
Ed
1e-07 1 1 0
-1 24 0 0 40
-2 39 8 0 0 40
-2 40 9 0 0 40
+1 23 0 0 40
+2 38 8 0 0 40
0
0101000
-+45 0 -45 1 *
++60 0 -60 1 *
Wi
0101100
-+38 0 -44 0 +44 1 -12 0 *
++34 0 -20 0 -59 0 +59 1 *
+Fa
+0 1e-07 12 0
+
+0101000
++19 0 *
+Wi
+
+0101100
++20 0 -58 0 +58 1 -25 0 *
+Ve
+1e-07
+0 27.5 31.5
+0 0
+
+0101101
+*
+Ed
+ 1e-07 1 1 0
+1 24 0 0 6.28318530717959
+2 39 8 0 0 6.28318530717959
+2 40 10 0 0 6.28318530717959
+0
+
+0101100
++16 0 -16 0 *
Wi
0101100
-+34 0 *
+-15 0 *
+Ve
+1e-07
+0 27.5 15.5
+0 0
+
+0101101
+*
+Ed
+ 1e-07 1 1 0
+1 25 0 0 6.28318530717959
+2 41 8 0 0 6.28318530717959
+2 42 11 0 0 6.28318530717959
+0
+
+0101100
++13 0 -13 0 *
Wi
0101100
-+29 0 *
+-12 0 *
Fa
0 1e-07 8 0
0101000
-+11 0 +10 0 +9 0 *
-Wi
++17 0 +14 0 +11 0 *
+Ed
+ 1e-07 1 1 0
+1 26 0 1 6
+3 43 44CN 10 0 1 6
+0
-0101100
-+12 0 -42 0 +42 1 -25 0 *
+0101000
++16 0 -32 0 *
Wi
0101100
-+21 0 *
+-31 0 +9 0 +15 0 -9 0 *
+Fa
+0 1e-07 10 0
+
+0101000
++8 0 *
+Ed
+ 1e-07 1 1 0
+1 27 0 1 6
+3 45 46CN 11 0 1 6
+0
+
+0101000
++13 0 -29 0 *
Wi
0101100
-+16 0 *
+-28 0 +6 0 +12 0 -6 0 *
Fa
-0 1e-07 9 0
+0 1e-07 11 0
0101000
-+7 0 +6 0 +5 0 *
++5 0 *
Sh
0101100
--59 0 -48 0 -39 0 +39 1 -36 0 -31 0 -26 0 -23 0 -18 0 -13 0
--8 0 -4 0 *
+-69 0 -56 0 +51 0 -48 0 +45 1 -40 0 -35 0 -26 0 -21 0 -18 0
+-10 0 -7 0 -4 0 *
So
0100000
diff --git a/recipes/01-mounting-bracket/output.png b/recipes/01-mounting-bracket/output.png
index 7151596..c4ab90b 100644
Binary files a/recipes/01-mounting-bracket/output.png and b/recipes/01-mounting-bracket/output.png differ
diff --git a/recipes/06-fan-blade/main.swift b/recipes/06-fan-blade/main.swift
index 7b7a8a0..8f14930 100644
--- a/recipes/06-fan-blade/main.swift
+++ b/recipes/06-fan-blade/main.swift
@@ -69,7 +69,7 @@ guard var blade = Shape.loft(profiles: profiles, solid: true) else { fatalError(
let hubR = rootChord * 0.55
let hub = Shape.cylinder(at: SIMD3(0, 0, -12), direction: SIMD3(0, 0, 1),
radius: hubR, height: 14)!
-blade = blade.union(hub) ?? blade
+blade = blade.union(hub)!
try ctx.add(blade, color: C.steel, name: "Fan blade")
diff --git a/recipes/README.md b/recipes/README.md
index bc587d7..f0931f4 100644
--- a/recipes/README.md
+++ b/recipes/README.md
@@ -9,7 +9,7 @@ tweak the parameters.
| # | Recipe | Preview | What it shows |
|---|--------|---------|---------------|
-| 01 | [Mounting bracket](01-mounting-bracket/) |
| sketch → extrude → `concaveEdges` fillet → drill |
+| 01 | [Mounting bracket](01-mounting-bracket/) |
| sketch → extrude → geometrically-selected inside-corner fillet → drill |
| 02 | [Helical compression spring](02-helical-spring/) |
| helix path + circular section → pipe sweep |
| 03 | [Pipe flange](03-pipe-flange/) |
| revolve + `circularPatternCut` bolt circle + chamfer |
| 04 | [Involute spur gear](04-spur-gear/) |
| involute tooth math → polygon → extrude → bore |