diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 61e6373be4..60d3c716f4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -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 diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 8250cc09d8..091173f752 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -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); diff --git a/src/libslic3r/IMEXHelpers.cpp b/src/libslic3r/IMEXHelpers.cpp index 159d2f505a..6ffb17a420 100644 --- a/src/libslic3r/IMEXHelpers.cpp +++ b/src/libslic3r/IMEXHelpers.cpp @@ -8,8 +8,11 @@ #include #include +#include + #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() 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) diff --git a/src/libslic3r/IMEXHelpers.hpp b/src/libslic3r/IMEXHelpers.hpp index 8a5b59deea..e862b86b21 100644 --- a/src/libslic3r/IMEXHelpers.hpp +++ b/src/libslic3r/IMEXHelpers.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -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 derives from ConfigOptionSingle, 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 +T imex_cfg_enum(const ConfigBase& cfg, const std::string& key) +{ + // dynamic_cast on BOTH halves here, unlike imex_cfg_int/_float. Every ConfigOptionEnum + // reports coEnum, so the type() comparison that option() (Config.hpp:2643) and the + // int/float accessors rely on cannot tell one enum type from another: it would accept a + // ConfigOptionEnum and static_cast it to this T. ConfigOptionEnum and + // ConfigOptionEnum are unrelated siblings (both derive from ConfigOptionSingle), 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*>(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*>(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(0); +} + // The on-disk suffix letter for `role`. Inverse of imex_role_from_suffix(). char imex_role_letter(ImexRole role); diff --git a/src/libslic3r/IMEXZones.cpp b/src/libslic3r/IMEXZones.cpp index 15c3fa140a..33007a628a 100644 --- a/src/libslic3r/IMEXZones.cpp +++ b/src/libslic3r/IMEXZones.cpp @@ -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("imex_gantry_count"); - auto* tpg_opt = printer_cfg.option("imex_tools_per_gantry"); - auto* layout_opt = printer_cfg.option>("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(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("imex_nozzle_clearance_x"); - auto* ch_opt = printer_cfg.option("imex_nozzle_clearance_y"); - auto* mgn_opt = printer_cfg.option("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 diff --git a/src/libslic3r/IMEXZones.hpp b/src/libslic3r/IMEXZones.hpp index d0c0e29912..d1181fc70b 100644 --- a/src/libslic3r/IMEXZones.hpp +++ b/src/libslic3r/IMEXZones.hpp @@ -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 diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 6eff03d300..55a5614b77 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -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("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("imex_tools_per_gantry"); - auto* wx_opt = printer_cfg.opt("imex_nozzle_clearance_x"); - auto* wy_opt = printer_cfg.opt("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 s_no_strings; - auto* gantry_opt = printer_cfg.opt("imex_gantry_count"); - auto* tpg_opt = printer_cfg.opt("imex_tools_per_gantry"); - auto* layout_opt = printer_cfg.opt>("imex_tool_layout"); - auto* wx_opt = printer_cfg.opt("imex_nozzle_clearance_x"); - auto* wy_opt = printer_cfg.opt("imex_nozzle_clearance_y"); - auto* fw_opt = printer_cfg.opt("imex_firmware_managed_zones"); auto* names_opt = printer_cfg.opt("imex_mode_names"); auto* tools_opt = printer_cfg.opt("imex_mode_active_tools"); const std::vector& 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(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(); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 4de6ddfc0f..713da9c5c8 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -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 diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 402e077ec2..657b1dfff6 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -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("imex_tools_per_gantry"); - auto* n_row_opt = printer_cfg.option("imex_gantry_count"); - auto* cw_opt = printer_cfg.option("imex_nozzle_clearance_x"); - auto* ch_opt = printer_cfg.option("imex_nozzle_clearance_y"); - auto* mgn_opt = printer_cfg.option("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("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>("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(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("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("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("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("imex_tools_per_gantry"); auto* pem_opt = printer_cfg.option("physical_extruder_map"); if (!tpg_opt || !pem_opt) return false; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index b9d2c7aaa9..6c350b4dc8 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -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() 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(*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>("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>("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(layout_field); choice) { - const int gantry_count = m_config->opt_int("imex_gantry_count"); - int current_val = 0; - if (auto* o = m_config->option>("imex_tool_layout")) - current_val = static_cast(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(imex_cfg_enum(*m_config, "imex_tool_layout")); // Normalize rear-* → front-* when collapsing to 1 gantry. if (gantry_count == 1 && (current_val == static_cast(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>("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); }