Modeling - Guard silently-uncapped solid in BRepOffsetAPI_ThruSections - #1
Modeling - Guard silently-uncapped solid in BRepOffsetAPI_ThruSections#1gsdali wants to merge 1 commit into
Conversation
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, 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.
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.
df53488 to
b3e7d4b
Compare
|
Pushed a corrected commit fixing the regression the previous run found. Root cause of the CI failure: the guard checked Before pushing, compiled and ran the actual |
|
Closing this. Opening a PR on this fork itself, to stage and CI-validate a patch before |
…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>
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
>= 2for a real gear. Bisected in
SecondMouseAU/OCCTSwiftScripts#108
away from every property of the gear down to this single geometric trigger. The resulting
healed()/fixSolid()-demotes-solid-to-shell symptom was filed and fixed downstream asSecondMouseAU/OCCTSwift#702; this
capping omission is the root cause Open-Cascade-SAS#702 never investigated, filed as its own issue at
SecondMouseAU/OCCTSwift#905.
Proposed Solution
This PR previously guarded both call sites on
myFirst.IsNull() || myLast.IsNull(). Thatattempt is superseded by this version — CI caught a real regression in it (see "First attempt"
below), and the corrected fix lives entirely inside
MakeSolid()instead.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.First attempt, and why it's replaced. The original version of this PR guarded on
myFirst.IsNull() || myLast.IsNull()at both call sites instead. That looks equivalent and isnot:
PerformPlan()also leaves the output face null, and returnstrue(no failure), when thewire is degenerate — so a null-face check cannot tell "no cap needed" apart from "cap needed and
failed." CI caught this directly:
BOPAlgo_PaveFillerTest.FuseConeLoftWithBox_DegeneratedEdge(acircle-to-vertex loft, always legitimately capped) regressed to
IsDone() == false. This versionfixes that: verified by compiling and running that exact upstream GTest, unmodified, against the
corrected fix (see Validation).
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.
Limitation, unchanged from before:
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.
clang++ -std=c++17 -fsyntax-onlyagainst the publicheaders bundled in the downstream OCCTSwift repo's
OCCT.xcframework(V8_0_1baseline):clean.
.cxxstandalone, link it ahead of theprebuilt
libOCCT-macos.a, no full OCCT rebuild): built two versions ofBRepOffsetAPI_ThruSections.cxx, unpatched and patched, and linked each against the newBRepOffsetAPI_ThruSections_Test.cxx.NonPlanarClosedWireCappingFailstest failsexactly as described above:
IsDone()readstruefor a two-wirek=2loft.DegenerateVertexEndStillSucceedstest and all three pre-existing tests in the file(
OCC10006_LoftAndFusion,BSplineProfilesWithDifferentPoleCount,OCC895_TwoCircularArcWires_NoTwist).BOPAlgo_PaveFiller_Test.cxxFuseConeLoftWithBox_DegeneratedEdgetest, unmodified, against the patched override: passes— direct confirmation this fix does not reintroduce the first attempt's regression, not just
an equivalent local test standing in for it. Confirmed (with the same setup) that it does
fail, with the identical failure message CI reported, under the first attempt's null-face
guard.
available in this environment; the override-link technique above compiles and links the
real changed sources against the real prebuilt OCCT archive, which is the closest available
substitute. This PR's own CI (GTest jobs on macOS/Linux/Windows) is the authoritative check.
Review Notes
Reproduced against the OCCTSwift 2.0.0 xcframework before writing this patch:
raw.shapeType == .Solid,subShapeCount(ofType: .face) == 180(wall-only; a workingk <= 1loft produces
182, wall + 2 caps),checkResult.isValid == false,checkResult.errorCount == 0,detailedCheckStatuses == [].🤖 Generated with Claude Code