#include "libslic3r/IMEXHelpers.hpp" #include #include #include #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 imex_pem_tool_for(int filament_id, const std::string& parallel_mode, const ConfigOptionInts& pem) { const bool imex_parallel = !parallel_mode.empty() && parallel_mode != kImexPrimaryMode; if (!imex_parallel || pem.values.empty()) return -1; return pem.get_at(filament_id); } bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned int toolchange_count) { return toolchange_count <= 1 && !parallel_mode.empty() && parallel_mode != kImexPrimaryMode; } std::string imex_multicolor_block_reason(const std::string& parallel_mode, const std::string& active_tools_str, int tools_per_gantry, const std::vector& used_filaments_0b, const ConfigOptionInts& pem) { // Not an IMEX parallel print → nothing to validate here. if (parallel_mode.empty() || parallel_mode == kImexPrimaryMode) return {}; // Single-color prints have no toolchanges to constrain. if (used_filaments_0b.size() <= 1) return {}; // MMU lane sharing detection: two or more used filaments routed to the same // physical head means a mid-print MMU swap on that head, which IMEX parallel // modes can't slave to the secondary gantry. if (!pem.values.empty()) { std::unordered_set seen_physicals; for (int filament : used_filaments_0b) { if (filament < 0 || filament >= (int)pem.values.size()) continue; const int phys = pem.get_at(filament); if (!seen_physicals.insert(phys).second) { return "Multi-color prints in IDEX/IQEX parallel modes require each filament " "to have its own dedicated physical extruder. Two or more of the active " "filaments are routed to the same physical head via the printer's " "physical extruder map (an MMU/AFC manifold), which the slaved gantry " "cannot follow."; } } } // Determine which physical head is the primary in this mode, then count how many // tool roles are assigned to tools on the same gantry. Multi-color requires at // least 2 (so there's a within-gantry toolchange topology). const int primary_physical = imex_primary_tool_for_mode(active_tools_str); if (primary_physical < 0) { return "The active IDEX/IQEX mode does not define a primary tool, so multi-color " "printing cannot be scheduled. Open the printer settings IDEX/IQEX Modes " "editor and assign a Primary role to one tool."; } const int tpg = std::max(1, tools_per_gantry); const int primary_gantry = primary_physical / tpg; int tools_on_primary_gantry = 0; for (const auto& [phys, role] : parse_imex_active_tools(active_tools_str)) { if (phys / tpg == primary_gantry) ++tools_on_primary_gantry; } if (tools_on_primary_gantry < 2) { return "Multi-color prints in IDEX/IQEX parallel modes require at least two tools " "assigned to the primary tool's gantry — there is no within-gantry " "toolchange path otherwise. Either reduce the print to a single filament, " "switch to Primary mode, or define a parallel mode that pairs two tools on " "the primary gantry."; } return {}; } int first_filament_for_physical_head(const ConfigOptionInts& pem, int physical) { const auto& v = pem.values; if (v.empty()) return (physical == 0) ? 0 : -1; // degenerate: treat as identity on head 0 for (size_t i = 0; i < v.size(); ++i) { if (v[i] == physical) return static_cast(i); } return -1; } bool has_mmu(const ConfigOptionInts& pem) { if (pem.values.size() < 2) return false; std::unordered_set seen; for (int pv : pem.values) { if (!seen.insert(pv).second) return true; } return false; } bool has_non_primary_mmu(const ConfigOptionInts& pem, int primary_physical) { if (pem.values.size() < 2) return false; std::unordered_set seen; for (int pv : pem.values) { if (pv == primary_physical) continue; if (!seen.insert(pv).second) return true; } return false; } std::vector> parse_imex_active_tools(const std::string& active_tools_for_mode) { std::vector> out; if (active_tools_for_mode.empty()) return out; std::istringstream ss(active_tools_for_mode); std::string tok; while (std::getline(ss, tok, ',')) { tok.erase(std::remove_if(tok.begin(), tok.end(), ::isspace), tok.end()); if (tok.empty()) continue; const auto colon = tok.find(':'); const std::string idx_str = (colon == std::string::npos) ? tok : tok.substr(0, colon); int phys = -1; try { phys = std::stoi(idx_str); } catch (...) { continue; } if (phys < 0 || phys >= (int)MAXIMUM_EXTRUDER_NUMBER) continue; ImexRole role = ImexRole::Copy; if (colon != std::string::npos) { const std::string r = tok.substr(colon + 1); if (r == "P") role = ImexRole::Primary; else if (r == "M") role = ImexRole::Mirror; // "C" and anything else → Copy. } out.emplace_back(phys, role); } return out; } int imex_primary_tool_for_mode(const std::string& active_tools_for_mode) { if (active_tools_for_mode.empty()) return -1; std::istringstream ss(active_tools_for_mode); std::string token; int first_bare = -1; while (std::getline(ss, token, ',')) { token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); if (token.empty()) continue; const auto colon = token.find(':'); const std::string idx_str = (colon == std::string::npos) ? token : token.substr(0, colon); int idx = -1; try { idx = std::stoi(idx_str); } catch (...) { continue; } if (idx < 0) continue; if (colon == std::string::npos) { // Backwards-compat: a bare index is Primary. if (first_bare < 0) first_bare = idx; continue; } const std::string role = token.substr(colon + 1); if (role == "P") return idx; } return first_bare; } std::map parse_imex_head_filament_map(const std::string& s) { std::map result; if (s.empty()) return result; std::istringstream ss(s); std::string token; while (std::getline(ss, token, ',')) { token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); if (token.empty()) continue; auto colon = token.find(':'); if (colon == std::string::npos) continue; try { int phys = std::stoi(token.substr(0, colon)); int slot = std::stoi(token.substr(colon + 1)); if (phys >= 0 && slot >= 1) result[phys] = slot; } catch (...) {} } 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) { auto it = plate_map.find(physical); if (it != plate_map.end()) { const int zero_based = it->second - 1; if (zero_based >= 0) return zero_based; } return first_filament_for_physical_head(pem, physical); } Transform3d imex_head_transform(int /*primary*/, int /*target*/, ImexRole role, const Vec2d& gantry_offset, const Vec2d& primary_zone_center) { switch (role) { case ImexRole::Primary: return Transform3d::Identity(); case ImexRole::Copy: return Eigen::Translation3d(gantry_offset.x(), gantry_offset.y(), 0.0) * Transform3d::Identity(); case ImexRole::Mirror: { const double len2 = gantry_offset.squaredNorm(); if (len2 < 1e-12) return Transform3d::Identity(); // True reflection about the zone-boundary plane (perpendicular to X, passing // through primary_zone_center.x + gantry_offset.x/2). This makes the ghost // land at the mirrored position within the target zone — matching where the // mirror tool will actually print — and reflects drag motion so X is inverted // while Y translates 1:1 by gantry_offset.y. Off-row Mirror targets (e.g. T3 // on a 2x2) still reflect across this X-plane rather than the diagonal, so // they visually match their on-row counterparts. // .linear() = Reflect(X) = diag(-1, 1, 1) // .translation() = (2*primary_zone_center.x + gantry_offset.x, gantry_offset.y, 0) // TODO: if a future IMEX printer has Y-oriented gantries, lift this to a // caller-supplied axis. Transform3d out = Transform3d::Identity(); out.linear() = Eigen::DiagonalMatrix(-1.0, 1.0, 1.0); out.translation() = Vec3d(2.0 * primary_zone_center.x() + gantry_offset.x(), gantry_offset.y(), 0.0); return out; } } return Transform3d::Identity(); } } // namespace Slic3r