mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-10 10:47:16 +00:00
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:
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user