diff --git a/resources/images/plate_ixex_mode.svg b/resources/images/plate_ixex_mode.svg
deleted file mode 100644
index 30a67cdf35..0000000000
--- a/resources/images/plate_ixex_mode.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/resources/images/plate_ixex_mode_dark.svg b/resources/images/plate_ixex_mode_dark.svg
deleted file mode 100644
index 981fd88447..0000000000
--- a/resources/images/plate_ixex_mode_dark.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/resources/images/plate_ixex_mode_hover.svg b/resources/images/plate_ixex_mode_hover.svg
deleted file mode 100644
index 9463e767df..0000000000
--- a/resources/images/plate_ixex_mode_hover.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/resources/images/plate_ixex_mode_hover_dark.svg b/resources/images/plate_ixex_mode_hover_dark.svg
deleted file mode 100644
index 301fa995ab..0000000000
--- a/resources/images/plate_ixex_mode_hover_dark.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index ec249a6ae5..31f1f421a7 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -3058,7 +3058,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// macro is active before any motion, homing, or probing in start G-code.
if (print.config().is_imex.value && !print.objects().empty()) {
const std::string& imex_mode = print.objects().front()->config().imex_parallel_mode.value;
- if (!imex_mode.empty() && imex_mode != "primary") {
+ if (!imex_mode.empty()) {
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 < mode_gcodes.size(); ++i) {
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index b4978b1263..5b05933e93 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -5506,7 +5506,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Tools per Gantry");
def->tooltip = L("Number of independent toolheads per gantry along X. 2 for IDEX-style.");
def->min = 1;
- def->max = 2;
+ def->max = 4;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(2));
diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp
index f64d9f5e4b..91ad35cadc 100644
--- a/src/slic3r/GUI/GCodeViewer.cpp
+++ b/src/slic3r/GUI/GCodeViewer.cpp
@@ -1652,78 +1652,99 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
return flip_y ? (gantry_count - 1 - raw) : raw;
};
- const Vec3f prim_pos = libvgcode::convert(curr_vertex.position);
- const float strip_width = (bed_x_max - bed_x_min) / (float)tools_per_gantry;
- const float row_strip_height = (bed_y_max - bed_y_min) / (float)gantry_count;
- const int pri_phys_col = (pri_tool >= 0) ? phys_col_of(pri_tool) : 0;
- const int pri_phys_row = (pri_tool >= 0) ? phys_row_of(pri_tool) : 0;
+ // Build active col/row sets — mirrors PartPlate::calc_imex_zones() so
+ // zone sizes and positions stay in sync with the bed visualization.
+ const int pri_phys_col = (pri_tool >= 0) ? phys_col_of(pri_tool) : 0;
+ const int pri_phys_row = (pri_tool >= 0) ? phys_row_of(pri_tool) : 0;
+ std::map phys_col_to_zone_gv, phys_row_to_zone_gv;
+ int n_active_cols_gv = 1, n_active_rows_gv = 1;
+ {
+ std::set ac, ar;
+ ac.insert(pri_phys_col); ar.insert(pri_phys_row);
+ for (int tid : sec_tool_ids) {
+ ac.insert(phys_col_of(tid));
+ ar.insert(phys_row_of(tid));
+ }
+ // col_to_zone / row_to_zone: physical index → zone index (sorted)
+ int k = 0;
+ for (int c : ac) phys_col_to_zone_gv[c] = k++;
+ k = 0;
+ for (int r : ar) phys_row_to_zone_gv[r] = k++;
+ n_active_cols_gv = (int)ac.size();
+ n_active_rows_gv = (int)ar.size();
+ }
+ auto zone_col = [&](int pc) { auto it = phys_col_to_zone_gv.find(pc); return it != phys_col_to_zone_gv.end() ? it->second : 0; };
+ auto zone_row = [&](int pr) { auto it = phys_row_to_zone_gv.find(pr); return it != phys_row_to_zone_gv.end() ? it->second : 0; };
- // Pre-pass: for each physical row, find the Copy reference column.
+ const Vec3f prim_pos = libvgcode::convert(curr_vertex.position);
+ const float strip_width = (bed_x_max - bed_x_min) / (float)n_active_cols_gv;
+ const float row_strip_height = (bed_y_max - bed_y_min) / (float)n_active_rows_gv;
+ const int pri_zone_col = zone_col(pri_phys_col);
+ const int pri_zone_row = zone_row(pri_phys_row);
+
+ // Pre-pass: for each physical row, find the Copy reference physical column.
// The primary row's reference is the primary itself.
- // Non-primary rows need a Copy tool; Mirror tools on that row
- // reflect that Copy's X position.
- std::map row_copy_col;
+ std::map row_copy_col; // phys_row → phys_col of copy reference
row_copy_col[pri_phys_row] = pri_phys_col;
for (int i = 0; i < sec_count; ++i) {
- if (sec_tool_states[sec_tool_ids[i]] == 2) {
- row_copy_col[phys_row_of(sec_tool_ids[i])] =
- phys_col_of(sec_tool_ids[i]);
- }
+ if (sec_tool_states[sec_tool_ids[i]] == 2)
+ row_copy_col[phys_row_of(sec_tool_ids[i])] = phys_col_of(sec_tool_ids[i]);
}
- // Primary carriage box: nozzle sits at the collision-side edge.
- // Default to centered; override once we know which side the secondaries are on.
- float pri_box_offset_x = (pri_phys_col == 0) ? 0.0f : -imex_box_wx; // nozzle at inner edge (facing center of bed)
- float pri_box_offset_y = -imex_box_wy; // nozzle at high-Y edge by default (gantry always behind nozzle); override if secondary is behind
+ // Primary carriage box
+ float pri_box_offset_x = (pri_zone_col == 0) ? 0.0f : -imex_box_wx;
+ float pri_box_offset_y = -imex_box_wy;
for (int i = 0; i < sec_count; ++i) {
int sc = phys_col_of(sec_tool_ids[i]);
int sr = phys_row_of(sec_tool_ids[i]);
- if (sc > pri_phys_col) { pri_box_offset_x = 0.0f; }
- else if (sc < pri_phys_col) { pri_box_offset_x = -imex_box_wx; }
- if (sr > pri_phys_row) { pri_box_offset_y = 0.0f; }
- else if (sr < pri_phys_row) { pri_box_offset_y = -imex_box_wy; }
+ if (sc > pri_phys_col) { pri_box_offset_x = 0.0f; }
+ else if (sc < pri_phys_col) { pri_box_offset_x = -imex_box_wx; }
+ if (sr > pri_phys_row) { pri_box_offset_y = 0.0f; }
+ else if (sr < pri_phys_row) { pri_box_offset_y = -imex_box_wy; }
}
carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0], pri_box_offset_x, pri_box_offset_y });
- const float pri_zone_x = bed_x_min + (float)pri_phys_col * strip_width;
- const float pri_zone_y = bed_y_min + (float)pri_phys_row * row_strip_height;
+ const float pri_zone_x = bed_x_min + (float)pri_zone_col * strip_width;
+ const float pri_zone_y = bed_y_min + (float)pri_zone_row * row_strip_height;
const float rel_x = prim_pos.x() - pri_zone_x;
const float rel_y = prim_pos.y() - pri_zone_y;
for (int i = 0; i < sec_count; ++i) {
int sec_phys_col = phys_col_of(sec_tool_ids[i]);
int sec_phys_row = phys_row_of(sec_tool_ids[i]);
+ int sec_zc = zone_col(sec_phys_col);
+ int sec_zr = zone_row(sec_phys_row);
const int sec_state = sec_tool_states[sec_tool_ids[i]];
- // Y: all tools in a row share a Y rail, so Y is always
- // zone-relative copy from the primary.
- float sec_zone_y = bed_y_min + (float)sec_phys_row * row_strip_height;
+ // Y: all tools on a row share a Y rail — always zone-relative copy.
+ float sec_zone_y = bed_y_min + (float)sec_zr * row_strip_height;
float sec_y = sec_zone_y + rel_y;
- // X: Copy places the tool at the same relative position within
- // its own zone. Mirror reflects the Copy reference on this row.
+ // X: Copy → same zone-relative position.
+ // Mirror → reflect copy reference across the boundary it shares with
+ // this mirror zone (left or right edge of copy zone depending on side).
float sec_x;
if (sec_state == 2) {
- float sec_zone_x = bed_x_min + (float)sec_phys_col * strip_width;
- sec_x = sec_zone_x + rel_x;
+ sec_x = bed_x_min + (float)sec_zc * strip_width + rel_x;
} else {
- // Mirror: find the Copy on the same row and reflect its X.
auto ref_it = row_copy_col.find(sec_phys_row);
- if (ref_it != row_copy_col.end()) {
- float ref_zone_x = bed_x_min + (float)ref_it->second * strip_width;
- float ref_x = ref_zone_x + rel_x;
- sec_x = bed_x_min + bed_x_max - ref_x;
+ int ref_phys_col = (ref_it != row_copy_col.end()) ? ref_it->second : pri_phys_col;
+ int ref_zc = zone_col(ref_phys_col);
+ float ref_zone_x = bed_x_min + (float)ref_zc * strip_width;
+ float ref_abs = ref_zone_x + rel_x;
+ if (sec_phys_col < ref_phys_col) {
+ // Mirror left of copy — reflects across copy zone's left edge
+ sec_x = 2.0f * ref_zone_x - ref_abs;
} else {
- sec_x = bed_x_min + bed_x_max - prim_pos.x();
+ // Mirror right of copy — reflects across copy zone's right edge
+ float ref_zone_right = bed_x_min + (float)(ref_zc + 1) * strip_width;
+ sec_x = 2.0f * ref_zone_right - ref_abs;
}
}
Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() };
m_sequential_view.m_imex_secondary_markers[i].set_world_position(sec_pos);
m_sequential_view.m_imex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f);
- // X: copy tools move in sync with primary so they share the same
- // X-facing orientation. Mirror tools approach from the opposite side
- // so the nozzle sits at the collision-side edge.
float sec_box_offset_x;
if (sec_state == 2) {
sec_box_offset_x = pri_box_offset_x;
@@ -1732,8 +1753,6 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
else if (sec_phys_col < pri_phys_col) sec_box_offset_x = 0.0f;
else sec_box_offset_x = pri_box_offset_x;
}
- // Y: always row-based — gantry is always behind nozzle regardless of
- // copy/mirror (a back-row copy still has its gantry at the back).
float sec_box_offset_y;
if (sec_phys_row > pri_phys_row) sec_box_offset_y = -imex_box_wy;
else if (sec_phys_row < pri_phys_row) sec_box_offset_y = 0.0f;
diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp
index 17447a6707..a5c2865574 100644
--- a/src/slic3r/GUI/PartPlate.cpp
+++ b/src/slic3r/GUI/PartPlate.cpp
@@ -579,6 +579,11 @@ void PartPlate::calc_imex_zones()
}
}
+ // If the mode name was found but has no tools (e.g. stale process-preset mode on a new
+ // printer that has no modes defined yet), there is nothing to compute.
+ if (tool_states.empty())
+ return;
+
// Identify the Primary tool from the mode definition
for (auto& [idx, state] : tool_states) {
if (state == 1) { auto [c, r] = tool_to_phys(idx); pri_col = c; pri_row = r; break; }
@@ -601,8 +606,33 @@ void PartPlate::calc_imex_zones()
auto bed_ext = get_extents(m_shape);
double x_min = bed_ext.min(0), x_max = bed_ext.max(0);
double y_min = bed_ext.min(1), y_max = bed_ext.max(1);
- double zone_w = (x_max - x_min) / n_cols;
- double zone_h = (y_max - y_min) / n_rows;
+
+ // Zone sizing is based on ACTIVE tool count per axis, not total grid dimensions.
+ // Inactive tools (absent from tool_states) donate their bed share to active neighbors.
+ // Sorted active col/row lists map physical index k → zone index k.
+ std::vector active_cols_v, active_rows_v;
+ {
+ std::set ac_set, ar_set;
+ for (auto& [idx, state] : tool_states) {
+ auto [c, r] = tool_to_phys(idx);
+ ac_set.insert(c); ar_set.insert(r);
+ }
+ active_cols_v.assign(ac_set.begin(), ac_set.end()); // sorted ascending
+ active_rows_v.assign(ar_set.begin(), ar_set.end());
+ }
+ int n_active_cols = std::max(1, (int)active_cols_v.size());
+ int n_active_rows = std::max(1, (int)active_rows_v.size());
+
+ std::map col_to_zone, row_to_zone;
+ for (int k = 0; k < n_active_cols; ++k) col_to_zone[active_cols_v[k]] = k;
+ for (int k = 0; k < n_active_rows; ++k) row_to_zone[active_rows_v[k]] = k;
+
+ double zone_w = (x_max - x_min) / n_active_cols;
+ double zone_h = (y_max - y_min) / n_active_rows;
+
+ // Zone index of the primary tool
+ int pri_col_k = col_to_zone.count(pri_col) ? col_to_zone[pri_col] : 0;
+ int pri_row_k = row_to_zone.count(pri_row) ? row_to_zone[pri_row] : 0;
// Separation axes: row-sep = secondaries on a different gantry, col-sep = different column
bool has_row_sep = false, has_col_sep = false;
@@ -647,18 +677,26 @@ void PartPlate::calc_imex_zones()
auto make_boxes = [&](const std::set>& cells) -> std::vector {
std::vector boxes;
if (cells.empty()) return boxes;
+ auto ck = [&](int c) { return col_to_zone.count(c) ? col_to_zone.at(c) : 0; };
+ auto rk = [&](int r) { return row_to_zone.count(r) ? row_to_zone.at(r) : 0; };
if (has_row_sep && !has_col_sep) {
std::set rows; for (auto& [c,r] : cells) rows.insert(r);
- for (int sr : rows)
- boxes.push_back({x_min, x_max, y_min + sr*zone_h, y_min + (sr+1)*zone_h});
+ for (int sr : rows) {
+ int k = rk(sr);
+ boxes.push_back({x_min, x_max, y_min + k*zone_h, y_min + (k+1)*zone_h});
+ }
} else if (has_col_sep && !has_row_sep) {
std::set cols; for (auto& [c,r] : cells) cols.insert(c);
- for (int sc : cols)
- boxes.push_back({x_min + sc*zone_w, x_min + (sc+1)*zone_w, y_min, y_max});
+ for (int sc : cols) {
+ int k = ck(sc);
+ boxes.push_back({x_min + k*zone_w, x_min + (k+1)*zone_w, y_min, y_max});
+ }
} else {
- for (auto& [sc,sr] : cells)
- boxes.push_back({x_min + sc*zone_w, x_min + (sc+1)*zone_w,
- y_min + sr*zone_h, y_min + (sr+1)*zone_h});
+ for (auto& [sc,sr] : cells) {
+ int ck_ = ck(sc), rk_ = rk(sr);
+ boxes.push_back({x_min + ck_*zone_w, x_min + (ck_+1)*zone_w,
+ y_min + rk_*zone_h, y_min + (rk_+1)*zone_h});
+ }
}
return boxes;
};
@@ -718,11 +756,11 @@ void PartPlate::calc_imex_zones()
}
};
- // Primary zone extent — mirrors in_primary_zone / make_boxes expansion logic
- double pz_x0 = has_col_sep ? x_min + pri_col * zone_w : x_min;
- double pz_x1 = has_col_sep ? x_min + (pri_col + 1) * zone_w : x_max;
- double pz_y0 = has_row_sep ? y_min + pri_row * zone_h : y_min;
- double pz_y1 = has_row_sep ? y_min + (pri_row + 1) * zone_h : y_max;
+ // Primary zone extent — uses zone indices so inactive tools don't shrink the zone
+ double pz_x0 = has_col_sep ? x_min + pri_col_k * zone_w : x_min;
+ double pz_x1 = has_col_sep ? x_min + (pri_col_k + 1) * zone_w : x_max;
+ double pz_y0 = has_row_sep ? y_min + pri_row_k * zone_h : y_min;
+ double pz_y1 = has_row_sep ? y_min + (pri_row_k + 1) * zone_h : y_max;
m_imex_primary_zone_box = BoundingBoxf(Vec2d(pz_x0, pz_y0), Vec2d(pz_x1, pz_y1));
// Helper: build a BoundingBoxf3 strip, clip to bed, add to members
@@ -753,26 +791,27 @@ void PartPlate::calc_imex_zones()
for (auto& [idx, state] : tool_states) {
if (state != 3) continue; // only mirror tools (state==3) require collision strips
auto [c, r] = tool_to_phys(idx);
- // X-boundary strips: any mirror in an adjacent column can cause an X collision.
- if (c == pri_col + 1) has_right_sec = true;
- if (c == pri_col - 1) has_left_sec = true;
- // Y-boundary strips: only a mirror DIRECTLY above/below (same column) causes a Y collision.
- // A mirror in a different column that happens to be in an adjacent row is only an X risk.
- if (r == pri_row + 1 && c == pri_col) has_top_sec = true;
- if (r == pri_row - 1 && c == pri_col) has_bottom_sec = true;
+ int zc = col_to_zone.count(c) ? col_to_zone.at(c) : -1;
+ int zr = row_to_zone.count(r) ? row_to_zone.at(r) : -1;
+ // X-boundary strips: mirror in the zone immediately adjacent to the primary zone.
+ if (zc == pri_col_k + 1) has_right_sec = true;
+ if (zc == pri_col_k - 1) has_left_sec = true;
+ // Y-boundary strips: mirror zone-adjacent in Y, same physical column as primary.
+ if (zr == pri_row_k + 1 && c == pri_col) has_top_sec = true;
+ if (zr == pri_row_k - 1 && c == pri_col) has_bottom_sec = true;
}
// X-axis boundaries (vertical strips, width = nozzle_clearance_x on primary side)
if (carriage_w > 0.0) {
if (has_right_sec) {
- double bnd_x = x_min + (pri_col + 1) * zone_w;
+ double bnd_x = x_min + (pri_col_k + 1) * zone_w;
double strip_inner = bnd_x - carriage_w;
add_strip(strip_inner, bnd_x, pz_y0, pz_y1);
if (margin > 0.0)
add_margin_fill(strip_inner - margin, strip_inner, pz_y0, pz_y1);
}
if (has_left_sec) {
- double bnd_x = x_min + pri_col * zone_w;
+ double bnd_x = x_min + pri_col_k * zone_w;
double strip_inner = bnd_x + carriage_w;
add_strip(bnd_x, strip_inner, pz_y0, pz_y1);
if (margin > 0.0)
@@ -783,14 +822,14 @@ void PartPlate::calc_imex_zones()
// Y-axis boundaries (horizontal strips, width = nozzle_clearance_y on primary side)
if (carriage_h > 0.0) {
if (has_top_sec) {
- double bnd_y = y_min + (pri_row + 1) * zone_h;
+ double bnd_y = y_min + (pri_row_k + 1) * zone_h;
double strip_inner = bnd_y - carriage_h;
add_strip(pz_x0, pz_x1, strip_inner, bnd_y);
if (margin > 0.0)
add_margin_fill(pz_x0, pz_x1, strip_inner - margin, strip_inner);
}
if (has_bottom_sec) {
- double bnd_y = y_min + pri_row * zone_h;
+ double bnd_y = y_min + pri_row_k * zone_h;
double strip_inner = bnd_y + carriage_h;
add_strip(pz_x0, pz_x1, bnd_y, strip_inner);
if (margin > 0.0)
@@ -830,6 +869,20 @@ std::string PartPlate::build_imex_cache_key() const
+ "|mg" + std::to_string(mgn_opt ? (int)(mgn_opt->value * 10) : 0);
}
+// Reposition the IMEX mode icon without requiring a full set_shape() rebuild.
+// Called when is_imex is toggled on a printer whose bed shape matches the current plate,
+// which would otherwise cause set_shape() to short-circuit before reaching icon calc.
+void PartPlate::refresh_imex_icon()
+{
+ if (!m_plater) return;
+ PresetBundle* preset = wxGetApp().preset_bundle;
+ if (!preset) return;
+ bool dual_bbl = preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2;
+ auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex");
+ if (is_imex_opt && is_imex_opt->value)
+ calc_vertex_for_icons(dual_bbl ? 7 : 6, m_imex_mode_icon);
+}
+
// Ensure zone geometry (secondary boxes, collision strips, visual meshes) is up to date.
// Called before any collision check AND before rendering so both paths share the same data.
void PartPlate::ensure_imex_zones()
@@ -7190,6 +7243,13 @@ void PartPlateList::load_cali_textures()
PartPlateList::is_load_cali_texture = true;
}
+void PartPlateList::refresh_imex_icons()
+{
+ const std::lock_guard local_lock(m_plates_mutex);
+ for (PartPlate* plate : m_plate_list)
+ plate->refresh_imex_icon();
+}
+
void PartPlateList::on_extruder_count_changed(int extruder_count)
{
for (unsigned int i = 0; i < (unsigned int) m_plate_list.size(); ++i) {
diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp
index 423de28585..6ff42c3ef6 100644
--- a/src/slic3r/GUI/PartPlate.hpp
+++ b/src/slic3r/GUI/PartPlate.hpp
@@ -191,6 +191,7 @@ private:
void ensure_imex_zones();
std::string build_imex_cache_key() const;
void render_imex_zones(bool force_default_color);
+ void refresh_imex_icon();
void calc_vertex_for_number(int index, bool one_number, GLModel &buffer);
void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model);
void calc_vertex_for_icons(int index, PickingModel &model);
@@ -952,6 +953,8 @@ public:
void load_cali_textures();
void on_extruder_count_changed(int extruder_count);
+ // Re-position the IMEX mode icon on all plates without a full shape rebuild.
+ void refresh_imex_icons();
void set_filament_count(int filament_count);
void on_filament_deleted(int filament_count, int filament_id);
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 6e5753c546..af49d6341b 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -16268,6 +16268,12 @@ void Plater::on_config_change(const DynamicPrintConfig &config)
opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") {
update_scheduled = true;
}
+ // IDEX/IQEX: when is_imex toggles the mode icon needs to be repositioned on every plate.
+ // set_shape() short-circuits when the bed geometry is unchanged, so we refresh explicitly.
+ else if (opt_key == "is_imex") {
+ p->partplate_list.refresh_imex_icons();
+ update_scheduled = true;
+ }
}
if (bed_shape_changed)
@@ -17652,7 +17658,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi
auto* names_opt = printer_cfg.option("imex_mode_names");
if (names_opt) {
for (const auto& n : names_opt->values)
- if (!n.empty()) modes.push_back(n);
+ if (!n.empty() && n != "primary") modes.push_back(n);
}
if (right_click) {
diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp
index 001bd5f305..7b7bdc8974 100644
--- a/src/slic3r/GUI/Tab.cpp
+++ b/src/slic3r/GUI/Tab.cpp
@@ -4431,7 +4431,7 @@ public:
m_n_rows = n_rows;
m_layout = layout;
for (size_t i = 0; i < names.size(); ++i)
- add_row(names[i], tools[i], gcodes[i]);
+ add_row(names[i], tools[i], gcodes[i], /*is_primary=*/(names[i] == "primary"));
Layout();
}
@@ -4440,11 +4440,31 @@ public:
auto* names = cfg.option("imex_mode_names");
auto* tools = cfg.option("imex_mode_active_tools");
auto* gcodes = cfg.option("imex_mode_gcodes");
+
+ // Primary row is always first and non-deletable. Look for an existing
+ // "primary" entry in the config (present in configs saved after #8 was
+ // implemented); fall back to empty tool/gcode for older configs.
+ std::string primary_tools, primary_gcode;
+ size_t primary_cfg_idx = std::string::npos;
+ if (names) {
+ for (size_t i = 0; i < names->values.size(); ++i) {
+ if (names->values[i] == "primary") {
+ primary_cfg_idx = i;
+ if (tools && i < tools->values.size()) primary_tools = tools->values[i];
+ if (gcodes && i < gcodes->values.size()) primary_gcode = gcodes->values[i];
+ break;
+ }
+ }
+ }
+ add_row("primary", primary_tools, primary_gcode, /*is_primary=*/true);
+
size_t n = names ? names->values.size() : 0;
- for (size_t i = 0; i < n; ++i)
+ for (size_t i = 0; i < n; ++i) {
+ if (i == primary_cfg_idx) continue; // already added above
add_row(names->values[i],
(tools && i < tools->values.size()) ? tools->values[i] : "",
(gcodes && i < gcodes->values.size()) ? gcodes->values[i] : "");
+ }
Layout();
}
@@ -4452,7 +4472,7 @@ public:
get_mode_data() const {
std::vector names, tools, gcodes;
for (auto& r : m_rows) {
- std::string nm = r.name->GetValue().ToStdString();
+ std::string nm = r.is_primary ? "primary" : r.name->GetValue().ToStdString();
if (nm.empty()) continue;
names.push_back(nm);
tools.push_back(active_tools_string(r));
@@ -4513,32 +4533,44 @@ private:
struct Row {
wxPanel* panel;
- wxTextCtrl* name;
+ wxTextCtrl* name; // nullptr for primary row (name is fixed)
wxTextCtrl* gcode;
std::vector btns;
- std::vector btn_states; // parallel to btns
- // tool_idx for btns[i]: computed during add_row, stored here for quick lookup
+ std::vector btn_states;
std::vector btn_tool_idx;
- // Full assignment map including tools not currently visible due to grid size.
- // Preserved across grid resizes so assignments restore when grid expands again.
std::map all_tool_states;
+ bool is_primary {false};
};
void add_row(const std::string& name = "",
const std::string& active_tools = "",
- const std::string& gcode = "")
+ const std::string& gcode = "",
+ bool is_primary = false)
{
Row r;
+ r.is_primary = is_primary;
r.panel = new wxPanel(this, wxID_ANY);
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
- r.name = new wxTextCtrl(r.panel, wxID_ANY, name, wxDefaultPosition, wxSize(130, -1));
- r.name->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); });
+ if (is_primary) {
+ r.name = nullptr;
+ auto* lbl = new wxStaticText(r.panel, wxID_ANY, _L("Primary"),
+ wxDefaultPosition, wxSize(130, -1));
+ wxFont f = lbl->GetFont();
+ f.SetWeight(wxFONTWEIGHT_BOLD);
+ lbl->SetFont(f);
+ sizer->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
+ } else {
+ r.name = new wxTextCtrl(r.panel, wxID_ANY, name, wxDefaultPosition, wxSize(130, -1));
+ r.name->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); });
+ }
auto* grid_panel = new wxPanel(r.panel, wxID_ANY);
auto* grid_sizer = new wxGridSizer(m_n_rows, m_n_cols, 2, 2);
auto tool_states = parse_tool_states(active_tools);
+ if (tool_states.empty())
+ tool_states[0] = 1; // default T0 → Primary when no assignment is stored
r.all_tool_states = tool_states; // preserve all assignments, including off-screen tools
wxPanel* this_panel = r.panel;
@@ -4617,17 +4649,21 @@ private:
wxDefaultPosition, wxSize(220, 54), wxTE_MULTILINE);
r.gcode->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); });
- auto* rm = new wxButton(r.panel, wxID_ANY, wxT("\u00d7"),
- wxDefaultPosition, wxSize(24, 24), wxBU_EXACTFIT);
- rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) {
- remove_row(this_panel);
- notify();
- });
-
- sizer->Add(r.name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
+ if (!is_primary) {
+ sizer->Add(r.name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
+ }
sizer->Add(grid_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
sizer->Add(r.gcode, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
- sizer->Add(rm, 0, wxALIGN_CENTER_VERTICAL);
+
+ if (!is_primary) {
+ auto* rm = new wxButton(r.panel, wxID_ANY, wxT("\u00d7"),
+ wxDefaultPosition, wxSize(24, 24), wxBU_EXACTFIT);
+ rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) {
+ remove_row(this_panel);
+ notify();
+ });
+ sizer->Add(rm, 0, wxALIGN_CENTER_VERTICAL);
+ }
r.panel->SetSizer(sizer);
m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, 4);
@@ -4638,6 +4674,7 @@ private:
void remove_row(wxPanel* panel) {
for (size_t i = 0; i < m_rows.size(); ++i) {
if (m_rows[i].panel == panel) {
+ if (m_rows[i].is_primary) return; // primary row is non-deletable
m_rows_sizer->Detach(panel);
m_rows.erase(m_rows.begin() + i);
Layout();
@@ -5734,13 +5771,14 @@ void TabPrinter::toggle_options()
// IDEX/IQEX: show carriage config options only when is_imex is enabled
bool is_imex = m_config->opt_bool("is_imex");
- for (auto el : {"imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout",
+ for (auto el : {"imex_gantry_count", "imex_tools_per_gantry",
"imex_nozzle_clearance_x", "imex_nozzle_clearance_y",
- "imex_carriage_margin", "imex_viz_theme"})
+ "imex_carriage_margin"})
toggle_option(el, is_imex);
- if (m_imex_layout_combo) m_imex_layout_combo->Show(is_imex);
- if (m_imex_theme_combo) m_imex_theme_combo->Show(is_imex);
- if (m_imex_modes_ctrl) m_imex_modes_ctrl->Show(is_imex);
+ // These lines have custom combo widgets — toggle_line hides the full row
+ toggle_line("imex_tool_layout", is_imex);
+ toggle_line("imex_viz_theme", is_imex);
+ if (m_imex_modes_ctrl) m_imex_modes_ctrl->Show(is_imex);
}
wxString extruder_number;
long val = 1;