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;