From a587859e84cec021489ec42a91f8fcd26e1fa0d3 Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Wed, 17 Jun 2026 23:45:26 -0500 Subject: [PATCH] Fix: show all print validation warnings instead of only the last (#14112) --- src/OrcaSlicer.cpp | 19 ++- src/libslic3r/Print.cpp | 127 +++++++-------- src/libslic3r/Print.hpp | 2 +- src/libslic3r/PrintBase.hpp | 4 +- src/libslic3r/SLAPrint.cpp | 2 +- src/libslic3r/SLAPrint.hpp | 2 +- src/slic3r/GUI/BackgroundSlicingProcess.cpp | 4 +- src/slic3r/GUI/BackgroundSlicingProcess.hpp | 2 +- src/slic3r/GUI/NotificationManager.hpp | 3 +- src/slic3r/GUI/Plater.cpp | 34 ++-- tests/fff_print/test_print.cpp | 166 ++++++++++++++++++++ 11 files changed, 270 insertions(+), 95 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 72b6775e55..4bed36082f 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -6058,9 +6058,9 @@ int CLI::run(int argc, char **argv) } (dynamic_cast(print))->is_BBL_printer() = is_bbl_vendor_preset; - StringObjectException warning; + std::vector warnings; print_fff->set_check_multi_filaments_compatibility(!allow_mix_temp); - auto err = print->validate(&warning); + auto err = print->validate(&warnings); if (!err.string.empty()) { if ((STRING_EXCEPT_LAYER_HEIGHT_EXCEEDS_LIMIT == err.type) && no_check) { BOOST_LOG_TRIVIAL(warning) << "got warnings: "<< err.string << std::endl; @@ -6094,8 +6094,9 @@ int CLI::run(int argc, char **argv) flush_and_exit(validate_error); } } - else if (!warning.string.empty()) { - BOOST_LOG_TRIVIAL(warning) << "got warnings: "<< warning.string << std::endl; + else if (!warnings.empty()) { + for (const auto& w : warnings) + BOOST_LOG_TRIVIAL(warning) << "got warnings: "<< w.string << std::endl; } if (print->empty()) { @@ -6115,8 +6116,14 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << "set print's callback to cli_status_callback."; print->set_status_callback(cli_status_callback); g_cli_callback_mgr.set_plate_info(index+1, (plate_to_slice== 0)?partplate_list.get_plate_count():1); - if (!warning.string.empty()) { - PrintBase::SlicingStatus slicing_status{4, warning.string, 0, 0}; + if (!warnings.empty()) { + std::string warning_text; + for (const auto& w : warnings) { + if (!warning_text.empty()) + warning_text += "\n"; + warning_text += w.string; + } + PrintBase::SlicingStatus slicing_status{4, warning_text, 0, 0}; cli_status_callback(slicing_status); } else { diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index a118a9330e..c813a18726 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1256,9 +1256,22 @@ StringObjectException Print::check_multi_filament_valid(const Print& print) } // Precondition: Print::validate() requires the Print::apply() to be called its invocation. -//BBS: refine seq-print validation logic.....FIXME:StringObjectException *warning can only contain one warning, but there might be many warnings, need a vector -StringObjectException Print::validate(StringObjectException *warning, Polygons* collison_polygons, std::vector>* height_polygons) const +//BBS: refine seq-print validation logic +StringObjectException Print::validate(std::vector *warnings, Polygons* collison_polygons, std::vector>* height_polygons) const { + auto add_warning = [warnings](StringObjectException w) { + w.is_warning = true; + if (warnings != nullptr) + warnings->push_back(std::move(w)); + }; + auto warn = [&](std::string msg, std::string opt_key = "", const ObjectBase* object = nullptr) { + StringObjectException w; + w.string = std::move(msg); + w.opt_key = std::move(opt_key); + w.object = object; + add_warning(std::move(w)); + }; + std::vector extruders = this->extruders(); unsigned int nozzles = m_config.nozzle_diameter.size(); @@ -1273,9 +1286,8 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (!ret.string.empty()) { ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP; - if (ret.is_warning && warning != nullptr) { - *warning = ret; - //return {}; + if (ret.is_warning) { + add_warning(ret); }else return ret; } @@ -1301,32 +1313,26 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* } else { //BBS - auto ret = layered_print_cleareance_valid(*this, warning); + StringObjectException layer_warning; + auto ret = layered_print_cleareance_valid(*this, &layer_warning); if (!ret.string.empty()) { ret.type = STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT; return ret; } + if (!layer_warning.string.empty()) + add_warning(layer_warning); } if (m_config.enable_prime_tower) { for (const PrintObject* object : m_objects) { - if (object->config().precise_z_height.value && warning != nullptr) { - StringObjectException warningtemp; - warningtemp.string = L("Enabling both precise Z height and the prime tower may cause slicing errors."); - warningtemp.opt_key = "precise_z_height"; - warningtemp.is_warning = true; - *warning = warningtemp; + if (object->config().precise_z_height.value) { + warn(L("Enabling both precise Z height and the prime tower may cause slicing errors."), "precise_z_height"); break; } } } else { - if (m_config.enable_wrapping_detection && warning!=nullptr) { - StringObjectException warningtemp; - warningtemp.string = L("A prime tower is required for clumping detection; otherwise, there may be flaws on the model."); - warningtemp.opt_key = "enable_prime_tower"; - warningtemp.is_warning = true; - *warning = warningtemp; - } + if (m_config.enable_wrapping_detection) + warn(L("A prime tower is required for clumping detection; otherwise, there may be flaws on the model."), "enable_prime_tower"); } if (m_config.spiral_mode) { @@ -1422,8 +1428,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (nozzle_diam - EPSILON > first_nozzle_diam || nozzle_diam + EPSILON < first_nozzle_diam || std::abs((filament_diam - first_filament_diam) / first_filament_diam) > 0.1) { // return { L("Different nozzle diameters and different filament diameters may not work well when prime tower is enabled. It's very experimental, please proceed with caucious.") }; - warning->string = L("Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution."); - warning->opt_key = "nozzle_diameter"; + warn(L("Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution."), "nozzle_diameter"); break; } } @@ -1580,22 +1585,15 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* // Orca: use organic as default object->config().support_style == smsDefault) { - if (warning) { - // Orca: check the support wall count and the base pattern - if (object->config().tree_support_wall_count > 1 && - object->config().support_base_pattern != SupportMaterialPattern::smpNone && - object->config().support_base_pattern != SupportMaterialPattern::smpDefault) { - warning->string = L("For Organic supports, two walls are supported only with the Hollow/Default base pattern."); - warning->opt_key = "support_base_pattern"; - } + // Orca: check the support wall count and the base pattern + if (object->config().tree_support_wall_count > 1 && + object->config().support_base_pattern != SupportMaterialPattern::smpNone && + object->config().support_base_pattern != SupportMaterialPattern::smpDefault) + warn(L("For Organic supports, two walls are supported only with the Hollow/Default base pattern."), "support_base_pattern"); - // Orca: check if the Lightning base pattern selected - if (object->config().support_base_pattern == SupportMaterialPattern::smpLightning) { - warning->string = L( - "The Lightning base pattern is not supported by this support type; Rectilinear will be used instead."); - warning->opt_key = "support_base_pattern"; - } - } + // Orca: check if the Lightning base pattern selected + if (object->config().support_base_pattern == SupportMaterialPattern::smpLightning) + warn(L("The Lightning base pattern is not supported by this support type; Rectilinear will be used instead."), "support_base_pattern"); float extrusion_width = std::min( support_material_flow(object).width(), @@ -1607,28 +1605,23 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (object->config().tree_support_branch_diameter_organic < object->config().tree_support_tip_diameter) return { L("Organic support branch diameter must not be smaller than support tree tip diameter."), object, "tree_support_branch_diameter_organic" }; } - } else if (object->config().support_base_pattern == SupportMaterialPattern::smpLightning && warning) { + } else if (object->config().support_base_pattern == SupportMaterialPattern::smpLightning) { // Orca: check if the Lightning base pattern selected - warning->string = L("The Lightning base pattern is not supported by this support type; Rectilinear will be used instead."); - warning->opt_key = "support_base_pattern"; - } else if (object->config().support_base_pattern == SupportMaterialPattern::smpNone && warning) { + warn(L("The Lightning base pattern is not supported by this support type; Rectilinear will be used instead."), "support_base_pattern"); + } else if (object->config().support_base_pattern == SupportMaterialPattern::smpNone) { // Orca: check if the Hollow base pattern selected - warning->string = L("The Hollow base pattern is not supported by this support type; Rectilinear will be used instead."); - warning->opt_key = "support_base_pattern"; + warn(L("The Hollow base pattern is not supported by this support type; Rectilinear will be used instead."), "support_base_pattern"); } } // Do we have custom support data that would not be used? // Notify the user in that case. - if (! object->has_support() && warning) { + if (! object->has_support()) { for (const ModelVolume* mv : object->model_object()->volumes) { bool has_enforcers = mv->is_support_enforcer() || (mv->is_model_part() && mv->supported_facets.has_facets(*mv, EnforcerBlockerType::ENFORCER)); if (has_enforcers) { - StringObjectException warningtemp; - warningtemp.string = L("Support enforcers are used but support is not enabled. Please enable support."); - warningtemp.object = object; - *warning = warningtemp; + warn(L("Support enforcers are used but support is not enabled. Please enable support."), "", object); break; } } @@ -1780,7 +1773,11 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* } // check if print speed/accel/jerk is higher than the maximum speed of the printer - if (warning) { + if (warnings) { + // The motion-ability checks are mutually exclusive (gated on warning_key), so collect the + // single one that fires into a local and push it once - separate from the precise-wall and + // shrinkage warnings below. + StringObjectException motion_warning; try { auto check_motion_ability_object_setting = [&](const std::vector& keys_to_check, double limit) -> std::string { std::string warning_key; @@ -1811,7 +1808,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* if (!ignore_jerk_validation) { if (m_default_object_config.default_jerk == 1 || m_default_object_config.outer_wall_jerk == 1 || m_default_object_config.inner_wall_jerk == 1) { - warning->string = L("Setting the jerk speed too low could lead to artifacts on curved surfaces"); + motion_warning.string = L("Setting the jerk speed too low could lead to artifacts on curved surfaces"); if (m_default_object_config.outer_wall_jerk == 1) warning_key = "outer_wall_jerk"; else if (m_default_object_config.inner_wall_jerk == 1) @@ -1819,7 +1816,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* else warning_key = "default_jerk"; - warning->opt_key = warning_key; + motion_warning.opt_key = warning_key; } if (warning_key.empty() && m_default_object_config.default_jerk > 0) { @@ -1829,20 +1826,20 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* warning_key.clear(); warning_key = check_motion_ability_object_setting(jerk_to_check, max_jerk); if (!warning_key.empty()) { - warning->string = L( + motion_warning.string = L( "The jerk setting exceeds the printer's maximum jerk (machine_max_jerk_x/machine_max_jerk_y).\n" "Orca will automatically cap the jerk speed to ensure it doesn't surpass the printer's capabilities.\n" "You can adjust the maximum jerk setting in your printer's configuration to get higher speeds."); - warning->opt_key = warning_key; + motion_warning.opt_key = warning_key; } } } // check junction deviation else if (m_default_object_config.default_junction_deviation.value > max_junction_deviation) { - warning->string = L( "Junction deviation setting exceeds the printer's maximum value (machine_max_junction_deviation).\n" + motion_warning.string = L( "Junction deviation setting exceeds the printer's maximum value (machine_max_junction_deviation).\n" "Orca will automatically cap the junction deviation to ensure it doesn't surpass the printer's capabilities.\n" "You can adjust the machine_max_junction_deviation value in your printer's configuration to get higher limits."); - warning->opt_key = "default_junction_deviation"; + motion_warning.opt_key = "default_junction_deviation"; } // check acceleration @@ -1877,12 +1874,12 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* }; warning_key = check_motion_ability_object_setting(accel_to_check, max_accel); if (!warning_key.empty()) { - warning->string = L("The acceleration setting exceeds the printer's maximum acceleration " + motion_warning.string = L("The acceleration setting exceeds the printer's maximum acceleration " "(machine_max_acceleration_extruding).\nOrca will " "automatically cap the acceleration speed to ensure it doesn't surpass the printer's " "capabilities.\nYou can adjust the " "machine_max_acceleration_extruding value in your printer's configuration to get higher speeds."); - warning->opt_key = warning_key; + motion_warning.opt_key = warning_key; } if (support_travel_acc) { const auto max_travel = m_config.machine_max_acceleration_travel.values[0]; @@ -1892,13 +1889,13 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* }; warning_key = check_motion_ability_object_setting(accel_to_check, max_travel); if (!warning_key.empty()) { - warning->string = L( + motion_warning.string = L( "The travel acceleration setting exceeds the printer's maximum travel acceleration " "(machine_max_acceleration_travel).\nOrca will " "automatically cap the travel acceleration speed to ensure it doesn't surpass the printer's " "capabilities.\nYou can adjust the " "machine_max_acceleration_travel value in your printer's configuration to get higher speeds."); - warning->opt_key = warning_key; + motion_warning.opt_key = warning_key; } } } @@ -1924,19 +1921,17 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* // } // check wall sequence and precise outer wall - if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter) { - warning->string = L("The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences."); - warning->opt_key = "precise_outer_wall"; - } + if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter) + warn(L("The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences."), "precise_outer_wall"); } catch (std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "Orca: validate motion ability failed: " << e.what() << std::endl; } + if (!motion_warning.string.empty()) + add_warning(motion_warning); } - if (!this->has_same_shrinkage_compensations()){ - warning->string = L("Filament shrinkage will not be used because filament shrinkage for the used filaments does not match."); - warning->opt_key = ""; - } + if (!this->has_same_shrinkage_compensations()) + warn(L("Filament shrinkage will not be used because filament shrinkage for the used filaments does not match.")); return {}; } diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 9206f2a390..75e749e7e1 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -932,7 +932,7 @@ public: } // Returns an empty string if valid, otherwise returns an error message. - StringObjectException validate(StringObjectException *warning = nullptr, Polygons* collison_polygons = nullptr, std::vector>* height_polygons = nullptr) const override; + StringObjectException validate(std::vector *warnings = nullptr, Polygons* collison_polygons = nullptr, std::vector>* height_polygons = nullptr) const override; double skirt_first_layer_height() const; Flow brim_flow() const; Flow skirt_flow() const; diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index 4158c23065..d4f99c764e 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -32,7 +32,7 @@ struct StringObjectException std::string string; ObjectBase const *object = nullptr; std::string opt_key; - StringExceptionType type; // warning type for tips + StringExceptionType type = STRING_EXCEPT_NOT_DEFINED; // warning type for tips bool is_warning = false; std::vector params; // warning params for tips }; @@ -396,7 +396,7 @@ public: // Validate the print, return empty string if valid, return error if process() cannot (or should not) be started. //BBS: add more paremeters to validate - virtual StringObjectException validate(StringObjectException *warning = nullptr, Polygons* collison_polygons = nullptr, std::vector>* height_polygons = nullptr) const { return {}; } + virtual StringObjectException validate(std::vector *warnings = nullptr, Polygons* collison_polygons = nullptr, std::vector>* height_polygons = nullptr) const { return {}; } enum ApplyStatus { // No change after the Print::apply() call. diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index ca07f8754e..cdefd3e10e 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -613,7 +613,7 @@ std::string SLAPrint::output_filename(const std::string &filename_base) const return this->PrintBase::output_filename(m_print_config.filename_format.value, ".sl1", filename_base, &config); } -StringObjectException SLAPrint::validate(StringObjectException *exception, Polygons *collison_polygons, std::vector> *height_polygons) const +StringObjectException SLAPrint::validate(std::vector *warnings, Polygons *collison_polygons, std::vector> *height_polygons) const { for(SLAPrintObject * po : m_objects) { diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index a8c49674fc..0eaa44a754 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -488,7 +488,7 @@ public: const SLAPrintStatistics& print_statistics() const { return m_print_statistics; } - StringObjectException validate(StringObjectException * warning = nullptr, + StringObjectException validate(std::vector * warnings = nullptr, Polygons * collison_polygons = nullptr, std::vector> *height_polygons = nullptr) const override; diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index c1624f4a4e..771eb025c7 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -675,13 +675,13 @@ bool BackgroundSlicingProcess::empty() const return m_print->empty(); } -StringObjectException BackgroundSlicingProcess::validate(StringObjectException *warning, Polygons* collison_polygons, std::vector>* height_polygons) +StringObjectException BackgroundSlicingProcess::validate(std::vector *warnings, Polygons* collison_polygons, std::vector>* height_polygons) { assert(m_print != nullptr); assert(m_print == m_fff_print); m_fff_print->is_BBL_printer() = wxGetApp().preset_bundle->is_bbl_vendor(); - return m_print->validate(warning, collison_polygons, height_polygons); + return m_print->validate(warnings, collison_polygons, height_polygons); } // Apply config over the print. Returns false, if the new config values caused any of the already diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp index 78e8971869..07d6b42df6 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp @@ -145,7 +145,7 @@ public: bool empty() const; // Validate the print. Returns an empty string if valid, returns an error message if invalid. // Call validate before calling start(). - StringObjectException validate(StringObjectException *warning = nullptr, Polygons* collison_polygons = nullptr, std::vector>* height_polygons = nullptr); + StringObjectException validate(std::vector *warnings = nullptr, Polygons* collison_polygons = nullptr, std::vector>* height_polygons = nullptr); // Set the export path of the G-code. // Once the path is set, the G-code diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index dd17f19e42..62df1bf627 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -1005,7 +1005,8 @@ private: NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, - NotificationType::SimplifySuggestion + NotificationType::SimplifySuggestion, + NotificationType::ValidateWarning }; //prepared (basic) notifications // non-static so its not loaded too early. If static, the translations wont load correctly. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3262d3fe02..5021da2054 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4625,6 +4625,7 @@ struct Plater::priv } void process_validation_warning(StringObjectException const &warning) const; + void process_validation_warnings(const std::vector &warnings) const; bool background_processing_enabled() const { #ifdef SUPPORT_BACKGROUND_PROCESSING @@ -7949,6 +7950,15 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni } } +void Plater::priv::process_validation_warnings(const std::vector &warnings) const +{ + // ValidateWarning stacks by text (m_multiple_types), so clear the stale set before re-adding. + notification_manager->close_notification_of_type(NotificationType::ValidateWarning); + for (const StringObjectException &warning : warnings) + if (!warning.string.empty()) + process_validation_warning(warning); +} + // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. @@ -8029,14 +8039,14 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors. //BBS: add is_warning logic - StringObjectException warning; + std::vector warnings; //BBS: refine seq-print logic Polygons polygons; std::vector> height_polygons; - StringObjectException err = background_process.validate(&warning, &polygons, &height_polygons); + StringObjectException err = background_process.validate(&warnings, &polygons, &height_polygons); // update string by type q->post_process_string_object_exception(err); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%")%err.string%warnings.size(); if (err.string.empty()) { this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); @@ -8047,9 +8057,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; - // Pass a warning from validation and either show a notification, - // or hide the old one. - process_validation_warning(warning); + process_validation_warnings(warnings); if (printer_technology == ptFFF) { view3D->get_canvas3d()->reset_sequential_print_clearance(); view3D->get_canvas3d()->set_as_dirty(); @@ -8062,7 +8070,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // Show error as notification. notification_manager->push_validate_error_notification(err); //also update the warnings - process_validation_warning(warning); + process_validation_warnings(warnings); return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; if (printer_technology == ptFFF) { const Print* print = background_process.fff_print(); @@ -17693,14 +17701,14 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) if (p->printer_technology == ptFFF) { //std::string plater_text = _u8L("An object is laid over the boundary of plate or exceeds the height limit.\n" // "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume.");; - StringObjectException warning; + std::vector warnings; Polygons polygons; std::vector> height_polygons; p->background_process.fff_print()->set_check_multi_filaments_compatibility(wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); - StringObjectException err = p->background_process.validate(&warning, &polygons, &height_polygons); + StringObjectException err = p->background_process.validate(&warnings, &polygons, &height_polygons); // update string by type post_process_string_object_exception(err); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%, model_fits %3%")%err.string%warning.string %model_fits; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%, model_fits %3%")%err.string%warnings.size() %model_fits; if (err.string.empty()) { p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); @@ -17708,9 +17716,7 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) p->notification_manager->close_notification_of_type(NotificationType::ValidateError); p->notification_manager->bbl_close_3mf_warn_notification(); - // Pass a warning from validation and either show a notification, - // or hide the old one. - p->process_validation_warning(warning); + p->process_validation_warnings(warnings); p->view3D->get_canvas3d()->reset_sequential_print_clearance(); p->view3D->get_canvas3d()->set_as_dirty(); p->view3D->get_canvas3d()->request_extra_frame(); @@ -17720,7 +17726,7 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); // Show error as notification. p->notification_manager->push_validate_error_notification(err); - p->process_validation_warning(warning); + p->process_validation_warnings(warnings); //model_fits = false; validate_error = true; p->view3D->get_canvas3d()->set_sequential_print_clearance_visible(true); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 8c59b30292..8b3a4b409f 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -3,9 +3,12 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/Print.hpp" #include "libslic3r/Layer.hpp" +#include "libslic3r/Model.hpp" #include "test_data.hpp" +#include + using namespace Slic3r; using namespace Slic3r::Test; @@ -103,3 +106,166 @@ SCENARIO("Print: Brim generation", "[Print]") { } } } + +// --------------------------------------------------------------------------- +// Print::validate() warning collection +// +// validate() returns its warnings in a vector. The warning paths deliberately +// differ in how many entries they produce; these tests pin down each behaviour: +// * independent checks -> stack (one entry each) +// * motion-ability -> coalesce into one (mutually exclusive, gated) +// * clumping detection -> one independent warning +// * layered clearance -> many collisions concatenated into one entry +// * null warnings pointer -> no-op, no crash, no blocking error +// --------------------------------------------------------------------------- +namespace { + +// Build `n` 20mm cubes (spread apart, or stacked at the origin when `overlap`) into +// `model`/`print` and apply `config`, leaving the print ready to validate(). No slicing needed. +void build_cubes(Slic3r::Model& model, Slic3r::Print& print, + DynamicPrintConfig config, int n, bool overlap) +{ + config.set_key_value("layer_change_gcode", new ConfigOptionString("G92 E0\n")); // validate() relative-E reset + + for (int i = 0; i < n; ++i) { + ModelObject* object = model.add_object(); + object->add_volume(Slic3r::Test::mesh(TestMesh::cube_20x20x20)); + ModelInstance* inst = object->add_instance(); + inst->set_offset(Vec3d(overlap ? 0.0 : i * 60.0, 0.0, 0.0)); + } + for (ModelObject* mo : model.objects) { + mo->ensure_on_bed(); + print.auto_assign_extruders(mo); + } + print.apply(model, config); +} + +// Build cubes and run validate(), collecting warnings; returns the blocking error. +StringObjectException validate_cubes(const DynamicPrintConfig& config, + std::vector& warnings, + int n = 1, bool overlap = false) +{ + Slic3r::Model model; + Slic3r::Print print; + build_cubes(model, print, config, n, overlap); + return print.validate(&warnings); +} + +size_t count_opt_key(const std::vector& warnings, const std::string& key) +{ + return std::count_if(warnings.begin(), warnings.end(), + [&](const StringObjectException& w) { return w.opt_key == key; }); +} + +// Make `default_acceleration` exceed the machine's extruding-acceleration limit. +void trigger_acceleration_warning(DynamicPrintConfig& c) +{ + c.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats{ 100. }); + c.set_key_value("default_acceleration", new ConfigOptionFloat(100000.)); +} + +// Make `default_jerk` exceed the machine's jerk limit (junction deviation off so +// the jerk check is not skipped). +void trigger_jerk_warning(DynamicPrintConfig& c) +{ + c.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats{ 0. }); + c.set_key_value("machine_max_jerk_x", new ConfigOptionFloats{ 1. }); + c.set_key_value("machine_max_jerk_y", new ConfigOptionFloats{ 1. }); + c.set_key_value("default_jerk", new ConfigOptionFloat(9999.)); +} + +// Precise outer wall is ignored unless the wall sequence is inner-outer. +void trigger_precise_wall_warning(DynamicPrintConfig& c) +{ + c.set_key_value("precise_outer_wall", new ConfigOptionBool(true)); + c.set_key_value("wall_sequence", new ConfigOptionEnum(WallSequence::OuterInner)); +} + +} // namespace + +TEST_CASE("Print::validate stacks independent warnings", "[Print][validate]") +{ + // Two unrelated checks (region precise-wall + machine acceleration) must each + // contribute their own entry. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + trigger_precise_wall_warning(config); + trigger_acceleration_warning(config); + + std::vector warnings; + StringObjectException err = validate_cubes(config, warnings); + + CHECK(err.string.empty()); + CHECK(warnings.size() >= 2); + CHECK(count_opt_key(warnings, "precise_outer_wall") == 1); // jump-to key is preserved + for (const auto& w : warnings) + CHECK(w.is_warning); // every collected entry is a warning +} + +TEST_CASE("Print::validate coalesces motion-ability warnings into one", "[Print][validate]") +{ + // The jerk/junction/acceleration checks are mutually exclusive (gated on a shared + // key), so adding a second motion trigger must NOT add a second warning. + DynamicPrintConfig accel_only = DynamicPrintConfig::full_print_config(); + trigger_acceleration_warning(accel_only); + std::vector w_accel; + CHECK(validate_cubes(accel_only, w_accel).string.empty()); + + DynamicPrintConfig accel_and_jerk = DynamicPrintConfig::full_print_config(); + trigger_acceleration_warning(accel_and_jerk); + trigger_jerk_warning(accel_and_jerk); + std::vector w_both; + CHECK(validate_cubes(accel_and_jerk, w_both).string.empty()); + + CHECK(w_accel.size() >= 1); + CHECK(w_both.size() == w_accel.size()); // the extra motion trigger collapses into the same warning +} + +TEST_CASE("Print::validate reports the clumping-detection warning", "[Print][validate]") +{ + // A distinct single-shot path: clumping/wrapping detection without a prime tower warns + // (and carries the enable_prime_tower jump-to key). enable_prime_tower must be off, as + // the warning lives in the no-prime-tower branch. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_key_value("enable_prime_tower", new ConfigOptionBool(false)); + config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true)); + + std::vector warnings; + StringObjectException err = validate_cubes(config, warnings); + + CHECK(err.string.empty()); + CHECK(count_opt_key(warnings, "enable_prime_tower") == 1); +} + +TEST_CASE("Print::validate concatenates layered-clearance collisions into one warning", "[Print][validate]") +{ + // In by-layer mode, layered_print_cleareance_valid folds every too-close pair into a + // single warning entry (newline-joined), unlike the per-check stacking above. Isolate + // that entry by type so unrelated default-config warnings don't affect the assertion. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + + std::vector warnings; + StringObjectException err = validate_cubes(config, warnings, /*n=*/3, /*overlap=*/true); + + CHECK(err.string.empty()); + auto is_layered = [](const StringObjectException& w) { + return w.type == STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT; }; + REQUIRE(std::count_if(warnings.begin(), warnings.end(), is_layered) == 1); // 3 objects, 2 collisions, 1 entry + auto it = std::find_if(warnings.begin(), warnings.end(), is_layered); + CHECK(it->string.find('\n') != std::string::npos); // the collisions were concatenated +} + +TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate]") +{ + // Callers may pass no warnings sink: a warning-producing config must not crash + // and must still return without a blocking error. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + trigger_precise_wall_warning(config); + trigger_acceleration_warning(config); + + Slic3r::Model model; + Slic3r::Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + + StringObjectException err = print.validate(); // warnings == nullptr + CHECK(err.string.empty()); +}