diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 47cc11a087..218070fa61 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -2029,19 +2029,21 @@ int CLI::run(int argc, char **argv) } }; - auto resolve_preset = [&ensure_cli_preset_bundle](const std::string &file, DynamicPrintConfig &config, + // One resolver for the whole run, so presets from the same vendor tree share its load. + std::unique_ptr system_preset_resolver; + auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config, std::string &config_type, const std::string &config_from, bool probe_type, std::string &error) { const auto *inherits = config.option(BBL_JSON_KEY_INHERITS); if (!probe_type && (inherits == nullptr || inherits->value.empty())) return true; - std::unique_ptr source_bundle; PresetBundle *bundle = nullptr; bool allow_source_manifest = false; if (config_from == "system") { - source_bundle = std::make_unique(); - bundle = source_bundle.get(); + if (!system_preset_resolver) + system_preset_resolver = std::make_unique(); + bundle = system_preset_resolver.get(); allow_source_manifest = true; } else { bundle = ensure_cli_preset_bundle(error); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 4b8fb03a02..54e5db27e4 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -484,7 +484,7 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; - auto collection_for_type = [](PresetBundle &bundle, Preset::Type preset_type) -> PresetCollection * { + auto collection_for_type = [](const PresetBundle &bundle, Preset::Type preset_type) -> const PresetCollection * { switch (preset_type) { case Preset::TYPE_PRINT: return &bundle.prints; case Preset::TYPE_FILAMENT: return &bundle.filaments; @@ -493,15 +493,15 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ } }; - PresetCollection *collection = collection_for_type(*this, type); + const PresetCollection *collection = collection_for_type(*this, type); if (collection == nullptr) { error = "Unsupported preset type"; return false; } const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal(); - auto find_loaded = [&](PresetBundle &bundle) -> const Preset * { - PresetCollection *loaded_collection = collection_for_type(bundle, type); + auto find_loaded = [&](const PresetBundle &bundle) -> const Preset * { + const PresetCollection *loaded_collection = collection_for_type(bundle, type); const Preset *resolved = nullptr; for (const Preset &preset : loaded_collection->get_presets()) { if (preset.file.empty()) @@ -549,30 +549,11 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ continue; try { - PresetBundle library_bundle; - const PresetBundle *base_bundle = nullptr; - if (vendor_id != ORCA_FILAMENT_LIBRARY && - boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) { - library_bundle.m_preserve_vendor_source_paths = true; - library_bundle.load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem, - compatibility_rule, nullptr, false); - if (library_bundle.error_count() != 0) { - error = "OrcaFilamentLibrary contains invalid presets"; - return false; - } - base_bundle = &library_bundle; - } - - PresetBundle source_bundle; - source_bundle.m_preserve_vendor_source_paths = true; - source_bundle.load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, - compatibility_rule, base_bundle, false); - if (source_bundle.error_count() != 0) { - error = "Vendor bundle contains invalid presets"; + const PresetBundle *loaded = load_source_vendor(root_dir, vendor_id, compatibility_rule, error); + if (loaded == nullptr) return false; - } - const Preset *resolved = find_loaded(source_bundle); + const Preset *resolved = find_loaded(*loaded); if (resolved == nullptr) { if (error.empty()) error = "Source file is not an instantiated preset in its vendor manifest"; @@ -591,6 +572,37 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ return false; } +const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir, + const std::string &vendor_id, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error) +{ + auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule); + if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end()) + return it->second.get(); + + // The library loads with no base of its own, so the tree a vendor inherits from + // is the same one that resolves the library's own presets. + const PresetBundle *library = nullptr; + if (vendor_id != ORCA_FILAMENT_LIBRARY && + boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) { + library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error); + if (library == nullptr) { + error = "OrcaFilamentLibrary contains invalid presets"; + return nullptr; + } + } + + auto bundle = std::make_unique(); + bundle->m_preserve_vendor_source_paths = true; + bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, false); + if (bundle->error_count() != 0) { + error = "Vendor bundle contains invalid presets"; + return nullptr; + } + return m_source_vendor_bundles.emplace(std::move(key), std::move(bundle)).first->second.get(); +} + bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type, const std::string &source_file, ForwardCompatibilitySubstitutionRule compatibility_rule, diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 353b6dc07d..88455fabf3 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -652,6 +653,17 @@ private: bool m_generate_vendor_caches { false }; bool m_preserve_vendor_source_paths { false }; + // Vendor trees loaded by resolve_preset_config's manifest path, so every preset + // resolved through this bundle shares one load per source root and vendor. The + // filament library is one such tree, shared by every vendor under its root. + std::map, std::unique_ptr> + m_source_vendor_bundles; + + const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir, + const std::string &vendor_id, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error); + // Orca: validation only - flag any printer with two or more compatible // filament presets sharing one filament_id (ambiguous AMS subtype match). bool check_duplicate_filament_subtypes() const; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index ecdede7053..73d244cf42 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -987,6 +987,193 @@ TEST_CASE("Resolution terminates when no vendor manifest exists", "[Preset][Bund CHECK(error == "Preset was not found in the loaded bundle"); } +TEST_CASE("Manifest-backed resolution reuses the vendor tree it already loaded", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path process_dir = dir.path() / "Acme" / "process"; + fs::create_directories(process_dir); + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" + << R"({"name":"fdm_process_common","sub_path":"process/base.json"},)" + << R"({"name":"Acme First","sub_path":"process/first.json"},)" + << R"({"name":"Acme Second","sub_path":"process/second.json"}]})"; + auto write_base = [&](double travel_speed) { + std::ofstream((process_dir / "base.json").string()) + << R"({"type":"process","name":"fdm_process_common","from":"system",)" + << R"("instantiation":"false","travel_speed":[")" << travel_speed << R"("]})"; + }; + auto write_child = [&](const std::string &file, const std::string &name) { + std::ofstream((process_dir / file).string()) + << R"({"type":"process","name":")" << name << R"(","from":"system",)" + << R"("instantiation":"true","inherits":"fdm_process_common"})"; + }; + write_base(111.0); + write_child("first.json", "Acme First"); + write_child("second.json", "Acme Second"); + + auto travel_speed = [&](PresetBundle &bundle, const std::string &file) { + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common"; + std::string error; + REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, (process_dir / file).string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + return raw.option("travel_speed")->values.front(); + }; + + PresetBundle bundle; + CHECK_THAT(travel_speed(bundle, "first.json"), Catch::Matchers::WithinAbs(111.0, 1e-6)); + + // Only a reload would see this change. + write_base(222.0); + CHECK_THAT(travel_speed(bundle, "second.json"), Catch::Matchers::WithinAbs(111.0, 1e-6)); + + PresetBundle fresh; + CHECK_THAT(travel_speed(fresh, "second.json"), Catch::Matchers::WithinAbs(222.0, 1e-6)); +} + +TEST_CASE("Manifest-backed resolution does not keep a vendor tree that failed to load", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path child_file = dir.path() / "Acme" / "process" / "child.json"; + auto write_manifest = [&](const std::string &leading_entry) { + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" << leading_entry + << R"({"name":"Acme Process","sub_path":"process/child.json"}]})"; + }; + write_manifest("123,"); + fs::create_directories(child_file.parent_path()); + std::ofstream(child_file.string()) + << R"({"type":"process","name":"Acme Process","from":"system",)" + << R"("instantiation":"true","layer_height":"0.2"})"; + + PresetBundle bundle; + auto resolve = [&](std::string &error) { + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common"; + return bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, child_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error); + }; + + std::string error; + CHECK_FALSE(resolve(error)); + CHECK_FALSE(error.empty()); + + write_manifest(""); + error.clear(); + CHECK(resolve(error)); + CHECK(error.empty()); +} + +TEST_CASE("Manifest-backed resolution reuses the library base for type-probed files", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path library_pet = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament" / "pet.json"; + const fs::path filament_dir = dir.path() / "Acme" / "filament"; + + std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string()) + << R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)" + << R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"}]})"; + fs::create_directories(library_pet.parent_path()); + auto write_library_pet = [&](double density) { + std::ofstream(library_pet.string()) + << R"({"type":"filament","name":"fdm_filament_pet","from":"system",)" + << R"("filament_id":"GFL99","instantiation":"false",)" + << R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})"; + }; + write_library_pet(1.27); + + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","filament_list":[)" + << R"({"name":"Acme PETG","sub_path":"filament/petg.json","filament_id":"GFA00"},)" + << R"({"name":"Acme PETG Matte","sub_path":"filament/petg_matte.json","filament_id":"GFA01"}]})"; + fs::create_directories(filament_dir); + auto write_child = [&](const std::string &file, const std::string &name, const std::string &filament_id) { + std::ofstream((filament_dir / file).string()) + << R"({"type":"filament","name":")" << name << R"(","from":"system",)" + << R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})"; + }; + write_child("petg.json", "Acme PETG", "GFA00"); + write_child("petg_matte.json", "Acme PETG Matte", "GFA01"); + + auto density = [](const DynamicPrintConfig &config) { + return config.option("filament_density")->values.front(); + }; + + PresetBundle bundle; + DynamicPrintConfig first; + first.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet"; + std::string error; + REQUIRE(bundle.resolve_preset_config(first, Preset::TYPE_FILAMENT, (filament_dir / "petg.json").string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK_THAT(density(first), Catch::Matchers::WithinAbs(1.27, 1e-6)); + + // Only a reload would see this change. + write_library_pet(1.5); + + DynamicPrintConfig second; + Preset::Type type = Preset::TYPE_INVALID; + REQUIRE(bundle.resolve_preset_config_type(second, type, (filament_dir / "petg_matte.json").string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK(type == Preset::TYPE_FILAMENT); + CHECK_THAT(density(second), Catch::Matchers::WithinAbs(1.27, 1e-6)); +} + +TEST_CASE("Manifest-backed resolution shares the library between vendors under one root", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path library_dir = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament"; + + std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string()) + << R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)" + << R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"},)" + << R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json","filament_id":"GFL98"}]})"; + fs::create_directories(library_dir); + auto write_library_pet = [&](double density) { + std::ofstream((library_dir / "pet.json").string()) + << R"({"type":"filament","name":"fdm_filament_pet","from":"system",)" + << R"("filament_id":"GFL99","instantiation":"false",)" + << R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})"; + }; + write_library_pet(1.27); + std::ofstream((library_dir / "generic_petg.json").string()) + << R"({"type":"filament","name":"Generic PETG","from":"system",)" + << R"("filament_id":"GFL98","instantiation":"true","inherits":"fdm_filament_pet"})"; + + auto write_vendor = [&](const std::string &vendor, const std::string &filament_id) { + const fs::path filament_dir = dir.path() / vendor / "filament"; + fs::create_directories(filament_dir); + std::ofstream((dir.path() / (vendor + ".json")).string()) + << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)" + << R"({"name":")" << vendor << R"( PETG","sub_path":"filament/petg.json","filament_id":")" << filament_id << R"("}]})"; + std::ofstream((filament_dir / "petg.json").string()) + << R"({"type":"filament","name":")" << vendor << R"( PETG","from":"system",)" + << R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})"; + return filament_dir / "petg.json"; + }; + const fs::path acme_petg = write_vendor("Acme", "GFA00"); + const fs::path beta_petg = write_vendor("Beta", "GFB00"); + + auto density = [&](PresetBundle &bundle, const fs::path &file) { + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet"; + std::string error; + REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_FILAMENT, file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + return raw.option("filament_density")->values.front(); + }; + + PresetBundle bundle; + CHECK_THAT(density(bundle, acme_petg), Catch::Matchers::WithinAbs(1.27, 1e-6)); + + // Only a reload would see this change. + write_library_pet(1.5); + CHECK_THAT(density(bundle, beta_petg), Catch::Matchers::WithinAbs(1.27, 1e-6)); + CHECK_THAT(density(bundle, library_dir / "generic_petg.json"), Catch::Matchers::WithinAbs(1.27, 1e-6)); + + PresetBundle fresh; + CHECK_THAT(density(fresh, beta_petg), Catch::Matchers::WithinAbs(1.5, 1e-6)); +} + // Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic // library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible // with that printer and the plater combo box lists the shared alias twice.