From 1e56b65146cbab9f1577240db9429e0e71ab0ec7 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 02:37:08 -0400 Subject: [PATCH 001/154] feat: Add iXex parallel printing support for IDEX and IQEX printers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces first-class parallel printing (copy/mirror modes) for printers with multiple independent X-axis carriages. Branded iXex (independent X extruder), targeting Klipper firmware with a firmware-agnostic design. - PrintConfig: new printer options declaring iXex capability and geometry (is_ixex, ixex_gantry_count, ixex_tools_per_gantry, carriage dims, tool layout, and per-mode name/role/gcode arrays) - Preset: iXex keys registered in printer and process preset option lists - Tab: IXexModesCtrl visual grid editor in Printer preset tab; mode dropdown in Process → Others tab; clear_pages() nulls iXex pointers to prevent dangling-pointer crash on preset save - PartPlate: 2D zone visualization (active/dimmed/dividers) and placement-violation detection (has_ixex_placement_violations) that blocks slicing when objects fall outside the primary zone - Plater: violation detection wired into update_background_process so the Slice button is disabled with an error notification on violation - GCode: mode-activation G-code injected before machine_start_gcode - GCodeViewer: multi-carriage toolhead markers in sequential preview, filament legend annotated with active carriage count and mode name --- src/libslic3r/GCode.cpp | 18 + src/libslic3r/Preset.cpp | 20 +- src/libslic3r/PrintConfig.cpp | 103 ++++++ src/libslic3r/PrintConfig.hpp | 16 + src/slic3r/GUI/GCodeViewer.cpp | 252 +++++++++++++ src/slic3r/GUI/GCodeViewer.hpp | 6 + src/slic3r/GUI/OG_CustomCtrl.cpp | 5 + src/slic3r/GUI/PartPlate.cpp | 613 ++++++++++++++++++++++++++++--- src/slic3r/GUI/PartPlate.hpp | 13 + src/slic3r/GUI/Plater.cpp | 9 + src/slic3r/GUI/Tab.cpp | 519 ++++++++++++++++++++++++++ src/slic3r/GUI/Tab.hpp | 8 + 12 files changed, 1536 insertions(+), 46 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index fe10cbcd41..e1d218553a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3054,6 +3054,24 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } + // iXex: inject mode G-code before machine_start_gcode so the carriage mode + // macro is active before any motion, homing, or probing in start G-code. + if (print.config().is_ixex.value && !print.objects().empty()) { + const std::string& ixex_mode = print.objects().front()->config().ixex_parallel_mode.value; + if (!ixex_mode.empty() && ixex_mode != "primary") { + const auto& mode_names = print.config().ixex_mode_names.values; + const auto& mode_gcodes = print.config().ixex_mode_gcodes.values; + for (size_t i = 0; i < mode_names.size() && i < mode_gcodes.size(); ++i) { + if (mode_names[i] == ixex_mode && !mode_gcodes[i].empty()) { + std::string ixex_gcode = this->placeholder_parser_process( + "ixex_mode_gcode", mode_gcodes[i], initial_extruder_id); + file.writeln(ixex_gcode); + break; + } + } + } + } + // Write the custom start G-code file.writeln(machine_start_gcode); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 826d8a1a19..b8df0aab53 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -605,6 +605,12 @@ void Preset::save(DynamicPrintConfig* parent_config) ConfigOptionVectorBase* opt_vec_inherit = static_cast(parent_config->option(option)); if (opt_vec_src->size() == 1) opt_dst->set(opt_src); + else if (opt_vec_src->size() != opt_vec_inherit->size()) { + // Size mismatch (e.g. new multi-extruder profile inheriting from a + // single-extruder base): nil-delta encoding requires matching sizes, + // so fall back to storing the full vector. + opt_dst->set(opt_src); + } else if (key_set1->find(option) != key_set1->end()) { opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 1); } @@ -955,6 +961,8 @@ static std::vector s_Preset_print_options { "enable_wrapping_detection", "seam_slope_type", "seam_slope_conditional", "scarf_angle_threshold", "scarf_joint_speed", "scarf_joint_flow_ratio", "seam_slope_start_height", "seam_slope_entire_loop", "seam_slope_min_length", "seam_slope_steps", "seam_slope_inner_walls", "scarf_overhang_threshold", "interlocking_beam", "interlocking_orientation", "interlocking_beam_layer_count", "interlocking_depth", "interlocking_boundary_avoidance", "interlocking_beam_width","calib_flowrate_topinfill_special_order", + // iXex parallel print mode (per-print selection) + "ixex_parallel_mode", }; static std::vector s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type", @@ -1031,7 +1039,11 @@ static std::vector s_Preset_printer_options { "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "z_offset", "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", - "bed_temperature_formula", "nozzle_flush_dataset" + "bed_temperature_formula", "nozzle_flush_dataset", + // iXex (independent X extruder) — printer capability and user-defined modes + "is_ixex", "ixex_gantry_count", "ixex_tools_per_gantry", "ixex_tool_layout", + "ixex_carriage_width_x", "ixex_carriage_width_y", "ixex_carriage_margin", "ixex_viz_theme", + "ixex_mode_names", "ixex_mode_active_tools", "ixex_mode_gcodes" }; static std::vector s_Preset_sla_print_options { @@ -1444,6 +1456,12 @@ Preset* PresetCollection::get_preset_differed_for_save(Preset& preset) ConfigOptionVectorBase* opt_vec_inherit = static_cast(parent_preset->config.option(option)); if (opt_vec_src->size() == 1) opt_dst->set(opt_src); + else if (opt_vec_src->size() != opt_vec_inherit->size()) { + // Size mismatch (e.g. new multi-extruder profile inheriting from a + // single-extruder base): nil-delta encoding requires matching sizes, + // so fall back to storing the full vector. + opt_dst->set(opt_src); + } else if (key_set1->find(option) != key_set1->end()) { opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 1); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c4b3c28ad9..5c75c87622 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -193,6 +193,7 @@ static t_config_enum_values s_keys_map_WipeTowerType { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(WipeTowerType) + static t_config_enum_values s_keys_map_FuzzySkinMode { { "displacement", int(FuzzySkinMode::Displacement) }, { "extrusion", int(FuzzySkinMode::Extrusion) }, @@ -5482,6 +5483,108 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); + // iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers + def = this->add("is_ixex", coBool); + def->label = L("iXex Printer"); + def->tooltip = L("Enable iXex (independent X extruder) support for IDEX/IQEX printers with multiple independent carriages."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("ixex_gantry_count", coInt); + def->label = L("Gantry Count"); + def->tooltip = L("Number of independent Y-axis gantries. 1 for IDEX/single-rail IQEX. 2 for dual-gantry systems (divides bed into rows)."); + def->min = 1; + def->max = 2; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(1)); + + def = this->add("ixex_tools_per_gantry", coInt); + 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->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(2)); + + def = this->add("ixex_primary_col", coInt); + def->label = L("Primary Tool Column"); + def->tooltip = L("X index (0-based) of the primary toolhead. 0 = leftmost column. Determines which zone is the active print area."); + def->min = 0; + def->max = 3; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(0)); + + def = this->add("ixex_primary_row", coInt); + def->label = L("Primary Tool Row"); + def->tooltip = L("Y index (0-based) of the primary toolhead's gantry. 0 = front gantry. Determines which zone is the active print area."); + def->min = 0; + def->max = 3; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(0)); + + def = this->add("ixex_tool_layout", coString); + 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 = this->add("ixex_carriage_width_x", coFloat); + def->label = L("Carriage Width X"); + def->tooltip = L("Toolhead footprint in the X direction (mm). Used to calculate usable bed zones and detect carriage collisions."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(30.0)); + + def = this->add("ixex_carriage_width_y", coFloat); + def->label = L("Carriage Width Y"); + def->tooltip = L("Toolhead footprint in the Y direction (mm). Used for collision detection."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(30.0)); + + def = this->add("ixex_carriage_margin", coFloat); + def->label = L("Safety Margin"); + def->tooltip = L("Non-blocking advisory clearance strip (mm) drawn inside the primary zone at each carriage boundary. Parts placed within this strip will still slice; it is a visual reminder to leave extra clearance near the zone edge."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 200; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.0)); + + def = this->add("ixex_viz_theme", coString); + def->label = L("Visualization Theme"); + def->tooltip = L("Color theme for iXex bed zone visualization. Choose a colorblind-friendly theme if needed."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("standard")); + + def = this->add("ixex_parallel_mode", coString); + def->label = L("iXex Print Mode"); + def->tooltip = L("Name of the active iXex parallel print mode, or \"primary\" for single-carriage printing. Requires iXex Printer enabled in the Printer preset (Printer \u2192 iXex tab)."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("primary")); + + def = this->add("ixex_mode_names", coStrings); + def->label = L("iXex Mode Names"); + def->tooltip = L("Display names for each user-defined iXex parallel print mode."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionStrings()); + + def = this->add("ixex_mode_active_tools", coStrings); + def->label = L("iXex Mode Active Tools"); + def->tooltip = L("Tool role assignments for each mode. Format: \"idx:P,idx:C,idx:M\" where P=Primary, C=Copy, M=Mirror (e.g. \"0:P,1:C,2:M,3:M\"). Managed by the iXex Modes editor in the Printer preset."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionStrings()); + + def = this->add("ixex_mode_gcodes", coStrings); + def->label = L("iXex Mode G-codes"); + def->tooltip = L("G-code or macro call to inject at print start for each mode."); + 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. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 54c26c312a..a32c773517 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -63,6 +63,7 @@ enum class WipeTowerType { Type2, }; + enum PrintHostType { htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink }; @@ -496,6 +497,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType) + CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) @@ -1035,6 +1037,9 @@ PRINT_CONFIG_CLASS_DEFINE( // Orca: internal use only ((ConfigOptionBool, calib_flowrate_topinfill_special_order)) // ORCA: special flag for flow rate calibration + + // iXex parallel print mode (per-print selection, stores mode name or "primary") + ((ConfigOptionString, ixex_parallel_mode)) ) // This object is mapped to Perl as Slic3r::Config::PrintRegion. @@ -1332,6 +1337,17 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionStrings, filament_start_gcode)) ((ConfigOptionBool, single_extruder_multi_material)) ((ConfigOptionBool, manual_filament_change)) + // iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers + ((ConfigOptionBool, is_ixex)) + ((ConfigOptionInt, ixex_gantry_count)) + ((ConfigOptionInt, ixex_tools_per_gantry)) + ((ConfigOptionInt, ixex_primary_col)) + ((ConfigOptionInt, ixex_primary_row)) + ((ConfigOptionFloat, ixex_carriage_width_x)) + ((ConfigOptionFloat, ixex_carriage_width_y)) + ((ConfigOptionStrings, ixex_mode_names)) + ((ConfigOptionStrings, ixex_mode_active_tools)) + ((ConfigOptionStrings, ixex_mode_gcodes)) ((ConfigOptionBool, single_extruder_multi_material_priming)) ((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionString, change_filament_gcode)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 4814548a10..0ea8749b33 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -5,6 +5,7 @@ #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/Geometry.hpp" +#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/LocalesUtils.hpp" @@ -43,6 +44,7 @@ #include #include +#include #include @@ -911,6 +913,9 @@ void GCodeViewer::SequentialView::render(const bool has_render_path, float legen // marker.set_world_offset(current_offset); marker.render(canvas_width, canvas_height, view_type); marker.render_position_window(viewer, canvas_width, canvas_height, view_type); + // iXex secondary carriage markers + for (auto& sec : m_ixex_secondary_markers) + sec.render(canvas_width, canvas_height, view_type); } //float bottom = wxGetApp().plater()->get_current_canvas3D()->get_canvas_size().get_height(); @@ -965,6 +970,7 @@ void GCodeViewer::init(ConfigOptionMode mode, PresetBundle* preset_bundle) } } + m_marker_filename = filename; m_sequential_view.marker.init(filename); // initializes point sizes @@ -1504,9 +1510,254 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex(); m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position)); m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f); + + // iXex: compute all carriage positions; update secondary nozzle markers; + // carriage_box_draws is populated for toolhead footprint rendering after sequential_view.render(). + // Okabe-Ito colorblind-safe palette — excludes orange (#E69F00) and sky blue (#56B4E9) + // which are used by the bed zone fills, ensuring the markers contrast against the background. + static const std::array s_carriage_colors = {{ + { 1.000f, 1.000f, 1.000f, 0.65f }, // primary — white + { 0.941f, 0.894f, 0.259f, 0.65f }, // secondary 1 — yellow (#F0E442) + { 0.835f, 0.369f, 0.000f, 0.65f }, // secondary 2 — vermilion (#D55E00) + { 0.800f, 0.475f, 0.655f, 0.65f }, // secondary 3 — reddish purple (#CC79A7) + }}; + struct CarriageDraw { Vec3f pos; ColorRGBA color; }; + std::vector carriage_box_draws; + float ixex_box_wx = 0.0f, ixex_box_wy = 0.0f; + { + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + bool ixex_active = false; + if (preset_bundle && m_sequential_view.m_show_marker) { + const DynamicPrintConfig& printer_cfg = preset_bundle->printers.get_edited_preset().config; + auto* is_ixex_opt = printer_cfg.opt("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) { + const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); + const std::string mode = mode_opt ? mode_opt->value : "primary"; + + if (mode != m_ixex_last_mode) { + m_sequential_view.m_ixex_secondary_markers.clear(); + m_ixex_last_mode = mode; + } + + if (!mode.empty() && mode != "primary") { + auto* mode_names_opt = printer_cfg.opt("ixex_mode_names"); + auto* active_tools_opt = printer_cfg.opt("ixex_mode_active_tools"); + + auto* tpg_opt = printer_cfg.opt("ixex_tools_per_gantry"); + auto* wx_opt = printer_cfg.opt("ixex_carriage_width_x"); + auto* wy_opt = printer_cfg.opt("ixex_carriage_width_y"); + int tools_per_gantry = tpg_opt ? std::max(1, tpg_opt->value) : 1; + ixex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f; + ixex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f; + + // Parse "idx:P/C/M" format — matches PartPlate::calc_ixex_zones() exactly. + // Primary = state 1 (P), Copy = 2 (C), Mirror = 3 (M). + // Inactive tools (state 0) are not included in the stored string. + int pri_tool = -1; + std::vector sec_tool_ids; + std::map sec_tool_states; // tool_id -> 2=Copy, 3=Mirror + if (mode_names_opt && active_tools_opt) { + for (size_t i = 0; i < mode_names_opt->values.size(); ++i) { + if (i < active_tools_opt->values.size() && mode_names_opt->values[i] == mode) { + std::istringstream ss(active_tools_opt->values[i]); + std::string tok; + while (std::getline(ss, tok, ',')) { + tok.erase(std::remove_if(tok.begin(), tok.end(), ::isspace), tok.end()); + if (tok.empty()) continue; + try { + auto colon = tok.find(':'); + int idx = std::stoi(tok.substr(0, colon != std::string::npos ? colon : tok.size())); + int state = 1; // default: primary + if (colon != std::string::npos) { + char role = std::toupper((unsigned char)tok[colon + 1]); + if (role == 'C') state = 2; + else if (role == 'M') state = 3; + } + if (state == 1) pri_tool = idx; + else { + sec_tool_ids.push_back(idx); + sec_tool_states[idx] = state; + } + } catch (...) {} + } + break; + } + } + } + int sec_count = (int)sec_tool_ids.size(); + + if (sec_count > 0) { + ixex_active = true; + + // Lazy init secondary nozzle markers + if ((int)m_sequential_view.m_ixex_secondary_markers.size() != sec_count) { + m_sequential_view.m_ixex_secondary_markers.resize(sec_count); + for (int i = 0; i < sec_count; ++i) { + m_sequential_view.m_ixex_secondary_markers[i].init(m_marker_filename); + m_sequential_view.m_ixex_secondary_markers[i].set_color( + s_carriage_colors[(i + 1) % s_carriage_colors.size()]); + } + } + + // Bed X and Y bounds — read from the current plate's shape, which is + // in world/GL coordinates (same space as curr_vertex.position), and is + // the exact same source used by PartPlate::calc_ixex_zones(). + float bed_x_min, bed_x_max, bed_y_min, bed_y_max; + { + PartPlate* curr_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); + const Pointfs& plate_shape = curr_plate->get_shape(); + if (!plate_shape.empty()) { + bed_x_min = (float)plate_shape[0].x(); + bed_x_max = bed_x_min; + bed_y_min = (float)plate_shape[0].y(); + bed_y_max = bed_y_min; + for (const auto& pt : plate_shape) { + bed_x_min = std::min(bed_x_min, (float)pt.x()); + bed_x_max = std::max(bed_x_max, (float)pt.x()); + bed_y_min = std::min(bed_y_min, (float)pt.y()); + bed_y_max = std::max(bed_y_max, (float)pt.y()); + } + } else { + // Fallback: use toolpath bounding box extent + bed_x_min = (float)m_paths_bounding_box.min.x(); + bed_x_max = (float)m_paths_bounding_box.max.x(); + bed_y_min = (float)m_paths_bounding_box.min.y(); + bed_y_max = (float)m_paths_bounding_box.max.y(); + } + } + + auto* gc_opt = printer_cfg.opt("ixex_gantry_count"); + int gantry_count = gc_opt ? std::max(1, gc_opt->value) : 1; + + // Apply the same flip logic as PartPlate::calc_ixex_zones() so physical + // grid positions match the bed zone visualization. + auto* layout_opt = printer_cfg.opt("ixex_tool_layout"); + const std::string layout_str = layout_opt ? layout_opt->value : "front-left"; + const bool flip_x = (layout_str == "front-right" || layout_str == "rear-right"); + const bool flip_y = (layout_str == "rear-left" || layout_str == "rear-right"); + + auto phys_col_of = [&](int tid) -> int { + int raw = tid % tools_per_gantry; + return flip_x ? (tools_per_gantry - 1 - raw) : raw; + }; + auto phys_row_of = [&](int tid) -> int { + int raw = tid / tools_per_gantry; + 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; + + // Pre-pass: for each physical row, find the Copy reference 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; + 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]); + } + } + + // Primary carriage box + carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0] }); + + 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 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]); + 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; + 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. + 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; + } 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; + } else { + sec_x = bed_x_min + bed_x_max - prim_pos.x(); + } + } + + Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() }; + m_sequential_view.m_ixex_secondary_markers[i].set_world_position(sec_pos); + m_sequential_view.m_ixex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f); + carriage_box_draws.push_back({ + sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()] }); + } + } + } + } + } + if (!ixex_active) + m_sequential_view.m_ixex_secondary_markers.clear(); + } + // BBS fixed buttom margin. m_moves_slider.pos_y m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type()); + // iXex: render toolhead footprint boxes for each active carriage. + // Each box is ixex_carriage_width_x × ixex_carriage_width_y, sitting above the nozzle tip. + if (!carriage_box_draws.empty() && ixex_box_wx > 0.0f && ixex_box_wy > 0.0f) { + // Rebuild box mesh if dimensions changed + const Vec2f new_dims{ ixex_box_wx, ixex_box_wy }; + if (!m_ixex_toolhead_box.is_initialized() || new_dims != m_ixex_toolhead_box_dims) { + m_ixex_toolhead_box_dims = new_dims; + const float box_h = std::max(ixex_box_wx, ixex_box_wy); // height ~ largest horizontal dim + indexed_triangle_set its = its_make_cube((double)ixex_box_wx, (double)ixex_box_wy, (double)box_h); + // Center in X and Y; Z starts at 0 (nozzle tip level) + for (auto& v : its.vertices) + v += Vec3f(-ixex_box_wx * 0.5f, -ixex_box_wy * 0.5f, 0.0f); + m_ixex_toolhead_box.init_from(its); + } + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader) { + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + shader->start_using(); + shader->set_uniform("emission_factor", 0.0f); + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + const Transform3d& view_matrix = camera.get_view_matrix(); + for (const auto& draw : carriage_box_draws) { + const Transform3d model_matrix = Geometry::translation_transform( + (draw.pos + Vec3f(0.0f, 0.0f, m_z_offset)).cast()); + shader->set_uniform("view_model_matrix", view_matrix * model_matrix); + const Matrix3d view_normal_matrix = + view_matrix.matrix().block(0, 0, 3, 3) * + model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + m_ixex_toolhead_box.set_color(draw.color); + m_ixex_toolhead_box.render(); + } + shader->stop_using(); + glsafe(::glDisable(GL_BLEND)); + } + } + #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS if (is_legend_shown()) { ImGuiWrapper& imgui = *Slic3r::GUI::wxGetApp().imgui(); @@ -2706,6 +2957,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector m_ixex_secondary_markers; // one per active secondary carriage in iXex mode GCodeWindow gcode_window; float m_scale = 1.0; bool m_show_marker = false; @@ -202,6 +204,10 @@ private: ConfigOptionMode m_user_mode; bool m_fold = {false}; + std::string m_marker_filename; // cached for lazy secondary marker init + std::string m_ixex_last_mode; // detect mode changes for secondary marker rebuild + GLModel m_ixex_toolhead_box; // shared box mesh for all carriage footprint overlays + Vec2f m_ixex_toolhead_box_dims{ 0.0f, 0.0f }; // (width_x, width_y) — rebuild when changed size_t m_extruders_count; std::vector m_filament_diameters; diff --git a/src/slic3r/GUI/OG_CustomCtrl.cpp b/src/slic3r/GUI/OG_CustomCtrl.cpp index 331d4a5db8..da4564f537 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.cpp +++ b/src/slic3r/GUI/OG_CustomCtrl.cpp @@ -196,6 +196,11 @@ wxPoint OG_CustomCtrl::get_pos(const Line& line, Field* field_in/* = nullptr*/) if (line.widget) { #ifndef DISABLE_BLINKING h_pos += (line.has_undo_ui() ? 3 : 1) * blinking_button_width; +#else + // When blinking is disabled, widget lines still need to honour + // option_label_at_right the same way regular lines do below. + if (opt_group->option_label_at_right) + add_buttons_width(blinking_button_width); #endif for (auto child : line.widget_sizer->GetChildren()) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 98c285f46a..0d0429db3c 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -451,6 +453,495 @@ void PartPlate::calc_exclude_triangles(const ExPolygon &poly) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":Unable to create exclude triangles\n"; } +// Forward declaration (defined later in this file) +static bool init_model_from_lines(GLModel &model, const Lines &lines, float z); + +void PartPlate::calc_ixex_zones() +{ + m_ixex_copy_zones.clear(); + m_ixex_mirror_zones.clear(); + m_ixex_zone_borders.reset(); + // Clear blocking vectors at the top so early returns don't leave stale data. + m_ixex_secondary_zone_boxes.clear(); + m_ixex_collision_zones.clear(); + m_ixex_collision_overlay.clear(); + m_ixex_margin_overlay.clear(); + + if (!wxGetApp().preset_bundle) + return; + + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* is_ixex_opt = printer_cfg.option("is_ixex"); + if (!is_ixex_opt || !is_ixex_opt->value) + return; + + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + std::string active_mode = mode_opt ? mode_opt->value : "primary"; + if (active_mode == "primary" || active_mode.empty()) + return; + + // Grid dimensions and tool layout from printer config + auto* gantry_opt = printer_cfg.option("ixex_gantry_count"); + auto* tpg_opt = printer_cfg.option("ixex_tools_per_gantry"); + auto* layout_opt = printer_cfg.option("ixex_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; + + // Layout: which corner is T0? 0=front-left, 1=front-right, 2=rear-left, 3=rear-right + // flip_x: col 0 is right(max-X); flip_y: row 0 is rear(max-Y) + std::string layout_str = layout_opt ? layout_opt->value : "front-left"; + bool flip_x = (layout_str == "front-right" || layout_str == "rear-right"); + bool flip_y = (layout_str == "rear-left" || layout_str == "rear-right"); + + // Convert tool index to physical (col=X-index, row=Y-index), col/row 0 = min-X/min-Y + auto tool_to_phys = [&](int idx) -> std::pair { + int raw_col = idx % n_cols, raw_row = idx / n_cols; + return { flip_x ? (n_cols - 1 - raw_col) : raw_col, + flip_y ? (n_rows - 1 - raw_row) : raw_row }; + }; + + int pri_col = 0, pri_row = 0; + + if (n_cols == 1 && n_rows == 1) + return; // nothing to dim with a single zone + + // Look up secondary tool indices (active in mode, but NOT the primary tool) + auto* names_opt = printer_cfg.option("ixex_mode_names"); + auto* tools_opt = printer_cfg.option("ixex_mode_active_tools"); + std::string active_tools_str; + if (names_opt && tools_opt) { + for (size_t i = 0; i < names_opt->values.size(); ++i) { + if (names_opt->values[i] == active_mode && i < tools_opt->values.size()) { + active_tools_str = tools_opt->values[i]; + break; + } + } + } + + // Parse "idx:P/C/M" format → map (1=Primary, 2=Copy, 3=Mirror) + // Backwards compat: plain "idx" → Primary + std::map tool_states; + { + std::istringstream ss(active_tools_str); + std::string token; + while (std::getline(ss, token, ',')) { + token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); + if (token.empty()) continue; + try { + auto colon = token.find(':'); + int idx, state = 1; + if (colon != std::string::npos) { + idx = std::stoi(token.substr(0, colon)); + char role = std::toupper((unsigned char)token[colon + 1]); + if (role == 'C') state = 2; + else if (role == 'M') state = 3; + } else { + idx = std::stoi(token); + } + if (idx >= 0 && idx < n_rows * n_cols) + tool_states[idx] = state; + } catch (...) {} + } + } + + // 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; } + } + + // Separate copy and mirror secondary cells using physical coordinates + std::set> copy_cells, mirror_cells; + for (auto& [idx, state] : tool_states) { + auto [c, r] = tool_to_phys(idx); + if (c == pri_col && r == pri_row) continue; // skip primary + if (state == 2) copy_cells.insert({c, r}); + else if (state == 3) mirror_cells.insert({c, r}); + } + + // All active secondary cells combined (for separation axis computation) + std::set> all_secondary; + for (auto& p : copy_cells) all_secondary.insert(p); + for (auto& p : mirror_cells) all_secondary.insert(p); + + 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; + + // Separation axes: row-sep = secondaries on a different gantry, col-sep = different column + bool has_row_sep = false, has_col_sep = false; + for (const auto& [sc, sr] : all_secondary) { + if (sr != pri_row) has_row_sep = true; + if (sc != pri_col) has_col_sep = true; + } + + // Primary zone extent (the clear printable area): + // row-sep only → full bed width × primary row's Y band + // col-sep only → primary col's X band × full bed height + // both → primary quadrant + auto in_primary_zone = [&](int col, int row) { + bool row_ok = !has_row_sep || (row == pri_row); + bool col_ok = !has_col_sep || (col == pri_col); + return row_ok && col_ok; + }; + + ExPolygon bed_poly; + generate_print_polygon(bed_poly); + + // Build a clipped filled GLModel for an expanded zone rect and push into a vector. + auto push_zone_fill = [&](std::vector& vec, double cx0, double cx1, double cy0, double cy1) { + ExPolygon cell; + cell.contour.append({ scale_(cx0), scale_(cy0) }); + cell.contour.append({ scale_(cx1), scale_(cy0) }); + cell.contour.append({ scale_(cx1), scale_(cy1) }); + cell.contour.append({ scale_(cx0), scale_(cy1) }); + ExPolygons clipped = intersection_ex({ cell }, { bed_poly }); + if (!clipped.empty()) { + GLModel m; + if (init_model_from_poly(m, clipped.front(), GROUND_Z)) + vec.push_back(std::move(m)); + } + }; + + // Build expanded zone rects for a set of cells. + // When secondaries share only a row difference (same column as primary) → expand to full X width. + // When secondaries share only a column difference → expand to full Y height. + // When both axes differ → per-quadrant. + struct BoxRect { double x0, x1, y0, y1; }; + auto make_boxes = [&](const std::set>& cells) -> std::vector { + std::vector boxes; + if (cells.empty()) return boxes; + 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}); + } 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}); + } 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}); + } + return boxes; + }; + + // Build fills for copy and mirror zones using expanded boxes (inactive cells not rendered). + auto push_expanded_fills = [&](std::vector& vec, const std::set>& cells) { + for (const auto& b : make_boxes(cells)) + push_zone_fill(vec, b.x0, b.x1, b.y0, b.y1); + }; + push_expanded_fills(m_ixex_copy_zones, copy_cells); + push_expanded_fills(m_ixex_mirror_zones, mirror_cells); + + // White borders: one outline per expanded box of each active secondary type. + auto build_rect_lines = [](const std::vector& boxes) -> Lines { + Lines lines; + for (const auto& b : boxes) { + lines.emplace_back(Point(scale_(b.x0), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y0))); + lines.emplace_back(Point(scale_(b.x1), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y1))); + lines.emplace_back(Point(scale_(b.x1), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y1))); + lines.emplace_back(Point(scale_(b.x0), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y0))); + } + return lines; + }; + + // Combine copy+mirror for borders (each expands independently then we collect all lines) + Lines all_border_lines = build_rect_lines(make_boxes(copy_cells)); + auto mirror_border_lines = build_rect_lines(make_boxes(mirror_cells)); + all_border_lines.insert(all_border_lines.end(), mirror_border_lines.begin(), mirror_border_lines.end()); + + if (!all_border_lines.empty()) + if (!init_model_from_lines(m_ixex_zone_borders, all_border_lines, GROUND_Z_GRIDLINE)) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": unable to create iXex zone border lines\n"; + + // --- Secondary zone blocking --- + // Collect the expanded bounding boxes for all secondary (copy+mirror) zones. + // check_outside() uses these to prevent objects being placed outside the primary zone. + for (const auto& b : make_boxes(copy_cells)) + m_ixex_secondary_zone_boxes.push_back( + BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4))); + for (const auto& b : make_boxes(mirror_cells)) + m_ixex_secondary_zone_boxes.push_back( + BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4))); + + // --- Carriage collision danger strips --- + // Only add strips at boundaries of the PRIMARY zone — objects are only placed in the + // primary zone, so secondary-to-secondary boundaries have no relevance. + // + // Strip width is carriage/2 on the primary side only: + // right X boundary: [bnd_x - carriage_w/2, bnd_x] + // left X boundary: [bnd_x, bnd_x + carriage_w/2] + // top Y boundary: [bnd_y - carriage_h/2, bnd_y] + // bottom Y boundary:[bnd_y, bnd_y + carriage_h/2] + // + // Strip length matches the primary zone extent (same expansion logic as make_boxes): + // !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("ixex_carriage_width_x"); + auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); + double carriage_w = cw_opt ? cw_opt->value : 0.0; + double carriage_h = ch_opt ? ch_opt->value : 0.0; + double margin = mgn_opt ? mgn_opt->value : 0.0; + + // Helper: build a filled GLModel polygon and push into margin overlay (advisory, non-blocking) + auto add_margin_fill = [&](double sx0, double sx1, double sy0, double sy1) { + ExPolygon cell; + cell.contour.append({ scale_(sx0), scale_(sy0) }); + cell.contour.append({ scale_(sx1), scale_(sy0) }); + cell.contour.append({ scale_(sx1), scale_(sy1) }); + cell.contour.append({ scale_(sx0), scale_(sy1) }); + ExPolygons clipped = intersection_ex({ cell }, { bed_poly }); + if (!clipped.empty()) { + GLModel m; + if (init_model_from_poly(m, clipped.front(), GROUND_Z)) + m_ixex_margin_overlay.push_back(std::move(m)); + } + }; + + // 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; + + // Helper: build a BoundingBoxf3 strip, clip to bed, add to members + auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) { + BoundingBoxf3 box; + box.min = Vec3d(sx0, sy0, -1.0); + box.max = Vec3d(sx1, sy1, 1e4); + m_ixex_collision_zones.push_back(box); + + ExPolygon cell; + cell.contour.append({ scale_(sx0), scale_(sy0) }); + cell.contour.append({ scale_(sx1), scale_(sy0) }); + cell.contour.append({ scale_(sx1), scale_(sy1) }); + cell.contour.append({ scale_(sx0), scale_(sy1) }); + ExPolygons clipped = intersection_ex({ cell }, { bed_poly }); + if (!clipped.empty()) { + GLModel m; + if (init_model_from_poly(m, clipped.front(), GROUND_Z)) + m_ixex_collision_overlay.push_back(std::move(m)); + } + }; + + // Determine which directions have mirror secondaries adjacent to the primary. + // Only mirror tools can cause carriage collisions — they move toward each other. + // Copy tools always move in the same direction, so no collision strip is needed. + bool has_right_sec = false, has_left_sec = false; + bool has_top_sec = false, has_bottom_sec = false; + 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; + } + + // X-axis boundaries (vertical strips, width = carriage_w/2 on primary side) + if (carriage_w > 0.0) { + if (has_right_sec) { + double bnd_x = x_min + (pri_col + 1) * zone_w; + double strip_inner = bnd_x - carriage_w * 0.5; + 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 strip_inner = bnd_x + carriage_w * 0.5; + add_strip(bnd_x, strip_inner, pz_y0, pz_y1); + if (margin > 0.0) + add_margin_fill(strip_inner, strip_inner + margin, pz_y0, pz_y1); + } + } + + // Y-axis boundaries (horizontal strips, width = carriage_h/2 on primary side) + if (carriage_h > 0.0) { + if (has_top_sec) { + double bnd_y = y_min + (pri_row + 1) * zone_h; + double strip_inner = bnd_y - carriage_h * 0.5; + 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 strip_inner = bnd_y + carriage_h * 0.5; + add_strip(pz_x0, pz_x1, bnd_y, strip_inner); + if (margin > 0.0) + add_margin_fill(pz_x0, pz_x1, strip_inner, strip_inner + margin); + } + } +} + +// Build a cache key from the current iXex config options, or "" if iXex is off. +static std::string build_ixex_cache_key() +{ + if (!wxGetApp().preset_bundle) + return ""; + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* is_ixex_opt = printer_cfg.option("is_ixex"); + if (!is_ixex_opt || !is_ixex_opt->value) + return ""; + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); + auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); + auto* cw_opt = printer_cfg.option("ixex_carriage_width_x"); + auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); + return (mode_opt ? mode_opt->value : "primary") + + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) + + "x" + std::to_string(n_row_opt ? n_row_opt->value : 1) + + "|cw" + std::to_string(cw_opt ? (int)cw_opt->value : 0) + + "|ch" + std::to_string(ch_opt ? (int)ch_opt->value : 0) + + "|mg" + std::to_string(mgn_opt ? (int)(mgn_opt->value * 10) : 0); +} + +// 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_ixex_zones() +{ + std::string cache_key = build_ixex_cache_key(); + if (cache_key != m_ixex_zones_mode_cache) { + m_ixex_zones_mode_cache = cache_key; + calc_ixex_zones(); + } +} + +bool PartPlate::has_ixex_placement_violations() +{ + ensure_ixex_zones(); + if (m_ixex_secondary_zone_boxes.empty() && m_ixex_collision_zones.empty()) + return false; + for (const auto& pr : obj_to_instance_set) { + int obj_id = pr.first; + int instance_id = pr.second; + if (!valid_instance(obj_id, instance_id)) + continue; + ModelInstance* instance = m_model->objects[obj_id]->instances[instance_id]; + Polygon hull = instance->convex_hull_2d(); + if (hull.points.empty()) + continue; + for (const auto& box : m_ixex_secondary_zone_boxes) { + if (!intersection({box.polygon(true)}, {hull}).empty()) + return true; + } + for (const auto& strip : m_ixex_collision_zones) { + if (!intersection({strip.polygon(true)}, {hull}).empty()) + return true; + } + } + return false; +} + +void PartPlate::render_ixex_zones(bool force_default_color) +{ + if (force_default_color) + return; + + ensure_ixex_zones(); + + // Read visualization theme from printer config. + struct IXexTheme { + ColorRGBA copy; // secondary copy zone fill + ColorRGBA mirror; // secondary mirror zone fill + ColorRGBA border; // zone outline + ColorRGBA danger; // blocking collision strip + ColorRGBA margin; // advisory safety margin + }; + + // Standard (Okabe-Ito orange + sky blue) + static const IXexTheme k_standard = { + { 0.337f, 0.706f, 0.914f, 0.45f }, // copy — sky blue #56B4E9 + { 0.902f, 0.624f, 0.000f, 0.45f }, // mirror — orange #E69F00 + { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white + { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red + { 0.300f, 0.900f, 0.200f, 0.40f }, // margin — lime green + }; + // Deuteranopia / Protanopia: avoids red-green confusion. + // Danger strip uses strong blue-violet (red invisible to protanopes). + static const IXexTheme k_deuteranopia = { + { 0.000f, 0.447f, 0.698f, 0.50f }, // copy — blue #0072B2 + { 0.941f, 0.894f, 0.259f, 0.50f }, // mirror — yellow #F0E442 + { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white + { 0.200f, 0.100f, 0.800f, 0.65f }, // danger — blue-violet (red not visible) + { 0.800f, 0.475f, 0.655f, 0.45f }, // margin — reddish purple #CC79A7 + }; + // Tritanopia: avoids blue-yellow confusion. + // Copy uses vermilion, mirror uses reddish-purple, margin uses teal. + static const IXexTheme k_tritanopia = { + { 0.835f, 0.369f, 0.000f, 0.50f }, // copy — vermilion #D55E00 + { 0.800f, 0.475f, 0.655f, 0.50f }, // mirror — reddish purple #CC79A7 + { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white + { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red (visible to tritanopes) + { 0.000f, 0.700f, 0.600f, 0.45f }, // margin — teal + }; + // High contrast: saturated, higher alpha for low-vision users. + static const IXexTheme k_high_contrast = { + { 0.000f, 0.600f, 1.000f, 0.65f }, // copy — vivid blue + { 1.000f, 0.600f, 0.000f, 0.65f }, // mirror — vivid orange + { 1.000f, 1.000f, 0.000f, 1.00f }, // border — bright yellow + { 1.000f, 0.000f, 0.000f, 0.75f }, // danger — full red + { 0.000f, 1.000f, 0.200f, 0.60f }, // margin — bright green + }; + + const IXexTheme* theme = &k_standard; + if (wxGetApp().preset_bundle) { + const DynamicPrintConfig& pcfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (auto* t = pcfg.option("ixex_viz_theme")) { + if (t->value == "deuteranopia") theme = &k_deuteranopia; + else if (t->value == "tritanopia") theme = &k_tritanopia; + else if (t->value == "high_contrast") theme = &k_high_contrast; + } + } + + // Both fills and border lines use the flat shader already active from render(). + glsafe(::glDepthMask(GL_FALSE)); + + if (!m_ixex_copy_zones.empty()) + for (GLModel& z : m_ixex_copy_zones) + if (z.is_initialized()) { z.set_color(theme->copy); z.render(); } + + if (!m_ixex_mirror_zones.empty()) + for (GLModel& z : m_ixex_mirror_zones) + if (z.is_initialized()) { z.set_color(theme->mirror); z.render(); } + + glsafe(::glDepthMask(GL_TRUE)); + + if (m_ixex_zone_borders.is_initialized()) { + m_ixex_zone_borders.set_color(theme->border); + glsafe(::glLineWidth(2.0f)); + m_ixex_zone_borders.render(); + glsafe(::glLineWidth(1.0f)); + } + + if (!m_ixex_collision_overlay.empty()) { + glsafe(::glDepthMask(GL_FALSE)); + for (GLModel& s : m_ixex_collision_overlay) + if (s.is_initialized()) { s.set_color(theme->danger); s.render(); } + glsafe(::glDepthMask(GL_TRUE)); + } + + if (!m_ixex_margin_overlay.empty()) { + glsafe(::glDepthMask(GL_FALSE)); + for (GLModel& b : m_ixex_margin_overlay) + if (b.is_initialized()) { b.set_color(theme->margin); b.render(); } + glsafe(::glDepthMask(GL_TRUE)); + } +} + void PartPlate::calc_triangles_from_polygon(const ExPolygon &poly, GLModel &render_model){ if (poly.empty()) { render_model.reset(); @@ -1882,7 +2373,7 @@ bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config) if (filament_type == "PETG") has_petg = true; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " check error:array bound"; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " check error:array bound"; } } } @@ -2147,7 +2638,7 @@ arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const Dynamic if (wipe_tower_brim_width_opt) { wp_brim_width = wipe_tower_brim_width_opt->getFloat(); if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wt_size.z()); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width; } x = std::clamp(x, margin, (float)plate_width - w - margin - wp_brim_width); @@ -2211,9 +2702,9 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height bool size_changed = false; //size changed means the machine changed bool pos_changed = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ % m_plate_index % m_origin.x() % m_origin.y() % m_origin.z() % m_width % m_depth % m_height; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ % with_instance_move % origin.x() % origin.y() % origin.z() % width % depth % height; size_changed = ((width != m_width) || (depth != m_depth) || (height != m_height)); pos_changed = (m_origin != origin); @@ -2253,7 +2744,7 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height offset.x() = offset.x() + off_x; offset.y() = offset.y() + off_y; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\ % obj_id % instance_id % off_x % off_y % offset.x() % offset.y(); instance->set_offset(offset); @@ -2390,7 +2881,7 @@ bool PartPlate::is_valid_gcode_file() return false; boost::filesystem::path gcode_file(m_gcode_result->filename); if (!boost::filesystem::exists(gcode_file)) { - BOOST_LOG_TRIVIAL(info) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename; + BOOST_LOG_TRIVIAL(warning) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename; return false; } return true; @@ -2469,6 +2960,9 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const //check whether instance is outside the plate or not bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box) { + // Ensure iXex zone geometry is current before any placement check. + ensure_ixex_zones(); + bool outside = true; ModelObject* object = m_model->objects[obj_id]; @@ -2512,6 +3006,32 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi outside = false; } + // iXex placement check (prepare-mode bounding-box test). + // Block objects that overlap secondary (copy/mirror) zones or the carriage + // danger strip at the primary zone boundary. Reuses the existing + // outside=true → instance_outside_set → update_states() → blocks slicing path. + if (!outside && (!m_ixex_secondary_zone_boxes.empty() || !m_ixex_collision_zones.empty())) { + Polygon obj_hull = instance->convex_hull_2d(); // scaled Clipper coords + // 1. Object must not touch any secondary zone. + for (const auto& box : m_ixex_secondary_zone_boxes) { + Polygon p = box.polygon(true); + if (!intersection({ p }, { obj_hull }).empty()) { + outside = true; + break; + } + } + // 2. Object must not enter the carriage danger strip inside the primary zone. + if (!outside) { + for (const auto& strip : m_ixex_collision_zones) { + Polygon strip_poly = strip.polygon(true); + if (!intersection({ strip_poly }, { obj_hull }).empty()) { + outside = true; + break; + } + } + } + } + return outside; } @@ -2602,7 +3122,7 @@ int PartPlate::add_instance(int obj_id, int instance_id, bool move_position, Bou m_ready_for_slice = true; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; return 0; } @@ -2696,7 +3216,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s if (skip_objects.find(instance->loaded_id) != skip_objects.end()) { instance->printable = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name; continue; } } @@ -2708,7 +3228,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s for ( size_t new_instance_id = 0; new_instance_id < newObj->instances.size(); new_instance_id++ ) { obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id)); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id; } } } @@ -2731,10 +3251,10 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s while (skip_objects.find(instance->loaded_id) != skip_objects.end()) { instance->loaded_id ++; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id; } } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name; } } } @@ -3056,7 +3576,7 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co if ((m_shape == new_shape)&&(m_exclude_area == new_exclude_areas) &&(m_height_to_lid == height_to_lid)&&(m_height_to_rod == height_to_rod)) { - BOOST_LOG_TRIVIAL(info) << "PartPlate same shape, skip directly"; + BOOST_LOG_TRIVIAL(warning) << "PartPlate same shape, skip directly"; return false; } @@ -3101,6 +3621,8 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co const BoundingBox& pp_bbox = poly.contour.bounding_box(); calc_gridlines(poly, pp_bbox); + m_ixex_zones_mode_cache = "\x01"; // force rebuild on next render + calc_ixex_zones(); calc_vertex_for_icons(0, m_del_icon); calc_vertex_for_icons(1, m_orient_icon); @@ -3205,6 +3727,7 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec render_background(force_background_color); render_exclude_area(force_background_color); + render_ixex_zones(force_background_color); if(m_selected && wxGetApp().plater()->get_enable_wrapping_detection()){ if(!m_wrapping_detection_triangles.is_initialized()){ auto points = get_plate_wrapping_detection_area(); @@ -3286,7 +3809,7 @@ void PartPlate::update_states() } } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; return; } @@ -3294,7 +3817,7 @@ void PartPlate::update_states() //invalid sliced result void PartPlate::update_slice_result_valid_state(bool valid) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid; m_slice_result_valid = valid; if (valid) m_slice_percent = 100.0f; @@ -3366,13 +3889,13 @@ int PartPlate::load_gcode_from_file(const std::string& filename) // BBS: use backup path to save temp gcode // auto path = get_tmp_gcode_path(); // if (boost::filesystem::exists(boost::filesystem::path(path))) { - // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str(); + // BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str(); // boost::nowide::remove(path.c_str()); //} // std::error_code error = rename_file(filename, path); // if (error) { - // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() % + // BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() % //error.message(); return -1; //} if (boost::filesystem::exists(filename)) { @@ -3383,7 +3906,7 @@ int PartPlate::load_gcode_from_file(const std::string& filename) update_slice_result_valid_state(true); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str(); } else { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find gcode file %1%") % filename.c_str(); @@ -3714,7 +4237,7 @@ PartPlateList::PartPlateList(int width, int depth, int height, Plater* platerObj :m_plate_width(width), m_plate_depth(depth), m_plate_height(height), m_plater(platerObj), m_model(modelObj), printer_technology(tech), unprintable_plate(this, Vec3d(0.0 + width * (1. + LOGICAL_PART_PLATE_GAP), 0.0, 0.0), width, depth, height, platerObj, modelObj, false, tech) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; init(); } @@ -4135,8 +4658,8 @@ void PartPlateList::reset_size(int width, int depth, int height, bool reload_obj { Vec3d origin1, origin2; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; if ((m_plate_width != width) || (m_plate_depth != depth) || (m_plate_height != height)) { m_plate_width = width; @@ -4352,7 +4875,7 @@ int PartPlateList::duplicate_plate(int index) // go over the instances and pair with the object for (size_t new_instance_id = 0; new_instance_id < object_copy->instances.size(); new_instance_id++){ new_plate->obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id)); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id; } } new_plate->translate_all_instance(plate_to_plate_offset); @@ -4715,7 +5238,7 @@ void PartPlateList::update_plate_cols() m_plate_count = m_plate_list.size(); m_plate_cols = compute_colum_count(m_plate_count); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols; return; } @@ -4796,7 +5319,7 @@ int PartPlateList::lock_plate(int index, bool state) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(":can not get plate for index %1%, size %2%") % index % m_plate_list.size(); return -1; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state; plate->lock(state); @@ -5714,7 +6237,7 @@ Print& PartPlateList::get_current_fff_print() const Print* print; current_plate = m_plate_list[m_current_plate]; - //BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; + //BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; assert(current_plate != NULL); current_plate->get_print((PrintBase **)&print, nullptr, nullptr); @@ -5728,7 +6251,7 @@ GCodeProcessorResult* PartPlateList::get_current_slice_result() const PartPlate* current_plate; current_plate = m_plate_list[m_current_plate]; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; assert(current_plate != NULL); return current_plate->get_slice_result(); @@ -5933,7 +6456,7 @@ int PartPlateList::rebuild_plates_after_deserialize(std::vector& previous_ //not used /*if (m_plate_width == 0) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!"); Vec3d max = m_plater->get_bed().get_bounding_box(false).max; Vec3d min = m_plater->get_bed().get_bounding_box(false).min; double z = m_plater->config()->opt_float("printable_height"); @@ -5947,7 +6470,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex { int ret = 0; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates; // sort by arrange_order std::sort(m_model->objects.begin(), m_model->objects.end(), [](auto a, auto b) {return a->instances[0]->arrange_order < b->instances[0]->arrange_order; }); @@ -5964,7 +6487,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex || !m_plate_list[i]->has_printable_instances()) { //delete it - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i; delete_plate(i); } else if (m_plate_list[i]->is_locked()) { @@ -5984,7 +6507,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex } #endif - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size(); return ret; } @@ -6002,10 +6525,10 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->locked = m_plate_list[i]->m_locked; plate_data_item->plate_index = m_plate_list[i]->m_plate_index; plate_data_item->plate_name = m_plate_list[i]->get_plate_name(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!") + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!") %(i+1) %m_plate_list[i]->thumbnail_data.width %m_plate_list[i]->thumbnail_data.height %m_plate_list[i]->thumbnail_data.pixels.size(); plate_data_item->plate_thumbnail.load_from(m_plate_list[i]->thumbnail_data); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!") + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!") %(i+1) %plate_data_item->plate_thumbnail.width %plate_data_item->plate_thumbnail.height %plate_data_item->plate_thumbnail.pixels.size(); plate_data_item->config.apply(*m_plate_list[i]->config()); @@ -6022,7 +6545,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->objects_and_instances.emplace_back(it->first, it->second); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ <m_gcode_result->filename % with_slice_info %m_plate_list[i]->is_slice_result_valid()%plate_data_item->objects_and_instances.size(); if (with_slice_info) { @@ -6055,12 +6578,12 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w } plate_data_item->is_support_used = print->is_support_used(); } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("print is null!"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("print is null!"); } //parse filament info plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result()); } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() << ", result valid = " << m_plate_list[i]->is_slice_result_valid(); } } @@ -6068,7 +6591,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_list.push_back(plate_data_item); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size(); return ret; } @@ -6094,7 +6617,7 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate index %1% seems invalid, skip it")% plate_data_list[i]->plate_index; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%") + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%") %i %plate_data_list[i]->gcode_file %plate_data_list[i]->is_sliced_valid %plate_data_list[i]->toolpath_outside %plate_data_list[i]->is_support_used %plate_data_list[i]->is_label_object_enabled; //load object and instance from 3mf //just test for file correct or not, we will rebuild later @@ -6123,17 +6646,17 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f gcode_result->warnings = plate_data_list[i]->warnings; gcode_result->filament_maps = plate_data_list[i]->filament_maps; if (m_plater && !plate_data_list[i]->thumbnail_file.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; if (boost::filesystem::exists(plate_data_list[i]->thumbnail_file)) { m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->thumbnail_file, m_plate_list[index]->thumbnail_data); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ <thumbnail_data.width %m_plate_list[index]->thumbnail_data.height %m_plate_list[index]->thumbnail_data.pixels.size(); } } if (m_plater && !plate_data_list[i]->no_light_thumbnail_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->no_light_thumbnail_file)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->no_light_thumbnail_file, m_plate_list[index]->no_light_thumbnail_data); } } @@ -6146,13 +6669,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f }*/ if (m_plater && !plate_data_list[i]->top_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->top_file)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->top_file, m_plate_list[index]->top_thumbnail_data); } } if (m_plater && !plate_data_list[i]->pick_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->pick_file)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->pick_file, m_plate_list[index]->pick_thumbnail_data); } } @@ -6588,7 +7111,7 @@ void PartPlateList::on_extruder_count_changed(int extruder_count) for (unsigned int i = 0; i < (unsigned int) m_plate_list.size(); ++i) { m_plate_list[i]->on_extruder_count_changed(extruder_count); } - BOOST_LOG_TRIVIAL(info) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count; + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count; } void PartPlateList::set_filament_count(int filament_count) @@ -6598,7 +7121,7 @@ void PartPlateList::set_filament_count(int filament_count) { m_plate_list[i]->set_filament_count(filament_count); } - BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; } void PartPlateList::on_filament_added(int filament_count) @@ -6608,7 +7131,7 @@ void PartPlateList::on_filament_added(int filament_count) { m_plate_list[i]->on_filament_added(); } - BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; } void PartPlateList::on_filament_deleted(int filament_count, int filament_id) @@ -6618,7 +7141,7 @@ void PartPlateList::on_filament_deleted(int filament_count, int filament_id) { m_plate_list[i]->on_filament_deleted(filament_count, filament_id); } - BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id; + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id; } }//end namespace GUI diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 9585c7aa95..a82d315328 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -135,6 +135,14 @@ private: GLModel m_logo_triangles; GLModel m_gridlines; GLModel m_gridlines_bolder; + std::vector m_ixex_copy_zones; // blue tint (Wong #56B4E9) + std::vector m_ixex_mirror_zones; // orange tint (Wong #E69F00) + GLModel m_ixex_zone_borders; // white outlines around active secondary zones + std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here + std::vector m_ixex_collision_zones; // carriage danger strips inside primary zone (mm) + std::vector m_ixex_collision_overlay; // red-orange rendered fill for danger strips + std::vector m_ixex_margin_overlay; // amber advisory bands just inside collision strips + std::string m_ixex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt GLModel m_height_limit_common; GLModel m_height_limit_bottom; GLModel m_height_limit_top; @@ -177,6 +185,9 @@ private: void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model); void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox); void calc_height_limit(); + void calc_ixex_zones(); + void ensure_ixex_zones(); + void render_ixex_zones(bool force_default_color); 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); @@ -432,6 +443,8 @@ public: { return m_ready_for_slice && !m_apply_invalid; } + // Returns true if any instance on this plate overlaps an iXex secondary or collision zone. + bool has_ixex_placement_violations(); void update_slice_ready_status(bool ready_slice) { m_ready_for_slice = ready_slice; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 64274adf19..2b07bc94a8 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7722,6 +7722,15 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool q->post_process_string_object_exception(err); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; + // iXex placement check: if objects overlap secondary zones, treat it as a + // validation error so the standard pathway handles button state, notifications, + // and auto-slice blocking consistently. + if (err.string.empty()) { + PartPlate* ixex_plate = partplate_list.get_curr_plate(); + if (ixex_plate && ixex_plate->has_ixex_placement_violations()) + err.string = _u8L("Cannot slice: objects are in secondary zones reserved for iXex parallel printing."); + } + if (err.string.empty()) { this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); notification_manager->set_all_slicing_errors_gray(true); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c94fd1c7a4..d19800a376 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -45,6 +45,7 @@ #include "Widgets/Label.hpp" #include "Widgets/TabCtrl.hpp" +#include "Widgets/ComboBox.hpp" #include "MarkdownTip.hpp" #include "Search.hpp" #include "BedShapeDialog.hpp" @@ -2673,6 +2674,26 @@ void TabPrint::build() optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); optgroup->append_single_option_line("enable_wrapping_detection"); + // Dynamic combo box — choices come from the printer preset's ixex_mode_names. + // Use create_line_with_widget so the undo bitmaps are set (has_undo_ui()=true), + // ensuring the widget x-position matches other option fields in OG_CustomCtrl. + create_line_with_widget(optgroup.get(), "ixex_parallel_mode", "", [this](wxWindow* parent) -> wxSizer* { + m_ixex_mode_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(12 * wxGetApp().em_unit(), -1), + 0, nullptr, wxCB_READONLY); + m_ixex_mode_combo->GetDropDown().SetUseContentWidth(true); + refresh_ixex_mode_combo(); + m_ixex_mode_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + if (!m_ixex_mode_combo || !m_config) return; + std::string val = m_ixex_mode_combo->GetStringSelection().ToStdString(); + m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString(val)); + on_value_change("ixex_parallel_mode", val); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_ixex_mode_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin"); @@ -2731,6 +2752,36 @@ void TabPrint::reload_config() { this->compatible_widget_reload(m_compatible_printers); Tab::reload_config(); + refresh_ixex_mode_combo(); +} + +void TabPrint::refresh_ixex_mode_combo() +{ + if (!m_ixex_mode_combo) return; + // Rebuild choices: "primary" + names from the active printer preset + m_ixex_mode_combo->Clear(); + m_ixex_mode_combo->Append("primary"); + if (m_preset_bundle) { + auto* names = m_preset_bundle->printers.get_edited_preset().config + .option("ixex_mode_names"); + if (names) { + for (const auto& n : names->values) + if (!n.empty()) m_ixex_mode_combo->Append(wxString::FromUTF8(n)); + } + } + // Restore current selection from process config (safe: option may not exist in older presets) + std::string cur = "primary"; + if (m_config) { + auto* opt = m_config->option("ixex_parallel_mode"); + if (opt) cur = opt->value; + } + if (!m_ixex_mode_combo->SetStringSelection(cur)) { + m_ixex_mode_combo->SetSelection(0); // fall back to "primary" + // The active mode no longer exists — reset config so stale name doesn't crash downstream. + if (m_config) + m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString("primary")); + on_value_change("ixex_parallel_mode", std::string("primary")); + } } void TabPrint::update_description_lines() @@ -4335,6 +4386,324 @@ void TabPrinter::build() m_printer_technology == ptSLA ? build_sla() : build_fff(); } +// --------------------------------------------------------------------------- +// IXexModesCtrl — visual parallel-mode editor for the iXex printer tab +// +// Tool button states: +// 0 = Inactive (grey) — not participating in this mode +// 1 = Primary (green) — the tool the slicer generates paths for +// 2 = Copy (blue) — firmware duplicates Primary at an offset +// 3 = Mirror (amber) — firmware mirrors Primary about an axis +// +// Active-tools string format: "idx:P,idx:C,idx:M" (backwards-compat: plain "idx" = Primary) +// --------------------------------------------------------------------------- +class IXexModesCtrl : public wxPanel { +public: + std::function on_change; + + // layout: 0=front-left, 1=front-right, 2=rear-left, 3=rear-right (T0 corner) + static int parse_layout(const std::string& s) { + if (s == "front-right") return 1; + if (s == "rear-left") return 2; + if (s == "rear-right") return 3; + return 0; // "front-left" default + } + + IXexModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0) + : wxPanel(parent, wxID_ANY), m_n_cols(std::max(1, n_cols)), m_n_rows(std::max(1, n_rows)), m_layout(layout) + { + m_outer = new wxBoxSizer(wxVERTICAL); + m_rows_sizer = new wxBoxSizer(wxVERTICAL); + + // --- Info panel: instruction text + color legend --- + auto* info_panel = new wxPanel(this, wxID_ANY); + auto* info_sizer = new wxBoxSizer(wxVERTICAL); + + auto* inst = new wxStaticText(info_panel, wxID_ANY, + _L("Each mode defines which tool heads participate and their roles. " + "The Primary tool (green) drives all sliced paths. " + "Copy (blue) and Mirror (amber) tools follow the Primary at the firmware level. " + "Click a tool button to cycle its role — clear the Primary before reassigning it.")); + inst->Wrap(620); + info_sizer->Add(inst, 0, wxBOTTOM, 6); + + // Color legend + auto* leg_sizer = new wxBoxSizer(wxHORIZONTAL); + struct { int state; const char* label; } legend[] = { + {1,"Primary"},{2,"Copy"},{3,"Mirror"} + }; + for (auto& l : legend) { + auto* swatch = new wxPanel(info_panel, wxID_ANY, wxDefaultPosition, wxSize(12, 12)); + swatch->SetBackgroundColour(btn_color(l.state)); + leg_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); + leg_sizer->Add(new wxStaticText(info_panel, wxID_ANY, l.label), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 16); + } + info_sizer->Add(leg_sizer, 0, wxBOTTOM, 6); + info_panel->SetSizer(info_sizer); + + // Column header row (fixed-pixel spacers to approximately align with mode row fields) + // Name field: 130px + 6px gap; Tool grid: n_cols*(36+2)-2 px + 6px gap + auto* hdr_panel = new wxPanel(this, wxID_ANY); + auto* hdr_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* hdr_name = new wxStaticText(hdr_panel, wxID_ANY, _L("Name")); + auto* hdr_tools = new wxStaticText(hdr_panel, wxID_ANY, _L("Tools")); + auto* hdr_gcode = new wxStaticText(hdr_panel, wxID_ANY, _L("G-code")); + int grid_px = m_n_cols * 38 - 2; // approx grid panel width + hdr_sizer->Add(hdr_name, 0, wxALIGN_CENTER_VERTICAL); + hdr_sizer->AddSpacer(std::max(0, 136 - hdr_name->GetBestSize().x)); // "Name" col = 136px total + hdr_sizer->Add(hdr_tools, 0, wxALIGN_CENTER_VERTICAL); + hdr_sizer->AddSpacer(std::max(0, grid_px + 6 - hdr_tools->GetBestSize().x)); // "Tools" col = grid_px+6 total + hdr_sizer->Add(hdr_gcode, 1, wxALIGN_CENTER_VERTICAL); + hdr_panel->SetSizer(hdr_sizer); + + auto* add_btn = new wxButton(this, wxID_ANY, _L("+ Add Mode"), + wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); + add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_row(); notify(); }); + + m_outer->Add(info_panel, 0, wxEXPAND | wxBOTTOM, 4); + m_outer->Add(hdr_panel, 0, wxEXPAND | wxBOTTOM, 2); + m_outer->Add(m_rows_sizer, 0, wxEXPAND); + m_outer->AddSpacer(4); + m_outer->Add(add_btn, 0); + SetSizer(m_outer); + } + + void set_grid_size(int n_cols, int n_rows, int layout = -1) { + n_cols = std::max(1, n_cols); + n_rows = std::max(1, n_rows); + if (layout < 0) layout = m_layout; + if (n_cols == m_n_cols && n_rows == m_n_rows && layout == m_layout) return; + auto [names, tools, gcodes] = get_mode_data(); + clear_rows(); + m_n_cols = n_cols; + 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]); + Layout(); + } + + void load_from_config(const DynamicPrintConfig& cfg) { + clear_rows(); + auto* names = cfg.option("ixex_mode_names"); + auto* tools = cfg.option("ixex_mode_active_tools"); + auto* gcodes = cfg.option("ixex_mode_gcodes"); + size_t n = names ? names->values.size() : 0; + for (size_t i = 0; i < n; ++i) + add_row(names->values[i], + (tools && i < tools->values.size()) ? tools->values[i] : "", + (gcodes && i < gcodes->values.size()) ? gcodes->values[i] : ""); + Layout(); + } + + std::tuple, std::vector, std::vector> + get_mode_data() const { + std::vector names, tools, gcodes; + for (auto& r : m_rows) { + std::string nm = r.name->GetValue().ToStdString(); + if (nm.empty()) continue; + names.push_back(nm); + tools.push_back(active_tools_string(r)); + gcodes.push_back(r.gcode->GetValue().ToStdString()); + } + return {names, tools, gcodes}; + } + + // Parse "idx:P,idx:C,idx:M" → map + // Backwards compat: plain "idx" (no role) → state 1 (Primary) + static std::map parse_tool_states(const std::string& s) { + std::map result; + if (s.empty()) return result; + std::istringstream ss(s); + std::string tok; + while (std::getline(ss, tok, ',')) { + tok.erase(std::remove_if(tok.begin(), tok.end(), ::isspace), tok.end()); + if (tok.empty()) continue; + try { + auto colon = tok.find(':'); + int idx, state = 1; + if (colon != std::string::npos) { + idx = std::stoi(tok.substr(0, colon)); + char role = std::toupper((unsigned char)tok[colon + 1]); + if (role == 'C') state = 2; + else if (role == 'M') state = 3; + } else { + idx = std::stoi(tok); + } + result[idx] = state; + } catch (...) {} + } + return result; + } + +private: + // State 0=inactive, 1=primary, 2=copy, 3=mirror + static wxColour btn_color(int state) { + switch (state) { + case 1: return wxColour(50, 160, 50); // green — Primary + case 2: return wxColour(60, 120, 210); // blue — Copy + case 3: return wxColour(210, 130, 20); // amber — Mirror + default: return wxColour(90, 90, 90); // grey — Inactive + } + } + static wxColour btn_fg(int /*state*/) { return *wxWHITE; } + static const char* state_role(int state) { + switch (state) { case 1: return "P"; case 2: return "C"; case 3: return "M"; default: return ""; } + } + + void apply_btn(wxButton* btn, int tool_idx, int state) { + // Always label as T{n} — the button color already encodes the role. + btn->SetLabel(wxString::Format("T%d", tool_idx)); + btn->SetBackgroundColour(btn_color(state)); + btn->SetForegroundColour(btn_fg(state)); + btn->Refresh(); + } + + struct Row { + wxPanel* panel; + wxTextCtrl* name; + 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_tool_idx; + }; + + void add_row(const std::string& name = "", + const std::string& active_tools = "", + const std::string& gcode = "") + { + Row r; + 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(); }); + + 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); + wxPanel* this_panel = r.panel; + + // Buttons rendered top=rear (high Y), bottom=front (low Y). + // Layout remapping: m_layout encodes which corner T0 is at physically. + // flip_x (layout 1,3): col 0 is on the right (max-X) instead of left + // flip_y (layout 2,3): row 0 is at the rear (max-Y) instead of front + // Display iterates: row from n_rows-1 down to 0 (rear→front = top→bottom). + // phys_row = row (loop var maps directly to physical bed Y index). + // tool raw index: raw_row = flip_y ? (n_rows-1-row) : row + // raw_col = flip_x ? (n_cols-1-col) : col + bool flip_x = (m_layout == 1 || m_layout == 3); + bool flip_y = (m_layout == 2 || m_layout == 3); + for (int row = m_n_rows - 1; row >= 0; --row) { + for (int col = 0; col < m_n_cols; ++col) { + int raw_row = flip_y ? (m_n_rows - 1 - row) : row; + int raw_col = flip_x ? (m_n_cols - 1 - col) : col; + int tool_idx = raw_row * m_n_cols + raw_col; + int state = 0; + auto it = tool_states.find(tool_idx); + if (it != tool_states.end()) state = it->second; + + auto* btn = new wxButton(grid_panel, wxID_ANY, wxEmptyString, + wxDefaultPosition, wxSize(36, 26), wxBU_EXACTFIT); + apply_btn(btn, tool_idx, state); + + int btn_pos = (int)r.btns.size(); + r.btns.push_back(btn); + r.btn_states.push_back(state); + r.btn_tool_idx.push_back(tool_idx); + + btn->Bind(wxEVT_BUTTON, [this, this_panel, btn_pos](wxCommandEvent&) { + // Find the row by panel pointer (stable across add/remove) + for (auto& row_ref : m_rows) { + if (row_ref.panel != this_panel) continue; + int& st = row_ref.btn_states[btn_pos]; + + // Check if another button already holds the Primary state + bool other_primary = false; + for (int j = 0; j < (int)row_ref.btn_states.size(); ++j) + if (j != btn_pos && row_ref.btn_states[j] == 1) { other_primary = true; break; } + + // Advance state, skipping Primary(1) if another tool is already Primary + st = (st + 1) % 4; + if (st == 1 && other_primary) + st = 2; // skip Primary → go straight to Copy + + apply_btn(row_ref.btns[btn_pos], row_ref.btn_tool_idx[btn_pos], st); + notify(); + break; + } + }); + + grid_sizer->Add(btn, 0); + } + } + grid_panel->SetSizer(grid_sizer); + + r.gcode = new wxTextCtrl(r.panel, wxID_ANY, gcode, + 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); + 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); + r.panel->SetSizer(sizer); + + m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, 4); + m_rows.push_back(std::move(r)); + Layout(); + } + + void remove_row(wxPanel* panel) { + for (size_t i = 0; i < m_rows.size(); ++i) { + if (m_rows[i].panel == panel) { + m_rows_sizer->Detach(panel); + m_rows.erase(m_rows.begin() + i); + Layout(); + // Defer widget destruction so any in-flight GTK events for + // panel's children (including the × button we're inside) finish + // processing before the wxEvtHandlers are freed. + wxTheApp->CallAfter([panel]() { panel->Destroy(); }); + return; + } + } + } + + void clear_rows() { + for (auto& r : m_rows) { m_rows_sizer->Detach(r.panel); r.panel->Destroy(); } + m_rows.clear(); + } + + std::string active_tools_string(const Row& r) const { + std::string s; + for (size_t i = 0; i < r.btn_states.size(); ++i) { + if (r.btn_states[i] == 0) continue; + if (!s.empty()) s += ','; + s += std::to_string(r.btn_tool_idx[i]) + ':' + state_role(r.btn_states[i]); + } + return s; + } + + void notify() { if (on_change) on_change(); } + + wxBoxSizer* m_outer; + wxBoxSizer* m_rows_sizer; + std::vector m_rows; + int m_n_cols, m_n_rows; + int m_layout {0}; // 0=front-left, 1=front-right, 2=rear-left, 3=rear-right +}; +// --------------------------------------------------------------------------- + void TabPrinter::build_fff() { if (!m_pages.empty()) @@ -4589,6 +4958,117 @@ void TabPrinter::build_fff() option.opt.height = gcode_field_height;//150; optgroup->append_single_option_line(option, "printer_machine_gcode#template-custom-g-code"); + page = add_options_page(L("iXex"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders + optgroup = page->new_optgroup(L("Carriage Configuration"), L"param_advanced"); + optgroup->append_single_option_line("is_ixex"); + optgroup->append_single_option_line("ixex_gantry_count"); + optgroup->append_single_option_line("ixex_tools_per_gantry"); + { + // Tool 0 corner selector + auto opt = optgroup->get_option("ixex_tool_layout"); + auto line = Line{ opt.opt.label, opt.opt.tooltip }; + line.append_option(opt); + line.widget = [this](wxWindow* parent) -> wxSizer* { + static const wxString choices[] = { + _L("Front-left"), _L("Front-right"), + _L("Rear-left"), _L("Rear-right") + }; + static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" }; + m_ixex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(12 * wxGetApp().em_unit(), -1), + 4, choices, wxCB_READONLY); + m_ixex_layout_combo->GetDropDown().SetUseContentWidth(true); + // Set current selection + std::string cur = "front-left"; + if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == values[i]) { m_ixex_layout_combo->SetSelection(i); break; } + m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this, values](wxCommandEvent&) { + int sel = m_ixex_layout_combo->GetSelection(); + std::string val = (sel >= 0 && sel < 4) ? values[sel] : "front-left"; + m_config->set_key_value("ixex_tool_layout", new ConfigOptionString(val)); + on_value_change("ixex_tool_layout", val); + if (m_ixex_modes_ctrl) + m_ixex_modes_ctrl->set_grid_size( + m_config->opt_int("ixex_tools_per_gantry"), + m_config->opt_int("ixex_gantry_count"), + IXexModesCtrl::parse_layout(val)); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_ixex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }; + optgroup->append_line(line); + } + optgroup->append_single_option_line("ixex_carriage_width_x"); + optgroup->append_single_option_line("ixex_carriage_width_y"); + optgroup->append_single_option_line("ixex_carriage_margin"); + + { + static const wxString theme_choices[] = { + _L("Standard"), + _L("Deuteranopia / Protanopia (red-green)"), + _L("Tritanopia (blue-yellow)"), + _L("High Contrast") }; + static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; + auto opt = optgroup->get_option("ixex_viz_theme"); + auto line = Line{ opt.opt.label, opt.opt.tooltip }; + line.append_option(opt); + line.widget = [this](wxWindow* parent) -> wxSizer* { + m_ixex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(22 * wxGetApp().em_unit(), -1), + 4, theme_choices, wxCB_READONLY); + m_ixex_theme_combo->GetDropDown().SetUseContentWidth(true); + std::string cur = "standard"; + if (auto* o = m_config->option("ixex_viz_theme")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; } + m_ixex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + int sel = m_ixex_theme_combo->GetSelection(); + std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard"; + m_config->set_key_value("ixex_viz_theme", new ConfigOptionString(val)); + on_value_change("ixex_viz_theme", val); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_ixex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }; + optgroup->append_line(line); + } + + { + // Parallel modes panel: visual toggle-grid list with + button + auto modes_og = page->new_optgroup(L("Parallel Modes"), L"param_advanced"); + auto line = Line{ L("Modes"), L("") }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) -> wxSizer* { + int n_cols = m_config->opt_int("ixex_tools_per_gantry"); + int n_rows = m_config->opt_int("ixex_gantry_count"); + std::string layout_str = "front-left"; + if (auto* o = m_config->option("ixex_tool_layout")) layout_str = o->value; + int layout = IXexModesCtrl::parse_layout(layout_str); + m_ixex_modes_ctrl = new IXexModesCtrl(parent, n_cols, n_rows, layout); + m_ixex_modes_ctrl->load_from_config(*m_config); + m_ixex_modes_ctrl->on_change = [this]() { + auto [names, tools, gcodes] = m_ixex_modes_ctrl->get_mode_data(); + m_config->set_key_value("ixex_mode_names", new ConfigOptionStrings(names)); + m_config->set_key_value("ixex_mode_active_tools", new ConfigOptionStrings(tools)); + m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); + update_dirty(); + on_value_change("ixex_mode_names", std::string("")); + // Refresh the process tab's mode dropdown immediately + if (auto* print_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINT))) + print_tab->refresh_ixex_mode_combo(); + }; + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); + return sizer; + }; + modes_og->append_line(line); + } + page = add_options_page(L("Notes"), "custom-gcode_note"); // ORCA: icon only visible on placeholders optgroup = page->new_optgroup(L("Notes"), "note", 0); option = optgroup->get_option("printer_notes"); @@ -5168,6 +5648,33 @@ void TabPrinter::reload_config() // so update it implicitly if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); + + if (m_config) { + // Sync layout combo + if (m_ixex_layout_combo) { + static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; + std::string cur = "front-left"; + if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == layout_vals[i]) { m_ixex_layout_combo->SetSelection(i); break; } + } + // Sync theme combo + if (m_ixex_theme_combo) { + static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; + std::string cur = "standard"; + if (auto* o = m_config->option("ixex_viz_theme")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; } + } + if (m_ixex_modes_ctrl) { + int n_cols = m_config->opt_int("ixex_tools_per_gantry"); + int n_rows = m_config->opt_int("ixex_gantry_count"); + std::string layout_str = "front-left"; + if (auto* o = m_config->option("ixex_tool_layout")) layout_str = o->value; + m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str)); + m_ixex_modes_ctrl->load_from_config(*m_config); + } + } } void TabPrinter::activate_selected_page(std::function throw_if_canceled) @@ -5184,6 +5691,9 @@ void TabPrinter::clear_pages() { Tab::clear_pages(); m_reset_to_filament_color = nullptr; + m_ixex_modes_ctrl = nullptr; + m_ixex_layout_combo = nullptr; + m_ixex_theme_combo = nullptr; } void TabPrinter::toggle_options() @@ -5393,6 +5903,15 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } + // Sync iXex tool grid whenever carriage configuration changes. + // set_grid_size() is a no-op when dimensions and layout are unchanged. + if (m_ixex_modes_ctrl) { + int n_cols = m_config->opt_int("ixex_tools_per_gantry"); + int n_rows = m_config->opt_int("ixex_gantry_count"); + std::string layout_str = m_config->opt_string("ixex_tool_layout"); + m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str)); + } + toggle_options(); } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 96b35b26e1..687cfeea73 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -41,6 +41,7 @@ #include "Widgets/RoundedRectangle.hpp" #include "Widgets/TextInput.hpp" #include "Widgets/CheckBox.hpp" // ORCA +#include "Widgets/ComboBox.hpp" class TabCtrl; @@ -463,10 +464,12 @@ public: void update() override; void clear_pages() override; bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } + void refresh_ixex_mode_combo(); private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr; + ComboBox* m_ixex_mode_combo {nullptr}; }; class TabPrintModel : public TabPrint @@ -588,6 +591,8 @@ public: void set_custom_gcode(const t_config_option_key& opt_key, const std::string& value) override; }; +class IXexModesCtrl; + class TabPrinter : public Tab { private: @@ -602,6 +607,9 @@ private: std::vector m_pages_sla; wxBoxSizer* m_presets_sizer {nullptr}; + IXexModesCtrl* m_ixex_modes_ctrl {nullptr}; + ComboBox* m_ixex_layout_combo {nullptr}; + ComboBox* m_ixex_theme_combo {nullptr}; public: ScalableButton* m_reset_to_filament_color = nullptr; From e786fabb6a0eaa022273e6d462fd815811a77b40 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 03:02:08 -0400 Subject: [PATCH 002/154] fix: restore info log levels in PartPlate accidentally raised to warning Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/PartPlate.cpp | 86 ++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 0d0429db3c..1fb2b80d5b 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -2373,7 +2373,7 @@ bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config) if (filament_type == "PETG") has_petg = true; } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " check error:array bound"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " check error:array bound"; } } } @@ -2638,7 +2638,7 @@ arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const Dynamic if (wipe_tower_brim_width_opt) { wp_brim_width = wipe_tower_brim_width_opt->getFloat(); if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wt_size.z()); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width; } x = std::clamp(x, margin, (float)plate_width - w - margin - wp_brim_width); @@ -2702,9 +2702,9 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height bool size_changed = false; //size changed means the machine changed bool pos_changed = false; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate_id %1%, before, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ % m_plate_index % m_origin.x() % m_origin.y() % m_origin.z() % m_width % m_depth % m_height; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": with_instance_move %1%, after, origin {%2%,%3%,%4%}, plate_width %5%, plate_depth %6%, plate_height %7%")\ % with_instance_move % origin.x() % origin.y() % origin.z() % width % depth % height; size_changed = ((width != m_width) || (depth != m_depth) || (height != m_height)); pos_changed = (m_origin != origin); @@ -2744,7 +2744,7 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height offset.x() = offset.x() + off_x; offset.y() = offset.y() + off_y; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": object %1%, instance %2%, moved {%3%,%4%} to {%5%, %6%}")\ % obj_id % instance_id % off_x % off_y % offset.x() % offset.y(); instance->set_offset(offset); @@ -2881,7 +2881,7 @@ bool PartPlate::is_valid_gcode_file() return false; boost::filesystem::path gcode_file(m_gcode_result->filename); if (!boost::filesystem::exists(gcode_file)) { - BOOST_LOG_TRIVIAL(warning) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename; + BOOST_LOG_TRIVIAL(info) << "invalid gcode file, file is missing, file = " << m_gcode_result->filename; return false; } return true; @@ -3122,7 +3122,7 @@ int PartPlate::add_instance(int obj_id, int instance_id, bool move_position, Bou m_ready_for_slice = true; } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; return 0; } @@ -3216,7 +3216,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s if (skip_objects.find(instance->loaded_id) != skip_objects.end()) { instance->printable = false; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skipped object, loaded_id %1%, name %2%, set to unprintable, no need to duplicate") % instance->loaded_id % object->name; continue; } } @@ -3228,7 +3228,7 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s for ( size_t new_instance_id = 0; new_instance_id < newObj->instances.size(); new_instance_id++ ) { obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id)); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % newObj->id().id; } } } @@ -3251,10 +3251,10 @@ void PartPlate::duplicate_all_instance(unsigned int dup_count, bool need_skip, s while (skip_objects.find(instance->loaded_id) != skip_objects.end()) { instance->loaded_id ++; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicated id %1% with skip, try new one %2%") %instance->id().id % instance->loaded_id; } } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set obj %1% instance %2%'s loaded_id to its id %3%, name %4%") % obj_id %instance_id %instance->loaded_id % object->name; } } } @@ -3576,7 +3576,7 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co if ((m_shape == new_shape)&&(m_exclude_area == new_exclude_areas) &&(m_height_to_lid == height_to_lid)&&(m_height_to_rod == height_to_rod)) { - BOOST_LOG_TRIVIAL(warning) << "PartPlate same shape, skip directly"; + BOOST_LOG_TRIVIAL(info) << "PartPlate same shape, skip directly"; return false; } @@ -3809,7 +3809,7 @@ void PartPlate::update_states() } } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice; return; } @@ -3817,7 +3817,7 @@ void PartPlate::update_states() //invalid sliced result void PartPlate::update_slice_result_valid_state(bool valid) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , update slice result from %2% to %3%") % m_plate_index %m_slice_result_valid %valid; m_slice_result_valid = valid; if (valid) m_slice_percent = 100.0f; @@ -3906,7 +3906,7 @@ int PartPlate::load_gcode_from_file(const std::string& filename) update_slice_result_valid_state(true); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found valid gcode file %1%") % filename.c_str(); } else { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find gcode file %1%") % filename.c_str(); @@ -4237,7 +4237,7 @@ PartPlateList::PartPlateList(int width, int depth, int height, Plater* platerObj :m_plate_width(width), m_plate_depth(depth), m_plate_height(height), m_plater(platerObj), m_model(modelObj), printer_technology(tech), unprintable_plate(this, Vec3d(0.0 + width * (1. + LOGICAL_PART_PLATE_GAP), 0.0, 0.0), width, depth, height, platerObj, modelObj, false, tech) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; init(); } @@ -4658,8 +4658,8 @@ void PartPlateList::reset_size(int width, int depth, int height, bool reload_obj { Vec3d origin1, origin2; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; if ((m_plate_width != width) || (m_plate_depth != depth) || (m_plate_height != height)) { m_plate_width = width; @@ -4875,7 +4875,7 @@ int PartPlateList::duplicate_plate(int index) // go over the instances and pair with the object for (size_t new_instance_id = 0; new_instance_id < object_copy->instances.size(); new_instance_id++){ new_plate->obj_to_instance_set.emplace(std::pair(new_obj_id, new_instance_id)); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": duplicate object into plate: index_pair [%1%,%2%], obj_id %3%") % new_obj_id % new_instance_id % object_copy->id().id; } } new_plate->translate_all_instance(plate_to_plate_offset); @@ -5238,7 +5238,7 @@ void PartPlateList::update_plate_cols() m_plate_count = m_plate_list.size(); m_plate_cols = compute_colum_count(m_plate_count); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_plate_count %1%, m_plate_cols change to %2%") % m_plate_count % m_plate_cols; return; } @@ -5319,7 +5319,7 @@ int PartPlateList::lock_plate(int index, bool state) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(":can not get plate for index %1%, size %2%") % index % m_plate_list.size(); return -1; } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":lock plate %1%, to state %2%") % index % state; plate->lock(state); @@ -6237,7 +6237,7 @@ Print& PartPlateList::get_current_fff_print() const Print* print; current_plate = m_plate_list[m_current_plate]; - //BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; + //BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; assert(current_plate != NULL); current_plate->get_print((PrintBase **)&print, nullptr, nullptr); @@ -6251,7 +6251,7 @@ GCodeProcessorResult* PartPlateList::get_current_slice_result() const PartPlate* current_plate; current_plate = m_plate_list[m_current_plate]; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":m_current_plate %1%, current_plate %2%") % m_current_plate % current_plate; assert(current_plate != NULL); return current_plate->get_slice_result(); @@ -6456,7 +6456,7 @@ int PartPlateList::rebuild_plates_after_deserialize(std::vector& previous_ //not used /*if (m_plate_width == 0) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!"); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": jump to the first init state, need to re-set size!"); Vec3d max = m_plater->get_bed().get_bounding_box(false).max; Vec3d min = m_plater->get_bed().get_bounding_box(false).min; double z = m_plater->config()->opt_float("printable_height"); @@ -6470,7 +6470,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex { int ret = 0; - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before rebuild, plates count %1%, recycle_plates %2%") % m_plate_list.size() % recycle_plates; // sort by arrange_order std::sort(m_model->objects.begin(), m_model->objects.end(), [](auto a, auto b) {return a->instances[0]->arrange_order < b->instances[0]->arrange_order; }); @@ -6487,7 +6487,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex || !m_plate_list[i]->has_printable_instances()) { //delete it - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":delete plate %1% for empty") % i; delete_plate(i); } else if (m_plate_list[i]->is_locked()) { @@ -6507,7 +6507,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex } #endif - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size(); return ret; } @@ -6525,10 +6525,10 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->locked = m_plate_list[i]->m_locked; plate_data_item->plate_index = m_plate_list[i]->m_plate_index; plate_data_item->plate_name = m_plate_list[i]->get_plate_name(); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!") + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% before load, width %2%, height %3%, size %4%!") %(i+1) %m_plate_list[i]->thumbnail_data.width %m_plate_list[i]->thumbnail_data.height %m_plate_list[i]->thumbnail_data.pixels.size(); plate_data_item->plate_thumbnail.load_from(m_plate_list[i]->thumbnail_data); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!") + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% after load, width %2%, height %3%, size %4%!") %(i+1) %plate_data_item->plate_thumbnail.width %plate_data_item->plate_thumbnail.height %plate_data_item->plate_thumbnail.pixels.size(); plate_data_item->config.apply(*m_plate_list[i]->config()); @@ -6545,7 +6545,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->objects_and_instances.emplace_back(it->first, it->second); } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ <m_gcode_result->filename % with_slice_info %m_plate_list[i]->is_slice_result_valid()%plate_data_item->objects_and_instances.size(); if (with_slice_info) { @@ -6578,12 +6578,12 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w } plate_data_item->is_support_used = print->is_support_used(); } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("print is null!"); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("print is null!"); } //parse filament info plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result()); } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() << ", result valid = " << m_plate_list[i]->is_slice_result_valid(); } } @@ -6591,7 +6591,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_list.push_back(plate_data_item); } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":stored %1% plates!") % m_plate_list.size(); return ret; } @@ -6617,7 +6617,7 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate index %1% seems invalid, skip it")% plate_data_list[i]->plate_index; } - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%") + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, gcode_file %2%, is_sliced_valid %3%, toolpath_outside %4%, is_support_used %5% is_label_object_enabled %6%") %i %plate_data_list[i]->gcode_file %plate_data_list[i]->is_sliced_valid %plate_data_list[i]->toolpath_outside %plate_data_list[i]->is_support_used %plate_data_list[i]->is_label_object_enabled; //load object and instance from 3mf //just test for file correct or not, we will rebuild later @@ -6646,17 +6646,17 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f gcode_result->warnings = plate_data_list[i]->warnings; gcode_result->filament_maps = plate_data_list[i]->filament_maps; if (m_plater && !plate_data_list[i]->thumbnail_file.empty()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; if (boost::filesystem::exists(plate_data_list[i]->thumbnail_file)) { m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->thumbnail_file, m_plate_list[index]->thumbnail_data); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ <thumbnail_data.width %m_plate_list[index]->thumbnail_data.height %m_plate_list[index]->thumbnail_data.pixels.size(); } } if (m_plater && !plate_data_list[i]->no_light_thumbnail_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->no_light_thumbnail_file)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load no_light_thumbnail_file from %2%.")%(i+1) %plate_data_list[i]->no_light_thumbnail_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->no_light_thumbnail_file, m_plate_list[index]->no_light_thumbnail_data); } } @@ -6669,13 +6669,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f }*/ if (m_plater && !plate_data_list[i]->top_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->top_file)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load top_thumbnail from %2%.")%(i+1) %plate_data_list[i]->top_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->top_file, m_plate_list[index]->top_thumbnail_data); } } if (m_plater && !plate_data_list[i]->pick_file.empty()) { if (boost::filesystem::exists(plate_data_list[i]->pick_file)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load pick_thumbnail from %2%.")%(i+1) %plate_data_list[i]->pick_file; m_plate_list[index]->load_thumbnail_data(plate_data_list[i]->pick_file, m_plate_list[index]->pick_thumbnail_data); } } @@ -7111,7 +7111,7 @@ void PartPlateList::on_extruder_count_changed(int extruder_count) for (unsigned int i = 0; i < (unsigned int) m_plate_list.size(); ++i) { m_plate_list[i]->on_extruder_count_changed(extruder_count); } - BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: extruder_count=%2%")% __FUNCTION__ %extruder_count; } void PartPlateList::set_filament_count(int filament_count) @@ -7121,7 +7121,7 @@ void PartPlateList::set_filament_count(int filament_count) { m_plate_list[i]->set_filament_count(filament_count); } - BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; } void PartPlateList::on_filament_added(int filament_count) @@ -7131,7 +7131,7 @@ void PartPlateList::on_filament_added(int filament_count) { m_plate_list[i]->on_filament_added(); } - BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%")% __FUNCTION__ %filament_count; } void PartPlateList::on_filament_deleted(int filament_count, int filament_id) @@ -7141,7 +7141,7 @@ void PartPlateList::on_filament_deleted(int filament_count, int filament_id) { m_plate_list[i]->on_filament_deleted(filament_count, filament_id); } - BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: filament_count=%2%, filament_id=%3%")% __FUNCTION__ %filament_count %filament_id; } }//end namespace GUI From c75231ec03871e1474cb27f503667f0f2b39e5ed Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 03:06:48 -0400 Subject: [PATCH 003/154] fix: remove spurious blank lines in PrintConfig.hpp Co-Authored-By: Claude Sonnet 4.6 --- src/libslic3r/PrintConfig.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index a32c773517..6c70772cda 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -63,7 +63,6 @@ enum class WipeTowerType { Type2, }; - enum PrintHostType { htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink }; @@ -497,7 +496,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType) - CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) From a03d2cd566b3bd43c7505ec8ef1d4dd69ff8d46b Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 03:11:17 -0400 Subject: [PATCH 004/154] fix: restore info log level in two commented-out PartPlate log lines Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/PartPlate.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 1fb2b80d5b..6f00eb69f3 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3889,13 +3889,13 @@ int PartPlate::load_gcode_from_file(const std::string& filename) // BBS: use backup path to save temp gcode // auto path = get_tmp_gcode_path(); // if (boost::filesystem::exists(boost::filesystem::path(path))) { - // BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str(); + // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": file %1% exists, delete it firstly") % filename.c_str(); // boost::nowide::remove(path.c_str()); //} // std::error_code error = rename_file(filename, path); // if (error) { - // BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() % + // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("Failed to rename the output G-code file from %1% to %2%, error code %3%") % filename.c_str() % path.c_str() % //error.message(); return -1; //} if (boost::filesystem::exists(filename)) { From 52974175186df0bd033a702510e76d3b64113c68 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 03:15:56 -0400 Subject: [PATCH 005/154] fix: remove phantom phys_row variable reference from iXex button layout comment Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index d19800a376..f8ab45eac2 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4592,9 +4592,8 @@ private: // flip_x (layout 1,3): col 0 is on the right (max-X) instead of left // flip_y (layout 2,3): row 0 is at the rear (max-Y) instead of front // Display iterates: row from n_rows-1 down to 0 (rear→front = top→bottom). - // phys_row = row (loop var maps directly to physical bed Y index). - // tool raw index: raw_row = flip_y ? (n_rows-1-row) : row - // raw_col = flip_x ? (n_cols-1-col) : col + // raw_row = flip_y ? (n_rows-1-row) : row + // raw_col = flip_x ? (n_cols-1-col) : col bool flip_x = (m_layout == 1 || m_layout == 3); bool flip_y = (m_layout == 2 || m_layout == 3); for (int row = m_n_rows - 1; row >= 0; --row) { From f4808ae7babe590d2bbc3dff47ac3d4bb6a3dafe Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 2 Apr 2026 13:38:19 -0400 Subject: [PATCH 006/154] fix: avoid capturing C array in lambda for ixex_tool_layout combo Arrays cannot be captured by value in C++ lambdas; replaced with a static local declared inside the event handler. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f8ab45eac2..9845867735 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4983,9 +4983,10 @@ void TabPrinter::build_fff() if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; for (int i = 0; i < 4; ++i) if (cur == values[i]) { m_ixex_layout_combo->SetSelection(i); break; } - m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this, values](wxCommandEvent&) { + m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; int sel = m_ixex_layout_combo->GetSelection(); - std::string val = (sel >= 0 && sel < 4) ? values[sel] : "front-left"; + std::string val = (sel >= 0 && sel < 4) ? layout_vals[sel] : "front-left"; m_config->set_key_value("ixex_tool_layout", new ConfigOptionString(val)); on_value_change("ixex_tool_layout", val); if (m_ixex_modes_ctrl) From 185b7084d2a88a261443e833631455d630e5b5fe Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Fri, 3 Apr 2026 23:31:21 -0400 Subject: [PATCH 007/154] fix: null m_ixex_mode_combo in TabPrint::clear_pages() to prevent dangling pointer crash Same class of bug as the TabPrinter fix: clear_pages() destroys all page widgets but didn't null m_ixex_mode_combo, causing a SIGSEGV in refresh_ixex_mode_combo() when load_current_preset() ran after a page rebuild (e.g. triggered by Plater::reset()). Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 9845867735..dd1d6d84f9 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2903,6 +2903,7 @@ void TabPrint::clear_pages() m_recommended_thin_wall_thickness_description_line = nullptr; m_top_bottom_shell_thickness_explanation = nullptr; + m_ixex_mode_combo = nullptr; } //BBS: GUI refactor From a3cac6c139fbb35a3eccc62dda604428864dc82f Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Sun, 5 Apr 2026 13:18:32 -0400 Subject: [PATCH 008/154] fix: remove iXex zone outline borders to eliminate aliasing flicker The thin rectangular outlines around copy/mirror zones caused visible aliasing and appeared to flash during interaction. Removed the border GLModel, its build code in calc_ixex_zones(), the render block, and the unused border field from IXexTheme. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/PartPlate.cpp | 32 -------------------------------- src/slic3r/GUI/PartPlate.hpp | 3 +-- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 6f00eb69f3..09db0bb072 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -460,7 +460,6 @@ void PartPlate::calc_ixex_zones() { m_ixex_copy_zones.clear(); m_ixex_mirror_zones.clear(); - m_ixex_zone_borders.reset(); // Clear blocking vectors at the top so early returns don't leave stale data. m_ixex_secondary_zone_boxes.clear(); m_ixex_collision_zones.clear(); @@ -638,26 +637,6 @@ void PartPlate::calc_ixex_zones() push_expanded_fills(m_ixex_copy_zones, copy_cells); push_expanded_fills(m_ixex_mirror_zones, mirror_cells); - // White borders: one outline per expanded box of each active secondary type. - auto build_rect_lines = [](const std::vector& boxes) -> Lines { - Lines lines; - for (const auto& b : boxes) { - lines.emplace_back(Point(scale_(b.x0), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y0))); - lines.emplace_back(Point(scale_(b.x1), scale_(b.y0)), Point(scale_(b.x1), scale_(b.y1))); - lines.emplace_back(Point(scale_(b.x1), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y1))); - lines.emplace_back(Point(scale_(b.x0), scale_(b.y1)), Point(scale_(b.x0), scale_(b.y0))); - } - return lines; - }; - - // Combine copy+mirror for borders (each expands independently then we collect all lines) - Lines all_border_lines = build_rect_lines(make_boxes(copy_cells)); - auto mirror_border_lines = build_rect_lines(make_boxes(mirror_cells)); - all_border_lines.insert(all_border_lines.end(), mirror_border_lines.begin(), mirror_border_lines.end()); - - if (!all_border_lines.empty()) - if (!init_model_from_lines(m_ixex_zone_borders, all_border_lines, GROUND_Z_GRIDLINE)) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": unable to create iXex zone border lines\n"; // --- Secondary zone blocking --- // Collect the expanded bounding boxes for all secondary (copy+mirror) zones. @@ -857,7 +836,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) struct IXexTheme { ColorRGBA copy; // secondary copy zone fill ColorRGBA mirror; // secondary mirror zone fill - ColorRGBA border; // zone outline ColorRGBA danger; // blocking collision strip ColorRGBA margin; // advisory safety margin }; @@ -866,7 +844,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) static const IXexTheme k_standard = { { 0.337f, 0.706f, 0.914f, 0.45f }, // copy — sky blue #56B4E9 { 0.902f, 0.624f, 0.000f, 0.45f }, // mirror — orange #E69F00 - { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red { 0.300f, 0.900f, 0.200f, 0.40f }, // margin — lime green }; @@ -875,7 +852,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) static const IXexTheme k_deuteranopia = { { 0.000f, 0.447f, 0.698f, 0.50f }, // copy — blue #0072B2 { 0.941f, 0.894f, 0.259f, 0.50f }, // mirror — yellow #F0E442 - { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white { 0.200f, 0.100f, 0.800f, 0.65f }, // danger — blue-violet (red not visible) { 0.800f, 0.475f, 0.655f, 0.45f }, // margin — reddish purple #CC79A7 }; @@ -884,7 +860,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) static const IXexTheme k_tritanopia = { { 0.835f, 0.369f, 0.000f, 0.50f }, // copy — vermilion #D55E00 { 0.800f, 0.475f, 0.655f, 0.50f }, // mirror — reddish purple #CC79A7 - { 1.000f, 1.000f, 1.000f, 0.90f }, // border — white { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red (visible to tritanopes) { 0.000f, 0.700f, 0.600f, 0.45f }, // margin — teal }; @@ -892,7 +867,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) static const IXexTheme k_high_contrast = { { 0.000f, 0.600f, 1.000f, 0.65f }, // copy — vivid blue { 1.000f, 0.600f, 0.000f, 0.65f }, // mirror — vivid orange - { 1.000f, 1.000f, 0.000f, 1.00f }, // border — bright yellow { 1.000f, 0.000f, 0.000f, 0.75f }, // danger — full red { 0.000f, 1.000f, 0.200f, 0.60f }, // margin — bright green }; @@ -920,12 +894,6 @@ void PartPlate::render_ixex_zones(bool force_default_color) glsafe(::glDepthMask(GL_TRUE)); - if (m_ixex_zone_borders.is_initialized()) { - m_ixex_zone_borders.set_color(theme->border); - glsafe(::glLineWidth(2.0f)); - m_ixex_zone_borders.render(); - glsafe(::glLineWidth(1.0f)); - } if (!m_ixex_collision_overlay.empty()) { glsafe(::glDepthMask(GL_FALSE)); diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index a82d315328..2bb575afca 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -137,8 +137,7 @@ private: GLModel m_gridlines_bolder; std::vector m_ixex_copy_zones; // blue tint (Wong #56B4E9) std::vector m_ixex_mirror_zones; // orange tint (Wong #E69F00) - GLModel m_ixex_zone_borders; // white outlines around active secondary zones - std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here +std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here std::vector m_ixex_collision_zones; // carriage danger strips inside primary zone (mm) std::vector m_ixex_collision_overlay; // red-orange rendered fill for danger strips std::vector m_ixex_margin_overlay; // amber advisory bands just inside collision strips From b9f7148080a5d7540d4b7ae691025a7210d5b282 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 6 Apr 2026 00:52:37 -0400 Subject: [PATCH 009/154] fix: constrain auto-arrange to iXex primary zone when parallel mode is active Adds std::optional m_ixex_primary_zone_box to PartPlate, populated by calc_ixex_zones() alongside the existing secondary/collision zone geometry. The new ixex_primary_zone() getter calls ensure_ixex_zones() so callers always get fresh data. ArrangeJob::process() replaces the full-bed bedpts with the primary zone corners when the getter returns a value, so auto-arrange no longer drops objects into the bed center. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 13 +++++++++++++ src/slic3r/GUI/PartPlate.cpp | 2 ++ src/slic3r/GUI/PartPlate.hpp | 7 ++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 2e43f0271e..dded97556a 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -540,6 +540,19 @@ void ArrangeJob::process(Ctl &ctl) Points bedpts = get_shrink_bedpts(m_plater->config(),params); + // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only. + if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { + if (auto pz = curr_plate->ixex_primary_zone()) { + BoundingBox scaled_pz = scaled(*pz); + bedpts = { + { scaled_pz.min.x(), scaled_pz.min.y() }, + { scaled_pz.max.x(), scaled_pz.min.y() }, + { scaled_pz.max.x(), scaled_pz.max.y() }, + { scaled_pz.min.x(), scaled_pz.max.y() }, + }; + } + } + bool enable_wrapping = global_config.option("enable_wrapping_detection")->value; partplate_list.preprocess_exclude_areas(params.excluded_regions, enable_wrapping, 1, scale_(1)); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 09db0bb072..f48e12cb76 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -465,6 +465,7 @@ void PartPlate::calc_ixex_zones() m_ixex_collision_zones.clear(); m_ixex_collision_overlay.clear(); m_ixex_margin_overlay.clear(); + m_ixex_primary_zone_box = std::nullopt; if (!wxGetApp().preset_bundle) return; @@ -689,6 +690,7 @@ void PartPlate::calc_ixex_zones() 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; + m_ixex_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 auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) { diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 2bb575afca..23d63da7e0 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -137,10 +138,11 @@ private: GLModel m_gridlines_bolder; std::vector m_ixex_copy_zones; // blue tint (Wong #56B4E9) std::vector m_ixex_mirror_zones; // orange tint (Wong #E69F00) -std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here + std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here std::vector m_ixex_collision_zones; // carriage danger strips inside primary zone (mm) std::vector m_ixex_collision_overlay; // red-orange rendered fill for danger strips std::vector m_ixex_margin_overlay; // amber advisory bands just inside collision strips + std::optional m_ixex_primary_zone_box; // primary zone extents in mm; empty when iXex is off/primary-only std::string m_ixex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt GLModel m_height_limit_common; GLModel m_height_limit_bottom; @@ -444,6 +446,9 @@ public: } // Returns true if any instance on this plate overlaps an iXex secondary or collision zone. bool has_ixex_placement_violations(); + // Returns the primary-zone bounding box in mm when an iXex parallel mode is active. + // Empty (nullopt) when iXex is off or the mode is "primary" (full-bed). + std::optional ixex_primary_zone() { ensure_ixex_zones(); return m_ixex_primary_zone_box; } void update_slice_ready_status(bool ready_slice) { m_ready_for_slice = ready_slice; From 1cacc6d02d88bd991690f5482e0e17fcaf7d316d Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 6 Apr 2026 01:02:44 -0400 Subject: [PATCH 010/154] fix: exclude iXex collision strips from auto-arrange placement Auto-arrange now injects the carriage collision zones as hard excluded regions (is_virt_object) when an iXex parallel mode is active, using the same BoundingBoxf3 data already stored for rendering and violation checks. Objects will no longer be placed in the mirror-edge danger strips. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 20 +++++++++++++++++++- src/slic3r/GUI/PartPlate.hpp | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index dded97556a..16e5531a74 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -540,7 +540,8 @@ void ArrangeJob::process(Ctl &ctl) Points bedpts = get_shrink_bedpts(m_plater->config(),params); - // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only. + // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only + // and treat carriage collision strips as hard excluded regions. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { if (auto pz = curr_plate->ixex_primary_zone()) { BoundingBox scaled_pz = scaled(*pz); @@ -550,6 +551,23 @@ void ArrangeJob::process(Ctl &ctl) { scaled_pz.max.x(), scaled_pz.max.y() }, { scaled_pz.min.x(), scaled_pz.max.y() }, }; + for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { + Polygon poly({ + { scaled(cz.min.x()), scaled(cz.min.y()) }, + { scaled(cz.max.x()), scaled(cz.min.y()) }, + { scaled(cz.max.x()), scaled(cz.max.y()) }, + { scaled(cz.min.x()), scaled(cz.max.y()) }, + }); + arrangement::ArrangePolygon ap; + ap.poly.contour = poly; + ap.translation = Vec2crd(0, 0); + ap.rotation = 0.0; + ap.is_virt_object = true; + ap.bed_idx = current_plate_index; + ap.height = 1; + ap.name = "IXexCollisionZone"; + params.excluded_regions.emplace_back(std::move(ap)); + } } } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 23d63da7e0..4954e6f3b5 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -449,6 +449,9 @@ public: // Returns the primary-zone bounding box in mm when an iXex parallel mode is active. // Empty (nullopt) when iXex is off or the mode is "primary" (full-bed). std::optional ixex_primary_zone() { ensure_ixex_zones(); return m_ixex_primary_zone_box; } + // Returns carriage collision strips (mm) for the current iXex mode. + // Always call ixex_primary_zone() first to ensure the cache is warm. + const std::vector& ixex_collision_zones() const { return m_ixex_collision_zones; } void update_slice_ready_status(bool ready_slice) { m_ready_for_slice = ready_slice; From a9ad17cea2b9f8331716d4e30f24890c7f941044 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 6 Apr 2026 01:36:33 -0400 Subject: [PATCH 011/154] fix: route iXex collision zones through m_unselected for effective exclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit params.excluded_regions flows into libnest2d PlacementConfig.m_excluded_regions which is declared but never consumed by the NFP placer — the field is dead code. m_unselected is the working path: it becomes fixeditems that are preloaded as physical fixed obstacles in the NFP computation, same as the wipe tower. Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 16e5531a74..9d1d778504 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -566,7 +566,7 @@ void ArrangeJob::process(Ctl &ctl) ap.bed_idx = current_plate_index; ap.height = 1; ap.name = "IXexCollisionZone"; - params.excluded_regions.emplace_back(std::move(ap)); + m_unselected.emplace_back(std::move(ap)); } } } From 167211c85c30d2bc6d23651756c5f012b15d734d Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 6 Apr 2026 14:53:18 -0400 Subject: [PATCH 012/154] =?UTF-8?q?refactor:=20rename=20ixex=5Fcarriage=5F?= =?UTF-8?q?width=20=E2=86=92=20ixex=5Fnozzle=5Fclearance;=20fix=20strip=20?= =?UTF-8?q?math=20and=20carriage=20box=20visualization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config key rename (breaking for saved profiles — call out in PR): ixex_carriage_width_x/y → ixex_nozzle_clearance_x/y Labels updated to "Nozzle Clearance X/Y" with consistent tooltips describing the measurement as nozzle-to-collision-side-edge distance. Strip math fix: Previously halved the clearance value (× 0.5) under the assumption the nozzle was centered in the carriage. The measurement is now the literal nozzle-to-edge distance, so the × 0.5 factor is removed. The collision strip width now equals the configured value directly. Carriage box visualization (GCodeViewer): - Add per-carriage box_offset_x/y so the nozzle marker sits at the physically correct edge of the footprint box rather than centered. - X: zone-based by default (nozzle at inner edge facing bed center). Copy secondaries inherit the primary's X orientation (same movement direction). Mirror secondaries use the collision-side edge. - Y: always row-based regardless of copy/mirror mode. Gantry is always behind the nozzle (high-Y); front-row primaries with a back-row secondary override to place nozzle at the low-Y edge. - Fix stale mesh bug: GLModel::init_from() is a no-op when already initialized. Call reset() before init_from() so mesh rebuilds correctly when nozzle clearance values change in config. - Remove m_ixex_toolhead_box_dims (was the now-unnecessary cache key). Co-Authored-By: Claude Sonnet 4.6 --- src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 12 +++---- src/libslic3r/PrintConfig.hpp | 4 +-- src/slic3r/GUI/GCodeViewer.cpp | 63 +++++++++++++++++++++++++--------- src/slic3r/GUI/GCodeViewer.hpp | 1 - src/slic3r/GUI/PartPlate.cpp | 30 ++++++++-------- src/slic3r/GUI/Tab.cpp | 4 +-- 7 files changed, 73 insertions(+), 43 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index b8df0aab53..4625a631f4 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1042,7 +1042,7 @@ static std::vector s_Preset_printer_options { "bed_temperature_formula", "nozzle_flush_dataset", // iXex (independent X extruder) — printer capability and user-defined modes "is_ixex", "ixex_gantry_count", "ixex_tools_per_gantry", "ixex_tool_layout", - "ixex_carriage_width_x", "ixex_carriage_width_y", "ixex_carriage_margin", "ixex_viz_theme", + "ixex_nozzle_clearance_x", "ixex_nozzle_clearance_y", "ixex_carriage_margin", "ixex_viz_theme", "ixex_mode_names", "ixex_mode_active_tools", "ixex_mode_gcodes" }; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5c75c87622..cf4fba50d8 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5530,17 +5530,17 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionString("front-left")); - def = this->add("ixex_carriage_width_x", coFloat); - def->label = L("Carriage Width X"); - def->tooltip = L("Toolhead footprint in the X direction (mm). Used to calculate usable bed zones and detect carriage collisions."); + def = this->add("ixex_nozzle_clearance_x", coFloat); + def->label = L("Nozzle Clearance X"); + def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in X. Used to calculate the width of the collision exclusion strip at each X boundary."); def->sidetext = L("mm"); def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(30.0)); - def = this->add("ixex_carriage_width_y", coFloat); - def->label = L("Carriage Width Y"); - def->tooltip = L("Toolhead footprint in the Y direction (mm). Used for collision detection."); + def = this->add("ixex_nozzle_clearance_y", coFloat); + def->label = L("Nozzle Clearance Y"); + def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in Y. Used to calculate the width of the collision exclusion strip at each Y boundary."); def->sidetext = L("mm"); def->min = 0; def->mode = comAdvanced; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 6c70772cda..1e89eba383 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1341,8 +1341,8 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionInt, ixex_tools_per_gantry)) ((ConfigOptionInt, ixex_primary_col)) ((ConfigOptionInt, ixex_primary_row)) - ((ConfigOptionFloat, ixex_carriage_width_x)) - ((ConfigOptionFloat, ixex_carriage_width_y)) + ((ConfigOptionFloat, ixex_nozzle_clearance_x)) + ((ConfigOptionFloat, ixex_nozzle_clearance_y)) ((ConfigOptionStrings, ixex_mode_names)) ((ConfigOptionStrings, ixex_mode_active_tools)) ((ConfigOptionStrings, ixex_mode_gcodes)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 0ea8749b33..b4e2a9627c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1521,7 +1521,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) { 0.835f, 0.369f, 0.000f, 0.65f }, // secondary 2 — vermilion (#D55E00) { 0.800f, 0.475f, 0.655f, 0.65f }, // secondary 3 — reddish purple (#CC79A7) }}; - struct CarriageDraw { Vec3f pos; ColorRGBA color; }; + struct CarriageDraw { Vec3f pos; ColorRGBA color; float box_offset_x = 0.0f; float box_offset_y = 0.0f; }; std::vector carriage_box_draws; float ixex_box_wx = 0.0f, ixex_box_wy = 0.0f; { @@ -1545,8 +1545,8 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) auto* active_tools_opt = printer_cfg.opt("ixex_mode_active_tools"); auto* tpg_opt = printer_cfg.opt("ixex_tools_per_gantry"); - auto* wx_opt = printer_cfg.opt("ixex_carriage_width_x"); - auto* wy_opt = printer_cfg.opt("ixex_carriage_width_y"); + auto* wx_opt = printer_cfg.opt("ixex_nozzle_clearance_x"); + auto* wy_opt = printer_cfg.opt("ixex_nozzle_clearance_y"); int tools_per_gantry = tpg_opt ? std::max(1, tpg_opt->value) : 1; ixex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f; ixex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f; @@ -1665,8 +1665,19 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) } } - // Primary carriage box - carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0] }); + // 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 : -ixex_box_wx; // nozzle at inner edge (facing center of bed) + float pri_box_offset_y = -ixex_box_wy; // nozzle at high-Y edge by default (gantry always behind nozzle); override if secondary is behind + 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 = -ixex_box_wx; } + if (sr > pri_phys_row) { pri_box_offset_y = 0.0f; } + else if (sr < pri_phys_row) { pri_box_offset_y = -ixex_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; @@ -1704,8 +1715,26 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() }; m_sequential_view.m_ixex_secondary_markers[i].set_world_position(sec_pos); m_sequential_view.m_ixex_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; + } else { + if (sec_phys_col > pri_phys_col) sec_box_offset_x = -ixex_box_wx; + 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 = -ixex_box_wy; + else if (sec_phys_row < pri_phys_row) sec_box_offset_y = 0.0f; + else sec_box_offset_y = -ixex_box_wy; carriage_box_draws.push_back({ - sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()] }); + sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()], + sec_box_offset_x, sec_box_offset_y }); } } } @@ -1719,17 +1748,19 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type()); // iXex: render toolhead footprint boxes for each active carriage. - // Each box is ixex_carriage_width_x × ixex_carriage_width_y, sitting above the nozzle tip. + // Each box is ixex_nozzle_clearance_x × ixex_nozzle_clearance_y, sitting above the nozzle tip. if (!carriage_box_draws.empty() && ixex_box_wx > 0.0f && ixex_box_wy > 0.0f) { - // Rebuild box mesh if dimensions changed - const Vec2f new_dims{ ixex_box_wx, ixex_box_wy }; - if (!m_ixex_toolhead_box.is_initialized() || new_dims != m_ixex_toolhead_box_dims) { - m_ixex_toolhead_box_dims = new_dims; - const float box_h = std::max(ixex_box_wx, ixex_box_wy); // height ~ largest horizontal dim + // Rebuild box mesh every frame — dimensions can change via config edit without a + // G-code reload, so dimension-based caching isn't safe. + // Mesh origin: nozzle at x=0, centered in Y, Z starts at nozzle tip level. + // Per-carriage box_offset_x shifts the mesh left or right so the nozzle lands + // at the correct (collision-side) edge. + { + const float box_h = std::max(ixex_box_wx, ixex_box_wy); indexed_triangle_set its = its_make_cube((double)ixex_box_wx, (double)ixex_box_wy, (double)box_h); - // Center in X and Y; Z starts at 0 (nozzle tip level) - for (auto& v : its.vertices) - v += Vec3f(-ixex_box_wx * 0.5f, -ixex_box_wy * 0.5f, 0.0f); + // No vertex pre-shifting — box_offset_x/y in the per-carriage transform + // positions the nozzle at the correct collision-side edge. + m_ixex_toolhead_box.reset(); m_ixex_toolhead_box.init_from(its); } @@ -1744,7 +1775,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) const Transform3d& view_matrix = camera.get_view_matrix(); for (const auto& draw : carriage_box_draws) { const Transform3d model_matrix = Geometry::translation_transform( - (draw.pos + Vec3f(0.0f, 0.0f, m_z_offset)).cast()); + (draw.pos + Vec3f(draw.box_offset_x, draw.box_offset_y, m_z_offset)).cast()); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 71880da962..ee3c1e1ec7 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -207,7 +207,6 @@ private: std::string m_marker_filename; // cached for lazy secondary marker init std::string m_ixex_last_mode; // detect mode changes for secondary marker rebuild GLModel m_ixex_toolhead_box; // shared box mesh for all carriage footprint overlays - Vec2f m_ixex_toolhead_box_dims{ 0.0f, 0.0f }; // (width_x, width_y) — rebuild when changed size_t m_extruders_count; std::vector m_filament_diameters; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index f48e12cb76..16a87009a1 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -653,18 +653,18 @@ void PartPlate::calc_ixex_zones() // Only add strips at boundaries of the PRIMARY zone — objects are only placed in the // primary zone, so secondary-to-secondary boundaries have no relevance. // - // Strip width is carriage/2 on the primary side only: - // right X boundary: [bnd_x - carriage_w/2, bnd_x] - // left X boundary: [bnd_x, bnd_x + carriage_w/2] - // top Y boundary: [bnd_y - carriage_h/2, bnd_y] - // bottom Y boundary:[bnd_y, bnd_y + carriage_h/2] + // Strip width is the literal nozzle clearance value on the primary side only: + // right X boundary: [bnd_x - nozzle_clearance_x, bnd_x] + // left X boundary: [bnd_x, bnd_x + nozzle_clearance_x] + // top Y boundary: [bnd_y - nozzle_clearance_y, bnd_y] + // bottom Y boundary:[bnd_y, bnd_y + nozzle_clearance_y] // // Strip length matches the primary zone extent (same expansion logic as make_boxes): // !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("ixex_carriage_width_x"); - auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); + auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); double carriage_w = cw_opt ? cw_opt->value : 0.0; double carriage_h = ch_opt ? ch_opt->value : 0.0; @@ -729,36 +729,36 @@ void PartPlate::calc_ixex_zones() if (r == pri_row - 1 && c == pri_col) has_bottom_sec = true; } - // X-axis boundaries (vertical strips, width = carriage_w/2 on primary side) + // 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 strip_inner = bnd_x - carriage_w * 0.5; + 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 strip_inner = bnd_x + carriage_w * 0.5; + double strip_inner = bnd_x + carriage_w; add_strip(bnd_x, strip_inner, pz_y0, pz_y1); if (margin > 0.0) add_margin_fill(strip_inner, strip_inner + margin, pz_y0, pz_y1); } } - // Y-axis boundaries (horizontal strips, width = carriage_h/2 on primary side) + // 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 strip_inner = bnd_y - carriage_h * 0.5; + 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 strip_inner = bnd_y + carriage_h * 0.5; + double strip_inner = bnd_y + carriage_h; add_strip(pz_x0, pz_x1, bnd_y, strip_inner); if (margin > 0.0) add_margin_fill(pz_x0, pz_x1, strip_inner, strip_inner + margin); @@ -779,8 +779,8 @@ static std::string build_ixex_cache_key() auto* mode_opt = process_cfg.option("ixex_parallel_mode"); auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); - auto* cw_opt = printer_cfg.option("ixex_carriage_width_x"); - auto* ch_opt = printer_cfg.option("ixex_carriage_width_y"); + auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); + auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); return (mode_opt ? mode_opt->value : "primary") + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index dd1d6d84f9..77283cfd8a 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5002,8 +5002,8 @@ void TabPrinter::build_fff() }; optgroup->append_line(line); } - optgroup->append_single_option_line("ixex_carriage_width_x"); - optgroup->append_single_option_line("ixex_carriage_width_y"); + optgroup->append_single_option_line("ixex_nozzle_clearance_x"); + optgroup->append_single_option_line("ixex_nozzle_clearance_y"); optgroup->append_single_option_line("ixex_carriage_margin"); { From ec40172019bacf5a3827c3fc9016bfbbb545ac3f Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Tue, 7 Apr 2026 23:25:22 -0400 Subject: [PATCH 013/154] feat: per-plate iXex mode selection with undo, dark mode icons, and menu fixes - Add per-plate iXex mode icon to the plate toolbar (normal, hover, dark, dark-hover SVG variants) - Left-click cycles through available modes; right-click shows a popup menu with all modes as radio items - Mode changes are recorded in the undo/redo snapshot system - Fix double context menu: suppress EVT_GLCANVAS_PLATE_RIGHT_CLICK and EVT_GLCANVAS_RIGHT_CLICK when the iXex icon popup was already shown - Remove ixex_parallel_mode combo from Print Settings > Other > Special mode (superseded by per-plate icon) - Remove dead code: refresh_ixex_mode_combo(), m_ixex_mode_combo member, related Tab reload hook - iXex mode persisted in 3MF project files via existing plate metadata serialization Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_ixex_mode.svg | 10 ++ resources/images/plate_ixex_mode_dark.svg | 10 ++ resources/images/plate_ixex_mode_hover.svg | 10 ++ .../images/plate_ixex_mode_hover_dark.svg | 10 ++ src/libslic3r/Format/bbs_3mf.cpp | 10 ++ src/libslic3r/PrintConfig.cpp | 16 ---- src/libslic3r/PrintConfig.hpp | 2 - src/slic3r/GUI/GCodeViewer.cpp | 8 +- src/slic3r/GUI/GLCanvas3D.cpp | 7 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 17 +++- src/slic3r/GUI/PartPlate.cpp | 96 +++++++++++++++++-- src/slic3r/GUI/PartPlate.hpp | 11 ++- src/slic3r/GUI/Plater.cpp | 54 +++++++++++ src/slic3r/GUI/Tab.cpp | 54 ----------- src/slic3r/GUI/Tab.hpp | 2 - 15 files changed, 226 insertions(+), 91 deletions(-) create mode 100644 resources/images/plate_ixex_mode.svg create mode 100644 resources/images/plate_ixex_mode_dark.svg create mode 100644 resources/images/plate_ixex_mode_hover.svg create mode 100644 resources/images/plate_ixex_mode_hover_dark.svg diff --git a/resources/images/plate_ixex_mode.svg b/resources/images/plate_ixex_mode.svg new file mode 100644 index 0000000000..30a67cdf35 --- /dev/null +++ b/resources/images/plate_ixex_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_dark.svg b/resources/images/plate_ixex_mode_dark.svg new file mode 100644 index 0000000000..981fd88447 --- /dev/null +++ b/resources/images/plate_ixex_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover.svg b/resources/images/plate_ixex_mode_hover.svg new file mode 100644 index 0000000000..9463e767df --- /dev/null +++ b/resources/images/plate_ixex_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover_dark.svg b/resources/images/plate_ixex_mode_hover_dark.svg new file mode 100644 index 0000000000..301fa995ab --- /dev/null +++ b/resources/images/plate_ixex_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index dcbf638483..b7513ea240 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -333,6 +333,7 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence"; static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums"; static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode"; +static constexpr const char* IXEX_PARALLEL_MODE_ATTR = "ixex_parallel_mode"; static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode"; static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps"; static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps"; @@ -4292,6 +4293,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::istringstream(value) >> std::boolalpha >> spiral_mode; m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode)); } + else if (key == IXEX_PARALLEL_MODE_ATTR) { + m_curr_plater->config.set_key_value("ixex_parallel_mode", new ConfigOptionString(value)); + } else if (key == FILAMENT_MAP_MODE_ATTR) { FilamentMapMode map_mode = FilamentMapMode::fmmAutoForFlush; @@ -7773,6 +7777,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (spiral_mode_opt) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n"; + { + auto* ixex_mode_opt = plate_data->config.option("ixex_parallel_mode"); + if (ixex_mode_opt && !ixex_mode_opt->value.empty() && ixex_mode_opt->value != "primary") + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IXEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << ixex_mode_opt->value << "\"/>\n"; + } + //filament map related ConfigOption* filament_map_mode_opt = plate_data->config.option("filament_map_mode"); t_config_enum_names filament_map_mode_names = ConfigOptionEnum::get_enum_names(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index cf4fba50d8..0b287b1dff 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5506,22 +5506,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(2)); - def = this->add("ixex_primary_col", coInt); - def->label = L("Primary Tool Column"); - def->tooltip = L("X index (0-based) of the primary toolhead. 0 = leftmost column. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - - def = this->add("ixex_primary_row", coInt); - def->label = L("Primary Tool Row"); - def->tooltip = L("Y index (0-based) of the primary toolhead's gantry. 0 = front gantry. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - def = this->add("ixex_tool_layout", coString); def->label = L("Tool 0 Corner"); def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 1e89eba383..a2739f1c9a 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1339,8 +1339,6 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, is_ixex)) ((ConfigOptionInt, ixex_gantry_count)) ((ConfigOptionInt, ixex_tools_per_gantry)) - ((ConfigOptionInt, ixex_primary_col)) - ((ConfigOptionInt, ixex_primary_row)) ((ConfigOptionFloat, ixex_nozzle_clearance_x)) ((ConfigOptionFloat, ixex_nozzle_clearance_y)) ((ConfigOptionStrings, ixex_mode_names)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b4e2a9627c..5163580c30 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1533,7 +1533,13 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) if (is_ixex_opt && is_ixex_opt->value) { const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); - const std::string mode = mode_opt ? mode_opt->value : "primary"; + std::string mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode overrides the process preset. + if (auto* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) { + std::string plate_mode = plate->get_ixex_mode(); + if (plate_mode != "primary") + mode = plate_mode; + } if (mode != m_ixex_last_mode) { m_sequential_view.m_ixex_secondary_markers.clear(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 5e23a9d4ae..66dc76661d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4648,9 +4648,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } //BBS change plate selection + bool plate_icon_popup_shown = false; if (!m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !m_mouse.dragging) { int hover_idx = m_hover_plate_idxs.front(); - wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true); + plate_icon_popup_shown = (wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true) == 1); if (m_hover_volume_idxs.empty()) deselect_all(); render(); @@ -4670,10 +4671,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_mouse.ignore_right_up && m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) { //BBS post right click event - if (!m_hover_plate_idxs.empty()) { + if (!m_hover_plate_idxs.empty() && !plate_icon_popup_shown) { post_event(RBtnPlateEvent(EVT_GLCANVAS_PLATE_RIGHT_CLICK, { logical_pos, m_hover_plate_idxs.front() })); } - else { + else if (!plate_icon_popup_shown) { // do not post the event if the user is panning the scene // or if right click was done over the wipe tower bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 9d1d778504..b4ec570d00 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -542,9 +542,16 @@ void ArrangeJob::process(Ctl &ctl) // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only // and treat carriage collision strips as hard excluded regions. + // NOTE: m_ixex_primary_zone_box and ixex_collision_zones() are in global (world) coordinates + // because they are derived from m_shape which includes the plate origin offset. The arranger + // always works in plate-local space (origin = 0,0), so we subtract the plate origin here. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { if (auto pz = curr_plate->ixex_primary_zone()) { - BoundingBox scaled_pz = scaled(*pz); + Vec3d plate_origin = curr_plate->get_origin(); + double ox = plate_origin.x(), oy = plate_origin.y(); + BoundingBoxf pz_local(Vec2d(pz->min.x() - ox, pz->min.y() - oy), + Vec2d(pz->max.x() - ox, pz->max.y() - oy)); + BoundingBox scaled_pz = scaled(pz_local); bedpts = { { scaled_pz.min.x(), scaled_pz.min.y() }, { scaled_pz.max.x(), scaled_pz.min.y() }, @@ -553,10 +560,10 @@ void ArrangeJob::process(Ctl &ctl) }; for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { Polygon poly({ - { scaled(cz.min.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.max.y()) }, - { scaled(cz.min.x()), scaled(cz.max.y()) }, + { scaled(cz.min.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.max.y() - oy) }, + { scaled(cz.min.x() - ox), scaled(cz.max.y() - oy) }, }); arrangement::ArrangePolygon ap; ap.poly.contour = poly; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 16a87009a1..0f2f45d919 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -365,6 +365,34 @@ std::vector PartPlate::get_plate_wrapping_detection_area() const return std::vector(); } +std::string PartPlate::get_ixex_mode() const +{ + if (m_config.has("ixex_parallel_mode")) { + auto* opt = m_config.option("ixex_parallel_mode"); + if (opt && !opt->value.empty()) + return opt->value; + } + return "primary"; +} + +void PartPlate::set_ixex_mode(const std::string& mode) +{ + if (mode.empty() || mode == "primary") { + m_config.erase("ixex_parallel_mode"); + } else { + m_config.set_key_value("ixex_parallel_mode", new ConfigOptionString(mode)); + } + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; // force zone rebuild +} + +void PartPlate::reset_ixex_mode() +{ + m_config.erase("ixex_parallel_mode"); + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; +} + void PartPlate::set_spiral_vase_mode(bool spiral_mode, bool as_global) { std::string key = "spiral_mode"; @@ -475,9 +503,14 @@ void PartPlate::calc_ixex_zones() if (!is_ixex_opt || !is_ixex_opt->value) return; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); - std::string active_mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode takes priority over the process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } if (active_mode == "primary" || active_mode.empty()) return; @@ -767,7 +800,8 @@ void PartPlate::calc_ixex_zones() } // Build a cache key from the current iXex config options, or "" if iXex is off. -static std::string build_ixex_cache_key() +// Reads per-plate mode from m_config first, falling back to the process preset. +std::string PartPlate::build_ixex_cache_key() const { if (!wxGetApp().preset_bundle) return ""; @@ -775,14 +809,20 @@ static std::string build_ixex_cache_key() auto* is_ixex_opt = printer_cfg.option("is_ixex"); if (!is_ixex_opt || !is_ixex_opt->value) return ""; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + // Per-plate mode takes priority over process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); - return (mode_opt ? mode_opt->value : "primary") + 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) + "|cw" + std::to_string(cw_opt ? (int)cw_opt->value : 0) @@ -1643,6 +1683,20 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture); + // iXex mode icon — only when is_ixex is active + { + PresetBundle* pb = wxGetApp().preset_bundle; + auto* is_ixex_opt = pb ? pb->printers.get_edited_preset().config.option("is_ixex") : nullptr; + if (is_ixex_opt && is_ixex_opt->value) { + if (hover_id == (int)PLATE_IXEX_MODE_ID) { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_hovered_texture); + std::string cur = get_ixex_mode(); + show_tooltip(_u8L("iXex mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); + } else { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_texture); + } + } + } if (m_partplate_list->render_plate_settings) { bool has_plate_settings = get_bed_type() != BedType::btDefault || get_print_seq() != PrintSequence::ByDefault || !get_first_layer_print_sequence().empty() || !get_other_layers_print_sequence().empty() || has_spiral_mode_config(); @@ -1952,6 +2006,13 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) bool dual_bbl = (preset && preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); + + // Register iXex mode icon only when iXex is active on the current printer preset. + if (preset) { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + register_model_for_picking(canvas, m_ixex_mode_icon, picking_id_component(PLATE_IXEX_MODE_ID)); + } } int PartPlate::picking_id_component(int idx) const @@ -3605,6 +3666,11 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co dual_bbl = (preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); calc_vertex_for_icons(dual_bbl ? 5 : 6, m_plate_filament_map_icon); calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); + { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + calc_vertex_for_icons(dual_bbl ? 7 : 6, m_ixex_mode_icon); + } calc_vertex_for_number(0, false, m_plate_idx_icon); // calc vertex for plate name @@ -4474,6 +4540,20 @@ void PartPlateList::generate_icon_textures() } } + // iXex mode icon textures (fall back gracefully if SVG not present yet) + { + file_name = path + (m_is_dark ? "plate_ixex_mode_dark.svg" : "plate_ixex_mode.svg"); + if (!m_ixex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode icon)") % file_name; + } + } + { + file_name = path + (m_is_dark ? "plate_ixex_mode_hover_dark.svg" : "plate_ixex_mode_hover.svg"); + if (!m_ixex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode hover icon)") % file_name; + } + } + std::string text_str = "01"; // ORCA also scale font size to prevent low res texture int size = wxGetApp().em_unit() * PARTPLATE_ICON_SIZE; @@ -4517,6 +4597,8 @@ void PartPlateList::release_icon_textures() m_plate_set_filament_map_hovered_texture.reset(); m_plate_name_edit_texture.reset(); m_plate_name_edit_hovered_texture.reset(); + m_ixex_mode_texture.reset(); + m_ixex_mode_hovered_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 4954e6f3b5..773524c77c 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -155,6 +155,7 @@ private: PickingModel m_plate_filament_map_icon; PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; + PickingModel m_ixex_mode_icon; GLModel m_plate_idx_icon; GLTexture m_texture; @@ -188,6 +189,7 @@ private: void calc_height_limit(); void calc_ixex_zones(); void ensure_ixex_zones(); + std::string build_ixex_cache_key() const; void render_ixex_zones(bool force_default_color); 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); @@ -220,7 +222,8 @@ private: public: static constexpr unsigned int PLATE_NAME_HOVER_ID = 6; static constexpr unsigned int PLATE_FILAMENT_MAP_ID = 8; - static constexpr unsigned int GRABBER_COUNT = 9; + static constexpr unsigned int PLATE_IXEX_MODE_ID = 9; + static constexpr unsigned int GRABBER_COUNT = 10; static ColorRGBA SELECT_COLOR; static ColorRGBA UNSELECT_COLOR; @@ -280,6 +283,10 @@ public: bool get_spiral_vase_mode() const; void set_spiral_vase_mode(bool spiral_mode, bool as_global); + std::string get_ixex_mode() const; + void set_ixex_mode(const std::string& mode); + void reset_ixex_mode(); + std::vector get_plate_wrapping_detection_area() const; //static const int plate_x_offset = 20; //mm @@ -627,6 +634,8 @@ class PartPlateList : public ObjectBase GLTexture m_plate_set_filament_map_hovered_texture; GLTexture m_plate_name_edit_texture; GLTexture m_plate_name_edit_hovered_texture; + GLTexture m_ixex_mode_texture; + GLTexture m_ixex_mode_hovered_texture; GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2b07bc94a8..176b46d535 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17640,7 +17640,61 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi update(); p->partplate_list.select_plate(0); } + else if (action == (int)PartPlate::PLATE_IXEX_MODE_ID) + { + ret = select_plate(plate_index); + if (!ret) { + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + // Build ordered mode list: "primary" first, then all named modes. + std::vector modes; + modes.push_back("primary"); + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* names_opt = printer_cfg.option("ixex_mode_names"); + if (names_opt) { + for (const auto& n : names_opt->values) + if (!n.empty()) modes.push_back(n); + } + if (right_click) { + // Show a popup menu with all modes. + wxMenu menu; + std::string current = curr_plate->get_ixex_mode(); + for (size_t i = 0; i < modes.size(); ++i) { + wxMenuItem* item = menu.AppendRadioItem(wxID_HIGHEST + (int)i, from_u8(modes[i])); + if (modes[i] == current) + item->Check(true); + } + menu.Bind(wxEVT_MENU, [this, curr_plate, &modes](wxCommandEvent& e) { + int idx = e.GetId() - wxID_HIGHEST; + if (idx >= 0 && idx < (int)modes.size()) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(modes[idx]); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + }); + p->view3D->get_canvas3d()->get_wxglcanvas()->PopupMenu(&menu); + ret = 1; // signal to caller: popup was shown, suppress plate context menu + } else { + // Left-click: cycle to next mode. + std::string current = curr_plate->get_ixex_mode(); + auto it = std::find(modes.begin(), modes.end(), current); + size_t next_idx = (it == modes.end()) ? 0 : ((it - modes.begin() + 1) % modes.size()); + std::string next_mode = modes[next_idx]; + if (next_mode != current) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(next_mode); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + } + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 77283cfd8a..d6355a3a5d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2674,26 +2674,6 @@ void TabPrint::build() optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); optgroup->append_single_option_line("enable_wrapping_detection"); - // Dynamic combo box — choices come from the printer preset's ixex_mode_names. - // Use create_line_with_widget so the undo bitmaps are set (has_undo_ui()=true), - // ensuring the widget x-position matches other option fields in OG_CustomCtrl. - create_line_with_widget(optgroup.get(), "ixex_parallel_mode", "", [this](wxWindow* parent) -> wxSizer* { - m_ixex_mode_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(12 * wxGetApp().em_unit(), -1), - 0, nullptr, wxCB_READONLY); - m_ixex_mode_combo->GetDropDown().SetUseContentWidth(true); - refresh_ixex_mode_combo(); - m_ixex_mode_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - if (!m_ixex_mode_combo || !m_config) return; - std::string val = m_ixex_mode_combo->GetStringSelection().ToStdString(); - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString(val)); - on_value_change("ixex_parallel_mode", val); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_mode_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin"); @@ -2752,36 +2732,6 @@ void TabPrint::reload_config() { this->compatible_widget_reload(m_compatible_printers); Tab::reload_config(); - refresh_ixex_mode_combo(); -} - -void TabPrint::refresh_ixex_mode_combo() -{ - if (!m_ixex_mode_combo) return; - // Rebuild choices: "primary" + names from the active printer preset - m_ixex_mode_combo->Clear(); - m_ixex_mode_combo->Append("primary"); - if (m_preset_bundle) { - auto* names = m_preset_bundle->printers.get_edited_preset().config - .option("ixex_mode_names"); - if (names) { - for (const auto& n : names->values) - if (!n.empty()) m_ixex_mode_combo->Append(wxString::FromUTF8(n)); - } - } - // Restore current selection from process config (safe: option may not exist in older presets) - std::string cur = "primary"; - if (m_config) { - auto* opt = m_config->option("ixex_parallel_mode"); - if (opt) cur = opt->value; - } - if (!m_ixex_mode_combo->SetStringSelection(cur)) { - m_ixex_mode_combo->SetSelection(0); // fall back to "primary" - // The active mode no longer exists — reset config so stale name doesn't crash downstream. - if (m_config) - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString("primary")); - on_value_change("ixex_parallel_mode", std::string("primary")); - } } void TabPrint::update_description_lines() @@ -2903,7 +2853,6 @@ void TabPrint::clear_pages() m_recommended_thin_wall_thickness_description_line = nullptr; m_top_bottom_shell_thickness_explanation = nullptr; - m_ixex_mode_combo = nullptr; } //BBS: GUI refactor @@ -5059,9 +5008,6 @@ void TabPrinter::build_fff() m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); update_dirty(); on_value_change("ixex_mode_names", std::string("")); - // Refresh the process tab's mode dropdown immediately - if (auto* print_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINT))) - print_tab->refresh_ixex_mode_combo(); }; auto* sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 687cfeea73..7d86278db7 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -464,12 +464,10 @@ public: void update() override; void clear_pages() override; bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } - void refresh_ixex_mode_combo(); private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr; - ComboBox* m_ixex_mode_combo {nullptr}; }; class TabPrintModel : public TabPrint From 5fa6480f6188f94a2789b122347048818b85a6f4 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Tue, 7 Apr 2026 23:25:22 -0400 Subject: [PATCH 014/154] feat: per-plate iXex mode selection with undo, dark mode icons, and menu fixes - Add per-plate iXex mode icon to the plate toolbar (normal, hover, dark, dark-hover SVG variants) - Left-click cycles through available modes; right-click shows a popup menu with all modes as radio items - Mode changes are recorded in the undo/redo snapshot system - Fix double context menu: suppress EVT_GLCANVAS_PLATE_RIGHT_CLICK and EVT_GLCANVAS_RIGHT_CLICK when the iXex icon popup was already shown - Remove ixex_parallel_mode combo from Print Settings > Other > Special mode (superseded by per-plate icon) - Remove dead code: refresh_ixex_mode_combo(), m_ixex_mode_combo member, related Tab reload hook - iXex mode persisted in 3MF project files via existing plate metadata serialization Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_ixex_mode.svg | 10 ++ resources/images/plate_ixex_mode_dark.svg | 10 ++ resources/images/plate_ixex_mode_hover.svg | 10 ++ .../images/plate_ixex_mode_hover_dark.svg | 10 ++ src/libslic3r/Format/bbs_3mf.cpp | 10 ++ src/libslic3r/PrintConfig.cpp | 16 ---- src/libslic3r/PrintConfig.hpp | 2 - src/slic3r/GUI/GCodeViewer.cpp | 8 +- src/slic3r/GUI/GLCanvas3D.cpp | 7 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 17 +++- src/slic3r/GUI/PartPlate.cpp | 96 +++++++++++++++++-- src/slic3r/GUI/PartPlate.hpp | 11 ++- src/slic3r/GUI/Plater.cpp | 54 +++++++++++ src/slic3r/GUI/Tab.cpp | 54 ----------- src/slic3r/GUI/Tab.hpp | 2 - 15 files changed, 226 insertions(+), 91 deletions(-) create mode 100644 resources/images/plate_ixex_mode.svg create mode 100644 resources/images/plate_ixex_mode_dark.svg create mode 100644 resources/images/plate_ixex_mode_hover.svg create mode 100644 resources/images/plate_ixex_mode_hover_dark.svg diff --git a/resources/images/plate_ixex_mode.svg b/resources/images/plate_ixex_mode.svg new file mode 100644 index 0000000000..30a67cdf35 --- /dev/null +++ b/resources/images/plate_ixex_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_dark.svg b/resources/images/plate_ixex_mode_dark.svg new file mode 100644 index 0000000000..981fd88447 --- /dev/null +++ b/resources/images/plate_ixex_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover.svg b/resources/images/plate_ixex_mode_hover.svg new file mode 100644 index 0000000000..9463e767df --- /dev/null +++ b/resources/images/plate_ixex_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover_dark.svg b/resources/images/plate_ixex_mode_hover_dark.svg new file mode 100644 index 0000000000..301fa995ab --- /dev/null +++ b/resources/images/plate_ixex_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index dcbf638483..b7513ea240 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -333,6 +333,7 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence"; static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums"; static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode"; +static constexpr const char* IXEX_PARALLEL_MODE_ATTR = "ixex_parallel_mode"; static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode"; static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps"; static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps"; @@ -4292,6 +4293,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::istringstream(value) >> std::boolalpha >> spiral_mode; m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode)); } + else if (key == IXEX_PARALLEL_MODE_ATTR) { + m_curr_plater->config.set_key_value("ixex_parallel_mode", new ConfigOptionString(value)); + } else if (key == FILAMENT_MAP_MODE_ATTR) { FilamentMapMode map_mode = FilamentMapMode::fmmAutoForFlush; @@ -7773,6 +7777,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (spiral_mode_opt) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n"; + { + auto* ixex_mode_opt = plate_data->config.option("ixex_parallel_mode"); + if (ixex_mode_opt && !ixex_mode_opt->value.empty() && ixex_mode_opt->value != "primary") + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IXEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << ixex_mode_opt->value << "\"/>\n"; + } + //filament map related ConfigOption* filament_map_mode_opt = plate_data->config.option("filament_map_mode"); t_config_enum_names filament_map_mode_names = ConfigOptionEnum::get_enum_names(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 67d2340a9a..30b2ead516 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5510,22 +5510,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(2)); - def = this->add("ixex_primary_col", coInt); - def->label = L("Primary Tool Column"); - def->tooltip = L("X index (0-based) of the primary toolhead. 0 = leftmost column. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - - def = this->add("ixex_primary_row", coInt); - def->label = L("Primary Tool Row"); - def->tooltip = L("Y index (0-based) of the primary toolhead's gantry. 0 = front gantry. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - def = this->add("ixex_tool_layout", coString); def->label = L("Tool 0 Corner"); def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 83d8b1ab97..bdc384b023 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1338,8 +1338,6 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, is_ixex)) ((ConfigOptionInt, ixex_gantry_count)) ((ConfigOptionInt, ixex_tools_per_gantry)) - ((ConfigOptionInt, ixex_primary_col)) - ((ConfigOptionInt, ixex_primary_row)) ((ConfigOptionFloat, ixex_nozzle_clearance_x)) ((ConfigOptionFloat, ixex_nozzle_clearance_y)) ((ConfigOptionStrings, ixex_mode_names)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b4e2a9627c..5163580c30 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1533,7 +1533,13 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) if (is_ixex_opt && is_ixex_opt->value) { const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); - const std::string mode = mode_opt ? mode_opt->value : "primary"; + std::string mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode overrides the process preset. + if (auto* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) { + std::string plate_mode = plate->get_ixex_mode(); + if (plate_mode != "primary") + mode = plate_mode; + } if (mode != m_ixex_last_mode) { m_sequential_view.m_ixex_secondary_markers.clear(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 5e23a9d4ae..66dc76661d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4648,9 +4648,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } //BBS change plate selection + bool plate_icon_popup_shown = false; if (!m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !m_mouse.dragging) { int hover_idx = m_hover_plate_idxs.front(); - wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true); + plate_icon_popup_shown = (wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true) == 1); if (m_hover_volume_idxs.empty()) deselect_all(); render(); @@ -4670,10 +4671,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_mouse.ignore_right_up && m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) { //BBS post right click event - if (!m_hover_plate_idxs.empty()) { + if (!m_hover_plate_idxs.empty() && !plate_icon_popup_shown) { post_event(RBtnPlateEvent(EVT_GLCANVAS_PLATE_RIGHT_CLICK, { logical_pos, m_hover_plate_idxs.front() })); } - else { + else if (!plate_icon_popup_shown) { // do not post the event if the user is panning the scene // or if right click was done over the wipe tower bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 9d1d778504..b4ec570d00 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -542,9 +542,16 @@ void ArrangeJob::process(Ctl &ctl) // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only // and treat carriage collision strips as hard excluded regions. + // NOTE: m_ixex_primary_zone_box and ixex_collision_zones() are in global (world) coordinates + // because they are derived from m_shape which includes the plate origin offset. The arranger + // always works in plate-local space (origin = 0,0), so we subtract the plate origin here. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { if (auto pz = curr_plate->ixex_primary_zone()) { - BoundingBox scaled_pz = scaled(*pz); + Vec3d plate_origin = curr_plate->get_origin(); + double ox = plate_origin.x(), oy = plate_origin.y(); + BoundingBoxf pz_local(Vec2d(pz->min.x() - ox, pz->min.y() - oy), + Vec2d(pz->max.x() - ox, pz->max.y() - oy)); + BoundingBox scaled_pz = scaled(pz_local); bedpts = { { scaled_pz.min.x(), scaled_pz.min.y() }, { scaled_pz.max.x(), scaled_pz.min.y() }, @@ -553,10 +560,10 @@ void ArrangeJob::process(Ctl &ctl) }; for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { Polygon poly({ - { scaled(cz.min.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.max.y()) }, - { scaled(cz.min.x()), scaled(cz.max.y()) }, + { scaled(cz.min.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.max.y() - oy) }, + { scaled(cz.min.x() - ox), scaled(cz.max.y() - oy) }, }); arrangement::ArrangePolygon ap; ap.poly.contour = poly; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 9d7b6d4e5f..9ea79e3643 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -365,6 +365,34 @@ std::vector PartPlate::get_plate_wrapping_detection_area() const return std::vector(); } +std::string PartPlate::get_ixex_mode() const +{ + if (m_config.has("ixex_parallel_mode")) { + auto* opt = m_config.option("ixex_parallel_mode"); + if (opt && !opt->value.empty()) + return opt->value; + } + return "primary"; +} + +void PartPlate::set_ixex_mode(const std::string& mode) +{ + if (mode.empty() || mode == "primary") { + m_config.erase("ixex_parallel_mode"); + } else { + m_config.set_key_value("ixex_parallel_mode", new ConfigOptionString(mode)); + } + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; // force zone rebuild +} + +void PartPlate::reset_ixex_mode() +{ + m_config.erase("ixex_parallel_mode"); + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; +} + void PartPlate::set_spiral_vase_mode(bool spiral_mode, bool as_global) { std::string key = "spiral_mode"; @@ -475,9 +503,14 @@ void PartPlate::calc_ixex_zones() if (!is_ixex_opt || !is_ixex_opt->value) return; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); - std::string active_mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode takes priority over the process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } if (active_mode == "primary" || active_mode.empty()) return; @@ -767,7 +800,8 @@ void PartPlate::calc_ixex_zones() } // Build a cache key from the current iXex config options, or "" if iXex is off. -static std::string build_ixex_cache_key() +// Reads per-plate mode from m_config first, falling back to the process preset. +std::string PartPlate::build_ixex_cache_key() const { if (!wxGetApp().preset_bundle) return ""; @@ -775,14 +809,20 @@ static std::string build_ixex_cache_key() auto* is_ixex_opt = printer_cfg.option("is_ixex"); if (!is_ixex_opt || !is_ixex_opt->value) return ""; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + // Per-plate mode takes priority over process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); - return (mode_opt ? mode_opt->value : "primary") + 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) + "|cw" + std::to_string(cw_opt ? (int)cw_opt->value : 0) @@ -1643,6 +1683,20 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture); + // iXex mode icon — only when is_ixex is active + { + PresetBundle* pb = wxGetApp().preset_bundle; + auto* is_ixex_opt = pb ? pb->printers.get_edited_preset().config.option("is_ixex") : nullptr; + if (is_ixex_opt && is_ixex_opt->value) { + if (hover_id == (int)PLATE_IXEX_MODE_ID) { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_hovered_texture); + std::string cur = get_ixex_mode(); + show_tooltip(_u8L("iXex mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); + } else { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_texture); + } + } + } if (m_partplate_list->render_plate_settings) { bool has_plate_settings = get_bed_type() != BedType::btDefault || get_print_seq() != PrintSequence::ByDefault || !get_first_layer_print_sequence().empty() || !get_other_layers_print_sequence().empty() || has_spiral_mode_config(); @@ -1952,6 +2006,13 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) bool dual_bbl = (preset && preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); + + // Register iXex mode icon only when iXex is active on the current printer preset. + if (preset) { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + register_model_for_picking(canvas, m_ixex_mode_icon, picking_id_component(PLATE_IXEX_MODE_ID)); + } } int PartPlate::picking_id_component(int idx) const @@ -3604,6 +3665,11 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co dual_bbl = (preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); calc_vertex_for_icons(dual_bbl ? 5 : 6, m_plate_filament_map_icon); calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); + { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + calc_vertex_for_icons(dual_bbl ? 7 : 6, m_ixex_mode_icon); + } calc_vertex_for_number(0, false, m_plate_idx_icon); // calc vertex for plate name @@ -4473,6 +4539,20 @@ void PartPlateList::generate_icon_textures() } } + // iXex mode icon textures (fall back gracefully if SVG not present yet) + { + file_name = path + (m_is_dark ? "plate_ixex_mode_dark.svg" : "plate_ixex_mode.svg"); + if (!m_ixex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode icon)") % file_name; + } + } + { + file_name = path + (m_is_dark ? "plate_ixex_mode_hover_dark.svg" : "plate_ixex_mode_hover.svg"); + if (!m_ixex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode hover icon)") % file_name; + } + } + std::string text_str = "01"; // ORCA also scale font size to prevent low res texture int size = wxGetApp().em_unit() * PARTPLATE_ICON_SIZE; @@ -4516,6 +4596,8 @@ void PartPlateList::release_icon_textures() m_plate_set_filament_map_hovered_texture.reset(); m_plate_name_edit_texture.reset(); m_plate_name_edit_hovered_texture.reset(); + m_ixex_mode_texture.reset(); + m_ixex_mode_hovered_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 4954e6f3b5..773524c77c 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -155,6 +155,7 @@ private: PickingModel m_plate_filament_map_icon; PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; + PickingModel m_ixex_mode_icon; GLModel m_plate_idx_icon; GLTexture m_texture; @@ -188,6 +189,7 @@ private: void calc_height_limit(); void calc_ixex_zones(); void ensure_ixex_zones(); + std::string build_ixex_cache_key() const; void render_ixex_zones(bool force_default_color); 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); @@ -220,7 +222,8 @@ private: public: static constexpr unsigned int PLATE_NAME_HOVER_ID = 6; static constexpr unsigned int PLATE_FILAMENT_MAP_ID = 8; - static constexpr unsigned int GRABBER_COUNT = 9; + static constexpr unsigned int PLATE_IXEX_MODE_ID = 9; + static constexpr unsigned int GRABBER_COUNT = 10; static ColorRGBA SELECT_COLOR; static ColorRGBA UNSELECT_COLOR; @@ -280,6 +283,10 @@ public: bool get_spiral_vase_mode() const; void set_spiral_vase_mode(bool spiral_mode, bool as_global); + std::string get_ixex_mode() const; + void set_ixex_mode(const std::string& mode); + void reset_ixex_mode(); + std::vector get_plate_wrapping_detection_area() const; //static const int plate_x_offset = 20; //mm @@ -627,6 +634,8 @@ class PartPlateList : public ObjectBase GLTexture m_plate_set_filament_map_hovered_texture; GLTexture m_plate_name_edit_texture; GLTexture m_plate_name_edit_hovered_texture; + GLTexture m_ixex_mode_texture; + GLTexture m_ixex_mode_hovered_texture; GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6c19a0ae43..f1b985120a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17640,7 +17640,61 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi update(); p->partplate_list.select_plate(0); } + else if (action == (int)PartPlate::PLATE_IXEX_MODE_ID) + { + ret = select_plate(plate_index); + if (!ret) { + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + // Build ordered mode list: "primary" first, then all named modes. + std::vector modes; + modes.push_back("primary"); + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* names_opt = printer_cfg.option("ixex_mode_names"); + if (names_opt) { + for (const auto& n : names_opt->values) + if (!n.empty()) modes.push_back(n); + } + if (right_click) { + // Show a popup menu with all modes. + wxMenu menu; + std::string current = curr_plate->get_ixex_mode(); + for (size_t i = 0; i < modes.size(); ++i) { + wxMenuItem* item = menu.AppendRadioItem(wxID_HIGHEST + (int)i, from_u8(modes[i])); + if (modes[i] == current) + item->Check(true); + } + menu.Bind(wxEVT_MENU, [this, curr_plate, &modes](wxCommandEvent& e) { + int idx = e.GetId() - wxID_HIGHEST; + if (idx >= 0 && idx < (int)modes.size()) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(modes[idx]); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + }); + p->view3D->get_canvas3d()->get_wxglcanvas()->PopupMenu(&menu); + ret = 1; // signal to caller: popup was shown, suppress plate context menu + } else { + // Left-click: cycle to next mode. + std::string current = curr_plate->get_ixex_mode(); + auto it = std::find(modes.begin(), modes.end(), current); + size_t next_idx = (it == modes.end()) ? 0 : ((it - modes.begin() + 1) % modes.size()); + std::string next_mode = modes[next_idx]; + if (next_mode != current) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(next_mode); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + } + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6a3b0a7444..7e02c63114 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2675,26 +2675,6 @@ void TabPrint::build() optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); optgroup->append_single_option_line("enable_wrapping_detection"); - // Dynamic combo box — choices come from the printer preset's ixex_mode_names. - // Use create_line_with_widget so the undo bitmaps are set (has_undo_ui()=true), - // ensuring the widget x-position matches other option fields in OG_CustomCtrl. - create_line_with_widget(optgroup.get(), "ixex_parallel_mode", "", [this](wxWindow* parent) -> wxSizer* { - m_ixex_mode_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(12 * wxGetApp().em_unit(), -1), - 0, nullptr, wxCB_READONLY); - m_ixex_mode_combo->GetDropDown().SetUseContentWidth(true); - refresh_ixex_mode_combo(); - m_ixex_mode_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - if (!m_ixex_mode_combo || !m_config) return; - std::string val = m_ixex_mode_combo->GetStringSelection().ToStdString(); - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString(val)); - on_value_change("ixex_parallel_mode", val); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_mode_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin"); @@ -2753,36 +2733,6 @@ void TabPrint::reload_config() { this->compatible_widget_reload(m_compatible_printers); Tab::reload_config(); - refresh_ixex_mode_combo(); -} - -void TabPrint::refresh_ixex_mode_combo() -{ - if (!m_ixex_mode_combo) return; - // Rebuild choices: "primary" + names from the active printer preset - m_ixex_mode_combo->Clear(); - m_ixex_mode_combo->Append("primary"); - if (m_preset_bundle) { - auto* names = m_preset_bundle->printers.get_edited_preset().config - .option("ixex_mode_names"); - if (names) { - for (const auto& n : names->values) - if (!n.empty()) m_ixex_mode_combo->Append(wxString::FromUTF8(n)); - } - } - // Restore current selection from process config (safe: option may not exist in older presets) - std::string cur = "primary"; - if (m_config) { - auto* opt = m_config->option("ixex_parallel_mode"); - if (opt) cur = opt->value; - } - if (!m_ixex_mode_combo->SetStringSelection(cur)) { - m_ixex_mode_combo->SetSelection(0); // fall back to "primary" - // The active mode no longer exists — reset config so stale name doesn't crash downstream. - if (m_config) - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString("primary")); - on_value_change("ixex_parallel_mode", std::string("primary")); - } } void TabPrint::update_description_lines() @@ -2904,7 +2854,6 @@ void TabPrint::clear_pages() m_recommended_thin_wall_thickness_description_line = nullptr; m_top_bottom_shell_thickness_explanation = nullptr; - m_ixex_mode_combo = nullptr; } //BBS: GUI refactor @@ -5060,9 +5009,6 @@ void TabPrinter::build_fff() m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); update_dirty(); on_value_change("ixex_mode_names", std::string("")); - // Refresh the process tab's mode dropdown immediately - if (auto* print_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINT))) - print_tab->refresh_ixex_mode_combo(); }; auto* sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 687cfeea73..7d86278db7 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -464,12 +464,10 @@ public: void update() override; void clear_pages() override; bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } - void refresh_ixex_mode_combo(); private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr; - ComboBox* m_ixex_mode_combo {nullptr}; }; class TabPrintModel : public TabPrint From f540cf87a88ce88468f12f15f0f826f8ee9a0d3f Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Wed, 8 Apr 2026 23:35:32 -0400 Subject: [PATCH 015/154] fix: preserve iXex tool assignments across gantry count changes - Add all_tool_states map to IXexModesCtrl Row, storing all tool assignments including those not currently visible due to grid size - active_tools_string() now serializes from all_tool_states so off-screen assignments survive the round trip through a smaller grid - Button clicks keep all_tool_states in sync with visible btn_states - Display anchors to the gantry row containing the Primary assignment so reducing gantry count keeps the meaningful row visible rather than always defaulting to row 0 Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 7e02c63114..d85a1278df 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4519,6 +4519,9 @@ private: std::vector btn_states; // parallel to btns // tool_idx for btns[i]: computed during add_row, stored here for quick lookup 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; }; void add_row(const std::string& name = "", @@ -4536,6 +4539,7 @@ private: auto* grid_sizer = new wxGridSizer(m_n_rows, m_n_cols, 2, 2); auto tool_states = parse_tool_states(active_tools); + r.all_tool_states = tool_states; // preserve all assignments, including off-screen tools wxPanel* this_panel = r.panel; // Buttons rendered top=rear (high Y), bottom=front (low Y). @@ -4545,11 +4549,21 @@ private: // Display iterates: row from n_rows-1 down to 0 (rear→front = top→bottom). // raw_row = flip_y ? (n_rows-1-row) : row // raw_col = flip_x ? (n_cols-1-col) : col + // + // row_start: anchor displayed rows to the gantry row containing the Primary + // assignment, so reducing gantry count keeps the meaningful row visible. + int primary_gantry_row = 0; + for (const auto& [idx, state] : tool_states) { + if (state == 1) { primary_gantry_row = idx / m_n_cols; break; } + } + int row_start = primary_gantry_row; // display rows [row_start .. row_start+m_n_rows-1] + bool flip_x = (m_layout == 1 || m_layout == 3); bool flip_y = (m_layout == 2 || m_layout == 3); for (int row = m_n_rows - 1; row >= 0; --row) { for (int col = 0; col < m_n_cols; ++col) { int raw_row = flip_y ? (m_n_rows - 1 - row) : row; + raw_row += row_start; // anchor to Primary's gantry row int raw_col = flip_x ? (m_n_cols - 1 - col) : col; int tool_idx = raw_row * m_n_cols + raw_col; int state = 0; @@ -4581,6 +4595,13 @@ private: if (st == 1 && other_primary) st = 2; // skip Primary → go straight to Copy + // Keep all_tool_states in sync so off-screen tools are preserved + int tidx = row_ref.btn_tool_idx[btn_pos]; + if (st == 0) + row_ref.all_tool_states.erase(tidx); + else + row_ref.all_tool_states[tidx] = st; + apply_btn(row_ref.btns[btn_pos], row_ref.btn_tool_idx[btn_pos], st); notify(); break; @@ -4635,11 +4656,13 @@ private: } std::string active_tools_string(const Row& r) const { + // Serialize from all_tool_states (not just visible buttons) so assignments + // for off-screen tools are preserved across grid size changes. std::string s; - for (size_t i = 0; i < r.btn_states.size(); ++i) { - if (r.btn_states[i] == 0) continue; + for (const auto& [idx, state] : r.all_tool_states) { + if (state == 0) continue; if (!s.empty()) s += ','; - s += std::to_string(r.btn_tool_idx[i]) + ':' + state_role(r.btn_states[i]); + s += std::to_string(idx) + ':' + state_role(state); } return s; } From 461c69c83e76f7a92f44a8d9e598bf6ff18c09fa Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 9 Apr 2026 14:11:06 -0400 Subject: [PATCH 016/154] =?UTF-8?q?refactor:=20rename=20iXex=20=E2=86=92?= =?UTF-8?q?=20IMEX=20throughout;=20use=20IDEX/IQEX=20as=20user-facing=20la?= =?UTF-8?q?bel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All config keys: ixex_* → imex_* (is_imex, imex_gantry_count, etc.) - All C++ identifiers: IXexModesCtrl → IMEXModesCtrl, m_ixex_* → m_imex_*, etc. - 3MF serialization key: ixex_parallel_mode → imex_parallel_mode - UI strings: iXex → IDEX/IQEX - SVG icons: plate_ixex_mode*.svg → plate_imex_mode*.svg - Remove unused ixex_mode.svg Breaking change for existing printer configs — acceptable pre-merge. Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_imex_mode.svg | 10 + resources/images/plate_imex_mode_dark.svg | 10 + resources/images/plate_imex_mode_hover.svg | 10 + .../images/plate_imex_mode_hover_dark.svg | 10 + src/libslic3r/Format/bbs_3mf.cpp | 12 +- src/libslic3r/GCode.cpp | 20 +- src/libslic3r/Preset.cpp | 12 +- src/libslic3r/PrintConfig.cpp | 46 ++-- src/libslic3r/PrintConfig.hpp | 22 +- src/slic3r/GUI/GCodeViewer.cpp | 98 ++++---- src/slic3r/GUI/GCodeViewer.hpp | 6 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 10 +- src/slic3r/GUI/PartPlate.cpp | 218 +++++++++--------- src/slic3r/GUI/PartPlate.hpp | 54 ++--- src/slic3r/GUI/Plater.cpp | 24 +- src/slic3r/GUI/Tab.cpp | 144 ++++++------ src/slic3r/GUI/Tab.hpp | 8 +- 17 files changed, 377 insertions(+), 337 deletions(-) create mode 100644 resources/images/plate_imex_mode.svg create mode 100644 resources/images/plate_imex_mode_dark.svg create mode 100644 resources/images/plate_imex_mode_hover.svg create mode 100644 resources/images/plate_imex_mode_hover_dark.svg diff --git a/resources/images/plate_imex_mode.svg b/resources/images/plate_imex_mode.svg new file mode 100644 index 0000000000..30a67cdf35 --- /dev/null +++ b/resources/images/plate_imex_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_imex_mode_dark.svg b/resources/images/plate_imex_mode_dark.svg new file mode 100644 index 0000000000..981fd88447 --- /dev/null +++ b/resources/images/plate_imex_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_imex_mode_hover.svg b/resources/images/plate_imex_mode_hover.svg new file mode 100644 index 0000000000..9463e767df --- /dev/null +++ b/resources/images/plate_imex_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_imex_mode_hover_dark.svg b/resources/images/plate_imex_mode_hover_dark.svg new file mode 100644 index 0000000000..301fa995ab --- /dev/null +++ b/resources/images/plate_imex_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index b7513ea240..4fbb00018d 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -333,7 +333,7 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence"; static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums"; static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode"; -static constexpr const char* IXEX_PARALLEL_MODE_ATTR = "ixex_parallel_mode"; +static constexpr const char* IMEX_PARALLEL_MODE_ATTR = "imex_parallel_mode"; static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode"; static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps"; static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps"; @@ -4293,8 +4293,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::istringstream(value) >> std::boolalpha >> spiral_mode; m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode)); } - else if (key == IXEX_PARALLEL_MODE_ATTR) { - m_curr_plater->config.set_key_value("ixex_parallel_mode", new ConfigOptionString(value)); + else if (key == IMEX_PARALLEL_MODE_ATTR) { + m_curr_plater->config.set_key_value("imex_parallel_mode", new ConfigOptionString(value)); } else if (key == FILAMENT_MAP_MODE_ATTR) { @@ -7778,9 +7778,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n"; { - auto* ixex_mode_opt = plate_data->config.option("ixex_parallel_mode"); - if (ixex_mode_opt && !ixex_mode_opt->value.empty() && ixex_mode_opt->value != "primary") - stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IXEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << ixex_mode_opt->value << "\"/>\n"; + auto* imex_mode_opt = plate_data->config.option("imex_parallel_mode"); + if (imex_mode_opt && !imex_mode_opt->value.empty() && imex_mode_opt->value != "primary") + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IMEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << imex_mode_opt->value << "\"/>\n"; } //filament map related diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 87a0ee1342..ec249a6ae5 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3054,18 +3054,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } - // iXex: inject mode G-code before machine_start_gcode so the carriage mode + // IDEX/IQEX: inject mode G-code before machine_start_gcode so the carriage mode // macro is active before any motion, homing, or probing in start G-code. - if (print.config().is_ixex.value && !print.objects().empty()) { - const std::string& ixex_mode = print.objects().front()->config().ixex_parallel_mode.value; - if (!ixex_mode.empty() && ixex_mode != "primary") { - const auto& mode_names = print.config().ixex_mode_names.values; - const auto& mode_gcodes = print.config().ixex_mode_gcodes.values; + 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") { + 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) { - if (mode_names[i] == ixex_mode && !mode_gcodes[i].empty()) { - std::string ixex_gcode = this->placeholder_parser_process( - "ixex_mode_gcode", mode_gcodes[i], initial_extruder_id); - file.writeln(ixex_gcode); + if (mode_names[i] == imex_mode && !mode_gcodes[i].empty()) { + std::string imex_gcode = this->placeholder_parser_process( + "imex_mode_gcode", mode_gcodes[i], initial_extruder_id); + file.writeln(imex_gcode); break; } } diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index adb56b90ac..fc28d8e5ca 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -961,8 +961,8 @@ static std::vector s_Preset_print_options { "enable_wrapping_detection", "seam_slope_type", "seam_slope_conditional", "scarf_angle_threshold", "scarf_joint_speed", "scarf_joint_flow_ratio", "seam_slope_start_height", "seam_slope_entire_loop", "seam_slope_min_length", "seam_slope_steps", "seam_slope_inner_walls", "scarf_overhang_threshold", "interlocking_beam", "interlocking_orientation", "interlocking_beam_layer_count", "interlocking_depth", "interlocking_boundary_avoidance", "interlocking_beam_width","calib_flowrate_topinfill_special_order", - // iXex parallel print mode (per-print selection) - "ixex_parallel_mode", + // IDEX/IQEX parallel print mode (per-print selection) + "imex_parallel_mode", }; static std::vector s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type", @@ -1040,10 +1040,10 @@ static std::vector s_Preset_printer_options { "z_offset", "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", "bed_temperature_formula", "nozzle_flush_dataset", - // iXex (independent X extruder) — printer capability and user-defined modes - "is_ixex", "ixex_gantry_count", "ixex_tools_per_gantry", "ixex_tool_layout", - "ixex_nozzle_clearance_x", "ixex_nozzle_clearance_y", "ixex_carriage_margin", "ixex_viz_theme", - "ixex_mode_names", "ixex_mode_active_tools", "ixex_mode_gcodes" + // IDEX/IQEX (independent X extruder) — printer capability and user-defined modes + "is_imex", "imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout", + "imex_nozzle_clearance_x", "imex_nozzle_clearance_y", "imex_carriage_margin", "imex_viz_theme", + "imex_mode_names", "imex_mode_active_tools", "imex_mode_gcodes" }; static std::vector s_Preset_sla_print_options { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 30b2ead516..b4978b1263 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5487,14 +5487,14 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); - // iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers - def = this->add("is_ixex", coBool); - def->label = L("iXex Printer"); - def->tooltip = L("Enable iXex (independent X extruder) support for IDEX/IQEX printers with multiple independent carriages."); + // IDEX/IQEX (independent X extruder) — parallel printing support for IDEX/IQEX printers + def = this->add("is_imex", coBool); + def->label = L("IDEX/IQEX Printer"); + def->tooltip = L("Enable IDEX/IQEX (independent X extruder) support for IDEX/IQEX printers with multiple independent carriages."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); - def = this->add("ixex_gantry_count", coInt); + def = this->add("imex_gantry_count", coInt); def->label = L("Gantry Count"); def->tooltip = L("Number of independent Y-axis gantries. 1 for IDEX/single-rail IQEX. 2 for dual-gantry systems (divides bed into rows)."); def->min = 1; @@ -5502,7 +5502,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(1)); - def = this->add("ixex_tools_per_gantry", coInt); + def = this->add("imex_tools_per_gantry", coInt); def->label = L("Tools per Gantry"); def->tooltip = L("Number of independent toolheads per gantry along X. 2 for IDEX-style."); def->min = 1; @@ -5510,7 +5510,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(2)); - def = this->add("ixex_tool_layout", coString); + def = this->add("imex_tool_layout", coString); 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. " @@ -5518,7 +5518,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionString("front-left")); - def = this->add("ixex_nozzle_clearance_x", coFloat); + def = this->add("imex_nozzle_clearance_x", coFloat); def->label = L("Nozzle Clearance X"); def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in X. Used to calculate the width of the collision exclusion strip at each X boundary."); def->sidetext = L("mm"); @@ -5526,7 +5526,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(30.0)); - def = this->add("ixex_nozzle_clearance_y", coFloat); + def = this->add("imex_nozzle_clearance_y", coFloat); def->label = L("Nozzle Clearance Y"); def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in Y. Used to calculate the width of the collision exclusion strip at each Y boundary."); def->sidetext = L("mm"); @@ -5534,7 +5534,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(30.0)); - def = this->add("ixex_carriage_margin", coFloat); + def = this->add("imex_carriage_margin", coFloat); def->label = L("Safety Margin"); def->tooltip = L("Non-blocking advisory clearance strip (mm) drawn inside the primary zone at each carriage boundary. Parts placed within this strip will still slice; it is a visual reminder to leave extra clearance near the zone edge."); def->sidetext = L("mm"); @@ -5543,32 +5543,32 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.0)); - def = this->add("ixex_viz_theme", coString); + def = this->add("imex_viz_theme", coString); def->label = L("Visualization Theme"); - def->tooltip = L("Color theme for iXex bed zone visualization. Choose a colorblind-friendly theme if needed."); + 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 = this->add("ixex_parallel_mode", coString); - def->label = L("iXex Print Mode"); - def->tooltip = L("Name of the active iXex parallel print mode, or \"primary\" for single-carriage printing. Requires iXex Printer enabled in the Printer preset (Printer \u2192 iXex tab)."); + def = this->add("imex_parallel_mode", coString); + def->label = L("IDEX/IQEX Print Mode"); + def->tooltip = L("Name of the active IDEX/IQEX parallel print mode, or \"primary\" for single-carriage printing. Requires IDEX/IQEX Printer enabled in the Printer preset (Printer \u2192 IDEX/IQEX tab)."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionString("primary")); - def = this->add("ixex_mode_names", coStrings); - def->label = L("iXex Mode Names"); - def->tooltip = L("Display names for each user-defined iXex parallel print mode."); + def = this->add("imex_mode_names", coStrings); + def->label = L("IDEX/IQEX Mode Names"); + def->tooltip = L("Display names for each user-defined IDEX/IQEX parallel print mode."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings()); - def = this->add("ixex_mode_active_tools", coStrings); - def->label = L("iXex Mode Active Tools"); - def->tooltip = L("Tool role assignments for each mode. Format: \"idx:P,idx:C,idx:M\" where P=Primary, C=Copy, M=Mirror (e.g. \"0:P,1:C,2:M,3:M\"). Managed by the iXex Modes editor in the Printer preset."); + def = this->add("imex_mode_active_tools", coStrings); + def->label = L("IDEX/IQEX Mode Active Tools"); + def->tooltip = L("Tool role assignments for each mode. Format: \"idx:P,idx:C,idx:M\" where P=Primary, C=Copy, M=Mirror (e.g. \"0:P,1:C,2:M,3:M\"). Managed by the IDEX/IQEX Modes editor in the Printer preset."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings()); - def = this->add("ixex_mode_gcodes", coStrings); - def->label = L("iXex Mode G-codes"); + def = this->add("imex_mode_gcodes", coStrings); + def->label = L("IDEX/IQEX Mode G-codes"); def->tooltip = L("G-code or macro call to inject at print start for each mode."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings()); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index bdc384b023..f0b414e44f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1035,8 +1035,8 @@ PRINT_CONFIG_CLASS_DEFINE( // Orca: internal use only ((ConfigOptionBool, calib_flowrate_topinfill_special_order)) // ORCA: special flag for flow rate calibration - // iXex parallel print mode (per-print selection, stores mode name or "primary") - ((ConfigOptionString, ixex_parallel_mode)) + // IDEX/IQEX parallel print mode (per-print selection, stores mode name or "primary") + ((ConfigOptionString, imex_parallel_mode)) ) // This object is mapped to Perl as Slic3r::Config::PrintRegion. @@ -1334,15 +1334,15 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionStrings, filament_start_gcode)) ((ConfigOptionBool, single_extruder_multi_material)) ((ConfigOptionBool, manual_filament_change)) - // iXex (independent X extruder) — parallel printing support for IDEX/IQEX printers - ((ConfigOptionBool, is_ixex)) - ((ConfigOptionInt, ixex_gantry_count)) - ((ConfigOptionInt, ixex_tools_per_gantry)) - ((ConfigOptionFloat, ixex_nozzle_clearance_x)) - ((ConfigOptionFloat, ixex_nozzle_clearance_y)) - ((ConfigOptionStrings, ixex_mode_names)) - ((ConfigOptionStrings, ixex_mode_active_tools)) - ((ConfigOptionStrings, ixex_mode_gcodes)) + // IDEX/IQEX (independent X extruder) — parallel printing support for IDEX/IQEX printers + ((ConfigOptionBool, is_imex)) + ((ConfigOptionInt, imex_gantry_count)) + ((ConfigOptionInt, imex_tools_per_gantry)) + ((ConfigOptionFloat, imex_nozzle_clearance_x)) + ((ConfigOptionFloat, imex_nozzle_clearance_y)) + ((ConfigOptionStrings, imex_mode_names)) + ((ConfigOptionStrings, imex_mode_active_tools)) + ((ConfigOptionStrings, imex_mode_gcodes)) ((ConfigOptionBool, single_extruder_multi_material_priming)) ((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionString, change_filament_gcode)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 5163580c30..f64d9f5e4b 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -913,8 +913,8 @@ void GCodeViewer::SequentialView::render(const bool has_render_path, float legen // marker.set_world_offset(current_offset); marker.render(canvas_width, canvas_height, view_type); marker.render_position_window(viewer, canvas_width, canvas_height, view_type); - // iXex secondary carriage markers - for (auto& sec : m_ixex_secondary_markers) + // IDEX/IQEX secondary carriage markers + for (auto& sec : m_imex_secondary_markers) sec.render(canvas_width, canvas_height, view_type); } @@ -1511,7 +1511,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position)); m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f); - // iXex: compute all carriage positions; update secondary nozzle markers; + // IDEX/IQEX: compute all carriage positions; update secondary nozzle markers; // carriage_box_draws is populated for toolhead footprint rendering after sequential_view.render(). // Okabe-Ito colorblind-safe palette — excludes orange (#E69F00) and sky blue (#56B4E9) // which are used by the bed zone fills, ensuring the markers contrast against the background. @@ -1523,41 +1523,41 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) }}; struct CarriageDraw { Vec3f pos; ColorRGBA color; float box_offset_x = 0.0f; float box_offset_y = 0.0f; }; std::vector carriage_box_draws; - float ixex_box_wx = 0.0f, ixex_box_wy = 0.0f; + float imex_box_wx = 0.0f, imex_box_wy = 0.0f; { PresetBundle* preset_bundle = wxGetApp().preset_bundle; - bool ixex_active = false; + bool imex_active = false; if (preset_bundle && m_sequential_view.m_show_marker) { const DynamicPrintConfig& printer_cfg = preset_bundle->printers.get_edited_preset().config; - auto* is_ixex_opt = printer_cfg.opt("is_ixex"); - if (is_ixex_opt && is_ixex_opt->value) { + auto* is_imex_opt = printer_cfg.opt("is_imex"); + if (is_imex_opt && is_imex_opt->value) { const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); + auto* mode_opt = process_cfg.opt("imex_parallel_mode"); std::string mode = mode_opt ? mode_opt->value : "primary"; // Per-plate mode overrides the process preset. if (auto* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) { - std::string plate_mode = plate->get_ixex_mode(); + std::string plate_mode = plate->get_imex_mode(); if (plate_mode != "primary") mode = plate_mode; } - if (mode != m_ixex_last_mode) { - m_sequential_view.m_ixex_secondary_markers.clear(); - m_ixex_last_mode = mode; + if (mode != m_imex_last_mode) { + m_sequential_view.m_imex_secondary_markers.clear(); + m_imex_last_mode = mode; } if (!mode.empty() && mode != "primary") { - auto* mode_names_opt = printer_cfg.opt("ixex_mode_names"); - auto* active_tools_opt = printer_cfg.opt("ixex_mode_active_tools"); + auto* mode_names_opt = printer_cfg.opt("imex_mode_names"); + auto* active_tools_opt = printer_cfg.opt("imex_mode_active_tools"); - auto* tpg_opt = printer_cfg.opt("ixex_tools_per_gantry"); - auto* wx_opt = printer_cfg.opt("ixex_nozzle_clearance_x"); - auto* wy_opt = printer_cfg.opt("ixex_nozzle_clearance_y"); + 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"); int tools_per_gantry = tpg_opt ? std::max(1, tpg_opt->value) : 1; - ixex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f; - ixex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f; + imex_box_wx = wx_opt ? (float)wx_opt->value : 30.0f; + imex_box_wy = wy_opt ? (float)wy_opt->value : 30.0f; - // Parse "idx:P/C/M" format — matches PartPlate::calc_ixex_zones() exactly. + // Parse "idx:P/C/M" format — matches PartPlate::calc_imex_zones() exactly. // Primary = state 1 (P), Copy = 2 (C), Mirror = 3 (M). // Inactive tools (state 0) are not included in the stored string. int pri_tool = -1; @@ -1594,21 +1594,21 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) int sec_count = (int)sec_tool_ids.size(); if (sec_count > 0) { - ixex_active = true; + imex_active = true; // Lazy init secondary nozzle markers - if ((int)m_sequential_view.m_ixex_secondary_markers.size() != sec_count) { - m_sequential_view.m_ixex_secondary_markers.resize(sec_count); + if ((int)m_sequential_view.m_imex_secondary_markers.size() != sec_count) { + m_sequential_view.m_imex_secondary_markers.resize(sec_count); for (int i = 0; i < sec_count; ++i) { - m_sequential_view.m_ixex_secondary_markers[i].init(m_marker_filename); - m_sequential_view.m_ixex_secondary_markers[i].set_color( + m_sequential_view.m_imex_secondary_markers[i].init(m_marker_filename); + m_sequential_view.m_imex_secondary_markers[i].set_color( s_carriage_colors[(i + 1) % s_carriage_colors.size()]); } } // Bed X and Y bounds — read from the current plate's shape, which is // in world/GL coordinates (same space as curr_vertex.position), and is - // the exact same source used by PartPlate::calc_ixex_zones(). + // the exact same source used by PartPlate::calc_imex_zones(). float bed_x_min, bed_x_max, bed_y_min, bed_y_max; { PartPlate* curr_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); @@ -1633,12 +1633,12 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) } } - auto* gc_opt = printer_cfg.opt("ixex_gantry_count"); + auto* gc_opt = printer_cfg.opt("imex_gantry_count"); int gantry_count = gc_opt ? std::max(1, gc_opt->value) : 1; - // Apply the same flip logic as PartPlate::calc_ixex_zones() so physical + // Apply the same flip logic as PartPlate::calc_imex_zones() so physical // grid positions match the bed zone visualization. - auto* layout_opt = printer_cfg.opt("ixex_tool_layout"); + auto* layout_opt = printer_cfg.opt("imex_tool_layout"); const std::string layout_str = layout_opt ? layout_opt->value : "front-left"; const bool flip_x = (layout_str == "front-right" || layout_str == "rear-right"); const bool flip_y = (layout_str == "rear-left" || layout_str == "rear-right"); @@ -1673,15 +1673,15 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) // 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 : -ixex_box_wx; // nozzle at inner edge (facing center of bed) - float pri_box_offset_y = -ixex_box_wy; // nozzle at high-Y edge by default (gantry always behind nozzle); override if secondary is behind + 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 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 = -ixex_box_wx; } + 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 = -ixex_box_wy; } + 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 }); @@ -1719,8 +1719,8 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) } Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() }; - m_sequential_view.m_ixex_secondary_markers[i].set_world_position(sec_pos); - m_sequential_view.m_ixex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f); + 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. @@ -1728,16 +1728,16 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) if (sec_state == 2) { sec_box_offset_x = pri_box_offset_x; } else { - if (sec_phys_col > pri_phys_col) sec_box_offset_x = -ixex_box_wx; + if (sec_phys_col > pri_phys_col) sec_box_offset_x = -imex_box_wx; 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 = -ixex_box_wy; + 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; - else sec_box_offset_y = -ixex_box_wy; + else sec_box_offset_y = -imex_box_wy; carriage_box_draws.push_back({ sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()], sec_box_offset_x, sec_box_offset_y }); @@ -1746,28 +1746,28 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) } } } - if (!ixex_active) - m_sequential_view.m_ixex_secondary_markers.clear(); + if (!imex_active) + m_sequential_view.m_imex_secondary_markers.clear(); } // BBS fixed buttom margin. m_moves_slider.pos_y m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type()); - // iXex: render toolhead footprint boxes for each active carriage. - // Each box is ixex_nozzle_clearance_x × ixex_nozzle_clearance_y, sitting above the nozzle tip. - if (!carriage_box_draws.empty() && ixex_box_wx > 0.0f && ixex_box_wy > 0.0f) { + // IDEX/IQEX: render toolhead footprint boxes for each active carriage. + // Each box is imex_nozzle_clearance_x × imex_nozzle_clearance_y, sitting above the nozzle tip. + if (!carriage_box_draws.empty() && imex_box_wx > 0.0f && imex_box_wy > 0.0f) { // Rebuild box mesh every frame — dimensions can change via config edit without a // G-code reload, so dimension-based caching isn't safe. // Mesh origin: nozzle at x=0, centered in Y, Z starts at nozzle tip level. // Per-carriage box_offset_x shifts the mesh left or right so the nozzle lands // at the correct (collision-side) edge. { - const float box_h = std::max(ixex_box_wx, ixex_box_wy); - indexed_triangle_set its = its_make_cube((double)ixex_box_wx, (double)ixex_box_wy, (double)box_h); + const float box_h = std::max(imex_box_wx, imex_box_wy); + indexed_triangle_set its = its_make_cube((double)imex_box_wx, (double)imex_box_wy, (double)box_h); // No vertex pre-shifting — box_offset_x/y in the per-carriage transform // positions the nozzle at the correct collision-side edge. - m_ixex_toolhead_box.reset(); - m_ixex_toolhead_box.init_from(its); + m_imex_toolhead_box.reset(); + m_imex_toolhead_box.init_from(its); } GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); @@ -1787,8 +1787,8 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); - m_ixex_toolhead_box.set_color(draw.color); - m_ixex_toolhead_box.render(); + m_imex_toolhead_box.set_color(draw.color); + m_imex_toolhead_box.render(); } shader->stop_using(); glsafe(::glDisable(GL_BLEND)); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index ee3c1e1ec7..a8e5607048 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -150,7 +150,7 @@ public: }; Marker marker; - std::vector m_ixex_secondary_markers; // one per active secondary carriage in iXex mode + std::vector m_imex_secondary_markers; // one per active secondary carriage in IDEX/IQEX mode GCodeWindow gcode_window; float m_scale = 1.0; bool m_show_marker = false; @@ -205,8 +205,8 @@ private: ConfigOptionMode m_user_mode; bool m_fold = {false}; std::string m_marker_filename; // cached for lazy secondary marker init - std::string m_ixex_last_mode; // detect mode changes for secondary marker rebuild - GLModel m_ixex_toolhead_box; // shared box mesh for all carriage footprint overlays + std::string m_imex_last_mode; // detect mode changes for secondary marker rebuild + GLModel m_imex_toolhead_box; // shared box mesh for all carriage footprint overlays size_t m_extruders_count; std::vector m_filament_diameters; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index b4ec570d00..218889a2f7 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -540,13 +540,13 @@ void ArrangeJob::process(Ctl &ctl) Points bedpts = get_shrink_bedpts(m_plater->config(),params); - // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only + // When an IDEX/IQEX parallel mode is active, constrain auto-arrange to the primary zone only // and treat carriage collision strips as hard excluded regions. - // NOTE: m_ixex_primary_zone_box and ixex_collision_zones() are in global (world) coordinates + // NOTE: m_imex_primary_zone_box and imex_collision_zones() are in global (world) coordinates // because they are derived from m_shape which includes the plate origin offset. The arranger // always works in plate-local space (origin = 0,0), so we subtract the plate origin here. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { - if (auto pz = curr_plate->ixex_primary_zone()) { + if (auto pz = curr_plate->imex_primary_zone()) { Vec3d plate_origin = curr_plate->get_origin(); double ox = plate_origin.x(), oy = plate_origin.y(); BoundingBoxf pz_local(Vec2d(pz->min.x() - ox, pz->min.y() - oy), @@ -558,7 +558,7 @@ void ArrangeJob::process(Ctl &ctl) { scaled_pz.max.x(), scaled_pz.max.y() }, { scaled_pz.min.x(), scaled_pz.max.y() }, }; - for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { + for (const BoundingBoxf3& cz : curr_plate->imex_collision_zones()) { Polygon poly({ { scaled(cz.min.x() - ox), scaled(cz.min.y() - oy) }, { scaled(cz.max.x() - ox), scaled(cz.min.y() - oy) }, @@ -572,7 +572,7 @@ void ArrangeJob::process(Ctl &ctl) ap.is_virt_object = true; ap.bed_idx = current_plate_index; ap.height = 1; - ap.name = "IXexCollisionZone"; + ap.name = "IMEXCollisionZone"; m_unselected.emplace_back(std::move(ap)); } } diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 88d3b4cf63..17447a6707 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -365,32 +365,32 @@ std::vector PartPlate::get_plate_wrapping_detection_area() const return std::vector(); } -std::string PartPlate::get_ixex_mode() const +std::string PartPlate::get_imex_mode() const { - if (m_config.has("ixex_parallel_mode")) { - auto* opt = m_config.option("ixex_parallel_mode"); + if (m_config.has("imex_parallel_mode")) { + auto* opt = m_config.option("imex_parallel_mode"); if (opt && !opt->value.empty()) return opt->value; } return "primary"; } -void PartPlate::set_ixex_mode(const std::string& mode) +void PartPlate::set_imex_mode(const std::string& mode) { if (mode.empty() || mode == "primary") { - m_config.erase("ixex_parallel_mode"); + m_config.erase("imex_parallel_mode"); } else { - m_config.set_key_value("ixex_parallel_mode", new ConfigOptionString(mode)); + m_config.set_key_value("imex_parallel_mode", new ConfigOptionString(mode)); } update_slice_result_valid_state(false); - m_ixex_zones_mode_cache = "\x01"; // force zone rebuild + m_imex_zones_mode_cache = "\x01"; // force zone rebuild } -void PartPlate::reset_ixex_mode() +void PartPlate::reset_imex_mode() { - m_config.erase("ixex_parallel_mode"); + m_config.erase("imex_parallel_mode"); update_slice_result_valid_state(false); - m_ixex_zones_mode_cache = "\x01"; + m_imex_zones_mode_cache = "\x01"; } void PartPlate::set_spiral_vase_mode(bool spiral_mode, bool as_global) @@ -484,30 +484,30 @@ void PartPlate::calc_exclude_triangles(const ExPolygon &poly) // Forward declaration (defined later in this file) static bool init_model_from_lines(GLModel &model, const Lines &lines, float z); -void PartPlate::calc_ixex_zones() +void PartPlate::calc_imex_zones() { - m_ixex_copy_zones.clear(); - m_ixex_mirror_zones.clear(); + m_imex_copy_zones.clear(); + m_imex_mirror_zones.clear(); // Clear blocking vectors at the top so early returns don't leave stale data. - m_ixex_secondary_zone_boxes.clear(); - m_ixex_collision_zones.clear(); - m_ixex_collision_overlay.clear(); - m_ixex_margin_overlay.clear(); - m_ixex_primary_zone_box = std::nullopt; + m_imex_secondary_zone_boxes.clear(); + m_imex_collision_zones.clear(); + m_imex_collision_overlay.clear(); + m_imex_margin_overlay.clear(); + m_imex_primary_zone_box = std::nullopt; if (!wxGetApp().preset_bundle) return; const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto* is_ixex_opt = printer_cfg.option("is_ixex"); - if (!is_ixex_opt || !is_ixex_opt->value) + auto* is_imex_opt = printer_cfg.option("is_imex"); + if (!is_imex_opt || !is_imex_opt->value) return; // Per-plate mode takes priority over the process preset. - std::string active_mode = get_ixex_mode(); + std::string active_mode = get_imex_mode(); if (active_mode == "primary") { const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + auto* mode_opt = process_cfg.option("imex_parallel_mode"); if (mode_opt && !mode_opt->value.empty()) active_mode = mode_opt->value; } @@ -515,9 +515,9 @@ void PartPlate::calc_ixex_zones() return; // Grid dimensions and tool layout from printer config - auto* gantry_opt = printer_cfg.option("ixex_gantry_count"); - auto* tpg_opt = printer_cfg.option("ixex_tools_per_gantry"); - auto* layout_opt = printer_cfg.option("ixex_tool_layout"); + 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; @@ -541,8 +541,8 @@ void PartPlate::calc_ixex_zones() return; // nothing to dim with a single zone // Look up secondary tool indices (active in mode, but NOT the primary tool) - auto* names_opt = printer_cfg.option("ixex_mode_names"); - auto* tools_opt = printer_cfg.option("ixex_mode_active_tools"); + auto* names_opt = printer_cfg.option("imex_mode_names"); + auto* tools_opt = printer_cfg.option("imex_mode_active_tools"); std::string active_tools_str; if (names_opt && tools_opt) { for (size_t i = 0; i < names_opt->values.size(); ++i) { @@ -668,18 +668,18 @@ void PartPlate::calc_ixex_zones() for (const auto& b : make_boxes(cells)) push_zone_fill(vec, b.x0, b.x1, b.y0, b.y1); }; - push_expanded_fills(m_ixex_copy_zones, copy_cells); - push_expanded_fills(m_ixex_mirror_zones, mirror_cells); + push_expanded_fills(m_imex_copy_zones, copy_cells); + push_expanded_fills(m_imex_mirror_zones, mirror_cells); // --- Secondary zone blocking --- // Collect the expanded bounding boxes for all secondary (copy+mirror) zones. // check_outside() uses these to prevent objects being placed outside the primary zone. for (const auto& b : make_boxes(copy_cells)) - m_ixex_secondary_zone_boxes.push_back( + m_imex_secondary_zone_boxes.push_back( BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4))); for (const auto& b : make_boxes(mirror_cells)) - m_ixex_secondary_zone_boxes.push_back( + m_imex_secondary_zone_boxes.push_back( BoundingBoxf3(Vec3d(b.x0, b.y0, -1.0), Vec3d(b.x1, b.y1, 1e4))); // --- Carriage collision danger strips --- @@ -696,9 +696,9 @@ void PartPlate::calc_ixex_zones() // !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("ixex_nozzle_clearance_x"); - auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); - auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); + 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"); double carriage_w = cw_opt ? cw_opt->value : 0.0; double carriage_h = ch_opt ? ch_opt->value : 0.0; double margin = mgn_opt ? mgn_opt->value : 0.0; @@ -714,7 +714,7 @@ void PartPlate::calc_ixex_zones() if (!clipped.empty()) { GLModel m; if (init_model_from_poly(m, clipped.front(), GROUND_Z)) - m_ixex_margin_overlay.push_back(std::move(m)); + m_imex_margin_overlay.push_back(std::move(m)); } }; @@ -723,14 +723,14 @@ void PartPlate::calc_ixex_zones() 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; - m_ixex_primary_zone_box = BoundingBoxf(Vec2d(pz_x0, pz_y0), Vec2d(pz_x1, pz_y1)); + 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 auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) { BoundingBoxf3 box; box.min = Vec3d(sx0, sy0, -1.0); box.max = Vec3d(sx1, sy1, 1e4); - m_ixex_collision_zones.push_back(box); + m_imex_collision_zones.push_back(box); ExPolygon cell; cell.contour.append({ scale_(sx0), scale_(sy0) }); @@ -741,7 +741,7 @@ void PartPlate::calc_ixex_zones() if (!clipped.empty()) { GLModel m; if (init_model_from_poly(m, clipped.front(), GROUND_Z)) - m_ixex_collision_overlay.push_back(std::move(m)); + m_imex_collision_overlay.push_back(std::move(m)); } }; @@ -799,29 +799,29 @@ void PartPlate::calc_ixex_zones() } } -// Build a cache key from the current iXex config options, or "" if iXex is off. +// Build a cache key from the current IDEX/IQEX config options, or "" if IDEX/IQEX is off. // Reads per-plate mode from m_config first, falling back to the process preset. -std::string PartPlate::build_ixex_cache_key() const +std::string PartPlate::build_imex_cache_key() const { if (!wxGetApp().preset_bundle) return ""; const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto* is_ixex_opt = printer_cfg.option("is_ixex"); - if (!is_ixex_opt || !is_ixex_opt->value) + auto* is_imex_opt = printer_cfg.option("is_imex"); + if (!is_imex_opt || !is_imex_opt->value) return ""; // Per-plate mode takes priority over process preset. - std::string active_mode = get_ixex_mode(); + std::string active_mode = get_imex_mode(); if (active_mode == "primary") { const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + auto* mode_opt = process_cfg.option("imex_parallel_mode"); if (mode_opt && !mode_opt->value.empty()) active_mode = mode_opt->value; } - auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); - auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); - auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); - auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); - auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); + 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"); 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) @@ -832,19 +832,19 @@ std::string PartPlate::build_ixex_cache_key() const // 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_ixex_zones() +void PartPlate::ensure_imex_zones() { - std::string cache_key = build_ixex_cache_key(); - if (cache_key != m_ixex_zones_mode_cache) { - m_ixex_zones_mode_cache = cache_key; - calc_ixex_zones(); + std::string cache_key = build_imex_cache_key(); + if (cache_key != m_imex_zones_mode_cache) { + m_imex_zones_mode_cache = cache_key; + calc_imex_zones(); } } -bool PartPlate::has_ixex_placement_violations() +bool PartPlate::has_imex_placement_violations() { - ensure_ixex_zones(); - if (m_ixex_secondary_zone_boxes.empty() && m_ixex_collision_zones.empty()) + ensure_imex_zones(); + if (m_imex_secondary_zone_boxes.empty() && m_imex_collision_zones.empty()) return false; for (const auto& pr : obj_to_instance_set) { int obj_id = pr.first; @@ -855,11 +855,11 @@ bool PartPlate::has_ixex_placement_violations() Polygon hull = instance->convex_hull_2d(); if (hull.points.empty()) continue; - for (const auto& box : m_ixex_secondary_zone_boxes) { + for (const auto& box : m_imex_secondary_zone_boxes) { if (!intersection({box.polygon(true)}, {hull}).empty()) return true; } - for (const auto& strip : m_ixex_collision_zones) { + for (const auto& strip : m_imex_collision_zones) { if (!intersection({strip.polygon(true)}, {hull}).empty()) return true; } @@ -867,15 +867,15 @@ bool PartPlate::has_ixex_placement_violations() return false; } -void PartPlate::render_ixex_zones(bool force_default_color) +void PartPlate::render_imex_zones(bool force_default_color) { if (force_default_color) return; - ensure_ixex_zones(); + ensure_imex_zones(); // Read visualization theme from printer config. - struct IXexTheme { + struct IMEXTheme { ColorRGBA copy; // secondary copy zone fill ColorRGBA mirror; // secondary mirror zone fill ColorRGBA danger; // blocking collision strip @@ -883,7 +883,7 @@ void PartPlate::render_ixex_zones(bool force_default_color) }; // Standard (Okabe-Ito orange + sky blue) - static const IXexTheme k_standard = { + static const IMEXTheme k_standard = { { 0.337f, 0.706f, 0.914f, 0.45f }, // copy — sky blue #56B4E9 { 0.902f, 0.624f, 0.000f, 0.45f }, // mirror — orange #E69F00 { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red @@ -891,7 +891,7 @@ void PartPlate::render_ixex_zones(bool force_default_color) }; // Deuteranopia / Protanopia: avoids red-green confusion. // Danger strip uses strong blue-violet (red invisible to protanopes). - static const IXexTheme k_deuteranopia = { + static const IMEXTheme k_deuteranopia = { { 0.000f, 0.447f, 0.698f, 0.50f }, // copy — blue #0072B2 { 0.941f, 0.894f, 0.259f, 0.50f }, // mirror — yellow #F0E442 { 0.200f, 0.100f, 0.800f, 0.65f }, // danger — blue-violet (red not visible) @@ -899,24 +899,24 @@ void PartPlate::render_ixex_zones(bool force_default_color) }; // Tritanopia: avoids blue-yellow confusion. // Copy uses vermilion, mirror uses reddish-purple, margin uses teal. - static const IXexTheme k_tritanopia = { + static const IMEXTheme k_tritanopia = { { 0.835f, 0.369f, 0.000f, 0.50f }, // copy — vermilion #D55E00 { 0.800f, 0.475f, 0.655f, 0.50f }, // mirror — reddish purple #CC79A7 { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red (visible to tritanopes) { 0.000f, 0.700f, 0.600f, 0.45f }, // margin — teal }; // High contrast: saturated, higher alpha for low-vision users. - static const IXexTheme k_high_contrast = { + static const IMEXTheme k_high_contrast = { { 0.000f, 0.600f, 1.000f, 0.65f }, // copy — vivid blue { 1.000f, 0.600f, 0.000f, 0.65f }, // mirror — vivid orange { 1.000f, 0.000f, 0.000f, 0.75f }, // danger — full red { 0.000f, 1.000f, 0.200f, 0.60f }, // margin — bright green }; - const IXexTheme* theme = &k_standard; + const IMEXTheme* theme = &k_standard; if (wxGetApp().preset_bundle) { const DynamicPrintConfig& pcfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - if (auto* t = pcfg.option("ixex_viz_theme")) { + if (auto* t = pcfg.option("imex_viz_theme")) { if (t->value == "deuteranopia") theme = &k_deuteranopia; else if (t->value == "tritanopia") theme = &k_tritanopia; else if (t->value == "high_contrast") theme = &k_high_contrast; @@ -926,27 +926,27 @@ void PartPlate::render_ixex_zones(bool force_default_color) // Both fills and border lines use the flat shader already active from render(). glsafe(::glDepthMask(GL_FALSE)); - if (!m_ixex_copy_zones.empty()) - for (GLModel& z : m_ixex_copy_zones) + if (!m_imex_copy_zones.empty()) + for (GLModel& z : m_imex_copy_zones) if (z.is_initialized()) { z.set_color(theme->copy); z.render(); } - if (!m_ixex_mirror_zones.empty()) - for (GLModel& z : m_ixex_mirror_zones) + if (!m_imex_mirror_zones.empty()) + for (GLModel& z : m_imex_mirror_zones) if (z.is_initialized()) { z.set_color(theme->mirror); z.render(); } glsafe(::glDepthMask(GL_TRUE)); - if (!m_ixex_collision_overlay.empty()) { + if (!m_imex_collision_overlay.empty()) { glsafe(::glDepthMask(GL_FALSE)); - for (GLModel& s : m_ixex_collision_overlay) + for (GLModel& s : m_imex_collision_overlay) if (s.is_initialized()) { s.set_color(theme->danger); s.render(); } glsafe(::glDepthMask(GL_TRUE)); } - if (!m_ixex_margin_overlay.empty()) { + if (!m_imex_margin_overlay.empty()) { glsafe(::glDepthMask(GL_FALSE)); - for (GLModel& b : m_ixex_margin_overlay) + for (GLModel& b : m_imex_margin_overlay) if (b.is_initialized()) { b.set_color(theme->margin); b.render(); } glsafe(::glDepthMask(GL_TRUE)); } @@ -1683,17 +1683,17 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture); - // iXex mode icon — only when is_ixex is active + // IDEX/IQEX mode icon — only when is_imex is active { PresetBundle* pb = wxGetApp().preset_bundle; - auto* is_ixex_opt = pb ? pb->printers.get_edited_preset().config.option("is_ixex") : nullptr; - if (is_ixex_opt && is_ixex_opt->value) { - if (hover_id == (int)PLATE_IXEX_MODE_ID) { - render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_hovered_texture); - std::string cur = get_ixex_mode(); - show_tooltip(_u8L("iXex mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); + auto* is_imex_opt = pb ? pb->printers.get_edited_preset().config.option("is_imex") : nullptr; + if (is_imex_opt && is_imex_opt->value) { + if (hover_id == (int)PLATE_IMEX_MODE_ID) { + render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_hovered_texture); + std::string cur = get_imex_mode(); + show_tooltip(_u8L("IDEX/IQEX mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); } else { - render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_texture); + render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_texture); } } } @@ -2007,11 +2007,11 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); - // Register iXex mode icon only when iXex is active on the current printer preset. + // Register IDEX/IQEX mode icon only when IDEX/IQEX is active on the current printer preset. if (preset) { - auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); - if (is_ixex_opt && is_ixex_opt->value) - register_model_for_picking(canvas, m_ixex_mode_icon, picking_id_component(PLATE_IXEX_MODE_ID)); + auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex"); + if (is_imex_opt && is_imex_opt->value) + register_model_for_picking(canvas, m_imex_mode_icon, picking_id_component(PLATE_IMEX_MODE_ID)); } } @@ -3024,8 +3024,8 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const //check whether instance is outside the plate or not bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box) { - // Ensure iXex zone geometry is current before any placement check. - ensure_ixex_zones(); + // Ensure IDEX/IQEX zone geometry is current before any placement check. + ensure_imex_zones(); bool outside = true; @@ -3070,14 +3070,14 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi outside = false; } - // iXex placement check (prepare-mode bounding-box test). + // IDEX/IQEX placement check (prepare-mode bounding-box test). // Block objects that overlap secondary (copy/mirror) zones or the carriage // danger strip at the primary zone boundary. Reuses the existing // outside=true → instance_outside_set → update_states() → blocks slicing path. - if (!outside && (!m_ixex_secondary_zone_boxes.empty() || !m_ixex_collision_zones.empty())) { + if (!outside && (!m_imex_secondary_zone_boxes.empty() || !m_imex_collision_zones.empty())) { Polygon obj_hull = instance->convex_hull_2d(); // scaled Clipper coords // 1. Object must not touch any secondary zone. - for (const auto& box : m_ixex_secondary_zone_boxes) { + for (const auto& box : m_imex_secondary_zone_boxes) { Polygon p = box.polygon(true); if (!intersection({ p }, { obj_hull }).empty()) { outside = true; @@ -3086,7 +3086,7 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi } // 2. Object must not enter the carriage danger strip inside the primary zone. if (!outside) { - for (const auto& strip : m_ixex_collision_zones) { + for (const auto& strip : m_imex_collision_zones) { Polygon strip_poly = strip.polygon(true); if (!intersection({ strip_poly }, { obj_hull }).empty()) { outside = true; @@ -3684,8 +3684,8 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co const BoundingBox& pp_bbox = poly.contour.bounding_box(); calc_gridlines(poly, pp_bbox); - m_ixex_zones_mode_cache = "\x01"; // force rebuild on next render - calc_ixex_zones(); + m_imex_zones_mode_cache = "\x01"; // force rebuild on next render + calc_imex_zones(); calc_vertex_for_icons(0, m_del_icon); calc_vertex_for_icons(1, m_orient_icon); @@ -3699,9 +3699,9 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co calc_vertex_for_icons(dual_bbl ? 5 : 6, m_plate_filament_map_icon); calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); { - auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); - if (is_ixex_opt && is_ixex_opt->value) - calc_vertex_for_icons(dual_bbl ? 7 : 6, m_ixex_mode_icon); + 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); } calc_vertex_for_number(0, false, m_plate_idx_icon); @@ -3795,7 +3795,7 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec render_background(force_background_color); render_exclude_area(force_background_color); - render_ixex_zones(force_background_color); + render_imex_zones(force_background_color); if(m_selected && wxGetApp().plater()->get_enable_wrapping_detection()){ if(!m_wrapping_detection_triangles.is_initialized()){ auto points = get_plate_wrapping_detection_area(); @@ -4572,17 +4572,17 @@ void PartPlateList::generate_icon_textures() } } - // iXex mode icon textures (fall back gracefully if SVG not present yet) + // IDEX/IQEX mode icon textures (fall back gracefully if SVG not present yet) { - file_name = path + (m_is_dark ? "plate_ixex_mode_dark.svg" : "plate_ixex_mode.svg"); - if (!m_ixex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode icon)") % file_name; + file_name = path + (m_is_dark ? "plate_imex_mode_dark.svg" : "plate_imex_mode.svg"); + if (!m_imex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IDEX/IQEX mode icon)") % file_name; } } { - file_name = path + (m_is_dark ? "plate_ixex_mode_hover_dark.svg" : "plate_ixex_mode_hover.svg"); - if (!m_ixex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode hover icon)") % file_name; + file_name = path + (m_is_dark ? "plate_imex_mode_hover_dark.svg" : "plate_imex_mode_hover.svg"); + if (!m_imex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IDEX/IQEX mode hover icon)") % file_name; } } @@ -4629,8 +4629,8 @@ void PartPlateList::release_icon_textures() m_plate_set_filament_map_hovered_texture.reset(); m_plate_name_edit_texture.reset(); m_plate_name_edit_hovered_texture.reset(); - m_ixex_mode_texture.reset(); - m_ixex_mode_hovered_texture.reset(); + m_imex_mode_texture.reset(); + m_imex_mode_hovered_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 773524c77c..423de28585 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -136,14 +136,14 @@ private: GLModel m_logo_triangles; GLModel m_gridlines; GLModel m_gridlines_bolder; - std::vector m_ixex_copy_zones; // blue tint (Wong #56B4E9) - std::vector m_ixex_mirror_zones; // orange tint (Wong #E69F00) - std::vector m_ixex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here - std::vector m_ixex_collision_zones; // carriage danger strips inside primary zone (mm) - std::vector m_ixex_collision_overlay; // red-orange rendered fill for danger strips - std::vector m_ixex_margin_overlay; // amber advisory bands just inside collision strips - std::optional m_ixex_primary_zone_box; // primary zone extents in mm; empty when iXex is off/primary-only - std::string m_ixex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt + std::vector m_imex_copy_zones; // blue tint (Wong #56B4E9) + std::vector m_imex_mirror_zones; // orange tint (Wong #E69F00) + std::vector m_imex_secondary_zone_boxes; // secondary (copy/mirror) zone rects — objects blocked here + std::vector m_imex_collision_zones; // carriage danger strips inside primary zone (mm) + std::vector m_imex_collision_overlay; // red-orange rendered fill for danger strips + std::vector m_imex_margin_overlay; // amber advisory bands just inside collision strips + std::optional m_imex_primary_zone_box; // primary zone extents in mm; empty when IDEX/IQEX is off/primary-only + std::string m_imex_zones_mode_cache{ "\x01" }; // cached mode+topology key; sentinel = unbuilt GLModel m_height_limit_common; GLModel m_height_limit_bottom; GLModel m_height_limit_top; @@ -155,7 +155,7 @@ private: PickingModel m_plate_filament_map_icon; PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; - PickingModel m_ixex_mode_icon; + PickingModel m_imex_mode_icon; GLModel m_plate_idx_icon; GLTexture m_texture; @@ -187,10 +187,10 @@ private: void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model); void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox); void calc_height_limit(); - void calc_ixex_zones(); - void ensure_ixex_zones(); - std::string build_ixex_cache_key() const; - void render_ixex_zones(bool force_default_color); + void calc_imex_zones(); + void ensure_imex_zones(); + std::string build_imex_cache_key() const; + void render_imex_zones(bool force_default_color); 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); @@ -222,7 +222,7 @@ private: public: static constexpr unsigned int PLATE_NAME_HOVER_ID = 6; static constexpr unsigned int PLATE_FILAMENT_MAP_ID = 8; - static constexpr unsigned int PLATE_IXEX_MODE_ID = 9; + static constexpr unsigned int PLATE_IMEX_MODE_ID = 9; static constexpr unsigned int GRABBER_COUNT = 10; static ColorRGBA SELECT_COLOR; @@ -283,9 +283,9 @@ public: bool get_spiral_vase_mode() const; void set_spiral_vase_mode(bool spiral_mode, bool as_global); - std::string get_ixex_mode() const; - void set_ixex_mode(const std::string& mode); - void reset_ixex_mode(); + std::string get_imex_mode() const; + void set_imex_mode(const std::string& mode); + void reset_imex_mode(); std::vector get_plate_wrapping_detection_area() const; @@ -451,14 +451,14 @@ public: { return m_ready_for_slice && !m_apply_invalid; } - // Returns true if any instance on this plate overlaps an iXex secondary or collision zone. - bool has_ixex_placement_violations(); - // Returns the primary-zone bounding box in mm when an iXex parallel mode is active. - // Empty (nullopt) when iXex is off or the mode is "primary" (full-bed). - std::optional ixex_primary_zone() { ensure_ixex_zones(); return m_ixex_primary_zone_box; } - // Returns carriage collision strips (mm) for the current iXex mode. - // Always call ixex_primary_zone() first to ensure the cache is warm. - const std::vector& ixex_collision_zones() const { return m_ixex_collision_zones; } + // Returns true if any instance on this plate overlaps an IDEX/IQEX secondary or collision zone. + bool has_imex_placement_violations(); + // Returns the primary-zone bounding box in mm when an IDEX/IQEX parallel mode is active. + // Empty (nullopt) when IDEX/IQEX is off or the mode is "primary" (full-bed). + std::optional imex_primary_zone() { ensure_imex_zones(); return m_imex_primary_zone_box; } + // Returns carriage collision strips (mm) for the current IDEX/IQEX mode. + // Always call imex_primary_zone() first to ensure the cache is warm. + const std::vector& imex_collision_zones() const { return m_imex_collision_zones; } void update_slice_ready_status(bool ready_slice) { m_ready_for_slice = ready_slice; @@ -634,8 +634,8 @@ class PartPlateList : public ObjectBase GLTexture m_plate_set_filament_map_hovered_texture; GLTexture m_plate_name_edit_texture; GLTexture m_plate_name_edit_hovered_texture; - GLTexture m_ixex_mode_texture; - GLTexture m_ixex_mode_hovered_texture; + GLTexture m_imex_mode_texture; + GLTexture m_imex_mode_hovered_texture; GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5c85f75f45..6e5753c546 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7722,13 +7722,13 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool q->post_process_string_object_exception(err); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; - // iXex placement check: if objects overlap secondary zones, treat it as a + // IDEX/IQEX placement check: if objects overlap secondary zones, treat it as a // validation error so the standard pathway handles button state, notifications, // and auto-slice blocking consistently. if (err.string.empty()) { - PartPlate* ixex_plate = partplate_list.get_curr_plate(); - if (ixex_plate && ixex_plate->has_ixex_placement_violations()) - err.string = _u8L("Cannot slice: objects are in secondary zones reserved for iXex parallel printing."); + PartPlate* imex_plate = partplate_list.get_curr_plate(); + if (imex_plate && imex_plate->has_imex_placement_violations()) + err.string = _u8L("Cannot slice: objects are in secondary zones reserved for IDEX/IQEX parallel printing."); } if (err.string.empty()) { @@ -17640,7 +17640,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi update(); p->partplate_list.select_plate(0); } - else if (action == (int)PartPlate::PLATE_IXEX_MODE_ID) + else if (action == (int)PartPlate::PLATE_IMEX_MODE_ID) { ret = select_plate(plate_index); if (!ret) { @@ -17649,7 +17649,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi std::vector modes; modes.push_back("primary"); const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto* names_opt = printer_cfg.option("ixex_mode_names"); + 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); @@ -17658,7 +17658,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi if (right_click) { // Show a popup menu with all modes. wxMenu menu; - std::string current = curr_plate->get_ixex_mode(); + std::string current = curr_plate->get_imex_mode(); for (size_t i = 0; i < modes.size(); ++i) { wxMenuItem* item = menu.AppendRadioItem(wxID_HIGHEST + (int)i, from_u8(modes[i])); if (modes[i] == current) @@ -17667,8 +17667,8 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi menu.Bind(wxEVT_MENU, [this, curr_plate, &modes](wxCommandEvent& e) { int idx = e.GetId() - wxID_HIGHEST; if (idx >= 0 && idx < (int)modes.size()) { - take_snapshot("set ixex mode"); - curr_plate->set_ixex_mode(modes[idx]); + take_snapshot("set imex mode"); + curr_plate->set_imex_mode(modes[idx]); update_project_dirty_from_presets(); set_plater_dirty(true); update(); @@ -17678,13 +17678,13 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi ret = 1; // signal to caller: popup was shown, suppress plate context menu } else { // Left-click: cycle to next mode. - std::string current = curr_plate->get_ixex_mode(); + std::string current = curr_plate->get_imex_mode(); auto it = std::find(modes.begin(), modes.end(), current); size_t next_idx = (it == modes.end()) ? 0 : ((it - modes.begin() + 1) % modes.size()); std::string next_mode = modes[next_idx]; if (next_mode != current) { - take_snapshot("set ixex mode"); - curr_plate->set_ixex_mode(next_mode); + take_snapshot("set imex mode"); + curr_plate->set_imex_mode(next_mode); update_project_dirty_from_presets(); set_plater_dirty(true); update(); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index a048597a4d..cbc95a328b 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4338,7 +4338,7 @@ void TabPrinter::build() } // --------------------------------------------------------------------------- -// IXexModesCtrl — visual parallel-mode editor for the iXex printer tab +// IMEXModesCtrl — visual parallel-mode editor for the IDEX/IQEX printer tab // // Tool button states: // 0 = Inactive (grey) — not participating in this mode @@ -4348,7 +4348,7 @@ void TabPrinter::build() // // Active-tools string format: "idx:P,idx:C,idx:M" (backwards-compat: plain "idx" = Primary) // --------------------------------------------------------------------------- -class IXexModesCtrl : public wxPanel { +class IMEXModesCtrl : public wxPanel { public: std::function on_change; @@ -4360,7 +4360,7 @@ public: return 0; // "front-left" default } - IXexModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0) + IMEXModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0) : wxPanel(parent, wxID_ANY), m_n_cols(std::max(1, n_cols)), m_n_rows(std::max(1, n_rows)), m_layout(layout) { m_outer = new wxBoxSizer(wxVERTICAL); @@ -4437,9 +4437,9 @@ public: void load_from_config(const DynamicPrintConfig& cfg) { clear_rows(); - auto* names = cfg.option("ixex_mode_names"); - auto* tools = cfg.option("ixex_mode_active_tools"); - auto* gcodes = cfg.option("ixex_mode_gcodes"); + auto* names = cfg.option("imex_mode_names"); + auto* tools = cfg.option("imex_mode_active_tools"); + auto* gcodes = cfg.option("imex_mode_gcodes"); size_t n = names ? names->values.size() : 0; for (size_t i = 0; i < n; ++i) add_row(names->values[i], @@ -4931,14 +4931,14 @@ void TabPrinter::build_fff() option.opt.height = gcode_field_height;//150; optgroup->append_single_option_line(option, "printer_machine_gcode#template-custom-g-code"); - page = add_options_page(L("iXex"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders + page = add_options_page(L("IDEX/IQEX"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders optgroup = page->new_optgroup(L("Carriage Configuration"), L"param_advanced"); - optgroup->append_single_option_line("is_ixex"); - optgroup->append_single_option_line("ixex_gantry_count"); - optgroup->append_single_option_line("ixex_tools_per_gantry"); + optgroup->append_single_option_line("is_imex"); + optgroup->append_single_option_line("imex_gantry_count"); + optgroup->append_single_option_line("imex_tools_per_gantry"); { // Tool 0 corner selector - auto opt = optgroup->get_option("ixex_tool_layout"); + auto opt = optgroup->get_option("imex_tool_layout"); auto line = Line{ opt.opt.label, opt.opt.tooltip }; line.append_option(opt); line.widget = [this](wxWindow* parent) -> wxSizer* { @@ -4947,37 +4947,37 @@ void TabPrinter::build_fff() _L("Rear-left"), _L("Rear-right") }; static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" }; - m_ixex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + m_imex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(12 * wxGetApp().em_unit(), -1), 4, choices, wxCB_READONLY); - m_ixex_layout_combo->GetDropDown().SetUseContentWidth(true); + m_imex_layout_combo->GetDropDown().SetUseContentWidth(true); // Set current selection std::string cur = "front-left"; - if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; + if (auto* o = m_config->option("imex_tool_layout")) cur = o->value; for (int i = 0; i < 4; ++i) - if (cur == values[i]) { m_ixex_layout_combo->SetSelection(i); break; } - m_ixex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + if (cur == values[i]) { m_imex_layout_combo->SetSelection(i); break; } + m_imex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; - int sel = m_ixex_layout_combo->GetSelection(); + int sel = m_imex_layout_combo->GetSelection(); std::string val = (sel >= 0 && sel < 4) ? layout_vals[sel] : "front-left"; - m_config->set_key_value("ixex_tool_layout", new ConfigOptionString(val)); - on_value_change("ixex_tool_layout", val); - if (m_ixex_modes_ctrl) - m_ixex_modes_ctrl->set_grid_size( - m_config->opt_int("ixex_tools_per_gantry"), - m_config->opt_int("ixex_gantry_count"), - IXexModesCtrl::parse_layout(val)); + m_config->set_key_value("imex_tool_layout", new ConfigOptionString(val)); + on_value_change("imex_tool_layout", val); + if (m_imex_modes_ctrl) + m_imex_modes_ctrl->set_grid_size( + m_config->opt_int("imex_tools_per_gantry"), + m_config->opt_int("imex_gantry_count"), + IMEXModesCtrl::parse_layout(val)); }); auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); + s->Add(m_imex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); return s; }; optgroup->append_line(line); } - optgroup->append_single_option_line("ixex_nozzle_clearance_x"); - optgroup->append_single_option_line("ixex_nozzle_clearance_y"); - optgroup->append_single_option_line("ixex_carriage_margin"); + optgroup->append_single_option_line("imex_nozzle_clearance_x"); + optgroup->append_single_option_line("imex_nozzle_clearance_y"); + optgroup->append_single_option_line("imex_carriage_margin"); { static const wxString theme_choices[] = { @@ -4986,27 +4986,27 @@ void TabPrinter::build_fff() _L("Tritanopia (blue-yellow)"), _L("High Contrast") }; static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; - auto opt = optgroup->get_option("ixex_viz_theme"); + auto opt = optgroup->get_option("imex_viz_theme"); auto line = Line{ opt.opt.label, opt.opt.tooltip }; line.append_option(opt); line.widget = [this](wxWindow* parent) -> wxSizer* { - m_ixex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + m_imex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(22 * wxGetApp().em_unit(), -1), 4, theme_choices, wxCB_READONLY); - m_ixex_theme_combo->GetDropDown().SetUseContentWidth(true); + m_imex_theme_combo->GetDropDown().SetUseContentWidth(true); std::string cur = "standard"; - if (auto* o = m_config->option("ixex_viz_theme")) cur = o->value; + if (auto* o = m_config->option("imex_viz_theme")) cur = o->value; for (int i = 0; i < 4; ++i) - if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; } - m_ixex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - int sel = m_ixex_theme_combo->GetSelection(); + if (cur == theme_vals[i]) { m_imex_theme_combo->SetSelection(i); break; } + m_imex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + int sel = m_imex_theme_combo->GetSelection(); std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard"; - m_config->set_key_value("ixex_viz_theme", new ConfigOptionString(val)); - on_value_change("ixex_viz_theme", val); + m_config->set_key_value("imex_viz_theme", new ConfigOptionString(val)); + on_value_change("imex_viz_theme", val); }); auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); + s->Add(m_imex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); return s; }; optgroup->append_line(line); @@ -5018,23 +5018,23 @@ void TabPrinter::build_fff() auto line = Line{ L("Modes"), L("") }; line.full_width = 1; line.widget = [this](wxWindow* parent) -> wxSizer* { - int n_cols = m_config->opt_int("ixex_tools_per_gantry"); - int n_rows = m_config->opt_int("ixex_gantry_count"); + int n_cols = m_config->opt_int("imex_tools_per_gantry"); + int n_rows = m_config->opt_int("imex_gantry_count"); std::string layout_str = "front-left"; - if (auto* o = m_config->option("ixex_tool_layout")) layout_str = o->value; - int layout = IXexModesCtrl::parse_layout(layout_str); - m_ixex_modes_ctrl = new IXexModesCtrl(parent, n_cols, n_rows, layout); - m_ixex_modes_ctrl->load_from_config(*m_config); - m_ixex_modes_ctrl->on_change = [this]() { - auto [names, tools, gcodes] = m_ixex_modes_ctrl->get_mode_data(); - m_config->set_key_value("ixex_mode_names", new ConfigOptionStrings(names)); - m_config->set_key_value("ixex_mode_active_tools", new ConfigOptionStrings(tools)); - m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); + if (auto* o = m_config->option("imex_tool_layout")) layout_str = o->value; + int layout = IMEXModesCtrl::parse_layout(layout_str); + m_imex_modes_ctrl = new IMEXModesCtrl(parent, n_cols, n_rows, layout); + m_imex_modes_ctrl->load_from_config(*m_config); + m_imex_modes_ctrl->on_change = [this]() { + auto [names, tools, gcodes] = m_imex_modes_ctrl->get_mode_data(); + m_config->set_key_value("imex_mode_names", new ConfigOptionStrings(names)); + m_config->set_key_value("imex_mode_active_tools", new ConfigOptionStrings(tools)); + m_config->set_key_value("imex_mode_gcodes", new ConfigOptionStrings(gcodes)); update_dirty(); - on_value_change("ixex_mode_names", std::string("")); + on_value_change("imex_mode_names", std::string("")); }; auto* sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); + sizer->Add(m_imex_modes_ctrl, 1, wxEXPAND); return sizer; }; modes_og->append_line(line); @@ -5622,28 +5622,28 @@ void TabPrinter::reload_config() if (m_config) { // Sync layout combo - if (m_ixex_layout_combo) { + if (m_imex_layout_combo) { static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; std::string cur = "front-left"; - if (auto* o = m_config->option("ixex_tool_layout")) cur = o->value; + if (auto* o = m_config->option("imex_tool_layout")) cur = o->value; for (int i = 0; i < 4; ++i) - if (cur == layout_vals[i]) { m_ixex_layout_combo->SetSelection(i); break; } + if (cur == layout_vals[i]) { m_imex_layout_combo->SetSelection(i); break; } } // Sync theme combo - if (m_ixex_theme_combo) { + if (m_imex_theme_combo) { static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; std::string cur = "standard"; - if (auto* o = m_config->option("ixex_viz_theme")) cur = o->value; + if (auto* o = m_config->option("imex_viz_theme")) cur = o->value; for (int i = 0; i < 4; ++i) - if (cur == theme_vals[i]) { m_ixex_theme_combo->SetSelection(i); break; } + if (cur == theme_vals[i]) { m_imex_theme_combo->SetSelection(i); break; } } - if (m_ixex_modes_ctrl) { - int n_cols = m_config->opt_int("ixex_tools_per_gantry"); - int n_rows = m_config->opt_int("ixex_gantry_count"); + 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"); std::string layout_str = "front-left"; - if (auto* o = m_config->option("ixex_tool_layout")) layout_str = o->value; - m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str)); - m_ixex_modes_ctrl->load_from_config(*m_config); + if (auto* o = m_config->option("imex_tool_layout")) layout_str = o->value; + m_imex_modes_ctrl->set_grid_size(n_cols, n_rows, IMEXModesCtrl::parse_layout(layout_str)); + m_imex_modes_ctrl->load_from_config(*m_config); } } } @@ -5662,9 +5662,9 @@ void TabPrinter::clear_pages() { Tab::clear_pages(); m_reset_to_filament_color = nullptr; - m_ixex_modes_ctrl = nullptr; - m_ixex_layout_combo = nullptr; - m_ixex_theme_combo = nullptr; + m_imex_modes_ctrl = nullptr; + m_imex_layout_combo = nullptr; + m_imex_theme_combo = nullptr; } void TabPrinter::toggle_options() @@ -5874,13 +5874,13 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } - // Sync iXex tool grid whenever carriage configuration changes. + // Sync IDEX/IQEX tool grid whenever carriage configuration changes. // set_grid_size() is a no-op when dimensions and layout are unchanged. - if (m_ixex_modes_ctrl) { - int n_cols = m_config->opt_int("ixex_tools_per_gantry"); - int n_rows = m_config->opt_int("ixex_gantry_count"); - std::string layout_str = m_config->opt_string("ixex_tool_layout"); - m_ixex_modes_ctrl->set_grid_size(n_cols, n_rows, IXexModesCtrl::parse_layout(layout_str)); + 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"); + std::string layout_str = m_config->opt_string("imex_tool_layout"); + m_imex_modes_ctrl->set_grid_size(n_cols, n_rows, IMEXModesCtrl::parse_layout(layout_str)); } toggle_options(); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 7d86278db7..c76f6b5f0d 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -589,7 +589,7 @@ public: void set_custom_gcode(const t_config_option_key& opt_key, const std::string& value) override; }; -class IXexModesCtrl; +class IMEXModesCtrl; class TabPrinter : public Tab { @@ -605,9 +605,9 @@ private: std::vector m_pages_sla; wxBoxSizer* m_presets_sizer {nullptr}; - IXexModesCtrl* m_ixex_modes_ctrl {nullptr}; - ComboBox* m_ixex_layout_combo {nullptr}; - ComboBox* m_ixex_theme_combo {nullptr}; + IMEXModesCtrl* m_imex_modes_ctrl {nullptr}; + ComboBox* m_imex_layout_combo {nullptr}; + ComboBox* m_imex_theme_combo {nullptr}; public: ScalableButton* m_reset_to_filament_color = nullptr; From 0d73cd13ba4e2aeee751c1faeb8cf8a571964ac3 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 9 Apr 2026 14:36:54 -0400 Subject: [PATCH 017/154] refactor: move IDEX/IQEX settings from standalone tab into Multimaterial section - Remove standalone IDEX/IQEX printer tab - Add IDEX/IQEX Configuration and Parallel Modes sections to the Multimaterial page, gated behind is_imex - Carriage config options hidden via toggle_options() when is_imex is off - Custom widgets (layout combo, theme combo, modes ctrl) show/hide accordingly Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/Tab.cpp | 224 +++++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 109 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index cbc95a328b..001bd5f305 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4931,115 +4931,6 @@ void TabPrinter::build_fff() option.opt.height = gcode_field_height;//150; optgroup->append_single_option_line(option, "printer_machine_gcode#template-custom-g-code"); - page = add_options_page(L("IDEX/IQEX"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders - optgroup = page->new_optgroup(L("Carriage Configuration"), L"param_advanced"); - optgroup->append_single_option_line("is_imex"); - optgroup->append_single_option_line("imex_gantry_count"); - optgroup->append_single_option_line("imex_tools_per_gantry"); - { - // Tool 0 corner selector - auto opt = optgroup->get_option("imex_tool_layout"); - auto line = Line{ opt.opt.label, opt.opt.tooltip }; - line.append_option(opt); - line.widget = [this](wxWindow* parent) -> wxSizer* { - static const wxString choices[] = { - _L("Front-left"), _L("Front-right"), - _L("Rear-left"), _L("Rear-right") - }; - static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" }; - m_imex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(12 * wxGetApp().em_unit(), -1), - 4, choices, wxCB_READONLY); - m_imex_layout_combo->GetDropDown().SetUseContentWidth(true); - // Set current selection - std::string cur = "front-left"; - if (auto* o = m_config->option("imex_tool_layout")) cur = o->value; - for (int i = 0; i < 4; ++i) - if (cur == values[i]) { m_imex_layout_combo->SetSelection(i); break; } - m_imex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; - int sel = m_imex_layout_combo->GetSelection(); - std::string val = (sel >= 0 && sel < 4) ? layout_vals[sel] : "front-left"; - m_config->set_key_value("imex_tool_layout", new ConfigOptionString(val)); - on_value_change("imex_tool_layout", val); - if (m_imex_modes_ctrl) - m_imex_modes_ctrl->set_grid_size( - m_config->opt_int("imex_tools_per_gantry"), - m_config->opt_int("imex_gantry_count"), - IMEXModesCtrl::parse_layout(val)); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_imex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }; - optgroup->append_line(line); - } - optgroup->append_single_option_line("imex_nozzle_clearance_x"); - optgroup->append_single_option_line("imex_nozzle_clearance_y"); - optgroup->append_single_option_line("imex_carriage_margin"); - - { - static const wxString theme_choices[] = { - _L("Standard"), - _L("Deuteranopia / Protanopia (red-green)"), - _L("Tritanopia (blue-yellow)"), - _L("High Contrast") }; - static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; - auto opt = optgroup->get_option("imex_viz_theme"); - auto line = Line{ opt.opt.label, opt.opt.tooltip }; - line.append_option(opt); - line.widget = [this](wxWindow* parent) -> wxSizer* { - m_imex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(22 * wxGetApp().em_unit(), -1), - 4, theme_choices, wxCB_READONLY); - m_imex_theme_combo->GetDropDown().SetUseContentWidth(true); - std::string cur = "standard"; - if (auto* o = m_config->option("imex_viz_theme")) cur = o->value; - for (int i = 0; i < 4; ++i) - if (cur == theme_vals[i]) { m_imex_theme_combo->SetSelection(i); break; } - m_imex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - int sel = m_imex_theme_combo->GetSelection(); - std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard"; - m_config->set_key_value("imex_viz_theme", new ConfigOptionString(val)); - on_value_change("imex_viz_theme", val); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_imex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }; - optgroup->append_line(line); - } - - { - // Parallel modes panel: visual toggle-grid list with + button - auto modes_og = page->new_optgroup(L("Parallel Modes"), L"param_advanced"); - 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"); - std::string layout_str = "front-left"; - if (auto* o = m_config->option("imex_tool_layout")) layout_str = o->value; - int layout = IMEXModesCtrl::parse_layout(layout_str); - m_imex_modes_ctrl = new IMEXModesCtrl(parent, n_cols, n_rows, layout); - m_imex_modes_ctrl->load_from_config(*m_config); - m_imex_modes_ctrl->on_change = [this]() { - auto [names, tools, gcodes] = m_imex_modes_ctrl->get_mode_data(); - m_config->set_key_value("imex_mode_names", new ConfigOptionStrings(names)); - m_config->set_key_value("imex_mode_active_tools", new ConfigOptionStrings(tools)); - m_config->set_key_value("imex_mode_gcodes", new ConfigOptionStrings(gcodes)); - update_dirty(); - on_value_change("imex_mode_names", std::string("")); - }; - auto* sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_imex_modes_ctrl, 1, wxEXPAND); - return sizer; - }; - modes_og->append_line(line); - } - page = add_options_page(L("Notes"), "custom-gcode_note"); // ORCA: icon only visible on placeholders optgroup = page->new_optgroup(L("Notes"), "note", 0); option = optgroup->get_option("printer_notes"); @@ -5378,6 +5269,111 @@ if (is_marlin_flavor) optgroup->append_single_option_line("machine_load_filament_time", "printer_multimaterial_advanced#filament-load-time"); optgroup->append_single_option_line("machine_unload_filament_time", "printer_multimaterial_advanced#filament-unload-time"); optgroup->append_single_option_line("machine_tool_change_time", "printer_multimaterial_advanced#tool-change-time"); + + // IDEX/IQEX (IMEX) parallel printing configuration + optgroup = page->new_optgroup(L("IDEX/IQEX Configuration"), L"param_advanced"); + optgroup->append_single_option_line("is_imex"); + optgroup->append_single_option_line("imex_gantry_count"); + optgroup->append_single_option_line("imex_tools_per_gantry"); + { + auto opt = optgroup->get_option("imex_tool_layout"); + auto line = Line{ opt.opt.label, opt.opt.tooltip }; + line.append_option(opt); + line.widget = [this](wxWindow* parent) -> wxSizer* { + static const wxString choices[] = { + _L("Front-left"), _L("Front-right"), + _L("Rear-left"), _L("Rear-right") + }; + static const char* values[] = { "front-left", "front-right", "rear-left", "rear-right" }; + m_imex_layout_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(12 * wxGetApp().em_unit(), -1), + 4, choices, wxCB_READONLY); + m_imex_layout_combo->GetDropDown().SetUseContentWidth(true); + std::string cur = "front-left"; + if (auto* o = m_config->option("imex_tool_layout")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == values[i]) { m_imex_layout_combo->SetSelection(i); break; } + m_imex_layout_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + static const char* layout_vals[] = { "front-left", "front-right", "rear-left", "rear-right" }; + int sel = m_imex_layout_combo->GetSelection(); + std::string val = (sel >= 0 && sel < 4) ? layout_vals[sel] : "front-left"; + m_config->set_key_value("imex_tool_layout", new ConfigOptionString(val)); + on_value_change("imex_tool_layout", val); + if (m_imex_modes_ctrl) + m_imex_modes_ctrl->set_grid_size( + m_config->opt_int("imex_tools_per_gantry"), + m_config->opt_int("imex_gantry_count"), + IMEXModesCtrl::parse_layout(val)); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_imex_layout_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }; + optgroup->append_line(line); + } + optgroup->append_single_option_line("imex_nozzle_clearance_x"); + optgroup->append_single_option_line("imex_nozzle_clearance_y"); + optgroup->append_single_option_line("imex_carriage_margin"); + { + static const wxString theme_choices[] = { + _L("Standard"), + _L("Deuteranopia / Protanopia (red-green)"), + _L("Tritanopia (blue-yellow)"), + _L("High Contrast") }; + static const char* theme_vals[] = { "standard", "deuteranopia", "tritanopia", "high_contrast" }; + auto opt = optgroup->get_option("imex_viz_theme"); + auto line = Line{ opt.opt.label, opt.opt.tooltip }; + line.append_option(opt); + line.widget = [this](wxWindow* parent) -> wxSizer* { + m_imex_theme_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, + wxSize(22 * wxGetApp().em_unit(), -1), + 4, theme_choices, wxCB_READONLY); + m_imex_theme_combo->GetDropDown().SetUseContentWidth(true); + std::string cur = "standard"; + if (auto* o = m_config->option("imex_viz_theme")) cur = o->value; + for (int i = 0; i < 4; ++i) + if (cur == theme_vals[i]) { m_imex_theme_combo->SetSelection(i); break; } + m_imex_theme_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { + int sel = m_imex_theme_combo->GetSelection(); + std::string val = (sel >= 0 && sel < 4) ? theme_vals[sel] : "standard"; + m_config->set_key_value("imex_viz_theme", new ConfigOptionString(val)); + on_value_change("imex_viz_theme", val); + }); + auto* s = new wxBoxSizer(wxHORIZONTAL); + s->Add(m_imex_theme_combo, 0, wxALIGN_CENTER_VERTICAL); + return s; + }; + optgroup->append_line(line); + } + { + auto modes_og = page->new_optgroup(L("IDEX/IQEX Parallel Modes"), L"param_advanced"); + 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"); + std::string layout_str = "front-left"; + if (auto* o = m_config->option("imex_tool_layout")) layout_str = o->value; + int layout = IMEXModesCtrl::parse_layout(layout_str); + m_imex_modes_ctrl = new IMEXModesCtrl(parent, n_cols, n_rows, layout); + m_imex_modes_ctrl->load_from_config(*m_config); + m_imex_modes_ctrl->on_change = [this]() { + auto [names, tools, gcodes] = m_imex_modes_ctrl->get_mode_data(); + m_config->set_key_value("imex_mode_names", new ConfigOptionStrings(names)); + m_config->set_key_value("imex_mode_active_tools", new ConfigOptionStrings(tools)); + m_config->set_key_value("imex_mode_gcodes", new ConfigOptionStrings(gcodes)); + update_dirty(); + on_value_change("imex_mode_names", std::string("")); + }; + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_imex_modes_ctrl, 1, wxEXPAND); + return sizer; + }; + modes_og->append_line(line); + } + m_pages.insert(m_pages.end() - n_after_single_extruder_MM, page); } @@ -5735,6 +5731,16 @@ void TabPrinter::toggle_options() toggle_option("extruders_count", !bSEMM); toggle_option("manual_filament_change", bSEMM); toggle_option("purge_in_prime_tower", bSEMM && supports_wipe_tower_2); + + // 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", + "imex_nozzle_clearance_x", "imex_nozzle_clearance_y", + "imex_carriage_margin", "imex_viz_theme"}) + 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); } wxString extruder_number; long val = 1; From 9a87363044e10704e62c35d98edb16a81fb1afa7 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 9 Apr 2026 17:09:24 -0400 Subject: [PATCH 018/154] feat: 4-tool support, primary row, zone sizing, and stability fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMEXModesCtrl: - Primary mode is now a non-deletable first row stored as sentinel "primary" in imex_mode_names; older configs load cleanly - New rows default T0 → Primary when no tool assignment is stored - Filter "primary" from plater popup/cycle list to prevent double entry - imex_tools_per_gantry cap raised 2 → 4 Zone sizing: - Zone width/height now based on active tool count only; inactive tools donate their bed share to active neighbors (fixes 4-tool layout) - Active col/row maps (col_to_zone/row_to_zone) applied consistently across zone fills, collision strips, and primary zone box GCodeViewer animation: - Mirror position formula fixed: left-of-copy reflects across copy zone's left edge; right-of-copy reflects across right edge (T3 was rendering on top of T1) - strip_width/row_strip_height use active counts, matching PartPlate Stability: - Early return in calc_imex_zones() when tool_states is empty; prevents OOB crash on new printer with stale process-preset mode name - is_imex toggle in on_config_change calls refresh_imex_icons() so the plate mode icon appears without requiring a new project GCode: - Remove "primary" guard so Primary mode gcode field is emitted at start of print Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_ixex_mode.svg | 10 -- resources/images/plate_ixex_mode_dark.svg | 10 -- resources/images/plate_ixex_mode_hover.svg | 10 -- .../images/plate_ixex_mode_hover_dark.svg | 10 -- src/libslic3r/GCode.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 2 +- src/slic3r/GUI/GCodeViewer.cpp | 101 +++++++++------- src/slic3r/GUI/PartPlate.cpp | 110 ++++++++++++++---- src/slic3r/GUI/PartPlate.hpp | 3 + src/slic3r/GUI/Plater.cpp | 8 +- src/slic3r/GUI/Tab.cpp | 88 ++++++++++---- 11 files changed, 220 insertions(+), 134 deletions(-) delete mode 100644 resources/images/plate_ixex_mode.svg delete mode 100644 resources/images/plate_ixex_mode_dark.svg delete mode 100644 resources/images/plate_ixex_mode_hover.svg delete mode 100644 resources/images/plate_ixex_mode_hover_dark.svg 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; From 1d68f917b0d35f8afa6fc334d08488d4e47209d5 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Thu, 9 Apr 2026 18:04:35 -0400 Subject: [PATCH 019/154] feat: add imex_mode/imex_mode_index/imex_mode_gcode placeholders with global var flow Register imex_mode (string), imex_mode_index (int), and imex_mode_gcode (string) in OtherSlicingStatesConfigDef so they appear in the placeholder search UI under Slicing State. Set all three via placeholder_parser().set() before any script processing in _do_export(). Process imex_mode_gcode first so {global} declarations defined there flow forward into machine_start_gcode. Co-Authored-By: Claude Sonnet 4.6 --- src/libslic3r/GCode.cpp | 54 ++++++++++++++++++++++++----------- src/libslic3r/PrintConfig.cpp | 12 ++++++++ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 31f1f421a7..87e4f2d25f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3036,6 +3036,40 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder))); this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); + // IDEX/IQEX: set {imex_mode}, {imex_mode_index}, {imex_mode_gcode} placeholders before + // any G-code script is processed so they are available in machine_start_gcode, + // filament_start_gcode, filament_end_gcode, change_filament_gcode, etc. + // {imex_mode_names} and {imex_mode_gcodes} array placeholders are already accessible + // via the printer config (e.g. {imex_mode_names[0]}, {imex_mode_gcodes[1]}). + std::string imex_active_mode; + int imex_active_mode_index = 0; + std::string imex_active_mode_gcode; + 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() ? "primary" : raw; + 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; + } + } + } + this->placeholder_parser().set("imex_mode", imex_active_mode); + this->placeholder_parser().set("imex_mode_index", imex_active_mode_index); + this->placeholder_parser().set("imex_mode_gcode", imex_active_mode_gcode); + + // IDEX/IQEX: process mode G-code BEFORE machine_start_gcode so that any + // {global abc = 1} declarations in the mode script are visible to machine_start_gcode. + // The processed result is buffered here and written to the file after temp setup below. + std::string imex_processed_gcode; + if (!imex_active_mode_gcode.empty()) + imex_processed_gcode = this->placeholder_parser_process( + "imex_mode_gcode", imex_active_mode_gcode, initial_extruder_id); + std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id); if (print.config().gcode_flavor != gcfKlipper) { // Set bed temperature if the start G-code does not contain any bed temp control G-codes. @@ -3054,23 +3088,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } - // IDEX/IQEX: inject mode G-code before machine_start_gcode so the carriage mode - // 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()) { - 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) { - if (mode_names[i] == imex_mode && !mode_gcodes[i].empty()) { - std::string imex_gcode = this->placeholder_parser_process( - "imex_mode_gcode", mode_gcodes[i], initial_extruder_id); - file.writeln(imex_gcode); - break; - } - } - } - } + // Write IMEX mode G-code first (processed above), then machine_start_gcode. + if (!imex_processed_gcode.empty()) + file.writeln(imex_processed_gcode); // Write the custom start G-code file.writeln(machine_start_gcode); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5b05933e93..310d799e19 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10509,6 +10509,18 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef() new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool."); new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone."); + + def = this->add("imex_mode", coString); + def->label = L("iMEX active mode"); + def->tooltip = L("Name of the active iMEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if iMEX is not enabled."); + + def = this->add("imex_mode_index", coInt); + def->label = L("iMEX active mode index"); + def->tooltip = L("Zero-based index of the active iMEX parallel print mode within imex_mode_names."); + + def = this->add("imex_mode_gcode", coString); + def->label = L("iMEX active mode G-code"); + def->tooltip = L("The raw mode G-code template for the active iMEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode."); } PrintStatisticsConfigDef::PrintStatisticsConfigDef() From 73b5b750c69e0cb948cd0bda781be557a34facbd Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Fri, 10 Apr 2026 00:10:03 -0400 Subject: [PATCH 020/154] feat: iMEX multi-material warning badge, conflict dialog, and mode row UI - Add warning badge (obj_warning.svg overlay) to the iMEX plate icon when a parallel mode is active alongside multi-material objects on the same plate - Add has_imex_multimaterial_conflict() using get_extruders(true) so only filaments actually used on the plate are checked - Move multi-material caution dialog from reslice() into on_action_slice_plate / on_action_slice_all so it fires exactly once per user action and does not disrupt GL thumbnail generation during Slice All - Fix is_imex missing from p->config init key list so on_config_change() diff detection correctly triggers refresh_imex_icons() - Defer imex_changed handling until after set_bed_shape() so m_shape is current - Replace plain remove button with ScalableButton (imex_remove.svg) in mode rows - Add EditGCodeDialog launch button per mode row for placeholder browsing Co-Authored-By: Claude Sonnet 4.6 --- resources/images/imex_remove.svg | 1 + src/slic3r/GUI/PartPlate.cpp | 78 +++++++++++++++++++++++++++++--- src/slic3r/GUI/PartPlate.hpp | 6 +++ src/slic3r/GUI/Plater.cpp | 61 ++++++++++++++++++++++++- src/slic3r/GUI/Tab.cpp | 25 ++++++++-- 5 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 resources/images/imex_remove.svg diff --git a/resources/images/imex_remove.svg b/resources/images/imex_remove.svg new file mode 100644 index 0000000000..6c18a21d66 --- /dev/null +++ b/resources/images/imex_remove.svg @@ -0,0 +1 @@ + diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index a5c2865574..e599f69171 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -879,8 +879,11 @@ void PartPlate::refresh_imex_icon() 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); + if (is_imex_opt && is_imex_opt->value) { + int imex_slot = dual_bbl ? 7 : 6; + calc_vertex_for_icons(imex_slot, m_imex_mode_icon); + calc_vertex_for_imex_warn_badge(imex_slot, m_imex_warn_icon); + } } // Ensure zone geometry (secondary boxes, collision strips, visual meshes) is up to date. @@ -920,6 +923,20 @@ bool PartPlate::has_imex_placement_violations() return false; } +bool PartPlate::has_imex_multimaterial_conflict() const +{ + // Condition 1: iMEX is active and the plate mode is non-primary + auto* pb = wxGetApp().preset_bundle; + if (!pb) return false; + auto* is_imex_opt = pb->printers.get_edited_preset().config.option("is_imex"); + if (!is_imex_opt || !is_imex_opt->value) return false; + if (get_imex_mode() == "primary") return false; + + // Condition 2: objects on this plate actually use more than one unique filament/extruder + std::vector used = get_extruders(true); + return used.size() > 1; +} + void PartPlate::render_imex_zones(bool force_default_color) { if (force_default_color) @@ -1237,6 +1254,37 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model) init_raycaster_from_model(model); } +// Positions the iMEX multi-material warning badge as a small overlay at the bottom-right +// corner of the iMEX mode icon. The icon slot index matches the one used in calc_vertex_for_icons. +void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &model) +{ + model.reset(); + + auto bed_ext = get_extents(m_shape); + Vec2d p = bed_ext[2]; + auto factor = bed_ext.size()(1) / 200.0; + float size = PARTPLATE_ICON_SIZE * factor; + float gap_left = PARTPLATE_ICON_GAP_LEFT * factor; + float gap_y = PARTPLATE_ICON_GAP_Y * factor; + float gap_top = PARTPLATE_ICON_GAP_TOP * factor; + + // Centre of the iMEX mode icon (top-left corner = p after offset) + p += Vec2d(gap_left, -1 * (imex_icon_index * (size + gap_y) + gap_top)); + + // Badge is half the icon size, anchored to the bottom-right corner of the icon slot + float badge = size * 0.55f; + Vec2d bp(p(0) + size - badge, p(1) - size); + + ExPolygon poly; + poly.contour.append({ scale_(bp(0)) , scale_(bp(1)) }); + poly.contour.append({ scale_(bp(0) + badge), scale_(bp(1)) }); + poly.contour.append({ scale_(bp(0) + badge), scale_(bp(1) + badge)}); + poly.contour.append({ scale_(bp(0)) , scale_(bp(1) + badge)}); + + if (!init_model_from_poly(model, poly, GROUND_Z + 0.01f)) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to generate geometry for iMEX warn badge\n"; +} + /* void PartPlate::calc_vertex_for_icons_background(int icon_count, GLModel &buffer) { @@ -1748,6 +1796,12 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else { render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_texture); } + // Warning badge: iMEX parallel mode active alongside multi-material objects + if (has_imex_multimaterial_conflict()) { + render_icon_texture(m_imex_warn_icon, m_partplate_list->m_imex_warn_texture); + if (hover_id == (int)PLATE_IMEX_MODE_ID) + show_tooltip(_u8L("Warning: this plate uses a parallel iMEX mode with multi-material objects. Proceed with caution — verify your G-code handles this combination correctly.")); + } } } @@ -2060,10 +2114,10 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); - // Register IDEX/IQEX mode icon only when IDEX/IQEX is active on the current printer preset. + // Register IDEX/IQEX mode icon only when IDEX/IQEX is active and geometry is initialized. if (preset) { auto* is_imex_opt = preset->printers.get_edited_preset().config.option("is_imex"); - if (is_imex_opt && is_imex_opt->value) + if (is_imex_opt && is_imex_opt->value && m_imex_mode_icon.mesh_raycaster) register_model_for_picking(canvas, m_imex_mode_icon, picking_id_component(PLATE_IMEX_MODE_ID)); } } @@ -3753,8 +3807,11 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); { 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); + if (is_imex_opt && is_imex_opt->value) { + int imex_slot = dual_bbl ? 7 : 6; + calc_vertex_for_icons(imex_slot, m_imex_mode_icon); + calc_vertex_for_imex_warn_badge(imex_slot, m_imex_warn_icon); + } } calc_vertex_for_number(0, false, m_plate_idx_icon); @@ -4625,6 +4682,14 @@ void PartPlateList::generate_icon_textures() } } + // iMEX multi-material conflict warning badge + { + file_name = path + "obj_warning.svg"; + if (!m_imex_warn_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iMEX warn badge)") % file_name; + } + } + // IDEX/IQEX mode icon textures (fall back gracefully if SVG not present yet) { file_name = path + (m_is_dark ? "plate_imex_mode_dark.svg" : "plate_imex_mode.svg"); @@ -4684,6 +4749,7 @@ void PartPlateList::release_icon_textures() m_plate_name_edit_hovered_texture.reset(); m_imex_mode_texture.reset(); m_imex_mode_hovered_texture.reset(); + m_imex_warn_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 6ff42c3ef6..858e50fccd 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -156,6 +156,7 @@ private: PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; PickingModel m_imex_mode_icon; + GLModel m_imex_warn_icon; // warning badge: iMEX parallel mode + multi-material conflict GLModel m_plate_idx_icon; GLTexture m_texture; @@ -195,6 +196,7 @@ private: 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); + void calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &model); // void calc_vertex_for_icons_background(int icon_count, GLModel &buffer); void render_background(bool force_default_color = false); void render_logo(bool bottom, bool render_cali = true); @@ -454,6 +456,9 @@ public: } // Returns true if any instance on this plate overlaps an IDEX/IQEX secondary or collision zone. bool has_imex_placement_violations(); + // Returns true when iMEX parallel mode is active (non-primary) AND the plate + // has objects assigned to more than one filament — a combination that warrants caution. + bool has_imex_multimaterial_conflict() const; // Returns the primary-zone bounding box in mm when an IDEX/IQEX parallel mode is active. // Empty (nullopt) when IDEX/IQEX is off or the mode is "primary" (full-bed). std::optional imex_primary_zone() { ensure_imex_zones(); return m_imex_primary_zone_box; } @@ -637,6 +642,7 @@ class PartPlateList : public ObjectBase GLTexture m_plate_name_edit_hovered_texture; GLTexture m_imex_mode_texture; GLTexture m_imex_mode_hovered_texture; + GLTexture m_imex_warn_texture; // warning badge for iMEX + multi-material conflict GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index af49d6341b..cd4b04712c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4821,7 +4821,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed", "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", "wipe_tower_filament", - "best_object_pos", "master_extruder_id" + "best_object_pos", "master_extruder_id", + "is_imex" })) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) @@ -9781,6 +9782,24 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; + + // iMEX parallel mode + multi-material caution check (current plate only) + { + PartPlate* plate = partplate_list.get_curr_plate(); + if (plate && plate->has_imex_multimaterial_conflict()) { + int plate_num = partplate_list.get_curr_plate_index() + 1; + wxString msg = wxString::Format( + _L("Plate %d has an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n" + "Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. " + "Proceed with caution and verify your G-code handles this combination correctly.\n\n" + "Continue slicing?"), + plate_num); + MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() != wxID_YES) + return; + } + } + //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); auto& print = q->get_partplate_list().get_current_fff_print(); @@ -9800,6 +9819,32 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) { if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; + + // iMEX parallel mode + multi-material caution check (all plates) + { + std::vector conflict_plates; + int plate_count = partplate_list.get_plate_count(); + for (int i = 0; i < plate_count; ++i) { + PartPlate* plate = partplate_list.get_plate(i); + if (plate && plate->has_imex_multimaterial_conflict()) + conflict_plates.push_back(i + 1); // 1-based for display + } + if (!conflict_plates.empty()) { + wxString plate_list; + for (int n : conflict_plates) + plate_list += wxString::Format(" %d", n); + wxString msg = wxString::Format( + _L("Plate(s)%s have an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n" + "Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. " + "Proceed with caution and verify your G-code handles this combination correctly.\n\n" + "Continue slicing?"), + plate_list); + MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() != wxID_YES) + return; + } + } + //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); auto& print = q->get_partplate_list().get_current_fff_print(); @@ -16180,6 +16225,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) { bool update_scheduled = false; bool bed_shape_changed = false; + bool imex_changed = false; //bool print_sequence_changed = false; t_config_option_keys diff_keys = p->config->diff(config); @@ -16270,8 +16316,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } // 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. + // Defer until after the loop so bed_shape_changed / set_bed_shape() run first if needed. else if (opt_key == "is_imex") { - p->partplate_list.refresh_imex_icons(); + imex_changed = true; update_scheduled = true; } } @@ -16279,6 +16326,16 @@ void Plater::on_config_change(const DynamicPrintConfig &config) if (bed_shape_changed) set_bed_shape(); + // After any bed-shape or is_imex change, ensure the iMEX mode icon geometry and + // raycaster are correct. set_shape() short-circuits when the bed is unchanged, + // so we call refresh_imex_icons() explicitly whenever is_imex is active. + // Always done AFTER set_bed_shape() so m_shape is current. + if (bed_shape_changed || imex_changed) { + auto* is_imex_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("is_imex"); + if (is_imex_opt && is_imex_opt->value) + p->partplate_list.refresh_imex_icons(); + } + config_change_notification(config, std::string("print_sequence")); if (update_scheduled) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 7b7bdc8974..90afb0928d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4653,17 +4653,34 @@ private: 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(r.gcode, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); + + // Placeholder search + (for non-primary) delete — stacked vertically + auto* btn_col = new wxBoxSizer(wxVERTICAL); + wxTextCtrl* gcode_ctrl = r.gcode; + auto* ph_btn = new ScalableButton(r.panel, wxID_ANY, "edit", wxEmptyString, + wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, 16); + ph_btn->SetToolTip(_L("Edit G-code / browse placeholders")); + ph_btn->Bind(wxEVT_BUTTON, [this, gcode_ctrl](wxCommandEvent&) { + EditGCodeDialog dlg(this, "imex_mode_gcode", gcode_ctrl->GetValue().ToStdString()); + if (dlg.ShowModal() == wxID_OK) + gcode_ctrl->SetValue(dlg.get_edited_gcode()); + }); + btn_col->Add(ph_btn, 0, wxBOTTOM, 2); if (!is_primary) { - auto* rm = new wxButton(r.panel, wxID_ANY, wxT("\u00d7"), - wxDefaultPosition, wxSize(24, 24), wxBU_EXACTFIT); + auto* rm = new ScalableButton(r.panel, wxID_ANY, "imex_remove", wxEmptyString, + wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, 16); + rm->SetToolTip(_L("Remove mode")); rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) { remove_row(this_panel); notify(); }); - sizer->Add(rm, 0, wxALIGN_CENTER_VERTICAL); + btn_col->Add(rm, 0); } + sizer->Add(btn_col, 0, wxALIGN_CENTER_VERTICAL); r.panel->SetSizer(sizer); m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, 4); From 5aa624b025975221a8fec353f51514c2bbf72606 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Sat, 11 Apr 2026 02:00:02 -0400 Subject: [PATCH 021/154] fix: Mark secondary carriages as used in iMEX parallel modes In iMEX (IDEX/IQEX) parallel printing modes (copy/mirror), only the primary extruder generates toolpaths. The firmware duplicates the primary's movements for secondary carriages, so they don't appear in tool_ordering.all_extruders(). This caused is_extruder_used[N] to return false for secondary tools even though they're physically active and moving. The fix adds logic to parse the active mode's tool assignments from imex_mode_active_tools config and marks all assigned tools as used. Changes: - Added null checks and bounds validation for config options - Skip empty tool strings to avoid unnecessary parsing - Reordered bounds checks for defensive programming - Added clarifying comments for exception handling This ensures is_extruder_used[N] is true for all tools in a parallel mode, allowing printer profiles to correctly enable heaters and emit cleanup G-code for all active carriages. Fixes: is_extruder_used[1] returns false in copy/mirror modes (#13086) Related: Comment [28]/[30], Comment [18] (is_extruder_used in G-code header) --- src/libslic3r/GCode.cpp | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 87e4f2d25f..cec0bf9550 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -2823,6 +2824,58 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato std::vector is_extruder_used(std::max(size_t(MAXIMUM_EXTRUDER_NUMBER), print.config().filament_diameter.size()), 0); for (unsigned int extruder : tool_ordering.all_extruders()) is_extruder_used[extruder] = true; + + // Orca iMEX: Also mark secondary carriages as used in parallel modes + // so is_extruder_used[N] is true for all physical tools in the active mode + if (print.config().is_imex.value && !print.objects().empty()) { + // Get the active mode for the first plate (all plates should have same mode in a single print) + // Assumes all objects share the same mode — validated upstream by the UI + const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value; + std::string active_mode = raw_mode.empty() ? "primary" : raw_mode; + + // Look up the mode's active tools from printer config + 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()) { + // Linear scan to find the matching mode (fine for small fixed sets of modes) + 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) { + const std::string& active_tools_str = tools_opt->values[i]; + // Skip empty tool strings (no active tools in this mode) + if (active_tools_str.empty()) continue; + + // Parse "idx:P,idx:C,idx:M" format to extract tool indices + std::istringstream ss(active_tools_str); + std::string token; + while (std::getline(ss, token, ',')) { + token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); + if (token.empty()) continue; + try { + auto colon = token.find(':'); + int idx; + if (colon != std::string::npos) { + idx = std::stoi(token.substr(0, colon)); + } else { + idx = std::stoi(token); + } + // Mark this tool as used (idx is 0-based tool index) + // idx < 0 would require a leading '-' in the string, which would fail stoi first + if (idx >= 0 && idx < (int)is_extruder_used.size()) { + is_extruder_used[idx] = true; + } + } catch (const std::invalid_argument&) { + // not a valid integer token, skip + } catch (const std::out_of_range&) { + // index out of integer range, skip + } + } + break; + } + } + } + } + this->placeholder_parser().set("is_extruder_used", new ConfigOptionBools(is_extruder_used)); { From ad0b42fd373ca2b2a3abe9167b2064121dbb1a3b Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 13 Apr 2026 17:45:11 -0400 Subject: [PATCH 022/154] feat: iMEX temperature handling and pre-slice warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Temperature emission (GCode.cpp, PrintConfig.cpp, ConfigManipulation.cpp): - Set temperatures for all active tools in layer_change_gcode for iMEX parallel modes (primary + secondary carriages) - Fix filament temperature commands so Layer 1 temperatures only emit on the first layer; subsequent layers use normal layer-change temperatures - Remove stray temperature commands that fired outside the intended context - Consolidate iMEX temperature handling into the second-layer transition; clean up emission logic and naming throughout Pre-slice warning dialogs (PartPlate.cpp/hpp, Plater.cpp): - Collect per-plate IMEX warnings before slicing: multi-material conflict, bed temperature mismatch between carriages (>5 °C delta), and filament type incompatibility - Show a dismissible Yes/No dialog from both "Slice Plate" and "Slice All" actions; No returns to 3D view, Yes proceeds to slice - Refresh plate thumbnails after the panel switch so previously-generated thumbnails are not left black Co-Authored-By: Claude Sonnet 4.6 --- src/libslic3r/GCode.cpp | 136 ++++++++++-------- src/libslic3r/PrintConfig.cpp | 12 +- src/slic3r/GUI/ConfigManipulation.cpp | 3 + src/slic3r/GUI/PartPlate.cpp | 18 +-- src/slic3r/GUI/PartPlate.hpp | 6 +- src/slic3r/GUI/Plater.cpp | 190 ++++++++++++++++++++------ 6 files changed, 247 insertions(+), 118 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index cec0bf9550..9c191ce4af 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2391,6 +2391,46 @@ static BambuBedType to_bambu_bed_type(BedType type) return bambu_bed_type; } +// Orca IMEX: Returns the tool indices active in the current IMEX mode. +// In copy/mirror parallel modes, secondary carriages (T1-T3) never receive tool-change +// commands — the firmware mirrors the primary's moves — so they don't appear in +// tool_ordering.all_extruders(). This helper parses imex_mode_active_tools to enumerate them. +// Format: "0:P,1:C,2:M,3:M" — only the leading integer index is used. +static std::vector get_imex_active_tools(const Print& print) +{ + std::vector active_tools; + + if (!print.config().is_imex.value || print.objects().empty()) + return active_tools; + + const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value; + const std::string active_mode = raw_mode.empty() ? "primary" : raw_mode; + + 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; + std::istringstream ss(tools_opt->values[i]); + std::string token; + while (std::getline(ss, token, ',')) { + token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); + if (token.empty()) continue; + try { + int idx = std::stoi(token); // leading integer before optional ':role' suffix + if (idx >= 0 && idx < (int)MAXIMUM_EXTRUDER_NUMBER) + active_tools.push_back(idx); + } catch (...) {} + } + break; + } + return active_tools; +} + void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb) { PROFILE_FUNC(); @@ -2825,56 +2865,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato for (unsigned int extruder : tool_ordering.all_extruders()) is_extruder_used[extruder] = true; - // Orca iMEX: Also mark secondary carriages as used in parallel modes - // so is_extruder_used[N] is true for all physical tools in the active mode - if (print.config().is_imex.value && !print.objects().empty()) { - // Get the active mode for the first plate (all plates should have same mode in a single print) - // Assumes all objects share the same mode — validated upstream by the UI - const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value; - std::string active_mode = raw_mode.empty() ? "primary" : raw_mode; - - // Look up the mode's active tools from printer config - 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()) { - // Linear scan to find the matching mode (fine for small fixed sets of modes) - 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) { - const std::string& active_tools_str = tools_opt->values[i]; - // Skip empty tool strings (no active tools in this mode) - if (active_tools_str.empty()) continue; - - // Parse "idx:P,idx:C,idx:M" format to extract tool indices - std::istringstream ss(active_tools_str); - std::string token; - while (std::getline(ss, token, ',')) { - token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); - if (token.empty()) continue; - try { - auto colon = token.find(':'); - int idx; - if (colon != std::string::npos) { - idx = std::stoi(token.substr(0, colon)); - } else { - idx = std::stoi(token); - } - // Mark this tool as used (idx is 0-based tool index) - // idx < 0 would require a leading '-' in the string, which would fail stoi first - if (idx >= 0 && idx < (int)is_extruder_used.size()) { - is_extruder_used[idx] = true; - } - } catch (const std::invalid_argument&) { - // not a valid integer token, skip - } catch (const std::out_of_range&) { - // index out of integer range, skip - } - } - break; - } - } - } - } + // Orca IMEX: Also mark secondary carriages as used in parallel modes + // so is_extruder_used[N] is true for all physical tools in the active mode. + // Secondary carriages never appear in tool_ordering.all_extruders() because + // the firmware duplicates the primary's moves — no tool-change commands are emitted. + for (int tool_idx : get_imex_active_tools(print)) + if (tool_idx < (int)is_extruder_used.size()) + is_extruder_used[tool_idx] = true; this->placeholder_parser().set("is_extruder_used", new ConfigOptionBools(is_extruder_used)); @@ -4409,6 +4406,7 @@ std::string GCode::generate_skirt(const Print &print, // In non-sequential mode, process_layer is called per each print_z height with all object and support layers accumulated. // For multi-material prints, this routine minimizes extruder switches by gathering extruder specific extrusion paths // and performing the extruder specific extrusions together. + LayerResult GCode::process_layer( const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. @@ -4559,6 +4557,7 @@ LayerResult GCode::process_layer( + "\n"; config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); } + //BBS: set layer time fan speed after layer change gcode gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n"; @@ -4675,16 +4674,31 @@ LayerResult GCode::process_layer( gcode += m_writer.set_jerk_xy(m_config.default_jerk.value); } - // Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent - // nozzle_temperature_initial_layer vs. temperature settings. - for (const Extruder& extruder : m_writer.extruders()) { - if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) && - extruder.id() != m_writer.filament()->id()) - // In single extruder multi material mode, set the temperature for the current extruder only. - continue; - int temperature = print.config().nozzle_temperature.get_at(extruder.id()); - if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(extruder.id())) - gcode += m_writer.set_temperature(temperature, false, extruder.id()); + // Transition from 1st to 2nd layer: set non-initial-layer nozzle temperatures. + // IMEX parallel modes: secondary carriages (T1-T3) never receive tool-change commands, + // so they're not in m_writer.extruders() and multiple_extruders==false (max id==0). + // Use the static set_temperature path for IMEX so the T index is always emitted. + if (print.config().is_imex.value) { + // All active tools need explicit temps — none receive tool-change commands, + // so we can't rely on the condition used for non-IMEX (temp != initial_layer_temp). + // A tool whose initial and regular temps are the same still needs to be set here. + const int num_nozzles = (int)print.config().nozzle_temperature.values.size(); + for (int tool_idx : get_imex_active_tools(print)) { + if (tool_idx >= num_nozzles) continue; + int temperature = print.config().nozzle_temperature.values[tool_idx]; + if (temperature > 0) + gcode += GCodeWriter::set_temperature(temperature, m_writer.config.gcode_flavor, false, tool_idx, "set IMEX tool temperature"); + } + } else { + for (const Extruder& extruder : m_writer.extruders()) { + if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) && + extruder.id() != m_writer.filament()->id()) + // In single extruder multi material mode, set the temperature for the current extruder only. + continue; + int temperature = print.config().nozzle_temperature.get_at(extruder.id()); + if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(extruder.id())) + gcode += m_writer.set_temperature(temperature, false, extruder.id()); + } } // BBS @@ -7534,6 +7548,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config); check_add_eol(gcode); } + if (m_config.enable_pressure_advance.get_at(new_filament_id)) { gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA @@ -7828,6 +7843,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo } check_add_eol(gcode); } + // Set the new extruder to the operating temperature. if (m_ooze_prevention.enable) gcode += m_ooze_prevention.post_toolchange(*this); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 310d799e19..4bd3361afc 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10511,16 +10511,16 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef() new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone."); def = this->add("imex_mode", coString); - def->label = L("iMEX active mode"); - def->tooltip = L("Name of the active iMEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if iMEX is not enabled."); + def->label = L("IMEX active mode"); + def->tooltip = L("Name of the active IMEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if IMEX is not enabled."); def = this->add("imex_mode_index", coInt); - def->label = L("iMEX active mode index"); - def->tooltip = L("Zero-based index of the active iMEX parallel print mode within imex_mode_names."); + def->label = L("IMEX active mode index"); + def->tooltip = L("Zero-based index of the active IMEX parallel print mode within imex_mode_names."); def = this->add("imex_mode_gcode", coString); - def->label = L("iMEX active mode G-code"); - def->tooltip = L("The raw mode G-code template for the active iMEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode."); + def->label = L("IMEX active mode G-code"); + def->tooltip = L("The raw mode G-code template for the active IMEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode."); } PrintStatisticsConfigDef::PrintStatisticsConfigDef() diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index e41886008b..f531aebafb 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -793,6 +793,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co bool has_ironing = (config->opt_enum("ironing_type") != IroningType::NoIroning); for (auto el : { "ironing_pattern", "ironing_flow", "ironing_spacing", "ironing_angle", "ironing_inset", "ironing_angle_fixed" }) toggle_line(el, has_ironing); + bool has_rectilinear_ironing = (config->opt_enum("ironing_pattern") == InfillPattern::ipRectilinear); + for (auto el : {"ironing_angle", "ironing_angle_fixed"}) + toggle_field(el, has_ironing && has_rectilinear_ironing); toggle_line("ironing_speed", has_ironing || has_support_ironing); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index e599f69171..594af28f19 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -925,7 +925,7 @@ bool PartPlate::has_imex_placement_violations() bool PartPlate::has_imex_multimaterial_conflict() const { - // Condition 1: iMEX is active and the plate mode is non-primary + // Condition 1: IMEX is active and the plate mode is non-primary auto* pb = wxGetApp().preset_bundle; if (!pb) return false; auto* is_imex_opt = pb->printers.get_edited_preset().config.option("is_imex"); @@ -1254,8 +1254,8 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model) init_raycaster_from_model(model); } -// Positions the iMEX multi-material warning badge as a small overlay at the bottom-right -// corner of the iMEX mode icon. The icon slot index matches the one used in calc_vertex_for_icons. +// Positions the IMEX multi-material warning badge as a small overlay at the bottom-right +// corner of the IMEX mode icon. The icon slot index matches the one used in calc_vertex_for_icons. void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &model) { model.reset(); @@ -1268,7 +1268,7 @@ void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &mo float gap_y = PARTPLATE_ICON_GAP_Y * factor; float gap_top = PARTPLATE_ICON_GAP_TOP * factor; - // Centre of the iMEX mode icon (top-left corner = p after offset) + // Centre of the IMEX mode icon (top-left corner = p after offset) p += Vec2d(gap_left, -1 * (imex_icon_index * (size + gap_y) + gap_top)); // Badge is half the icon size, anchored to the bottom-right corner of the icon slot @@ -1282,7 +1282,7 @@ void PartPlate::calc_vertex_for_imex_warn_badge(int imex_icon_index, GLModel &mo poly.contour.append({ scale_(bp(0)) , scale_(bp(1) + badge)}); if (!init_model_from_poly(model, poly, GROUND_Z + 0.01f)) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to generate geometry for iMEX warn badge\n"; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to generate geometry for IMEX warn badge\n"; } /* @@ -1796,11 +1796,11 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else { render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_texture); } - // Warning badge: iMEX parallel mode active alongside multi-material objects + // Warning badge: IMEX parallel mode active alongside multi-material objects if (has_imex_multimaterial_conflict()) { render_icon_texture(m_imex_warn_icon, m_partplate_list->m_imex_warn_texture); if (hover_id == (int)PLATE_IMEX_MODE_ID) - show_tooltip(_u8L("Warning: this plate uses a parallel iMEX mode with multi-material objects. Proceed with caution — verify your G-code handles this combination correctly.")); + show_tooltip(_u8L("Warning: this plate uses a parallel IMEX mode with multi-material objects. Proceed with caution — verify your G-code handles this combination correctly.")); } } } @@ -4682,11 +4682,11 @@ void PartPlateList::generate_icon_textures() } } - // iMEX multi-material conflict warning badge + // IMEX multi-material conflict warning badge { file_name = path + "obj_warning.svg"; if (!m_imex_warn_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iMEX warn badge)") % file_name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (IMEX warn badge)") % file_name; } } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 858e50fccd..e77e13fb60 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -156,7 +156,7 @@ private: PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; PickingModel m_imex_mode_icon; - GLModel m_imex_warn_icon; // warning badge: iMEX parallel mode + multi-material conflict + GLModel m_imex_warn_icon; // warning badge: IMEX parallel mode + multi-material conflict GLModel m_plate_idx_icon; GLTexture m_texture; @@ -456,7 +456,7 @@ public: } // Returns true if any instance on this plate overlaps an IDEX/IQEX secondary or collision zone. bool has_imex_placement_violations(); - // Returns true when iMEX parallel mode is active (non-primary) AND the plate + // Returns true when IMEX parallel mode is active (non-primary) AND the plate // has objects assigned to more than one filament — a combination that warrants caution. bool has_imex_multimaterial_conflict() const; // Returns the primary-zone bounding box in mm when an IDEX/IQEX parallel mode is active. @@ -642,7 +642,7 @@ class PartPlateList : public ObjectBase GLTexture m_plate_name_edit_hovered_texture; GLTexture m_imex_mode_texture; GLTexture m_imex_mode_hovered_texture; - GLTexture m_imex_warn_texture; // warning badge for iMEX + multi-material conflict + GLTexture m_imex_warn_texture; // warning badge for IMEX + multi-material conflict GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cd4b04712c..b969d2c8c7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -9778,25 +9779,131 @@ void Plater::priv::on_action_open_project(SimpleEvent&) } //BBS: GUI refactor: slice plate +// Orca IMEX: Collects all IMEX-related warnings for a plate before slicing. +// Returns one formatted bullet string per issue. Empty means no concerns. +// Checks: +// 1. Multi-material objects alongside a parallel mode (existing badge condition) +// 2. Bed temperature conflict: primary tool controls the bed; secondary tools warned +// if their optimal bed temp differs from the primary's by more than 5°C +// 3. Filament type mismatch: materials with incompatible requirements +static std::vector collect_imex_warnings(PartPlate* plate) +{ + std::vector warnings; + if (!plate) return warnings; + + // Check 1: multi-material conflict (same condition that drives the badge) + if (plate->has_imex_multimaterial_conflict()) + warnings.push_back(_L("Multi-material objects detected — secondary carriages may not behave as expected with multi-material prints.")); + + // Checks 2 & 3 only apply when a non-primary parallel mode is active + const std::string mode = plate->get_imex_mode(); + if (mode == "primary") return warnings; + + PresetBundle* bundle = wxGetApp().preset_bundle; + if (!bundle) return warnings; + + // Resolve active tool indices for this mode from printer config + const auto& printer_cfg = bundle->printers.get_edited_preset().config; + const auto* mode_names_opt = printer_cfg.option("imex_mode_names"); + const auto* tools_opt = printer_cfg.option("imex_mode_active_tools"); + + // Upper bound: filament_presets.size() is the practical limit, capped at MAXIMUM_EXTRUDER_NUMBER (64) + const size_t max_tool = std::min(bundle->filament_presets.size(), MAXIMUM_EXTRUDER_NUMBER); + + std::vector active_tools; + if (mode_names_opt && tools_opt) { + for (size_t i = 0; i < mode_names_opt->values.size(); ++i) { + if (i >= tools_opt->values.size() || mode_names_opt->values[i] != mode) continue; + std::istringstream ss(tools_opt->values[i]); + std::string token; + while (std::getline(ss, token, ',')) { + token.erase(std::remove_if(token.begin(), token.end(), ::isspace), token.end()); + if (token.empty()) continue; + try { + int idx = std::stoi(token); + if (idx >= 0 && (max_tool == 0 || (size_t)idx < max_tool)) + active_tools.push_back(idx); + } catch (...) {} + } + break; + } + } + + if (active_tools.size() < 2) return warnings; + + const int primary_tool = active_tools[0]; + const DynamicPrintConfig& full_cfg = bundle->full_config(); + // Bed temps are per-plate-type; resolve the active plate's bed type to get the right key. + const BedType bed_type = bundle->project_config.opt_enum("curr_bed_type"); + const std::string bed_temp_key = get_bed_temp_key(bed_type); + const auto* bed_temps = bed_temp_key.empty() ? nullptr : full_cfg.option(bed_temp_key); + const auto& filament_presets = bundle->filament_presets; + + // Primary tool's bed temp and filament type + const int primary_bed_temp = (bed_temps && primary_tool < (int)bed_temps->values.size()) + ? bed_temps->values[primary_tool] : 0; + std::string primary_display_type; + if (primary_tool < (int)filament_presets.size()) { + Preset* p = bundle->filaments.find_preset(filament_presets[primary_tool]); + if (p) p->get_filament_type(primary_display_type); + } + if (primary_display_type.empty()) primary_display_type = "unknown"; + + for (size_t i = 1; i < active_tools.size(); ++i) { + const int tool_idx = active_tools[i]; + + std::string secondary_display_type; + if (tool_idx < (int)filament_presets.size()) { + Preset* p = bundle->filaments.find_preset(filament_presets[tool_idx]); + if (p) p->get_filament_type(secondary_display_type); + } + if (secondary_display_type.empty()) secondary_display_type = "unknown"; + + // Check 2: bed temperature — primary wins, secondary may not get what it needs + if (bed_temps && primary_bed_temp > 0 && tool_idx < (int)bed_temps->values.size()) { + const int secondary_bed_temp = bed_temps->values[tool_idx]; + if (secondary_bed_temp > 0 && std::abs(secondary_bed_temp - primary_bed_temp) > 5) { + warnings.push_back(wxString::Format( + _L("Bed temperature conflict: T%d (%s) sets the bed to %d\u00B0C \u2014 " + "T%d (%s) requires %d\u00B0C. The primary tool controls the bed; " + "secondary tools print at whatever temperature it sets."), + primary_tool, primary_display_type, primary_bed_temp, + tool_idx, secondary_display_type, secondary_bed_temp)); + } + } + + // Check 3: filament type mismatch + if (primary_display_type != secondary_display_type && + primary_display_type != "unknown" && secondary_display_type != "unknown") { + warnings.push_back(wxString::Format( + _L("Filament type mismatch: T%d uses %s and T%d uses %s. " + "These materials have incompatible requirements and are not designed to print together."), + primary_tool, primary_display_type, + tool_idx, secondary_display_type)); + } + } + + return warnings; +} + void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; - - // iMEX parallel mode + multi-material caution check (current plate only) + // IMEX parallel mode warnings (current plate only) { PartPlate* plate = partplate_list.get_curr_plate(); - if (plate && plate->has_imex_multimaterial_conflict()) { + std::vector warnings = collect_imex_warnings(plate); + if (!warnings.empty()) { int plate_num = partplate_list.get_curr_plate_index() + 1; - wxString msg = wxString::Format( - _L("Plate %d has an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n" - "Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. " - "Proceed with caution and verify your G-code handles this combination correctly.\n\n" - "Continue slicing?"), - plate_num); - MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO); - if (dlg.ShowModal() != wxID_YES) + wxString msg = wxString::Format(_L("Plate %d has IDEX/IQEX parallel mode active with the following concerns:\n\n"), plate_num); + for (const wxString& w : warnings) + msg += L"\u2022 " + w + "\n\n"; + msg += _L("Continue slicing?"); + MessageDialog dlg(q, msg, _L("IDEX/IQEX Parallel Mode Warning"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() != wxID_YES) { + q->select_view_3D("3D"); return; + } } } @@ -9811,6 +9918,8 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) m_slice_all = false; q->reslice(); q->select_view_3D("Preview"); + // Regenerate any thumbnails that were wiped by the panel switch above. + q->update_all_plate_thumbnails(); } } @@ -9818,30 +9927,30 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) void Plater::priv::on_action_slice_all(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; - - // iMEX parallel mode + multi-material caution check (all plates) + // IMEX parallel mode warnings (all plates) { - std::vector conflict_plates; + wxString combined_msg; + std::vector warned_plates; int plate_count = partplate_list.get_plate_count(); for (int i = 0; i < plate_count; ++i) { PartPlate* plate = partplate_list.get_plate(i); - if (plate && plate->has_imex_multimaterial_conflict()) - conflict_plates.push_back(i + 1); // 1-based for display + std::vector warnings = collect_imex_warnings(plate); + if (warnings.empty()) continue; + warned_plates.push_back(plate); + combined_msg += wxString::Format(_L("Plate %d:\n"), i + 1); + for (const wxString& w : warnings) + combined_msg += L" \u2022 " + w + "\n"; + combined_msg += "\n"; } - if (!conflict_plates.empty()) { - wxString plate_list; - for (int n : conflict_plates) - plate_list += wxString::Format(" %d", n); - wxString msg = wxString::Format( - _L("Plate(s)%s have an IDEX/IQEX parallel mode active alongside multi-material objects.\n\n" - "Secondary tool heads operate at the firmware level and may not behave as expected with multi-material prints. " - "Proceed with caution and verify your G-code handles this combination correctly.\n\n" - "Continue slicing?"), - plate_list); - MessageDialog dlg(q, msg, _L("IDEX/IQEX Multi-Material Caution"), wxICON_WARNING | wxYES | wxNO); - if (dlg.ShowModal() != wxID_YES) + if (!combined_msg.empty()) { + wxString msg = _L("The following IDEX/IQEX parallel mode concerns were detected:\n\n") + + combined_msg + + _L("Continue slicing?"); + MessageDialog dlg(q, msg, _L("IDEX/IQEX Parallel Mode Warning"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() != wxID_YES) { + q->select_view_3D("3D"); return; + } } } @@ -9861,6 +9970,8 @@ void Plater::priv::on_action_slice_all(SimpleEvent&) q->reslice(); if (!m_is_publishing) q->select_view_3D("Preview"); + // Regenerate any thumbnails that were wiped by the panel switch above. + q->update_all_plate_thumbnails(); //BBS: wish to select all plates stats item preview->get_canvas3d()->_update_select_plate_toolbar_stats_item(true); } @@ -13394,7 +13505,6 @@ void Plater::invalid_all_plate_thumbnails() { if (using_exported_file() || skip_thumbnail_invalid) return; - BOOST_LOG_TRIVIAL(info) << "thumb: invalid all"; for (int i = 0; i < get_partplate_list().get_plate_count(); i++) { PartPlate* plate = get_partplate_list().get_plate(i); plate->thumbnail_data.reset(); @@ -15256,10 +15366,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy ThumbnailData* thumbnail_data = &p->partplate_list.get_plate(i)->thumbnail_data; if (p->partplate_list.get_plate(i)->thumbnail_data.is_valid() && using_exported_file()) { //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i }; p->generate_thumbnail(p->partplate_list.get_plate(i)->thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho); @@ -15269,9 +15379,9 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy ThumbnailData *no_light_thumbnail_data = &p->partplate_list.get_plate(i)->no_light_thumbnail_data; if (p->partplate_list.get_plate(i)->no_light_thumbnail_data.is_valid() && using_exported_file()) { // no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; p->generate_thumbnail(p->partplate_list.get_plate(i)->no_light_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true); @@ -15286,10 +15396,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy ThumbnailData* top_thumbnail = &p->partplate_list.get_plate(i)->top_thumbnail_data; if (top_thumbnail->is_valid() && using_exported_file()) { //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; p->generate_thumbnail(p->partplate_list.get_plate(i)->top_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, false); @@ -15299,10 +15409,10 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy ThumbnailData* picking_thumbnail = &p->partplate_list.get_plate(i)->pick_thumbnail_data; if (picking_thumbnail->is_valid() && using_exported_file()) { //no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i; } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; p->generate_thumbnail(p->partplate_list.get_plate(i)->pick_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, true,true); @@ -16326,7 +16436,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) if (bed_shape_changed) set_bed_shape(); - // After any bed-shape or is_imex change, ensure the iMEX mode icon geometry and + // After any bed-shape or is_imex change, ensure the IMEX mode icon geometry and // raycaster are correct. set_shape() short-circuits when the bed is unchanged, // so we call refresh_imex_icons() explicitly whenever is_imex is active. // Always done AFTER set_bed_shape() so m_shape is current. From 857bfbfb82a0ded4a6f9413afd53e9195aad5298 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Tue, 14 Apr 2026 09:17:23 -0400 Subject: [PATCH 023/154] fix: guard against empty option_set in OG_CustomCtrl widget-only lines update_visibility() and render() both called front() on an empty vector when a CtrlLine had no options (pure widget lines). Added an early-return path in update_visibility() and a null-guard in render(). Co-Authored-By: Claude Sonnet 4.6 --- src/slic3r/GUI/OG_CustomCtrl.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/OG_CustomCtrl.cpp b/src/slic3r/GUI/OG_CustomCtrl.cpp index da4564f537..3eb3ac2edc 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.cpp +++ b/src/slic3r/GUI/OG_CustomCtrl.cpp @@ -723,6 +723,19 @@ void OG_CustomCtrl::CtrlLine::update_visibility(ConfigOptionMode mode) return; const std::vector