mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 11:51:33 +00:00
The mode-type tag (imex_mode_types config + imex_mode_type_for helper + Split sentinel) was added in0bb1cefas scaffolding for the Split rendering work that landed in4370ccaand then got reverted ind9be71b. 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>
306 lines
12 KiB
C++
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
|