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);