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 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-04-10 00:10:03 -04:00
co-authored by Claude Sonnet 4.6
parent 1d68f917b0
commit 73b5b750c6
5 changed files with 159 additions and 12 deletions
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><line x1="2.5" y1="2.5" x2="13.5" y2="13.5" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round;opacity:1"/><line x1="13.5" y1="2.5" x2="2.5" y2="13.5" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round;opacity:1"/></svg>

After

Width:  |  Height:  |  Size: 354 B

+72 -6
View File
@@ -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<ConfigOptionBool>("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<ConfigOptionBool>("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<int> 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<ConfigOptionBool>("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<ConfigOptionBool>("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();
}
+6
View File
@@ -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<BoundingBoxf> 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;
+59 -2
View File
@@ -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<NotificationManager>(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<int> 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<ConfigOptionBool>("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)
+21 -4
View File
@@ -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);