From 22bd674c1d075435970f4c1087e0411d654df31e Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Tue, 29 Sep 2026 13:03:55 +0800 Subject: [PATCH] Load a CLI Project's Printer and Process Settings as the GUI Does The CLI filled only the keys a project lacked from its system preset. The GUI builds a project preset differently: the project's values go over the default preset, without the print-host keys, and every key the project does not list in different_settings_to_system is refreshed to its base system preset's current value. After a profile update the two sliced the same project differently. That step now lives in Preset::load_external_config, which takes plain configs and gets the base preset from a callback, so the collection lookup stays in PresetCollection. PresetBundle::project_different_keys builds the kept-key set from a project's escaped entry, adding the preset bookkeeping keys, and is used by both the GUI and the CLI. PresetCollection::load_external_preset calls the shared step with no change in behaviour. The CLI now builds the project's printer and process configs with the same step, passing the system preset from resolve_system_preset. That drops the hand-kept skip list for print-host and variant-layout keys and the legacy-key check: the shared step already excludes print-host keys and maps per-variant values onto the base preset's variant layout. The --uptodate path and a printer or process given on the command line are left as they were. Because the GUI's kept-key set always holds the bookkeeping keys, the refresh runs for every project that names a system preset, so the CLI now loads that preset's vendor on every such run. --- src/OrcaSlicer.cpp | 69 ++++---- src/libslic3r/Preset.cpp | 82 +++++---- src/libslic3r/Preset.hpp | 9 + src/libslic3r/PresetBundle.cpp | 49 ++---- src/libslic3r/PresetBundle.hpp | 3 + tests/cli/test_cli_project_missing_keys.sh | 19 ++- .../libslic3r/test_preset_bundle_loading.cpp | 155 ++++++++++++++++++ 7 files changed, 275 insertions(+), 111 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 4ad8beac94..4ea0d57c7d 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -3123,48 +3123,43 @@ int CLI::run(int argc, char **argv) return 0; }; - // A project saved before a printer or process option existed has no value for it. The GUI takes such - // keys from the project's system preset (load_external_preset refreshes every key the project did not - // override), so fill them from there too instead of leaving them to the option default. - // The extruder variant keys describe the project's variant layout and are kept as they are, so an - // older project is not left with a variant list from one layout and ids from another. - auto fill_missing_project_keys = [this, &ensure_system_preset_resolver](const std::string &system_name, Preset::Type type) { + // Load the project's printer and process settings as the GUI loads its presets: over the default preset, + // with every key the project does not list as changed, including keys saved before an option existed, + // taken from its current system preset. + auto load_project_preset = [this, &ensure_system_preset_resolver, ¤t_different_settings, filament_count](const std::string &system_name, Preset::Type type) { if (system_name.empty()) return; - static const std::set skip_keys = { - "inherits", "compatible_printers", "compatible_prints", "compatible_printers_condition", "compatible_prints_condition", - "print_settings_id", "filament_settings_id", "printer_settings_id", - "print_host", "print_host_webui", "printhost_apikey", "printhost_cafile", "printhost_user", "printhost_password", "printhost_port", - "printer_extruder_id", "printer_extruder_variant", "print_extruder_id", "print_extruder_variant", "extruder_variant_list"}; - const std::vector &options = type == Preset::TYPE_PRINTER ? Preset::printer_options() : Preset::print_options(); - // Keys the legacy handler drops on load can never be in a project, so they do not count as missing. - auto dropped_on_load = [](std::string key) { - std::string value; - PrintConfigDef::handle_legacy(key, value); - return key.empty(); - }; - std::vector missing; - for (const std::string &key : options) - if (m_print_config.option(key) == nullptr && skip_keys.count(key) == 0 && !dropped_on_load(key)) - missing.push_back(key); - if (missing.empty()) - return; + // Preset bookkeeping the CLI keeps in its own groups, e.g. inherits_group and print_compatible_printers. + static const std::set bookkeeping_keys = {"inherits", "compatible_printers", "compatible_prints", "compatible_printers_condition", + "compatible_prints_condition", "print_settings_id", "printer_settings_id"}; + const size_t index = type == Preset::TYPE_PRINTER ? filament_count + 1 : 0; + PresetBundle &resolver = ensure_system_preset_resolver(); DynamicPrintConfig system_config; - std::string error; - if (!ensure_system_preset_resolver().resolve_system_preset(system_config, type, system_name, config_substitution_rule, error)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system preset '%1%' not resolved (%2%); keys missing from the project keep their defaults") % system_name % error; - return; + t_config_option_keys keys; + const DynamicPrintConfig config = Preset::load_external_config(type, + type == Preset::TYPE_PRINTER ? resolver.printers.default_preset_for(m_print_config).config : resolver.prints.default_preset().config, + m_print_config, PresetBundle::project_different_keys(index < current_different_settings.size() ? current_different_settings[index] : std::string()), + [&](const std::string &) -> DynamicPrintConfig * { + std::string error; + if (resolver.resolve_system_preset(system_config, type, system_name, config_substitution_rule, error)) + return &system_config; + BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system preset '%1%' not resolved (%2%); the project keeps its values") % system_name % error; + return nullptr; + }, &keys); + for (const std::string &key : keys) { + const ConfigOption *opt = config.option(key); + const ConfigOption *old = m_print_config.option(key); + if (bookkeeping_keys.count(key) != 0 || opt == nullptr || (old != nullptr && *old == *opt)) + continue; + BOOST_LOG_TRIVIAL(info) << boost::format("CLI: %1% from '%2%': %3% -> %4%") % key % system_name % (old ? old->serialize() : std::string("(missing)")) % opt->serialize(); + m_print_config.set_key_value(key, opt->clone()); } - for (const std::string &key : missing) - if (const ConfigOption *opt = system_config.option(key)) { - m_print_config.set_key_value(key, opt->clone()); - BOOST_LOG_TRIVIAL(info) << boost::format("CLI: %1% missing from the project, taken from '%2%': %3%") % key % system_name % opt->serialize(); - } }; - if (new_printer_name.empty()) - fill_missing_project_keys(current_printer_system_name, Preset::TYPE_PRINTER); - if (new_process_name.empty()) - fill_missing_project_keys(current_process_system_name, Preset::TYPE_PRINT); + // The --uptodate path refreshes the project from its own system configs. + if (new_printer_name.empty() && load_machine_config.empty()) + load_project_preset(current_printer_system_name, Preset::TYPE_PRINTER); + if (new_process_name.empty() && load_process_config.empty()) + load_project_preset(current_process_system_name, Preset::TYPE_PRINT); std::vector& different_settings = m_print_config.option("different_settings_to_system", true)->values; std::vector& inherits_group = m_print_config.option("inherits_group", true)->values; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9af8969490..3b6ab7f292 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -974,6 +974,37 @@ void Preset::get_extruder_names_and_keysets(Type type, std::string& extruder_id_ } } +DynamicPrintConfig Preset::load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config, + const std::set &different_settings_list, + const std::function &find_base, + t_config_option_keys *keys) +{ + // Load the preset over a default preset, so that the missing fields are filled in from the default preset. + DynamicPrintConfig cfg(default_config); + // SoftFever: ignore print connection info from project + auto cfg_keys = cfg.keys(); + cfg_keys.erase(std::remove_if(cfg_keys.begin(), cfg_keys.end(), + [](std::string &val) { + return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" || + val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port"; + }), + cfg_keys.end()); + cfg.apply_only(project_config, cfg_keys, true); + + //add different settings check logic, replace the old system preset's default value with new system preset's default values + if (!different_settings_list.empty()) { + if (DynamicPrintConfig *base_config = find_base(Preset::inherits(cfg))) { + std::string extruder_id_name, extruder_variant_name; + std::set *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + cfg.update_non_diff_values_to_base_config(*base_config, cfg_keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); + } + } + if (keys) + *keys = std::move(cfg_keys); + return cfg; +} + bool Preset::has_lidar(PresetBundle *preset_bundle) { bool has_lidar = false; @@ -2596,20 +2627,6 @@ std::pair PresetCollection::load_external_preset( const Semver file_version, const std::string filament_id) { - // Load the preset over a default preset, so that the missing fields are filled in from the default preset. - DynamicPrintConfig cfg(this->default_preset_for(combined_config).config); - // SoftFever: ignore print connection info from project - auto keys = cfg.keys(); - keys.erase(std::remove_if(keys.begin(), keys.end(), - [](std::string &val) { - return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" || - val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port"; - }), - keys.end()); - cfg.apply_only(combined_config, keys, true); - std::string &inherits = Preset::inherits(cfg); - - //add different settings check logic, replace the old system preset's default value with new system preset's default values std::deque::iterator it = this->find_preset_internal(original_name); bool found = it != m_presets.end() && it->name == original_name; if (! found) { @@ -2618,24 +2635,25 @@ std::pair PresetCollection::load_external_preset( found = it != m_presets.end(); } - std::string extruder_id_name, extruder_variant_name; - std::set *key_set1 = nullptr, *key_set2 = nullptr; - Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); - - if (!inherits.empty() && (different_settings_list.size() > 0)) { - auto iter = this->find_preset_internal(inherits); - if (iter == m_presets.end() || iter->name != inherits) - iter = this->find_preset_renamed(inherits); - if (iter != m_presets.end()) { - //std::vector dirty_options = cfg.diff(iter->config); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path; - cfg.update_non_diff_values_to_base_config(iter->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); - } - } - else if (found && it->is_system && (different_settings_list.size() > 0)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path; - cfg.update_non_diff_values_to_base_config(it->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); - } + t_config_option_keys keys; + DynamicPrintConfig cfg = Preset::load_external_config(m_type, this->default_preset_for(combined_config).config, combined_config, different_settings_list, + [this, &original_name, &path, found, it](const std::string &inherits) -> DynamicPrintConfig * { + if (!inherits.empty()) { + auto iter = this->find_preset_internal(inherits); + if (iter == m_presets.end() || iter->name != inherits) + iter = this->find_preset_renamed(inherits); + if (iter == m_presets.end()) + return nullptr; + BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path; + return &iter->config; + } + if (found && it->is_system) { + BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path; + return &it->config; + } + return nullptr; + }, &keys); + std::string &inherits = Preset::inherits(cfg); //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , path %2%, name %3%, original_name %4%, inherits %5%")%Preset::get_type_string(m_type) %path %name %original_name %inherits; diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 5b32d90ac6..f7a8dd6bcb 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -403,6 +403,15 @@ public: std::string get_current_printer_type(PresetBundle *preset_bundle); // get current preset type static void get_extruder_names_and_keysets(Type type, std::string& extruder_id_name, std::string& extruder_variant_name, std::set** p_key_set1, std::set** p_key_set2); + // Config of a preset loaded from a project or config file: the project's values over the type's + // default preset config, without the print-host keys. When different_settings_list is not empty, + // every key not listed in it is then refreshed from the base system preset, which find_base returns + // for the project's "inherits" (nullptr when there is none), with the listed per-variant values + // mapped onto the base's extruder variants. keys, if given, receives the keys taken from the project. + static DynamicPrintConfig load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config, + const std::set &different_settings_list, + const std::function &find_base, + t_config_option_keys *keys = nullptr); std::string get_printer_id() const { return vendor ? vendor->id : ""; } bool has_lidar(PresetBundle *preset_bundle); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 53430cb347..8cd2bd8234 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4613,6 +4613,15 @@ const std::set ignore_settings_list ={ "print_settings_id", "filament_settings_id", "printer_settings_id" }; +std::set PresetBundle::project_different_keys(const std::string &different_settings) +{ + std::vector keys; + Slic3r::unescape_strings_cstyle(different_settings, keys); + std::set keys_set(keys.begin(), keys.end()); + keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + return keys_set; +} + DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional> filament_maps_new, std::optional> filament_volume_maps_new) const { DynamicPrintConfig out; @@ -5318,15 +5327,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (!is_published) { //BBS: add different settings logic BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id"); - std::vector print_different_keys_vector; - std::string print_different_settings = different_values[0]; - Slic3r::unescape_strings_cstyle(print_different_settings, print_different_keys_vector); - std::set print_different_keys_set(print_different_keys_vector.begin(), print_different_keys_vector.end()); - //if (!has_different_settings_to_system) { - // print_different_keys_set.clear(); - //} - //else - print_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + std::set print_different_keys_set = project_different_keys(different_values[0]); if (!print_compatible_printers.empty()) { ConfigOptionStrings* compatible_printers = config.option("compatible_printers", true); compatible_printers->values = print_compatible_printers; @@ -5337,15 +5338,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool //clear compatible printers clear_compatible_printers(config); - std::vector printer_different_keys_vector; - std::string printer_different_settings = different_values[num_filaments + 1]; - Slic3r::unescape_strings_cstyle(printer_different_settings, printer_different_keys_vector); - std::set printer_different_keys_set(printer_different_keys_vector.begin(), printer_different_keys_vector.end()); - //if (!has_different_settings_to_system) { - // printer_different_keys_set.clear(); - //} - //else - printer_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + std::set printer_different_keys_set = project_different_keys(different_values[num_filaments + 1]); //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id"); load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string()); @@ -5364,15 +5357,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool Preset *loaded = nullptr; //BBS: add different settings logic - std::vector filament_different_keys_vector; - std::string filament_different_settings = different_values[1]; - Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); - std::set filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end()); - //if (!has_different_settings_to_system) { - // filament_different_keys_set.clear(); - //} - //else - filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + std::set filament_different_keys_set = project_different_keys(different_values[1]); std::string filament_id = filament_ids[0]; //BBS: add config related logs @@ -5435,15 +5420,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool cfg.opt_string("inherits", true) = inherits_values[i + 1]; //BBS: add different settings logic - std::vector filament_different_keys_vector; - std::string filament_different_settings = different_values[i+1]; - Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); - std::set filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end()); - //if (!has_different_settings_to_system) { - // filament_different_keys_set.clear(); - //} - //else - filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + std::set filament_different_keys_set = project_different_keys(different_values[i+1]); std::string filament_id = filament_ids[i]; diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 220c9b8f37..8171395849 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -234,6 +234,9 @@ public: // ORCA: utility function to find the vendor for a given preset name static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type); + // Keys a project keeps when its presets are loaded: those listed in its escaped + // "different_settings_to_system" entry for the preset, plus the preset bookkeeping keys. + static std::set project_different_keys(const std::string &different_settings); PresetBundle(); PresetBundle(const PresetBundle &rhs); diff --git a/tests/cli/test_cli_project_missing_keys.sh b/tests/cli/test_cli_project_missing_keys.sh index d22ca6bec2..4c55709ea0 100644 --- a/tests/cli/test_cli_project_missing_keys.sh +++ b/tests/cli/test_cli_project_missing_keys.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash -# End-to-end check that the CLI fills settings missing from a project from the project's system presets. +# End-to-end check that the CLI loads a project's printer and process settings as the GUI does. # -# A project saved before an option existed has no value for it. The GUI takes such keys from the -# project's system printer and process presets, not from the option defaults, and the CLI must slice -# the project with the same values. A project is exported from the shipped Bambu Lab P1S presets, one -# printer key and one process key are removed from it, one kept key is changed, and it is sliced again. +# The GUI takes every key a project does not list as changed from the project's current system preset: +# keys saved before an option existed, and keys holding an older system value. Keys the project lists +# in different_settings_to_system keep the project's value. A project is exported from the shipped +# Bambu Lab P1S presets; one printer key and one process key are removed, one printer key and one +# process key are changed without being listed, one key is changed and listed, and it is sliced again. # # usage: test_cli_project_missing_keys.sh set -u @@ -46,7 +47,8 @@ slice base "$WORK/cube.stl" \ --load-settings "$PROFILES/machine/Bambu Lab P1S 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \ --load-filaments "$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json" -# The removed keys, with their option defaults from PrintConfig.cpp, and a kept key with a new value. +# The removed keys, with their option defaults from PrintConfig.cpp; stale keys changed without being +# listed as different, which must come back with the system value; and a listed key the project keeps. "$PY" - "$WORK/base/out.3mf" "$WORK/old.3mf" <<'EOF' || exit $? import json, sys, zipfile @@ -62,8 +64,13 @@ with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED print("SKIP: %s is %s in the system preset, the option default, so the test cannot tell them apart" % (key, default)) sys.exit(77) expected = {key: config.pop(key) for key in missing} + for key in ("top_shell_layers", "extruder_clearance_height_to_rod"): + expected[key] = config[key] + config[key] = str(int(float(config[key])) + 1) expected["wall_loops"] = str(int(config["wall_loops"]) + 1) config["wall_loops"] = expected["wall_loops"] + different = config["different_settings_to_system"] + different[0] = ";".join([k for k in different[0].split(";") if k] + ["wall_loops"]) data = json.dumps(config, indent=4) zout.writestr(item, data) with open(dst + ".expected.json", "w") as f: diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 291be98ee8..46d07fb356 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -5583,3 +5583,158 @@ TEST_CASE("A system preset no vendor lists is not resolved", "[Preset][Bundle]") ForwardCompatibilitySubstitutionRule::EnableSilent, error)); CHECK_FALSE(error.empty()); } + +namespace { + +// A default preset config for type, built the way PresetBundle builds its default presets. +DynamicPrintConfig external_default_config(Preset::Type type) +{ + DynamicPrintConfig config; + config.apply_only(static_cast(FullPrintConfig::defaults()), + type == Preset::TYPE_PRINTER ? Preset::printer_options() : Preset::print_options()); + Preset::inherits(config); + return config; +} + +// A find_base callback that counts its calls and returns base. +auto base_finder(DynamicPrintConfig *base, int &calls) +{ + return [base, &calls](const std::string &) { + ++calls; + return base; + }; +} + +} // namespace + +TEST_CASE("A key missing from a project takes the default preset value when there is no base", "[Preset][ExternalPreset]") +{ + DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT); + defaults.set("wall_loops", 7, true); + const DynamicPrintConfig project; + int calls = 0; + + const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"}, + base_finder(nullptr, calls)); + CHECK(calls == 1); + CHECK(config.opt_int("wall_loops") == 7); +} + +TEST_CASE("A key missing from a project takes its base preset value", "[Preset][ExternalPreset]") +{ + DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT); + defaults.set("wall_loops", 7, true); + DynamicPrintConfig base = defaults; + base.set("wall_loops", 5, true); + const DynamicPrintConfig project; + int calls = 0; + + const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"}, + base_finder(&base, calls)); + CHECK(config.opt_int("wall_loops") == 5); +} + +TEST_CASE("A project key listed as different keeps the project value", "[Preset][ExternalPreset]") +{ + const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT); + DynamicPrintConfig base = defaults; + base.set("wall_loops", 5, true); + DynamicPrintConfig project; + project.set("wall_loops", 4, true); + int calls = 0; + + const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"wall_loops"}, + base_finder(&base, calls)); + CHECK(config.opt_int("wall_loops") == 4); +} + +TEST_CASE("A project key not listed as different is refreshed to the base value", "[Preset][ExternalPreset]") +{ + const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT); + DynamicPrintConfig base = defaults; + base.set("wall_loops", 5, true); + DynamicPrintConfig project; + project.set("wall_loops", 4, true); + project.set("inherits", "Base Process", true); + std::string inherits; + + const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"}, + [&base, &inherits](const std::string &name) { + inherits = name; + return &base; + }); + CHECK(inherits == "Base Process"); + CHECK(config.opt_int("wall_loops") == 5); +} + +TEST_CASE("An empty different settings list keeps the project values without a base", "[Preset][ExternalPreset]") +{ + DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT); + defaults.set("top_shell_layers", 7, true); + DynamicPrintConfig base = defaults; + base.set("wall_loops", 5, true); + base.set("top_shell_layers", 9, true); + DynamicPrintConfig project; + project.set("wall_loops", 4, true); + int calls = 0; + + const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {}, base_finder(&base, calls)); + CHECK(calls == 0); + CHECK(config.opt_int("wall_loops") == 4); + CHECK(config.opt_int("top_shell_layers") == 7); +} + +TEST_CASE("Print-host keys are never taken from a project", "[Preset][ExternalPreset]") +{ + DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINTER); + defaults.set("print_host", "", true); + defaults.set("printhost_apikey", "", true); + DynamicPrintConfig project; + project.set("print_host", "http://project-host", true); + project.set("printhost_apikey", "project-key", true); + project.set("printer_notes", "project notes", true); + t_config_option_keys keys; + int calls = 0; + + const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINTER, defaults, project, {"printer_notes"}, + base_finder(nullptr, calls), &keys); + CHECK(config.opt_string("print_host").empty()); + CHECK(config.opt_string("printhost_apikey").empty()); + CHECK(config.opt_string("printer_notes") == "project notes"); + CHECK_FALSE(contains_key(keys, "print_host")); + CHECK_FALSE(contains_key(keys, "printhost_apikey")); + CHECK(contains_key(keys, "printer_notes")); +} + +TEST_CASE("A per-variant project value maps onto its base preset's variant layout", "[Preset][ExternalPreset]") +{ + const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT); + DynamicPrintConfig base = defaults; + base.set_key_value("print_extruder_id", new ConfigOptionInts({1, 1})); + base.set_key_value("print_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"})); + base.set_key_value("outer_wall_speed", new ConfigOptionFloats({200., 300.})); + base.set_key_value("inner_wall_speed", new ConfigOptionFloats({250., 350.})); + // A project saved before its printer had a High Flow variant. + DynamicPrintConfig project; + project.set_key_value("print_extruder_id", new ConfigOptionInts({1})); + project.set_key_value("print_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard"})); + project.set_key_value("outer_wall_speed", new ConfigOptionFloats({100.})); + project.set_key_value("inner_wall_speed", new ConfigOptionFloats({150.})); + int calls = 0; + + const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"outer_wall_speed"}, + base_finder(&base, calls)); + CHECK(config.option("print_extruder_variant")->values == + std::vector{"Direct Drive Standard", "Direct Drive High Flow"}); + // The listed key keeps the project's Standard value and takes High Flow from the base. + check_double_vector(config.option("outer_wall_speed")->values, {100., 300.}); + check_double_vector(config.option("inner_wall_speed")->values, {250., 350.}); +} + +TEST_CASE("A project's different settings always keep the preset bookkeeping keys", "[Preset][ExternalPreset]") +{ + const std::set keys = PresetBundle::project_different_keys(escape_strings_cstyle({"wall_loops", "top_shell_layers"})); + for (const char *key : {"wall_loops", "top_shell_layers", "inherits", "print_settings_id", "filament_settings_id", "printer_settings_id"}) + CHECK(keys.count(key) == 1); + CHECK(PresetBundle::project_different_keys(std::string()).count("inherits") == 1); +}