From df101ead91da5643413a291eb2c8bccffa73d2db Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 4 Sep 2026 00:29:52 +0800 Subject: [PATCH] fix more issues --- .../Creality/machine/Creality K1 SE.json | 2 +- .../Creality/machine/Creality K1C.json | 2 +- .../Creality/machine/Creality K2 Plus.json | 2 +- .../Creality/machine/Creality K2 Pro.json | 2 +- .../Creality/machine/Creality K2.json | 2 +- .../machine/Flashforge Guider 3 Ultra.json | 2 +- .../machine/D12 230 PRO M2 DIRECT.json | 2 +- .../machine/D12 230 PRO SMARTPAD DIRECT.json | 2 +- .../machine/D12 300 PRO M2 DIRECT.json | 2 +- .../machine/D12 300 PRO SMARTPAD DIRECT.json | 2 +- .../machine/D12 500 PRO M2 DIRECT.json | 2 +- .../machine/D12 500 PRO SMARTPAD DIRECT.json | 2 +- .../OrcaSlicer_profile_validator.cpp | 2 +- src/slic3r/Utils/CrealityPrintAgent.cpp | 79 ++++++++-- tests/slic3rutils/CMakeLists.txt | 1 + tests/slic3rutils/test_creality_cfs_match.cpp | 141 ++++++++++++++++++ 16 files changed, 218 insertions(+), 29 deletions(-) create mode 100644 tests/slic3rutils/test_creality_cfs_match.cpp diff --git a/resources/profiles/Creality/machine/Creality K1 SE.json b/resources/profiles/Creality/machine/Creality K1 SE.json index 9f6806ee46..1cb9769e0e 100644 --- a/resources/profiles/Creality/machine/Creality K1 SE.json +++ b/resources/profiles/Creality/machine/Creality K1 SE.json @@ -9,5 +9,5 @@ "bed_texture": "creality_k1se_buildplate_texture.svg", "hotend_model": "", "default_bed_type": "Textured PEI Plate", - "default_materials": "Generic ABS @Creality K1-all;Generic ASA @Creality K1-all;Generic PA-CF @Creality K1-all;Generic PC @Creality K1-all;Generic PETG @Creality K1-all;Generic PLA @Creality K1-all;Generic PLA High Speed @Creality K1-all;Generic PLA Matte @Creality K1-all;Generic PLA Silk @Creality K1-all;Generic PLA-CF @Creality K1-all;Generic TPU @Creality K1-all" + "default_materials": "Generic ABS @Creality K1-all;Generic ABS @K1 SE-all;Generic ASA @Creality K1-all;Generic ASA @K1 SE-all;Generic PA-CF @Creality K1-all;Generic PC @Creality K1-all;Generic PETG @Creality K1-all;Generic PETG @K1 SE-all;Generic PLA @Creality K1-all;Generic PLA @K1 SE-all;Generic PLA High Speed @Creality K1-all;Generic PLA Matte @Creality K1-all;Generic PLA Silk @Creality K1-all;Generic PLA-CF @Creality K1-all;Generic PLA-CF @K1 SE-all;Generic TPU @Creality K1-all;Generic TPU @K1 SE-all" } diff --git a/resources/profiles/Creality/machine/Creality K1C.json b/resources/profiles/Creality/machine/Creality K1C.json index 4283aabfd4..18e80dab92 100644 --- a/resources/profiles/Creality/machine/Creality K1C.json +++ b/resources/profiles/Creality/machine/Creality K1C.json @@ -9,5 +9,5 @@ "bed_texture": "creality_k1c_buildplate_texture.svg", "hotend_model": "", "default_bed_type": "Textured PEI Plate", - "default_materials": "Generic ABS @Creality K1-all;Generic ASA @Creality K1-all;Generic PA-CF @Creality K1-all;Generic PC @Creality K1-all;Generic PETG @Creality K1-all;Generic PLA @Creality K1-all;Generic PLA High Speed @Creality K1-all;Generic PLA Matte @Creality K1-all;Generic PLA Silk @Creality K1-all;Generic PLA-CF @Creality K1-all;Generic TPU @Creality K1-all" + "default_materials": "Generic ABS @Creality K1-all;Generic ABS @K1C-all;Generic ASA @Creality K1-all;Generic ASA @K1C-all;Generic PA-CF @Creality K1-all;Generic PA-CF @K1C-all;Generic PC @Creality K1-all;Generic PC @K1C-all;Generic PETG @Creality K1-all;Generic PETG @K1C-all;Generic PLA @Creality K1-all;Generic PLA @K1C-all;Generic PLA High Speed @Creality K1-all;Generic PLA Matte @Creality K1-all;Generic PLA Silk @Creality K1-all;Generic PLA-CF @Creality K1-all;Generic PLA-CF @K1C-all;Generic TPU @Creality K1-all;Generic TPU @K1C-all" } diff --git a/resources/profiles/Creality/machine/Creality K2 Plus.json b/resources/profiles/Creality/machine/Creality K2 Plus.json index d70e5e517a..ea4430128a 100644 --- a/resources/profiles/Creality/machine/Creality K2 Plus.json +++ b/resources/profiles/Creality/machine/Creality K2 Plus.json @@ -9,5 +9,5 @@ "bed_texture": "creality_k2plus_buildplate_texture.svg", "hotend_model": "", "default_bed_type": "Textured PEI Plate", - "default_materials": "Generic ABS @Creality K2-all;Generic ASA @Creality K2-all;Generic PETG @Creality K2-all;Generic PLA @Creality K2-all;Generic PLA High Speed @Creality K2-all;Generic PLA Matte @Creality K2-all;Generic PLA Silk @Creality K2-all" + "default_materials": "Generic ABS @Creality K2-all;Generic ABS @K2 Plus-all;Generic ASA @Creality K2-all;Generic ASA @K2 Plus-all;Generic PETG @Creality K2-all;Generic PETG @K2 Plus-all;Generic PLA @Creality K2-all;Generic PLA @K2 Plus-all;Generic PLA High Speed @Creality K2-all;Generic PLA Matte @Creality K2-all;Generic PLA Silk @Creality K2-all" } diff --git a/resources/profiles/Creality/machine/Creality K2 Pro.json b/resources/profiles/Creality/machine/Creality K2 Pro.json index 3df797399b..937117d11d 100644 --- a/resources/profiles/Creality/machine/Creality K2 Pro.json +++ b/resources/profiles/Creality/machine/Creality K2 Pro.json @@ -9,5 +9,5 @@ "bed_texture": "creality_k2pro_buildplate_texture.svg", "default_bed_type": "Textured PEI Plate", "hotend_model": "", - "default_materials": "Generic ABS @Creality K2-all;Generic ASA @Creality K2-all;Generic PETG @Creality K2-all;Generic PLA @Creality K2-all;Generic PLA High Speed @Creality K2-all;Generic PLA Matte @Creality K2-all;Generic PLA Silk @Creality K2-all" + "default_materials": "Generic ABS @Creality K2-all;Generic ABS @K2 Pro-all;Generic ASA @Creality K2-all;Generic ASA @K2 Pro-all;Generic PETG @Creality K2-all;Generic PETG @K2 Pro-all;Generic PLA @Creality K2-all;Generic PLA @K2 Pro-all;Generic PLA High Speed @Creality K2-all;Generic PLA Matte @Creality K2-all;Generic PLA Silk @Creality K2-all" } diff --git a/resources/profiles/Creality/machine/Creality K2.json b/resources/profiles/Creality/machine/Creality K2.json index 3ea29942b7..cc852576fe 100644 --- a/resources/profiles/Creality/machine/Creality K2.json +++ b/resources/profiles/Creality/machine/Creality K2.json @@ -9,5 +9,5 @@ "default_bed_type": "Textured PEI Plate", "bed_texture": "creality_k2_buildplate_texture.svg", "hotend_model": "", - "default_materials": "Generic ABS @Creality K2-all;Generic ASA @Creality K2-all;Generic PETG @Creality K2-all;Generic PLA @Creality K2-all;Generic PLA High Speed @Creality K2-all;Generic PLA Matte @Creality K2-all;Generic PLA Silk @Creality K2-all" + "default_materials": "Generic ABS @Creality K2-all;Generic ABS @K2-all;Generic ASA @Creality K2-all;Generic ASA @K2-all;Generic PETG @Creality K2-all;Generic PETG @K2-all;Generic PLA @Creality K2-all;Generic PLA @K2-all;Generic PLA High Speed @Creality K2-all;Generic PLA Matte @Creality K2-all;Generic PLA Silk @Creality K2-all" } diff --git a/resources/profiles/Flashforge/machine/Flashforge Guider 3 Ultra.json b/resources/profiles/Flashforge/machine/Flashforge Guider 3 Ultra.json index c95215a762..a830f2db98 100644 --- a/resources/profiles/Flashforge/machine/Flashforge Guider 3 Ultra.json +++ b/resources/profiles/Flashforge/machine/Flashforge Guider 3 Ultra.json @@ -8,5 +8,5 @@ "bed_model": "flashforge_g3u_buildplate_model.stl", "bed_texture": "flashforge_g3u_buildplate_texture.svg", "hotend_model": "", - "default_materials": "Generic PVA @Flashforge;Generic HIPS @Flashforge;Generic PETG-CF @Flashforge G3U;Generic PETG @Flashforge G3U;Generic PLA-CF @Flashforge G3U;Generic PLA @Flashforge G3U;Generic ASA @Flashforge G3U;Generic ABS @Flashforge G3U;FusRock PET-CF;FusRock PAHT-CF;FusRock NexPA-CF25;FusRock S-Multi;FusRock S-PAHT;Polymaker CoPA;Polymaker S1" + "default_materials": "FusRock NexPA-CF25;FusRock PAHT-CF;FusRock PAHT-CF @G3U 0.6 Nozzle;FusRock PET-CF;FusRock PET-CF @G3U 0.6 Nozzle;FusRock S-Multi;FusRock S-PAHT;Generic ABS @Flashforge G3U;Generic ABS @Flashforge G3U 0.6 Nozzle;Generic ASA @Flashforge G3U;Generic ASA @Flashforge G3U 0.6 Nozzle;Generic HIPS @Flashforge;Generic HIPS @Flashforge G3U 0.6 Nozzle;Generic PETG @Flashforge G3U;Generic PETG @Flashforge G3U 0.6 Nozzle;Generic PETG @Flashforge G3U 0.8 Nozzle;Generic PETG-CF @Flashforge G3U;Generic PETG-CF @Flashforge G3U 0.6 Nozzle;Generic PETG-CF @Flashforge G3U 0.8 Nozzle;Generic PLA @Flashforge G3U;Generic PLA-CF @Flashforge G3U;Generic PLA-CF @Flashforge G3U 0.6 Nozzle;Generic PLA-CF @Flashforge G3U 0.8 Nozzle;Generic PVA @Flashforge;Polymaker CoPA;Polymaker S1" } diff --git a/resources/profiles/Wanhao France/machine/D12 230 PRO M2 DIRECT.json b/resources/profiles/Wanhao France/machine/D12 230 PRO M2 DIRECT.json index 45e7243ee3..cf912002ee 100644 --- a/resources/profiles/Wanhao France/machine/D12 230 PRO M2 DIRECT.json +++ b/resources/profiles/Wanhao France/machine/D12 230 PRO M2 DIRECT.json @@ -8,5 +8,5 @@ "bed_model": "Wanhao_D12-230_buildplate_model.stl", "bed_texture": "D12_texture.png", "hotend_model": "Wanhao D12-300_hotend.stl", - "default_materials": "YUMI PLA Bowden;YUMI PETG;" + "default_materials": "YUMI PLA Direct Drive;YUMI PETG;" } diff --git a/resources/profiles/Wanhao France/machine/D12 230 PRO SMARTPAD DIRECT.json b/resources/profiles/Wanhao France/machine/D12 230 PRO SMARTPAD DIRECT.json index b74b11321e..24a798c531 100644 --- a/resources/profiles/Wanhao France/machine/D12 230 PRO SMARTPAD DIRECT.json +++ b/resources/profiles/Wanhao France/machine/D12 230 PRO SMARTPAD DIRECT.json @@ -8,5 +8,5 @@ "bed_model": "Wanhao_D12-230_buildplate_model.stl", "bed_texture": "D12_texture.png", "hotend_model": "Wanhao D12-300_hotend.stl", - "default_materials": "YUMI PLA Bowden;YUMI PETG;" + "default_materials": "YUMI PLA Direct Drive;YUMI PETG;" } diff --git a/resources/profiles/Wanhao France/machine/D12 300 PRO M2 DIRECT.json b/resources/profiles/Wanhao France/machine/D12 300 PRO M2 DIRECT.json index a51f0bafbf..7447f4481b 100644 --- a/resources/profiles/Wanhao France/machine/D12 300 PRO M2 DIRECT.json +++ b/resources/profiles/Wanhao France/machine/D12 300 PRO M2 DIRECT.json @@ -8,5 +8,5 @@ "bed_model": "Wanhao_D12-300_buildplate_model.stl", "bed_texture": "D12_texture.png", "hotend_model": "Wanhao D12-300_hotend.stl", - "default_materials": "YUMI PLA Bowden;YUMI PETG;" + "default_materials": "YUMI PLA Direct Drive;YUMI PETG;" } diff --git a/resources/profiles/Wanhao France/machine/D12 300 PRO SMARTPAD DIRECT.json b/resources/profiles/Wanhao France/machine/D12 300 PRO SMARTPAD DIRECT.json index 563d05ca53..8290a99d9e 100644 --- a/resources/profiles/Wanhao France/machine/D12 300 PRO SMARTPAD DIRECT.json +++ b/resources/profiles/Wanhao France/machine/D12 300 PRO SMARTPAD DIRECT.json @@ -8,5 +8,5 @@ "bed_model": "Wanhao_D12-300_buildplate_model.stl", "bed_texture": "D12_texture.png", "hotend_model": "Wanhao D12-300_hotend.stl", - "default_materials": "YUMI PLA Bowden;YUMI PETG;" + "default_materials": "YUMI PLA Direct Drive;YUMI PETG;" } diff --git a/resources/profiles/Wanhao France/machine/D12 500 PRO M2 DIRECT.json b/resources/profiles/Wanhao France/machine/D12 500 PRO M2 DIRECT.json index d54d0d6e51..3c85fac7eb 100644 --- a/resources/profiles/Wanhao France/machine/D12 500 PRO M2 DIRECT.json +++ b/resources/profiles/Wanhao France/machine/D12 500 PRO M2 DIRECT.json @@ -8,5 +8,5 @@ "bed_model": "Wanhao_D12-500_buildplate_model.stl", "bed_texture": "D12_texture.png", "hotend_model": "Wanhao D12-300_hotend.stl", - "default_materials": "YUMI PLA Bowden;YUMI PETG;" + "default_materials": "YUMI PLA Direct Drive;YUMI PETG;" } diff --git a/resources/profiles/Wanhao France/machine/D12 500 PRO SMARTPAD DIRECT.json b/resources/profiles/Wanhao France/machine/D12 500 PRO SMARTPAD DIRECT.json index 2fe6842bc8..3588797494 100644 --- a/resources/profiles/Wanhao France/machine/D12 500 PRO SMARTPAD DIRECT.json +++ b/resources/profiles/Wanhao France/machine/D12 500 PRO SMARTPAD DIRECT.json @@ -8,5 +8,5 @@ "bed_model": "Wanhao_D12-500_buildplate_model.stl", "bed_texture": "D12_texture.png", "hotend_model": "Wanhao D12-300_hotend.stl", - "default_materials": "YUMI PLA Bowden;YUMI PETG;" + "default_materials": "YUMI PLA Direct Drive;YUMI PETG;" } diff --git a/src/dev-utils/OrcaSlicer_profile_validator.cpp b/src/dev-utils/OrcaSlicer_profile_validator.cpp index 21c6b33180..67d0ba444d 100644 --- a/src/dev-utils/OrcaSlicer_profile_validator.cpp +++ b/src/dev-utils/OrcaSlicer_profile_validator.cpp @@ -392,7 +392,7 @@ int main(int argc, char* argv[]) ("generate_presets,g", po::value()->default_value(false), "Generate user presets for mock test") ("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer to expand all custom g-code (catches placeholder/flow errors that static checks miss). Off unless this flag is present.") ("outdir,o", po::value()->default_value(""), "With -s, also save each printer's g-code to this folder (as __.gcode) for manual inspection. Optional.") - ("check_filament_subtypes,f", po::bool_switch()->default_value(false), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.") + ("check_filament_subtypes,f", po::bool_switch()->default_value(true), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.") ("log_level,l", po::value()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs."); // clang-format on diff --git a/src/slic3r/Utils/CrealityPrintAgent.cpp b/src/slic3r/Utils/CrealityPrintAgent.cpp index 55d99ff07b..9b3bd5843e 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.cpp +++ b/src/slic3r/Utils/CrealityPrintAgent.cpp @@ -8,7 +8,9 @@ #include #include +#include #include +#include namespace Slic3r { @@ -16,6 +18,12 @@ namespace { constexpr const char* CrealityPrintAgent_VERSION = "0.1.0"; +std::string to_lower(std::string s) +{ + for (auto& c : s) c = static_cast(std::tolower(static_cast(c))); + return s; +} + bool has_visible_base_preset(const PresetCollection& filaments, const std::string& filament_id) { for (const auto& p : filaments.get_presets()) { @@ -27,19 +35,47 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin return false; } +// Lower-case words of a preset name with the "@scope" suffix dropped: +// "Generic PLA Matte @Creality K2-all" -> {"generic", "pla", "matte"}. +std::vector name_words(const std::string& name) +{ + std::vector words; + std::string word; + for (char c : name.substr(0, name.find('@'))) { + if (std::isalnum(static_cast(c))) { + word += static_cast(std::tolower(static_cast(c))); + } else if (!word.empty()) { + words.push_back(word); + word.clear(); + } + } + if (!word.empty()) + words.push_back(word); + return words; +} + } // namespace // Score visible compatible filament presets against the CFS spool metadata and // return the best-matching filament_id. Scoring: // +20 preset name contains brand_name as a substring // (e.g. "Hyper PLA" in "Hyper PLA @Creality K2 0.4 nozzle") -// +10 preset name contains the vendor substring (e.g. "Creality") -// Tiebreak: prefer the SYSTEM (shipped) preset over user copies. Brand- -// specific system presets carry their own filament_id; user copies of -// generic presets inherit a generic filament_id from their parent, so -// preferring the user copy can collapse a brand-specific match back to -// "Generic PLA" via the inherited id. Plus: this code targets upstream -// OrcaSlicer where shipping the user's local tuning would be wrong. +// +10 the preset belongs to the spool's vendor - by its owning VendorProfile OR by +// its name. The profile test is what finds the vendor's own generics, which are +// named "Generic @" and do not repeat the vendor; the name test +// still finds a third party filament shipped inside that vendor's bundle, which +// carries the bundle owner's profile but names its real brand. +// -5 per word of the preset name the spool never mentioned ("generic" excepted - +// it marks the unbranded base product rather than a qualifier), so the least +// specific preset that still explains the spool wins. Without it a spool +// reporting only "PLA" scores "Generic PLA High Speed" and "Generic PLA Matte" +// exactly as high as "Generic PLA"; those are three products with three +// filament_ids, so whichever sorted first won and the printer got the wrong +// one. Applied after the score gate, so it only reorders genuine matches. +// Tiebreak: prefer the SYSTEM (shipped) preset over user copies, then by name so the +// winner never depends on how std::sort leaves equal elements. User copies of generic +// presets inherit a generic filament_id from their parent, so preferring the user copy +// can collapse a brand-specific match back to "Generic PLA" via the inherited id. // Requires the preset's declared filament_type to equal the spool's base type // (PLA/PETG/ABS/...) so we never auto-pick a PETG preset for a PLA spool. // Falls back to filaments.filament_id_by_type(base_type) when nothing scores. @@ -48,15 +84,17 @@ std::string CrealityPrintAgent::match_filament_preset(const PresetCollection& fi const std::string& brand_name, const std::string& base_type) { - auto to_lower = [](std::string s) { - for (auto& c : s) c = static_cast(std::tolower(static_cast(c))); - return s; - }; - const std::string vendor_lower = to_lower(vendor); const std::string brand_lower = to_lower(brand_name); const std::string type_lower = to_lower(base_type); + // Everything the spool told us about itself, as words. A word in a preset's name + // that is not in here is a qualifier the spool never claimed. + std::set spool_words{"generic"}; + for (const std::string& src : {brand_lower, vendor_lower, type_lower}) + for (auto& w : name_words(src)) + spool_words.insert(std::move(w)); + struct Match { const Preset* preset; int score; @@ -83,11 +121,20 @@ std::string CrealityPrintAgent::match_filament_preset(const PresetCollection& fi int score = 0; if (!brand_lower.empty() && name_lower.find(brand_lower) != std::string::npos) score += 20; - if (!vendor_lower.empty() && name_lower.find(vendor_lower) != std::string::npos) + // Profile OR name - neither alone covers both the vendor's own generics and the + // third party filaments shipped inside its bundle. See the header comment. + if (!vendor_lower.empty() + && ((p.vendor != nullptr && to_lower(p.vendor->name) == vendor_lower) + || name_lower.find(vendor_lower) != std::string::npos)) score += 10; - if (score > 0) - matches.push_back({&p, score, !p.is_system && !p.is_default}); + if (score == 0) continue; + + for (const auto& w : name_words(p.name)) + if (spool_words.count(w) == 0) + score -= 5; + + matches.push_back({&p, score, !p.is_system && !p.is_default}); } if (matches.empty()) { @@ -105,7 +152,7 @@ std::string CrealityPrintAgent::match_filament_preset(const PresetCollection& fi [](const Match& a, const Match& b) { if (a.score != b.score) return a.score > b.score; if (a.is_user != b.is_user) return !a.is_user; // prefer system over user - return false; + return a.preset->name < b.preset->name; // keep the winner deterministic }); BOOST_LOG_TRIVIAL(info) diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index ebbd62b820..eca1af2b86 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -1,6 +1,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp + test_creality_cfs_match.cpp test_dev_mapping.cpp test_filament_bitmap_utils.cpp test_network_versions.cpp diff --git a/tests/slic3rutils/test_creality_cfs_match.cpp b/tests/slic3rutils/test_creality_cfs_match.cpp new file mode 100644 index 0000000000..06606684b7 --- /dev/null +++ b/tests/slic3rutils/test_creality_cfs_match.cpp @@ -0,0 +1,141 @@ +#include + +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/Utils/CrealityPrintAgent.hpp" + +using namespace Slic3r; + +namespace { + +// A standalone filament collection, built the same way PresetBundle builds its own, so the +// CFS matcher can be exercised without loading the shipped profiles or touching a printer. +struct FilamentTestCollection : public PresetCollection +{ + FilamentTestCollection() + : PresetCollection(Preset::TYPE_FILAMENT, Preset::filament_options(), + static_cast(FullPrintConfig::defaults())) + {} +}; + +struct FilamentSpec +{ + const char *name; + const char *filament_id; + const char *filament_type; + bool is_library = false; // belongs to the Orca Filament Library, not the printer vendor + bool is_system = true; +}; + +// Load the specs into the collection, then stamp the fields the matcher reads. Done in a second +// pass because load_preset() keeps m_presets sorted, so a reference taken during the first pass +// can be left dangling by a later load. +void populate(PresetCollection &filaments, const std::vector &specs, + const VendorProfile &vendor, const VendorProfile &library) +{ + for (const FilamentSpec &spec : specs) { + DynamicPrintConfig config(filaments.default_preset().config); + config.option("filament_type", true)->values = {spec.filament_type}; + filaments.load_preset(std::string(), spec.name, config, /*select=*/false); + } + for (auto it = filaments.begin(); it != filaments.end(); ++it) { + const auto spec = std::find_if(specs.begin(), specs.end(), + [&](const FilamentSpec &s) { return it->name == s.name; }); + if (spec == specs.end()) + continue; + it->filament_id = spec->filament_id; + it->vendor = spec->is_library ? &library : &vendor; + it->is_system = spec->is_system; + it->is_default = false; + it->is_visible = true; + it->is_compatible = true; + } +} + +// What a stock K2 with a 0.4 nozzle sees after the filament_id rework: the vendor's plain generics +// carry no "Creality" in their name (they are "Generic @"), while the subtype +// variants that do keep a vendor scope are separate products with their own filament_ids. +const std::vector k2_stock_nozzle{ + {"AliZ PLA @System", "ALIZ-PLA", "PLA", /*is_library=*/true}, + {"Generic PETG @K2-all", "PETG-GENERIC", "PETG"}, + {"Generic PLA @K2-all", "PLA-GENERIC", "PLA"}, + {"Generic PLA High Speed @Creality K2-all","PLA-HS", "PLA"}, + {"Generic PLA Matte @Creality K2-all", "PLA-MATTE", "PLA"}, + {"Generic PLA Silk @Creality K2-all", "PLA-SILK", "PLA"}, + {"Hyper PLA @K2-all", "PLA-HYPER", "PLA"}, +}; + +std::string match(const std::vector &specs, const std::string &spool_vendor, + const std::string &spool_name, const std::string &base_type) +{ + FilamentTestCollection filaments; + VendorProfile vendor("Creality"); + VendorProfile library(PresetBundle::ORCA_FILAMENT_LIBRARY); + vendor.name = "Creality"; + library.name = PresetBundle::ORCA_FILAMENT_LIBRARY; + populate(filaments, specs, vendor, library); + return CrealityPrintAgent::match_filament_preset(filaments, spool_vendor, spool_name, base_type); +} + +} // namespace + +// Orca: a CFS spool that names no recognised product must map to the vendor's plain generic. The +// subtype variants ("High Speed", "Matte", "Silk") score just as well on vendor alone, and since +// each is its own product with its own filament_id, letting one of them win sends the printer the +// id of a filament the user does not have loaded. +TEST_CASE("An unbranded CFS spool maps to the vendor's plain generic, not a subtype", "[CFS][Creality]") +{ + CHECK(match(k2_stock_nozzle, "Creality", "", "PLA") == "PLA-GENERIC"); +} + +// Orca: the vendor bonus reads the preset's owning VendorProfile. "Generic PETG @K2-all" does not +// repeat "Creality" anywhere in its name, so a name based vendor test scored nothing for it and the +// spool fell through to the collection wide first-of-type - an unrelated third party PETG. +TEST_CASE("A CFS spool matches its vendor's presets even when the name omits the vendor", "[CFS][Creality]") +{ + CHECK(match(k2_stock_nozzle, "Creality", "", "PETG") == "PETG-GENERIC"); +} + +TEST_CASE("A branded CFS spool still beats the generic", "[CFS][Creality]") +{ + CHECK(match(k2_stock_nozzle, "Creality", "Hyper PLA", "PLA") == "PLA-HYPER"); +} + +// Orca: the specificity penalty must not stop a spool that genuinely asks for a subtype from +// getting it - only unclaimed qualifiers are penalised. +TEST_CASE("A CFS spool that names a subtype gets that subtype", "[CFS][Creality]") +{ + CHECK(match(k2_stock_nozzle, "Creality", "Generic PLA Silk", "PLA") == "PLA-SILK"); + CHECK(match(k2_stock_nozzle, "Creality", "Generic PLA Matte", "PLA") == "PLA-MATTE"); +} + +// Orca: a third party spool matches no preset by brand or vendor, so it falls back to the first +// visible preset of the same type rather than returning nothing. Which one that is depends on the +// collection's ordering, so only the type is pinned here - what matters is that a PLA spool never +// comes back empty and never comes back as another material. +TEST_CASE("A CFS spool from an unknown vendor falls back to a preset of the same type", "[CFS][Creality]") +{ + const std::string matched = match(k2_stock_nozzle, "SomeOtherBrand", "", "PLA"); + REQUIRE_FALSE(matched.empty()); + const auto spec = std::find_if(k2_stock_nozzle.begin(), k2_stock_nozzle.end(), + [&](const FilamentSpec &s) { return matched == s.filament_id; }); + REQUIRE(spec != k2_stock_nozzle.end()); + CHECK(std::string(spec->filament_type) == "PLA"); +} + + +// Orca: Creality's bundle also ships third party filaments ("eSUN PLA+ @K2 Plus-all"). Those carry +// Creality's VendorProfile but name their real brand, so the vendor bonus has to accept a name +// match as well as a profile match - otherwise an eSUN spool stops matching its own preset. +TEST_CASE("A third party spool matches its preset inside the printer vendor's bundle", "[CFS][Creality]") +{ + const std::vector with_third_party{ + {"Generic PLA @K2-all", "PLA-GENERIC", "PLA"}, + {"eSUN PLA+ @K2 Plus-all", "ESUN-PLA", "PLA"}, // shipped by Creality, branded eSUN + }; + CHECK(match(with_third_party, "eSUN", "", "PLA") == "ESUN-PLA"); + // ... and a Creality spool still prefers Creality's own generic over the eSUN preset, which + // carries Creality's profile too but adds a brand word the spool never claimed. + CHECK(match(with_third_party, "Creality", "", "PLA") == "PLA-GENERIC"); +}