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 1b3c714..9493f5a 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 (a circle concentric with the revolve axis, at a known radius) 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,31 @@ 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. +- **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 fd1a45c..49b8fa6 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 @@ -27,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", @@ -60,8 +74,38 @@ 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). +// +// 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 { 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: chamferDistance, + 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 a4e40e0..c4f4340 100644 Binary files a/recipes/03-pipe-flange/output.png and b/recipes/03-pipe-flange/output.png differ