diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index cec0bf9550..9c191ce4af 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -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 get_imex_active_tools(const Print& print) +{ + std::vector 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("imex_mode_names"); + const auto* tools_opt = print.config().option("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("imex_mode_names"); - const auto* tools_opt = print.config().option("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); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 310d799e19..4bd3361afc 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -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() diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index e41886008b..f531aebafb 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -793,6 +793,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co bool has_ironing = (config->opt_enum("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("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); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index e599f69171..594af28f19 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -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("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; } } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 858e50fccd..e77e13fb60 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -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; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cd4b04712c..b969d2c8c7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -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 collect_imex_warnings(PartPlate* plate) +{ + std::vector 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("imex_mode_names"); + const auto* tools_opt = printer_cfg.option("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 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("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(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 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 conflict_plates; + wxString combined_msg; + std::vector 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 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.