From dd7a9815b80b5ec546eeb35b2ef86f763878ee4e Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 15 Jul 2026 19:08:03 +0800 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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/5] 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();