mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
test(imex): extract physical→logical helpers + cover them with unit tests
Three of the four IMEX physical-vs-logical fixes landed earlier on this
branch (fbc58d2a1d, fa048babeb, a38b95bf45, e11e7d46df) used inline
lambdas / direct loops to translate physical extruder indices to logical
filament slots. No test coverage existed for the specific composition,
even though the underlying primitives (resolve_filament_for_head,
first_filament_for_physical_head) were tested.
Pull two patterns out of Plater.cpp and GCode.cpp into IMEXHelpers as
named helpers, then unit-test them:
imex_primary_logical_from_objects(used_slots_1b, pem, primary_physical)
Walks the plate's used filament slots (1-based) and returns the
first one whose pem entry maps to the primary's physical extruder.
This is what the warning's `logical_for_primary` now delegates to —
moves the "look at object assignments, not pem first-routed default"
behavior introduced in fa048babeb out of the lambda and into a
separately-testable function.
imex_secondary_logical_slots(active_physicals, primary_physical,
plate_head_filament_map, pem)
Iterates IMEX active physicals, skips the one matching primary,
resolves each remainder via resolve_filament_for_head (per-plate
override + first-routed fallback), deduplicates, drops -1 entries.
Replaces the inline loop in GCode::_do_export's is_extruder_used
marking (a38b95bf45 + e11e7d46df).
10 new test cases in test_imex_helpers.cpp cover the cases that
correspond directly to bugs hit:
imex_primary_logical_from_objects:
- AFC primary picks the object's slot (the user's specific bug)
- Multi-color AFC primary returns first input-order match
- Direct extruder primary unambiguous (no MMU)
- No object routed to primary returns -1
- Empty inputs (no objects, empty pem)
imex_secondary_logical_slots:
- Copy mode skips primary, falls back to first-routed
- IQEX 4-mode enumerates all three secondaries
- Per-plate override wins over first-routed
- Drops unrouted physicals + deduplicates
- Only-primary-active returns empty
All [IMEX] + [Variant] + [3mf] regression: 166 assertions / 61 cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d41e0d09b4
commit
41457635e9
@@ -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));
|
||||
|
||||
@@ -160,6 +160,37 @@ std::map<int,int> parse_imex_head_filament_map(const std::string& s)
|
||||
return result;
|
||||
}
|
||||
|
||||
int imex_primary_logical_from_objects(const std::vector<int>& 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<int> imex_secondary_logical_slots(const std::vector<int>& active_physicals,
|
||||
int primary_physical,
|
||||
const std::map<int,int>& plate_head_filament_map,
|
||||
const ConfigOptionInts& pem)
|
||||
{
|
||||
std::vector<int> out;
|
||||
std::unordered_set<int> 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<int,int>& plate_map,
|
||||
const ConfigOptionInts& pem,
|
||||
int physical)
|
||||
|
||||
@@ -129,6 +129,37 @@ int resolve_filament_for_head(const std::map<int,int>& 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<int>& 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<int> imex_secondary_logical_slots(const std::vector<int>& active_physicals,
|
||||
int primary_physical,
|
||||
const std::map<int,int>& 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.
|
||||
|
||||
@@ -10040,13 +10040,13 @@ static std::vector<wxString> 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<int> 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 {
|
||||
|
||||
@@ -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<int>{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<int>{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<int, int> plate_map{{1, 6}};
|
||||
auto out = imex_secondary_logical_slots({0, 1}, /*primary*/0, plate_map, pem);
|
||||
REQUIRE(out == std::vector<int>{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<int>{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<int, int> 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<int>{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);
|
||||
|
||||
Reference in New Issue
Block a user