diff --git a/src/libslic3r/IMEXHelpers.cpp b/src/libslic3r/IMEXHelpers.cpp index 87cf8dcc7b..5ffa82d616 100644 --- a/src/libslic3r/IMEXHelpers.cpp +++ b/src/libslic3r/IMEXHelpers.cpp @@ -6,8 +6,33 @@ #include #include +#include "libslic3r/PresetBundle.hpp" + namespace Slic3r { +ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem, + const ConfigOptionInts* printer_extruder_id) +{ + if (explicit_pem && explicit_pem->values.size() >= 2) + return *explicit_pem; + ConfigOptionInts derived; + if (printer_extruder_id) { + derived.values.reserve(printer_extruder_id->values.size()); + for (int v : printer_extruder_id->values) + derived.values.push_back(v - 1); + } + return derived; +} + +ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb) +{ + const ConfigOptionInts* explicit_pem = pb.project_config.option("physical_extruder_map"); + if (!explicit_pem || explicit_pem->values.size() < 2) + explicit_pem = pb.printers.get_edited_preset().config.option("physical_extruder_map"); + const ConfigOptionInts* pei = pb.printers.get_edited_preset().config.option("printer_extruder_id"); + return effective_physical_extruder_map(explicit_pem, pei); +} + int first_filament_for_physical_head(const ConfigOptionInts& pem, int physical) { const auto& v = pem.values; diff --git a/src/libslic3r/IMEXHelpers.hpp b/src/libslic3r/IMEXHelpers.hpp index 739d67f879..4b487ab7f6 100644 --- a/src/libslic3r/IMEXHelpers.hpp +++ b/src/libslic3r/IMEXHelpers.hpp @@ -11,6 +11,25 @@ namespace Slic3r { +class PresetBundle; + +// Returns the effective physical_extruder_map given an optionally-explicit map and +// the printer's `printer_extruder_id`. If `explicit_pem` has size >= 2 the caller +// authored one, and it is returned verbatim. Otherwise the map is auto-derived +// from `printer_extruder_id` by converting each 1-indexed value to 0-indexed. +// Returns an empty ConfigOptionInts if neither source yields any values. +// Slice-time (PrintApply) and GUI ghost-color paths both call this so a printer +// profile without an explicit pem still gets a consistent mapping. +ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem, + const ConfigOptionInts* printer_extruder_id); + +// GUI overload: resolves the effective pem from a live PresetBundle using the +// project_config → printer preset fallback, then derives from printer_extruder_id +// if neither holds a user-authored map (size >= 2). Use this instead of open-coding +// the lookup at ghost-color, tooltip, click-gate, and cache-key call sites so they +// all agree on what the slicer will see. +ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb); + // Returns the lowest 0-based logical filament index L such that pem[L] == physical. // Returns -1 if no filament routes to `physical`. // Degenerate case: an empty pem returns 0 when `physical == 0` (identity-on-head-0 diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 7e54e202bd..dfe85dfd83 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1,4 +1,5 @@ #include "ClipperUtils.hpp" +#include "IMEXHelpers.hpp" #include "Model.hpp" #include "Print.hpp" @@ -1168,11 +1169,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ { auto* pem = new_full_config.option("physical_extruder_map", true); const auto* pei = new_full_config.option("printer_extruder_id"); - if (pem && pei && !pei->values.empty() && pem->values.size() <= 1) { - pem->values.clear(); - for (int v : pei->values) - pem->values.push_back(v - 1); // convert 1-indexed extruder IDs to 0-indexed - } + if (pem) + pem->values = effective_physical_extruder_map(pem, pei).values; } m_ori_full_print_config = new_full_config; diff --git a/src/slic3r/GUI/IMEXFilamentPickerPopover.hpp b/src/slic3r/GUI/IMEXFilamentPickerPopover.hpp index 02b24b3651..43f190018c 100644 --- a/src/slic3r/GUI/IMEXFilamentPickerPopover.hpp +++ b/src/slic3r/GUI/IMEXFilamentPickerPopover.hpp @@ -30,7 +30,10 @@ private: void on_filament_selected(int slot_1_based); PartPlate* m_plate; - const ConfigOptionInts& m_pem; + // Owned by value: callers may pass a stack-local derived pem + // (e.g. effective_physical_extruder_map result) that won't outlive + // the popover's async lifetime. + ConfigOptionInts m_pem; int m_physical_head; CommitCallback m_on_commit; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 5e1617dd05..8df101c560 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -438,14 +438,9 @@ ColorRGBA PartPlate::get_imex_head_filament_color(int physical_head) const if (!pb) return GLVolume::UNPRINTABLE_COLOR; - const ConfigOptionInts* pem = pb->project_config.option("physical_extruder_map"); - if (!pem || pem->values.size() < 2) - pem = pb->printers.get_edited_preset().config.option("physical_extruder_map"); - if (!pem) - return GLVolume::UNPRINTABLE_COLOR; - + const ConfigOptionInts pem = effective_physical_extruder_map(*pb); const auto plate_map = get_imex_head_filament_map(); - const int logical = resolve_filament_for_head(plate_map, *pem, physical_head); + const int logical = resolve_filament_for_head(plate_map, pem, physical_head); if (logical < 0) return GLVolume::UNPRINTABLE_COLOR; @@ -979,13 +974,11 @@ std::string PartPlate::build_imex_ghost_cache_key() const if (k.empty()) return ""; // ghost-off when zones-off if (auto* pb = wxGetApp().preset_bundle) { - const ConfigOptionInts* pem = pb->project_config.option("physical_extruder_map"); - if (!pem || pem->values.size() < 2) - pem = pb->printers.get_edited_preset().config.option("physical_extruder_map"); - if (pem) { - k += "|pem"; - for (int v : pem->values) { k += ':'; k += std::to_string(v); } - } + // Keyed on the effective pem (project → printer → pei-derived) so a printer + // swap that only changes printer_extruder_id still invalidates the ghost cache. + const ConfigOptionInts pem = effective_physical_extruder_map(*pb); + k += "|pem"; + for (int v : pem.values) { k += ':'; k += std::to_string(v); } } k += "|obj"; for (const auto& oi : obj_to_instance_set) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 01f925b62e..09ec8f8ef3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -18543,14 +18543,15 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co return t; } - const ConfigOptionInts* pem = wxGetApp().preset_bundle->project_config.option("physical_extruder_map"); - if (!pem || pem->values.size() < 2) - pem = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("physical_extruder_map"); - + const ConfigOptionInts pem = effective_physical_extruder_map(*wxGetApp().preset_bundle); const auto map = plate->get_imex_head_filament_map(); - const int logical = pem ? resolve_filament_for_head(map, *pem, physical_head) : -1; + const int logical = resolve_filament_for_head(map, pem, physical_head); if (logical < 0) { - t.label = "T" + std::to_string(physical_head) + " -> (no filament routed)"; + // No filament slot resolves to this physical head. Usually means the printer's + // extruder count is understated relative to the filament palette (user added a + // filament without extending printer_extruder_id in the Machine tab). + t.label = "T" + std::to_string(physical_head) + " — no filament routed\n" + "Add an extruder in the Machine tab so this head has a filament slot."; return t; } t.filament_slot_1based = logical + 1; @@ -18563,13 +18564,10 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co void Plater::on_imex_ghost_click(int physical_head) { - const ConfigOptionInts* pem = wxGetApp().preset_bundle->project_config.option("physical_extruder_map"); - if (!pem || pem->values.size() < 2) - pem = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("physical_extruder_map"); - if (!pem) return; + const ConfigOptionInts pem = effective_physical_extruder_map(*wxGetApp().preset_bundle); int lane_count = 0; - for (int pv : pem->values) if (pv == physical_head) ++lane_count; + for (int pv : pem.values) if (pv == physical_head) ++lane_count; if (lane_count < 2) return; // single-lane head -> click is a no-op, tooltip conveyed status PartPlate* plate = p->partplate_list.get_curr_plate(); @@ -18577,7 +18575,7 @@ void Plater::on_imex_ghost_click(int physical_head) auto* picker = new IMEXFilamentPickerPopover( p->view3D->get_canvas3d()->get_wxglcanvas(), - plate, *pem, physical_head, + plate, pem, physical_head, [this]() { take_snapshot("edit imex head filament"); update_project_dirty_from_presets(); diff --git a/tests/libslic3r/test_imex_helpers.cpp b/tests/libslic3r/test_imex_helpers.cpp index 248a1feec2..7f660be78f 100644 --- a/tests/libslic3r/test_imex_helpers.cpp +++ b/tests/libslic3r/test_imex_helpers.cpp @@ -13,6 +13,42 @@ static ConfigOptionInts make_pem(std::vector v) { return o; } +TEST_CASE("effective_physical_extruder_map — explicit wins", "[IMEX]") { + auto explicit_pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + auto pei = make_pem({1, 2, 3, 4}); // would derive to {0,1,2,3} + auto out = effective_physical_extruder_map(&explicit_pem, &pei); + REQUIRE(out.values == std::vector{0, 0, 0, 0, 1, 2, 3}); +} + +TEST_CASE("effective_physical_extruder_map — default pem falls back to pei derive", "[IMEX]") { + auto default_pem = make_pem({0}); // size 1, the PrintConfig default + auto pei = make_pem({1, 2}); // 1-indexed IDEX + auto out = effective_physical_extruder_map(&default_pem, &pei); + REQUIRE(out.values == std::vector{0, 1}); // 1-indexed → 0-indexed +} + +TEST_CASE("effective_physical_extruder_map — null explicit, pei present", "[IMEX]") { + auto pei = make_pem({1, 2, 3}); + auto out = effective_physical_extruder_map(nullptr, &pei); + REQUIRE(out.values == std::vector{0, 1, 2}); +} + +TEST_CASE("effective_physical_extruder_map — both absent yields empty", "[IMEX]") { + auto out = effective_physical_extruder_map(nullptr, nullptr); + REQUIRE(out.values.empty()); +} + +TEST_CASE("effective_physical_extruder_map — IDEX ghost-color regression guard", "[IMEX]") { + // Printer with printer_extruder_id = [1, 2] and no explicit pem (default {0}). + // Before the GUI fix, this scenario produced a black ghost on T1 because + // first_filament_for_physical_head({0}, 1) == -1. + auto default_pem = make_pem({0}); + auto pei = make_pem({1, 2}); + auto pem = effective_physical_extruder_map(&default_pem, &pei); + REQUIRE(first_filament_for_physical_head(pem, 0) == 0); + REQUIRE(first_filament_for_physical_head(pem, 1) == 1); // no longer -1 +} + TEST_CASE("first_filament_for_physical_head — identity pem", "[IMEX]") { auto pem = make_pem({0, 1, 2, 3}); REQUIRE(first_filament_for_physical_head(pem, 0) == 0);