From 416e7f7321362d98686cff8739490bb3fcfa5124 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Wed, 12 Aug 2026 21:53:38 +0200 Subject: [PATCH] Mate conflicts: mark the row that carries them, and name the way out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detect_mate_conflicts() has been filling m_doc.mate_conflicts on every recompute since the kernel half landed, and nothing read it. The diagnostics existed and were invisible — a conflicting assembly looked exactly like a working one. The tree row is where they go, because the tree is where the user is already looking for which feature to change. Three states, in precedence order: disabled -> dim. A SUPPRESSED mate is the user's answer to a conflict, so it must read as suppressed rather than keep shouting about it. conflict -> warn. otherwise -> normal. Selecting a marked row puts the reason on the status line — "Mate3 already positions Body 2", the cycle, the self-mate — and names the eye as the way to suppress it. A message that describes a problem with no action is a message that gets ignored; the action here is already one click away on the row just selected. Deliberately NOT a modal, and deliberately not treated as a document error. The document still evaluates with a conflict present: the mate graph merely has more than one answer for a body, and which one wins is the thing the user needs to see. Blocking the loop to say so would interrupt without helping. The dimming of non-involved bodies from the original UX proposal is still not implemented, on purpose: under transform composition a failure mid-chain propagates, so "not involved" is not a well-defined set, and dimming the wrong bodies would hide the context needed to understand the conflict. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-bioq. --- src/slic3r/GUI/DesignPanel.cpp | 38 ++++++++++++++++++++++++++++++---- src/slic3r/GUI/DesignPanel.hpp | 3 +++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index e5022b5ca3..ca281df9f2 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -2934,6 +2934,15 @@ DesignPanel::DesignPanel(wxWindow* parent) m_doc.features[sel].type != CadFeatureType::Sketch && !m_doc.body.IsNull()); m_viewport->set_body_highlight(body); + // A conflicting mate says WHY on selection, and names the one action that resolves it. + // Suppress is the generic per-feature enable toggle (the eye), so the answer is already + // one click away on a row the user has just selected — the message points at it instead + // of describing a problem with no way out. + if (const std::string* why = (sel >= 0) ? mate_conflict_reason(sel) : nullptr) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + set_status(wxString::FromUTF8(*why) + _L(" — the eye suppresses this mate")); + m_status->Refresh(); + } }); // Feature-tree edit actions: act on the selected feature (delete / reorder). These sit in the @@ -5852,6 +5861,16 @@ int DesignPanel::tree_icon_for(CadFeatureType t) return 0; } +// The reason detect_mate_conflicts() recorded for this feature, or nullptr. A linear scan: an +// assembly has a handful of mates and at most that many conflicts, so a map would cost more to +// build than the scans it saves. +const std::string* DesignPanel::mate_conflict_reason(int feature) const +{ + for (const auto& c : m_doc.mate_conflicts) + if (c.first == feature) return &c.second; + return nullptr; +} + // What to say when nothing is selected and no tool is open. Lives in one place because it is // needed from two: after an edit empties the document, and at startup — where after_tree_edit() // has never run, which is exactly why a fresh tab used to show a blank line. @@ -5946,12 +5965,23 @@ void DesignPanel::refresh_tree() // translucent rectangles (otherwise a Plane feature is invisible in the canvas). refresh_datum_planes(); update_reference_planes(); // body added/removed -> show/hide the XY/XZ/YZ origin planes - for (const auto& f : m_doc.features) { + for (size_t fi = 0; fi < m_doc.features.size(); ++fi) { + const CadFeature& f = m_doc.features[fi]; const int img = tree_icon_for(f.type); wxTreeItemId id = m_tree->AppendItem(root, wxString::FromUTF8(f.name), img, img); - // Hidden (disabled) features are greyed so the show/hide state reads at a glance. - m_tree->SetItemTextColour(id, f.enabled ? dp_item_text() - : dp_item_dim()); + // Three states, in this order of precedence: + // disabled -> dim. A SUPPRESSED mate is the user's answer to a conflict, so it must + // read as suppressed rather than keep shouting about the conflict. + // conflict -> warn. detect_mate_conflicts() refills m_doc.mate_conflicts on every + // recompute; the mark goes on the row that carries it, because the tree + // is where the user is already looking for which feature to change. + // otherwise -> normal. + // A conflict is NOT a document error — the document still evaluates — so the row is + // marked and never hidden, and the reason goes to the status line on selection rather + // than into a modal that interrupts without offering an action. + m_tree->SetItemTextColour(id, !f.enabled ? dp_item_dim() + : mate_conflict_reason(int(fi)) != nullptr ? wxColour(235, 110, 110) + : dp_item_text()); m_tree_items.push_back(id); } refresh_parts(); // bodies live in their own list below the tree, never clipped by history diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index 2397e5e2d5..580a1b2eb3 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -644,6 +644,9 @@ private: // Every status write goes through here so long hints wrap instead of clipping. void set_status(const wxString& text); wxString idle_hint() const; // what to say when nothing is selected + // Reason detect_mate_conflicts() recorded for a feature, or nullptr. Marks the tree row and + // feeds the status line; a conflict is a diagnostic, not a document error. + const std::string* mate_conflict_reason(int feature) const; wxMenuItem* append_offer_item(wxMenu* menu, int id, const wxString& text, const struct OfferVerb& v);