Read IMEX geometry defaults from print_config_def instead of literals

Five sites carried a hardcoded fallback for imex_tools_per_gantry that had
to match the value registered in print_config_def, with nothing enforcing
the agreement, and several explanatory comments miscounted the sites they
described or cited stale line numbers.

Add imex_cfg_int/_float/_bool/_enum<T> to IMEXHelpers, which return the
value registered for the key when it is absent from the config, so the
registration is the single source and there is nothing left to keep in
sync. Route every read of the IMEX geometry keys through them: 32 call
sites across IMEXZones, PartPlate, GCodeViewer and Tab. The only direct
lookup left is the bail in PartPlate::imex_multicolor_block_reason, which
must not default because it reports a routing conflict and a defaulted
grid would produce a false warning.

imex_cfg_enum uses dynamic_cast on both halves rather than the type()
comparison the others use: every ConfigOptionEnum<T> reports coEnum, so a
type() check cannot tell one enum type from another and would cast a
ConfigOptionEnum<OtherEnum> to the requested T. The ConfigOptionPercent :
ConfigOptionFloat inheritance that rules dynamic_cast out for the float
accessor has no analogue for enums.

Correct the comments that prompted this: the cache-key input list in
PartPlate named five inputs for a nine-part key, the ImexMarkerKey note
in GCodeViewer called imex_tool_layout an input only the preview reads
when the plate keys it too, and four file:line citations pointed at the
wrong lines. Values are unchanged at every converted site; cache key
strings keep their existing representation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-09-04 20:34:04 -04:00
co-authored by Claude Opus 5
parent e45c241466
commit 6c67fcfe07
10 changed files with 198 additions and 104 deletions
+1 -1
View File
@@ -3408,7 +3408,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// pollute is_extruder_used with the *first* slot routed to the primary's
// physical extruder, which is generally not the slot the user assigned to
// the printing object. Same skip-primary pattern as the IMEX PA emission
// path (GCode.cpp ~3265).
// path (GCode.cpp:3917).
//
// For secondaries: translate physical -> logical via the per-plate
// imex_head_filament_map (set by the IMEX ghost picker), with
+1 -1
View File
@@ -291,7 +291,7 @@ std::string GCodeWriter::set_temperature(unsigned int temperature, bool wait, in
} else {
// Every caller of this overload addresses filaments by LOGICAL id, but M104/M109
// name a physical heater -- so translate at the one point they all pass through.
// The static overload below is already physical-in (GCode.cpp:5931,
// The static overload below is already physical-in (GCode.cpp:5952,
// GCode/GCodeProcessor.cpp:1410) and must not be remapped, which is why the
// translation lives here and not there.
tool = imex_physical_heater_for(this->config.is_imex.value, this->config.physical_extruder_map, tool);
+60
View File
@@ -8,8 +8,11 @@
#include <sstream>
#include <unordered_set>
#include <boost/log/trivial.hpp>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r {
@@ -196,6 +199,63 @@ bool has_non_primary_mmu(const ConfigOptionInts& pem, int primary_physical)
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<ConfigOptionFloat>() 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<T> 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<ConfigOptionInt>(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<const ConfigOptionInt*>(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<ConfigOptionBool>(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<const ConfigOptionBool*>(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<ConfigOptionFloat>(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<const ConfigOptionFloat*>(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)
+42 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cassert>
#include <map>
#include <optional>
#include <string>
@@ -155,7 +156,7 @@ int imex_pem_tool_for(int filament_id, const std::string& parallel_mode, const C
// so it does not depend on a parallel mode being active the way imex_pem_tool_for does.
//
// Gated on is_imex because physical_extruder_map carries two readings in this tree: the
// BBL paths index it by extruder id (GCode.cpp:3333, WipeTower.cpp:1353), the IMEX paths
// BBL paths index it by extruder id (GCode.cpp:3295, WipeTower.cpp:1353), the IMEX paths
// by filament id. Those coincide only when the filament and nozzle counts match, so an
// ungated mapping would impose the IMEX reading on profiles that mean the other one --
// fdm_bbl_3dp_002_common ships a non-identity [1,0] and is spared today only because
@@ -331,6 +332,46 @@ inline constexpr ImexRoleDesc kImexRoleTable[] = {
{ ImexRole::Span, 'S' },
};
// Reads an IMEX geometry key, falling back to the value registered for that option in
// print_config_def when the key is absent from `cfg`, rather than to a literal repeated at the
// call site. Only a partial or hand-built config reaches the fallback -- anything instantiated
// from the ConfigDef carries every key -- but that is exactly the case a literal gets wrong.
int imex_cfg_int(const ConfigBase& cfg, const std::string& key);
double imex_cfg_float(const ConfigBase& cfg, const std::string& key);
// Enum sibling. ConfigOptionEnum<T> derives from ConfigOptionSingle<T>, not from ConfigOptionInt,
// so it cannot go through imex_cfg_int(). Header-inline because it is a template.
bool imex_cfg_bool(const ConfigBase& cfg, const std::string& key);
// Logs an unregistered-key lookup. Out-of-line to keep boost.log out of this header.
void imex_cfg_report_unregistered(const char* fn, const std::string& key);
template<class T>
T imex_cfg_enum(const ConfigBase& cfg, const std::string& key)
{
// dynamic_cast on BOTH halves here, unlike imex_cfg_int/_float. Every ConfigOptionEnum<T>
// reports coEnum, so the type() comparison that option<TYPE>() (Config.hpp:2643) and the
// int/float accessors rely on cannot tell one enum type from another: it would accept a
// ConfigOptionEnum<OtherEnum> and static_cast it to this T. ConfigOptionEnum<A> and
// ConfigOptionEnum<B> are unrelated siblings (both derive from ConfigOptionSingle<T>), so a
// dynamic_cast rejects the mismatch outright. The ConfigOptionPercent : ConfigOptionFloat
// inheritance that rules dynamic_cast out for imex_cfg_float has no analogue for enums.
if (const ConfigOption* opt = cfg.option(key))
if (const auto* e = dynamic_cast<const ConfigOptionEnum<T>*>(opt))
return e->value;
if (const ConfigOptionDef* def = print_config_def.get(key))
if (const ConfigOption* dv = def->default_value.get())
if (const auto* e = dynamic_cast<const ConfigOptionEnum<T>*>(dv))
return e->value;
// Same policy as imex_cfg_int/_float: an unregistered key is a typo the compiler cannot
// catch, so say so rather than return a silent zero that reads as a legitimate enumerator.
// The log call is out-of-line so this header, which much of libslic3r and the GUI includes,
// does not pull in boost.log for a path that only a typo reaches.
assert(false && "imex_cfg_enum: key not registered in print_config_def");
imex_cfg_report_unregistered("imex_cfg_enum", key);
return static_cast<T>(0);
}
// The on-disk suffix letter for `role`. Inverse of imex_role_from_suffix().
char imex_role_letter(ImexRole role);
+10 -18
View File
@@ -27,15 +27,12 @@ ImexZoneLayout compute_imex_zone_layout(const DynamicPrintConfig& printer_cfg,
return out;
// Grid dimensions and tool layout from printer config
auto* gantry_opt = printer_cfg.option<ConfigOptionInt>("imex_gantry_count");
auto* tpg_opt = printer_cfg.option<ConfigOptionInt>("imex_tools_per_gantry");
auto* layout_opt = printer_cfg.option<ConfigOptionEnum<ImexToolLayout>>("imex_tool_layout");
int n_cols = tpg_opt ? std::max(1, tpg_opt->value) : 2;
int n_rows = gantry_opt ? std::max(1, gantry_opt->value) : 1;
int n_cols = std::max(1, imex_cfg_int(printer_cfg, "imex_tools_per_gantry"));
int n_rows = std::max(1, imex_cfg_int(printer_cfg, "imex_gantry_count"));
// Which corner is T0? flip_x: col 0 is right(max-X); flip_y: row 0 is rear(max-Y)
const ImexToolLayout layout = layout_opt ? layout_opt->value : ImexToolLayout::FrontLeft;
const ImexToolLayout layout = imex_cfg_enum<ImexToolLayout>(printer_cfg, "imex_tool_layout");
bool flip_x = (layout == ImexToolLayout::FrontRight || layout == ImexToolLayout::RearRight);
bool flip_y = (layout == ImexToolLayout::RearLeft || layout == ImexToolLayout::RearRight);
@@ -269,18 +266,13 @@ ImexZoneLayout compute_imex_zone_layout(const DynamicPrintConfig& printer_cfg,
// !has_row_sep → full bed height; has_row_sep → primary row only
// !has_col_sep → full bed width; has_col_sep → primary column only
auto* cw_opt = printer_cfg.option<ConfigOptionFloat>("imex_nozzle_clearance_x");
auto* ch_opt = printer_cfg.option<ConfigOptionFloat>("imex_nozzle_clearance_y");
auto* mgn_opt = printer_cfg.option<ConfigOptionFloat>("imex_carriage_margin");
// Fallbacks mirror the values registered in PrintConfig.cpp: 30.0 for both clearances
// (6664, 6672), 0.0 for the margin (6680). The clearances must NOT fall back to 0.0 --
// both strip loops below are gated on `carriage_w > 0.0` / `carriage_h > 0.0`, so a zero
// would silently emit no collision strips at all while the preview still draws 30 mm
// toolhead boxes. A config built from the ConfigDef always carries these; a partial or
// hand-built one is the only way to reach the fallback.
double carriage_w = cw_opt ? cw_opt->value : 30.0;
double carriage_h = ch_opt ? ch_opt->value : 30.0;
double margin = mgn_opt ? mgn_opt->value : 0.0;
// Both strip blocks below are gated on `carriage_w > 0.0` / `carriage_h > 0.0`, so a zero
// clearance emits no collision strips at all while the preview still draws toolhead boxes.
// That is why these read through imex_cfg_float() rather than carrying a literal: the value
// for an absent key comes from the option's own registration, not from a number repeated here.
double carriage_w = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_x");
double carriage_h = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_y");
double margin = imex_cfg_float(printer_cfg, "imex_carriage_margin");
// Primary zone extent (the clear printable area):
// row-sep only → full bed width × primary row's Y band
+3 -2
View File
@@ -75,8 +75,9 @@ struct ImexZoneLayout
// `printer_cfg` — the edited printer preset's config. Read for is_imex, the tool grid
// (imex_gantry_count / imex_tools_per_gantry / imex_tool_layout), the
// mode roster (imex_mode_names / imex_mode_active_tools) and the strip
// widths (imex_nozzle_clearance_x / _y, imex_carriage_margin). A missing
// option falls back to the same default the caller would have seen.
// widths (imex_nozzle_clearance_x / _y, imex_carriage_margin). A key absent
// from the config falls back to the value registered for that option in
// print_config_def, via the imex_cfg_* accessors.
// `plate_mode` — the plate's own IMEX mode. Wins over the process preset unless it is
// `kImexPrimaryMode`.
// `process_mode` — the process preset's `imex_parallel_mode`, used only as the fallback
+14 -25
View File
@@ -1628,23 +1628,16 @@ GCodeViewer::ImexMarkerPlan GCodeViewer::resolve_imex_marker_plan(const DynamicP
// would place them off-bed. The firmware physically fans the centered
// toolpath out into the zones, so the centered preview with no
// secondaries is the honest representation.
auto* fw_opt = printer_cfg.opt<ConfigOptionBool>("imex_firmware_managed_zones");
if (fw_opt && fw_opt->value)
if (imex_cfg_bool(printer_cfg, "imex_firmware_managed_zones"))
return plan;
if (mode.empty() || mode == kImexPrimaryMode)
return plan;
auto* tpg_opt = printer_cfg.opt<ConfigOptionInt>("imex_tools_per_gantry");
auto* wx_opt = printer_cfg.opt<ConfigOptionFloat>("imex_nozzle_clearance_x");
auto* wy_opt = printer_cfg.opt<ConfigOptionFloat>("imex_nozzle_clearance_y");
// Only the gantry grouping and the mirror axis are decided here; the zone
// grid itself comes from compute_imex_zone_layout() below. The fallback is 2,
// matching PrintConfig.cpp:6643, compute_imex_zone_layout() and
// PartPlate::calc_imex_ghosts() -- all four must agree, or a missing key
// groups tools against a grid divided a different way.
const int tools_per_gantry = tpg_opt ? std::max(1, tpg_opt->value) : 2;
plan.box_wx = wx_opt ? (float)wx_opt->value : 30.0f;
plan.box_wy = wy_opt ? (float)wy_opt->value : 30.0f;
// Only the gantry grouping and the mirror axis are decided here; the zone grid itself comes
// from compute_imex_zone_layout() below, which reads the same keys through the same accessor.
const int tools_per_gantry = std::max(1, imex_cfg_int(printer_cfg, "imex_tools_per_gantry"));
plan.box_wx = (float)imex_cfg_float(printer_cfg, "imex_nozzle_clearance_x");
plan.box_wy = (float)imex_cfg_float(printer_cfg, "imex_nozzle_clearance_y");
// Parse "idx:P/C/M" via shared helpers — matches PartPlate::calc_imex_zones.
// The role travels as an ImexRole all the way to the marker placement
@@ -1911,12 +1904,6 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
// deliberately made against the live config rather than against a freshly built
// key, so an unchanged frame allocates nothing at all.
static const std::vector<std::string> s_no_strings;
auto* gantry_opt = printer_cfg.opt<ConfigOptionInt>("imex_gantry_count");
auto* tpg_opt = printer_cfg.opt<ConfigOptionInt>("imex_tools_per_gantry");
auto* layout_opt = printer_cfg.opt<ConfigOptionEnum<ImexToolLayout>>("imex_tool_layout");
auto* wx_opt = printer_cfg.opt<ConfigOptionFloat>("imex_nozzle_clearance_x");
auto* wy_opt = printer_cfg.opt<ConfigOptionFloat>("imex_nozzle_clearance_y");
auto* fw_opt = printer_cfg.opt<ConfigOptionBool>("imex_firmware_managed_zones");
auto* names_opt = printer_cfg.opt<ConfigOptionStrings>("imex_mode_names");
auto* tools_opt = printer_cfg.opt<ConfigOptionStrings>("imex_mode_active_tools");
const std::vector<std::string>& mode_names = names_opt ? names_opt->values : s_no_strings;
@@ -1924,12 +1911,14 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
ImexMarkerKey::Scalars key;
key.plate_index = plate_list.get_curr_plate_index();
key.gantry_count = gantry_opt ? gantry_opt->value : 0;
key.tools_per_gantry = tpg_opt ? tpg_opt->value : 0;
key.tool_layout = layout_opt ? (int)layout_opt->value : -1;
key.clearance_x = wx_opt ? wx_opt->value : 0.0;
key.clearance_y = wy_opt ? wy_opt->value : 0.0;
key.firmware_managed = fw_opt && fw_opt->value;
// Same accessor the plan is built from: a key holding a different fallback than
// the value actually laid out would compare equal across a real change.
key.gantry_count = imex_cfg_int(printer_cfg, "imex_gantry_count");
key.tools_per_gantry = imex_cfg_int(printer_cfg, "imex_tools_per_gantry");
key.tool_layout = (int)imex_cfg_enum<ImexToolLayout>(printer_cfg, "imex_tool_layout");
key.clearance_x = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_x");
key.clearance_y = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_y");
key.firmware_managed = imex_cfg_bool(printer_cfg, "imex_firmware_managed_zones");
key.bed_min_x = bed_extents.min.x();
key.bed_min_y = bed_extents.min.y();
key.bed_max_x = bed_extents.max.x();
+12 -4
View File
@@ -243,10 +243,18 @@ private:
// nothing that changes between frames. A stale plan would put the preview markers
// somewhere the plate's own zones and ghosts do not agree with, which is exactly the
// drift the shared layout call exists to prevent -- so this deliberately mirrors
// PartPlate::build_imex_cache_key(), and adds the two inputs only the preview reads:
// the plate's bed extents (zone centres scale with them) and imex_tool_layout (the
// T0-corner flips inside compute_imex_zone_layout). imex_carriage_margin is absent on
// purpose: it only sizes the plate's advisory bands, which the preview never draws.
// PartPlate::build_imex_cache_key(). The two keys are not field-for-field identical, and
// the differences are deliberate rather than incidental:
// - bed extents and plate_index are here and not there. Zone centres scale with the
// extents, and one preview serves every plate, so the preview must key what the plate
// gets for free -- it re-bakes on set_shape() and is keyed by being that plate.
// - mode_names / mode_active_tools hold the printer's WHOLE mode table; the plate resolves
// one active mode and keys that roster plus its primary head. Same information reached
// two ways, so a change to the active mode moves both keys.
// - imex_carriage_margin is there and not here: it only sizes the plate's advisory bands,
// which the preview never draws.
// imex_tool_layout is in both, which the plate's key gained for the reason this one has it:
// the T0-corner flip moves every zone rectangle while nothing else keyed changes.
struct ImexMarkerKey
{
// Scalar half. Built fresh on the stack each frame -- it allocates nothing -- and
+30 -33
View File
@@ -655,8 +655,11 @@ void PartPlate::calc_imex_zones()
// Inputs that contribute to the key (any change must invalidate the IMEX zone cache):
// - active_mode (per-plate or process-preset fallback)
// - imex_tools_per_gantry, imex_gantry_count (grid shape)
// - imex_tool_layout (which corner tool 0 occupies)
// - imex_nozzle_clearance_x, imex_nozzle_clearance_y (zone widths / collision strips)
// - imex_carriage_margin (zone shrink)
// - the active mode's tool roster and its Primary head (same name, different roster)
// - imex_firmware_managed_zones (suppresses ghost generation)
//
// IMPORTANT: if you add a printer config option that affects zone geometry, ghost transforms,
// or collision strips, it MUST be incorporated here — otherwise ghost meshes and zone overlays
@@ -683,11 +686,6 @@ std::string PartPlate::build_imex_cache_key() const
if (mode_opt && !mode_opt->value.empty())
active_mode = mode_opt->value;
}
auto* n_col_opt = printer_cfg.option<ConfigOptionInt>("imex_tools_per_gantry");
auto* n_row_opt = printer_cfg.option<ConfigOptionInt>("imex_gantry_count");
auto* cw_opt = printer_cfg.option<ConfigOptionFloat>("imex_nozzle_clearance_x");
auto* ch_opt = printer_cfg.option<ConfigOptionFloat>("imex_nozzle_clearance_y");
auto* mgn_opt = printer_cfg.option<ConfigOptionFloat>("imex_carriage_margin");
// Mode NAME alone is not printer identity: two presets can define the same mode
// name with different tool rosters/primary, and imex_firmware_managed_zones
// suppresses ghost generation entirely. Both shape the baked zone/ghost set, so
@@ -696,23 +694,24 @@ std::string PartPlate::build_imex_cache_key() const
std::string active_tools;
int primary_phys = -1;
resolve_active_mode_tools(active_tools, primary_phys); // empty on failure — keyed as such
auto* fw_opt = printer_cfg.option<ConfigOptionBool>("imex_firmware_managed_zones");
// imex_tool_layout decides which physical corner tool 0 occupies (flip_x / flip_y inside
// compute_imex_zone_layout), so every zone rectangle, collision strip and ghost offset moves
// when it changes while every other keyed field stays put. Without it the key is identical
// across a layout change and the baked zones go stale; that this currently appears to work is
// incidental -- some other path happens to rebuild -- not something to rely on.
auto* layout_opt = printer_cfg.option<ConfigOptionEnum<ImexToolLayout>>("imex_tool_layout");
// Geometry values go through imex_cfg_* so the key is built from the same numbers
// compute_imex_zone_layout() lays out with; a literal here could key on a value the layout
// never used.
return active_mode
+ "|" + std::to_string(n_col_opt ? n_col_opt->value : 2)
+ "x" + std::to_string(n_row_opt ? n_row_opt->value : 1)
+ "|ly" + std::to_string(layout_opt ? (int)layout_opt->value : 0)
+ "|cw" + std::to_string(cw_opt ? (int)(cw_opt->value * 10) : 0)
+ "|ch" + std::to_string(ch_opt ? (int)(ch_opt->value * 10) : 0)
+ "|mg" + std::to_string(mgn_opt ? (int)(mgn_opt->value * 10) : 0)
+ "|" + std::to_string(imex_cfg_int(printer_cfg, "imex_tools_per_gantry"))
+ "x" + std::to_string(imex_cfg_int(printer_cfg, "imex_gantry_count"))
+ "|ly" + std::to_string((int)imex_cfg_enum<ImexToolLayout>(printer_cfg, "imex_tool_layout"))
+ "|cw" + std::to_string((int)(imex_cfg_float(printer_cfg, "imex_nozzle_clearance_x") * 10))
+ "|ch" + std::to_string((int)(imex_cfg_float(printer_cfg, "imex_nozzle_clearance_y") * 10))
+ "|mg" + std::to_string((int)(imex_cfg_float(printer_cfg, "imex_carriage_margin") * 10))
+ "|t" + active_tools
+ "|p" + std::to_string(primary_phys)
+ "|fw" + ((fw_opt && fw_opt->value) ? "1" : "0");
+ "|fw" + (imex_cfg_bool(printer_cfg, "imex_firmware_managed_zones") ? "1" : "0");
}
// Reposition the IMEX mode icon without requiring a full set_shape() rebuild.
@@ -831,9 +830,8 @@ void PartPlate::calc_imex_ghosts()
// placement at primary_zone_center + gantry_offset) would draw them at positions the
// firmware doesn't honor (e.g. off-bed once the centered slice is in play). Suppress
// ghost generation entirely so we don't lie about something the slicer doesn't own.
if (auto* fw_opt = wxGetApp().preset_bundle->printers.get_edited_preset()
.config.option<ConfigOptionBool>("imex_firmware_managed_zones");
fw_opt && fw_opt->value)
if (imex_cfg_bool(wxGetApp().preset_bundle->printers.get_edited_preset().config,
"imex_firmware_managed_zones"))
return;
// Zone centers are the basis for ghost placement; make sure they exist before
@@ -847,14 +845,10 @@ void PartPlate::calc_imex_ghosts()
// non-primary gantry is represented by a single ghost — its column-paired rep —
// so non-rep tools on aggregated gantries are skipped. This single source of
// pairing truth keeps ghost emission and zone aggregation in lockstep.
// 2 matches the value registered in PrintConfig.cpp:6643 and the fallback in
// compute_imex_zone_layout(). All three must agree: the zone rectangles come from the
// library, so a different grouping default here would pair tools against a grid that
// was divided differently.
int tpg = 2;
if (auto* tpg_opt = wxGetApp().preset_bundle->printers.get_edited_preset()
.config.option<ConfigOptionInt>("imex_tools_per_gantry"))
tpg = std::max(1, tpg_opt->value);
// Grouping must divide the grid the same way compute_imex_zone_layout() does, so the key is
// read through the same accessor rather than against a literal repeated here.
const int tpg = std::max(1, imex_cfg_int(
wxGetApp().preset_bundle->printers.get_edited_preset().config, "imex_tools_per_gantry"));
const ImexGantryGrouping grouping =
group_imex_active_tools_by_gantry(active_tools_str, tpg);
auto is_aggregated = [&](int phys) -> bool {
@@ -998,14 +992,11 @@ void PartPlate::update_imex_ghost_transforms(
// moves the ghost exactly as a rebuild would place it. See the note there: for an
// aggregated gantry both X frames sit on the bed centerline, which zeroes gantry_offset.x
// so the cross-gantry mirror tracks primary's X and reflects in Y.
// 2 matches the value registered in PrintConfig.cpp:6643 and the fallback in
// compute_imex_zone_layout(). All three must agree: the zone rectangles come from the
// library, so a different grouping default here would pair tools against a grid that
// was divided differently.
int tpg = 2;
if (auto* tpg_opt = wxGetApp().preset_bundle->printers.get_edited_preset()
.config.option<ConfigOptionInt>("imex_tools_per_gantry"))
tpg = std::max(1, tpg_opt->value);
// Same accessor, same reason as calc_imex_ghosts() above: this must group the grid the way
// compute_imex_zone_layout() does. The grouping/is_aggregated preamble that follows is
// duplicated from that function; the duplication predates this change and is left alone.
const int tpg = std::max(1, imex_cfg_int(
wxGetApp().preset_bundle->printers.get_edited_preset().config, "imex_tools_per_gantry"));
const ImexGantryGrouping grouping =
group_imex_active_tools_by_gantry(active_tools_str, tpg);
auto is_aggregated = [&](int phys) -> bool {
@@ -1229,6 +1220,12 @@ bool PartPlate::has_imex_multimaterial_conflict() const
const std::string mode = get_imex_mode();
if (mode == kImexPrimaryMode) return false;
// Both keys bail rather than defaulting through imex_cfg_int(), deliberately, and for the same
// reason: this reports a routing CONFLICT, so it must run on the printer's real configuration
// or not at all. Without physical_extruder_map there is no mapping to check; with a defaulted
// grid the conflict would be computed against a shape the printer does not have. A false
// conflict warning is worse than staying quiet, so an absent key means "no answer" here --
// unlike the geometry paths, where a defaulted value still describes a drawable bed.
auto* tpg_opt = printer_cfg.option<ConfigOptionInt>("imex_tools_per_gantry");
auto* pem_opt = printer_cfg.option<ConfigOptionInts>("physical_extruder_map");
if (!tpg_opt || !pem_opt) return false;
+25 -19
View File
@@ -5489,6 +5489,18 @@ PageShp TabPrinter::build_kinematics_page()
* but "Motion ability" and "Single extruder MM setup" too
* (These pages can changes according to the another values of a current preset)
* */
// Grid shape for the IMEX modes editor, for the three sites that build or resize the control:
// build_unregular_pages(), reload_config() and update_fff(). They must read these keys the way
// compute_imex_zone_layout() does, so this goes through the shared accessors rather than
// opt_int(), which does not throw on an absent key -- it dereferences the null that
// option<ConfigOptionInt>() returns (Config.hpp:2991), where every other reader falls back.
static void imex_grid_shape(const DynamicPrintConfig* cfg, int& n_cols, int& n_rows, int& layout)
{
n_cols = std::max(1, imex_cfg_int(*cfg, "imex_tools_per_gantry"));
n_rows = std::max(1, imex_cfg_int(*cfg, "imex_gantry_count"));
layout = IMEXModesCtrl::parse_layout(imex_cfg_enum<ImexToolLayout>(*cfg, "imex_tool_layout"));
}
void TabPrinter::build_unregular_pages(bool from_initial_build/* = false*/)
{
size_t n_before_extruders = 2; // Count of pages before Extruder pages
@@ -5682,11 +5694,9 @@ if (is_marlin_flavor)
auto line = Line{ L("Modes"), L("") };
line.full_width = 1;
line.widget = [this](wxWindow* parent) -> wxSizer* {
int n_cols = m_config->opt_int("imex_tools_per_gantry");
int n_rows = m_config->opt_int("imex_gantry_count");
ImexToolLayout layout = ImexToolLayout::FrontLeft;
if (auto* o = m_config->option<ConfigOptionEnum<ImexToolLayout>>("imex_tool_layout")) layout = o->value;
m_imex_modes_ctrl = new IMEXModesCtrl(parent, n_cols, n_rows, IMEXModesCtrl::parse_layout(layout));
int n_cols = 0, n_rows = 0, layout = 0;
imex_grid_shape(m_config, n_cols, n_rows, layout);
m_imex_modes_ctrl = new IMEXModesCtrl(parent, n_cols, n_rows, layout);
// Lazy lookup pointing at the *saved* state of the currently-selected
// preset (not the parent). Per-row reset means "discard in-session
// edits to this row" — matches the page-level reset semantic and works
@@ -6052,11 +6062,9 @@ void TabPrinter::reload_config()
// standard Choice fields; only the modes grid still needs explicit re-sync
// because it spans three options at once and isn't a Field.
if (m_imex_modes_ctrl) {
int n_cols = m_config->opt_int("imex_tools_per_gantry");
int n_rows = m_config->opt_int("imex_gantry_count");
ImexToolLayout layout = ImexToolLayout::FrontLeft;
if (auto* o = m_config->option<ConfigOptionEnum<ImexToolLayout>>("imex_tool_layout")) layout = o->value;
m_imex_modes_ctrl->set_grid_size(n_cols, n_rows, IMEXModesCtrl::parse_layout(layout));
int n_cols = 0, n_rows = 0, layout = 0;
imex_grid_shape(m_config, n_cols, n_rows, layout);
m_imex_modes_ctrl->set_grid_size(n_cols, n_rows, layout);
m_imex_modes_ctrl->load_from_config(*m_config);
}
}
@@ -6307,10 +6315,10 @@ void TabPrinter::toggle_options()
if (is_imex) {
if (Field* layout_field = get_field("imex_tool_layout"); layout_field) {
if (auto* choice = dynamic_cast<Choice*>(layout_field); choice) {
const int gantry_count = m_config->opt_int("imex_gantry_count");
int current_val = 0;
if (auto* o = m_config->option<ConfigOptionEnum<ImexToolLayout>>("imex_tool_layout"))
current_val = static_cast<int>(o->value);
const int gantry_count = std::max(1, imex_cfg_int(*m_config, "imex_gantry_count"));
// Not const: the rear-* -> front-* normalization below reassigns it.
int current_val =
static_cast<int>(imex_cfg_enum<ImexToolLayout>(*m_config, "imex_tool_layout"));
// Normalize rear-* → front-* when collapsing to 1 gantry.
if (gantry_count == 1 && (current_val == static_cast<int>(ImexToolLayout::RearLeft)
@@ -6583,11 +6591,9 @@ void TabPrinter::update_fff()
// refreshes the rows because that path actually changes the config, which
// matches_config() then detects.
if (m_imex_modes_ctrl) {
int n_cols = m_config->opt_int("imex_tools_per_gantry");
int n_rows = m_config->opt_int("imex_gantry_count");
ImexToolLayout layout = ImexToolLayout::FrontLeft;
if (auto* o = m_config->option<ConfigOptionEnum<ImexToolLayout>>("imex_tool_layout")) layout = o->value;
m_imex_modes_ctrl->set_grid_size(n_cols, n_rows, IMEXModesCtrl::parse_layout(layout));
int n_cols = 0, n_rows = 0, layout = 0;
imex_grid_shape(m_config, n_cols, n_rows, layout);
m_imex_modes_ctrl->set_grid_size(n_cols, n_rows, layout);
if (!m_imex_modes_ctrl->matches_config(*m_config))
m_imex_modes_ctrl->load_from_config(*m_config);
}