mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
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:
co-authored by
Claude Opus 5
parent
98135cf529
commit
799f840218
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user