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
+84 -5
View File
@@ -18,6 +18,9 @@
#include <BRepOffsetAPI_MakePipe.hxx>
#include <BRepOffsetAPI_MakePipeShell.hxx>
#include <BRepOffsetAPI_MakeThickSolid.hxx>
#include <BRepBuilderAPI_Sewing.hxx>
#include <BRepBuilderAPI_MakeSolid.hxx>
#include <ShapeAnalysis_FreeBounds.hxx>
#include <BRepOffsetAPI_MakeOffsetShape.hxx>
#include <BRepOffsetAPI_MakeFilling.hxx>
#include <BRepOffsetAPI_DraftAngle.hxx>
@@ -2954,12 +2957,88 @@ void CadDocument::apply_thicken_surface(std::vector<CadBody>& bodies, const CadF
const double off = f.thicken_flip ? -std::abs(f.thicken_thickness)
: std::abs(f.thicken_thickness);
BRepOffsetAPI_MakeThickSolid mts;
mts.MakeThickSolidBySimple(bodies[tgt].shape, off);
mts.Build();
if (!mts.IsDone()) throw std::runtime_error("thicken-surface: failed");
TopoDS_Shape solid = mts.Shape();
const TopoDS_Shape& sheet = bodies[tgt].shape;
// MakeThickSolidBySimple offsets each face along its own normal and sews the result; it never
// extends neighbours to meet, so at every edge where two faces join at an angle the corner
// material is simply absent. A flat sheet is therefore exact while a 4-walled box is not
// (measured: 29648 against the 44000 that (60^2-50^2)*40 requires).
//
// ByJoin is the call that mitres those corners, and it is what the Shell feature already uses
// successfully a few hundred lines up — but it CANNOT be handed an open sheet. In OCCT,
// BRepOffset_MakeOffset::MakeThickSolid builds the solid only inside `if (!myFaces.IsEmpty())`
// (BRepOffset_MakeOffset.cxx:1115): with no closing faces it stops after the offset shell and
// returns that. So it reports IsDone() and a non-null shape containing no TopAbs_SOLID at all
// — which is exactly how two earlier attempts failed, and why no parameter could have fixed it.
//
// So close the sheet first, then use the working call: cap the free rims, sew into a closed
// shell, make a solid, and hollow it inward passing the caps as the faces to remove. The caps
// come back off, leaving the wall.
// A SINGLE face has no edge shared with a neighbour, so there is no corner to mitre and
// BySimple is already exact on it (flat 60x60 by 5 -> 18000.000). It also has a free
// boundary, so "does it have free wires" is NOT the question to ask here — capping a lone
// face with its own rim sews a zero-thickness shell and the offset then measures 6000.
int n_faces = 0;
for (TopExp_Explorer fe(sheet, TopAbs_FACE); fe.More(); fe.Next()) ++n_faces;
TopTools_ListOfShape caps;
if (n_faces > 1) {
ShapeAnalysis_FreeBounds fb(sheet);
for (TopExp_Explorer we(fb.GetClosedWires(), TopAbs_WIRE); we.More(); we.Next()) {
BRepBuilderAPI_MakeFace mk(TopoDS::Wire(we.Current()));
if (!mk.IsDone())
throw std::runtime_error("thicken-surface: cannot cap the sheet rim "
"(is it planar?)");
caps.Append(mk.Face());
}
}
TopoDS_Shape solid;
if (caps.IsEmpty()) {
BRepOffsetAPI_MakeThickSolid mts;
mts.MakeThickSolidBySimple(sheet, off);
mts.Build();
if (!mts.IsDone()) throw std::runtime_error("thicken-surface: failed");
solid = mts.Shape();
} else {
BRepBuilderAPI_Sewing sewer(1.0e-3);
sewer.Add(sheet);
for (TopTools_ListIteratorOfListOfShape it(caps); it.More(); it.Next())
sewer.Add(it.Value());
sewer.Perform();
TopoDS_Shell closed_shell;
for (TopExp_Explorer se(sewer.SewedShape(), TopAbs_SHELL); se.More(); se.Next()) {
if (!closed_shell.IsNull())
throw std::runtime_error("thicken-surface: the capped sheet split into "
"more than one shell");
closed_shell = TopoDS::Shell(se.Current());
}
if (closed_shell.IsNull() || !BRep_Tool::IsClosed(closed_shell))
throw std::runtime_error("thicken-surface: the capped sheet is not closed");
// A shell sewn from an extruded sheet carries no guarantee that its faces point outward,
// and BRepBuilderAPI_MakeSolid does not fix that. Offsetting an inside-out solid sends the
// wall the wrong way: measured bbox 70x70x40 for an inward offset on a 60x60 box, with a
// volume larger than its own bounding box because the result then overlaps itself. A
// negative mass IS the inverted orientation, so use it as the test.
TopoDS_Solid capped = BRepBuilderAPI_MakeSolid(closed_shell).Solid();
{
GProp_GProps vp;
BRepGProp::VolumeProperties(capped, vp);
if (vp.Mass() < 0.0) capped.Reverse();
}
BRepOffsetAPI_MakeThickSolid mts;
mts.MakeThickSolidByJoin(capped, caps, -std::abs(off), 1.0e-3);
mts.Build();
if (!mts.IsDone()) throw std::runtime_error("thicken-surface: failed");
solid = mts.Shape();
}
if (solid.IsNull()) throw std::runtime_error("thicken-surface: produced no geometry");
// IsDone() is NOT a success test here — the failed attempts had IsDone() true and no solid.
if (!TopExp_Explorer(solid, TopAbs_SOLID).More())
throw std::runtime_error("thicken-surface: produced a shell, not a solid");
{
GProp_GProps props;
+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;