From aeaa3c5d66c78bfc3c9d3ca956cabea453383626 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 19 Aug 2026 16:57:45 +0800 Subject: [PATCH] Bug fix for perfect name matching --- src/libslic3r/PresetBundle.cpp | 38 +++- src/libslic3r/PublishSettings.hpp | 4 + src/slic3r/GUI/Plater.cpp | 4 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 9 +- tests/libslic3r/test_3mf.cpp | 4 +- .../libslic3r/test_preset_bundle_loading.cpp | 199 ++++++++++++++++++ 6 files changed, 242 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 7a90036fe2..bc6df4c3a9 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4894,11 +4894,19 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Mirror first_visible_idx()'s start index so suppressed default presets are // never picked as a slot material. const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0; - // Candidate preference for a published entry: exact setting_id (variant-level, - // since "Generic PLA" and "Generic PLA Matte" share filament_id), then exact - // filament_id, then vendor+type, then type only (a type-only pick may surface an - // unrelated preset, e.g. a different vendor's PLA). + // Candidate preference for a published entry: exact preset name (unambiguous + // even when ids are shared between variants or missing from older files), then + // exact setting_id (variant-level, since "Generic PLA" and "Generic PLA Matte" + // share filament_id), then exact filament_id, then vendor+type, then type only + // (a type-only pick may surface an unrelated preset, e.g. a different vendor's + // PLA). auto candidate_score = [](const Preset &candidate, const PublishedMaterialEntry &entry) -> int { + if (!entry.preset_name.empty()) { + const std::string bare_name = entry.preset_name.substr(0, entry.preset_name.find('@')); + if (candidate.name == entry.preset_name || candidate.alias == entry.preset_name || + candidate.name == bare_name || candidate.alias == bare_name) + return 4; + } if (!entry.setting_id.empty() && candidate.setting_id == entry.setting_id) return 3; const ConfigOptionStrings *types = candidate.config.opt("filament_type"); @@ -4926,10 +4934,13 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool int best_score = -1; for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) - continue; const int score = candidate_score(candidate, entry); if (score > best_score) { + // Exact identity tiers (name / setting_id) win even when the + // preset is hidden or already referenced by another slot; the + // alias re-pointing pass below still de-aliases afterwards. + if (score < 3 && (!candidate.is_visible || used_preset_names.count(candidate.name) != 0)) + continue; best_score = score; initial_preset = candidate.name; } @@ -4979,9 +4990,13 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool continue; for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible || referenced_elsewhere(candidate.name, size_t(-1))) - continue; const int score = candidate_score(candidate, entry); + // Exact identity tiers (name / setting_id) may use a hidden preset; + // the referenced check stays: re-pointing exists to de-alias. + if (score < 3 && !candidate.is_visible) + continue; + if (referenced_elsewhere(candidate.name, size_t(-1))) + continue; if (score > best_score) { best_score = score; replacement = candidate.name; @@ -5106,11 +5121,14 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool std::string best_name; for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible) - continue; const int score = candidate_score(candidate, entry); if (score <= best_score) continue; + // Lower tiers (vendor+type, type only) need a visible preset; + // an exact identity match (name / setting_id) wins even when + // the preset is hidden in the library. + if (score < 3 && !candidate.is_visible) + continue; if (unreferenced_only) { bool used = false; for (size_t s = 0; s < this->filament_presets.size(); ++s) diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index ecb51523f5..0dc579c2a9 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -40,6 +40,10 @@ struct PublishedMaterialEntry { // used on load to match the exact published variant, which filament_id alone cannot // distinguish ("Generic PLA" and "Generic PLA Matte" share their inherited id). std::string setting_id; + // Canonical name of the author's slot preset (e.g. "Generic PLA @System"). The receiver + // prefers an exact name/alias match over id matching: ids can be shared across variants + // or missing from older files, the name is what the author actually selected. + std::string preset_name; // 0-based author filament slot; -1 (hand-crafted files) is skipped. int slot{-1}; std::vector keys; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index be56cc1f66..50d613de33 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7252,6 +7252,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ entry.filament_id = mat["filament_id"].get(); if (mat.contains("setting_id") && mat["setting_id"].is_string()) entry.setting_id = mat["setting_id"].get(); + if (mat.contains("name") && mat["name"].is_string()) + entry.preset_name = mat["name"].get(); } if (m.contains("slot") && m["slot"].is_number_integer()) entry.slot = m["slot"].get(); @@ -16251,7 +16253,7 @@ int Plater::export_published_3mf(const std::vector& published_keys, j.push_back(key); nlohmann::json jm = nlohmann::json::array(); for (const Slic3r::PublishedMaterialEntry& e : material_keys) - jm.push_back({ {"material", {{"filament_type", e.filament_type}, {"filament_vendor", e.filament_vendor}, {"filament_id", e.filament_id}, {"setting_id", e.setting_id}}}, {"slot", e.slot}, {"keys", e.keys}, + jm.push_back({ {"material", {{"filament_type", e.filament_type}, {"filament_vendor", e.filament_vendor}, {"filament_id", e.filament_id}, {"setting_id", e.setting_id}, {"name", e.preset_name}}}, {"slot", e.slot}, {"keys", e.keys}, {"full", e.full}, {"full_keys", e.full_keys}, {"publish_type", e.publish_type}, {"type", e.publish_type_value}, {"publish_color", e.publish_color}, {"color", e.color} }); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index f62d7af3a4..881560f93c 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -924,11 +924,14 @@ std::vector PublishSettingsDialog::GetPublishedM entry.filament_id = cat.filament_id; entry.slot = static_cast(cat.filament_slot); // The author's preset id distinguishes exact variants that share filament_id - // ("Generic PLA" vs "Generic PLA Matte"), so the receiver can match precisely. + // ("Generic PLA" vs "Generic PLA Matte"), so the receiver can match precisely; the + // preset name is the most direct identity and is matched first on load. PresetBundle *bundle = wxGetApp().preset_bundle; if (bundle != nullptr && cat.filament_slot < bundle->filament_presets.size()) { - if (const Preset *preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true)) - entry.setting_id = preset->setting_id; + if (const Preset *preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true)) { + entry.setting_id = preset->setting_id; + entry.preset_name = preset->name; + } } // "Full Publish": the whole filament preset is embedded; type and colour are implicitly // published, and the per-key rows are disabled / their state ignored. diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 34ff31183f..818c354628 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -684,7 +684,7 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded const std::vector published_keys = { "layer_height", "retraction_length" }; const std::vector material_keys = { - { "PLA", "Generic", "GFL99", "", 0, { "filament_retraction_length" } } + { "PLA", "Generic", "GFL99", "", "Generic PLA", 0, { "filament_retraction_length" } } }; DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); @@ -792,7 +792,7 @@ SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") { model.add_default_instances(); const std::string material_keys_json = - R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf"},"slot":1,"keys":[],"full":true,"full_keys":["filament_retraction_length","filament_colour"],"publish_type":true,"type":"PLA","publish_color":false,"color":""}])"; + R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf","name":"Generic PLA Matte @System"},"slot":1,"keys":[],"full":true,"full_keys":["filament_retraction_length","filament_colour"],"publish_type":true,"type":"PLA","publish_color":false,"color":""}])"; model.model_info = std::make_shared(); model.model_info->metadata_items["published_material_keys"] = material_keys_json; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 3a9ff77be9..11162922fc 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -942,6 +942,205 @@ TEST_CASE("Published 3MF replaces a mismatched slot with the exact published mat CHECK(pub.skipped_keys.empty()); } +// The author's preset name travels in the file, so a type mismatch resolves to the exact +// published material even when the identity fields are absent or stale (older files): a +// "Generic PLA" author must land on the receiver's "Generic PLA", never on a same-type +// substitute like "Bambu PLA Basic". +TEST_CASE("Published 3MF replaces a mismatched slot with the exact preset by name", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "OGFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9 }; + return config; + }; + + auto add_pla = [](PresetBundle &bundle, const char *name, const char *id, const char *vendor, const char *setting_id) { + Preset &preset = add_inmemory_preset(bundle.filaments, name); + preset.filament_id = id; + preset.setting_id = setting_id; + preset.config.opt_string("filament_type", 0u) = "PLA"; + preset.config.opt_string("filament_vendor", 0u) = vendor; + preset.config.opt("filament_retraction_length", true)->values = { 0.5 }; + return &preset; + }; + + SECTION("full identity (name, setting_id, filament_id): the name match wins") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.filament_id = "OGFL99"; + entry.filament_vendor = "Generic"; + entry.setting_id = "RcBNzytWgwRrwXXz"; + entry.preset_name = "Generic PLA @System"; + entry.full_keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filament_presets[0] == "Generic PLA @System"); + CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @System"); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("only the name is present (broken/stale ids): still the exact preset") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @System"; + entry.full_keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filament_presets[0] == "Generic PLA @System"); + CHECK(bundle.filaments.find_preset("Bambu PLA Basic @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @System"); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("the exact preset is hidden in the library: the exact match still wins") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + Preset &generic_pla = *add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); + generic_pla.is_visible = false; + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @System"; + entry.full_keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filament_presets[0] == "Generic PLA @System"); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("grown slot (author slot 1) is seeded with the exact preset by name") { + auto two_slot_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic" }; + config.opt("filament_ids")->values = { "OGFL99", "OGFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9, 0.8 }; + return config; + }; + + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 1; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @System"; + entry.full_keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = two_slot_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 2); + CHECK(bundle.filament_presets[0] == "My PETG"); + CHECK(bundle.filament_presets[1] == "Generic PLA @System"); + // The full dump applies the author's slot-1 value onto the grown slot's preset. + CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.8 }); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("no exact preset in the library: falls back to the substitute") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @System"; + entry.filament_id = "OGFL99"; + entry.full_keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filament_presets[0] == "Bambu PLA Basic @System"); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Bambu PLA Basic @System (substitute: no exact material match)"); + } +} + // The replacement search prefers the published identity: exact filament_id, then vendor+type, // then type only (collection order decides equal scores; the pick is reported as a substitute // when it is not the exact published material).