From 0d36324323fc8074f3aa06aa6c38e01bd9404eb4 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Fri, 2 Oct 2026 11:17:29 -0400 Subject: [PATCH] Show the plate's IMEX mode on its mode button The plate's mode button showed one icon whatever the mode. It now shows the mode the plate slices as, the one its ghosts follow. imex_mode_kind() reads it from the heads beside the mode's primary: none is Normal, any Span head is Custom (the multicolor modes), otherwise any Mirror head is Mirror, and heads that all copy are Copy. One Mirror head is enough because an IQEX mirror mode copies within the primary's gantry. The icons are Felix14-v2's: the four kinds, each in light and dark with a hover state, replacing the single mode icon. Two fixes to them: the light Normal border used the dark theme's gray, and an opacity="undefined" attribute hid one of its strokes in nanosvg. The knight outlines are drawn at 1.0 rather than 0.8, matching the other plate icons and keeping Copy and Mirror apart when zoomed out. Co-authored-by: Felix14_v2 <75726196+Felix14-v2@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- resources/images/plate_imex_copy_mode.svg | 7 ++++ .../images/plate_imex_copy_mode_dark.svg | 7 ++++ .../images/plate_imex_copy_mode_hover.svg | 7 ++++ .../plate_imex_copy_mode_hover_dark.svg | 7 ++++ resources/images/plate_imex_custom_mode.svg | 10 +++++ .../images/plate_imex_custom_mode_dark.svg | 10 +++++ .../images/plate_imex_custom_mode_hover.svg | 10 +++++ .../plate_imex_custom_mode_hover_dark.svg | 10 +++++ resources/images/plate_imex_mirror_mode.svg | 7 ++++ .../images/plate_imex_mirror_mode_dark.svg | 7 ++++ .../images/plate_imex_mirror_mode_hover.svg | 7 ++++ .../plate_imex_mirror_mode_hover_dark.svg | 7 ++++ resources/images/plate_imex_mode.svg | 10 ----- resources/images/plate_imex_mode_dark.svg | 10 ----- resources/images/plate_imex_mode_hover.svg | 10 ----- .../images/plate_imex_mode_hover_dark.svg | 10 ----- resources/images/plate_imex_normal_mode.svg | 10 +++++ .../images/plate_imex_normal_mode_dark.svg | 10 +++++ .../images/plate_imex_normal_mode_hover.svg | 10 +++++ .../plate_imex_normal_mode_hover_dark.svg | 10 +++++ src/libslic3r/IMEXHelpers.cpp | 14 +++++++ src/libslic3r/IMEXHelpers.hpp | 9 +++++ src/slic3r/GUI/PartPlate.cpp | 37 +++++++++++-------- src/slic3r/GUI/PartPlate.hpp | 4 +- tests/libslic3r/test_imex_helpers.cpp | 20 ++++++++++ 25 files changed, 203 insertions(+), 57 deletions(-) create mode 100644 resources/images/plate_imex_copy_mode.svg create mode 100644 resources/images/plate_imex_copy_mode_dark.svg create mode 100644 resources/images/plate_imex_copy_mode_hover.svg create mode 100644 resources/images/plate_imex_copy_mode_hover_dark.svg create mode 100644 resources/images/plate_imex_custom_mode.svg create mode 100644 resources/images/plate_imex_custom_mode_dark.svg create mode 100644 resources/images/plate_imex_custom_mode_hover.svg create mode 100644 resources/images/plate_imex_custom_mode_hover_dark.svg create mode 100644 resources/images/plate_imex_mirror_mode.svg create mode 100644 resources/images/plate_imex_mirror_mode_dark.svg create mode 100644 resources/images/plate_imex_mirror_mode_hover.svg create mode 100644 resources/images/plate_imex_mirror_mode_hover_dark.svg delete mode 100644 resources/images/plate_imex_mode.svg delete mode 100644 resources/images/plate_imex_mode_dark.svg delete mode 100644 resources/images/plate_imex_mode_hover.svg delete mode 100644 resources/images/plate_imex_mode_hover_dark.svg create mode 100644 resources/images/plate_imex_normal_mode.svg create mode 100644 resources/images/plate_imex_normal_mode_dark.svg create mode 100644 resources/images/plate_imex_normal_mode_hover.svg create mode 100644 resources/images/plate_imex_normal_mode_hover_dark.svg diff --git a/resources/images/plate_imex_copy_mode.svg b/resources/images/plate_imex_copy_mode.svg new file mode 100644 index 0000000000..b938e909a1 --- /dev/null +++ b/resources/images/plate_imex_copy_mode.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_copy_mode_dark.svg b/resources/images/plate_imex_copy_mode_dark.svg new file mode 100644 index 0000000000..c87b0e0ffe --- /dev/null +++ b/resources/images/plate_imex_copy_mode_dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_copy_mode_hover.svg b/resources/images/plate_imex_copy_mode_hover.svg new file mode 100644 index 0000000000..4238552426 --- /dev/null +++ b/resources/images/plate_imex_copy_mode_hover.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_copy_mode_hover_dark.svg b/resources/images/plate_imex_copy_mode_hover_dark.svg new file mode 100644 index 0000000000..4633b747fe --- /dev/null +++ b/resources/images/plate_imex_copy_mode_hover_dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_custom_mode.svg b/resources/images/plate_imex_custom_mode.svg new file mode 100644 index 0000000000..99bc534382 --- /dev/null +++ b/resources/images/plate_imex_custom_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_custom_mode_dark.svg b/resources/images/plate_imex_custom_mode_dark.svg new file mode 100644 index 0000000000..d31da3bb3a --- /dev/null +++ b/resources/images/plate_imex_custom_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_custom_mode_hover.svg b/resources/images/plate_imex_custom_mode_hover.svg new file mode 100644 index 0000000000..c0cc65a2a4 --- /dev/null +++ b/resources/images/plate_imex_custom_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_custom_mode_hover_dark.svg b/resources/images/plate_imex_custom_mode_hover_dark.svg new file mode 100644 index 0000000000..4b5682dbb2 --- /dev/null +++ b/resources/images/plate_imex_custom_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_mirror_mode.svg b/resources/images/plate_imex_mirror_mode.svg new file mode 100644 index 0000000000..92f57755b3 --- /dev/null +++ b/resources/images/plate_imex_mirror_mode.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_mirror_mode_dark.svg b/resources/images/plate_imex_mirror_mode_dark.svg new file mode 100644 index 0000000000..b0ab1ee257 --- /dev/null +++ b/resources/images/plate_imex_mirror_mode_dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_mirror_mode_hover.svg b/resources/images/plate_imex_mirror_mode_hover.svg new file mode 100644 index 0000000000..1772d2c7fb --- /dev/null +++ b/resources/images/plate_imex_mirror_mode_hover.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_mirror_mode_hover_dark.svg b/resources/images/plate_imex_mirror_mode_hover_dark.svg new file mode 100644 index 0000000000..d50131ed4b --- /dev/null +++ b/resources/images/plate_imex_mirror_mode_hover_dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_mode.svg b/resources/images/plate_imex_mode.svg deleted file mode 100644 index 30a67cdf35..0000000000 --- a/resources/images/plate_imex_mode.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/images/plate_imex_mode_dark.svg b/resources/images/plate_imex_mode_dark.svg deleted file mode 100644 index 981fd88447..0000000000 --- a/resources/images/plate_imex_mode_dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/images/plate_imex_mode_hover.svg b/resources/images/plate_imex_mode_hover.svg deleted file mode 100644 index 9463e767df..0000000000 --- a/resources/images/plate_imex_mode_hover.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/images/plate_imex_mode_hover_dark.svg b/resources/images/plate_imex_mode_hover_dark.svg deleted file mode 100644 index 301fa995ab..0000000000 --- a/resources/images/plate_imex_mode_hover_dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/images/plate_imex_normal_mode.svg b/resources/images/plate_imex_normal_mode.svg new file mode 100644 index 0000000000..2d2324236f --- /dev/null +++ b/resources/images/plate_imex_normal_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_normal_mode_dark.svg b/resources/images/plate_imex_normal_mode_dark.svg new file mode 100644 index 0000000000..86d4b2ec0e --- /dev/null +++ b/resources/images/plate_imex_normal_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_normal_mode_hover.svg b/resources/images/plate_imex_normal_mode_hover.svg new file mode 100644 index 0000000000..3cdfb350fa --- /dev/null +++ b/resources/images/plate_imex_normal_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/plate_imex_normal_mode_hover_dark.svg b/resources/images/plate_imex_normal_mode_hover_dark.svg new file mode 100644 index 0000000000..70b26ed81b --- /dev/null +++ b/resources/images/plate_imex_normal_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/libslic3r/IMEXHelpers.cpp b/src/libslic3r/IMEXHelpers.cpp index f1a93f1619..57162638d4 100644 --- a/src/libslic3r/IMEXHelpers.cpp +++ b/src/libslic3r/IMEXHelpers.cpp @@ -308,6 +308,20 @@ std::vector> parse_imex_active_tools(const std::string& return out; } +ImexModeKind imex_mode_kind(const std::string& active_tools_for_mode) +{ + const int primary = imex_primary_tool_for_mode(active_tools_for_mode); + std::set roles; + for (const auto& [phys, role] : parse_imex_active_tools(active_tools_for_mode)) + if (phys != primary && role != ImexRole::Primary) + roles.insert(role); + if (roles.empty()) + return ImexModeKind::Primary; + if (roles.count(ImexRole::Span)) + return ImexModeKind::Custom; + return roles.count(ImexRole::Mirror) ? ImexModeKind::Mirror : ImexModeKind::Copy; +} + ImexGantryGrouping group_imex_active_tools_by_gantry(const std::string& active_tools_for_mode, int tools_per_gantry) { diff --git a/src/libslic3r/IMEXHelpers.hpp b/src/libslic3r/IMEXHelpers.hpp index 9a55f4ce63..eb54b6ee0f 100644 --- a/src/libslic3r/IMEXHelpers.hpp +++ b/src/libslic3r/IMEXHelpers.hpp @@ -439,6 +439,15 @@ ImexRole imex_role_from_suffix(const std::string& suffix); // in hand and need the full head/role list (e.g. ghost factory/updater). std::vector> parse_imex_active_tools(const std::string& active_tools_for_mode); +// What a mode does, as the plate's mode icon shows it. The heads beside the primary that +// imex_primary_tool_for_mode() finds decide: none is Primary, any Span head is Custom (the +// multicolor modes), any Mirror head is Mirror, and all heads copying is Copy. One Mirror head is +// enough because an IQEX mirror mode copies within the primary's gantry. +enum class ImexModeKind { Primary, Copy, Mirror, Custom }; +inline constexpr size_t kImexModeKindCount = 4; +static_assert(size_t(ImexModeKind::Custom) + 1 == kImexModeKindCount); +ImexModeKind imex_mode_kind(const std::string& active_tools_for_mode); + // ============================================================================= // The one derivation of "what does this plate's mode do, and does its filament reach // the primary" — shared by the hard block and the pre-slice warning diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 2bd7f2ee87..dab20fb356 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -2115,11 +2115,17 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) PresetBundle* pb = wxGetApp().preset_bundle; auto* is_imex_opt = pb ? pb->printers.get_edited_preset().config.option("is_imex") : nullptr; if (is_imex_opt && is_imex_opt->value) { + // The icon shows the mode the plate slices as, the one its ghosts follow. + std::string active_tools; + int primary_phys = -1; + const size_t kind = size_t(resolve_active_mode_tools(active_tools, primary_phys) ? + imex_mode_kind(active_tools) : + ImexModeKind::Primary); // Both the mode name and the conflict warning describe the same hovered icon, // and set_hover_tooltip records one string per frame, so they are composed here. std::string hover_tip; if (hover_id == (int)PLATE_IMEX_MODE_ID) { - render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_hovered_texture); + render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_hovered_textures[kind]); // The stored value, not the resolved one: this line carries the click // affordances, and the menu checkmark and the left-click cycle both act // on what the plate stores. Reporting the inherited mode here would leave @@ -2151,7 +2157,7 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) hover_tip = (boost::format("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)") % cur).str(); } } else { - render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_texture); + render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_textures[kind]); } // Warning badge: IMEX parallel mode active alongside multi-material objects if (has_imex_multimaterial_conflict()) { @@ -5407,18 +5413,17 @@ void PartPlateList::generate_icon_textures() } } - // IDEX/IQEX mode icon textures (fall back gracefully if SVG not present yet) + // IDEX/IQEX mode icon textures, one per ImexModeKind (fall back gracefully if an SVG is missing) { - file_name = path + (m_is_dark ? "plate_imex_mode_dark.svg" : "plate_imex_mode.svg"); - if (!m_imex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IDEX/IQEX mode icon)") % file_name; - } - } - { - file_name = path + (m_is_dark ? "plate_imex_mode_hover_dark.svg" : "plate_imex_mode_hover.svg"); - if (!m_imex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IDEX/IQEX mode hover icon)") % file_name; - } + static const char* kind_names[] = { "normal", "copy", "mirror", "custom" }; // the Primary kind shows "normal" + static_assert(std::size(kind_names) == kImexModeKindCount && std::tuple_size_v == kImexModeKindCount); + for (size_t kind = 0; kind < kImexModeKindCount; ++kind) + for (const bool hovered : { false, true }) { + file_name = path + "plate_imex_" + kind_names[kind] + "_mode" + (hovered ? "_hover" : "") + (m_is_dark ? "_dark.svg" : ".svg"); + GLTexture& texture = hovered ? m_imex_mode_hovered_textures[kind] : m_imex_mode_textures[kind]; + if (!texture.load_from_svg_file(file_name, true, false, false, icon_size)) + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IDEX/IQEX mode icon)") % file_name; + } } std::string text_str = "01"; @@ -5464,8 +5469,10 @@ void PartPlateList::release_icon_textures() m_plate_set_filament_map_hovered_texture.reset(); m_plate_name_edit_texture.reset(); m_plate_name_edit_hovered_texture.reset(); - m_imex_mode_texture.reset(); - m_imex_mode_hovered_texture.reset(); + for (GLTexture& texture : m_imex_mode_textures) + texture.reset(); + for (GLTexture& texture : m_imex_mode_hovered_textures) + texture.reset(); m_imex_warn_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index b74606572e..687e743343 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -769,8 +769,8 @@ class PartPlateList : public ObjectBase GLTexture m_plate_set_filament_map_hovered_texture; GLTexture m_plate_name_edit_texture; GLTexture m_plate_name_edit_hovered_texture; - GLTexture m_imex_mode_texture; - GLTexture m_imex_mode_hovered_texture; + std::array m_imex_mode_textures; // indexed by ImexModeKind + std::array m_imex_mode_hovered_textures; GLTexture m_imex_warn_texture; // warning badge for IMEX + multi-material conflict GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option diff --git a/tests/libslic3r/test_imex_helpers.cpp b/tests/libslic3r/test_imex_helpers.cpp index 0c4fb5dbf7..e27630a78a 100644 --- a/tests/libslic3r/test_imex_helpers.cpp +++ b/tests/libslic3r/test_imex_helpers.cpp @@ -873,6 +873,26 @@ TEST_CASE("parse_imex_active_tools - Span suffix whitespace tolerant", "[IMEX]") REQUIRE(out[1].second == ImexRole::Span); } +TEST_CASE("A mode's icon kind follows the roles of the heads beside its primary", "[IMEX]") { + const auto [roster, kind] = GENERATE(table({ + { "", ImexModeKind::Primary }, // a mode with no row prints as Primary + { "0:P", ImexModeKind::Primary }, + { "0", ImexModeKind::Primary }, // a legacy bare index is the primary + { "0:P,0:C", ImexModeKind::Primary }, // the primary's head, whatever else it lists + { "0:P,1:C", ImexModeKind::Copy }, + { "0:P,1:C,2:C,3:C", ImexModeKind::Copy }, + { "0,1,2", ImexModeKind::Copy }, + { "0:P,1:M", ImexModeKind::Mirror }, + { "0:P,1:C,2:M,3:M", ImexModeKind::Mirror }, // IQEX mirror copies within the primary's gantry + { "0,1:M", ImexModeKind::Mirror }, + { "0:P,1:S,2:C,3:C", ImexModeKind::Custom }, + { "0:P,1:S,2:M,3:M", ImexModeKind::Custom }, + { "0:P,1:S", ImexModeKind::Custom }, + })); + CAPTURE(roster); + REQUIRE(imex_mode_kind(roster) == kind); +} + TEST_CASE("group_imex_active_tools_by_gantry - paired-gantry mc-mirror aggregates", "[IMEX]") { // 2x2 IQEX, primary T0, T1 declared Span (multicolor partner on primary's gantry), // T2/T3 mirror with column-pairing T2↔T0 and T3↔T1.