diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ee936e95ca..c070d2a63f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2896,12 +2896,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato const int primary_physical = pem.values.empty() ? -1 : pem.get_at((int)initial_extruder_id); - for (int phys_tool : get_imex_active_tools(print)) { - if (phys_tool == primary_physical) continue; - const int logical = resolve_filament_for_head(plate_head_map, pem, phys_tool); - if (logical >= 0 && logical < (int)is_extruder_used.size()) + for (int logical : imex_secondary_logical_slots( + get_imex_active_tools(print), primary_physical, plate_head_map, pem)) + if (logical < (int)is_extruder_used.size()) is_extruder_used[logical] = true; - } } this->placeholder_parser().set("is_extruder_used", new ConfigOptionBools(is_extruder_used)); diff --git a/src/libslic3r/IMEXHelpers.cpp b/src/libslic3r/IMEXHelpers.cpp index 592a85c539..32cdfb24ac 100644 --- a/src/libslic3r/IMEXHelpers.cpp +++ b/src/libslic3r/IMEXHelpers.cpp @@ -160,6 +160,37 @@ std::map parse_imex_head_filament_map(const std::string& s) return result; } +int imex_primary_logical_from_objects(const std::vector& used_slots_1b, + const ConfigOptionInts& pem, + int primary_physical) +{ + if (pem.values.empty()) return -1; + const int pem_size = (int)pem.values.size(); + for (int slot_1b : used_slots_1b) { + const int slot_0b = slot_1b - 1; + if (slot_0b >= 0 && slot_0b < pem_size && pem.values[slot_0b] == primary_physical) + return slot_0b; + } + return -1; +} + +std::vector imex_secondary_logical_slots(const std::vector& active_physicals, + int primary_physical, + const std::map& plate_head_filament_map, + const ConfigOptionInts& pem) +{ + std::vector out; + std::unordered_set seen; + for (int phys : active_physicals) { + if (phys == primary_physical) continue; + const int logical = resolve_filament_for_head(plate_head_filament_map, pem, phys); + if (logical < 0) continue; + if (seen.insert(logical).second) + out.push_back(logical); + } + return out; +} + int resolve_filament_for_head(const std::map& plate_map, const ConfigOptionInts& pem, int physical) diff --git a/src/libslic3r/IMEXHelpers.hpp b/src/libslic3r/IMEXHelpers.hpp index 82b5b890bd..0d987a0af7 100644 --- a/src/libslic3r/IMEXHelpers.hpp +++ b/src/libslic3r/IMEXHelpers.hpp @@ -129,6 +129,37 @@ int resolve_filament_for_head(const std::map& plate_map, const ConfigOptionInts& pem, int physical); +// Returns the 0-based logical filament slot the IMEX *primary* tool prints with, +// based on what the plate's objects are actually assigned to. Walks `used_slots_1b` +// (1-based filament indices, e.g. from PartPlate::get_extruders) and returns the +// first one whose pem entry maps to `primary_physical`. Returns -1 if no object on +// the plate routes to the primary's physical extruder, or if pem is empty. +// +// On non-MMU/non-AFC printers (one logical per physical) this is unambiguous; on +// AFC layouts where multiple logicals route to one physical, the first match wins +// — sufficient for warning labels and is_extruder_used marking. Multi-color +// primaries on the AFC manifold may want all matches; that's a separate iteration. +int imex_primary_logical_from_objects(const std::vector& used_slots_1b, + const ConfigOptionInts& pem, + int primary_physical); + +// Returns the 0-based logical filament slots that IMEX *secondary* carriages +// will load during the print. Iterates `active_physicals` (the set returned by +// imex_mode_active_tools parsing — physical extruder indices), skips entries +// equal to `primary_physical` (the primary is owned by tool_ordering / +// per-object filament assignment, not enumerated here), and resolves each +// remaining physical via `resolve_filament_for_head` (per-plate override +// first, then first_filament_for_physical_head as fallback). +// +// Returned slots are deduplicated and -1 entries (no routing found) are +// dropped. Use this for is_extruder_used marking and pre-slice warnings' +// secondary lookup; the caller still owns whatever it does with the slots +// (mark a bool array, compare temps, etc.). +std::vector imex_secondary_logical_slots(const std::vector& active_physicals, + int primary_physical, + const std::map& plate_head_filament_map, + const ConfigOptionInts& pem); + enum class ImexRole { Primary, Copy, Mirror }; // Parses `imex_mode_active_tools[mode]` into a list of (physical_head, role) pairs. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 10bc8a6cbf..253072fac9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10040,13 +10040,13 @@ static std::vector collect_imex_warnings(PartPlate* plate) auto logical_for_primary = [&](int physical_idx) -> int { // get_extruders() returns 1-based filament slots used by objects on this plate. - const std::vector used_slots_1b = plate->get_extruders(true); - for (int slot_1b : used_slots_1b) { - const int slot_0b = slot_1b - 1; - if (slot_0b >= 0 && slot_0b < (int)pem.values.size() - && pem.values[slot_0b] == physical_idx) - return slot_0b; - } + const int from_objects = imex_primary_logical_from_objects( + plate->get_extruders(true), pem, physical_idx); + if (from_objects >= 0) return from_objects; + // No object on the plate routes to this physical: defensive fallback so the + // warning still has *something* to name. In practice this shouldn't fire when + // the active mode says this physical is primary — there has to be something + // assigned to it for the slicer to print. return first_filament_for_physical_head(pem, physical_idx); }; auto logical_for_secondary = [&](int physical_idx) -> int { diff --git a/tests/libslic3r/test_imex_helpers.cpp b/tests/libslic3r/test_imex_helpers.cpp index 289e9baeab..a1e160fdb4 100644 --- a/tests/libslic3r/test_imex_helpers.cpp +++ b/tests/libslic3r/test_imex_helpers.cpp @@ -128,6 +128,88 @@ TEST_CASE("has_mmu — empty / single-entry pem", "[IMEX]") { REQUIRE_FALSE(has_mmu(make_pem({0}))); } +TEST_CASE("imex_primary_logical_from_objects — AFC primary picks the object's slot", "[IMEX]") { + // User's Neo XP 0.6: pem maps slots 0-3 to physical 0 (4-lane AFC manifold), + // slots 4-6 to physicals 1/2/3. Object assigned to 1-based slot 3 = 0-based 2. + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + REQUIRE(imex_primary_logical_from_objects({3}, pem, 0) == 2); +} + +TEST_CASE("imex_primary_logical_from_objects — multi-color AFC primary returns first match", "[IMEX]") { + // Two objects on the AFC manifold (slots 0 and 2 in 1-based = slots 0 and 2 in + // 0-based wait that's wrong let me redo). Two objects: 1-based slots 1 and 3 + // (= 0-based 0 and 2). Both route to physical 0 via pem. First in input wins. + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + REQUIRE(imex_primary_logical_from_objects({1, 3}, pem, 0) == 0); // first match + REQUIRE(imex_primary_logical_from_objects({3, 1}, pem, 0) == 2); // order matters +} + +TEST_CASE("imex_primary_logical_from_objects — direct extruder primary unambiguous", "[IMEX]") { + // Object on 1-based slot 5 = 0-based 4 (direct extruder T1 in the user's layout). + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + REQUIRE(imex_primary_logical_from_objects({5}, pem, 1) == 4); +} + +TEST_CASE("imex_primary_logical_from_objects — no object routed to primary returns -1", "[IMEX]") { + // Object on 1-based slot 5 (= physical 1) but primary_physical is 0. No object + // on the plate routes to T0 — caller should fall back / treat as missing. + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + REQUIRE(imex_primary_logical_from_objects({5}, pem, 0) == -1); +} + +TEST_CASE("imex_primary_logical_from_objects — empty inputs", "[IMEX]") { + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + REQUIRE(imex_primary_logical_from_objects({}, pem, 0) == -1); // no objects + ConfigOptionInts empty_pem; + REQUIRE(imex_primary_logical_from_objects({1, 3}, empty_pem, 0) == -1); // empty pem +} + +TEST_CASE("imex_secondary_logical_slots — copy mode skips primary, falls back to first-routed", "[IMEX]") { + // User's setup: copy mode active = [0, 1] (T0 primary, T1 copy), no plate map override. + // Secondary T1 should resolve to first slot whose pem is 1 = slot 4. + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + auto out = imex_secondary_logical_slots({0, 1}, /*primary*/0, /*plate_map*/{}, pem); + REQUIRE(out == std::vector{4}); +} + +TEST_CASE("imex_secondary_logical_slots — IQEX 4-mode enumerates all secondaries", "[IMEX]") { + // iq-copy / iq-mirror: active = [0, 1, 2, 3], primary = 0. + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + auto out = imex_secondary_logical_slots({0, 1, 2, 3}, /*primary*/0, /*plate_map*/{}, pem); + REQUIRE(out == std::vector{4, 5, 6}); // first slot for each physical 1, 2, 3 +} + +TEST_CASE("imex_secondary_logical_slots — per-plate override wins for secondary", "[IMEX]") { + // User picks slot 6 (1-based) for T1 via the IMEX ghost picker. plate_map[1] = 6. + // resolve_filament_for_head should subtract 1: 0-based slot 5. + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + std::map plate_map{{1, 6}}; + auto out = imex_secondary_logical_slots({0, 1}, /*primary*/0, plate_map, pem); + REQUIRE(out == std::vector{5}); +} + +TEST_CASE("imex_secondary_logical_slots — drops unrouted physicals, deduplicates", "[IMEX]") { + // pem only has 2 entries (slot 0 -> phys 0, slot 1 -> phys 1). active includes + // a phys 2 that has no logical → should be dropped. Also: same pem has both + // slots routing to the same physical to test dedup. + auto pem = make_pem({0, 1}); + auto out = imex_secondary_logical_slots({0, 1, 2}, /*primary*/0, {}, pem); + REQUIRE(out == std::vector{1}); // phys 2 unrouted, primary skipped, only phys 1's slot 1 left + + // Dedup: two physicals resolving to the same logical (via plate_map override). + std::map dup_map{{1, 1}, {2, 1}}; // both T1 and T2 → 1-based slot 1 = 0-based 0 + auto pem2 = make_pem({0, 0, 0}); + auto out2 = imex_secondary_logical_slots({0, 1, 2}, /*primary*/0, dup_map, pem2); + REQUIRE(out2 == std::vector{0}); // both secondaries point at slot 0; only emitted once +} + +TEST_CASE("imex_secondary_logical_slots — only-primary-active returns empty", "[IMEX]") { + // Primary mode (just T0 active) → no secondaries. + auto pem = make_pem({0, 0, 0, 0, 1, 2, 3}); + auto out = imex_secondary_logical_slots({0}, /*primary*/0, {}, pem); + REQUIRE(out.empty()); +} + TEST_CASE("parse_imex_head_filament_map — round-trip", "[IMEX]") { auto m = parse_imex_head_filament_map("0:3,4:5"); REQUIRE(m.size() == 2);