From ee2c40ea854870e54c70dd6ab84c4573cf8aee76 Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:00:28 -0300 Subject: [PATCH] Fix Mesh Boolean negative (cut) text object (#16275) * Fix Mesh Boolean negative (cut) text object * Update test_meshboolean.cpp * missing headers * Update test_meshboolean.cpp * Refactor mcut difference test for source splits * Apply suggestion from @raistlin7447 --- src/libslic3r/MeshBoolean.cpp | 23 +++++++++++++++- tests/libslic3r/test_meshboolean.cpp | 40 +++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/MeshBoolean.cpp b/src/libslic3r/MeshBoolean.cpp index 44d54be99d..37e3c84b3f 100644 --- a/src/libslic3r/MeshBoolean.cpp +++ b/src/libslic3r/MeshBoolean.cpp @@ -1045,7 +1045,28 @@ void do_boolean(McutMesh& srcMesh, const McutMesh& cutMesh, const std::string& b // But we can force it to work by spliting the src mesh into disconnected components, // and do booleans seperately, then merge all the results. indexed_triangle_set all_its; - if (boolean_opts == "UNION" || boolean_opts == "A_NOT_B") { + if (boolean_opts == "A_NOT_B") { + // Each cut can leave the source with several disconnected components, which mcut rejects + // in the next dispatch, so re-split after every cut part (e.g. each letter of a text). + std::vector parts = std::move(src_parts); + for (size_t j = 0; j < cut_parts.size(); j++) { + auto cut_part = triangle_mesh_to_mcut(cut_parts[j]); + std::vector next_parts; + for (indexed_triangle_set &part : parts) { + auto src_part = triangle_mesh_to_mcut(part); + if (do_boolean_single(*src_part, *cut_part, boolean_opts)) { + TriangleMesh tri_part = mcut_to_triangle_mesh(*src_part); + std::vector pieces = its_split(tri_part.its); + std::move(pieces.begin(), pieces.end(), std::back_inserter(next_parts)); + } else + next_parts.emplace_back(std::move(part)); + } + parts = std::move(next_parts); + } + for (const indexed_triangle_set &part : parts) + its_merge(all_its, part); + } + else if (boolean_opts == "UNION") { for (size_t i = 0; i < src_parts.size(); i++) { auto src_part = triangle_mesh_to_mcut(src_parts[i]); for (size_t j = 0; j < cut_parts.size(); j++) { diff --git a/tests/libslic3r/test_meshboolean.cpp b/tests/libslic3r/test_meshboolean.cpp index 6aebd56481..fc04ba27fc 100644 --- a/tests/libslic3r/test_meshboolean.cpp +++ b/tests/libslic3r/test_meshboolean.cpp @@ -1,9 +1,11 @@ #include #include #include - +#include +#include #include #include +#include using namespace Slic3r; @@ -24,3 +26,39 @@ TEST_CASE("CGAL and TriangleMesh conversions", "[MeshBoolean]") { REQUIRE(! MeshBoolean::cgal::does_self_intersect(M)); } + +TEST_CASE("mcut difference handles source splits between cuts", "[MeshBoolean]") { + TriangleMesh body = make_cube(30., 10., 10.); + + TriangleMesh tool; + + // First cut splits the source into two disconnected components. + TriangleMesh slab = make_cube(2., 12., 20.); + slab.translate(Vec3f(14.f, -1.f, -5.f)); + its_merge(tool.its, slab.its); + + // These cuts must still be applied after the source has been split. + TriangleMesh left_hole = make_cube(4., 4., 20.); + left_hole.translate(Vec3f(3.f, 3.f, -5.f)); + its_merge(tool.its, left_hole.its); + + TriangleMesh right_hole = make_cube(4., 4., 20.); + right_hole.translate(Vec3f(21.f, 3.f, -5.f)); + its_merge(tool.its, right_hole.its); + + std::vector result; + MeshBoolean::mcut::make_boolean(body, tool, result, "A_NOT_B"); + + REQUIRE(result.size() == 1); + + const std::vector components = + its_split(result.front().its); + + REQUIRE(components.size() == 2); + + // 3000 - 200 - 160 - 160 = 2480. + REQUIRE_THAT( + result.front().volume(), + Catch::Matchers::WithinRel(2480., 1e-3) + ); +}