From 3a5a0575efffa8bcef16230a0707885cd7aa512f Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 23 Jun 2021 15:24:05 +0200 Subject: [PATCH 01/37] Fixed after removing TBB deprecated APIs --- src/libslic3r/Thread.cpp | 8 +++----- src/libslic3r/utils.cpp | 12 +++++++++++- src/slic3r/GUI/BackgroundSlicingProcess.hpp | 1 - 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/Thread.cpp b/src/libslic3r/Thread.cpp index 51dec618e..25c29d273 100644 --- a/src/libslic3r/Thread.cpp +++ b/src/libslic3r/Thread.cpp @@ -10,11 +10,11 @@ #include #include #include -#include #include #include #include "Thread.hpp" +#include "Utils.hpp" namespace Slic3r { @@ -199,16 +199,14 @@ void name_tbb_thread_pool_threads() // TBB will respect the task affinity mask on Linux and spawn less threads than std::thread::hardware_concurrency(). // const size_t nthreads_hw = std::thread::hardware_concurrency(); const size_t nthreads_hw = tbb::this_task_arena::max_concurrency(); - size_t nthreads = nthreads_hw; + size_t nthreads = nthreads_hw; #ifdef SLIC3R_PROFILE // Shiny profiler is not thread safe, thus disable parallelization. + disable_multi_threading(); nthreads = 1; #endif - if (nthreads != nthreads_hw) - tbb::global_control(tbb::global_control::max_allowed_parallelism, nthreads); - std::atomic nthreads_running(0); std::condition_variable cv; std::mutex cv_m; diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 763be8af7..62ce67ae4 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -43,7 +43,13 @@ #include #include -#include +// We are using quite an old TBB 2017 U7, which does not support global control API officially. +// Before we update our build servers, let's use the old API, which is deprecated in up to date TBB. +#ifdef TBB_HAS_GLOBAL_CONTROL + #include +#else + #include +#endif #if defined(__linux__) || defined(__GNUC__ ) #include @@ -118,7 +124,11 @@ void trace(unsigned int level, const char *message) void disable_multi_threading() { // Disable parallelization so the Shiny profiler works +#ifdef TBB_HAS_GLOBAL_CONTROL tbb::global_control(tbb::global_control::max_allowed_parallelism, 1); +#else // TBB_HAS_GLOBAL_CONTROL + static tbb::task_scheduler_init *tbb_init = new tbb::task_scheduler_init(1); +#endif // TBB_HAS_GLOBAL_CONTROL } static std::string g_var_dir; diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp index 6f5cd8852..cf9b07249 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp @@ -255,7 +255,6 @@ private: std::shared_ptr m_ui_task; PrintState m_step_state; - mutable tbb::mutex m_step_state_mutex; bool set_step_started(BackgroundSlicingProcessStep step); void set_step_done(BackgroundSlicingProcessStep step); bool is_step_done(BackgroundSlicingProcessStep step) const; From 204ae62449a992c09f41d409bb030d368cf17108 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 23 Jun 2021 16:37:18 +0200 Subject: [PATCH 02/37] Slight improvement in cost of background slicing cancellation. --- src/libslic3r/PrintBase.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index 8cb997c77..08711a7b2 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -448,9 +448,9 @@ public: // Canceled internally from Print::apply() through the Print/PrintObject::invalidate_step() or ::invalidate_all_steps(). CANCELED_INTERNAL = 2 }; - CancelStatus cancel_status() const { return m_cancel_status; } + CancelStatus cancel_status() const { return m_cancel_status.load(std::memory_order_acquire); } // Has the calculation been canceled? - bool canceled() const { return m_cancel_status != NOT_CANCELED; } + bool canceled() const { return m_cancel_status.load(std::memory_order_acquire) != NOT_CANCELED; } // Cancel the running computation. Stop execution of all the background threads. void cancel() { m_cancel_status = CANCELED_BY_USER; } void cancel_internal() { m_cancel_status = CANCELED_INTERNAL; } @@ -481,7 +481,7 @@ protected: // If the background processing stop was requested, throw CanceledException. // To be called by the worker thread and its sub-threads (mostly launched on the TBB thread pool) regularly. - void throw_if_canceled() const { if (m_cancel_status) throw CanceledException(); } + void throw_if_canceled() const { if (m_cancel_status.load(std::memory_order_acquire)) throw CanceledException(); } // Wrapper around this->throw_if_canceled(), so that throw_if_canceled() may be passed to a function without making throw_if_canceled() public. PrintTryCancel make_try_cancel() const { return PrintTryCancel(this); } From 33efb94ebd33e5fac97d37cf8e8c8827cced9023 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Wed, 23 Jun 2021 17:23:21 +0200 Subject: [PATCH 03/37] Updated SL1S bed model. --- resources/profiles/PrusaResearch/sl1s_bed.stl | Bin 180784 -> 292184 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/profiles/PrusaResearch/sl1s_bed.stl b/resources/profiles/PrusaResearch/sl1s_bed.stl index 0c7479962bee023305bbef367dd879dcf96b5cc0..a9bca83f60db12ec6c6c8205edf4d1da36c4f1af 100644 GIT binary patch literal 292184 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 From 83285e3c4e72179c239beddea1f3682b420049ad Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Wed, 23 Jun 2021 17:58:11 +0200 Subject: [PATCH 04/37] Updated SL1S tilt times. --- resources/profiles/PrusaResearch.idx | 1 + resources/profiles/PrusaResearch.ini | 27 +++------------------------ 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index bf9229753..fa8d86a0c 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,4 +1,5 @@ min_slic3r_version = 2.4.0-alpha0 +1.4.0-alpha3 Updated SL1S tilt times. 1.4.0-alpha2 Updated Prusa MINI machine limits. 1.4.0-alpha1 Added new SL1S resin profiles. 1.4.0-alpha0 Bumped up config version. diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 3dd910057..f95c2b61c 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -5,7 +5,7 @@ name = Prusa Research # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the PrusaSlicer configuration to be downgraded. -config_version = 1.4.0-alpha2 +config_version = 1.4.0-alpha3 # Where to get the updates from? config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaResearch/ changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1% @@ -5447,13 +5447,6 @@ initial_exposure_time = 25 material_type = Tough material_vendor = Peopoly -[sla_material:Peopoly White Tough @0.025 SL1S] -inherits = *0.025_sl1s* -exposure_time = 1.8 -initial_exposure_time = 25 -material_type = Tough -material_vendor = Peopoly - [sla_material:Peopoly Deft White @0.025 SL1S] inherits = *0.025_sl1s* exposure_time = 1.8 @@ -5575,13 +5568,6 @@ initial_exposure_time = 25 material_type = Tough material_vendor = Peopoly -[sla_material:Peopoly White Tough @0.05 SL1S] -inherits = *0.05_sl1s* -exposure_time = 2 -initial_exposure_time = 25 -material_type = Tough -material_vendor = Peopoly - [sla_material:Peopoly Deft White @0.05 SL1S] inherits = *0.05_sl1s* exposure_time = 2 @@ -5703,13 +5689,6 @@ initial_exposure_time = 25 material_type = Tough material_vendor = Peopoly -[sla_material:Peopoly White Tough @0.1 SL1S] -inherits = *0.1_sl1s* -exposure_time = 2.6 -initial_exposure_time = 25 -material_type = Tough -material_vendor = Peopoly - [sla_material:Peopoly Deft White @0.1 SL1S] inherits = *0.1_sl1s* exposure_time = 2.6 @@ -6458,8 +6437,8 @@ display_pixels_y = 1620 display_width = 128 elefant_foot_compensation = 0.2 elefant_foot_min_width = 0.2 -fast_tilt_time = 4 -slow_tilt_time = 5.7 +fast_tilt_time = 2.5 +slow_tilt_time = 5 gamma_correction = 1 max_print_height = 150 min_exposure_time = 1 From 1378d2084a91e5e8fbae4dadf01024ffca4a599a Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 24 Jun 2021 13:59:48 +0200 Subject: [PATCH 05/37] Fix of #6650 - show estimated print time on ruler is not working + Parent center of the MessageDialogs --- src/slic3r/GUI/DoubleSlider.cpp | 4 ++-- src/slic3r/GUI/MsgDialog.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/DoubleSlider.cpp b/src/slic3r/GUI/DoubleSlider.cpp index 75a0617c6..e39b40ce4 100644 --- a/src/slic3r/GUI/DoubleSlider.cpp +++ b/src/slic3r/GUI/DoubleSlider.cpp @@ -2010,11 +2010,11 @@ void Control::show_cog_icon_context_menu() []() { return true; }, [this]() { return m_extra_style == 0; }, GUI::wxGetApp().plater()); append_menu_check_item(ruler_mode_menu, wxID_ANY, _L("Show object height"), _L("Show object height on the ruler"), - [this](wxCommandEvent&) { m_extra_style & wxSL_AUTOTICKS ? m_extra_style &= wxSL_AUTOTICKS : m_extra_style |= wxSL_AUTOTICKS; }, ruler_mode_menu, + [this](wxCommandEvent&) { m_extra_style & wxSL_AUTOTICKS ? m_extra_style ^= wxSL_AUTOTICKS : m_extra_style |= wxSL_AUTOTICKS; }, ruler_mode_menu, []() { return true; }, [this]() { return m_extra_style & wxSL_AUTOTICKS; }, GUI::wxGetApp().plater()); append_menu_check_item(ruler_mode_menu, wxID_ANY, _L("Show estimated print time"), _L("Show estimated print time on the ruler"), - [this](wxCommandEvent&) { m_extra_style & wxSL_VALUE_LABEL ? m_extra_style &= wxSL_VALUE_LABEL : m_extra_style |= wxSL_VALUE_LABEL; }, ruler_mode_menu, + [this](wxCommandEvent&) { m_extra_style & wxSL_VALUE_LABEL ? m_extra_style ^= wxSL_VALUE_LABEL : m_extra_style |= wxSL_VALUE_LABEL; }, ruler_mode_menu, []() { return true; }, [this]() { return m_extra_style & wxSL_VALUE_LABEL; }, GUI::wxGetApp().plater()); append_submenu(&menu, ruler_mode_menu, wxID_ANY, _L("Ruler mode"), _L("Set ruler mode"), "", diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 0c52d8b58..49dec0c5d 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -32,6 +32,7 @@ MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &he boldfont.SetWeight(wxFONTWEIGHT_BOLD); this->SetFont(wxGetApp().normal_font()); + this->CenterOnParent(); auto *topsizer = new wxBoxSizer(wxHORIZONTAL); auto *rightsizer = new wxBoxSizer(wxVERTICAL); From 9b70efc46ad2b71c714ce1f131e2508aba322d1d Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 25 Jun 2021 16:44:06 +0200 Subject: [PATCH 06/37] Windows specific: Transactional saving of PrusaSlicer.ini to ensure that configuration could be recovered in the case PrusaSlicer.ini is corrupted during saving. The config is first written into a temp file marked with a MD5 checksum. Once the file is saved, it is copied to a backup file first, then moved to PrusaSlicer.ini. When loading PrusaSlicer.ini fails, the backup file will be loaded instead, however only if its MD5 checksum is valid. The following "Fixes" comments are for github triggers. We implemented a workaround, not a fix, we don't actually know how the data corruption happens and why. Most likely the "Move file" Windows API is not atomic and if PrusaSlicer crashes on another thread while moving the file, PrusaSlicer.ini will only be partially saved, with the rest of the file filled with nulls. We did not "fix" the issue, we just hope that our workaround will help in majority of cases. Fixes prusaslicer wont open 2.3 windows 10 #5812 Fixes Won't Open - Windows 10 #4915 Fixes PrusaSlicer Crashes upon opening with "'=' character not found in line error" #2438 Fixes Fails to open on blank slic3r.ini %user%\AppData\Roaming\Slic3rPE --- src/libslic3r/AppConfig.cpp | 167 ++++++++++++++++++++++++++++++------ src/libslic3r/AppConfig.hpp | 2 +- 2 files changed, 143 insertions(+), 26 deletions(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 81e4db2ba..87eeca198 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -4,7 +4,7 @@ #include "Exception.hpp" #include "LocalesUtils.hpp" #include "Thread.hpp" - +#include "format.hpp" #include #include @@ -18,9 +18,13 @@ #include #include #include +#include -//#include -//#include "I18N.hpp" +#ifdef WIN32 +//FIXME replace the two following includes with after it becomes mainstream. +#include +#include +#endif namespace Slic3r { @@ -177,25 +181,114 @@ void AppConfig::set_defaults() erase("", "object_settings_size"); } +#ifdef WIN32 +static std::string appconfig_md5_hash_line(const std::string_view data) +{ + //FIXME replace the two following includes with after it becomes mainstream. + // return boost::md5(data).hex_str_value(); + // boost::uuids::detail::md5 is an internal namespace thus it may change in the future. + // Also this implementation is not the fastest, it was designed for short blocks of text. + using boost::uuids::detail::md5; + md5 md5_hash; + // unsigned int[4], 128 bits + md5::digest_type md5_digest{}; + std::string md5_digest_str; + md5_hash.process_bytes(data.data(), data.size()); + md5_hash.get_digest(md5_digest); + boost::algorithm::hex(md5_digest, md5_digest + std::size(md5_digest), std::back_inserter(md5_digest_str)); + // MD5 hash is 32 HEX digits long. + assert(md5_digest_str.size() == 32); + // This line will be emited at the end of the file. + return "# MD5 checksum " + md5_digest_str + "\n"; +}; + +// Assume that the last line with the comment inside the config file contains a checksum and that the user didn't modify the config file. +static bool verify_config_file_checksum(boost::nowide::ifstream &ifs) +{ + auto read_whole_config_file = [&ifs]() -> std::string { + std::stringstream ss; + ss << ifs.rdbuf(); + return ss.str(); + }; + + ifs.seekg(0, boost::nowide::ifstream::beg); + std::string whole_config = read_whole_config_file(); + + // The checksum should be on the last line in the config file. + if (size_t last_comment_pos = whole_config.find_last_of('#'); last_comment_pos != std::string::npos) { + // Split read config into two parts, one with checksum, and the second part is part with configuration from the checksum was computed. + // Verify existence and validity of the MD5 checksum line at the end of the file. + // When the checksum isn't found, the checksum was not saved correctly, it was removed or it is an older config file without the checksum. + // If the checksum is incorrect, then the file was either not saved correctly or modified. + if (std::string_view(whole_config.c_str() + last_comment_pos, whole_config.size() - last_comment_pos) == appconfig_md5_hash_line({ whole_config.data(), last_comment_pos })) + return true; + } + return false; +} +#endif + std::string AppConfig::load() { // 1) Read the complete config file into a boost::property_tree. namespace pt = boost::property_tree; pt::ptree tree; - boost::nowide::ifstream ifs(AppConfig::config_path()); + boost::nowide::ifstream ifs; + bool recovered = false; + try { + ifs.open(AppConfig::config_path()); +#ifdef WIN32 + // Verify the checksum of the config file without taking just for debugging purpose. + if (!verify_config_file_checksum(ifs)) + BOOST_LOG_TRIVIAL(info) << "The configuration file " << AppConfig::config_path() << + " has a wrong MD5 checksum or the checksum is missing. This may indicate a file corruption or a harmless user edit."; + + ifs.seekg(0, boost::nowide::ifstream::beg); +#endif pt::read_ini(ifs, tree); } catch (pt::ptree_error& ex) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - // ! But to avoid the use of _utf8 (related to use of wxWidgets) - // we will rethrow this exception from the place of load() call, if returned value wouldn't be empty - /* - throw Slic3r::RuntimeError( - _utf8(L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.")) + - "\n\n" + AppConfig::config_path() + "\n\n" + ex.what()); - */ - return ex.what(); +#ifdef WIN32 + // The configuration file is corrupted, try replacing it with the backup configuration. + ifs.close(); + std::string backup_path = (boost::format("%1%.bak") % AppConfig::config_path()).str(); + if (boost::filesystem::exists(backup_path)) { + // Compute checksum of the configuration backup file and try to load configuration from it when the checksum is correct. + boost::nowide::ifstream backup_ifs(backup_path); + if (!verify_config_file_checksum(backup_ifs)) { + BOOST_LOG_TRIVIAL(error) << format("Both \"%1%\" and \"%2%\" are corrupted. It isn't possible to restore configuration from the backup.", AppConfig::config_path(), backup_path); + backup_ifs.close(); + boost::filesystem::remove(backup_path); + } else if (std::string error_message; copy_file(backup_path, AppConfig::config_path(), error_message, false) != SUCCESS) { + BOOST_LOG_TRIVIAL(error) << format("Configuration file \"%1%\" is corrupted. Failed to restore from backup \"%2%\": %3%", AppConfig::config_path(), backup_path, error_message); + backup_ifs.close(); + boost::filesystem::remove(backup_path); + } else { + BOOST_LOG_TRIVIAL(info) << format("Configuration file \"%1%\" was corrupted. It has been succesfully restored from the backup \"%2%\".", AppConfig::config_path(), backup_path); + // Try parse configuration file after restore from backup. + try { + ifs.open(AppConfig::config_path()); + pt::read_ini(ifs, tree); + recovered = true; + } catch (pt::ptree_error& ex) { + BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\" after it has been restored from backup: %2%", AppConfig::config_path(), ex.what()); + } + } + } else +#endif // WIN32 + BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\": %2%", AppConfig::config_path(), ex.what()); + if (! recovered) { + // Report the initial error of parsing PrusaSlicer.ini. + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + // ! But to avoid the use of _utf8 (related to use of wxWidgets) + // we will rethrow this exception from the place of load() call, if returned value wouldn't be empty + /* + throw Slic3r::RuntimeError( + _utf8(L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.")) + + "\n\n" + AppConfig::config_path() + "\n\n" + ex.what()); + */ + return ex.what(); + } } // 2) Parse the property_tree, extract the sections and key / value pairs. @@ -272,22 +365,21 @@ void AppConfig::save() const auto path = config_path(); std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str(); - boost::nowide::ofstream c; - c.open(path_pid, std::ios::out | std::ios::trunc); + std::stringstream config_ss; if (m_mode == EAppMode::Editor) - c << "# " << Slic3r::header_slic3r_generated() << std::endl; + config_ss << "# " << Slic3r::header_slic3r_generated() << std::endl; else - c << "# " << Slic3r::header_gcodeviewer_generated() << std::endl; + config_ss << "# " << Slic3r::header_gcodeviewer_generated() << std::endl; // Make sure the "no" category is written first. for (const auto& kvp : m_storage[""]) - c << kvp.first << " = " << kvp.second << std::endl; + config_ss << kvp.first << " = " << kvp.second << std::endl; // Write the other categories. for (const auto& category : m_storage) { if (category.first.empty()) continue; - c << std::endl << "[" << category.first << "]" << std::endl; + config_ss << std::endl << "[" << category.first << "]" << std::endl; for (const auto& kvp : category.second) - c << kvp.first << " = " << kvp.second << std::endl; + config_ss << kvp.first << " = " << kvp.second << std::endl; } // Write vendor sections for (const auto &vendor : m_vendors) { @@ -295,17 +387,42 @@ void AppConfig::save() for (const auto &model : vendor.second) { size_sum += model.second.size(); } if (size_sum == 0) { continue; } - c << std::endl << "[" << VENDOR_PREFIX << vendor.first << "]" << std::endl; + config_ss << std::endl << "[" << VENDOR_PREFIX << vendor.first << "]" << std::endl; for (const auto &model : vendor.second) { - if (model.second.size() == 0) { continue; } + if (model.second.empty()) { continue; } const std::vector variants(model.second.begin(), model.second.end()); const auto escaped = escape_strings_cstyle(variants); - c << MODEL_PREFIX << model.first << " = " << escaped << std::endl; + config_ss << MODEL_PREFIX << model.first << " = " << escaped << std::endl; } } - c.close(); + // One empty line before the MD5 sum. + config_ss << std::endl; + std::string config_str = config_ss.str(); + boost::nowide::ofstream c; + c.open(path_pid, std::ios::out | std::ios::trunc); + c << config_str; +#ifdef WIN32 + // WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API + // provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition, + // we save the config file into a backup first before moving it to the final destination. + c << appconfig_md5_hash_line(config_str); +#endif + c.close(); + +#ifdef WIN32 + // Make a backup of the configuration file before copying it to the final destination. + std::string error_message; + std::string backup_path = (boost::format("%1%.bak") % path).str(); + // Copy configuration file with PID suffix into the configuration file with "bak" suffix. + if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS) + BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration."; +#endif + + // Rename the config atomically. + // On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile. + // To cope with that, we already made a backup of the config on Windows. rename_file(path_pid, path); m_dirty = false; } diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index c1bc0837c..d4f125f60 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -37,7 +37,7 @@ public: // Load the slic3r.ini from a user profile directory (or a datadir, if configured). // return error string or empty strinf - std::string load(); + std::string load(); // Store the slic3r.ini into a user profile directory (or a datadir, if configured). void save(); From ad336e2cc0eeb0c42abefb0755607bb252842b6f Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 25 Jun 2021 17:53:31 +0200 Subject: [PATCH 07/37] Because of a crash in PrusaSlicer 2.3.0/2.3.1 when showing an update notification with some locales, we don't want PrusaSlicer 2.3.0/2.3.1 to show this notification. On the other hand, we would like PrusaSlicer 2.3.2 to show an update notification of the upcoming PrusaSlicer 2.4.0. Thus we will let PrusaSlicer 2.3.2 and couple of follow-up versions to download the version number from an alternate file until the PrusaSlicer 2.3.0/2.3.1 are phased out, then we will revert to the original name. --- src/libslic3r/AppConfig.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 87eeca198..cee9eafdc 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -30,7 +30,12 @@ namespace Slic3r { static const std::string VENDOR_PREFIX = "vendor:"; static const std::string MODEL_PREFIX = "model:"; -static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version"; +// Because of a crash in PrusaSlicer 2.3.0/2.3.1 when showing an update notification with some locales, we don't want PrusaSlicer 2.3.0/2.3.1 +// to show this notification. On the other hand, we would like PrusaSlicer 2.3.2 to show an update notification of the upcoming PrusaSlicer 2.4.0. +// Thus we will let PrusaSlicer 2.3.2 and couple of follow-up versions to download the version number from an alternate file until the PrusaSlicer 2.3.0/2.3.1 +// are phased out, then we will revert to the original name. +//static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version"; +static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version2"; const std::string AppConfig::SECTION_FILAMENTS = "filaments"; const std::string AppConfig::SECTION_MATERIALS = "sla_materials"; From 0f3cabb5d9c62649c9055798566be1c00533279f Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Sun, 27 Jun 2021 16:04:23 +0200 Subject: [PATCH 08/37] Support for forward compatibility of configurations, user and system config bundles, project files (3MFs, AMFs). When loading these files, the caller may decide whether to substitute some of the configuration values the current PrusaSlicer version does not understand with some reasonable default value, and whether to report it. If substitution is disabled, an exception is being thrown as before this commit. If substitution is enabled, list of substitutions is returned by the API to be presented to the user. This allows us to introduce for example new firmware flavor key in PrusaSlicer 2.4 while letting PrusaSlicer 2.3.2 to fall back to some default and to report it to the user. When slicing from command line, substutions are performed by default and reported into the console, however substitutions may be either disabled or made silent with the new "config-compatibility" command line option. Substitute enums and bools only. Allow booleans to be parsed as true: "1", "enabled", "on" case insensitive false: "0", "disabled", "off" case insensitive This will allow us in the future for example to switch the draft_shield boolean to an enum with the following values: "disabled" / "enabled" / "limited". Added "enum_bitmask.hpp" - support for type safe sets of options. See for example PresetBundle::load_configbundle(... LoadConfigBundleAttributes flags) for an example of intended usage. WIP: GUI for reporting the list of config substitutions needs to be implemented by @YuSanka. --- src/PrusaSlicer.cpp | 24 +++- src/libslic3r/CMakeLists.txt | 1 + src/libslic3r/Config.cpp | 91 +++++++++----- src/libslic3r/Config.hpp | 81 +++++++++++-- src/libslic3r/Format/3mf.cpp | 51 ++++---- src/libslic3r/Format/3mf.hpp | 3 +- src/libslic3r/Format/AMF.cpp | 32 ++--- src/libslic3r/Format/AMF.hpp | 2 +- src/libslic3r/Format/PRUS.cpp | 7 +- src/libslic3r/Format/SL1.cpp | 10 +- src/libslic3r/Format/SL1.hpp | 8 +- src/libslic3r/GCode/GCodeProcessor.cpp | 5 +- src/libslic3r/Model.cpp | 39 +++--- src/libslic3r/Model.hpp | 19 ++- src/libslic3r/Preset.cpp | 16 ++- src/libslic3r/Preset.hpp | 28 ++++- src/libslic3r/PresetBundle.cpp | 128 +++++++++++++------- src/libslic3r/PresetBundle.hpp | 25 ++-- src/libslic3r/PrintConfig.cpp | 23 +++- src/libslic3r/PrintConfig.hpp | 5 +- src/libslic3r/enum_bitmask.hpp | 80 ++++++++++++ src/libslic3r/pchheader.hpp | 1 + src/slic3r/Config/Snapshot.cpp | 2 +- src/slic3r/GUI/ConfigWizard.cpp | 9 +- src/slic3r/GUI/GUI_App.cpp | 63 +++++----- src/slic3r/GUI/GUI_App.hpp | 4 +- src/slic3r/GUI/GUI_Init.cpp | 33 +---- src/slic3r/GUI/GUI_Init.hpp | 4 + src/slic3r/GUI/Jobs/SLAImportJob.cpp | 11 +- src/slic3r/GUI/MainFrame.cpp | 22 +++- src/slic3r/GUI/MainFrame.hpp | 2 +- src/slic3r/GUI/Plater.cpp | 19 ++- src/slic3r/Utils/FixModelByWin10.cpp | 3 +- src/slic3r/Utils/PresetUpdater.cpp | 35 +++--- tests/fff_print/test_data.cpp | 4 +- tests/fff_print/test_flow.cpp | 2 +- tests/fff_print/test_gcodewriter.cpp | 2 +- tests/fff_print/test_print.cpp | 2 +- tests/fff_print/test_printgcode.cpp | 2 +- tests/fff_print/test_skirt_brim.cpp | 22 ++-- tests/libslic3r/test_3mf.cpp | 8 +- tests/libslic3r/test_config.cpp | 14 +-- tests/libslic3r/test_placeholder_parser.cpp | 2 +- xs/src/perlglue.cpp | 5 +- xs/xsp/Config.xsp | 4 +- xs/xsp/Flow.xsp | 2 +- xs/xsp/Model.xsp | 2 +- 47 files changed, 643 insertions(+), 314 deletions(-) create mode 100644 src/libslic3r/enum_bitmask.hpp diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 50e5096bf..5212d66c8 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -118,7 +118,8 @@ int CLI::run(int argc, char **argv) boost::algorithm::iends_with(boost::filesystem::path(argv[0]).filename().string(), "gcodeviewer"); #endif // _WIN32 - const std::vector &load_configs = m_config.option("load", true)->values; + const std::vector &load_configs = m_config.option("load", true)->values; + const ForwardCompatibilitySubstitutionRule config_substitution_rule = m_config.option>("config_compatibility", true)->value; // load config files supplied via --load for (auto const &file : load_configs) { @@ -130,13 +131,19 @@ int CLI::run(int argc, char **argv) return 1; } } - DynamicPrintConfig config; + DynamicPrintConfig config; + ConfigSubstitutions config_substitutions; try { - config.load(file); + config_substitutions = config.load(file, config_substitution_rule); } catch (std::exception &ex) { - boost::nowide::cerr << "Error while reading config file: " << ex.what() << std::endl; + boost::nowide::cerr << "Error while reading config file \"" << file << "\": " << ex.what() << std::endl; return 1; } + if (! config_substitutions.empty()) { + boost::nowide::cout << "The following configuration values were substituted when loading \" << file << \":\n"; + for (const ConfigSubstitution &subst : config_substitutions) + boost::nowide::cout << "\tkey = \"" << subst.opt_def->opt_key << "\"\t loaded = \"" << subst.old_value << "\tsubstituted = \"" << subst.new_value->serialize() << "\"\n"; + } config.normalize_fdm(); PrinterTechnology other_printer_technology = get_printer_technology(config); if (printer_technology == ptUnknown) { @@ -174,7 +181,9 @@ int CLI::run(int argc, char **argv) try { // When loading an AMF or 3MF, config is imported as well, including the printer technology. DynamicPrintConfig config; - model = Model::read_from_file(file, &config, true); + ConfigSubstitutionContext config_substitutions(config_substitution_rule); + //FIXME should we check the version here? // | Model::LoadAttribute::CheckVersion ? + model = Model::read_from_file(file, &config, &config_substitutions, Model::LoadAttribute::AddDefaultInstances); PrinterTechnology other_printer_technology = get_printer_technology(config); if (printer_technology == ptUnknown) { printer_technology = other_printer_technology; @@ -183,6 +192,11 @@ int CLI::run(int argc, char **argv) boost::nowide::cerr << "Mixing configurations for FFF and SLA technologies" << std::endl; return 1; } + if (! config_substitutions.substitutions.empty()) { + boost::nowide::cout << "The following configuration values were substituted when loading \" << file << \":\n"; + for (const ConfigSubstitution& subst : config_substitutions.substitutions) + boost::nowide::cout << "\tkey = \"" << subst.opt_def->opt_key << "\"\t loaded = \"" << subst.old_value << "\tsubstituted = \"" << subst.new_value->serialize() << "\"\n"; + } // config is applied to m_print_config before the current m_config values. config += std::move(m_print_config); m_print_config = std::move(config); diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index fe09e6557..bfe8427d0 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -33,6 +33,7 @@ add_library(libslic3r STATIC EdgeGrid.hpp ElephantFootCompensation.cpp ElephantFootCompensation.hpp + enum_bitmask.hpp ExPolygon.cpp ExPolygon.hpp ExPolygonCollection.cpp diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index bd396243c..b9f9b266d 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -23,6 +23,10 @@ #include #include +//FIXME for GCodeFlavor and gcfMarlin (for forward-compatibility conversion) +// This is not nice, likely it would be better to pass the ConfigSubstitutionContext to handle_legacy(). +#include "PrintConfig.hpp" + namespace Slic3r { // Escape \n, \r and backslash @@ -211,6 +215,10 @@ std::string escape_ampersand(const std::string& str) return std::string(out.data(), outptr - out.data()); } +void ConfigOptionDeleter::operator()(ConfigOption* p) { + delete p; +} + std::vector ConfigOptionDef::cli_args(const std::string &key) const { std::vector args; @@ -361,7 +369,8 @@ std::ostream& ConfigDef::print_cli_help(std::ostream& out, bool show_defaults, s // right: option description std::string descr = def.tooltip; - if (show_defaults && def.default_value && def.type != coBool + bool show_defaults_this = show_defaults || def.opt_key == "config_compatibility"; + if (show_defaults_this && def.default_value && def.type != coBool && (def.type != coString || !def.default_value->serialize().empty())) { descr += " ("; if (!def.sidetext.empty()) { @@ -469,7 +478,7 @@ void ConfigBase::set(const std::string &opt_key, double value, bool create) } } -bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, bool append) +bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, ConfigSubstitutionContext& substitutions_ctxt, bool append) { t_config_option_key opt_key = opt_key_src; std::string value = value_src; @@ -479,29 +488,29 @@ bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src, if (opt_key.empty()) // Ignore the option. return true; - return this->set_deserialize_raw(opt_key, value, append); + return this->set_deserialize_raw(opt_key, value, substitutions_ctxt, append); } -void ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const std::string &value_src, bool append) +void ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const std::string &value_src, ConfigSubstitutionContext& substitutions_ctxt, bool append) { - if (! this->set_deserialize_nothrow(opt_key_src, value_src, append)) + if (! this->set_deserialize_nothrow(opt_key_src, value_src, substitutions_ctxt, append)) throw BadOptionTypeException(format("ConfigBase::set_deserialize() failed for parameter \"%1%\", value \"%2%\"", opt_key_src, value_src)); } -void ConfigBase::set_deserialize(std::initializer_list items) +void ConfigBase::set_deserialize(std::initializer_list items, ConfigSubstitutionContext& substitutions_ctxt) { for (const SetDeserializeItem &item : items) - this->set_deserialize(item.opt_key, item.opt_value, item.append); + this->set_deserialize(item.opt_key, item.opt_value, substitutions_ctxt, item.append); } -bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, const std::string &value, bool append) +bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, const std::string &value, ConfigSubstitutionContext& substitutions_ctxt, bool append) { - t_config_option_key opt_key = opt_key_src; + t_config_option_key opt_key = opt_key_src; // Try to deserialize the option by its name. - const ConfigDef *def = this->def(); + const ConfigDef *def = this->def(); if (def == nullptr) throw NoDefinitionException(opt_key); - const ConfigOptionDef *optdef = def->get(opt_key); + const ConfigOptionDef *optdef = def->get(opt_key); if (optdef == nullptr) { // If we didn't find an option, look for any other option having this as an alias. for (const auto &opt : def->options) { @@ -523,14 +532,35 @@ bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, con // Aliasing for example "solid_layers" to "top_solid_layers" and "bottom_solid_layers". for (const t_config_option_key &shortcut : optdef->shortcut) // Recursive call. - if (! this->set_deserialize_raw(shortcut, value, append)) + if (! this->set_deserialize_raw(shortcut, value, substitutions_ctxt, append)) return false; return true; } ConfigOption *opt = this->option(opt_key, true); assert(opt != nullptr); - return opt->deserialize(value, append); + bool success = opt->deserialize(value, append); + if (! success && substitutions_ctxt.rule != ForwardCompatibilitySubstitutionRule::Disable && + // Only allow substitutions of an enum value by another enum value or a boolean value with an enum value. + // That means, we expect enum values being added in the future and possibly booleans being converted to enums. + (optdef->type == coEnum || optdef->type == coBool)) + { + // Deserialize failed, try to substitute with a default value. + assert(substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::Enable || substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::EnableSilent); + + opt->set(optdef->default_value.get()); + + if (substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::Enable) { + // Log the substitution. + ConfigSubstitution config_substitution; + config_substitution.opt_def = optdef; + config_substitution.old_value = value;//std::unique_ptr(opt); + config_substitution.new_value = ConfigOptionUniquePtr(this->option(opt_key, true)->clone()); + substitutions_ctxt.substitutions.emplace_back(std::move(config_substitution)); + } + return true; + } + return success; } // Return an absolute value of a possibly relative config variable. @@ -589,36 +619,37 @@ void ConfigBase::setenv_() const } } -void ConfigBase::load(const std::string &file) +ConfigSubstitutions ConfigBase::load(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule) { - if (is_gcode_file(file)) - this->load_from_gcode_file(file); - else - this->load_from_ini(file); + return is_gcode_file(file) ? + this->load_from_gcode_file(file, true /* check header */, compatibility_rule) : + this->load_from_ini(file, compatibility_rule); } -void ConfigBase::load_from_ini(const std::string &file) +ConfigSubstitutions ConfigBase::load_from_ini(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule) { boost::property_tree::ptree tree; boost::nowide::ifstream ifs(file); boost::property_tree::read_ini(ifs, tree); - this->load(tree); + return this->load(tree, compatibility_rule); } -void ConfigBase::load(const boost::property_tree::ptree &tree) +ConfigSubstitutions ConfigBase::load(const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule) { + ConfigSubstitutionContext substitutions_ctxt(compatibility_rule); for (const boost::property_tree::ptree::value_type &v : tree) { try { t_config_option_key opt_key = v.first; - this->set_deserialize(opt_key, v.second.get_value()); + this->set_deserialize(opt_key, v.second.get_value(), substitutions_ctxt); } catch (UnknownOptionException & /* e */) { // ignore } } + return std::move(substitutions_ctxt.substitutions); } // Load the config keys from the tail of a G-code file. -void ConfigBase::load_from_gcode_file(const std::string& file, bool check_header) +ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, bool check_header, ForwardCompatibilitySubstitutionRule compatibility_rule) { // Read a 64k block from the end of the G-code. boost::nowide::ifstream ifs(file); @@ -639,13 +670,15 @@ void ConfigBase::load_from_gcode_file(const std::string& file, bool check_header ifs.read(data.data(), data_length); ifs.close(); - size_t key_value_pairs = load_from_gcode_string(data.data()); + ConfigSubstitutionContext substitutions_ctxt(compatibility_rule); + size_t key_value_pairs = load_from_gcode_string(data.data(), substitutions_ctxt); if (key_value_pairs < 80) throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs)); + return std::move(substitutions_ctxt.substitutions); } // Load the config keys from the given string. -size_t ConfigBase::load_from_gcode_string(const char* str) +size_t ConfigBase::load_from_gcode_string(const char* str, ConfigSubstitutionContext& substitutions) { if (str == nullptr) return 0; @@ -690,7 +723,7 @@ size_t ConfigBase::load_from_gcode_string(const char* str) if (key == nullptr) break; try { - this->set_deserialize(std::string(key, key_end), std::string(value, end)); + this->set_deserialize(std::string(key, key_end), std::string(value, end), substitutions); ++num_key_value_pairs; } catch (UnknownOptionException & /* e */) { @@ -719,7 +752,7 @@ void ConfigBase::null_nullables() ConfigOption *opt = this->optptr(opt_key, false); assert(opt != nullptr); if (opt->nullable()) - opt->deserialize("nil"); + opt->deserialize("nil", ForwardCompatibilitySubstitutionRule::Disable); } } @@ -883,8 +916,10 @@ bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option // Do not unescape single string values, the unescaping is left to the calling shell. static_cast(opt_base)->value = value; } else { + // Just bail out if the configuration value is not understood. + ConfigSubstitutionContext context(ForwardCompatibilitySubstitutionRule::Disable); // Any scalar value of a type different from Bool and String. - if (! this->set_deserialize_nothrow(opt_key, value, false)) { + if (! this->set_deserialize_nothrow(opt_key, value, context, false)) { boost::nowide::cerr << "Invalid value supplied for --" << token.c_str() << std::endl; return false; } diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 14b4b7c2e..bf356dfe6 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -16,6 +16,7 @@ #include "Exception.hpp" #include "Point.hpp" +#include #include #include #include @@ -163,6 +164,41 @@ enum PrinterTechnology : unsigned char ptAny }; +enum ForwardCompatibilitySubstitutionRule +{ + Disable, + Enable, + EnableSilent, +}; + +class ConfigOption; +class ConfigOptionDef; +// For forward definition of ConfigOption in ConfigOptionUniquePtr, we have to define a custom deleter. +struct ConfigOptionDeleter { void operator()(ConfigOption* p); }; +using ConfigOptionUniquePtr = std::unique_ptr; + +// When parsing a configuration value, if the old_value is not understood by this PrusaSlicer version, +// it is being substituted with some default value that this PrusaSlicer could work with. +// This structure serves to inform the user about the substitutions having been done during file import. +struct ConfigSubstitution { + const ConfigOptionDef *opt_def { nullptr }; + std::string old_value; + ConfigOptionUniquePtr new_value; +}; + +using ConfigSubstitutions = std::vector; + +// Filled in by ConfigBase::set_deserialize_raw(), which based on "rule" either bails out +// or performs substitutions when encountering an unknown configuration value. +struct ConfigSubstitutionContext +{ + ConfigSubstitutionContext(ForwardCompatibilitySubstitutionRule rl) : rule(rl) {} + bool empty() const throw() { return substitutions.empty(); } + + ForwardCompatibilitySubstitutionRule rule; + ConfigSubstitutions substitutions; +}; + // A generic value of a configuration option. class ConfigOption { public: @@ -768,7 +804,7 @@ public: return escape_string_cstyle(this->value); } - bool deserialize(const std::string &str, bool append = false) override + bool deserialize(const std::string &str, bool append = false) override { UNUSED(append); return unescape_string_cstyle(str, this->value); @@ -1272,8 +1308,15 @@ public: bool deserialize(const std::string &str, bool append = false) override { UNUSED(append); - this->value = (str.compare("1") == 0); - return true; + if (str == "1" || boost::iequals(str, "enabled") || boost::iequals(str, "on")) { + this->value = true; + return true; + } + if (str == "0" || boost::iequals(str, "disabled") || boost::iequals(str, "off")) { + this->value = false; + return true; + } + return false; } private: @@ -1687,6 +1730,14 @@ public: static const constexpr char *nocli = "~~~noCLI"; }; +inline bool operator<(const ConfigSubstitution &lhs, const ConfigSubstitution &rhs) throw() { + return lhs.opt_def->opt_key < rhs.opt_def->opt_key || + (lhs.opt_def->opt_key == rhs.opt_def->opt_key && lhs.old_value < rhs.old_value); +} +inline bool operator==(const ConfigSubstitution &lhs, const ConfigSubstitution &rhs) throw() { + return lhs.opt_def == rhs.opt_def && lhs.old_value == rhs.old_value; +} + // Map from a config option name to its definition. // The definition does not carry an actual value of the config option, only its constant default value. // t_config_option_key is std::string @@ -1765,6 +1816,8 @@ public: } }; + + // An abstract configuration store. class ConfigBase : public ConfigOptionResolver { @@ -1853,9 +1906,11 @@ public: // Set a configuration value from a string, it will call an overridable handle_legacy() // to resolve renamed and removed configuration keys. - bool set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, bool append = false); + bool set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, ConfigSubstitutionContext& substitutions, bool append = false); // May throw BadOptionTypeException() if the operation fails. - void set_deserialize(const t_config_option_key &opt_key, const std::string &str, bool append = false); + void set_deserialize(const t_config_option_key &opt_key, const std::string &str, ConfigSubstitutionContext& config_substitutions, bool append = false); + void set_deserialize_strict(const t_config_option_key &opt_key, const std::string &str, bool append = false) + { ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; this->set_deserialize(opt_key, str, ctxt, append); } struct SetDeserializeItem { SetDeserializeItem(const char *opt_key, const char *opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {} SetDeserializeItem(const std::string &opt_key, const std::string &opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {} @@ -1870,17 +1925,19 @@ public: std::string opt_key; std::string opt_value; bool append = false; }; // May throw BadOptionTypeException() if the operation fails. - void set_deserialize(std::initializer_list items); + void set_deserialize(std::initializer_list items, ConfigSubstitutionContext& substitutions); + void set_deserialize_strict(std::initializer_list items) + { ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; this->set_deserialize(items, ctxt); } double get_abs_value(const t_config_option_key &opt_key) const; double get_abs_value(const t_config_option_key &opt_key, double ratio_over) const; void setenv_() const; - void load(const std::string &file); - void load_from_ini(const std::string &file); - void load_from_gcode_file(const std::string& file, bool check_header = true); + ConfigSubstitutions load(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule); + ConfigSubstitutions load_from_ini(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule); + ConfigSubstitutions load_from_gcode_file(const std::string &file, bool check_header /* = true */, ForwardCompatibilitySubstitutionRule compatibility_rule); // Returns number of key/value pairs extracted. - size_t load_from_gcode_string(const char* str); - void load(const boost::property_tree::ptree &tree); + size_t load_from_gcode_string(const char* str, ConfigSubstitutionContext& substitutions); + ConfigSubstitutions load(const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule); void save(const std::string &file) const; // Set all the nullable values to nils. @@ -1888,7 +1945,7 @@ public: private: // Set a configuration value from a string. - bool set_deserialize_raw(const t_config_option_key &opt_key_src, const std::string &str, bool append); + bool set_deserialize_raw(const t_config_option_key& opt_key_src, const std::string& value, ConfigSubstitutionContext& substitutions, bool append); }; // Configuration store with dynamic number of configuration values. diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index fbf27c548..c2ba011a8 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -419,7 +419,7 @@ namespace Slic3r { _3MF_Importer(); ~_3MF_Importer(); - bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, bool check_version); + bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version); private: void _destroy_xml_parser(); @@ -434,16 +434,16 @@ namespace Slic3r { XML_ErrorString(XML_GetErrorCode(m_xml_parser)); } - bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config); + bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); void _extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_sla_drain_holes_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_custom_gcode_per_print_z_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, const std::string& archive_filename); + void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename); bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); // handlers to parse the .model file @@ -510,7 +510,7 @@ namespace Slic3r { bool _handle_start_config_metadata(const char** attributes, unsigned int num_attributes); bool _handle_end_config_metadata(); - bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes); + bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); // callbacks to parse the .model file static void XMLCALL _handle_start_model_xml_element(void* userData, const char* name, const char** attributes); @@ -539,7 +539,7 @@ namespace Slic3r { _destroy_xml_parser(); } - bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, bool check_version) + bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version) { m_version = 0; m_check_version = check_version; @@ -560,7 +560,7 @@ namespace Slic3r { m_curr_characters.clear(); clear_errors(); - return _load_model_from_file(filename, model, config); + return _load_model_from_file(filename, model, config, config_substitutions); } void _3MF_Importer::_destroy_xml_parser() @@ -581,7 +581,7 @@ namespace Slic3r { XML_StopParser(m_xml_parser, false); } - bool _3MF_Importer::_load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config) + bool _3MF_Importer::_load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions) { mz_zip_archive archive; mz_zip_zero_struct(&archive); @@ -635,7 +635,7 @@ namespace Slic3r { } else if (boost::algorithm::iequals(name, LAYER_CONFIG_RANGES_FILE)) { // extract slic3r layer config ranges file - _extract_layer_config_ranges_from_archive(archive, stat); + _extract_layer_config_ranges_from_archive(archive, stat, config_substitutions); } else if (boost::algorithm::iequals(name, SLA_SUPPORT_POINTS_FILE)) { // extract sla support points file @@ -647,7 +647,7 @@ namespace Slic3r { } else if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { // extract slic3r print config file - _extract_print_config_from_archive(archive, stat, config, filename); + _extract_print_config_from_archive(archive, stat, config, config_substitutions, filename); } else if (boost::algorithm::iequals(name, CUSTOM_GCODE_PER_PRINT_Z_FILE)) { // extract slic3r layer config ranges file @@ -704,7 +704,7 @@ namespace Slic3r { new_model_object->clear_instances(); new_model_object->add_instance(*model_object->instances.back()); model_object->delete_last_instance(); - if (!_generate_volumes(*new_model_object, *geometry, volumes)) + if (!_generate_volumes(*new_model_object, *geometry, volumes, config_substitutions)) return false; } } @@ -759,7 +759,7 @@ namespace Slic3r { if (metadata.key == "name") model_object->name = metadata.value; else - model_object->config.set_deserialize(metadata.key, metadata.value); + model_object->config.set_deserialize(metadata.key, metadata.value, config_substitutions); } // select object's detected volumes @@ -775,7 +775,7 @@ namespace Slic3r { volumes_ptr = &volumes; } - if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr)) + if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr, config_substitutions)) return false; } @@ -867,7 +867,10 @@ namespace Slic3r { return true; } - void _3MF_Importer::_extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, const std::string& archive_filename) + void _3MF_Importer::_extract_print_config_from_archive( + mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, + DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, + const std::string& archive_filename) { if (stat.m_uncomp_size > 0) { std::string buffer((size_t)stat.m_uncomp_size, 0); @@ -876,7 +879,7 @@ namespace Slic3r { add_error("Error while reading config data to buffer"); return; } - config.load_from_gcode_string(buffer.data()); + config.load_from_gcode_string(buffer.data(), config_substitutions); } } @@ -942,7 +945,7 @@ namespace Slic3r { } } - void _3MF_Importer::_extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) + void _3MF_Importer::_extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) { if (stat.m_uncomp_size > 0) { std::string buffer((size_t)stat.m_uncomp_size, 0); @@ -987,8 +990,7 @@ namespace Slic3r { continue; std::string opt_key = option.second.get(".opt_key"); std::string value = option.second.data(); - - config.set_deserialize(opt_key, value); + config.set_deserialize(opt_key, value, config_substitutions); } config_ranges[{ min_z, max_z }].assign_config(std::move(config)); @@ -1827,7 +1829,7 @@ namespace Slic3r { return true; } - bool _3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes) + bool _3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions) { if (!object.volumes.empty()) { add_error("Found invalid volumes count"); @@ -1943,7 +1945,7 @@ namespace Slic3r { else if (metadata.key == SOURCE_IN_METERS) volume->source.is_converted_from_meters = metadata.value == "1"; else - volume->config.set_deserialize(metadata.key, metadata.value); + volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); } } @@ -2953,16 +2955,15 @@ bool _3MF_Exporter::_add_custom_gcode_per_print_z_file_to_archive( mz_zip_archiv return true; } -bool load_3mf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version) +bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version) { - if (path == nullptr || config == nullptr || model == nullptr) + if (path == nullptr || model == nullptr) return false; // All import should use "C" locales for number formatting. CNumericLocalesSetter locales_setter; - - _3MF_Importer importer; - bool res = importer.load_model_from_file(path, *model, *config, check_version); + _3MF_Importer importer; + bool res = importer.load_model_from_file(path, *model, config, config_substitutions, check_version); importer.log_errors(); return res; } diff --git a/src/libslic3r/Format/3mf.hpp b/src/libslic3r/Format/3mf.hpp index a09a1b834..b91e90da7 100644 --- a/src/libslic3r/Format/3mf.hpp +++ b/src/libslic3r/Format/3mf.hpp @@ -25,11 +25,12 @@ namespace Slic3r { }; class Model; + struct ConfigSubstitutionContext; class DynamicPrintConfig; struct ThumbnailData; // Load the content of a 3mf file into the given model and preset bundle. - extern bool load_3mf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version); + extern bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version); // Save the given model and the config data contained in the given Print into a 3mf file. // The model could be modified during the export process if meshes are not repaired or have no shared vertices diff --git a/src/libslic3r/Format/AMF.cpp b/src/libslic3r/Format/AMF.cpp index 94318a930..d03cfd4fa 100644 --- a/src/libslic3r/Format/AMF.cpp +++ b/src/libslic3r/Format/AMF.cpp @@ -64,10 +64,11 @@ namespace Slic3r struct AMFParserContext { - AMFParserContext(XML_Parser parser, DynamicPrintConfig* config, Model* model) : + AMFParserContext(XML_Parser parser, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model) : m_parser(parser), m_model(*model), - m_config(config) + m_config(config), + m_config_substitutions(config_substitutions) { m_path.reserve(12); } @@ -258,6 +259,8 @@ struct AMFParserContext std::string m_value[5]; // Pointer to config to update if config data are stored inside the amf file DynamicPrintConfig *m_config { nullptr }; + // Config substitution rules and collected config substitution log. + ConfigSubstitutionContext *m_config_substitutions { nullptr }; private: AMFParserContext& operator=(AMFParserContext&); @@ -702,8 +705,9 @@ void AMFParserContext::endElement(const char * /* name */) } case NODE_TYPE_METADATA: - if ((m_config != nullptr) && strncmp(m_value[0].c_str(), SLIC3R_CONFIG_TYPE, strlen(SLIC3R_CONFIG_TYPE)) == 0) - m_config->load_from_gcode_string(m_value[1].c_str()); + if ((m_config != nullptr) && strncmp(m_value[0].c_str(), SLIC3R_CONFIG_TYPE, strlen(SLIC3R_CONFIG_TYPE)) == 0) { + m_config->load_from_gcode_string(m_value[1].c_str(), *m_config_substitutions); + } else if (strncmp(m_value[0].c_str(), "slic3r.", 7) == 0) { const char *opt_key = m_value[0].c_str() + 7; if (print_config_def.options.find(opt_key) != print_config_def.options.end()) { @@ -721,7 +725,7 @@ void AMFParserContext::endElement(const char * /* name */) config = &it->second; } if (config) - config->set_deserialize(opt_key, m_value[1]); + config->set_deserialize(opt_key, m_value[1], *m_config_substitutions); } else if (m_path.size() == 3 && m_path[1] == NODE_TYPE_OBJECT && m_object && strcmp(opt_key, "layer_height_profile") == 0) { // Parse object's layer height profile, a semicolon separated list of floats. char *p = m_value[1].data(); @@ -849,7 +853,7 @@ void AMFParserContext::endDocument() } // Load an AMF file into a provided model. -bool load_amf_file(const char *path, DynamicPrintConfig *config, Model *model) +bool load_amf_file(const char *path, DynamicPrintConfig *config, ConfigSubstitutionContext *config_substitutions, Model *model) { if ((path == nullptr) || (model == nullptr)) return false; @@ -866,7 +870,7 @@ bool load_amf_file(const char *path, DynamicPrintConfig *config, Model *model) return false; } - AMFParserContext ctx(parser, config, model); + AMFParserContext ctx(parser, config, config_substitutions, model); XML_SetUserData(parser, (void*)&ctx); XML_SetElementHandler(parser, AMFParserContext::startElement, AMFParserContext::endElement); XML_SetCharacterDataHandler(parser, AMFParserContext::characters); @@ -908,7 +912,7 @@ bool load_amf_file(const char *path, DynamicPrintConfig *config, Model *model) return result; } -bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig* config, Model* model, bool check_version) +bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version) { if (stat.m_uncomp_size == 0) { @@ -924,7 +928,7 @@ bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_fi return false; } - AMFParserContext ctx(parser, config, model); + AMFParserContext ctx(parser, config, config_substitutions, model); XML_SetUserData(parser, (void*)&ctx); XML_SetElementHandler(parser, AMFParserContext::startElement, AMFParserContext::endElement); XML_SetCharacterDataHandler(parser, AMFParserContext::characters); @@ -984,7 +988,7 @@ bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_fi } // Load an AMF archive into a provided model. -bool load_amf_archive(const char* path, DynamicPrintConfig* config, Model* model, bool check_version) +bool load_amf_archive(const char* path, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version) { if ((path == nullptr) || (model == nullptr)) return false; @@ -1010,7 +1014,7 @@ bool load_amf_archive(const char* path, DynamicPrintConfig* config, Model* model { try { - if (!extract_model_from_archive(archive, stat, config, model, check_version)) + if (!extract_model_from_archive(archive, stat, config, config_substitutions, model, check_version)) { close_zip_reader(&archive); BOOST_LOG_TRIVIAL(error) << "Archive does not contain a valid model"; @@ -1052,13 +1056,13 @@ bool load_amf_archive(const char* path, DynamicPrintConfig* config, Model* model // Load an AMF file into a provided model. // If config is not a null pointer, updates it if the amf file/archive contains config data -bool load_amf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version) +bool load_amf(const char* path, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version) { CNumericLocalesSetter locales_setter; // use "C" locales and point as a decimal separator if (boost::iends_with(path, ".amf.xml")) // backward compatibility with older slic3r output - return load_amf_file(path, config, model); + return load_amf_file(path, config, config_substitutions, model); else if (boost::iends_with(path, ".amf")) { boost::nowide::ifstream file(path, boost::nowide::ifstream::binary); @@ -1069,7 +1073,7 @@ bool load_amf(const char* path, DynamicPrintConfig* config, Model* model, bool c file.read(zip_mask.data(), 2); file.close(); - return (zip_mask == "PK") ? load_amf_archive(path, config, model, check_version) : load_amf_file(path, config, model); + return (zip_mask == "PK") ? load_amf_archive(path, config, config_substitutions, model, check_version) : load_amf_file(path, config, config_substitutions, model); } else return false; diff --git a/src/libslic3r/Format/AMF.hpp b/src/libslic3r/Format/AMF.hpp index b4e2c54ab..a073071fc 100644 --- a/src/libslic3r/Format/AMF.hpp +++ b/src/libslic3r/Format/AMF.hpp @@ -7,7 +7,7 @@ class Model; class DynamicPrintConfig; // Load the content of an amf file into the given model and configuration. -extern bool load_amf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version); +extern bool load_amf(const char* path, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version); // Save the given model and the config data into an amf file. // The model could be modified during the export process if meshes are not repaired or have no shared vertices diff --git a/src/libslic3r/Format/PRUS.cpp b/src/libslic3r/Format/PRUS.cpp index abf30a53b..586fbafb5 100644 --- a/src/libslic3r/Format/PRUS.cpp +++ b/src/libslic3r/Format/PRUS.cpp @@ -284,11 +284,8 @@ static void extract_model_from_archive( volume->name = name; } // Set the extruder to the volume. - if (extruder_id != (unsigned int)-1) { - char str_extruder[64]; - sprintf(str_extruder, "%ud", extruder_id); - volume->config.set_deserialize("extruder", str_extruder); - } + if (extruder_id != (unsigned int)-1) + volume->config.set("extruder", int(extruder_id)); } // Load a PrusaControl project file into a provided model. diff --git a/src/libslic3r/Format/SL1.cpp b/src/libslic3r/Format/SL1.cpp index f556b0ead..6d779b94e 100644 --- a/src/libslic3r/Format/SL1.cpp +++ b/src/libslic3r/Format/SL1.cpp @@ -287,13 +287,13 @@ std::vector extract_slices_from_sla_archive( } // namespace -void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) +ConfigSubstitutions import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) { ArchiveData arch = extract_sla_archive(zipfname, "png"); - out.load(arch.profile); + return out.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); } -void import_sla_archive( +ConfigSubstitutions import_sla_archive( const std::string & zipfname, Vec2i windowsize, indexed_triangle_set & out, @@ -305,7 +305,7 @@ void import_sla_archive( windowsize.y() = std::max(2, windowsize.y()); ArchiveData arch = extract_sla_archive(zipfname, "thumbnail"); - profile.load(arch.profile); + ConfigSubstitutions config_substitutions = profile.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); RasterParams rstp = get_raster_params(profile); rstp.win = {windowsize.y(), windowsize.x()}; @@ -317,6 +317,8 @@ void import_sla_archive( if (!slices.empty()) out = slices_to_mesh(slices, 0, slicp.layerh, slicp.initial_layerh); + + return config_substitutions; } using ConfMap = std::map; diff --git a/src/libslic3r/Format/SL1.hpp b/src/libslic3r/Format/SL1.hpp index a3e6ce26c..2c7e1edc1 100644 --- a/src/libslic3r/Format/SL1.hpp +++ b/src/libslic3r/Format/SL1.hpp @@ -38,23 +38,23 @@ public: } }; -void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out); +ConfigSubstitutions import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out); -void import_sla_archive( +ConfigSubstitutions import_sla_archive( const std::string & zipfname, Vec2i windowsize, indexed_triangle_set & out, DynamicPrintConfig & profile, std::function progr = [](int) { return true; }); -inline void import_sla_archive( +inline ConfigSubstitutions import_sla_archive( const std::string & zipfname, Vec2i windowsize, indexed_triangle_set & out, std::function progr = [](int) { return true; }) { DynamicPrintConfig profile; - import_sla_archive(zipfname, windowsize, out, profile, progr); + return import_sla_archive(zipfname, windowsize, out, profile, progr); } } // namespace Slic3r::sla diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index a3f2f2219..65679f120 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1285,7 +1285,10 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr if (m_producer == EProducer::PrusaSlicer || m_producer == EProducer::Slic3rPE || m_producer == EProducer::Slic3r) { DynamicPrintConfig config; config.apply(FullPrintConfig::defaults()); - config.load_from_gcode_file(filename, false); + // Silently substitute unknown values by new ones for loading configurations from PrusaSlicer's own G-code. + // Showing substitution log or errors may make sense, but we are not really reading many values from the G-code config, + // thus a probability of incorrect substitution is low and the G-code viewer is a consumer-only anyways. + config.load_from_gcode_file(filename, false, ForwardCompatibilitySubstitutionRule::EnableSilent); apply_config(config); } else if (m_producer == EProducer::Simplify3D) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 84566f4b1..5943f960e 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -96,13 +96,17 @@ void Model::update_links_bottom_up_recursive() } } -Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* config, bool add_default_instances, bool check_version) +// Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well. +Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, LoadAttributes options) { Model model; DynamicPrintConfig temp_config; + ConfigSubstitutionContext temp_config_substitutions_context(ForwardCompatibilitySubstitutionRule::EnableSilent); if (config == nullptr) config = &temp_config; + if (config_substitutions == nullptr) + config_substitutions = &temp_config_substitutions_context; bool result = false; if (boost::algorithm::iends_with(input_file, ".stl")) @@ -110,9 +114,10 @@ Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* c else if (boost::algorithm::iends_with(input_file, ".obj")) result = load_obj(input_file.c_str(), &model); else if (boost::algorithm::iends_with(input_file, ".amf") || boost::algorithm::iends_with(input_file, ".amf.xml")) - result = load_amf(input_file.c_str(), config, &model, check_version); + result = load_amf(input_file.c_str(), config, config_substitutions, &model, options & LoadAttribute::CheckVersion); else if (boost::algorithm::iends_with(input_file, ".3mf")) - result = load_3mf(input_file.c_str(), config, &model, false); + //FIXME options & LoadAttribute::CheckVersion ? + result = load_3mf(input_file.c_str(), *config, *config_substitutions, &model, false); else if (boost::algorithm::iends_with(input_file, ".prusa")) result = load_prus(input_file.c_str(), &model); else @@ -127,24 +132,29 @@ Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* c for (ModelObject *o : model.objects) o->input_file = input_file; - if (add_default_instances) + if (options & LoadAttribute::AddDefaultInstances) model.add_default_instances(); CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, config); CustomGCode::check_mode_for_custom_gcode_per_print_z(model.custom_gcode_per_print_z); + sort_remove_duplicates(config_substitutions->substitutions); return model; } -Model Model::read_from_archive(const std::string& input_file, DynamicPrintConfig* config, bool add_default_instances, bool check_version) +// Loading model from a file (3MF or AMF), not from a simple geometry file (STL or OBJ). +Model Model::read_from_archive(const std::string& input_file, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, LoadAttributes options) { + assert(config != nullptr); + assert(config_substitutions != nullptr); + Model model; bool result = false; if (boost::algorithm::iends_with(input_file, ".3mf")) - result = load_3mf(input_file.c_str(), config, &model, check_version); + result = load_3mf(input_file.c_str(), *config, *config_substitutions, &model, options & LoadAttribute::CheckVersion); else if (boost::algorithm::iends_with(input_file, ".zip.amf")) - result = load_amf(input_file.c_str(), config, &model, check_version); + result = load_amf(input_file.c_str(), config, config_substitutions, &model, options & LoadAttribute::CheckVersion); else throw Slic3r::RuntimeError("Unknown file format. Input file must have .3mf or .zip.amf extension."); @@ -165,7 +175,7 @@ Model Model::read_from_archive(const std::string& input_file, DynamicPrintConfig o->input_file = input_file; } - if (add_default_instances) + if (options & LoadAttribute::AddDefaultInstances) model.add_default_instances(); CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, config); @@ -398,13 +408,12 @@ bool Model::looks_like_multipart_object() const } // Generate next extruder ID string, in the range of (1, max_extruders). -static inline std::string auto_extruder_id(unsigned int max_extruders, unsigned int &cntr) +static inline int auto_extruder_id(unsigned int max_extruders, unsigned int &cntr) { - char str_extruder[64]; - sprintf(str_extruder, "%ud", cntr + 1); - if (++ cntr == max_extruders) + int out = ++ cntr; + if (cntr == max_extruders) cntr = 0; - return str_extruder; + return out; } void Model::convert_multipart_object(unsigned int max_extruders) @@ -431,7 +440,7 @@ void Model::convert_multipart_object(unsigned int max_extruders) auto copy_volume = [o, max_extruders, &counter, &extruder_counter](ModelVolume *new_v) { assert(new_v != nullptr); new_v->name = o->name + "_" + std::to_string(counter++); - new_v->config.set_deserialize("extruder", auto_extruder_id(max_extruders, extruder_counter)); + new_v->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); return new_v; }; if (o->instances.empty()) { @@ -1738,7 +1747,7 @@ size_t ModelVolume::split(unsigned int max_extruders) this->object->volumes[ivolume]->center_geometry_after_creation(); this->object->volumes[ivolume]->translate(offset); this->object->volumes[ivolume]->name = name + "_" + std::to_string(idx + 1); - this->object->volumes[ivolume]->config.set_deserialize("extruder", auto_extruder_id(max_extruders, extruder_counter)); + this->object->volumes[ivolume]->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); this->object->volumes[ivolume]->m_is_splittable = 0; delete mesh; ++ idx; diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index c6a54d5c6..f835aa0ea 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -12,6 +12,7 @@ #include "TriangleMesh.hpp" #include "Arrange.hpp" #include "CustomGCode.hpp" +#include "enum_bitmask.hpp" #include #include @@ -1031,8 +1032,20 @@ public: OBJECTBASE_DERIVED_COPY_MOVE_CLONE(Model) - static Model read_from_file(const std::string& input_file, DynamicPrintConfig* config = nullptr, bool add_default_instances = true, bool check_version = false); - static Model read_from_archive(const std::string& input_file, DynamicPrintConfig* config, bool add_default_instances = true, bool check_version = false); + enum class LoadAttribute : int { + AddDefaultInstances, + CheckVersion + }; + using LoadAttributes = enum_bitmask; + + static Model read_from_file( + const std::string& input_file, + DynamicPrintConfig* config = nullptr, ConfigSubstitutionContext* config_substitutions = nullptr, + LoadAttributes options = LoadAttribute::AddDefaultInstances); + static Model read_from_archive( + const std::string& input_file, + DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, + LoadAttributes options = LoadAttribute::AddDefaultInstances); // Add a new ModelObject to this Model, generate a new ID for this ModelObject. ModelObject* add_object(); @@ -1097,6 +1110,8 @@ private: } }; +ENABLE_ENUM_BITMASK_OPERATORS(Model::LoadAttribute) + #undef OBJECTBASE_DERIVED_COPY_MOVE_CLONE #undef OBJECTBASE_DERIVED_PRIVATE_COPY_MOVE diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index df088935f..97457d63b 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -666,7 +666,9 @@ void PresetCollection::add_default_preset(const std::vector &keys, // Load all presets found in dir_path. // Throws an exception on error. -void PresetCollection::load_presets(const std::string &dir_path, const std::string &subdir) +void PresetCollection::load_presets( + const std::string &dir_path, const std::string &subdir, + PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule substitution_rule) { // Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points, // see https://github.com/prusa3d/PrusaSlicer/issues/732 @@ -693,7 +695,9 @@ void PresetCollection::load_presets(const std::string &dir_path, const std::stri // Load the preset file, apply preset values on top of defaults. try { DynamicPrintConfig config; - config.load_from_ini(preset.file); + ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); + if (! config_substitutions.empty()) + substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) }); // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. const Preset &default_preset = this->default_preset_for(config); preset.config = default_preset.config; @@ -1580,7 +1584,9 @@ PhysicalPrinterCollection::PhysicalPrinterCollection( const std::vector; + // Collections of presets of the same type (one of the Print, Filament or Printer type). class PresetCollection { @@ -280,7 +304,7 @@ public: void add_default_preset(const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name); // Load ini files of the particular type from the provided directory path. - void load_presets(const std::string &dir_path, const std::string &subdir); + void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule); // Load a preset from an already parsed config file, insert it into the sorted sequence of presets // and select it, losing previous modifications. @@ -692,7 +716,7 @@ public: const std::deque& operator()() const { return m_printers; } // Load ini files of the particular type from the provided directory path. - void load_printers(const std::string& dir_path, const std::string& subdir); + void load_printers(const std::string& dir_path, const std::string& subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule); void load_printers_from_presets(PrinterPresetCollection &printer_presets); // Load printer from the loaded configuration void load_printer(const std::string& path, const std::string& name, DynamicPrintConfig&& config, bool select, bool save=false); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index e7f818d08..fc7987b29 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -187,7 +187,7 @@ void PresetBundle::setup_directories() } } -void PresetBundle::load_presets(AppConfig &config, const std::string &preferred_model_id) +PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule substitution_rule, const std::string &preferred_model_id) { // First load the vendor specific system presets. std::string errors_cummulative = this->load_system_presets(); @@ -200,33 +200,35 @@ void PresetBundle::load_presets(AppConfig &config, const std::string &preferred_ // Store the print/filament/printer presets at the same location as the upstream Slic3r. #endif ; + + PresetsConfigSubstitutions substitutions; try { - this->prints.load_presets(dir_user_presets, "print"); + this->prints.load_presets(dir_user_presets, "print", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->sla_prints.load_presets(dir_user_presets, "sla_print"); + this->sla_prints.load_presets(dir_user_presets, "sla_print", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->filaments.load_presets(dir_user_presets, "filament"); + this->filaments.load_presets(dir_user_presets, "filament", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->sla_materials.load_presets(dir_user_presets, "sla_material"); + this->sla_materials.load_presets(dir_user_presets, "sla_material", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->printers.load_presets(dir_user_presets, "printer"); + this->printers.load_presets(dir_user_presets, "printer", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->physical_printers.load_printers(dir_user_presets, "physical_printer"); + this->physical_printers.load_printers(dir_user_presets, "physical_printer", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } @@ -236,6 +238,8 @@ void PresetBundle::load_presets(AppConfig &config, const std::string &preferred_ throw Slic3r::RuntimeError(errors_cummulative); this->load_selections(config, preferred_model_id); + + return substitutions; } // Load system presets into this PresetBundle. @@ -255,13 +259,13 @@ std::string PresetBundle::load_system_presets() // Load the config bundle, flatten it. if (first) { // Reset this PresetBundle and load the first vendor config. - this->load_configbundle(dir_entry.path().string(), LOAD_CFGBNDLE_SYSTEM); + this->load_configbundle(dir_entry.path().string(), PresetBundle::LoadSystem); first = false; } else { // Load the other vendor configs, merge them with this PresetBundle. // Report duplicate profiles. PresetBundle other; - other.load_configbundle(dir_entry.path().string(), LOAD_CFGBNDLE_SYSTEM); + other.load_configbundle(dir_entry.path().string(), PresetBundle::LoadSystem); std::vector duplicates = this->merge_presets(std::move(other)); if (! duplicates.empty()) { errors_cummulative += "Vendor configuration file " + name + " contains the following presets with names used by other vendors: "; @@ -690,15 +694,15 @@ DynamicPrintConfig PresetBundle::full_sla_config() const // Instead of a config file, a G-code may be loaded containing the full set of parameters. // In the future the configuration will likely be read from an AMF file as well. // If the file is loaded successfully, its print / filament / printer profiles will be activated. -void PresetBundle::load_config_file(const std::string &path) +ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule) { if (is_gcode_file(path)) { DynamicPrintConfig config; config.apply(FullPrintConfig::defaults()); - config.load_from_gcode_file(path); + ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, true /* check_header */, compatibility_rule); Preset::normalize(config); load_config_file_config(path, true, std::move(config)); - return; + return config_substitutions; } // 1) Try to load the config file into a boost property tree. @@ -717,6 +721,7 @@ void PresetBundle::load_config_file(const std::string &path) // 2) Continue based on the type of the configuration file. ConfigFileType config_file_type = guess_config_file_type(tree); + ConfigSubstitutions config_substitutions; switch (config_file_type) { case CONFIG_FILE_TYPE_UNKNOWN: throw Slic3r::RuntimeError(std::string("Unknown configuration file type: ") + path); @@ -727,15 +732,18 @@ void PresetBundle::load_config_file(const std::string &path) // Initialize a config from full defaults. DynamicPrintConfig config; config.apply(FullPrintConfig::defaults()); - config.load(tree); + config_substitutions = config.load(tree, compatibility_rule); Preset::normalize(config); load_config_file_config(path, true, std::move(config)); - break; - } - case CONFIG_FILE_TYPE_CONFIG_BUNDLE: - load_config_file_config_bundle(path, tree); - break; + return config_substitutions; } + case CONFIG_FILE_TYPE_CONFIG_BUNDLE: + return load_config_file_config_bundle(path, tree); + } + + // This shall never happen. Suppres compiler warnings. + assert(false); + return ConfigSubstitutions{}; } // Load a config file from a boost property_tree. This is a private method called from load_config_file. @@ -907,16 +915,24 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } // Load the active configuration of a config bundle from a boost property_tree. This is a private method called from load_config_file. -void PresetBundle::load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree) +ConfigSubstitutions PresetBundle::load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree) { // 1) Load the config bundle into a temp data. PresetBundle tmp_bundle; - // Load the config bundle, don't save the loaded presets to user profile directory. - tmp_bundle.load_configbundle(path, 0); + // Load the config bundle, but don't save the loaded presets to user profile directory, as only the presets marked as active in the loaded preset bundle + // will be loaded into the master PresetBundle and activated. + auto [presets_substitutions, presets_imported] = tmp_bundle.load_configbundle(path, {}); + std::string bundle_name = std::string(" - ") + boost::filesystem::path(path).filename().string(); // 2) Extract active configs from the config bundle, copy them and activate them in this bundle. - auto load_one = [&path, &bundle_name](PresetCollection &collection_dst, PresetCollection &collection_src, const std::string &preset_name_src, bool activate) -> std::string { + ConfigSubstitutions config_substitutions; + auto load_one = [this, &path, &bundle_name, &presets_substitutions = presets_substitutions, &config_substitutions]( + PresetCollection &collection_dst, PresetCollection &collection_src, const std::string &preset_name_src, bool activate) -> std::string { + // If there are substitutions reported for this preset, move them to config_substitutions. + if (auto it = std::find_if(presets_substitutions.begin(), presets_substitutions.end(), [&preset_name_src](const PresetConfigSubstitutions& subs){ return subs.preset_name == preset_name_src; }); + it != presets_substitutions.end() && ! it->substitutions.empty()) + append(config_substitutions, std::move(it->substitutions)); Preset *preset_src = collection_src.find_preset(preset_name_src, false); Preset *preset_dst = collection_dst.find_preset(preset_name_src, false); assert(preset_src != nullptr); @@ -970,6 +986,9 @@ void PresetBundle::load_config_file_config_bundle(const std::string &path, const this->filament_presets[i] = load_one(this->filaments, tmp_bundle.filaments, tmp_bundle.filament_presets[i], false); this->update_compatible(PresetSelectCompatibleType::Never); + + sort_remove_duplicates(config_substitutions); + return std::move(config_substitutions); } // Process the Config Bundle loaded as a Boost property tree. @@ -1114,11 +1133,20 @@ static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree, co // Load a config bundle file, into presets and store the loaded presets into separate files // of the local configuration directory. -size_t PresetBundle::load_configbundle(const std::string &path, unsigned int flags) +std::pair PresetBundle::load_configbundle(const std::string &path, LoadConfigBundleAttributes flags) { - if (flags & (LOAD_CFGBNDLE_RESET_USER_PROFILE | LOAD_CFGBNDLE_SYSTEM)) - // Reset this bundle, delete user profile files if LOAD_CFGBNDLE_SAVE. - this->reset(flags & LOAD_CFGBNDLE_SAVE); + // Enable substitutions for user config bundle, throw an exception when loading a system profile. + ConfigSubstitutionContext substitution_context { + flags.has(LoadConfigBundleAttribute::LoadSystem) ? + ForwardCompatibilitySubstitutionRule::Disable : + ForwardCompatibilitySubstitutionRule::Enable + }; + + PresetsConfigSubstitutions substitutions; + + if (flags.has(LoadConfigBundleAttribute::ResetUserProfile) || flags.has(LoadConfigBundleAttribute::LoadSystem)) + // Reset this bundle, delete user profile files if SaveImported. + this->reset(flags.has(LoadConfigBundleAttribute::SaveImported)); // 1) Read the complete config file into a boost::property_tree. namespace pt = boost::property_tree; @@ -1131,25 +1159,24 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla } const VendorProfile *vendor_profile = nullptr; - if (flags & (LOAD_CFGBNDLE_SYSTEM | LOAD_CFGBUNDLE_VENDOR_ONLY)) { + if (flags.has(LoadConfigBundleAttribute::LoadSystem) || flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) { auto vp = VendorProfile::from_ini(tree, path); if (vp.models.size() == 0) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer model defined.") % path; - return 0; + return std::make_pair(PresetsConfigSubstitutions{}, 0); } else if (vp.num_variants() == 0) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer variant defined") % path; - return 0; + return std::make_pair(PresetsConfigSubstitutions{}, 0); } vendor_profile = &this->vendors.insert({vp.id, vp}).first->second; } - if (flags & LOAD_CFGBUNDLE_VENDOR_ONLY) { - return 0; - } + if (flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) + return std::make_pair(PresetsConfigSubstitutions{}, 0); // 1.5) Flatten the config bundle by applying the inheritance rules. Internal profiles (with names starting with '*') are removed. // If loading a user config bundle, do not flatten with the system profiles, but keep the "inherits" flag intact. - flatten_configbundle_hierarchy(tree, ((flags & LOAD_CFGBNDLE_SYSTEM) == 0) ? this : nullptr); + flatten_configbundle_hierarchy(tree, flags.has(LoadConfigBundleAttribute::LoadSystem) ? nullptr : this); // 2) Parse the property_tree, extract the active preset names and the profiles, save them into local config files. // Parse the obsolete preset names, to be deleted when upgrading from the old configuration structure. @@ -1246,7 +1273,8 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla DynamicPrintConfig config; std::string alias_name; std::vector renamed_from; - auto parse_config_section = [§ion, &alias_name, &renamed_from, &path](DynamicPrintConfig &config) { + auto parse_config_section = [§ion, &alias_name, &renamed_from, &substitution_context, &path](DynamicPrintConfig &config) { + substitution_context.substitutions.clear(); for (auto &kvp : section.second) { if (kvp.first == "alias") alias_name = kvp.second.data(); @@ -1256,7 +1284,8 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla section.first << "\" contains invalid \"renamed_from\" key, which is being ignored."; } } - config.set_deserialize(kvp.first, kvp.second.data()); + // Throws on parsing error. For system presets, no substituion is being done, but an exception is thrown. + config.set_deserialize(kvp.first, kvp.second.data(), substitution_context); } }; if (presets == &this->printers) { @@ -1277,7 +1306,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla if (! incorrect_keys.empty()) BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << section.first << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; - if ((flags & LOAD_CFGBNDLE_SYSTEM) && presets == &printers) { + if (flags.has(LoadConfigBundleAttribute::LoadSystem) && presets == &printers) { // Filter out printer presets, which are not mentioned in the vendor profile. // These presets are considered not installed. auto printer_model = config.opt_string("printer_model"); @@ -1312,7 +1341,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla section.first << "\" has already been loaded from another Confing Bundle."; continue; } - } else if ((flags & LOAD_CFGBNDLE_SYSTEM) == 0) { + } else if (! flags.has(LoadConfigBundleAttribute::LoadSystem)) { // This is a user config bundle. const Preset *existing = presets->find_preset(preset_name, false); if (existing != nullptr) { @@ -1341,9 +1370,9 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla / presets->section_name() / file_name).make_preferred(); // Load the preset into the list of presets, save it to disk. Preset &loaded = presets->load_preset(file_path.string(), preset_name, std::move(config), false); - if (flags & LOAD_CFGBNDLE_SAVE) + if (flags.has(LoadConfigBundleAttribute::SaveImported)) loaded.save(); - if (flags & LOAD_CFGBNDLE_SYSTEM) { + if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { loaded.is_system = true; loaded.vendor = vendor_profile; } @@ -1364,7 +1393,10 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla else loaded.alias = std::move(alias_name); loaded.renamed_from = std::move(renamed_from); - + if (! substitution_context.empty()) + substitutions.push_back({ + preset_name, presets->type(), PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); ++ presets_loaded; } @@ -1373,8 +1405,9 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla const DynamicPrintConfig& default_config = ph_printers->default_config(); DynamicPrintConfig config = default_config; + substitution_context.substitutions.clear(); for (auto& kvp : section.second) - config.set_deserialize(kvp.first, kvp.second.data()); + config.set_deserialize(kvp.first, kvp.second.data(), substitution_context); // Report configuration fields, which are misplaced into a wrong group. std::string incorrect_keys = Preset::remove_invalid_keys(config, default_config); @@ -1400,14 +1433,17 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla #endif / "physical_printer" / file_name).make_preferred(); // Load the preset into the list of presets, save it to disk. - ph_printers->load_printer(file_path.string(), ph_printer_name, std::move(config), false, flags & LOAD_CFGBNDLE_SAVE); - - ++ph_printers_loaded; + ph_printers->load_printer(file_path.string(), ph_printer_name, std::move(config), false, flags.has(LoadConfigBundleAttribute::SaveImported)); + if (! substitution_context.empty()) + substitutions.push_back({ + ph_printer_name, Preset::TYPE_PHYSICAL_PRINTER, PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); + ++ ph_printers_loaded; } } // 3) Activate the presets and physical printer if any exists. - if ((flags & LOAD_CFGBNDLE_SYSTEM) == 0) { + if (! flags.has(LoadConfigBundleAttribute::LoadSystem)) { if (! active_print.empty()) prints.select_preset_by_name(active_print, true); if (! active_sla_print.empty()) @@ -1427,7 +1463,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla this->update_compatible(PresetSelectCompatibleType::Never); } - return presets_loaded + ph_printers_loaded; + return std::make_pair(std::move(substitutions), presets_loaded + ph_printers_loaded); } void PresetBundle::update_multi_material_filament_presets() diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 5902d4208..9f75ba6c2 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -3,6 +3,7 @@ #include "Preset.hpp" #include "AppConfig.hpp" +#include "enum_bitmask.hpp" #include #include @@ -26,7 +27,7 @@ public: // Load ini files of all types (print, filament, printer) from Slic3r::data_dir() / presets. // Load selections (current print, current filaments, current printer) from config.ini - void load_presets(AppConfig &config, const std::string &preferred_model_id = std::string()); + PresetsConfigSubstitutions load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule rule, const std::string &preferred_model_id = std::string()); // Export selections (current print, current filaments, current printer) into config.ini void export_selections(AppConfig &config); @@ -82,24 +83,26 @@ public: // Instead of a config file, a G-code may be loaded containing the full set of parameters. // In the future the configuration will likely be read from an AMF file as well. // If the file is loaded successfully, its print / filament / printer profiles will be activated. - void load_config_file(const std::string &path); + ConfigSubstitutions load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule); // Load a config bundle file, into presets and store the loaded presets into separate files // of the local configuration directory. // Load settings into the provided settings instance. // Activate the presets stored in the config bundle. // Returns the number of presets loaded successfully. - enum { + enum LoadConfigBundleAttribute { // Save the profiles, which have been loaded. - LOAD_CFGBNDLE_SAVE = 1, + SaveImported, // Delete all old config profiles before loading. - LOAD_CFGBNDLE_RESET_USER_PROFILE = 2, + ResetUserProfile, // Load a system config bundle. - LOAD_CFGBNDLE_SYSTEM = 4, - LOAD_CFGBUNDLE_VENDOR_ONLY = 8, + LoadSystem, + LoadVendorOnly, }; - // Load the config bundle, store it to the user profile directory by default. - size_t load_configbundle(const std::string &path, unsigned int flags = LOAD_CFGBNDLE_SAVE); + using LoadConfigBundleAttributes = enum_bitmask; + // Load the config bundle based on the flags. + // Don't do any config substitutions when loading a system profile, perform and report substitutions otherwise. + std::pair load_configbundle(const std::string &path, LoadConfigBundleAttributes flags); // Export a config bundle file containing all the presets and the names of the active presets. void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); @@ -155,12 +158,14 @@ private: // and the external config is just referenced, not stored into user profile directory. // If it is not an external config, then the config will be stored into the user profile directory. void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config); - void load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree); + ConfigSubstitutions load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree); DynamicPrintConfig full_fff_config() const; DynamicPrintConfig full_sla_config() const; }; +ENABLE_ENUM_BITMASK_OPERATORS(PresetBundle::LoadConfigBundleAttribute) + } // namespace Slic3r #endif /* slic3r_PresetBundle_hpp_ */ diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5cd33c1cb..d8fc3083c 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -172,6 +172,13 @@ static const t_config_enum_values s_keys_map_BrimType = { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BrimType) +static const t_config_enum_values s_keys_map_ForwardCompatibilitySubstitutionRule = { + { "disable", ForwardCompatibilitySubstitutionRule::Disable }, + { "enable", ForwardCompatibilitySubstitutionRule::Enable }, + { "enable_silent", ForwardCompatibilitySubstitutionRule::EnableSilent } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) + static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology) { for (std::pair &kvp : options) @@ -3622,7 +3629,7 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } else if (opt_key == "bed_size" && !value.empty()) { opt_key = "bed_shape"; ConfigOptionPoint p; - p.deserialize(value); + p.deserialize(value, ForwardCompatibilitySubstitutionRule::Disable); std::ostringstream oss; oss << "0x0," << p.value(0) << "x0," << p.value(0) << "x" << p.value(1) << ",0x" << p.value(1); value = oss.str(); @@ -4171,6 +4178,20 @@ CLIMiscConfigDef::CLIMiscConfigDef() def->label = L("Ignore non-existent config files"); def->tooltip = L("Do not fail if a file supplied to --load does not exist."); + def = this->add("config_compatibility", coEnum); + def->label = L("Forward-compatibility rule when loading configurations from config files and project files (3MF, AMF)."); + def->tooltip = L("This version of PrusaSlicer may not understand configurations produced by newest PrusaSlicer versions. " + "For example, newer PrusaSlicer may extend the list of supported firmware flavors. One may decide to " + "bail out or to substitute an unknown value with a default silently or verbosely."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("disable"); + def->enum_values.push_back("enable"); + def->enum_values.push_back("enable_silent"); + def->enum_labels.push_back(L("Bail out on unknown configuration values")); + def->enum_labels.push_back(L("Enable reading unknown configuration values by verbosely substituting them with defaults.")); + def->enum_labels.push_back(L("Enable reading unknown configuration values by silently substituting them with defaults.")); + def->set_default_value(new ConfigOptionEnum(ForwardCompatibilitySubstitutionRule::Enable)); + def = this->add("load", coStrings); def->label = L("Load config file"); def->tooltip = L("Load configuration from the specified file. It can be used more than once to load options from multiple files."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f2b9901d6..890f8518e 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -141,6 +141,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SeamPosition) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLADisplayOrientation) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLAPillarConnectionMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BrimType) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS @@ -1086,8 +1087,8 @@ public: bool set_key_value(const std::string &opt_key, ConfigOption *opt) { bool out = m_data.set_key_value(opt_key, opt); this->touch(); return out; } template void set(const std::string &opt_key, T value) { m_data.set(opt_key, value, true); this->touch(); } - void set_deserialize(const t_config_option_key &opt_key, const std::string &str, bool append = false) - { m_data.set_deserialize(opt_key, str, append); this->touch(); } + void set_deserialize(const t_config_option_key &opt_key, const std::string &str, ConfigSubstitutionContext &substitution_context, bool append = false) + { m_data.set_deserialize(opt_key, str, substitution_context, append); this->touch(); } bool erase(const t_config_option_key &opt_key) { bool out = m_data.erase(opt_key); if (out) this->touch(); return out; } // Getters are thread safe. diff --git a/src/libslic3r/enum_bitmask.hpp b/src/libslic3r/enum_bitmask.hpp new file mode 100644 index 000000000..4c2076313 --- /dev/null +++ b/src/libslic3r/enum_bitmask.hpp @@ -0,0 +1,80 @@ +#ifndef slic3r_enum_bitmask_hpp_ +#define slic3r_enum_bitmask_hpp_ + +// enum_bitmask for passing a set of attributes to a function in a type safe way. +// Adapted from https://gpfault.net/posts/typesafe-bitmasks.txt.html +// with hints from https://www.strikerx3.dev/cpp/2019/02/27/typesafe-enum-class-bitmasks-in-cpp.html + +#include + +namespace Slic3r { + +// enum_bitmasks can only be used with enums. +template::value>::type> +class enum_bitmask { + // The type we'll use for storing the value of our bitmask should be the same as the enum's underlying type. + using underlying_type = typename std::underlying_type::type; + + // This method helps us avoid having to explicitly set enum values to powers of two. + static constexpr underlying_type mask_value(option_type o) { return 1 << static_cast(o); } + + // Private ctor to be used internally. + explicit constexpr enum_bitmask(underlying_type o) : m_bits(o) {} + +public: + // Default ctor creates a bitmask with no options selected. + constexpr enum_bitmask() : m_bits(0) {} + + // Creates a enum_bitmask with just one bit set. + // This ctor is intentionally non-explicit, to allow passing an options to a function: + // FunctionExpectingBitmask(Options::Opt1) + constexpr enum_bitmask(option_type o) : m_bits(mask_value(o)) {} + + // Set the bit corresponding to the given option. + constexpr enum_bitmask operator|(option_type t) { return enum_bitmask(m_bits | mask_value(t)); } + + // Combine with another enum_bitmask of the same type. + constexpr enum_bitmask operator|(enum_bitmask t) { return enum_bitmask(m_bits | t.m_bits); } + + // Get the value of the bit corresponding to the given option. + constexpr bool operator&(option_type t) { return m_bits & mask_value(t); } + constexpr bool has(option_type t) { return m_bits & mask_value(t); } + +private: + underlying_type m_bits = 0; +}; + +// For enabling free functions producing enum_bitmask<> type from bit operations on enums. +template struct is_enum_bitmask_type { static const bool enable = false; }; +#define ENABLE_ENUM_BITMASK_OPERATORS(x) template<> struct is_enum_bitmask_type { static const bool enable = true; }; +template inline constexpr bool is_enum_bitmask_type_v = is_enum_bitmask_type::enable; + +// Creates an enum_bitmask from two options, convenient for passing of options to a function: +// FunctionExpectingBitmask(Options::Opt1 | Options::Opt2 | Options::Opt3) +template +constexpr std::enable_if_t, enum_bitmask> operator|(option_type lhs, option_type rhs) { + static_assert(std::is_enum_v); + return enum_bitmask{lhs} | rhs; +} + +template +constexpr std::enable_if_t, enum_bitmask> operator|(option_type lhs, enum_bitmask rhs) { + static_assert(std::is_enum_v); + return enum_bitmask{lhs} | rhs; +} + +template +constexpr std::enable_if_t, enum_bitmask> only_if(bool condition, option_type opt) { + static_assert(std::is_enum_v); + return condition ? enum_bitmask{opt} : enum_bitmask{}; +} + +template +constexpr std::enable_if_t, enum_bitmask> only_if(bool condition, enum_bitmask opt) { + static_assert(std::is_enum_v); + return condition ? opt : enum_bitmask{}; +} + +} // namespace Slic3r + +#endif // slic3r_enum_bitmask_hpp_ diff --git a/src/libslic3r/pchheader.hpp b/src/libslic3r/pchheader.hpp index ad22b855a..e6591f574 100644 --- a/src/libslic3r/pchheader.hpp +++ b/src/libslic3r/pchheader.hpp @@ -115,6 +115,7 @@ #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" +#include "enum_bitmask.hpp" #include "format.hpp" #include "I18N.hpp" #include "MultiPoint.hpp" diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index ecfc32b58..56722173b 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -413,7 +413,7 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: ++ it; // Read the active config bundle, parse the config version. PresetBundle bundle; - bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); + bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly); for (const auto &vp : bundle.vendors) if (vp.second.id == cfg.name) cfg.version.config_version = vp.second.config_version; diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index bc66532a0..084af8b14 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -64,7 +64,9 @@ bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bu this->is_prusa_bundle = ais_prusa_bundle; std::string path_string = source_path.string(); - size_t presets_loaded = preset_bundle->load_configbundle(path_string, PresetBundle::LOAD_CFGBNDLE_SYSTEM); + auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle(path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem); + // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. + assert(config_substitutions.empty()); auto first_vendor = preset_bundle->vendors.begin(); if (first_vendor == preset_bundle->vendors.end()) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; @@ -2592,7 +2594,10 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese } } - preset_bundle->load_presets(*app_config, preferred_model); + // Reloading the configs after some modifications were done to PrusaSlicer.ini. + // Just perform the substitutions silently, as the substitutions were already presented to the user on application start-up + // and the Wizard shall not create any new values that would require substitution. + PresetsConfigSubstitutions substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, preferred_model); if (page_custom->custom_wanted()) { page_firmware->apply_custom_config(*custom_config); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 9cf71e07d..943124849 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -624,6 +624,13 @@ void GUI_App::post_init() this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); } else { + if (! this->init_params->preset_substitutions.empty()) { + // TODO: Add list of changes from all_substitutions + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } + #if 0 // Load the cummulative config over the currently active profiles. //FIXME if multiple configs are loaded, only the last one will have an effect. @@ -652,6 +659,24 @@ void GUI_App::post_init() if (! this->init_params->extra_config.empty()) this->mainframe->load_config(this->init_params->extra_config); } + + // The extra CallAfter() is needed because of Mac, where this is the only way + // to popup a modal dialog on start without screwing combo boxes. + // This is ugly but I honestly found no better way to do it. + // Neither wxShowEvent nor wxWindowCreateEvent work reliably. + if (this->preset_updater) { + this->check_updates(false); + CallAfter([this] { + this->config_wizard_startup(); + this->preset_updater->slic3r_update_notify(); + this->preset_updater->sync(preset_bundle); + }); + } + +#ifdef _WIN32 + // Sets window property to mainframe so other instances can indentify it. + OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); +#endif //WIN32 } IMPLEMENT_APP(GUI_App) @@ -885,7 +910,7 @@ bool GUI_App::on_init_inner() // Suppress the '- default -' presets. preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); try { - preset_bundle->load_presets(*app_config); + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); } catch (const std::exception &ex) { show_error(nullptr, ex.what()); } @@ -948,7 +973,6 @@ bool GUI_App::on_init_inner() if (! plater_) return; - if (app_config->dirty() && app_config->get("autosave") == "1") app_config->save(); @@ -969,33 +993,6 @@ bool GUI_App::on_init_inner() #endif this->post_init(); } - - // Preset updating & Configwizard are done after the above initializations, - // and after MainFrame is created & shown. - // The extra CallAfter() is needed because of Mac, where this is the only way - // to popup a modal dialog on start without screwing combo boxes. - // This is ugly but I honestly found no better way to do it. - // Neither wxShowEvent nor wxWindowCreateEvent work reliably. - - static bool once = true; - if (once) { - once = false; - - if (preset_updater != nullptr) { - check_updates(false); - - CallAfter([this] { - config_wizard_startup(); - preset_updater->slic3r_update_notify(); - preset_updater->sync(preset_bundle); - }); - } - -#ifdef _WIN32 - //sets window property to mainframe so other instances can indentify it - OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); -#endif //WIN32 - } }); m_initialized = true; @@ -1872,7 +1869,13 @@ void GUI_App::add_config_menu(wxMenuBar *menu) Config::SnapshotDB::singleton().take_snapshot(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK); try { app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); - preset_bundle->load_presets(*app_config); + if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); + ! all_substitutions.empty()) { + // TODO: + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } // Load the currently selected preset into the GUI, update the preset selection box. load_current_presets(); } catch (std::exception &ex) { diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 9579a030d..5cc8641ef 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -273,7 +273,7 @@ public: NotificationManager* notification_manager(); // Parameters extracted from the command line to be passed to GUI after initialization. - const GUI_InitParams* init_params { nullptr }; + GUI_InitParams* init_params { nullptr }; AppConfig* app_config{ nullptr }; PresetBundle* preset_bundle{ nullptr }; @@ -281,7 +281,7 @@ public: MainFrame* mainframe{ nullptr }; Plater* plater_{ nullptr }; - PresetUpdater* get_preset_updater() { return preset_updater; } + PresetUpdater* get_preset_updater() { return preset_updater; } wxBookCtrlBase* tab_panel() const ; int extruders_cnt() const; diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index 839782741..92223a767 100644 --- a/src/slic3r/GUI/GUI_Init.cpp +++ b/src/slic3r/GUI/GUI_Init.cpp @@ -50,39 +50,8 @@ int GUI_Run(GUI_InitParams ¶ms) // gui->autosave = m_config.opt_string("autosave"); GUI::GUI_App::SetInstance(gui); gui->init_params = ¶ms; -/* - gui->CallAfter([gui, this, &load_configs, params.start_as_gcodeviewer] { - if (!gui->initialized()) { - return; - } - if (params.start_as_gcodeviewer) { - if (!m_input_files.empty()) - gui->plater()->load_gcode(wxString::FromUTF8(m_input_files[0].c_str())); - } else { -#if 0 - // Load the cummulative config over the currently active profiles. - //FIXME if multiple configs are loaded, only the last one will have an effect. - // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). - // As of now only the full configs are supported here. - if (!m_print_config.empty()) - gui->mainframe->load_config(m_print_config); -#endif - if (!load_configs.empty()) - // Load the last config to give it a name at the UI. The name of the preset may be later - // changed by loading an AMF or 3MF. - //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. - gui->mainframe->load_config_file(load_configs.back()); - // If loading a 3MF file, the config is loaded from the last one. - if (!m_input_files.empty()) - gui->plater()->load_files(m_input_files, true, true); - if (!m_extra_config.empty()) - gui->mainframe->load_config(m_extra_config); - } - }); -*/ - int result = wxEntry(params.argc, params.argv); - return result; + return wxEntry(params.argc, params.argv); } catch (const Slic3r::Exception &ex) { boost::nowide::cerr << ex.what() << std::endl; wxMessageBox(boost::nowide::widen(ex.what()), _L("PrusaSlicer GUI initialization failed"), wxICON_STOP); diff --git a/src/slic3r/GUI/GUI_Init.hpp b/src/slic3r/GUI/GUI_Init.hpp index c420c9554..2adf618a4 100644 --- a/src/slic3r/GUI/GUI_Init.hpp +++ b/src/slic3r/GUI/GUI_Init.hpp @@ -1,6 +1,7 @@ #ifndef slic3r_GUI_Init_hpp_ #define slic3r_GUI_Init_hpp_ +#include #include namespace Slic3r { @@ -12,6 +13,9 @@ struct GUI_InitParams int argc; char **argv; + // Substitutions of unknown configuration values done during loading of user presets. + PresetsConfigSubstitutions preset_substitutions; + std::vector load_configs; DynamicPrintConfig extra_config; std::vector input_files; diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index 9998f42c7..f6e3976ea 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -140,22 +140,27 @@ void SLAImportJob::process() if (p->path.empty()) return; std::string path = p->path.ToUTF8().data(); + ConfigSubstitutions config_substitutions; try { switch (p->sel) { case Sel::modelAndProfile: - import_sla_archive(path, p->win, p->mesh, p->profile, progr); + config_substitutions = import_sla_archive(path, p->win, p->mesh, p->profile, progr); break; case Sel::modelOnly: - import_sla_archive(path, p->win, p->mesh, progr); + config_substitutions = import_sla_archive(path, p->win, p->mesh, progr); break; case Sel::profileOnly: - import_sla_archive(path, p->profile); + config_substitutions = import_sla_archive(path, p->profile); break; } } catch (std::exception &ex) { p->err = ex.what(); } + + if (! config_substitutions.empty()) { + //FIXME Add reporting here "Loading profiles found following incompatibilities." + } update_status(100, was_canceled() ? _(L("Importing canceled.")) : _(L("Importing done."))); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 9c7533acd..389782a8f 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1518,6 +1518,7 @@ void MainFrame::update_menubar() m_changeable_menu_items[miPrinterTab] ->SetBitmap(create_menu_bitmap(is_fff ? "printer" : "sla_printer")); } +#if 0 // To perform the "Quck Slice", "Quick Slice and Save As", "Repeat last Quick Slice" and "Slice to SVG". void MainFrame::quick_slice(const int qs) { @@ -1643,6 +1644,7 @@ void MainFrame::quick_slice(const int qs) // }; // Slic3r::GUI::catch_error(this, []() { if (m_progress_dialog) m_progress_dialog->Destroy(); }); } +#endif void MainFrame::reslice_now() { @@ -1729,7 +1731,13 @@ void MainFrame::load_config_file() bool MainFrame::load_config_file(const std::string &path) { try { - wxGetApp().preset_bundle->load_config_file(path); + ConfigSubstitutions config_substitutions = wxGetApp().preset_bundle->load_config_file(path, ForwardCompatibilitySubstitutionRule::Enable); + if (! config_substitutions.empty()) { + // TODO: Add list of changes from all_substitutions + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } } catch (const std::exception &ex) { show_error(this, ex.what()); return false; @@ -1793,14 +1801,22 @@ void MainFrame::load_configbundle(wxString file/* = wxEmptyString, const bool re wxGetApp().app_config->update_config_dir(get_dir_name(file)); - auto presets_imported = 0; + size_t presets_imported = 0; + PresetsConfigSubstitutions config_substitutions; try { - presets_imported = wxGetApp().preset_bundle->load_configbundle(file.ToUTF8().data()); + std::tie(config_substitutions, presets_imported) = wxGetApp().preset_bundle->load_configbundle(file.ToUTF8().data(), PresetBundle::LoadConfigBundleAttribute::SaveImported); } catch (const std::exception &ex) { show_error(this, ex.what()); return; } + if (! config_substitutions.empty()) { + // TODO: Add list of changes from all_substitutions + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } + // Load the currently selected preset into the GUI, update the preset selection box. wxGetApp().load_current_presets(); diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 40fff23da..94779c163 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -170,7 +170,7 @@ public: bool is_last_input_file() const { return !m_qs_last_input_file.IsEmpty(); } bool is_dlg_layout() const { return m_layout == ESettingsLayout::Dlg; } - void quick_slice(const int qs = qsUndef); +// void quick_slice(const int qs = qsUndef); void reslice_now(); void repair_stl(); void export_config(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 13e866965..0c34f21dd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2237,7 +2237,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ DynamicPrintConfig config; { DynamicPrintConfig config_loaded; - model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, false, load_config); + ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::Enable }; + model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, only_if(load_config, Model::LoadAttribute::CheckVersion)); if (load_config && !config_loaded.empty()) { // Based on the printer technology field found in the loaded config, select the base for the config, PrinterTechnology printer_technology = Preset::printer_technology(config_loaded); @@ -2261,6 +2262,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ // and place the loaded config over the base. config += std::move(config_loaded); } + if (! config_substitutions.empty()) { + // TODO: + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } this->model.custom_gcode_per_print_z = model.custom_gcode_per_print_z; } @@ -2330,7 +2337,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } else { - model = Slic3r::Model::read_from_file(path.string(), nullptr, false, load_config); + model = Slic3r::Model::read_from_file(path.string(), nullptr, nullptr, only_if(load_config, Model::LoadAttribute::CheckVersion)); for (auto obj : model.objects) if (obj->name.empty()) obj->name = fs::path(obj->input_file).filename().string(); @@ -3215,7 +3222,7 @@ void Plater::priv::replace_with_stl() Model new_model; try { - new_model = Model::read_from_file(path, nullptr, true, false); + new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); @@ -3388,7 +3395,7 @@ void Plater::priv::reload_from_disk() Model new_model; try { - new_model = Model::read_from_file(path, nullptr, true, false); + new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); @@ -4599,7 +4606,9 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator // Switch to the other printer technology. Switch to the last printer active for that particular technology. AppConfig *app_config = wxGetApp().app_config; app_config->set("presets", "printer", (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name); - wxGetApp().preset_bundle->load_presets(*app_config); + //FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive. + // Anyways, don't report any config value substitutions, they have been already reported to the user at application start up. + wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); // load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> Object_list::update_and_show_object_settings_item(), // but the Object list still keeps pointer to the old Model. Avoid a crash by removing selection first. this->sidebar->obj_list()->unselect_objects(); diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index 92eab9307..e2970acf1 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -379,7 +379,8 @@ void fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx) // PresetBundle bundle; on_progress(L("Loading repaired model"), 80); DynamicPrintConfig config; - bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), &config, &model, false); + ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::EnableSilent }; + bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), config, config_substitutions, &model, false); boost::filesystem::remove(path_dst); if (! loaded) throw Slic3r::RuntimeError(L("Import of the repaired 3mf file failed")); diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index f0310073f..ae8f2abb5 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -612,7 +612,7 @@ void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons update.install(); PresetBundle bundle; - bundle.load_configbundle(update.source.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); + bundle.load_configbundle(update.source.string(), PresetBundle::LoadConfigBundleAttribute::LoadSystem); BOOST_LOG_TRIVIAL(info) << format("Deleting %1% conflicting presets", bundle.prints.size() + bundle.filaments.size() + bundle.printers.size()); @@ -710,6 +710,17 @@ void PresetUpdater::slic3r_update_notify() } } +static void reload_configs_update_gui() +{ + // Reload global configuration + auto* app_config = GUI::wxGetApp().app_config; + // System profiles should not trigger any substitutions, user profiles may trigger substitutions, but these substitutions + // were already presented to the user on application start up. Just do substitutions now and keep quiet about it. + GUI::wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); + GUI::wxGetApp().load_current_presets(); + GUI::wxGetApp().plater()->set_bed_shape(); +} + PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, bool no_notification) const { if (! p->enabled_config_update) { return R_NOOP; } @@ -767,7 +778,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 } //forced update - if(incompatible_version) + if (incompatible_version) { BOOST_LOG_TRIVIAL(info) << format("Update of %1% bundles available. At least one requires higher version of Slicer.", updates.updates.size()); @@ -782,14 +793,8 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 const auto res = dlg.ShowModal(); if (res == wxID_OK) { BOOST_LOG_TRIVIAL(info) << "User wants to update..."; - p->perform_updates(std::move(updates)); - - // Reload global configuration - auto* app_config = GUI::wxGetApp().app_config; - GUI::wxGetApp().preset_bundle->load_presets(*app_config); - GUI::wxGetApp().load_current_presets(); - GUI::wxGetApp().plater()->set_bed_shape(); + reload_configs_update_gui(); return R_UPDATE_INSTALLED; } else { @@ -814,11 +819,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; p->perform_updates(std::move(updates)); - - // Reload global configuration - auto* app_config = GUI::wxGetApp().app_config; - GUI::wxGetApp().preset_bundle->load_presets(*app_config); - GUI::wxGetApp().load_current_presets(); + reload_configs_update_gui(); return R_UPDATE_INSTALLED; } else { @@ -871,11 +872,7 @@ void PresetUpdater::on_update_notification_confirm() if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; p->perform_updates(std::move(p->waiting_updates)); - - // Reload global configuration - auto* app_config = GUI::wxGetApp().app_config; - GUI::wxGetApp().preset_bundle->load_presets(*app_config); - GUI::wxGetApp().load_current_presets(); + reload_configs_update_gui(); p->has_waiting_updates = false; //return R_UPDATE_INSTALLED; } diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_data.cpp index d55f9f061..f5424dfd9 100644 --- a/tests/fff_print/test_data.cpp +++ b/tests/fff_print/test_data.cpp @@ -200,14 +200,14 @@ void init_print(std::initializer_list input_meshes, Slic3r::Print void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list config_items, bool comments) { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize(config_items); + config.set_deserialize_strict(config_items); init_print(meshes, print, model, config, comments); } void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list config_items, bool comments) { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize(config_items); + config.set_deserialize_strict(config_items); init_print(meshes, print, model, config, comments); } diff --git a/tests/fff_print/test_flow.cpp b/tests/fff_print/test_flow.cpp index dc73f4b6e..81f748e19 100644 --- a/tests/fff_print/test_flow.cpp +++ b/tests/fff_print/test_flow.cpp @@ -19,7 +19,7 @@ SCENARIO("Extrusion width specifics", "[Flow]") { GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer and a 20mm cube mesh") { // this is a sharedptr DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize({ + config.set_deserialize_strict({ { "brim_width", 2 }, { "skirts", 1 }, { "perimeters", 3 }, diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 9ee319c94..a4c8aa09a 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -10,7 +10,7 @@ SCENARIO("lift() is not ignored after unlift() at normal values of Z", "[GCodeWr GIVEN("A config from a file and a single extruder.") { GCodeWriter writer; GCodeConfig &config = writer.config; - config.load(std::string(TEST_DATA_DIR) + "/fff_print_tests/test_gcodewriter/config_lift_unlift.ini"); + config.load(std::string(TEST_DATA_DIR) + "/fff_print_tests/test_gcodewriter/config_lift_unlift.ini", ForwardCompatibilitySubstitutionRule::Disable); std::vector extruder_ids {0}; writer.set_extruders(extruder_ids); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index fc2ac3dee..a139e4c2b 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -50,7 +50,7 @@ SCENARIO("Print: Skirt generation", "[Print]") { SCENARIO("Print: Changing number of solid surfaces does not cause all surfaces to become internal.", "[Print]") { GIVEN("sliced 20mm cube and config with top_solid_surfaces = 2 and bottom_solid_surfaces = 1") { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize({ + config.set_deserialize_strict({ { "top_solid_layers", 2 }, { "bottom_solid_layers", 1 }, { "layer_height", 0.25 }, // get a known number of layers diff --git a/tests/fff_print/test_printgcode.cpp b/tests/fff_print/test_printgcode.cpp index d6f6eca58..2a45bd200 100644 --- a/tests/fff_print/test_printgcode.cpp +++ b/tests/fff_print/test_printgcode.cpp @@ -224,7 +224,7 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode]") { { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_num_extruders(4); - config.set_deserialize({ + config.set_deserialize_strict({ { "start_gcode", "; Extruder [current_extruder]" }, { "infill_extruder", 2 }, { "solid_infill_extruder", 2 }, diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 097f72dcc..8f508f323 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -31,7 +31,7 @@ static int get_brim_tool(const std::string &gcode) TEST_CASE("Skirt height is honored", "[Skirt]") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 1 }, { "skirt_height", 5 }, { "perimeters", 0 }, @@ -64,7 +64,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { GIVEN("A default configuration") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_num_extruders(4); - config.set_deserialize({ + config.set_deserialize_strict({ { "support_material_speed", 99 }, { "first_layer_height", 0.3 }, { "gcode_comments", true }, @@ -78,7 +78,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { }); WHEN("Brim width is set to 5") { - config.set_deserialize({ + config.set_deserialize_strict({ { "perimeters", 0 }, { "skirts", 0 }, { "brim_width", 5 } @@ -100,7 +100,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { } WHEN("Skirt area is smaller than the brim") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 1 }, { "brim_width", 10} }); @@ -110,7 +110,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { } WHEN("Skirt height is 0 and skirts > 0") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 2 }, { "skirt_height", 0 } }); @@ -123,7 +123,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { // This is a real error! One shall print the brim with the external perimeter extruder! WHEN("Perimeter extruder = 2 and support extruders = 3") { THEN("Brim is printed with the extruder used for the perimeters of first object") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "brim_width", 5 }, { "perimeter_extruder", 2 }, @@ -137,7 +137,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { } WHEN("Perimeter extruder = 2, support extruders = 3, raft is enabled") { THEN("brim is printed with same extruder as skirt") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "brim_width", 5 }, { "perimeter_extruder", 2 }, @@ -153,7 +153,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { #endif WHEN("brim width to 1 with layer_width of 0.5") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "first_layer_extrusion_width", 0.5 }, { "brim_width", 1 } @@ -167,7 +167,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { #if 0 WHEN("brim ears on a square") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "first_layer_extrusion_width", 0.5 }, { "brim_width", 1 }, @@ -182,7 +182,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { } WHEN("brim ears on a square but with a too small max angle") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "first_layer_extrusion_width", 0.5 }, { "brim_width", 1 }, @@ -198,7 +198,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { #endif WHEN("Object is plated with overhang support and a brim") { - config.set_deserialize({ + config.set_deserialize_strict({ { "layer_height", 0.4 }, { "first_layer_height", 0.4 }, { "skirts", 1 }, diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index d0f459e4d..5ab000d04 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -14,7 +14,8 @@ SCENARIO("Reading 3mf file", "[3mf]") { WHEN("3mf model is read") { std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf"; DynamicPrintConfig config; - bool ret = load_3mf(path.c_str(), &config, &model, false); + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; + bool ret = load_3mf(path.c_str(), config, ctxt, &model, false); THEN("load should succeed") { REQUIRE(ret); } @@ -56,7 +57,10 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") { // load back the model from the 3mf file Model dst_model; DynamicPrintConfig dst_config; - load_3mf(test_file.c_str(), &dst_config, &dst_model, false); + { + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; + load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false); + } boost::filesystem::remove(test_file); // compare meshes diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 417a29dca..7fbf31b11 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -9,7 +9,7 @@ SCENARIO("Generic config validation performs as expected.", "[Config]") { GIVEN("A config generated from default options") { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); WHEN( "perimeter_extrusion_width is set to 250%, a valid value") { - config.set_deserialize("perimeter_extrusion_width", "250%"); + config.set_deserialize_strict("perimeter_extrusion_width", "250%"); THEN( "The config is read as valid.") { REQUIRE(config.validate().empty()); } @@ -40,7 +40,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A boolean option is set to a string value representing a 0 or 1") { - CHECK_NOTHROW(config.set_deserialize("gcode_comments", "1")); + CHECK_NOTHROW(config.set_deserialize_strict("gcode_comments", "1")); THEN("The underlying value is set correctly.") { REQUIRE(config.opt("gcode_comments")->getBool() == true); } @@ -59,7 +59,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A numeric option is set from serialized string") { - config.set_deserialize("bed_temperature", "100"); + config.set_deserialize_strict("bed_temperature", "100"); THEN("The underlying value is set correctly.") { REQUIRE(config.opt("bed_temperature")->get_at(0) == 100); } @@ -92,7 +92,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } WHEN("A numeric option is set to a non-numeric value.") { THEN("A BadOptionTypeException exception is thown.") { - REQUIRE_THROWS_AS(config.set_deserialize("perimeter_speed", "zzzz"), BadOptionTypeException); + REQUIRE_THROWS_AS(config.set_deserialize_strict("perimeter_speed", "zzzz"), BadOptionTypeException); } THEN("The value does not change.") { REQUIRE(config.opt("perimeter_speed")->getFloat() == 60.0); @@ -117,7 +117,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A float or percent is set as a percent through the string interface.") { - config.set_deserialize("first_layer_extrusion_width", "100%"); + config.set_deserialize_strict("first_layer_extrusion_width", "100%"); THEN("Value and percent flag are 100/true") { auto tmp = config.opt("first_layer_extrusion_width"); REQUIRE(tmp->percent == true); @@ -125,7 +125,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A float or percent is set as a float through the string interface.") { - config.set_deserialize("first_layer_extrusion_width", "100"); + config.set_deserialize_strict("first_layer_extrusion_width", "100"); THEN("Value and percent flag are 100/false") { auto tmp = config.opt("first_layer_extrusion_width"); REQUIRE(tmp->percent == false); @@ -195,7 +195,7 @@ SCENARIO("Config ini load/save interface", "[Config]") { WHEN("new_from_ini is called") { Slic3r::DynamicPrintConfig config; std::string path = std::string(TEST_DATA_DIR) + "/test_config/new_from_ini.ini"; - config.load_from_ini(path); + config.load_from_ini(path, ForwardCompatibilitySubstitutionRule::Disable); THEN("Config object contains ini file options.") { REQUIRE(config.option_throw("filament_colour", false)->values.size() == 1); REQUIRE(config.option_throw("filament_colour", false)->values.front() == "#ABCD"); diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp index 8c56afc6d..59784e940 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -9,7 +9,7 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { PlaceholderParser parser; auto config = DynamicPrintConfig::full_print_config(); - config.set_deserialize( { + config.set_deserialize_strict( { { "printer_notes", " PRINTER_VENDOR_PRUSA3D PRINTER_MODEL_MK2 " }, { "nozzle_diameter", "0.6;0.6;0.6;0.6" }, { "temperature", "357;359;363;378" } diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp index aec6ceb6a..20288243e 100644 --- a/xs/src/perlglue.cpp +++ b/xs/src/perlglue.cpp @@ -282,7 +282,7 @@ bool ConfigBase__set(ConfigBase* THIS, const t_config_option_key &opt_key, SV* v break; } default: - if (! opt->deserialize(std::string(SvPV_nolen(value)))) + if (! opt->deserialize(std::string(SvPV_nolen(value)), ForwardCompatibilitySubstitutionRule::Disable)) return false; } return true; @@ -295,7 +295,8 @@ bool ConfigBase__set_deserialize(ConfigBase* THIS, const t_config_option_key &op size_t len; const char * c = SvPV(str, len); std::string value(c, len); - return THIS->set_deserialize_nothrow(opt_key, value); + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; + return THIS->set_deserialize_nothrow(opt_key, value, ctxt); } void ConfigBase__set_ifndef(ConfigBase* THIS, const t_config_option_key &opt_key, SV* value, bool deserialize) diff --git a/xs/xsp/Config.xsp b/xs/xsp/Config.xsp index 52a5e7d83..703427035 100644 --- a/xs/xsp/Config.xsp +++ b/xs/xsp/Config.xsp @@ -55,7 +55,7 @@ %code%{ auto config = new DynamicPrintConfig(); try { - config->load(path); + config->load(path, ForwardCompatibilitySubstitutionRule::Disable); RETVAL = config; } catch (std::exception& e) { delete config; @@ -119,7 +119,7 @@ %code%{ auto config = new FullPrintConfig(); try { - config->load(path); + config->load(path, ForwardCompatibilitySubstitutionRule::Disable); RETVAL = static_cast(config); } catch (std::exception& e) { delete config; diff --git a/xs/xsp/Flow.xsp b/xs/xsp/Flow.xsp index 019af16f2..3056b4001 100644 --- a/xs/xsp/Flow.xsp +++ b/xs/xsp/Flow.xsp @@ -30,7 +30,7 @@ _new_from_width(CLASS, role, width, nozzle_diameter, height) float height; CODE: ConfigOptionFloatOrPercent optwidth; - optwidth.deserialize(width); + optwidth.deserialize(width, ForwardCompatibilitySubstitutionRule::Disable); RETVAL = new Flow(Flow::new_from_config_width(role, optwidth, nozzle_diameter, height)); OUTPUT: RETVAL diff --git a/xs/xsp/Model.xsp b/xs/xsp/Model.xsp index 93067ebe3..e7a171efd 100644 --- a/xs/xsp/Model.xsp +++ b/xs/xsp/Model.xsp @@ -22,7 +22,7 @@ %name{read_from_file} Model(std::string input_file, bool add_default_instances = true) %code%{ try { - RETVAL = new Model(Model::read_from_file(input_file, nullptr, add_default_instances)); + RETVAL = new Model(Model::read_from_file(input_file, nullptr, nullptr, only_if(add_default_instances, Model::LoadAttribute::AddDefaultInstances))); } catch (std::exception& e) { croak("Error while opening %s: %s\n", input_file.c_str(), e.what()); } From e4e8c5df129b1f90cca148c03e10d8a4e2126da8 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Sun, 27 Jun 2021 17:36:25 +0200 Subject: [PATCH 09/37] As an example of using the enum_bitmask infrastructure for type safe sets of optional boolean parameters, the cut function "keep upper", "keep lower" and "flip lower" boolean parameters were converted into a single type safe enum_bitmask. Such a coding style is certainly wordier than the original code, but much safer and more readable than the error prone "boolean, boolean, boolean" function call parameter list. --- src/PrusaSlicer.cpp | 2 +- src/libslic3r/Model.cpp | 35 +++++++++++++++------------- src/libslic3r/Model.hpp | 7 +++++- src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 6 ++++- src/slic3r/GUI/Plater.cpp | 9 +++---- src/slic3r/GUI/Plater.hpp | 5 +++- 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 5212d66c8..3f43c3ebd 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -376,7 +376,7 @@ int CLI::run(int argc, char **argv) o->cut(Z, m_config.opt_float("cut"), &out); } #else - model.objects.front()->cut(0, m_config.opt_float("cut"), true, true, true); + model.objects.front()->cut(0, m_config.opt_float("cut"), ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::FlipLower); #endif model.delete_object(size_t(0)); } diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 5943f960e..3ddf65333 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1143,17 +1143,18 @@ bool ModelObject::needed_repair() const return false; } -ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, bool keep_lower, bool rotate_lower) +ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttributes attributes) { - if (!keep_upper && !keep_lower) { return {}; } + if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) + return {}; BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - start"; // Clone the object to duplicate instances, materials etc. - ModelObject* upper = keep_upper ? ModelObject::new_clone(*this) : nullptr; - ModelObject* lower = keep_lower ? ModelObject::new_clone(*this) : nullptr; + ModelObject* upper = attributes.has(ModelObjectCutAttribute::KeepUpper) ? ModelObject::new_clone(*this) : nullptr; + ModelObject* lower = attributes.has(ModelObjectCutAttribute::KeepLower) ? ModelObject::new_clone(*this) : nullptr; - if (keep_upper) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { upper->set_model(nullptr); upper->sla_support_points.clear(); upper->sla_drain_holes.clear(); @@ -1162,7 +1163,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b upper->input_file.clear(); } - if (keep_lower) { + if (attributes.has(ModelObjectCutAttribute::KeepLower)) { lower->set_model(nullptr); lower->sla_support_points.clear(); lower->sla_drain_holes.clear(); @@ -1202,8 +1203,10 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b volume->set_transformation(Geometry::Transformation(instance_matrix * volume_matrix)); - if (keep_upper) { upper->add_volume(*volume); } - if (keep_lower) { lower->add_volume(*volume); } + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + upper->add_volume(*volume); + if (attributes.has(ModelObjectCutAttribute::KeepLower)) + lower->add_volume(*volume); } else if (! volume->mesh().empty()) { @@ -1223,19 +1226,19 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b indexed_triangle_set upper_its, lower_its; mesh.require_shared_vertices(); cut_mesh(mesh.its, float(z), &upper_its, &lower_its); - if (keep_upper) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { upper_mesh = TriangleMesh(upper_its); upper_mesh.repair(); upper_mesh.reset_repair_stats(); } - if (keep_lower) { + if (attributes.has(ModelObjectCutAttribute::KeepLower)) { lower_mesh = TriangleMesh(lower_its); lower_mesh.repair(); lower_mesh.reset_repair_stats(); } } - if (keep_upper && upper_mesh.facets_count() > 0) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && upper_mesh.facets_count() > 0) { ModelVolume* vol = upper->add_volume(upper_mesh); vol->name = volume->name; // Don't copy the config's ID. @@ -1244,7 +1247,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b assert(vol->config.id() != volume->config.id()); vol->set_material(volume->material_id(), *volume->material()); } - if (keep_lower && lower_mesh.facets_count() > 0) { + if (attributes.has(ModelObjectCutAttribute::KeepLower) && lower_mesh.facets_count() > 0) { ModelVolume* vol = lower->add_volume(lower_mesh); vol->name = volume->name; // Don't copy the config's ID. @@ -1255,7 +1258,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b // Compute the lower part instances' bounding boxes to figure out where to place // the upper part - if (keep_upper) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { for (size_t i = 0; i < instances.size(); i++) { lower_bboxes[i].merge(instances[i]->transform_mesh_bounding_box(lower_mesh, true)); } @@ -1266,7 +1269,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b ModelObjectPtrs res; - if (keep_upper && upper->volumes.size() > 0) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && upper->volumes.size() > 0) { upper->invalidate_bounding_box(); upper->center_around_origin(); @@ -1286,7 +1289,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b res.push_back(upper); } - if (keep_lower && lower->volumes.size() > 0) { + if (attributes.has(ModelObjectCutAttribute::KeepLower) && lower->volumes.size() > 0) { lower->invalidate_bounding_box(); lower->center_around_origin(); @@ -1297,7 +1300,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b instance->set_transformation(Geometry::Transformation()); instance->set_offset(offset); - instance->set_rotation(Vec3d(rotate_lower ? Geometry::deg2rad(180.0) : 0.0, 0.0, rot_z)); + instance->set_rotation(Vec3d(attributes.has(ModelObjectCutAttribute::FlipLower) ? Geometry::deg2rad(180.0) : 0.0, 0.0, rot_z)); } res.push_back(lower); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index f835aa0ea..59ab14b4c 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -2,6 +2,7 @@ #define slic3r_Model_hpp_ #include "libslic3r.h" +#include "enum_bitmask.hpp" #include "Geometry.hpp" #include "ObjectID.hpp" #include "Point.hpp" @@ -227,6 +228,10 @@ enum class ModelVolumeType : int { SUPPORT_ENFORCER, }; +enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, FlipLower }; +using ModelObjectCutAttributes = enum_bitmask; +ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute); + // A printable object, possibly having multiple print volumes (each with its own set of parameters and materials), // and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials. // Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed, @@ -345,7 +350,7 @@ public: size_t materials_count() const; size_t facets_count() const; bool needed_repair() const; - ModelObjectPtrs cut(size_t instance, coordf_t z, bool keep_upper = true, bool keep_lower = true, bool rotate_lower = false); // Note: z is in world coordinates + ModelObjectPtrs cut(size_t instance, coordf_t z, ModelObjectCutAttributes attributes); void split(ModelObjectPtrs* new_objects); void merge(); // Support for non-uniform scaling of instances. If an instance is rotated by angles, which are not multiples of ninety degrees, diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index f0813732f..9499bd6de 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -15,6 +15,7 @@ #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GUI_ObjectManipulation.hpp" #include "libslic3r/AppConfig.hpp" +#include "libslic3r/Model.hpp" namespace Slic3r { @@ -231,7 +232,10 @@ void GLGizmoCut::perform_cut(const Selection& selection) coordf_t object_cut_z = m_cut_z - first_glvolume->get_sla_shift_z(); if (object_cut_z > 0.) - wxGetApp().plater()->cut(object_idx, instance_idx, object_cut_z, m_keep_upper, m_keep_lower, m_rotate_lower); + wxGetApp().plater()->cut(object_idx, instance_idx, object_cut_z, + only_if(m_keep_upper, ModelObjectCutAttribute::KeepUpper) | + only_if(m_keep_lower, ModelObjectCutAttribute::KeepLower) | + only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower)); else { // the object is SLA-elevated and the plane is under it. } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0c34f21dd..7460b43dd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5315,21 +5315,20 @@ void Plater::toggle_layers_editing(bool enable) wxPostEvent(canvas3D()->get_wxglcanvas(), SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); } -void Plater::cut(size_t obj_idx, size_t instance_idx, coordf_t z, bool keep_upper, bool keep_lower, bool rotate_lower) +void Plater::cut(size_t obj_idx, size_t instance_idx, coordf_t z, ModelObjectCutAttributes attributes) { wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); auto *object = p->model.objects[obj_idx]; wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); - if (!keep_upper && !keep_lower) { + if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) return; - } Plater::TakeSnapshot snapshot(this, _L("Cut by Plane")); wxBusyCursor wait; - const auto new_objects = object->cut(instance_idx, z, keep_upper, keep_lower, rotate_lower); + const auto new_objects = object->cut(instance_idx, z, attributes); remove(obj_idx); p->load_model_objects(new_objects); @@ -5337,9 +5336,7 @@ void Plater::cut(size_t obj_idx, size_t instance_idx, coordf_t z, bool keep_uppe Selection& selection = p->get_selection(); size_t last_id = p->model.objects.size() - 1; for (size_t i = 0; i < new_objects.size(); ++i) - { selection.add_object((unsigned int)(last_id - i), i == 0); - } } void Plater::export_gcode(bool prefer_removable) diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 49bc952b8..6f69ee4dc 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -9,6 +9,7 @@ #include "Selection.hpp" +#include "libslic3r/enum_bitmask.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -24,6 +25,8 @@ namespace Slic3r { class Model; class ModelObject; +enum class ModelObjectCutAttribute : int; +using ModelObjectCutAttributes = enum_bitmask; class ModelInstance; class Print; class SLAPrint; @@ -204,7 +207,7 @@ public: void convert_unit(ConversionType conv_type); void toggle_layers_editing(bool enable); - void cut(size_t obj_idx, size_t instance_idx, coordf_t z, bool keep_upper = true, bool keep_lower = true, bool rotate_lower = false); + void cut(size_t obj_idx, size_t instance_idx, coordf_t z, ModelObjectCutAttributes attributes); void export_gcode(bool prefer_removable); void export_stl(bool extended = false, bool selection_only = false); From 211110ce53d48c9acdf266e30a98fc0152cae26f Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Sun, 27 Jun 2021 17:45:41 +0200 Subject: [PATCH 10/37] Fixing some compiler warnings. --- src/libslic3r/PresetBundle.cpp | 5 +++-- src/libslic3r/utils.cpp | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index fc7987b29..6f0b2e4d3 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -922,12 +922,13 @@ ConfigSubstitutions PresetBundle::load_config_file_config_bundle(const std::stri // Load the config bundle, but don't save the loaded presets to user profile directory, as only the presets marked as active in the loaded preset bundle // will be loaded into the master PresetBundle and activated. auto [presets_substitutions, presets_imported] = tmp_bundle.load_configbundle(path, {}); + UNUSED(presets_imported); std::string bundle_name = std::string(" - ") + boost::filesystem::path(path).filename().string(); // 2) Extract active configs from the config bundle, copy them and activate them in this bundle. ConfigSubstitutions config_substitutions; - auto load_one = [this, &path, &bundle_name, &presets_substitutions = presets_substitutions, &config_substitutions]( + auto load_one = [&path, &bundle_name, &presets_substitutions = presets_substitutions, &config_substitutions]( PresetCollection &collection_dst, PresetCollection &collection_src, const std::string &preset_name_src, bool activate) -> std::string { // If there are substitutions reported for this preset, move them to config_substitutions. if (auto it = std::find_if(presets_substitutions.begin(), presets_substitutions.end(), [&preset_name_src](const PresetConfigSubstitutions& subs){ return subs.preset_name == preset_name_src; }); @@ -988,7 +989,7 @@ ConfigSubstitutions PresetBundle::load_config_file_config_bundle(const std::stri this->update_compatible(PresetSelectCompatibleType::Never); sort_remove_duplicates(config_substitutions); - return std::move(config_substitutions); + return config_substitutions; } // Process the Config Bundle loaded as a Boost property tree. diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 62ce67ae4..dfc4f5a3e 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -8,6 +8,7 @@ #include "Platform.hpp" #include "Time.hpp" +#include "libslic3r.h" #ifdef WIN32 #include @@ -128,6 +129,7 @@ void disable_multi_threading() tbb::global_control(tbb::global_control::max_allowed_parallelism, 1); #else // TBB_HAS_GLOBAL_CONTROL static tbb::task_scheduler_init *tbb_init = new tbb::task_scheduler_init(1); + UNUSED(tbb_init); #endif // TBB_HAS_GLOBAL_CONTROL } From 95c879cf0074885684cbede8d1c9dcd65cf6b1e7 Mon Sep 17 00:00:00 2001 From: Pascal de Bruijn Date: Sun, 27 Jun 2021 18:51:41 +0200 Subject: [PATCH 11/37] creality.ini: beta support for CR-10 SMART --- resources/profiles/Creality.ini | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/resources/profiles/Creality.ini b/resources/profiles/Creality.ini index b055641ee..91c4e37bc 100644 --- a/resources/profiles/Creality.ini +++ b/resources/profiles/Creality.ini @@ -131,6 +131,15 @@ bed_model = cr10s4_bed.stl bed_texture = cr10s4.svg default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +[printer_model:CR10SMART] +name = Creality CR-10 SMART +variants = 0.4 +technology = FFF +family = CR +bed_model = cr10v2_bed.stl +bed_texture = cr10spro.svg +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY + [printer_model:CR10MINI] name = Creality CR-10 Mini variants = 0.4 @@ -998,6 +1007,14 @@ max_print_height = 400 printer_model = CR6MAX printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_CR6MAX\nPRINTER_HAS_BOWDEN +[printer:Creality CR-10 SMART] +inherits = *common*; *straingauge* +retract_length = 6 +bed_shape = 5x5,295x5,295x295,5x295 +max_print_height = 400 +printer_model = CR10SMART +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_CR10SMART\nPRINTER_HAS_BOWDEN + [printer:Creality CR-10 Mini] inherits = *common* retract_length = 6 From 3cf1f568d09164e6ac762dfad0518705e91690f1 Mon Sep 17 00:00:00 2001 From: Pascal de Bruijn Date: Sun, 27 Jun 2021 19:07:17 +0200 Subject: [PATCH 12/37] creality.ini: beta support for Ender-7 --- resources/profiles/Creality.ini | 16 ++++++++++++++++ resources/profiles/Creality/ender7.svg | 4 ++++ resources/profiles/Creality/ender7_bed.stl | Bin 0 -> 19884 bytes 3 files changed, 20 insertions(+) create mode 100644 resources/profiles/Creality/ender7.svg create mode 100644 resources/profiles/Creality/ender7_bed.stl diff --git a/resources/profiles/Creality.ini b/resources/profiles/Creality.ini index 91c4e37bc..2ade2d508 100644 --- a/resources/profiles/Creality.ini +++ b/resources/profiles/Creality.ini @@ -86,6 +86,15 @@ bed_model = ender6_bed.stl bed_texture = ender6.svg default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +[printer_model:ENDER7] +name = Creality Ender-7 +variants = 0.4 +technology = FFF +family = ENDER +bed_model = ender7_bed.stl +bed_texture = ender7.svg +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY + [printer_model:ENDER2] name = Creality Ender-2 variants = 0.4 @@ -969,6 +978,13 @@ max_print_height = 400 printer_model = ENDER6 printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_ENDER6\nPRINTER_HAS_BOWDEN +[printer:Creality Ender-7] +inherits = *common*; *descendingz* +bed_shape = 5x5,245x5,245x245,5x245 +max_print_height = 300 +printer_model = ENDER7 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_ENDER7\nPRINTER_HAS_BOWDEN + [printer:Creality Ender-2] inherits = *common* renamed_from = "Creality ENDER-2" diff --git a/resources/profiles/Creality/ender7.svg b/resources/profiles/Creality/ender7.svg new file mode 100644 index 000000000..29e93753f --- /dev/null +++ b/resources/profiles/Creality/ender7.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/profiles/Creality/ender7_bed.stl b/resources/profiles/Creality/ender7_bed.stl new file mode 100644 index 0000000000000000000000000000000000000000..9cf19e483d982b08c1543c2130a25a8f6c01ee94 GIT binary patch literal 19884 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 literal 0 HcmV?d00001 From 43346a871c6a91343094702443572f4a049a94cd Mon Sep 17 00:00:00 2001 From: Pascal de Bruijn Date: Sun, 27 Jun 2021 19:20:25 +0200 Subject: [PATCH 13/37] creality.ini: beta support for Sermoon D1 --- resources/profiles/Creality.ini | 17 +++++++++++++++++ resources/profiles/Creality/sermoond1.svg | 4 ++++ resources/profiles/Creality/sermoond1_bed.stl | Bin 0 -> 19884 bytes 3 files changed, 21 insertions(+) create mode 100644 resources/profiles/Creality/sermoond1.svg create mode 100644 resources/profiles/Creality/sermoond1_bed.stl diff --git a/resources/profiles/Creality.ini b/resources/profiles/Creality.ini index 2ade2d508..e861e8425 100644 --- a/resources/profiles/Creality.ini +++ b/resources/profiles/Creality.ini @@ -293,6 +293,15 @@ default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @ #bed_texture = cr10spro.svg #default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +[printer_model:SERMOOND1] +name = Creality Sermoon-D1 +variants = 0.4 +technology = FFF +family = SERMOON +bed_model = sermoond1_bed.stl +bed_texture = sermoond1.svg +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY + # All presets starting with asterisk, for example *common*, are intermediate and they will # not make it into the user interface. @@ -1151,3 +1160,11 @@ printer_notes = Don't remove the following keywords! These keywords are used in #max_print_height = 400 #printer_model = CRXPRO #printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_CRXPRO\nPRINTER_HAS_BOWDEN + +[printer:Creality Sermoon-D1] +inherits = *common*; *descendingz* +retract_length = 1 +bed_shape = 5x5,275x5,275x255,5x255 +max_print_height = 310 +printer_model = SERMOOND1 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_SERMOOND1\nPRINTER_HAS_BOWDEN diff --git a/resources/profiles/Creality/sermoond1.svg b/resources/profiles/Creality/sermoond1.svg new file mode 100644 index 000000000..1a93d9537 --- /dev/null +++ b/resources/profiles/Creality/sermoond1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/profiles/Creality/sermoond1_bed.stl b/resources/profiles/Creality/sermoond1_bed.stl new file mode 100644 index 0000000000000000000000000000000000000000..edab280e9a93b6169a5afb4f6cdadee141bdaf7e GIT binary patch literal 19884 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 literal 0 HcmV?d00001 From 5743eb9c17dd0a89308eb8c7a8f3f87d2d0162f3 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Sun, 27 Jun 2021 20:26:42 +0200 Subject: [PATCH 14/37] Ender 7 thumbnail For https://github.com/prusa3d/PrusaSlicer/pull/6659 --- .../profiles/Creality/ENDER7_thumbnail.png | Bin 0 -> 48679 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/profiles/Creality/ENDER7_thumbnail.png diff --git a/resources/profiles/Creality/ENDER7_thumbnail.png b/resources/profiles/Creality/ENDER7_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..0ede9649a4144c055576ee340294796b6b30bdcd GIT binary patch literal 48679 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 I!`^ks)-i%#S2g1jEbW2 z5QU}`&wjSe`xRQTpMhjrzRjguhfc;(2;p;1KxxgBaD?tB_w$zHNdDX#2fI}S^p*=+ z!m4UDuub+ygdY935@}{q^m(ss+!yPw_NKojRrK=}h&QGr%e7J%&Ei^9?gzF61S}^S zw6f;_uL{hE)-naHPzoIk;1;VTKIVD$h?5sLMrT=LV7p08<)k9X=Dyio5H>B8N=9$1 zeD&@II$X-sN-_9lv|^y!U?MpI$|4~;g27EQwFo*=NM4~nkoLjj5~=d#y+~P6Q5Ee` zQ270+QFj`6`_&B}WG*VIASo*P{|)e8ZSUz`G5ivJLihu^zZys}#Cbz_jEWgGkumJz zX)$|5%<1Y+^z4tZ*vuuAwD2Jp{R4=?RY6#$+5}pV!dDRe6BCj%;!mF+z5bX`9$`Oq)LW;8dD3Q>|}4t|SIXiV3T@uAFJR zd9KYjd|V{3rJY}Yb3Wsm+zE`1j*Lc#6%xr7BB3u3(lv);sNcs}i^SA9K80F;J)%Qr zt`8D{<69AdnWm^mm*|s&j`ku6N279Xk37FF$CZ5S6l9x~z>RF;3N<|Z@J5dMJ}dF) z{kV3S?t$r#GTZCakOc8W20;2Q??I)qSq)8ka-;1fIpsx4ms|6Kst?FAU7 z#;sbw8Nn9>TSoyN*=nD=0M@uiyWi9RNqS=*yv^*omqP?|{CRU-wsFnjh8!cY)*R-Bo~jgeSqG;4*7?a{Mz|d zHf@Js)c+F0e3_pC{K^HtGE0O9R=-yyL^aNlI2_ge;vL*oDwwMgQ!M@rO(>ptVKVS? zJSqTP*4J4NFIvEYcMQLq;Y)Q^rtcm!xMOo=Bd}W#ES($uaW3_-r|E20WBNrp`l*Xa z*n3w)i24j;=>tg{Ns}K-^H$(W{WJ#Z_CK4rWclv?-)j8sE&x3~e1~mZA>Th(^MmJX z3XGSr#0bB)!Md#)lj2p~FA)IZpR)co%R|CthyLR3-E9v9Fw8zIUwhevq5exO^yQ2) zKs#`2zDJ!k%(@wGH1oXDcQ+N3shpHHizPxbf6kOzpTF6yau^^UCu%(Q51>U< zYU6qYg#Qt(gk4!ikQFmN2L)l$$2O!4bH{f@Vdn8~FJ0{>&nf$>&EIXkDeJQIDW=x~ zXyXkovLC49qqg2faP#6r_;*Ae;wnYKt8bTzn9HC1KZ{^?;{vD%5R(y(rQawNHar*~ zjdTmld&zQvn8$YPa~N74NcnLuI^<@OLFPAxc95EL!C)n200F{Mo2NOLCj_s&A>{En zKsls>N3$Ljz^zZSpQv^6-t#%(k8B+Q;7RbVM2p$y0>$-^aVl*3-*?A#XUZMD5vLfflko+mQ`|YSwoUKl|->qN(Nnc_zIT6!BTC*pHK^}WXq4r6 z43B47^U^qNEO@}ic0)wJyl^PYIgx;Lr%81MApW>pf8VLCKT(PIe8s^tBNOCmQ)50% zQ`msSB7W&U1c4t=`zCw^l3FB0QTn%CB}Be|>kAn1`{*4Ehme2=L@yX|3-YKuF9`^!_#Zr{Hl!6fe^Sc0sKqd(sR z&uG4|4nP)p8!@WA+3>i0zs>r5J4Cz4>x20(yQRU(!MBKL zneirAcO$?8QXK^OSSPDBXh{X%hZO=^FuJ+F#4}7Xm6nzYZ2tS#7|ZfKEX-d)Wq<2_ zQMZc?H`2MWsi`X3IK_4{MykL94-Ze*%*>1{ha3@W6s5B49ox4E;Q9xAJahp`)be+I zmdk+Jw)XYD_a^82WY%v=z8yGU^q=timPCMRwl74Vcpmt&z(}XpE0ZiaYJPb+yiOL@ ze$F&&)4KC&PgzyxOHz)485*Ft6joK$D^zT!6%Cny@;^}4f#4^cWJTBeE9k|kROS=1 zgyymY;8sw$RcBSQ`IvAX_qkU4bJFk#q5hQ%OS=#Ic~JtIl@=@b;)=+yK|&?j2?+`O z5P(H(7j1);dh<_d8spSL;W@UiL(BcNqA4-m_m~Ch%d-!HU~ksU@r6v5!HuQ}?u1XX z`hJ?cW{(3`liG-yPAA))&Je!FUsenDq;l=(oFiLb7pI`0pr(ZtQJj^{Q8?1xeWj~x z-g&OAt+gJ+{+3O6ds0@Fsel+o@gJ1yf?aj%UC6?gozJo*2;D8}PSu#;GZxR;d?MZi zH#k=_biCN=cz}EAiW}dQicD>zL2Q|#UN8z*&Ub}>|2Tbp(6m~C`yEJmh40h_v-0V60VXr)}E?w|c+ zjyW#2VaR}iw0!p+Hh@Pdfg^U^ z?eOx~Tz-p??IG_3@WEB#AMJJ_ZH+5AZW(na8#h@4Cr(<=pp-B z3wF{;DSpzRTBtir%wh2l;z{9)Ow4A*ot~aTgysI-P7gr#ug^8RJ>w}f{rDI-h1KlZ>;pPnKy0(tG4UNCp8Eu)NQr16(| z5It(o@928k-`_vTpLh)&8u<2#6zuZsmH9~r^RjX6n!i^Ja)cUx1ZzIF?Xj{wcq+|8 zdg6wHvv8Z>hc0giJ0`fiyI6}~)#(GoQC52GWBAY}(4zpsN=yA%3MG{)19jS6?PTWa zA0dUYhqyt8;}Ck4XwzZ2P;I2wpG$33`>hueQ7dSnU#KIg-)Fowc{uQ_tvA` zKHpu2gC85A=xAxNu+TBrwY0R9*4Jj=6%=DGk;EZ8cX`Ta_~Du10N%XudKX9&*cLA+ zukS8Uum=qQMr<>vY4LDOxoNfuCP~!UFmOe5p0ctshvG0|XwRMg&`V^IY;#-yvM}<2 zI)(i&kH>q2*I$cg8y%l_zkfBY-Ssp^5^2_W_H*zi7YSbS*C_@$y! zuc3&J7Juz(#RG73d`Pc1)g9Y?MXFD4bUIBe3kN^DP15q*2rd)6t`VlPD=#hA!}5Mw ztB}UZdKp-6Yt-Fr{_H+XP0Mo`L_OXr1~^it&BOsJ3DJ{n+BvbtUA%Uxd#oVM-j3Tq zz!UGrE}|P1ZGnT%^#CbTI@o$xe6d(^>M(Gr;mnaZs}vr8@L3ElUQ8fwHWt{WMC3A#-#hi~x4`f}ZlQ;h7JPITqjeQxM- zayIefT*zhckb?b33;|c104zdTW-QN(7_euHJ4g$=USH0Qk7h>At57$Wu2TF&V7?J5 z=npaURs>ipM4V+BzlU>aadox+WXbK-mJQLLbg9@MK96M!M4wU8?th`Ep-Ij}59!~P ze^}mPA#pUk<#w=gyNQ8)q0cY)Eh4}q_laI+n`bwo#Iq3pv@ZbA<+Y70;I^IjYrgMz z@As-Rvg|@B41gAT3y@^YeffR7HwbR2f2AbjaqW><;7JeE>3u6}X3eAERNqr$wR~Ll zqxbjc`t0n(*u$jJKnUPJS_a=8jxENKmjkGq@iaxGH6`0)e0jx_I=)U%C2lU%P2wBFzjj97|;IGX#q^ z9>eTd-*7+j>D#XK;AvC#+r|B_WV1{MXkfuZCcDwHP&4Dj=~Xi{y!?THynX*Xqiwy8 z{jot1E4FJ>&Rv1|bTE9oHdi{LOD{{h{d&0ZT?KZe&^W|VM+nlzWo0gWg zGzvpvQp0YrYjs8QV}T&li#JB)$PVT!Kz_5|p&A4z!K$B5reFDd!S&TO5d!S^4;Hec z&%hJB6M^ggLXtXStM%4}F*eNX_#^yOhk}j{{y#{6`cPJuaoWh%Hgq0#x;|c1H4=qs zAP@1y#l`3TQYc9IXAbeKtPZ+}E73*q{>Q(lz+EA`X#zS}qS#Llxc-QCx?I>lF}^(5 zLf)c`OlT=9D|h<=3ea<@{aG;S7#Sy*lg*blJ?;1iKl>{)+L+Q2FB5z_3NIj2A;-QtU@@q9|^kvo@ft57Ci z1yIb});UX$GH4;-KGvaeRpcI_i6B%C6Nn3FPEtbgCkCWbQOc#8v4UAIHRnmml?j@g zb35v7`N>7_~p#A@l`n7#@>3)3h56|B6Ai%Z4| zikuw5zyIjb@(jp#dBhhOVc+7uyXa?mIYGA$K6=9j+zq3J5aqWYWYE)Bx6(j^Vj zjdX)_cO%^?-KEmq-BQxsARt{zH^PE+H@x@%;pO4s(>`~1@BPipnRCtz+OeM<_nfV) zP$}RjG_=v?T=lyDRa7ddoA|cE8qGQQ%D*oL*0|XBg?Q_^xVU(Jj0mHPRMXa8NH@bs z_r8P`Z`b|Gsngj$j2zvsMfeZB#Ka&8IP-Y8325)+K722O|WjuzL7dI{h~DOn%L zBtv*a@zIgS`-o4Qz!rw*vJlN%cD#fvwdLd(T$)2t zfV1%z(b_5hCi^9W3cN=h6a@rBpwMzoRXGFucX$effF5OdkSdO_Ed7XnA|+8wvtA7& zGcz-Z(x&WB-+9`^@|3zTO478Tn;&-^kxnnw)i=gRb*EGpp$!Rs4<9}U+PzJ*%WX8I zn&4-A6@|fHYqq32=1Sl|<9`L+mzd3rlvtD?hL8uTpvfU3Jb|zCSiAY9RFlugQgd${ z*9p@VZua5ykQ+;Fy0;MaamSQSk&rOncc$_L%}y=K)yUa zp)pxGn~Pl$MJUO6o-rS|;T?RiXeity-@(br4}yRor60<)>QS>7XU=+s5*U`Eo(deV zN;E2WC1ole8%D`%E%-DdddRQ1A`^8%e~ZYzg79VgLC)yaOD9q# z+3Sv~?zh%|{5v`lG*6iZ+B?5i@WXh?rNCc}$Y$x;f=`(HeL;jl%%7rwW>$aRPtO4I z8W~sc_Xgj!zu+R+2WsL7U&{+LT`s(zU77H15B7NAh6qYR?l8m;HXhr5l$O$3g7kWn z#|2hFiYQwZ3_Urib7c@aQB1t?;@ah=QH@D&Z_}qnAv#)a_q>E0Rn8j_cG2P>ZKff#Nd1-$JM~)df`~fh~!>+Kc&Es?&U~KFFx_T{;*U;)m zkTZ$>>aA$t55rC`_FJDW*&GS@KW4*_vDR+ghEjLiOiK5UYJHzj_t)>L{(Q9O;?Z|; zok;r8O*pz|6|eJLdz{X@+c@AKhpG2gkW)kCX_N;*xy0FS;waz+??$}1_Yay9X8mEP zt2^h^7^uu+HWs{anb7{;CCE^t?0_Eqk+KhS%U|~MveS7mOaFZ%V%I;aNurvu_hD>-Wvi@aQ~3btbXV|l+c|rD#g7jqHdBwG-Z!55 zZv{O+J`X|AcWA$n?ypNB;DELa9PJ#AwH-VA`^Ep=K6Zscok10)nn!CLo;q^-Awey= zs>zwfRSCfPUFW?P?L!BJFFY+;A`)7I6+tyEl&z+~1(nb*=jZ1w&EF!zBZIt;7VDnn zezf{r+cs5KE3zBP5WZOh8km6#p)%#H*&^GD>Gij_WcWcJL>-#mW>qROfSX+0UM(+R zq;;Ckich;Q!NQl7)hGd8z?JD@LvOaDSj9Cp`}vT0MM2fyzkfG&?@St@=7l8#iwDSa zv;@-(TqB%#yWdlUy*)$e9g8Ivh1VsnJGOYpK$*!X6c1o{Oo<2=F!G&$fVXVRxz%_9rF!^~rxd~`iXtQH;?m=_H^$mC&M(kWxHlA8s;WrbJ@Sih zjtEdv?pxtMJLR*qzgY3bN=w8qA39Z|7zq~zn4t~pK0coY*8HlkqAVgp3z4xKiaXT~ zh&h-m8-QKbEBr*`g?a54eEqxi@S{-XmThQ=BrRwX*;dcMaR{4K364hcF%DDT5IIijg;NCR2$R2(_bnruWJ{;c-y z1Oxp|_@9IkB4yt6Y~s`#Nyd=8T-6@cL6>DNMQoJdUwmJU0#$G!@A!YS>ep|a!lGRW z36+K9=u#xhLA|va-=0a8<0@=$+u!gA(cWB8Y?(PGF3W&8b=cfC`?L^h_;P`xvK0%V z$>)J`2rB* zU+ro643&`ISjOyVzoXXSDGHz>J_*{*PFnN5i9Bc&r%XVxadf18_wL=d;T@E(1C3>6 zWgfsIt5KDcpFQCEPJVVUr_@b@6=AQ1t0_~4OBYF5oWiA8HfN%MtuchM3&IT(@7V|> z3(7CGArG_W-l^D-|GDE!U}Xz<6)tEaIfb~%iGrd2eHr$m#5C{moNI&!yd=$ zj2@pqs{qti)S!%={2LnwyZ38rIn-DY-x9116iU@7VFpM0`%pr&?Z2|v&&g^v6%2(H zwuu$CSUU=8nwoGFa4;z9x1R*qV&pE9B_X2ZCk(3`K2E9Jz90mumBQdB%=c$a9O{EXwNXepvKrs;D zFog3wrS)XV=$Pmh2l*i5nS49FqbZE~qY@<&Xg9Y!r_a*5x=V`K!zoAa-Uc=6aRdBA z&S-JWEsY(I%Oo|#i*Qa5bnY@qBmPZ#PJ~nzK=~^Z#lr^SPuIy9Np@P5>qoc!+C7u( ztWEQbN7u<<^a+#H4Yd@mAuUY`(zsw7^m-}@)(u9u|fdv zM7vG4l~$mNTPut8ZW!q3)H0{YowA-i3PYs|Y%Y>VCh)TZ`}#67|NRv8mtNoKgymkS zESc?8%5M;|2YuKd7Hw|aP_x{|JdmS0PC>1R_HYUb3F?j4A+-4#oju!Efd5yl6wMA# zCOc!8U@q9kZ%T2~IbLDQ%fHFQCzwn~Mze&E$gGG4I=wH!&L|-$;;0D0ARRo4LjoHXec7FBvnJm zBcW1n3&}%JaE2lalpT>E&Wdxb6%`yb)L1BY`jL^5&A;fqja5}Yg)Z#aRQ>oNe7A*Z zJS}Lm9rm4galvWvx+(Hv`XqweB9Kruh*ZY6$qI|~AAR@`mwG$<5J%z&rEG?j&z{V} z_=DnSC}Bnd)>4-)j&1Cw1Vy>6-O+Bw!p(%! zo^7P6?H%m0CV8PJN88zM-~Zh-vZ{mtXwtFsA!$Jg2cdlmcFAp%Iq&l?n{NV0@9x^W z>8=)GaG9G@-jJ7yYM>Bxl8>=q%xl$1S^`bH@ezqLyTi{sT+mkk@<2?=q`)nl7K<;v44 z!;>6D_m=z{94|-?{L3{Z+%u|DJY$I3{`xMAk zb5k_-60-@h4lhQTsVrouK~R)nrn0STZwgr~AZYt{-ntx=_*7@z@^r@~mFA)+sBv*~ z>lagqd3eQ`jSp%l#|xl8v^`L%e5DSTg^!k%S2(hP#|9Jr8zA8HZjM1EC8yMW*2hG5HJ)>n?G)=tHL~qCHZFkZH1yJU9(*?|T z?#_2y_EoSgCo+SV8Y~N2TM7EM$(6DM@RPBeA<;GEeL}adXo!g5<+6@MlbXx?ND2PG7a-mQJ=&sG02h8H_t!I7*R~vF({sSFtajsrCCZ(f zt+0_=B*ryARmV3BESTG7W$9A(#Xpz%bcqjA!=xO zQ(xUu^TH>c-w$cVNgOASK*%pixThSZ84HHbk3p&CZI%y2t=Ro#W-Mk^(JCHYM z2Zqa554k~Ce5jX7w8O-ue>H~M*aTP-srz9G&@l$x38-7uQ%XoMsjGLZ@LEIA@!=Q%TJ0v6KSOA^kWWQej4#4>)lSfNu_s=@l z<8CUrh+}Da;dN$}&bzp2W?LbM(_FZ|Hy=zk){*ch?Gh%*_eM>~G;*m5u1nHe<(8S8 zrdSIum2+QYJyrw=_LROB+JvmJ)x^J4vCNO`4^3PHBlcfJ8zO~VUOqYdJ;w(EN3do^tjM#5qN^m3>dvz@-sk-SoDExq!~c|bPyK8+FGG-i*; z5V0jb7Ntq)P($wZ zf2smC9H55%Ma(1iI))`0z5m9z_-S#s95HP5p21(KwF)A35t0E?(WEg8`sZ89HrWO6 z2@-lKo8RE{wuA+<4T54aMP7rfcsaN5h-)f;PKu=8kkYonI3YC)$UP!K)ho?A=dZqS zE&e+_wKkV{olh4;$X*<;gtW|V3j?DW85W+?pV;@M;5xvaEtjqbh@Ls*lb124xq`zKsmCWN!<8kGmOv z)uBPl7&%RBd|1jYZMUj?%3GKMp^!Y^b)0}nzZs`o1Fh$NBJ>+Dr=FRBNw;LxB2skk zB*H}GAM8m!4?S$qRrKP!g;~b&{%~(e-ZxHQVs_3%;xpLTpmonEeWj#{HF8TRP4?<& zZx;euD|*sir^L(YUcpqFUR$jCm4HiENp;)yqi)^(h^-+lUEPR({}{e!BFW;^St-M} zcXWV3Ba$S(w)#8P2ebT@qVZKX%aw`NoTuPvg2Tc5jLRfyk`n^8RJOm#R3mTBMP0!}>1EnB z-0?JNm#fz^`+@xpIehN_aI$?*_^j<52Cfd~`UpZRY<`u7nQamc^oL)nYUx!7j8`)7 zF67fOXlT}<3ws(G`EnXxy9?%t^Qep*8%!KEn?7#dyiTr$^YhV&cwV8n5VBf2o?Tg8 zt7<9|G-K%@UkZ}^#p3-`swU3cG!yxfUa+e9(Q^!G#JPng3V+9#wJR(F`~BvIopE1= z6^ob)+qa}rr9sagPvl9FQ-kE>WYdvE&a<`C_1GwFqp$Bs zD&5Od5-1sqlylPfv+wPTDnVKPbQexTCCL<{MKvS(gesle0os@n-NFdcn3mvaiU(9& z=F_KOUM8B)*~sT(KLtrlOtl*J5R%oS6cew?H5Px@2!=Xn|Gp9#*3imtEhfMoiW?3= zd{gT`3>nYmexv+yZ}Y1@I*LV*Eb*_HgamxE<67jD--BMVcC47Z{G06vUOI}_okx+M z@~mCb-aXFYM?B)-v)Hdp=^_bhm$6%A}k&kCCz(bSC?8iH@0JaH(S z!~i=AiU_dEz8(*;47>T&zau|&K-`4LTm}TsI%2pWQSGyoP@fL2wxyha>UT3xOUz~9 zZ869CMEXaaJ^yKZ?jWCXXPmb=<3Ytc<*{;qj8%iVHv=l`T_tRlbWrZGFGM0}@5Tt?q zPRbT95$TLWYAK>*r(qTXDq|sLq(0PkT3XzMEPwim(?7^Z=liO#pJc)JzSPuQ5F4ET z&*(TT$nB5lS0zI9KxBftFHD^48n+9RPxh3#AQ*{ok8)3>erg~=PnPC5FU;o`{?r#O z8=}y{;(`hy4MizErNd2gbP|%xAwYdOONcZ0@;=WGYr)8u8SuIY9j&%urx|(>lKSko z&@nKGb7X`~oeuE49BOeLt;F2|{di1FOuQs&AszYZ45VflUQxZ_En~7c6cI%O#DU!# zZTR`9KK-l-No)~pZ%rcsoLo7FuPBc*xB_v3OI#P!z5)c^7x8X4gN*?EJ zGcDH#{@po->82!q5v7-EEF-Sd8OP(bGhmcWyk7<7mlG*vu*2gUc~QLF-2Pc?x4?c1 z5J?=NrYixyR!AWoaQ5SOx#(yB%HJfqptcrTNT+xA-i5n*(3BlOum$z?Sb%V5LxeKo zRlqL#hYJTExz2n9Yc=5UIGxi90u;EQi7Xzokwlui1LZ(8vHWk6lyrN8=|Ybh(OF!$ zWWuj-Rk|HWn5(ZK;EhB2$jxQN8B$zv3qFee5T!>cp<`l_to;r(F<{LDL&WKIXx*^O zLX6&jfjXQLeKQ_ za^&Qsl9QE{c1mHfVy!uu9uG!&g=NUpxA+f*bq$;5CJL`BXUPpLYd1s@%&~T`VMm@kyykDE1z>kY z1!aZ;B^0Ii^wwfM3dTJMFgFSW*uld)Vq#*TMC#00wLp)M%uJ%s9v+3o#RxOjIMM9i z3Q;sQ9rV{PVXUC{aLrCD(YZ1RA>`>v+PBiFqBdXN5OWRmwbA2lVj`&Mk<SVB}RN zXNz+rNUFPG16fRz4v_{&I1gV+Dx92Lh5O6v-A}8t1*^QU!e4D>yZO`=6)!Nispo4R z>37r2=C1$zORA34djc2^Zl166o}UVDn&(@tC+A&}65JqCs?&>$-$cnx;MNtmL5p{r ze@BuiP~dQ+#Q3XW*=)t#;DE|T_mC`hONb!o?itKBl%OyKyg*WwE7bBbW8`5cG=W|> zdVXGuZAe8!1A$^-XYGSPRZoHfEnwmiFNnWq0&QO2Z=QRRN1CqNw*S}GtIjOf%i|1?_o13P9nEyJ%T=s^uunNey6y3 z57+YEp_^QaMIm&3JJ%NdySUh&=U-QgQ?Z{aDu6;x;|@eGYf5sm7e1>|XKPWBnE_BS zMVvQ5YUMbn;o=s6WcVl}EscD-zc0#!7g|VH+}QZpo+G0;LK3d2sfpL?jJ$sf0^AxI zT2%uB1XUbG#8HNGj0i0ajj-8SHGq-E59rxQb{x8N_w=Mnk;_Qz20|-X6m;;JXl-qc zj*(GMBoD#sGtKLmLkpQkO#X3qg#CrWHOVHI_M(s}QTAF*eC2?^&p(Naieiv+%W;d9$NLPdZh`qFxh zJRJx+D+jDFHL8e+!Mz{FohM$NW8%0^oPy{g_YV%lR1+Zi5lOanzPF?+uH_XB0IxN? zGok{w_G-4(AAYKs!+#xm8uanhz~i7GN;GALXbn6sh7@r&0!fAxIM^zZDPwgl8oV66x$Z}AZxo{%{2A-Ekev;`cNZw@xxPN% z(y+EV@1_F0odU|WZRQsZhH&8A0kUv3W#7c^`sr;^M+b3cX~yq<*d;}Z+^4}v+}GB< zOgkfz@i)xI-F`fvrzycO~~Gf4moD$Y&M z%mf334h{}ZKH&HH^XCr~0UYg8tVYkI_ZjKtbmQxfA4;Fy-NjNZe$USX$+xJa1kr3L zT8xb#tfE4uL=7lKDw?>$d6+{s+s8Pe< z#c46c>o#t?7hwKBy;=y%vbXUO!1rAACcy8mDi!{e!Rr@aR zLkmcFgN%*%7yv~610AQ`0x+xB%S+D|rxi78P)lPYIs(GrAPuiq{#zg%fnOmZp${z5 z^n-)=#!n_38Gc93#z)OgtNmaR-h*BHIO5t;NuZ%e|B-scMC=e24 z!g5?k2^&jFCW_e;iy23j{sg7}j)1~3pb#QvON2NUDS1ej3f(@5%mZynybjlBB(=>R zx?-r4G1X`~Qenlu&*^g-zFL9pTiAD>V@-szoM``o`jE%K>&nP6mV24uRu|1D9XP-c z`|za(viAj0t(T_w7Gz4{yNt-tq9R36!%bMq;QU^K%29MvzUwe~oL0B2x5E##s#cVZ zQ%bsM{z7MyPc&!nLJ|e)&{Bhqr70j@aK) z$nosoY@#Vp6SvXm6eG)VjW2V!6JWtosV*)YI+* zAZ=Y8n}PQQ07w5T_abqr8h!Bo1eZiaAwm(T!zEg+kHl-PSn^jq-#y8U686t%YN{@N z0@*}_o(-ouMR<|NrKx$4PSVz&G=Qie5hWA!f|AA!kJI{GjLc0>PlNKuI0_K5NaMM| zcmM?bUF0ls4k0obJtfnr*kfLs(PYA{+E-JMG|99Q ztoW1^k`4y+Z;tYc3R{dFilu~kk(K_{2#oPz_p`{UqTRaEs zpj3&yp(TGiSbKr8)0?rh`x*(4GZxk1nC?9S;zv{rF$4`}&J$jSCUe*fg22(E-~>T;TM z5y?ZvxhPeX-re%lOn{)|dtv0DR?+M8v8vKiw$aYCz->u;Ru=IGGqT~mNN7dR3pYu& z(3>nQm!r7PJM*Xhm~)mB|K?HysX@=XfS@1u_473ICQUP&SvsH6s(s&ZXqZP-zKi>t zV{Tg3xPe$2jN0?1!)ps|p0T!h)K57wTI$-YZDV)$hFgJ;Wa^+ zJndzfTE|}bn@{*5)k{{E@P;}`CN=ewx+g+U;9pNV-CC5n?kn7*#pWt5NufwrDj`dh-mQQB%;sLg5jyewJ>y|4BmTjRgQf^UK=T7q zwhDTW$yeccxsvJMt8h|El+bC<>%@q=CqJ}e;$qpJcvnG_`2X1 zKOtWp6?SC$wUwprR}Q;P#02$+W>0;_-(GMHc;m?G8b>YhD~H$rJWQkJaLt%%xPPWB=sk#I zm2?UUXtU#hPVh8M3*My8;`8-bT^vL^3BZDejz;Z}c+q$T!^IkpmSYO*)f961adVif zzl9C#zr7Y_U}ZkGT3wh~c@S2}v>|M?7kcko;q3a9euT%Y*Ql#`cVXR#?BL)qG~Y!% z#$gt1V-!a>wBwSiK+9E`_Yv^0Rq&u54^=l9@K)mNso@FGA!fQfQY(cgQSs2fYbw@h zK+@UID}T!yzjQv%@CC=BDapk{njqK}+(BvhUAAQUes;{-;5F8<{M<*B3pYUQmzO^2 zaFAtm#%%hEE1wGX;VMN!^k&eO65k}BuM|QrqeM&MZrb?AB*~d;5<6O|P`UVMIO%u( zHW=Nt%h_$mMoYh~n$K7@rFcm`cyi5+;K#9|uU*** zF66)|PW&O(GK`yF9WcIh#%P*0t(3RA^9v#r*8Zi^biXlRIyw(wa~yo!-{(8xWv=zx zRi~)F#|mQ!PUmBQT^MRo@qzEOx3gQIlSpJ?B#4X*f$cRsRbFCt%MaQ5))~s*HJs0tZ#{ zdzXA1$n{T8S3@4iv1NrD!^Tgnv4fBLULQ(SZ_=NmvwOTg&f)-3syX$%>%Q|C#oFza zw*i~@DG!(Y?R#~#|G68_`V}24+wV1%)o(A=DG-)mHdA(m5|bP9>~hD*gZRvSY~ zx8hxuB;BECYlZdm=*zd~WqU`&daXh(Q znm)7UaZ)}ECgm*|#!Uxz;g7_bzN%6w#q+<^v-VFIEZy~p%tMZCbT&cPea!%J&prJIKFm`Rs@-ml>yk+QiXxrcz%TMUDz6x4i zte|DFoE#@tol(mjh~NLs&6srj>OBf^+xvxOr#Ls*knx03qW0&=EK#AvOWv*{cetpS z;47pF7+o@YRRn$>F7xgcR9zARXUp9z19kpzb(@dOo`qrx--YjxOyB>+I7lkg@!pVe zYWJi#aC>8*t|^V-$^%{DN-%ol7%tpaaL<;VD7Bas`X0Ham#9GNt*(xZASMb-ssMHo z6beQmQcR+(O!9vxz3;&YTh4dA-||WBE13Bdm6aOiz9&ul>xhl727OHR-=?Hy`r0Vn6`KA19Ya*b($&7I5wLKQ?1GObYmAX>?^yP*@l;Bw>DUW zZ50J(Ap#|I3k``xlM<_@jLjZU5Vcax*7odq z@sr(^D&|K=;g1YWi?uOuhDdrxn_DM5f0+;4)Kp1a68TcW&U9f8Un@>K*U?Zrt80Ld ziF8dajK767f^f%%IptGjPwrryg)zBw+wXCcV8KwwD#H1&B&v8{&#WQ__a$$wUDu8V5*>8lti>*OqQ8JDV)sV>R!_^rci`51ZMigP)hOt+@| z+|(`GDzg6~w7oyF)XbeqKVWX9i7poRfaR{R)G%n7p}oafT)nCGeYgSzYmDhvG7s%@ zWk5zpCV$b@JJ)pf(p_!Fh?qZdyrL_Iu8RVRZi{m7iTR49)8wqLAye4eBk5f%MFqN- zM;}qaBM<4={v+C&e&0}}#Fkmr*Rt2>_Cm$+MoPZ^Fo^K&v@0KwOBbFRqa0}xM7uui*zLEgn(BK6&lz>mvBTv$p4c6F{)ZL-IydK z%Eg{Bg-wfESrJT3D8^eXjc-^z;WIIaGvSH++o2~_?Sq&0Mo#=XpxZ0dPKSJ&);P~h zB3O1}JkKv?U--vVd#?*UdhlY4PA>1BY~qMZ&VxroLrbfhBC_9oaBvXlc2HTn69P)mr+;hA!`Zioq*qSB}sumafnsXZT;O zdGcp?a9H-c;q@$SUXR8EQg$4D3-&2`oIR2jRr(xXf8$)`WlKdHEp3CpKh5{bdk1}0 z6)%s)Au6i3a)pBRpfzQ^?J9YWy|Tih5`{>>% zf=xApu1a?h zc`DK)7rv>R4hrjHxPP{(sF0E-$oE#V(!mPZC2Z|(_x;OlJM+!g*UhpReXN{T-T=qh z(?T#W(8tKg=~9Jg(U(4xh;RmLS^RIdBa7B~^vuu)UaO^JNBq^rNH|?Z{8H|aTSO&C ze1gex30Lb_#ebK5CA@p+b3c#Az;Fs5JiZn#6 zR^zCg&XB4Pk%s4KMlfTyJZuND!;Whf> zXx}@=%{e{VY-E+vQ(mAt^h#y@u}%L_W`F;~k)RDhuy6R=h>Ec6_*V&7w;5 z--$%m1M_v$aEL3+(Sb&uy6xo!wTLcx-R=Wj>H%JL)_B210(+5-Ip>x z4H1~rx%_r7iIS~d%qzh`^+`-R&L*#0?iaHS5(;ynzr<&!P5yX?4(oIN( zzDa*Kv$WL1$2&FO?1C_oS(Gp^g4!Yk2Rpf`0ISf`dJFymHX?K`h>bS|#;WOajVPX4 zB&CC7#O#d zGbc9TOvU`75Mb?C^!~n3DPFO}PUzQTN&6YopW4MXWI?vb#FN$Ylf&+}5_}54ve{F< zH+R|gSw-kmkrn)|P~Ls;sbS!U6o)Y+`5ML|POl~R%_?|qFBd}%wdIDF>NNR}==t9B zCNM|g5Vu>qD^nlIlLcMDWsF$WtNkWw4+Y8pZ{%?*}q22P_*TzVd7j> zxU>&vE8UH;p-6T8HKfGTG7Y&B;SCbJ|C>bSvzMVMl7`?@zll*XCH#0KA1vCZ2+IpJ zatnO4SZjB;u$!;CH#ja{&U%CPh8u)wlj#F1Ni!lM!_$g2io@z?7WtPvunPC-N21{4 zyB3`ft8gf7hil&RCsqx$XC$LLD$9?uLcFl=u5B41WsCaVj}^6*7XFy-9C+zooU--^2fT}bbaRUYVYpu-b@EC|42F$BuR+V z!u}i^-p}BYlN`#Px}@>x!=?U@`>BtzUQQ&-IY{l}9FsF-Z=a_EUoLxqy3{=vNva?Bcit|2_a68$bcz1OVlD(e&l--Iw(?;1!=KPYFIICzQB|#2V0X##q zo1&_!GrFX4u+sGtppQO%no1uXO(HatDVdD`xKn_~N%eGhA4H`9G@!9ufB`3td%NrW z_x)xp%}jrdgONp%BK_Y^&&tpkQnnTLz3u8>muGQ)w^ORK$o<6Ehc@!^hn`R1z>Es( zcnj}_3WglO@~bVqGa@nZ6(Yp=jq-#2B1+A|?EvuLyZeuY0Sz(m8Zcowjp}sY-Mm}L zWo1D7Vcg=F(;oQxVg(B6^1bcfIe$6ZO*4j&NI)~!8X#>@XXm4M+s}K_e)uD)awzs0Ag?7jQY0x;{9@Sy zMx)hBd)BdIO0W}}n?}t4(=(t5b>t}uZE}l37>@yLjs`JSYb#C?JDrSpalZ7p0n>|K zo7HoBx$^*DizIs?npm88b90jw3(l|Zk`~QoOdBK|N|^Kd4a$THO#BL&?T#q4)hlhx z%vrH?7f;p43W2!-|ItTOB-trc`1S*BJyYNZ0wxypaPfF**|ip@*DOHRk0_B(BT*sz zpqQod$7Qp(PqEL09VBXT^hN0LW+m}q&1a?M3ES3|<$c&@^k5Xld$pcxZ@)qAogoK$ zIy!60use^_{}!-gzZak5LJ2uLD6(&EkOx_LP?u5fGc!(G$4@Is@e!9*gvw6crgf}2 zv;^q<7?GP_?SCH|(2{D|?0sS(A#@4%5b-|uJ1`NXjD`cH6Dt;Y@v18G4X2wf%k%x4 z#>jUk9+YQ@v#H4C1KK1(MA-4@e7XJ7BNWhMe|Cd;b5i!`;lS#!+(Jy=J5ImD8spxi zkfh)Id>f`ho-RW@>0oW$`WYD1u!?)9f4q}DWzgkweUESWdFG8Y=JaZvu8BE2?ffjV z;MbEFmIgyDigCr{Jk4n?#j*D2_DFQQpvpRlv22n|0m@Y7=y;2qT$HH^>2xcFW~YsS zkqmh}ZT2B()DR>&HO0R!Ki^s3yFf!lhx@XUc%g?L$IzkhN1cz8)&T;(- z#7bfTQLKMwk?`|lh}fmq7ZJB1>5LRAfi1!v$RKm&*$6$~9P7&D4j%Q-%Bu0C>ACvNvQr-VDNm# zt0)!I(OEbCSFd!N7zH4pjf4xA3fhXyyl`i1X1ZdEGHC*tB(i@xjZnGO+Bq^BndJtqzsFk0V!q!fU z7nfDSSl@QSOJL>EXQD~>Zj_`&CpkOPM!3%5d~hU#pu^)hQ^a>~oMUK_@z}xPDB{)E z)XF!zYZhDaH#MXw#0pI`B?X5nM{6sATJ$eyMm^Ie^NYFM&^r8Aa#RGTt{z;oQOttE zDuhiUe*O;UDJo^tWm$E=N{e-_V|_XQP9!sM-h2NDec)!Kd?UK`2CKN+7@qZBz(M2ke7(TxbZVWqytKq!(ADKX(*686JvRrR zy3|Py5+r4X1B=kjMAfGyr;6nRqBdsY-#$RCUj_B>v`pRiV@eus|q76k#YWkAu9 zCgMvI6S3}|12i-=$O+Jar76rrF=(W5Unep+%MLT`et>s$8Id`Y2CS@gN;fS%wejg_ zZg831az2gCH;!LoQxuH^14@)e&-MJu8jxo$0Hq5%OM@M;eaz88Nr(}@)7X13#p-ma zA_A45T4mKVdRRQ*&Ww6?(NIGBobGR;Z(Sq3dE<-;A|&^XU{17nfPcC#adP zoKxqL3_Gz0$;*jbYx|s6x5d%${=sv-Z)hURWlz^CRZCOTu+3(Uw%h$s3xuMO<2p;Y zH8~j>s)(GBI(Jn!Oq%uGR<9P2_Y#LgH$-meKwXe#BfBiXZ16KzfaS7glfmP>`9~3F7$I2*t5o`Bnrhaq?aogbh~6)=Rc=P}Ve->lPLk7O{eXsWUu} ztYdw(scdSMYSzpZXW2>p=nMKd;l7x|zY;L%>zhe<_GlRwh1xyJhuzdZP!WL=LyzMnX<>4uhIbqQoEs z4s1r0?Sp{;D(e0F7iVaASQLf5=b@=)jhnYsM^DRi==zS$hHcWC`p0pTwHmD*F~e-& z-K0oooyxCZx7YfDXW#d$|Hl2>jsHQ$Y^Mp}&4^sv5C}@X^Fxx8&-}^UhowsNq}$XG zONf}>==`1wrYcfTXVJ`ON0LM3D(%C>Fh+^6tyV&wP4_$0)GiN2h7=aXAM#pg#(J0< z2Scvxg)0vE@mP(}=cWhVpmy$Yr}=pe=n}_u*TN86b1Mx$_k}x&_}$e4^EEiH*M_IX zjHxxecCM~iDYl3rFXwSZ?dEhVqG!M7%dGH`iwN?vK^O^s$*R+An2y!YAKcFhRr1~G zg1O{1v7jh@6y|4v!zqduwIAv#@L14C{R5jeIoSLRKck5qNS*ZP3Hr*g{@KfV$nBv^~MDTN5 z*6os%;Akan#{#W0m{dg~pRB$q}4|$kyKR6{~;XzaVUlb^8++ z@W7hI+PNuyb#-Ncx%ovM$8~&Hg$iM@)y1rJaD1~cie+em=hea>RZXt%@>%)mSi2`y zt^`hTu6Fl)cxN#|HS$^FQB1rj3WE>brm&VmDPD%^7BK@JWcxQZ%19n1*qjnIC|`}4 zhnjt&N&io;WpRINUZY8>K8C&=Dm@Kd0aPwT4X%`3g%rw;_Lcx_ZnCfNpZgTn1{{cJ zn`thL;N&8N5+bZ-$QXx-F0F#$*HRmkrkHxlMAQKA;e1O&)!8xbL4fxo~1MKKXq zNh3xiJFo0%*NYF6t{}>=i^<9d`xQqY78psiTIYeHQy30bwi^mXFCQ|DB;YQPh9)e2 zGeiOy-hTG_Btt03%3IeEMH8z|g&vL;tWLfuJ|rMrVt}!CR}2?}ro{Cd(Vm(k(%O;U zG+U}uWxGbvv}R*&Ei}&mXouHrjj*mjs--62y0Y~(7&&ylM~x1kQ!`{ju=2H-L*2-l z#S*wUr|E(}5wU&7`0`_b3uC_>@{t~eW|}TsBM64Pt}25F!EhuNqs1+}Tjryh{SC_z zDvg>u)UGm^yg5x;?6ULRs(U=udH7o@J+CHSK>V>bA0@c-aGHb2bxA{~!fne9vTzMT zM4^vYDl^&VXIgo5kS8?#_bY zEsG32A*e7}Pef3#)6vzHqu}s?VUkZubiX=Tjks`?bZx-AkCRh zEwx%FtDTESp4jQ(Y!%vEud2!YK6Jn9#@x8w@+K9W3O)2W^T!NRS!|B+3pg>K1%wXg z6TZ24wSITQqYaTTNsb6AQ9hL9!BS@ne>UF%s(*6O%?}Lz{TvH-cuD#v&>?v2M*ocs~#>n%3g%5t^mb~%ZrzEObO)0QNiY8X_CPaudR1TPxEZITc zSg(ncLMDe*dFbgP_C1>O3LvPt(m%O1JVbqceY52;g-_UqXd54v-n9{9VPSDl5K=QR z49w3{%3SC<_LLC7KSP*AGrT)&tsV^i086*)aUI)43n-aWtJ<|DDl-OB z3r^`w5Yqz;EVoE&Kr8XzIrES9l}jM*E9q;BL=r}&D!o3zH~FxRBW#v*Li1~GvrE%o zb1@+I-J;4eysYwqYu=Gjch$aN)1uNGXZTD(S$zQKrInrhzLjM{S!kEpyx zhv13uVE;`+<(fE2g=b>LX0O^dvb44X0P!2L0B4f$cgf`0dp`S~9`k+kH|Ott7?c@Ms8_3nB?DtW?`=kw>330;C+ zz4;$g$EYeS3cdU5jJNmvoWe_{i(XK03IrPYTDXr({Z#XZ0!l9mt!fY>?pn^a= zo|v48`&cRElSee?#{UqH{zdqDTFj{>>PElw-*4-sMk%~8@&DSFiSXyinfUF)=(-|$nmYZuCX@UCy%O21A&EU^K_~{Nv+h( z&d)epgOw~vnD|z`Ntp+%(mlZ`q;hsCKs2D_L({MqIInApmKYN~ntAOG|Kr(c`J9sjGEo@9@26NXWbl0-`oIF|$C7+B4X@EHv^iWG9Ff z##6gCXmJ-eo{KK6O=Wcdq1UmP|9RW!KBuC)o!74*ID~|hQC<6c%I)Rxu~Aph-TCdx zW#>PjfAI$d2d#+7BE@Cc`}_6v^?t^iiD-%EBV5Rl-$wxCk5~t$Wvbye7!fyodQ1Gx zS^+fl+x?R+*IG`WkUDb2n&(ZYq>qV7_-4x|BzIx$`1i-$sO}707)5}o?3^%gx^g@g zq6cKv9EC~nu*f)+m{g=gLoAH<335p2`RDzyi_X#4+1mq`PF(ZcV-l0C8(d8f>T!TMOSYHw{rFnztSiqFioKdx>DDCq3ec>)f zf&>zkvMHvBu-fj*(kc6$)1gzAl6EK=;hbA|lxnY%Kyt=&A~=mVF$z+E454txP_>j_ zk{~;=QtP|A^3e>l5^|-}mVN2fK$JSJ#hMi~ zQ8oC{$K#GdmZ1gwl>X*0Yo9#Dc2}UXe!(j_u5232nDVTz8dxM`*uGzlGf`_bmf1ITz$8w^$}*q zP9?XTOk1|hvLRwvpU1*Xz+KK_8m^fMSn)+;fPQ9zZ}4uiJK?$)n!2HTxbZnF)AsQx zit1Cc)CrG22dqXPU!Me!GLc0U0w-WTtlYR)HkgqE^UD+UQwVGW9*Y$CtcU%n-L6)t zua5QL=(^Lcq8tAawU)jb{#fQQL()F0jt#}L4##>q$oB~XKc!e%9=R5WeQ1DdPDf&JpwLDC1++}Zgy zJxx&pfr7DM_+Sze7I%=kNzd%rL@C>>&ym{57^@_yHsSO>$>$Zdj7w~yi8Gv%tg62% zoXkbgcr97n#0UW_-D{{&81TCi@9SAk=`2B|`AI@#PuA+fH8ls;S{W*4xbT@ji8JgD zm*p*cyJE-xzR)3ahq&{QE(BN<=L5L7>Iz`T@o%)oClH@bCp?Exs%^lb!a{^MAW zA1@~C?%K=ZjHVj%;sK9brFS|Y+W9CFCYJ_Bxg0DteMld%b{!iAZbr90zM#{%fK8Dl zKSyJ;H0z!-$zkd<4pbVXmpy_*eF_YcM(KV~;3__*F=-!~=VWvC&f7j_H1;{Vh5RCI zeQ$2Z(i`y;>g>}Su8r}|(%qfQu8D|b6%A}mpiSuL==e0_0IXErF?0S%D1=bmQa#42 z^2~1fS&lP)10iHoy38QwyF)~ZO8PQ19#jFx3_)tkMQct`t7L?`GDFVuSxa=ykQNmN z5lF(}2x27yu#`{xxUblMqb6T;T>zC@QE~AmqeBC2W=00oNNV~g>ZR!Ma@%EG*TZdBuy?so z9}*KFWnZYPPu;r%A_@8b;LBD##(ryJFno&9Zi!EX zF|?Lix;v1Kr1L3Py;XkYO%)YIg3XYA27OkhKAOkzV@(=p#sJ0~uG}bC!pJU z68(XW`0Z0CCmeiW!%9Ky+)TsmCDcY>T&XgvZp-{6IE}F_VJ~sjgN<@ zp90g1?~TM{=G*6T>#p=>5D+qsO1hoJZE$slboFFHc+1}>bw}y)N$w>TnFed zNx!^lIy$=SI1X%hU8xsX+17UZ>f!3c!<7?kXoc-%np0k09xk($(_FPlAh1|6KAQ=m z_sY+5|9)MFha#Ty9BWaRVvKxE8u>jjGZP6g#S;@XtUP=vBqRW)c1+%iv}S2>5})i8 zLbu|zRh>AUxB$59EnWdsL?o&|4sYF$7oPz3afebkc;!zTcdhwdra_$_8!$;Caayb= z$j!w$|BK*D@)^(7&k^3-hE|J7ISVQ=<0yCNk**csGvTnruiin5)^6#lsSrGoNhk6? zu=B(mqW0zo$WiM9zhs>UpTh;M5MY^0tsR5?<5TXJFipYKVY9=<2I_Qd(A)Jo5Yezg1Ge@A*I{ z{zrY}s|vv2*0j@RV4$U4tVC9rdM#Mvbv@i2kNbXs6|P}kji3BI9S=P6coH2IwNko& za{=)GeMb(PR>zRq`BR>O=d*8I?xY6h^WOqUxc=Rg%-xhfC+Y=X=hX(@{oU%rUCT_R z{zJ^BnD59}-j%N4hka5;>3=uCG|jQ=&&Mx0ZUZ#72aA9b;V|bmIByhJc+;;VQSBMl z$X5tiX?LSDlH1x&O`atgL`4??@u?<)Q9CU~p?#Sm3cfVq^;;0!-QAr?G5&U^kpc*P zJSPZdClH(yK_Q`8t_zhGiT(KljEjfYBd%O-jA6OxhBZt}UNoS+x3v}V11;bx#UMER z9oqSUn=1l$l$<4OUgpQEFs?Xow>1oeJ%8F82Sv~)?su2JD2RvIf+g70A*FlSaf zE?4|lE0JGmredr2S5k(px^`NRuC=DvoOXduMUE3Cqz)iC5dyK*V;X8|Y6IpUk8f4x zDJy0KoJPV1s7q~ZT#sINg#$)COz9(QYd!Kihlh#+A7Kqfk(*)w00fA2qRiJC8CRov z6tVs*pC2yq9;i%jeyTG?L_yy;=P6{=HynGVC*J10#8+(5?%!a0$%G0iVg20s9$K#z zdLT&rJqkGiN><@CXy%@yqoSaQu_Aj~+(Yr2i(c0H`4UM%X`xmfgl&Fm8D~jI7OAbr z(o$zsUQI_&AGm|`-I@IgVYXViB@k~E1(ZlYMTG{mbY2hB)4G2Yf@UkLTOHU#azXc6 zoQl$SyTF=Uovb%eN1ghUN}VUwdZ5SlX)NdRfk|ex*TMYh$MSN{rN%>(@ZyYT&QMi zbsrQQpYY1e$_o3H%$?Bv)+=do7ySm$ke1w4J%xs!p~%2h9a@~sOwLdu0+@oahOQ(xH>G4;T-MXzS)}=n&tNuRPuH(&BH*L~Hj4>S?G%rIL#&`)RYX#V z`3?x*u)%L8aQt8~xD?%Tp|f4MWIcTt-(D+Mlcd#kyL`M2@``w<%KbK84f^$HMH)Ce z0uMKo8GN@A8lAgr0sqt|l{0(n9xZe>Ha354>lXYZa<3Q0a$l^t&l=i(@Xfm%ySu&o za=5$OGc<&Mb9>7#DG7#!IEgGZ`HrM9a9nphacMqH3&$h9bGLBkGOe5e%&yI7<#6_Z z<}ylGPFWy*q-TN{f4$fsM@}M_gnZpEWLe|c`fU(66BW;5y#%yJ2~M*v z!YNyo7Gkw4IL^4eLU5U6=4D0M?=Yo=co2jw+=zixxcdHBKw|19E;xb zpM^U04y1%i@-*&mB3fNn< zaLb+R(5=D0YhRF?n~V=vHlS>bfNP*7p=-XFeY46H(d=hux0B`G@&k_g15`KjZI_Wm z;+MhAHlwuh@$nHp#DW}s1ff8wkkRP;dK|zVUYUA~3R`~hU*1o2T55c5TBVn)Q8u}> z6s{(bU?o6M%K!O=C9&lAVBZHxY)&>{xhLl4qEAmxso#(lT+cgMs=xtUOqVOcd8rW~ z6d*jkLhNac=dzXm$~TZ~f14Dt&%|pW^O~ueqbNi|6g9o+IZ*xT4DhgWrpNaTYS%*r zd1{;itKmq(UuR|uCI&4|Oe!4W(Ipa2Q`oX*nM(MlGv<3rkTN_zyeWZiW66Jtqe@3P zN%POC{g{zjZAK^Dcg;6@l^;M?%Af}^At9^ayM5E({mEkGFHrR}tjlF@(~WDicMlZY zTup?s%9&A)yNczmcI8AdcK!LzEeh~F>h_<4n>NL$X~bjK*B?O#{=0siM5sFfzdBK; zrQVeOaMuoyW0k#8Jo+77B2=AR8a;&esI{dG69q%tKR+}4XEp9wc@2yH(}Hac_8i1{ z`aYXGK5F2VDk};f9wv~tL8y&gfkTU>#Qx0O#eihMhJQKATvmkug-_N90LxhM^%M{? zz-$lr*WN&$0@aw*ZM%3(wUw(h zj9DBsro6no8Q`%9de?C=3yi$-n|LY-Kq#P%Ydx)zFW2q;_6@sYhW7ps-vgn{^{{gR zQf?0OGaNWRwYiS+6hC$2KP78eb!?CT5ZCa>+)F=@9)wB0ydI4z)8*ygzOz6&i2!VQJd%N7 zGhKR&5M8dvy$V=QClK_R2r?vD^ZNNOwmWeB5qFk;mhtlgiKL^L62-8Uanmd%nh91#T7tc36 zKq*Oi7cQigIrO!&Jz*YGyd+({SLNbc3NtGJt`?@e#hWw+EBX|FB;1DDg%9 z2q4P>pI;W1wGqKEH^ll9aTl}1`7ifoDwlI#c&R`zyWS4YV=Ie@gCHR(W@*qxehLaE z47p-I#0gCaGAT@6HR{lxR6fjX?ntU8m9r#8w%QmIGZyg^mkqldL2LWj2zN;! zY4ovfgVFV%;F#EEFyL6PtPv9}o#!JVVgU0)5)o&q;HZUZ*cP<$4QsXfe;l(PBWUXFNwNPb4K`Hy~v7 zlhYs#rM>$j)R;H;jkM|2z(#-grAT4!hnW+H`{#{&0Eyr3`{kWGOC~NLNsD8H4ixC% zCXA9m70%x!^o`~n_n@UE^ZSIJhiI!&y!5$>Io#Phh)@bgBPjop@rLa5^XNB$_={3WmR?d%7KnYNV+UPjQ$-^L~rthLcY zgSY6POXa?bnsp0+`+a!yGqT17t*WJy(pV^1iuD((CJdJRo;n3P32%~TQ;id&M4?Kr zKa1^-nwCLXz})h#Vuo5mfJD8FdHOX>CGxQCuZ8O*sPUcu+qa(APwpKabng~kzCEMO z=jR%e*#E3U?k+U=qpq^!G3%5;^u4o$^v&dlmDy)>_BUPDhC^0gInqZ?Lu5CRYLz{T zXjOPje;n0+np8Ye28H0@;9s0PA8nE}*w@7p)yk$Ug8mH{hh1N{wpRFt`Ud|QekG;J znLan47RkMOLkVfZoGZr_hc#a)nU`prbMzWO5+}?eLijP&($-yLX5##l;AIoZ>YKyp z=p^+F>imodo-KYU59&-cT8Uz6ntZ)$Mef9v%Wtn_>b(w)GD^h-%FY5RVDZ-%<+h>M z5$&xC;WmTyS>zTd3d!*goLZh4%QJ2DG+Y<%J{B2KD(mir%V=E3nm0*AX z+@Yo1sEUgt256;RHC<)amQLCYgD4*NS{Cy|2RNv$Cxmg~G|dp|Y>uL)GV&*Se1nC_ zZdNIjdwHaa92K5;iN}_$x;>LIoSLxt@u0u?=-*!#nzqk;#S%TnIr(>#C~pP?VMD$2 z6|1E+n^#ma6Jqw{A5R<_xR8XEc-RoQxrGHCfRkPa1g(3Zw)0;8R2Y}J6#UU6J$OFh zk?#uBDsIed#_Vg;+^-!@&x#b`nYam)8D+eFq`YFrTYuXkS6y0{} zeckg`kNT7c`9;LB@`;d@!get{A~}Kg$6M~#37~H3C(>-V%bj5{+s&;QAo4Yv@LN;o zBDq8O$2w*MBsFd-m(Vwvro2U^@}^JIRC=kRY%GwD@U;qM3CZZ-bUzZ-(WX&Fc zsO%LwCZm?)zd?S?pKQnBWF2V_VsJAN-8@y?RwW@oA+aSr$;ruG>G0Ti%`^#RB6_rG zCoHs;Y_GBUwn>=*h9$>dHC~~;n3zDU-?E~F`TCB+qeg4k6u$9XA5CSt@UTTfrHYYy$0f%JPpJus|p3iu|&x@60d@kWvp{%6zQs4Hd` z_pAhB0;IcKc7LMoIR$tAuA98fBpH<{OboZ%ZsOU}blxg1)~X75s)-9m@njH#OXwAJ zet(*DOvU~1i#NcLEz`wVR$ar68LiHVWlqh=7@kCS4a11lY^H1e=R8+1u@Kj~S+Fg} zp})`ZdjHe(-Y4DtPqTE>^iWlsFgN?b}r-2-+p+{G8W`vXpMR;(WpWUVLlTA}51- zJ2xoK*yBV7DK5;<(ILli3u6Uf{4K@4(X#;LOF@;=lr=H{l1>CtqCBDSg68PzJZW8V zUkZ@g$becczHARPe(+VfoM?rxu-{>eD#YF|1?~H#h!9ADH=Klh=q2ofPRP{YC+b9x zvjN3K_orDVg;$GF;JpltKec3v!qKp%(;q*E>7zs7zY)ou}K9Lm-gRa!(Dq-}!LAlTQ@+PanWhhC{LNkoH+;Bnf$6HJG<6 z_nIWpGf0X?vOo1OTzob&WZ_ZkC*4YZ)qe-qY|eX%gDW&tf5rw=`z8vQ7(9xwdElO! zn7UlQUl9`%-`#foJ2Bn+FO#cO;>8QngT;pZM@m2ymG2l><-yl(Kre|{d(*$m2_3L2 z-@9AFL7UEN4N(OFHVaJw>!uGErVl#@b>sKvfbLxPS!z%0V{QiD+ojdcU8LWRuY4>n zDcNe4tlIf&bfIOAQmR~(P$yDO5Fy#-$vwbc7Ebx~T}Z^A{*ur{HBN%Z`CV&yl++mZ zR2z!triXv>ui<*Ebgf`Hn`p-d2Ymta>@?@%y1LucILhaXkgr8XKtfjVZD&`dF0YjT zcBbup!LqLbke)>P_Lt1LOgfL@Ps?4aQv~Y1eDsaT!&$56K8s$-CB3-&*1n0!vMD0x z#+J(1iGAj-z&Z$W2xZ_~mj5TBCqavo0PcHVSdimk95BoV;E2LPQnC$-|E4`&233S& zrvE74-{B}P#D7KduDFQ#zy1lfUsF{5l_OY<=uK9QH-D8{5L4E;&utCOE@N?|H z^=F?-PytO7zPb8ax(yv^RfbRPedx_;pOu!DCXZNSy0KxF=#+~^e4g9SZvlAMMgo%ql`L}(>?;nCb3;M#1R;z?ZOUW0sdv2YD zkH#N9@cm-Kk#ThJyJ@Go?C=4o``izHey#I*e6^=8zugIVuuq7NEcFl)lIam0lrUuwLYvCPeXvicx*T$=sP@2pThn&i1js}Ng+tc$4&6t&q} z+*lVU`rZ27=K3wKv1Gt^4-VSRt*lDevqxKjzTxae^$p+Dpnu~1zww9mE4<(n zygT4Fe+U5H)wjj=v~+{liY>Y70rG!ATyk#V;(pK0_b9pY@DqDJ9K0 zZY7Ula%!m(_vU7DY9>K&wrVpXX%qL|s%u{Yqd4M9rObK>aBTiqsaG6qwuVK%u2yKC z;R@V|y8y|tP_GgOfArFHnu`q?t21OJz&s$|ec7R@7%~V;gALaZl_hD|YLk zxCJI)TDT{FUmUj!I%hqrZrRTI^V0WT6*ZM`(w>|#b`Yb>9~S?uAj_xu&q3Gyeiv`h zwqU+ELh|^+rDg6n@G8TAblX-ApGrR2>{92jfJQr1bS!T3ii%x6z`SDm zW{JGXD#Z~U2mp7648eo=|JKPOb z2S%14>_%tvk?k@~UVUh@4}lm}txaR~u0bcygaZN6h5)fF+oFwy#*8Yo+>8=5rylTq z+NV5541KUl^uVycm>n&^7M~lQSeRfpqUgDqebWqkd*pU8qn~a%cBV0_#vG$G7q|*= zKf|r}>QcYK3y-EZziMN<+r}3fRRsrIS#8;X@?8Q(l|`k}n$dawxB{8K_F`g&h0AsS z^g5vY#&ljeA8N3Y@0Rb{J1mH}?Y)DBEEsqYzs=_&+ZL?AktdUz!Q_h3+%Ol3Dj|MB zU|3Mf9TBm{+kWEP=!_SKguGAcWxDBQrPCo}0X;F)W2u%_*lQIE_$N=gObVkxEQeI% zjjw-%^DrzL_!XhP`%q30<_p6>sTeqt zWO0;Q%iX+f<^Z8EF;!$?@ReUbSSTW}d-L=7WIr(`+D<}epVcZLCh6 zwzjq|g0D_xTD9jq&6gTKBS&%p)R$P56Th%1hN4`y`rj41bnOTJZE{$3Tc^3R@h;Bnkq+ieU1sBqZ}s}4Q@jp|S`GUoN$E7%`Xe~!GBNf&^tRE0poF~1VVlW?nS|Gsvlf1SgfG0E!k>I^Rh+jNblYg@ z_;f^9?rNBZv($*?2VDgwW*?}GBbpL*L0M!yX+5cAOZve+q|@ysZNs(0@YLERhbM zt2dSEo#=jF&5}@t(4mS3qZ8>y5+#~|BdvkYYqY9D2iSz#gt@o(7IU7I=>f-NoRL{s z)DILPUB3M^=YWsD2`6&jS2E`+_lcv@TLsleYrac9G9wyH>7$|cMhEJ4@~d(`wv*)A z{d2zU@eRs%A?KV#%jS%9NWiADz)A$FuwlBNK=L2`rpU69olCe1xbDP5>0XJ_=bRZl z=Kfn$X!Zyo{u8P>Zm*Cy%)gJ^lo}_dLQa~wlI88o8A#Bw9XK}73UodZe4&Ufn&WSV z#Ep~3hmeQ<`^v(v#fC0qN`ZKs97jN4yD_=3K?}Hdn@hnKE{pY?`EXRLWcrtYgH!g| z-KYhVa@|GkM?!l?I5gr!H8scq29`_}3o|niKrH|`KF-+&wW9wXSNIdDiZrFDjZx{3 z+y1C(`t*j>D3!uz4bS9wo9wX@9A3z-nv+i$v( zhR5M{;|$(-+JdlMb7ch*BXT@j$7Id>gf29H*r!#BzZHRPs zwV`(UJYwy@p`r1#P?_P#CIT?*AQJ?K6EXwTN2@pu3@r?DO_A(Kw67~2`(F)7u77b+ zvrfftnNU!K)H_zDgP=#{v{5}z%|~!8vY^2+`5*nBp;H5y?#BvZ~)gh|h?|#D7f)wpBkl3gD{# zE|iAeu5TzBuLvox3B>{}FH6b!EEVLvqzT{j0~Elrue{MtbCh*xF4|6jq;e8SwScH# zZi@L{n~|MCE-;vp9klT+Y#mK~4x}V8t}HE2oh@{RVp#pm4Ec-vngLDHqWnqvgI4$3 z9}RqrnXtuA_F1kbRKG5>5$8r-2HzY%Kfwj}*~z`ffmgg$c`_m%eca+oM>S)F)8O#9 z!RWb@EUV4yRBGa_9%>?=C}CnGaNr0Ol@vup$r!!~+G}L__`EPu_p4!r0h7p1$g&D9 ze`(gm&ty5}LKSl14W}{!7&Z*XMW>#sJ)pC37MNGcl?0}QES|Vb;GRf76X_Kgj;hpImCx(l45Cq>Y1@wcwBmc zarHO#SPpX1Af+ZIx8(I{MEC0-eTU)`5)(qtyP2Z<@+SfPA+GwAIkjJnCYWAgs#}QC zX%1Of*~SfB<7meZg(ZY#Xn&Wje~Zo-3zlrRBv zZ|%c`N;vjN$Uqp;z9Rb@!6KaU3v4 z#vjg>@`}p{U_By#-l}xpIuM}|wetFXNEZvoZVWCc)gilk%x`Y36(E`sVyao5$RCLV z@`)u3xz_61=6H$Vn<(++pVv&J)%)hF_sl9NGkY)-HjvW@@IQ_5?Gn;xKLmh4;6JJg za=L9NoGOGfFA08qD0327bp#Y_)z!RoQhtb@LE*RcDmvMaFf|dpJ}@g7pq2ufAj(p* zH=Se^-XNi5(4OZ+AD-MxMa?PP=ZFxs_|RFd_s_CaIS^Q6K2#9QBK$v*m~Vekg>+F{ z8#K79yPon8k*98|s}n9jOqH7qE%PEhlnU_KvYQ;n0yV!|%-+5)sGZCfm6m=#XIP^m z@Ak4(OP1fD6ys@XShXYZ$CLqucf4=Zmq_;b9geHfy=c1N6!`jA1x&`Q%pM_2U=nj; zO!}iy!$f;N!zt&z(P1quX{kL+{`gZQjdb-~%y**}W9b3;cn12skIT8yk|wA+Cc!Dq zU9i1KclTxBDC&iBOZAc7%iI=E_mpg_U{B|=D_53yG zUD)-)6E8w@3u8-&7^bRQ`I_|r+X_Ei$`HCPj=Mb69mJwHSt~8Zj1BN2s`rZ+8a>h6 zXF~S}I|qp!)8D>{IW#aS7}R+^R~dqS6UR#rIrbOxJLk~8^^&)wosRQgdl|xw!D+=Y z14(k^V48^Gzc zUwmx}$gRdLU0s>dc!N0S&VCyQ&p^epp~METUNf))Cy(J*Z1PQVyFMXbcVZ)WIW2J+ zJ;r5>G+=ZL2ovb(6J7=+94}GZC@d>X)rZYGHiQii6M(w28XWoOm!gLgsCr%+41J6p zI`Sh?R#xT*$WI>QQh2me{CBOQPdhxx>m6{Iu(L&FqSQp<8Y<^Ph%i*~*KQ%8fVuvu zH~F~C6o5d^e~TOpr( zp8F+Dt#2b%mvF|9xMP7wr3F(R18pyHf6*Ok*l|qR*+=g1@(}FTnK(bZ_lM)LtTGTK zndKFu;=(GCEGPLgY1pbyl{sdz`00rxDG(WO7Z~{I@AH9a9CY`njoI;1OxET`i}O;A zTOTe|No&DJI(B)Psgo1}urR0r;Q_b)=ZMv1Qrq7&C$ml={*(L3YOGLcaRL}M!bG{4 z;>|-h3sAYELUzkzS`hVTIoX!K=?0A`JbZG{_7HkL)`Hz%47SP*cRUs#Ei?1N?tEU@ zS)gtJqYD(g)A;zM=(s(0u39*WxmKAKMa6|wk&lGH_P}!)iS&{5id1s)PrBbh z7_%<(5HEtz&ip%YT#zrH5O0nDI4=MO6c^W#OJ-ta=P4bRM$6_DmLTs{z{N^?oAg4B&vx_*^u3_>Wdp$&RlD( z#elFH;s7ohtK>L9C3H-}BihhkBGFaAD0o{5OkjA@)}#<8W)R!XW>Q^Cv(iX_*cEe1 z@Y%FWI!VLBxcf}jpeD`KXB`Ki0iIuyc8_eysS}mzGLaQ%1IE(y1Jk1{cF&6Or{fU^W>iRHPc-x;CZZCU}zI7u=WZKcT5y-r1QC3d;c6b3;=&k`n zMmbYc#?t+Z5Ph*s1(bGZ_5OTkp#Owr!WJVe-fOzyrazn%HidvD^fCeUeR%k{7#or7 zzAY<=9>8~J)#22HulfIDd$Rq-?`V;LaI{jbaDwvN^pRVda~GH$wV3krKP?k)9J?oR zrW0jcs%nOfnBf%2SO?ysJ^V*i*basK_geBe=HypaDS*+L0Ya*Qo$WvLZr(MQ69c(kFL7yYGDmc$u!V;X zcp3j?b?(b)nR)g@RV_4x1AR~@f_t$SMd@qc`pIL=+ac@TvV3|uCl39}%kp=|#Bl2C z)OP{&CUeQdR!~7ffV~&KJ_0Go8~qJ14l%--mwzp`Ltsp)qoVSnzn#k7S-6w61Ta`` zZbHF;&PVF+kQ{qbx6pIyDBwx=DsoLs0Cs4z#$CzL+re1FY;qalSLF#Js6-;R>!@nc zs>I0ZTi6#t+gKSdCT!`;4-Q#DI$XJ($MnZOgK_`Om0uCY6pEN?UupCGjPX(1zXRsv zZpN%By9=ilKgRxYmM;?eE1BycCBG+0zZ9Vka*XS!t)llK$ACn-qcC9vJX0_iQ@&q* zhb1)o0daRel0lX&)x#4{HF`vjPqn$2`F_7&Dc19b>hM?uSdgK#?Y+x&Z*W2Xc9dl# z?Bm}0i#PmCePmPb4@Ob681uH$zE?5{&rST1xE>v29N9mzi~8Kf zLk)S%EdXh^+5%#hJhU%{YTG_nA5<6EDPvNX#-2SmR5^a(Y1)61o`3w;8h?diRdw!A zteQPzhESh2!j7qQlFjr)udbO42F|u zlA>K%sFsI$rbg3C=qm-qR}X%le%m9y9?;Y4zArd3iE!rU8V?3@LNRBT?NeiB8(3W)@|8T1`r<8w~V z%y?ea0Z{cK;MDtTq1BW7h5OcffJNu@I8M>S!GTMwat4sC%5z$Cp~&FnCE8rYNI2FI zW(|lH#EN0{QQH@UgVGiGC3l|IZypislJ#C6zHSw*%6{X<#~u>^XgcSX!e?h;BaYn6 z3a>?fpo#mMN4_51&H(`}zkq-v-Cmb9H)E`~t9M73D~sT z2Hl_KKis+Wrr%9OODh1?Zi7)(smGW)bHwg#5DcD)ZT1Kj@0!f|gi5tp)(JGZ>JE}X zYPYy**}*w;Aw^hF+MpENO{s0(Kkky}umY5mNL*(pg9_(INYT5jSq=56BMsIR((1Pz ze*3^u35BnI-F*FwDtP0=esx=78k}v^)1>b*FxBIo#~3-h+^RV)V;#uJiU7Vh4agAo zo}L#;5u!%n5ZUzx`7;QmYr^@P3?>A7uYq4X@>>9o(rzP+CWGk`Ff)L9s5e`T$KJGB zoj!x@jsr(+rQJ4%f%UR`=iE_1kjhzq}IKIY0gRAv82JpNRQ>%+usbnyPagr40w< zOPQlK#}a{28jNKhrUrL6&)sgs0&}GQzmJ1~{~pX&8DD0ECxn22KUGByg=&~t*#7_> C?@{#t literal 0 HcmV?d00001 From e6295bbfe813f9331354ccbfbe2aae4aa0b79aab Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 28 Jun 2021 07:44:59 +0200 Subject: [PATCH 15/37] Fix of wxWidgets3.0 again wxGetApp not declared in this scope #6655 --- src/slic3r/GUI/GUI_Utils.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/GUI_Utils.cpp b/src/slic3r/GUI/GUI_Utils.cpp index 78548e111..89c81fd2a 100644 --- a/src/slic3r/GUI/GUI_Utils.cpp +++ b/src/slic3r/GUI/GUI_Utils.cpp @@ -1,15 +1,15 @@ #include "GUI_Utils.hpp" +#include "GUI_App.hpp" #include #include #include #ifdef _WIN32 -#include -#include "GUI_App.hpp" -#include "libslic3r/AppConfig.hpp" -#include -#endif + #include + #include "libslic3r/AppConfig.hpp" + #include +#endif // _WIN32 #include #include From 23b26cb3f3369967d4743b78608e81eda7986d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Fri, 25 Jun 2021 18:13:03 +0200 Subject: [PATCH 16/37] Fixed the issue where the object wasn't displayed after pressing the ESC key in the multi-material painting gizmo. key in the multi-material painting gizmo. Fixed also a similar issue in support and seam gizmos where an object was displayed with the wrong color after pressing the ESC key. --- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp | 7 +++++++ src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp | 2 +- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 3274f000c..5fa4ab51d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -21,6 +21,7 @@ void GLGizmoFdmSupports::on_shutdown() { m_angle_threshold_deg = 0.f; m_parent.use_slope(false); + m_parent.toggle_model_objects_visibility(true); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index c6dced670..2ebf0d26e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -19,6 +19,7 @@ namespace Slic3r::GUI { void GLGizmoMmuSegmentation::on_shutdown() { m_parent.use_slope(false); + m_parent.toggle_model_objects_visibility(true); } std::string GLGizmoMmuSegmentation::on_get_name() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp index d3c0c7d04..6b28e8ca7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -16,6 +16,13 @@ namespace Slic3r::GUI { +void GLGizmoSeam::on_shutdown() +{ + m_parent.toggle_model_objects_visibility(true); +} + + + bool GLGizmoSeam::on_init() { m_shortcut_key = WXK_CONTROL_P; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp index d97bad10f..196fe5023 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp @@ -27,7 +27,7 @@ private: void update_from_model_object() override; void on_opening() override {} - void on_shutdown() override {} + void on_shutdown() override; // This map holds all translated description texts, so they can be easily referenced during layout calculations // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. From 48789e5ae12fd4e4182eab8355522e4e801595c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Fri, 25 Jun 2021 18:17:18 +0200 Subject: [PATCH 17/37] Added a limitation on the number of usable extruders in the multi-material gizmo. If a printer has more extruders than this limit, a notification about it is shown. --- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 22 ++++++++++++++++++- .../GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 8 ++++++- src/slic3r/GUI/NotificationManager.hpp | 4 +++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 2ebf0d26e..2abc35344 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -8,6 +8,7 @@ #include "slic3r/GUI/BitmapCache.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/NotificationManager.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" @@ -16,6 +17,22 @@ namespace Slic3r::GUI { +static inline void show_notification_extruders_limit_exceeded() +{ + wxGetApp() + .plater() + ->get_notification_manager() + ->push_notification(NotificationType::MmSegmentationExceededExtrudersLimit, NotificationManager::NotificationLevel::RegularNotification, + GUI::format(_L("Your printer has more extruders than the multi-material painting gizmo supports. For this reason, only the " + "first %1% extruders will be able to be used for painting."), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)); +} + +void GLGizmoMmuSegmentation::on_opening() +{ + if (wxGetApp().extruders_edited_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)) + show_notification_extruders_limit_exceeded(); +} + void GLGizmoMmuSegmentation::on_shutdown() { m_parent.use_slope(false); @@ -132,6 +149,9 @@ void GLGizmoMmuSegmentation::set_painter_gizmo_data(const Selection &selection) ModelObject *model_object = m_c->selection_info()->model_object(); int prev_extruders_count = int(m_original_extruders_colors.size()); if (prev_extruders_count != wxGetApp().extruders_edited_cnt() || get_extruders_colors() != m_original_extruders_colors) { + if (wxGetApp().extruders_edited_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)) + show_notification_extruders_limit_exceeded(); + this->init_extruders_data(); // Reinitialize triangle selectors because of change of extruder count need also change the size of GLIndexedVertexArray if (prev_extruders_count != wxGetApp().extruders_edited_cnt()) @@ -158,7 +178,7 @@ static void render_extruders_combo(const std::string &labe ImGui::BeginGroup(); ImVec2 combo_pos = ImGui::GetCursorScreenPos(); if (ImGui::BeginCombo(label.c_str(), "")) { - for (size_t extruder_idx = 0; extruder_idx < extruders.size(); ++extruder_idx) { + for (size_t extruder_idx = 0; extruder_idx < std::min(extruders.size(), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT); ++extruder_idx) { ImGui::PushID(int(extruder_idx)); ImVec2 start_position = ImGui::GetCursorScreenPos(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index f5c97801b..5ece0ec2a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -36,6 +36,12 @@ public: void set_painter_gizmo_data(const Selection& selection) override; + // TriangleSelector::serialization/deserialization has a limit to store 19 different states. + // EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored. + // When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization + // will be also extended to support additional states, requiring at least one state to remain free out of 19 states. + static const constexpr size_t EXTRUDERS_LIMIT = 16; + protected: std::array get_cursor_sphere_left_button_color() const override; std::array get_cursor_sphere_right_button_color() const override; @@ -63,7 +69,7 @@ private: void update_model_object() const override; void update_from_model_object() override; - void on_opening() override {} + void on_opening() override; void on_shutdown() override; PainterGizmoType get_painter_type() const override; diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 1bcb93de0..17db606c0 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -87,7 +87,9 @@ enum class NotificationType DesktopIntegrationSuccess, DesktopIntegrationFail, UndoDesktopIntegrationSuccess, - UndoDesktopIntegrationFail + UndoDesktopIntegrationFail, + // Notification that a printer has more extruders than are supported by MM Gizmo/segmentation. + MmSegmentationExceededExtrudersLimit }; From ce738102c6d22b4d92621666e1c2f49ba998eb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Fri, 25 Jun 2021 18:20:17 +0200 Subject: [PATCH 18/37] Optimized the projection of painted triangles in multi-material segmentation. Added heuristics to skip most of the expensive calculations in cases where it is certain that performing these calculations would be useless. --- src/libslic3r/MultiMaterialSegmentation.cpp | 43 +++++++++++++-------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 2fbdf85bc..8edfe946e 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -89,28 +89,37 @@ struct PaintedLineVisitor bool operator()(coord_t iy, coord_t ix) { // Called with a row and column of the grid cell, which is intersected by a line. - auto cell_data_range = grid.cell_data_range(iy, ix); - const Vec2d v1 = line_to_test.vector().cast(); + auto cell_data_range = grid.cell_data_range(iy, ix); + const Vec2d v1 = line_to_test.vector().cast(); + const double v1_sqr_norm = v1.squaredNorm(); + const double heuristic_thr_part = line_to_test.length() + append_threshold; for (auto it_contour_and_segment = cell_data_range.first; it_contour_and_segment != cell_data_range.second; ++it_contour_and_segment) { - Line grid_line = grid.line(*it_contour_and_segment); - const Vec2d v2 = grid_line.vector().cast(); + Line grid_line = grid.line(*it_contour_and_segment); + const Vec2d v2 = grid_line.vector().cast(); + double heuristic_thr_sqr = Slic3r::sqr(heuristic_thr_part + grid_line.length()); + + // An inexpensive heuristic to test whether line_to_test and grid_line can be somewhere close enough to each other. + // This helps filter out cases when the following expensive calculations are useless. + if ((grid_line.a - line_to_test.a).cast().squaredNorm() > heuristic_thr_sqr || + (grid_line.b - line_to_test.a).cast().squaredNorm() > heuristic_thr_sqr || + (grid_line.a - line_to_test.b).cast().squaredNorm() > heuristic_thr_sqr || + (grid_line.b - line_to_test.b).cast().squaredNorm() > heuristic_thr_sqr) + continue; + // When lines have too different length, it is necessary to normalize them - if (Slic3r::sqr(v1.dot(v2)) > cos_threshold2 * v1.squaredNorm() * v2.squaredNorm()) { + if (Slic3r::sqr(v1.dot(v2)) > cos_threshold2 * v1_sqr_norm * v2.squaredNorm()) { // The two vectors are nearly collinear (their mutual angle is lower than 30 degrees) if (painted_lines_set.find(*it_contour_and_segment) == painted_lines_set.end()) { - double dist_1 = grid_line.distance_to(line_to_test.a); - double dist_2 = grid_line.distance_to(line_to_test.b); - double dist_3 = line_to_test.distance_to(grid_line.a); - double dist_4 = line_to_test.distance_to(grid_line.b); - double total_dist = std::min(std::min(dist_1, dist_2), std::min(dist_3, dist_4)); - - if (total_dist < 50 * SCALED_EPSILON) { + if (grid_line.distance_to_squared(line_to_test.a) < append_threshold2 || + grid_line.distance_to_squared(line_to_test.b) < append_threshold2 || + line_to_test.distance_to_squared(grid_line.a) < append_threshold2 || + line_to_test.distance_to_squared(grid_line.b) < append_threshold2) { Line line_to_test_projected; project_line_on_line(grid_line, line_to_test, &line_to_test_projected); - if (Line(grid_line.a, line_to_test_projected.a).length() > Line(grid_line.a, line_to_test_projected.b).length()) { + if ((line_to_test_projected.a - grid_line.a).cast().squaredNorm() > (line_to_test_projected.b - grid_line.a).cast().squaredNorm()) line_to_test_projected.reverse(); - } + painted_lines.push_back({it_contour_and_segment->first, it_contour_and_segment->second, line_to_test_projected, this->color}); painted_lines_set.insert(*it_contour_and_segment); } @@ -125,9 +134,11 @@ struct PaintedLineVisitor std::vector &painted_lines; Line line_to_test; std::unordered_set, boost::hash>> painted_lines_set; - int color = -1; + int color = -1; - static inline const double cos_threshold2 = Slic3r::sqr(cos(M_PI * 30. / 180.)); + static inline const double cos_threshold2 = Slic3r::sqr(cos(M_PI * 30. / 180.)); + static inline const double append_threshold = 50 * SCALED_EPSILON; + static inline const double append_threshold2 = Slic3r::sqr(append_threshold); }; static std::vector to_colored_lines(const Polygon &polygon, int color) From a426093f1200dd4178a86838cd91eefc0fd759b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Fri, 25 Jun 2021 18:28:33 +0200 Subject: [PATCH 19/37] Replaced boost::rtree in multi-material segmentation with much faster ClosestPointInRadiusLookup. --- src/libslic3r/MultiMaterialSegmentation.cpp | 152 +++++++++----------- 1 file changed, 67 insertions(+), 85 deletions(-) diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 8edfe946e..1c700c3ee 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -10,14 +10,8 @@ #include #include - #include -#include -#include -#include -#include - namespace Slic3r { struct ColoredLine { Line line; @@ -165,6 +159,7 @@ static Polygon colored_points_to_polygon(const std::vector &lines) static Polygons colored_points_to_polygon(const std::vector> &lines) { Polygons out; + out.reserve(lines.size()); for (const std::vector &l : lines) out.emplace_back(colored_points_to_polygon(l)); return out; @@ -495,6 +490,12 @@ static std::vector> colorize_polygons(const Polygons &p using boost::polygon::voronoi_diagram; +static inline Point mk_point(const Voronoi::VD::vertex_type *point) { return Point(coord_t(point->x()), coord_t(point->y())); } + +static inline Point mk_point(const Voronoi::Internal::point_type &point) { return Point(coord_t(point.x()), coord_t(point.y())); } + +static inline Point mk_point(const voronoi_diagram::vertex_type &point) { return Point(coord_t(point.x()), coord_t(point.y())); } + struct MMU_Graph { enum class ARC_TYPE { BORDER, NON_BORDER }; @@ -627,24 +628,63 @@ struct MMU_Graph { return this->is_vertex_on_contour(edge_iterator->vertex0()) && this->is_vertex_on_contour(edge_iterator->vertex1()); } + + // All Voronoi vertices are post-processes to merge very close vertices to single. Witch eliminates issues with intersection edges. + // Also, Voronoi vertices outside of the bounding of input polygons are throw away by marking them. + void append_voronoi_vertices(const Geometry::VoronoiDiagram &vd, const Polygons &color_poly_tmp, BoundingBox bbox) { + bbox.offset(SCALED_EPSILON); + + struct CPoint + { + CPoint() = delete; + CPoint(const Point &point, size_t contour_idx, size_t point_idx) : m_point(point), m_point_idx(point_idx), m_contour_idx(contour_idx) {} + CPoint(const Point &point, size_t point_idx) : m_point(point), m_point_idx(point_idx), m_contour_idx(0) {} + const Point m_point; + size_t m_point_idx; + size_t m_contour_idx; + + [[nodiscard]] const Point &point() const { return m_point; } + bool operator==(const CPoint &rhs) const { return this->m_point == rhs.m_point && this->m_contour_idx == rhs.m_contour_idx && this->m_point_idx == rhs.m_point_idx; } + }; + struct CPointAccessor { const Point* operator()(const CPoint &pt) const { return &pt.point(); }}; + typedef ClosestPointInRadiusLookup CPointLookupType; + + CPointLookupType closest_voronoi_point(3 * coord_t(SCALED_EPSILON)); + CPointLookupType closest_contour_point(3 * coord_t(SCALED_EPSILON)); + for (const Polygon &polygon : color_poly_tmp) + for (const Point &pt : polygon.points) + closest_contour_point.insert(CPoint(pt, &polygon - &color_poly_tmp.front(), &pt - &polygon.points.front())); + + for (const voronoi_diagram::vertex_type &vertex : vd.vertices()) { + vertex.color(-1); + Point vertex_point = mk_point(vertex); + + const Point &first_point = this->nodes[this->get_arc(vertex.incident_edge()->cell()->source_index()).from_idx].point; + const Point &second_point = this->nodes[this->get_arc(vertex.incident_edge()->twin()->cell()->source_index()).from_idx].point; + + if (vertex_equal_to_point(&vertex, first_point)) { + assert(vertex.color() != vertex.incident_edge()->cell()->source_index()); + assert(vertex.color() != vertex.incident_edge()->twin()->cell()->source_index()); + vertex.color(this->get_arc(vertex.incident_edge()->cell()->source_index()).from_idx); + } else if (vertex_equal_to_point(&vertex, second_point)) { + assert(vertex.color() != vertex.incident_edge()->cell()->source_index()); + assert(vertex.color() != vertex.incident_edge()->twin()->cell()->source_index()); + vertex.color(this->get_arc(vertex.incident_edge()->twin()->cell()->source_index()).from_idx); + } else if (bbox.contains(vertex_point)) { + if (auto [contour_pt, c_dist_sqr] = closest_contour_point.find(vertex_point); contour_pt != nullptr && c_dist_sqr < 3 * SCALED_EPSILON) { + vertex.color(this->get_global_index(contour_pt->m_contour_idx, contour_pt->m_point_idx)); + } else if (auto [voronoi_pt, v_dist_sqr] = closest_voronoi_point.find(vertex_point); voronoi_pt == nullptr || v_dist_sqr >= 3 * SCALED_EPSILON) { + closest_voronoi_point.insert(CPoint(vertex_point, this->nodes_count())); + vertex.color(this->nodes_count()); + this->nodes.push_back({vertex_point}); + } else { + vertex.color(voronoi_pt->m_point_idx); + } + } + } + } }; -namespace bg = boost::geometry; -namespace bgm = boost::geometry::model; -namespace bgi = boost::geometry::index; - -// float is needed because for coord_t bgi::intersects throws "bad numeric conversion: positive overflow" -using rtree_point_t = bgm::point; -using rtree_t = bgi::rtree, bgi::rstar<16, 4>>; - -static inline rtree_point_t mk_rtree_point(const Point &pt) { return rtree_point_t(float(pt.x()), float(pt.y())); } - -static inline Point mk_point(const Voronoi::VD::vertex_type *point) { return Point(coord_t(point->x()), coord_t(point->y())); } - -static inline Point mk_point(const Voronoi::Internal::point_type &point) { return Point(coord_t(point.x()), coord_t(point.y())); } - -static inline Point mk_point(const voronoi_diagram::vertex_type &point) { return Point(coord_t(point.x()), coord_t(point.y())); } - static inline void mark_processed(const voronoi_diagram::const_edge_iterator &edge_iterator) { edge_iterator->color(true); @@ -706,7 +746,7 @@ static MMU_Graph build_graph(size_t layer_idx, const std::vector lines_colored = to_lines(color_poly); - Polygons color_poly_tmp = colored_points_to_polygon(color_poly); + const Polygons color_poly_tmp = colored_points_to_polygon(color_poly); const Points points = to_points(color_poly_tmp); const Lines lines = to_lines(color_poly_tmp); @@ -730,6 +770,7 @@ static MMU_Graph build_graph(size_t layer_idx, const std::vector void { - auto is_equal_points = [](const Point &p1, const Point &p2) { return p1 == p2 || (p1 - p2).cast().norm() <= 3 * SCALED_EPSILON; }; - - BoundingBox bbox = get_extents(color_poly_tmp); - bbox.offset(SCALED_EPSILON); - // EdgeGrid is used for vertices near to contour and rtree for other vertices - // FIXME Lukas H.: Get rid of EdgeGrid and rtree. Use only one structure for both cases. - EdgeGrid::Grid grid; - grid.set_bbox(bbox); - grid.create(color_poly_tmp, coord_t(scale_(10.))); - rtree_t rtree; - for (const voronoi_diagram::vertex_type &vertex : vd.vertices()) { - vertex.color(-1); - Point vertex_point = mk_point(vertex); - - const Point &first_point = graph.nodes[graph.get_arc(vertex.incident_edge()->cell()->source_index()).from_idx].point; - const Point &second_point = graph.nodes[graph.get_arc(vertex.incident_edge()->twin()->cell()->source_index()).from_idx].point; - - if (vertex_equal_to_point(&vertex, first_point)) { - assert(vertex.color() != vertex.incident_edge()->cell()->source_index()); - assert(vertex.color() != vertex.incident_edge()->twin()->cell()->source_index()); - vertex.color(graph.get_arc(vertex.incident_edge()->cell()->source_index()).from_idx); - } else if (vertex_equal_to_point(&vertex, second_point)) { - assert(vertex.color() != vertex.incident_edge()->cell()->source_index()); - assert(vertex.color() != vertex.incident_edge()->twin()->cell()->source_index()); - vertex.color(graph.get_arc(vertex.incident_edge()->twin()->cell()->source_index()).from_idx); - } else if (bbox.contains(vertex_point)) { - EdgeGrid::Grid::ClosestPointResult cp = grid.closest_point_signed_distance(vertex_point, coord_t(3 * SCALED_EPSILON)); - if (cp.valid()) { - size_t global_idx = graph.get_global_index(cp.contour_idx, cp.start_point_idx); - size_t global_idx_next = graph.get_global_index(cp.contour_idx, (cp.start_point_idx + 1) % color_poly_tmp[cp.contour_idx].points.size()); - vertex.color(is_equal_points(vertex_point, graph.nodes[global_idx].point) ? global_idx : global_idx_next); - } else { - if (rtree.empty()) { - rtree.insert(std::make_pair(mk_rtree_point(vertex_point), graph.nodes_count())); - vertex.color(graph.nodes_count()); - graph.nodes.push_back({vertex_point}); - } else { - std::vector> closest; - rtree.query(bgi::nearest(mk_rtree_point(vertex_point), 1), std::back_inserter(closest)); - assert(!closest.empty()); - rtree_point_t r_point = closest.front().first; - Point closest_p(bg::get<0>(r_point), bg::get<1>(r_point)); - if (Line(vertex_point, closest_p).length() > 3 * SCALED_EPSILON) { - rtree.insert(std::make_pair(mk_rtree_point(vertex_point), graph.nodes_count())); - vertex.color(graph.nodes_count()); - graph.nodes.push_back({vertex_point}); - } else { - vertex.color(closest.front().second); - } - } - } - } - } - }; - - append_voronoi_vertices_to_graph(); + BoundingBox bbox = get_extents(color_poly_tmp); + graph.append_voronoi_vertices(vd, color_poly_tmp, bbox); auto get_prev_contour_line = [&lines_colored, &color_poly, &graph](const voronoi_diagram::const_edge_iterator &edge_it) -> ColoredLine { size_t contour_line_local_idx = lines_colored[edge_it->cell()->source_index()].local_line_idx; @@ -814,7 +797,6 @@ static MMU_Graph build_graph(size_t layer_idx, const std::vector>> multi_material_segmentati // [P0, P2] a [P0, P1] float t1 = (float(layer->slice_z) - facet[0].z()) / (facet[1].z() - facet[0].z()); line_end_f = facet[0] + t1 * (facet[1] - facet[0]); - } else if (facet[1].z() <= layer->slice_z) { + } else { // [P0, P2] a [P1, P2] float t2 = (float(layer->slice_z) - facet[1].z()) / (facet[2].z() - facet[1].z()); line_end_f = facet[1] + t2 * (facet[2] - facet[1]); From cb93c8ce9947a094354df00a56f8b2c8f90bbb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Fri, 25 Jun 2021 18:33:57 +0200 Subject: [PATCH 20/37] Used function smooth_outward in multi-material segmentation to get rid of artifacts arisen after merging multi-volume objects. --- src/libslic3r/MultiMaterialSegmentation.cpp | 11 ++++++----- src/libslic3r/MutablePolygon.hpp | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 1c700c3ee..d9c79a44e 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -4,6 +4,7 @@ #include "Layer.hpp" #include "Print.hpp" #include "VoronoiVisualUtils.hpp" +#include "MutablePolygon.hpp" #include #include @@ -1421,7 +1422,7 @@ std::vector>> multi_material_segmentati // All expolygons are expanded by SCALED_EPSILON, merged, and then shrunk again by SCALED_EPSILON // to ensure that very close polygons will be merged. ex_polygons = union_ex(ex_polygons); - // Remove all expolygons and holes with an area less than 0.01mm^2 + // Remove all expolygons and holes with an area less than 0.1mm^2 remove_small_and_small_holes(ex_polygons, Slic3r::sqr(scale_(0.1f))); // Occasionally, some input polygons contained self-intersections that caused problems with Voronoi diagrams // and consequently with the extraction of colored segments by function extract_colored_segments. @@ -1430,19 +1431,19 @@ std::vector>> multi_material_segmentati // Such close points sometimes caused that the Voronoi diagram has self-intersecting edges around these vertices. // This consequently leads to issues with the extraction of colored segments by function extract_colored_segments. // Calling expolygons_simplify fixed these issues. - input_expolygons[layer_idx] = simplify_polygons_ex(to_polygons(expolygons_simplify(offset_ex(ex_polygons, float(-10 * SCALED_EPSILON)), 5 * SCALED_EPSILON))); - input_polygons[layer_idx] = to_polygons(input_expolygons[layer_idx]); + input_expolygons[layer_idx] = smooth_outward(expolygons_simplify(offset_ex(ex_polygons, -10.f * float(SCALED_EPSILON)), 5 * SCALED_EPSILON), 10 * coord_t(SCALED_EPSILON)); + input_polygons[layer_idx] = to_polygons(input_expolygons[layer_idx]); } }); // end of parallel_for BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - slices preparation in parallel - end"; for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) { throw_on_cancel_callback(); - BoundingBox bbox(get_extents(input_expolygons[layer_idx])); + BoundingBox bbox(get_extents(input_polygons[layer_idx])); // Projected triangles may slightly exceed the input polygons. bbox.offset(20 * SCALED_EPSILON); edge_grids[layer_idx].set_bbox(bbox); - edge_grids[layer_idx].create(input_expolygons[layer_idx], coord_t(scale_(10.))); + edge_grids[layer_idx].create(input_polygons[layer_idx], coord_t(scale_(10.))); } BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - projection of painted triangles - begin"; diff --git a/src/libslic3r/MutablePolygon.hpp b/src/libslic3r/MutablePolygon.hpp index 14d7787cf..1b2b4e445 100644 --- a/src/libslic3r/MutablePolygon.hpp +++ b/src/libslic3r/MutablePolygon.hpp @@ -3,6 +3,7 @@ #include "Point.hpp" #include "Polygon.hpp" +#include "ExPolygon.hpp" namespace Slic3r { @@ -330,6 +331,24 @@ inline Polygons smooth_outward(Polygons polygons, coord_t clip_dist_scaled) return polygons; } +inline ExPolygons smooth_outward(ExPolygons expolygons, coord_t clip_dist_scaled) +{ + MutablePolygon mp; + for (ExPolygon &expolygon : expolygons) { + mp.assign(expolygon.contour, expolygon.contour.size() * 2); + smooth_outward(mp, clip_dist_scaled); + mp.polygon(expolygon.contour); + for (Polygon &hole : expolygon.holes) { + mp.assign(hole, hole.size() * 2); + smooth_outward(mp, clip_dist_scaled); + mp.polygon(hole); + } + expolygon.holes.erase(std::remove_if(expolygon.holes.begin(), expolygon.holes.end(), [](const auto &p) { return p.empty(); }), expolygon.holes.end()); + } + expolygons.erase(std::remove_if(expolygons.begin(), expolygons.end(), [](const auto &p) { return p.empty(); }), expolygons.end()); + return expolygons; +} + } #endif // slic3r_MutablePolygon_hpp_ From b14345012da71198de4ddd2009895d16440bf491 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 28 Jun 2021 12:33:22 +0200 Subject: [PATCH 21/37] PrusaLink - cherrypick pick from stable branch Added to config enums, visible in Physical Printer Dialog and class derived from Octoprint --- src/libslic3r/PrintConfig.cpp | 3 + src/libslic3r/PrintConfig.hpp | 2 +- src/slic3r/GUI/Field.cpp | 11 ++- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 88 +++++++++++++++++++++--- src/slic3r/GUI/PhysicalPrinterDialog.hpp | 6 +- src/slic3r/Utils/OctoPrint.cpp | 44 ++++++++++++ src/slic3r/Utils/OctoPrint.hpp | 25 +++++++ src/slic3r/Utils/PrintHost.cpp | 1 + 8 files changed, 164 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d8fc3083c..a1d86d469 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -67,6 +67,7 @@ static t_config_enum_values s_keys_map_MachineLimitsUsage { CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(MachineLimitsUsage) static t_config_enum_values s_keys_map_PrintHostType { + { "prusalink", htPrusaLink }, { "octoprint", htOctoPrint }, { "duet", htDuet }, { "flashair", htFlashAir }, @@ -1779,11 +1780,13 @@ void PrintConfigDef::init_fff_params() def->tooltip = L("Slic3r can upload G-code files to a printer host. This field must contain " "the kind of the host."); def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("prusalink"); def->enum_values.push_back("octoprint"); def->enum_values.push_back("duet"); def->enum_values.push_back("flashair"); def->enum_values.push_back("astrobox"); def->enum_values.push_back("repetier"); + def->enum_labels.push_back("PrusaLink"); def->enum_labels.push_back("OctoPrint"); def->enum_labels.push_back("Duet"); def->enum_labels.push_back("FlashAir"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 890f8518e..efe71822b 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -44,7 +44,7 @@ enum class MachineLimitsUsage { }; enum PrintHostType { - htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier + htPrusaLink, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier }; enum AuthorizationType { diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 532e44e70..e194898ee 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -1141,6 +1141,10 @@ void Choice::set_value(const boost::any& value, bool change_event) } case coEnum: { int val = boost::any_cast(value); + if (m_opt_id.compare("host_type") == 0 && val != 0 && + m_opt.enum_values.size() > field->GetCount()) // for case, when PrusaLink isn't used as a HostType + val--; + if (m_opt_id == "top_fill_pattern" || m_opt_id == "bottom_fill_pattern" || m_opt_id == "fill_pattern") { std::string key; @@ -1197,7 +1201,7 @@ void Choice::set_values(const wxArrayString &values) auto ww = dynamic_cast(window); auto value = ww->GetValue(); ww->Clear(); - ww->Append(""); +// ww->Append(""); for (const auto &el : values) ww->Append(el); ww->SetValue(value); @@ -1219,7 +1223,10 @@ boost::any& Choice::get_value() if (m_opt.type == coEnum) { - if (m_opt_id == "top_fill_pattern" || m_opt_id == "bottom_fill_pattern" || m_opt_id == "fill_pattern") { + if (m_opt_id.compare("host_type") == 0 && m_opt.enum_values.size() > field->GetCount()) { + // for case, when PrusaLink isn't used as a HostType + m_value = field->GetSelection()+1; + } else if (m_opt_id == "top_fill_pattern" || m_opt_id == "bottom_fill_pattern" || m_opt_id == "fill_pattern") { const std::string& key = m_opt.enum_values[field->GetSelection()]; m_value = int(ConfigOptionEnum::get_enum_values().at(key)); } diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index bb0191b4d..7d20b15e5 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -68,7 +68,8 @@ PresetForPrinter::PresetForPrinter(PhysicalPrinterDialog* parent, const std::str // update Print Host upload from the selected preset m_parent->get_printer()->update_from_preset(*preset); // update values in parent (PhysicalPrinterDialog) - m_parent->update(); + m_parent->update(true); + } // update PrinterTechnology if it was changed @@ -154,7 +155,8 @@ void PresetForPrinter::msw_rescale() PhysicalPrinterDialog::PhysicalPrinterDialog(wxWindow* parent, wxString printer_name) : DPIDialog(parent, wxID_ANY, _L("Physical Printer"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), -1), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), - m_printer("", wxGetApp().preset_bundle->physical_printers.default_config()) + m_printer("", wxGetApp().preset_bundle->physical_printers.default_config()), + had_all_mk3(!printer_name.empty()) { SetFont(wxGetApp().normal_font()); #ifndef _WIN32 @@ -455,7 +457,7 @@ void PhysicalPrinterDialog::update_printhost_buttons() m_printhost_browse_btn->Enable(host->has_auto_discovery()); } -void PhysicalPrinterDialog::update() +void PhysicalPrinterDialog::update(bool printer_change) { m_optgroup->reload_config(); @@ -463,13 +465,24 @@ void PhysicalPrinterDialog::update() // Only offer the host type selection for FFF, for SLA it's always the SL1 printer (at the moment) bool supports_multiple_printers = false; if (tech == ptFFF) { - m_optgroup->show_field("host_type"); - m_optgroup->hide_field("printhost_authorization_type"); - m_optgroup->show_field("printhost_apikey", true); - for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) - m_optgroup->hide_field(opt_key); + update_host_type(printer_change); const auto opt = m_config->option>("host_type"); - supports_multiple_printers = opt && opt->value == htRepetier; + m_optgroup->show_field("host_type"); + if (opt->value == htPrusaLink) + { + m_optgroup->show_field("printhost_authorization_type"); + AuthorizationType auth_type = m_config->option>("printhost_authorization_type")->value; + m_optgroup->show_field("printhost_apikey", auth_type == AuthorizationType::atKeyPassword); + for (const char* opt_key : { "printhost_user", "printhost_password" }) + m_optgroup->show_field(opt_key, auth_type == AuthorizationType::atUserPassword); + } else { + m_optgroup->hide_field("printhost_authorization_type"); + m_optgroup->show_field("printhost_apikey", true); + for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) + m_optgroup->hide_field(opt_key); + supports_multiple_printers = opt && opt->value == htRepetier; + } + } else { m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false); @@ -493,6 +506,57 @@ void PhysicalPrinterDialog::update() this->Layout(); } +void PhysicalPrinterDialog::update_host_type(bool printer_change) +{ + if (m_presets.empty()) + return; + bool all_presets_are_from_mk3_family = true; + + for (PresetForPrinter* prstft : m_presets) { + std::string preset_name = prstft->get_preset_name(); + if (Preset* preset = wxGetApp().preset_bundle->printers.find_preset(preset_name)) { + std::string model_id = preset->config.opt_string("printer_model"); + if (preset->vendor && preset->vendor->name == "Prusa Research") { + const std::vector& models = preset->vendor->models; + auto it = std::find_if(models.begin(), models.end(), + [model_id](const VendorProfile::PrinterModel& model) { return model.id == model_id; }); + if (it != models.end() && it->family == "MK3") + continue; + } else if (!preset->vendor && model_id.rfind("MK3", 0) == 0) { + continue; + } + + } + all_presets_are_from_mk3_family = false; + break; + } + + Field* ht = m_optgroup->get_field("host_type"); + + wxArrayString types; + // Append localized enum_labels + assert(ht->m_opt.enum_labels.size() == ht->m_opt.enum_values.size()); + for (size_t i = 0; i < ht->m_opt.enum_labels.size(); i++) { + if (ht->m_opt.enum_values[i] == "prusalink" && !all_presets_are_from_mk3_family) + continue; + types.Add(_(ht->m_opt.enum_labels[i])); + } + + Choice* choice = dynamic_cast(ht); + choice->set_values(types); + auto set_to_choice_and_config = [this, choice](PrintHostType type) { + choice->set_value(static_cast(type)); + m_config->set_key_value("host_type", new ConfigOptionEnum(type)); + }; + if ((printer_change && all_presets_are_from_mk3_family) || (!had_all_mk3 && all_presets_are_from_mk3_family)) + set_to_choice_and_config(htPrusaLink); + else if ((printer_change && !all_presets_are_from_mk3_family) || (!all_presets_are_from_mk3_family && m_config->option>("host_type")->value == htPrusaLink)) + set_to_choice_and_config(htOctoPrint); + else + choice->set_value(m_config->option("host_type")->getInt()); + had_all_mk3 = all_presets_are_from_mk3_family; +} + wxString PhysicalPrinterDialog::get_printer_name() { @@ -628,8 +692,9 @@ void PhysicalPrinterDialog::AddPreset(wxEvent& event) m_presets_sizer->Add(m_presets.back()->sizer(), 1, wxEXPAND | wxTOP, BORDER_W); update_full_printer_names(); - this->Fit(); + + update_host_type(true); } void PhysicalPrinterDialog::DeletePreset(PresetForPrinter* preset_for_printer) @@ -657,7 +722,8 @@ void PhysicalPrinterDialog::DeletePreset(PresetForPrinter* preset_for_printer) this->Layout(); this->Fit(); + + update_host_type(true); } - }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index 7ee1f7d92..cb9a48b3e 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -85,7 +85,8 @@ public: PhysicalPrinterDialog(wxWindow* parent, wxString printer_name); ~PhysicalPrinterDialog(); - void update(); + void update(bool printer_change = false); + void update_host_type(bool printer_change); void update_printhost_buttons(); void update_printers(); wxString get_printer_name(); @@ -95,10 +96,11 @@ public: PrinterTechnology get_printer_technology(); void DeletePreset(PresetForPrinter* preset_for_printer); - protected: void on_dpi_changed(const wxRect& suggested_rect) override; void on_sys_color_changed() override {}; + + bool had_all_mk3; }; diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index fad45f822..f01e3ad41 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -213,4 +213,48 @@ void SL1Host::set_auth(Http &http) const } } +// PrusaLink +PrusaLink::PrusaLink(DynamicPrintConfig* config) : + OctoPrint(config), + authorization_type(dynamic_cast*>(config->option("printhost_authorization_type"))->value), + username(config->opt_string("printhost_user")), + password(config->opt_string("printhost_password")) +{ +} + +const char* PrusaLink::get_name() const { return "PrusaLink"; } + +wxString PrusaLink::get_test_ok_msg() const +{ + return _(L("Connection to PrusaLink works correctly.")); +} + +wxString PrusaLink::get_test_failed_msg(wxString& msg) const +{ + return GUI::from_u8((boost::format("%s: %s") + % _utf8(L("Could not connect to PrusaLink")) + % std::string(msg.ToUTF8())).str()); +} + +bool PrusaLink::validate_version_text(const boost::optional& version_text) const +{ + return version_text ? (boost::starts_with(*version_text, "PrusaLink") || boost::starts_with(*version_text, "OctoPrint")) : false; +} + +void PrusaLink::set_auth(Http& http) const +{ + switch (authorization_type) { + case atKeyPassword: + http.header("X-Api-Key", get_apikey()); + break; + case atUserPassword: + http.auth_digest(username, password); + break; + } + + if (!get_cafile().empty()) { + http.ca_file(get_cafile()); + } +} + } diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp index f1b36096c..62bdfb6fa 100644 --- a/src/slic3r/Utils/OctoPrint.hpp +++ b/src/slic3r/Utils/OctoPrint.hpp @@ -70,6 +70,31 @@ private: std::string password; }; +class PrusaLink : public OctoPrint +{ +public: + PrusaLink(DynamicPrintConfig* config); + ~PrusaLink() override = default; + + const char* get_name() const override; + + wxString get_test_ok_msg() const override; + wxString get_test_failed_msg(wxString& msg) const override; + bool can_start_print() const override { return true; } + +protected: + bool validate_version_text(const boost::optional& version_text) const override; + +private: + void set_auth(Http& http) const override; + + // Host authorization type. + AuthorizationType authorization_type; + // username and password for HTTP Digest Authentization (RFC RFC2617) + std::string username; + std::string password; +}; + } #endif diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 589679e47..53200a4c9 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -50,6 +50,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) case htFlashAir: return new FlashAir(config); case htAstroBox: return new AstroBox(config); case htRepetier: return new Repetier(config); + case htPrusaLink: return new PrusaLink(config); default: return nullptr; } } else { From 85d4e1c60b187989d13b8f4ca16a3591891c78d7 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 14 Jun 2021 12:18:46 +0200 Subject: [PATCH 22/37] Workaround of boost::filesystem::copy_file() incompatibility on some file systems (eCrypt ...) Should fix #4716 #6588 From 77e25c5022f6fda2c67a9cce50bf482286f3424b Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 28 Jun 2021 13:21:07 +0200 Subject: [PATCH 23/37] fix of #6588 - using same copy function for updating presets as for exporting gcode --- src/slic3r/Utils/PresetUpdater.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index ae8f2abb5..078c2fe20 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -56,16 +56,15 @@ static const char *TMP_EXTENSION = ".download"; void copy_file_fix(const fs::path &source, const fs::path &target) { - static const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read; // aka 644 - BOOST_LOG_TRIVIAL(debug) << format("PresetUpdater: Copying %1% -> %2%", source, target); - - // Make sure the file has correct permission both before and after we copy over it - if (fs::exists(target)) { - fs::permissions(target, perms); + std::string error_message; + CopyFileResult cfr = copy_file(source.string(), target.string(), error_message, false); + if (cfr != CopyFileResult::SUCCESS) { + BOOST_LOG_TRIVIAL(error) << "Copying failed(" << cfr << "): " << error_message; + throw Slic3r::CriticalException(GUI::format( + _L("Copying of file %1% to %2% failed: %3%"), + source, target, error_message)); } - fs::copy_file(source, target, fs::copy_option::overwrite_if_exists); - fs::permissions(target, perms); } struct Update From 92cf46773a318d605f6a28a86e3eeb298faf6d00 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Mon, 28 Jun 2021 14:55:15 +0200 Subject: [PATCH 24/37] Hotfix for arrange not working for objects with huge translation. --- src/libnest2d/include/libnest2d/geometry_traits.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libnest2d/include/libnest2d/geometry_traits.hpp b/src/libnest2d/include/libnest2d/geometry_traits.hpp index 7ea437339..f388e37b1 100644 --- a/src/libnest2d/include/libnest2d/geometry_traits.hpp +++ b/src/libnest2d/include/libnest2d/geometry_traits.hpp @@ -501,8 +501,9 @@ inline P _Box

::center() const BP2D_NOEXCEPT { using Coord = TCoord

; P ret = { // No rounding here, we dont know if these are int coords - Coord( (getX(minc) + getX(maxc)) / Coord(2) ), - Coord( (getY(minc) + getY(maxc)) / Coord(2) ) + // Doing the division like this increases the max range of x and y coord + getX(minc) / Coord(2) + getX(maxc) / Coord(2), + getY(minc) / Coord(2) + getY(maxc) / Coord(2) }; return ret; From d737142f37d6dbc488749783d96c9ef9a457e23c Mon Sep 17 00:00:00 2001 From: Pascal de Bruijn Date: Mon, 28 Jun 2021 18:02:00 +0200 Subject: [PATCH 25/37] creality.ini: make print profiles inclusive of Sermoon printers --- resources/profiles/Creality.ini | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/profiles/Creality.ini b/resources/profiles/Creality.ini index e861e8425..2cafa3b0b 100644 --- a/resources/profiles/Creality.ini +++ b/resources/profiles/Creality.ini @@ -459,36 +459,36 @@ top_solid_layers = 4 [print:0.08mm SUPERDETAIL @CREALITY] inherits = *0.08mm* -compatible_printers_condition = printer_model=~/(ENDER|CR).*/ and nozzle_diameter[0]==0.4 +compatible_printers_condition = printer_model=~/(ENDER|CR|SERMOON).*/ and nozzle_diameter[0]==0.4 [print:0.10mm HIGHDETAIL @CREALITY] inherits = *0.10mm* renamed_from = "0.10mm HIGHDETAIL @ENDER3" -compatible_printers_condition = printer_model=~/(ENDER|CR).*/ and nozzle_diameter[0]==0.4 +compatible_printers_condition = printer_model=~/(ENDER|CR|SERMOON).*/ and nozzle_diameter[0]==0.4 [print:0.12mm DETAIL @CREALITY] inherits = *0.12mm* renamed_from = "0.12mm DETAIL @ENDER3" -compatible_printers_condition = printer_model=~/(ENDER|CR).*/ and nozzle_diameter[0]==0.4 +compatible_printers_condition = printer_model=~/(ENDER|CR|SERMOON).*/ and nozzle_diameter[0]==0.4 [print:0.16mm OPTIMAL @CREALITY] inherits = *0.16mm* renamed_from = "0.15mm OPTIMAL @ENDER3"; "0.15mm OPTIMAL @CREALITY" -compatible_printers_condition = printer_model=~/(ENDER|CR).*/ and nozzle_diameter[0]==0.4 +compatible_printers_condition = printer_model=~/(ENDER|CR|SERMOON).*/ and nozzle_diameter[0]==0.4 [print:0.20mm NORMAL @CREALITY] inherits = *0.20mm* renamed_from = "0.20mm NORMAL @ENDER3" -compatible_printers_condition = printer_model=~/(ENDER|CR).*/ and nozzle_diameter[0]==0.4 +compatible_printers_condition = printer_model=~/(ENDER|CR|SERMOON).*/ and nozzle_diameter[0]==0.4 [print:0.24mm DRAFT @CREALITY] inherits = *0.24mm* renamed_from = "0.24mm DRAFT @ENDER3" -compatible_printers_condition = printer_model=~/(ENDER|CR).*/ and nozzle_diameter[0]==0.4 +compatible_printers_condition = printer_model=~/(ENDER|CR|SERMOON).*/ and nozzle_diameter[0]==0.4 [print:0.28mm SUPERDRAFT @CREALITY] inherits = *0.28mm* -compatible_printers_condition = printer_model=~/(ENDER|CR).*/ and nozzle_diameter[0]==0.4 +compatible_printers_condition = printer_model=~/(ENDER|CR|SERMOON).*/ and nozzle_diameter[0]==0.4 # When submitting new filaments please print the following temperature tower at 0.1mm layer height: # https://www.thingiverse.com/thing:2615842 From e32cde80549b82bb523e20971c059c1b58b5d8ba Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Mon, 28 Jun 2021 19:05:30 +0200 Subject: [PATCH 26/37] Creality - bumped up version to 0.1.0 --- resources/profiles/Creality.idx | 2 ++ resources/profiles/Creality.ini | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/profiles/Creality.idx b/resources/profiles/Creality.idx index caa304bad..412be2ccf 100644 --- a/resources/profiles/Creality.idx +++ b/resources/profiles/Creality.idx @@ -1,3 +1,5 @@ +min_slic3r_version = 2.3.2-alpha0 +0.1.0 Added Ender-7, Sermoon D1, CR-10 SMART min_slic3r_version = 2.3.1-beta 0.0.17 Updated start g-code. Added specific start g-code for straingauge printers. Improved output filename format. Added filament profile. 0.0.16 Updated CR6-SE start g-code. Added and updated filament profiles. diff --git a/resources/profiles/Creality.ini b/resources/profiles/Creality.ini index 2cafa3b0b..9c4dc373b 100644 --- a/resources/profiles/Creality.ini +++ b/resources/profiles/Creality.ini @@ -5,7 +5,7 @@ name = Creality # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the PrusaSlicer configuration to be downgraded. -config_version = 0.0.17 +config_version = 0.1.0 # Where to get the updates from? config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/Creality/ # changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1% From e11b8135ff74031c44201688f4ef06ed5bc4be48 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Tue, 29 Jun 2021 10:47:57 +0200 Subject: [PATCH 27/37] Updated SL1S bed texture. --- resources/profiles/PrusaResearch/sl1s.svg | 108 ++++++++++------------ 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/resources/profiles/PrusaResearch/sl1s.svg b/resources/profiles/PrusaResearch/sl1s.svg index f9301bae6..990382ffd 100644 --- a/resources/profiles/PrusaResearch/sl1s.svg +++ b/resources/profiles/PrusaResearch/sl1s.svg @@ -1,64 +1,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 7fce368cca404ff0935a522cf2316efbded6ccfd Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Tue, 29 Jun 2021 11:40:50 +0200 Subject: [PATCH 28/37] Fix crash after SL1 import --- src/libslic3r/Model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 3ddf65333..a382a4258 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1640,7 +1640,7 @@ bool ModelVolume::is_splittable() const { // the call mesh.is_splittable() is expensive, so cache the value to calculate it only once if (m_is_splittable == -1) - m_is_splittable = (int)this->mesh().is_splittable(); + m_is_splittable = its_is_splittable(this->mesh().its); return m_is_splittable == 1; } From 4652733201f446f062e37574d4a44f80c1541592 Mon Sep 17 00:00:00 2001 From: Oleksandra Yushchenko Date: Tue, 29 Jun 2021 16:23:45 +0200 Subject: [PATCH 29/37] MSW specific: Implementation of the own notebook control * MSW specific: First implementation of the Notebook control to support Dark/Light color modes * MSW specific: Set mode sizer to the Notebook control. * MSW specific: Added icons to the Notepad control + There is no need to restart application after the changing of the color mode * Fixed non-MSW build * Updated color for SavePresetDialog + Added wrapper to wxMessageBox for mom-MSW platforms --- src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/GUI.hpp | 1 - src/slic3r/GUI/GUI_App.cpp | 77 +++---- src/slic3r/GUI/GUI_App.hpp | 11 +- src/slic3r/GUI/GUI_Factories.cpp | 15 ++ src/slic3r/GUI/GUI_Factories.hpp | 2 + src/slic3r/GUI/GUI_Preview.hpp | 1 - src/slic3r/GUI/GUI_Utils.cpp | 2 +- src/slic3r/GUI/GUI_Utils.hpp | 4 +- src/slic3r/GUI/KBShortcutsDialog.cpp | 15 +- src/slic3r/GUI/MainFrame.cpp | 71 +++++-- src/slic3r/GUI/MainFrame.hpp | 3 +- src/slic3r/GUI/MsgDialog.cpp | 9 +- src/slic3r/GUI/MsgDialog.hpp | 15 +- src/slic3r/GUI/Notebook.cpp | 134 ++++++++++++ src/slic3r/GUI/Notebook.hpp | 305 +++++++++++++++++++++++++++ src/slic3r/GUI/Plater.cpp | 26 ++- src/slic3r/GUI/Plater.hpp | 4 + src/slic3r/GUI/Preferences.cpp | 47 +++-- src/slic3r/GUI/Preferences.hpp | 8 +- src/slic3r/GUI/SavePresetDialog.cpp | 7 +- src/slic3r/GUI/Tab.cpp | 40 +++- src/slic3r/GUI/Tab.hpp | 2 +- src/slic3r/GUI/wxExtensions.cpp | 36 +++- src/slic3r/GUI/wxExtensions.hpp | 1 + 25 files changed, 703 insertions(+), 135 deletions(-) create mode 100644 src/slic3r/GUI/Notebook.cpp create mode 100644 src/slic3r/GUI/Notebook.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index f6739b0d8..76fd8d989 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -181,6 +181,8 @@ set(SLIC3R_GUI_SOURCES GUI/Mouse3DController.hpp GUI/DoubleSlider.cpp GUI/DoubleSlider.hpp + GUI/Notebook.cpp + GUI/Notebook.hpp GUI/ObjectDataViewModel.cpp GUI/ObjectDataViewModel.hpp GUI/InstanceCheck.cpp diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index a90115933..c70dffcc3 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -10,7 +10,6 @@ namespace boost::filesystem { class path; } class wxWindow; class wxMenuBar; -class wxNotebook; class wxComboCtrl; class wxFileDialog; class wxTopLevelWindow; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 943124849..73f41e449 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -72,6 +72,7 @@ #include "DesktopIntegrationDialog.hpp" #include "BitmapCache.hpp" +#include "Notebook.hpp" #ifdef __WXMSW__ #include @@ -929,8 +930,6 @@ bool GUI_App::on_init_inner() if (scrn && is_editor()) scrn->SetText(_L("Preparing settings tabs") + dots); - m_tabs_as_menu = dark_mode() || app_config->get("tabs_as_menu") == "1"; - mainframe = new MainFrame(); // hide settings tabs after first Layout if (is_editor()) @@ -1043,6 +1042,7 @@ void GUI_App::init_label_colours() m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); + m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); #else m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); #endif @@ -1090,24 +1090,22 @@ void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool ju btn->Bind(wxEVT_LEAVE_WINDOW, [focus_button](wxMouseEvent& event) { focus_button(false); event.Skip(); }); } } - else if (dark_mode()) { - if (wxTextCtrl* text = dynamic_cast(window)) { - if (text->GetBorder() != wxBORDER_SIMPLE) - text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); - } - else if (wxCheckListBox* list = dynamic_cast(window)) { - list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); - list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - for (size_t i = 0; i < list->GetCount(); i++) - if (wxOwnerDrawn* item = list->GetItem(i)) { - item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - item->SetTextColour(m_color_label_default); - } - return; - } - else if (dynamic_cast(window)) - window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); + else if (wxTextCtrl* text = dynamic_cast(window)) { + if (text->GetBorder() != wxBORDER_SIMPLE) + text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); } + else if (wxCheckListBox* list = dynamic_cast(window)) { + list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); + list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + for (size_t i = 0; i < list->GetCount(); i++) + if (wxOwnerDrawn* item = list->GetItem(i)) { + item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + item->SetTextColour(m_color_label_default); + } + return; + } + else if (dynamic_cast(window)) + window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); if (!just_font) window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); @@ -1138,8 +1136,6 @@ void GUI_App::UpdateDlgDarkUI(wxDialog* dlg) void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) { #ifdef _WIN32 - if (!dark_mode()) - return; UpdateDarkUI(dvc, highlited); wxItemAttr attr(dark_mode() ? m_color_highlight_default : m_color_label_default, m_color_window_default, @@ -1155,8 +1151,6 @@ void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) { #ifdef _WIN32 - if (!dark_mode()) - return; wxGetApp().UpdateDarkUI(parent); auto children = parent->GetChildren(); @@ -1222,6 +1216,11 @@ void GUI_App::set_label_clr_sys(const wxColour& clr) app_config->save(); } +bool GUI_App::tabs_as_menu() const +{ + return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); +} + wxSize GUI_App::get_min_size() const { return wxSize(76*m_em_unit, 49 * m_em_unit); @@ -1366,6 +1365,14 @@ void fatal_error(wxWindow* parent) // exit 1; // #ys_FIXME } +#ifdef _WIN32 +void GUI_App::force_colors_update() +{ + NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); + m_force_colors_update = true; +} +#endif + // Called after the Preferences dialog is closed and the program settings are saved. // Update the UI based on the current preferences. void GUI_App::update_ui_from_settings() @@ -1373,13 +1380,13 @@ void GUI_App::update_ui_from_settings() update_label_colours(); mainframe->update_ui_from_settings(); -#if 0 //#ifdef _WIN32 // #ysDarkMSW - Use to force dark colors for SystemLightMode - if (m_force_sys_colors_update) { - m_force_sys_colors_update = false; - mainframe->force_sys_color_changed(); - mainframe->diff_dialog.force_sys_color_changed(); +#ifdef _WIN32 + if (m_force_colors_update) { + m_force_colors_update = false; + mainframe->force_color_changed(); + mainframe->diff_dialog.force_color_changed(); if (m_wizard) - m_wizard->force_sys_color_changed(); + m_wizard->force_color_changed(); } #endif } @@ -1763,6 +1770,11 @@ void GUI_App::update_mode() { sidebar().update_mode(); +#ifdef _MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); +#endif + for (auto tab : tabs_list) tab->update_mode(); @@ -1902,13 +1914,6 @@ void GUI_App::add_config_menu(wxMenuBar *menu) this->plater_->refresh_print(); if (dlg.recreate_GUI()) { -#ifdef _MSW_DARK_MODE - if (dlg.color_mode_changed()) { - NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); - init_label_colours(); - } -#endif - m_tabs_as_menu = dark_mode() || app_config->get("tabs_as_menu") == "1"; recreate_GUI(_L("Restart application") + dots); return; } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 5cc8641ef..d88085ce7 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -124,11 +124,10 @@ private: wxColour m_color_highlight_label_default; wxColour m_color_hovered_btn_label; wxColour m_color_highlight_default; - //bool m_force_sys_colors_update { false }; // #ysDarkMSW - Use to force dark colors for SystemLightMode + wxColour m_color_selected_btn_bg; + bool m_force_colors_update { false }; #endif - bool m_tabs_as_menu{ false }; - wxFont m_small_font; wxFont m_bold_font; wxFont m_normal_font; @@ -202,7 +201,9 @@ public: #ifdef _WIN32 const wxColour& get_label_highlight_clr() { return m_color_highlight_label_default; } const wxColour& get_highlight_default_clr() { return m_color_highlight_default; } -// void force_sys_colors_update() { m_force_sys_colors_update = true; } // #ysDarkMSW - Use to force dark colors for SystemLightMode + const wxColour& get_color_hovered_btn_label() { return m_color_hovered_btn_label; } + const wxColour& get_color_selected_btn_bg() { return m_color_selected_btn_bg; } + void force_colors_update(); #endif const wxFont& small_font() { return m_small_font; } @@ -210,7 +211,7 @@ public: const wxFont& normal_font() { return m_normal_font; } const wxFont& code_font() { return m_code_font; } int em_unit() const { return m_em_unit; } - bool tabs_as_menu() const { return m_tabs_as_menu;} + bool tabs_as_menu() const; wxSize get_min_size() const; float toolbar_icon_scale(const bool is_limited = false) const; void set_auto_toolbar_icon_scale(float scale) const; diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index aeb2ab7e0..c4782615c 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1115,5 +1115,20 @@ void MenuFactory::sys_color_changed() } } +void MenuFactory::sys_color_changed(wxMenuBar* menubar) +{ + for (size_t id = 0; id < menubar->GetMenuCount(); id++) { + wxMenu* menu = menubar->GetMenu(id); + msw_rescale_menu(menu); +#ifdef _WIN32 + // but under MSW we have to update item's bachground color + for (wxMenuItem* item : menu->GetMenuItems()) + update_menu_item_def_colors(item); +#endif + } + menubar->Refresh(); +} + + } //namespace GUI } //namespace Slic3r diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index 2c3e03521..e8928d3ff 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -44,6 +44,8 @@ public: void msw_rescale(); void sys_color_changed(); + static void sys_color_changed(wxMenuBar* menu_bar); + wxMenu* default_menu(); wxMenu* object_menu(); wxMenu* sla_object_menu(); diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index 7b1fcad19..2165b11c0 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -9,7 +9,6 @@ #include #include "libslic3r/GCode/GCodeProcessor.hpp" -class wxNotebook; class wxGLCanvas; class wxBoxSizer; class wxStaticText; diff --git a/src/slic3r/GUI/GUI_Utils.cpp b/src/slic3r/GUI/GUI_Utils.cpp index 89c81fd2a..73bfb3bec 100644 --- a/src/slic3r/GUI/GUI_Utils.cpp +++ b/src/slic3r/GUI/GUI_Utils.cpp @@ -174,7 +174,7 @@ bool check_dark_mode() { #ifdef _WIN32 void update_dark_ui(wxWindow* window) { - bool is_dark = wxGetApp().app_config->get("dark_color_mode") == "1" ? true : check_dark_mode(); + bool is_dark = wxGetApp().app_config->get("dark_color_mode") == "1";// ? true : check_dark_mode();// #ysDarkMSW - Allow it when we deside to support the sustem colors for application window->SetBackgroundColour(is_dark ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); window->SetForegroundColour(is_dark ? wxColour(250, 250, 250) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); } diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp index 66e0ae2aa..a5ba218b5 100644 --- a/src/slic3r/GUI/GUI_Utils.hpp +++ b/src/slic3r/GUI/GUI_Utils.hpp @@ -175,8 +175,8 @@ public: const wxFont& normal_font() const { return m_normal_font; } void enable_force_rescale() { m_force_rescale = true; } -#if 0 //#ifdef _WIN32 // #ysDarkMSW - Use to force dark colors for SystemLightMode - void force_sys_color_changed() +#ifdef _WIN32 + void force_color_changed() { update_dark_ui(this); on_sys_color_changed(); diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 2c7822a9e..23df99bd9 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -3,13 +3,13 @@ #include "I18N.hpp" #include "libslic3r/Utils.hpp" #include "GUI.hpp" +#include "Notebook.hpp" #include #include #include "GUI_App.hpp" #include "wxExtensions.hpp" #include "MainFrame.hpp" #include -#include namespace Slic3r { namespace GUI { @@ -18,8 +18,6 @@ KBShortcutsDialog::KBShortcutsDialog() : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, wxString(wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME) + " - " + _L("Keyboard Shortcuts"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { -// SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - // fonts const wxFont& font = wxGetApp().normal_font(); const wxFont& bold_font = wxGetApp().bold_font(); @@ -31,13 +29,10 @@ KBShortcutsDialog::KBShortcutsDialog() #ifdef _MSW_DARK_MODE wxBookCtrlBase* book; - if (wxGetApp().dark_mode()) { - book = new wxListbook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP); - wxGetApp().UpdateDarkUI(book); - wxGetApp().UpdateDarkUI(dynamic_cast(book)->GetListView()); - } - else - book = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP); +// if (wxGetApp().dark_mode()) + book = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP); +/* else + book = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP);*/ #else wxNotebook* book = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP); #endif diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 389782a8f..561a03f44 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -41,6 +41,8 @@ #include "GUI_App.hpp" #include "UnsavedChangesDialog.hpp" #include "MsgDialog.hpp" +#include "Notebook.hpp" +#include "GUI_Factories.hpp" #ifdef _WIN32 #include @@ -427,8 +429,13 @@ void MainFrame::update_layout() case ESettingsLayout::Old: { m_plater->Reparent(m_tabpanel); +#ifdef _MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(m_tabpanel)->InsertPage(0, m_plater, _L("Plater"), std::string("plater")); + else +#endif m_tabpanel->InsertPage(0, m_plater, _L("Plater")); - m_main_sizer->Add(m_tabpanel, 1, wxEXPAND); + m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 1); m_plater->Show(); m_tabpanel->Show(); // update Tabs @@ -447,6 +454,11 @@ void MainFrame::update_layout() m_tabpanel->Hide(); m_main_sizer->Add(m_tabpanel, 1, wxEXPAND); m_plater_page = new wxPanel(m_tabpanel); +#ifdef _MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(m_tabpanel)->InsertPage(0, m_plater_page, _L("Plater"), std::string("plater")); + else +#endif m_tabpanel->InsertPage(0, m_plater_page, _L("Plater")); // empty panel just for Plater tab */ m_plater->Show(); break; @@ -455,7 +467,7 @@ void MainFrame::update_layout() { m_main_sizer->Add(m_plater, 1, wxEXPAND); m_tabpanel->Reparent(&m_settings_dialog); - m_settings_dialog.GetSizer()->Add(m_tabpanel, 1, wxEXPAND); + m_settings_dialog.GetSizer()->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 2); m_tabpanel->Show(); m_plater->Show(); @@ -476,6 +488,11 @@ void MainFrame::update_layout() } } +#ifdef _MSW_DARK_MODE + // Sizer with buttons for mode changing + m_plater->sidebar().show_mode_sizer(wxGetApp().tabs_as_menu() || m_layout != ESettingsLayout::Old); +#endif + #ifdef __WXMSW__ if (update_scaling_state != State::noUpdate) { @@ -640,7 +657,7 @@ void MainFrame::init_tabpanel() wxGetApp().UpdateDarkUI(m_tabpanel); } else - m_tabpanel = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); + m_tabpanel = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME, true); #else m_tabpanel = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); #endif @@ -652,7 +669,7 @@ void MainFrame::init_tabpanel() m_settings_dialog.set_tabpanel(m_tabpanel); #ifdef __WXMSW__ - m_tabpanel->Bind(/*wxEVT_LISTBOOK_PAGE_CHANGED*/wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) { + m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) { #else m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, [this](wxBookCtrlEvent& e) { #endif @@ -763,20 +780,25 @@ void MainFrame::register_win32_callbacks() void MainFrame::create_preset_tabs() { wxGetApp().update_label_colours_from_appconfig(); - add_created_tab(new TabPrint(m_tabpanel)); - add_created_tab(new TabFilament(m_tabpanel)); - add_created_tab(new TabSLAPrint(m_tabpanel)); - add_created_tab(new TabSLAMaterial(m_tabpanel)); - add_created_tab(new TabPrinter(m_tabpanel)); + add_created_tab(new TabPrint(m_tabpanel), "cog"); + add_created_tab(new TabFilament(m_tabpanel), "spool"); + add_created_tab(new TabSLAPrint(m_tabpanel), "cog"); + add_created_tab(new TabSLAMaterial(m_tabpanel), "resin"); + add_created_tab(new TabPrinter(m_tabpanel), wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF ? "printer" : "sla_printer"); } -void MainFrame::add_created_tab(Tab* panel) +void MainFrame::add_created_tab(Tab* panel, const std::string& bmp_name /*= ""*/) { panel->create_preset_tab(); const auto printer_tech = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology(); if (panel->supports_printer_technology(printer_tech)) +#ifdef _MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(m_tabpanel)->AddPage(panel, panel->title(), bmp_name); + else +#endif m_tabpanel->AddPage(panel, panel->title()); } @@ -960,6 +982,12 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect) wxGetApp().update_fonts(this); this->SetFont(this->normal_font()); +#ifdef _MSW_DARK_MODE + // update common mode sizer + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(m_tabpanel)->Rescale(); +#endif + // update Plater wxGetApp().plater()->msw_rescale(); @@ -968,9 +996,8 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect) for (auto tab : wxGetApp().tabs_list) tab->msw_rescale(); - wxMenuBar* menu_bar = this->GetMenuBar(); - for (size_t id = 0; id < menu_bar->GetMenuCount(); id++) - msw_rescale_menu(menu_bar->GetMenu(id)); + for (size_t id = 0; id < m_menubar->GetMenuCount(); id++) + msw_rescale_menu(m_menubar->GetMenu(id)); // Workarounds for correct Window rendering after rescale @@ -1003,6 +1030,11 @@ void MainFrame::on_sys_color_changed() #ifdef __WXMSW__ wxGetApp().UpdateDarkUI(m_tabpanel); m_statusbar->update_dark_ui(); +#ifdef _MSW_DARK_MODE + // update common mode sizer + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(m_tabpanel)->Rescale(); +#endif #endif // update Plater @@ -1012,10 +1044,7 @@ void MainFrame::on_sys_color_changed() for (auto tab : wxGetApp().tabs_list) tab->sys_color_changed(); - // msw_rescale_menu updates just icons, so use it - wxMenuBar* menu_bar = this->GetMenuBar(); - for (size_t id = 0; id < menu_bar->GetMenuCount(); id++) - msw_rescale_menu(menu_bar->GetMenu(id)); + MenuFactory::sys_color_changed(m_menubar); this->Refresh(); } @@ -2103,8 +2132,8 @@ SettingsDialog::SettingsDialog(MainFrame* mainframe) this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); #else this->SetFont(wxGetApp().normal_font()); -#endif // __WXMSW__ this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif // __WXMSW__ // Load the icon either from the exe, or from the ico file. #if _WIN32 @@ -2185,6 +2214,12 @@ void SettingsDialog::on_dpi_changed(const wxRect& suggested_rect) const int& em = em_unit(); const wxSize& size = wxSize(85 * em, 50 * em); +#ifdef _MSW_DARK_MODE + // update common mode sizer + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(m_tabpanel)->Rescale(); +#endif + // update Tabs for (auto tab : wxGetApp().tabs_list) tab->msw_rescale(); diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 94779c163..a5c6b57ca 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -18,7 +18,6 @@ #include "Event.hpp" #include "UnsavedChangesDialog.hpp" -class wxNotebook; class wxBookCtrlBase; class wxProgressDialog; @@ -154,7 +153,7 @@ public: void init_tabpanel(); void create_preset_tabs(); - void add_created_tab(Tab* panel); + void add_created_tab(Tab* panel, const std::string& bmp_name = ""); bool is_active_and_shown_tab(Tab* tab); // Register Win32 RawInput callbacks (3DConnexion) and removable media insert / remove callbacks. // Called from wxEVT_ACTIVATE, as wxEVT_CREATE was not reliable (bug in wxWidgets?). diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 49dec0c5d..3a894b84f 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -17,6 +17,7 @@ #include "I18N.hpp" #include "ConfigWizard.hpp" #include "wxExtensions.hpp" +#include "Mainframe.hpp" #include "GUI_App.hpp" namespace Slic3r { @@ -24,7 +25,7 @@ namespace GUI { MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &headline, wxWindowID button_id, wxBitmap bitmap) - : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + : wxDialog(parent ? parent : dynamic_cast(wxGetApp().mainframe), wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) , boldfont(wxGetApp().normal_font()/*wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)*/) , content_sizer(new wxBoxSizer(wxVERTICAL)) , btn_sizer(new wxBoxSizer(wxHORIZONTAL)) @@ -136,6 +137,7 @@ ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &msg, bool monospaced_ SetMaxSize(wxSize(-1, CONTENT_MAX_HEIGHT*wxGetApp().em_unit())); Fit(); + this->CenterOnParent(); } // WarningDialog @@ -157,9 +159,10 @@ WarningDialog::WarningDialog(wxWindow *parent, wxGetApp().UpdateDlgDarkUI(this); Fit(); + this->CenterOnParent(); } - +#ifdef _WIN32 // MessageDialog MessageDialog::MessageDialog(wxWindow* parent, @@ -181,7 +184,9 @@ MessageDialog::MessageDialog(wxWindow* parent, wxGetApp().UpdateDlgDarkUI(this); Fit(); + this->CenterOnParent(); } +#endif } } diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index 8c49107d9..e34177e7e 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -83,7 +83,7 @@ public: virtual ~WarningDialog() = default; }; - +#ifdef _WIN32 // Generic message dialog, used intead of wxMessageDialog class MessageDialog : public MsgDialog { @@ -98,6 +98,19 @@ public: MessageDialog &operator=(const MessageDialog&) = delete; virtual ~MessageDialog() = default; }; +#else +// just a wrapper to wxMessageBox to use the same code on all platforms +class MessageDialog : public wxMessageDialog +{ +public: + MessageDialog(wxWindow* parent, + const wxString& message, + const wxString& caption = wxEmptyString, + long style = wxOK) + : wxMessageDialog(parent, message, caption, style) {} + ~MessageDialog() {} +}; +#endif } diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp new file mode 100644 index 000000000..0aa11da5b --- /dev/null +++ b/src/slic3r/GUI/Notebook.cpp @@ -0,0 +1,134 @@ +#include "Notebook.hpp" + +#ifdef _WIN32 + +#include "GUI_App.hpp" +#include "wxExtensions.hpp" + +#include +#include + +wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent); + +ButtonsListCtrl::ButtonsListCtrl(wxWindow *parent, bool add_mode_buttons/* = false*/) : + wxControl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxTAB_TRAVERSAL) +{ +#ifdef __WINDOWS__ + SetDoubleBuffered(true); +#endif //__WINDOWS__ + + m_sizer = new wxBoxSizer(wxHORIZONTAL); + this->SetSizer(m_sizer); + + if (add_mode_buttons) { + m_mode_sizer = new ModeSizer(this, int(0.5 * em_unit(this))); + m_sizer->AddStretchSpacer(20); + m_sizer->Add(m_mode_sizer, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + } + + this->Bind(wxEVT_PAINT, &ButtonsListCtrl::OnPaint, this); +} + +void ButtonsListCtrl::OnPaint(wxPaintEvent&) +{ + Slic3r::GUI::wxGetApp().UpdateDarkUI(this); + const wxSize sz = GetSize(); + wxPaintDC dc(this); + + if (m_selection < 0 || m_selection >= (int)m_pageButtons.size()) + return; + + // highlight selected button + + const wxColour& selected_btn_bg = Slic3r::GUI::wxGetApp().get_color_selected_btn_bg(); + const wxColour& default_btn_bg = Slic3r::GUI::wxGetApp().get_highlight_default_clr(); + const wxColour& btn_marker_color = Slic3r::GUI::wxGetApp().get_color_hovered_btn_label(); + for (int idx = 0; idx < m_pageButtons.size(); idx++) { + wxButton* btn = m_pageButtons[idx]; + + btn->SetBackgroundColour(idx == m_selection ? selected_btn_bg : default_btn_bg); + + wxPoint pos = btn->GetPosition(); + wxSize size = btn->GetSize(); + const wxColour& clr = idx == m_selection ? btn_marker_color : default_btn_bg; + dc.SetPen(clr); + dc.SetBrush(clr); + dc.DrawRectangle(pos.x, sz.y - 3, size.x, 3); + } + + dc.SetPen(btn_marker_color); + dc.SetBrush(btn_marker_color); + dc.DrawRectangle(1, sz.y - 1, sz.x, 1); +} + +void ButtonsListCtrl::UpdateMode() +{ + m_mode_sizer->SetMode(Slic3r::GUI::wxGetApp().get_mode()); +} + +void ButtonsListCtrl::Rescale() +{ + m_mode_sizer->msw_rescale(); + for (ScalableButton* btn : m_pageButtons) + btn->msw_rescale(); +} + +void ButtonsListCtrl::SetSelection(int sel) +{ + if (m_selection == sel) + return; + m_selection = sel; + Refresh(); +} + +bool ButtonsListCtrl::InsertPage(size_t n, const wxString& text, bool bSelect/* = false*/, const std::string& bmp_name/* = ""*/) +{ + ScalableButton* btn = new ScalableButton(this, wxID_ANY, bmp_name, text, wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); + btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) { + if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) { + m_selection = it - m_pageButtons.begin(); + wxCommandEvent evt = wxCommandEvent(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED); + evt.SetId(m_selection); + wxPostEvent(this->GetParent(), evt); + Refresh(); + } + }); + Slic3r::GUI::wxGetApp().UpdateDarkUI(btn); + m_pageButtons.insert(m_pageButtons.begin() + n, btn); + m_sizer->Insert(n, new wxSizerItem(btn, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3)); + m_sizer->Layout(); + return true; +} + +void ButtonsListCtrl::RemovePage(size_t n) +{ + ScalableButton* btn = m_pageButtons[n]; + m_pageButtons.erase(m_pageButtons.begin() + n); + m_sizer->Remove(n); + btn->Reparent(nullptr); + btn->Destroy(); + m_sizer->Layout(); +} + +bool ButtonsListCtrl::SetPageImage(size_t n, const std::string& bmp_name) const +{ + if (n >= m_pageButtons.size()) + return false; + return m_pageButtons[n]->SetBitmap_(bmp_name); +} + +void ButtonsListCtrl::SetPageText(size_t n, const wxString& strText) +{ + ScalableButton* btn = m_pageButtons[n]; + btn->SetLabel(strText); +} + +wxString ButtonsListCtrl::GetPageText(size_t n) const +{ + ScalableButton* btn = m_pageButtons[n]; + return btn->GetLabel(); +} + +#endif // _WIN32 + + diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp new file mode 100644 index 000000000..ef42679e9 --- /dev/null +++ b/src/slic3r/GUI/Notebook.hpp @@ -0,0 +1,305 @@ +#ifndef slic3r_Notebook_hpp_ +#define slic3r_Notebook_hpp_ + +#ifdef _WIN32 + +#include + +class ModeSizer; +class ScalableButton; + +// custom message the ButtonsListCtrl sends to its parent (Notebook) to notify a selection change: +wxDECLARE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent); + +class ButtonsListCtrl : public wxControl +{ +public: + ButtonsListCtrl(wxWindow* parent, bool add_mode_buttons = false); + ~ButtonsListCtrl() {} + + void OnPaint(wxPaintEvent&); + void SetSelection(int sel); + void UpdateMode(); + void Rescale(); + bool InsertPage(size_t n, const wxString& text, bool bSelect = false, const std::string& bmp_name = ""); + void RemovePage(size_t n); + bool SetPageImage(size_t n, const std::string& bmp_name) const; + void SetPageText(size_t n, const wxString& strText); + wxString GetPageText(size_t n) const; + +private: + wxWindow* m_parent; + wxBoxSizer* m_sizer; + std::vector m_pageButtons; + int m_selection {-1}; + ModeSizer* m_mode_sizer {nullptr}; +}; + +class Notebook: public wxBookCtrlBase +{ +public: + Notebook(wxWindow * parent, + wxWindowID winid = wxID_ANY, + const wxPoint & pos = wxDefaultPosition, + const wxSize & size = wxDefaultSize, + long style = 0, + bool add_mode_buttons = false) + { + Init(); + Create(parent, winid, pos, size, style, add_mode_buttons); + } + + bool Create(wxWindow * parent, + wxWindowID winid = wxID_ANY, + const wxPoint & pos = wxDefaultPosition, + const wxSize & size = wxDefaultSize, + long style = 0, + bool add_mode_buttons = false) + { + if (!wxBookCtrlBase::Create(parent, winid, pos, size, style | wxBK_TOP)) + return false; + + m_bookctrl = new ButtonsListCtrl(this, add_mode_buttons); + + wxSizer* mainSizer = new wxBoxSizer(IsVertical() ? wxVERTICAL : wxHORIZONTAL); + + if (style & wxBK_RIGHT || style & wxBK_BOTTOM) + mainSizer->Add(0, 0, 1, wxEXPAND, 0); + + m_controlSizer = new wxBoxSizer(IsVertical() ? wxHORIZONTAL : wxVERTICAL); + m_controlSizer->Add(m_bookctrl, wxSizerFlags(1).Expand()); + wxSizerFlags flags; + if (IsVertical()) + flags.Expand(); + else + flags.CentreVertical(); + mainSizer->Add(m_controlSizer, flags.Border(wxALL, m_controlMargin)); + SetSizer(mainSizer); + + this->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt) + { + if (int page_idx = evt.GetId(); page_idx >= 0) + SetSelection(page_idx); + }); + return true; + } + + + // Methods specific to this class. + + // A method allowing to add a new page without any label (which is unused + // by this control) and show it immediately. + bool ShowNewPage(wxWindow * page) + { + return AddPage(page, wxString(), ""/*true *//* select it */); + } + + + // Set effect to use for showing/hiding pages. + void SetEffects(wxShowEffect showEffect, wxShowEffect hideEffect) + { + m_showEffect = showEffect; + m_hideEffect = hideEffect; + } + + // Or the same effect for both of them. + void SetEffect(wxShowEffect effect) + { + SetEffects(effect, effect); + } + + // And the same for time outs. + void SetEffectsTimeouts(unsigned showTimeout, unsigned hideTimeout) + { + m_showTimeout = showTimeout; + m_hideTimeout = hideTimeout; + } + + void SetEffectTimeout(unsigned timeout) + { + SetEffectsTimeouts(timeout, timeout); + } + + + // Implement base class pure virtual methods. + + // adds a new page to the control + bool AddPage(wxWindow* page, + const wxString& text, + const std::string& bmp_name, + bool bSelect = false) + { + DoInvalidateBestSize(); + return InsertPage(GetPageCount(), page, text, bmp_name, bSelect); + } + + // Page management + virtual bool InsertPage(size_t n, + wxWindow * page, + const wxString & text, + bool bSelect = false, + int imageId = NO_IMAGE) override + { + if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) + return false; + + GetBtnsListCtrl()->InsertPage(n, text, bSelect); + + if (!DoSetSelectionAfterInsertion(n, bSelect)) + page->Hide(); + + return true; + } + + bool InsertPage(size_t n, + wxWindow * page, + const wxString & text, + const std::string& bmp_name = "", + bool bSelect = false) + { + if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) + return false; + + GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name); + + if (!DoSetSelectionAfterInsertion(n, bSelect)) + page->Hide(); + + return true; + } + + virtual int SetSelection(size_t n) override + { + GetBtnsListCtrl()->SetSelection(n); + return DoSetSelection(n, SetSelection_SendEvent); + } + + virtual int ChangeSelection(size_t n) override + { + GetBtnsListCtrl()->SetSelection(n); + return DoSetSelection(n); + } + + // Neither labels nor images are supported but we still store the labels + // just in case the user code attaches some importance to them. + virtual bool SetPageText(size_t n, const wxString & strText) override + { + wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page")); + + GetBtnsListCtrl()->SetPageText(n, strText); + + return true; + } + + virtual wxString GetPageText(size_t n) const override + { + wxCHECK_MSG(n < GetPageCount(), wxString(), wxS("Invalid page")); + return GetBtnsListCtrl()->GetPageText(n); + } + + virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override + { + return false; + } + + virtual int GetPageImage(size_t WXUNUSED(n)) const override + { + return NO_IMAGE; + } + + bool SetPageImage(size_t n, const std::string& bmp_name) + { + return GetBtnsListCtrl()->SetPageImage(n, bmp_name); + } + + // Override some wxWindow methods too. + virtual void SetFocus() override + { + wxWindow* const page = GetCurrentPage(); + if (page) + page->SetFocus(); + } + + ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast(m_bookctrl); } + + void UpdateMode() + { + GetBtnsListCtrl()->UpdateMode(); + } + + void Rescale() + { + GetBtnsListCtrl()->Rescale(); + } + +protected: + virtual void UpdateSelectedPage(size_t WXUNUSED(newsel)) override + { + // Nothing to do here, but must be overridden to avoid the assert in + // the base class version. + } + + virtual wxBookCtrlEvent * CreatePageChangingEvent() const override + { + return new wxBookCtrlEvent(wxEVT_BOOKCTRL_PAGE_CHANGING, + GetId()); + } + + virtual void MakeChangedEvent(wxBookCtrlEvent & event) override + { + event.SetEventType(wxEVT_BOOKCTRL_PAGE_CHANGED); + } + + virtual wxWindow * DoRemovePage(size_t page) override + { + wxWindow* const win = wxBookCtrlBase::DoRemovePage(page); + if (win) + { + GetBtnsListCtrl()->RemovePage(page); + DoSetSelectionAfterRemoval(page); + } + + return win; + } + + virtual void DoSize() override + { + wxWindow* const page = GetCurrentPage(); + if (page) + page->SetSize(GetPageRect()); + } + + virtual void DoShowPage(wxWindow * page, bool show) override + { + if (show) + page->ShowWithEffect(m_showEffect, m_showTimeout); + else + page->HideWithEffect(m_hideEffect, m_hideTimeout); + } + +private: + void Init() + { + // We don't need any border as we don't have anything to separate the + // page contents from. + SetInternalBorder(0); + + // No effects by default. + m_showEffect = + m_hideEffect = wxSHOW_EFFECT_NONE; + + m_showTimeout = + m_hideTimeout = 0; + } + + wxShowEffect m_showEffect, + m_hideEffect; + + unsigned m_showTimeout, + m_hideTimeout; + + ButtonsListCtrl* m_ctrl{ nullptr }; + +}; +#endif // _WIN32 +#endif // slic3r_Notebook_hpp_ diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7460b43dd..064ac1b67 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -583,7 +583,7 @@ struct Sidebar::priv wxScrolledWindow *scrolled; wxPanel* presets_panel; // Used for MSW better layouts - ModeSizer *mode_sizer; + ModeSizer *mode_sizer {nullptr}; wxFlexGridSizer *sizer_presets; PlaterPresetComboBox *combo_print; std::vector combos_filament; @@ -754,7 +754,8 @@ Sidebar::Sidebar(Plater *parent) p->sliced_info = new SlicedInfo(p->scrolled); // Sizer in the scrolled area - scrolled_sizer->Add(p->mode_sizer, 0, wxALIGN_CENTER_HORIZONTAL/*RIGHT | wxBOTTOM | wxRIGHT, 5*/); + if (p->mode_sizer) + scrolled_sizer->Add(p->mode_sizer, 0, wxALIGN_CENTER_HORIZONTAL); is_msw ? scrolled_sizer->Add(p->presets_panel, 0, wxEXPAND | wxLEFT, margin_5) : scrolled_sizer->Add(p->sizer_presets, 0, wxEXPAND | wxLEFT, margin_5); @@ -944,13 +945,16 @@ void Sidebar::update_presets(Preset::Type preset_type) void Sidebar::update_mode_sizer() const { - p->mode_sizer->SetMode(m_mode); + if (p->mode_sizer) + p->mode_sizer->SetMode(m_mode); } void Sidebar::change_top_border_for_mode_sizer(bool increase_border) { - p->mode_sizer->set_items_flag(increase_border ? wxTOP : 0); - p->mode_sizer->set_items_border(increase_border ? int(0.5 * wxGetApp().em_unit()) : 0); + if (p->mode_sizer) { + p->mode_sizer->set_items_flag(increase_border ? wxTOP : 0); + p->mode_sizer->set_items_border(increase_border ? int(0.5 * wxGetApp().em_unit()) : 0); + } } void Sidebar::update_reslice_btn_tooltip() const @@ -965,7 +969,8 @@ void Sidebar::msw_rescale() { SetMinSize(wxSize(40 * wxGetApp().em_unit(), -1)); - p->mode_sizer->msw_rescale(); + if (p->mode_sizer) + p->mode_sizer->msw_rescale(); for (PlaterPresetComboBox* combo : std::vector { p->combo_print, p->combo_sla_print, @@ -1009,7 +1014,8 @@ void Sidebar::sys_color_changed() for (wxWindow* btn : std::vector{ p->btn_reslice, p->btn_export_gcode }) wxGetApp().UpdateDarkUI(btn, true); - p->mode_sizer->msw_rescale(); + if (p->mode_sizer) + p->mode_sizer->msw_rescale(); p->frequently_changed_parameters->sys_color_changed(); p->object_settings->sys_color_changed(); #endif @@ -1394,6 +1400,12 @@ void Sidebar::collapse(bool collapse) wxGetApp().app_config->set("collapsed_sidebar", collapse ? "1" : "0"); } +#ifdef _MSW_DARK_MODE +void Sidebar::show_mode_sizer(bool show) +{ + p->mode_sizer->Show(show); +} +#endif void Sidebar::update_ui_from_settings() { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 6f69ee4dc..fc4001ba5 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -112,6 +112,10 @@ public: void update_searcher(); void update_ui_from_settings(); +#ifdef _MSW_DARK_MODE + void show_mode_sizer(bool show); +#endif + std::vector& combos_filament(); Search::OptionsSearcher& get_searcher(); std::string& get_search_line(); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 6626ce9a5..7b1558599 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -6,7 +6,7 @@ #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" #include -#include +#include "Notebook.hpp" namespace Slic3r { namespace GUI { @@ -58,15 +58,12 @@ void PreferencesDialog::build() #ifdef _MSW_DARK_MODE wxBookCtrlBase* tabs; - if (wxGetApp().dark_mode()) { - tabs = new wxListbook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME | wxNO_BORDER); - wxGetApp().UpdateDarkUI(tabs); - wxGetApp().UpdateDarkUI(dynamic_cast(tabs)->GetListView()); - } - else { +// if (wxGetApp().dark_mode()) + tabs = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME | wxNB_DEFAULT); +/* else { tabs = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME | wxNB_DEFAULT); tabs->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - } + }*/ #else wxNotebook* tabs = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL |wxNB_NOPAGETHEME | wxNB_DEFAULT ); tabs->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); @@ -345,7 +342,6 @@ void PreferencesDialog::build() def.set_default_value(new ConfigOptionBool{ app_config->get("dark_color_mode") == "1" }); option = Option(def, "dark_color_mode"); m_optgroup_gui->append_single_option_line(option); -#endif def.label = L("Set settings tabs as menu items (experimental)"); def.type = coBool; @@ -354,6 +350,7 @@ void PreferencesDialog::build() def.set_default_value(new ConfigOptionBool{ app_config->get("tabs_as_menu") == "1" }); option = Option(def, "tabs_as_menu"); m_optgroup_gui->append_single_option_line(option); +#endif def.label = L("Use custom size for toolbar icons"); def.type = coBool; @@ -414,11 +411,7 @@ void PreferencesDialog::accept() // if (m_values.find("no_defaults") != m_values.end() // warning_catcher(this, wxString::Format(_L("You need to restart %s to make the changes effective."), SLIC3R_APP_NAME)); - std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu" -#ifdef _MSW_DARK_MODE - ,"dark_color_mode" -#endif - }; + std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu" }; for (const std::string& option : options_to_recreate_GUI) { if (m_values.find(option) != m_values.end()) { @@ -432,9 +425,6 @@ void PreferencesDialog::accept() wxICON_QUESTION | wxYES | wxNO); if (dialog.ShowModal() == wxID_YES) { m_recreate_GUI = true; -#ifdef _MSW_DARK_MODE - m_color_mode_changed = m_values.find("dark_color_mode") != m_values.end(); -#endif } else { for (const std::string& option : options_to_recreate_GUI) @@ -495,6 +485,11 @@ void PreferencesDialog::accept() EndModal(wxID_OK); +#ifdef _MSW_DARK_MODE + if (m_values.find("dark_color_mode") != m_values.end()) + wxGetApp().force_colors_update(); +#endif + if (m_settings_layout_changed) ;// application will be recreated after Preference dialog will be destroyed else @@ -585,7 +580,9 @@ void PreferencesDialog::create_icon_size_slider() void PreferencesDialog::create_settings_mode_widget() { - bool dark_mode = wxGetApp().dark_mode(); +#ifdef _MSW_DARK_MODE + bool disable_new_layout = wxGetApp().tabs_as_menu(); +#endif std::vector choices = { _L("Old regular layout with the tab bar"), _L("New layout, access via settings button in the top menu"), _L("Settings in non-modal window") }; @@ -596,7 +593,7 @@ void PreferencesDialog::create_settings_mode_widget() app_config->get("dlg_settings_layout_mode") == "1" ? 2 : 0; #ifdef _MSW_DARK_MODE - if (dark_mode) { + if (disable_new_layout) { choices = { _L("Old regular layout with the tab bar"), _L("Settings in non-modal window") }; selection = app_config->get("dlg_settings_layout_mode") == "1" ? 1 : 0; @@ -621,14 +618,18 @@ void PreferencesDialog::create_settings_mode_widget() int dlg_id = 2; #ifdef _MSW_DARK_MODE - if (dark_mode) + if (disable_new_layout) dlg_id = 1; #endif - btn->Bind(wxEVT_RADIOBUTTON, [this, id, dlg_id, dark_mode](wxCommandEvent& ) { + btn->Bind(wxEVT_RADIOBUTTON, [this, id, dlg_id +#ifdef _MSW_DARK_MODE + , disable_new_layout +#endif + ](wxCommandEvent& ) { m_values["old_settings_layout_mode"] = (id == 0) ? "1" : "0"; #ifdef _MSW_DARK_MODE - if (!dark_mode) + if (!disable_new_layout) m_values["new_settings_layout_mode"] = (id == 1) ? "1" : "0"; #endif m_values["dlg_settings_layout_mode"] = (id == dlg_id) ? "1" : "0"; @@ -637,7 +638,7 @@ void PreferencesDialog::create_settings_mode_widget() } auto sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(/*m_layout_mode_box*/stb_sizer, 1, wxALIGN_CENTER_VERTICAL); + sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); } diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index 2ad3858ad..bcfafff5d 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -25,7 +25,6 @@ class PreferencesDialog : public DPIDialog std::shared_ptr m_optgroup_render; #endif // ENABLE_ENVIRONMENT_MAP wxSizer* m_icon_size_sizer; - wxRadioBox* m_layout_mode_box; wxColourPickerCtrl* m_sys_colour {nullptr}; wxColourPickerCtrl* m_mod_colour {nullptr}; bool isOSX {false}; @@ -35,9 +34,7 @@ class PreferencesDialog : public DPIDialog bool m_seq_top_gcode_indices_changed{ false }; #endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER bool m_recreate_GUI{false}; -#ifdef _MSW_DARK_MODE - bool m_color_mode_changed {false}; -#endif + public: explicit PreferencesDialog(wxWindow* parent); ~PreferencesDialog() = default; @@ -48,9 +45,6 @@ public: bool seq_seq_top_gcode_indices_changed() const { return m_seq_top_gcode_indices_changed; } #endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER bool recreate_GUI() const { return m_recreate_GUI; } -#ifdef _MSW_DARK_MODE - bool color_mode_changed() const { return m_color_mode_changed; } -#endif void build(); void accept(); diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index 9c5e72fde..8fe10c882 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -208,12 +208,13 @@ SavePresetDialog::~SavePresetDialog() void SavePresetDialog::build(std::vector types, std::string suffix) { - SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); #if defined(__WXMSW__) // ys_FIXME! temporary workaround for correct font scaling // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); +#else + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); #endif // __WXMSW__ if (suffix.empty()) @@ -240,6 +241,10 @@ void SavePresetDialog::build(std::vector types, std::string suffix topSizer->SetSizeHints(this); this->CenterOnScreen(); + +#ifdef _WIN32 + wxGetApp().UpdateDlgDarkUI(this); +#endif } void SavePresetDialog::AddItem(Preset::Type type, const std::string& suffix) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 8311525ec..8aaab1ed5 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -44,6 +44,7 @@ #include "UnsavedChangesDialog.hpp" #include "SavePresetDialog.hpp" #include "MsgDialog.hpp" +#include "Notebook.hpp" #ifdef WIN32 #include @@ -255,8 +256,11 @@ void Tab::create_preset_tab() m_modified_label_clr = wxGetApp().get_label_clr_modified(); m_default_text_clr = wxGetApp().get_label_clr_default(); +#ifdef _MSW_DARK_MODE // Sizer with buttons for mode changing - m_mode_sizer = new ModeSizer(panel, int (0.5*em_unit(this))); + if (wxGetApp().tabs_as_menu()) +#endif + m_mode_sizer = new ModeSizer(panel, int (0.5*em_unit(this))); const float scale_factor = /*wxGetApp().*/em_unit(this)*0.1;// GetContentScaleFactor(); m_hsizer = new wxBoxSizer(wxHORIZONTAL); @@ -285,9 +289,11 @@ void Tab::create_preset_tab() // m_hsizer->AddStretchSpacer(32); // StretchSpacer has a strange behavior under OSX, so // There is used just additional sizer for m_mode_sizer with right alignment - auto mode_sizer = new wxBoxSizer(wxVERTICAL); - mode_sizer->Add(m_mode_sizer, 1, wxALIGN_RIGHT); - m_hsizer->Add(mode_sizer, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, wxOSX ? 15 : 10); + if (m_mode_sizer) { + auto mode_sizer = new wxBoxSizer(wxVERTICAL); + mode_sizer->Add(m_mode_sizer, 1, wxALIGN_RIGHT); + m_hsizer->Add(mode_sizer, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, wxOSX ? 15 : 10); + } //Horizontal sizer to hold the tree and the selected page. m_hsizer = new wxBoxSizer(wxHORIZONTAL); @@ -490,7 +496,8 @@ void Tab::OnActivate() // Workaroud for Menu instead of NoteBook #ifdef _MSW_DARK_MODE - if (wxGetApp().tabs_as_menu()) { +// if (wxGetApp().tabs_as_menu()) + { wxSize sz = m_presets_choice->GetSize(); wxSize ok_sz = wxSize(35 * m_em_unit, m_presets_choice->GetBestSize().y+1); if (sz != ok_sz) { @@ -943,7 +950,8 @@ void Tab::update_mode() m_mode = wxGetApp().get_mode(); // update mode for ModeSizer - m_mode_sizer->SetMode(m_mode); + if (m_mode_sizer) + m_mode_sizer->SetMode(m_mode); update_visibility(); @@ -969,7 +977,8 @@ void Tab::msw_rescale() { m_em_unit = em_unit(m_parent); - m_mode_sizer->msw_rescale(); + if (m_mode_sizer) + m_mode_sizer->msw_rescale(); m_presets_choice->msw_rescale(); m_treectrl->SetMinSize(wxSize(20 * m_em_unit, -1)); @@ -1026,7 +1035,8 @@ void Tab::sys_color_changed() update_label_colours(); #ifdef _WIN32 wxWindowUpdateLocker noUpdates(this); - m_mode_sizer->msw_rescale(); + if (m_mode_sizer) + m_mode_sizer->msw_rescale(); wxGetApp().UpdateDarkUI(this); wxGetApp().UpdateDarkUI(m_treectrl); #endif @@ -3085,7 +3095,15 @@ void Tab::load_current_preset() } if (tab->supports_printer_technology(printer_technology)) { - wxGetApp().tab_panel()->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title()); +#ifdef _MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) { + std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" : + tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "resin" : "cog"; + dynamic_cast(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(), bmp_name); + } + else +#endif + wxGetApp().tab_panel()->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title()); #ifdef __linux__ // the tabs apparently need to be explicitly shown on Linux (pull request #1563) int page_id = wxGetApp().tab_panel()->FindPage(tab); wxGetApp().tab_panel()->GetPage(page_id)->Show(true); @@ -3099,6 +3117,10 @@ void Tab::load_current_preset() } static_cast(this)->m_printer_technology = printer_technology; m_active_page = tmp_page; +#ifdef _MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(wxGetApp().tab_panel())->SetPageImage(wxGetApp().tab_panel()->FindPage(this), printer_technology == ptFFF ? "printer" : "sla_printer"); +#endif } on_presets_changed(); if (printer_technology == ptFFF) { diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 762123e60..65c817cb6 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -129,7 +129,7 @@ protected: wxScrolledWindow* m_page_view {nullptr}; wxBoxSizer* m_page_sizer {nullptr}; - ModeSizer* m_mode_sizer; + ModeSizer* m_mode_sizer {nullptr}; struct PresetDependencies { Preset::Type type = Preset::TYPE_INVALID; diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 377f50b9b..f3be89ef9 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -838,9 +838,13 @@ ScalableButton::ScalableButton( wxWindow * parent, Create(parent, id, label, pos, size, style); Slic3r::GUI::wxGetApp().UpdateDarkUI(this); - SetBitmap(create_scaled_bitmap(icon_name, parent, m_px_cnt)); - if (m_use_default_disabled_bitmap) - SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); + if (!icon_name.empty()) { + SetBitmap(create_scaled_bitmap(icon_name, parent, m_px_cnt)); + if (m_use_default_disabled_bitmap) + SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); + if (!label.empty()) + SetBitmapMargins(int(0.5* em_unit(parent)), 0); + } if (size != wxDefaultSize) { @@ -873,6 +877,20 @@ void ScalableButton::SetBitmap_(const ScalableBitmap& bmp) m_current_icon_name = bmp.name(); } +bool ScalableButton::SetBitmap_(const std::string& bmp_name) +{ + m_current_icon_name = bmp_name; + if (m_current_icon_name.empty()) + return false; + + wxBitmap bmp = create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt); + SetBitmap(bmp); + SetBitmapCurrent(bmp); + if (m_use_default_disabled_bitmap) + SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); + return true; +} + void ScalableButton::SetBitmapDisabled_(const ScalableBitmap& bmp) { SetBitmapDisabled(bmp.bmp()); @@ -898,11 +916,13 @@ void ScalableButton::msw_rescale() { Slic3r::GUI::wxGetApp().UpdateDarkUI(this, m_has_border); - SetBitmap(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt)); - if (!m_disabled_icon_name.empty()) - SetBitmapDisabled(create_scaled_bitmap(m_disabled_icon_name, m_parent, m_px_cnt)); - else if (m_use_default_disabled_bitmap) - SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); + if (!m_current_icon_name.empty()) { + SetBitmap(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt)); + if (!m_disabled_icon_name.empty()) + SetBitmapDisabled(create_scaled_bitmap(m_disabled_icon_name, m_parent, m_px_cnt)); + else if (m_use_default_disabled_bitmap) + SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); + } if (m_width > 0 || m_height>0) { diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 1664a51f3..2b8965ba2 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -231,6 +231,7 @@ public: ~ScalableButton() {} void SetBitmap_(const ScalableBitmap& bmp); + bool SetBitmap_(const std::string& bmp_name); void SetBitmapDisabled_(const ScalableBitmap &bmp); int GetBitmapHeight(); void UseDefaultBitmapDisabled(); From f02821a82dcb61311d32178989601aca920d366b Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 29 Jun 2021 17:55:23 +0200 Subject: [PATCH 30/37] Renamed the "marlinfirmware" firmware flavor to "marlin2". --- resources/profiles/PrusaResearch.ini | 2 +- src/libslic3r/PrintConfig.cpp | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index f95c2b61c..57b185872 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -6325,7 +6325,7 @@ thumbnails = 16x16,220x124 bed_shape = 0x0,180x0,180x180,0x180 default_filament_profile = "Prusament PLA" default_print_profile = 0.15mm QUALITY @MINI -gcode_flavor = marlinfirmware +gcode_flavor = marlin2 machine_max_acceleration_e = 5000 machine_max_acceleration_extruding = 1250 machine_max_acceleration_retracting = 1250 diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a1d86d469..76dbac263 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -50,7 +50,7 @@ static t_config_enum_values s_keys_map_GCodeFlavor { { "teacup", gcfTeacup }, { "makerware", gcfMakerWare }, { "marlin", gcfMarlinLegacy }, - { "marlinfirmware", gcfMarlinFirmware }, + { "marlin2", gcfMarlinFirmware }, { "sailfish", gcfSailfish }, { "smoothie", gcfSmoothie }, { "mach3", gcfMach3 }, @@ -1253,7 +1253,7 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("teacup"); def->enum_values.push_back("makerware"); def->enum_values.push_back("marlin"); - def->enum_values.push_back("marlinfirmware"); + def->enum_values.push_back("marlin2"); def->enum_values.push_back("sailfish"); def->enum_values.push_back("mach3"); def->enum_values.push_back("machinekit"); @@ -3618,8 +3618,12 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } catch (boost::bad_lexical_cast &) { value = "0"; } - } else if (opt_key == "gcode_flavor" && value == "makerbot") { - value = "makerware"; + } else if (opt_key == "gcode_flavor") { + if (value == "makerbot") + value = "makerware"; + else if (value == "marlinfirmware") + // the "new" marlin firmware flavor used to be called "marlinfirmware" for some time during PrusaSlicer 2.4.0-alpha development. + value = "marlin2"; } else if (opt_key == "fill_density" && value.find("%") == std::string::npos) { try { // fill_density was turned into a percent value From 8da083b70274d65dec8fb8fa361f1beeb92bac53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Wed, 30 Jun 2021 08:56:35 +0200 Subject: [PATCH 31/37] Fixed compilation on Linux --- src/slic3r/GUI/MsgDialog.cpp | 2 +- src/slic3r/GUI/MsgDialog.hpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 3a894b84f..9af642a25 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -17,7 +17,7 @@ #include "I18N.hpp" #include "ConfigWizard.hpp" #include "wxExtensions.hpp" -#include "Mainframe.hpp" +#include "slic3r/GUI/MainFrame.hpp" #include "GUI_App.hpp" namespace Slic3r { diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index e34177e7e..ac4fa44e6 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -7,6 +7,7 @@ #include #include #include +#include class wxBoxSizer; class wxCheckBox; From ffc0e36570c6a7f3a0d0b4e871d96c65d04a8023 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Wed, 30 Jun 2021 10:02:14 +0200 Subject: [PATCH 32/37] Fixed locales switching on macOS --- src/libslic3r/LocalesUtils.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/LocalesUtils.cpp b/src/libslic3r/LocalesUtils.cpp index 8f4d26642..64ab700ed 100644 --- a/src/libslic3r/LocalesUtils.cpp +++ b/src/libslic3r/LocalesUtils.cpp @@ -19,6 +19,7 @@ CNumericLocalesSetter::CNumericLocalesSetter() #else // APPLE m_original_locale = uselocale((locale_t)0); m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_original_locale); + uselocale(m_new_locale); #endif } From a0328772b95915b13087554668e59275fcd38ba2 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 30 Jun 2021 10:44:04 +0200 Subject: [PATCH 33/37] Follow-up https://github.com/prusa3d/PrusaSlicer/commit/4652733201f446f062e37574d4a44f80c1541592 - Fixed Text alignment for Notebook tabs without bitmaps --- src/slic3r/GUI/Notebook.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index 0aa11da5b..25a0b565f 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -83,7 +83,7 @@ void ButtonsListCtrl::SetSelection(int sel) bool ButtonsListCtrl::InsertPage(size_t n, const wxString& text, bool bSelect/* = false*/, const std::string& bmp_name/* = ""*/) { - ScalableButton* btn = new ScalableButton(this, wxID_ANY, bmp_name, text, wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); + ScalableButton* btn = new ScalableButton(this, wxID_ANY, bmp_name, text, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER | (bmp_name.empty() ? 0 : wxBU_LEFT)); btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) { if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) { m_selection = it - m_pageButtons.begin(); From 1d3b259c0ab427c2c26009a6d8bda75ef3e67eec Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Wed, 30 Jun 2021 11:59:16 +0200 Subject: [PATCH 34/37] Fixed incorrect use of _NDEBUG instead of NDEBUG Some debugging code was compiled even in release mode because of this --- src/libslic3r/AppConfig.hpp | 4 ++-- src/libslic3r/MutablePolygon.cpp | 2 +- src/libslic3r/TriangleSelector.cpp | 22 +++++++++++----------- src/libslic3r/TriangleSelector.hpp | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index d4f125f60..07b09c5c8 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -63,12 +63,12 @@ public: { std::string value; this->get("", key, value); return value; } void set(const std::string §ion, const std::string &key, const std::string &value) { -#ifndef _NDEBUG +#ifndef NDEBUG std::string key_trimmed = key; boost::trim_all(key_trimmed); assert(key_trimmed == key); assert(! key_trimmed.empty()); -#endif // _NDEBUG +#endif // NDEBUG std::string &old = m_storage[section][key]; if (old != value) { old = value; diff --git a/src/libslic3r/MutablePolygon.cpp b/src/libslic3r/MutablePolygon.cpp index dc1d47731..403d625bf 100644 --- a/src/libslic3r/MutablePolygon.cpp +++ b/src/libslic3r/MutablePolygon.cpp @@ -166,7 +166,7 @@ static bool clip_narrow_corner( assert(orient1 > 0 == blocked); assert(orient2 > 0 == blocked); } -#endif // _NDEBUG +#endif // NDEBUG if (polygon.size() < 3 || (forward == Far && backward == Far)) { polygon.clear(); } else { diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index d7efd09b1..a3f342319 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -3,9 +3,9 @@ #include -#ifndef _NDEBUG +#ifndef NDEBUG #define EXPENSIVE_DEBUG_CHECKS -#endif // _NDEBUG +#endif // NDEBUG namespace Slic3r { @@ -19,7 +19,7 @@ static inline Vec3i root_neighbors(const TriangleMesh &mesh, int triangle_id) return neighbors; } -#ifndef _NDEBUG +#ifndef NDEBUG bool TriangleSelector::verify_triangle_midpoints(const Triangle &tr) const { for (int i = 0; i < 3; ++ i) { @@ -57,7 +57,7 @@ bool TriangleSelector::verify_triangle_neighbors(const Triangle &tr, const Vec3i } return true; } -#endif // _NDEBUG +#endif // NDEBUG // sides_to_split==-1 : just restore previous split void TriangleSelector::Triangle::set_division(int sides_to_split, int special_side_idx) @@ -308,12 +308,12 @@ int TriangleSelector::triangle_midpoint_or_allocate(int itriangle, int vertexi, } assert(m_vertices[midpoint].ref_cnt == 0); } else { -#ifndef _NDEBUG +#ifndef NDEBUG Vec3f c1 = 0.5f * (m_vertices[vertexi].v + m_vertices[vertexj].v); Vec3f c2 = m_vertices[midpoint].v; float d = (c2 - c1).norm(); assert(std::abs(d) < EPSILON); -#endif // _NDEBUG +#endif // NDEBUG assert(m_vertices[midpoint].ref_cnt > 0); } return midpoint; @@ -816,13 +816,13 @@ void TriangleSelector::perform_split(int facet_idx, const Vec3i &neighbors, Enfo assert(tr.is_split()); // indices of triangle vertices -#ifdef _NDEBUG +#ifdef NDEBUG boost::container::small_vector verts_idxs; -#else // _NDEBUG +#else // NDEBUG // For easier debugging. std::vector verts_idxs; verts_idxs.reserve(6); -#endif // _NDEBUG +#endif // NDEBUG for (int j=0, idx = tr.special_side(); j<3; ++j, idx = next_idx_modulo(idx, 3)) verts_idxs.push_back(tr.verts_idxs[idx]); @@ -861,13 +861,13 @@ void TriangleSelector::perform_split(int facet_idx, const Vec3i &neighbors, Enfo break; } -#ifndef _NDEBUG +#ifndef NDEBUG assert(this->verify_triangle_neighbors(tr, neighbors)); for (int i = 0; i <= tr.number_of_split_sides(); ++i) { Vec3i n = this->child_neighbors(tr, neighbors, i); assert(this->verify_triangle_neighbors(m_triangles[tr.children[i]], n)); } -#endif // _NDEBUG +#endif // NDEBUG } bool TriangleSelector::has_facets(EnforcerBlockerType state) const diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 5115bb02a..6efae2150 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -204,10 +204,10 @@ private: int triangle_midpoint(int itriangle, int vertexi, int vertexj) const; int triangle_midpoint_or_allocate(int itriangle, int vertexi, int vertexj); -#ifndef _NDEBUG +#ifndef NDEBUG bool verify_triangle_neighbors(const Triangle& tr, const Vec3i& neighbors) const; bool verify_triangle_midpoints(const Triangle& tr) const; -#endif // _NDEBUG +#endif // NDEBUG void get_facets_strict_recursive( const Triangle &tr, From fd36cb772ddc6a284f10b6c1c586baecf9f9431a Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Thu, 1 Jul 2021 16:37:18 +0200 Subject: [PATCH 35/37] Decreased Area Fill (SL1S). --- resources/profiles/PrusaResearch.idx | 1 + resources/profiles/PrusaResearch.ini | 25 ++----------------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index fa8d86a0c..a09ebd5b3 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,4 +1,5 @@ min_slic3r_version = 2.4.0-alpha0 +1.4.0-alpha4 Decreased Area Fill (SL1S). 1.4.0-alpha3 Updated SL1S tilt times. 1.4.0-alpha2 Updated Prusa MINI machine limits. 1.4.0-alpha1 Added new SL1S resin profiles. diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 57b185872..74fead88d 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -5,7 +5,7 @@ name = Prusa Research # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the PrusaSlicer configuration to be downgraded. -config_version = 1.4.0-alpha3 +config_version = 1.4.0-alpha4 # Where to get the updates from? config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaResearch/ changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1% @@ -5440,13 +5440,6 @@ initial_exposure_time = 25 material_type = Tough material_vendor = 3DM -[sla_material:Peopoly Clear Tough @0.025 SL1S] -inherits = *0.025_sl1s* -exposure_time = 1.8 -initial_exposure_time = 25 -material_type = Tough -material_vendor = Peopoly - [sla_material:Peopoly Deft White @0.025 SL1S] inherits = *0.025_sl1s* exposure_time = 1.8 @@ -5561,13 +5554,6 @@ initial_exposure_time = 25 material_type = Tough material_vendor = 3DM -[sla_material:Peopoly Clear Tough @0.05 SL1S] -inherits = *0.05_sl1s* -exposure_time = 2 -initial_exposure_time = 25 -material_type = Tough -material_vendor = Peopoly - [sla_material:Peopoly Deft White @0.05 SL1S] inherits = *0.05_sl1s* exposure_time = 2 @@ -5682,13 +5668,6 @@ initial_exposure_time = 25 material_type = Tough material_vendor = 3DM -[sla_material:Peopoly Clear Tough @0.1 SL1S] -inherits = *0.1_sl1s* -exposure_time = 2.6 -initial_exposure_time = 25 -material_type = Tough -material_vendor = Peopoly - [sla_material:Peopoly Deft White @0.1 SL1S] inherits = *0.1_sl1s* exposure_time = 2.6 @@ -6447,7 +6426,7 @@ min_initial_exposure_time = 1 max_initial_exposure_time = 300 printer_correction = 1,1,1 relative_correction = 1,1 -area_fill = 50 +area_fill = 45 # The obsolete presets will be removed when upgrading from the legacy configuration structure (up to Slic3r 1.39.2) to 1.40.0 and newer. [obsolete_presets] From 029330d656825e773cbfa739312a965ab91d9b8f Mon Sep 17 00:00:00 2001 From: David Kocik Date: Fri, 2 Jul 2021 15:13:03 +0200 Subject: [PATCH 36/37] fix of infinite loop in notification lines calulating #6583 --- src/slic3r/GUI/NotificationManager.cpp | 33 +++++++++++++------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 921f9f2b1..6f51f0cf8 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -272,27 +272,28 @@ void NotificationManager::PopNotification::count_lines() // find next suitable endline if (ImGui::CalcTextSize(text.substr(last_end).c_str()).x >= m_window_width - m_window_width_offset) { // more than one line till end - size_t next_space = text.find_first_of(' ', last_end); - if (next_space > 0) { - size_t next_space_candidate = text.find_first_of(' ', next_space + 1); + int next_space = text.find_first_of(' ', last_end); + if (next_space > 0 && next_space < text.length()) { + int next_space_candidate = text.find_first_of(' ', next_space + 1); while (next_space_candidate > 0 && ImGui::CalcTextSize(text.substr(last_end, next_space_candidate - last_end).c_str()).x < m_window_width - m_window_width_offset) { next_space = next_space_candidate; next_space_candidate = text.find_first_of(' ', next_space + 1); } - // when one word longer than line. - if (ImGui::CalcTextSize(text.substr(last_end, next_space - last_end).c_str()).x > m_window_width - m_window_width_offset) { - float width_of_a = ImGui::CalcTextSize("a").x; - int letter_count = (int)((m_window_width - m_window_width_offset) / width_of_a); - while (last_end + letter_count < text.size() && ImGui::CalcTextSize(text.substr(last_end, letter_count).c_str()).x < m_window_width - m_window_width_offset) { - letter_count++; - } - m_endlines.push_back(last_end + letter_count); - last_end += letter_count; - } - else { - m_endlines.push_back(next_space); - last_end = next_space + 1; + } else { + next_space = text.length(); + } + // when one word longer than line. + if (ImGui::CalcTextSize(text.substr(last_end, next_space - last_end).c_str()).x > m_window_width - m_window_width_offset) { + float width_of_a = ImGui::CalcTextSize("a").x; + int letter_count = (int)((m_window_width - m_window_width_offset) / width_of_a); + while (last_end + letter_count < text.size() && ImGui::CalcTextSize(text.substr(last_end, letter_count).c_str()).x < m_window_width - m_window_width_offset) { + letter_count++; } + m_endlines.push_back(last_end + letter_count); + last_end += letter_count; + } else { + m_endlines.push_back(next_space); + last_end = next_space + 1; } } else { From 3fa78b52b2958be49c442136d8b4971b0a0d4d5c Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Fri, 2 Jul 2021 17:05:26 +0200 Subject: [PATCH 37/37] Slightly improved 'no first layer extrusions' error message --- src/libslic3r/GCode.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ca76a2320..24bd4939f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -523,7 +523,8 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // Check that there are extrusions on the very first layer. if (layers_to_print.size() == 1u) { if (!has_extrusions) - throw Slic3r::SlicingError(_(L("There is an object with no extrusions on the first layer."))); + throw Slic3r::SlicingError(_(L("There is an object with no extrusions in the first layer.")) + "\n" + + _(L("Object name")) + ": " + object.model_object()->name); } // In case there are extrusions on this layer, check there is a layer to lay it on. @@ -541,7 +542,7 @@ std::vector GCode::collect_layers_to_print(const PrintObjec if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON) { const_cast(object.print())->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, - _(L("Empty layers detected. Make sure the object is printable.")) + "\n\n" + + _(L("Empty layers detected. Make sure the object is printable.")) + "\n" + _(L("Object name")) + ": " + object.model_object()->name + "\n" + _(L("Print z")) + ": " + std::to_string(layers_to_print.back().print_z()) + "\n\n" + _(L("This is " "usually caused by negligibly small extrusions or by a faulty model. Try to repair "