feat(imex): polish UI per PR feedback + add parallel-mode safeguards

Felix14-v2 PR review feedback (https://github.com/OrcaSlicer/OrcaSlicer/pull/13086#issuecomment-4323696312)
plus the slice-time validation work that follows from his bug list.

UI polish:
- Capitalize "Primary" in display (right-click mode menu, plate tooltip).
  Sentinel stays lowercase for wire compatibility.
- Pre-slice-warnings checkbox now uses Orca's ::CheckBox so it matches the
  green toggle style of the rest of the IDEX/IQEX configuration page.
- DPI-scaled the IMEXModesCtrl (modes editor) sub-panels, button grid,
  text wraps, gcode textarea, and the ghost-tooltip swatch (imgui.scaled).
  Legend swatches sized to body-text height for visual balance.
- Primary mode tool buttons in the modes editor are now disabled (read-
  only): cycling roles on the IMEX-off mode is a no-op and confusing.
- Modes editor sub-panels now explicitly inherit the app's window-default
  dark colour so chromeless ScalableButtons don't render with a visible
  light box around their icons on GTK dark themes.
- Per-mode-line reset arrows in the modes editor: each row gets a small
  reset bitmap that snaps that row's name+tools+gcode triplet back to the
  saved preset's value. matches_config() guard on the page-level reload
  prevents the textbox-being-typed-into from being destroyed mid-keystroke.
- New View menu item "Show IDEX/IQEX Toolhead" — toggles the per-carriage
  footprint boxes during G-code preview playback. Gated to Preview tab +
  IMEX printer; backed by app_config so it persists.

Coordinated config migrations:
- imex_tool_layout and imex_viz_theme migrate from coString to coEnum
  (ImexToolLayout / ImexVizTheme). Existing wire format preserved so
  saved presets and 3MFs deserialize unchanged. Side-benefit: both now
  pick up standard Field rendering and so finally show reset arrows.

Slice-time safeguards (the validation half):
- imex_suppresses_bare_toolchange(parallel_mode, count): suppresses the
  slicer's bare T<n> at print-start in any IMEX parallel mode (the user's
  imex_mode_gcode + machine_start_gcode owns tool activation there). Mid-
  print T<n> emits normally — Print::validate blocks the configurations
  where mid-print T<n> wouldn't make sense. Applied to both code paths
  inside GCode::set_extruder (the long multi-extruder path AND the
  single-extruder path that fires when multiple_extruders=false).
- imex_multicolor_block_reason(): hard-stop validator returning a user-
  facing reason string when the active IMEX configuration can't physically
  support multi-color. Catches IDEX (1 tool/gantry), 2-tool-active IQEX
  (no within-gantry swap topology), and any MMU/AFC lane sharing among
  used filaments. Wired into Print::validate as a slice blocker, and into
  PartPlate::has_imex_multimaterial_conflict so the plater badge agrees
  with the slice block (no more false positives where the badge warns but
  the slice goes through).
- New imex_mode_types config option (parallel array to imex_mode_names)
  and imex_mode_type_for() helper. Mode-type tag drives behaviour: zones,
  ghosts, and validation interpret modes differently per type. Initial
  types: "primary", "copy", "mirror", "split". Split modes are explicitly
  designed for paired-gantry IQEX multi-color and bypass the gantry-pair
  check in imex_multicolor_block_reason (MMU sharing still blocks them).

Test coverage:
- New unit tests cover imex_suppresses_bare_toolchange (4 cases),
  imex_multicolor_block_reason (8 cases including IDEX, IQEX 2-/4-tool-
  active, MMU sharing, missing primary, Split type), and imex_mode_type_for
  (3 cases including legacy fallback). Total: 186 IMEX assertions across
  71 test cases, all passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-04-27 22:51:05 -04:00
