feat: iMEX temperature handling and pre-slice warnings

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 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-04-13 17:45:11 -04:00
co-authored by Claude Sonnet 4.6
parent 5aa624b025
commit ad0b42fd37
6 changed files with 247 additions and 118 deletions
+76 -60
View File
@@ -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<int> get_imex_active_tools(const Print& print)
{
std::vector<int> 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<ConfigOptionStrings>("imex_mode_names");
const auto* tools_opt = print.config().option<ConfigOptionStrings>("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<ConfigOptionStrings>("imex_mode_names");
const auto* tools_opt = print.config().option<ConfigOptionStrings>("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);
+6 -6
View File
@@ -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()
+3
View File
@@ -793,6 +793,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
bool has_ironing = (config->opt_enum<IroningType>("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<InfillPattern>("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);
+9 -9
View File
@@ -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<ConfigOptionBool>("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;
}
}
+3 -3
View File
@@ -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;
+150 -40
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <numeric>
#include <limits>
#include <sstream>
#include <vector>
#include <string>
#include <regex>
@@ -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<wxString> collect_imex_warnings(PartPlate* plate)
{
std::vector<wxString> 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<ConfigOptionStrings>("imex_mode_names");
const auto* tools_opt = printer_cfg.option<ConfigOptionStrings>("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<int> 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<BedType>("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<ConfigOptionInts>(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<wxString> 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<int> conflict_plates;
wxString combined_msg;
std::vector<PartPlate*> 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<wxString> 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.