Thicken Surface: fill the corners of a closed-loop wall

snaporca-wm4s. Thickening the 4-walled open box (60x60 in plan, 40 tall, no caps)
by 5 produced volume 29648.15 where the geometry requires (60^2-50^2)*40 = 44000
— about 67% of it. The corner material at the four vertical edges was simply
absent.

CAUSE. MakeThickSolidBySimple offsets each face along its own normal and sews;
it never extends neighbours to meet, so wherever two faces join at an angle the
corner is empty. A flat sheet has no such join and was always exact (18000.000),
which is why the defect looked like a measurement artefact.

WHY THE TWO EARLIER ATTEMPTS COULD NOT HAVE WORKED. Both switched to ByJoin —
plain, then with Intersection/GeomAbs_Intersection — and both returned a shell,
not a solid, so the body lost its volume entirely and both were reverted. That is
not a parameter problem: in OCCT, BRepOffset_MakeOffset::MakeThickSolid builds a
solid only inside `if (!myFaces.IsEmpty())` (BRepOffset_MakeOffset.cxx:1115).
Handed an open sheet with no closing faces, it stops after the offset shell and
returns it, reporting IsDone() with a non-null shape containing no TopAbs_SOLID.
ByJoin hollows a CLOSED solid by removing faces; an open sheet is outside its
contract.

FIX. Close the sheet, then use the call that mitres: cap the free rims
(ShapeAnalysis_FreeBounds -> MakeFace), sew shell+caps into a closed shell, make
a solid, and hollow it inward passing the caps as the faces to remove — the caps
come back off and leave the wall. Two details, each found by measurement rather
than reasoning:

* A shell sewn from an extruded sheet carries no guarantee of outward
  orientation, and MakeSolid does not fix it. Inside-out, the inward offset goes
  OUTWARD: measured bbox 70x70x40 and volume 339141.59, larger than its own
  bounding box because the result overlaps itself. A negative GProp mass is
  exactly that inversion, so it is the test; Reverse() on it.
* A SINGLE face has no neighbour to mitre and must keep the BySimple path. It
  does have a free boundary, so "has free wires" is the wrong question — capping
  a lone face with its own rim sews a zero-thickness shell and measures 6000
  against 18000.

Also: IsDone() is not a success test here, since both failed attempts had it
true. The code now explores for TopAbs_SOLID and refuses a shell.

Tests: new case asserts 44000 with the wall's bbox at 60x60x40 (catching the
inverted-orientation shape, which has the right volume nowhere near the right
place), plus the flat-sheet control at 18000 that must not regress. Full kernel
suite green: 2502 assertions in 187 test cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-08-15 23:13:32 +02:00
co-authored by Claude Opus 5
parent 98135cf529
commit 799f840218
2 changed files with 129 additions and 5 deletions
+45
View File
@@ -5159,6 +5159,51 @@ TEST_CASE("mass properties of a sheet body report area only, never a volume",
REQUIRE(solid.inertia[8] > 0.0);
}
// snaporca-wm4s. The wall of a thickened open box must contain the corner material. Thickening
// each face along its own normal and sewing (MakeThickSolidBySimple) leaves the four vertical
// corners empty and measured 29648.15 where the geometry requires 44000; the two controls below
// were exact before and must stay exact, since they are what a corner-only fix must not disturb.
TEST_CASE("thicken surface fills the corners of a closed-loop wall", "[CadDocument][surface]")
{
using Catch::Matchers::WithinRel;
SECTION("open box: (60^2 - 50^2) * 40") {
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 60, 60, 0, "Rect");
REQUIRE(sk >= 0);
doc.add_surface_extrude(sk, 40.0, "Skin"); // 4 walls, no caps
REQUIRE(doc.recompute());
REQUIRE(doc.body_mass_properties(0).surface_area == Approx(9600.0)); // the sheet is what we think
REQUIRE(doc.add_thicken_surface(0, 5.0, false, "Wall") >= 0);
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
auto w = doc.body_mass_properties(1);
REQUIRE(w.is_solid); // NOT a shell: the old ByJoin attempts
REQUIRE_THAT(w.volume, WithinRel(44000.0, 1e-6)); // returned volume 0.0 / is_solid false
// The wall must sit ON the sheet, not around it: an inverted capped solid offsets the
// wrong way and lands at 70x70 with a volume larger than its own bounding box.
const auto bb = doc.display_body_meshes[1].bounding_box();
REQUIRE_THAT(bb.max.x() - bb.min.x(), WithinRel(60.0, 1e-3));
REQUIRE_THAT(bb.max.z() - bb.min.z(), WithinRel(40.0, 1e-3));
}
SECTION("control, flat sheet stays exact: 3600 * 5") {
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 60, 60, 0, "Rect");
REQUIRE(doc.add_surface_fill(sk, "Face") >= 0); // one flat face, no rim to mitre
REQUIRE(doc.recompute());
REQUIRE(doc.add_thicken_surface(0, 5.0, false, "Plate") >= 0);
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
auto p = doc.body_mass_properties(1);
REQUIRE(p.is_solid);
REQUIRE_THAT(p.volume, WithinRel(18000.0, 1e-6));
}
}
TEST_CASE("surface-offset creates another sheet shifted outward", "[CadDocument][surface]")
{
using Catch::Matchers::WithinAbs;