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 |