From 4f76b8b0afaefdd8ae1d582f3f986cb423172e68 Mon Sep 17 00:00:00 2001 From: gsdali <51393997+gsdali@users.noreply.github.com> Date: Wed, 5 Aug 2026 01:07:11 +1000 Subject: [PATCH 1/2] fix: chamfer the OD and raised-face rim instead of every edge (#103) Shape.chamfered(distance:) chamfers every edge, and cannot build a chamfer at all on this shape: the revolve produces a seam edge on every periodic cylindrical face it creates (the bore, the OD, the raised-face wall, and each of the eight bolt holes), and BRepFilletAPI_MakeChamfer cannot resolve a blend on the seam of a periodic surface, where both "adjacent" faces are really the same face. Verified this holds at every distance from 1 mm down to 0.001 mm in isolation, so it is not a size problem the old ?? flange fallback happened to paper over; it degrades every single time. Select the OD and the raised-face rim edges geometrically with Shape.edges(where:), filtering by radius from the axis and isCircle (which excludes every seam), then chamfer just those with Shape.chamferedWithFullHistory(distance:edges:). The step's base (where the disk-front annulus meets the raised-face wall) is left out: it is a reentrant corner, so chamfering it adds material instead of breaking one, and chamfering it together with the rim exhausts the 2mm-tall wall and fails outright. The chamfer result is force-unwrapped rather than falling back with ??, so a future regression crashes loudly instead of silently shipping an un-chamfered body. Volume: 228927.86 -> 228302.68 mm^3 (-625.18 mm^3, -0.27%), comfortably above the ~235 mm^3 a single 1mm OD chamfer alone should remove, and subShapeCount(ofType: .solid) is 1 both before and after. Regenerated output.brep and output.png to match. Co-Authored-By: Claude Opus 5 (1M context) --- recipes/03-pipe-flange/README.md | 24 +- recipes/03-pipe-flange/main.swift | 34 ++- recipes/03-pipe-flange/output.brep | 383 +++++++++++++++++++---------- recipes/03-pipe-flange/output.png | Bin 22126 -> 21899 bytes 4 files changed, 305 insertions(+), 136 deletions(-) diff --git a/recipes/03-pipe-flange/README.md b/recipes/03-pipe-flange/README.md index 1b3c714..5b21695 100644 --- a/recipes/03-pipe-flange/README.md +++ b/recipes/03-pipe-flange/README.md @@ -28,8 +28,9 @@ never crosses the axis. The section is faced with `Shape.face(from:)`, and the * what closes the result into a solid rather than a shell (see Gotchas). The bolt circle is cut with `Shape.circularPatternCut`: a single cylindrical hole tool is built at the bolt-circle radius (oriented along Y), then patterned `boltCount` times around the axis -and subtracted as one compound. Finally a small all-edge chamfer breaks the sharp -corners; it falls back to the un-chamfered body if the blend fails. +and subtracted as one compound. Finally a chamfer breaks the OD and the raised-face rim, +selected by geometry (radius from the axis, plus `isCircle` to exclude the revolve's seam +edges) rather than by chamfering every edge; see Gotchas. ## OCCTSwift APIs used @@ -38,7 +39,8 @@ corners; it falls back to the un-chamfered body if the blend fails. - `Shape.revolved(axisOrigin:axisDirection:)`: surface of revolution (on the faced section) - `Shape.cylinder(at:direction:radius:height:)`: the bolt-hole tool - `Shape.circularPatternCut(tool:axisPoint:axisDirection:count:angle:)`: the bolt circle (OCCTSwift v1.3.1) -- `Shape.chamfered(distance:)`: edge break (optional) +- `Shape.edges(where:)`: select the OD and raised-face rim edges geometrically +- `Shape.chamferedWithFullHistory(distance:edges:)`: chamfer just those edges ## Gotchas @@ -52,8 +54,20 @@ corners; it falls back to the un-chamfered body if the blend fails. position. Revolving an XY profile about Z would sweep a flat disk, not a solid. - Keep every profile radius `≥ boreRadius`: a profile that touches or crosses the axis produces a degenerate or self-intersecting revolution. -- `chamfered(distance:)` blends **all** edges. On a flange with many bolt-hole edges this - can be slow or fail; the recipe guards it with `?? flange` so the body still emits. +- `Shape.chamfered(distance:)` blends **all** edges, and cannot build a chamfer at all on + this shape (OCCTSwiftScripts #103): the revolve produces a seam edge on every periodic + cylindrical face it creates, the bore, the OD, the raised-face wall, and each of the + eight bolt holes, and `BRepFilletAPI_MakeChamfer` cannot resolve a blend on a seam, + where both "adjacent" faces are really the same face. That failure held at every + distance tested, from 1 mm down to 0.001 mm, so it is not a size problem, and it is not + fixed by using a smaller chamfer. The recipe instead selects only the OD and the + raised-face rim edges with `Shape.edges(where:)` and chamfers those with + `Shape.chamferedWithFullHistory(distance:edges:)`, which avoids every seam. The step's + base (where the disk-front annulus meets the raised-face wall) is left alone: it is a + reentrant corner, so chamfering it adds material instead of breaking a corner, and + chamfering it together with the rim exhausts the 2mm-tall wall and fails outright. + A failed chamfer here is not swallowed: the recipe force-unwraps the result, so a + regression crashes loudly instead of silently shipping an un-chamfered body. - Use `circularPatternCut`, **not** `circularPattern`, for the bolt circle. `circularPattern` patterns the whole *body*, applied to a holed flange it produces overlapping flange copies (≈8× the volume) with the holes filled in. `circularPatternCut` patterns the *tool* and diff --git a/recipes/03-pipe-flange/main.swift b/recipes/03-pipe-flange/main.swift index fd1a45c..6453b56 100644 --- a/recipes/03-pipe-flange/main.swift +++ b/recipes/03-pipe-flange/main.swift @@ -10,8 +10,17 @@ // with Shape.face(from:) and revolving the face closes the ends into a solid // (OCCTSwiftScripts #100). The bolt circle is cut with Shape.circularPatternCut // (OCCTSwift v1.3.1, #169), which patterns a single hole tool around the axis -// and subtracts the whole compound in one call. A small all-edge chamfer breaks -// sharp corners; it degrades gracefully if it fails. +// and subtracts the whole compound in one call. A chamfer breaks the OD and +// the raised-face rim; it targets those edges specifically because +// Shape.chamfered(distance:), which chamfers every edge, cannot handle this +// shape at all (OCCTSwiftScripts #103): every seam edge produced by the +// revolve (the closing edge of each periodic cylindrical face: the bore, the +// OD, the raised-face wall, and all eight bolt holes) fails to chamfer in +// isolation at any distance from 1 mm down to 0.001 mm, because +// BRepFilletAPI_MakeChamfer cannot resolve a blend on the seam of a periodic +// surface, where both "adjacent" faces are really the same face. Excluding the +// seams and chamfering only the OD and the raised-face rim's outer edge with +// Shape.chamferedWithFullHistory(distance:edges:) avoids every seam. // // Run: swift run occtkit run recipes/03-pipe-flange/main.swift --format brep @@ -60,8 +69,25 @@ flange = flange.circularPatternCut(tool: holeTool, axisPoint: .zero, axisDirection: SIMD3(0, 1, 0), count: boltCount, angle: 2 * .pi)! -// ── Break all sharp edges (optional; falls back if the chamfer fails) ───────── -flange = flange.chamfered(distance: 1.0) ?? flange +// ── Break the exposed sharp edges: the OD (front + back) and the raised face's +// outer rim. The step's base, where the disk-front annulus meets the raised-face +// wall, is a reentrant corner, not a sharp edge, so it is excluded: chamfering it +// would add material rather than break a corner, and doing that alongside the rim +// exhausts the 2mm-tall wall and fails outright. Select by geometry, not raw edge +// index, so the recipe still finds the right edges if the parameters change +// (same reasoning as recipe 01's concaveEdges() selection). +func nearAxisRadius(_ edge: borrowing Edge) -> Double? { + guard edge.isCircle, let bounds = edge.parameterBounds, + let p = edge.point(at: (bounds.first + bounds.last) / 2) else { return nil } + return (p.x * p.x + p.z * p.z).squareRoot() +} +let chamferTargets = flange.edges(where: { edge in + guard let r = nearAxisRadius(edge) else { return false } + if abs(r - outerRadius) < 1e-3 { return true } // OD, front + back + if abs(r - raisedRadius) < 1e-3 && edge.bounds.min.y > thickness + 1e-3 { return true } // raised-face rim + return false +}) +flange = flange.chamferedWithFullHistory(distance: 1.0, edges: chamferTargets.map(\.index))!.result try ctx.add(flange, color: C.brass, name: "Pipe flange") diff --git a/recipes/03-pipe-flange/output.brep b/recipes/03-pipe-flange/output.brep index 574cf54..29d959a 100644 --- a/recipes/03-pipe-flange/output.brep +++ b/recipes/03-pipe-flange/output.brep @@ -2,9 +2,9 @@ DBRep_DrawableShape CASCADE Topology V3, (c) Open Cascade Locations 0 -Curve2ds 52 -2 0 0 -1 0 0 1 75 -1 0 0 1 0 +Curve2ds 64 +1 0 -1.4142135623730951 1 0 +2 0 0 -1 0 0 1 74 2 -42.426406871192839 -42.42640687119286 0.70710678118654768 -0.70710678118654735 -0.70710678118654735 -0.70710678118654768 7 1 1.0146536357569551e-17 1 1 0 2 1.3322676295501878e-14 -60 1 2.2204460492503131e-16 2.2204460492503131e-16 -1 7 @@ -23,10 +23,12 @@ Curve2ds 52 1 0 1 1 0 2 42.426406871192846 42.426406871192853 -0.70710678118654757 0.70710678118654746 0.70710678118654746 0.70710678118654757 7 1 1.0146536357569526e-17 1 1 0 -1 0 15 1 0 -2 0 0 1 0 -0 1 75 +8 -1.4142135623730951 0 1 6.2831853071795862 0 0 1 +8 -1.4142135623730951 0 1 0 0 0 1 +1 0 0 1 0 +1 0 1 1 0 2 42.426406871192839 -42.42640687119286 -0.70710678118654768 -0.70710678118654735 0.70710678118654735 -0.70710678118654768 7 1 1.0146536357569551e-17 16 1 0 1 6.2831853071795862 -0 0 1 @@ -49,14 +51,30 @@ Curve2ds 52 1 0 16 1 0 2 -42.426406871192846 42.426406871192853 0.70710678118654757 0.70710678118654746 -0.70710678118654746 0.70710678118654757 7 1 1.0146536357569526e-17 16 1 0 -2 0 0 1 0 -0 1 50 +1 6.2831853071795862 0 0 1 +1 0 0 0 1 1 0 0 1 0 -1 0 2 1 0 +1 0 14 1 0 +1 0 1.4142135623730951 1 0 +2 0 0 1 0 -0 1 74 2 0 0 1 0 -0 1 50 +1 0 0 1 0 +1 0 -1.4142135623730951 1 0 +2 0 0 1 0 -0 1 49 +8 0 1.4142135623730951 1 6.2831853071795862 0 0 1 +8 0 1.4142135623730951 1 0 0 0 1 -Curves 33 -2 0 0 0 0 1 0 1 0 0 0 0 -1 75 +1 6.2831853071795862 0 0 1 +1 0 0 0 1 +1 0 0 1 0 +1 0 1 1 0 +8 -1.4142135623730951 0 +1 6.2831853071795862 0 0 1 +8 -1.4142135623730951 0 +1 0 0 0 1 +Curves 39 +2 0 0 0 -0 1 -0 1 0 0 0 0 -1 74 2 42.426406871192839 0 42.42640687119286 0 1 0 -0.70710678118654768 0 0.70710678118654735 0.70710678118654735 -0 0.70710678118654768 7 2 -1.3322676295501878e-14 0 60 0 1 0 -1 0 -2.2204460492503131e-16 -2.2204460492503131e-16 0 1 7 2 -42.42640687119286 0 42.426406871192846 0 1 0 -0.70710678118654746 0 -0.70710678118654768 -0.70710678118654768 0 0.70710678118654746 7 @@ -66,8 +84,8 @@ Curves 33 2 -60 0 -7.3478807948841199e-15 0 1 0 1.2246467991473532e-16 0 -1 -1 0 -1.2246467991473532e-16 7 2 6.6613381477509392e-15 0 -60 0 1 0 1 -0 1.1102230246251565e-16 1.1102230246251565e-16 0 -1 7 2 -42.426406871192846 0 -42.426406871192853 0 1 0 0.70710678118654757 0 -0.70710678118654746 -0.70710678118654746 0 -0.70710678118654757 7 -2 0 15 0 0 1 0 1 0 0 0 0 -1 75 -1 75 0 0 0 1 0 +1 75 1 0 0.70710678118654757 0.70710678118654746 0 +2 0 1 0 -0 1 -0 1 0 0 0 0 -1 75 2 42.426406871192839 15 42.42640687119286 0 1 0 -0.70710678118654768 0 0.70710678118654735 0.70710678118654735 -0 0.70710678118654768 7 1 37.476659402887002 -1 47.37615433949869 0 1 0 2 -1.3322676295501878e-14 15 60 0 1 0 -1 0 -2.2204460492503131e-16 -2.2204460492503131e-16 0 1 7 @@ -86,14 +104,21 @@ Curves 33 1 7.0000000000000071 -1 -60 0 1 0 2 -42.426406871192846 15 -42.426406871192853 0 1 0 0.70710678118654757 0 -0.70710678118654746 -0.70710678118654746 0 -0.70710678118654757 7 1 -37.476659402887016 -1 -47.376154339498683 0 1 0 +1 75 0 0 0 1 0 +2 0 14 0 -0 1 0 1 0 0 0 0 -1 75 +2 0 15 0 -0 1 0 1 0 0 0 0 -1 74 2 0 15 0 0 1 0 1 0 0 0 0 -1 50 -2 0 17 0 0 1 0 1 0 0 0 0 -1 50 +2 0 17 0 -0 1 0 1 0 0 0 0 -1 49 +1 75 14 0 -0.70710678118654757 0.70710678118654746 0 1 50 15 0 0 1 0 +2 0 16 0 -0 1 0 1 0 0 0 0 -1 50 +1 50 16 0 0.70710678118654757 -0.70710678118654746 0 Polygon3D 0 PolygonOnTriangulations 0 -Surfaces 14 +Surfaces 17 1 0 0 0 0 1 0 -1 -0 -0 0 0 -1 -2 0 0 0 0 1 0 1 0 0 0 0 -1 75 +3 0 1 0 -0 1 -0 1 0 0 0 0 -1 75 +0.78539816339744839 2 42.426406871192839 -1 42.42640687119286 0 1 0 -0.70710678118654768 0 0.70710678118654735 0.70710678118654735 0 0.70710678118654768 7 2 -1.3322676295501878e-14 -1 60 0 1 0 -1 0 -2.2204460492503131e-16 -2.2204460492503131e-16 0 1 7 2 -42.42640687119286 -1 42.426406871192846 0 1 0 -0.70710678118654746 0 -0.70710678118654768 -0.70710678118654768 0 0.70710678118654746 7 @@ -103,15 +128,20 @@ Surfaces 14 2 -60 -1 -7.3478807948841199e-15 0 1 0 1.2246467991473532e-16 0 -1 -1 0 -1.2246467991473532e-16 7 2 6.6613381477509392e-15 -1 -60 0 1 0 1 0 1.1102230246251565e-16 1.1102230246251565e-16 0 -1 7 2 -42.426406871192846 -1 -42.426406871192853 0 1 0 0.70710678118654757 0 -0.70710678118654746 -0.70710678118654746 0 -0.70710678118654757 7 +2 0 0 0 0 1 0 1 0 0 0 0 -1 75 1 0 15 0 0 1 0 1 0 0 0 0 -1 1 0 17 0 0 1 0 1 0 0 0 0 -1 +3 0 14 0 -0 1 0 1 0 0 -0 0 -1 75 +-0.78539816339744839 2 0 15 0 0 1 0 1 0 0 0 0 -1 50 +3 0 16 0 0 -1 -0 1 0 0 -0 0 -1 50 +0.78539816339744839 Triangulations 0 -TShapes 105 +TShapes 120 Ve 1e-07 -75 0 0 +74 0 0 0 0 0101101 @@ -119,16 +149,16 @@ Ve Ed 1e-07 1 1 0 1 1 0 0 6.28318530717959 -2 1 1 0 0 6.28318530717959 -2 2 2 0 0 6.28318530717959 +2 1 2 0 0 6.28318530717959 +2 2 1 0 0 6.28318530717959 0 0101000 -+105 0 -105 0 * ++120 0 -120 0 * Wi 0101100 --104 0 * +-119 0 * Ve 1.00000007105427e-07 37.476659402887 0 47.3761543394987 @@ -144,11 +174,11 @@ Ed 0 0101100 -+102 0 -102 0 * ++117 0 -117 0 * Wi 0101100 -+101 0 * ++116 0 * Ve 1e-07 -7.00000000000001 0 60 @@ -164,11 +194,11 @@ Ed 0 0101100 -+99 0 -99 0 * ++114 0 -114 0 * Wi 0101100 -+98 0 * ++113 0 * Ve 1e-07 -47.3761543394987 0 37.476659402887 @@ -184,11 +214,11 @@ Ed 0 0101100 -+96 0 -96 0 * ++111 0 -111 0 * Wi 0101100 -+95 0 * ++110 0 * Ve 1e-07 60 0 7 @@ -204,11 +234,11 @@ Ed 0 0101100 -+93 0 -93 0 * ++108 0 -108 0 * Wi 0101100 -+92 0 * ++107 0 * Ve 1.00000007105427e-07 47.3761543394987 0 -37.476659402887 @@ -224,11 +254,11 @@ Ed 0 0101100 -+90 0 -90 0 * ++105 0 -105 0 * Wi 0101100 -+89 0 * ++104 0 * Ve 1e-07 25 0 0 @@ -244,11 +274,11 @@ Ed 0 0101000 -+87 0 -87 0 * ++102 0 -102 0 * Wi 0101100 -+86 0 * ++101 0 * Ve 1e-07 -60 0 -7.00000000000001 @@ -264,11 +294,11 @@ Ed 0 0101100 -+84 0 -84 0 * ++99 0 -99 0 * Wi 0101100 -+83 0 * ++98 0 * Ve 1e-07 7.00000000000001 0 -60 @@ -284,11 +314,11 @@ Ed 0 0101100 -+81 0 -81 0 * ++96 0 -96 0 * Wi 0101100 -+80 0 * ++95 0 * Ve 1.00000007105427e-07 -37.476659402887 0 -47.3761543394987 @@ -304,50 +334,50 @@ Ed 0 0101100 -+78 0 -78 0 * ++93 0 -93 0 * Wi 0101100 -+77 0 * ++92 0 * Fa 0 1e-07 1 0 0101000 -+103 0 +100 0 +97 0 +94 0 +91 0 +88 0 +85 0 +82 0 +79 0 +76 0 ++118 0 +115 0 +112 0 +109 0 +106 0 +103 0 +100 0 +97 0 +94 0 +91 0 * Ve 1e-07 -75 15 0 +75 1 0 0 0 0101101 * Ed 1e-07 1 1 0 -1 11 0 0 6.28318530717959 -2 21 2 0 0 6.28318530717959 -2 22 12 0 0 6.28318530717959 +1 11 0 -1.4142135623731 0 +3 21 22CN 2 0 -1.4142135623731 0 0 0101000 -+74 0 -74 0 * ++120 0 -89 0 * Ed 1e-07 1 1 0 -1 12 0 0 15 -3 23 24CN 2 0 0 15 +1 12 0 0 6.28318530717959 +2 23 2 0 0 6.28318530717959 +2 24 12 0 0 6.28318530717959 0 0101000 -+105 0 -74 0 * ++89 0 -89 0 * Wi 0101100 -+104 0 -73 0 -72 0 +72 0 * +-88 0 +119 0 -87 0 +88 0 * Fa -0 1e-07 2 0 +0 0 2 0 0101000 -+71 0 * ++86 0 * Ve 1.00000007105427e-07 37.476659402887 15 47.3761543394987 @@ -358,12 +388,12 @@ Ve Ed 1e-07 1 1 0 1 13 0 2.78178084688463e-16 6.28318530717959 -2 25 12 0 2.78178084688463e-16 6.28318530717959 +2 25 13 0 2.78178084688463e-16 6.28318530717959 2 26 3 0 2.78178084688463e-16 6.28318530717959 0 0101100 -+69 0 -69 0 * ++84 0 -84 0 * Ed 1e-07 1 1 0 1 14 0 1 16 @@ -371,16 +401,16 @@ Ed 0 0101000 -+102 0 -69 0 * ++117 0 -84 0 * Wi 0101100 --68 0 +67 0 +101 0 -67 0 * +-83 0 +82 0 +116 0 -82 0 * Fa 0 1e-07 3 0 0101000 -+66 0 * ++81 0 * Ve 1e-07 -7.00000000000001 15 60 @@ -391,12 +421,12 @@ Ve Ed 1e-07 1 1 0 1 15 0 0 6.28318530717959 -2 29 12 0 0 6.28318530717959 +2 29 13 0 0 6.28318530717959 2 30 4 0 0 6.28318530717959 0 0101100 -+64 0 -64 0 * ++79 0 -79 0 * Ed 1e-07 1 1 0 1 16 0 1 16 @@ -404,16 +434,16 @@ Ed 0 0101000 -+99 0 -64 0 * ++114 0 -79 0 * Wi 0101100 --63 0 +62 0 +98 0 -62 0 * +-78 0 +77 0 +113 0 -77 0 * Fa 0 1e-07 4 0 0101000 -+61 0 * ++76 0 * Ve 1e-07 -47.3761543394987 15 37.476659402887 @@ -424,12 +454,12 @@ Ve Ed 1e-07 1 1 0 1 17 0 0 6.28318530717959 -2 31 12 0 0 6.28318530717959 +2 31 13 0 0 6.28318530717959 2 32 5 0 0 6.28318530717959 0 0101100 -+59 0 -59 0 * ++74 0 -74 0 * Ed 1e-07 1 1 0 1 18 0 1 16 @@ -437,16 +467,16 @@ Ed 0 0101000 -+96 0 -59 0 * ++111 0 -74 0 * Wi 0101100 --58 0 +57 0 +95 0 -57 0 * +-73 0 +72 0 +110 0 -72 0 * Fa 0 1e-07 5 0 0101000 -+56 0 * ++71 0 * Ve 1e-07 60 15 7 @@ -457,12 +487,12 @@ Ve Ed 1e-07 1 1 0 1 19 0 0 6.28318530717959 -2 33 12 0 0 6.28318530717959 +2 33 13 0 0 6.28318530717959 2 34 6 0 0 6.28318530717959 0 0101100 -+54 0 -54 0 * ++69 0 -69 0 * Ed 1e-07 1 1 0 1 20 0 1 16 @@ -470,16 +500,16 @@ Ed 0 0101000 -+93 0 -54 0 * ++108 0 -69 0 * Wi 0101100 --53 0 +52 0 +92 0 -52 0 * +-68 0 +67 0 +107 0 -67 0 * Fa 0 1e-07 6 0 0101000 -+51 0 * ++66 0 * Ve 1.00000007105427e-07 47.3761543394987 15 -37.476659402887 @@ -490,12 +520,12 @@ Ve Ed 1e-07 1 1 0 1 21 0 1.00875766104946e-16 6.28318530717959 -2 35 12 0 1.00875766104946e-16 6.28318530717959 +2 35 13 0 1.00875766104946e-16 6.28318530717959 2 36 7 0 1.00875766104946e-16 6.28318530717959 0 0101100 -+49 0 -49 0 * ++64 0 -64 0 * Ed 1e-07 1 1 0 1 22 0 1 16 @@ -503,16 +533,16 @@ Ed 0 0101000 -+90 0 -49 0 * ++105 0 -64 0 * Wi 0101100 --48 0 +47 0 +89 0 -47 0 * +-63 0 +62 0 +104 0 -62 0 * Fa 0 1e-07 7 0 0101000 -+46 0 * ++61 0 * Ve 1e-07 25 17 0 @@ -523,12 +553,12 @@ Ve Ed 1e-07 1 1 0 1 23 0 0 6.28318530717959 -2 37 13 0 0 6.28318530717959 +2 37 14 0 0 6.28318530717959 2 38 8 0 0 6.28318530717959 0 0101000 -+44 0 -44 0 * ++59 0 -59 0 * Ed 1e-07 1 1 0 1 24 0 0 17 @@ -536,16 +566,16 @@ Ed 0 0101000 -+44 0 -87 0 * ++59 0 -102 0 * Wi 0101100 -+43 0 -86 0 -42 0 +42 0 * ++58 0 -101 0 -57 0 +57 0 * Fa 0 1e-07 8 0 0101000 -+41 0 * ++56 0 * Ve 1e-07 -60 15 -7.00000000000001 @@ -556,12 +586,12 @@ Ve Ed 1e-07 1 1 0 1 25 0 0 6.28318530717959 -2 41 12 0 0 6.28318530717959 +2 41 13 0 0 6.28318530717959 2 42 9 0 0 6.28318530717959 0 0101100 -+39 0 -39 0 * ++54 0 -54 0 * Ed 1e-07 1 1 0 1 26 0 1 16 @@ -569,16 +599,16 @@ Ed 0 0101000 -+84 0 -39 0 * ++99 0 -54 0 * Wi 0101100 --38 0 +37 0 +83 0 -37 0 * +-53 0 +52 0 +98 0 -52 0 * Fa 0 1e-07 9 0 0101000 -+36 0 * ++51 0 * Ve 1e-07 7.00000000000001 15 -60 @@ -589,12 +619,12 @@ Ve Ed 1e-07 1 1 0 1 27 0 0 6.28318530717959 -2 43 12 0 0 6.28318530717959 +2 43 13 0 0 6.28318530717959 2 44 10 0 0 6.28318530717959 0 0101100 -+34 0 -34 0 * ++49 0 -49 0 * Ed 1e-07 1 1 0 1 28 0 1 16 @@ -602,16 +632,16 @@ Ed 0 0101000 -+81 0 -34 0 * ++96 0 -49 0 * Wi 0101100 --33 0 +32 0 +80 0 -32 0 * +-48 0 +47 0 +95 0 -47 0 * Fa 0 1e-07 10 0 0101000 -+31 0 * ++46 0 * Ve 1.00000007105427e-07 -37.476659402887 15 -47.3761543394987 @@ -622,12 +652,12 @@ Ve Ed 1e-07 1 1 0 1 29 0 8.86511592917583e-17 6.28318530717959 -2 45 12 0 8.86511592917583e-17 6.28318530717959 +2 45 13 0 8.86511592917583e-17 6.28318530717959 2 46 11 0 8.86511592917583e-17 6.28318530717959 0 0101100 -+29 0 -29 0 * ++44 0 -44 0 * Ed 1e-07 1 1 0 1 30 0 1 16 @@ -635,40 +665,89 @@ Ed 0 0101000 -+78 0 -29 0 * ++93 0 -44 0 * Wi 0101100 --28 0 +27 0 +77 0 -27 0 * +-43 0 +42 0 +92 0 -42 0 * Fa 0 1e-07 11 0 0101000 -+26 0 * ++41 0 * +Ve +1e-07 +75 14 0 +0 0 + +0101101 +* +Ed + 1e-07 1 1 0 +1 31 0 1 14 +3 47 48CN 12 0 1 14 +0 + +0101000 ++89 0 -39 0 * +Ed + 1e-07 1 1 0 +1 32 0 0 6.28318530717959 +2 49 15 0 0 6.28318530717959 +2 50 12 0 0 6.28318530717959 +0 + +0101000 ++39 0 -39 0 * Wi 0101100 -+73 0 * +-38 0 +87 0 -37 0 +38 0 * +Fa +0 1e-07 12 0 + +0101000 ++36 0 * +Ve +1e-07 +74 15 0 +0 0 + +0101101 +* +Ed + 1e-07 1 1 0 +1 33 0 0 6.28318530717959 +2 51 15 0 0 6.28318530717959 +2 52 13 0 0 6.28318530717959 +0 + +0101000 ++34 0 -34 0 * Wi 0101100 --58 0 * ++33 0 * Wi 0101100 --63 0 * +-73 0 * Wi 0101100 --68 0 * +-78 0 * +Wi + +0101100 +-83 0 * Wi 0101100 --38 0 * +-53 0 * Wi 0101100 --28 0 * +-43 0 * Ve 1e-07 50 15 0 @@ -678,86 +757,136 @@ Ve * Ed 1e-07 1 1 0 -1 31 0 0 6.28318530717959 -2 47 12 0 0 6.28318530717959 -2 48 14 0 0 6.28318530717959 +1 34 0 0 6.28318530717959 +2 53 13 0 0 6.28318530717959 +2 54 16 0 0 6.28318530717959 0 0101000 -+18 0 -18 0 * ++26 0 -26 0 * Wi 0101100 --17 0 * +-25 0 * Wi 0101100 --53 0 * +-68 0 * Wi 0101100 --33 0 * +-48 0 * Wi 0101100 --48 0 * +-63 0 * Fa -0 1e-07 12 0 +0 1e-07 13 0 0101000 -+24 0 +23 0 +22 0 +21 0 +20 0 +19 0 +16 0 +15 0 +14 0 +13 0 ++32 0 +31 0 +30 0 +29 0 +28 0 +27 0 +24 0 +23 0 +22 0 +21 0 * Ve 1e-07 -50 17 0 +49 17 0 0 0 0101101 * Ed 1e-07 1 1 0 -1 32 0 0 6.28318530717959 -2 49 14 0 0 6.28318530717959 -2 50 13 0 0 6.28318530717959 +1 35 0 0 6.28318530717959 +2 55 17 0 0 6.28318530717959 +2 56 14 0 0 6.28318530717959 0 0101000 -+11 0 -11 0 * ++19 0 -19 0 * Wi 0101100 -+10 0 * ++18 0 * Wi 0101100 --43 0 * +-58 0 * Fa -0 1e-07 13 0 +0 1e-07 14 0 0101000 -+9 0 +8 0 * ++17 0 +16 0 * Ed 1e-07 1 1 0 -1 33 0 0 2 -3 51 52CN 14 0 0 2 +1 36 0 0 1.4142135623731 +3 57 58CN 15 0 0 1.4142135623731 0 0101000 -+18 0 -11 0 * ++39 0 -34 0 * Wi 0101100 -+17 0 -10 0 -6 0 +6 0 * +-14 0 +37 0 -33 0 +14 0 * Fa -0 1e-07 14 0 +0 0 15 0 + +0101000 ++13 0 * +Ve +1e-07 +50 16 0 +0 0 + +0101101 +* +Ed + 1e-07 1 1 0 +1 37 0 0 1 +3 59 60CN 16 0 0 1 +0 + +0101000 +-11 0 +26 0 * +Ed + 1e-07 1 1 0 +1 38 0 0 6.28318530717959 +2 61 17 0 0 6.28318530717959 +2 62 16 0 0 6.28318530717959 +0 + +0101000 ++11 0 -11 0 * +Wi + +0101100 +-10 0 -9 0 +25 0 +10 0 * +Fa +0 1e-07 16 0 + +0101000 ++8 0 * +Ed + 1e-07 1 1 0 +1 39 0 -1.4142135623731 0 +3 63 64CN 17 0 -1.4142135623731 0 +0 + +0101000 ++19 0 -11 0 * +Wi + +0101100 +-6 0 +18 0 -9 0 +6 0 * +Fa +0 0 17 0 0101000 +5 0 * Sh 0101100 -+75 0 +70 0 -65 0 -60 0 -55 0 -50 0 -45 0 +40 0 -35 0 -30 0 --25 0 +12 0 +7 0 +4 0 * ++90 0 +85 0 -80 0 -75 0 -70 0 -65 0 -60 0 +55 0 -50 0 -45 0 +-40 0 +35 0 +20 0 +15 0 +12 0 +7 0 -4 0 * So 0100000 diff --git a/recipes/03-pipe-flange/output.png b/recipes/03-pipe-flange/output.png index a4e40e05a660d0c4b08cc0323cfc0b9926ea9391..c4f4340bdca4494d0bab4ab3bb6c344379a641fc 100644 GIT binary patch literal 21899 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 X{w(3N+&K76RJz;B+P8{sT0j0jBkN-< literal 22126 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 Zqkwrx()X!?a0>A6#x))F5>>m${||A%P`3a8 From f51c061a594715281235959d7cafdb9cbe1f237a Mon Sep 17 00:00:00 2001 From: gsdali <51393997+gsdali@users.noreply.github.com> Date: Wed, 5 Aug 2026 06:26:37 +1000 Subject: [PATCH 2/2] fix: address review on #104 (selector semantics, parameter, OKF, docs) Item 1, the selector now states its real condition. It previously sampled one point at the parametric midpoint and asked "does some point on this edge lie at radius R", which is weaker than "is this a circle concentric with the revolve axis of radius R". Now uses centerOfCurvature(at:) to pin the centre to the axis and 1/curvature(at:) for the true radius, so bolt-hole circles are excluded on their own geometry rather than incidentally by the y-gate. Reporting this accurately: I could not construct a case on this shape where the old and new predicates disagree. The aliasing values the review suggests (boltCircleRadius 55, 57, 68, 43) all either leave both predicates agreeing at 3 edges, or change the geometry so that the extra matches are genuine concentric arcs. So this is a correctness-of-intent change, not a latent bug fix, and it is behaviour-neutral: volume is 228302.67972902392 before and after, identical to the committed reference. Item 2, chamferDistance is now a named parameter with its coupling documented: it must stay below both raisedHeight and thickness, since the chamfer is cut into those walls and the failure is now a hard crash rather than a graceful degrade. Item 3, added okf/decisions/revolve-seams-cannot-be-chamfered.md plus index and log lines. The reusable constraint is that a full revolve makes every periodic face carry a seam edge, BRepFilletAPI cannot blend a seam because both adjacent faces are the same face, and the all-edge convenience call therefore fails entirely rather than skipping it. Item 4, the README now states that excluding seams via isCircle only holds for a polygonal profile: a revolve's seams are the profile edges themselves, so an arc in the half-section makes the seam circular. Points at Edge.isSeam(on:) for that case, and notes the concentricity test excludes seams regardless. Item 5, renamed nearAxisRadius to axisCircleRadius, dropped the unusual `borrowing Edge` for a plain parameter matching edges(where:)'s own signature, and switched the call to trailing-closure form. Item 6, docs/SCRIPT_WORKFLOW.md's cheatsheet now carries the seam caveat on both chamfered(distance:) and the all-edge filleted(radius:), which has the same trap. Co-Authored-By: Claude Opus 5 (1M context) --- docs/SCRIPT_WORKFLOW.md | 7 ++- okf/decisions/index.md | 2 + .../revolve-seams-cannot-be-chamfered.md | 63 +++++++++++++++++++ okf/log.md | 5 ++ recipes/03-pipe-flange/README.md | 15 ++++- recipes/03-pipe-flange/main.swift | 34 +++++++--- 6 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 okf/decisions/revolve-seams-cannot-be-chamfered.md diff --git a/docs/SCRIPT_WORKFLOW.md b/docs/SCRIPT_WORKFLOW.md index a141e29..2eb940f 100644 --- a/docs/SCRIPT_WORKFLOW.md +++ b/docs/SCRIPT_WORKFLOW.md @@ -138,9 +138,12 @@ shape.subtracting(other) // Cut shape.intersection(with: other) // Common volume shape.split(by: tool) // Returns [Shape] -shape.filleted(radius: 1.0) // All edges +shape.filleted(radius: 1.0) // All edges; same seam caveat as chamfered below shape.filleted(edges: [e1, e2], radius: 1.0) -shape.chamfered(distance: 0.5) +shape.chamfered(distance: 0.5) // ALL edges at once; returns nil on a full revolve, + // whose periodic faces each carry an unblendable seam. + // Select edges instead: chamferedWithFullHistory(distance:edges:). + // See okf/decisions/revolve-seams-cannot-be-chamfered.md shape.shelled(thickness: 1.0) // Hollow out shape.offset(by: 2.0) // Offset surface diff --git a/okf/decisions/index.md b/okf/decisions/index.md index 888d29a..0e4b881 100644 --- a/okf/decisions/index.md +++ b/okf/decisions/index.md @@ -15,3 +15,5 @@ that need standalone rationale. faces a wire for you, `revolve` and `sweep` do not. Assert `solidCount >= 1`. * [errexit is suppressed in `||` context](errexit-is-suppressed-in-or-context.md): a function 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. diff --git a/okf/decisions/revolve-seams-cannot-be-chamfered.md b/okf/decisions/revolve-seams-cannot-be-chamfered.md new file mode 100644 index 0000000..51d5563 --- /dev/null +++ b/okf/decisions/revolve-seams-cannot-be-chamfered.md @@ -0,0 +1,63 @@ +--- +type: decision +title: A full revolve cannot be chamfered or filleted with the all-edge convenience call +description: Every periodic face a full revolve creates contributes a seam edge, and BRepFilletAPI cannot blend a seam because both adjacent faces are the same face. Select edges explicitly instead. +resource: https://github.com/SecondMouseAU/OCCTSwiftScripts/issues/103 +tags: [decision, occtswift, chamfer, fillet, revolve, topology] +timestamp: 2026-08-05 +--- + +# Decision + +Do not call `Shape.chamfered(distance:)` or the equivalent all-edge fillet on the result of a +full 360 degree revolve. It will return nil. Select the edges you actually want and use +`Shape.chamferedWithFullHistory(distance:edges:)` or `filleted(edges:radius:)`. + +# Why + +A full revolve produces **periodic** faces, and every periodic face carries a **seam edge** where +the surface closes on itself. `BRepFilletAPI_MakeChamfer` cannot blend a seam: a chamfer or fillet +is defined between two adjacent faces, and at a seam both "adjacent" faces are the same face. + +The all-edge convenience call bundles every edge into a single operation, so one unblendable seam +fails the entire call. It returns nil rather than skipping the edge. + +Measured on the pipe flange in +[#103](https://github.com/SecondMouseAU/OCCTSwiftScripts/issues/103): 11 of 33 edges were seams, +one per periodic face (bore, OD, raised-face wall, and each of the eight bolt holes). Isolating +every edge and chamfering it alone showed each circular edge succeeds by itself, while all 11 +seams fail alone at every distance tried, from 1 mm down to 0.001 mm. It is structural, not a +size problem. + +# How to select instead + +Prefer a predicate that states the real geometric condition. For circles concentric with the +revolve axis, `centerOfCurvature(at:)` pins the centre to the axis and `1 / curvature(at:)` gives +the true radius: + +```swift +func axisCircleRadius(_ edge: Edge) -> Double? { + guard edge.isCircle, let b = edge.parameterBounds else { return nil } + let mid = (b.first + b.last) / 2 + guard let c = edge.centerOfCurvature(at: mid), + let k = edge.curvature(at: mid), k > 1e-9, + abs(c.x) < 1e-6, abs(c.z) < 1e-6 else { return nil } // axis is Y here + return 1 / k +} +``` + +Sampling a single point and taking its distance from the axis answers the weaker question "does +some point on this edge lie at radius R", which is not the same test. + +**Caveat on using `isCircle` to exclude seams.** The seam edges of a revolve are the profile edges +themselves. With a polygonal half-section they are lines, so `isCircle` happens to exclude them. +Put an arc or a fillet in the profile and the seam becomes circular, and that exclusion silently +stops working. `Edge.isSeam(on:)` is the direct test when a profile is not purely polygonal. + +# Related + +Reentrant edges are a second trap in the same area: chamfering one **adds** material rather than +breaking a corner. On the flange, chamfering the raised-face step base alone increased volume by +about 158 mm3. Note also that `convexEdges()` and `concaveEdges()` classified that shape in a way +that disagreed with what chamfering the edges actually did, so geometric selection was preferred +over the classifier there. diff --git a/okf/log.md b/okf/log.md index c41ed76..4ef82e8 100644 --- a/okf/log.md +++ b/okf/log.md @@ -9,6 +9,11 @@ made every check it performed decorative. * **Creation**: Recorded the wire-sweep-factories-are-not-symmetric decision. * **Creation**: Recorded the errexit-is-suppressed-in-or-context decision. +* **Update**: Fixed recipe 03's chamfer, which had never applied (#103). `chamfered(distance:)` + bundles all edges into one operation and a full revolve always contributes unblendable seam + edges, so the call returned nil and a `??` fallback hid it. Now selects the OD and raised-face + rim explicitly. +* **Creation**: Recorded the revolve-seams-cannot-be-chamfered decision. ## 2026-08-04 diff --git a/recipes/03-pipe-flange/README.md b/recipes/03-pipe-flange/README.md index 5b21695..9493f5a 100644 --- a/recipes/03-pipe-flange/README.md +++ b/recipes/03-pipe-flange/README.md @@ -29,8 +29,8 @@ what closes the result into a solid rather than a shell (see Gotchas). The bolt is cut with `Shape.circularPatternCut`: a single cylindrical hole tool is built at the bolt-circle radius (oriented along Y), then patterned `boltCount` times around the axis and subtracted as one compound. Finally a chamfer breaks the OD and the raised-face rim, -selected by geometry (radius from the axis, plus `isCircle` to exclude the revolve's seam -edges) rather than by chamfering every edge; see Gotchas. +selected by geometry (a circle concentric with the revolve axis, at a known radius) rather +than by chamfering every edge; see Gotchas. ## OCCTSwift APIs used @@ -68,6 +68,17 @@ edges) rather than by chamfering every edge; see Gotchas. chamfering it together with the rim exhausts the 2mm-tall wall and fails outright. A failed chamfer here is not swallowed: the recipe force-unwraps the result, so a regression crashes loudly instead of silently shipping an un-chamfered body. +- **The selector tests concentricity, not "some point at radius R".** It uses + `centerOfCurvature(at:)` to require the circle's centre on the revolve axis, and + `1 / curvature(at:)` for its true radius. Sampling one point and measuring its distance from + the axis answers a weaker question, and would leave the exclusion of bolt-hole circles resting + on the y-coordinate gate rather than on the geometry itself. +- **Excluding seams by `isCircle` alone only works for a polygonal profile.** A revolve's seam + edges are the profile edges themselves, so with this straight-sided half-section they are + lines. Put an arc or a fillet in the profile and the seam becomes circular, at which point + `isCircle` stops excluding it. `Edge.isSeam(on:)` is the direct test if a variant ever needs + one. Here the concentricity requirement excludes seams regardless, since a seam lies in a + plane through the axis rather than on a circle around it. - Use `circularPatternCut`, **not** `circularPattern`, for the bolt circle. `circularPattern` patterns the whole *body*, applied to a holed flange it produces overlapping flange copies (≈8× the volume) with the holes filled in. `circularPatternCut` patterns the *tool* and diff --git a/recipes/03-pipe-flange/main.swift b/recipes/03-pipe-flange/main.swift index 6453b56..49b8fa6 100644 --- a/recipes/03-pipe-flange/main.swift +++ b/recipes/03-pipe-flange/main.swift @@ -36,6 +36,11 @@ let raisedHeight: Double = 2 // raised-face height above the disk (mm) let boltCircleRadius: Double = 60 // bolt-circle radius (mm) let boltCount: Int = 8 // number of bolt holes let boltRadius: Double = 7 // bolt-hole radius (mm) +// Must stay below both `raisedHeight` and `thickness`: the chamfer is cut into those +// walls, and one taller than the wall it breaks exhausts the material and fails. The +// failure is now a hard crash rather than a silent no-op (see the chamfer step below), +// so this is a real constraint, not a preference. +let chamferDistance: Double = 1 // edge-break size on the OD and raised-face rim (mm) let ctx = ScriptContext(metadata: ManifestMetadata( name: "Pipe flange", @@ -76,18 +81,31 @@ flange = flange.circularPatternCut(tool: holeTool, axisPoint: .zero, // exhausts the 2mm-tall wall and fails outright. Select by geometry, not raw edge // index, so the recipe still finds the right edges if the parameters change // (same reasoning as recipe 01's concaveEdges() selection). -func nearAxisRadius(_ edge: borrowing Edge) -> Double? { - guard edge.isCircle, let bounds = edge.parameterBounds, - let p = edge.point(at: (bounds.first + bounds.last) / 2) else { return nil } - return (p.x * p.x + p.z * p.z).squareRoot() +// +// The test is "is this a circle concentric with the revolve axis, of radius R", not +// "does some point on this edge lie at radius R". Sampling a single point would answer +// the weaker question: a bolt-hole circle spans radius 53 to 67 here, so it would alias +// onto `raisedRadius` the moment `boltCircleRadius` dropped to 55, leaving only the +// incidental y-gate to exclude it. `centerOfCurvature` pins the circle to the axis and +// `1 / curvature` gives its true radius, so the predicate says what it means. +func axisCircleRadius(_ edge: Edge) -> Double? { + guard edge.isCircle, + let bounds = edge.parameterBounds else { return nil } + let mid = (bounds.first + bounds.last) / 2 + guard let centre = edge.centerOfCurvature(at: mid), + let k = edge.curvature(at: mid), k > 1e-9 else { return nil } + // Revolve axis is Y, so a concentric circle's centre sits on x = z = 0. + guard abs(centre.x) < 1e-6, abs(centre.z) < 1e-6 else { return nil } + return 1 / k } -let chamferTargets = flange.edges(where: { edge in - guard let r = nearAxisRadius(edge) else { return false } +let chamferTargets = flange.edges { edge in + guard let r = axisCircleRadius(edge) else { return false } if abs(r - outerRadius) < 1e-3 { return true } // OD, front + back if abs(r - raisedRadius) < 1e-3 && edge.bounds.min.y > thickness + 1e-3 { return true } // raised-face rim return false -}) -flange = flange.chamferedWithFullHistory(distance: 1.0, edges: chamferTargets.map(\.index))!.result +} +flange = flange.chamferedWithFullHistory(distance: chamferDistance, + edges: chamferTargets.map(\.index))!.result try ctx.add(flange, color: C.brass, name: "Pipe flange")