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
+15 -1
View File
@@ -479,7 +479,11 @@ TriangleMesh SketchEngine::tessellate(const TopoDS_Shape& shape,
its.indices.reserve(raw.indices.size());
its.vertices.reserve(raw.vertices.size() / 2);
for (const auto& tri : raw.indices) {
std::vector<int> kept_face;
kept_face.reserve(tri_face.size());
for (size_t ti = 0; ti < raw.indices.size(); ++ti) {
const auto& tri = raw.indices[ti];
stl_triangle_vertex_indices new_tri;
for (int j = 0; j < 3; ++j) {
const stl_vertex& v = raw.vertices[tri[j]];
@@ -494,8 +498,18 @@ TriangleMesh SketchEngine::tessellate(const TopoDS_Shape& shape,
new_tri[j] = it->second;
}
}
// Drop triangles that welding collapsed to a repeated vertex. OCCT emits one at the
// pole of every degenerate surface parameterization — a sphere patch at a filleted
// corner has exactly one — and its v->v edge can never pair with a neighbour, so the
// mesh reports an open edge per corner and the slicer declares the model non-manifold
// and tells the user to repair it elsewhere. The triangle has zero area: removing it
// changes no geometry, only the mesh's bookkeeping.
if (new_tri[0] == new_tri[1] || new_tri[1] == new_tri[2] || new_tri[0] == new_tri[2])
continue;
its.indices.push_back(new_tri);
kept_face.push_back(tri_face[ti]);
}
tri_face.swap(kept_face); // tri_face stays index-aligned with its.indices
return TriangleMesh(std::move(its));
}
+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());
}