From 73b5b750c69e0cb948cd0bda781be557a34facbd Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Fri, 10 Apr 2026 00:10:03 -0400 Subject: [PATCH] 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);