From 695932ad8728eb1557e1d557264ccfb256e40424 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sun, 26 Jul 2026 17:27:40 +0200 Subject: [PATCH] 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 --- src/libslic3r/SketchEngine.cpp | 16 +++++++++++++- tests/libslic3r/test_caddocument.cpp | 31 +++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/SketchEngine.cpp b/src/libslic3r/SketchEngine.cpp index 2b9c9c40bb..193d7360bf 100644 --- a/src/libslic3r/SketchEngine.cpp +++ b/src/libslic3r/SketchEngine.cpp @@ -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 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)); } diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index 21ee86e268..efdebec407 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -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); -} \ No newline at end of file +} + +// 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()); +}