validator: detect ambiguous (duplicate) filament subtypes per printer (#14459)

* validator: detect duplicate filament subtype per printer (opt-in)

A filament is matched from the AMS by (filament_id + printer compatibility);
if two compatible filament presets for one printer share a filament_id, the
match is ambiguous and the runtime silently picks whichever loads first.

Add PresetBundle::check_duplicate_filament_subtypes(), gated behind a new
has_errors(check_duplicate_filament_subtypes) parameter and the validator's
-f/--check_filament_subtypes flag (off by default). For each system printer it
groups its vendor's compatible filament presets by filament_id and errors on
any group of 2+, reporting each preset as a clickable file:// URI with a single
"how to fix" hint. CI runs it for BBL only (-v BBL -f) until the other vendors'
profiles are cleaned up.

* profiles: fix ambiguous BBL filament matches

Resolve the duplicate-filament-subtype errors flagged by the validator:
- align compatible_printers with Bambu Studio where Orca over-claimed a nozzle
  that already has a dedicated preset (Bambu PLA Basic/Matte/ABS @BBL H2DP;
  Bambu ASA/PETG HF @BBL H2DP 0.6 nozzle; Fiberon PETG-ESD @BBL X1)
- fix a copy-pasted printer name in Overture Matte PLA @BBL A1M 0.2 nozzle
- fix a wrong inherits in Panchroma PLA Silk @BBL X1C 0.2 nozzle (was inheriting
  Panchroma PLA @base, giving it filament_id GFPM001 instead of GFPM004)

Bump BBL profile version.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
SoftFever
2026-06-28 00:42:02 +08:00
committed by GitHub
parent a0846ec215
commit c2e91cb86c
13 changed files with 134 additions and 21 deletions

View File

@@ -5471,7 +5471,7 @@ void PresetBundle::set_default_suppressed(bool default_suppressed)
printers.set_default_suppressed(default_suppressed);
}
bool PresetBundle::has_errors() const
bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const
{
if (m_errors != 0 || printers.m_errors != 0 || filaments.m_errors != 0 || prints.m_errors != 0)
return true;
@@ -5491,9 +5491,106 @@ bool PresetBundle::has_errors() const
}
}
if (check_duplicate_filament_subtypes && this->check_duplicate_filament_subtypes())
has_errors = true;
return has_errors;
}
// Orca: turn a preset file path into an absolute file:// URI for log messages.
// Printed unquoted on its own line, this is the one format clickable in both the
// VS Code integrated terminal (Cmd/Ctrl+click) and macOS Terminal.app
// (Cmd+double-click); quotes or literal spaces break link detection in both, so
// the characters that would terminate the URI token are percent-encoded.
static std::string preset_file_uri(const std::string &file)
{
std::string path;
try {
path = boost::filesystem::canonical(file).generic_string();
} catch (...) {
path = file;
}
std::string uri = "file://";
if (path.empty() || path.front() != '/')
uri += '/'; // Windows drive paths (e.g. C:/...) need the extra leading slash
for (char c : path) {
switch (c) {
case ' ': uri += "%20"; break;
case '#': uri += "%23"; break;
case '%': uri += "%25"; break;
default: uri += c;
}
}
return uri;
}
// Orca: a filament is matched from the AMS by (filament_id + printer compatibility).
// For any one printer, at most one instantiated filament preset with a given
// filament_id may be compatible - otherwise the AMS match is ambiguous and the
// runtime silently picks whichever loads first. This validator-only check flags
// any printer that has two or more compatible filament presets sharing a filament_id.
bool PresetBundle::check_duplicate_filament_subtypes() const
{
// Pre-collect system filament presets (each carries its effective filament_id,
// inherited from its @base at load time), grouped by vendor so we only test a
// printer against its own vendor's filaments. A vendor's compatible_printers
// only names that vendor's printers, so same-vendor scoping is correctness
// preserving and avoids an O(all printers x all filaments) sweep.
std::map<std::string, std::vector<const Preset *>> filaments_by_vendor;
for (const auto &preset : filaments) {
if (!preset.is_system || preset.filament_id.empty() || preset.vendor == nullptr)
continue;
filaments_by_vendor[preset.vendor->name].push_back(&preset);
}
bool found_duplicates = false;
for (const auto &printer : printers) {
if (!printer.is_system || printer.vendor == nullptr)
continue;
auto vendor_it = filaments_by_vendor.find(printer.vendor->name);
if (vendor_it == filaments_by_vendor.end())
continue;
const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer);
// std::map keeps the reported errors in a deterministic (sorted) order.
std::map<std::string, std::vector<const Preset *>> by_filament_id;
for (const Preset *fil : vendor_it->second)
if (is_compatible_with_printer(filaments.get_preset_with_vendor_profile(*fil), active_printer))
by_filament_id[fil->filament_id].push_back(fil);
for (const auto &entry : by_filament_id) {
if (entry.second.size() < 2)
continue;
found_duplicates = true;
// List each conflicting preset with a clickable file:// URI on its own
// line, so the profile author can jump straight to the files to fix.
std::string presets;
for (const Preset *p : entry.second)
presets += "\n - " + p->name + "\n " + preset_file_uri(p->file);
BOOST_LOG_TRIVIAL(error)
<< "Ambiguous AMS filament match: " << entry.second.size()
<< " filament presets share filament_id \"" << entry.first
<< "\" and are all compatible with printer \"" << printer.name
<< "\". When matching an AMS spool the slicer cannot tell them apart and"
" silently picks whichever loads first." << presets;
}
}
// Print the troubleshooting guidance once, not per error, to keep the log readable.
if (found_duplicates)
BOOST_LOG_TRIVIAL(error) << "\n========================================\n"
<< "How to fix \"Ambiguous AMS filament match\" errors: make sure only ONE filament"
" preset with a given filament_id is compatible with each printer. Either"
"\n (a) remove the overlapping printer from a preset's \"compatible_printers\""
" list (e.g. a '@printer' preset over-claiming a nozzle that already has its own"
" '@printer 0.x nozzle' preset), or"
"\n (b) if these are genuinely different materials, give each its own"
" \"filament_id\" - a common cause is a wrong \"inherits\" pointing at another"
" material's @base preset.";
return found_duplicates;
}
// Orca: BundleMetadata method implementations
bool BundleMetadata::load_from_json(const std::string& path)
{