redesign filament_id (#15513)

# Description

This PR redesigns `filament_id` across OrcaSlicer's profile library, so
that one filament
product now carries one consistent id everywhere it ships, instead of a
hand-written value that
unrelated materials routinely shared. Minting scripts and CI checks come
with it so future
profiles comply by construction: a new filament takes its id from the
tool, and the checks
reject a hand-written, duplicated or drifted one before it can merge.

Unique, non-duplicated ids are a precondition for AMS-style spool
syncing to be dependable —
the id is what a printer matches a physical spool against, and while two
products share one, the
match is a coin toss. This PR lays that groundwork. A follow-up PR will
publish an OrcaSlicer
materials reference on the wiki, giving every system profile one place
to point at.

`filament_id` names one filament product, and it is what a device
matches a physical spool
against: Bambu AMS, Creality CFS, the Qidi box, Klipper and Snapmaker
all resolve a tray to a
preset by id alone, first hit wins. Those ids were written by hand, and
on `main` 99 of them
stand for 674 different products — `GFL99` alone covers 132, from
Anycubic PLA to Bambu PLA
Matte. Every consequence is silent: a spool resolves to whichever preset
happens to load first,
tray names and support-material flags are read off the wrong material,
and the second preset
holding a duplicated id disappears from the tray-edit dialog entirely.

The id is now a hash of `(filament_vendor, filament_type, filament
name)`, so one spool product
carries one id in every bundle that ships it, two vendors shipping the
same product converge on it
without coordinating, and a collision cannot be authored by hand. Every
ambiguity across 48
vendors is fixed rather than excused — no grandfather list and no
per-vendor carve-out, Bambu's
bundle included — and the resulting landscape is frozen in
`scripts/filament_id_snapshot.json`,
so a change to any filament's identity lands as a reviewable diff to one
file. CI now runs the
duplicate-subtype validation tree-wide instead of over Bambu only.

Letting the id follow the product meant correcting the identities
themselves. Generics that
shipped under a vendor prefix now have one name and one id everywhere
(`Blocks Generic PETG` is
`Generic PETG @Blocks`), presets whose `filament_vendor` or
`filament_type` contradicted the
spool are fixed, and duplicate pairs are collapsed onto the
better-configured survivor. Renames
carry `renamed_from`, so existing projects and user presets keep
resolving.

Bambu's printers, its AMS and its cloud know only Bambu's own catalog
ids, so those ids leave
the profiles entirely. The Bambu bundle mints like every other vendor,
and the printer agent
swaps in the catalog value only where an id crosses to or from a Bambu
printer — outbound MQTT
and FTP, the AMS mapping sent with a job, the ids written into a 3mf the
printer will read —
mapping back on the way in, so status messages, SD-card prints and
projects saved by an older
Orca or by BambuStudio all still resolve. The correspondence is
generated from BambuStudio's
own shipped bundle by `scripts/update_bambu_filament_ids.py`; an id with
no row is forwarded
untouched, a missing or malformed map degrades to no translation rather
than taking the app
down, and an agent whose printers already speak Orca's ids translates
nothing.

Two device-side bugs this work surfaced are fixed here as well. The Orca
Filament Library was
missing from the AMS material and calibration dialogs, which treated a
filament with no
`compatible_printers` as compatible with nothing while the rest of the
app treats it as
compatible with everything; and Creality CFS sync on a K2-family stock
0.4 nozzle picked the
wrong preset, an imprecise matcher that duplicate ids had been masking.

`docs/HLSD/filament_id.md` is the authoring rule for all of this.
`scripts/orca_id_tool.py`
mints both `filament_id` and `setting_id`, replacing
`assign_vendor_setting_ids.py`, and the
local `check_profile` scripts gained per-vendor scoping so one vendor
can be checked without a
tree-wide run. `scripts/tests/` covers the tooling; `tests/slic3rutils/`
covers the boundary
translation and the CFS matcher.

Ids change for most products and nothing forwards the old value, so a
tray or a calibration
record still holding one falls back to matching by filament type until
the filament is selected
once. Beyond the profile fixes above no print settings change, except
that a few presets stop
claiming printers a dedicated variant already covers.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

`scripts/check_profile.sh`, the local twin of the "Check profiles" CI
job, with
`scripts/check_profile.bat` as its Windows entry point, passes on this
branch: the extra JSON
check reports no errors and no warnings, system validation and the now
tree-wide
filament-subtype check load all 66 vendors cleanly, all 1013 printer
presets slice, and
custom-preset validation passes against every fixture archive from
v1.9.0 to v2.4.2.

The id tooling has 176 unit tests (`python -m unittest discover -s
scripts/tests`). The C++
suites pass too — 332 in `tests/libslic3r` and 126 in
`tests/slic3rutils`, the latter including
the boundary-translation and Creality CFS matching cases added here.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
SoftFever
2026-09-07 19:22:11 +08:00
committed by GitHub
4460 changed files with 29822 additions and 13818 deletions

View File

@@ -545,7 +545,7 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::str
return "";
// Dedicated namespace for preset setting_ids, distinct from the cloud per-user
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/assign_vendor_setting_ids.py;
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_id_tool.py;
// never change this constant.
static const boost::uuids::uuid vendor_namespace =
boost::uuids::string_generator()("c1f4d9e2-7a3b-5c8d-9e0f-1a2b3c4d5e6f");

View File

@@ -93,8 +93,8 @@ class PresetBundle;
// Deterministic preset setting_id: uuid5(vendor/type/name) -> 16 base62 chars.
// Pure function of a system preset's identity, so the value can be assigned by
// scripts/assign_vendor_setting_ids.py and recomputed here when a profile ships
// without it. MUST stay byte-identical to scripts/assign_vendor_setting_ids.py.
// scripts/orca_id_tool.py and recomputed here when a profile ships without it.
// MUST stay byte-identical to scripts/orca_id_tool.py.
// This is NOT the per-user cloud-sync setting_id
// (OrcaCloudServiceAgent::generate_uuid_for_setting_id) - do not conflate them.
std::string generate_preset_setting_id(const std::string& vendor,

View File

@@ -3383,6 +3383,24 @@ std::vector<size_t> PresetBundle::physical_filament_config_indices() const
}
// Orca: the AMS lookups below resolve a tray's filament_id to the FIRST compatible base
// preset. When several presets match the same id for the selected printer the pick is
// arbitrary (a profile bug - see the validator's check_duplicate_filament_subtypes), so
// scan past a successful match and warn about the runners-up. Behavior is unchanged.
static void warn_ambiguous_filament_id_match(const PresetCollection &filaments, PresetCollection::ConstIterator match, const std::string &filament_id)
{
if (match == filaments.end())
return;
std::string others;
for (auto it = std::next(match); it != filaments.end(); ++it)
if (it->is_compatible && filaments.get_preset_base(*it) == &*it && it->filament_id == filament_id)
others += (others.empty() ? "\"" : ", \"") + it->name + "\"";
if (!others.empty())
BOOST_LOG_TRIVIAL(warning) << "Ambiguous AMS filament match: filament_id \"" << filament_id
<< "\" matches multiple presets compatible with the selected printer; picked \"" << match->name
<< "\", also matches " << others;
}
void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
{
combox_info.clear();
@@ -3405,6 +3423,7 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
}
auto iter = std::find_if(filaments.begin(), filaments.end(),
[this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
auto filament_type = ams.opt_string("filament_type", 0u);
@@ -3507,6 +3526,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
if (!filament_type.empty()) {
@@ -4019,6 +4039,9 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
return filament_infos;
}
// ORCA TODO: currently, this function assumes the printer name follows the pattern of "<printer_model> <nozzle_diameter>", e.g.
// printer_type: "Bambu Lab X2D", nozzle_diameter_str: "0.4 nozzle" => printer_name: "Bambu Lab X2D 0.4 nozzle". If the printer name does
// not follow this pattern, the function may not work correctly.
std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only)
{
std::set<std::string> printer_names;
@@ -4049,6 +4072,40 @@ std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle
return printer_names;
}
std::vector<Preset *> PresetBundle::get_filament_presets_for_machine(const std::string &printer_type,
const std::string &nozzle_diameter_str,
bool include_user_presets)
{
// Printer model plus nozzle diameter is expected to resolve to a single system printer preset;
// get_printer_names_by_printer_type_and_nozzle asserts as much in debug builds.
const std::set<std::string> printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str);
const Preset *printer = printer_names.empty() ? nullptr : printers.find_preset(*printer_names.begin());
if (printer == nullptr)
return {};
// Preset::is_visible is deliberately not consulted: it tracks what the Configuration Wizard
// installed, while the caller identifies a physically connected machine the user may never
// have installed - gating on it would empty the list for exactly those machines.
const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(*printer);
// Loop invariant - the two argument is_compatible_with_printer() would rebuild it per preset.
DynamicPrintConfig printer_config;
printer_config.set_key_value("printer_preset", new ConfigOptionString(printer->name));
if (const ConfigOption *opt = printer->config.option("nozzle_diameter"))
printer_config.set_key_value("num_extruders", new ConfigOptionInt((int) static_cast<const ConfigOptionFloats *>(opt)->values.size()));
std::vector<Preset *> compatible;
for (Preset &preset : filaments) {
/* The situation where the preset is not offered is as follows:
1. Not a root preset
2. Not a system preset and the printer firmware does not support user presets */
if (filaments.get_preset_base(preset) != &preset || (!preset.is_system && !include_user_presets))
continue;
if (is_compatible_with_printer(filaments.get_preset_with_vendor_profile(preset), active_printer, &printer_config))
compatible.push_back(&preset);
}
return compatible;
}
bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(
const std::string &printer_type, std::string& nozzle_diameter_str, std::string &setting_id, std::string &tag_uid, std::string &nozzle_temp_min, std::string &nozzle_temp_max, std::string& preset_setting_id)
{
@@ -4057,7 +4114,11 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m
std::map<std::string, std::vector<Preset const *>> filament_list = filaments.get_filament_presets();
std::set<std::string> printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str);
for (const Preset *preset : filament_list.find(setting_id)->second) {
auto filament_iter = filament_list.find(setting_id);
if (filament_iter == filament_list.end())
return is_equation;
for (const Preset *preset : filament_iter->second) {
if (tag_uid == "0" || (tag_uid.size() == 16 && tag_uid.substr(12, 2) == "01")) continue;
if (preset && !preset->is_user()) continue;
ConfigOption * printer_opt = const_cast<Preset *>(preset)->config.option("compatible_printers");
@@ -5232,8 +5293,8 @@ std::string PresetBundle::load_vendor_preset(
loaded.description = entry.description;
loaded.setting_id = entry.setting_id;
// Derive the preset setting_id on the fly when a profile ships without one,
// matching scripts/assign_vendor_setting_ids.py. Only instantiated presets
// carry an id; non-instantiated base profiles return earlier above. This never
// matching scripts/orca_id_tool.py. Only instantiated presets carry an id;
// non-instantiated base profiles return earlier above. This never
// touches the per-user cloud-sync setting_id written into user .info files.
if (loaded.setting_id.empty() && entry.instantiation == "true")
loaded.setting_id = generate_preset_setting_id(
@@ -6170,7 +6231,11 @@ bool PresetBundle::check_duplicate_filament_subtypes() const
// 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.
// preserving and avoids an O(all printers x all filaments) sweep. The one
// exception is the Orca Filament Library: its presets have empty
// compatible_printers (= compatible with every printer, minus the alias-shadowing
// exclusions that is_compatible_with_printer checks via m_excluded_from), so they
// are tested against every vendor's printers as well.
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)
@@ -6178,20 +6243,29 @@ bool PresetBundle::check_duplicate_filament_subtypes() const
filaments_by_vendor[preset.vendor->name].push_back(&preset);
}
const std::vector<const Preset *> no_filaments;
const auto library_it = filaments_by_vendor.find(ORCA_FILAMENT_LIBRARY);
const std::vector<const Preset *> &library_filaments = library_it == filaments_by_vendor.end() ? no_filaments : library_it->second;
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())
const std::vector<const Preset *> &vendor_filaments = vendor_it == filaments_by_vendor.end() ? no_filaments : vendor_it->second;
if (vendor_filaments.empty() && library_filaments.empty())
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)
for (const Preset *fil : vendor_filaments)
if (is_compatible_with_printer(filaments.get_preset_with_vendor_profile(*fil), active_printer))
by_filament_id[fil->filament_id].push_back(fil);
if (&vendor_filaments != &library_filaments)
for (const Preset *fil : library_filaments)
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)
@@ -6199,9 +6273,15 @@ bool PresetBundle::check_duplicate_filament_subtypes() const
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.
// A preset from another bundle (the Orca Filament Library) is tagged with
// its vendor so the source bundle is obvious.
std::string presets;
for (const Preset *p : entry.second)
presets += "\n - " + p->name + "\n " + preset_file_uri(p->file);
for (const Preset *p : entry.second) {
presets += "\n - " + p->name;
if (p->vendor != nullptr && p->vendor->name != printer.vendor->name)
presets += " [" + p->vendor->name + "]";
presets += "\n " + preset_file_uri(p->file);
}
BOOST_LOG_TRIVIAL(error)
<< "Ambiguous AMS filament match: " << entry.second.size()
<< " filament presets share filament_id \"" << entry.first

View File

@@ -350,6 +350,13 @@ public:
std::vector<std::vector<DynamicPrintConfig>> get_extruder_filament_info() const;
std::set<std::string> get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only = true);
// Orca: the root filament presets a connected machine can use, resolved with the rule the rest
// of the app applies (is_compatible_with_printer): an empty compatible_printers means every
// printer, minus the alias shadowing exclusions the Orca Filament Library records in
// Preset::m_excluded_from.
std::vector<Preset *> get_filament_presets_for_machine(const std::string &printer_type,
const std::string &nozzle_diameter_str,
bool include_user_presets);
bool check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(const std::string &printer_type,
std::string & nozzle_diameter_str,
std::string & setting_id,