From 612e0e3932e9bae52b1c7cea29765bd918f9e818 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 3 Sep 2026 00:55:35 -0400 Subject: [PATCH] Fall back to Primary when an IMEX mode does not resolve, and keep imex keys out of the config block Closes review comment 2, part of 18, and one non-IMEX regression the review did not cover. An imex_parallel_mode naming no entry in imex_mode_names still entered the parallel branches. get_imex_active_tools() returned empty and the else was skipped, so no head received its 1st-to-2nd layer temperature transition, and imex_suppresses_bare_toolchange() still dropped the initial T on the expectation that a mode script would select the tool. validate() did not catch it because its guard is declared_primary >= 0 and an unresolved mode yields -1. You reach it by renaming a mode after a plate is set to it, or by opening a 3MF whose printer preset names its modes differently. m_imex_parallel_mode is no longer assigned before the lookup; a non-Primary name that matches no row now warns and re-resolves against the Primary row, which is the fallback the PR description already claimed. A mode that resolves to an empty tool roster takes the same path, since the emitted G-code is wrong in the same way. Warned rather than blocked: opening someone else's 3MF is a legitimate way to get here and the Primary reading prints correctly, so refusing to slice would turn a recoverable situation into a dead end. Silent was not an option either, because the plate keeps showing the stale mode name while drawing no zones. Separately, the 14 imex config keys all register non-nil defaults, so append_full_config was emitting "; imex_* = " into every exported G-code, including on single-nozzle printers with nothing to do with the feature. They are banned from the dump, matching the treatment already given to the fast-purge, extruder-change and timelapse keys, so the config block is byte-identical to the pre-IMEX baseline for the whole shipping fleet. Nothing reads them back: GCodeProcessor has no imex reference, and the two per-plate keys round-trip through the 3MF's model_settings.config on an independent path. Co-Authored-By: Claude Opus 5 (1M context) --- src/libslic3r/GCode.cpp | 121 ++++++++++++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 23 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 47ef3352d1..58d681890c 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2883,19 +2883,10 @@ static std::vector get_imex_active_tools(const Print& print) if (active_mode == kImexPrimaryMode) return active_tools; - const auto* mode_names_opt = print.config().option("imex_mode_names"); - const auto* tools_opt = print.config().option("imex_mode_active_tools"); - - if (!mode_names_opt || !tools_opt || mode_names_opt->values.empty() || tools_opt->values.empty()) - return active_tools; - - for (size_t i = 0; i < mode_names_opt->values.size(); ++i) { - if (i >= tools_opt->values.size() || mode_names_opt->values[i] != active_mode) - continue; - for (const auto& [phys, role] : parse_imex_active_tools(tools_opt->values[i])) - active_tools.push_back(phys); - break; - } + // An unresolved mode, and a mode the tools array is too short to cover, both hand back + // an empty tools string, which parses to no tools. + for (const auto& [phys, role] : parse_imex_active_tools(find_imex_mode(print.config(), active_mode).active_tools)) + active_tools.push_back(phys); return active_tools; } @@ -3685,19 +3676,78 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (print.config().is_imex.value && !print.objects().empty()) { const std::string& raw = print.objects().front()->config().imex_parallel_mode.value; imex_active_mode = raw.empty() ? kImexPrimaryMode : raw; - m_imex_parallel_mode = imex_active_mode; m_imex_head_filament_map = parse_imex_head_filament_map( print.objects().front()->config().imex_head_filament_map.value); - const auto& mode_names = print.config().imex_mode_names.values; - const auto& mode_gcodes = print.config().imex_mode_gcodes.values; - for (size_t i = 0; i < mode_names.size(); ++i) { - if (mode_names[i] == imex_active_mode) { - imex_active_mode_index = (int)i; - if (i < mode_gcodes.size()) - imex_active_mode_gcode = mode_gcodes[i]; - break; - } + // Look `name` up in the printer's mode table, filling in its index, script and + // tool roster. False means the name matches no row, i.e. the mode is unresolved. + // find_imex_mode() owns the "which row, and what if a sibling array is short" + // rule for every IMEX consumer; see IMEXHelpers.hpp. + std::string imex_active_mode_tools; + auto resolve_mode = [&](const std::string& name) { + const ImexMode mode = find_imex_mode(print.config(), name); + if (!mode.found()) + return false; + imex_active_mode_index = mode.index; + imex_active_mode_gcode = mode.gcode; + imex_active_mode_tools = mode.active_tools; + return true; + }; + // An unresolved mode name falls back to Primary rather than being carried into the + // parallel-mode paths. `imex_parallel_mode` is a plain string stored on the plate and + // matched against the printer's `imex_mode_names` by value, so it goes stale whenever + // the two drift apart -- a mode renamed or deleted after a plate was set to it, or a + // project shared between printer presets that name their modes differently. + // + // Without the fallback the exporter took every "not Primary" branch while every lookup + // keyed on the mode name came back empty, and wrote a file that is wrong rather than + // merely unconfigured: the 1st->2nd layer temperature branch is mutually exclusive with + // the standard one, so an empty active-tool roster meant NO head got its transition and + // all of them held nozzle_temperature_initial_layer for the whole print; and + // imex_suppresses_bare_toolchange() dropped the initial T on the assumption that a + // mode script would select the tool, while the mode script -- also looked up by name -- + // did not exist. Print::validate()'s IMEX rules did not catch it either: they resolve + // the same name to an empty tools string, which yields no declared primary and skips + // the guard. Primary is the one interpretation that is always well-formed: single + // carriage, ordinary temperatures, ordinary tool changes. + // + // Warned rather than silent, and warned rather than blocked. Silent is not an option: + // the plate still labels itself with the stale mode and draws no zones (PartPlate's + // zone builder returns early on the same unresolved state), so a user who asked for two + // parts in copy mode would get one with nothing anywhere saying why. Blocking is not an + // option either -- opening someone else's 3MF on a differently-named preset is a + // legitimate way to arrive here, and the Primary interpretation prints correctly, so + // refusing to slice would be a regression for a case that has a good answer. + // + // The second trigger below is the ragged-table case, and it lands here rather than + // anywhere else because the damage is identical: the mode's name resolves, but the + // printer's `imex_mode_active_tools` is too short to reach its row (or the row is + // explicitly empty), so the roster every branch above keys on comes back empty and + // the export takes the parallel path with nothing in it. Nothing used to report + // that -- half the lookup sites in the tree turned a short tools array into "mode + // not found" and the other half did not, and this one did not. One rule now, in + // find_imex_mode(), and one warning, here. + const bool imex_mode_resolved = resolve_mode(imex_active_mode); + if (imex_active_mode != kImexPrimaryMode && (!imex_mode_resolved || imex_active_mode_tools.empty())) { + print.active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + imex_mode_resolved + ? Slic3r::format(_(L("The IMEX mode \"%1%\" has no tools assigned on the selected printer. " + "Printing in Primary mode instead. Assign the mode's tools in Printer " + "Settings, or pick another mode from the plate's IMEX button.")), + imex_active_mode) + : Slic3r::format(_(L("This plate is set to the IMEX mode \"%1%\", which the selected printer " + "does not define. Printing in Primary mode instead. Pick a mode from the " + "plate's IMEX button, or restore the mode in Printer Settings.")), + imex_active_mode)); + imex_active_mode = kImexPrimaryMode; + imex_active_mode_index = 0; + imex_active_mode_gcode.clear(); + imex_active_mode_tools.clear(); + // Primary is a real row in the table (always the first one), so it may carry its own + // setup script; resolve it the same way any other selected mode would be. + resolve_mode(imex_active_mode); } + m_imex_parallel_mode = imex_active_mode; } this->placeholder_parser().set("imex_mode", imex_active_mode); this->placeholder_parser().set("imex_mode_index", imex_active_mode_index); @@ -7216,6 +7266,31 @@ void GCode::append_full_config(const Print &print, std::string &str) // banning it from the dump has no effect on the feature; the only H2C/H2D delta is the M9711/M971 // snapshot reposition. "farthest_point_timelapse"sv, + // The IMEX (IDEX/IQEX parallel printing) keys are newly-registered printer/process keys whose + // defaults are non-nil, so leaving them in the dump would add fourteen `; imex_* = ` + // lines to every printer's config block — an ordinary single-nozzle machine included. Excluding + // them keeps the config-dump byte-identical for the whole shipping fleet; the IMEX printers pay + // the same price the timelapse/prime-volume keys above already pay, and the active mode is still + // visible in the body of their g-code through the injected imex_mode_gcodes macro. Every key is + // read at slice time from m_config (get_imex_active_tools / find_imex_mode / the zone layout + // helpers) and never parsed back out of the dump, so banning them costs the feature nothing: + // the g-code viewer's overlay reads the loaded presets, not the config block, and the two + // per-plate process keys round-trip through the 3MF's model_settings.config plate metadata + // (bbs_3mf.cpp IMEX_PARALLEL_MODE_ATTR / IMEX_HEAD_FILAMENT_MAP_ATTR), not through this dump. + "is_imex"sv, + "imex_firmware_managed_zones"sv, + "imex_gantry_count"sv, + "imex_tools_per_gantry"sv, + "imex_tool_layout"sv, + "imex_nozzle_clearance_x"sv, + "imex_nozzle_clearance_y"sv, + "imex_carriage_margin"sv, + "imex_viz_theme"sv, + "imex_mode_names"sv, + "imex_mode_active_tools"sv, + "imex_mode_gcodes"sv, + "imex_parallel_mode"sv, + "imex_head_filament_map"sv, "compatible_printers"sv, "compatible_prints"sv, "filament_colour_type"sv,