fix(#633): blendedEdges reports which duplicate entries were overwritten - #712
Conversation
BRepFilletAPI_MakeFillet::Add(radius, edge) writes to an edge's own slot within its fillet contour, so naming the same edge index twice in blendedEdges(_:)'s edgeRadii silently overwrites the first radius with the second, with no signal to the caller. Re-measured on this tree: last-wins, confirmed. Sequenced after #639, which recommended extending the same FilletResult shape rather than a new one. blendedEdgesWithReport(_:) adds overwrittenDuplicateIndices (computed Swift-side, no OCCT round trip needed) and also adopts #639's declined-edge mechanism for this entry point, so both silent axes of this call are covered rather than leaving one blank. blendedEdges(_:) itself is unchanged. Closes #633
Independent verificationIt took #709's recommendation rather than inventing a second reporting shape, which was the main risk here. Measured the behaviour myself: The triple case is the one worth having: reporting per losing entry rather than per distinct edge is what makes the count mean "how many radii were discarded", and it is the semantics that a deduplicating implementation would silently get wrong. My own injection, replacing the computed list with Verified: full suite 5,363 tests, 0 failures, four gate scripts and their three One process note, not about this PR's contentThe agent reported back twice without a PR URL, both times just saying it was waiting on CI. The work was complete and pushed both times. Worth knowing when reading the task log rather than the branch. On SEMVERI asked for a recorded-exception entry and a counter re-check when dispatching this. That instruction was wrong, and I have stopped giving it. The recorded-exception mechanism is for breaks shipping within a major line, which |
ReviewScope reviewed: the full 11-file diff (bridge header/impl, Swift API, new test file, docs). I fetched the surrounding context this diff builds on — OverviewAdds Verified correctness
One finding: the new init's doc comment misattributes why it's neededThe doc comment reads: "...so the three existing Minor / disclosed-scope notes (not defects)
Code quality / conventions / performance / security / test coverage
Bottom lineSolid, low-risk change: it reuses an established mechanism correctly, the one non-obvious Swift-semantics claim it makes checks out under an actual compiler, and the buffer-safety contract holds. The only thing worth fixing before/after merge is the inverted reasoning in the new init's doc comment — cosmetic, not a functional issue. 🤖 Posted by Claude Code review |
…d three Review finding. The comment said the three existing WithReport call sites need the explicit init to keep defaulting to empty. They do not: a let with a default is dropped from the synthesized memberwise init entirely, so those three would have compiled unchanged and silently taken the default. The actual reason is blendedEdgesWithReport, the first caller that needs to pass a NON-default value, which a synthesized init structurally cannot express. Correct fix, wrong stated cause, pointing at the callers that were never at risk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed. The comment pointed at the three existing Thanks for compiling the Not acted on, and I agree with your framing: 7 tests pass, four gate scripts green, zero em-dashes. |
What & why
BRepFilletAPI_MakeFillet::Add(radius, edge)resolves an edge's own slot within its filletcontour and writes there, so
blendedEdges(_:)naming the same edge index twice in itsedgeRadiilist writes that slot twice: the secondAddsilently overwrites the first radius,with no exception, no false return, and nothing in the result to say a radius was ever discarded.
Re-measured on this tree rather than trusted from the issue (
swift run Censuses cluster-b):blendedEdges([(0, 2.0), (0, 5.0)])gives the identical volume (946.349541) toblendedEdges([(0, 5.0)])and differs fromblendedEdges([(0, 2.0)])(991.415927) -- last-wins,confirmed.
Closes #633
This is the last open member of Cluster B (#665).
The decision
Sequenced deliberately after #639, which decided the reporting mechanism for this same family's
other silent axis (an edge OCCT declines outright) and, in its own PR, recommended extending the
same
Shape.FilletResultshape to also cover this issue's axis rather than re-deriving amechanism. That recommendation is taken here; I found no reason to deviate from it.
Report, do not reject or pick a "winning" direction. Converging
blendedEdges(_:)onto reject,or onto first-wins (matching the chamfer family, measured below), would change what it returns for
every existing caller who happens to name an edge twice and currently gets a built shape -- a
behaviour change wider than this issue, and the same move #639 rejected for its own axis.
blendedEdges(_:)itself is byte-for-byte unchanged.Fillet and chamfer disagree on duplicate-index direction, measured again rather than assumed.
Scripts/repro/cluster-b-fillet-edge-contract/'s own headline finding stands: the fillet family(
blendedEdges,filletEvolving,FilletBuilder.addEdge) is last-wins, the chamfer family(
chamferedTwoDistances,chamferedDistAngle,ChamferBuilder.addEdge) is first-wins, bothinternally consistent within their own family. Unifying the two onto one direction is a bigger,
separate decision (picking a direction and changing it for a whole family) than this issue's own
title suggests, and is left open.
What changed
blendedEdgesWithReport(_:), a newWithReportsibling, returns the existingShape.FilletResult-- extended with a second field,overwrittenDuplicateIndices-- rather thana new result type (#490's standing lesson against parallel encodings of one idea). It mirrors
declinedEdgeIndices's own convention: an edge requested three times, with two of those overwritten,reports
[edge, edge], not[edge]-- the count matches how many entries of the caller's list werediscarded, not how many distinct edges were involved.
Computing the duplicate report needs no OCCT round trip at all: an edge index maps to exactly one
edge via
Shape's ownTopExpenumeration, so which entries lose is a property of the caller'sedgeRadiiarray alone, resolved Swift-side (Shape.overwrittenDuplicateIndices(in:),Shape+Modeling.swift).blendedEdgesWithReport(_:)also adopts #639's declined-edge mechanism for this entry point --OCCTShapeBlendEdgesgained the same two nullable trailing out-parametersOCCTShapeFilletEdges/OCCTShapeFilletEdgesLinearalready carry, threaded through the sharedocctShapeFilletEdgeListskeleton -- rather than leaving that field permanently empty for this onesibling. A report that answered one axis and silently stayed blank on the other would be the same
shape of silent-wrong-answer this whole cluster exists to fix.
FilletResultgained an explicitpublic init: aletproperty with a default value is droppedentirely from Swift's synthesized memberwise init (only a
varwith a default is exposed as anoverridable parameter -- confirmed by hand before relying on it, see the doc comment on the new
init), so the three existing
WithReportcall sites needed an explicit default to keep compilingunchanged.
filletEvolving(_:)measures the identical last-wins mechanism (documented onEvolvingFilletEdge's own doc comment, confirmed again by this PR's re-measurement), and isdeliberately left alone: extending
filletEvolvingWithReport(_:)with the same field is a naturalfollow-up, not a requirement of an issue scoped to
blendedEdges(_:)by its own title.Docs
docs/SEMVER.md: recorded as additive, following The fillet family cannot tell a caller that OCCT declined some of the edges it named #639's own "not an exception" precedent --re-checked the counter arithmetic (three non-compiling + ten behavioural = thirteen sections) and
confirmed this PR does not move it.
docs/CHANGELOG.md,docs/API_REFERENCE.md(+2 to the derived operation count viaScripts/count-operations.py --fix: the new method, and the new explicitFilletResult.initthecounter also picks up as a public entry point -- 4,304 to 4,306),
README.mdheadline count.docs/reference/Shape-Features.md:Shape.FilletResult,blendedEdges(_:)and the newblendedEdgesWithReport(_:)entries.Scripts/repro/cluster-b-fillet-edge-contract/README.mdandClusterB.swift: the measured gridvalues did not move (still OVERWRITE/SKIP the same way), only the annotations on the now-fixed
row, plus a paragraph correcting the "one contract, chosen and applied" framing to "report, not
converge," matching what The fillet family cannot tell a caller that OCCT declined some of the edges it named #639 actually did for its own axis.
Test plan
New test:
Tests/OCCTModelingTests/Issue633BlendedEdgesDuplicateReportTests.swift, 7 cases,following
okf/policies/prove-the-test-fails.md. Injection matrix (each restored andconfirmed green afterward):
return [])overwrittenDuplicateIndicesfail; the 4 that expect it empty, or checkdeclinedEdgeIndicesonly, correctly stay greenArray(Set(overwritten))) instead of mirroring the request[0, 0]collapses to[0]); the single-duplicate test is insensitive to this distinction and stays greenOCCTShapeBlendEdgesstops forwarding its two new out-parameters (nullptr, nullptralways)declinedEdgeIndicesfail; both duplicate-only tests, unaffected by this axis, correctly stay greenClean
swift build, 0 errors, no new warnings, noOCCTSWIFT_LOCAL, no localLibraries/(pins
v2.0.0-kernel.1).Full
swift test, no env overrides: 5,363 tests passing (baseline 5,356 + 7 new), 2consecutive clean runs, no crash flake.
swift run Censuses cluster-a45 rows,cluster-b16 rows -- unchanged.Four gate scripts (
check-bridge-index.py,check-null-handle-guards.py,check-docs-defaults.py,count-operations.py) and the three--self-tests, from the reporoot: all pass.
Zero em-dashes (code, docs, this PR body).
Notes for the reviewer
see "The decision" above. Converging the two families is a real question with a real cost (a
behaviour change on every existing caller of at least one family), and is deliberately left open
rather than folded into an observability fix.
overwrittenDuplicateIndicesis computed entirely Swift-side; the only bridge change isthreading The fillet family cannot tell a caller that OCCT declined some of the edges it named #639's existing declined-edge out-parameters through
OCCTShapeBlendEdges, followingthe exact pattern already established for its three siblings.
FilletResult.initneeding to be explicit (rather than relying on the synthesizedmemberwise init) surprised me -- verified with a standalone Swift snippet before writing the fix,
since a
letproperty's default silently not becoming an overridable init parameter is easy toassume works like a
var's does.