Modeling Algorithms - Guard silently-uncapped solid in BRepOffsetAPI_ThruSections - #1462
Open
gsdali wants to merge 1 commit into
Open
Modeling Algorithms - Guard silently-uncapped solid in BRepOffsetAPI_ThruSections#1462gsdali wants to merge 1 commit into
gsdali wants to merge 1 commit into
Conversation
This was referenced Aug 14, 2026
CreateRuled() and CreateSmoothed() both call the static MakeSolid() helper to
close a loft's two open ends, then call Done() unconditionally regardless of
whether MakeSolid() actually built the cap faces.
MakeSolid() caps each end via PerformPlan(), which finds a plane
(BRepBuilderAPI_FindPlane, then a BRepLib_FindSurface-backed MakeFace(wire)
fallback that likewise only matches a plane or a surface already attached to
the wire's edges, never a cylinder, cone, or general surface). A closed
section wire with two or more periods of out-of-plane variation around the
loop is genuinely non-planar and has no such surface, so PerformPlan() fails.
MakeSolid() already tracks this in its own local `B`, threaded through both
PerformPlan() calls, and discards it: it force-marks the shell and solid
Closed(true) unconditionally before returning. The unconditional Done()
afterward makes IsDone() report true and Shape() return a TopoDS_Solid that
is missing both end faces and is not actually closed. BRepCheck_Analyzer
catches this later with no localized error (errorCount 0, empty
detailedCheckStatuses), by which point most callers have already trusted
IsDone().
Fix: after the capping block, if `B` is still false, nullify both output
faces and throw StdFail_NotDone, matching the exception MakeSolid() already
throws for a null shell two lines above. No signature change and no
call-site change: both call sites already run inside Build()'s only
try/catch (catch (Standard_Failure const&) { NotDone(); return; }), so the
throw propagates there and IsDone() correctly reads false.
The nullify matters because face1/face2 alias the caller's myFirst/myLast,
exposed publicly via FirstShape()/LastShape(): if wire1's PerformPlan()
succeeds (writing a real face) but wire2's fails, `B` ends false on wire2's
failure alone, and without the nullify myFirst would still hold a genuine,
but never-added-to-the-shell, face after a failed Build().
This intentionally does not check face nullness (myFirst.IsNull() ||
myLast.IsNull()): PerformPlan() also leaves the output face null, and
returns true (no failure), when every edge of the wire is degenerate -- a
single-vertex "point" section built via AddVertex(), e.g. a cone's apex,
which needs no cap face at all. A null-face check cannot tell that apart
from a genuine capping failure; `B` already carries the distinction, since
PerformPlan()'s degenerate-wire shortcut leaves it true. An earlier attempt
at this fix used the null-face check and regressed
BOPAlgo_PaveFillerTest.FuseConeLoftWithBox_DegeneratedEdge (a
circle-to-vertex loft) for exactly this reason; this version does not.
Repro: a closed wire with r held constant and z = amp * cos(k * theta) caps
fine at k = 0 (flat) and k = 1 (nonzero z-spread but secretly planar, the
intersection of the cylinder r = const with a tilted plane) and fails
silently at k >= 2, for isSolid = true with isRuled either true or false.
Root-caused from a bevel gear ThruSections loft (period == teeth >= 2 from a
per-vertex radial * sin(pitchAngle) term) in
SecondMouseAU/OCCTSwiftScripts#108; the resulting heal()-demotes-solid-to-shell
symptom is SecondMouseAU/OCCTSwift#702; this is filed as its own root-cause
issue at SecondMouseAU/OCCTSwift#905.
Adds two GTest cases to BRepOffsetAPI_ThruSections_Test.cxx:
NonPlanarClosedWireCappingFails (a two-wire k=2 loft, must now fail) and
DegenerateVertexEndStillSucceeds (a circle-to-vertex loft, the shape of the
regression above, must still succeed).
Verified with clang-format (no diff against this tree's .clang-format) and
by override-linking both the unpatched and patched .cxx ahead of the
Libraries/OCCT.xcframework archive bundled in the downstream OCCTSwift repo:
against the unpatched override the new NonPlanarClosedWireCappingFails test
fails exactly as described above (IsDone() reads true); against the patched
override it passes, along with DegenerateVertexEndStillSucceeds and all
three pre-existing tests in the file. Also compiled and ran the actual
BOPAlgo_PaveFiller_Test.cxx FuseConeLoftWithBox_DegeneratedEdge test
unmodified against the patched override: passes, directly confirming this
fix does not reintroduce the first attempt's regression.
gsdali
force-pushed
the
fix/thrusections-silent-solid-capping
branch
from
August 14, 2026 12:13
b3e7d4b to
43309fd
Compare
Contributor
Author
|
Pushed a small strengthening found during downstream review (SecondMouseAU/OCCTSwift#909): if |
gsdali
added a commit
to SecondMouseAU/OCCTSwift
that referenced
this pull request
Aug 14, 2026
Real findings from /code-review, addressed: - MakeSolid()'s new throw could leave face1/face2 (aliased to myFirst/ myLast, exposed via FirstShape()/LastShape()) holding a real, never-added-to-the-shell face when one wire's PerformPlan() succeeded before the other's failed. Nullify both before throwing. Re-verified via override-link: same 5/5 GTest pass, same FuseConeLoftWithBox_DegeneratedEdge pass. Pushed the corrected commit to the same gsdali/OCCT fork branch backing Open-Cascade-SAS/OCCT#1462 (force-push, not a new PR) -- CI re-running there. - Scripts/patches/README.md's "Pin consequence" paragraph was self- contradictory: said 0026 was outside the pin "same as 0022-0025" while also listing the pin as including 0014-0025 (which includes them). Package.swift's own census says all fifteen, through 0025, are baked into the v2.0.0 release asset. Corrected. - Documented, not fixed: why GetStatus() stays _Done on this failure path (Build()'s generic catch doesn't touch myStatus for any exception source; fixing it needs a call-site change the whole point of this fix was to avoid, for a value nothing in this tree reads), and why this patch throws rather than following 0025's (#597) surface-a-diagnostic-number pattern (different problem shape: wrong pass/fail vs. wrong diagnostic). - Added Scripts/repro/905-thrusections-capping-guard/, matching every other carried patch's convention (a standalone ground-truth probe + real before/after transcripts) -- missing from the first commit. - CLAUDE.md: added a pointer to the existing FillingSurface/BRepFill_Filling composable workaround for a caller who hits this on a real non-planar loft, since ThruSections' own capping stays plane-only. Filed separately rather than folded in here: a pre-existing, unrelated bridge gap the reviewer found (ThruSectionsBuilder.generatedFace(from:) doesn't check IsDone(), can return stale data after a failed reused build() -- not introduced or worsened by this fix, same shape already existed for every other exception this file can throw). #910. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gsdali
added a commit
to SecondMouseAU/OCCTSwift
that referenced
this pull request
Aug 14, 2026
…patch ThruSectionsBuilder(isSolid: true) silently omits both end-cap faces for a closed section wire with k >= 2 periods of out-of-plane variation around the loop. MakeSolid()'s local `B` already tracks whether capping succeeded and discards it, unconditionally marking the result Closed(true) regardless. Carry Scripts/patches/0026-BRepOffsetAPI_ThruSections-capping-guard-905.patch: throw StdFail_NotDone when B is still false after the capping block, matching the exception this same function already throws for a null shell. No signature or call-site change. Distinguishes a genuine capping failure from a degenerate (point) end needing no cap, which an earlier null-face-based attempt could not (it regressed BOPAlgo_PaveFillerTest.FuseConeLoftWithBox_DegeneratedEdge). Validated via override-link (compile the changed source standalone, link ahead of the prebuilt OCCT archive, no full rebuild): the new upstream GTest fails against the unpatched source and passes against the patched one, and the actual FuseConeLoftWithBox_DegeneratedEdge GTest passes unmodified against the patched source. Filed upstream as Open-Cascade-SAS/OCCT#1462, CI green. Documents the process correction: an earlier session had opened a same-repo staging PR on the gsdali/OCCT fork (gsdali/OCCT#1, that fork's first PR) to CI-validate a first attempt before submitting upstream. That isn't how any other carried patch here was validated, and the first attempt's null-face guard is what regressed the GTest above. gsdali/OCCT#1 is closed as erroneous; this commit's Scripts/patches/README.md entry says so. Not yet in a rebuilt xcframework -- Package.swift's pin is unchanged. That is a separate release-engineering step, tracked the same way patches 0022-0025 already are. Closes #905 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gsdali
added a commit
to SecondMouseAU/OCCTSwift
that referenced
this pull request
Aug 14, 2026
Real findings from /code-review, addressed: - MakeSolid()'s new throw could leave face1/face2 (aliased to myFirst/ myLast, exposed via FirstShape()/LastShape()) holding a real, never-added-to-the-shell face when one wire's PerformPlan() succeeded before the other's failed. Nullify both before throwing. Re-verified via override-link: same 5/5 GTest pass, same FuseConeLoftWithBox_DegeneratedEdge pass. Pushed the corrected commit to the same gsdali/OCCT fork branch backing Open-Cascade-SAS/OCCT#1462 (force-push, not a new PR) -- CI re-running there. - Scripts/patches/README.md's "Pin consequence" paragraph was self- contradictory: said 0026 was outside the pin "same as 0022-0025" while also listing the pin as including 0014-0025 (which includes them). Package.swift's own census says all fifteen, through 0025, are baked into the v2.0.0 release asset. Corrected. - Documented, not fixed: why GetStatus() stays _Done on this failure path (Build()'s generic catch doesn't touch myStatus for any exception source; fixing it needs a call-site change the whole point of this fix was to avoid, for a value nothing in this tree reads), and why this patch throws rather than following 0025's (#597) surface-a-diagnostic-number pattern (different problem shape: wrong pass/fail vs. wrong diagnostic). - Added Scripts/repro/905-thrusections-capping-guard/, matching every other carried patch's convention (a standalone ground-truth probe + real before/after transcripts) -- missing from the first commit. - CLAUDE.md: added a pointer to the existing FillingSurface/BRepFill_Filling composable workaround for a caller who hits this on a real non-planar loft, since ThruSections' own capping stays plane-only. Filed separately rather than folded in here: a pre-existing, unrelated bridge gap the reviewer found (ThruSectionsBuilder.generatedFace(from:) doesn't check IsDone(), can return stale data after a failed reused build() -- not introduced or worsened by this fix, same shape already existed for every other exception this file can throw). #910. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pre-Submission Checks
.github/CONTRIBUTING.md.Group - Summaryformat.Problem / Motivation
BRepOffsetAPI_ThruSections(isSolid: true)silently omits both end-cap faces when a closedsection wire has two or more periods of out-of-plane variation around the loop, a genuinely
non-planar closed curve.
Build()still callsDone()and reportsIsDone() == true, andShape()returns aTopoDS_Solidthat is missing two faces and is not actually closed.BRepCheck_Analyzercatches this later, but with no localized error (errorCount == 0, nodetailed check statuses), so a caller has already trusted
IsDone()by the time anything flagsit.
Root cause:
MakeSolid()(the static helper bothCreateRuled()andCreateSmoothed()call toclose a loft's two open ends) caps each end via
PerformPlan(), which triesBRepBuilderAPI_FindPlaneand then aBRepLib_FindSurface-backedMakeFace(wire)fallback.Neither finds anything but a plane or a surface already attached to the wire's edges. A section
wire with
k >= 2periods of out-of-plane variation around the loop has no such surface, soPerformPlan()fails for it.MakeSolid()already tracks this in its own localB, threadedthrough both
PerformPlan()calls — and discards it: it force-marks the shell and solidClosed(true)unconditionally before returning, and the caller then callsDone()unconditionally too.
A single period of out-of-plane variation (e.g.
z = amp * cos(theta)at constant radius) capsfine, because that curve is secretly planar: it is exactly the intersection of the cylinder
r = constwith a tilted plane, soBRepBuilderAPI_FindPlanefinds it.k >= 2has no suchplane and no other analytic surface either.
Found downstream of a bevel gear
ThruSectionsloft in the OCCTSwift ecosystem, where eachsection is a toothed profile whose out-of-plane period equals the tooth count, always
>= 2fora real gear.
Proposed Solution
MakeSolid()'s localBalready distinguishes exactly the two cases that matter: it staystruewhen capping either succeeded or wasn't needed (PerformPlan()'s own degenerate-wireshortcut — every edge of the wire is degenerate, a single-vertex "point" section built via
AddVertex(), e.g. a cone's apex, which needs no cap face and correctly reports success with anull face), and ends
falseonly when capping was genuinely attempted and failed. After thecapping block, if
Bis stillfalse, throwStdFail_NotDone, matching the exception this samefunction already throws for a null shell two lines above:
No signature change and no call-site change: both call sites already run inside
Build()'s onlytry/catch(catch (Standard_Failure const&) { NotDone(); return; }), so the throw propagatesthere and
IsDone()correctly readsfalse.This does not make
BRepOffsetAPI_ThruSectionsable to cap a genuinely non-planar wire (thatwould need a real capping algorithm, e.g. an N-sided patch, out of scope here). It converts a
silent wrong success into a correctly reported failure, matching every other failure path in this
file.
A null-face check (
myFirst.IsNull() || myLast.IsNull()) looks like an equally natural fix andis not:
PerformPlan()also leaves the output face null, and returnstrue(no failure), whenthe wire is degenerate, so a null-face check cannot tell "no cap needed" apart from "cap needed
and failed." Checked directly against
BOPAlgo_PaveFillerTest.FuseConeLoftWithBox_DegeneratedEdge(a circle-to-vertex loft, in this same PR's GTest additions) before settling on the
B-based fix— a null-face guard regresses that test,
Bdoes not.Limitation:
GetStatus()is not updated by this fix and staysBRepFill_ThruSectionErrorStatus_Doneon this path.Build()'s catch block doesn't touchmyStatusfor any exception source, not one this fix introduces —IsDone()is the correct,fixed signal.
Validation
Checks performed:
clang-format --dry-run --Werroragainst this tree's own.clang-format: zero violationson both changed files.
BRepOffsetAPI_ThruSections_Test.cxx:NonPlanarClosedWireCappingFails(a two-wirek=2loft, must now fail) andDegenerateVertexEndStillSucceeds(a circle-to-vertex loft, must still succeed — the sameshape as
BOPAlgo_PaveFillerTest.FuseConeLoftWithBox_DegeneratedEdge, added as a direct,local regression guard for it in this file too).
Run GTest/Test/Regression Testjobs including the No-PCH debug build) is green.
.cxxstandalone, linkit ahead of a prebuilt
libOCCT-macos.a, no full OCCT rebuild) both an unpatched and apatched build. Against the unpatched build,
NonPlanarClosedWireCappingFailsfails exactlyas described above (
IsDone()readstrue). Against the patched build, it passes, alongwith
DegenerateVertexEndStillSucceedsand all pre-existing tests in the file. Alsocompiled and ran the actual
BOPAlgo_PaveFiller_Test.cxxFuseConeLoftWithBox_DegeneratedEdgetest, unmodified, against the patched build: passes.🤖 Generated with Claude Code