diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 50e5096bf..5212d66c8 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -118,7 +118,8 @@ int CLI::run(int argc, char **argv) boost::algorithm::iends_with(boost::filesystem::path(argv[0]).filename().string(), "gcodeviewer"); #endif // _WIN32 - const std::vector &load_configs = m_config.option("load", true)->values; + const std::vector &load_configs = m_config.option("load", true)->values; + const ForwardCompatibilitySubstitutionRule config_substitution_rule = m_config.option>("config_compatibility", true)->value; // load config files supplied via --load for (auto const &file : load_configs) { @@ -130,13 +131,19 @@ int CLI::run(int argc, char **argv) return 1; } } - DynamicPrintConfig config; + DynamicPrintConfig config; + ConfigSubstitutions config_substitutions; try { - config.load(file); + config_substitutions = config.load(file, config_substitution_rule); } catch (std::exception &ex) { - boost::nowide::cerr << "Error while reading config file: " << ex.what() << std::endl; + boost::nowide::cerr << "Error while reading config file \"" << file << "\": " << ex.what() << std::endl; return 1; } + if (! config_substitutions.empty()) { + boost::nowide::cout << "The following configuration values were substituted when loading \" << file << \":\n"; + for (const ConfigSubstitution &subst : config_substitutions) + boost::nowide::cout << "\tkey = \"" << subst.opt_def->opt_key << "\"\t loaded = \"" << subst.old_value << "\tsubstituted = \"" << subst.new_value->serialize() << "\"\n"; + } config.normalize_fdm(); PrinterTechnology other_printer_technology = get_printer_technology(config); if (printer_technology == ptUnknown) { @@ -174,7 +181,9 @@ int CLI::run(int argc, char **argv) try { // When loading an AMF or 3MF, config is imported as well, including the printer technology. DynamicPrintConfig config; - model = Model::read_from_file(file, &config, true); + ConfigSubstitutionContext config_substitutions(config_substitution_rule); + //FIXME should we check the version here? // | Model::LoadAttribute::CheckVersion ? + model = Model::read_from_file(file, &config, &config_substitutions, Model::LoadAttribute::AddDefaultInstances); PrinterTechnology other_printer_technology = get_printer_technology(config); if (printer_technology == ptUnknown) { printer_technology = other_printer_technology; @@ -183,6 +192,11 @@ int CLI::run(int argc, char **argv) boost::nowide::cerr << "Mixing configurations for FFF and SLA technologies" << std::endl; return 1; } + if (! config_substitutions.substitutions.empty()) { + boost::nowide::cout << "The following configuration values were substituted when loading \" << file << \":\n"; + for (const ConfigSubstitution& subst : config_substitutions.substitutions) + boost::nowide::cout << "\tkey = \"" << subst.opt_def->opt_key << "\"\t loaded = \"" << subst.old_value << "\tsubstituted = \"" << subst.new_value->serialize() << "\"\n"; + } // config is applied to m_print_config before the current m_config values. config += std::move(m_print_config); m_print_config = std::move(config); diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index fe09e6557..bfe8427d0 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -33,6 +33,7 @@ add_library(libslic3r STATIC EdgeGrid.hpp ElephantFootCompensation.cpp ElephantFootCompensation.hpp + enum_bitmask.hpp ExPolygon.cpp ExPolygon.hpp ExPolygonCollection.cpp diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index bd396243c..b9f9b266d 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -23,6 +23,10 @@ #include #include +//FIXME for GCodeFlavor and gcfMarlin (for forward-compatibility conversion) +// This is not nice, likely it would be better to pass the ConfigSubstitutionContext to handle_legacy(). +#include "PrintConfig.hpp" + namespace Slic3r { // Escape \n, \r and backslash @@ -211,6 +215,10 @@ std::string escape_ampersand(const std::string& str) return std::string(out.data(), outptr - out.data()); } +void ConfigOptionDeleter::operator()(ConfigOption* p) { + delete p; +} + std::vector ConfigOptionDef::cli_args(const std::string &key) const { std::vector args; @@ -361,7 +369,8 @@ std::ostream& ConfigDef::print_cli_help(std::ostream& out, bool show_defaults, s // right: option description std::string descr = def.tooltip; - if (show_defaults && def.default_value && def.type != coBool + bool show_defaults_this = show_defaults || def.opt_key == "config_compatibility"; + if (show_defaults_this && def.default_value && def.type != coBool && (def.type != coString || !def.default_value->serialize().empty())) { descr += " ("; if (!def.sidetext.empty()) { @@ -469,7 +478,7 @@ void ConfigBase::set(const std::string &opt_key, double value, bool create) } } -bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, bool append) +bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, ConfigSubstitutionContext& substitutions_ctxt, bool append) { t_config_option_key opt_key = opt_key_src; std::string value = value_src; @@ -479,29 +488,29 @@ bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src, if (opt_key.empty()) // Ignore the option. return true; - return this->set_deserialize_raw(opt_key, value, append); + return this->set_deserialize_raw(opt_key, value, substitutions_ctxt, append); } -void ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const std::string &value_src, bool append) +void ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const std::string &value_src, ConfigSubstitutionContext& substitutions_ctxt, bool append) { - if (! this->set_deserialize_nothrow(opt_key_src, value_src, append)) + if (! this->set_deserialize_nothrow(opt_key_src, value_src, substitutions_ctxt, append)) throw BadOptionTypeException(format("ConfigBase::set_deserialize() failed for parameter \"%1%\", value \"%2%\"", opt_key_src, value_src)); } -void ConfigBase::set_deserialize(std::initializer_list items) +void ConfigBase::set_deserialize(std::initializer_list items, ConfigSubstitutionContext& substitutions_ctxt) { for (const SetDeserializeItem &item : items) - this->set_deserialize(item.opt_key, item.opt_value, item.append); + this->set_deserialize(item.opt_key, item.opt_value, substitutions_ctxt, item.append); } -bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, const std::string &value, bool append) +bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, const std::string &value, ConfigSubstitutionContext& substitutions_ctxt, bool append) { - t_config_option_key opt_key = opt_key_src; + t_config_option_key opt_key = opt_key_src; // Try to deserialize the option by its name. - const ConfigDef *def = this->def(); + const ConfigDef *def = this->def(); if (def == nullptr) throw NoDefinitionException(opt_key); - const ConfigOptionDef *optdef = def->get(opt_key); + const ConfigOptionDef *optdef = def->get(opt_key); if (optdef == nullptr) { // If we didn't find an option, look for any other option having this as an alias. for (const auto &opt : def->options) { @@ -523,14 +532,35 @@ bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, con // Aliasing for example "solid_layers" to "top_solid_layers" and "bottom_solid_layers". for (const t_config_option_key &shortcut : optdef->shortcut) // Recursive call. - if (! this->set_deserialize_raw(shortcut, value, append)) + if (! this->set_deserialize_raw(shortcut, value, substitutions_ctxt, append)) return false; return true; } ConfigOption *opt = this->option(opt_key, true); assert(opt != nullptr); - return opt->deserialize(value, append); + bool success = opt->deserialize(value, append); + if (! success && substitutions_ctxt.rule != ForwardCompatibilitySubstitutionRule::Disable && + // Only allow substitutions of an enum value by another enum value or a boolean value with an enum value. + // That means, we expect enum values being added in the future and possibly booleans being converted to enums. + (optdef->type == coEnum || optdef->type == coBool)) + { + // Deserialize failed, try to substitute with a default value. + assert(substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::Enable || substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::EnableSilent); + + opt->set(optdef->default_value.get()); + + if (substitutions_ctxt.rule == ForwardCompatibilitySubstitutionRule::Enable) { + // Log the substitution. + ConfigSubstitution config_substitution; + config_substitution.opt_def = optdef; + config_substitution.old_value = value;//std::unique_ptr(opt); + config_substitution.new_value = ConfigOptionUniquePtr(this->option(opt_key, true)->clone()); + substitutions_ctxt.substitutions.emplace_back(std::move(config_substitution)); + } + return true; + } + return success; } // Return an absolute value of a possibly relative config variable. @@ -589,36 +619,37 @@ void ConfigBase::setenv_() const } } -void ConfigBase::load(const std::string &file) +ConfigSubstitutions ConfigBase::load(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule) { - if (is_gcode_file(file)) - this->load_from_gcode_file(file); - else - this->load_from_ini(file); + return is_gcode_file(file) ? + this->load_from_gcode_file(file, true /* check header */, compatibility_rule) : + this->load_from_ini(file, compatibility_rule); } -void ConfigBase::load_from_ini(const std::string &file) +ConfigSubstitutions ConfigBase::load_from_ini(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule) { boost::property_tree::ptree tree; boost::nowide::ifstream ifs(file); boost::property_tree::read_ini(ifs, tree); - this->load(tree); + return this->load(tree, compatibility_rule); } -void ConfigBase::load(const boost::property_tree::ptree &tree) +ConfigSubstitutions ConfigBase::load(const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule) { + ConfigSubstitutionContext substitutions_ctxt(compatibility_rule); for (const boost::property_tree::ptree::value_type &v : tree) { try { t_config_option_key opt_key = v.first; - this->set_deserialize(opt_key, v.second.get_value()); + this->set_deserialize(opt_key, v.second.get_value(), substitutions_ctxt); } catch (UnknownOptionException & /* e */) { // ignore } } + return std::move(substitutions_ctxt.substitutions); } // Load the config keys from the tail of a G-code file. -void ConfigBase::load_from_gcode_file(const std::string& file, bool check_header) +ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, bool check_header, ForwardCompatibilitySubstitutionRule compatibility_rule) { // Read a 64k block from the end of the G-code. boost::nowide::ifstream ifs(file); @@ -639,13 +670,15 @@ void ConfigBase::load_from_gcode_file(const std::string& file, bool check_header ifs.read(data.data(), data_length); ifs.close(); - size_t key_value_pairs = load_from_gcode_string(data.data()); + ConfigSubstitutionContext substitutions_ctxt(compatibility_rule); + size_t key_value_pairs = load_from_gcode_string(data.data(), substitutions_ctxt); if (key_value_pairs < 80) throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs)); + return std::move(substitutions_ctxt.substitutions); } // Load the config keys from the given string. -size_t ConfigBase::load_from_gcode_string(const char* str) +size_t ConfigBase::load_from_gcode_string(const char* str, ConfigSubstitutionContext& substitutions) { if (str == nullptr) return 0; @@ -690,7 +723,7 @@ size_t ConfigBase::load_from_gcode_string(const char* str) if (key == nullptr) break; try { - this->set_deserialize(std::string(key, key_end), std::string(value, end)); + this->set_deserialize(std::string(key, key_end), std::string(value, end), substitutions); ++num_key_value_pairs; } catch (UnknownOptionException & /* e */) { @@ -719,7 +752,7 @@ void ConfigBase::null_nullables() ConfigOption *opt = this->optptr(opt_key, false); assert(opt != nullptr); if (opt->nullable()) - opt->deserialize("nil"); + opt->deserialize("nil", ForwardCompatibilitySubstitutionRule::Disable); } } @@ -883,8 +916,10 @@ bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option // Do not unescape single string values, the unescaping is left to the calling shell. static_cast(opt_base)->value = value; } else { + // Just bail out if the configuration value is not understood. + ConfigSubstitutionContext context(ForwardCompatibilitySubstitutionRule::Disable); // Any scalar value of a type different from Bool and String. - if (! this->set_deserialize_nothrow(opt_key, value, false)) { + if (! this->set_deserialize_nothrow(opt_key, value, context, false)) { boost::nowide::cerr << "Invalid value supplied for --" << token.c_str() << std::endl; return false; } diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 14b4b7c2e..bf356dfe6 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -16,6 +16,7 @@ #include "Exception.hpp" #include "Point.hpp" +#include #include #include #include @@ -163,6 +164,41 @@ enum PrinterTechnology : unsigned char ptAny }; +enum ForwardCompatibilitySubstitutionRule +{ + Disable, + Enable, + EnableSilent, +}; + +class ConfigOption; +class ConfigOptionDef; +// For forward definition of ConfigOption in ConfigOptionUniquePtr, we have to define a custom deleter. +struct ConfigOptionDeleter { void operator()(ConfigOption* p); }; +using ConfigOptionUniquePtr = std::unique_ptr; + +// When parsing a configuration value, if the old_value is not understood by this PrusaSlicer version, +// it is being substituted with some default value that this PrusaSlicer could work with. +// This structure serves to inform the user about the substitutions having been done during file import. +struct ConfigSubstitution { + const ConfigOptionDef *opt_def { nullptr }; + std::string old_value; + ConfigOptionUniquePtr new_value; +}; + +using ConfigSubstitutions = std::vector; + +// Filled in by ConfigBase::set_deserialize_raw(), which based on "rule" either bails out +// or performs substitutions when encountering an unknown configuration value. +struct ConfigSubstitutionContext +{ + ConfigSubstitutionContext(ForwardCompatibilitySubstitutionRule rl) : rule(rl) {} + bool empty() const throw() { return substitutions.empty(); } + + ForwardCompatibilitySubstitutionRule rule; + ConfigSubstitutions substitutions; +}; + // A generic value of a configuration option. class ConfigOption { public: @@ -768,7 +804,7 @@ public: return escape_string_cstyle(this->value); } - bool deserialize(const std::string &str, bool append = false) override + bool deserialize(const std::string &str, bool append = false) override { UNUSED(append); return unescape_string_cstyle(str, this->value); @@ -1272,8 +1308,15 @@ public: bool deserialize(const std::string &str, bool append = false) override { UNUSED(append); - this->value = (str.compare("1") == 0); - return true; + if (str == "1" || boost::iequals(str, "enabled") || boost::iequals(str, "on")) { + this->value = true; + return true; + } + if (str == "0" || boost::iequals(str, "disabled") || boost::iequals(str, "off")) { + this->value = false; + return true; + } + return false; } private: @@ -1687,6 +1730,14 @@ public: static const constexpr char *nocli = "~~~noCLI"; }; +inline bool operator<(const ConfigSubstitution &lhs, const ConfigSubstitution &rhs) throw() { + return lhs.opt_def->opt_key < rhs.opt_def->opt_key || + (lhs.opt_def->opt_key == rhs.opt_def->opt_key && lhs.old_value < rhs.old_value); +} +inline bool operator==(const ConfigSubstitution &lhs, const ConfigSubstitution &rhs) throw() { + return lhs.opt_def == rhs.opt_def && lhs.old_value == rhs.old_value; +} + // Map from a config option name to its definition. // The definition does not carry an actual value of the config option, only its constant default value. // t_config_option_key is std::string @@ -1765,6 +1816,8 @@ public: } }; + + // An abstract configuration store. class ConfigBase : public ConfigOptionResolver { @@ -1853,9 +1906,11 @@ public: // Set a configuration value from a string, it will call an overridable handle_legacy() // to resolve renamed and removed configuration keys. - bool set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, bool append = false); + bool set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, ConfigSubstitutionContext& substitutions, bool append = false); // May throw BadOptionTypeException() if the operation fails. - void set_deserialize(const t_config_option_key &opt_key, const std::string &str, bool append = false); + void set_deserialize(const t_config_option_key &opt_key, const std::string &str, ConfigSubstitutionContext& config_substitutions, bool append = false); + void set_deserialize_strict(const t_config_option_key &opt_key, const std::string &str, bool append = false) + { ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; this->set_deserialize(opt_key, str, ctxt, append); } struct SetDeserializeItem { SetDeserializeItem(const char *opt_key, const char *opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {} SetDeserializeItem(const std::string &opt_key, const std::string &opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {} @@ -1870,17 +1925,19 @@ public: std::string opt_key; std::string opt_value; bool append = false; }; // May throw BadOptionTypeException() if the operation fails. - void set_deserialize(std::initializer_list items); + void set_deserialize(std::initializer_list items, ConfigSubstitutionContext& substitutions); + void set_deserialize_strict(std::initializer_list items) + { ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; this->set_deserialize(items, ctxt); } double get_abs_value(const t_config_option_key &opt_key) const; double get_abs_value(const t_config_option_key &opt_key, double ratio_over) const; void setenv_() const; - void load(const std::string &file); - void load_from_ini(const std::string &file); - void load_from_gcode_file(const std::string& file, bool check_header = true); + ConfigSubstitutions load(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule); + ConfigSubstitutions load_from_ini(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule); + ConfigSubstitutions load_from_gcode_file(const std::string &file, bool check_header /* = true */, ForwardCompatibilitySubstitutionRule compatibility_rule); // Returns number of key/value pairs extracted. - size_t load_from_gcode_string(const char* str); - void load(const boost::property_tree::ptree &tree); + size_t load_from_gcode_string(const char* str, ConfigSubstitutionContext& substitutions); + ConfigSubstitutions load(const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule); void save(const std::string &file) const; // Set all the nullable values to nils. @@ -1888,7 +1945,7 @@ public: private: // Set a configuration value from a string. - bool set_deserialize_raw(const t_config_option_key &opt_key_src, const std::string &str, bool append); + bool set_deserialize_raw(const t_config_option_key& opt_key_src, const std::string& value, ConfigSubstitutionContext& substitutions, bool append); }; // Configuration store with dynamic number of configuration values. diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index fbf27c548..c2ba011a8 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -419,7 +419,7 @@ namespace Slic3r { _3MF_Importer(); ~_3MF_Importer(); - bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, bool check_version); + bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version); private: void _destroy_xml_parser(); @@ -434,16 +434,16 @@ namespace Slic3r { XML_ErrorString(XML_GetErrorCode(m_xml_parser)); } - bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config); + bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); void _extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_sla_drain_holes_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_custom_gcode_per_print_z_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, const std::string& archive_filename); + void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename); bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); // handlers to parse the .model file @@ -510,7 +510,7 @@ namespace Slic3r { bool _handle_start_config_metadata(const char** attributes, unsigned int num_attributes); bool _handle_end_config_metadata(); - bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes); + bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); // callbacks to parse the .model file static void XMLCALL _handle_start_model_xml_element(void* userData, const char* name, const char** attributes); @@ -539,7 +539,7 @@ namespace Slic3r { _destroy_xml_parser(); } - bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, bool check_version) + bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version) { m_version = 0; m_check_version = check_version; @@ -560,7 +560,7 @@ namespace Slic3r { m_curr_characters.clear(); clear_errors(); - return _load_model_from_file(filename, model, config); + return _load_model_from_file(filename, model, config, config_substitutions); } void _3MF_Importer::_destroy_xml_parser() @@ -581,7 +581,7 @@ namespace Slic3r { XML_StopParser(m_xml_parser, false); } - bool _3MF_Importer::_load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config) + bool _3MF_Importer::_load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions) { mz_zip_archive archive; mz_zip_zero_struct(&archive); @@ -635,7 +635,7 @@ namespace Slic3r { } else if (boost::algorithm::iequals(name, LAYER_CONFIG_RANGES_FILE)) { // extract slic3r layer config ranges file - _extract_layer_config_ranges_from_archive(archive, stat); + _extract_layer_config_ranges_from_archive(archive, stat, config_substitutions); } else if (boost::algorithm::iequals(name, SLA_SUPPORT_POINTS_FILE)) { // extract sla support points file @@ -647,7 +647,7 @@ namespace Slic3r { } else if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { // extract slic3r print config file - _extract_print_config_from_archive(archive, stat, config, filename); + _extract_print_config_from_archive(archive, stat, config, config_substitutions, filename); } else if (boost::algorithm::iequals(name, CUSTOM_GCODE_PER_PRINT_Z_FILE)) { // extract slic3r layer config ranges file @@ -704,7 +704,7 @@ namespace Slic3r { new_model_object->clear_instances(); new_model_object->add_instance(*model_object->instances.back()); model_object->delete_last_instance(); - if (!_generate_volumes(*new_model_object, *geometry, volumes)) + if (!_generate_volumes(*new_model_object, *geometry, volumes, config_substitutions)) return false; } } @@ -759,7 +759,7 @@ namespace Slic3r { if (metadata.key == "name") model_object->name = metadata.value; else - model_object->config.set_deserialize(metadata.key, metadata.value); + model_object->config.set_deserialize(metadata.key, metadata.value, config_substitutions); } // select object's detected volumes @@ -775,7 +775,7 @@ namespace Slic3r { volumes_ptr = &volumes; } - if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr)) + if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr, config_substitutions)) return false; } @@ -867,7 +867,10 @@ namespace Slic3r { return true; } - void _3MF_Importer::_extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, const std::string& archive_filename) + void _3MF_Importer::_extract_print_config_from_archive( + mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, + DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, + const std::string& archive_filename) { if (stat.m_uncomp_size > 0) { std::string buffer((size_t)stat.m_uncomp_size, 0); @@ -876,7 +879,7 @@ namespace Slic3r { add_error("Error while reading config data to buffer"); return; } - config.load_from_gcode_string(buffer.data()); + config.load_from_gcode_string(buffer.data(), config_substitutions); } } @@ -942,7 +945,7 @@ namespace Slic3r { } } - void _3MF_Importer::_extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) + void _3MF_Importer::_extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) { if (stat.m_uncomp_size > 0) { std::string buffer((size_t)stat.m_uncomp_size, 0); @@ -987,8 +990,7 @@ namespace Slic3r { continue; std::string opt_key = option.second.get(".opt_key"); std::string value = option.second.data(); - - config.set_deserialize(opt_key, value); + config.set_deserialize(opt_key, value, config_substitutions); } config_ranges[{ min_z, max_z }].assign_config(std::move(config)); @@ -1827,7 +1829,7 @@ namespace Slic3r { return true; } - bool _3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes) + bool _3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions) { if (!object.volumes.empty()) { add_error("Found invalid volumes count"); @@ -1943,7 +1945,7 @@ namespace Slic3r { else if (metadata.key == SOURCE_IN_METERS) volume->source.is_converted_from_meters = metadata.value == "1"; else - volume->config.set_deserialize(metadata.key, metadata.value); + volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); } } @@ -2953,16 +2955,15 @@ bool _3MF_Exporter::_add_custom_gcode_per_print_z_file_to_archive( mz_zip_archiv return true; } -bool load_3mf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version) +bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version) { - if (path == nullptr || config == nullptr || model == nullptr) + if (path == nullptr || model == nullptr) return false; // All import should use "C" locales for number formatting. CNumericLocalesSetter locales_setter; - - _3MF_Importer importer; - bool res = importer.load_model_from_file(path, *model, *config, check_version); + _3MF_Importer importer; + bool res = importer.load_model_from_file(path, *model, config, config_substitutions, check_version); importer.log_errors(); return res; } diff --git a/src/libslic3r/Format/3mf.hpp b/src/libslic3r/Format/3mf.hpp index a09a1b834..b91e90da7 100644 --- a/src/libslic3r/Format/3mf.hpp +++ b/src/libslic3r/Format/3mf.hpp @@ -25,11 +25,12 @@ namespace Slic3r { }; class Model; + struct ConfigSubstitutionContext; class DynamicPrintConfig; struct ThumbnailData; // Load the content of a 3mf file into the given model and preset bundle. - extern bool load_3mf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version); + extern bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version); // Save the given model and the config data contained in the given Print into a 3mf file. // The model could be modified during the export process if meshes are not repaired or have no shared vertices diff --git a/src/libslic3r/Format/AMF.cpp b/src/libslic3r/Format/AMF.cpp index 94318a930..d03cfd4fa 100644 --- a/src/libslic3r/Format/AMF.cpp +++ b/src/libslic3r/Format/AMF.cpp @@ -64,10 +64,11 @@ namespace Slic3r struct AMFParserContext { - AMFParserContext(XML_Parser parser, DynamicPrintConfig* config, Model* model) : + AMFParserContext(XML_Parser parser, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model) : m_parser(parser), m_model(*model), - m_config(config) + m_config(config), + m_config_substitutions(config_substitutions) { m_path.reserve(12); } @@ -258,6 +259,8 @@ struct AMFParserContext std::string m_value[5]; // Pointer to config to update if config data are stored inside the amf file DynamicPrintConfig *m_config { nullptr }; + // Config substitution rules and collected config substitution log. + ConfigSubstitutionContext *m_config_substitutions { nullptr }; private: AMFParserContext& operator=(AMFParserContext&); @@ -702,8 +705,9 @@ void AMFParserContext::endElement(const char * /* name */) } case NODE_TYPE_METADATA: - if ((m_config != nullptr) && strncmp(m_value[0].c_str(), SLIC3R_CONFIG_TYPE, strlen(SLIC3R_CONFIG_TYPE)) == 0) - m_config->load_from_gcode_string(m_value[1].c_str()); + if ((m_config != nullptr) && strncmp(m_value[0].c_str(), SLIC3R_CONFIG_TYPE, strlen(SLIC3R_CONFIG_TYPE)) == 0) { + m_config->load_from_gcode_string(m_value[1].c_str(), *m_config_substitutions); + } else if (strncmp(m_value[0].c_str(), "slic3r.", 7) == 0) { const char *opt_key = m_value[0].c_str() + 7; if (print_config_def.options.find(opt_key) != print_config_def.options.end()) { @@ -721,7 +725,7 @@ void AMFParserContext::endElement(const char * /* name */) config = &it->second; } if (config) - config->set_deserialize(opt_key, m_value[1]); + config->set_deserialize(opt_key, m_value[1], *m_config_substitutions); } else if (m_path.size() == 3 && m_path[1] == NODE_TYPE_OBJECT && m_object && strcmp(opt_key, "layer_height_profile") == 0) { // Parse object's layer height profile, a semicolon separated list of floats. char *p = m_value[1].data(); @@ -849,7 +853,7 @@ void AMFParserContext::endDocument() } // Load an AMF file into a provided model. -bool load_amf_file(const char *path, DynamicPrintConfig *config, Model *model) +bool load_amf_file(const char *path, DynamicPrintConfig *config, ConfigSubstitutionContext *config_substitutions, Model *model) { if ((path == nullptr) || (model == nullptr)) return false; @@ -866,7 +870,7 @@ bool load_amf_file(const char *path, DynamicPrintConfig *config, Model *model) return false; } - AMFParserContext ctx(parser, config, model); + AMFParserContext ctx(parser, config, config_substitutions, model); XML_SetUserData(parser, (void*)&ctx); XML_SetElementHandler(parser, AMFParserContext::startElement, AMFParserContext::endElement); XML_SetCharacterDataHandler(parser, AMFParserContext::characters); @@ -908,7 +912,7 @@ bool load_amf_file(const char *path, DynamicPrintConfig *config, Model *model) return result; } -bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig* config, Model* model, bool check_version) +bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version) { if (stat.m_uncomp_size == 0) { @@ -924,7 +928,7 @@ bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_fi return false; } - AMFParserContext ctx(parser, config, model); + AMFParserContext ctx(parser, config, config_substitutions, model); XML_SetUserData(parser, (void*)&ctx); XML_SetElementHandler(parser, AMFParserContext::startElement, AMFParserContext::endElement); XML_SetCharacterDataHandler(parser, AMFParserContext::characters); @@ -984,7 +988,7 @@ bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_fi } // Load an AMF archive into a provided model. -bool load_amf_archive(const char* path, DynamicPrintConfig* config, Model* model, bool check_version) +bool load_amf_archive(const char* path, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version) { if ((path == nullptr) || (model == nullptr)) return false; @@ -1010,7 +1014,7 @@ bool load_amf_archive(const char* path, DynamicPrintConfig* config, Model* model { try { - if (!extract_model_from_archive(archive, stat, config, model, check_version)) + if (!extract_model_from_archive(archive, stat, config, config_substitutions, model, check_version)) { close_zip_reader(&archive); BOOST_LOG_TRIVIAL(error) << "Archive does not contain a valid model"; @@ -1052,13 +1056,13 @@ bool load_amf_archive(const char* path, DynamicPrintConfig* config, Model* model // Load an AMF file into a provided model. // If config is not a null pointer, updates it if the amf file/archive contains config data -bool load_amf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version) +bool load_amf(const char* path, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version) { CNumericLocalesSetter locales_setter; // use "C" locales and point as a decimal separator if (boost::iends_with(path, ".amf.xml")) // backward compatibility with older slic3r output - return load_amf_file(path, config, model); + return load_amf_file(path, config, config_substitutions, model); else if (boost::iends_with(path, ".amf")) { boost::nowide::ifstream file(path, boost::nowide::ifstream::binary); @@ -1069,7 +1073,7 @@ bool load_amf(const char* path, DynamicPrintConfig* config, Model* model, bool c file.read(zip_mask.data(), 2); file.close(); - return (zip_mask == "PK") ? load_amf_archive(path, config, model, check_version) : load_amf_file(path, config, model); + return (zip_mask == "PK") ? load_amf_archive(path, config, config_substitutions, model, check_version) : load_amf_file(path, config, config_substitutions, model); } else return false; diff --git a/src/libslic3r/Format/AMF.hpp b/src/libslic3r/Format/AMF.hpp index b4e2c54ab..a073071fc 100644 --- a/src/libslic3r/Format/AMF.hpp +++ b/src/libslic3r/Format/AMF.hpp @@ -7,7 +7,7 @@ class Model; class DynamicPrintConfig; // Load the content of an amf file into the given model and configuration. -extern bool load_amf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version); +extern bool load_amf(const char* path, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version); // Save the given model and the config data into an amf file. // The model could be modified during the export process if meshes are not repaired or have no shared vertices diff --git a/src/libslic3r/Format/PRUS.cpp b/src/libslic3r/Format/PRUS.cpp index abf30a53b..586fbafb5 100644 --- a/src/libslic3r/Format/PRUS.cpp +++ b/src/libslic3r/Format/PRUS.cpp @@ -284,11 +284,8 @@ static void extract_model_from_archive( volume->name = name; } // Set the extruder to the volume. - if (extruder_id != (unsigned int)-1) { - char str_extruder[64]; - sprintf(str_extruder, "%ud", extruder_id); - volume->config.set_deserialize("extruder", str_extruder); - } + if (extruder_id != (unsigned int)-1) + volume->config.set("extruder", int(extruder_id)); } // Load a PrusaControl project file into a provided model. diff --git a/src/libslic3r/Format/SL1.cpp b/src/libslic3r/Format/SL1.cpp index f556b0ead..6d779b94e 100644 --- a/src/libslic3r/Format/SL1.cpp +++ b/src/libslic3r/Format/SL1.cpp @@ -287,13 +287,13 @@ std::vector extract_slices_from_sla_archive( } // namespace -void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) +ConfigSubstitutions import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) { ArchiveData arch = extract_sla_archive(zipfname, "png"); - out.load(arch.profile); + return out.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); } -void import_sla_archive( +ConfigSubstitutions import_sla_archive( const std::string & zipfname, Vec2i windowsize, indexed_triangle_set & out, @@ -305,7 +305,7 @@ void import_sla_archive( windowsize.y() = std::max(2, windowsize.y()); ArchiveData arch = extract_sla_archive(zipfname, "thumbnail"); - profile.load(arch.profile); + ConfigSubstitutions config_substitutions = profile.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); RasterParams rstp = get_raster_params(profile); rstp.win = {windowsize.y(), windowsize.x()}; @@ -317,6 +317,8 @@ void import_sla_archive( if (!slices.empty()) out = slices_to_mesh(slices, 0, slicp.layerh, slicp.initial_layerh); + + return config_substitutions; } using ConfMap = std::map; diff --git a/src/libslic3r/Format/SL1.hpp b/src/libslic3r/Format/SL1.hpp index a3e6ce26c..2c7e1edc1 100644 --- a/src/libslic3r/Format/SL1.hpp +++ b/src/libslic3r/Format/SL1.hpp @@ -38,23 +38,23 @@ public: } }; -void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out); +ConfigSubstitutions import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out); -void import_sla_archive( +ConfigSubstitutions import_sla_archive( const std::string & zipfname, Vec2i windowsize, indexed_triangle_set & out, DynamicPrintConfig & profile, std::function progr = [](int) { return true; }); -inline void import_sla_archive( +inline ConfigSubstitutions import_sla_archive( const std::string & zipfname, Vec2i windowsize, indexed_triangle_set & out, std::function progr = [](int) { return true; }) { DynamicPrintConfig profile; - import_sla_archive(zipfname, windowsize, out, profile, progr); + return import_sla_archive(zipfname, windowsize, out, profile, progr); } } // namespace Slic3r::sla diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index a3f2f2219..65679f120 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1285,7 +1285,10 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr if (m_producer == EProducer::PrusaSlicer || m_producer == EProducer::Slic3rPE || m_producer == EProducer::Slic3r) { DynamicPrintConfig config; config.apply(FullPrintConfig::defaults()); - config.load_from_gcode_file(filename, false); + // Silently substitute unknown values by new ones for loading configurations from PrusaSlicer's own G-code. + // Showing substitution log or errors may make sense, but we are not really reading many values from the G-code config, + // thus a probability of incorrect substitution is low and the G-code viewer is a consumer-only anyways. + config.load_from_gcode_file(filename, false, ForwardCompatibilitySubstitutionRule::EnableSilent); apply_config(config); } else if (m_producer == EProducer::Simplify3D) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 84566f4b1..5943f960e 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -96,13 +96,17 @@ void Model::update_links_bottom_up_recursive() } } -Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* config, bool add_default_instances, bool check_version) +// Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well. +Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, LoadAttributes options) { Model model; DynamicPrintConfig temp_config; + ConfigSubstitutionContext temp_config_substitutions_context(ForwardCompatibilitySubstitutionRule::EnableSilent); if (config == nullptr) config = &temp_config; + if (config_substitutions == nullptr) + config_substitutions = &temp_config_substitutions_context; bool result = false; if (boost::algorithm::iends_with(input_file, ".stl")) @@ -110,9 +114,10 @@ Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* c else if (boost::algorithm::iends_with(input_file, ".obj")) result = load_obj(input_file.c_str(), &model); else if (boost::algorithm::iends_with(input_file, ".amf") || boost::algorithm::iends_with(input_file, ".amf.xml")) - result = load_amf(input_file.c_str(), config, &model, check_version); + result = load_amf(input_file.c_str(), config, config_substitutions, &model, options & LoadAttribute::CheckVersion); else if (boost::algorithm::iends_with(input_file, ".3mf")) - result = load_3mf(input_file.c_str(), config, &model, false); + //FIXME options & LoadAttribute::CheckVersion ? + result = load_3mf(input_file.c_str(), *config, *config_substitutions, &model, false); else if (boost::algorithm::iends_with(input_file, ".prusa")) result = load_prus(input_file.c_str(), &model); else @@ -127,24 +132,29 @@ Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* c for (ModelObject *o : model.objects) o->input_file = input_file; - if (add_default_instances) + if (options & LoadAttribute::AddDefaultInstances) model.add_default_instances(); CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, config); CustomGCode::check_mode_for_custom_gcode_per_print_z(model.custom_gcode_per_print_z); + sort_remove_duplicates(config_substitutions->substitutions); return model; } -Model Model::read_from_archive(const std::string& input_file, DynamicPrintConfig* config, bool add_default_instances, bool check_version) +// Loading model from a file (3MF or AMF), not from a simple geometry file (STL or OBJ). +Model Model::read_from_archive(const std::string& input_file, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, LoadAttributes options) { + assert(config != nullptr); + assert(config_substitutions != nullptr); + Model model; bool result = false; if (boost::algorithm::iends_with(input_file, ".3mf")) - result = load_3mf(input_file.c_str(), config, &model, check_version); + result = load_3mf(input_file.c_str(), *config, *config_substitutions, &model, options & LoadAttribute::CheckVersion); else if (boost::algorithm::iends_with(input_file, ".zip.amf")) - result = load_amf(input_file.c_str(), config, &model, check_version); + result = load_amf(input_file.c_str(), config, config_substitutions, &model, options & LoadAttribute::CheckVersion); else throw Slic3r::RuntimeError("Unknown file format. Input file must have .3mf or .zip.amf extension."); @@ -165,7 +175,7 @@ Model Model::read_from_archive(const std::string& input_file, DynamicPrintConfig o->input_file = input_file; } - if (add_default_instances) + if (options & LoadAttribute::AddDefaultInstances) model.add_default_instances(); CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, config); @@ -398,13 +408,12 @@ bool Model::looks_like_multipart_object() const } // Generate next extruder ID string, in the range of (1, max_extruders). -static inline std::string auto_extruder_id(unsigned int max_extruders, unsigned int &cntr) +static inline int auto_extruder_id(unsigned int max_extruders, unsigned int &cntr) { - char str_extruder[64]; - sprintf(str_extruder, "%ud", cntr + 1); - if (++ cntr == max_extruders) + int out = ++ cntr; + if (cntr == max_extruders) cntr = 0; - return str_extruder; + return out; } void Model::convert_multipart_object(unsigned int max_extruders) @@ -431,7 +440,7 @@ void Model::convert_multipart_object(unsigned int max_extruders) auto copy_volume = [o, max_extruders, &counter, &extruder_counter](ModelVolume *new_v) { assert(new_v != nullptr); new_v->name = o->name + "_" + std::to_string(counter++); - new_v->config.set_deserialize("extruder", auto_extruder_id(max_extruders, extruder_counter)); + new_v->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); return new_v; }; if (o->instances.empty()) { @@ -1738,7 +1747,7 @@ size_t ModelVolume::split(unsigned int max_extruders) this->object->volumes[ivolume]->center_geometry_after_creation(); this->object->volumes[ivolume]->translate(offset); this->object->volumes[ivolume]->name = name + "_" + std::to_string(idx + 1); - this->object->volumes[ivolume]->config.set_deserialize("extruder", auto_extruder_id(max_extruders, extruder_counter)); + this->object->volumes[ivolume]->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); this->object->volumes[ivolume]->m_is_splittable = 0; delete mesh; ++ idx; diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index c6a54d5c6..f835aa0ea 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -12,6 +12,7 @@ #include "TriangleMesh.hpp" #include "Arrange.hpp" #include "CustomGCode.hpp" +#include "enum_bitmask.hpp" #include #include @@ -1031,8 +1032,20 @@ public: OBJECTBASE_DERIVED_COPY_MOVE_CLONE(Model) - static Model read_from_file(const std::string& input_file, DynamicPrintConfig* config = nullptr, bool add_default_instances = true, bool check_version = false); - static Model read_from_archive(const std::string& input_file, DynamicPrintConfig* config, bool add_default_instances = true, bool check_version = false); + enum class LoadAttribute : int { + AddDefaultInstances, + CheckVersion + }; + using LoadAttributes = enum_bitmask; + + static Model read_from_file( + const std::string& input_file, + DynamicPrintConfig* config = nullptr, ConfigSubstitutionContext* config_substitutions = nullptr, + LoadAttributes options = LoadAttribute::AddDefaultInstances); + static Model read_from_archive( + const std::string& input_file, + DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, + LoadAttributes options = LoadAttribute::AddDefaultInstances); // Add a new ModelObject to this Model, generate a new ID for this ModelObject. ModelObject* add_object(); @@ -1097,6 +1110,8 @@ private: } }; +ENABLE_ENUM_BITMASK_OPERATORS(Model::LoadAttribute) + #undef OBJECTBASE_DERIVED_COPY_MOVE_CLONE #undef OBJECTBASE_DERIVED_PRIVATE_COPY_MOVE diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index df088935f..97457d63b 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -666,7 +666,9 @@ void PresetCollection::add_default_preset(const std::vector &keys, // Load all presets found in dir_path. // Throws an exception on error. -void PresetCollection::load_presets(const std::string &dir_path, const std::string &subdir) +void PresetCollection::load_presets( + const std::string &dir_path, const std::string &subdir, + PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule substitution_rule) { // Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points, // see https://github.com/prusa3d/PrusaSlicer/issues/732 @@ -693,7 +695,9 @@ void PresetCollection::load_presets(const std::string &dir_path, const std::stri // Load the preset file, apply preset values on top of defaults. try { DynamicPrintConfig config; - config.load_from_ini(preset.file); + ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); + if (! config_substitutions.empty()) + substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) }); // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. const Preset &default_preset = this->default_preset_for(config); preset.config = default_preset.config; @@ -1580,7 +1584,9 @@ PhysicalPrinterCollection::PhysicalPrinterCollection( const std::vector; + // Collections of presets of the same type (one of the Print, Filament or Printer type). class PresetCollection { @@ -280,7 +304,7 @@ public: void add_default_preset(const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name); // Load ini files of the particular type from the provided directory path. - void load_presets(const std::string &dir_path, const std::string &subdir); + void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule); // Load a preset from an already parsed config file, insert it into the sorted sequence of presets // and select it, losing previous modifications. @@ -692,7 +716,7 @@ public: const std::deque& operator()() const { return m_printers; } // Load ini files of the particular type from the provided directory path. - void load_printers(const std::string& dir_path, const std::string& subdir); + void load_printers(const std::string& dir_path, const std::string& subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule); void load_printers_from_presets(PrinterPresetCollection &printer_presets); // Load printer from the loaded configuration void load_printer(const std::string& path, const std::string& name, DynamicPrintConfig&& config, bool select, bool save=false); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index e7f818d08..fc7987b29 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -187,7 +187,7 @@ void PresetBundle::setup_directories() } } -void PresetBundle::load_presets(AppConfig &config, const std::string &preferred_model_id) +PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule substitution_rule, const std::string &preferred_model_id) { // First load the vendor specific system presets. std::string errors_cummulative = this->load_system_presets(); @@ -200,33 +200,35 @@ void PresetBundle::load_presets(AppConfig &config, const std::string &preferred_ // Store the print/filament/printer presets at the same location as the upstream Slic3r. #endif ; + + PresetsConfigSubstitutions substitutions; try { - this->prints.load_presets(dir_user_presets, "print"); + this->prints.load_presets(dir_user_presets, "print", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->sla_prints.load_presets(dir_user_presets, "sla_print"); + this->sla_prints.load_presets(dir_user_presets, "sla_print", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->filaments.load_presets(dir_user_presets, "filament"); + this->filaments.load_presets(dir_user_presets, "filament", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->sla_materials.load_presets(dir_user_presets, "sla_material"); + this->sla_materials.load_presets(dir_user_presets, "sla_material", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->printers.load_presets(dir_user_presets, "printer"); + this->printers.load_presets(dir_user_presets, "printer", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { - this->physical_printers.load_printers(dir_user_presets, "physical_printer"); + this->physical_printers.load_printers(dir_user_presets, "physical_printer", substitutions, substitution_rule); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } @@ -236,6 +238,8 @@ void PresetBundle::load_presets(AppConfig &config, const std::string &preferred_ throw Slic3r::RuntimeError(errors_cummulative); this->load_selections(config, preferred_model_id); + + return substitutions; } // Load system presets into this PresetBundle. @@ -255,13 +259,13 @@ std::string PresetBundle::load_system_presets() // Load the config bundle, flatten it. if (first) { // Reset this PresetBundle and load the first vendor config. - this->load_configbundle(dir_entry.path().string(), LOAD_CFGBNDLE_SYSTEM); + this->load_configbundle(dir_entry.path().string(), PresetBundle::LoadSystem); first = false; } else { // Load the other vendor configs, merge them with this PresetBundle. // Report duplicate profiles. PresetBundle other; - other.load_configbundle(dir_entry.path().string(), LOAD_CFGBNDLE_SYSTEM); + other.load_configbundle(dir_entry.path().string(), PresetBundle::LoadSystem); std::vector duplicates = this->merge_presets(std::move(other)); if (! duplicates.empty()) { errors_cummulative += "Vendor configuration file " + name + " contains the following presets with names used by other vendors: "; @@ -690,15 +694,15 @@ DynamicPrintConfig PresetBundle::full_sla_config() const // Instead of a config file, a G-code may be loaded containing the full set of parameters. // In the future the configuration will likely be read from an AMF file as well. // If the file is loaded successfully, its print / filament / printer profiles will be activated. -void PresetBundle::load_config_file(const std::string &path) +ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule) { if (is_gcode_file(path)) { DynamicPrintConfig config; config.apply(FullPrintConfig::defaults()); - config.load_from_gcode_file(path); + ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, true /* check_header */, compatibility_rule); Preset::normalize(config); load_config_file_config(path, true, std::move(config)); - return; + return config_substitutions; } // 1) Try to load the config file into a boost property tree. @@ -717,6 +721,7 @@ void PresetBundle::load_config_file(const std::string &path) // 2) Continue based on the type of the configuration file. ConfigFileType config_file_type = guess_config_file_type(tree); + ConfigSubstitutions config_substitutions; switch (config_file_type) { case CONFIG_FILE_TYPE_UNKNOWN: throw Slic3r::RuntimeError(std::string("Unknown configuration file type: ") + path); @@ -727,15 +732,18 @@ void PresetBundle::load_config_file(const std::string &path) // Initialize a config from full defaults. DynamicPrintConfig config; config.apply(FullPrintConfig::defaults()); - config.load(tree); + config_substitutions = config.load(tree, compatibility_rule); Preset::normalize(config); load_config_file_config(path, true, std::move(config)); - break; - } - case CONFIG_FILE_TYPE_CONFIG_BUNDLE: - load_config_file_config_bundle(path, tree); - break; + return config_substitutions; } + case CONFIG_FILE_TYPE_CONFIG_BUNDLE: + return load_config_file_config_bundle(path, tree); + } + + // This shall never happen. Suppres compiler warnings. + assert(false); + return ConfigSubstitutions{}; } // Load a config file from a boost property_tree. This is a private method called from load_config_file. @@ -907,16 +915,24 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } // Load the active configuration of a config bundle from a boost property_tree. This is a private method called from load_config_file. -void PresetBundle::load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree) +ConfigSubstitutions PresetBundle::load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree) { // 1) Load the config bundle into a temp data. PresetBundle tmp_bundle; - // Load the config bundle, don't save the loaded presets to user profile directory. - tmp_bundle.load_configbundle(path, 0); + // Load the config bundle, but don't save the loaded presets to user profile directory, as only the presets marked as active in the loaded preset bundle + // will be loaded into the master PresetBundle and activated. + auto [presets_substitutions, presets_imported] = tmp_bundle.load_configbundle(path, {}); + std::string bundle_name = std::string(" - ") + boost::filesystem::path(path).filename().string(); // 2) Extract active configs from the config bundle, copy them and activate them in this bundle. - auto load_one = [&path, &bundle_name](PresetCollection &collection_dst, PresetCollection &collection_src, const std::string &preset_name_src, bool activate) -> std::string { + ConfigSubstitutions config_substitutions; + auto load_one = [this, &path, &bundle_name, &presets_substitutions = presets_substitutions, &config_substitutions]( + PresetCollection &collection_dst, PresetCollection &collection_src, const std::string &preset_name_src, bool activate) -> std::string { + // If there are substitutions reported for this preset, move them to config_substitutions. + if (auto it = std::find_if(presets_substitutions.begin(), presets_substitutions.end(), [&preset_name_src](const PresetConfigSubstitutions& subs){ return subs.preset_name == preset_name_src; }); + it != presets_substitutions.end() && ! it->substitutions.empty()) + append(config_substitutions, std::move(it->substitutions)); Preset *preset_src = collection_src.find_preset(preset_name_src, false); Preset *preset_dst = collection_dst.find_preset(preset_name_src, false); assert(preset_src != nullptr); @@ -970,6 +986,9 @@ void PresetBundle::load_config_file_config_bundle(const std::string &path, const this->filament_presets[i] = load_one(this->filaments, tmp_bundle.filaments, tmp_bundle.filament_presets[i], false); this->update_compatible(PresetSelectCompatibleType::Never); + + sort_remove_duplicates(config_substitutions); + return std::move(config_substitutions); } // Process the Config Bundle loaded as a Boost property tree. @@ -1114,11 +1133,20 @@ static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree, co // Load a config bundle file, into presets and store the loaded presets into separate files // of the local configuration directory. -size_t PresetBundle::load_configbundle(const std::string &path, unsigned int flags) +std::pair PresetBundle::load_configbundle(const std::string &path, LoadConfigBundleAttributes flags) { - if (flags & (LOAD_CFGBNDLE_RESET_USER_PROFILE | LOAD_CFGBNDLE_SYSTEM)) - // Reset this bundle, delete user profile files if LOAD_CFGBNDLE_SAVE. - this->reset(flags & LOAD_CFGBNDLE_SAVE); + // Enable substitutions for user config bundle, throw an exception when loading a system profile. + ConfigSubstitutionContext substitution_context { + flags.has(LoadConfigBundleAttribute::LoadSystem) ? + ForwardCompatibilitySubstitutionRule::Disable : + ForwardCompatibilitySubstitutionRule::Enable + }; + + PresetsConfigSubstitutions substitutions; + + if (flags.has(LoadConfigBundleAttribute::ResetUserProfile) || flags.has(LoadConfigBundleAttribute::LoadSystem)) + // Reset this bundle, delete user profile files if SaveImported. + this->reset(flags.has(LoadConfigBundleAttribute::SaveImported)); // 1) Read the complete config file into a boost::property_tree. namespace pt = boost::property_tree; @@ -1131,25 +1159,24 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla } const VendorProfile *vendor_profile = nullptr; - if (flags & (LOAD_CFGBNDLE_SYSTEM | LOAD_CFGBUNDLE_VENDOR_ONLY)) { + if (flags.has(LoadConfigBundleAttribute::LoadSystem) || flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) { auto vp = VendorProfile::from_ini(tree, path); if (vp.models.size() == 0) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer model defined.") % path; - return 0; + return std::make_pair(PresetsConfigSubstitutions{}, 0); } else if (vp.num_variants() == 0) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer variant defined") % path; - return 0; + return std::make_pair(PresetsConfigSubstitutions{}, 0); } vendor_profile = &this->vendors.insert({vp.id, vp}).first->second; } - if (flags & LOAD_CFGBUNDLE_VENDOR_ONLY) { - return 0; - } + if (flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) + return std::make_pair(PresetsConfigSubstitutions{}, 0); // 1.5) Flatten the config bundle by applying the inheritance rules. Internal profiles (with names starting with '*') are removed. // If loading a user config bundle, do not flatten with the system profiles, but keep the "inherits" flag intact. - flatten_configbundle_hierarchy(tree, ((flags & LOAD_CFGBNDLE_SYSTEM) == 0) ? this : nullptr); + flatten_configbundle_hierarchy(tree, flags.has(LoadConfigBundleAttribute::LoadSystem) ? nullptr : this); // 2) Parse the property_tree, extract the active preset names and the profiles, save them into local config files. // Parse the obsolete preset names, to be deleted when upgrading from the old configuration structure. @@ -1246,7 +1273,8 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla DynamicPrintConfig config; std::string alias_name; std::vector renamed_from; - auto parse_config_section = [§ion, &alias_name, &renamed_from, &path](DynamicPrintConfig &config) { + auto parse_config_section = [§ion, &alias_name, &renamed_from, &substitution_context, &path](DynamicPrintConfig &config) { + substitution_context.substitutions.clear(); for (auto &kvp : section.second) { if (kvp.first == "alias") alias_name = kvp.second.data(); @@ -1256,7 +1284,8 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla section.first << "\" contains invalid \"renamed_from\" key, which is being ignored."; } } - config.set_deserialize(kvp.first, kvp.second.data()); + // Throws on parsing error. For system presets, no substituion is being done, but an exception is thrown. + config.set_deserialize(kvp.first, kvp.second.data(), substitution_context); } }; if (presets == &this->printers) { @@ -1277,7 +1306,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla if (! incorrect_keys.empty()) BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << section.first << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; - if ((flags & LOAD_CFGBNDLE_SYSTEM) && presets == &printers) { + if (flags.has(LoadConfigBundleAttribute::LoadSystem) && presets == &printers) { // Filter out printer presets, which are not mentioned in the vendor profile. // These presets are considered not installed. auto printer_model = config.opt_string("printer_model"); @@ -1312,7 +1341,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla section.first << "\" has already been loaded from another Confing Bundle."; continue; } - } else if ((flags & LOAD_CFGBNDLE_SYSTEM) == 0) { + } else if (! flags.has(LoadConfigBundleAttribute::LoadSystem)) { // This is a user config bundle. const Preset *existing = presets->find_preset(preset_name, false); if (existing != nullptr) { @@ -1341,9 +1370,9 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla / presets->section_name() / file_name).make_preferred(); // Load the preset into the list of presets, save it to disk. Preset &loaded = presets->load_preset(file_path.string(), preset_name, std::move(config), false); - if (flags & LOAD_CFGBNDLE_SAVE) + if (flags.has(LoadConfigBundleAttribute::SaveImported)) loaded.save(); - if (flags & LOAD_CFGBNDLE_SYSTEM) { + if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { loaded.is_system = true; loaded.vendor = vendor_profile; } @@ -1364,7 +1393,10 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla else loaded.alias = std::move(alias_name); loaded.renamed_from = std::move(renamed_from); - + if (! substitution_context.empty()) + substitutions.push_back({ + preset_name, presets->type(), PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); ++ presets_loaded; } @@ -1373,8 +1405,9 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla const DynamicPrintConfig& default_config = ph_printers->default_config(); DynamicPrintConfig config = default_config; + substitution_context.substitutions.clear(); for (auto& kvp : section.second) - config.set_deserialize(kvp.first, kvp.second.data()); + config.set_deserialize(kvp.first, kvp.second.data(), substitution_context); // Report configuration fields, which are misplaced into a wrong group. std::string incorrect_keys = Preset::remove_invalid_keys(config, default_config); @@ -1400,14 +1433,17 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla #endif / "physical_printer" / file_name).make_preferred(); // Load the preset into the list of presets, save it to disk. - ph_printers->load_printer(file_path.string(), ph_printer_name, std::move(config), false, flags & LOAD_CFGBNDLE_SAVE); - - ++ph_printers_loaded; + ph_printers->load_printer(file_path.string(), ph_printer_name, std::move(config), false, flags.has(LoadConfigBundleAttribute::SaveImported)); + if (! substitution_context.empty()) + substitutions.push_back({ + ph_printer_name, Preset::TYPE_PHYSICAL_PRINTER, PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); + ++ ph_printers_loaded; } } // 3) Activate the presets and physical printer if any exists. - if ((flags & LOAD_CFGBNDLE_SYSTEM) == 0) { + if (! flags.has(LoadConfigBundleAttribute::LoadSystem)) { if (! active_print.empty()) prints.select_preset_by_name(active_print, true); if (! active_sla_print.empty()) @@ -1427,7 +1463,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla this->update_compatible(PresetSelectCompatibleType::Never); } - return presets_loaded + ph_printers_loaded; + return std::make_pair(std::move(substitutions), presets_loaded + ph_printers_loaded); } void PresetBundle::update_multi_material_filament_presets() diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 5902d4208..9f75ba6c2 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -3,6 +3,7 @@ #include "Preset.hpp" #include "AppConfig.hpp" +#include "enum_bitmask.hpp" #include #include @@ -26,7 +27,7 @@ public: // Load ini files of all types (print, filament, printer) from Slic3r::data_dir() / presets. // Load selections (current print, current filaments, current printer) from config.ini - void load_presets(AppConfig &config, const std::string &preferred_model_id = std::string()); + PresetsConfigSubstitutions load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule rule, const std::string &preferred_model_id = std::string()); // Export selections (current print, current filaments, current printer) into config.ini void export_selections(AppConfig &config); @@ -82,24 +83,26 @@ public: // Instead of a config file, a G-code may be loaded containing the full set of parameters. // In the future the configuration will likely be read from an AMF file as well. // If the file is loaded successfully, its print / filament / printer profiles will be activated. - void load_config_file(const std::string &path); + ConfigSubstitutions load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule); // Load a config bundle file, into presets and store the loaded presets into separate files // of the local configuration directory. // Load settings into the provided settings instance. // Activate the presets stored in the config bundle. // Returns the number of presets loaded successfully. - enum { + enum LoadConfigBundleAttribute { // Save the profiles, which have been loaded. - LOAD_CFGBNDLE_SAVE = 1, + SaveImported, // Delete all old config profiles before loading. - LOAD_CFGBNDLE_RESET_USER_PROFILE = 2, + ResetUserProfile, // Load a system config bundle. - LOAD_CFGBNDLE_SYSTEM = 4, - LOAD_CFGBUNDLE_VENDOR_ONLY = 8, + LoadSystem, + LoadVendorOnly, }; - // Load the config bundle, store it to the user profile directory by default. - size_t load_configbundle(const std::string &path, unsigned int flags = LOAD_CFGBNDLE_SAVE); + using LoadConfigBundleAttributes = enum_bitmask; + // Load the config bundle based on the flags. + // Don't do any config substitutions when loading a system profile, perform and report substitutions otherwise. + std::pair load_configbundle(const std::string &path, LoadConfigBundleAttributes flags); // Export a config bundle file containing all the presets and the names of the active presets. void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); @@ -155,12 +158,14 @@ private: // and the external config is just referenced, not stored into user profile directory. // If it is not an external config, then the config will be stored into the user profile directory. void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config); - void load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree); + ConfigSubstitutions load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree); DynamicPrintConfig full_fff_config() const; DynamicPrintConfig full_sla_config() const; }; +ENABLE_ENUM_BITMASK_OPERATORS(PresetBundle::LoadConfigBundleAttribute) + } // namespace Slic3r #endif /* slic3r_PresetBundle_hpp_ */ diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5cd33c1cb..d8fc3083c 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -172,6 +172,13 @@ static const t_config_enum_values s_keys_map_BrimType = { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BrimType) +static const t_config_enum_values s_keys_map_ForwardCompatibilitySubstitutionRule = { + { "disable", ForwardCompatibilitySubstitutionRule::Disable }, + { "enable", ForwardCompatibilitySubstitutionRule::Enable }, + { "enable_silent", ForwardCompatibilitySubstitutionRule::EnableSilent } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) + static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology) { for (std::pair &kvp : options) @@ -3622,7 +3629,7 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } else if (opt_key == "bed_size" && !value.empty()) { opt_key = "bed_shape"; ConfigOptionPoint p; - p.deserialize(value); + p.deserialize(value, ForwardCompatibilitySubstitutionRule::Disable); std::ostringstream oss; oss << "0x0," << p.value(0) << "x0," << p.value(0) << "x" << p.value(1) << ",0x" << p.value(1); value = oss.str(); @@ -4171,6 +4178,20 @@ CLIMiscConfigDef::CLIMiscConfigDef() def->label = L("Ignore non-existent config files"); def->tooltip = L("Do not fail if a file supplied to --load does not exist."); + def = this->add("config_compatibility", coEnum); + def->label = L("Forward-compatibility rule when loading configurations from config files and project files (3MF, AMF)."); + def->tooltip = L("This version of PrusaSlicer may not understand configurations produced by newest PrusaSlicer versions. " + "For example, newer PrusaSlicer may extend the list of supported firmware flavors. One may decide to " + "bail out or to substitute an unknown value with a default silently or verbosely."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("disable"); + def->enum_values.push_back("enable"); + def->enum_values.push_back("enable_silent"); + def->enum_labels.push_back(L("Bail out on unknown configuration values")); + def->enum_labels.push_back(L("Enable reading unknown configuration values by verbosely substituting them with defaults.")); + def->enum_labels.push_back(L("Enable reading unknown configuration values by silently substituting them with defaults.")); + def->set_default_value(new ConfigOptionEnum(ForwardCompatibilitySubstitutionRule::Enable)); + def = this->add("load", coStrings); def->label = L("Load config file"); def->tooltip = L("Load configuration from the specified file. It can be used more than once to load options from multiple files."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f2b9901d6..890f8518e 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -141,6 +141,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SeamPosition) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLADisplayOrientation) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLAPillarConnectionMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BrimType) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS @@ -1086,8 +1087,8 @@ public: bool set_key_value(const std::string &opt_key, ConfigOption *opt) { bool out = m_data.set_key_value(opt_key, opt); this->touch(); return out; } template void set(const std::string &opt_key, T value) { m_data.set(opt_key, value, true); this->touch(); } - void set_deserialize(const t_config_option_key &opt_key, const std::string &str, bool append = false) - { m_data.set_deserialize(opt_key, str, append); this->touch(); } + void set_deserialize(const t_config_option_key &opt_key, const std::string &str, ConfigSubstitutionContext &substitution_context, bool append = false) + { m_data.set_deserialize(opt_key, str, substitution_context, append); this->touch(); } bool erase(const t_config_option_key &opt_key) { bool out = m_data.erase(opt_key); if (out) this->touch(); return out; } // Getters are thread safe. diff --git a/src/libslic3r/enum_bitmask.hpp b/src/libslic3r/enum_bitmask.hpp new file mode 100644 index 000000000..4c2076313 --- /dev/null +++ b/src/libslic3r/enum_bitmask.hpp @@ -0,0 +1,80 @@ +#ifndef slic3r_enum_bitmask_hpp_ +#define slic3r_enum_bitmask_hpp_ + +// enum_bitmask for passing a set of attributes to a function in a type safe way. +// Adapted from https://gpfault.net/posts/typesafe-bitmasks.txt.html +// with hints from https://www.strikerx3.dev/cpp/2019/02/27/typesafe-enum-class-bitmasks-in-cpp.html + +#include + +namespace Slic3r { + +// enum_bitmasks can only be used with enums. +template::value>::type> +class enum_bitmask { + // The type we'll use for storing the value of our bitmask should be the same as the enum's underlying type. + using underlying_type = typename std::underlying_type::type; + + // This method helps us avoid having to explicitly set enum values to powers of two. + static constexpr underlying_type mask_value(option_type o) { return 1 << static_cast(o); } + + // Private ctor to be used internally. + explicit constexpr enum_bitmask(underlying_type o) : m_bits(o) {} + +public: + // Default ctor creates a bitmask with no options selected. + constexpr enum_bitmask() : m_bits(0) {} + + // Creates a enum_bitmask with just one bit set. + // This ctor is intentionally non-explicit, to allow passing an options to a function: + // FunctionExpectingBitmask(Options::Opt1) + constexpr enum_bitmask(option_type o) : m_bits(mask_value(o)) {} + + // Set the bit corresponding to the given option. + constexpr enum_bitmask operator|(option_type t) { return enum_bitmask(m_bits | mask_value(t)); } + + // Combine with another enum_bitmask of the same type. + constexpr enum_bitmask operator|(enum_bitmask t) { return enum_bitmask(m_bits | t.m_bits); } + + // Get the value of the bit corresponding to the given option. + constexpr bool operator&(option_type t) { return m_bits & mask_value(t); } + constexpr bool has(option_type t) { return m_bits & mask_value(t); } + +private: + underlying_type m_bits = 0; +}; + +// For enabling free functions producing enum_bitmask<> type from bit operations on enums. +template struct is_enum_bitmask_type { static const bool enable = false; }; +#define ENABLE_ENUM_BITMASK_OPERATORS(x) template<> struct is_enum_bitmask_type { static const bool enable = true; }; +template inline constexpr bool is_enum_bitmask_type_v = is_enum_bitmask_type::enable; + +// Creates an enum_bitmask from two options, convenient for passing of options to a function: +// FunctionExpectingBitmask(Options::Opt1 | Options::Opt2 | Options::Opt3) +template +constexpr std::enable_if_t, enum_bitmask> operator|(option_type lhs, option_type rhs) { + static_assert(std::is_enum_v); + return enum_bitmask{lhs} | rhs; +} + +template +constexpr std::enable_if_t, enum_bitmask> operator|(option_type lhs, enum_bitmask rhs) { + static_assert(std::is_enum_v); + return enum_bitmask{lhs} | rhs; +} + +template +constexpr std::enable_if_t, enum_bitmask> only_if(bool condition, option_type opt) { + static_assert(std::is_enum_v); + return condition ? enum_bitmask{opt} : enum_bitmask{}; +} + +template +constexpr std::enable_if_t, enum_bitmask> only_if(bool condition, enum_bitmask opt) { + static_assert(std::is_enum_v); + return condition ? opt : enum_bitmask{}; +} + +} // namespace Slic3r + +#endif // slic3r_enum_bitmask_hpp_ diff --git a/src/libslic3r/pchheader.hpp b/src/libslic3r/pchheader.hpp index ad22b855a..e6591f574 100644 --- a/src/libslic3r/pchheader.hpp +++ b/src/libslic3r/pchheader.hpp @@ -115,6 +115,7 @@ #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" +#include "enum_bitmask.hpp" #include "format.hpp" #include "I18N.hpp" #include "MultiPoint.hpp" diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index ecfc32b58..56722173b 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -413,7 +413,7 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: ++ it; // Read the active config bundle, parse the config version. PresetBundle bundle; - bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY); + bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly); for (const auto &vp : bundle.vendors) if (vp.second.id == cfg.name) cfg.version.config_version = vp.second.config_version; diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index bc66532a0..084af8b14 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -64,7 +64,9 @@ bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bu this->is_prusa_bundle = ais_prusa_bundle; std::string path_string = source_path.string(); - size_t presets_loaded = preset_bundle->load_configbundle(path_string, PresetBundle::LOAD_CFGBNDLE_SYSTEM); + auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle(path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem); + // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. + assert(config_substitutions.empty()); auto first_vendor = preset_bundle->vendors.begin(); if (first_vendor == preset_bundle->vendors.end()) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; @@ -2592,7 +2594,10 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese } } - preset_bundle->load_presets(*app_config, preferred_model); + // Reloading the configs after some modifications were done to PrusaSlicer.ini. + // Just perform the substitutions silently, as the substitutions were already presented to the user on application start-up + // and the Wizard shall not create any new values that would require substitution. + PresetsConfigSubstitutions substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, preferred_model); if (page_custom->custom_wanted()) { page_firmware->apply_custom_config(*custom_config); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 9cf71e07d..943124849 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -624,6 +624,13 @@ void GUI_App::post_init() this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); } else { + if (! this->init_params->preset_substitutions.empty()) { + // TODO: Add list of changes from all_substitutions + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } + #if 0 // Load the cummulative config over the currently active profiles. //FIXME if multiple configs are loaded, only the last one will have an effect. @@ -652,6 +659,24 @@ void GUI_App::post_init() if (! this->init_params->extra_config.empty()) this->mainframe->load_config(this->init_params->extra_config); } + + // The extra CallAfter() is needed because of Mac, where this is the only way + // to popup a modal dialog on start without screwing combo boxes. + // This is ugly but I honestly found no better way to do it. + // Neither wxShowEvent nor wxWindowCreateEvent work reliably. + if (this->preset_updater) { + this->check_updates(false); + CallAfter([this] { + this->config_wizard_startup(); + this->preset_updater->slic3r_update_notify(); + this->preset_updater->sync(preset_bundle); + }); + } + +#ifdef _WIN32 + // Sets window property to mainframe so other instances can indentify it. + OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); +#endif //WIN32 } IMPLEMENT_APP(GUI_App) @@ -885,7 +910,7 @@ bool GUI_App::on_init_inner() // Suppress the '- default -' presets. preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); try { - preset_bundle->load_presets(*app_config); + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); } catch (const std::exception &ex) { show_error(nullptr, ex.what()); } @@ -948,7 +973,6 @@ bool GUI_App::on_init_inner() if (! plater_) return; - if (app_config->dirty() && app_config->get("autosave") == "1") app_config->save(); @@ -969,33 +993,6 @@ bool GUI_App::on_init_inner() #endif this->post_init(); } - - // Preset updating & Configwizard are done after the above initializations, - // and after MainFrame is created & shown. - // The extra CallAfter() is needed because of Mac, where this is the only way - // to popup a modal dialog on start without screwing combo boxes. - // This is ugly but I honestly found no better way to do it. - // Neither wxShowEvent nor wxWindowCreateEvent work reliably. - - static bool once = true; - if (once) { - once = false; - - if (preset_updater != nullptr) { - check_updates(false); - - CallAfter([this] { - config_wizard_startup(); - preset_updater->slic3r_update_notify(); - preset_updater->sync(preset_bundle); - }); - } - -#ifdef _WIN32 - //sets window property to mainframe so other instances can indentify it - OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); -#endif //WIN32 - } }); m_initialized = true; @@ -1872,7 +1869,13 @@ void GUI_App::add_config_menu(wxMenuBar *menu) Config::SnapshotDB::singleton().take_snapshot(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK); try { app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); - preset_bundle->load_presets(*app_config); + if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); + ! all_substitutions.empty()) { + // TODO: + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } // Load the currently selected preset into the GUI, update the preset selection box. load_current_presets(); } catch (std::exception &ex) { diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 9579a030d..5cc8641ef 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -273,7 +273,7 @@ public: NotificationManager* notification_manager(); // Parameters extracted from the command line to be passed to GUI after initialization. - const GUI_InitParams* init_params { nullptr }; + GUI_InitParams* init_params { nullptr }; AppConfig* app_config{ nullptr }; PresetBundle* preset_bundle{ nullptr }; @@ -281,7 +281,7 @@ public: MainFrame* mainframe{ nullptr }; Plater* plater_{ nullptr }; - PresetUpdater* get_preset_updater() { return preset_updater; } + PresetUpdater* get_preset_updater() { return preset_updater; } wxBookCtrlBase* tab_panel() const ; int extruders_cnt() const; diff --git a/src/slic3r/GUI/GUI_Init.cpp b/src/slic3r/GUI/GUI_Init.cpp index 839782741..92223a767 100644 --- a/src/slic3r/GUI/GUI_Init.cpp +++ b/src/slic3r/GUI/GUI_Init.cpp @@ -50,39 +50,8 @@ int GUI_Run(GUI_InitParams ¶ms) // gui->autosave = m_config.opt_string("autosave"); GUI::GUI_App::SetInstance(gui); gui->init_params = ¶ms; -/* - gui->CallAfter([gui, this, &load_configs, params.start_as_gcodeviewer] { - if (!gui->initialized()) { - return; - } - if (params.start_as_gcodeviewer) { - if (!m_input_files.empty()) - gui->plater()->load_gcode(wxString::FromUTF8(m_input_files[0].c_str())); - } else { -#if 0 - // Load the cummulative config over the currently active profiles. - //FIXME if multiple configs are loaded, only the last one will have an effect. - // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). - // As of now only the full configs are supported here. - if (!m_print_config.empty()) - gui->mainframe->load_config(m_print_config); -#endif - if (!load_configs.empty()) - // Load the last config to give it a name at the UI. The name of the preset may be later - // changed by loading an AMF or 3MF. - //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. - gui->mainframe->load_config_file(load_configs.back()); - // If loading a 3MF file, the config is loaded from the last one. - if (!m_input_files.empty()) - gui->plater()->load_files(m_input_files, true, true); - if (!m_extra_config.empty()) - gui->mainframe->load_config(m_extra_config); - } - }); -*/ - int result = wxEntry(params.argc, params.argv); - return result; + return wxEntry(params.argc, params.argv); } catch (const Slic3r::Exception &ex) { boost::nowide::cerr << ex.what() << std::endl; wxMessageBox(boost::nowide::widen(ex.what()), _L("PrusaSlicer GUI initialization failed"), wxICON_STOP); diff --git a/src/slic3r/GUI/GUI_Init.hpp b/src/slic3r/GUI/GUI_Init.hpp index c420c9554..2adf618a4 100644 --- a/src/slic3r/GUI/GUI_Init.hpp +++ b/src/slic3r/GUI/GUI_Init.hpp @@ -1,6 +1,7 @@ #ifndef slic3r_GUI_Init_hpp_ #define slic3r_GUI_Init_hpp_ +#include #include namespace Slic3r { @@ -12,6 +13,9 @@ struct GUI_InitParams int argc; char **argv; + // Substitutions of unknown configuration values done during loading of user presets. + PresetsConfigSubstitutions preset_substitutions; + std::vector load_configs; DynamicPrintConfig extra_config; std::vector input_files; diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index 9998f42c7..f6e3976ea 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -140,22 +140,27 @@ void SLAImportJob::process() if (p->path.empty()) return; std::string path = p->path.ToUTF8().data(); + ConfigSubstitutions config_substitutions; try { switch (p->sel) { case Sel::modelAndProfile: - import_sla_archive(path, p->win, p->mesh, p->profile, progr); + config_substitutions = import_sla_archive(path, p->win, p->mesh, p->profile, progr); break; case Sel::modelOnly: - import_sla_archive(path, p->win, p->mesh, progr); + config_substitutions = import_sla_archive(path, p->win, p->mesh, progr); break; case Sel::profileOnly: - import_sla_archive(path, p->profile); + config_substitutions = import_sla_archive(path, p->profile); break; } } catch (std::exception &ex) { p->err = ex.what(); } + + if (! config_substitutions.empty()) { + //FIXME Add reporting here "Loading profiles found following incompatibilities." + } update_status(100, was_canceled() ? _(L("Importing canceled.")) : _(L("Importing done."))); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 9c7533acd..389782a8f 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1518,6 +1518,7 @@ void MainFrame::update_menubar() m_changeable_menu_items[miPrinterTab] ->SetBitmap(create_menu_bitmap(is_fff ? "printer" : "sla_printer")); } +#if 0 // To perform the "Quck Slice", "Quick Slice and Save As", "Repeat last Quick Slice" and "Slice to SVG". void MainFrame::quick_slice(const int qs) { @@ -1643,6 +1644,7 @@ void MainFrame::quick_slice(const int qs) // }; // Slic3r::GUI::catch_error(this, []() { if (m_progress_dialog) m_progress_dialog->Destroy(); }); } +#endif void MainFrame::reslice_now() { @@ -1729,7 +1731,13 @@ void MainFrame::load_config_file() bool MainFrame::load_config_file(const std::string &path) { try { - wxGetApp().preset_bundle->load_config_file(path); + ConfigSubstitutions config_substitutions = wxGetApp().preset_bundle->load_config_file(path, ForwardCompatibilitySubstitutionRule::Enable); + if (! config_substitutions.empty()) { + // TODO: Add list of changes from all_substitutions + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } } catch (const std::exception &ex) { show_error(this, ex.what()); return false; @@ -1793,14 +1801,22 @@ void MainFrame::load_configbundle(wxString file/* = wxEmptyString, const bool re wxGetApp().app_config->update_config_dir(get_dir_name(file)); - auto presets_imported = 0; + size_t presets_imported = 0; + PresetsConfigSubstitutions config_substitutions; try { - presets_imported = wxGetApp().preset_bundle->load_configbundle(file.ToUTF8().data()); + std::tie(config_substitutions, presets_imported) = wxGetApp().preset_bundle->load_configbundle(file.ToUTF8().data(), PresetBundle::LoadConfigBundleAttribute::SaveImported); } catch (const std::exception &ex) { show_error(this, ex.what()); return; } + if (! config_substitutions.empty()) { + // TODO: Add list of changes from all_substitutions + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } + // Load the currently selected preset into the GUI, update the preset selection box. wxGetApp().load_current_presets(); diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 40fff23da..94779c163 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -170,7 +170,7 @@ public: bool is_last_input_file() const { return !m_qs_last_input_file.IsEmpty(); } bool is_dlg_layout() const { return m_layout == ESettingsLayout::Dlg; } - void quick_slice(const int qs = qsUndef); +// void quick_slice(const int qs = qsUndef); void reslice_now(); void repair_stl(); void export_config(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 13e866965..0c34f21dd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2237,7 +2237,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ DynamicPrintConfig config; { DynamicPrintConfig config_loaded; - model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, false, load_config); + ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::Enable }; + model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, only_if(load_config, Model::LoadAttribute::CheckVersion)); if (load_config && !config_loaded.empty()) { // Based on the printer technology field found in the loaded config, select the base for the config, PrinterTechnology printer_technology = Preset::printer_technology(config_loaded); @@ -2261,6 +2262,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ // and place the loaded config over the base. config += std::move(config_loaded); } + if (! config_substitutions.empty()) { + // TODO: + show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities." + " To recover these files, incompatible values were changed to default values." + " But data in files won't be changed until you save them in PrusaSlicer."))); + } this->model.custom_gcode_per_print_z = model.custom_gcode_per_print_z; } @@ -2330,7 +2337,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } else { - model = Slic3r::Model::read_from_file(path.string(), nullptr, false, load_config); + model = Slic3r::Model::read_from_file(path.string(), nullptr, nullptr, only_if(load_config, Model::LoadAttribute::CheckVersion)); for (auto obj : model.objects) if (obj->name.empty()) obj->name = fs::path(obj->input_file).filename().string(); @@ -3215,7 +3222,7 @@ void Plater::priv::replace_with_stl() Model new_model; try { - new_model = Model::read_from_file(path, nullptr, true, false); + new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); @@ -3388,7 +3395,7 @@ void Plater::priv::reload_from_disk() Model new_model; try { - new_model = Model::read_from_file(path, nullptr, true, false); + new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); @@ -4599,7 +4606,9 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator // Switch to the other printer technology. Switch to the last printer active for that particular technology. AppConfig *app_config = wxGetApp().app_config; app_config->set("presets", "printer", (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name); - wxGetApp().preset_bundle->load_presets(*app_config); + //FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive. + // Anyways, don't report any config value substitutions, they have been already reported to the user at application start up. + wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); // load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> Object_list::update_and_show_object_settings_item(), // but the Object list still keeps pointer to the old Model. Avoid a crash by removing selection first. this->sidebar->obj_list()->unselect_objects(); diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index 92eab9307..e2970acf1 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -379,7 +379,8 @@ void fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx) // PresetBundle bundle; on_progress(L("Loading repaired model"), 80); DynamicPrintConfig config; - bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), &config, &model, false); + ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::EnableSilent }; + bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), config, config_substitutions, &model, false); boost::filesystem::remove(path_dst); if (! loaded) throw Slic3r::RuntimeError(L("Import of the repaired 3mf file failed")); diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index f0310073f..ae8f2abb5 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -612,7 +612,7 @@ void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons update.install(); PresetBundle bundle; - bundle.load_configbundle(update.source.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM); + bundle.load_configbundle(update.source.string(), PresetBundle::LoadConfigBundleAttribute::LoadSystem); BOOST_LOG_TRIVIAL(info) << format("Deleting %1% conflicting presets", bundle.prints.size() + bundle.filaments.size() + bundle.printers.size()); @@ -710,6 +710,17 @@ void PresetUpdater::slic3r_update_notify() } } +static void reload_configs_update_gui() +{ + // Reload global configuration + auto* app_config = GUI::wxGetApp().app_config; + // System profiles should not trigger any substitutions, user profiles may trigger substitutions, but these substitutions + // were already presented to the user on application start up. Just do substitutions now and keep quiet about it. + GUI::wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); + GUI::wxGetApp().load_current_presets(); + GUI::wxGetApp().plater()->set_bed_shape(); +} + PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, bool no_notification) const { if (! p->enabled_config_update) { return R_NOOP; } @@ -767,7 +778,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 } //forced update - if(incompatible_version) + if (incompatible_version) { BOOST_LOG_TRIVIAL(info) << format("Update of %1% bundles available. At least one requires higher version of Slicer.", updates.updates.size()); @@ -782,14 +793,8 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 const auto res = dlg.ShowModal(); if (res == wxID_OK) { BOOST_LOG_TRIVIAL(info) << "User wants to update..."; - p->perform_updates(std::move(updates)); - - // Reload global configuration - auto* app_config = GUI::wxGetApp().app_config; - GUI::wxGetApp().preset_bundle->load_presets(*app_config); - GUI::wxGetApp().load_current_presets(); - GUI::wxGetApp().plater()->set_bed_shape(); + reload_configs_update_gui(); return R_UPDATE_INSTALLED; } else { @@ -814,11 +819,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; p->perform_updates(std::move(updates)); - - // Reload global configuration - auto* app_config = GUI::wxGetApp().app_config; - GUI::wxGetApp().preset_bundle->load_presets(*app_config); - GUI::wxGetApp().load_current_presets(); + reload_configs_update_gui(); return R_UPDATE_INSTALLED; } else { @@ -871,11 +872,7 @@ void PresetUpdater::on_update_notification_confirm() if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; p->perform_updates(std::move(p->waiting_updates)); - - // Reload global configuration - auto* app_config = GUI::wxGetApp().app_config; - GUI::wxGetApp().preset_bundle->load_presets(*app_config); - GUI::wxGetApp().load_current_presets(); + reload_configs_update_gui(); p->has_waiting_updates = false; //return R_UPDATE_INSTALLED; } diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_data.cpp index d55f9f061..f5424dfd9 100644 --- a/tests/fff_print/test_data.cpp +++ b/tests/fff_print/test_data.cpp @@ -200,14 +200,14 @@ void init_print(std::initializer_list input_meshes, Slic3r::Print void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list config_items, bool comments) { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize(config_items); + config.set_deserialize_strict(config_items); init_print(meshes, print, model, config, comments); } void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list config_items, bool comments) { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize(config_items); + config.set_deserialize_strict(config_items); init_print(meshes, print, model, config, comments); } diff --git a/tests/fff_print/test_flow.cpp b/tests/fff_print/test_flow.cpp index dc73f4b6e..81f748e19 100644 --- a/tests/fff_print/test_flow.cpp +++ b/tests/fff_print/test_flow.cpp @@ -19,7 +19,7 @@ SCENARIO("Extrusion width specifics", "[Flow]") { GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer and a 20mm cube mesh") { // this is a sharedptr DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize({ + config.set_deserialize_strict({ { "brim_width", 2 }, { "skirts", 1 }, { "perimeters", 3 }, diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 9ee319c94..a4c8aa09a 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -10,7 +10,7 @@ SCENARIO("lift() is not ignored after unlift() at normal values of Z", "[GCodeWr GIVEN("A config from a file and a single extruder.") { GCodeWriter writer; GCodeConfig &config = writer.config; - config.load(std::string(TEST_DATA_DIR) + "/fff_print_tests/test_gcodewriter/config_lift_unlift.ini"); + config.load(std::string(TEST_DATA_DIR) + "/fff_print_tests/test_gcodewriter/config_lift_unlift.ini", ForwardCompatibilitySubstitutionRule::Disable); std::vector extruder_ids {0}; writer.set_extruders(extruder_ids); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index fc2ac3dee..a139e4c2b 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -50,7 +50,7 @@ SCENARIO("Print: Skirt generation", "[Print]") { SCENARIO("Print: Changing number of solid surfaces does not cause all surfaces to become internal.", "[Print]") { GIVEN("sliced 20mm cube and config with top_solid_surfaces = 2 and bottom_solid_surfaces = 1") { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize({ + config.set_deserialize_strict({ { "top_solid_layers", 2 }, { "bottom_solid_layers", 1 }, { "layer_height", 0.25 }, // get a known number of layers diff --git a/tests/fff_print/test_printgcode.cpp b/tests/fff_print/test_printgcode.cpp index d6f6eca58..2a45bd200 100644 --- a/tests/fff_print/test_printgcode.cpp +++ b/tests/fff_print/test_printgcode.cpp @@ -224,7 +224,7 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode]") { { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_num_extruders(4); - config.set_deserialize({ + config.set_deserialize_strict({ { "start_gcode", "; Extruder [current_extruder]" }, { "infill_extruder", 2 }, { "solid_infill_extruder", 2 }, diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 097f72dcc..8f508f323 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -31,7 +31,7 @@ static int get_brim_tool(const std::string &gcode) TEST_CASE("Skirt height is honored", "[Skirt]") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 1 }, { "skirt_height", 5 }, { "perimeters", 0 }, @@ -64,7 +64,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { GIVEN("A default configuration") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_num_extruders(4); - config.set_deserialize({ + config.set_deserialize_strict({ { "support_material_speed", 99 }, { "first_layer_height", 0.3 }, { "gcode_comments", true }, @@ -78,7 +78,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { }); WHEN("Brim width is set to 5") { - config.set_deserialize({ + config.set_deserialize_strict({ { "perimeters", 0 }, { "skirts", 0 }, { "brim_width", 5 } @@ -100,7 +100,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { } WHEN("Skirt area is smaller than the brim") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 1 }, { "brim_width", 10} }); @@ -110,7 +110,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { } WHEN("Skirt height is 0 and skirts > 0") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 2 }, { "skirt_height", 0 } }); @@ -123,7 +123,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { // This is a real error! One shall print the brim with the external perimeter extruder! WHEN("Perimeter extruder = 2 and support extruders = 3") { THEN("Brim is printed with the extruder used for the perimeters of first object") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "brim_width", 5 }, { "perimeter_extruder", 2 }, @@ -137,7 +137,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { } WHEN("Perimeter extruder = 2, support extruders = 3, raft is enabled") { THEN("brim is printed with same extruder as skirt") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "brim_width", 5 }, { "perimeter_extruder", 2 }, @@ -153,7 +153,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { #endif WHEN("brim width to 1 with layer_width of 0.5") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "first_layer_extrusion_width", 0.5 }, { "brim_width", 1 } @@ -167,7 +167,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { #if 0 WHEN("brim ears on a square") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "first_layer_extrusion_width", 0.5 }, { "brim_width", 1 }, @@ -182,7 +182,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { } WHEN("brim ears on a square but with a too small max angle") { - config.set_deserialize({ + config.set_deserialize_strict({ { "skirts", 0 }, { "first_layer_extrusion_width", 0.5 }, { "brim_width", 1 }, @@ -198,7 +198,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { #endif WHEN("Object is plated with overhang support and a brim") { - config.set_deserialize({ + config.set_deserialize_strict({ { "layer_height", 0.4 }, { "first_layer_height", 0.4 }, { "skirts", 1 }, diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index d0f459e4d..5ab000d04 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -14,7 +14,8 @@ SCENARIO("Reading 3mf file", "[3mf]") { WHEN("3mf model is read") { std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf"; DynamicPrintConfig config; - bool ret = load_3mf(path.c_str(), &config, &model, false); + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; + bool ret = load_3mf(path.c_str(), config, ctxt, &model, false); THEN("load should succeed") { REQUIRE(ret); } @@ -56,7 +57,10 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") { // load back the model from the 3mf file Model dst_model; DynamicPrintConfig dst_config; - load_3mf(test_file.c_str(), &dst_config, &dst_model, false); + { + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; + load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false); + } boost::filesystem::remove(test_file); // compare meshes diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 417a29dca..7fbf31b11 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -9,7 +9,7 @@ SCENARIO("Generic config validation performs as expected.", "[Config]") { GIVEN("A config generated from default options") { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); WHEN( "perimeter_extrusion_width is set to 250%, a valid value") { - config.set_deserialize("perimeter_extrusion_width", "250%"); + config.set_deserialize_strict("perimeter_extrusion_width", "250%"); THEN( "The config is read as valid.") { REQUIRE(config.validate().empty()); } @@ -40,7 +40,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A boolean option is set to a string value representing a 0 or 1") { - CHECK_NOTHROW(config.set_deserialize("gcode_comments", "1")); + CHECK_NOTHROW(config.set_deserialize_strict("gcode_comments", "1")); THEN("The underlying value is set correctly.") { REQUIRE(config.opt("gcode_comments")->getBool() == true); } @@ -59,7 +59,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A numeric option is set from serialized string") { - config.set_deserialize("bed_temperature", "100"); + config.set_deserialize_strict("bed_temperature", "100"); THEN("The underlying value is set correctly.") { REQUIRE(config.opt("bed_temperature")->get_at(0) == 100); } @@ -92,7 +92,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } WHEN("A numeric option is set to a non-numeric value.") { THEN("A BadOptionTypeException exception is thown.") { - REQUIRE_THROWS_AS(config.set_deserialize("perimeter_speed", "zzzz"), BadOptionTypeException); + REQUIRE_THROWS_AS(config.set_deserialize_strict("perimeter_speed", "zzzz"), BadOptionTypeException); } THEN("The value does not change.") { REQUIRE(config.opt("perimeter_speed")->getFloat() == 60.0); @@ -117,7 +117,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A float or percent is set as a percent through the string interface.") { - config.set_deserialize("first_layer_extrusion_width", "100%"); + config.set_deserialize_strict("first_layer_extrusion_width", "100%"); THEN("Value and percent flag are 100/true") { auto tmp = config.opt("first_layer_extrusion_width"); REQUIRE(tmp->percent == true); @@ -125,7 +125,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A float or percent is set as a float through the string interface.") { - config.set_deserialize("first_layer_extrusion_width", "100"); + config.set_deserialize_strict("first_layer_extrusion_width", "100"); THEN("Value and percent flag are 100/false") { auto tmp = config.opt("first_layer_extrusion_width"); REQUIRE(tmp->percent == false); @@ -195,7 +195,7 @@ SCENARIO("Config ini load/save interface", "[Config]") { WHEN("new_from_ini is called") { Slic3r::DynamicPrintConfig config; std::string path = std::string(TEST_DATA_DIR) + "/test_config/new_from_ini.ini"; - config.load_from_ini(path); + config.load_from_ini(path, ForwardCompatibilitySubstitutionRule::Disable); THEN("Config object contains ini file options.") { REQUIRE(config.option_throw("filament_colour", false)->values.size() == 1); REQUIRE(config.option_throw("filament_colour", false)->values.front() == "#ABCD"); diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp index 8c56afc6d..59784e940 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -9,7 +9,7 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { PlaceholderParser parser; auto config = DynamicPrintConfig::full_print_config(); - config.set_deserialize( { + config.set_deserialize_strict( { { "printer_notes", " PRINTER_VENDOR_PRUSA3D PRINTER_MODEL_MK2 " }, { "nozzle_diameter", "0.6;0.6;0.6;0.6" }, { "temperature", "357;359;363;378" } diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp index aec6ceb6a..20288243e 100644 --- a/xs/src/perlglue.cpp +++ b/xs/src/perlglue.cpp @@ -282,7 +282,7 @@ bool ConfigBase__set(ConfigBase* THIS, const t_config_option_key &opt_key, SV* v break; } default: - if (! opt->deserialize(std::string(SvPV_nolen(value)))) + if (! opt->deserialize(std::string(SvPV_nolen(value)), ForwardCompatibilitySubstitutionRule::Disable)) return false; } return true; @@ -295,7 +295,8 @@ bool ConfigBase__set_deserialize(ConfigBase* THIS, const t_config_option_key &op size_t len; const char * c = SvPV(str, len); std::string value(c, len); - return THIS->set_deserialize_nothrow(opt_key, value); + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; + return THIS->set_deserialize_nothrow(opt_key, value, ctxt); } void ConfigBase__set_ifndef(ConfigBase* THIS, const t_config_option_key &opt_key, SV* value, bool deserialize) diff --git a/xs/xsp/Config.xsp b/xs/xsp/Config.xsp index 52a5e7d83..703427035 100644 --- a/xs/xsp/Config.xsp +++ b/xs/xsp/Config.xsp @@ -55,7 +55,7 @@ %code%{ auto config = new DynamicPrintConfig(); try { - config->load(path); + config->load(path, ForwardCompatibilitySubstitutionRule::Disable); RETVAL = config; } catch (std::exception& e) { delete config; @@ -119,7 +119,7 @@ %code%{ auto config = new FullPrintConfig(); try { - config->load(path); + config->load(path, ForwardCompatibilitySubstitutionRule::Disable); RETVAL = static_cast(config); } catch (std::exception& e) { delete config; diff --git a/xs/xsp/Flow.xsp b/xs/xsp/Flow.xsp index 019af16f2..3056b4001 100644 --- a/xs/xsp/Flow.xsp +++ b/xs/xsp/Flow.xsp @@ -30,7 +30,7 @@ _new_from_width(CLASS, role, width, nozzle_diameter, height) float height; CODE: ConfigOptionFloatOrPercent optwidth; - optwidth.deserialize(width); + optwidth.deserialize(width, ForwardCompatibilitySubstitutionRule::Disable); RETVAL = new Flow(Flow::new_from_config_width(role, optwidth, nozzle_diameter, height)); OUTPUT: RETVAL diff --git a/xs/xsp/Model.xsp b/xs/xsp/Model.xsp index 93067ebe3..e7a171efd 100644 --- a/xs/xsp/Model.xsp +++ b/xs/xsp/Model.xsp @@ -22,7 +22,7 @@ %name{read_from_file} Model(std::string input_file, bool add_default_instances = true) %code%{ try { - RETVAL = new Model(Model::read_from_file(input_file, nullptr, add_default_instances)); + RETVAL = new Model(Model::read_from_file(input_file, nullptr, nullptr, only_if(add_default_instances, Model::LoadAttribute::AddDefaultInstances))); } catch (std::exception& e) { croak("Error while opening %s: %s\n", input_file.c_str(), e.what()); }