diff --git a/Scripts/repro/censuses/ClusterB.swift b/Scripts/repro/censuses/ClusterB.swift index 22f0ea15..a67046c3 100644 --- a/Scripts/repro/censuses/ClusterB.swift +++ b/Scripts/repro/censuses/ClusterB.swift @@ -443,19 +443,26 @@ enum ClusterB { // MARK: - chamfer2D(edgePairs:distances:) [2D, face-level, batch] do { - // NOT called live: rectFace.chamfer2D(edgePairs: [(0, 1), (0, 1)], distances: [1.0, 2.0]) - // SIGSEGVs (uncatchable -- an OS signal, the same shape of defect CLAUDE.md's "Known - // OCCT Bugs" section already documents for this family). Confirmed in isolation before - // writing this note: it crashes on the FIRST duplicated-pair call, immediately, with no - // other statement in this block reached first. Likely mechanism (not chased further, - // since diagnosing it is a fix, not a census): BRepFilletAPI_MakeFillet2d::AddChamfer - // rebuilds the face incrementally, so the edge handles resolved from the ORIGINAL - // `edgeMap` before the loop starts are stale by the second call on the same pair -- - // `fillet2D`'s equivalent duplicate-vertex call does NOT crash (measured below), so - // this is specific to AddChamfer, not the shared TopTools_IndexedMapOfShape lookup. - // This is a NEW finding, not previously in #520/#568/#612/#633/#639, and out of scope - // to fix here per #665's own instruction that this is the census only. - let dupVerdict = "CRASH (SIGSEGV, uncatchable): a duplicated edge pair [(0,1),(0,1)] crashes BRepFilletAPI_MakeFillet2d::AddChamfer on its second call. Not run live in this census -- see comment above." + // #705: this used to be UNSAFE to call live. A duplicated edge pair, e.g. + // [(0,1),(0,1)], SIGSEGV'd (uncatchable -- an OS signal) inside the repeat call's own + // BRepFilletAPI_MakeFillet2d::AddChamfer, which rebuilds the face incrementally, so a + // second call naming the same pair resolved against edge handles the first call had + // already made stale. Fixed in OCCTFace2DChamfer (OCCTBridge_Modeling.mm) by rejecting + // the whole call when any two entries name the same pair, in either order, before + // AddChamfer ever sees the second one -- matching fillet2D's own contract for a + // duplicated vertex on this same builder (#568). Reusing ONE edge across two + // DIFFERENT pairs (chamfering adjacent corners of a polygon) is unaffected and still + // measured non-nil below; only the identical pair repeated is refused. Now run live. + let dup = rectFace.chamfer2D(edgePairs: [(0, 1), (0, 1)], distances: [1.0, 2.0]) + let reversedDup = rectFace.chamfer2D(edgePairs: [(0, 1), (1, 0)], distances: [1.0, 2.0]) + let sharedEdgeDifferentPairs = rectFace.chamfer2D( + edgePairs: [(0, 1), (1, 2)], distances: [1.0, 1.0]) + let dupVerdict: String + if dup == nil && reversedDup == nil && sharedEdgeDifferentPairs != nil { + dupVerdict = "REJECT (nil) on a duplicated edge pair, order-independent (fixed by #705, was CRASH: SIGSEGV, uncatchable); one edge across two DIFFERENT pairs stays non-nil" + } else { + dupVerdict = "unexpected: dup \(dup == nil ? "nil" : "non-nil"), reversedDup \(reversedDup == nil ? "nil" : "non-nil"), sharedEdgeDifferentPairs \(sharedEdgeDifferentPairs == nil ? "nil" : "non-nil")" + } let outOfRange = rectFace.chamfer2D(edgePairs: [(0, 1), (2, 99_999)], distances: [1.0, 1.0]) record("2D chamfer (face)", "chamfer2D(edgePairs:distances:)", duplicate: dupVerdict, diff --git a/Scripts/repro/cluster-b-fillet-edge-contract/README.md b/Scripts/repro/cluster-b-fillet-edge-contract/README.md index 11723603..e32a53ff 100644 --- a/Scripts/repro/cluster-b-fillet-edge-contract/README.md +++ b/Scripts/repro/cluster-b-fillet-edge-contract/README.md @@ -68,7 +68,7 @@ python3 Scripts/repro/cluster-b-fillet-edge-contract/classify_fillet_sites.py -- | chamfer (with history) | `chamferedWithFullHistory(distance:edges:)` | N/A: uniform distance (dup 995.000000 == single-edge 995.000000) | REJECT (nil) | SKIP (non-nil, area 476.191927 vs unfilleted 500.000000) | REJECT (nil) | | offset-per-face | `offsetPerFace(defaultOffset:faceOffsets:)` | N/A: `faceOffsets` is a `Dictionary`, a duplicate key cannot be constructed | REJECT (nil), fixed by **#541** (see "Corrections" below) | N/A: `BRepOffset_MakeOffset` has no per-face decline analogous to `Add()` on a free-boundary edge | ACCEPTS (non-nil): applies `defaultOffset` uniformly, a legitimate no-override request | | 2D fillet (face) | `fillet2D(vertexIndices:radii:)` | REJECT (nil) on a duplicated vertex index | REJECT (nil), fixed by #568 | UNMEASURED: no open/degenerate planar-face fixture built here | REJECT (nil), Swift-side guard | -| 2D chamfer (face) | `chamfer2D(edgePairs:distances:)` | **CRASH (SIGSEGV, uncatchable)** on a duplicated edge pair -- see "New findings" below | REJECT (nil), fixed by #568 | UNMEASURED, same reason as `fillet2D` | REJECT (nil), Swift-side guard | +| 2D chamfer (face) | `chamfer2D(edgePairs:distances:)` | REJECT (nil), order-independent, fixed by #705 (was **CRASH (SIGSEGV, uncatchable)**, see "New findings" below) | REJECT (nil), fixed by #568 | UNMEASURED, same reason as `fillet2D` | REJECT (nil), Swift-side guard | | fillet (class API) | `FilletBuilder.addEdge(_:radius:)` | **OVERWRITE: last radius wins**, same mechanism as #633, unaudited by #489/#520/#568 | N/A: takes an `Edge`, not an index | foreign edge (a different `Shape` entirely): `addEdge` returns `true`, `build()` REJECT (nil) | `build()` with zero `addEdge` calls: REJECT (nil) | | chamfer (class API) | `ChamferBuilder.addEdge(_:distance:)` | **FIRST WINS**, matches `chamferedTwoDistances` -- `addEdge` itself prefers the first call, not just the bridge's hand-rolled loop | N/A: takes an `Edge`, not an index | foreign edge: `addEdge` returns `true`, `build()` REJECT (nil) | `build()` with zero `addEdge` calls: REJECT (nil) | @@ -102,16 +102,23 @@ family, not just deciding to reject/dedupe/document. ## New findings (not in #520/#568/#612/#633/#639) -1. **`chamfer2D(edgePairs:distances:)` SIGSEGVs (uncatchable) on a duplicated edge pair.** - `rectFace.chamfer2D(edgePairs: [(0, 1), (0, 1)], distances: [1.0, 2.0])` crashes the process on +1. **`chamfer2D(edgePairs:distances:)` SIGSEGVs (uncatchable) on a duplicated edge pair -- + fixed by #705, this census's own row now measures it live.** At the time this census landed, + `rectFace.chamfer2D(edgePairs: [(0, 1), (0, 1)], distances: [1.0, 2.0])` crashed the process on the pair's *second* occurrence. Confirmed in isolation before writing the census's own note (not run live in the shipped artifact -- an OS signal is uncatchable, per this repo's own `CLAUDE.md` precedent for this exact family). `fillet2D`'s equivalent duplicate-*vertex* call - does **not** crash (it rejects, cleanly), so this is specific to `BRepFilletAPI_MakeFillet2d - ::AddChamfer`, not the shared `TopTools_IndexedMapOfShape` lookup both functions use. Likely - mechanism, not chased further since diagnosing it is a fix and this is a census: `AddChamfer` - rebuilds the face incrementally, so the edge handles resolved from the *original* `edgeMap` - before the loop starts are stale by a second call naming the same pair. + does **not** crash (it rejects, cleanly), so this was specific to `BRepFilletAPI_MakeFillet2d + ::AddChamfer`, not the shared `TopTools_IndexedMapOfShape` lookup both functions use. The + mechanism this census guessed at without chasing further -- diagnosing it was a fix, not a + census -- was confirmed exactly by #705: `AddChamfer` rebuilds the face incrementally, so the + edge handles resolved from the *original* `edgeMap` before the loop starts were stale by a + second call naming the same pair, order-independent (`(0, 1)` then `(1, 0)` crashed identically + to `(0, 1)` twice). #705 fixed it by rejecting the whole call on a repeated pair, in either + order, before `AddChamfer` sees the second one -- reusing one edge across two *different* pairs + (e.g. chamfering adjacent corners of a polygon with `(0, 1)` then `(1, 2)`) is unaffected and + still measures non-nil. `ClusterB.swift`'s own `chamfer2D` block now calls the duplicate case + live rather than noting it as unsafe. 2. **The `FilletBuilder`/`ChamferBuilder` class API has zero index resolution or value validation of any kind.** No `occtUseSubShapesByIndex`, no `occtValidFilletRadius`. A foreign edge (one belonging to an entirely different `Shape`) is silently accepted by `addEdge` (returns `true`) diff --git a/Sources/OCCTBridge/src/OCCTBridge_Modeling.mm b/Sources/OCCTBridge/src/OCCTBridge_Modeling.mm index 339142c9..067a1978 100644 --- a/Sources/OCCTBridge/src/OCCTBridge_Modeling.mm +++ b/Sources/OCCTBridge/src/OCCTBridge_Modeling.mm @@ -4280,6 +4280,32 @@ OCCTShapeRef OCCTFace2DChamfer(OCCTShapeRef shape, TopoDS_Shape e1 = occtMappedSubShapeAt(edgeMap, edge1Indices[i]); TopoDS_Shape e2 = occtMappedSubShapeAt(edgeMap, edge2Indices[i]); if (e1.IsNull() || e2.IsNull()) return nullptr; + + // #705: the exact same edge pair named twice SIGSEGVs, uncatchably, inside the repeat + // call's own BRepFilletAPI_MakeFillet2d::AddChamfer. Measured order-independent: (0,1) + // then (1,0) crashes the same way as (0,1) twice. Root cause is an upstream OCCT + // defect, not this bridge's: AddChamfer(edge1, edge2, ...) calls + // ChFi2d::FindConnectedEdges to look up the pair's shared vertex and dereferences the + // two edges it returns without checking the returned status first; that lookup leaves + // both edges null on every failure path, and the pair's second call fails it, because + // the shared vertex was already consumed chamfering the pair the first time. The + // sibling overload (AddChamfer(edge, vertex, distance, angle)) checks the identical + // status correctly. Filed upstream as OCCT#1431 (repro) / OCCT#1432 (fix). The kernel + // patch carrying that fix lands in its own PR and is inert until the pinned + // xcframework is rebuilt, so this guard is what protects callers meanwhile. Reusing ONE edge across two DIFFERENT + // pairs is ordinary and measured safe, e.g. chamfering adjacent corners of a rectangle + // with (0,1) then (1,2); only the identical pair repeated crashes, so this checks the + // pair, not the individual indices. Rejected rather than skipped, matching fillet2D's + // own contract for a duplicated vertex (#568): this site already rejects the whole + // batch on one bad index instead of dropping just that entry, and a repeated pair has + // the same "which distance wins" ambiguity a bad index does, so the whole call fails + // instead of guessing. + for (int32_t j = 0; j < i; j++) { + bool sameOrder = edge1Indices[i] == edge1Indices[j] && edge2Indices[i] == edge2Indices[j]; + bool swappedOrder = edge1Indices[i] == edge2Indices[j] && edge2Indices[i] == edge1Indices[j]; + if (sameOrder || swappedOrder) return nullptr; + } + chamfer.AddChamfer(TopoDS::Edge(e1), TopoDS::Edge(e2), distances[i], distances[i]); } diff --git a/Sources/OCCTSwift/Shape+Geom2d.swift b/Sources/OCCTSwift/Shape+Geom2d.swift index f15180ef..3307d775 100644 --- a/Sources/OCCTSwift/Shape+Geom2d.swift +++ b/Sources/OCCTSwift/Shape+Geom2d.swift @@ -55,6 +55,16 @@ extension Shape { /// rather than being skipped (#568). Previously the pair was dropped and the corners that /// did resolve were cut, reported as a complete result. /// + /// - Note: The same edge pair named twice fails the whole call rather than crashing (#705). + /// This is an upstream OCCT defect in `BRepFilletAPI_MakeFillet2d::AddChamfer`, not this + /// wrapper's own: the pair's second call finds its shared vertex already consumed by the + /// first chamfer, and the resulting failure returns two null edges that `AddChamfer` + /// dereferences without checking for first. The process SIGSEGV'd, uncatchably, before this + /// guard existed. The check is order independent: `(0, 1)` and `(1, 0)` name the same pair + /// and both are refused. Reusing one edge across two *different* pairs is unaffected and + /// still works, e.g. chamfering every corner of a rectangle with + /// `(0, 1), (1, 2), (2, 3), (3, 0)`. + /// /// - Parameters: /// - edgePairs: Array of (edge1Index, edge2Index) pairs identifying adjacent edges /// - distances: Chamfer distance for each edge pair @@ -64,6 +74,9 @@ extension Shape { /// let face = Shape.face(from: Wire.rectangle(width: 20, height: 20)!)! /// let cut = face.chamfer2D(edgePairs: [(0, 1), (2, 3)], distances: [2, 2]) /// print(cut?.edgeCount ?? 0) // 6: two corners replaced by chamfer edges + /// + /// // A repeated pair is refused, not crashed, and not silently collapsed to one chamfer. + /// print(face.chamfer2D(edgePairs: [(0, 1), (0, 1)], distances: [1, 2]) == nil) // true /// ``` public func chamfer2D(edgePairs: [(Int, Int)], distances: [Double]) -> Shape? { guard !edgePairs.isEmpty, edgePairs.count == distances.count else { return nil } diff --git a/Tests/OCCTModelingTests/Issue568IndexSkipTests.swift b/Tests/OCCTModelingTests/Issue568IndexSkipTests.swift index 642850fa..fd095cf2 100644 --- a/Tests/OCCTModelingTests/Issue568IndexSkipTests.swift +++ b/Tests/OCCTModelingTests/Issue568IndexSkipTests.swift @@ -201,4 +201,34 @@ struct Issue568IndexSkipTests { // 4 edges, two corners cut off: each chamfer adds one edge. #expect(result?.edgeCount == 6) } + + // MARK: - 2D chamfer duplicate pair (#705) + + /// #705: the same edge pair named twice used to SIGSEGV the process, uncatchably, inside the + /// second `BRepFilletAPI_MakeFillet2d::AddChamfer` call. Confirmed in a separate process + /// before this fix landed (raw exit code 139); this test only exercises the fixed, in-process + /// behaviour, matching the "ordinary, safe" half of that instruction. The pair is order + /// independent: `(0, 1)` and `(1, 0)` name the same two edges and both are refused. + @Test("A 2D chamfer is refused when the same edge pair is named twice") + func chamfer2DRejectsDuplicatePair() { + let face = Shape.face(from: Wire.rectangle(width: 20, height: 20)!)! + #expect(face.chamfer2D(edgePairs: [(0, 1), (0, 1)], distances: [1.0, 2.0]) == nil) + #expect(face.chamfer2D(edgePairs: [(0, 1), (1, 0)], distances: [1.0, 2.0]) == nil) + #expect(face.chamfer2D(edgePairs: [(0, 1), (0, 1), (0, 1)], distances: [1.0, 1.0, 1.0]) == nil) + } + + /// The duplicate-pair guard has to key on the PAIR, not on either index alone: chamfering + /// every corner of a rectangle legitimately reuses each edge across two DIFFERENT pairs + /// (edge 1 closes both the (0,1) and the (1,2) corner). Measured safe before this fix and + /// must stay safe after it. + @Test("A 2D chamfer still cuts every corner when adjacent pairs share an edge") + func chamfer2DAcceptsSharedEdgeAcrossDifferentPairs() { + let face = Shape.face(from: Wire.rectangle(width: 20, height: 20)!)! + let result = face.chamfer2D( + edgePairs: [(0, 1), (1, 2), (2, 3), (3, 0)], + distances: [1.0, 1.0, 1.0, 1.0]) + #expect(result != nil) + // 4 edges, all four corners cut off: each chamfer adds one edge. + #expect(result?.edgeCount == 8) + } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bc02ff09..68a0656f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -169,6 +169,63 @@ silent no-op on any machine that had built before: the old tag's sources were co under the new version's number. It now requires `HEAD` to be at the tag the script names and aborts otherwise, naming the tree so a diagnostic probe left by an investigation is not destroyed silently. +### `chamfer2D` SIGSEGVs, uncatchably, on a repeated edge pair (#705) + +Found by Cluster B's edge/vertex-index census (#665, `Scripts/repro/cluster-b-fillet-edge-contract/`), +which records the crash rather than running it live, since an in-process OS signal would kill the +census itself. `Shape.chamfer2D(edgePairs:distances:)` crashed the whole process, uncatchably, when +the same edge pair appeared twice: + +```swift +let wire = Wire.polygon3D([SIMD3(0, 0, 0), SIMD3(10, 0, 0), SIMD3(10, 10, 0), SIMD3(0, 10, 0)], closed: true)! +let rectFace = Shape.face(from: wire)! +_ = rectFace.chamfer2D(edgePairs: [(0, 1), (0, 1)], distances: [1.0, 2.0]) // SIGSEGV, exit 139 +``` + +Confirmed in a separate process (a temporarily-repointed `Sources/OCCTTest/main.swift`, restored +after), since an in-process crash kills the test runner rather than failing one test: + +| Input | Before | After | +|---|---|---| +| `[(0, 1), (0, 1)]` | SIGSEGV, exit 139 | `nil`, exit 0 | +| `[(0, 1), (1, 0)]` (reversed, same pair) | SIGSEGV, exit 139 | `nil`, exit 0 | +| `[(0, 1), (0, 1), (0, 1)]` | SIGSEGV, exit 139 | `nil`, exit 0 | +| `[(0, 1), (1, 2)]` (one edge, two different pairs) | non-nil, unaffected | non-nil, unaffected | +| `[(0, 1), (1, 2), (2, 3), (3, 0)]` (every corner) | non-nil, unaffected | non-nil, unaffected | + +The crash is inside the repeat call's own `BRepFilletAPI_MakeFillet2d::AddChamfer`, an OS signal +the bridge's `catch (...)` cannot absorb, and it is an upstream OCCT defect, not this bridge's own. +`AddChamfer(edge1, edge2, ...)` calls `ChFi2d::FindConnectedEdges` to look up the pair's shared +vertex and dereferences the two edges it returns without checking the returned status first, and +that lookup leaves both edges null on every failure path. A pair's second call fails the lookup, +because its shared vertex was already consumed chamfering the pair the first time. The sibling +overload (`AddChamfer(edge, vertex, distance, angle)`) checks the identical status correctly, which +is the precedent the upstream filing cites. Reusing one edge across two *different* pairs, i.e. +chamfering adjacent corners of a polygon, the ordinary multi-corner case, does not crash, measured +above. Only the identical pair repeated does, order-independent. A kernel patch is carried +separately, tracked in a follow-up PR; the guard below is what protects callers until it ships. + +**Fixed**: `OCCTFace2DChamfer` (`OCCTBridge_Modeling.mm`) now checks each pair against every prior +pair in the same call before invoking `AddChamfer`, and rejects the whole request (returns `nullptr`) +on a match in either order. This matches `fillet2D(vertexIndices:radii:)`'s own contract for a +duplicated vertex on the same builder (#568) and the #568 idiom already used one line above in the +same function for an out-of-range index: the whole call fails rather than guessing which of two +distances to keep. #633 is open on the wider family's duplicate-index direction (fillet is +last-wins, chamfer is first-wins) and is deliberately not settled here; this fix removes a crash, +not a vote in that debate, though it is recorded as a data point for it. + +This is a behaviour change on a public API with no compile error, recorded in +[`SEMVER.md`](SEMVER.md#recorded-exception-unreleased-chamfer2d-refuses-a-repeated-edge-pair-instead-of-crashing-705). +A call with no repeated pair, which includes every existing caller, is unaffected. Tests: +`Tests/OCCTModelingTests/Issue568IndexSkipTests.swift`'s `chamfer2DRejectsDuplicatePair` and +`chamfer2DAcceptsSharedEdgeAcrossDifferentPairs`, the latter proven to catch an overly broad fix by +injecting one (reject on any repeated single index rather than a repeated pair) and confirming it +turns the shared-edge test red, then restoring. + +The census's own row for `chamfer2D` is updated from `CRASH (SIGSEGV, uncatchable)` to the measured +`REJECT (nil)`, now safe to run live: `swift run Censuses cluster-b` calls it directly rather than +noting it as unsafe to run. + ### Pass 1b of the #377 duplication audit #### `AAG` rode the lossy `faces()`, so `detectPocketsAAG()` answered 2 or 1 for the same geometry depending on compound member order (#642) diff --git a/docs/SEMVER.md b/docs/SEMVER.md index 789158e6..9a3c324b 100644 --- a/docs/SEMVER.md +++ b/docs/SEMVER.md @@ -34,6 +34,7 @@ The other nine change behaviour **without breaking the build**, which is the set - [#642](#recorded-exception-unreleased-aag-builds-nodes-from-face-occurrences-642) moves `AAG`'s node set from distinct faces to face occurrences, so `detectPocketsAAG()` and `buildAAG().nodes` can return more entries on a shape with a shared face. - [#651](#recorded-exception-unreleased-nbedgesnbfacesnbvertices-are-deprecated-and-forward-to-the-deduplicated-count-651) deprecates `Shape.nbEdges`/`nbFaces`/`nbVertices` in favour of `edgeCount`/`faceCount`/`vertexCount`, and changes what the deprecated three return on the way. - [#699](#recorded-exception-unreleased-aag-adjacency-and-convexity-are-scoped-to-one-solid-699) restricts `AAG`'s adjacency/convexity checks to same-solid face pairs, further changing what `detectPocketsAAG()` can return on a multi-solid compound: the same public API #642 already named, corrected further rather than a new one opened. +- [#705](#recorded-exception-unreleased-chamfer2d-refuses-a-repeated-edge-pair-instead-of-crashing-705) makes `chamfer2D(edgePairs:distances:)` return `nil` on a repeated edge pair; it used to SIGSEGV the process, uncatchably. A thirteenth was **not** taken: [#609](#held-for-the-next-major-v200)'s twelve breaks are held for v2.0.0 instead. @@ -363,6 +364,43 @@ The exception was taken because: - Named in [`CHANGELOG.md`](CHANGELOG.md) with the measurement, and to be named in the release notes. +#### Recorded exception: Unreleased, `chamfer2D` refuses a repeated edge pair instead of crashing (#705) + +**One behaviour change, not a compile error, and not really a break at all: the old answer was an +uncatchable process crash.** `chamfer2D(edgePairs:distances:)` SIGSEGVs when the same edge pair +appears twice, confirmed in a separate process before this fix (raw exit code 139), because of an +upstream OCCT defect: `BRepFilletAPI_MakeFillet2d::AddChamfer` looks up the pair's shared vertex and +dereferences the edges that lookup returns without checking its status first, and the repeat call's +lookup fails because that vertex was already consumed chamfering the pair the first time. Recorded +here before the tag is cut: + +| Break | What a caller does | +|---|---| +| `chamfer2D(edgePairs:distances:)` returns `nil` when the same pair (in either order) appears more than once in `edgePairs` | Nothing on a call with no repeated pair, which includes chamfering every corner of a polygon (adjacent pairs legitimately share one edge, e.g. `(0, 1), (1, 2)`; only the identical pair repeated is refused). A caller building `edgePairs` from a selection or a loop that can produce an exact duplicate now gets `nil` instead of a crash, and should dedupe before calling | + +The exception was taken because: + +- **This is a crash fix, not a contract redesign.** There was no prior "answer" to disagree with: + the process went down. `nil` is strictly more information than a SIGSEGV, and every existing + caller that never produced a duplicate pair is unaffected. +- **It matches the sibling contract already on this builder.** `fillet2D(vertexIndices:radii:)`, + the other entry point on the same `BRepFilletAPI_MakeFillet2d`, already rejects a duplicated + vertex index (#568) rather than doing anything with it. `chamfer2D` could not fail the same + incidental way, since its duplicate crashes inside OCCT before `Build()`/`IsDone()` ever run, so + an explicit guard was required either way, and reject is the answer already established one call + away. +- **Reject, not first-wins or last-wins.** #633 is open on the wider fillet/chamfer family's + duplicate-index direction (fillet is last-wins, chamfer is first-wins, measured by + `Scripts/repro/cluster-b-fillet-edge-contract/`), and this entry point is deliberately left out + of that debate: picking a "wins" direction here would still require silently discarding one of + two distances with no signal to the caller, the same ambiguity #568 already refused to resolve + by guessing. This is a data point for #633, not an attempt to settle it. +- **Reusing one edge across two different pairs is unaffected**, which is pinned by a positive + test chamfering every corner of a rectangle (`(0, 1), (1, 2), (2, 3), (3, 0)`, each edge shared + by two pairs) that must keep succeeding. +- Named in [`CHANGELOG.md`](CHANGELOG.md) with the measurement, and to be named in the release + notes. + ### MINOR — `x.y.0` A minor bump is for additive change. Two routes: diff --git a/docs/reference/Shape-Measurement.md b/docs/reference/Shape-Measurement.md index 0278b1a8..4f94dc8e 100644 --- a/docs/reference/Shape-Measurement.md +++ b/docs/reference/Shape-Measurement.md @@ -885,6 +885,13 @@ public func chamfer2D(edgePairs: [(Int, Int)], distances: [Double]) -> Shape? - **Note:** *either* half of a pair naming no edge of that first face fails the whole call rather than being skipped (#568); previously the pair was dropped and the corners that did resolve were cut, reported as a complete result. +- **Note:** the same edge pair named twice fails the whole call rather than crashing (#705); this + is an upstream OCCT defect in `BRepFilletAPI_MakeFillet2d::AddChamfer`, not this wrapper's own. + The pair's second call finds its shared vertex already consumed by the first chamfer, and the + resulting failure returns two null edges that `AddChamfer` dereferences without checking for + first, and the process SIGSEGV'd, uncatchably, before this guard existed. The check is order + independent, so `(0, 1)` and `(1, 0)` both name the refused pair; reusing one edge across two + *different* pairs (chamfering every corner of a rectangle) is unaffected. ---