diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index abef0c29816..f7cd015c9fc 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15332,6 +15332,15 @@ msgstr "检测到高温材料与低温材料同时使用。混合打印可能导 msgid "Detected both high and low temperature materials. Mixed printing may result in extruder clogging, nozzle damage, or layer adhesion issues. To continue printing, enable \"Allow mixed printing of high and low temperature materials\" in Preferences." msgstr "检测到高温材料与低温材料同时使用。混合打印可能导致挤出机堵塞、喷嘴损坏或层间粘附问题。如需继续打印,请在「偏好设置」中开启 \"允许高/低温材料混合打印\"。" +msgid "Flow ratio is 0%, resulting in zero extrusion and no valid toolpath. " +msgstr "流量比例为 0%,挤出量为零,无法生成有效打印路径。" + +msgid "Filament(s):" +msgstr "耗材:" + +msgid "Please set the flow ratio to a value greater than 0." +msgstr "请将其设置为大于 0 的值。" + msgid "This material combination may cause risks. Do you want to continue?" msgstr "此材料组合可能存在风险,是否继续?" @@ -15348,4 +15357,25 @@ msgid "Other Colors" msgstr "其他颜色" msgid "Multiple Color" -msgstr "多色" \ No newline at end of file +msgstr "多色" + +msgid "High temperature:" +msgstr "高温耗材:" + +msgid "Low temperature:" +msgstr "低温耗材:" + +msgid "The following plates contain mixed high and low temperature materials:" +msgstr "以下盘存在高温与低温耗材混用:" + +msgid "unknown" +msgstr "未知" + +msgid "does not exist." +msgstr "不存在。" + +msgid "" +"To continue printing, enable \"Allow mixed printing of high and low " +"temperature materials\" in Preferences." +msgstr "" +"如需继续打印,请在「偏好设置」中开启「允许高低温耗材混打」。" \ No newline at end of file diff --git a/src/Snapmaker_Orca.cpp b/src/Snapmaker_Orca.cpp index 2e2f4421e3e..b4f9b5cde80 100644 --- a/src/Snapmaker_Orca.cpp +++ b/src/Snapmaker_Orca.cpp @@ -5028,7 +5028,7 @@ int CLI::run(int argc, char **argv) case STRING_EXCEPT_FILAMENT_NOT_MATCH_BED_TYPE: validate_error = CLI_FILAMENT_NOT_MATCH_BED_TYPE; break; - case STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP: + case STRING_EXCEPT_FILAMENTS_MIXING_TEMP: validate_error = CLI_FILAMENTS_DIFFERENT_TEMP; break; case STRING_EXCEPT_OBJECT_COLLISION_IN_SEQ_PRINT: diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp index 9889fe90cbf..491be9d2eed 100644 --- a/src/libslic3r/GCodeReader.cpp +++ b/src/libslic3r/GCodeReader.cpp @@ -126,6 +126,14 @@ template bool GCodeReader::parse_file_raw_internal(const std::string &filename, ParseLineCallback parse_line_callback, LineEndCallback line_end_callback) { FilePtr in{ boost::nowide::fopen(filename.c_str(), "rb") }; + if (in.f == nullptr) { + // fopen failed — file missing, inaccessible, or path invalid. + // Returning false here prevents ::fread(buffer.data(), 1, ..., NULL) + // below, which would otherwise trigger a CRT invalid-parameter crash. + BOOST_LOG_TRIVIAL(error) << "GCodeReader::parse_file_raw_internal: " + << "failed to open file '" << filename << "'"; + return false; + } // Read the input stream 64kB at a time, extract lines and process them. std::vector buffer(65536 * 10, 0); diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index b680ac274e4..c01c4d29a49 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -19,10 +19,11 @@ namespace Slic3r { enum StringExceptionType { STRING_EXCEPT_NOT_DEFINED = 0, STRING_EXCEPT_FILAMENT_NOT_MATCH_BED_TYPE = 1, - STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP = 2, + STRING_EXCEPT_FILAMENTS_MIXING_TEMP = 2, STRING_EXCEPT_OBJECT_COLLISION_IN_SEQ_PRINT = 3, STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT = 4, STRING_EXCEPT_LAYER_HEIGHT_EXCEEDS_LIMIT = 5, + STRING_EXCEPT_FLOW_RATIO_ZERO = 6, STRING_EXCEPT_COUNT }; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 837889ac872..2c3515693d2 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -7922,7 +7922,9 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED; } else { - if (!plate_list.get_plate(i)->can_slice()) + if (!plate_list.get_plate(i)->can_slice() || + wxGetApp().plater()->is_plate_blocked_by_filament_temp_mixing(i) || + wxGetApp().plater()->is_plate_blocked_by_flow_ratio_zero(i)) m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED; else { if (plate_list.get_plate(i)->get_slicing_percent() < 0.0f) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index ba317d2be97..cc2c77c74f1 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1923,6 +1923,10 @@ bool MainFrame::get_enable_slice_status() { enable = false; } + else if (m_plater->is_plate_blocked_by_flow_ratio_zero(part_plate_list.get_curr_plate_index())) + { + enable = false; + } } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cc0e8ecef11..8178d8ea953 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -207,20 +207,140 @@ static const std::pair THUMBNAIL_SIZE_3MF = { 512, 5 namespace Slic3r { namespace GUI { -static std::string filament_temp_mixing_warning_text() +// Safe translation helper: returns the UTF-8 form of a translated string as a +// std::string. This avoids a dangling-pointer trap in I18N::translate_utf8() +// (which returns wxGetTranslation(...).ToUTF8().data() — a pointer into a +// temporary wxString). We keep the translated wxString alive across the +// utf8_str() call and copy the bytes into a std::string before returning. +static std::string tr_u8(const char* s) { - return _u8L("Detected both high and low temperature materials. " - "Mixed printing may result in extruder clogging, " - "nozzle damage, or layer adhesion issues."); + const wxString ws = _L(s); + const wxScopedCharBuffer buf = ws.utf8_str(); + return std::string(buf.data(), buf.length()); } -static std::string filament_temp_mixing_error_text() +// Build a user-facing label for a single filament slot, in the form +// "[n] PresetName". Falls back to "[n] (unknown)" if the preset can't be +// resolved — same defensive style as the null checks in check_filament_temp_mixing. +static std::string filament_display_label(int slot_1based) { - return _u8L("Detected both high and low temperature materials. " - "Mixed printing may result in extruder clogging, " - "nozzle damage, or layer adhesion issues. " - "To continue printing, enable \"Allow mixed printing " + const int slot_0_based = slot_1based - 1; + PresetBundle* bundle = wxGetApp().preset_bundle; + std::string name = tr_u8("unknown"); + if (bundle != nullptr + && slot_0_based >= 0 + && slot_0_based < static_cast(bundle->filament_presets.size())) { + const Preset* preset = bundle->filaments.find_preset(bundle->filament_presets[slot_0_based], true); + if (preset != nullptr) + name = preset->name; + } + return std::string("[") + std::to_string(slot_1based) + "] " + name; +} + +// Compose a single comma-separated line of "[n] Preset" labels from a list +// of 1-based slot numbers. Used inside High/Low temperature group lines. +static std::string format_filament_slot_list(const std::vector& slots_1based) +{ + std::string out; + for (size_t i = 0; i < slots_1based.size(); ++i) { + if (i != 0) + out += ", "; + out += filament_display_label(slots_1based[i]); + } + return out; +} + +// Append the High/Low temperature grouping block (two lines, only groups that +// are non-empty) to `out`. Called by both the single-plate and slice-all +// text builders so the layout stays identical. +static void append_filament_temp_mixing_groups(std::string& out, const Plater::FilamentTempMixingDetail& detail) +{ + if (!detail.high_temp_slots_1based.empty()) { + out += tr_u8("High temperature:"); + out += " "; + out += format_filament_slot_list(detail.high_temp_slots_1based); + out += "\n"; + } + if (!detail.low_temp_slots_1based.empty()) { + out += tr_u8("Low temperature:"); + out += " "; + out += format_filament_slot_list(detail.low_temp_slots_1based); + out += "\n"; + } +} + +static std::string filament_temp_mixing_warning_text(const Plater::FilamentTempMixingDetail& detail) +{ + std::string out = tr_u8("Detected both high and low temperature materials. " + "Mixed printing may result in extruder clogging, " + "nozzle damage, or layer adhesion issues."); + out += "\n\n"; + append_filament_temp_mixing_groups(out, detail); + return out; +} + +static std::string filament_temp_mixing_error_text(const Plater::FilamentTempMixingDetail& detail) +{ + std::string out = tr_u8("Detected both high and low temperature materials. " + "Mixed printing may result in extruder clogging, " + "nozzle damage, or layer adhesion issues."); + out += "\n\n"; + append_filament_temp_mixing_groups(out, detail); + out += "\n"; + out += tr_u8("To continue printing, enable \"Allow mixed printing " "of high and low temperature materials\" in Preferences."); + return out; +} + +// Build the single-line banner for the flow-ratio-zero pre-slice blocker. +// Layout (single line, semicolon-separated): +// "Flow ratio is 0%, ... no valid toolpath. Filament(s): [1] PLA White; [3] PETG Black. Please set ..." +// Uses std::string += concatenation (no boost::format) to avoid format_error exceptions, +// matching the filament_temp_mixing_*_text blueprints above. +static std::string flow_ratio_zero_error_text(const Plater::FlowRatioZeroDetail& detail) +{ + std::string out = tr_u8("Flow ratio is 0%, resulting in zero extrusion " + "and no valid toolpath. "); + out += tr_u8("Filament(s):"); + out += " "; + for (size_t i = 0; i < detail.offender_slots_1based.size(); ++i) { + if (i != 0) + out += "; "; + out += filament_display_label(detail.offender_slots_1based[i]); + } + out += ". "; + out += tr_u8("Please set the flow ratio to a value greater than 0."); + return out; +} + +// Slice-all variants: list every plate that has a mixing conflict, each with +// its own High/Low grouping. Plates without conflicts are not shown. +static std::string filament_temp_mixing_warning_text_slice_all(const std::vector& plates) +{ + std::string out = tr_u8("The following plates contain mixed high and low temperature materials:"); + out += "\n\n"; + for (const Plater::PlateMixingInfo& info : plates) { + out += tr_u8("Plate"); + out += " " + std::to_string(info.plate_index_1based) + "\n"; + append_filament_temp_mixing_groups(out, info.detail); + out += "\n"; + } + return out; +} + +static std::string filament_temp_mixing_error_text_slice_all(const std::vector& plates) +{ + std::string out = tr_u8("The following plates contain mixed high and low temperature materials:"); + out += "\n\n"; + for (const Plater::PlateMixingInfo& info : plates) { + out += tr_u8("Plate"); + out += " " + std::to_string(info.plate_index_1based) + "\n"; + append_filament_temp_mixing_groups(out, info.detail); + out += "\n"; + } + out += tr_u8("To continue printing, enable \"Allow mixed printing " + "of high and low temperature materials\" in Preferences."); + return out; } static bool model_object_is_on_plate(PartPlate* plate, size_t obj_idx, const ModelObject* model_object) @@ -244,58 +364,49 @@ static bool model_object_is_on_plate(PartPlate* plate, size_t obj_idx, const Mod static void collect_filament_slots_from_config( const DynamicPrintConfig& config, - int num_filaments, - std::set& used_slots) + int num_filaments, std::set& used_slots_0_based) { - static const std::vector keys_1based = { - "wall_filament", - "sparse_infill_filament", - "solid_infill_filament" - }; - for (const char* key : keys_1based) - { - const ConfigOptionInt* option = config.option(key); - if (option != nullptr && option->value >= 1 && option->value <= num_filaments) - used_slots.insert(option->value - 1); - } - - static const std::vector keys_0based = { + // Support/feature filaments + static const std::vector feature_keys = { "support_filament", "support_interface_filament", + "wall_filament", + "sparse_infill_filament", + "solid_infill_filament", "wipe_tower_filament" }; - for (const char* key : keys_0based) + for (const char* key : feature_keys) { const ConfigOptionInt* option = config.option(key); if (option != nullptr && option->value >= 1 && option->value <= num_filaments) - used_slots.insert(option->value - 1); + used_slots_0_based.insert(option->value - 1); } + // Primary filament (extruder) const ConfigOptionInt* extruder_option = config.option("extruder"); if (extruder_option != nullptr && extruder_option->value >= 1 && extruder_option->value <= num_filaments) - used_slots.insert(extruder_option->value - 1); + used_slots_0_based.insert(extruder_option->value - 1); } static void collect_filament_slots_from_model_config( const ModelConfigObject& config, - int num_filaments, - std::set& used_slots) + int num_filaments, std::set& used_slots_0_based) { + // Primary filament (extruder) if (config.has("extruder")) { const int extruder_id = config.extruder(); if (extruder_id >= 1 && extruder_id <= num_filaments) - used_slots.insert(extruder_id - 1); + used_slots_0_based.insert(extruder_id - 1); } - // Per-object feature-specific keys (wall_filament, etc.) may be - // overridden independently of the object's primary extruder. + // Support/feature filaments static const std::vector feature_keys = { + "support_filament", + "support_interface_filament", "wall_filament", "sparse_infill_filament", "solid_infill_filament", - "support_filament", - "support_interface_filament", "wipe_tower_filament" }; for (const char* key : feature_keys) @@ -304,7 +415,7 @@ static void collect_filament_slots_from_model_config( { const int val = config.opt_int(key); if (val >= 1 && val <= num_filaments) - used_slots.insert(val - 1); + used_slots_0_based.insert(val - 1); } } } @@ -8851,6 +8962,13 @@ struct Plater::priv bool filament_temp_mixing_notification_initialized = false; int filament_temp_mixing_notification_plate = -1; FilamentTempMixingState filament_temp_mixing_notification_state = FilamentTempMixingState::Compatible; + // NotificationManager::close_validate_* uses exact-text match, so we must + // remember the exact strings we last pushed and use those (not freshly + // regenerated ones) to close the previous notification. Empty = nothing pushed. + std::string filament_temp_mixing_last_error_text; + std::string filament_temp_mixing_last_warning_text; + // Same pattern as above, for the flow_ratio_zero banner. + std::string flow_ratio_zero_last_error_text; MenuFactory menus; @@ -9505,6 +9623,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_FILAMENT_USAGE_CHANGED, [this](SimpleEvent&) { filament_usage_sync_pending = false; this->q->sync_filament_temp_mixing_notification(); + this->q->sync_flow_ratio_zero_notification(); }); main_frame->m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, &priv::on_tab_selection_changing, this); @@ -9765,13 +9884,12 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) //BBS: set on_slice to false q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { if (q->is_view3D_shown()) { - if (q->has_sliceable_plate_for_slice_all()) { - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); - wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); - return; - } - if (q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index())) { + PartPlate* curr = q->get_partplate_list().get_curr_plate(); + if (curr && (!curr->can_slice() || + q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index()) || + q->is_plate_blocked_by_flow_ratio_zero(q->get_partplate_list().get_curr_plate_index()))) { q->sync_filament_temp_mixing_notification(); + q->sync_flow_ratio_zero_notification(); q->select_view_3D("Preview", true); return; } @@ -10735,11 +10853,22 @@ std::vector Plater::priv::load_files(const std::vector& input_ NotificationManager *notify_manager = q->get_notification_manager(); std::string error_message = L("Invalid values found in the 3mf:"); error_message += "\n"; - for (std::map::iterator it=validity.begin(); it!=validity.end(); ++it) + bool any_entry = false; + for (std::map::iterator it=validity.begin(); it!=validity.end(); ++it) { + // filament_flow_ratio == 0 is handled by the dedicated + // flow_ratio_zero pre-slice banner (which also names the + // specific offending filament slot). Skip it here to avoid + // a redundant, less informative notification. + if (it->first == "filament_flow_ratio") + continue; error_message += "-" + it->first + ": " + it->second + "\n"; - error_message += "\n"; - error_message += L("Please correct them in the param tabs"); - notify_manager->bbl_show_3mf_warn_notification(error_message); + any_entry = true; + } + if (any_entry) { + error_message += "\n"; + error_message += L("Please correct them in the param tabs"); + notify_manager->bbl_show_3mf_warn_notification(error_message); + } } } if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string()); @@ -12312,7 +12441,15 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool else { return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; } - if (filament_ok && invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) + // flow_ratio_zero sync MUST run after the blanket close_notification_of_type + // (ValidateError) above — otherwise our banner gets closed immediately. + // Its return value gates UPDATE_BACKGROUND_PROCESS_INVALID so that + // restart_background_process() refuses to start the slice when the + // plate is blocked (Preview-tab switch / reslice path). + const bool flow_ratio_ok = q->sync_flow_ratio_zero_notification(); + if (!flow_ratio_ok) + return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; + if (filament_ok && flow_ratio_ok && invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; if (printer_technology == ptFFF) { @@ -12329,6 +12466,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool //also update the warnings process_validation_warning(warning); q->sync_filament_temp_mixing_notification(); + q->sync_flow_ratio_zero_notification(); return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; if (printer_technology == ptFFF) { const Print* print = background_process.fff_print(); @@ -12351,6 +12489,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool if (background_process.empty()) { process_validation_warning({}); q->sync_filament_temp_mixing_notification(); + q->sync_flow_ratio_zero_notification(); } actualize_slicing_warnings(*this->background_process.current_print()); actualize_object_warnings(*this->background_process.current_print()); @@ -17618,6 +17757,20 @@ void Plater::load_gcode(const wxString& filename) ) return; + // Reject a missing / inaccessible file up front. Without this check the + // code below would walk through process_file → parse_file_raw_internal, + // which used to crash on a NULL FILE* (now it just returns false), and + // surface the misleading "does not contain valid G-code" message even when + // the real problem is that the file doesn't exist at all. + if (!wxFileExists(filename)) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": file does not exist: " << filename; + MessageDialog(this, + _L("The selected file") + ":\n" + filename + "\n" + _L("does not exist."), + wxString(GCODEVIEWER_APP_NAME) + " - " + _L("Error occurs while loading G-code file"), + wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal(); + return; + } + m_last_loaded_gcode = filename; // BSS: create a new project when load_gcode, force close previous one @@ -17777,6 +17930,7 @@ std::vector Plater::load_files(const std::vector& input_files, // m_apply_invalid; no per-plate flag initialization is needed. notify_filament_usage_changed(); sync_filament_temp_mixing_notification(); + sync_flow_ratio_zero_notification(); } return loaded; } @@ -19970,6 +20124,23 @@ int Plater::start_next_slice() return -1; } + // Independent dimension: flow_ratio == 0. Two separate blockers (see + // top_cover_design.md 14.7) — both must be checked, never collapsed into + // an else-if chain that would let one mask the other. + if (is_plate_blocked_by_flow_ratio_zero(p->partplate_list.get_curr_plate_index())) + { + sync_flow_ratio_zero_notification(); + if (p->m_slice_all) + { + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Finished, nullptr); + wxQueueEvent(this, evt.Clone()); + return 0; + } + p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); + return -1; + } + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true, false, false); @@ -20752,139 +20923,145 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const bool Plater::check_filament_temp_mixing(int plate_index) { - const int plate_count = p->partplate_list.get_plate_count(); - if (plate_index < 0 || plate_index >= plate_count) - return true; - - const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); - const ConfigOptionStrings* filament_type_option = full_cfg.option("filament_type"); - if (filament_type_option == nullptr || filament_type_option->values.empty()) - return true; - - const int num_filaments = static_cast(filament_type_option->values.size()); - std::set used_slots; + FilamentTempMixingDetail unused; + return check_filament_temp_mixing(plate_index, unused); +} - PartPlate* plate = p->partplate_list.get_plate(plate_index); - if (plate == nullptr) - return true; +bool Plater::check_filament_temp_mixing(int plate_index, FilamentTempMixingDetail& detail) +{ + detail.high_temp_slots_1based.clear(); + detail.low_temp_slots_1based.clear(); - bool has_object_on_plate = false; - for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) + // Boundary checks + PartPlate* plate = nullptr; + const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); + const ConfigOptionStrings* filament_type_option = full_cfg.option("filament_type"); { - const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; - if (model_object_is_on_plate(plate, obj_idx, model_object)) - { - has_object_on_plate = true; - break; - } - } - if (!has_object_on_plate) - return true; + if (filament_type_option == nullptr || filament_type_option->values.empty()) + return true; - // Also collect from current plate's config for any plate-level overrides - collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots); + const int plate_count = p->partplate_list.get_plate_count(); + if (plate_index < 0 || plate_index >= plate_count) + return true; - // Collect from ModelVolume painting extruders for objects on the - // current plate. Also track whether any object relies on the global - // default extruder (extruder=0) so we can resolve it at the end. - bool uses_default_extruder = false; - for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) - { - const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; - if (!model_object_is_on_plate(plate, obj_idx, model_object)) - continue; - collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots); - if (!model_object->config.has("extruder") || model_object->config.extruder() == 0) - uses_default_extruder = true; - for (const ModelVolume* model_volume : model_object->volumes) - { - collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots); - for (int extruder_id : model_volume->get_extruders()) - { - if (extruder_id >= 1 && extruder_id <= num_filaments) - used_slots.insert(extruder_id - 1); + plate = p->partplate_list.get_plate(plate_index); + if (plate == nullptr) + return true; + + bool has_object_on_plate = false; + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (model_object_is_on_plate(plate, obj_idx, model_object)) { + has_object_on_plate = true; + break; } } + if (!has_object_on_plate) + return true; } - // Collect from the Plater working config. The approach balances - // sensitivity against false positives: - // - Global features (wipe tower, support) always apply → always collected. - // - Feature-specific keys (wall_filament, infill) depend on the global - // process defaults. They are only collected when at least one object - // on the plate uses the default extruder (e=0), which means those - // defaults WILL affect the actual slicing output. + // Collect filament slots actually used on this plate + std::set used_slots_0_based; { - // Always collect: features that cannot be overridden per-object. - static const std::vector always_collect = { - "wipe_tower_filament", - "support_filament", - "support_interface_filament" - }; - for (const char* key : always_collect) - { - const ConfigOptionInt* option = this->config()->option(key); - if (option != nullptr && option->value >= 1 && option->value <= num_filaments) - used_slots.insert(option->value - 1); + // Plate config + const int num_filaments = static_cast(filament_type_option->values.size()); + collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots_0_based); + + // ModelObject config + bool uses_default_extruder = false; + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (!model_object_is_on_plate(plate, obj_idx, model_object)) + continue; + collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots_0_based); + + if (!model_object->config.has("extruder") || model_object->config.extruder() == 0) + uses_default_extruder = true; + + // ModelVolume config + for (const ModelVolume* model_volume : model_object->volumes) { + collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots_0_based); + for (int extruder_id : model_volume->get_extruders()) { + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots_0_based.insert(extruder_id - 1); + } + } } - // If any object uses e=0, the global process defaults for - // wall / infill extruders apply and must be collected. - if (uses_default_extruder) + // Collect from the Plater working config. The approach balances + // sensitivity against false positives: + // - Global features (wipe tower, support) always apply → always collected. + // - Feature-specific keys (wall_filament, infill) depend on the global + // process defaults. They are only collected when at least one object + // on the plate uses the default extruder (e=0), which means those + // defaults WILL affect the actual slicing output. { - static const std::vector default_keys = { - "wall_filament", - "sparse_infill_filament", - "solid_infill_filament" - }; - for (const char* key : default_keys) - { + // Always collect: features that cannot be overridden per-object. + static const std::vector always_collect = {"wipe_tower_filament", "support_filament", "support_interface_filament"}; + for (const char* key : always_collect) { const ConfigOptionInt* option = this->config()->option(key); if (option != nullptr && option->value >= 1 && option->value <= num_filaments) - used_slots.insert(option->value - 1); + used_slots_0_based.insert(option->value - 1); + } + + // If any object uses e=0, the global process defaults for + // wall / infill extruders apply and must be collected. + if (uses_default_extruder) { + static const std::vector default_keys = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"}; + for (const char* key : default_keys) { + const ConfigOptionInt* option = config()->option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots_0_based.insert(option->value - 1); + } } } - } - // Resolve the global default extruder if any object on this plate - // uses extruder=0. p->config does not include the "extruder" key - // (it is not in the initializer list at priv constructor), so we - // must read it from full_config() instead. - if (uses_default_extruder) - { - const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); - if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) - used_slots.insert(extruder_opt->value - 1); + // Resolve the global default extruder if any object on this plate + // uses extruder=0. p->config does not include the "extruder" key + // (it is not in the initializer list at priv constructor), so we + // must read it from full_config() instead. + if (uses_default_extruder) { + const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); + if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) + used_slots_0_based.insert(extruder_opt->value - 1); + } } - - if (used_slots.empty()) + if (used_slots_0_based.empty()) return true; // Read filament_is_high_temperature directly from each filament preset's // own config rather than through full_config(). full_config() builds a // merged snapshot that may lag behind when called from Sidebar hooks // (the edited preset config hasn't been committed yet). - PresetBundle* bundle = wxGetApp().preset_bundle; + // + // `used_slots_0_based` is a std::set so iteration is ascending and + // de-duplicated; the 1-based vectors we fill here inherit that ordering. bool has_high = false, has_low = false; - - for (int slot : used_slots) { - if (slot < static_cast(bundle->filament_presets.size())) - { - const Preset* preset = bundle->filaments.find_preset( - bundle->filament_presets[slot], true); - if (preset != nullptr) - { - const bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); - if (is_high) - has_high = true; - else - has_low = true; + PresetBundle* bundle = wxGetApp().preset_bundle; + for (int slot : used_slots_0_based) { + if (slot < 0 || slot >= static_cast(bundle->filament_presets.size())) + continue; + const Preset* preset = bundle->filaments.find_preset(bundle->filament_presets[slot], true); + if (preset == nullptr) + continue; + const bool is_high = preset->config.opt_bool("filament_is_high_temperature", 0); + if (is_high) { + has_high = true; + detail.high_temp_slots_1based.push_back(slot + 1); + } else { + has_low = true; + detail.low_temp_slots_1based.push_back(slot + 1); } } } - const bool compatible = !(has_high && has_low); + const bool compatible = !(has_high && has_low); + if (compatible) { + // No conflict — clear detail so callers can't read stale partial data. + detail.high_temp_slots_1based.clear(); + detail.low_temp_slots_1based.clear(); + } return compatible; } @@ -20927,7 +21104,9 @@ int Plater::find_next_sliceable_plate_for_slice_all(int start_plate_index) for (int plate_index = start_plate_index; plate_index < plate_count; ++plate_index) { PartPlate* plate = p->partplate_list.get_plate(plate_index); - if (plate != nullptr && plate->can_slice() && !is_plate_blocked_by_filament_temp_mixing(plate_index)) + if (plate != nullptr && plate->can_slice() + && !is_plate_blocked_by_filament_temp_mixing(plate_index) + && !is_plate_blocked_by_flow_ratio_zero(plate_index)) return plate_index; } @@ -20936,39 +21115,59 @@ int Plater::find_next_sliceable_plate_for_slice_all(int start_plate_index) bool Plater::sync_filament_temp_mixing_notification() { - const int curr_plate_index = get_partplate_list().get_curr_plate_index(); PartPlate* curr_plate = get_partplate_list().get_curr_plate(); if (curr_plate == nullptr) { BOOST_LOG_TRIVIAL(warning) << "[Plater] sync_filament_temp_mixing_notification: curr_plate is null"; return true; } + + const int curr_plate_index = get_partplate_list().get_curr_plate_index(); const FilamentTempMixingState mixing_state = get_filament_temp_mixing_state(curr_plate_index); bool slicing_allowed = true; + // 1. Always close the previously-pushed notifications using their exact + // cached text. NotificationManager::close_validate_* matches on text, + // so we cannot use a freshly regenerated template string here — it + // would not match the previous (possibly different) body. + NotificationManager* nm = get_notification_manager(); + if (!p->filament_temp_mixing_last_error_text.empty()) { + nm->close_validate_error_notification(p->filament_temp_mixing_last_error_text); + p->filament_temp_mixing_last_error_text.clear(); + } + if (!p->filament_temp_mixing_last_warning_text.empty()) { + nm->close_validate_warning_notification(p->filament_temp_mixing_last_warning_text); + p->filament_temp_mixing_last_warning_text.clear(); + } + + // 2. Compute the per-plate detail (used by both warning and error text). + FilamentTempMixingDetail detail; + if (mixing_state != FilamentTempMixingState::Compatible) + check_filament_temp_mixing(curr_plate_index, detail); + switch (mixing_state) { case FilamentTempMixingState::Compatible: - get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); - get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); // Filament temp mixing is compatible — only clear our own notification, // do NOT touch m_apply_invalid. Bed type mismatch or other validation // errors must not be cleared by the filament temp mixing system. slicing_allowed = true; break; - case FilamentTempMixingState::AllowedWarning: - get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); - get_notification_manager()->push_notification( + case FilamentTempMixingState::AllowedWarning: { + const std::string body = tr_u8("WARNING:") + "\n" + filament_temp_mixing_warning_text(detail); + nm->push_notification( NotificationType::ValidateWarning, NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + filament_temp_mixing_warning_text()); + body); + p->filament_temp_mixing_last_warning_text = body; slicing_allowed = true; break; + } case FilamentTempMixingState::BlockedError: { StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - err.string = filament_temp_mixing_error_text(); - get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); - get_notification_manager()->push_validate_error_notification(err); + err.type = STRING_EXCEPT_FILAMENTS_MIXING_TEMP; + err.string = filament_temp_mixing_error_text(detail); + nm->push_validate_error_notification(err); + p->filament_temp_mixing_last_error_text = err.string; // Blocking is enforced through get_enable_slice_status() / find_next_sliceable_plate_for_slice_all() // which independently check is_plate_blocked_by_filament_temp_mixing(). // Do NOT set m_apply_invalid — that flag belongs to the background validation system. @@ -20990,20 +21189,182 @@ bool Plater::sync_filament_temp_mixing_notification() return slicing_allowed; } +// flow_ratio_zero per-plate validation. Mirrors check_filament_temp_mixing / +// sync_filament_temp_mixing_notification structure. Only two states (ok / blocked) +// because there is no user-pref override for flow_ratio == 0. +bool Plater::check_flow_ratio_zero(int plate_index) +{ + FlowRatioZeroDetail unused; + return check_flow_ratio_zero(plate_index, unused); +} + +bool Plater::check_flow_ratio_zero(int plate_index, FlowRatioZeroDetail& detail) +{ + detail.offender_slots_1based.clear(); + + // Boundary checks (mirror check_filament_temp_mixing). + PartPlate* plate = nullptr; + const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); + const ConfigOptionStrings* filament_type_option = full_cfg.option("filament_type"); + { + if (filament_type_option == nullptr || filament_type_option->values.empty()) + return true; + + const int plate_count = p->partplate_list.get_plate_count(); + if (plate_index < 0 || plate_index >= plate_count) + return true; + + plate = p->partplate_list.get_plate(plate_index); + if (plate == nullptr) + return true; + + bool has_object_on_plate = false; + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (model_object_is_on_plate(plate, obj_idx, model_object)) { + has_object_on_plate = true; + break; + } + } + if (!has_object_on_plate) + return true; + } + + // Collect filament slots actually used on this plate (same shared logic). + std::set used_slots_0_based; + { + const int num_filaments = static_cast(filament_type_option->values.size()); + collect_filament_slots_from_config(*plate->config(), num_filaments, used_slots_0_based); + + bool uses_default_extruder = false; + for (size_t obj_idx = 0; obj_idx < wxGetApp().model().objects.size(); ++obj_idx) { + const ModelObject* model_object = wxGetApp().model().objects[obj_idx]; + if (!model_object_is_on_plate(plate, obj_idx, model_object)) + continue; + collect_filament_slots_from_model_config(model_object->config, num_filaments, used_slots_0_based); + + if (!model_object->config.has("extruder") || model_object->config.extruder() == 0) + uses_default_extruder = true; + + for (const ModelVolume* model_volume : model_object->volumes) { + collect_filament_slots_from_model_config(model_volume->config, num_filaments, used_slots_0_based); + for (int extruder_id : model_volume->get_extruders()) { + if (extruder_id >= 1 && extruder_id <= num_filaments) + used_slots_0_based.insert(extruder_id - 1); + } + } + } + + { + static const std::vector always_collect = {"wipe_tower_filament", "support_filament", "support_interface_filament"}; + for (const char* key : always_collect) { + const ConfigOptionInt* option = this->config()->option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots_0_based.insert(option->value - 1); + } + + if (uses_default_extruder) { + static const std::vector default_keys = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"}; + for (const char* key : default_keys) { + const ConfigOptionInt* option = config()->option(key); + if (option != nullptr && option->value >= 1 && option->value <= num_filaments) + used_slots_0_based.insert(option->value - 1); + } + } + } + + if (uses_default_extruder) { + const ConfigOptionInt* extruder_opt = full_cfg.option("extruder"); + if (extruder_opt != nullptr && extruder_opt->value >= 1 && extruder_opt->value <= num_filaments) + used_slots_0_based.insert(extruder_opt->value - 1); + } + } + if (used_slots_0_based.empty()) + return true; + + // Read filament_flow_ratio from each used preset's own config (same defensive + // pattern as filament_is_high_temperature in check_filament_temp_mixing). + // `<=` rather than `== 0.0` to dodge floating-point comparison traps. + { + PresetBundle* bundle = wxGetApp().preset_bundle; + for (int slot : used_slots_0_based) { + if (slot < 0 || slot >= static_cast(bundle->filament_presets.size())) + continue; + const Preset* preset = bundle->filaments.find_preset(bundle->filament_presets[slot], true); + if (preset == nullptr) + continue; + const double ratio = preset->config.opt_float("filament_flow_ratio", 0); + if (ratio <= 0.0) + detail.offender_slots_1based.push_back(slot + 1); + } + } + + return detail.offender_slots_1based.empty(); +} + +bool Plater::is_plate_blocked_by_flow_ratio_zero(int plate_index) +{ + return !check_flow_ratio_zero(plate_index); +} + +bool Plater::sync_flow_ratio_zero_notification() +{ + PartPlate* curr_plate = get_partplate_list().get_curr_plate(); + if (curr_plate == nullptr) { + BOOST_LOG_TRIVIAL(warning) << "[Plater] sync_flow_ratio_zero_notification: curr_plate is null"; + return true; + } + + const int curr_plate_index = get_partplate_list().get_curr_plate_index(); + FlowRatioZeroDetail detail; + const bool blocked = !check_flow_ratio_zero(curr_plate_index, detail); + bool slicing_allowed = true; + + NotificationManager* nm = get_notification_manager(); + + // 1. Close any previously-pushed banner using its exact cached text. + // close_validate_error_notification() matches by text, so we must use + // the cached string (which may differ from a freshly regenerated body). + if (!p->flow_ratio_zero_last_error_text.empty()) { + nm->close_validate_error_notification(p->flow_ratio_zero_last_error_text); + p->flow_ratio_zero_last_error_text.clear(); + } + + // 2. Push a fresh banner only if blocked. Blocking itself is enforced + // independently by get_enable_slice_status() / find_next_sliceable_plate_for_slice_all() + // via is_plate_blocked_by_flow_ratio_zero(), so we do NOT touch m_apply_invalid + // (that flag belongs to the background validation system). + if (blocked) { + StringObjectException err; + err.type = STRING_EXCEPT_FLOW_RATIO_ZERO; + err.string = flow_ratio_zero_error_text(detail); + nm->push_validate_error_notification(err); + p->flow_ratio_zero_last_error_text = err.string; + slicing_allowed = false; + } + + const bool can_slice = curr_plate->can_slice() && slicing_allowed; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, can_slice); + return slicing_allowed; +} + bool Plater::guard_before_slice_plate() { sync_filament_temp_mixing_notification(); + sync_flow_ratio_zero_notification(); return confirm_filament_temp_mixing_before_slice(); } bool Plater::guard_before_slice_all() { + sync_flow_ratio_zero_notification(); return confirm_filament_temp_mixing_before_slice_all(); } bool Plater::confirm_filament_temp_mixing_before_slice() { - switch (get_filament_temp_mixing_state()) + const FilamentTempMixingState state = get_filament_temp_mixing_state(); + switch (state) { case FilamentTempMixingState::Compatible: return true; @@ -21014,11 +21375,20 @@ bool Plater::confirm_filament_temp_mixing_before_slice() break; default: BOOST_LOG_TRIVIAL(warning) << "[Plater] confirm_filament_temp_mixing_before_slice: unknown state " - << static_cast(get_filament_temp_mixing_state()); + << static_cast(state); return true; } - MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + // Show the actual high/low filament breakdown so the user can see which + // slots are involved before deciding. MessageDialog takes wxString, so + // convert the UTF-8 body produced by the text builder. + FilamentTempMixingDetail detail; + check_filament_temp_mixing(p->partplate_list.get_curr_plate_index(), detail); + const std::string body = filament_temp_mixing_warning_text(detail) + + "\n" + + tr_u8("Do you want to continue?"); + + MessageDialog dlg(this, wxString::FromUTF8(body.c_str()), _L("Confirm slicing"), wxICON_WARNING | wxOK | wxCANCEL); dlg.SetButtonLabel(wxID_OK, _L("Confirm")); dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); @@ -21030,25 +21400,34 @@ bool Plater::confirm_filament_temp_mixing_before_slice_all() if (!has_sliceable_plate_for_slice_all()) return false; - // Only count plates that can be sliced AND haven't been sliced - // yet. Already-sliced plates don't need re-confirmation. - bool has_allowed_warning = false; + // Collect every sliceable, not-yet-sliced plate that is currently in the + // AllowedWarning state, along with its high/low filament breakdown so the + // confirmation dialog can show per-plate details. + std::vector mixing_plates; for (int plate_index = 0; plate_index < p->partplate_list.get_plate_count(); ++plate_index) { PartPlate* plate = p->partplate_list.get_plate(plate_index); - if (plate != nullptr && plate->can_slice() && - !plate->is_slice_result_valid() && - get_filament_temp_mixing_state(plate_index) == FilamentTempMixingState::AllowedWarning) - { - has_allowed_warning = true; - break; - } + if (plate == nullptr || !plate->can_slice() || plate->is_slice_result_valid()) + continue; + if (get_filament_temp_mixing_state(plate_index) != FilamentTempMixingState::AllowedWarning) + continue; + + PlateMixingInfo info; + info.plate_index_1based = plate_index + 1; + // state == AllowedWarning guarantees a mixing conflict exists, so the + // detail will be populated. + check_filament_temp_mixing(plate_index, info.detail); + mixing_plates.push_back(info); } - if (!has_allowed_warning) + if (mixing_plates.empty()) return true; - MessageDialog dlg(this, _L("This material combination may cause risks. Do you want to continue?"), + const std::string body = filament_temp_mixing_warning_text_slice_all(mixing_plates) + + "\n" + + tr_u8("Do you want to continue?"); + + MessageDialog dlg(this, wxString::FromUTF8(body.c_str()), _L("Confirm slicing"), wxICON_WARNING | wxOK | wxCANCEL); dlg.SetButtonLabel(wxID_OK, _L("Confirm")); dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); @@ -21909,6 +22288,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); sync_filament_temp_mixing_notification(); + sync_flow_ratio_zero_notification(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; return ret; @@ -21981,6 +22361,7 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) } sync_filament_temp_mixing_notification(); + sync_flow_ratio_zero_notification(); } PartPlate* part_plate = p->partplate_list.get_curr_plate(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 05683b8d5a5..452fae82a5b 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -240,6 +240,27 @@ class Plater: public wxPanel BlockedError }; + // Per-plate breakdown of used filaments that participate in a high/low + // temperature mixing conflict. Slot numbers are 1-based to match what the + // user sees in the UI. Vectors are sorted ascending and de-duplicated. + struct FilamentTempMixingDetail { + std::vector high_temp_slots_1based; + std::vector low_temp_slots_1based; + }; + + // One plate's mixing detail, paired with its 1-based plate number. + // Used to build the Slice All notification body. + struct PlateMixingInfo { + int plate_index_1based; + FilamentTempMixingDetail detail; + }; + + // Per-plate breakdown of filaments whose flow ratio is 0 and would produce + // zero extrusion. Slot numbers are 1-based to match the UI display. + struct FlowRatioZeroDetail { + std::vector offender_slots_1based; + }; + Plater(wxWindow *parent, MainFrame *main_frame); Plater(Plater &&) = delete; Plater(const Plater &) = delete; @@ -522,6 +543,10 @@ class Plater: public wxPanel /// @param plate_index Plate index to check. /// @return True if compatible or plate index is invalid; false if high/low temperature materials are mixed. bool check_filament_temp_mixing(int plate_index); + /// @brief Same as above, plus fills `detail` with the 1-based used slots on the plate grouped by + /// high/low temperature. `detail` is only meaningful when this overload returns false + /// (i.e. a mixing conflict exists). On compatible / invalid plates, `detail` is cleared. + bool check_filament_temp_mixing(int plate_index, FilamentTempMixingDetail& detail); /// @brief Get high/low temperature material mixing state for the current plate. /// @return Current plate material mixing state. FilamentTempMixingState get_filament_temp_mixing_state(); @@ -543,6 +568,20 @@ class Plater: public wxPanel /// Sync notification state with current filament temp mixing status. /// Returns true if slicing is allowed, false if high/low temperature mixing blocks slicing. bool sync_filament_temp_mixing_notification(); + /// @brief Check whether any used filament on a specific plate has flow ratio == 0. + /// @param plate_index Plate index to check. + /// @return True if all used filaments have positive flow ratio (or plate is invalid/empty); false otherwise. + bool check_flow_ratio_zero(int plate_index); + /// @brief Same as above, plus fills `detail` with the 1-based slots of offending filaments. + /// `detail` is only meaningful when this overload returns false. On valid plates, `detail` is cleared. + bool check_flow_ratio_zero(int plate_index, FlowRatioZeroDetail& detail); + /// @brief Check whether a specific plate is blocked because one of its used filaments has flow ratio == 0. + /// @param plate_index Plate index to check. + /// @return True if slicing this plate is blocked; otherwise false. + bool is_plate_blocked_by_flow_ratio_zero(int plate_index); + /// Sync notification state with current flow-ratio-zero status. + /// Returns true if slicing is allowed, false if any used filament has flow ratio == 0. + bool sync_flow_ratio_zero_notification(); /// Check and guard filament temp mixing before slicing current plate. bool guard_before_slice_plate(); /// Check and guard filament temp mixing before slicing all plates.