Files
OrcaSlicer/src/libslic3r/IMEXHelpers.cpp
T
Clifford GarwoodandClaude Opus 4.7 66891899e9 refactor(imex): drop unused Split mode-type infrastructure
The mode-type tag (imex_mode_types config + imex_mode_type_for helper +
Split sentinel) was added in 0bb1cef as scaffolding for the Split rendering
work that landed in 4370cca and then got reverted in d9be71b. With Phase 2-4
gone, this scaffolding is now unused dead code — and the design we settled
on instead is to leave topology entirely implicit (parsed from active_tools_str)
rather than carrying a per-mode type tag the user would otherwise have to
manage explicitly.

The multi-color slicing block + bare T<n> suppression that were the actual
substance of the safeguards work stay in place:
- imex_multicolor_block_reason still allows multi-color exactly when 2+ tools
  are active on the primary's gantry — Felix's hypothetical IQEX paired-gantry
  case works through this path, no new mode type required.
- Slicer-side: bare T<n> stays suppressed at print-start in IMEX parallel modes;
  mid-print T<n> emits naturally for the legitimate IQEX 4-tool-active scenario.

Removed:
- ConfigOptionStrings imex_mode_types (PrintConfig.hpp/cpp + Preset.cpp key list)
- imex_mode_type_for helper + kImexModeType{Primary,Copy,Mirror,Split} sentinels
- mode_type parameter on imex_multicolor_block_reason and the Split short-circuit
- mode_type plumbing in Print::validate and PartPlate::has_imex_multimaterial_conflict
- Three unit tests for imex_mode_type_for + two Split-specific multicolor block tests

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 00:48:39 -04:00

306 lines
12 KiB
C++

#include "libslic3r/IMEXHelpers.hpp"
#include <algorithm>
#include <cassert>
#include <cctype>
#include <sstream>
#include <unordered_set>
#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<ConfigOptionInts>("physical_extruder_map");
if (!explicit_pem || explicit_pem->values.size() < 2)
explicit_pem = pb.printers.get_edited_preset().config.option<ConfigOptionInts>("physical_extruder_map");
const ConfigOptionInts* pei = pb.printers.get_edited_preset().config.option<ConfigOptionInts>("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<int>& 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<int> 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<int>(i);
}
return -1;
}
bool has_mmu(const ConfigOptionInts& pem)
{
if (pem.values.size() < 2)
return false;
std::unordered_set<int> 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<int> seen;
for (int pv : pem.values) {
if (pv == primary_physical)
continue;
if (!seen.insert(pv).second)
return true;
}
return false;
}
std::vector<std::pair<int, ImexRole>> parse_imex_active_tools(const std::string& active_tools_for_mode)
{
std::vector<std::pair<int, ImexRole>> 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<int,int> parse_imex_head_filament_map(const std::string& s)
{
std::map<int,int> 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<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)
{
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<double, 3>(-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