From 30d54f0074214ff4858d3c40c9bee238cbdc5765 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 25 Jul 2026 12:15:30 +0200 Subject: [PATCH] M8c: interference detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_interference(min_volume) reports every pair of solid bodies whose intersection encloses more than min_volume, as {body_a, body_b, volume}. It reports only — no geometry is mutated, so calling it cannot disturb mates or placements. Read-only at the MCP surface too: no checkpoint, no recompute. Sheet bodies are skipped up front: an intersection involving one encloses no volume, so the boolean would be wasted work. Bodies that merely touch share a face and enclose nothing, so face-to-face contact is not an interference. A boolean that fails on one pair must not lose the report for every other pair, so each pair is guarded — and OCCT raises Standard_Failure, which is not a std::exception and would otherwise escape. No new serialized fields, no recipe bump: this reads `bodies`, which is recompute output and was never serialized. No separate MCP listing for instances and mates: describe_scene already emits the feature tree, and Mate has rendered there correctly since M8a fixed feature_type_name. Tests assert the exact overlap volume (20*20*4 = 1600 mm^3), both negative cases (clearly apart, and exact face contact), that sheets are skipped, that the min_volume gate silences a real overlap, and that a clash created by a Fastened mate is detected — which ties the M8b placement work to this report. Suite 139 cases / 1960 assertions green. McpControl.cpp is reviewed but not compiled by kernel-test.sh, which builds only libslic3r_tests. Co-Authored-By: Claude Opus 5 (1M context) --- src/libslic3r/CadDocument.cpp | 34 ++++++++ src/libslic3r/CadDocument.hpp | 7 ++ src/slic3r/GUI/McpControl.cpp | 18 ++++ tests/libslic3r/test_caddocument.cpp | 119 +++++++++++++++++++++++++++ 4 files changed, 178 insertions(+) diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index 7e1e1866ea..23eb598c45 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -3359,6 +3359,40 @@ int CadDocument::add_surface_fill(int sketch_ref, const std::string& name) return int(features.size()) - 1; } +std::vector CadDocument::check_interference(double min_volume) const +{ + std::vector out; + const int n = int(bodies.size()); + + for (int i = 0; i < n; ++i) { + if (bodies[i].shape.IsNull() || is_sheet_shape(bodies[i].shape)) continue; + for (int j = i + 1; j < n; ++j) { + if (bodies[j].shape.IsNull() || is_sheet_shape(bodies[j].shape)) continue; + + double v = 0; + // A boolean that blows up on one pair must not lose the report for the others, + // and OCCT signals those as Standard_Failure, which is NOT a std::exception. + try { + BRepAlgoAPI_Common common(bodies[i].shape, bodies[j].shape); + common.Build(); + if (!common.IsDone()) continue; + const TopoDS_Shape s = common.Shape(); + if (s.IsNull()) continue; + GProp_GProps props; + BRepGProp::VolumeProperties(s, props); + v = std::abs(props.Mass()); + } catch (const Standard_Failure&) { + continue; + } + + // Bodies that merely touch share a face and enclose no volume, so the + // threshold is what separates contact from interference. + if (v > min_volume) out.push_back({i, j, v}); + } + } + return out; +} + // ponytail: derived from the OCCT shape type; no stored flag, bodies aren't serialized anyway. bool CadDocument::is_sheet_shape(const TopoDS_Shape& s) { diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp index 20aa70a616..386769c821 100644 --- a/src/libslic3r/CadDocument.hpp +++ b/src/libslic3r/CadDocument.hpp @@ -575,6 +575,13 @@ public: GeometryEngine::MassProps body_mass_properties(int body_index) const; + // One overlapping pair of solid bodies. Indices are into `bodies`, a_ < b_. + struct Interference { int body_a{-1}; int body_b{-1}; double volume{0}; }; + // Every pair of solid bodies whose intersection encloses more than min_volume (mm^3). + // Reports only — mutates nothing, so mates and placements are unaffected by calling it. + // Sheet bodies are skipped: an intersection involving one encloses no volume. + std::vector check_interference(double min_volume = 1e-6) const; + // ponytail: derived from the OCCT shape type; no stored flag, bodies aren't serialized anyway. static bool is_sheet_shape(const TopoDS_Shape& s); // true if TopExp finds no TopAbs_SOLID diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index 8acb9e4926..7c1feecb09 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -339,6 +339,10 @@ json describe_tools() json{{"name", "angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 0}}, json{{"name", "flip"}, {"type", "boolean"}, {"default", false}}, })}}, + json{{"name", "check_interference"}, {"summary", "Solid bodies that overlap, as {body_a, body_b, volume}. Read-only; bodies that merely touch enclose no volume and are not reported."}, + {"params", json::array({ + json{{"name", "min_volume"}, {"type", "number"}, {"unit", "mm^3"}, {"default", 1e-6}, {"description", "overlap volume above which a pair counts as interfering"}}, + })}}, json{{"name", "query_topology"}, {"summary", "Measured faces (centroid/normal/cylinder) and edges (length/circle) of a body."}, {"params", json::array({ json{{"name", "body"}, {"type", "integer"}, {"default", 0}}, @@ -1344,6 +1348,19 @@ json action_mate(DesignPanel* panel, const json& params) return json{{"ok", ok}, {"mate_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } +json action_check_interference(DesignPanel* panel, const json& params) +{ + const double min_volume = params.value("min_volume", 1e-6); + CadDocument& doc = panel->mcp_doc(); + // Read-only: no checkpoint(), no recompute(), no mcp_after_change(). + const auto hits = doc.check_interference(min_volume); + json arr = json::array(); + for (const auto& h : hits) { + arr.push_back(json{{"body_a", h.body_a}, {"body_b", h.body_b}, {"volume", h.volume}}); + } + return json{{"ok", true}, {"count", int(hits.size())}, {"interferences", arr}}; +} + json action_set_variable(DesignPanel* panel, const json& params) { if (!params.contains("name")) throw std::runtime_error("set_variable needs 'name'"); @@ -1429,6 +1446,7 @@ std::string handle_on_main(const std::string& method, const json& params, const if (method == "surface_loft") return rpc_result(id, action_surface_loft(panel, params)); if (method == "surface_fill") return rpc_result(id, action_surface_fill(panel, params)); if (method == "mate") return rpc_result(id, action_mate(panel, params)); + if (method == "check_interference") return rpc_result(id, action_check_interference(panel, params)); return rpc_error(id, -32601, "Unknown method: " + method); } catch (const Standard_Failure& ex) { // OCCT errors are NOT std::exception return rpc_error(id, -32000, std::string("OCCT: ") + (ex.GetMessageString() ? ex.GetMessageString() : "failure")); diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index 2462574f76..bf6ffc66da 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -6229,4 +6229,123 @@ TEST_CASE("cylindrical mate with FaceAndDirection preserves rotation", "[CadDocu } REQUIRE(rot_preserved); // If cylindrical behaved like slider, the face would be at (1,0,0) instead. +} + +// --- Interference detection (M8c) --- + +TEST_CASE("interference: overlapping bodies reported with overlap volume", "[CadDocument][interference]") +{ + using Catch::Matchers::WithinAbs; + + CadDocument doc; + // A: 20x20x10 box centred on the origin in XY, z in [0,10] + int sk_a = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "BoxA"); + doc.add_extrude(sk_a, 10.0, false, BooleanMode::New, "EA"); + // B: 20x20x10 box, same footprint + int sk_b = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "BoxB"); + doc.add_extrude(sk_b, 10.0, false, BooleanMode::New, "EB"); + REQUIRE(doc.recompute()); + REQUIRE(doc.bodies.size() == 2); + + // Lift B by 6mm: the two overlap over 4mm of height => 20*20*4 = 1600 mm^3. + doc.add_transform(1, Vec3d(0, 0, 6), Vec3d(0, 0, 1), Vec3d(0, 0, 0), 0.0, false, "LiftB"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + + auto hits = doc.check_interference(); + REQUIRE(hits.size() == 1); + REQUIRE(hits[0].body_a == 0); + REQUIRE(hits[0].body_b == 1); + REQUIRE_THAT(hits[0].volume, WithinAbs(1600.0, 1e-3)); +} + +TEST_CASE("interference: disjoint and merely touching bodies are not reported", "[CadDocument][interference]") +{ + CadDocument doc; + int sk_a = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "BoxA"); + doc.add_extrude(sk_a, 10.0, false, BooleanMode::New, "EA"); + int sk_b = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "BoxB"); + doc.add_extrude(sk_b, 10.0, false, BooleanMode::New, "EB"); + REQUIRE(doc.recompute()); + + SECTION("clearly apart") { + doc.add_transform(1, Vec3d(0, 0, 50), Vec3d(0, 0, 1), Vec3d(0, 0, 0), 0.0, false, "MoveB"); + REQUIRE(doc.recompute()); + REQUIRE(doc.check_interference().empty()); + } + + SECTION("face-to-face contact encloses no volume") { + // B sits exactly on top of A: they share a face but nothing overlaps. + doc.add_transform(1, Vec3d(0, 0, 10), Vec3d(0, 0, 1), Vec3d(0, 0, 0), 0.0, false, "StackB"); + REQUIRE(doc.recompute()); + REQUIRE(doc.check_interference().empty()); + } +} + +TEST_CASE("interference: sheet bodies are skipped", "[CadDocument][interference]") +{ + CadDocument doc; + int sk_a = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "BoxA"); + doc.add_extrude(sk_a, 10.0, false, BooleanMode::New, "EA"); + // A sheet passing straight through the solid: it has no volume, so no interference. + int sk_s = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 40, 40, 0, "Sheet"); + doc.add_surface_extrude(sk_s, 5.0, "SE"); + REQUIRE(doc.recompute()); + REQUIRE(doc.bodies.size() == 2); + REQUIRE(CadDocument::is_sheet_shape(doc.bodies[1].shape)); + + REQUIRE(doc.check_interference().empty()); +} + +TEST_CASE("interference: reports every overlapping pair", "[CadDocument][interference]") +{ + CadDocument doc; + for (int i = 0; i < 3; ++i) { + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Box"); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "E"); + } + REQUIRE(doc.recompute()); + REQUIRE(doc.bodies.size() == 3); + + // 0 and 1 stay coincident (overlapping); 2 is moved clear of both. + doc.add_transform(2, Vec3d(0, 0, 100), Vec3d(0, 0, 1), Vec3d(0, 0, 0), 0.0, false, "MoveC"); + REQUIRE(doc.recompute()); + + auto hits = doc.check_interference(); + REQUIRE(hits.size() == 1); + REQUIRE(hits[0].body_a == 0); + REQUIRE(hits[0].body_b == 1); + + // min_volume gates the report: a threshold above the overlap silences it. + REQUIRE(doc.check_interference(1e9).empty()); +} + +TEST_CASE("interference: detects a clash created by a mate", "[CadDocument][interference]") +{ + CadDocument doc; + int sk_a = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "BoxA"); + doc.add_extrude(sk_a, 10.0, false, BooleanMode::New, "EA"); + int sk_b = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "BoxB"); + doc.add_extrude(sk_b, 10.0, false, BooleanMode::New, "EB"); + REQUIRE(doc.recompute()); + + // Park B well clear, so the clash below is created by the mate and nothing else. + doc.add_transform(1, Vec3d(0, 0, 80), Vec3d(0, 0, 1), Vec3d(0, 0, 0), 0.0, false, "ParkB"); + REQUIRE(doc.recompute()); + REQUIRE(doc.check_interference().empty()); + + // Fastened mate onto a connector inside A's volume drives B into A. + int cs_fixed = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(0, 0, 5), "CS_Fixed"); + doc.features[cs_fixed].coordsys_body = 0; + int cs_moving = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(0, 0, 85), "CS_Moving"); + doc.features[cs_moving].coordsys_body = 1; + REQUIRE(doc.recompute()); + + doc.add_mate(0, cs_fixed, cs_moving, 0.0, 0.0, false, "Clash"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + + auto hits = doc.check_interference(); + REQUIRE(hits.size() == 1); + REQUIRE(hits[0].volume > 1.0); } \ No newline at end of file