co-authored by Claude Opus 4.7
parent b10d6e1b9f
commit 0bb1cef78e
15 changed files with 793 additions and 212 deletions
+54 -4
View File
@@ -543,6 +543,24 @@ static const t_config_enum_values s_keys_map_FilamentMapMode = {
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FilamentMapMode)
// IMEX: keep the legacy hyphenated wire strings so existing presets / 3MFs
// deserialize unchanged after the coString -> coEnum migration.
static const t_config_enum_values s_keys_map_ImexToolLayout = {
{ "front-left", int(ImexToolLayout::FrontLeft) },
{ "front-right", int(ImexToolLayout::FrontRight) },
{ "rear-left", int(ImexToolLayout::RearLeft) },
{ "rear-right", int(ImexToolLayout::RearRight) }
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ImexToolLayout)
static const t_config_enum_values s_keys_map_ImexVizTheme = {
{ "standard", int(ImexVizTheme::Standard) },
{ "deuteranopia", int(ImexVizTheme::Deuteranopia) },
{ "tritanopia", int(ImexVizTheme::Tritanopia) },
{ "high_contrast", int(ImexVizTheme::HighContrast) }
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ImexVizTheme)
//BBS
std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type)
@@ -5561,13 +5579,22 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(2));
def = this->add("imex_tool_layout", coString);
def = this->add("imex_tool_layout", coEnum);
def->label = L("Tool 0 Corner");
def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. "
"Determines how tool indices map to bed zones. "
"front = lower Y (near the operator), rear = higher Y (back of machine).");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionString("front-left"));
def->enum_keys_map = &ConfigOptionEnum<ImexToolLayout>::get_enum_values();
def->enum_values.push_back("front-left");
def->enum_values.push_back("front-right");
def->enum_values.push_back("rear-left");
def->enum_values.push_back("rear-right");
def->enum_labels.push_back(L("Front-left"));
def->enum_labels.push_back(L("Front-right"));
def->enum_labels.push_back(L("Rear-left"));
def->enum_labels.push_back(L("Rear-right"));
def->set_default_value(new ConfigOptionEnum<ImexToolLayout>(ImexToolLayout::FrontLeft));
def = this->add("imex_nozzle_clearance_x", coFloat);
def->label = L("Nozzle Clearance X");
@@ -5594,11 +5621,20 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("imex_viz_theme", coString);
def = this->add("imex_viz_theme", coEnum);
def->label = L("Visualization Theme");
def->tooltip = L("Color theme for IDEX/IQEX bed zone visualization. Choose a colorblind-friendly theme if needed.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionString("standard"));
def->enum_keys_map = &ConfigOptionEnum<ImexVizTheme>::get_enum_values();
def->enum_values.push_back("standard");
def->enum_values.push_back("deuteranopia");
def->enum_values.push_back("tritanopia");
def->enum_values.push_back("high_contrast");
def->enum_labels.push_back(L("Standard"));
def->enum_labels.push_back(L("Deuteranopia / Protanopia (red-green)"));
def->enum_labels.push_back(L("Tritanopia (blue-yellow)"));
def->enum_labels.push_back(L("High Contrast"));
def->set_default_value(new ConfigOptionEnum<ImexVizTheme>(ImexVizTheme::Standard));
def = this->add("imex_parallel_mode", coString);
def->label = L("IDEX/IQEX Print Mode");
@@ -5631,6 +5667,20 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
// Mode-type tag drives how zones, ghosts, and slice-time validation interpret a
// given mode entry. Parallel to imex_mode_names. Values: "primary", "copy",
// "mirror", "split". Empty / missing entries fall back to "primary" except for
// entries named "copy" or "mirror" (legacy compat with pre-type configs).
def = this->add("imex_mode_types", coStrings);
def->label = L("IDEX/IQEX Mode Types");
def->tooltip = L("Per-mode topology tag. \"copy\" / \"mirror\" produce one ghost per "
"non-primary tool at the corresponding mirrored/copied position. "
"\"split\" splits the bed in half along the gantry axis and aggregates "
"each gantry's tools into a single multi-color print region — used "
"for paired-gantry IQEX multi-color printing.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("manual_filament_change", coBool);
def->label = L("Manual Filament Change");
def->tooltip = L("Enable this option to omit the custom Change filament G-code only at the beginning of the print. "