fix(#639): the fillet family reports declined edges instead of only skipping them - #709
Conversation
…kipping them BRepFilletAPI_MakeFillet::Add silently does nothing for an edge it cannot fillet, most commonly a free-boundary edge of an open shell, so filleted(edges:radius:), filleted(edges:startRadius:endRadius:) and filletEvolving(_:) built successfully while skipping the edge with no way for a caller to learn which one or how many. filletedWithReport(edges:radius:), filletedWithReport(edges:startRadius:endRadius:) and filletEvolvingWithReport(_:) are new, additive siblings that return a Shape.FilletResult naming the declined edges by index. The bridge computes this from Contour(edge) == 0 after Add() and before Build(), the only signal OCCT itself exposes for a declined edge: there is no reason available, only which. Two of the issue's own named members needed no new code at all, only documentation: filletedWithFullHistory's ShapeHistoryRecord already distinguishes a declined edge via !isDeleted && generated.isEmpty, and FilletBuilder.contour(for:) already answers the identical question. Skip stays the behaviour for every entry point; converging on reject was considered and rejected, since it would change filleted/blendedEdges on every open shell. Closes #639 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Independent verificationReport rather than reject is the right call, and the reasoning is the part that matters: Measured the decline set myself, building the census's open-shell fixture independently: Matches the census and the PR exactly. Then checked the correction to the census, which is the most useful thing in this report. The The query already existed and nobody had tried it. Worth noting my first attempt at that probe was my Injection, mine rather than the matrix in the PR: making Verified: all three CI checks green including the full macOS suite, four gate scripts and their The self-correction worth keepingThe report says its own first hypothesis for the history recipe was wrong: a declined edge is not Merge order, since three PRs are open into this branch#706 and #709 both touch Suggested: #706, then #708, then #709. #706 before #708 because the bridge guard must land before |
Code review (independent)Scope: the PR diff (+737/-30, 16 files), cross-checked against OCCT's refman contract for Correctness -- verified, sound
ConventionsFollows the repo's patterns closely: Test coverageStrong. 7 cases cover all five issue-named members plus two closed-solid negative controls; the declined set Performance / security
Nits (non-blocking)
VerdictApprove. The design choice (observe, don't reject) is the right one and argued with institutional memory; the mechanism rests on a verified OCCT contract ( |
Review nits, plus the test gap it noted alongside them. nit 1: declinedEdgeIndices mirrors the request list rather than deduplicating, so a declined edge named twice is reported twice. Undocumented until now, and the review also noted no test drove a duplicate through a WithReport method, so the two close together. The test asserts count == 2 and Set == [declined], and deduplicating the report inside occtFilletWriteDeclined fails it, which is the plausible-but-wrong implementation it exists to rule out. The fixture needs an accepted edge alongside the duplicated declined one. My first version requested only the declined edge twice, which leaves nothing to fillet, so Build() fails and the call returns nil with no report to read. That is different behaviour, not this one. nit 2: outDeclinedCount's header contract said "0 if the call fails before OCCT is touched", which does not cover a Build() failure, where the count is written and NULL is returned. Now says to read it only when the returned shape is non-NULL, and why. nit 3: the - Returns: line ended "including an edge that is not this shape's" with no noun, on all four methods. Pre-existing on the two plain siblings and copied onto the two new ones; completed in all four. SEMVER arithmetic re-checked after the merge, since #705 added an exception: three plus ten is thirteen sections, so twelve to thirteen, "other nine" to "other ten", "a thirteenth was not taken" to "a fourteenth", and #639's own note about not moving the count updated to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All four nits addressed, plus the test gap you noted alongside them. Nit 1 and the test gap close together. Worth recording that my first version of that test was wrong: it requested only the declined edge, twice. That leaves nothing to fillet, so Nit 2 correct: the contract did not cover a Nit 3 done on all four, including the two pre-existing siblings the truncation was copied from. Nit 4, merge order: rebased onto the merged base. #706 and #708 are both in. Two conflicts, both taking each side: the census README wanted base's And the SEMVER arithmetic had gone stale again, exactly as predicted. #705 added an exception, so the counters were one behind: three plus ten is thirteen sections, against a headline saying twelve. Fixed to thirteen, "other ten", "a fourteenth was not taken", and this PR's own note about not moving the count. That is the third time those counters have drifted; worth a gate script eventually. Verified: full suite 5,356 tests, 0 failures, censuses 45 and 16, four gate scripts and their three Thanks for verifying the |
What & why
BRepFilletAPI_MakeFillet::Addsilently does nothing for an edge it cannot fillet, most commonly a free-boundary edge of an open shell.filleted(edges:radius:),filleted(edges:startRadius:endRadius:)andfilletEvolving(_:)have always skipped such an edge rather than rejecting the whole call, correctly, but had no way to tell a caller which edges those were, or how many.The Cluster B census (
Scripts/repro/cluster-b-fillet-edge-contract/) measured this concretely: filleting all 12 edges of an open shell (a box with one face dropped, sewn) accepts 8 and silently declines 4 ([6, 9, 10, 11]).Closes #639
The decision
Report, do not reject. Converging every declining entry point onto rejecting a batch with any declined edge would change
filleted(edges:radius:),filleted(edges:startRadius:endRadius:)andblendedEdges(_:)on every open shell: a behaviour change wider than this issue, and the same mistake an earlier draft of #633 made and withdrew. Skip stays the answer; the fix is observability, following #482'sFillingSurface.refusedConstraintCountprecedent.Contour(edge) == 0, populated byAdd()and notBuild(), is the only signal OCCT itself exposes for a declined edge.Addreturns nothing, andNbFaultyContours()/BadShape()/StripeStatus()describe a contour that failed duringBuild(), which an edge OCCT never added to any contour never reaches. So this reports which edges were declined (a list of indices, not just a count), and not why: no reason is reachable from this API.What changed
Three genuinely new entry points, for the three members that had no side channel at all:
filletedWithReport(edges:radius:)filletedWithReport(edges:startRadius:endRadius:)filletEvolvingWithReport(_:)Each returns a new
Shape.FilletResult { shape, declinedEdgeIndices }. The bridge computes the report with a new shared helper (occtFilletDeclinedIndices/occtFilletWriteDeclined) that re-checksContour(edge)for each requested index afterAdd()and beforeBuild(). The three underlying bridge functions gained two nullable trailing out-parameters; existing non-reporting call sites passniland see no change in cost or behaviour.Two of the issue's own named members needed no new code, only documentation, measured rather than assumed:
filletedWithFullHistory(radius:edges:)'sShapeHistoryRefalready distinguishes a declined edge via!record.isDeleted && record.generated.isEmpty. My first hypothesis for this recipe was wrong, and measuring it caught that: a declined edge is not necessarilymodified.isEmptytoo. On the shell fixture every declined edge shows onemodifiedentry, a different edge instance with a shorter length (10.0 to 8.0), because an accepted neighbour's fillet trims the declined edge's shared endpoint.modifiedalone is not the signal;generated/isDeletedare.FilletBuilder.contour(for:)already answers the identicalContour(edge) == 0question directly, readable right afteraddEdgewith nobuild()required. This also corrects a Cluster B census finding: its claim that the class API has "no per-edge signal beyondaddEdge's own Bool return" was wrong, it simply never tried this query. Measured directly: a foreign edge from a different shape givesaddEdgereturnstrue,contour(for:)returns0, the identical signal a genuine same-shape decline gives.blendedEdges(_:)(#633's own site) does not adopt this mechanism here, and is out of scope for this PR per the task boundaries. Recommendation for #633: adopt the sameFilletResult-shaped report, extended to also carry which duplicate indices were overwritten.Docs
docs/SEMVER.md: recorded as additive (not one of the twelve "recorded exceptions", since nothing existing changed signature or behaviour; checked and confirmed the counter arithmetic is unaffected).docs/CHANGELOG.md,docs/API_REFERENCE.md(+3 to the operation count, 4301 to 4304),README.mdheadline count.docs/reference/Shape-Features.md,Shape-Measurement.md,Shape-Healing.md: new entries plus the declined-edge recipe forfilletedWithFullHistory.Scripts/repro/cluster-b-fillet-edge-contract/README.mdandClusterB.swift: the measured grid values did not move (still SKIP/REJECT the same way), only the annotations on the now-fixed rows, plus theFilletBuilderclass-API row's correction.swift run Censuses cluster-bstill emits 16 rows,cluster-astill 45.Test plan
Tests/OCCTModelingTests/Issue639FilletDeclinedEdgeReportTests.swift, 7 cases covering all 5 members named in the issue plus 2 negative controls, followingokf/policies/prove-the-test-fails.md. Injection matrix:occtFilletWriteDeclinedreports emptyWithReport"names declined set" tests fail; the 2 "empty on closed solid" negative controls correctly stay greenOCCTFilletBuilderContouralways returns 1FilletBuilderrecipe test fails (both its assertions)OCCTBooleanHistoryIsDeletedalways returns trueEach injection restored and confirmed green again afterward.
swift build, 0 errors, no new warnings, noOCCTSWIFT_LOCAL.swift test, no env overrides: 5353 tests passing (baseline 5346 + 7 new), 2 consecutive clean runs, no crash flake.swift run Censuses cluster-astill 45 rows,cluster-bstill 16 rows.check-bridge-index.py,check-null-handle-guards.py,check-docs-defaults.pyeach pass with and without--self-test;count-operations.py(no--self-test) passes, README/API_REFERENCE totals now match derived (4304).Notes for the reviewer
FilletBuilder'scontour(for:)gives you0for both "never added" and "added but declined". Documented explicitly; a caller who wants to tell these apart has to track its own added-edges list, which it already does since it calledaddEdgeitself.