From dd7a9815b80b5ec546eeb35b2ef86f763878ee4e Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 15 Jul 2026 19:08:03 +0800 Subject: [PATCH 1/7] fix: unify filament temp mixing blocking with plate error gating - Remove has_sliceable_plate_for_slice_all() hijack in Preview tab switch that triggered unintended "slice all" instead of switching to preview - Add m_filament_temp_blocked flag to PartPlate so can_slice() reflects filament temp mixing state, consistent with m_apply_invalid for object-outside errors - Let sync_filament_temp_mixing_notification() manage its own flag, independent of the background validation system - Simplify EVT_GLVIEWTOOLBAR_PREVIEW to use can_slice() as unified gate for both error types Downstream effects: red exclamation icon on plate thumbnails, disabled slice/export buttons, and blocked auto-slice on preview switch now work uniformly for both object-outside and filament-temp-mixing errors. --- src/slic3r/GUI/PartPlate.hpp | 7 ++++++- src/slic3r/GUI/Plater.cpp | 18 +++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 2542eda53fe..73633636ec7 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -104,6 +104,7 @@ class PartPlate : public ObjectBase bool m_ready_for_slice; bool m_slice_result_valid; bool m_apply_invalid {false}; + bool m_filament_temp_blocked {false}; float m_slice_percent; Print *m_print; //Print reference, not own it, no need to serialize @@ -402,7 +403,7 @@ class PartPlate : public ObjectBase //can be sliced or not bool can_slice() const { - return m_ready_for_slice && !m_apply_invalid; + return m_ready_for_slice && !m_apply_invalid && !m_filament_temp_blocked; } void update_slice_ready_status(bool ready_slice) { @@ -418,6 +419,10 @@ class PartPlate : public ObjectBase { m_apply_invalid = invalid; } + void update_filament_temp_blocked(bool blocked) + { + m_filament_temp_blocked = blocked; + } //is slice result valid or not bool is_slice_result_valid() const diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cc0e8ecef11..5757aeb1f12 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9765,13 +9765,8 @@ 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())) { - q->sync_filament_temp_mixing_notification(); + PartPlate* curr = q->get_partplate_list().get_curr_plate(); + if (curr && !curr->can_slice()) { q->select_view_3D("Preview", true); return; } @@ -20948,14 +20943,13 @@ bool Plater::sync_filament_temp_mixing_notification() switch (mixing_state) { case FilamentTempMixingState::Compatible: + curr_plate->update_filament_temp_blocked(false); 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: + curr_plate->update_filament_temp_blocked(false); get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_notification_manager()->push_notification( NotificationType::ValidateWarning, @@ -20967,11 +20961,9 @@ bool Plater::sync_filament_temp_mixing_notification() StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = filament_temp_mixing_error_text(); + curr_plate->update_filament_temp_blocked(true); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_notification_manager()->push_validate_error_notification(err); - // 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. slicing_allowed = false; break; } From 22b3c7a477e09b15bf4948c234d147127fdcefb7 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 15 Jul 2026 20:11:10 +0800 Subject: [PATCH 2/7] fix: use on-demand checks instead of cached flag for filament temp mixing Replace the per-plate cached m_filament_temp_blocked flag with on-demand is_plate_blocked_by_filament_temp_mixing() checks in the rendering and preview-switch paths. This eliminates staleness risks identified during code review where non-current plates could retain stale blocked state after filament changes, undo/redo, or plate switching during slicing. Changes: - GLCanvas3D: add on-demand check for red exclamation on plate thumbnails - Plater: add on-demand check in EVT_GLVIEWTOOLBAR_PREVIEW alongside can_slice(), keep sync_filament_temp_mixing_notification() for notification freshness - PartPlate: revert cached flag; can_slice() unchanged Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/GLCanvas3D.cpp | 3 ++- src/slic3r/GUI/PartPlate.hpp | 7 +------ src/slic3r/GUI/Plater.cpp | 13 +++++++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 3c4943628a0..663ae1327ee 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -7922,7 +7922,8 @@ 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)) 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/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 73633636ec7..2542eda53fe 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -104,7 +104,6 @@ class PartPlate : public ObjectBase bool m_ready_for_slice; bool m_slice_result_valid; bool m_apply_invalid {false}; - bool m_filament_temp_blocked {false}; float m_slice_percent; Print *m_print; //Print reference, not own it, no need to serialize @@ -403,7 +402,7 @@ class PartPlate : public ObjectBase //can be sliced or not bool can_slice() const { - return m_ready_for_slice && !m_apply_invalid && !m_filament_temp_blocked; + return m_ready_for_slice && !m_apply_invalid; } void update_slice_ready_status(bool ready_slice) { @@ -419,10 +418,6 @@ class PartPlate : public ObjectBase { m_apply_invalid = invalid; } - void update_filament_temp_blocked(bool blocked) - { - m_filament_temp_blocked = blocked; - } //is slice result valid or not bool is_slice_result_valid() const diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5757aeb1f12..faed9d70194 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9766,7 +9766,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { if (q->is_view3D_shown()) { PartPlate* curr = q->get_partplate_list().get_curr_plate(); - if (curr && !curr->can_slice()) { + if (curr && (!curr->can_slice() || + q->is_plate_blocked_by_filament_temp_mixing(q->get_partplate_list().get_curr_plate_index()))) { + q->sync_filament_temp_mixing_notification(); q->select_view_3D("Preview", true); return; } @@ -20943,13 +20945,14 @@ bool Plater::sync_filament_temp_mixing_notification() switch (mixing_state) { case FilamentTempMixingState::Compatible: - curr_plate->update_filament_temp_blocked(false); 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: - curr_plate->update_filament_temp_blocked(false); get_notification_manager()->close_validate_error_notification(filament_temp_mixing_error_text()); get_notification_manager()->push_notification( NotificationType::ValidateWarning, @@ -20961,9 +20964,11 @@ bool Plater::sync_filament_temp_mixing_notification() StringObjectException err; err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; err.string = filament_temp_mixing_error_text(); - curr_plate->update_filament_temp_blocked(true); get_notification_manager()->close_validate_warning_notification(filament_temp_mixing_warning_text()); get_notification_manager()->push_validate_error_notification(err); + // 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. slicing_allowed = false; break; } From f6e653221ed3861ecb2fe0a59d641c10f511087f Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 16 Jul 2026 11:46:28 +0800 Subject: [PATCH 3/7] refactor: simplify filament slot collection in check_filament_temp_mixing Merge duplicate key lists, rename used_slots for clarity, and group logic into scoped blocks. No semantic change. --- src/slic3r/GUI/Plater.cpp | 235 +++++++++++++++++--------------------- 1 file changed, 106 insertions(+), 129 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index faed9d70194..24833287fc8 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -244,58 +244,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 +295,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); } } } @@ -20749,139 +20740,125 @@ 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(); + // Boundary checks + 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 num_filaments = static_cast(filament_type_option->values.size()); - std::set used_slots; - - PartPlate* 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; + 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; 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 < 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; + } } } } - const bool compatible = !(has_high && has_low); + const bool compatible = !(has_high && has_low); return compatible; } From fc4c307129b72172173b6fb99e63ef0b59504a80 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 16 Jul 2026 12:28:41 +0800 Subject: [PATCH 4/7] refactor: rename STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP to STRING_EXCEPT_FILAMENTS_MIXING_TEMP Better reflects that the check is about high/low temperature material mixing, not just "different" temperatures. --- src/Snapmaker_Orca.cpp | 2 +- src/libslic3r/PrintBase.hpp | 2 +- src/slic3r/GUI/Plater.cpp | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) 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/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index b680ac274e4..ff519465c97 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -19,7 +19,7 @@ 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, diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 24833287fc8..e1cb99febba 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -20910,12 +20910,13 @@ 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; @@ -20939,7 +20940,7 @@ bool Plater::sync_filament_temp_mixing_notification() break; case FilamentTempMixingState::BlockedError: { StringObjectException err; - err.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; + err.type = STRING_EXCEPT_FILAMENTS_MIXING_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); From 4d7ffd500d43596c68babd07f7d07981b49d18a2 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 23 Jul 2026 16:08:25 +0800 Subject: [PATCH 5/7] feat: show per-filament high/low temp breakdown in mixing notifications Add per-filament high/low temperature breakdown to the filament temp mixing warning/error notifications so users can see exactly which slots are in conflict, both for single-plate and slice-all modes. - Plater::check_filament_temp_mixing now exposes a FilamentTempMixingDetail (high/low 1-based slot lists) via a new overload - Notification text becomes dynamic; sync_filament_temp_mixing_notification caches the exact pushed strings to work around NotificationManager's exact-text close matching - confirm_filament_temp_mixing_before_slice / _before_slice_all use the same breakdown in their modal dialogs - Safe translation helper tr_u8() avoids the dangling-pointer trap in I18N::translate_utf8() that crashed in non-English locales - Add zh_CN translations for the new strings Also fix a separate crash: GCodeReader::parse_file_raw_internal no longer dereferences a NULL FILE* when fopen fails (e.g. loading a non-existent gcode file at startup), and Plater::load_gcode now checks wxFileExists up front to give a precise "does not exist" error instead of the vague "does not contain valid G-code". --- .../i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 23 +- src/libslic3r/GCodeReader.cpp | 8 + src/slic3r/GUI/Plater.cpp | 260 +++++++++++++++--- src/slic3r/GUI/Plater.hpp | 19 ++ 4 files changed, 267 insertions(+), 43 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index abef0c29816..5af1b62f4d2 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15348,4 +15348,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/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/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index e1cb99febba..6ec4c5c3ed0 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -207,20 +207,119 @@ 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; +} + +// 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) @@ -8842,6 +8941,11 @@ 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; MenuFactory menus; @@ -17606,6 +17710,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 @@ -20740,6 +20858,15 @@ void Plater::config_change_notification(const DynamicPrintConfig &config, const bool Plater::check_filament_temp_mixing(int plate_index) { + FilamentTempMixingDetail unused; + return check_filament_temp_mixing(plate_index, unused); +} + +bool Plater::check_filament_temp_mixing(int plate_index, FilamentTempMixingDetail& detail) +{ + detail.high_temp_slots_1based.clear(); + detail.low_temp_slots_1based.clear(); + // Boundary checks PartPlate* plate = nullptr; const DynamicPrintConfig& full_cfg = wxGetApp().preset_bundle->full_config(); @@ -20841,24 +20968,35 @@ bool Plater::check_filament_temp_mixing(int plate_index) // 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). + // + // `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; { PresetBundle* bundle = wxGetApp().preset_bundle; for (int slot : used_slots_0_based) { - 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; - } + 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); + 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; } @@ -20920,30 +21058,49 @@ bool Plater::sync_filament_temp_mixing_notification() 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_MIXING_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.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. @@ -20978,7 +21135,8 @@ bool Plater::guard_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; @@ -20989,11 +21147,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")); @@ -21005,25 +21172,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")); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 05683b8d5a5..650fcc845f9 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -240,6 +240,21 @@ 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; + }; + Plater(wxWindow *parent, MainFrame *main_frame); Plater(Plater &&) = delete; Plater(const Plater &) = delete; @@ -522,6 +537,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(); From 66f40ee7c047f485bf88c3c9f0a310de53ce4322 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 23 Jul 2026 19:41:07 +0800 Subject: [PATCH 6/7] feat: block slicing when filament flow ratio is 0 When a used filament's flow_ratio is 0, the slicer previously failed downstream with a misleading "bed beyond limits" error. Now it is caught up-front: the slice button greys out and a red banner names the offending filament(s) in "[n] PresetName" form, semicolon- separated for multiple offenders. Mirrors the existing filament_temp_mixing validator structure: - New STRING_EXCEPT_FLOW_RATIO_ZERO enum value. - New FlowRatioZeroDetail + 4 methods on Plater. - check_flow_ratio_zero() reuses the shared slot-collection helpers and reads filament_flow_ratio from each used preset. - sync_flow_ratio_zero_notification() caches the last pushed text for exact-text close/repush, and runs AFTER the ValidateError blanket-close in update_background_process so it isn't killed. - 11 sync trigger points + MainFrame gate + Slice All skip rule. - zh_CN translations for the 3 new banner strings. --- .../i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 9 + src/libslic3r/PrintBase.hpp | 1 + src/slic3r/GUI/MainFrame.cpp | 4 + src/slic3r/GUI/Plater.cpp | 218 +++++++++++++++++- src/slic3r/GUI/Plater.hpp | 20 ++ 5 files changed, 250 insertions(+), 2 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 5af1b62f4d2..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 "此材料组合可能存在风险,是否继续?" diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index ff519465c97..c01c4d29a49 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -23,6 +23,7 @@ enum StringExceptionType { 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/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 6ec4c5c3ed0..e0a8d6e0312 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -292,6 +292,27 @@ static std::string filament_temp_mixing_error_text(const Plater::FilamentTempMix 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) @@ -8946,6 +8967,8 @@ struct Plater::priv // 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; @@ -9600,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); @@ -9862,8 +9886,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) if (q->is_view3D_shown()) { 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_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; } @@ -12404,6 +12430,9 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool else { return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; } + // flow_ratio_zero sync MUST run after the blanket close_notification_of_type + // (ValidateError) above — otherwise our banner gets closed immediately. + q->sync_flow_ratio_zero_notification(); if (filament_ok && invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; @@ -12421,6 +12450,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(); @@ -12443,6 +12473,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()); @@ -17883,6 +17914,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; } @@ -20076,6 +20108,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); @@ -21039,7 +21088,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; } @@ -21122,14 +21173,175 @@ 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(); } @@ -22060,6 +22272,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; @@ -22132,6 +22345,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 650fcc845f9..452fae82a5b 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -255,6 +255,12 @@ class Plater: public wxPanel 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; @@ -562,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. From adcefdad266e490aa8031db0f0ee6cd085405537 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 23 Jul 2026 20:55:24 +0800 Subject: [PATCH 7/7] fix: enforce flow_ratio_zero block across reslice path, 3MF dedup, and preview indicator - Feed sync_flow_ratio_zero_notification() return value into UPDATE_BACKGROUND_PROCESS_INVALID so the reslice path (Preview Tab switch -> do_reslice -> update_background_process) is also blocked, not just the explicit slice button path. - Filter filament_flow_ratio out of the 3MF validity loop so the legacy BBL3MFInfo popup does not duplicate the dedicated flow_ratio_zero ValidateError banner. - Add is_plate_blocked_by_flow_ratio_zero to the GLCanvas3D per-plate toolbar gate so the Preview tab shows the red SLICE_FAILED indicator on blocked plates, matching the filament_temp_mixing behavior. --- src/slic3r/GUI/GLCanvas3D.cpp | 3 ++- src/slic3r/GUI/Plater.cpp | 28 ++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index bc336c29c4a..2c3515693d2 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -7923,7 +7923,8 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() } else { if (!plate_list.get_plate(i)->can_slice() || - wxGetApp().plater()->is_plate_blocked_by_filament_temp_mixing(i)) + 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/Plater.cpp b/src/slic3r/GUI/Plater.cpp index e0a8d6e0312..8178d8ea953 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10853,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()); @@ -12432,8 +12443,13 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool } // flow_ratio_zero sync MUST run after the blanket close_notification_of_type // (ValidateError) above — otherwise our banner gets closed immediately. - q->sync_flow_ratio_zero_notification(); - if (filament_ok && invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) + // 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) {