From aa36ec9e4214c2cad5ec6aec63d3777adee7973e Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 23 Sep 2026 16:21:55 +0800 Subject: [PATCH] fix: resolve filament vendor/type across split base presets WebGuide's filament list dropped presets whose vendor and type came from different ancestors, and looped forever on inherits cycles. Walk the full inherits chain with a path-scoped guard, fill only missing values, and skip presets that still lack vendor or type. Add tests for split-base resolution and validate the real profile tree. --- scripts/tests/test_filament_id.py | 45 +++++++++++++++++++++ src/slic3r/GUI/WebGuideDialog.cpp | 67 +++++++++++++++++-------------- src/slic3r/GUI/WebGuideDialog.hpp | 4 ++ 3 files changed, 85 insertions(+), 31 deletions(-) diff --git a/scripts/tests/test_filament_id.py b/scripts/tests/test_filament_id.py index 001763fca9..fc62f5abc3 100644 --- a/scripts/tests/test_filament_id.py +++ b/scripts/tests/test_filament_id.py @@ -405,6 +405,20 @@ class TestTripleResolution(unittest.TestCase): self.assertEqual(afi.resolve_triple("MyPLA @P1", fmap, {}), ("MyVendor", "PLA", "MyPLA")) + def test_split_vendor_and_type_bases_resolve(self): + # A partial base is normal, not an error: vendor and type may live on + # different ancestors, with an intermediate supplying neither (the + # Snapmaker shape). The pair is complete at the instantiated preset. + recs = [ + self.rec("APLA @P1", inherits="mid"), + self.rec("mid", inherits="typebase"), + self.rec("typebase", filament_type=["PLA"], inherits="vendorbase"), + self.rec("vendorbase", filament_vendor=["AV"]), + ] + fmap = {r["name"]: r for r in recs} + self.assertEqual(afi.resolve_triple("APLA @P1", fmap, {}), + ("AV", "PLA", "APLA")) + # --------------------------------------------------------------------------- # checks on synthetic trees @@ -417,6 +431,24 @@ class TestChecks(OfCleanTreeCase): self.assertNotIn("[ERROR]", out) self.assertNotIn("[WARNING]", out) + def test_instantiated_preset_over_partial_bases_is_silent(self): + # Vendor and type split across two non-instantiated bases, an + # intermediate base with neither: base profiles are allowed to be + # partial. Only the instantiated preset must resolve both. + self.t.write_preset("VendorA", preset("XPLA vendorbase", instantiation=False, + filament_vendor="XV")) + self.t.write_preset("VendorA", preset("XPLA typebase", instantiation=False, + filament_type="PLA", + inherits="XPLA vendorbase")) + self.t.write_preset("VendorA", preset("XPLA mid", instantiation=False, + inherits="XPLA typebase")) + self.t.write_preset("VendorA", preset( + "XPLA @P1", inherits="XPLA mid", + filament_id=afi.generate_filament_id("XV", "PLA", "XPLA"), + compatible_printers=["P1"])) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + def test_check1_unknown_non_of_id(self): self.t.write_preset("VendorA", preset("BPLA @base", filament_id="BOGUS_9", instantiation=False, @@ -1523,6 +1555,19 @@ class TestRealTree(unittest.TestCase): self.assertEqual(analysis["missing_effective"], []) self.assertEqual(analysis["read_errors"], []) + def test_every_instantiated_filament_resolves_vendor_and_type(self): + # The property the web guide resolves at load: a partial base is fine as + # long as the instantiated preset ends up with both fields. Guards the + # split-base bundles (Snapmaker, Anker, SeeMeCNC). + analysis = afi.analyze_tree(REAL_PROFILES) + unresolved = [ + (vendor, rec["name"], rec["triple"][0], rec["triple"][1]) + for vendor, filaments in analysis["vendors"].items() + for rec in filaments.values() + if rec["instantiation"] and not (rec["triple"][0] and rec["triple"][1]) + ] + self.assertEqual(unresolved, []) + # --------------------------------------------------------------------------- # review-fix regressions # --------------------------------------------------------------------------- diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 78030c68da..9cea8bec28 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -1159,10 +1159,25 @@ bool GuideFrame::run() } int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType) +{ + std::unordered_set visiting; + return GetFilamentInfo(VendorDirectory, pFilaList, filepath, sVendor, sType, visiting); +} + +int GuideFrame::GetFilamentInfo(const std::string& VendorDirectory, json& pFilaList, + const std::string& filepath, std::string& sVendor, + std::string& sType, std::unordered_set& visiting) { //GetStardardFilePath(filepath); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " GetFilamentInfo:VendorDirectory - " << VendorDirectory << ", Filepath - "< #include #include +#include #include @@ -110,6 +111,9 @@ public: void on_dpi_changed(const wxRect &suggested_rect) {} private: + int GetFilamentInfo(const std::string& VendorDirectory, json& pFilaList, const std::string& filepath, + std::string& sVendor, std::string& sType, std::unordered_set& visiting); + GUI_App *m_MainPtr; AppConfig m_appconfig_new;