Drop the degenerate triangle OCCT emits at every filleted corner

A filleted solid arrived on the plate as a broken model: the slicer reported
"8 non-manifold edges" on an 80x50x12 box with r=3 on all edges, and advised
repairing it in another CAD application -- the exact round trip the Design tab
exists to remove. The same box without the fillet committed cleanly.

Measured rather than guessed. Each of the 8 bad edges is degenerate, both
endpoints the same vertex:

    open tri=145  edge=1 face=5 v59(3.000000 3.000000 0.000000) v59(3.000000 3.000000 0.000000)
    open tri=538  edge=1 face=6 v87(3.000000 3.000000 12.000000) v87(...)
    ... one per corner, 8 corners

OCCT triangulates a degenerate surface parameterization with a triangle at the
pole; a corner sphere patch has exactly one. Its two pole nodes are distinct in
the per-face triangulation and collapse to a single vertex when the faces are
welded, leaving a zero-area triangle whose v->v edge can never pair with a
neighbour. its_face_neighbors counts it as open, and the field the object panel
prints as "non-manifold edges" is in fact stats.open_edges.

So the geometry was never wrong -- the B-rep volume matches the Steiner formula
for a box dilated by a ball to 0.016%. Only the bookkeeping was.

Dropping those triangles after the weld removes 8 of 3492 and takes open_edges
to 0. Zero area, so nothing about the shape changes. tri_face is compacted in
the same pass, since it must stay index-aligned with the triangle list that the
face picking and per-body colouring both index into.

Guarded by a new [CadDocument] case that asserts open_edges == 0, no degenerate
triangle survives, and both per-triangle maps still match the triangle count.
The existing suite only ever checked B-rep volumes and areas, which is why a
mesh defect this visible went unnoticed: 150 cases / 2049 assertions green on
both forks.

snaporca-agw
This commit is contained in:
Tommaso Bianchi
2026-07-26 17:27:40 +02:00
parent 2c5ddd4102
commit 695932ad87
2 changed files with 45 additions and 2 deletions
+30 -1
View File
@@ -6590,4 +6590,33 @@ TEST_CASE("interference: detects a clash created by a mate", "[CadDocument][inte
auto hits = doc.check_interference();
REQUIRE(hits.size() == 1);
REQUIRE(hits[0].volume > 1.0);
}
}
// A filleted solid must reach the plate as a watertight mesh. OCCT emits one degenerate
// triangle at the pole of every corner sphere patch; welded, its v->v edge counts as an open
// edge and the slicer tells the user to go repair the model in another CAD application --
// the exact round trip this feature exists to remove. snaporca-agw.
TEST_CASE("CadDocument filleted solid tessellates watertight", "[CadDocument]")
{
CadDocument doc;
SketchProfile sp;
sp.points.push_back(Vec2d( 0, 0));
sp.points.push_back(Vec2d( 80, 0));
sp.points.push_back(Vec2d( 80, 50));
sp.points.push_back(Vec2d( 0, 50));
sp.closed = true;
const int sk = doc.add_sketch_profile(sp, SketchPlane::XY(), "P");
doc.add_extrude(sk, 12.0, false, BooleanMode::New, "E");
doc.add_fillet(3.0, FaceGroup::All, "F");
REQUIRE(doc.recompute());
CHECK(doc.display_mesh.stats().open_edges == 0);
CHECK(its_num_open_edges(doc.display_mesh.its) == 0);
// No degenerate triangles survive the weld, and the per-triangle face map stays aligned.
size_t degenerate = 0;
for (const auto& t : doc.display_mesh.its.indices)
if (t[0] == t[1] || t[1] == t[2] || t[0] == t[2]) ++degenerate;
REQUIRE(degenerate == 0);
REQUIRE(doc.display_tri_face.size() == doc.display_mesh.its.indices.size());
REQUIRE(doc.display_tri_body.size() == doc.display_mesh.its.indices.size());
}