#include "libslic3r/IMEXHelpers.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Config.hpp" #include "libslic3r.h" #include "Point.hpp" #include "BoundingBox.hpp" #include "Polygon.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/I18N.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PrintConfig.hpp" // Mark string for localization and translate. #define L(s) Slic3r::I18N::translate(s) namespace Slic3r { namespace { // Strip every whitespace byte from a token in place. // // Not `::isspace` directly: it takes an int that must be representable as unsigned char // (or equal EOF), and `char` is signed on all three targets, so any byte >= 0x80 is sign- // extended to a negative int and the call is undefined. The strings parsed below come from // printer profiles and 3MF metadata, neither of which is guaranteed ASCII, so the unsigned // char cast is the same one the rest of the codebase already applies to std::toupper. void strip_whitespace(std::string& token) { token.erase(std::remove_if(token.begin(), token.end(), [](unsigned char c) { return std::isspace(c) != 0; }), token.end()); } } // namespace ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem, int nozzle_count) { // One entry per logical extruder, so a profile has authored a map only when its length // matches. The single-element PrintConfig default does not, and is replaced rather than // treated as a one-extruder machine. if (explicit_pem && nozzle_count > 0 && (int) explicit_pem->values.size() == nozzle_count) return *explicit_pem; ConfigOptionInts derived; derived.values.resize(std::max(nozzle_count, 1)); std::iota(derived.values.begin(), derived.values.end(), 0); 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 auto* nozzles = pb.printers.get_edited_preset().config.option("nozzle_diameter"); return effective_physical_extruder_map(explicit_pem, nozzles ? (int) nozzles->values.size() : 0); } 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; // Bounds-checked, not get_at() -- see the note in IMEXHelpers.hpp for why the two index // spaces diverge. Emitting no tool qualifier is correct for a miss; pinning PA onto the // primary's carriage is not. if (filament_id < 0 || filament_id >= (int) pem.values.size()) return -1; return pem.values[filament_id]; } int imex_physical_heater_for(bool is_imex, const ConfigOptionInts& pem, int logical_id) { // Bounds-check rather than get_at(): get_at() CLAMPS to values.front(), which would // silently retarget an out-of-range id at whatever heater sits in slot 0. if (!is_imex || logical_id < 0 || logical_id >= (int) pem.values.size()) return logical_id; return pem.values[logical_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.values[filament]; // bounds-checked above; get_at would clamp if (!seen_physicals.insert(phys).second) { return L("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. const int primary_physical = imex_primary_tool_for_mode(active_tools_str); if (primary_physical < 0) { return L("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."); } // Walk the active tools once: collect the set of distinct gantries spanned by // the mode and check for an explicit Span marker on the primary's gantry. // Span declares "this tool is the multicolor partner of primary on the same // gantry" — without it, two tools on primary's gantry mean two independent // copies, not a within-gantry toolchange topology, which can't carry // multicolor in a parallel mode. const int tpg = std::max(1, tools_per_gantry); const int primary_gantry = primary_physical / tpg; bool span_on_primary_gantry = false; std::set active_gantries; for (const auto& [phys, role] : parse_imex_active_tools(active_tools_str)) { active_gantries.insert(phys / tpg); if (phys / tpg == primary_gantry && role == ImexRole::Span) span_on_primary_gantry = true; } // Single-gantry mode: the active tools all sit on one gantry, so no second // gantry is being copied/mirrored to. The "Copy"/"Mirror" label is decorative // — there's no actual parallel printing happening, just a regular multi-tool // single-gantry print. Multi-color in this configuration is a misconfiguration // (use Primary mode for that), and the user's mode_gcode would still emit // parallel-print firmware setup that doesn't apply here. if (active_gantries.size() < 2) { return L("Multi-color in this mode isn't a parallel-print scenario — all of the " "active tools sit on a single gantry, so there's no second gantry being " "copied or mirrored to. Switch the plate to Primary mode for multi-color " "printing on a single gantry, or define an IDEX/IQEX mode that includes " "tools on a second gantry."); } // Dual-gantry mode but no Span tool on the primary's gantry — the slicer would // emit toolchanges between filaments but the mode doesn't declare a within-gantry // multicolor partner, so the swap can't carry. The user might intend independent // copies (each gantry tool prints its own object) which is incompatible with // mid-print multicolor in a parallel mode. if (!span_on_primary_gantry) { return L("Multi-color prints in IDEX/IQEX parallel modes require a Span tool on the " "primary's gantry — without one, the mode doesn't declare a within-gantry " "multicolor partner. Either reduce the print to a single filament, switch to " "Primary mode, or open the printer settings IDEX/IQEX Modes editor and mark a " "tool on the primary's gantry as Span."); } // Only the primary and its Span partners print the plate's colors; every other active head // replays them. A color routed anywhere else -- a copying head, one the mode leaves inactive, // or past the end of the map -- has no head of its own to print from. std::set color_heads{ primary_physical }; for (const auto& [phys, role] : parse_imex_active_tools(active_tools_str)) if (phys / tpg == primary_gantry && role == ImexRole::Span) color_heads.insert(phys); for (int filament : used_filaments_0b) { const int phys = pem.values.empty() ? filament : (filament >= 0 && filament < (int) pem.values.size()) ? pem.values[filament] : -1; if (color_heads.count(phys)) continue; std::string tools; for (int head : color_heads) tools += (tools.empty() ? "T" : "/T") + std::to_string(head); // Filaments are numbered from 1 as the sidebar shows them, heads by their T index; the map // decides which head a filament is on, so with MMU lanes the two numbers differ. if (phys < 0) // TRN %1% is a list of tools such as "T0/T1", %2% a filament number. return (boost::format(L("In this Span mode, colors are printed only by %1%, but filament %2% isn't routed to any " "tool. Use filaments routed to %1%, or switch this plate to Primary mode.")) % tools % (filament + 1)).str(); // TRN %1% is a list of tools such as "T0/T1", %2% a filament number, %3% a tool number. return (boost::format(L("In this Span mode, colors are printed only by %1%, but filament %2% is routed to T%3%. Use " "filaments routed to %1%, or switch this plate to Primary mode.")) % tools % (filament + 1) % phys).str(); } 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; } // The def-side match is on type() rather than a dynamic_cast: ConfigOptionPercent and // ConfigOptionFloatOrPercent both derive from ConfigOptionFloat, so a cast would accept a // registration whose value cfg.option() had just refused, and silently return // the default while a real value sat in the config. For the int, float and bool accessors here // both halves therefore agree on the type. imex_cfg_enum() is the exception and goes the other // way -- dynamic_cast on both halves -- because every ConfigOptionEnum reports coEnum and a // type() check cannot tell one enum type from another; see the comment on it in IMEXHelpers.hpp. // // Reaching the final return means the key is not registered at all -- a typo in the literal, which // the compiler cannot catch. For the clearances that would be worse than the literal it replaced: // zero suppresses every collision strip. assert() is compiled out under NDEBUG, so it catches this // in a debug build only; the log line is what remains in Release, and the return is still a // degraded value. This is a programming-error path, not a runtime-input one. void imex_cfg_report_unregistered(const char* fn, const std::string& key) { BOOST_LOG_TRIVIAL(error) << fn << ": no registered default for " << key; } int imex_cfg_int(const ConfigBase& cfg, const std::string& key) { if (const auto* opt = cfg.option(key)) return opt->value; if (const ConfigOptionDef* def = print_config_def.get(key)) if (const ConfigOption* dv = def->default_value.get()) if (dv->type() == ConfigOptionInt::static_type()) return static_cast(dv)->value; assert(false && "imex_cfg_int: key not registered in print_config_def"); imex_cfg_report_unregistered("imex_cfg_int", key); return 0; } bool imex_cfg_bool(const ConfigBase& cfg, const std::string& key) { if (const auto* opt = cfg.option(key)) return opt->value; if (const ConfigOptionDef* def = print_config_def.get(key)) if (const ConfigOption* dv = def->default_value.get()) if (dv->type() == ConfigOptionBool::static_type()) return static_cast(dv)->value; assert(false && "imex_cfg_bool: key not registered in print_config_def"); imex_cfg_report_unregistered("imex_cfg_bool", key); return false; } double imex_cfg_float(const ConfigBase& cfg, const std::string& key) { if (const auto* opt = cfg.option(key)) return opt->value; if (const ConfigOptionDef* def = print_config_def.get(key)) if (const ConfigOption* dv = def->default_value.get()) if (dv->type() == ConfigOptionFloat::static_type()) return static_cast(dv)->value; assert(false && "imex_cfg_float: key not registered in print_config_def"); imex_cfg_report_unregistered("imex_cfg_float", key); return 0.0; } char imex_role_letter(ImexRole role) { for (const ImexRoleDesc& d : kImexRoleTable) if (d.role == role) return d.letter; // Only reachable if a role was added to the enum but not to kImexRoleTable. Serializing // it as Copy keeps the string parseable rather than emitting a token no parser accepts. return 'C'; } ImexRole imex_role_from_suffix(const std::string& suffix) { if (suffix.size() == 1) for (const ImexRoleDesc& d : kImexRoleTable) if (d.letter == suffix[0]) return d.role; return ImexRole::Copy; } 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, ',')) { strip_whitespace(tok); 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; // A bare token (no ':') is Copy here by design -- see the header note; the // "bare == Primary" backwards-compat rule lives in imex_primary_tool_for_mode(). const ImexRole role = (colon == std::string::npos) ? ImexRole::Copy : imex_role_from_suffix(tok.substr(colon + 1)); out.emplace_back(phys, role); } 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) { ImexGantryGrouping out; const int tpg = std::max(1, tools_per_gantry); const int primary = imex_primary_tool_for_mode(active_tools_for_mode); if (primary < 0) return out; out.primary_phys = primary; out.primary_gantry = primary / tpg; const int primary_col = primary % tpg; // Bucket every active tool by its gantry index. Skip the primary entry itself // since it carries no role-driven semantics for grouping (it's just the source). std::map>> by_gantry; bool span_seen = false; for (const auto& [phys, role] : parse_imex_active_tools(active_tools_for_mode)) { if (phys < 0) continue; const int g = phys / tpg; by_gantry[g].emplace_back(phys, role); if (g == out.primary_gantry && role == ImexRole::Span) span_seen = true; } out.span_on_primary = span_seen; for (auto& [g, tools] : by_gantry) { ImexGantryGroup grp; grp.gantry_index = g; grp.tools = tools; // Pick representative: column-paired to primary if active, else lowest phys. const int paired_phys = g * tpg + primary_col; auto pick = std::find_if(tools.begin(), tools.end(), [&](const auto& pr) { return pr.first == paired_phys; }); if (pick == tools.end()) pick = std::min_element(tools.begin(), tools.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); grp.representative_phys = pick->first; grp.representative_role = pick->second; // Aggregate iff Span is on primary's gantry, this is a non-primary gantry, // it has ≥2 tools, and all those tools share the same role (so the merged // visualization is unambiguous). Mixed-role gantries fall back to per-tool. const bool same_role = std::all_of(tools.begin(), tools.end(), [&](const auto& pr) { return pr.second == grp.representative_role; }); grp.aggregate = span_seen && g != out.primary_gantry && tools.size() >= 2 && same_role; out.groups.push_back(std::move(grp)); } 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, ',')) { strip_whitespace(token); 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; } // Same suffix reading as parse_imex_active_tools(), so the two can never disagree // about which letter means Primary. if (imex_role_from_suffix(token.substr(colon + 1)) == ImexRole::Primary) 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, ',')) { strip_whitespace(token); 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)); // Same absolute sanity cap parse_imex_active_tools applies to its own physical // index: neither can exceed the slicer-wide extruder ceiling. This only rejects // nonsense; the bound that matters (slot must index a filament that exists) is // in resolve_filament_for_head, which is the one that knows the count. if (phys >= 0 && phys < (int) MAXIMUM_EXTRUDER_NUMBER && slot >= 1 && slot <= (int) MAXIMUM_EXTRUDER_NUMBER) 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; } ImexRouting imex_resolve_routing(const ConfigBase& cfg, const std::string& parallel_mode, const std::vector& used_slots_1b, const ConfigOptionInts& pem) { ImexRouting out; // An empty mode is a plate that never carried one; kImexPrimaryMode is the reserved // "no parallel printing" sentinel. Neither resolves a row, so neither is looked up. if (parallel_mode.empty() || parallel_mode == kImexPrimaryMode) return out; out.parallel = true; // One lookup of the mode table, one parse of its roster, one search for the Primary // marker. Every field below is derived from these, so no caller can re-derive one of // them differently. out.active_tools = find_imex_mode(cfg, parallel_mode).active_tools; out.tools = parse_imex_active_tools(out.active_tools); out.primary_phys = imex_primary_tool_for_mode(out.active_tools); if (out.primary_phys >= 0) out.primary_logical = imex_primary_logical_from_objects(used_slots_1b, pem, out.primary_phys); // Bounds-checked exactly as imex_primary_logical_from_objects checks: get_at() would // clamp an out-of-range slot to values.front(), which is how a head that nothing routes // to can end up listed as a head something routes to. out.routed_heads.reserve(used_slots_1b.size()); for (int slot_1b : used_slots_1b) { const int slot_0b = slot_1b - 1; if (slot_0b >= 0 && slot_0b < (int) pem.values.size()) out.routed_heads.push_back(pem.values[slot_0b]); } std::sort(out.routed_heads.begin(), out.routed_heads.end()); out.routed_heads.erase(std::unique(out.routed_heads.begin(), out.routed_heads.end()), out.routed_heads.end()); // The pem emptiness test is not folded into primary_logical: an unpopulated map means // the printer has declared no routing at all, which is not the same claim as "this // plate's filaments go somewhere else", and only the latter is an error. out.primary_unrouted = out.primary_phys >= 0 && !pem.values.empty() && out.primary_logical < 0; return out; } 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; // pem is indexed by filament slot here, so an override outside it names a filament // this printer cannot route. Bounding here rather // than at the parse site is deliberate: the parser is handed a raw string with no // notion of how many filaments the project has, while every consumer of this // function's result indexes a per-filament array. The picker only ever offers // indices into pem (IMEXFilamentPickerPopover::build_row), so nothing the UI can // author is rejected -- only a hand-edited or corrupt 3MF. Such an override falls // through to the printer's own routing, i.e. it behaves as if it were absent, // rather than resolving to a wrong filament that get_at() would silently clamp. if (zero_based >= 0 && zero_based < (int) pem.values.size()) return zero_based; } return first_filament_for_physical_head(pem, physical); } ImexMirrorAxis imex_mirror_axis_for(int primary_phys, int target_phys, int tools_per_gantry) { const int tpg = std::max(1, tools_per_gantry); return (target_phys / tpg) == (primary_phys / tpg) ? ImexMirrorAxis::X : ImexMirrorAxis::Y; } Transform3d imex_head_transform(int /*primary*/, int /*target*/, ImexRole role, const Vec2d& gantry_offset, const Vec2d& primary_zone_center, ImexMirrorAxis mirror_axis) { switch (role) { case ImexRole::Primary: case ImexRole::Span: // Span is the within-gantry multicolor partner of Primary: it prints the same // objects in the same zone via mid-print toolchanges, so it has no zone of its own // to be translated or reflected into. Identity is the only transform that keeps it // on top of the primary. calc_imex_ghosts skips Span outright (no ghost is baked // for it, since the primary's ghost already covers that zone), so today this case // is unreachable — it is here so that a future caller which does reach it gets the // right answer rather than the fall-through, and so -Wswitch stays quiet. 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 between the two zones: perpendicular // to `mirror_axis`, passing through the midpoint of the zone centers along it. The // ghost lands at the mirrored position within the target zone — matching where the // mirror tool actually prints — and drag reflects across that plane, so the mirrored // axis inverts while the other translates 1:1. // X: linear = diag(-1, 1, 1), translation = (2*center.x + off.x, off.y, 0) // Y: linear = diag( 1,-1, 1), translation = (off.x, 2*center.y + off.y, 0) // Both have det = -1: a real mirror image, not a 180° rotation (which would be // diag(-1,-1,1), det = +1, and would print the primary's part merely turned around). Transform3d out = Transform3d::Identity(); if (mirror_axis == ImexMirrorAxis::Y) { out.linear() = Eigen::DiagonalMatrix(1.0, -1.0, 1.0); out.translation() = Vec3d(gantry_offset.x(), 2.0 * primary_zone_center.y() + gantry_offset.y(), 0.0); } else { 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(); } Vec2d compute_imex_slice_offset(bool firmware_managed, const std::string& parallel_mode, const std::optional& primary_zone_box) { if (!firmware_managed) return Vec2d::Zero(); if (parallel_mode.empty()) return Vec2d::Zero(); if (parallel_mode == kImexPrimaryMode) return Vec2d::Zero(); if (!primary_zone_box.has_value()) return Vec2d::Zero(); return primary_zone_box->center(); } namespace { // Builds the ImexMode for row `i`, padding whatever sibling array does not reach it. // `names` is known non-null and `i` known in range; `tools` / `gcodes` may be either. ImexMode imex_mode_at(const ConfigOptionStrings* names, const ConfigOptionStrings* tools, const ConfigOptionStrings* gcodes, size_t i) { ImexMode m; m.index = (int) i; m.name = names->values[i]; const bool has_tools = tools != nullptr && i < tools->values.size(); const bool has_gcode = gcodes != nullptr && i < gcodes->values.size(); if (has_tools) m.active_tools = tools->values[i]; if (has_gcode) m.gcode = gcodes->values[i]; m.ragged = !has_tools || !has_gcode; return m; } } // namespace ImexMode find_imex_mode(const ConfigBase& cfg, const std::string& name) { const auto* names = cfg.option("imex_mode_names"); if (names == nullptr) return {}; // The siblings are fetched once, not per row: a missing option is the same answer for // every row, and it is padding rather than a reason to fail the lookup. const auto* tools = cfg.option("imex_mode_active_tools"); const auto* gcodes = cfg.option("imex_mode_gcodes"); for (size_t i = 0; i < names->values.size(); ++i) if (names->values[i] == name) return imex_mode_at(names, tools, gcodes, i); return {}; } std::vector imex_mode_table(const ConfigBase& cfg) { std::vector table; const auto* names = cfg.option("imex_mode_names"); if (names == nullptr) return table; const auto* tools = cfg.option("imex_mode_active_tools"); const auto* gcodes = cfg.option("imex_mode_gcodes"); table.reserve(names->values.size()); for (size_t i = 0; i < names->values.size(); ++i) table.push_back(imex_mode_at(names, tools, gcodes, i)); return table; } std::vector imex_plate_mode_choices(const ConfigBase& cfg) { std::vector choices{ kImexPrimaryMode }; for (const ImexMode& m : imex_mode_table(cfg)) if (!m.name.empty() && std::find(choices.begin(), choices.end(), m.name) == choices.end()) choices.push_back(m.name); return choices; } bool imex_hull_violates_zones(const std::vector& zones, const Polygon& hull) { if (hull.points.empty()) return false; for (const auto& box : zones) { if (!intersection({box.polygon(true)}, {hull}).empty()) return true; } return false; } } // namespace Slic3r