diff --git a/resources/icons/info.svg b/resources/icons/info.svg new file mode 100644 index 000000000..276b26061 --- /dev/null +++ b/resources/icons/info.svg @@ -0,0 +1,71 @@ + +image/svg+xml + + + + + + + + + + diff --git a/resources/icons/white/info.svg b/resources/icons/white/info.svg new file mode 100644 index 000000000..db227aa32 --- /dev/null +++ b/resources/icons/white/info.svg @@ -0,0 +1,71 @@ + +image/svg+xml + + + + + + + + + + diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 08bcf49c9..4fdf17b16 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -110,7 +110,8 @@ int CLI::run(int argc, char **argv) boost::algorithm::iends_with(boost::filesystem::path(argv[0]).filename().string(), GCODEVIEWER_APP_CMD); #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) { @@ -122,13 +123,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 = Slic3r::printer_technology(config); if (printer_technology == ptUnknown) { @@ -166,7 +173,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 = Slic3r::printer_technology(config); if (printer_technology == ptUnknown) { printer_technology = other_printer_technology; @@ -175,6 +184,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/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 8172f02e9..04ba24988 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -3,6 +3,7 @@ #include "AppConfig.hpp" #include "Exception.hpp" #include "Thread.hpp" +#include "format.hpp" #include #include @@ -16,9 +17,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 { @@ -263,15 +268,103 @@ 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) { +#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 @@ -283,6 +376,7 @@ std::string AppConfig::load() */ return ex.what(); } + } // 2) Parse the property_tree, extract the sections and key / value pairs. for (const auto §ion : tree) { @@ -358,22 +452,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 std::pair &kvp : m_storage[""]) - c << kvp.first << " = " << kvp.second << std::endl; + for (const auto& kvp : m_storage[""]) + 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; - for (const std::pair &kvp : category.second) - c << kvp.first << " = " << kvp.second << std::endl; + config_ss << std::endl << "[" << category.first << "]" << std::endl; + for (const auto& kvp : category.second) + config_ss << kvp.first << " = " << kvp.second << std::endl; } // Write vendor sections for (const auto &vendor : m_vendors) { @@ -381,17 +474,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; } } + // 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 c8ccd18cd..0a53a5330 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(); diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 33e79aa88..1678d2e2f 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -34,6 +34,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 f664c91fd..35e4a0b26 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -21,6 +21,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 { @@ -244,6 +248,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; @@ -270,7 +278,7 @@ ConfigOption* ConfigOptionDef::create_empty_option() const case coPercents: return new ConfigOptionPercentsNullable(); case coFloatsOrPercents: return new ConfigOptionFloatsOrPercentsNullable(); case coBools: return new ConfigOptionBoolsNullable(); - default: throw Slic3r::RuntimeError(std::string("Unknown option type for nullable option ") + this->label); + default: throw ConfigurationError(std::string("Unknown option type for nullable option ") + this->label); } } else { switch (this->type) { @@ -291,7 +299,7 @@ ConfigOption* ConfigOptionDef::create_empty_option() const case coBool: return new ConfigOptionBool(); case coBools: return new ConfigOptionBools(); case coEnum: return new ConfigOptionEnumGeneric(this->enum_keys_map); - default: throw Slic3r::RuntimeError(std::string("Unknown option type for option ") + this->label); + default: throw ConfigurationError(std::string("Unknown option type for option ") + this->label); } } } @@ -394,7 +402,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()) { @@ -525,7 +534,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; @@ -535,23 +544,22 @@ 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)) { - throw BadOptionTypeException(format("ConfigBase::set_deserialize() failed for parameter \"%1%\", value \"%2%\"", opt_key_src, value_src)); - } + if (! this->set_deserialize_nothrow(opt_key_src, value_src, substitutions_ctxt, append)) + throw BadOptionValueException(format("Invalid value provided for parameter %1%: %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); + for (const SetDeserializeItem &item : items) + 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; // Try to deserialize the option by its name. @@ -580,7 +588,7 @@ 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; } @@ -588,10 +596,56 @@ bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, con ConfigOption *opt = this->option(opt_key, true); if (opt == nullptr) throw new UnknownOptionException(opt_key); + bool success = true; + if (!optdef->can_phony || !value.empty()) { + success = true; + bool substituted = false; + if (optdef->type == coBools && substitutions_ctxt.rule != ForwardCompatibilitySubstitutionRule::Disable) { + //FIXME Special handling of vectors of bools, quick and not so dirty solution before PrusaSlicer 2.3.2 release. + bool nullable = opt->nullable(); + ConfigHelpers::DeserializationSubstitution default_value = ConfigHelpers::DeserializationSubstitution::DefaultsToFalse; + if (optdef->default_value) { + // Default value for vectors of booleans used in a "per extruder" context, thus the default contains just a single value. + assert(dynamic_cast*>(optdef->default_value.get())); + auto &values = static_cast*>(optdef->default_value.get())->values; + if (values.size() == 1 && values.front() == 1) + default_value = ConfigHelpers::DeserializationSubstitution::DefaultsToTrue; + } + auto result = nullable ? + static_cast(opt)->deserialize_with_substitutions(value, append, default_value) : + static_cast(opt)->deserialize_with_substitutions(value, append, default_value); + success = result != ConfigHelpers::DeserializationResult::Failed; + substituted = result == ConfigHelpers::DeserializationResult::Substituted; + } else { + 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) && ConfigHelpers::looks_like_enum_value(value)) { + // Deserialize failed, try to substitute with a default value. + assert(substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::Enable || substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::EnableSilent); + if (optdef->type == coEnum && opt_key == "gcode_flavor" && (value == "marlin2" || value == "marlinfirmware")) + static_cast*>(opt)->value = gcfMarlin; + else if (optdef->type == coBool) + static_cast(opt)->value = ConfigHelpers::enum_looks_like_true_value(value); + else + // Just use the default of the option. + opt->set(optdef->default_value.get()); + success = true; + substituted = true; + } + } - bool ok = true; - if (!optdef->can_phony || !value.empty()) - ok = opt->deserialize(value, append); + if (substituted && (substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::Enable || + substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent)) { + // Log the substitution. + ConfigSubstitution config_substitution; + config_substitution.opt_def = optdef; + config_substitution.old_value = value; + config_substitution.new_value = ConfigOptionUniquePtr(opt->clone()); + substitutions_ctxt.substitutions.emplace_back(std::move(config_substitution)); + } + } //set phony status if (optdef->can_phony) if(value.empty()) @@ -600,8 +654,7 @@ bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, con opt->set_phony(false); else opt->set_phony(false); - - return ok; + return success; } // Return an absolute value of a possibly relative config variable. @@ -662,7 +715,7 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key) const cast_opt->get_abs_value(this->get_abs_value(opt_def->ratio_over)); } std::stringstream ss; ss << "ConfigBase::get_abs_value(): "<< opt_key<<" has not a valid option type for get_abs_value()"; - throw Slic3r::RuntimeError(ss.str()); + throw ConfigurationError(ss.str()); } // Return an absolute value of a possibly relative config variable. @@ -673,7 +726,7 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key, double rati const ConfigOption *raw_opt = this->option(opt_key); assert(raw_opt != nullptr); if (raw_opt->type() != coFloatOrPercent) - throw Slic3r::RuntimeError("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent"); + throw ConfigurationError("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent"); // Compute absolute value. return static_cast(raw_opt)->get_abs_value(ratio_over); } @@ -696,67 +749,78 @@ 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, 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) { + try { 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); + } catch (const ConfigurationError &e) { + throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file, e.what())); +} } -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) +ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule) { + try { // Read a 64k block from the end of the G-code. - boost::nowide::ifstream ifs(file); - { - const char slic3r_gcode_header[] = "; generated by Slic3r "; - const char slic3rpp_gcode_header[] = "; generated by Slic3r++ "; - const char superslicer_gcode_header[] = "; generated by SuperSlicer "; - const char prusaslicer_gcode_header[] = "; generated by PrusaSlicer "; - std::string firstline; - std::getline(ifs, firstline); - if (strncmp(slic3r_gcode_header, firstline.c_str(), strlen(slic3r_gcode_header)) != 0 && + boost::nowide::ifstream ifs(file); + { + const char slic3r_gcode_header[] = "; generated by Slic3r "; + const char slic3rpp_gcode_header[] = "; generated by Slic3r++ "; + const char superslicer_gcode_header[] = "; generated by SuperSlicer "; + const char prusaslicer_gcode_header[] = "; generated by PrusaSlicer "; + std::string firstline; + std::getline(ifs, firstline); + if (strncmp(slic3r_gcode_header, firstline.c_str(), strlen(slic3r_gcode_header)) != 0 && strncmp(slic3rpp_gcode_header, firstline.c_str(), strlen(slic3rpp_gcode_header)) != 0 && strncmp(superslicer_gcode_header, firstline.c_str(), strlen(superslicer_gcode_header)) != 0 && strncmp(prusaslicer_gcode_header, firstline.c_str(), strlen(prusaslicer_gcode_header)) != 0) - throw Slic3r::RuntimeError("Not a g-code recognized for configuration import."); - } + throw ConfigurationError("Not a g-code recognized for configuration import."); + } ifs.seekg(0, ifs.end); - auto file_length = ifs.tellg(); - auto data_length = std::min(65535, file_length); - ifs.seekg(file_length - data_length, ifs.beg); + auto file_length = ifs.tellg(); + auto data_length = std::min(65535, file_length); + ifs.seekg(file_length - data_length, ifs.beg); std::vector data(size_t(data_length) + 1, 0); 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)); + throw ConfigurationError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs)); + return std::move(substitutions_ctxt.substitutions); + } catch (const ConfigurationError &e) { + throw ConfigurationError(format("Failed loading configuration from G-code \"%1%\": %2%", file, e.what())); +} } // 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; @@ -801,8 +865,7 @@ size_t ConfigBase::load_from_gcode_string(const char* str) if (key == nullptr) break; try { - //change it from set_deserialize to set_deserialize_nothrow to allow bad/old config to swtch to default value. - if(this->set_deserialize_nothrow(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 */) { @@ -839,7 +902,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); } } @@ -890,7 +953,7 @@ ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key, bool cre throw NoDefinitionException(opt_key); const ConfigOptionDef *optdef = def->get(opt_key); if (optdef == nullptr) -// throw Slic3r::RuntimeError(std::string("Invalid option name: ") + opt_key); +// throw ConfigurationError(std::string("Invalid option name: ") + opt_key); // Let the parent decide what to do if the opt_key is not defined by this->def(). return nullptr; ConfigOption *opt = optdef->create_default_option(); @@ -1010,8 +1073,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 058bebeb6..258757d07 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 @@ -75,23 +76,59 @@ enum OptionCategory : int }; std::string toString(OptionCategory opt); -/// Specialization of std::exception to indicate that an unknown config option has been encountered. -class UnknownOptionException : public Slic3r::RuntimeError { -public: - UnknownOptionException() : - Slic3r::RuntimeError("Unknown option exception") {} - UnknownOptionException(const std::string &opt_key) : - Slic3r::RuntimeError(std::string("Unknown option exception: ") + opt_key) {} +namespace ConfigHelpers { + inline bool looks_like_enum_value(std::string value) + { + boost::trim(value); + if (value.empty() || value.size() > 64 || ! isalpha(value.front())) + return false; + for (const char c : value) + if (! (isalnum(c) || c == '_' || c == '-')) + return false; + return true; + } + + inline bool enum_looks_like_true_value(std::string value) { + boost::trim(value); + return boost::iequals(value, "enabled") || boost::iequals(value, "on"); + } + + enum class DeserializationSubstitution { + Disabled, + DefaultsToFalse, + DefaultsToTrue + }; + + enum class DeserializationResult { + Loaded, + Substituted, + Failed, + }; }; -/// Indicate that the ConfigBase derived class does not provide config definition (the method def() returns null). -class NoDefinitionException : public Slic3r::RuntimeError +// Base for all exceptions thrown by the configuration layer. +class ConfigurationError : public Slic3r::RuntimeError { +public: + using RuntimeError::RuntimeError; +}; + +// Specialization of std::exception to indicate that an unknown config option has been encountered. +class UnknownOptionException : public ConfigurationError { +public: + UnknownOptionException() : + ConfigurationError("Unknown option exception") {} + UnknownOptionException(const std::string &opt_key) : + ConfigurationError(std::string("Unknown option exception: ") + opt_key) {} +}; + +// Indicate that the ConfigBase derived class does not provide config definition (the method def() returns null). +class NoDefinitionException : public ConfigurationError { public: NoDefinitionException() : - Slic3r::RuntimeError("No definition exception") {} + ConfigurationError("No definition exception") {} NoDefinitionException(const std::string &opt_key) : - Slic3r::RuntimeError(std::string("No definition exception: ") + opt_key) {} + ConfigurationError(std::string("No definition exception: ") + opt_key) {} }; // a bit more specific than a runtime_error class ConfigurationException : public std::runtime_error @@ -103,13 +140,22 @@ public: std::runtime_error(std::string("Configuration exception: ") + opt_key) {} }; -/// Indicate that an unsupported accessor was called on a config option. -class BadOptionTypeException : public Slic3r::RuntimeError +// Indicate that an unsupported accessor was called on a config option. +class BadOptionTypeException : public ConfigurationError { public: - BadOptionTypeException() : Slic3r::RuntimeError("Bad option type exception") {} - BadOptionTypeException(const std::string &message) : Slic3r::RuntimeError(message) {} - BadOptionTypeException(const char* message) : Slic3r::RuntimeError(message) {} + BadOptionTypeException() : ConfigurationError("Bad option type exception") {} + BadOptionTypeException(const std::string &message) : ConfigurationError(message) {} + BadOptionTypeException(const char* message) : ConfigurationError(message) {} +}; + +// Indicate that an option has been deserialized from an invalid value. +class BadOptionValueException : public ConfigurationError +{ +public: + BadOptionValueException() : ConfigurationError("Bad option value exception") {} + BadOptionValueException(const std::string &message) : ConfigurationError(message) {} + BadOptionValueException(const char* message) : ConfigurationError(message) {} }; // Type of a configuration value. @@ -208,8 +254,47 @@ inline OutputFormat operator&=(OutputFormat& a, OutputFormat b) { a = a & b; return a; } +enum ForwardCompatibilitySubstitutionRule +{ + // Disable susbtitution, throw exception if an option value is not recognized. + Disable, + // Enable substitution of an unknown option value with default. Log the substitution. + Enable, + // Enable substitution of an unknown option value with default. Don't log the substitution. + EnableSilent, + // Enable substitution of an unknown option value with default. Log substitutions in user profiles, don't log substitutions in system profiles. + EnableSystemSilent, + // Enable silent substitution of an unknown option value with default when loading user profiles. Throw on an unknown option value in a system profile. + EnableSilentDisableSystem, +}; +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 { @@ -277,7 +362,7 @@ public: void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionSingle: Assigning an incompatible type"); + throw ConfigurationError("ConfigOptionSingle: Assigning an incompatible type"); assert(dynamic_cast*>(rhs)); this->value = static_cast*>(rhs)->value; this->phony = rhs->phony; @@ -286,7 +371,7 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionSingle: Comparing incompatible types"); + throw ConfigurationError("ConfigOptionSingle: Comparing incompatible types"); assert(dynamic_cast*>(&rhs)); return this->value == static_cast*>(&rhs)->value; } @@ -352,7 +437,7 @@ public: void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionVector: Assigning an incompatible type"); + throw ConfigurationError("ConfigOptionVector: Assigning an incompatible type"); assert(dynamic_cast*>(rhs)); this->values = static_cast*>(rhs)->values; this->phony = rhs->phony; @@ -370,12 +455,12 @@ public: if (opt->type() == this->type()) { auto other = static_cast*>(opt); if (other->values.empty()) - throw Slic3r::RuntimeError("ConfigOptionVector::set(): Assigning from an empty vector"); + throw ConfigurationError("ConfigOptionVector::set(): Assigning from an empty vector"); this->values.emplace_back(other->values.front()); } else if (opt->type() == this->scalar_type()) this->values.emplace_back(static_cast*>(opt)->value); else - throw Slic3r::RuntimeError("ConfigOptionVector::set():: Assigning an incompatible type"); + throw ConfigurationError("ConfigOptionVector::set():: Assigning an incompatible type"); } } @@ -394,12 +479,12 @@ public: // Assign the first value of the rhs vector. auto other = static_cast*>(rhs); if (other->values.empty()) - throw Slic3r::RuntimeError("ConfigOptionVector::set_at(): Assigning from an empty vector"); + throw ConfigurationError("ConfigOptionVector::set_at(): Assigning from an empty vector"); this->values[i] = other->get_at(j); } else if (rhs->type() == this->scalar_type()) this->values[i] = static_cast*>(rhs)->value; else - throw Slic3r::RuntimeError("ConfigOptionVector::set_at(): Assigning an incompatible type"); + throw ConfigurationError("ConfigOptionVector::set_at(): Assigning an incompatible type"); } const T& get_at(size_t i) const @@ -426,7 +511,7 @@ public: if (opt_default == nullptr) this->values.resize(n, this->default_value); if (opt_default->type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionVector::resize(): Extending with an incompatible type."); + throw ConfigurationError("ConfigOptionVector::resize(): Extending with an incompatible type."); if(static_cast*>(opt_default)->values.empty()) this->values.resize(n, this->default_value); else @@ -446,7 +531,7 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionVector: Comparing incompatible types"); + throw ConfigurationError("ConfigOptionVector: Comparing incompatible types"); assert(dynamic_cast*>(&rhs)); return this->values == static_cast*>(&rhs)->values; } @@ -458,9 +543,9 @@ public: // An option overrides another option if it is not nil and not equal. bool overriden_by(const ConfigOption *rhs) const override { if (this->nullable()) - throw Slic3r::RuntimeError("Cannot override a nullable ConfigOption."); + throw ConfigurationError("Cannot override a nullable ConfigOption."); if (rhs->type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionVector.overriden_by() applied to different types."); + throw ConfigurationError("ConfigOptionVector.overriden_by() applied to different types."); auto rhs_vec = static_cast*>(rhs); if (! rhs->nullable()) // Overridding a non-nullable object with another non-nullable object. @@ -478,9 +563,9 @@ public: // Apply an override option, possibly a nullable one. bool apply_override(const ConfigOption *rhs) override { if (this->nullable()) - throw Slic3r::RuntimeError("Cannot override a nullable ConfigOption."); + throw ConfigurationError("Cannot override a nullable ConfigOption."); if (rhs->type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionVector.apply_override() applied to different types."); + throw ConfigurationError("ConfigOptionVector.apply_override() applied to different types."); auto rhs_vec = static_cast*>(rhs); if (! rhs->nullable()) { // Overridding a non-nullable object with another non-nullable object. @@ -571,7 +656,7 @@ public: bool operator==(const ConfigOptionFloatsTempl &rhs) const { return vectors_equal(this->values, rhs.values); } bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionFloatsTempl: Comparing incompatible types"); + throw ConfigurationError("ConfigOptionFloatsTempl: Comparing incompatible types"); assert(dynamic_cast*>(&rhs)); return vectors_equal(this->values, static_cast*>(&rhs)->values); } @@ -618,7 +703,7 @@ public: if (NULLABLE) this->values.push_back(nil_value()); else - throw Slic3r::RuntimeError("Deserializing nil into a non-nullable object"); + throw ConfigurationError("Deserializing nil into a non-nullable object"); } else { std::istringstream iss(item_str); double value; @@ -643,9 +728,9 @@ protected: if (NULLABLE) ss << "nil"; else - throw Slic3r::RuntimeError("Serializing NaN"); + throw ConfigurationError("Serializing NaN"); } else - throw Slic3r::RuntimeError("Serializing invalid number"); + throw ConfigurationError("Serializing invalid number"); } static bool vectors_equal(const std::vector &v1, const std::vector &v2) { if (NULLABLE) { @@ -764,7 +849,7 @@ public: if (NULLABLE) this->values.push_back(nil_value()); else - throw Slic3r::RuntimeError("Deserializing nil into a non-nullable object"); + throw ConfigurationError("Deserializing nil into a non-nullable object"); } else { std::istringstream iss(item_str); int32_t value; @@ -781,7 +866,7 @@ private: if (NULLABLE) ss << "nil"; else - throw Slic3r::RuntimeError("Serializing NaN"); + throw ConfigurationError("Serializing NaN"); } else ss << v; } @@ -811,7 +896,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); @@ -970,7 +1055,7 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionFloatOrPercent: Comparing incompatible types"); + throw ConfigurationError("ConfigOptionFloatOrPercent: Comparing incompatible types"); assert(dynamic_cast(&rhs)); return *this == *static_cast(&rhs); } @@ -981,7 +1066,7 @@ public: void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw ConfigurationException("ConfigOptionFloatOrPercent: Assigning an incompatible type"); + throw ConfigurationError("ConfigOptionFloatOrPercent: Assigning an incompatible type"); assert(dynamic_cast(rhs)); *this = *static_cast(rhs); } @@ -1046,7 +1131,7 @@ public: bool operator==(const ConfigOptionFloatsOrPercentsTempl &rhs) const { return vectors_equal(this->values, rhs.values); } bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionFloatsOrPercentsTempl: Comparing incompatible types"); + throw ConfigurationError("ConfigOptionFloatsOrPercentsTempl: Comparing incompatible types"); assert(dynamic_cast*>(&rhs)); return vectors_equal(this->values, static_cast*>(&rhs)->values); } @@ -1093,7 +1178,7 @@ public: if (NULLABLE) this->values.push_back(nil_value()); else - throw Slic3r::RuntimeError("Deserializing nil into a non-nullable object"); + throw ConfigurationError("Deserializing nil into a non-nullable object"); } else { bool percent = item_str.find_first_of("%") != std::string::npos; std::istringstream iss(item_str); @@ -1121,9 +1206,9 @@ protected: if (NULLABLE) ss << "nil"; else - throw Slic3r::RuntimeError("Serializing NaN"); + throw ConfigurationError("Serializing NaN"); } else - throw Slic3r::RuntimeError("Serializing invalid number"); + throw ConfigurationError("Serializing invalid number"); } static bool vectors_equal(const std::vector &v1, const std::vector &v2) { if (NULLABLE) { @@ -1312,8 +1397,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") { + this->value = true; + return true; + } + if (str == "0") { + this->value = false; + return true; + } + return false; } private: @@ -1374,24 +1466,39 @@ public: } return vv; } - - bool deserialize(const std::string &str, bool append = false) override + + ConfigHelpers::DeserializationResult deserialize_with_substitutions(const std::string &str, bool append, ConfigHelpers::DeserializationSubstitution substitution) { if (! append) this->values.clear(); std::istringstream is(str); std::string item_str; + bool substituted = false; while (std::getline(is, item_str, ',')) { boost::trim(item_str); + unsigned char new_value = 0; if (item_str == "nil") { if (NULLABLE) this->values.push_back(nil_value()); else - throw Slic3r::RuntimeError("Deserializing nil into a non-nullable object"); + throw ConfigurationError("Deserializing nil into a non-nullable object"); + } else if (item_str == "1") { + new_value = true; + } else if (item_str == "0") { + new_value = false; + } else if (substitution != ConfigHelpers::DeserializationSubstitution::Disabled && ConfigHelpers::looks_like_enum_value(item_str)) { + new_value = ConfigHelpers::enum_looks_like_true_value(item_str) || substitution == ConfigHelpers::DeserializationSubstitution::DefaultsToTrue; + substituted = true; } else - this->values.push_back(item_str.compare("1") == 0); + return ConfigHelpers::DeserializationResult::Failed; + this->values.push_back(new_value); } - return true; + return substituted ? ConfigHelpers::DeserializationResult::Substituted : ConfigHelpers::DeserializationResult::Loaded; + } + + bool deserialize(const std::string &str, bool append = false) override + { + return this->deserialize_with_substitutions(str, append, ConfigHelpers::DeserializationSubstitution::Disabled) == ConfigHelpers::DeserializationResult::Loaded; } protected: @@ -1400,7 +1507,7 @@ protected: if (NULLABLE) ss << "nil"; else - throw Slic3r::RuntimeError("Serializing NaN"); + throw ConfigurationError("Serializing NaN"); } else ss << (v ? "1" : "0"); } @@ -1436,14 +1543,14 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionEnum: Comparing incompatible types"); + throw ConfigurationError("ConfigOptionEnum: Comparing incompatible types"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum return this->value == (T)rhs.getInt(); } void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionEnum: Assigning an incompatible type"); + throw ConfigurationError("ConfigOptionEnum: Assigning an incompatible type"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum this->value = (T)rhs->getInt(); this->phony = rhs->phony; @@ -1522,14 +1629,14 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionEnumGeneric: Comparing incompatible types"); + throw ConfigurationError("ConfigOptionEnumGeneric: Comparing incompatible types"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum return this->value == rhs.getInt(); } void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw Slic3r::RuntimeError("ConfigOptionEnumGeneric: Assigning an incompatible type"); + throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum this->value = rhs->getInt(); this->phony = rhs->phony; @@ -1585,7 +1692,7 @@ public: case coInts: { auto opt = new ConfigOptionIntsNullable(); archive(*opt); return opt; } case coPercents: { auto opt = new ConfigOptionPercentsNullable();archive(*opt); return opt; } case coBools: { auto opt = new ConfigOptionBoolsNullable(); archive(*opt); return opt; } - default: throw Slic3r::RuntimeError(std::string("ConfigOptionDef::load_option_from_archive(): Unknown nullable option type for option ") + this->opt_key); + default: throw ConfigurationError(std::string("ConfigOptionDef::load_option_from_archive(): Unknown nullable option type for option ") + this->opt_key); } } else { switch (this->type) { @@ -1604,7 +1711,7 @@ public: case coBool: { auto opt = new ConfigOptionBool(); archive(*opt); return opt; } case coBools: { auto opt = new ConfigOptionBools(); archive(*opt); return opt; } case coEnum: { auto opt = new ConfigOptionEnumGeneric(this->enum_keys_map); archive(*opt); return opt; } - default: throw Slic3r::RuntimeError(std::string("ConfigOptionDef::load_option_from_archive(): Unknown option type for option ") + this->opt_key); + default: throw ConfigurationError(std::string("ConfigOptionDef::load_option_from_archive(): Unknown option type for option ") + this->opt_key); } } } @@ -1616,7 +1723,7 @@ public: case coInts: archive(*static_cast(opt)); break; case coPercents: archive(*static_cast(opt));break; case coBools: archive(*static_cast(opt)); break; - default: throw Slic3r::RuntimeError(std::string("ConfigOptionDef::save_option_to_archive(): Unknown nullable option type for option ") + this->opt_key); + default: throw ConfigurationError(std::string("ConfigOptionDef::save_option_to_archive(): Unknown nullable option type for option ") + this->opt_key); } } else { switch (this->type) { @@ -1635,7 +1742,7 @@ public: case coBool: archive(*static_cast(opt)); break; case coBools: archive(*static_cast(opt)); break; case coEnum: archive(*static_cast(opt)); break; - default: throw Slic3r::RuntimeError(std::string("ConfigOptionDef::save_option_to_archive(): Unknown option type for option ") + this->opt_key); + default: throw ConfigurationError(std::string("ConfigOptionDef::save_option_to_archive(): Unknown option type for option ") + this->opt_key); } } // Make the compiler happy, shut up the warnings. @@ -1731,6 +1838,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 @@ -1758,7 +1873,7 @@ public: return out; } - /// Iterate through all of the CLI options and write them to a stream. + // Iterate through all of the CLI options and write them to a stream. std::ostream& print_cli_help( std::ostream& out, bool show_defaults, std::function filter = [](const ConfigOptionDef &){ return true; }) const; @@ -1809,6 +1924,8 @@ public: } }; + + // An abstract configuration store. class ConfigBase : public ConfigOptionResolver { @@ -1905,9 +2022,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) {} @@ -1922,17 +2041,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); + 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, 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, bool to_prusa = false) const; // Set all the nullable values to nils. @@ -1940,7 +2061,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. @@ -2110,9 +2231,9 @@ private: template void serialize(Archive &ar) { ar(options); } }; -/// Configuration store with a static definition of configuration values. -/// In Slic3r, the static configuration stores are during the slicing / g-code generation for efficiency reasons, -/// because the configuration values could be accessed directly. +// Configuration store with a static definition of configuration values. +// In Slic3r, the static configuration stores are during the slicing / g-code generation for efficiency reasons, +// because the configuration values could be accessed directly. class StaticConfig : public virtual ConfigBase { public: diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 1f84d8e24..a91a25bef 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -427,7 +427,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(); @@ -442,16 +442,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 @@ -518,7 +518,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); @@ -547,7 +547,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; @@ -568,7 +568,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() @@ -590,7 +590,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); @@ -653,7 +653,7 @@ namespace Slic3r { 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 @@ -665,7 +665,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); print_config_parsed = true; } if (boost::algorithm::iequals(name, CUSTOM_GCODE_PER_PRINT_Z_FILE)) @@ -687,7 +687,8 @@ namespace Slic3r { } //parsed superslicer/prusa files if slic3r not found //note that is we successfully read one of the config file, then the other ones should also have the same name - auto read_from_other_storage = [this, &print_config_parsed, num_entries, &archive, &stat, &config, &model, &filename](const std::string &print_config_name, const std::string& model_config_name) -> bool { + auto read_from_other_storage = [this, &print_config_parsed, num_entries, &archive, &stat, &config, &model, &filename, &config_substitutions] + (const std::string &print_config_name, const std::string& model_config_name) -> bool { for (mz_uint i = 0; i < num_entries; ++i) { if (mz_zip_reader_file_stat(&archive, i, &stat)) @@ -699,7 +700,7 @@ namespace Slic3r { if (boost::algorithm::iequals(name, print_config_name)) { // 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); print_config_parsed = true; } else if (boost::algorithm::iequals(name, model_config_name)) { @@ -774,7 +775,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 @@ -791,7 +792,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; } @@ -868,7 +869,12 @@ 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) { @@ -879,7 +885,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); } } @@ -954,7 +960,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) { @@ -1003,8 +1009,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)); @@ -1895,7 +1900,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()) { @@ -1997,7 +2002,7 @@ namespace Slic3r { else if (metadata.key == SOURCE_IN_INCHES) volume->source.is_converted_from_inches = metadata.value == "1"; else - volume->config.set_deserialize(metadata.key, metadata.value); + volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); } } @@ -2962,13 +2967,13 @@ 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; _3MF_Importer importer; - bool res = importer.load_model_from_file(path, *model, *config, check_version); + 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 ccfd9356d..553b31898 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 82e9fe192..1134f4631 100644 --- a/src/libslic3r/Format/AMF.cpp +++ b/src/libslic3r/Format/AMF.cpp @@ -62,10 +62,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); } @@ -256,6 +257,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&); @@ -704,8 +707,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()) { @@ -723,7 +727,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(); @@ -848,7 +852,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; @@ -865,7 +869,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); @@ -909,7 +913,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) { @@ -925,7 +929,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); @@ -985,7 +989,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; @@ -1011,7 +1015,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); printf("Archive does not contain a valid model"); @@ -1053,11 +1057,11 @@ 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) { 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); @@ -1068,7 +1072,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 138195cd6..e834104e7 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 e2c38d957..6f86a203f 100644 --- a/src/libslic3r/Format/PRUS.cpp +++ b/src/libslic3r/Format/PRUS.cpp @@ -286,11 +286,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 eedf19a0d..5f26c463b 100644 --- a/src/libslic3r/Format/SL1.cpp +++ b/src/libslic3r/Format/SL1.cpp @@ -203,7 +203,7 @@ RasterParams get_raster_params(const DynamicPrintConfig &cfg) if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h || !opt_mirror_x || !opt_mirror_y || !opt_orient) - throw Slic3r::FileIOError("Invalid SL1 file"); + throw Slic3r::FileIOError("Invalid SL1 / SL1S file"); RasterParams rstp; @@ -229,7 +229,7 @@ SliceParams get_slice_params(const DynamicPrintConfig &cfg) auto *opt_init_layerh = cfg.option("initial_layer_height"); if (!opt_layerh || !opt_init_layerh) - throw Slic3r::FileIOError("Invalid SL1 file"); + throw Slic3r::FileIOError("Invalid SL1 / SL1S file"); return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()}; } @@ -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, Vec2i32 windowsize, TriangleMesh & 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_triangle_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 71c9959b6..f214e7c07 100644 --- a/src/libslic3r/Format/SL1.hpp +++ b/src/libslic3r/Format/SL1.hpp @@ -25,23 +25,23 @@ public: void export_print(Zipper &zipper, const SLAPrint &print, const std::string &projectname = "") override; }; -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, Vec2i32 windowsize, TriangleMesh & out, DynamicPrintConfig & profile, std::function progr = [](int) { return true; }); -inline void import_sla_archive( +inline ConfigSubstitutions import_sla_archive( const std::string & zipfname, Vec2i32 windowsize, TriangleMesh & 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 e94766bd6..26aa35bcb 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -878,7 +878,10 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr if (m_producer == EProducer::PrusaSlicer || m_producer == EProducer::SuperSlicer || m_producer == EProducer::Slic3rPE || m_producer == EProducer::Slic3r) { DynamicPrintConfig config; config.apply(FullPrintConfig::defaults()); - config.load_from_gcode_file(filename); + // 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, ForwardCompatibilitySubstitutionRule::EnableSilent); apply_config(config); } } diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index 49d75e5e0..ed0c70071 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -49,7 +49,7 @@ public: // collection of expolygons representing the bridged areas (thus not // needing support material) - Polygons bridged; +// Polygons bridged; // collection of polylines representing the unsupported bridge edges Polylines unsupported_bridge_edges; diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index 214bd6e87..3a5bcfd5b 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -327,7 +327,7 @@ void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Poly }else if (bd.detect_angle(custom_angle)) { bridges[idx_last].bridge_angle = bd.angle; if (this->layer()->object()->config().support_material) { - polygons_append(this->bridged, intersection(bd.coverage(), to_polygons(initial))); + //polygons_append(this->bridged, intersection(bd.coverage(), to_polygons(initial))); append(this->unsupported_bridge_edges, bd.unsupported_edges()); } } else { diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 7246dde30..11098c972 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -121,13 +121,17 @@ bool Model::equals(const Model& rhs) const { return true; } -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")) @@ -135,9 +139,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 @@ -152,24 +157,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."); @@ -191,7 +201,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); @@ -424,13 +434,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) @@ -457,7 +466,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()) { @@ -1766,7 +1775,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)); delete mesh; ++ idx; } diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index b1b86f4a6..2190080c8 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 @@ -996,8 +997,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); bool equals(const Model& rhs) const; @@ -1062,6 +1075,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 c7b69e862..834071890 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -919,7 +919,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 @@ -946,7 +948,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; @@ -1829,7 +1833,9 @@ PhysicalPrinterCollection::PhysicalPrinterCollection( const std::vector; + // Collections of presets of the same type (one of the Print, Filament or Printer type). class PresetCollection { @@ -308,7 +333,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. @@ -680,7 +705,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 e5f561342..886fae4d3 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -162,10 +162,12 @@ 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(); + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + std::tie(substitutions, errors_cummulative) = this->load_system_presets(substitution_rule); const std::string dir_user_presets = data_dir() #ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR @@ -175,33 +177,34 @@ 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 ; + 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(); } @@ -211,16 +214,26 @@ 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. // For each vendor, there will be a single PresetBundle loaded. -std::string PresetBundle::load_system_presets() +std::pair PresetBundle::load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule) { + if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) + // Loading system presets, don't log substitutions. + compatibility_rule = ForwardCompatibilitySubstitutionRule::EnableSilent; + else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) + // Loading system presets, throw on unknown option value. + compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; + // Here the vendor specific read only Config Bundles are stored. - boost::filesystem::path dir = (boost::filesystem::path(data_dir()) / "vendor").make_preferred(); - std::string errors_cummulative; - bool first = true; + boost::filesystem::path dir = (boost::filesystem::path(data_dir()) / "vendor").make_preferred(); + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + bool first = true; for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) if (Slic3r::is_ini_file(dir_entry)) { std::string name = dir_entry.path().filename().string(); @@ -230,13 +243,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); + append(substitutions, this->load_configbundle(dir_entry.path().string(), PresetBundle::LoadSystem, compatibility_rule).first); 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); + append(substitutions, other.load_configbundle(dir_entry.path().string(), PresetBundle::LoadSystem, compatibility_rule).first); 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: "; @@ -258,7 +271,7 @@ std::string PresetBundle::load_system_presets() } this->update_system_maps(); - return errors_cummulative; + return std::make_pair(std::move(substitutions), errors_cummulative); } // Merge one vendor's presets with the other vendor's presets, report duplicates. @@ -666,15 +679,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, 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. @@ -685,33 +698,41 @@ void PresetBundle::load_config_file(const std::string &path) } catch (const std::ifstream::failure &err) { throw Slic3r::RuntimeError(std::string("The Config Bundle cannot be loaded: ") + path + "\n\tReason: " + err.what()); } catch (const boost::property_tree::file_parser_error &err) { - throw Slic3r::RuntimeError((boost::format("Failed loading the Config Bundle \"%1%\": %2% at line %3%") - % err.filename() % err.message() % err.line()).str()); + throw Slic3r::RuntimeError(format("Failed loading the Config Bundle \"%1%\": %2% at line %3%", + err.filename(), err.message(), err.line())); } catch (const std::runtime_error &err) { throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + path + "\n\tReason: " + err.what()); } // 2) Continue based on the type of the configuration file. ConfigFileType config_file_type = guess_config_file_type(tree); - switch (config_file_type) { - case CONFIG_FILE_TYPE_UNKNOWN: - throw Slic3r::RuntimeError(std::string("Unknown configuration file type: ") + path); - case CONFIG_FILE_TYPE_APP_CONFIG: - throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + path + ". This is an application config file."); - case CONFIG_FILE_TYPE_CONFIG: - { - // Initialize a config from full defaults. - DynamicPrintConfig config; - config.apply(FullPrintConfig::defaults()); - config.load(tree); - 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; + ConfigSubstitutions config_substitutions; + try { + switch (config_file_type) { + case CONFIG_FILE_TYPE_UNKNOWN: + throw Slic3r::RuntimeError(std::string("Unknown configuration file type: ") + path); + case CONFIG_FILE_TYPE_APP_CONFIG: + throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + path + ". This is an application config file."); + case CONFIG_FILE_TYPE_CONFIG: + { + // Initialize a config from full defaults. + DynamicPrintConfig config; + config.apply(FullPrintConfig::defaults()); + config_substitutions = config.load(tree, compatibility_rule); + Preset::normalize(config); + load_config_file_config(path, true, std::move(config)); + return config_substitutions; + } + case CONFIG_FILE_TYPE_CONFIG_BUNDLE: + return load_config_file_config_bundle(path, tree, compatibility_rule); + } + } catch (const ConfigurationError &e) { + throw Slic3r::RuntimeError(format("Invalid configuration file %1%: %2%", path, e.what())); } + + // 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. @@ -883,16 +904,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, ForwardCompatibilitySubstitutionRule compatibility_rule) { // 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, {}, compatibility_rule); + 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 = [this, &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); @@ -942,6 +971,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. @@ -1086,11 +1118,16 @@ 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, ForwardCompatibilitySubstitutionRule compatibility_rule) { - 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 { compatibility_rule }; + 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; @@ -1103,25 +1140,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. @@ -1188,7 +1224,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla active_sla_material = kvp.second.data(); } else if (kvp.first == "printer") { active_printer = kvp.second.data(); - }else if (kvp.first == "physical_printer") { + } else if (kvp.first == "physical_printer") { active_physical_printer = kvp.second.data(); } } @@ -1225,30 +1261,36 @@ 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) { - for (auto &kvp : section.second) { - if (kvp.first == "alias") - alias_name = kvp.second.data(); - else if (kvp.first == "renamed_from") { - if (! unescape_strings_cstyle(kvp.second.data(), renamed_from)) { - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The preset \"" << - section.first << "\" contains invalid \"renamed_from\" key, which is being ignored."; - } - } - config.set_deserialize(kvp.first, kvp.second.data()); + try { + 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(); + else if (kvp.first == "renamed_from") { + if (! unescape_strings_cstyle(kvp.second.data(), renamed_from)) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The preset \"" << + section.first << "\" contains invalid \"renamed_from\" key, which is being ignored."; + } + } + // 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) { + // Select the default config based on the printer_technology field extracted from kvp. + DynamicPrintConfig config_src; + parse_config_section(config_src); + default_config = &presets->default_preset_for(config_src).config; + config = *default_config; + config.apply(config_src); + } else { + default_config = &presets->default_preset().config; + config = *default_config; + parse_config_section(config); } - }; - if (presets == &this->printers) { - // Select the default config based on the printer_technology field extracted from kvp. - DynamicPrintConfig config_src; - parse_config_section(config_src); - default_config = &presets->default_preset_for(config_src).config; - config = *default_config; - config.apply(config_src); - } else { - default_config = &presets->default_preset().config; - config = *default_config; - parse_config_section(config); + } catch (const ConfigurationError &e) { + throw ConfigurationError(format("Invalid configuration bundle \"%1%\", section [%2%]: ", path, section.first) + e.what()); } Preset::normalize(config); // Report configuration fields, which are misplaced into a wrong group. @@ -1256,7 +1298,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"); @@ -1291,7 +1333,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) { @@ -1320,9 +1362,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; } @@ -1343,7 +1385,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; } @@ -1352,8 +1397,13 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla const DynamicPrintConfig& default_config = ph_printers->default_config(); DynamicPrintConfig config = default_config; - for (auto& kvp : section.second) - config.set_deserialize(kvp.first, kvp.second.data()); + substitution_context.substitutions.clear(); + try { + for (auto& kvp : section.second) + config.set_deserialize(kvp.first, kvp.second.data(), substitution_context); + } catch (const ConfigurationError &e) { + throw ConfigurationError(format("Invalid configuration bundle \"%1%\", section [%2%]: ", path, section.first) + e.what()); + } // Report configuration fields, which are misplaced into a wrong group. std::string incorrect_keys = Preset::remove_invalid_keys(config, default_config); @@ -1379,14 +1429,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()) @@ -1406,7 +1459,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 5d7cc84ba..cd26a0968 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 // This is done just once on application start up. - void load_presets(AppConfig &config, const std::string &preferred_model_id = ""); + PresetsConfigSubstitutions load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule rule, const std::string &preferred_model_id = ""); // Export selections (current print, current filaments, current printer) into config.ini void export_selections(AppConfig &config); @@ -82,24 +83,27 @@ 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, ForwardCompatibilitySubstitutionRule compatibility_rule); // 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); @@ -136,7 +140,7 @@ public: static const char *PRUSA_BUNDLE; private: - std::string load_system_presets(); + std::pair load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule); // Merge one vendor's presets with the other vendor's presets, report duplicates. std::vector merge_presets(PresetBundle &&other); // Update renamed_from and alias maps of system profiles. @@ -155,12 +159,15 @@ 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, ForwardCompatibilitySubstitutionRule compatibility_rule); 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 8bc6c6f3e..ef302e61e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -74,7 +74,7 @@ void PrintConfigDef::init_common_params() def = this->add("thumbnails", coPoints); def->label = L("Thumbnails size"); - def->tooltip = L("Picture sizes to be stored into a .gcode and .sl1 files, in the following format: \"XxY, XxY, ...\""); + def->tooltip = L("Picture sizes to be stored into a .gcode and .sl1 / .sl1s files, in the following format: \"XxY, XxY, ...\""); def->mode = comExpert; def->min = 0; def->max = 2048; @@ -2723,12 +2723,14 @@ 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_values.push_back("klipper"); + def->enum_labels.push_back("PrusaLink"); def->enum_labels.push_back("OctoPrint"); def->enum_labels.push_back("Duet"); def->enum_labels.push_back("FlashAir"); @@ -5283,7 +5285,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(); @@ -6528,6 +6530,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 Slic3r may not understand configurations produced by newest Slic3r versions. " + "For example, newer Slic3r 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("Bail out on unknown configuration values"); + def->enum_labels.push_back("Enable reading unknown configuration values by verbosely substituting them with defaults."); + def->enum_labels.push_back("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 e6975ceb9..32e731e76 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -72,6 +72,7 @@ enum class MachineLimitsUsage : uint8_t { }; enum PrintHostType { + htPrusaLink, htOctoPrint, htDuet, htFlashAir, @@ -222,6 +223,7 @@ template<> inline const t_config_enum_values& ConfigOptionEnum inline const t_config_enum_values& ConfigOptionEnum::get_enum_values() { static t_config_enum_values keys_map = { + {"prusalink", htPrusaLink}, {"octoprint", htOctoPrint}, {"duet", htDuet}, {"flashair", htFlashAir}, @@ -370,6 +372,15 @@ template<> inline const t_config_enum_values& ConfigOptionEnum::get_en return keys_map; } +template<> inline const t_config_enum_values& ConfigOptionEnum::get_enum_values() { + static const t_config_enum_values keys_map = { + { "disable", ForwardCompatibilitySubstitutionRule::Disable }, + { "enable", ForwardCompatibilitySubstitutionRule::Enable }, + { "enable_silent", ForwardCompatibilitySubstitutionRule::EnableSilent } + }; + + return keys_map; +} // Defines each and every confiuration option of Slic3r, including the properties of the GUI dialogs. // Does not store the actual values, but defines default values. class PrintConfigDef : public ConfigDef @@ -1901,8 +1912,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/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 9ad25dda1..e7c219da0 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -2925,6 +2925,16 @@ namespace Slic3r { return this->slice_volumes(zs, SlicingMode::Regular, volumes); } +//FIXME The admesh repair function may break the face connectivity, rather refresh it here as the slicing code relies on it. +static void fix_mesh_connectivity(TriangleMesh &mesh) +{ + auto nr_degenerated = mesh.stl.stats.degenerate_facets; + stl_check_facets_exact(&mesh.stl); + if (nr_degenerated != mesh.stl.stats.degenerate_facets) + // stl_check_facets_exact() removed some newly degenerated faces. Some faces could become degenerate after some mesh transformation. + stl_generate_shared_vertices(&mesh.stl, mesh.its); +} + std::vector PrintObject::slice_volumes( const std::vector& z, SlicingMode mode, size_t slicing_mode_normal_below_layer, SlicingMode mode_below, @@ -2937,10 +2947,8 @@ namespace Slic3r { TriangleMesh mesh(volumes.front()->mesh()); mesh.transform(volumes.front()->get_matrix(), true); assert(mesh.repaired); - if (volumes.size() == 1 && mesh.repaired) { - //FIXME The admesh repair function may break the face connectivity, rather refresh it here as the slicing code relies on it. - stl_check_facets_exact(&mesh.stl); - } + if (volumes.size() == 1 && mesh.repaired) + fix_mesh_connectivity(mesh); for (size_t idx_volume = 1; idx_volume < volumes.size(); ++idx_volume) { const ModelVolume& model_volume = *volumes[idx_volume]; TriangleMesh vol_mesh(model_volume.mesh()); @@ -2973,10 +2981,8 @@ namespace Slic3r { //FIXME better to split the mesh into separate shells, perform slicing over each shell separately and then to use a Boolean operation to merge them. TriangleMesh mesh(volume.mesh()); mesh.transform(volume.get_matrix(), true); - if (mesh.repaired) { - //FIXME The admesh repair function may break the face connectivity, rather refresh it here as the slicing code relies on it. - stl_check_facets_exact(&mesh.stl); - } + if (mesh.repaired) + fix_mesh_connectivity(mesh); if (mesh.stl.stats.number_of_facets > 0) { mesh.transform(m_trafo, true); // apply XY shift diff --git a/src/libslic3r/ShortestPath.cpp b/src/libslic3r/ShortestPath.cpp index 6eff2ada0..f7e7d5bca 100644 --- a/src/libslic3r/ShortestPath.cpp +++ b/src/libslic3r/ShortestPath.cpp @@ -1547,6 +1547,11 @@ static inline void do_crossover(const std::vector &edges_in, std::vect assert(edges_in.size() == edges_out.size()); } +// Worst time complexity: O(min(n, 100) * (n * log n + n^2) +// Expected time complexity: O(min(n, 100) * (n * log n + k * n) +// where n is the number of edges and k is the number of connection_lengths candidates after the first one +// is found that improves the total cost. +//FIXME there are likley better heuristics to lower the time complexity. static inline void reorder_by_two_exchanges_with_segment_flipping(std::vector &edges) { if (edges.size() < 2) @@ -1556,7 +1561,8 @@ static inline void reorder_by_two_exchanges_with_segment_flipping(std::vector edges_tmp(edges); std::vector> connection_lengths(edges.size() - 1, std::pair(0., 0)); std::vector connection_tried(edges.size(), false); - for (size_t iter = 0; iter < edges.size(); ++ iter) { + const size_t max_iterations = std::min(edges.size(), size_t(100)); + for (size_t iter = 0; iter < max_iterations; ++ iter) { // Initialize connection costs and connection lengths. for (size_t i = 1; i < edges.size(); ++ i) { const FlipEdge &e1 = edges[i - 1]; @@ -1621,6 +1627,8 @@ static inline void reorder_by_two_exchanges_with_segment_flipping(std::vector &edges) { if (edges.size() < 3) { @@ -1703,6 +1711,7 @@ static inline void reorder_by_three_exchanges_with_segment_flipping(std::vector< } } } +#endif typedef Eigen::Matrix Matrixd; @@ -1774,6 +1783,8 @@ static inline std::pair minimum_crossover_cost( return std::make_pair(cost_min, flip_min); } +#if 0 +// Currently not used, too slow. static inline void reorder_by_three_exchanges_with_segment_flipping2(std::vector &edges) { if (edges.size() < 3) { @@ -1870,8 +1881,11 @@ static inline void reorder_by_three_exchanges_with_segment_flipping2(std::vector } } } +#endif // Flip the sequences of polylines to lower the total length of connecting lines. +// Used by the infill generator if the infill is not connected with perimeter lines +// and to order the brim lines. static inline void improve_ordering_by_two_exchanges_with_segment_flipping(Polylines &polylines, bool fixed_start) { #ifndef NDEBUG @@ -1923,6 +1937,7 @@ static inline void improve_ordering_by_two_exchanges_with_segment_flipping(Polyl #endif /* NDEBUG */ } +// Used to optimize order of infill lines and brim lines. Polylines chain_polylines(Polylines &&polylines, const Point *start_near) { #ifdef DEBUG_SVG_OUTPUT diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index f9be1f494..665b930ba 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -226,7 +226,7 @@ inline typename CONTAINER_TYPE::value_type& next_value_modulo(typename CONTAINER return container[next_idx_modulo(idx, container.size())]; } -extern std::string xml_escape(std::string text); +extern std::string xml_escape(std::string text, bool is_marked = false); #if defined __GNUC__ && __GNUC__ < 5 && !defined __clang__ 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 a1d6da5fe..0135fd712 100644 --- a/src/libslic3r/pchheader.hpp +++ b/src/libslic3r/pchheader.hpp @@ -111,6 +111,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/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 0d03de17b..ef77f2263 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -424,6 +424,53 @@ std::error_code rename_file(const std::string &from, const std::string &to) } #ifdef __linux__ +// Copied from boost::filesystem. +// Called by copy_file_linux() in case linux sendfile() API is not supported. +int copy_file_linux_read_write(int infile, int outfile, uintmax_t file_size) +{ + std::vector buf( + // Prefer the buffer to be larger than the file size so that we don't have + // to perform an extra read if the file fits in the buffer exactly. + std::clamp(file_size + (file_size < ~static_cast(0u)), + // Min and max buffer sizes are selected to minimize the overhead from system calls. + // The values are picked based on coreutils cp(1) benchmarking data described here: + // https://github.com/coreutils/coreutils/blob/d1b0257077c0b0f0ee25087efd46270345d1dd1f/src/ioblksize.h#L23-L72 + 8u * 1024u, 256u * 1024u), + 0); + +#if defined(POSIX_FADV_SEQUENTIAL) + ::posix_fadvise(infile, 0, 0, POSIX_FADV_SEQUENTIAL); +#endif + + // Don't use file size to limit the amount of data to copy since some filesystems, like procfs or sysfs, + // provide files with generated content and indicate that their size is zero or 4096. Just copy as much data + // as we can read from the input file. + while (true) { + ssize_t sz_read = ::read(infile, buf.data(), buf.size()); + if (sz_read == 0) + break; + if (sz_read < 0) { + int err = errno; + if (err == EINTR) + continue; + return err; + } + // Allow for partial writes - see Advanced Unix Programming (2nd Ed.), + // Marc Rochkind, Addison-Wesley, 2004, page 94 + for (ssize_t sz_wrote = 0; sz_wrote < sz_read;) { + ssize_t sz = ::write(outfile, buf.data() + sz_wrote, static_cast(sz_read - sz_wrote)); + if (sz < 0) { + int err = errno; + if (err == EINTR) + continue; + return err; + } + sz_wrote += sz; + } + } + return 0; +} + // Copied from boost::filesystem, to support copying a file to a weird filesystem, which does not support changing file attributes, // for example ChromeOS Linux integration or FlashAIR WebDAV. // Copied and simplified from boost::filesystem::detail::copy_file() with option = overwrite_if_exists and with just the Linux path kept, @@ -465,7 +512,7 @@ bool copy_file_linux(const boost::filesystem::path &from, const boost::filesyste err = errno; goto fail; } - + const mode_t from_mode = from_stat.st_mode; if (!S_ISREG(from_mode)) { err = ENOSYS; @@ -520,6 +567,19 @@ bool copy_file_linux(const boost::filesystem::path &from, const boost::filesyste ssize_t sz = ::sendfile(outfile.fd, infile.fd, nullptr, size_to_copy); if (sz < 0) { err = errno; + if (offset == 0u) { + // sendfile may fail with EINVAL if the underlying filesystem does not support it. + // See https://patchwork.kernel.org/project/linux-nfs/patch/20190411183418.4510-1-olga.kornievskaia@gmail.com/ + // https://bugzilla.redhat.com/show_bug.cgi?id=1783554. + // https://github.com/boostorg/filesystem/commit/4b9052f1e0b2acf625e8247582f44acdcc78a4ce + if (err == EINVAL || err == EOPNOTSUPP) { + err = copy_file_linux_read_write(infile.fd, outfile.fd, from_stat.st_size); + if (err < 0) + goto fail; + // Succeeded. + break; + } + } if (err == EINTR) continue; if (err == 0) @@ -529,7 +589,6 @@ bool copy_file_linux(const boost::filesystem::path &from, const boost::filesyste offset += sz; } } - // If we created a new file with an explicitly added S_IWUSR permission, // we may need to update its mode bits to match the source file. if (to_mode != from_mode && ::fchmod(outfile.fd, from_mode) != 0) { @@ -781,7 +840,7 @@ unsigned get_current_pid() #endif } -std::string xml_escape(std::string text) +std::string xml_escape(std::string text, bool is_marked/* = false*/) { std::string::size_type pos = 0; for (;;) @@ -796,8 +855,8 @@ std::string xml_escape(std::string text) case '\"': replacement = """; break; case '\'': replacement = "'"; break; case '&': replacement = "&"; break; - case '<': replacement = "<"; break; - case '>': replacement = ">"; break; + case '<': replacement = is_marked ? "<" :"<"; break; + case '>': replacement = is_marked ? ">" :">"; break; default: break; } diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index 47b8384ec..ff9da5fb4 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -8,14 +8,23 @@ #include #include #include +#include #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/format.hpp" #include "libslic3r/libslic3r.h" #include "libslic3r/Time.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/FileParserError.hpp" #include "libslic3r/Utils.hpp" +#include "../GUI/GUI.hpp" +#include "../GUI/GUI_App.hpp" +#include "../GUI/I18N.hpp" +#include "../GUI/MainFrame.hpp" + +#include + #define SLIC3R_SNAPSHOTS_DIR "snapshots" #define SLIC3R_SNAPSHOT_FILE "snapshot.ini" @@ -362,7 +371,8 @@ static void copy_config_dir_single_level(const boost::filesystem::path &path_src for (auto &dir_entry : boost::filesystem::directory_iterator(path_src)) if (Slic3r::is_ini_file(dir_entry)) - boost::filesystem::copy_file(dir_entry.path(), path_dst / dir_entry.path().filename(), boost::filesystem::copy_option::overwrite_if_exists); + if (std::string error_message; copy_file(dir_entry.path().string(), (path_dst / dir_entry.path().filename()).string(), error_message, false) != SUCCESS) + throw Slic3r::RuntimeError(format("Failed copying \"%1%\" to \"%2%\": %3%", path_src.string(), path_dst.string(), error_message)); } static void delete_existing_ini_files(const boost::filesystem::path &path) @@ -413,7 +423,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, ForwardCompatibilitySubstitutionRule::EnableSilent); for (const auto &vp : bundle.vendors) if (vp.second.id == cfg.name) cfg.version.config_version = vp.second.config_version; @@ -433,14 +443,27 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: } boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id; - boost::filesystem::create_directory(snapshot_dir); - // Backup the presets. - for (const char *subdir : snapshot_subdirs) - copy_config_dir_single_level(data_dir / subdir, snapshot_dir / subdir); - snapshot.save_ini((snapshot_dir / "snapshot.ini").string()); - assert(m_snapshots.empty() || m_snapshots.back().time_captured <= snapshot.time_captured); - m_snapshots.emplace_back(std::move(snapshot)); + try { + boost::filesystem::create_directory(snapshot_dir); + + // Backup the presets. + for (const char *subdir : snapshot_subdirs) + copy_config_dir_single_level(data_dir / subdir, snapshot_dir / subdir); + snapshot.save_ini((snapshot_dir / "snapshot.ini").string()); + assert(m_snapshots.empty() || m_snapshots.back().time_captured <= snapshot.time_captured); + m_snapshots.emplace_back(std::move(snapshot)); + } catch (...) { + if (boost::filesystem::is_directory(snapshot_dir)) { + try { + // Clean up partially copied snapshot. + boost::filesystem::remove_all(snapshot_dir); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Failed taking snapshot and failed removing the snapshot directory " << snapshot_dir; + } + } + throw; + } return m_snapshots.back(); } @@ -551,6 +574,32 @@ SnapshotDB& SnapshotDB::singleton() return instance; } +const Snapshot* take_config_snapshot_report_error(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment) +{ + try { + return &SnapshotDB::singleton().take_snapshot(app_config, reason, comment); + } catch (std::exception &err) { + show_error(static_cast(wxGetApp().mainframe), + _L("Taking a configuration snapshot failed.") + "\n\n" + from_u8(err.what())); + return nullptr; + } +} + +bool take_config_snapshot_cancel_on_error(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment, const std::string &message) +{ + try { + SnapshotDB::singleton().take_snapshot(app_config, reason, comment); + return true; + } catch (std::exception &err) { + wxRichMessageDialog dlg(static_cast(wxGetApp().mainframe), + _L("PrusaSlicer has encountered an error while taking a configuration snapshot.") + "\n\n" + from_u8(err.what()) + "\n\n" + from_u8(message), + _L("PrusaSlicer error"), + wxYES_NO); + dlg.SetYesNoLabels(_L("Continue"), _L("Abort")); + return dlg.ShowModal() == wxID_YES; + } +} + } // namespace Config } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/Config/Snapshot.hpp b/src/slic3r/Config/Snapshot.hpp index 48add8a1a..f45300633 100644 --- a/src/slic3r/Config/Snapshot.hpp +++ b/src/slic3r/Config/Snapshot.hpp @@ -127,6 +127,13 @@ private: std::vector m_snapshots; }; +// Take snapshot on SnapshotDB::singleton(). If taking snapshot fails, report an error and return nullptr. +const Snapshot* take_config_snapshot_report_error(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment); + +// Take snapshot on SnapshotDB::singleton(). If taking snapshot fails, report "message", and present a "Continue" or "Abort" buttons to respond. +// Return true on success and on "Continue" to continue with the process (for example installation of presets). +bool take_config_snapshot_cancel_on_error(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment, const std::string &message); + } // namespace Config } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 5e9961e2e..1aa93d22f 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -59,7 +59,11 @@ 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); + // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. + auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( + path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); + // 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; @@ -2358,7 +2362,7 @@ bool ConfigWizard::priv::check_and_install_missing_materials(Technology technolo return true; } -void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater) +bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater) { const auto enabled_vendors = appconfig_new.vendors(); @@ -2407,14 +2411,14 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese break; } - if (snapshot) { - SnapshotDB::singleton().take_snapshot(*app_config, snapshot_reason); - } + if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Continue with applying configuration changes?"))) + return false; if (install_bundles.size() > 0) { // Install bundles from resources. // Don't create snapshot - we've already done that above if applicable. - updater->install_bundles_rsrc(std::move(install_bundles), false); + if (! updater->install_bundles_rsrc(std::move(install_bundles), false)) + return false; } else { BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources"; } @@ -2490,7 +2494,11 @@ 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. + // Throw on substitutions in system profiles, as the system profiles provided over the air should be compatible with this PrusaSlicer version. + preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, preferred_model); if (page_custom->custom_wanted()) { page_firmware->apply_custom_config(*custom_config); @@ -2504,6 +2512,8 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese // Update the selections from the compatibilty. preset_bundle->export_selections(*app_config); + + return true; } @@ -2762,7 +2772,8 @@ bool ConfigWizard::run(RunReason reason, StartPage start_page) p->set_start_page(start_page); if (ShowModal() == wxID_OK) { - p->apply_config(app.app_config, app.preset_bundle, app.preset_updater); + if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater)) + return false; app.app_config->set_legacy_datadir(false); app.update_mode(); app.obj_manipul()->update_ui_from_settings(); diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index 3aba9af8c..79577264a 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -622,7 +622,7 @@ struct ConfigWizard::priv bool on_bnt_finish(); bool check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id = std::string()); - void apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater); + bool apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater); // #ys_FIXME_alise void update_presets_in_config(const std::string& section, const std::string& alias_key, bool add); diff --git a/src/slic3r/GUI/ExtraRenderers.cpp b/src/slic3r/GUI/ExtraRenderers.cpp index 27e2c1224..ad7fe5d0f 100644 --- a/src/slic3r/GUI/ExtraRenderers.cpp +++ b/src/slic3r/GUI/ExtraRenderers.cpp @@ -306,9 +306,11 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR // to avoid event propagation to other sidebar items c_editor->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { evt.StopPropagation(); +#ifdef __linux__ // FinishEditing grabs new selection and triggers config update. We better call // it explicitly, automatic update on KILL_FOCUS didn't work on Linux. this->FinishEditing(); +#endif }); return c_editor; diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index e54519c70..2d82cabee 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -1185,6 +1185,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 == "solid_fill_pattern" || m_opt_id == "fill_pattern" || m_opt_id == "support_material_interface_pattern" || m_opt_id == "brim_ears_pattern") val = idx_from_enum_value(val); @@ -1223,6 +1227,8 @@ void Choice::set_value(const boost::any& value, bool change_event) val = idx_from_enum_value(val); else if (m_opt_id.compare("output_format") == 0) val = idx_from_enum_value(val); + else if (m_opt_id.compare("config_compatibility") == 0) + val = idx_from_enum_value(val); field->SetSelection(val); break; } @@ -1279,7 +1285,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); @@ -1301,7 +1307,10 @@ boost::any& Choice::get_value() if (m_opt.type == coEnum) { - int ret_enum = field->GetSelection(); + int ret_enum = field->GetSelection(); + 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 + ret_enum++; if (m_opt_id == "top_fill_pattern" || m_opt_id == "bottom_fill_pattern" || m_opt_id == "solid_fill_pattern" || m_opt_id == "support_material_interface_pattern" || m_opt_id == "fill_pattern" || m_opt_id == "brim_ears_pattern") convert_to_enum_value(ret_enum); @@ -1340,6 +1349,8 @@ boost::any& Choice::get_value() convert_to_enum_value(ret_enum); else if (m_opt_id.compare("output_format") == 0) convert_to_enum_value(ret_enum); + else if(m_opt_id.compare("config_compatibility") == 0) + convert_to_enum_value(ret_enum); } else if (m_opt.gui_type == "f_enum_open") { const int ret_enum = field->GetSelection(); diff --git a/src/slic3r/GUI/FirmwareDialog.cpp b/src/slic3r/GUI/FirmwareDialog.cpp index 879e7fe34..1b4fafeac 100644 --- a/src/slic3r/GUI/FirmwareDialog.cpp +++ b/src/slic3r/GUI/FirmwareDialog.cpp @@ -65,6 +65,8 @@ enum { USB_PID_MMU_APP = 4, USB_PID_CW1_BOOT = 7, USB_PID_CW1_APP = 8, + USB_PID_CW1S_BOOT = 14, + USB_PID_CW1S_APP = 15, }; // This enum discriminates the kind of information in EVT_AVRDUDE, @@ -308,7 +310,7 @@ void FirmwareDialog::priv::update_flash_enabled() void FirmwareDialog::priv::load_hex_file(const wxString &path) { hex_file = HexFile(path.wx_str()); - const bool autodetect = hex_file.device == HexFile::DEV_MM_CONTROL || hex_file.device == HexFile::DEV_CW1; + const bool autodetect = hex_file.device == HexFile::DEV_MM_CONTROL || hex_file.device == HexFile::DEV_CW1 || hex_file.device == HexFile::DEV_CW1S; set_autodetect(autodetect); } @@ -636,6 +638,10 @@ void FirmwareDialog::priv::perform_upload() this->prepare_avr109(Avr109Pid(USB_PID_CW1_BOOT, USB_PID_CW1_APP)); break; + case HexFile::DEV_CW1S: + this->prepare_avr109(Avr109Pid(USB_PID_CW1S_BOOT, USB_PID_CW1S_APP)); + break; + default: this->prepare_mk2(); break; @@ -761,11 +767,10 @@ const char* FirmwareDialog::priv::avr109_dev_name(Avr109Pid usb_pid) { switch (usb_pid.boot) { case USB_PID_MMU_BOOT: return "Original Prusa MMU 2.0 Control"; - break; case USB_PID_CW1_BOOT: return "Original Prusa CW1"; - break; - + case USB_PID_CW1S_BOOT: + return "Original Prusa CW1S"; default: throw Slic3r::RuntimeError((boost::format("Invalid avr109 device USB PID: %1%") % usb_pid.boot).str()); } } diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 3ed8a7021..a961033b7 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #if __APPLE__ @@ -18,6 +19,7 @@ #include "AboutDialog.hpp" #include "MsgDialog.hpp" +#include "format.hpp" #include "libslic3r/Print.hpp" @@ -273,6 +275,127 @@ void warning_catcher(wxWindow* parent, const wxString& message) msg.ShowModal(); } +static wxString bold(const wxString& str) +{ + return wxString::Format("%s", str); +}; + +static wxString bold_string(const wxString& str) +{ + return wxString::Format("\"%s\"", str); +}; + +static void add_config_substitutions(const ConfigSubstitutions& conf_substitutions, wxString& changes) +{ + changes += ""; + for (const ConfigSubstitution& conf_substitution : conf_substitutions) { + wxString new_val; + const ConfigOptionDef* def = conf_substitution.opt_def; + if (!def) + continue; + switch (def->type) { + case coEnum: + { + const std::vector& labels = def->enum_labels; + const std::vector& values = def->enum_values; + int val = conf_substitution.new_value->getInt(); + + bool is_infill = def->opt_key == "top_fill_pattern" || + def->opt_key == "bottom_fill_pattern" || + def->opt_key == "fill_pattern"; + + // Each infill doesn't use all list of infill declared in PrintConfig.hpp. + // So we should "convert" val to the correct one + if (is_infill) { + for (const auto& key_val : *def->enum_keys_map) + if ((int)key_val.second == val) { + auto it = std::find(values.begin(), values.end(), key_val.first); + if (it == values.end()) + break; + auto idx = it - values.begin(); + new_val = wxString("\"") + values[idx] + "\"" + " (" + from_u8(_utf8(labels[idx])) + ")"; + break; + } + if (new_val.IsEmpty()) { + assert(false); + new_val = _L("Undefined"); + } + } + else + new_val = wxString("\"") + values[val] + "\"" + " (" + from_u8(_utf8(labels[val])) + ")"; + break; + } + case coBool: + new_val = conf_substitution.new_value->getBool() ? "true" : "false"; + break; + case coBools: + if (conf_substitution.new_value->nullable()) + for (const char v : static_cast(conf_substitution.new_value.get())->values) + new_val += std::string(v == ConfigOptionBoolsNullable::nil_value() ? "nil" : v ? "true" : "false") + ", "; + else + for (const char v : static_cast(conf_substitution.new_value.get())->values) + new_val += std::string(v ? "true" : "false") + ", "; + if (! new_val.empty()) + new_val.erase(new_val.begin() + new_val.size() - 2, new_val.end()); + break; + default: + assert(false); + } + + changes += format_wxstr(""; + } + changes += "
\"%1%\" (%2%): ", def->opt_key, _(def->label)) + + format_wxstr(_L("%1% was substituted with %2%"), bold_string(conf_substitution.old_value), bold(new_val)) + + "
"; +} + +static wxString substitution_message(const wxString& changes) +{ + return + _L("Most likely the configuration was produced by a newer version of PrusaSlicer or by some PrusaSlicer fork.") + " " + + _L("The following values were substituted:") + "\n" + changes + "\n\n" + + _L("Review the substitutions and adjust them if needed."); +} + +void show_substitutions_info(const PresetsConfigSubstitutions& presets_config_substitutions) +{ + wxString changes; + + auto preset_type_name = [](Preset::Type type) { + switch (type) { + case Preset::TYPE_FFF_PRINT: return _L("Print settings"); + case Preset::TYPE_SLA_PRINT: return _L("SLA print settings"); + case Preset::TYPE_FFF_FILAMENT: return _L("Filament"); + case Preset::TYPE_SLA_MATERIAL: return _L("SLA material"); + case Preset::TYPE_PRINTER: return _L("Printer"); + case Preset::TYPE_PHYSICAL_PRINTER: return _L("Physical Printer"); + default: assert(false); return wxString(); + } + }; + + for (const PresetConfigSubstitutions& substitution : presets_config_substitutions) { + changes += "\n\n" + format_wxstr("%1% : %2%", preset_type_name(substitution.preset_type), bold_string(substitution.preset_name)); + if (!substitution.preset_file.empty()) + changes += format_wxstr(" (%1%)", substitution.preset_file); + + add_config_substitutions(substitution.substitutions, changes); + } + + InfoDialog msg(nullptr, _L("Configuration bundle was loaded, however some configuration values were not recognized."), substitution_message(changes)); + msg.ShowModal(); +} + +void show_substitutions_info(const ConfigSubstitutions& config_substitutions, const std::string& filename) +{ + wxString changes = "\n"; + add_config_substitutions(config_substitutions, changes); + + InfoDialog msg(nullptr, + format_wxstr(_L("Configuration file \"%1%\" was loaded, however some configuration values were not recognized."), from_u8(filename)), + substitution_message(changes)); + msg.ShowModal(); +} + void create_combochecklist(wxComboCtrl* comboCtrl, const std::string& text, const std::string& items) { if (comboCtrl == nullptr) diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index a90115933..f80d18344 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -7,6 +7,7 @@ namespace boost::filesystem { class path; } #include #include "libslic3r/Config.hpp" +#include "libslic3r/Preset.hpp" class wxWindow; class wxMenuBar; @@ -49,6 +50,8 @@ void show_info(wxWindow* parent, const wxString& message, const wxString& title void show_info(wxWindow* parent, const char* message, const char* title = nullptr); inline void show_info(wxWindow* parent, const std::string& message,const std::string& title = std::string()) { show_info(parent, message.c_str(), title.c_str()); } void warning_catcher(wxWindow* parent, const wxString& message); +void show_substitutions_info(const PresetsConfigSubstitutions& presets_config_substitutions); +void show_substitutions_info(const ConfigSubstitutions& config_substitutions, const std::string& filename); // Creates a wxCheckListBoxComboPopup inside the given wxComboCtrl, filled with the given text and items. // Items data must be separated by '|', and contain the item name to be shown followed by its initial value (0 for false, 1 for true). diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index cbfecff38..18f0d1982 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -443,7 +443,9 @@ wxString file_wildcards(FileType file_type, const std::string &custom_extension) /* FT_TEX */ "Texture (*.png, *.svg)|*.png;*.PNG;*.svg;*.SVG", - /* FT_PNGZIP */ "Masked SLA files (*.sl1)|*.sl1;*.SL1", + /* FT_SL1 */ "Masked SLA files (*.sl1, *.sl1s)|*.sl1;*.SL1;*.sl1s;*.SL1S", + // Workaround for OSX file picker, for some reason it always saves with the 1st extension. + /* FT_SL1S */ "Masked SLA files (*.sl1s, *.sl1)|*.sl1s;*.SL1S;*.sl1;*.SL1", }; std::string out = defaults[file_type]; @@ -634,6 +636,9 @@ 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()) + show_substitutions_info(this->init_params->preset_substitutions); + #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. @@ -653,6 +658,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) @@ -923,7 +946,10 @@ 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); + // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. + // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force + // installation of a compatible system preset, thus nullifying the system preset substitutions. + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); } catch (const std::exception &ex) { show_error(nullptr, ex.what()); } @@ -976,7 +1002,6 @@ bool GUI_App::on_init_inner() if (! plater_) return; - if (app_config->dirty() && app_config->get("autosave") == "1") app_config->save(); @@ -997,33 +1022,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; @@ -1819,9 +1817,10 @@ void GUI_App::add_config_menu(wxMenuBar *menu) child->SetFont(normal_font()); if (dlg.ShowModal() == wxID_OK) - app_config->set("on_snapshot", - Slic3r::GUI::Config::SnapshotDB::singleton().take_snapshot( - *app_config, Slic3r::GUI::Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()).id); + if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( + *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); + snapshot != nullptr) + app_config->set("on_snapshot", snapshot->id); } break; case ConfigMenuSnapshots: @@ -1832,13 +1831,24 @@ void GUI_App::add_config_menu(wxMenuBar *menu) ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); dlg.ShowModal(); if (!dlg.snapshot_to_activate().empty()) { - if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) - Config::SnapshotDB::singleton().take_snapshot(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK); + if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && + ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", + GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) + break; try { app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); - preset_bundle->load_presets(*app_config); + // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system + // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users + // are known to be creative and mess with the config files in various ways. + if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); + ! all_substitutions.empty()) + show_substitutions_info(all_substitutions); + // Load the currently selected preset into the GUI, update the preset selection box. load_current_presets(); + + // update config wizard in respect to the new config + update_wizard_from_config(); } catch (std::exception &ex) { GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); } @@ -2015,6 +2025,17 @@ void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) } } +void GUI_App::update_wizard_from_config() +{ + if (!m_wizard) + return; + // If ConfigWizard was created before changing of the configuration, + // we have to destroy it to have possibility to create it again in respect to the new config's parameters + m_wizard->Reparent(nullptr); + m_wizard->Destroy(); + m_wizard = nullptr; +} + bool GUI_App::OnExceptionInMainLoop() { generic_exception_handle(); @@ -2175,7 +2196,13 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage { wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + if (reason == ConfigWizard::RR_USER) + if (PresetUpdater::UpdateResult result = preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD); + result == PresetUpdater::R_ALL_CANCELED) + return false; + if (! m_wizard) { + wxBusyCursor wait; m_wizard = new ConfigWizard(mainframe); } @@ -2334,7 +2361,7 @@ void GUI_App::check_updates(const bool verbose) { PresetUpdater::UpdateResult updater_result; try { - updater_result = preset_updater->config_update(app_config->orig_version(), verbose); + updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { mainframe->Close(); } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index ba764badb..3dfb8cc38 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -63,7 +63,9 @@ enum FileType FT_TEX, - FT_PNGZIP, + FT_SL1, + // Workaround for OSX file picker, for some reason it always saves with the 1st extension. + FT_SL1S, FT_SIZE, }; @@ -226,6 +228,7 @@ public: bool check_print_host_queue(); bool checked_tab(Tab* tab); void load_current_presets(bool check_printer_presets = true); + void update_wizard_from_config(); wxString current_language_code() const { return m_wxLocale->GetCanonicalName(); } // Translate the language code to a code, for which Prusa Research maintains translations. Defaults to "en_US". @@ -250,7 +253,7 @@ public: // 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 }; @@ -260,7 +263,7 @@ public: std::mutex not_modal_dialog_mutex; wxDialog* not_modal_dialog = nullptr; - PresetUpdater* get_preset_updater() { return preset_updater; } + PresetUpdater* get_preset_updater() { return preset_updater; } wxNotebook* tab_panel() const ; int extruders_cnt() const; diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index a5561d7a5..dd80e8846 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(SLIC3R_APP_NAME " 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 f5e4c976f..a309bd0d3 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -33,7 +33,7 @@ public: m_filepicker = new wxFilePickerCtrl(this, wxID_ANY, from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")), - "SL1 archive files (*.sl1, *.zip)|*.sl1;*.SL1;*.zip;*.ZIP", + "SL1 / SL1S archive files (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP", wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST); szfilepck->Add(new wxStaticText(this, wxID_ANY, _L("Import file") + ": "), 0, wxALIGN_CENTER); @@ -113,13 +113,14 @@ public: Plater *plater; Sel sel = Sel::modelAndProfile; - - TriangleMesh mesh; - DynamicPrintConfig profile; - wxString path; - Vec2i32 win = {2, 2}; - std::string err; - + + TriangleMesh mesh; + DynamicPrintConfig profile; + wxString path; + Vec2i32 win = {2, 2}; + std::string err; + ConfigSubstitutions config_substitutions; + priv(Plater *plt): plater{plt} {} }; @@ -142,13 +143,13 @@ void SLAImportJob::process() try { switch (p->sel) { case Sel::modelAndProfile: - import_sla_archive(path, p->win, p->mesh, p->profile, progr); + p->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); + p->config_substitutions = import_sla_archive(path, p->win, p->mesh, progr); break; case Sel::profileOnly: - import_sla_archive(path, p->profile); + p->config_substitutions = import_sla_archive(path, p->profile); break; } @@ -181,6 +182,7 @@ void SLAImportJob::prepare() p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : path.ToUTF8(); p->sel = dlg.get_selection(); p->win = dlg.get_marchsq_windowsize(); + p->config_substitutions.clear(); } else { p->path = ""; } @@ -223,8 +225,11 @@ void SLAImportJob::finalize() bool is_centered = false; p->plater->sidebar().obj_list()->load_mesh_object(p->mesh, name, is_centered); } - + + if (! p->config_substitutions.empty()) + show_substitutions_info(p->config_substitutions, p->path.ToUTF8().data()); + reset(); } -}} +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index c0f3cfc6e..f60550606 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1268,7 +1268,7 @@ void MainFrame::init_menubar_as_editor() [this](wxCommandEvent&) { if (m_plater) m_plater->add_model(true); }, "import_plater", nullptr, [this](){return m_plater != nullptr; }, this); - append_menu_item(import_menu, wxID_ANY, _L("Import SL1 archive") + dots, _L("Load an SL1 archive"), + append_menu_item(import_menu, wxID_ANY, _L("Import SL1 / SL1S archive") + dots, _L("Load an SL1 / Sl1S archive"), [this](wxCommandEvent&) { if (m_plater) m_plater->import_sl1_archive(); }, "import_plater", nullptr, [this](){return m_plater != nullptr; }, this); @@ -1625,6 +1625,7 @@ void MainFrame::update_menubar() m_changeable_menu_items[miPrinterTab] ->SetBitmap(create_scaled_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) { @@ -1747,6 +1748,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() { @@ -1829,7 +1831,9 @@ 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()) + show_substitutions_info(config_substitutions, path); } catch (const std::exception& ex) { show_error(this, ex.what()); return false; @@ -1885,14 +1889,20 @@ 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()); + // Report all substitutions. + std::tie(config_substitutions, presets_imported) = wxGetApp().preset_bundle->load_configbundle( + file.ToUTF8().data(), PresetBundle::LoadConfigBundleAttribute::SaveImported, ForwardCompatibilitySubstitutionRule::Enable); } catch (const std::exception &ex) { show_error(this, ex.what()); return; } + if (! config_substitutions.empty()) + show_substitutions_info(config_substitutions); + // 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 6da34046e..167f86d72 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -187,7 +187,7 @@ public: bool is_last_input_file() const { return !m_qs_last_input_file.IsEmpty(); } ESettingsLayout get_layout() const { return m_layout; } - void quick_slice(const int qs = qsUndef); +// void quick_slice(const int qs = qsUndef); void reslice_now(); void repair_stl(); void export_config(bool to_prusa = false); diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 36abe4616..992e60f5d 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -58,7 +58,7 @@ MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &he logo = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap); - topsizer->Add(logo, 0, wxALL, BORDER); + topsizer->Add(logo, 0, /*wxALL*/wxTOP | wxBOTTOM | wxLEFT, BORDER); topsizer->Add(rightsizer, 1, wxALL | wxEXPAND, BORDER); SetSizerAndFit(topsizer); @@ -107,5 +107,57 @@ ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &msg, bool monospaced_ Fit(); } + +// InfoDialog + +InfoDialog::InfoDialog(wxWindow* parent, const wxString &title, const wxString& msg) + : MsgDialog(parent, wxString::Format(_L("%s information"), SLIC3R_APP_NAME), title) + , msg(msg) +{ + this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + // Text shown as HTML, so that mouse selection and Ctrl-V to copy will work. + wxHtmlWindow* html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO); + { + wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + wxFont monospace = wxGetApp().code_font(); + wxColour text_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + wxColour bgr_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + auto text_clr_str = wxString::Format(wxT("#%02X%02X%02X"), text_clr.Red(), text_clr.Green(), text_clr.Blue()); + auto bgr_clr_str = wxString::Format(wxT("#%02X%02X%02X"), bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()); + const int font_size = font.GetPointSize() - 1; + int size[] = { font_size, font_size, font_size, font_size, font_size, font_size, font_size }; + html->SetFonts(font.GetFaceName(), monospace.GetFaceName(), size); + html->SetBorders(2); + + // calculate html page size from text + int lines = msg.Freq('\n'); + + if (msg.Contains("")) { + int pos = 0; + while (pos < (int)msg.Len() && pos != wxNOT_FOUND) { + pos = msg.find("", pos + 1); + lines+=2; + } + } + int page_height = std::min((font.GetPixelSize().y + 1) * lines, 68 * wxGetApp().em_unit()); + wxSize page_size(68 * wxGetApp().em_unit(), page_height); + + html->SetMinSize(page_size); + + std::string msg_escaped = xml_escape(msg.ToUTF8().data(), true); + boost::replace_all(msg_escaped, "\r\n", "
"); + boost::replace_all(msg_escaped, "\n", "
"); + html->SetPage("" + wxString::FromUTF8(msg_escaped.data()) + ""); + content_sizer->Add(html, 1, wxEXPAND); + } + + // Set info bitmap + logo->SetBitmap(create_scaled_bitmap("info", this, 84)); + + Fit(); +} + + } } diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index 70032089b..85d524a44 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -66,6 +66,22 @@ private: }; +// Generic info dialog, used for displaying exceptions +class InfoDialog : public MsgDialog +{ +public: + InfoDialog(wxWindow *parent, const wxString &title, const wxString &msg); + InfoDialog(InfoDialog&&) = delete; + InfoDialog(const InfoDialog&) = delete; + InfoDialog&operator=(InfoDialog&&) = delete; + InfoDialog&operator=(const InfoDialog&) = delete; + virtual ~InfoDialog() = default; + +private: + wxString msg; +}; + + } } diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 20d5e4ea7..dfca32b7a 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -9,6 +9,7 @@ #include "libslic3r/AppConfig.hpp" #include "wxExtensions.hpp" +#include "libslic3r/Config.hpp" #include #include @@ -36,7 +37,25 @@ wxDEFINE_EVENT(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, EjectDriveNotificationClicke wxDEFINE_EVENT(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, ExportGcodeNotificationClickedEvent); wxDEFINE_EVENT(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, PresetUpdateAvailableClickedEvent); -namespace Notifications_Internal{ +const NotificationManager::NotificationData NotificationManager::basic_notifications[] = { + // {NotificationType::SlicingNotPossible, NotificationLevel::RegularNotification, 10, _u8L("Slicing is not possible.")}, + // {NotificationType::ExportToRemovableFinished, NotificationLevel::ImportantNotification, 0, _u8L("Exporting finished."), _u8L("Eject drive.") }, + {NotificationType::Mouse3dDisconnected, NotificationLevel::RegularNotification, 10, _u8L("3D Mouse disconnected.") }, + // {NotificationType::Mouse3dConnected, NotificationLevel::RegularNotification, 5, _u8L("3D Mouse connected.") }, + // {NotificationType::NewPresetsAviable, NotificationLevel::ImportantNotification, 20, _u8L("New Presets are available."), _u8L("See here.") }, + {NotificationType::PresetUpdateAvailable, NotificationLevel::ImportantNotification, 20, _u8L("Configuration update is available."), _u8L("See more."), [](wxEvtHandler* evnthndlr){ + if (evnthndlr != nullptr) wxPostEvent(evnthndlr, PresetUpdateAvailableClickedEvent(EVT_PRESET_UPDATE_AVAILABLE_CLICKED)); return true; }}, + {NotificationType::NewAppAvailable, NotificationLevel::ImportantNotification, 20, _u8L("New version is available."), _u8L("See Releases page."), [](wxEvtHandler* evnthndlr){ + wxLaunchDefaultBrowser("https://github.com/" SLIC3R_GITHUB "/releases"); return true; }}, + {NotificationType::EmptyColorChangeCode, NotificationLevel::RegularNotification, 10, + _u8L("You have just added a G-code for color change, but its value is empty.\n" + "To export the G-code correctly, check the \"Color Change G-code\" in \"Printer Settings > Custom G-code\"") }, + //{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotification, 20, _u8L("New vesion of PrusaSlicer is available.", _u8L("Download page.") }, + //{NotificationType::LoadingFailed, NotificationLevel::RegularNotification, 20, _u8L("Loading of model has Failed") }, + //{NotificationType::DeviceEjected, NotificationLevel::RegularNotification, 10, _u8L("Removable device has been safely ejected")} // if we want changeble text (like here name of device), we need to do it as CustomNotification +}; + +namespace { ImFont* add_default_font(float pixel_size) { ImGuiIO& io = ImGui::GetIO(); @@ -48,7 +67,7 @@ namespace Notifications_Internal{ return font; } - static inline void push_style_color(ImGuiCol idx, const ImVec4& col, bool fading_out, float current_fade_opacity) + inline void push_style_color(ImGuiCol idx, const ImVec4& col, bool fading_out, float current_fade_opacity) { if (fading_out) ImGui::PushStyleColor(idx, ImVec4(col.x, col.y, col.z, col.w * current_fade_opacity)); @@ -138,8 +157,8 @@ NotificationManager::PopNotification::PopNotification(const NotificationData &n, , m_last_remaining_time (n.duration) , m_counting_down (n.duration != 0) , m_text1 (n.text1) - , m_hypertext (n.hypertext) - , m_text2 (n.text2) + , m_hypertext (n.hypertext) + , m_text2 (n.text2) , m_evt_handler (evt_handler) , m_notification_start (GLCanvas3D::timestamp_now()) { @@ -187,26 +206,26 @@ void NotificationManager::PopNotification::render(GLCanvas3D& canvas, float init // color change based on fading out bool fading_pop = false; if (m_fading_out) { - Notifications_Internal::push_style_color(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_fading_out, m_current_fade_opacity); - Notifications_Internal::push_style_color(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_Text), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_Text), m_fading_out, m_current_fade_opacity); fading_pop = true; } // background color if (m_is_gray) { ImVec4 backcolor(0.7f, 0.7f, 0.7f, 0.5f); - Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); } else if (m_data.level == NotificationLevel::ErrorNotification) { ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); backcolor.x += 0.3f; - Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); } else if (m_data.level == NotificationLevel::WarningNotification) { ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); backcolor.x += 0.3f; backcolor.y += 0.15f; - Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); } // name of window - probably indentifies window and is shown so last_end add whitespaces according to id @@ -304,23 +323,23 @@ NotificationManager::PopNotification::RenderResult NotificationManager::PopNotif if (m_fading_out) { if (!m_paused) m_current_fade_opacity -= 1.f / ((m_fading_time + 1.f) * 60.f); - Notifications_Internal::push_style_color(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_fading_out, m_current_fade_opacity); - Notifications_Internal::push_style_color(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_Text), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_Text), m_fading_out, m_current_fade_opacity); fading_pop = true; } // background color if (m_is_gray) { ImVec4 backcolor(0.7f, 0.7f, 0.7f, 0.5f); - Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); } else if (m_data.level == NotificationLevel::ErrorNotification) { ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); backcolor.x += 0.3f; - Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); } else if (m_data.level == NotificationLevel::WarningNotification) { ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); backcolor.x += 0.3f; backcolor.y += 0.15f; - Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); } //name of window - probably indentifies window and is shown so last_end add whitespaces according to id @@ -414,7 +433,7 @@ void NotificationManager::PopNotification::init() if (ImGui::CalcTextSize(text.substr(last_end).c_str()).x >= m_window_width - m_window_width_offset) { // more than one line till end int next_space = text.find_first_of(' ', last_end); - if (next_space > 0) { + 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; @@ -524,7 +543,7 @@ void NotificationManager::PopNotification::render_hypertext(ImGuiWrapper& imgui, orange_color.y += 0.2f; //text - Notifications_Internal::push_style_color(ImGuiCol_Text, orange_color, m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_Text, orange_color, m_fading_out, m_current_fade_opacity); ImGui::SetCursorPosX(text_x); ImGui::SetCursorPosY(text_y); imgui.text(text.c_str()); @@ -547,8 +566,8 @@ void NotificationManager::PopNotification::render_close_button(ImGuiWrapper& img orange_color.w = 0.8f; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); - Notifications_Internal::push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_fading_out, m_current_fade_opacity); - Notifications_Internal::push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_fading_out, m_current_fade_opacity); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); @@ -647,9 +666,9 @@ void NotificationManager::PopNotification::render_minimize_button(ImGuiWrapper& { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); - Notifications_Internal::push_style_color(ImGuiCol_ButtonActive, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_fading_out, m_current_fade_opacity); - Notifications_Internal::push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_fading_out, m_current_fade_opacity); - Notifications_Internal::push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_ButtonActive, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_fading_out, m_current_fade_opacity); //button - if part if treggered @@ -852,8 +871,9 @@ void NotificationManager::ExportFinishedNotification::render_text(ImGuiWrapper& for (size_t i = 0; i < m_lines_count; i++) { if (m_text1.size() >= m_endlines[i]) { std::string line = m_text1.substr(last_end, m_endlines[i] - last_end); - if (i < m_lines_count - 1) - last_end = m_endlines[i] + (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0); + last_end = m_endlines[i]; + if (m_text1.size() > m_endlines[i]) + last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0); ImGui::SetCursorPosX(x_offset); ImGui::SetCursorPosY(starting_y + i * shift_y); imgui.text(line.c_str()); @@ -881,8 +901,8 @@ void NotificationManager::ExportFinishedNotification::render_eject_button(ImGuiW orange_color.w = 0.8f; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); - Notifications_Internal::push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_fading_out, m_current_fade_opacity); - Notifications_Internal::push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_fading_out, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_fading_out, m_current_fade_opacity); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); std::string button_text; @@ -937,7 +957,7 @@ void NotificationManager::ExportFinishedNotification::render_eject_button(ImGuiW } bool NotificationManager::ExportFinishedNotification::on_text_click() { - Notifications_Internal::open_folder(m_export_dir_path); + open_folder(m_export_dir_path); return false; } //------ProgressBar---------------- @@ -980,10 +1000,10 @@ NotificationManager::NotificationManager(wxEvtHandler* evt_handler) : } void NotificationManager::push_notification(const NotificationType type, int timestamp) { - auto it = std::find_if(basic_notifications.begin(), basic_notifications.end(), + auto it = std::find_if(std::begin(basic_notifications), std::end(basic_notifications), boost::bind(&NotificationData::type, boost::placeholders::_1) == type); - assert(it != basic_notifications.end()); - if (it != basic_notifications.end()) + assert(it != std::end(basic_notifications)); + if (it != std::end(basic_notifications)) push_notification_data(*it, timestamp); } void NotificationManager::push_notification(const std::string& text, int timestamp) diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index de7204038..ee13ae8da 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -167,7 +167,7 @@ private: // Callback for hypertext - returns true if notification should close after triggering // Usually sends event to UI thread thru wxEvtHandler. // Examples in basic_notifications. - std::function callback { nullptr }; + std::function callback; const std::string text2; }; @@ -231,7 +231,7 @@ private: //returns top in actual frame float get_current_top() const { return m_top_y; } const NotificationType get_type() const { return m_data.type; } - const NotificationData get_data() const { return m_data; } + const NotificationData& get_data() const { return m_data; } const bool is_gray() const { return m_is_gray; } // Call equals one second down void substract_remaining_time(int seconds) { m_remaining_time -= seconds; } @@ -383,10 +383,10 @@ private: ProgressBarNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, float percentage) : PopNotification(n, id_provider, evt_handler) { set_percentage(percentage); } void set_percentage(float percent) { m_percentage = percent; if (percent >= 1.0f) m_progress_complete = true; else m_progress_complete = false; } protected: - virtual void init(); - virtual void render_text(ImGuiWrapper& imgui, + void init(); + void render_text(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, - const float win_pos_x, const float win_pos_y); + const float win_pos_x, const float win_pos_y) override; void render_bar(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y); @@ -410,20 +410,20 @@ private: std::string m_export_dir_path; protected: // Reserves space on right for more buttons - virtual void count_spaces() override; - virtual void render_text(ImGuiWrapper& imgui, - const float win_size_x, const float win_size_y, - const float win_pos_x, const float win_pos_y) override; + void count_spaces() override; + void render_text(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; // Renders also button to open directory with exported path and eject removable media - virtual void render_close_button(ImGuiWrapper& imgui, - const float win_size_x, const float win_size_y, - const float win_pos_x, const float win_pos_y) override; + void render_close_button(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; void render_eject_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y); - virtual void render_minimize_button(ImGuiWrapper& imgui, const float win_pos_x, const float win_pos_y) override + void render_minimize_button(ImGuiWrapper& imgui, const float win_pos_x, const float win_pos_y) override { m_minimize_b_visible = false; } - virtual bool on_text_click() override; + bool on_text_click() override; // local time of last hover for showing tooltip long m_hover_time { 0 }; }; @@ -465,23 +465,7 @@ private: #endif // ENABLE_NEW_NOTIFICATIONS_FADE_OUT */ //prepared (basic) notifications - const std::vector basic_notifications = { -// {NotificationType::SlicingNotPossible, NotificationLevel::RegularNotification, 10, _u8L("Slicing is not possible.")}, -// {NotificationType::ExportToRemovableFinished, NotificationLevel::ImportantNotification, 0, _u8L("Exporting finished."), _u8L("Eject drive.") }, - {NotificationType::Mouse3dDisconnected, NotificationLevel::RegularNotification, 10, _u8L("3D Mouse disconnected.") }, -// {NotificationType::Mouse3dConnected, NotificationLevel::RegularNotification, 5, _u8L("3D Mouse connected.") }, -// {NotificationType::NewPresetsAviable, NotificationLevel::ImportantNotification, 20, _u8L("New Presets are available."), _u8L("See here.") }, - {NotificationType::PresetUpdateAvailable, NotificationLevel::ImportantNotification, 20, _u8L("Configuration update is available."), _u8L("See more."), [](wxEvtHandler* evnthndlr){ - if (evnthndlr != nullptr) wxPostEvent(evnthndlr, PresetUpdateAvailableClickedEvent(EVT_PRESET_UPDATE_AVAILABLE_CLICKED)); return true; }}, - {NotificationType::NewAppAvailable, NotificationLevel::ImportantNotification, 20, _u8L("New version is available."), _u8L("See Releases page."), [](wxEvtHandler* evnthndlr){ - wxLaunchDefaultBrowser("https://github.com/" SLIC3R_GITHUB "/releases"); return true; }}, - {NotificationType::EmptyColorChangeCode, NotificationLevel::RegularNotification, 10, - _u8L("You have just added a G-code for color change, but its value is empty.\n" - "To export the G-code correctly, check the \"Color Change G-code\" in \"Printer Settings > Custom G-code\"") }, - //{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotification, 20, _u8L("New vesion of PrusaSlicer is available.", _u8L("Download page.") }, - //{NotificationType::LoadingFailed, NotificationLevel::RegularNotification, 20, _u8L("Loading of model has Failed") }, - //{NotificationType::DeviceEjected, NotificationLevel::RegularNotification, 10, _u8L("Removable device has been safely ejected")} // if we want changeble text (like here name of device), we need to do it as CustomNotification - }; + static const NotificationData basic_notifications[]; }; }//namespace GUI diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index a9e3449d4..3d842d0e4 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -977,6 +977,8 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config ret = static_cast(config.option>(opt_key)->value); } else if (opt_key == "output_format") { ret = static_cast(config.option>(opt_key)->value); + } else if (opt_key == "config_compatibility") { + ret = static_cast(config.option>(opt_key)->value); } } break; diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index a98ef4593..474c5fd77 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -67,7 +67,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 @@ -153,7 +154,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()); SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); @@ -484,7 +486,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(); @@ -492,19 +494,30 @@ 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 && 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; + } + // hide api key for klipper if (opt && opt->value == htKlipper) { m_optgroup->hide_field("printhost_apikey"); } - } + } else { m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false); m_optgroup->hide_field("host_type"); @@ -534,6 +547,58 @@ 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() { return m_printer_name->GetValue(); @@ -661,8 +726,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) @@ -689,7 +755,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/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a210dd8eb..3306b0928 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2399,7 +2399,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); @@ -2425,6 +2426,8 @@ 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()) + show_substitutions_info(config_substitutions.substitutions, filename.string()); this->model.custom_gcode_per_print_z = model.custom_gcode_per_print_z; } @@ -2445,11 +2448,15 @@ 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(); } + } catch (const ConfigurationError &e) { + std::string message = GUI::format(_L("Failed loading file \"%1%\" due to an invalid configuration."), filename.string()) + "\n\n" + e.what(); + GUI::show_error(q, message); + continue; } catch (const std::exception &e) { GUI::show_error(q, e.what()); continue; @@ -3316,7 +3323,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(); @@ -4706,7 +4713,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(); @@ -5477,13 +5486,14 @@ void Plater::export_gcode(bool prefer_removable) fs::path output_path; { - wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SL1 file as:"), + std::string ext = default_output_file.extension().string(); + wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SL1 / SL1S file as:"), start_dir, from_path(default_output_file.filename()), - GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : FT_PNGZIP, default_output_file.extension().string()), + GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : boost::iequals(ext, ".sl1s") ? FT_SL1S : FT_SL1, ext), wxFD_SAVE | wxFD_OVERWRITE_PROMPT ); - if (dlg.ShowModal() == wxID_OK) + if (dlg.ShowModal() == wxID_OK) output_path = into_path(dlg.GetPath()); } diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 1c777dc3b..d52e7dbb3 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -887,9 +887,13 @@ void PlaterPresetComboBox::update() if (!tooltip.IsEmpty()) SetToolTip(tooltip); +#ifdef __WXMSW__ + // Use this part of code just on Windows to avoid of some layout issues on Linux + // see https://github.com/prusa3d/PrusaSlicer/issues/5163 and https://github.com/prusa3d/PrusaSlicer/issues/5505 // Update control min size after rescale (changed Display DPI under MSW) if (GetMinWidth() != 20 * m_em_unit) SetMinSize(wxSize(20 * m_em_unit, GetSize().GetHeight())); +#endif //__WXMSW__ } void PlaterPresetComboBox::msw_rescale() diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index e9687873b..6e29881a2 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -952,6 +952,8 @@ static wxString get_string_value(std::string opt_key, const DynamicPrintConfig& return get_string_from_enum(opt_key, config); if (opt_key == "complete_objects_sort") return get_string_from_enum(opt_key, config); + if (opt_key == "config_compatibility") + return get_string_from_enum(opt_key, config); if (opt_key == "display_orientation") return get_string_from_enum(opt_key, config); if (opt_key == "output_format") diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp index 44e39990b..dc2841743 100644 --- a/src/slic3r/GUI/UpdateDialogs.cpp +++ b/src/slic3r/GUI/UpdateDialogs.cpp @@ -85,8 +85,11 @@ bool MsgUpdateSlic3r::disable_version_check() const // MsgUpdateConfig -MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates) : - MsgDialog(nullptr, _(L("Configuration update")), _(L("Configuration update is available")), wxID_NONE) +MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates, bool force_before_wizard/* = false*/) : + MsgDialog(nullptr, force_before_wizard ? _L("Opening Configuration Wizard") : _L("Configuration update"), + force_before_wizard ? _L("PrusaSlicer is not using the newest configuration available.\n" + "Configuration Wizard may not offer the latest printers, filaments and SLA materials to be installed. ") : + _L("Configuration update is available"), wxID_NONE) { auto *text = new wxStaticText(this, wxID_ANY, _(L( "Would you like to install it?\n\n" @@ -130,11 +133,17 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates) : content_sizer->Add(versions); content_sizer->AddSpacer(2*VERT_SPACING); - auto *btn_cancel = new wxButton(this, wxID_CANCEL); - btn_sizer->Add(btn_cancel); - btn_sizer->AddSpacer(HORIZ_SPACING); - auto *btn_ok = new wxButton(this, wxID_OK); + auto* btn_ok = new wxButton(this, wxID_OK, force_before_wizard ? _L("Install") : "OK"); btn_sizer->Add(btn_ok); + btn_sizer->AddSpacer(HORIZ_SPACING); + if (force_before_wizard) { + auto* btn_no_install = new wxButton(this, wxID_ANY, _L("Don't install")); + btn_no_install->Bind(wxEVT_BUTTON, [this](wxEvent&) { this->EndModal(wxID_CLOSE); }); + btn_sizer->Add(btn_no_install); + btn_sizer->AddSpacer(HORIZ_SPACING); + } + auto* btn_cancel = new wxButton(this, wxID_CANCEL); + btn_sizer->Add(btn_cancel); btn_ok->SetFocus(); Fit(); diff --git a/src/slic3r/GUI/UpdateDialogs.hpp b/src/slic3r/GUI/UpdateDialogs.hpp index 6d355065a..aa3a10677 100644 --- a/src/slic3r/GUI/UpdateDialogs.hpp +++ b/src/slic3r/GUI/UpdateDialogs.hpp @@ -54,7 +54,8 @@ public: {} }; - MsgUpdateConfig(const std::vector &updates); + // force_before_wizard - indicates that check of updated is forced before ConfigWizard opening + MsgUpdateConfig(const std::vector &updates, bool force_before_wizard = false); MsgUpdateConfig(MsgUpdateConfig &&) = delete; MsgUpdateConfig(const MsgUpdateConfig &) = delete; MsgUpdateConfig &operator=(MsgUpdateConfig &&) = delete; diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index bcab6daaf..9df656701 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -377,7 +377,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/HexFile.cpp b/src/slic3r/Utils/HexFile.cpp index 26596f629..a13fcab02 100644 --- a/src/slic3r/Utils/HexFile.cpp +++ b/src/slic3r/Utils/HexFile.cpp @@ -19,6 +19,7 @@ static HexFile::DeviceKind parse_device_kind(const std::string &str) else if (str == "mk3") { return HexFile::DEV_MK3; } else if (str == "mm-control") { return HexFile::DEV_MM_CONTROL; } else if (str == "cw1") { return HexFile::DEV_CW1; } + else if (str == "cw1s") { return HexFile::DEV_CW1S; } else { return HexFile::DEV_GENERIC; } } diff --git a/src/slic3r/Utils/HexFile.hpp b/src/slic3r/Utils/HexFile.hpp index 742ae00e6..b32d110ed 100644 --- a/src/slic3r/Utils/HexFile.hpp +++ b/src/slic3r/Utils/HexFile.hpp @@ -17,6 +17,7 @@ struct HexFile DEV_MK3, DEV_MM_CONTROL, DEV_CW1, + DEV_CW1S, }; boost::filesystem::path path; diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index bb864c98d..f0d9982e4 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -178,7 +178,7 @@ const char* SL1Host::get_name() const { return "SL1Host"; } wxString SL1Host::get_test_ok_msg () const { - return wxString::Format(_L("Connection to %s works correctly."), "Prusa SL1"); + return wxString::Format(_L("Connection to %s works correctly."), "Prusa SL1 / SL1S"); } wxString SL1Host::get_test_failed_msg (wxString &msg) const @@ -209,4 +209,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 c91dc5834..7c17f3793 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/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 643355f37..fdb5193b7 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -56,15 +56,18 @@ 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); + // Permissions should be copied from the source file by copy_file(). We are not sure about the source + // permissions, let's rewrite them with 644. + static constexpr const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read; fs::permissions(target, perms); } @@ -168,7 +171,7 @@ struct PresetUpdater::priv void check_install_indices() const; Updates get_config_updates(const Semver& old_slic3r_version) const; - void perform_updates(Updates &&updates, bool snapshot = true) const; + bool perform_updates(Updates &&updates, bool snapshot = true) const; void set_waiting_updates(Updates u); }; @@ -474,7 +477,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version } if (recommended->config_version < vp.config_version) { - BOOST_LOG_TRIVIAL(warning) << format("Recommended config version for the currently running " SLIC3R_APP_NAME " is older than the currently installed config for vendor %1%. This should not happen.", idx.vendor()); + BOOST_LOG_TRIVIAL(warning) << format("Recommended config version for the currently running %1% is older than the currently installed config for vendor %2%. This should not happen.", SLIC3R_APP_NAME, idx.vendor()); continue; } @@ -545,7 +548,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version found = true; } else { BOOST_LOG_TRIVIAL(warning) << format("The recommended config version for vendor `%1%` in resources does not match the recommended\n" - " config version for this version of " SLIC3R_APP_NAME ". Corrupted installation?", idx.vendor()); + " config version for this version of `%2%`. Corrupted installation?", idx.vendor(), SLIC3R_APP_NAME); } } } @@ -598,12 +601,14 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version return updates; } -void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) const +bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) const { if (updates.incompats.size() > 0) { if (snapshot) { BOOST_LOG_TRIVIAL(info) << "Taking a snapshot..."; - SnapshotDB::singleton().take_snapshot(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_DOWNGRADE); + if (! GUI::Config::take_config_snapshot_cancel_on_error(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_DOWNGRADE, "", + _u8L("Continue and install configuration updates?"))) + return false; } BOOST_LOG_TRIVIAL(info) << format("Deleting %1% incompatible bundles", updates.incompats.size()); @@ -618,7 +623,9 @@ void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons if (snapshot) { BOOST_LOG_TRIVIAL(info) << "Taking a snapshot..."; - SnapshotDB::singleton().take_snapshot(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_UPGRADE); + if (! GUI::Config::take_config_snapshot_cancel_on_error(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_UPGRADE, "", + _u8L("Continue and install configuration updates?"))) + return false; } BOOST_LOG_TRIVIAL(info) << format("Performing %1% updates", updates.updates.size()); @@ -629,7 +636,8 @@ void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons update.install(); PresetBundle bundle; - bundle.load_configbundle(update.source.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); + // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. + bundle.load_configbundle(update.source.string(), PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); BOOST_LOG_TRIVIAL(info) << format("Deleting %1% conflicting presets", bundle.prints.size() + bundle.filaments.size() + bundle.printers.size()); @@ -661,6 +669,8 @@ void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons for (const auto &name : bundle.obsolete_presets.printers) { obsolete_remover("printer", name); } } } + + return true; } void PresetUpdater::priv::set_waiting_updates(Updates u) @@ -727,7 +737,20 @@ void PresetUpdater::slic3r_update_notify() } } -PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, bool no_notification) const +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. + // However throw on substitutions in system profiles, those shall never happen with system profiles installed over the air. + GUI::wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem); + GUI::wxGetApp().load_current_presets(); + GUI::wxGetApp().plater()->set_bed_shape(); + GUI::wxGetApp().update_wizard_from_config(); +} + +PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params) const { if (! p->enabled_config_update) { return R_NOOP; } @@ -761,11 +784,9 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 // This effectively removes the incompatible bundles: // (snapshot is taken beforehand) - p->perform_updates(std::move(updates)); - - if (!GUI::wxGetApp().run_wizard(GUI::ConfigWizard::RR_DATA_INCOMPAT)) { + if (! p->perform_updates(std::move(updates)) || + ! GUI::wxGetApp().run_wizard(GUI::ConfigWizard::RR_DATA_INCOMPAT)) return R_INCOMPAT_EXIT; - } return R_INCOMPAT_CONFIGURED; } @@ -784,7 +805,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()); @@ -799,14 +820,9 @@ 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(); + if (! p->perform_updates(std::move(updates))) + return R_INCOMPAT_EXIT; + reload_configs_update_gui(); return R_UPDATE_INSTALLED; } else { @@ -816,36 +832,36 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 } // regular update - if (no_notification) { - BOOST_LOG_TRIVIAL(info) << format("Update of %1% bundles available. Asking for confirmation ...", p->waiting_updates.updates.size()); - - std::vector updates_msg; - for (const auto& update : updates.updates) { - std::string changelog_url = update.version.config_version.prerelease() == nullptr ? update.changelog_url : std::string(); - updates_msg.emplace_back(update.vendor, update.version.config_version, update.version.comment, std::move(changelog_url)); - } - - GUI::MsgUpdateConfig dlg(updates_msg); - - const auto res = dlg.ShowModal(); - 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(); - return R_UPDATE_INSTALLED; - } - else { - BOOST_LOG_TRIVIAL(info) << "User refused the update"; - return R_UPDATE_REJECT; - } - } else { + if (params == UpdateParams::SHOW_NOTIFICATION) { p->set_waiting_updates(updates); GUI::wxGetApp().plater()->get_notification_manager()->push_notification(GUI::NotificationType::PresetUpdateAvailable); } + else { + BOOST_LOG_TRIVIAL(info) << format("Update of %1% bundles available. Asking for confirmation ...", p->waiting_updates.updates.size()); + + std::vector updates_msg; + for (const auto& update : updates.updates) { + std::string changelog_url = update.version.config_version.prerelease() == nullptr ? update.changelog_url : std::string(); + updates_msg.emplace_back(update.vendor, update.version.config_version, update.version.comment, std::move(changelog_url)); + } + + GUI::MsgUpdateConfig dlg(updates_msg, params == UpdateParams::FORCED_BEFORE_WIZARD); + + const auto res = dlg.ShowModal(); + if (res == wxID_OK) { + BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; + if (! p->perform_updates(std::move(updates))) + return R_ALL_CANCELED; + reload_configs_update_gui(); + return R_UPDATE_INSTALLED; + } + else { + BOOST_LOG_TRIVIAL(info) << "User refused the update"; + if (params == UpdateParams::FORCED_BEFORE_WIZARD && res == wxID_CANCEL) + return R_ALL_CANCELED; + return R_UPDATE_REJECT; + } + } // MsgUpdateConfig will show after the notificaation is clicked } else { @@ -855,7 +871,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 return R_NOOP; } -void PresetUpdater::install_bundles_rsrc(std::vector bundles, bool snapshot) const +bool PresetUpdater::install_bundles_rsrc(std::vector bundles, bool snapshot) const { Updates updates; @@ -867,7 +883,7 @@ void PresetUpdater::install_bundles_rsrc(std::vector bundles, bool updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); } - p->perform_updates(std::move(updates), snapshot); + return p->perform_updates(std::move(updates), snapshot); } void PresetUpdater::on_update_notification_confirm() @@ -887,20 +903,14 @@ void PresetUpdater::on_update_notification_confirm() const auto res = dlg.ShowModal(); 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(); - p->has_waiting_updates = false; - //return R_UPDATE_INSTALLED; + if (p->perform_updates(std::move(p->waiting_updates))) { + reload_configs_update_gui(); + p->has_waiting_updates = false; + } } else { BOOST_LOG_TRIVIAL(info) << "User refused the update"; - //return R_UPDATE_REJECT; - } - + } } } diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index 0ca363c61..d7eeb5604 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -35,18 +35,24 @@ public: R_INCOMPAT_CONFIGURED, R_UPDATE_INSTALLED, R_UPDATE_REJECT, - R_UPDATE_NOTIFICATION + R_UPDATE_NOTIFICATION, + R_ALL_CANCELED + }; + + enum class UpdateParams { + SHOW_TEXT_BOX, // force modal textbox + SHOW_NOTIFICATION, // only shows notification + FORCED_BEFORE_WIZARD // indicates that check of updated is forced before ConfigWizard opening }; // If updating is enabled, check if updates are available in cache, if so, ask about installation. // A false return value implies Slic3r should exit due to incompatibility of configuration. // Providing old slic3r version upgrade profiles on upgrade of an application even in case // that the config index installed from the Internet is equal to the index contained in the installation package. - // no_notification = force modal textbox, otherwise some cases only shows notification - UpdateResult config_update(const Semver &old_slic3r_version, bool no_notification) const; + UpdateResult config_update(const Semver &old_slic3r_version, UpdateParams params) const; // "Update" a list of bundles from resources (behaves like an online update). - void install_bundles_rsrc(std::vector bundles, bool snapshot = true) const; + bool install_bundles_rsrc(std::vector bundles, bool snapshot = true) const; void on_update_notification_confirm(); private: diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 455494277..52ec8bbfe 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -60,6 +60,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) const auto host_type = opt != nullptr ? opt->value : htOctoPrint; switch (host_type) { + case htPrusaLink: return new PrusaLink(config); case htOctoPrint: return new OctoPrint(config); case htDuet: return new Duet(config); case htFlashAir: return new FlashAir(config); diff --git a/tests/data/cpp/test_data.cpp b/tests/data/cpp/test_data.cpp index e09976571..830aa1a5f 100644 --- a/tests/data/cpp/test_data.cpp +++ b/tests/data/cpp/test_data.cpp @@ -203,14 +203,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 c9052a668..2ba1c7de5 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 f81fd9b99..ca82cc20e 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 e2891efaa..9009cd97c 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -62,7 +62,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.5 }, // get a known number of layers diff --git a/tests/fff_print/test_printgcode.cpp b/tests/fff_print/test_printgcode.cpp index 10d3af9a3..87bbea88d 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 fb41ef93b..035d47b11 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 9ffc45b61..ddfeecd80 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -12,7 +12,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()); } @@ -43,7 +43,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); } @@ -62,7 +62,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); } @@ -95,7 +95,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"), BadOptionValueException); } THEN("The value does not change.") { REQUIRE(config.opt("perimeter_speed")->getFloat() == 60.0); @@ -120,7 +120,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); @@ -128,7 +128,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); @@ -198,7 +198,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 3dd8acda7..3035ddb75 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -10,7 +10,7 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { auto config = DynamicPrintConfig::full_print_config(); // To test the "first_layer_extrusion_width" over "nozzle_diameter" chain. - 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 b8f996797..d1c9bfa0c 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 b57df5e37..2f8a8f2f8 100644 --- a/xs/xsp/Flow.xsp +++ b/xs/xsp/Flow.xsp @@ -41,7 +41,7 @@ _new_from_width(CLASS, role, width, nozzle_diameter, height, bridge_flow_ratio) float bridge_flow_ratio; CODE: ConfigOptionFloatOrPercent optwidth; - optwidth.deserialize(width); + optwidth.deserialize(width, ForwardCompatibilitySubstitutionRule::Disable); RETVAL = new Flow(Flow::new_from_config_width(role, optwidth, nozzle_diameter, height, bridge_flow_ratio)); OUTPUT: RETVAL diff --git a/xs/xsp/Layer.xsp b/xs/xsp/Layer.xsp index fdcc26eb6..5d006e676 100644 --- a/xs/xsp/Layer.xsp +++ b/xs/xsp/Layer.xsp @@ -18,8 +18,6 @@ %code%{ RETVAL = &THIS->thin_fills; %}; Ref fill_surfaces() %code%{ RETVAL = &THIS->fill_surfaces; %}; - Polygons bridged() - %code%{ RETVAL = THIS->bridged; %}; Ref perimeters() %code%{ RETVAL = &THIS->perimeters; %}; Ref fills() 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()); }