From d8103f794b964b203229ce03b6554b5ae86df335 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Fri, 14 Aug 2026 07:45:32 +0200 Subject: [PATCH] CAD: a mate needs B to have a body, so stop offering one when it does not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mate palette called all five kinds viable on connectors created as Point(world), which belong to no body. Clicking one built the Mate feature and the RECOMPUTE then failed with "mate: mate_cs_b has no associated body" — the refusal arrived one step too late, after the feature was already in the tree, and the user is told about it by an error line rather than by the palette that offered the thing. mate_options now checks the same two conditions the apply path throws on: B must have a body (B is the connector whose body moves; A is the fixed reference and needs none), and that body must still resolve. Either way all five kinds go non-viable with a reason, so the offer and the kernel cannot disagree. Verified on the rig: with two Point(world) connectors, every row is now dimmed and reads "connector B is not attached to a body — a mate moves B's body". That also exercises the dimmed-with-a-reason presentation for the first time, which until now had nothing to show because every kind was always viable. Tests: a new [mate] case covering no body, a body that no longer resolves, and the revival once B is given one. One existing case needed its setup widened rather than its assertion weakened: "an unrecorded fingerprint does not make a type non-viable" built two body-less connectors, so it was asserting a side effect of the old permissiveness instead of the property it is named for. Its connectors now have a body, leaving the missing fingerprint as the only variable. Full [CadDocument] suite: 2458 assertions in 185 cases. --- src/libslic3r/CadDocument.cpp | 21 +++++++++++++ tests/libslic3r/test_caddocument.cpp | 46 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index c9a001fb78..1050242407 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -1335,6 +1335,27 @@ std::vector CadDocument::mate_options(int cs_a, int cs_ return out; } + // B is the connector on the body that MOVES, so B must belong to one; A is the fixed + // reference and needs no body. Without this every kind was offered on a Point(world) + // connector and the mate only failed at RECOMPUTE, with "mate_cs_b has no associated body" — + // one step too late, after the feature already existed in the tree. The same two conditions + // the apply path throws on are checked here, so the offer and the kernel cannot disagree. + const int body_b = features[cs_b].coordsys_body; + if (body_b < 0) { + for (auto& o : out) { + o.viable = false; + o.reason = "connector B is not attached to a body — a mate moves B's body"; + } + return out; + } + if (body_b >= int(bodies.size()) || bodies[body_b].shape.IsNull()) { + for (auto& o : out) { + o.viable = false; + o.reason = "connector B's body no longer exists"; + } + return out; + } + const int ka = features[cs_a].coordsys_face_kind; const int kb = features[cs_b].coordsys_face_kind; auto face_desc = [](int kind) -> std::string { return kind == GeomAbs_Plane ? "a flat face" : "a curved face"; }; diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index 2fced4bbf2..4f388acf8a 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -7722,6 +7722,43 @@ TEST_CASE("mate_options always returns five entries in kind order", "[CadDocumen for (int i = 0; i < 5; ++i) REQUIRE(opts2[i].kind == i); } +TEST_CASE("a connector with no body dims every mate kind, with a reason", "[CadDocument][mate]") +{ + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Box"); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude"); + REQUIRE(doc.recompute()); + + // Point(world) connectors belong to no body, which is the default the GUI hands you. + int a = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(0, 0, 0), "A"); + int b = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(30, 0, 0), "B"); + REQUIRE(doc.features[b].coordsys_body < 0); + + // B is the connector whose body MOVES, so without one no kind can apply. The offer used to + // call all five viable and the mate only failed at recompute, after the feature existed. + auto opts = doc.mate_options(a, b); + REQUIRE(opts.size() == 5); + for (int i = 0; i < 5; ++i) { + REQUIRE(opts[i].kind == i); + REQUIRE_FALSE(opts[i].viable); + REQUIRE(opts[i].reason.find("not attached to a body") != std::string::npos); + } + + // A needs no body — it is the fixed reference. Giving B one is enough to revive the offer. + doc.features[b].coordsys_body = 0; + auto opts2 = doc.mate_options(a, b); + REQUIRE(opts2[0].viable); // Fastened is frame-only + REQUIRE(opts2[0].reason.empty()); + + // A body index that no longer resolves is refused too, and says so differently. + doc.features[b].coordsys_body = 7; + auto opts3 = doc.mate_options(a, b); + for (const auto& o : opts3) { + REQUIRE_FALSE(o.viable); + REQUIRE(o.reason.find("no longer exists") != std::string::npos); + } +} + TEST_CASE("a flat-face pair offers Fastened, Planar and Slider but not the axial types", "[CadDocument][mate]") { CadDocument doc; @@ -7767,8 +7804,17 @@ TEST_CASE("a flat-face pair offers Fastened, Planar and Slider but not the axial TEST_CASE("an unrecorded fingerprint does not make a type non-viable", "[CadDocument][mate]") { CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "Box"); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude"); + REQUIRE(doc.recompute()); + int a = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(0, 0, 0), "A"); int b = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(1, 0, 0), "B"); + // The MISSING FINGERPRINT is the whole subject here, so nothing else may be missing: B is + // given a body, because a connector without one is refused for that reason instead and the + // case would no longer isolate what this test is named for. + doc.features[a].coordsys_body = 0; + doc.features[b].coordsys_body = 0; // PointWorld connectors never record a face fingerprint: face_kind stays -1. REQUIRE(doc.features[a].coordsys_face_kind == -1); REQUIRE(doc.features[b].coordsys_face_kind == -1);