From ec40172019bacf5a3827c3fc9016bfbbb545ac3f Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Tue, 7 Apr 2026 23:25:22 -0400 Subject: [PATCH] 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