From 2c0867619c1276454c9c6f9ae73c99de00e35f06 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 4 Jul 2026 16:28:35 +0800 Subject: [PATCH] v3.0: content-addressed mint tooling, succession runtime, W1 hardening Implements phase v3.0 of filament_id_plan_v3.md - tooling and client prerequisites. No filament_id values change in this commit. Tooling (scripts/assign_filament_ids.py; 106 unit tests): - The mint key becomes "filament_product/// ", resolved loader-faithfully from the declarer's flattened config - bundle-independent and content-addressed; identity fixes re-id by design and are made safe by the succession ledger. --mint now takes the triple. - The snapshot gains "triples" and "triple_exceptions" sections, folded into the equality gate: any vendor/type/name change surfaces as a reviewable snapshot diff. Check 3 rewritten to triple-mint conformance with (id, triple) grandfathering; check 5 generalized from OFL generics to every OFL-riding vendor preset; new check 8 (triple integrity) and check 9 (succession integrity). - The retired ledger moves to resources/profiles/retired_filament_ids.json so it ships with the app; schema {claims, successor} plus cross-island "hints". The 14 v1 entries are migrated with mode-rule successors. Vanished ids in a foreign island's space (GF*/QD_*) are RELEASED with a hint instead of retired: the island catalog owns them and may legitimately (re)ship them, which check 4 must never block. - New maintenance modes: --remint VENDOR, --drop-redundant-ids VENDOR, --add-hint "OLD=NEW", and --update-snapshot --forget-never-shipped FILE (never-shipped ids drop from lineage; chains splice through them). Runtime (C++): - Succession helpers in libslic3r/Preset (pure chain-follow + lazy ledger load from resources), consulted only on resolution miss in get_filament_by_filament_id, both AMS sync predicates, add_ams_filaments, setting_id_to_type, and the calibration-history lookup; behavior is byte-identical while the ledger has no matching entry. Catch2 coverage in tests/libslic3r. - W1 hardening: check_ams_filament_valid no longer remote-wipes trays or rewrites temps for P-shaped ids that a system preset carries (ten such system ids ship today); the size==8 && [0]=='P' assert is relaxed; the unguarded filament_list find deref in the temp-equation check returns non-destructively on a miss. - MoonrakerPrinterAgent: the 23 hardcoded OFL generic ids are replaced by a runtime lookup of "Generic @System" (id-equivalent on today's shipped profiles, follows future re-mints automatically); scripts/test_moonraker_lane_data.py derives its expectations from the shipped profiles and gains --check-ofl-map. Profile data (W3 - type is now a key component, so type bugs are fixed before any re-mint): - 17 same-name-different-type groups corrected across 50 files (OFL Generic PETG-CF/PE-CF/PP-CF; Flashforge ASA Basic/ASA-CF/ABS-CF/HIPS/ PAHT-CF/PLA Silk; FusRock PAHT; Creality Generic PA6-CF; InfiMech PETG; Anycubic TPU 95A / TPU for ACE; Prusa Generic PA-CF/PLA-CF) - every value validated against MaterialType::all(); 3 Snapmaker U1 roots gain their missing filament_vendor. Flattened-config equivalence vs the previous commit: exactly the 54 intended diffs, zero BBL. - doc/developer-reference/filament_id.md rewritten for the v3 rule. Ambiguous type divergences (Prusa Generic TPU/TPU HF FLEX-vs-TPU, FusRock S-Multi/S-PAHT) and all same-type vendor-tag divergences are deferred to the v3.2 vendor worksheets, catalogued with analysis. --- doc/developer-reference/filament_id.md | 74 +- filament_id_plan_v3.md | 12 +- ...95A @Anycubic Kobra S1 Max 0.6 nozzle.json | 2 +- ...ACE @Anycubic Kobra S1 Max 0.4 nozzle.json | 2 +- ...ACE @Anycubic Kobra S1 Max 0.6 nozzle.json | 2 +- .../Generic PA6-CF @K1 Max_CFS-C-all.json | 2 +- .../filament/Generic PA6-CF @K1C-all.json | 2 +- .../Generic PA6-CF @K1C_CFS-C-all.json | 2 +- .../Generic PA6-CF @K1_CFS-C-all.json | 2 +- .../filament/Generic PA6-CF @K2 Plus-all.json | 2 +- .../filament/Generic PA6-CF @K2 Pro-all.json | 2 +- .../filament/Flashforge ABS-CF @FF G4.json | 3 + .../filament/Flashforge ABS-CF @FF G4P.json | 3 + ...shforge ASA Basic @FF AD5X 0.6 nozzle.json | 3 + ...shforge ASA Basic @FF AD5X 0.8 nozzle.json | 3 + .../Flashforge ASA Basic @FF AD5X.json | 3 + .../filament/Flashforge ASA-CF @FF G4.json | 2 +- .../filament/Flashforge ASA-CF @FF G4P.json | 2 +- .../filament/Flashforge ASA-CF.json | 2 +- .../Flashforge HIPS @FF G4 0.6 HF nozzle.json | 3 + .../Flashforge HIPS @FF G4 0.6 nozzle.json | 3 + .../Flashforge HIPS @FF G4 0.8 HF nozzle.json | 3 + ...Flashforge HIPS @FF G4P 0.6 HF nozzle.json | 3 + .../Flashforge HIPS @FF G4P 0.6 nozzle.json | 3 + ...Flashforge HIPS @FF G4P 0.8 HF nozzle.json | 3 + .../filament/Flashforge PAHT-CF @FF G4.json | 2 +- .../filament/Flashforge PAHT-CF @FF G4P.json | 2 +- ...shforge PLA Silk @FF AD5X 0.25 nozzle.json | 2 +- ...ashforge PLA Silk @FF AD5X 0.6 nozzle.json | 2 +- ...ashforge PLA Silk @FF AD5X 0.8 nozzle.json | 2 +- .../Flashforge PLA Silk @FF AD5X.json | 2 +- .../filament/Flashforge PLA Silk @FF C5.json | 2 +- .../filament/Flashforge PLA Silk @FF C5P.json | 2 +- .../FusRock PAHT @FF G4 0.6 HF nozzle.json | 3 + .../FusRock PAHT @FF G4 0.8 HF nozzle.json | 3 + .../FusRock PAHT @FF G4P 0.6 HF nozzle.json | 3 + .../FusRock PAHT @FF G4P 0.8 HF nozzle.json | 3 + .../filament/FusRock PAHT-CF @FF G4.json | 2 +- .../filament/FusRock PAHT-CF @FF G4P.json | 2 +- .../FusRock/FusRock Generic PAHT-CF.json | 2 +- .../filament/Generic PLA Silk @FF C5.json | 2 +- .../filament/Generic PLA Silk @FF C5P.json | 2 +- .../EX+APS/fdm_filament_pet @EX+APS.json | 2 +- .../filament/EX/fdm_filament_pet @EX.json | 2 +- .../filament/Generic PE-CF @base.json | 5 +- .../filament/Generic PETG-CF @base.json | 5 +- .../filament/Generic PP-CF @base.json | 5 +- .../filament/Prusa Generic PA-CF @base.json | 5 +- .../filament/Prusa Generic PLA-CF @base.json | 5 +- .../filament/Snapmaker PLA Lite @U1 base.json | 3 + .../Snapmaker PLA Matte @U1 base.json | 3 + .../Snapmaker PLA SnapSpeed @U1 base.json | 3 + resources/profiles/retired_filament_ids.json | 90 + scripts/assign_filament_ids.py | 941 +- scripts/filament_id_snapshot.json | 11001 +++++++++++++++- scripts/retired_filament_ids.json | 47 - scripts/test_moonraker_lane_data.py | 115 +- scripts/tests/test_filament_id.py | 917 +- src/libslic3r/Preset.cpp | 54 + src/libslic3r/Preset.hpp | 17 + src/libslic3r/PresetBundle.cpp | 31 +- src/slic3r/GUI/CaliHistoryDialog.cpp | 8 +- src/slic3r/GUI/DeviceManager.cpp | 42 +- src/slic3r/GUI/PresetComboBoxes.cpp | 7 + src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 90 +- tests/libslic3r/CMakeLists.txt | 1 + .../libslic3r/test_filament_id_succession.cpp | 61 + 67 files changed, 13311 insertions(+), 330 deletions(-) create mode 100644 resources/profiles/retired_filament_ids.json delete mode 100644 scripts/retired_filament_ids.json create mode 100644 tests/libslic3r/test_filament_id_succession.cpp diff --git a/doc/developer-reference/filament_id.md b/doc/developer-reference/filament_id.md index a45268b6aa..377c390422 100644 --- a/doc/developer-reference/filament_id.md +++ b/doc/developer-reference/filament_id.md @@ -69,9 +69,12 @@ the `Generic X` base name, write no id key). `Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library preset on your printers), sets a non-empty `compatible_printers`, and writes no id key. A vendor-*branded* filament never rides a generic family id. -5. **Ids are immutable once shipped.** Renaming a family does not change its id — use - `renamed_from`. A shipped id is never recycled for a different material: old ids live on in - user presets and 3mf files and a recycled id would silently match the wrong material. +5. **Ids follow the product identity.** The id is a pure function of the product triple + `(filament_vendor, filament_type, family name)` — correcting any of them re-mints the id + **by design**, and `--update-snapshot` records the old id in the shipped succession ledger + with its successor so device trays, calibration records, and user presets keep resolving + (`renamed_from` still gates preset-*name* compatibility as before). A shipped id is never + recycled for a different material: retired ids are blocked forever. ## Minting — nobody invents ids @@ -81,15 +84,20 @@ New ids are deterministic, computed exactly like the `setting_id` precedent ```text FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id") = c4d3ff49-4c32-5534-a3e3-00894157ab97 -filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE, "filament_family//") ) +filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE, + "filament_product///") ) ``` `base62_6` is the low 6 base62 digits (alphabet `0-9A-Za-z`) of the UUID taken as a big-endian -integer, most-significant digit first — 8 chars total, within the AMS length limit. `` -is the vendor bundle name (the stem of the vendor index json, e.g. `Qidi`, -`OrcaFilamentLibrary`); `` is the family base name. On the rare collision with any -existing or retired id, the minter salts the input (`…/1`, `…/2`, …) until free and the result -is frozen in the file. Example: family `MyBrand PLA` in vendor `Orca 3D` mints `OFvCEY5V`. +integer, most-significant digit first — 8 chars total, within the AMS length limit. The triple +comes from the family root's *flattened* config: `` is the filament +**manufacturer** (`"Polymaker"`, or `"Generic"` for generics — never the printer brand), +`` the material type, `` the root's base name; the two config +values are inheritable list options and the first element counts. The key contains no bundle +name, so the same product mints the same id in every bundle — hoisting a family into +OrcaFilamentLibrary never changes its id. On the rare collision with any existing or retired +id, the minter salts the input (`…/1`, `…/2`, …) until free and the result is frozen in the +file. Example: `Polymaker/PLA/PolyLite PLA` mints `OF5CgdDq`. Workflow for a new family: @@ -101,8 +109,12 @@ python scripts/assign_filament_ids.py --check # 4. verify — the sam # 5. Commit the profile edits together with scripts/filament_id_snapshot.json. ``` -`--mint "Vendor/Family"` prints the id for one family without touching anything. The default -run is idempotent and never rewrites a valid existing id. +`--mint "filament_vendor/filament_type/family_name"` prints the id a triple would mint without +touching anything. The default run is idempotent and never rewrites a valid existing id. +Maintenance modes (normally only used by id migrations): `--remint VENDOR` re-derives a +vendor's declared ids from their triples, `--drop-redundant-ids VENDOR` deletes declarations +that merely re-declare an inherited OFL id, and `--add-hint "OLD=NEW"` records a cross-island +succession hint. If you skip the tooling, CI fails and prints the remedy: the expected id for your family, and the instruction to run `python scripts/assign_filament_ids.py --update-snapshot` and commit @@ -116,7 +128,20 @@ the resulting diff. | `QD_*` | Qidi device protocol | frozen device contract; Qidi vendor only | | `P` + 7 hex chars (case-insensitive), `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp`) | never appears in system profiles | | every already-shipped id | grandfather snapshot | frozen as-is; new claims need maintainer sign-off | -| every retired id | `scripts/retired_filament_ids.json` | never used again, for anything | +| every retired id | `resources/profiles/retired_filament_ids.json` | never used again, for anything | + +## The succession ledger + +`resources/profiles/retired_filament_ids.json` ships with the app. Each retired id maps to +`{"claims": [...], "successor": }` — successor chains are followed to the live end — +and a `hints` map carries the same forwarding for ids Orca cannot retire because another +island owns them (e.g. a `GF*` id whose material also exists as an OFL family). The client +consults the ledger **only on resolution miss** (AMS tray sync, tray-id type lookup, +calibration history, filament-id preset lookup): a live preset always wins first, so BBL +installs resolve `GF*` natively and behavior is unchanged wherever the raw id still exists. +This is what makes identity-driven re-mints (structure rule 5) safe: the old id keeps +resolving to the family's current preset instead of degrading to a `Generic ` fallback. +The file is append-only and maintained exclusively by `--update-snapshot` / `--add-hint`. ## How CI enforces this @@ -131,14 +156,22 @@ deterministically (running it twice changes nothing). The checks, in brief: - **Format** — every id is either in the snapshot, `OF` + 6 base62 chars, BBL's, or Qidi `QD_*`. -- **Snapshot equality** — tree claims == snapshot claims, both directions. -- **Mint conformance** — a non-grandfathered `OF*` claim must equal the mint (or a salt - iteration) of its `(vendor, family)`; the error prints the expected id. -- **Retired reuse** — any tree id present in `scripts/retired_filament_ids.json` is an error. +- **Snapshot equality** — tree claims == snapshot claims **and** tree triples == snapshot + triples, both directions: any `filament_vendor`/`filament_type`/family-name change surfaces + as a snapshot diff. +- **Mint conformance** — a non-grandfathered `OF*` id must equal the mint (or a salt + iteration) of its declarer's product triple; the error prints the expected id. +- **Retired reuse** — any tree id present in `resources/profiles/retired_filament_ids.json` is an error. Ids that fully vanish from the tree are appended there by `--update-snapshot`; the file is **append-only**. -- **Alias hygiene** — a tuned generic must keep the library preset's base name and a non-empty - `compatible_printers` (structure rule 4); the error names the rename as the cause. +- **Alias hygiene** — any vendor preset riding an OFL family id must keep the library + preset's base name, a non-empty `compatible_printers`, and no own id key (structure rule 4); + the error names the rename as the cause. +- **Triple integrity** — every declarer outside the BBL/`QD_*` islands must resolve a + non-empty `filament_vendor` and `filament_type` (generics use `"Generic"`), and all + declarers of one family within a bundle must agree on the triple. +- **Succession integrity** — retired successor chains terminate at a live id (or null) with + no cycles; `hints` keys are live, island-owned ids. - **Reserved namespaces** — `GF*` outside BBL, `QD_*` outside Qidi, `P<7-hex>` or `null` anywhere, unless that exact claim is grandfathered in the snapshot. - **Structure** — no `filament_id` key on instantiated presets; no declared-vs-inherited id @@ -161,7 +194,10 @@ check instead. inherit the family's root, write no id key. - **A tuned generic ("our profile for Generic PLA")?** Inherit `Generic PLA @System`, keep the `Generic PLA` base name, set `compatible_printers`, write no id key. -- **I need to rename a family.** Keep the id, add `renamed_from`. Ids never change. +- **I need to rename a family (or fix its `filament_vendor`/`filament_type`).** Add + `renamed_from` for the name, run `--remint ` then `--update-snapshot`: the id + re-derives from the corrected identity and the old id lands in the succession ledger + pointing at the new one. Commit the profile, snapshot, and ledger diffs together. - **CI says my family needs an id.** Run `python scripts/assign_filament_ids.py`, then `--update-snapshot`, and commit both diffs. Do not type an id by hand. diff --git a/filament_id_plan_v3.md b/filament_id_plan_v3.md index 5cf4cf61a4..5a4bdedb26 100644 --- a/filament_id_plan_v3.md +++ b/filament_id_plan_v3.md @@ -84,7 +84,12 @@ holding OFL-only materials, on-device PA-calibration records, user-root preset c followed to the live end). A retired id may never be minted again (check 4 unchanged). Cross-island *hint* entries are permitted for ids Orca cannot retire because another island owns them (e.g. `GFL99 → `): consulted only when no live - preset matches, so BBL installs still resolve `GFL99` natively first. + preset matches, so BBL installs still resolve `GFL99` natively first. By the same + principle, an id in a foreign island's space (`GF*`/`QD_*`) that vanishes from the tree + is **released with a hint, never retired** — the island's catalog owns it and may + legitimately (re)ship it later, which check 4 must never block. (Implementation + amendment, v3.0: `--update-snapshot` routes such ids to `hints` automatically; hint + keys may be absent from the tree.) - **Shipped and consulted at runtime.** The ledger ships in `resources/`; a small helper (`resolve_filament_id_succession(id)`) follows the chain and is consulted **only on resolution miss**, before the `Generic ` name fallback, in: @@ -134,8 +139,9 @@ those families twice. **v3.1 — OFL re-mint.** Every OFL-declared id re-derives from its triple (mirrors like `OGFA00`, generics like `OGFL99`, blocks like `OEPLAB00` — all of it); each old shipped id -gains a succession entry pointing at its replacement; the 26 BBL×OFL byte-shared ids -dissolve (BBL keeps its id; the OFL family gets its own — two-island purity). Snapshot +gains a succession entry pointing at its replacement; the 26 GF-shaped ids OFL declares +(measured at v3.0: only one of them, `GFOT001`, is also declared by BBL) are released to +the BBL island space with hints at the re-minted families — two-island purity. Snapshot regenerated; full gate battery + fixture overlays (this phase touches preset-visibility machinery only via ids, but the fixtures are cheap insurance). diff --git a/resources/profiles/Anycubic/filament/Anycubic TPU 95A @Anycubic Kobra S1 Max 0.6 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic TPU 95A @Anycubic Kobra S1 Max 0.6 nozzle.json index ababa2bbb9..6b5699d6eb 100644 --- a/resources/profiles/Anycubic/filament/Anycubic TPU 95A @Anycubic Kobra S1 Max 0.6 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic TPU 95A @Anycubic Kobra S1 Max 0.6 nozzle.json @@ -13,7 +13,7 @@ "Anycubic" ], "filament_type": [ - "PEBA" + "TPU" ], "compatible_printers": [ "Anycubic Kobra S1 Max 0.6 nozzle" diff --git a/resources/profiles/Anycubic/filament/Anycubic TPU for ACE @Anycubic Kobra S1 Max 0.4 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic TPU for ACE @Anycubic Kobra S1 Max 0.4 nozzle.json index 7f8d97a333..7d4b5e30c3 100644 --- a/resources/profiles/Anycubic/filament/Anycubic TPU for ACE @Anycubic Kobra S1 Max 0.4 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic TPU for ACE @Anycubic Kobra S1 Max 0.4 nozzle.json @@ -13,7 +13,7 @@ "Anycubic" ], "filament_type": [ - "TPU for ACE" + "TPU" ], "compatible_printers": [ "Anycubic Kobra S1 Max 0.4 nozzle" diff --git a/resources/profiles/Anycubic/filament/Anycubic TPU for ACE @Anycubic Kobra S1 Max 0.6 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic TPU for ACE @Anycubic Kobra S1 Max 0.6 nozzle.json index e9b2079dff..7b820c0957 100644 --- a/resources/profiles/Anycubic/filament/Anycubic TPU for ACE @Anycubic Kobra S1 Max 0.6 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic TPU for ACE @Anycubic Kobra S1 Max 0.6 nozzle.json @@ -13,7 +13,7 @@ "Anycubic" ], "filament_type": [ - "PEBA" + "TPU" ], "compatible_printers": [ "Anycubic Kobra S1 Max 0.6 nozzle" diff --git a/resources/profiles/Creality/filament/Generic PA6-CF @K1 Max_CFS-C-all.json b/resources/profiles/Creality/filament/Generic PA6-CF @K1 Max_CFS-C-all.json index c9aa3f2918..82cd2c4faa 100644 --- a/resources/profiles/Creality/filament/Generic PA6-CF @K1 Max_CFS-C-all.json +++ b/resources/profiles/Creality/filament/Generic PA6-CF @K1 Max_CFS-C-all.json @@ -87,7 +87,7 @@ "0" ], "filament_type": [ - "PA-CF" + "PA6-CF" ], "filament_vendor": [ "Generic" diff --git a/resources/profiles/Creality/filament/Generic PA6-CF @K1C-all.json b/resources/profiles/Creality/filament/Generic PA6-CF @K1C-all.json index c3108d354a..12f1662b2d 100644 --- a/resources/profiles/Creality/filament/Generic PA6-CF @K1C-all.json +++ b/resources/profiles/Creality/filament/Generic PA6-CF @K1C-all.json @@ -87,7 +87,7 @@ "0" ], "filament_type": [ - "PA-CF" + "PA6-CF" ], "filament_vendor": [ "Generic" diff --git a/resources/profiles/Creality/filament/Generic PA6-CF @K1C_CFS-C-all.json b/resources/profiles/Creality/filament/Generic PA6-CF @K1C_CFS-C-all.json index d745af0009..9e3a508a5d 100644 --- a/resources/profiles/Creality/filament/Generic PA6-CF @K1C_CFS-C-all.json +++ b/resources/profiles/Creality/filament/Generic PA6-CF @K1C_CFS-C-all.json @@ -87,7 +87,7 @@ "0" ], "filament_type": [ - "PA-CF" + "PA6-CF" ], "filament_vendor": [ "Generic" diff --git a/resources/profiles/Creality/filament/Generic PA6-CF @K1_CFS-C-all.json b/resources/profiles/Creality/filament/Generic PA6-CF @K1_CFS-C-all.json index 6eb1569816..ee75cdd734 100644 --- a/resources/profiles/Creality/filament/Generic PA6-CF @K1_CFS-C-all.json +++ b/resources/profiles/Creality/filament/Generic PA6-CF @K1_CFS-C-all.json @@ -87,7 +87,7 @@ "0" ], "filament_type": [ - "PA-CF" + "PA6-CF" ], "filament_vendor": [ "Generic" diff --git a/resources/profiles/Creality/filament/Generic PA6-CF @K2 Plus-all.json b/resources/profiles/Creality/filament/Generic PA6-CF @K2 Plus-all.json index 162ec5e7d1..71c4ef7a31 100644 --- a/resources/profiles/Creality/filament/Generic PA6-CF @K2 Plus-all.json +++ b/resources/profiles/Creality/filament/Generic PA6-CF @K2 Plus-all.json @@ -84,7 +84,7 @@ "0" ], "filament_type": [ - "PA-CF" + "PA6-CF" ], "filament_vendor": [ "Generic" diff --git a/resources/profiles/Creality/filament/Generic PA6-CF @K2 Pro-all.json b/resources/profiles/Creality/filament/Generic PA6-CF @K2 Pro-all.json index 6049b3b882..5c71b8ea97 100644 --- a/resources/profiles/Creality/filament/Generic PA6-CF @K2 Pro-all.json +++ b/resources/profiles/Creality/filament/Generic PA6-CF @K2 Pro-all.json @@ -84,7 +84,7 @@ "0" ], "filament_type": [ - "PA-CF" + "PA6-CF" ], "filament_vendor": [ "Generic" diff --git a/resources/profiles/Flashforge/filament/Flashforge ABS-CF @FF G4.json b/resources/profiles/Flashforge/filament/Flashforge ABS-CF @FF G4.json index b45e46341e..f1917abb9a 100644 --- a/resources/profiles/Flashforge/filament/Flashforge ABS-CF @FF G4.json +++ b/resources/profiles/Flashforge/filament/Flashforge ABS-CF @FF G4.json @@ -39,6 +39,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material: ABS-CF\n" ], + "filament_type": [ + "ABS-CF" + ], "compatible_printers": [ "Flashforge Guider4 0.4 nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge ABS-CF @FF G4P.json b/resources/profiles/Flashforge/filament/Flashforge ABS-CF @FF G4P.json index 16bcc1aeb3..03194dd913 100644 --- a/resources/profiles/Flashforge/filament/Flashforge ABS-CF @FF G4P.json +++ b/resources/profiles/Flashforge/filament/Flashforge ABS-CF @FF G4P.json @@ -39,6 +39,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material: ABS-CF\n" ], + "filament_type": [ + "ABS-CF" + ], "compatible_printers": [ "Flashforge Guider4 Pro 0.4 nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X 0.6 nozzle.json b/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X 0.6 nozzle.json index bf4a2d3194..eb123cc3ad 100644 --- a/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X 0.6 nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X 0.6 nozzle.json @@ -27,6 +27,9 @@ "filament_settings_id": [ "Flashforge ASA Basic @FF AD5X 0.6 nozzle" ], + "filament_type": [ + "ASA" + ], "compatible_printers": [ "Flashforge AD5X 0.6 nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X 0.8 nozzle.json b/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X 0.8 nozzle.json index f74bcf4ab7..2e7ff56b63 100644 --- a/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X 0.8 nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X 0.8 nozzle.json @@ -27,6 +27,9 @@ "filament_settings_id": [ "Flashforge ASA Basic @FF AD5X 0.8 nozzle" ], + "filament_type": [ + "ASA" + ], "compatible_printers": [ "Flashforge AD5X 0.8 nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X.json b/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X.json index 4c3018a061..a63e1c7dfe 100644 --- a/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X.json +++ b/resources/profiles/Flashforge/filament/Flashforge ASA Basic @FF AD5X.json @@ -27,6 +27,9 @@ "filament_settings_id": [ "Flashforge ASA Basic @FF AD5X" ], + "filament_type": [ + "ASA" + ], "compatible_printers": [ "Flashforge AD5X 0.4 nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge ASA-CF @FF G4.json b/resources/profiles/Flashforge/filament/Flashforge ASA-CF @FF G4.json index d59ed639a3..62f2f92006 100644 --- a/resources/profiles/Flashforge/filament/Flashforge ASA-CF @FF G4.json +++ b/resources/profiles/Flashforge/filament/Flashforge ASA-CF @FF G4.json @@ -37,7 +37,7 @@ "; filament start gcode\n;right_extruder_material: ASA-CF\n" ], "filament_type": [ - "ASA" + "ASA-CF" ], "compatible_printers": [ "Flashforge Guider4 0.4 nozzle" diff --git a/resources/profiles/Flashforge/filament/Flashforge ASA-CF @FF G4P.json b/resources/profiles/Flashforge/filament/Flashforge ASA-CF @FF G4P.json index ed2467c6e7..8d88edb2ad 100644 --- a/resources/profiles/Flashforge/filament/Flashforge ASA-CF @FF G4P.json +++ b/resources/profiles/Flashforge/filament/Flashforge ASA-CF @FF G4P.json @@ -37,7 +37,7 @@ "; filament start gcode\n;right_extruder_material: ASA-CF\n" ], "filament_type": [ - "ASA" + "ASA-CF" ], "compatible_printers": [ "Flashforge Guider4 Pro 0.4 nozzle" diff --git a/resources/profiles/Flashforge/filament/Flashforge ASA-CF.json b/resources/profiles/Flashforge/filament/Flashforge ASA-CF.json index 30ead39e25..b400c7d253 100644 --- a/resources/profiles/Flashforge/filament/Flashforge ASA-CF.json +++ b/resources/profiles/Flashforge/filament/Flashforge ASA-CF.json @@ -16,7 +16,7 @@ "Flashforge ASA-CF" ], "filament_type": [ - "ASA" + "ASA-CF" ], "hot_plate_temp": [ "100" diff --git a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.6 HF nozzle.json b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.6 HF nozzle.json index cd5ca2143f..9622514949 100644 --- a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.6 HF nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.6 HF nozzle.json @@ -63,6 +63,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:HIPS" ], + "filament_type": [ + "HIPS" + ], "compatible_printers": [ "Flashforge Guider4 0.6 HF nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.6 nozzle.json b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.6 nozzle.json index e4b4e3171d..1f6bec0997 100644 --- a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.6 nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.6 nozzle.json @@ -63,6 +63,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:HIPS" ], + "filament_type": [ + "HIPS" + ], "compatible_printers": [ "Flashforge Guider4 0.6 nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.8 HF nozzle.json b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.8 HF nozzle.json index 03cf58e9e0..684f303f65 100644 --- a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.8 HF nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4 0.8 HF nozzle.json @@ -63,6 +63,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:HIPS" ], + "filament_type": [ + "HIPS" + ], "compatible_printers": [ "Flashforge Guider4 0.8 HF nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.6 HF nozzle.json b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.6 HF nozzle.json index 7ea19866be..2468478e6a 100644 --- a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.6 HF nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.6 HF nozzle.json @@ -63,6 +63,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:HIPS" ], + "filament_type": [ + "HIPS" + ], "compatible_printers": [ "Flashforge Guider4 Pro 0.6 HF nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.6 nozzle.json b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.6 nozzle.json index 3a44280bc2..7b28d67e08 100644 --- a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.6 nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.6 nozzle.json @@ -63,6 +63,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:HIPS" ], + "filament_type": [ + "HIPS" + ], "compatible_printers": [ "Flashforge Guider4 Pro 0.6 nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.8 HF nozzle.json b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.8 HF nozzle.json index 5943947947..4356a46961 100644 --- a/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.8 HF nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge HIPS @FF G4P 0.8 HF nozzle.json @@ -63,6 +63,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:HIPS" ], + "filament_type": [ + "HIPS" + ], "compatible_printers": [ "Flashforge Guider4 Pro 0.8 HF nozzle" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge PAHT-CF @FF G4.json b/resources/profiles/Flashforge/filament/Flashforge PAHT-CF @FF G4.json index 784d279539..62f9c06854 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PAHT-CF @FF G4.json +++ b/resources/profiles/Flashforge/filament/Flashforge PAHT-CF @FF G4.json @@ -61,7 +61,7 @@ "; filament start gcode\n;right_extruder_material:PA-CF\n" ], "filament_type": [ - "PA-CF" + "PAHT-CF" ], "compatible_printers": [ "Flashforge Guider4 0.4 nozzle" diff --git a/resources/profiles/Flashforge/filament/Flashforge PAHT-CF @FF G4P.json b/resources/profiles/Flashforge/filament/Flashforge PAHT-CF @FF G4P.json index f5b823f2ea..b7f3bc5386 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PAHT-CF @FF G4P.json +++ b/resources/profiles/Flashforge/filament/Flashforge PAHT-CF @FF G4P.json @@ -61,7 +61,7 @@ "; filament start gcode\n;right_extruder_material:PA-CF\n" ], "filament_type": [ - "PA-CF" + "PAHT-CF" ], "compatible_printers": [ "Flashforge Guider4 Pro 0.4 nozzle" diff --git a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.25 nozzle.json b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.25 nozzle.json index 3074f553f0..e3d9c57369 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.25 nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.25 nozzle.json @@ -31,7 +31,7 @@ "Flashforge PLA Silk @FF AD5X 0.25 nozzle" ], "filament_type": [ - "SILK" + "PLA" ], "compatible_printers": [ "Flashforge AD5X 0.25 nozzle" diff --git a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.6 nozzle.json b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.6 nozzle.json index 9de378b7cc..058dce03c0 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.6 nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.6 nozzle.json @@ -22,7 +22,7 @@ "Flashforge PLA Silk @FF AD5X 0.6 nozzle" ], "filament_type": [ - "SILK" + "PLA" ], "compatible_printers": [ "Flashforge AD5X 0.6 nozzle" diff --git a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.8 nozzle.json b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.8 nozzle.json index c3dbf86193..c2f9796beb 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.8 nozzle.json +++ b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X 0.8 nozzle.json @@ -22,7 +22,7 @@ "Flashforge PLA Silk @FF AD5X 0.8 nozzle" ], "filament_type": [ - "SILK" + "PLA" ], "compatible_printers": [ "Flashforge AD5X 0.8 nozzle" diff --git a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X.json b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X.json index 5d7ff07d01..6ebc636b10 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X.json +++ b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF AD5X.json @@ -22,7 +22,7 @@ "Flashforge PLA Silk @FF AD5X" ], "filament_type": [ - "SILK" + "PLA" ], "hot_plate_temp": [ "60" diff --git a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF C5.json b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF C5.json index 069b924da4..7aa85e29bf 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF C5.json +++ b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF C5.json @@ -54,7 +54,7 @@ "; filament start gcode" ], "filament_type": [ - "SILK" + "PLA" ], "hot_plate_temp": [ "65" diff --git a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF C5P.json b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF C5P.json index 4c9b89d97e..67ca938c59 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF C5P.json +++ b/resources/profiles/Flashforge/filament/Flashforge PLA Silk @FF C5P.json @@ -57,7 +57,7 @@ "; filament start gcode" ], "filament_type": [ - "SILK" + "PLA" ], "nozzle_temperature": [ "230" diff --git a/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4 0.6 HF nozzle.json b/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4 0.6 HF nozzle.json index 06012ecf04..646f8971b2 100644 --- a/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4 0.6 HF nozzle.json +++ b/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4 0.6 HF nozzle.json @@ -36,6 +36,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:PA" ], + "filament_type": [ + "PAHT" + ], "compatible_printers": [ "Flashforge Guider4 0.6 HF nozzle" ], diff --git a/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4 0.8 HF nozzle.json b/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4 0.8 HF nozzle.json index 57380ab9e4..b0cb4812c8 100644 --- a/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4 0.8 HF nozzle.json +++ b/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4 0.8 HF nozzle.json @@ -36,6 +36,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:PA" ], + "filament_type": [ + "PAHT" + ], "compatible_printers": [ "Flashforge Guider4 0.8 HF nozzle" ], diff --git a/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4P 0.6 HF nozzle.json b/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4P 0.6 HF nozzle.json index c6b435b34b..dbccec4551 100644 --- a/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4P 0.6 HF nozzle.json +++ b/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4P 0.6 HF nozzle.json @@ -36,6 +36,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:PA" ], + "filament_type": [ + "PAHT" + ], "compatible_printers": [ "Flashforge Guider4 Pro 0.6 HF nozzle" ], diff --git a/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4P 0.8 HF nozzle.json b/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4P 0.8 HF nozzle.json index e69ba59f78..07e8702da2 100644 --- a/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4P 0.8 HF nozzle.json +++ b/resources/profiles/Flashforge/filament/FusRock PAHT @FF G4P 0.8 HF nozzle.json @@ -36,6 +36,9 @@ "filament_start_gcode": [ "; filament start gcode\n;right_extruder_material:PA" ], + "filament_type": [ + "PAHT" + ], "compatible_printers": [ "Flashforge Guider4 Pro 0.8 HF nozzle" ], diff --git a/resources/profiles/Flashforge/filament/FusRock PAHT-CF @FF G4.json b/resources/profiles/Flashforge/filament/FusRock PAHT-CF @FF G4.json index ac82265489..2e264e4b2f 100644 --- a/resources/profiles/Flashforge/filament/FusRock PAHT-CF @FF G4.json +++ b/resources/profiles/Flashforge/filament/FusRock PAHT-CF @FF G4.json @@ -55,7 +55,7 @@ "; filament start gcode\n;right_extruder_material:PA-CF\n" ], "filament_type": [ - "PA-CF" + "PAHT-CF" ], "compatible_printers": [ "Flashforge Guider4 0.4 nozzle" diff --git a/resources/profiles/Flashforge/filament/FusRock PAHT-CF @FF G4P.json b/resources/profiles/Flashforge/filament/FusRock PAHT-CF @FF G4P.json index 71b3ae9af1..0aaa19dd68 100644 --- a/resources/profiles/Flashforge/filament/FusRock PAHT-CF @FF G4P.json +++ b/resources/profiles/Flashforge/filament/FusRock PAHT-CF @FF G4P.json @@ -55,7 +55,7 @@ "; filament start gcode\n;right_extruder_material:PA-CF\n" ], "filament_type": [ - "PA-CF" + "PAHT-CF" ], "compatible_printers": [ "Flashforge Guider4 Pro 0.4 nozzle" diff --git a/resources/profiles/Flashforge/filament/FusRock/FusRock Generic PAHT-CF.json b/resources/profiles/Flashforge/filament/FusRock/FusRock Generic PAHT-CF.json index 98dcbdde4c..16ce25c253 100644 --- a/resources/profiles/Flashforge/filament/FusRock/FusRock Generic PAHT-CF.json +++ b/resources/profiles/Flashforge/filament/FusRock/FusRock Generic PAHT-CF.json @@ -168,7 +168,7 @@ "0" ], "filament_type": [ - "PA-CF" + "PAHT-CF" ], "filament_unload_time": [ "0" diff --git a/resources/profiles/Flashforge/filament/Generic PLA Silk @FF C5.json b/resources/profiles/Flashforge/filament/Generic PLA Silk @FF C5.json index 16e1f0d278..2087257bcb 100644 --- a/resources/profiles/Flashforge/filament/Generic PLA Silk @FF C5.json +++ b/resources/profiles/Flashforge/filament/Generic PLA Silk @FF C5.json @@ -53,7 +53,7 @@ "; filament start gcode" ], "filament_type": [ - "SILK" + "PLA" ], "hot_plate_temp": [ "65" diff --git a/resources/profiles/Flashforge/filament/Generic PLA Silk @FF C5P.json b/resources/profiles/Flashforge/filament/Generic PLA Silk @FF C5P.json index eb0aa584da..a5de4c8f4a 100644 --- a/resources/profiles/Flashforge/filament/Generic PLA Silk @FF C5P.json +++ b/resources/profiles/Flashforge/filament/Generic PLA Silk @FF C5P.json @@ -56,7 +56,7 @@ "; filament start gcode" ], "filament_type": [ - "SILK" + "PLA" ], "nozzle_temperature": [ "230" diff --git a/resources/profiles/InfiMech/filament/EX+APS/fdm_filament_pet @EX+APS.json b/resources/profiles/InfiMech/filament/EX+APS/fdm_filament_pet @EX+APS.json index 396373accc..0d8b7f36ef 100644 --- a/resources/profiles/InfiMech/filament/EX+APS/fdm_filament_pet @EX+APS.json +++ b/resources/profiles/InfiMech/filament/EX+APS/fdm_filament_pet @EX+APS.json @@ -44,7 +44,7 @@ "12" ], "filament_type": [ - "PETG Basic" + "PETG" ], "filament_density": [ "1.27" diff --git a/resources/profiles/InfiMech/filament/EX/fdm_filament_pet @EX.json b/resources/profiles/InfiMech/filament/EX/fdm_filament_pet @EX.json index c718df9d89..0213942153 100644 --- a/resources/profiles/InfiMech/filament/EX/fdm_filament_pet @EX.json +++ b/resources/profiles/InfiMech/filament/EX/fdm_filament_pet @EX.json @@ -44,7 +44,7 @@ "12" ], "filament_type": [ - "PETG Basic" + "PETG" ], "filament_density": [ "1.27" diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PE-CF @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PE-CF @base.json index 74aea8b342..48212f7143 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PE-CF @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PE-CF @base.json @@ -4,5 +4,8 @@ "inherits": "fdm_filament_pe", "from": "system", "filament_id": "OFJfx0Cl", - "instantiation": "false" + "instantiation": "false", + "filament_type": [ + "PE-CF" + ] } diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @base.json index 21b93a089d..08bc8ad547 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @base.json @@ -4,5 +4,8 @@ "inherits": "fdm_filament_pet", "from": "system", "filament_id": "OFDCk0cz", - "instantiation": "false" + "instantiation": "false", + "filament_type": [ + "PETG-CF" + ] } diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @base.json index f1898eb1f6..f5fefcf0cb 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @base.json @@ -4,5 +4,8 @@ "inherits": "fdm_filament_pp", "from": "system", "filament_id": "OFRS2Ggt", - "instantiation": "false" + "instantiation": "false", + "filament_type": [ + "PP-CF" + ] } diff --git a/resources/profiles/Prusa/filament/Prusa Generic PA-CF @base.json b/resources/profiles/Prusa/filament/Prusa Generic PA-CF @base.json index 0b0059fa9a..350ab54889 100644 --- a/resources/profiles/Prusa/filament/Prusa Generic PA-CF @base.json +++ b/resources/profiles/Prusa/filament/Prusa Generic PA-CF @base.json @@ -4,5 +4,8 @@ "inherits": "fdm_filament_pa", "from": "system", "filament_id": "OFXfgh61", - "instantiation": "false" + "instantiation": "false", + "filament_type": [ + "PA-CF" + ] } diff --git a/resources/profiles/Prusa/filament/Prusa Generic PLA-CF @base.json b/resources/profiles/Prusa/filament/Prusa Generic PLA-CF @base.json index f45f0e0bf0..c899728e68 100644 --- a/resources/profiles/Prusa/filament/Prusa Generic PLA-CF @base.json +++ b/resources/profiles/Prusa/filament/Prusa Generic PLA-CF @base.json @@ -4,5 +4,8 @@ "inherits": "fdm_filament_pla", "from": "system", "filament_id": "OFv8SRZh", - "instantiation": "false" + "instantiation": "false", + "filament_type": [ + "PLA-CF" + ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Lite @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Lite @U1 base.json index b75944c3ec..351390aace 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA Lite @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Lite @U1 base.json @@ -46,6 +46,9 @@ "filament_type": [ "PLA" ], + "filament_vendor": [ + "Snapmaker" + ], "filament_density": [ "1.24" ], diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 base.json index ac2ea870ea..816b04dcd5 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 base.json @@ -46,6 +46,9 @@ "filament_type": [ "PLA" ], + "filament_vendor": [ + "Snapmaker" + ], "filament_density": [ "1.24" ], diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 base.json index adaaf5c3c6..12caa4b25c 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 base.json @@ -46,6 +46,9 @@ "filament_type": [ "PLA" ], + "filament_vendor": [ + "Snapmaker" + ], "filament_density": [ "1.24" ], diff --git a/resources/profiles/retired_filament_ids.json b/resources/profiles/retired_filament_ids.json new file mode 100644 index 0000000000..3840098c11 --- /dev/null +++ b/resources/profiles/retired_filament_ids.json @@ -0,0 +1,90 @@ +{ + "hints": {}, + "retired": { + "GFABS": { + "claims": [ + "Anycubic/Anycubic ABS" + ], + "successor": "OFf2b9wu" + }, + "GFL92": { + "claims": [ + "Anycubic/Anycubic PLA" + ], + "successor": "OFgK2Xvv" + }, + "GFL93": { + "claims": [ + "Anycubic/Anycubic PLA+" + ], + "successor": "OFgrGfPy" + }, + "GFPLA": { + "claims": [ + "Anycubic/Anycubic PLA" + ], + "successor": "OFgK2Xvv" + }, + "GFPLA High Speed": { + "claims": [ + "Anycubic/Anycubic PLA High Speed" + ], + "successor": "OFgSK1hL" + }, + "GFPLA Matte": { + "claims": [ + "Anycubic/Anycubic PLA Matte" + ], + "successor": "OFvYfHPB" + }, + "GFPLA Silk": { + "claims": [ + "Anycubic/Anycubic PLA Silk" + ], + "successor": "OFjdTQd6" + }, + "GFPLA+": { + "claims": [ + "Anycubic/Anycubic PLA+" + ], + "successor": "OFgrGfPy" + }, + "P11851ba": { + "claims": [ + "Artillery/Artillery PET" + ], + "successor": "OFw0qp7w" + }, + "P284941e": { + "claims": [ + "Artillery/Artillery PETG", + "Artillery/Artillery PETG Basic" + ], + "successor": "OFEYKrjb" + }, + "P2fbf0c0": { + "claims": [ + "Artillery/Artillery TPU" + ], + "successor": "OFTbsWVg" + }, + "P39db358": { + "claims": [ + "Artillery/Artillery ABS" + ], + "successor": "OFw1S8lL" + }, + "P8163162": { + "claims": [ + "Artillery/Artillery PVA" + ], + "successor": "OFc2SnEp" + }, + "P87436f6": { + "claims": [ + "Artillery/Artillery ASA" + ], + "successor": "OFtmT4Nd" + } + } +} diff --git a/scripts/assign_filament_ids.py b/scripts/assign_filament_ids.py index 8100ecb880..3945c2ec8d 100644 --- a/scripts/assign_filament_ids.py +++ b/scripts/assign_filament_ids.py @@ -1,44 +1,69 @@ #!/usr/bin/env python3 """ -Mint deterministic filament_id values for OrcaSlicer system filament families and +Mint deterministic filament_id values for OrcaSlicer system filament products and validate the tree against the sanctioned-state ledger. -Policy (companion to assign_vendor_setting_ids.py; see filament_id_plan.md): - * filament_id is a MATERIAL-FAMILY id: one commercial product line = one id, - shared by all of that material's per-printer/per-nozzle variants. The key is - declared only on family ROOT presets (instantiation != "true"); instantiated - variants inherit it through `inherits` and never write the key themselves. - * New ids are a pure function of the family's identity: +Policy (companion to assign_vendor_setting_ids.py; see filament_id_plan_v3.md): + * filament_id is a PRODUCT id: one commercial product line = one id, shared by + all of that material's per-printer/per-nozzle variants in every bundle. + OrcaFilamentLibrary (OFL) is the product catalog: a family's id is declared + once, on its family root (instantiation != "true"); vendor bundles carry + only specializations of OFL families — same base name, non-empty + compatible_printers, no filament_id key — which resolve the OFL id through + the loader's inherits/base-bundle walk. Products OFL does not carry mint + their id in the vendor bundle with the same rule; the key is + bundle-independent, so hoisting a family into OFL never changes its id. + * New ids are content-addressed by the product triple, resolved from the + declaring preset's flattened config (filament_vendor and filament_type are + inheritable list options — first element; family name = preset base name): filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE, - "filament_family//") ) - where is the vendor bundle name (stem of the vendor index json) and - is the family name = preset base name (name with /\\s?@.*$/ stripped). + "filament_product///") ) 8 chars total, which satisfies the AMS length limit. Nobody invents ids by - hand; on the astronomically rare collision with any existing or retired id the - input is salted ("/1", "/2", ...) until free and the result is frozen in file. + hand; on the astronomically rare collision with any existing or retired id + the input is salted ("/1", "/2", ...) until free and the result is frozen + in file. Identity changes (a family rename, a filament_vendor/filament_type + fix) change the id BY DESIGN — the succession ledger keeps old ids + resolving. * Reserved id spaces that are never minted into or altered: - GF* Bambu AMS/RFID catalog (vendor BBL untouchable) - QD_* Qidi device protocol - P + 7 hex chars (case-insensitive) and the literal "null" user-custom presets (CreatePresetsDialog.cpp) - every already-shipped id, grandfathered via scripts/filament_id_snapshot.json - * scripts/filament_id_snapshot.json is the sanctioned-state ledger: it must - exactly equal the tree-derived state at all times, so any id/claim change shows - up as a reviewable diff to that file (the maintainer gate). Ids that fully - vanish from the tree are appended to scripts/retired_filament_ids.json; a - retired id may never be used again for anything. + * scripts/filament_id_snapshot.json is the sanctioned-state ledger (ids, + claims and declared triples): it must exactly equal the tree-derived state + at all times, so any id/claim/triple change shows up as a reviewable diff to + that file (the maintainer gate). Ids that fully vanish from the tree are + appended to the SHIPPED succession ledger + resources/profiles/retired_filament_ids.json as + {"claims": [...], "successor": } — successor picked by the mode + rule over the vanished id's old claims — and the runtime follows those + chains (plus cross-island "hints" for ids Orca cannot retire, e.g. GF*) + on resolution miss. A retired id may never be used again for anything. + Vanished ids in a FOREIGN island's space (GF*/QD_*) are released with a + hint instead of retired: the island's catalog owns them and may + legitimately (re)ship them. The effective-id resolution below is loader-faithful (PresetBundle.cpp load_vendor_configs_from_json): own filament_id key, else walk `inherits` within the vendor map, with OrcaFilamentLibrary base-bundle fallback; once a chain enters OFL it stays in OFL; a vendor chain that dead-ends id-less retries its direct -parent in the OFL map. +parent in the OFL map. filament_vendor / filament_type resolve the same way. Run from anywhere: python3 scripts/assign_filament_ids.py (default) mint + insert ids for id-less families; idempotent, never rewrites a valid existing id; a no-op on a fully-idded tree - --mint "V/Family" print the id that would be minted; touches nothing + --mint "Vendor/Type/Family" + print the id that triple would mint; touches nothing --update-snapshot regenerate the snapshot from the tree; retire vanished ids + with a mode-rule successor (--forget-never-shipped FILE + drops listed never-shipped ids from lineage instead) + --add-hint "OLD=NEW" + record a cross-island succession hint (standalone) + --remint VENDOR re-derive VENDOR's declared ids from their triples and + rewrite mismatches in place (repeatable) + --drop-redundant-ids VENDOR + delete declarations that re-declare an inherited OFL id --check run the validation checks (also run by CI through orca_extra_profile_check.py); exit nonzero on errors """ @@ -62,7 +87,10 @@ FILAMENT_ID_LENGTH = 6 # base62 digits after the "OF" prefix -> 8 chars total SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) PROFILES_DIR = os.path.normpath(os.path.join(SCRIPTS_DIR, "..", "resources", "profiles")) SNAPSHOT_PATH = os.path.join(SCRIPTS_DIR, "filament_id_snapshot.json") -RETIRED_PATH = os.path.join(SCRIPTS_DIR, "retired_filament_ids.json") +# The succession ledger ships with the app (resources/) so the runtime can +# follow retired->successor chains and cross-island hints on resolution miss. +RETIRED_PATH = os.path.join(PROFILES_DIR, "retired_filament_ids.json") +RETIRED_REL = "resources/profiles/retired_filament_ids.json" OFL = "OrcaFilamentLibrary" @@ -73,8 +101,6 @@ USER_CUSTOM_ID_RE = re.compile(r"^P[0-9A-Fa-f]{7}$", re.IGNORECASE) # Family name = preset base name: strip the first "@..." suffix. The space before # "@" is optional because names like "Afinia PLA@HS" exist. BASE_NAME_RE = re.compile(r"\s?@.*$") -# OFL generic presets relevant for alias hygiene (check 5). -OFL_GENERIC_RE = re.compile(r"^Generic .* @System$") # Salt iterations accepted by the mint-conformance check (check 3). MAX_CHECK_SALT = 8 @@ -115,15 +141,15 @@ def base_name(name): return BASE_NAME_RE.sub("", name, count=1) -def generate_filament_id(vendor, family, salt=0): - """Deterministic "OF" + 6-char base62 filament_id for a material family. +def generate_filament_id(filament_vendor, filament_type, family, salt=0): + """Deterministic "OF" + 6-char base62 filament_id for a filament product. - input = "filament_family//" (+ "/" when salted); - u = uuid5(FILAMENT_ID_NAMESPACE, input); the id tail is the low - FILAMENT_ID_LENGTH base62 digits of int(u.bytes, "big"), most-significant - first — the same derivation as generate_preset_setting_id. + input = "filament_product///" + (+ "/" when salted); u = uuid5(FILAMENT_ID_NAMESPACE, input); the id + tail is the low FILAMENT_ID_LENGTH base62 digits of int(u.bytes, "big"), + most-significant first — the same derivation as generate_preset_setting_id. """ - key = f"filament_family/{vendor}/{family}" + key = f"filament_product/{filament_vendor}/{filament_type}/{family}" if salt: key = f"{key}/{salt}" u = uuid.uuid5(FILAMENT_ID_NAMESPACE, key) @@ -135,13 +161,14 @@ def generate_filament_id(vendor, family, salt=0): return "OF" + "".join(reversed(digits)) -def mint_filament_id(vendor, family, taken): - """Mint the family's id, salting past any id in `taken` (existing + retired).""" +def mint_filament_id(filament_vendor, filament_type, family, taken): + """Mint the product's id, salting past any id in `taken` (existing + retired).""" for salt in range(10000): - candidate = generate_filament_id(vendor, family, salt) + candidate = generate_filament_id(filament_vendor, filament_type, family, salt) if candidate not in taken: return candidate - raise RuntimeError(f"could not mint a free filament_id for {vendor}/{family}") + raise RuntimeError( + f"could not mint a free filament_id for {filament_vendor}/{filament_type}/{family}") # --------------------------------------------------------------------------- @@ -157,7 +184,7 @@ def list_vendor_names(profiles_dir): """Vendor bundles = subdirectories with a matching .json index file. (Ignores stray non-bundle entries such as the tracked "user" directory, - which has no user.json index.) + which has no user.json index, and the retired_filament_ids.json ledger.) """ profiles_dir = str(profiles_dir) return sorted( @@ -196,6 +223,8 @@ def load_vendor_filaments(profiles_dir, vendor): "inherits": data.get("inherits"), "instantiation": str(data.get("instantiation", "")).lower() == "true", "compatible_printers": data.get("compatible_printers") or [], + "filament_vendor": data.get("filament_vendor"), + "filament_type": data.get("filament_type"), } return presets, errors @@ -248,6 +277,53 @@ def resolve_filament_id(name, filaments, ofl_filaments, seen=None, in_ofl=False, return None, "missing", entry +def resolve_filament_field(name, field, filaments, ofl_filaments, seen=None, in_ofl=False): + """Resolve an inheritable list option (filament_vendor / filament_type) with + the same hop semantics as resolve_filament_id: own value, else walk + `inherits` in the vendor map with OFL base-bundle fallback. Values are list + options — the first element counts; "" when the chain never defines one. + """ + if seen is None: + seen = set() + if name in seen: + return "" + seen.add(name) + if in_ofl: + rec = ofl_filaments.get(name) + else: + rec = filaments.get(name) + if rec is None and name in ofl_filaments: + rec, in_ofl = ofl_filaments[name], True + if rec is None: + return "" + value = rec.get(field) + if isinstance(value, str): + value = [value] + if value and value[0]: + return value[0] + parent = rec.get("inherits") + if parent: + found = resolve_filament_field(parent, field, filaments, ofl_filaments, seen, in_ofl) + if found or in_ofl: + return found + if parent in ofl_filaments: + return resolve_filament_field(parent, field, filaments, ofl_filaments, set(), True) + return found + return "" + + +def resolve_triple(name, filaments, ofl_filaments): + """The preset's mint-key triple (filament_vendor, filament_type, family).""" + return (resolve_filament_field(name, "filament_vendor", filaments, ofl_filaments), + resolve_filament_field(name, "filament_type", filaments, ofl_filaments), + base_name(name)) + + +def is_island_declaration(vendor, fid): + """Declarations frozen outside the OF mint domain: all of BBL, QD_* in Qidi.""" + return vendor == "BBL" or (vendor == "Qidi" and fid.startswith("QD_")) + + def analyze_tree(profiles_dir): """Load every vendor bundle and derive the full filament_id state. @@ -260,6 +336,7 @@ def analyze_tree(profiles_dir): ofl_filaments, ofl_errors = ( load_vendor_filaments(profiles_dir, OFL) if OFL in vendor_names else ({}, []) ) + ofl_declared = {r["filament_id"] for r in ofl_filaments.values() if r.get("filament_id")} vendors = {} read_errors = list(ofl_errors) @@ -287,21 +364,35 @@ def analyze_tree(profiles_dir): instantiated_with_id = [] # "Vendor/PresetName" (own filament_id key on an instantiated preset) overrides = [] # (vendor, name, declared, inherited, file) missing_effective = [] # (vendor, name, file) instantiated presets resolving no id - alias_candidates = [] # (vendor, rec, ofl_entry) presets id-resolved through an OFL generic + alias_candidates = [] # (vendor, rec, ofl_entry, own_key) presets riding an OFL family + triples = {} # fid -> set of triples of its non-island declarers + declarer_triples = [] # (vendor, rec, fid, triple) per non-island declarer + family_triples = {} # (vendor, family) -> {triple: [declarer names]} for vendor, filaments in vendors.items(): occurring = vendor_ids.setdefault(vendor, set()) for rec in filaments.values(): if rec.get("filament_id"): - occurring.add(rec["filament_id"]) - declared_ids.setdefault(vendor, set()).add(rec["filament_id"]) - ids.setdefault(rec["filament_id"], set()) + fid = rec["filament_id"] + occurring.add(fid) + declared_ids.setdefault(vendor, set()).add(fid) + ids.setdefault(fid, set()) + if not is_island_declaration(vendor, fid): + triple = resolve_triple(rec["name"], filaments, ofl_filaments) + rec["triple"] = triple + declarer_triples.append((vendor, rec, fid, triple)) + triples.setdefault(fid, set()).add(triple) + family_triples.setdefault( + (vendor, base_name(rec["name"])), {}).setdefault( + triple, []).append(rec["name"]) if rec.get("inherits"): - inherited, _src, _e = resolve_filament_id( + inherited, _src, skip_entry = resolve_filament_id( rec["name"], filaments, ofl_filaments, skip_own=True) - if inherited and inherited != rec["filament_id"]: - overrides.append( - (vendor, rec["name"], rec["filament_id"], inherited, rec["file"])) + if inherited and inherited != fid: + overrides.append((vendor, rec["name"], fid, inherited, rec["file"])) + if (vendor != OFL and skip_entry and inherited + and inherited in ofl_declared): + alias_candidates.append((vendor, rec, skip_entry, True)) if not rec["instantiation"]: continue eff = rec.get("eff_filament_id") @@ -312,18 +403,25 @@ def analyze_tree(profiles_dir): ids.setdefault(eff, set()).add(f"{vendor}/{base_name(rec['name'])}") if rec.get("filament_id"): instantiated_with_id.append(f"{vendor}/{rec['name']}") - if vendor != OFL and rec["ofl_entry"] and OFL_GENERIC_RE.match(rec["ofl_entry"]): - alias_candidates.append((vendor, rec, rec["ofl_entry"])) + if vendor != OFL and rec["ofl_entry"] and eff in ofl_declared: + alias_candidates.append((vendor, rec, rec["ofl_entry"], False)) - # Alias hygiene (check 5): a vendor preset that tunes an OFL generic (no id - # anywhere in its vendor-side chain) is matched to the OFL preset by ALIAS - # (base name); renaming it re-exposes the OFL generic and creates a live - # duplicate, and empty compatible_printers cannot claim any printer. + # Alias hygiene (check 5): a vendor preset that rides an OFL family (its id + # resolves through OFL) is matched to the OFL preset by ALIAS (base name); + # renaming it re-exposes the OFL preset and creates a live duplicate, empty + # compatible_printers cannot claim any printer, and declaring its own + # filament_id key forks the family off the catalog id. alias_violations = [] # (vendor, name, ofl_entry, reason, file) - for vendor, rec, entry in alias_candidates: + for vendor, rec, entry, own_key in alias_candidates: expected = base_name(entry) own_base = base_name(rec["name"]) - if own_base != expected: + if own_key: + alias_violations.append(( + vendor, rec["name"], entry, + f'declares its own filament_id "{rec["filament_id"]}" — dropping the key ' + f"lets it resolve the OFL family's id through inherits", + rec["file"])) + elif own_base != expected: alias_violations.append(( vendor, rec["name"], entry, f'base name "{own_base}" != "{expected}" — the rename re-exposes the ' @@ -335,6 +433,18 @@ def analyze_tree(profiles_dir): "empty compatible_printers cannot shadow the OFL preset anywhere", rec["file"])) + # Cross-bundle triple divergence (check 8, warning only): the same family + # name declared in several bundles with different triples cannot converge + # on one id until the divergence is fixed. + name_bundles = {} + for (vendor, family), tmap in family_triples.items(): + name_bundles.setdefault(family, {})[vendor] = frozenset(tmap) + cross_bundle_triples = [ + (family, {v: sorted(ts) for v, ts in per_vendor.items()}) + for family, per_vendor in sorted(name_bundles.items()) + if len(per_vendor) > 1 and len(set(per_vendor.values())) > 1 + ] + return { "vendors": vendors, "read_errors": read_errors, @@ -347,11 +457,18 @@ def analyze_tree(profiles_dir): "missing_effective": sorted(missing_effective), "alias_violations": alias_violations, "alias_exceptions": sorted(f"{v}/{n}" for v, n, _e, _r, _f in alias_violations), + "triples": {fid: sorted(list(t) for t in ts) for fid, ts in triples.items()}, + "triple_sets": triples, + "declarer_triples": declarer_triples, + "family_triples": family_triples, + "triple_exceptions": sorted( + f"{v}/{f}" for (v, f), tmap in family_triples.items() if len(tmap) > 1), + "cross_bundle_triples": cross_bundle_triples, } # --------------------------------------------------------------------------- -# Snapshot / retired-ledger IO +# Snapshot / succession-ledger IO # --------------------------------------------------------------------------- def snapshot_from_analysis(analysis): @@ -360,6 +477,8 @@ def snapshot_from_analysis(analysis): "instantiated_with_id": analysis["instantiated_with_id"], "id_overrides": analysis["id_overrides"], "alias_exceptions": analysis["alias_exceptions"], + "triples": analysis["triples"], + "triple_exceptions": analysis["triple_exceptions"], } @@ -368,16 +487,28 @@ def load_snapshot(path): if not os.path.exists(path): return None data = load_json(path) - for key in ("ids", "instantiated_with_id", "id_overrides", "alias_exceptions"): - data.setdefault(key, {} if key == "ids" else []) + for key in ("ids", "triples"): + data.setdefault(key, {}) + for key in ("instantiated_with_id", "id_overrides", "alias_exceptions", + "triple_exceptions"): + data.setdefault(key, []) return data -def load_retired(path): - """Return the retired-ids map {id: ["Vendor/Family", ...]} ({} if absent).""" +def load_ledger(path): + """The shipped succession ledger: + {"retired": {id: {"claims": [...], "successor": id|None}}, "hints": {id: id}}. + Empty maps when the file is absent. + """ if not os.path.exists(path): - return {} - return load_json(path).get("retired", {}) + return {"retired": {}, "hints": {}} + data = load_json(path) + return {"retired": data.get("retired", {}), "hints": data.get("hints", {})} + + +def load_retired(path): + """The ledger's retired map (see load_ledger); {} when the file is absent.""" + return load_ledger(path)["retired"] def write_ledger(path, obj): @@ -387,6 +518,72 @@ def write_ledger(path, obj): f.write("\n") +def claim_effective_ids(analysis): + """Reverse of analysis["ids"]: claim "Vendor/Family" -> set of effective ids.""" + claim_ids = {} + for fid, claims in analysis["ids"].items(): + for claim in claims: + claim_ids.setdefault(claim, set()).add(fid) + return claim_ids + + +def successor_by_mode(claims, claim_ids): + """Mode rule for a retiring id's successor: every old claim whose family + still exists votes for the family's current effective id(s); most votes + wins, ties break to the lexicographically smallest id, no votes -> None. + Returns (successor_or_None, vote_counts). + """ + counts = {} + for claim in claims: + for fid in claim_ids.get(claim, ()): + counts[fid] = counts.get(fid, 0) + 1 + if not counts: + return None, counts + best = max(counts.values()) + return min(f for f, n in counts.items() if n == best), counts + + +def migrate_retired_entries(old_retired, claim_ids): + """One-shot v3 schema migration: list-valued entries {id: [claims]} become + {id: {"claims": [...], "successor": }}; object entries + pass through untouched. + """ + migrated = {} + for fid, entry in old_retired.items(): + if isinstance(entry, dict): + migrated[fid] = entry + continue + successor, _counts = successor_by_mode(entry, claim_ids) + migrated[fid] = {"claims": sorted(entry), "successor": successor} + return migrated + + +def follow_succession(fid, seen, retired, hints, live): + """Follow retired->successor / hints->target forwarding starting at `fid`. + + Returns (status, node): "null" (chain ended on an heirless retirement), + "live" (reached a live tree id), "cycle", or "dead-end" (an id that is + neither live nor forwarded anywhere). `seen` pre-seeds the cycle guard. + """ + seen = set(seen) + cur = fid + while True: + if cur is None: + return "null", None + if cur in seen: + return "cycle", cur + seen.add(cur) + if cur in live: + return "live", cur + entry = retired.get(cur) + if entry is not None: + cur = entry.get("successor") if isinstance(entry, dict) else None + elif cur in hints: + cur = hints[cur] + else: + return "dead-end", cur + + # --------------------------------------------------------------------------- # Reserved namespaces # --------------------------------------------------------------------------- @@ -414,17 +611,28 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, the snapshot, or match ^OF[0-9A-Za-z]{6}$, or belong to vendor BBL, or be a QD_* id within vendor Qidi. 2. Snapshot equality, both directions: the tree-derived id->families multimap - must equal the snapshot exactly (the snapshot diff is the maintainer gate). - 3. Mint conformance: an OF-format id's claim that is not grandfathered must - equal the mint of (vendor, family) or a salted iteration. + AND the id->triples map of the declarers must equal the snapshot exactly + (the snapshot diff is the maintainer gate). + 3. Mint conformance: a non-island OF-format declaration must equal the mint + of the declarer's triple or a salted iteration, unless that exact + (id, triple) pair is grandfathered in the snapshot. 4. Retired ids may never occur again. - 5. Alias hygiene: a tuned OFL generic must keep the generic's base name and - claim printers via non-empty compatible_printers. + 5. Alias hygiene: a vendor preset riding an OFL family must keep the OFL + base name, claim printers via non-empty compatible_printers, and declare + no filament_id key of its own. 6. Reserved namespaces (GF*/QD_*/P-hex/"null") only for their owner vendors, except claims grandfathered in the snapshot. 7. Structure ratchet: (a) no NEW instantiated preset carries its own filament_id key; (b) no NEW declared-vs-inherited id drift; (c) every instantiated filament resolves an effective id (a hard load error in C++). + 8. Triple integrity: (a) every non-island declarer resolves non-empty + filament_vendor and filament_type (hard error, no grandfathering); + (b) declarers of one (bundle, family) resolve identical triples, unless + grandfathered in snapshot triple_exceptions; cross-bundle divergence on + the same family name is a warning only. + 9. Succession integrity: retired successor chains terminate at a live id or + null without cycles; hint keys are live island-declared ids and hint + chains terminate live. """ _utf8_console() errors = 0 @@ -433,7 +641,9 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, if snapshot is None: print_error(f"filament_id snapshot not found at {snapshot_path}; {UPDATE_HINT}") return 1 - retired = load_retired(retired_path) + ledger = load_ledger(retired_path) + retired = ledger["retired"] + hints = ledger["hints"] for msg in analysis["read_errors"]: print_error(msg) @@ -475,38 +685,48 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, if fid not in tree_ids: print_error( f'filament_id stability: snapshot id "{fid}" vanished from the tree ' - f"(ids are immutable once shipped); {UPDATE_HINT}") + f"(shipped ids retire through the succession ledger); {UPDATE_HINT}") errors += 1 continue for claim in snap_ids[fid]: if claim not in tree_ids[fid]: print_error( f'filament_id stability: snapshot claim "{claim}" of id "{fid}" ' - f"vanished from the tree (ids are immutable once shipped); {UPDATE_HINT}") + f"vanished from the tree; {UPDATE_HINT}") + errors += 1 + # ... and the declared triples, both directions. + snap_triples = snapshot["triples"] + tree_triples = analysis["triples"] + for fid in sorted(tree_triples): + for t in tree_triples[fid]: + if t not in snap_triples.get(fid, []): + print_error( + f'filament_id "{fid}" triple "{"/".join(t)}" is not sanctioned by ' + f"scripts/filament_id_snapshot.json; {UPDATE_HINT}") + errors += 1 + for fid in sorted(snap_triples): + for t in snap_triples[fid]: + if t not in tree_triples.get(fid, []): + print_error( + f'filament_id triple stability: snapshot triple "{"/".join(t)}" of ' + f'id "{fid}" vanished from the tree; {UPDATE_HINT}') errors += 1 - # -- 3. mint conformance for OF-format ids ------------------------------ - for fid in sorted(tree_ids): + # -- 3. mint conformance for OF-format declarations ---------------------- + for vendor, rec, fid, triple in sorted( + analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])): if not OF_ID_RE.match(fid): continue - for claim in tree_ids[fid]: - if claim in snap_ids.get(fid, []): - continue # grandfathered - vendor, family = claim.split("/", 1) - if fid not in analysis["declared_ids"].get(vendor, set()): - # The claiming vendor never declares this id — it is inherited - # from another vendor's root (an OFL generic family): correctly - # tuned generics ride the OFL family id by design, so only the - # snapshot gate (check 2) applies to this claim. - continue - minted = [generate_filament_id(vendor, family, s) for s in range(MAX_CHECK_SALT + 1)] - if fid not in minted: - print_error( - f'filament_id "{fid}" of family "{claim}" does not match its mint: ' - f'expected "{minted[0]}" (or a salted iteration); paste the expected ' - f"id into the family root (or, for an intentionally shared id, " - f"{UPDATE_HINT})") - errors += 1 + if list(triple) in snap_triples.get(fid, []): + continue # grandfathered (id, triple) pair + minted = [generate_filament_id(*triple, salt=s) for s in range(MAX_CHECK_SALT + 1)] + if fid not in minted: + print_error( + f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does ' + f'not match the mint of its triple "{"/".join(triple)}": expected ' + f'"{minted[0]}" (or a salted iteration); paste the expected id into the ' + f"family root (or, for an intentionally kept id, {UPDATE_HINT})") + errors += 1 # -- 4. retired ids may never come back --------------------------------- for vendor in sorted(analysis["vendor_ids"]): @@ -514,18 +734,19 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, if fid in retired: print_error( f'filament_id "{fid}" ({vendor}) is retired ' - f"(scripts/retired_filament_ids.json) and may never be reused") + f"({RETIRED_REL}) and may never be reused") errors += 1 - # -- 5. alias hygiene for tuned OFL generics ----------------------------- + # -- 5. alias hygiene for presets riding OFL families -------------------- exceptions = set(snapshot["alias_exceptions"]) for vendor, name, entry, reason, file in analysis["alias_violations"]: if f"{vendor}/{name}" in exceptions: continue print_error( - f'preset "{name}" ({file}) tunes the OFL generic "{entry}" but {reason}; ' - f'keep the OFL base name and non-empty compatible_printers, or give the ' - f"family its own minted id") + f'preset "{name}" ({file}) rides the OFL family "{entry}" but {reason}; ' + f"a vendor specialization keeps the OFL base name, sets non-empty " + f"compatible_printers and declares no filament_id key — or the family " + f"gets its own minted id") errors += 1 # -- 6. reserved namespaces ---------------------------------------------- @@ -562,8 +783,10 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, f'inherits chain resolves "{inherited}"; a preset must not override its ' f"family's id") errors += 1 + ofl_map = analysis["vendors"].get(OFL, {}) for vendor, name, file in analysis["missing_effective"]: - expected = generate_filament_id(vendor, base_name(name)) + triple = resolve_triple(name, analysis["vendors"][vendor], ofl_map) + expected = generate_filament_id(*triple) print_error( f'instantiated filament "{name}" ({file}) resolves no filament_id anywhere ' f"in its inherits chain — this is a hard load error in the C++ loader; " @@ -571,6 +794,95 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, f'"{vendor}/{base_name(name)}": "{expected}", salted if taken)') errors += 1 + # -- 8. triple integrity --------------------------------------------------- + for vendor, rec, fid, triple in sorted( + analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])): + if triple[0] and triple[1]: + continue + missing = " and ".join( + k for k, v in (("filament_vendor", triple[0]), + ("filament_type", triple[1])) if not v) + print_error( + f'preset "{rec["name"]}" ({rec["file"]}) declares filament_id "{fid}" but ' + f"resolves empty {missing}; the mint key needs both (generic materials " + f'use filament_vendor "Generic")') + errors += 1 + triple_exceptions = set(snapshot["triple_exceptions"]) + for (vendor, family), tmap in sorted(analysis["family_triples"].items()): + if len(tmap) < 2 or f"{vendor}/{family}" in triple_exceptions: + continue + detail = "; ".join( + f'"{"/".join(t)}" ({", ".join(sorted(names))})' + for t, names in sorted(tmap.items())) + print_error( + f'family "{vendor}/{family}" declarers resolve divergent triples: {detail}; ' + f"declarers of one family must agree on (filament_vendor, filament_type)") + errors += 1 + for family, per_vendor in analysis["cross_bundle_triples"]: + detail = "; ".join( + f'{v}: {", ".join("/".join(t) for t in ts)}' + for v, ts in sorted(per_vendor.items())) + print_warning( + f'family name "{family}" resolves different triples across bundles ' + f"({detail}); bundles of one product converge on one id only once " + f"their triples agree") + + # -- 9. succession integrity (ledger-internal) ----------------------------- + live = set(tree_ids) + for fid in sorted(retired): + entry = retired[fid] + if (not isinstance(entry, dict) + or not isinstance(entry.get("claims", []), list) + or not (entry.get("successor") is None + or isinstance(entry.get("successor"), str))): + print_error( + f'retired entry "{fid}" ({RETIRED_REL}) is not of the form ' + f'{{"claims": [...], "successor": }}') + errors += 1 + continue + status, node = follow_succession(entry.get("successor"), {fid}, retired, hints, live) + if status == "cycle": + print_error( + f'succession chain of retired id "{fid}" cycles at "{node}" ({RETIRED_REL})') + errors += 1 + elif status == "dead-end": + print_error( + f'succession chain of retired id "{fid}" dead-ends at "{node}", which is ' + f"neither a live tree id nor retired/hinted ({RETIRED_REL})") + errors += 1 + for key in sorted(hints): + target = hints[key] + _is_reserved, island = reserved_space_owner(key) + if island is None: + print_error( + f'hint key "{key}" ({RETIRED_REL}) is not in an island id space ' + f"(GF*/QD_*); non-island ids are retired with a successor instead") + errors += 1 + continue + if key in retired: + print_error( + f'hint key "{key}" ({RETIRED_REL}) is also retired; an id is either ' + f"retired (Orca-owned) or hinted (island-owned), never both") + errors += 1 + continue + # A hint key may be absent from the tree (released to its island); when + # live, only island vendors may declare it. + outsiders = sorted( + v for v in analysis["declared_ids"] + if key in analysis["declared_ids"][v] and not is_island_declaration(v, key)) + if outsiders: + print_error( + f'hint key "{key}" ({RETIRED_REL}) is declared by non-island vendor(s) ' + f"{outsiders}; hints apply only once no non-island vendor declares the " + f"id — re-mint those declarers first") + errors += 1 + status, node = follow_succession(target, {key}, retired, hints, live) + if status != "live": + print_error( + f'hint "{key}" -> "{target}" ({RETIRED_REL}) must chain-terminate at a ' + f"live tree id (got {status} at \"{node}\")") + errors += 1 + return errors @@ -579,9 +891,19 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, # --------------------------------------------------------------------------- def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, - retired_path=RETIRED_PATH, allow_shared_catalog=False): + retired_path=RETIRED_PATH, allow_shared_catalog=False, + forget_path=None): """Regenerate the snapshot from the tree; retire ids that fully vanished. + Vanished ids gain a succession-ledger entry whose successor is picked by + the mode rule over their old claims (None when the family died heirless); + vanished ids in a foreign island's space (GF*/QD_*) are released with a + hint instead — the island catalog owns them, so they must stay mintable + there and must never be blocked by check 4. + --forget-never-shipped lists ids to drop from lineage instead of retiring + (never shipped => nothing outside the tree references them); succession + chains through them are spliced to their mode-rule successor. + Refuses to sanction NEW reserved-namespace ids (or new claims on them) for non-owner vendors unless --allow-shared-catalog is passed. Idempotent: a second run over an unchanged tree changes nothing. Returns 0 on success. @@ -593,6 +915,21 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, old_snap = load_snapshot(snapshot_path) old_ids = old_snap["ids"] if old_snap else {} + forget = set() + if forget_path: + data = load_json(forget_path) + if not isinstance(data, list) or not all(isinstance(x, str) for x in data): + print_error(f"--forget-never-shipped expects a JSON list of ids: {forget_path}") + return 1 + forget = set(data) + still_live = sorted(forget & set(new_snap["ids"])) + if still_live: + for fid in still_live: + print_error( + f'--forget-never-shipped: id "{fid}" is still in the tree and can ' + f"neither be forgotten nor retired") + return 1 + # Gate: new reserved-namespace ids / claims for non-owner vendors. refusals = [] for fid, claims in sorted(new_snap["ids"].items()): @@ -622,22 +959,69 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, # Retirement is permanent: refuse to sanction a tree that resurrects a # retired id (check 4 would reject the resulting snapshot forever anyway). - retired = load_retired(retired_path) + ledger = load_ledger(retired_path) + retired = ledger["retired"] reused = sorted(set(new_snap["ids"]) & set(retired)) if reused: for fid in reused: print_error( f'refusing to sanction retired filament_id "{fid}" ' - f"(scripts/retired_filament_ids.json is append-only; retired ids may " + f"({RETIRED_REL} is append-only; retired ids may " f"never be reused — mint a fresh id for the family instead)") return 1 - # Retire ids that fully vanished from the tree (append-only ledger). + # Retire ids that fully vanished from the tree (append-only ledger), with a + # mode-rule successor so devices and user presets keep resolving. Vanished + # ids in a FOREIGN island's reserved space (GF*/QD_*) are never retired — + # the island's catalog still owns them and may legitimately (re)ship them — + # they are RELEASED, with a succession hint so they keep resolving on miss. + claim_ids = claim_effective_ids(analysis) newly_retired = [] + forgotten = [] + released = [] for fid in sorted(old_ids): - if fid not in new_snap["ids"]: - retired[fid] = sorted(set(retired.get(fid, [])) | set(old_ids[fid])) - newly_retired.append(fid) + if fid in new_snap["ids"]: + continue + if fid in forget: + forgotten.append(fid) + print_info(f'forgotten (never shipped): "{fid}" — no retirement entry') + continue + _is_reserved, island = reserved_space_owner(fid) + if island is not None: + successor, _counts = successor_by_mode(old_ids[fid], claim_ids) + if successor and fid not in ledger["hints"]: + ledger["hints"][fid] = successor + released.append(fid) + print_info( + f'released to the {island} island space: "{fid}"' + + (f' — hint -> "{ledger["hints"][fid]}"' if fid in ledger["hints"] + else " (no successor computable; --add-hint if one is needed)")) + continue + entry = retired.get(fid) + if not isinstance(entry, dict): + successor, counts = successor_by_mode(old_ids[fid], claim_ids) + if len(counts) > 1: + losers = sorted(set(counts) - {successor}) + print_warning( + f'retiring "{fid}": successor "{successor}" won the claim vote ' + f"({counts[successor]}); losing candidate(s): {', '.join(losers)}") + entry = {"claims": list(entry) if entry else [], "successor": successor} + entry["claims"] = sorted(set(entry["claims"]) | set(old_ids[fid])) + retired[fid] = entry + newly_retired.append(fid) + + # Splice succession chains through forgotten ids so they stay live. + spliced = [] + for fid in sorted(retired): + entry = retired[fid] + successor = entry.get("successor") if isinstance(entry, dict) else None + if successor in forget: + new_successor, _counts = successor_by_mode(old_ids.get(successor, []), claim_ids) + entry["successor"] = new_successor + spliced.append(fid) + print_info( + f'spliced succession: "{fid}" -> {json.dumps(new_successor)} ' + f'(through forgotten "{successor}")') # Diff summary. added_ids = sorted(set(new_snap["ids"]) - set(old_ids)) @@ -649,20 +1033,29 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, len(set(claims) - set(new_snap["ids"].get(fid, []))) for fid, claims in old_ids.items()) old_snap = old_snap or {"ids": {}, "instantiated_with_id": [], "id_overrides": [], - "alias_exceptions": []} + "alias_exceptions": [], "triples": {}, + "triple_exceptions": []} changed = new_snap != old_snap if changed: write_ledger(snapshot_path, new_snap) - if newly_retired or not os.path.exists(retired_path): - write_ledger(retired_path, {"retired": retired}) + if newly_retired or spliced or released or not os.path.exists(retired_path): + write_ledger(retired_path, {"retired": retired, "hints": ledger["hints"]}) print_info(f"snapshot ids : {len(new_snap['ids'])} (+{len(added_ids)} / -{len(removed_ids)})") print_info(f"claims added : {added_claims}") print_info(f"claims removed : {removed_claims}") + print_info(f"declared triples : {len(new_snap['triples'])}") print_info(f"ids retired now : {len(newly_retired)}" + (f" ({', '.join(newly_retired)})" if newly_retired else "")) - for section in ("instantiated_with_id", "id_overrides", "alias_exceptions"): + if forgotten: + print_info(f"ids forgotten : {len(forgotten)} ({', '.join(forgotten)})") + if released: + print_info(f"ids released : {len(released)} ({', '.join(released)})") + if spliced: + print_info(f"chains spliced : {len(spliced)} ({', '.join(spliced)})") + for section in ("instantiated_with_id", "id_overrides", "alias_exceptions", + "triple_exceptions"): before, after = len(old_snap.get(section, [])), len(new_snap[section]) print_info(f"{section:<18}: {after} ({after - before:+d})") if changed: @@ -672,8 +1065,64 @@ def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, return 0 +def add_hints(pairs, profiles_dir=PROFILES_DIR, retired_path=RETIRED_PATH): + """--add-hint "OLD=NEW": forward an island-space id to a live id. + + OLD must sit in an island id space (GF*/QD_*), must not be retired, and — + when live in the tree — may be declared only by island vendors (Orca cannot + retire it). An absent OLD is fine: island catalogs own ids Orca never + shipped or has released. NEW must be live in the tree. All pairs validate + before anything is written. Returns 0 on success. + """ + _utf8_console() + analysis = analyze_tree(profiles_dir) + errors = 0 + for msg in analysis["read_errors"]: + print_error(msg) + errors += 1 + ledger = load_ledger(retired_path) + live = set(analysis["ids"]) + parsed = [] + for pair in pairs: + if "=" not in pair: + print_error(f'--add-hint expects "OLD=NEW", got "{pair}"') + errors += 1 + continue + old, new = pair.split("=", 1) + _is_reserved, island = reserved_space_owner(old) + outsiders = sorted( + v for v in analysis["declared_ids"] + if old in analysis["declared_ids"][v] and not is_island_declaration(v, old)) + if old in ledger["retired"]: + print_error(f'--add-hint: "{old}" is retired; its succession entry already forwards it') + errors += 1 + elif island is None: + print_error( + f'--add-hint: "{old}" is not in an island id space (GF*/QD_*); ' + f"non-island ids are retired with a successor by --update-snapshot") + errors += 1 + elif outsiders: + print_error( + f'--add-hint: "{old}" is declared by non-island vendor(s) {outsiders}; ' + f"hints apply only once no non-island vendor declares the id — re-mint " + f"those declarers first") + errors += 1 + elif new not in live: + print_error(f'--add-hint: target "{new}" is not a live tree id') + errors += 1 + else: + parsed.append((old, new)) + if errors: + return 1 + for old, new in parsed: + ledger["hints"][old] = new + print_info(f'hint added: "{old}" -> "{new}"') + write_ledger(retired_path, {"retired": ledger["retired"], "hints": ledger["hints"]}) + return 0 + + # --------------------------------------------------------------------------- -# Default run: mint + insert ids for id-less families +# Byte-preserving profile edits # --------------------------------------------------------------------------- def insert_filament_id(text, new_id): @@ -694,33 +1143,90 @@ def insert_filament_id(text, new_id): return text, 0 -def write_filament_id(path, new_id): - """Insert new_id into the profile at path, byte-preserving everything else. +def replace_filament_id_value(text, old_id, new_id): + """Swap the JSON string VALUE on the `"filament_id"` line, byte-preserving + everything else. Returns (text, replacements made). + """ + pattern = re.compile( + r'(^[ \t]*"filament_id"[ \t]*:[ \t]*)' + + re.escape(json.dumps(old_id, ensure_ascii=False)), + re.MULTILINE) + return pattern.subn( + lambda m: m.group(1) + json.dumps(new_id, ensure_ascii=False), text, count=1) - Binary IO keeps the file's original line endings (LF or CRLF) and exact - formatting apart from the inserted line; the result is re-parsed to - guarantee it is still valid JSON. + +def delete_filament_id_line(text, old_id): + """Delete the `"filament_id"` line, byte-preserving the rest. + + Handles both the canonical layout (trailing comma) and a last-property + layout (comma on the preceding line). Returns (text, deletions made). + """ + val = re.escape(json.dumps(old_id, ensure_ascii=False)) + m = re.search(r'^[ \t]*"filament_id"[ \t]*:[ \t]*' + val + r'[ \t]*,[ \t]*\r?\n', + text, re.MULTILINE) + if m: + return text[:m.start()] + text[m.end():], 1 + m = re.search(r',[ \t]*\r?\n[ \t]*"filament_id"[ \t]*:[ \t]*' + val + r'[ \t]*(?=\r?\n)', + text) + if m: + return text[:m.start()] + text[m.end():], 1 + return text, 0 + + +def _edit_profile(path, edit): + """Apply `edit(text) -> (text, n)` to the profile at path, byte-preserving. + + Binary IO keeps the file's original line endings (LF or CRLF), BOM and exact + formatting apart from the edit; the result is re-parsed and returned so the + caller can verify the outcome. Raises when the edit found no anchor. """ with open(path, "rb") as f: raw = f.read() bom = raw.startswith(b"\xef\xbb\xbf") text = raw.decode("utf-8-sig") - text, n = insert_filament_id(text, new_id) + text, n = edit(text) if n == 0: - raise RuntimeError(f"Could not insert filament_id into {path}") - json.loads(text) # fail loudly if the edit broke the JSON + raise RuntimeError(f"could not apply filament_id edit to {path}") + data = json.loads(text) # fail loudly if the edit broke the JSON with open(path, "wb") as f: f.write((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8")) + return data +def write_filament_id(path, new_id): + """Insert new_id into the profile at path, byte-preserving everything else.""" + _edit_profile(path, lambda text: insert_filament_id(text, new_id)) + + +def rewrite_filament_id(path, old_id, new_id): + """Replace the filament_id value old_id -> new_id; re-parses to verify.""" + data = _edit_profile(path, lambda text: replace_filament_id_value(text, old_id, new_id)) + if data.get("filament_id") != new_id: + raise RuntimeError(f'rewrite of filament_id "{old_id}" -> "{new_id}" in {path} ' + f"did not take effect") + + +def remove_filament_id(path, old_id): + """Delete the filament_id declaration line; re-parses to verify.""" + data = _edit_profile(path, lambda text: delete_filament_id_line(text, old_id)) + if "filament_id" in data: + raise RuntimeError(f'deletion of filament_id "{old_id}" from {path} did not take effect') + + +# --------------------------------------------------------------------------- +# Default run: mint + insert ids for id-less families +# --------------------------------------------------------------------------- + def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, retired_path=RETIRED_PATH): """Mint + insert ids for id-less families. Never rewrites a valid existing id. A family = (vendor, base name) group over instantiated filaments with no - effective id. The minted id is inserted into the family's root(s): the - presets its members inherit that carry no id, or the member itself when it - has no vendor-side parent. Returns (files_changed, errors). + effective id. The minted id is a pure function of the family's triple — + (filament_vendor, filament_type) resolved on the root(s), family name — and + is inserted into the family's root(s): the presets its members inherit that + carry no id, or the member itself when it has no vendor-side parent. + Returns (files_changed, errors). """ analysis = analyze_tree(profiles_dir) errors = 0 @@ -767,6 +1273,7 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, root_claims.setdefault((vendor, root["name"]), set()).add(key) roots[key] = family_roots + ofl_map = analysis["vendors"].get(OFL, {}) files_changed = 0 families_minted = 0 for key, family_roots in sorted(roots.items()): @@ -782,7 +1289,29 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, f"roots so each family has its own") errors += 1 continue - new_id = mint_filament_id(vendor, family, taken) + vendor_map = analysis["vendors"][vendor] + fields = {(resolve_filament_field(n, "filament_vendor", vendor_map, ofl_map), + resolve_filament_field(n, "filament_type", vendor_map, ofl_map)) + for n in family_roots} + if len(fields) > 1: + print_error( + f'cannot mint for family "{vendor}/{family}": its roots resolve ' + f"divergent (filament_vendor, filament_type) pairs {sorted(fields)}; " + f"align the fields first") + errors += 1 + continue + fvendor, ftype = next(iter(fields)) + if not fvendor or not ftype: + missing = " and ".join( + k for k, v in (("filament_vendor", fvendor), + ("filament_type", ftype)) if not v) + print_error( + f'cannot mint for family "{vendor}/{family}": it resolves empty ' + f'{missing}; the mint key needs both (generic materials use ' + f'filament_vendor "Generic")') + errors += 1 + continue + new_id = mint_filament_id(fvendor, ftype, family, taken) taken.add(new_id) families_minted += 1 for name in sorted(family_roots): @@ -798,6 +1327,130 @@ def assign_missing_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, return files_changed, errors +# --------------------------------------------------------------------------- +# --remint / --drop-redundant-ids (v3.1/v3.2 migration modes) +# --------------------------------------------------------------------------- + +def remint_vendors(vendor_list, profiles_dir=PROFILES_DIR, retired_path=RETIRED_PATH): + """Re-derive every non-island declared id in the given vendors from its + triple; rewrite mismatching declarations in place (byte-preserving). Never + touches the snapshot — run --update-snapshot afterwards and review the diff. + + A candidate id is blocked when it is a retired key, a hints key, or occurs + in the tree with any triple set other than exactly {T}. Equal-triple reuse + is convergence: the same product must end up under the same id everywhere. + Returns (rewritten, errors). + """ + _utf8_console() + analysis = analyze_tree(profiles_dir) + errors = 0 + for msg in analysis["read_errors"]: + print_error(msg) + errors += 1 + if "BBL" in vendor_list: + print_error("--remint BBL is forbidden (the GF* catalog is frozen)") + return 0, errors + 1 + unknown = sorted(set(vendor_list) - set(analysis["vendors"])) + if unknown: + for v in unknown: + print_error(f"--remint: unknown vendor {v!r}") + return 0, errors + len(unknown) + + ledger = load_ledger(retired_path) + blocked_keys = set(ledger["retired"]) | set(ledger["hints"]) + occurring = set() + for vids in analysis["vendor_ids"].values(): + occurring |= vids + triple_sets = analysis["triple_sets"] + + assigned = {} # triple -> id chosen this run (all declarers converge) + run_taken = {} # id -> triple (a run-local mint may not collide either) + + def want_id(triple): + if triple in assigned: + return assigned[triple] + for salt in range(10000): + cand = generate_filament_id(*triple, salt=salt) + if cand in blocked_keys: + continue + if cand in occurring and triple_sets.get(cand, set()) != {triple}: + continue + if run_taken.get(cand, triple) != triple: + continue + assigned[triple] = cand + run_taken[cand] = triple + return cand + raise RuntimeError(f"could not mint a free filament_id for triple {triple}") + + scanned = 0 + rewritten = 0 + for vendor in sorted(set(vendor_list)): + recs = sorted( + (rec for rec in analysis["vendors"][vendor].values() if rec.get("filament_id")), + key=lambda r: r["file"]) + for rec in recs: + fid = rec["filament_id"] + if is_island_declaration(vendor, fid): + continue + scanned += 1 + want = want_id(rec["triple"]) + if fid == want: + continue + rewrite_filament_id(rec["path"], fid, want) + rewritten += 1 + print_info(f'{rec["file"]}: "{fid}" -> "{want}" ' + f'(triple "{"/".join(rec["triple"])}")') + print_info(f"declarations scanned : {scanned}") + print_info(f"declarations reminted: {rewritten}") + if rewritten: + print_warning(f"now {UPDATE_HINT}") + return rewritten, errors + + +def drop_redundant_ids(vendor, profiles_dir=PROFILES_DIR): + """Delete filament_id declarations in `vendor` that merely re-declare an OFL + family's id path: ignoring its own key, the preset's inherits chain enters + OFL and resolves an OFL-declared id, and the preset keeps the OFL family's + base name. Such a preset is a specialization and rides the OFL id (v3.2(b)). + Never touches the snapshot. Returns (dropped, errors). + """ + _utf8_console() + analysis = analyze_tree(profiles_dir) + errors = 0 + for msg in analysis["read_errors"]: + print_error(msg) + errors += 1 + if vendor == "BBL": + print_error("--drop-redundant-ids BBL is forbidden (the GF* catalog is frozen)") + return 0, errors + 1 + if vendor not in analysis["vendors"]: + print_error(f"--drop-redundant-ids: unknown vendor {vendor!r}") + return 0, errors + 1 + + vendor_map = analysis["vendors"][vendor] + ofl_map = analysis["vendors"].get(OFL, {}) + ofl_declared = {r["filament_id"] for r in ofl_map.values() if r.get("filament_id")} + dropped = 0 + for rec in sorted(vendor_map.values(), key=lambda r: r["file"]): + fid = rec.get("filament_id") + if not fid or is_island_declaration(vendor, fid) or not rec.get("inherits"): + continue + resolved, _src, entry = resolve_filament_id( + rec["name"], vendor_map, ofl_map, skip_own=True) + if not (entry and resolved and resolved in ofl_declared): + continue + if base_name(rec["name"]) != base_name(entry): + continue + remove_filament_id(rec["path"], fid) + dropped += 1 + print_info(f'{rec["file"]}: dropped filament_id "{fid}" ' + f'(rides OFL "{entry}", id "{resolved}")') + print_info(f"declarations dropped : {dropped}") + if dropped: + print_warning(f"now {UPDATE_HINT}") + return dropped, errors + + # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -807,33 +1460,73 @@ def main(argv=None): parser = argparse.ArgumentParser( description="Mint deterministic filament_id values for id-less filament " "families and validate the tree against the sanctioned snapshot.") - parser.add_argument("--mint", metavar='"Vendor/Family"', - help="print the id that would be minted for a family; " - "touches nothing") + parser.add_argument("--mint", metavar='"Vendor/Type/Family"', + help="print the id the (filament_vendor, filament_type, " + "family name) triple would mint; touches nothing") parser.add_argument("--update-snapshot", action="store_true", help="regenerate scripts/filament_id_snapshot.json from the " - "tree; ids that fully vanished are retired") + "tree; ids that fully vanished are retired with a " + "mode-rule successor") parser.add_argument("--check", action="store_true", help="run the filament_id checks; exit nonzero on errors") parser.add_argument("--allow-shared-catalog", action="store_true", help="with --update-snapshot: allow sanctioning new claims " "on reserved-namespace ids for non-owner vendors") + parser.add_argument("--forget-never-shipped", metavar="JSON_FILE", + help="with --update-snapshot: vanished ids listed in the file " + "(JSON list) are dropped without a retirement entry and " + "succession chains are spliced through them; refused if " + "a listed id is still in the tree") + parser.add_argument("--add-hint", metavar='"OLD=NEW"', action="append", + help="record a cross-island succession hint (island-owned " + "live id -> live id) in the shipped ledger; standalone, " + "repeatable") + parser.add_argument("--remint", metavar="VENDOR", action="append", + help="re-derive VENDOR's declared filament_ids from their " + "triples and rewrite mismatches in place; repeatable") + parser.add_argument("--drop-redundant-ids", metavar="VENDOR", + help="delete filament_id declarations in VENDOR that " + "re-declare the OFL family id they already resolve " + "through inherits") parser.add_argument("--profiles", default=PROFILES_DIR, help="profiles directory (default: resources/profiles)") args = parser.parse_args(argv) + if args.forget_never_shipped and not args.update_snapshot: + parser.error("--forget-never-shipped requires --update-snapshot") + if args.mint: - if "/" not in args.mint: - parser.error('--mint expects "Vendor/Family"') - vendor, family = args.mint.split("/", 1) + parts = args.mint.split("/", 2) + if len(parts) != 3 or not all(parts): + parser.error('--mint expects "filament_vendor/filament_type/family_name" ' + "with all three components non-empty (check 8 rejects empty " + "vendor/type in the tree)") snapshot = load_snapshot(SNAPSHOT_PATH) or {"ids": {}} taken = set(snapshot["ids"]) | set(load_retired(RETIRED_PATH)) - print(mint_filament_id(vendor, family, taken)) + print(mint_filament_id(parts[0], parts[1], parts[2], taken)) return 0 + if args.add_hint: + if args.update_snapshot or args.check or args.remint or args.drop_redundant_ids: + parser.error("--add-hint runs standalone") + return add_hints(args.add_hint, args.profiles, RETIRED_PATH) + + if args.remint: + if args.update_snapshot or args.check or args.drop_redundant_ids: + parser.error("--remint cannot be combined with other modes") + _changed, errors = remint_vendors(args.remint, args.profiles, RETIRED_PATH) + return 1 if errors else 0 + + if args.drop_redundant_ids: + if args.update_snapshot or args.check: + parser.error("--drop-redundant-ids cannot be combined with other modes") + _changed, errors = drop_redundant_ids(args.drop_redundant_ids, args.profiles) + return 1 if errors else 0 + if args.update_snapshot: return update_snapshot(args.profiles, SNAPSHOT_PATH, RETIRED_PATH, - allow_shared_catalog=args.allow_shared_catalog) + allow_shared_catalog=args.allow_shared_catalog, + forget_path=args.forget_never_shipped) if args.check: errors = check_filament_ids(args.profiles, SNAPSHOT_PATH, RETIRED_PATH) diff --git a/scripts/filament_id_snapshot.json b/scripts/filament_id_snapshot.json index 24d51970bc..f3004c5077 100644 --- a/scripts/filament_id_snapshot.json +++ b/scripts/filament_id_snapshot.json @@ -1,5 +1,1042 @@ { - "alias_exceptions": [], + "alias_exceptions": [ + "Afinia/Afinia PLA", + "Afinia/Afinia PLA@HS", + "Afinia/Afinia Value PLA", + "Afinia/Afinia Value PLA@HS", + "Anker/Anker Generic PA @base", + "Anker/Anker Generic PA-CF @base", + "Anker/Anker Generic PC @base", + "Anker/Anker Generic PETG @base", + "Anker/Anker Generic PETG-CF @base", + "Anker/Anker Generic PLA @base", + "Anker/Anker Generic PLA Silk @base", + "Anker/Anker Generic PLA+ @base", + "Anker/Anker Generic PLA-CF @base", + "Anycubic/Anycubic Generic PA", + "Anycubic/Anycubic Generic PA-CF", + "Anycubic/Anycubic Generic PC", + "Anycubic/Anycubic Generic PETG", + "Anycubic/Anycubic Generic PLA", + "Anycubic/Anycubic Generic PLA-CF", + "Anycubic/Anycubic PA @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PA @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PA @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PA6-CF @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PA6-CF @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PA6-CF @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PAHT-CF @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PAHT-CF @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PAHT-CF @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PC @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PC @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PC @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PC-CF @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PC-CF @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PC-CF @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PC-GF @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PC-GF @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PC-GF @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PET-CF @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PET-CF @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PET-CF @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra 3 Max 0.4 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra 3 Max 0.6 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra 3 Max 0.8 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra S1 0.4 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra S1 Max 0.25 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PETG @Anycubic Kobra X 0.4 nozzle", + "Anycubic/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra 3 Max 0.4 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra 3 Max 0.6 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra 3 Max 0.8 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra S1 0.4 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra S1 Max 0.25 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PLA @Anycubic Kobra X 0.4 nozzle", + "Anycubic/Anycubic PLA Glow @Anycubic Kobra X 0.4 nozzle", + "Anycubic/Anycubic PLA High Speed @Anycubic Kobra 3 Max 0.4 nozzle", + "Anycubic/Anycubic PLA High Speed @Anycubic Kobra S1 0.4 nozzle", + "Anycubic/Anycubic PLA High Speed @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PLA High Speed @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PLA High Speed @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PLA High Speed @Anycubic Kobra X 0.4 nozzle", + "Anycubic/Anycubic PLA Luminous @Anycubic Kobra 3 Max 0.4 nozzle", + "Anycubic/Anycubic PLA Matte @Anycubic Kobra 3 Max 0.4 nozzle", + "Anycubic/Anycubic PLA Matte @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PLA Matte @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PLA Matte @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PLA Matte @Anycubic Kobra X 0.4 nozzle", + "Anycubic/Anycubic PLA Silk @Anycubic Kobra 3 Max 0.4 nozzle", + "Anycubic/Anycubic PLA Silk @Anycubic Kobra S1 0.4 nozzle", + "Anycubic/Anycubic PLA Silk @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PLA Silk @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PLA Silk @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PLA Silk @Anycubic Kobra X 0.4 nozzle", + "Anycubic/Anycubic PLA Translucent @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PLA+ @Anycubic Kobra 3 Max 0.4 nozzle", + "Anycubic/Anycubic PLA+ @Anycubic Kobra S1 0.4 nozzle", + "Anycubic/Anycubic PLA+ @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Anycubic PLA+ @Anycubic Kobra S1 Max 0.6 nozzle", + "Anycubic/Anycubic PLA+ @Anycubic Kobra S1 Max 0.8 nozzle", + "Anycubic/Anycubic PLA+ @Anycubic Kobra X 0.4 nozzle", + "Anycubic/Fiberon PA6-CF20 @Anycubic Kobra S1", + "Anycubic/Generic PETG @Anycubic Kobra S1 Max 0.4 nozzle", + "Anycubic/Generic PETG @Anycubic Kobra X 0.4 nozzle", + "Anycubic/Generic PETG Basic @Anycubic Kobra 3 Max 0.4 nozzle", + "Anycubic/Panchroma PLA @Anycubic Kobra S1", + "Anycubic/Polymaker PLA Pro @Anycubic Kobra S1", + "Anycubic/Polymaker PLA Pro Metallic @Anycubic Kobra S1", + "Artillery/Artillery Generic PETG", + "Artillery/Artillery Generic PLA", + "Artillery/Artillery Generic PLA-CF", + "BBL/BETA HIPS @base", + "BBL/BETA PAHT-CF @base", + "BBL/BETA PETG @base", + "BBL/BETA PETG Fluorescence @base", + "BBL/BETA PETG Glitter @base", + "BBL/BETA PETG Glow @base", + "BBL/BETA PETG Gradient @base", + "BBL/BETA PETG HF @base", + "BBL/BETA PETG Heat Color Change @base", + "BBL/BETA PETG Marble @base", + "BBL/BETA PETG Matte @base", + "BBL/BETA PETG Metallic @base", + "BBL/BETA PETG Transparent @base", + "BBL/BETA PETG UV Color Change @base", + "BBL/BETA PETG-CF @base", + "BBL/BETA PETG-GF @base", + "BBL/BETA PLA Basic @base", + "BBL/BETA PLA Chameleon @base", + "BBL/BETA PLA Fluorescence @base", + "BBL/BETA PLA Glitter @base", + "BBL/BETA PLA Glow @base", + "BBL/BETA PLA Gradient @base", + "BBL/BETA PLA Heat Color Change @base", + "BBL/BETA PLA High Speed @base", + "BBL/BETA PLA High Temp @base", + "BBL/BETA PLA Marble @base", + "BBL/BETA PLA Matte @base", + "BBL/BETA PLA Metal @base", + "BBL/BETA PLA Metallic @base", + "BBL/BETA PLA PRO @base", + "BBL/BETA PLA Silk @base", + "BBL/BETA PLA Silk+ @base", + "BBL/BETA PLA Transparent @base", + "BBL/BETA PLA UV Color Change @base", + "BBL/BETA PLA Wood @base", + "BBL/BETA PLA Youth @base", + "BBL/BETA PLA-CF @base", + "BBL/Bambu PA-CF @base", + "BBL/Bambu PA6-CF @base", + "BBL/Bambu PA6-GF @base", + "BBL/Bambu PAHT-CF @base", + "BBL/Bambu PC @base", + "BBL/Bambu PC FR @base", + "BBL/Bambu PET-CF @base", + "BBL/Bambu PETG Basic @base", + "BBL/Bambu PETG HF @base", + "BBL/Bambu PETG Translucent @base", + "BBL/Bambu PETG-CF @base", + "BBL/Bambu PLA Aero @base", + "BBL/Bambu PLA Basic @base", + "BBL/Bambu PLA Dynamic @base", + "BBL/Bambu PLA Galaxy @base", + "BBL/Bambu PLA Glow @base", + "BBL/Bambu PLA Lite @base", + "BBL/Bambu PLA Marble @base", + "BBL/Bambu PLA Matte @base", + "BBL/Bambu PLA Metal @base", + "BBL/Bambu PLA Silk @base", + "BBL/Bambu PLA Silk+ @base", + "BBL/Bambu PLA Sparkle @base", + "BBL/Bambu PLA Tough @base", + "BBL/Bambu PLA Tough+ @base", + "BBL/Bambu PLA Translucent @base", + "BBL/Bambu PLA Wood @base", + "BBL/Bambu PLA-CF @base", + "BBL/Bambu PPA-CF @base", + "BBL/Bambu Support For PA/PET @base", + "BBL/Bambu Support For PLA @base", + "BBL/Bambu Support For PLA/PETG @base", + "BBL/Bambu Support G @base", + "BBL/Bambu Support W @base", + "BBL/COEX NYLEX UNFILLED @BBL X1C", + "BBL/Fiberon PA12-CF @base", + "BBL/Fiberon PA12-CF10 @base", + "BBL/Fiberon PA6-CF @base", + "BBL/Fiberon PA6-CF20 @base", + "BBL/Fiberon PA6-GF @base", + "BBL/Fiberon PA6-GF25 @base", + "BBL/Fiberon PA612-CF @base", + "BBL/Fiberon PA612-CF15 @base", + "BBL/Fiberon PET-CF @base", + "BBL/Fiberon PET-CF17 @base", + "BBL/Fiberon PETG-ESD @base", + "BBL/Fiberon PETG-rCF @base", + "BBL/Fiberon PETG-rCF08 @base", + "BBL/Generic BVOH @base", + "BBL/Generic EVA @base", + "BBL/Generic HIPS @base", + "BBL/Generic PA @base", + "BBL/Generic PA-CF @base", + "BBL/Generic PC @base", + "BBL/Generic PCTG @base", + "BBL/Generic PE @base", + "BBL/Generic PE-CF @base", + "BBL/Generic PETG @base", + "BBL/Generic PETG HF @base", + "BBL/Generic PETG-CF @base", + "BBL/Generic PHA @base", + "BBL/Generic PLA @base", + "BBL/Generic PLA High Speed @base", + "BBL/Generic PLA Silk @base", + "BBL/Generic PLA-CF @base", + "BBL/Generic PP @base", + "BBL/Generic PP-CF @base", + "BBL/Generic PP-GF @base", + "BBL/Generic PPA-CF @base", + "BBL/Generic PPA-GF @base", + "BBL/Generic SBS @base", + "BBL/Numakers PLA+ @base", + "BBL/Overture Matte PLA @base", + "BBL/Overture PLA @base", + "BBL/Overture PLA Pro @base", + "BBL/Panchroma CoPE @base", + "BBL/Panchroma PLA @base", + "BBL/Panchroma PLA Celestial @base", + "BBL/Panchroma PLA Galaxy @base", + "BBL/Panchroma PLA Glow @base", + "BBL/Panchroma PLA Luminous @base", + "BBL/Panchroma PLA Marble @base", + "BBL/Panchroma PLA Matte @base", + "BBL/Panchroma PLA Metallic @base", + "BBL/Panchroma PLA Neon @base", + "BBL/Panchroma PLA Satin @base", + "BBL/Panchroma PLA Silk @base", + "BBL/Panchroma PLA Starlight @base", + "BBL/Panchroma PLA Temp Shift @base", + "BBL/Panchroma PLA Translucent @base", + "BBL/Panchroma PLA UV Shift @base", + "BBL/PolyLite CosPLA @base", + "BBL/PolyLite PETG @base", + "BBL/PolyLite PLA @base", + "BBL/PolyLite PLA Galaxy @base", + "BBL/PolyLite PLA Glow @base", + "BBL/PolyLite PLA Luminous @base", + "BBL/PolyLite PLA Neon @base", + "BBL/PolyLite PLA Pro @base", + "BBL/PolyLite PLA Starlight @base", + "BBL/PolyLite PLA Translucent @base", + "BBL/PolyTerra PLA @base", + "BBL/PolyTerra PLA Marble @base", + "BBL/PolyTerra PLA+ @base", + "BBL/Polymaker HT-PLA @base", + "BBL/Polymaker HT-PLA-GF @base", + "BBL/SUNLU PETG @base", + "BBL/SUNLU PLA Marble @base", + "BBL/SUNLU PLA Matte @base", + "BBL/SUNLU PLA+ 2.0 @base", + "BBL/SUNLU PLA+ @base", + "BBL/SUNLU Silk PLA+ @base", + "BBL/SUNLU Wood PLA @base", + "BBL/eSUN PLA+ @base", + "Blocks/Blocks Generic PA", + "Blocks/Blocks Generic PA-CF", + "Blocks/Blocks Generic PC", + "Blocks/Blocks Generic PLA", + "Blocks/Blocks Generic PLA-CF", + "CONSTRUCT3D/C1 Generic High Flow PETG", + "CONSTRUCT3D/C1 Generic PETG", + "CONSTRUCT3D/C1 Generic PLA", + "Chuanying/Chuanying Generic HS PLA", + "Chuanying/Chuanying Generic PETG", + "Chuanying/Chuanying Generic PETG-CF10", + "Chuanying/Chuanying Generic PLA", + "Chuanying/Chuanying Generic PLA-CF10", + "Chuanying/Chuanying Generic PLA-Silk", + "Co Print/CoPrint Generic PLA", + "CoLiDo/CoLiDo Generic PETG @CoLiDo DIY 4.0", + "CoLiDo/CoLiDo Generic PETG @CoLiDo X16", + "CoLiDo/CoLiDo Generic PLA @CoLiDo DIY 4.0", + "CoLiDo/CoLiDo Generic PLA @CoLiDo X16", + "CoLiDo/CoLiDo PETG @CoLiDo SR1", + "CoLiDo/CoLiDo PLA @CoLiDo SR1", + "CoLiDo/CoLiDo PLA Silk @CoLiDo SR1", + "CoLiDo/CoLiDo PLA+ @CoLiDo DIY 4.0 V2", + "Comgrow/Comgrow Generic PETG", + "Comgrow/Comgrow Generic PLA", + "Creality/CR-Nylon @K1 Max_CFS-C-all", + "Creality/CR-Nylon @K1C-all", + "Creality/CR-Nylon @K1C_CFS-C-all", + "Creality/CR-Nylon @K1_CFS-C-all", + "Creality/CR-Nylon @K2 Plus-all", + "Creality/CR-PLA @Ender-3 V4-all", + "Creality/CR-PLA @Hi-all", + "Creality/CR-PLA @K1 Max_CFS-C-all", + "Creality/CR-PLA @K1 SE-all", + "Creality/CR-PLA @K1 SE_CFS-C-all", + "Creality/CR-PLA @K1C-all", + "Creality/CR-PLA @K1C_CFS-C-all", + "Creality/CR-PLA @K1_CFS-C-all", + "Creality/CR-PLA @K2 Plus-all", + "Creality/CR-PLA @K2 Pro-all", + "Creality/CR-PLA @K2 SE-all", + "Creality/CR-PLA @K2-all", + "Creality/CR-PLA @SPARKX i7-all", + "Creality/CR-PLA Carbon @K1 Max_CFS-C-all", + "Creality/CR-PLA Carbon @K1C-all", + "Creality/CR-PLA Carbon @K1C_CFS-C-all", + "Creality/CR-PLA Carbon @K1_CFS-C-all", + "Creality/CR-PLA Carbon @K2 Plus-all", + "Creality/CR-PLA Fluo @K1 Max_CFS-C-all", + "Creality/CR-PLA Fluo @K1C-all", + "Creality/CR-PLA Fluo @K1C_CFS-C-all", + "Creality/CR-PLA Fluo @K1_CFS-C-all", + "Creality/CR-PLA Fluo @K2 Plus-all", + "Creality/CR-PLA Fluo @K2 Pro-all", + "Creality/CR-PLA Fluo @K2-all", + "Creality/CR-PLA Fluo @SPARKX i7-all", + "Creality/CR-PLA Matte @Ender-3 V4-all", + "Creality/CR-PLA Matte @K1 Max_CFS-C-all", + "Creality/CR-PLA Matte @K1C-all", + "Creality/CR-PLA Matte @K1C_CFS-C-all", + "Creality/CR-PLA Matte @K1_CFS-C-all", + "Creality/CR-PLA Matte @K2 Plus-all", + "Creality/CR-PLA Matte @K2 Pro-all", + "Creality/CR-PLA Matte @K2-all", + "Creality/CR-PLA Matte @SPARKX i7-all", + "Creality/CR-Silk @Ender-3 V4-all", + "Creality/CR-Silk @Hi-all", + "Creality/CR-Silk @K1 Max_CFS-C-all", + "Creality/CR-Silk @K1 SE-all", + "Creality/CR-Silk @K1 SE_CFS-C-all", + "Creality/CR-Silk @K1C-all", + "Creality/CR-Silk @K1C_CFS-C-all", + "Creality/CR-Silk @K1_CFS-C-all", + "Creality/CR-Silk @K2 Plus-all", + "Creality/CR-Silk @K2 Pro-all", + "Creality/CR-Silk @K2 SE-all", + "Creality/CR-Silk @K2-all", + "Creality/CR-Silk @SPARKX i7-all", + "Creality/CR-Wood @K1 Max_CFS-C-all", + "Creality/CR-Wood @K1C-all", + "Creality/CR-Wood @K1C_CFS-C-all", + "Creality/CR-Wood @K1_CFS-C-all", + "Creality/CR-Wood @K2 Plus-all", + "Creality/Creality Generic PA-CF", + "Creality/Creality Generic PC", + "Creality/Creality Generic PETG", + "Creality/Creality Generic PLA", + "Creality/Creality Generic PLA-CF", + "Creality/Creality HF Generic PLA", + "Creality/Creality HF Generic Speed PLA", + "Creality/EN-PLA+ @K1 Max_CFS-C-all", + "Creality/EN-PLA+ @K1C-all", + "Creality/EN-PLA+ @K1C_CFS-C-all", + "Creality/EN-PLA+ @K1_CFS-C-all", + "Creality/EN-PLA+ @K2 Plus-all", + "Creality/EN-PLA+ @K2 Pro-all", + "Creality/EN-PLA+ @K2-all", + "Creality/EN-PLA+ @SPARKX i7-all", + "Creality/ENDER FAST PLA @Hi-all", + "Creality/ENDER FAST PLA @K2 Plus-all", + "Creality/ENDER FAST PLA @K2 Pro-all", + "Creality/ENDER FAST PLA @K2-all", + "Creality/ENDER FAST PLA @SPARKX i7-all", + "Creality/Ender-PLA @Ender-3 V4-all", + "Creality/Ender-PLA @Hi-all", + "Creality/Ender-PLA @K1 Max_CFS-C-all", + "Creality/Ender-PLA @K1C-all", + "Creality/Ender-PLA @K1C_CFS-C-all", + "Creality/Ender-PLA @K1_CFS-C-all", + "Creality/Ender-PLA @K2 Plus-all", + "Creality/Ender-PLA @K2 Pro-all", + "Creality/Ender-PLA @K2-all", + "Creality/Ender-PLA @SPARKX i7-all", + "Creality/Generic HIPS @K1 Max_CFS-C-all", + "Creality/Generic HIPS @K1C-all", + "Creality/Generic HIPS @K1C_CFS-C-all", + "Creality/Generic HIPS @K1_CFS-C-all", + "Creality/Generic HIPS @K2 Plus-all", + "Creality/Generic PA @K1 Max_CFS-C-all", + "Creality/Generic PA @K1C-all", + "Creality/Generic PA @K1C_CFS-C-all", + "Creality/Generic PA @K1_CFS-C-all", + "Creality/Generic PA @K2 Plus-all", + "Creality/Generic PA @K2 Pro-all", + "Creality/Generic PA @K2-all", + "Creality/Generic PA-CF @K1 Max_CFS-C-all", + "Creality/Generic PA-CF @K1C-all", + "Creality/Generic PA-CF @K1C_CFS-C-all", + "Creality/Generic PA-CF @K1_CFS-C-all", + "Creality/Generic PA-CF @K2 Plus-all", + "Creality/Generic PA12-CF @K2 Plus-all", + "Creality/Generic PA6-CF @K1 Max_CFS-C-all", + "Creality/Generic PA6-CF @K1C-all", + "Creality/Generic PA6-CF @K1C_CFS-C-all", + "Creality/Generic PA6-CF @K1_CFS-C-all", + "Creality/Generic PA6-CF @K2 Plus-all", + "Creality/Generic PA6-CF @K2 Pro-all", + "Creality/Generic PA612-CF @K2 Plus-all", + "Creality/Generic PA612-CF @K2 Pro-all", + "Creality/Generic PAHT-CF @K1 Max_CFS-C-all", + "Creality/Generic PAHT-CF @K1C-all", + "Creality/Generic PAHT-CF @K1C_CFS-C-all", + "Creality/Generic PAHT-CF @K1_CFS-C-all", + "Creality/Generic PAHT-CF @K2 Plus-all", + "Creality/Generic PAHT-CF @K2 Pro-all", + "Creality/Generic PAHT-CF @K2-all", + "Creality/Generic PC @K1 Max_CFS-C-all", + "Creality/Generic PC @K1C-all", + "Creality/Generic PC @K1C_CFS-C-all", + "Creality/Generic PC @K1_CFS-C-all", + "Creality/Generic PC @K2 Plus-all", + "Creality/Generic PC @K2 Pro-all", + "Creality/Generic PET @K1 Max_CFS-C-all", + "Creality/Generic PET @K1C-all", + "Creality/Generic PET @K1C_CFS-C-all", + "Creality/Generic PET @K1_CFS-C-all", + "Creality/Generic PET @K2 Plus-all", + "Creality/Generic PET @K2 Pro-all", + "Creality/Generic PET @K2-all", + "Creality/Generic PET-CF @K1 Max_CFS-C-all", + "Creality/Generic PET-CF @K1C-all", + "Creality/Generic PET-CF @K1C_CFS-C-all", + "Creality/Generic PET-CF @K1_CFS-C-all", + "Creality/Generic PET-CF @K2 Plus-all", + "Creality/Generic PET-CF @K2 Pro-all", + "Creality/Generic PLA @Ender-3 V4-all", + "Creality/Generic PLA @Hi-all", + "Creality/Generic PLA @K1 Max_CFS-C-all", + "Creality/Generic PLA @K1 SE-all", + "Creality/Generic PLA @K1 SE_CFS-C-all", + "Creality/Generic PLA @K1C-all", + "Creality/Generic PLA @K1C_CFS-C-all", + "Creality/Generic PLA @K1_CFS-C-all", + "Creality/Generic PLA @K2 Plus-all", + "Creality/Generic PLA @K2 Pro-all", + "Creality/Generic PLA @K2 SE-all", + "Creality/Generic PLA @K2-all", + "Creality/Generic PLA @SPARKX i7-all", + "Creality/Generic PLA-CF @Ender-3 V4-all", + "Creality/Generic PLA-CF @K1 Max_CFS-C-all", + "Creality/Generic PLA-CF @K1 SE-all", + "Creality/Generic PLA-CF @K1 SE_CFS-C-all", + "Creality/Generic PLA-CF @K1C-all", + "Creality/Generic PLA-CF @K1C_CFS-C-all", + "Creality/Generic PLA-CF @K1_CFS-C-all", + "Creality/Generic PLA-CF @K2 Plus-all", + "Creality/Generic PLA-CF @K2 Pro-all", + "Creality/Generic PLA-CF @K2 SE-all", + "Creality/Generic PLA-CF @K2-all", + "Creality/Generic PLA-CF @SPARKX i7-all", + "Creality/Generic PLA-Silk @Ender-3 V4-all", + "Creality/Generic PLA-Silk @Hi-all", + "Creality/Generic PLA-Silk @K1 Max_CFS-C-all", + "Creality/Generic PLA-Silk @K1 SE-all", + "Creality/Generic PLA-Silk @K1 SE_CFS-C-all", + "Creality/Generic PLA-Silk @K1C-all", + "Creality/Generic PLA-Silk @K1C_CFS-C-all", + "Creality/Generic PLA-Silk @K1_CFS-C-all", + "Creality/Generic PLA-Silk @K2 Plus-all", + "Creality/Generic PLA-Silk @K2 Pro-all", + "Creality/Generic PLA-Silk @K2 SE-all", + "Creality/Generic PLA-Silk @K2-all", + "Creality/Generic PLA-Silk @SPARKX i7-all", + "Creality/Generic PP @K1 Max_CFS-C-all", + "Creality/Generic PP @K1C-all", + "Creality/Generic PP @K1C_CFS-C-all", + "Creality/Generic PP @K1_CFS-C-all", + "Creality/Generic PP @K2 Plus-all", + "Creality/Generic PP @K2 Pro-all", + "Creality/Generic PP @K2-all", + "Creality/Generic Support for PA @K2 Plus-all", + "Creality/Generic Support for PLA @K2 Plus-all", + "Creality/HP Ultra PLA @K1 Max_CFS-C-all", + "Creality/HP Ultra PLA @K1C-all", + "Creality/HP Ultra PLA @K1C_CFS-C-all", + "Creality/HP Ultra PLA @K1_CFS-C-all", + "Creality/HP Ultra PLA @K2 Plus-all", + "Creality/Hyper L-W PLA @Hi-all", + "Creality/Hyper L-W PLA @K1 Max_CFS-C-all", + "Creality/Hyper L-W PLA @K1 SE-all", + "Creality/Hyper L-W PLA @K1 SE_CFS-C-all", + "Creality/Hyper L-W PLA @K1C-all", + "Creality/Hyper L-W PLA @K1C_CFS-C-all", + "Creality/Hyper L-W PLA @K1_CFS-C-all", + "Creality/Hyper L-W PLA @K2 Plus-all", + "Creality/Hyper L-W PLA @K2 Pro-all", + "Creality/Hyper L-W PLA @K2-all", + "Creality/Hyper Luminous @Hi-all", + "Creality/Hyper Luminous @K1C-all", + "Creality/Hyper Luminous @K2 Plus-all", + "Creality/Hyper Luminous @K2 Pro-all", + "Creality/Hyper Luminous @K2-all", + "Creality/Hyper Luminous @SPARKX i7-all", + "Creality/Hyper Marble @Hi-all", + "Creality/Hyper Marble @K1 Max_CFS-C-all", + "Creality/Hyper Marble @K1C-all", + "Creality/Hyper Marble @K1C_CFS-C-all", + "Creality/Hyper Marble @K1_CFS-C-all", + "Creality/Hyper Marble @K2 Plus-all", + "Creality/Hyper Marble @K2 Pro-all", + "Creality/Hyper Marble @K2 SE-all", + "Creality/Hyper Marble @K2-all", + "Creality/Hyper Marble @SPARKX i7-all", + "Creality/Hyper PA6-CF @K2 Plus-all", + "Creality/Hyper PA6-CF @K2 Pro-all", + "Creality/Hyper PA612-CF @K2 Plus-all", + "Creality/Hyper PA612-CF @K2 Pro-all", + "Creality/Hyper PAHT-CF @K1 Max_CFS-C-all", + "Creality/Hyper PAHT-CF @K1C-all", + "Creality/Hyper PAHT-CF @K1C_CFS-C-all", + "Creality/Hyper PAHT-CF @K1_CFS-C-all", + "Creality/Hyper PAHT-CF @K2 Plus-all", + "Creality/Hyper PAHT-CF @K2 Pro-all", + "Creality/Hyper PAHT-CF @K2-all", + "Creality/Hyper PC @K2 Plus-all", + "Creality/Hyper PC @K2 Pro-all", + "Creality/Hyper PLA @Ender-3 V4-all", + "Creality/Hyper PLA @Hi-all", + "Creality/Hyper PLA @K1 Max_CFS-C-all", + "Creality/Hyper PLA @K1 SE-all", + "Creality/Hyper PLA @K1 SE_CFS-C-all", + "Creality/Hyper PLA @K1C-all", + "Creality/Hyper PLA @K1C_CFS-C-all", + "Creality/Hyper PLA @K1_CFS-C-all", + "Creality/Hyper PLA @K2 Plus-all", + "Creality/Hyper PLA @K2 Pro-all", + "Creality/Hyper PLA @K2 SE-all", + "Creality/Hyper PLA @K2-all", + "Creality/Hyper PLA @SPARKX i7-all", + "Creality/Hyper PLA-CF @Ender-3 V4-all", + "Creality/Hyper PLA-CF @Hi-all", + "Creality/Hyper PLA-CF @K1 Max_CFS-C-all", + "Creality/Hyper PLA-CF @K1 SE-all", + "Creality/Hyper PLA-CF @K1 SE_CFS-C-all", + "Creality/Hyper PLA-CF @K1C-all", + "Creality/Hyper PLA-CF @K1C_CFS-C-all", + "Creality/Hyper PLA-CF @K1_CFS-C-all", + "Creality/Hyper PLA-CF @K2 Plus-all", + "Creality/Hyper PLA-CF @K2 Pro-all", + "Creality/Hyper PLA-CF @K2 SE-all", + "Creality/Hyper PLA-CF @K2-all", + "Creality/Hyper PLA-CF @SPARKX i7-all", + "Creality/Hyper PPA-CF @K2 Plus-all", + "Creality/Hyper PPA-CF @K2 Pro-all", + "Creality/Hyper Stardust @Hi-all", + "Creality/Hyper Stardust @K1 Max_CFS-C-all", + "Creality/Hyper Stardust @K1C-all", + "Creality/Hyper Stardust @K1C_CFS-C-all", + "Creality/Hyper Stardust @K1_CFS-C-all", + "Creality/Hyper Stardust @K2 Plus-all", + "Creality/Hyper Stardust @K2 Pro-all", + "Creality/Hyper Stardust @K2 SE-all", + "Creality/Hyper Stardust @K2-all", + "Creality/Hyper Stardust @SPARKX i7-all", + "Creality/Panchroma PLA Matte @K2 Plus-all", + "Creality/Panchroma PLA Satin @K2 Plus-all", + "Creality/PolySonic PLA @K2 Plus-all", + "Creality/PolySonic PLA Pro @K2 Plus-all", + "Creality/Soleyin Ultra PLA @Ender-3 V4-all", + "Creality/Soleyin Ultra PLA @Hi-all", + "Creality/Soleyin Ultra PLA @K1 Max_CFS-C-all", + "Creality/Soleyin Ultra PLA @K1C-all", + "Creality/Soleyin Ultra PLA @K1C_CFS-C-all", + "Creality/Soleyin Ultra PLA @K1_CFS-C-all", + "Creality/Soleyin Ultra PLA @K2 Plus-all", + "Creality/Soleyin Ultra PLA @K2 Pro-all", + "Creality/Soleyin Ultra PLA @K2 SE-all", + "Creality/Soleyin Ultra PLA @K2-all", + "Creality/Soleyin Ultra PLA @SPARKX i7-all", + "Creality/eSUN PET-Basic @K2 Plus-all", + "Creality/eSUN PLA+ @K2 Plus-all", + "Creality/eSUN PLA-CF @K2 Plus-all", + "Creality/eSUN PLA-HS @K2 Plus-all", + "Creality/eSUN PLA-LW @K2 Plus-all", + "Creality/eSUN PLA-Lite @K2 Plus-all", + "Creality/eSUN PLA-Matte @K2 Plus-all", + "Creality/eSUN PLA-Silk @K2 Plus-all", + "Cubicon/Cubicon PLA @base", + "Custom/Generic PC @MyToolChanger", + "Custom/Generic PETG @MyToolChanger", + "Custom/Generic PLA @MyToolChanger", + "Custom/Generic PLA-CF @MyToolChanger", + "Custom/Generic PVA @MyToolChanger", + "Custom/Generic TPU @MyToolChanger", + "DeltaMaker/DeltaMaker Brand PLA", + "DeltaMaker/DeltaMaker Generic PETG", + "DeltaMaker/DeltaMaker Generic PLA", + "Dremel/Dremel Generic PLA", + "Elegoo/Elegoo PAHT @base", + "Elegoo/Elegoo PC @base", + "Elegoo/Elegoo PETG @base", + "Elegoo/Elegoo PLA @base", + "Elegoo/Generic PA @base", + "Elegoo/Generic PC @base", + "Elegoo/Generic PET @base", + "Elegoo/Generic PETG @base", + "Elegoo/Generic PLA @base", + "FLSun/FLSun Generic PA", + "FLSun/FLSun Generic PA-CF", + "FLSun/FLSun Generic PC", + "FLSun/FLSun Generic PETG", + "FLSun/FLSun Generic PLA", + "FLSun/FLSun Generic PLA-CF", + "Flashforge/Flashforge ABS Basic @FF C5", + "Flashforge/Flashforge ABS Basic @FF C5P", + "Flashforge/Flashforge ABS-GF @FF C5", + "Flashforge/Flashforge ABS-GF @FF C5P", + "Flashforge/Flashforge ASA Basic @FF C5", + "Flashforge/Flashforge ASA Basic @FF C5P", + "Flashforge/Flashforge ASA-CF @FF C5", + "Flashforge/Flashforge ASA-CF @FF C5P", + "Flashforge/Flashforge ASA-GF @FF C5", + "Flashforge/Flashforge ASA-GF @FF C5P", + "Flashforge/Flashforge Generic HS PLA", + "Flashforge/Flashforge Generic PETG", + "Flashforge/Flashforge Generic PETG-CF10", + "Flashforge/Flashforge Generic PLA", + "Flashforge/Flashforge Generic PLA-CF10", + "Flashforge/Flashforge Generic PLA-Silk", + "Flashforge/Flashforge HIPS @FF C5", + "Flashforge/Flashforge HIPS @FF C5P", + "Flashforge/Flashforge HS PETG @FF C5", + "Flashforge/Flashforge HS PETG @FF C5P", + "Flashforge/Flashforge HS PLA @FF C5", + "Flashforge/Flashforge HS PLA @FF C5P", + "Flashforge/Flashforge PA-CF @FF C5", + "Flashforge/Flashforge PA-CF @FF C5P", + "Flashforge/Flashforge PAHT-CF @FF C5", + "Flashforge/Flashforge PAHT-CF @FF C5P", + "Flashforge/Flashforge PET-CF @FF C5", + "Flashforge/Flashforge PET-CF @FF C5P", + "Flashforge/Flashforge PETG", + "Flashforge/Flashforge PETG Pro @FF C5", + "Flashforge/Flashforge PETG Pro @FF C5P", + "Flashforge/Flashforge PETG Transparent @FF C5", + "Flashforge/Flashforge PETG Transparent @FF C5P", + "Flashforge/Flashforge PETG-CF @FF C5", + "Flashforge/Flashforge PETG-CF @FF C5P", + "Flashforge/Flashforge PLA", + "Flashforge/Flashforge PLA Matte @FF C5", + "Flashforge/Flashforge PLA Matte @FF C5P", + "Flashforge/Flashforge PLA Metal @FF C5", + "Flashforge/Flashforge PLA Metal @FF C5P", + "Flashforge/Flashforge PLA Pro @FF C5", + "Flashforge/Flashforge PLA Pro @FF C5P", + "Flashforge/Flashforge PLA Basic @FF C5", + "Flashforge/Flashforge PLA Basic @FF C5P", + "Flashforge/Flashforge PLA Color Change @FF C5", + "Flashforge/Flashforge PLA Color Change @FF C5P", + "Flashforge/Flashforge PLA Galaxy @FF C5", + "Flashforge/Flashforge PLA Galaxy @FF C5P", + "Flashforge/Flashforge PLA Luminous @FF C5", + "Flashforge/Flashforge PLA Luminous @FF C5P", + "Flashforge/Flashforge PLA Silk @FF C5", + "Flashforge/Flashforge PLA Silk @FF C5P", + "Flashforge/Flashforge PLA-CF @FF C5", + "Flashforge/Flashforge PLA-CF @FF C5P", + "Flashforge/Flashforge PPA-CF @FF C5", + "Flashforge/Flashforge PPA-CF @FF C5P", + "Flashforge/Flashforge PPS-CF @FF C5", + "Flashforge/Flashforge PPS-CF @FF C5P", + "Flashforge/Flashforge PVA @FF C5", + "Flashforge/Flashforge PVA @FF C5P", + "Flashforge/Flashforge TPU-64D @FF C5", + "Flashforge/Flashforge TPU-64D @FF C5P", + "Flashforge/Flashforge TPU-90A @FF C5", + "Flashforge/Flashforge TPU-90A @FF C5P", + "Flashforge/Flashforge TPU-95A @FF C5", + "Flashforge/Flashforge TPU-95A @FF C5P", + "Flashforge/FusRock PAHT-CF @FF C5", + "Flashforge/FusRock PAHT-CF @FF C5P", + "Flashforge/FusRock S-Multi @FF C5", + "Flashforge/FusRock S-Multi @FF C5P", + "Flashforge/FusRock S-PAHT @FF C5", + "Flashforge/FusRock S-PAHT @FF C5P", + "Flashforge/Generic PETG @Flashforge Artemis", + "Flashforge/Generic TPU-64D @FF C5", + "Flashforge/Generic TPU-64D @FF C5P", + "Flashforge/Generic TPU-90A @FF C5", + "Flashforge/Generic TPU-90A @FF C5P", + "Flashforge/Generic TPU-95A @FF C5", + "Flashforge/Generic TPU-95A @FF C5P", + "Flashforge/SUNLU PETG @base", + "Flashforge/SUNLU PLA Marble @base", + "Flashforge/SUNLU PLA Matte @base", + "Flashforge/SUNLU PLA+ 2.0 @base", + "Flashforge/SUNLU PLA+ @base", + "Flashforge/SUNLU Silk PLA+ @base", + "Flashforge/SUNLU Wood PLA @base", + "FlyingBear/FlyingBear Generic PA-CF", + "FlyingBear/FlyingBear Generic PC", + "FlyingBear/FlyingBear Generic PETG", + "FlyingBear/FlyingBear Generic PLA", + "InfiMech/InfiMech Generic PA-CF", + "InfiMech/InfiMech Generic PC", + "InfiMech/InfiMech Generic PETG", + "InfiMech/InfiMech Generic PLA", + "LH/LHS PC CF", + "LH/LHS PCTG", + "LH/LHS PETG", + "LH/LHS PLA", + "LONGER/Generic PETG @LONGER LK10", + "LONGER/Generic PETG @LONGER LK10 Plus", + "LONGER/Generic PLA @LONGER LK10", + "LONGER/Generic PLA @LONGER LK10 Plus", + "Lulzbot/Lulzbot 2.85mm ABS", + "Lulzbot/Lulzbot 2.85mm PETG", + "Lulzbot/Lulzbot 2.85mm PLA", + "OrcaArena/Arena PA-CF @base", + "OrcaArena/Arena PAHT-CF @base", + "OrcaArena/Arena PC @base", + "OrcaArena/Arena PET-CF @base", + "OrcaArena/Arena PETG Basic @base", + "OrcaArena/Arena PETG-CF @base", + "OrcaArena/Arena PLA Basic @base", + "OrcaArena/Arena PLA Impact @base", + "OrcaArena/Arena PLA Marble @base", + "OrcaArena/Arena PLA Matte @base", + "OrcaArena/Arena PLA Metal @base", + "OrcaArena/Arena PLA Silk @base", + "OrcaArena/Arena PLA Sparkle @base", + "OrcaArena/Arena PLA Tough @base", + "OrcaArena/Arena PLA-CF @base", + "OrcaArena/Arena Support G @base", + "OrcaArena/Arena Support W @base", + "OrcaArena/OrcaArena Generic PA", + "OrcaArena/OrcaArena Generic PA-CF", + "OrcaArena/OrcaArena Generic PC @base", + "OrcaArena/OrcaArena Generic PETG @base", + "OrcaArena/OrcaArena Generic PETG-CF @base", + "OrcaArena/OrcaArena Generic PLA @base", + "OrcaArena/OrcaArena Generic PLA Silk @base", + "OrcaArena/OrcaArena Generic PLA-CF @base", + "OrcaArena/PolyLite PLA @base", + "OrcaArena/PolyTerra PLA @base", + "Peopoly/Peopoly Generic PLA", + "Peopoly/Peopoly Lancer PLA-C", + "Phrozen/Phrozen PLA @Phrozen Arco 0.4 nozzle", + "Prusa/Prusa Generic PA @MK3.5", + "Prusa/Prusa Generic PA @MK3.5 0.25", + "Prusa/Prusa Generic PA @MK3.5 0.6", + "Prusa/Prusa Generic PA @MK3.5 0.8", + "Prusa/Prusa Generic PA @base", + "Prusa/Prusa Generic PA-CF @MK3.5", + "Prusa/Prusa Generic PA-CF @MK3.5 0.25", + "Prusa/Prusa Generic PA-CF @MK3.5 0.6", + "Prusa/Prusa Generic PA-CF @MK3.5 0.8", + "Prusa/Prusa Generic PA-CF @base", + "Prusa/Prusa Generic PETG @CORE One", + "Prusa/Prusa Generic PETG @MK3.5", + "Prusa/Prusa Generic PETG @MK3.5 0.25", + "Prusa/Prusa Generic PETG @MK3.5 0.6", + "Prusa/Prusa Generic PETG @MK3.5 0.8", + "Prusa/Prusa Generic PETG @MK4S", + "Prusa/Prusa Generic PETG @base", + "Prusa/Prusa Generic PETG HF @MK3.5", + "Prusa/Prusa Generic PETG HF @MK3.5 0.6", + "Prusa/Prusa Generic PETG HF @MK3.5 0.8", + "Prusa/Prusa Generic PETG HF @base", + "Prusa/Prusa Generic PLA @CORE One", + "Prusa/Prusa Generic PLA @MK3.5", + "Prusa/Prusa Generic PLA @MK3.5 0.25", + "Prusa/Prusa Generic PLA @MK3.5 0.6", + "Prusa/Prusa Generic PLA @MK3.5 0.8", + "Prusa/Prusa Generic PLA @MK4S", + "Prusa/Prusa Generic PLA @base", + "Prusa/Prusa Generic PLA HF @MK3.5", + "Prusa/Prusa Generic PLA HF @MK3.5 0.6", + "Prusa/Prusa Generic PLA HF @MK3.5 0.8", + "Prusa/Prusa Generic PLA HF @base", + "Prusa/Prusa Generic PLA Silk @CORE One", + "Prusa/Prusa Generic PLA-CF @MK3.5", + "Prusa/Prusa Generic PLA-CF @MK3.5 0.25", + "Prusa/Prusa Generic PLA-CF @MK3.5 0.6", + "Prusa/Prusa Generic PLA-CF @MK3.5 0.8", + "Prusa/Prusa Generic PLA-CF @base", + "Prusa/Prusament PA-CF @CORE One", + "Prusa/Prusament PETG @CORE One", + "Prusa/Prusament PETG @base", + "Prusa/Prusament PLA @CORE One", + "Prusa/Prusament PLA @base", + "Prusa/Prusament rPLA @CORE One", + "Prusa/Prusament rPLA @base", + "Qidi/Bambu PETG", + "Qidi/Bambu PLA", + "Qidi/HATCHBOX PETG @Qidi", + "Qidi/HATCHBOX PLA @Qidi", + "Qidi/Overture PLA @Qidi", + "Qidi/PolyLite PLA @Qidi", + "Qidi/QIDI PA-Ultra", + "Qidi/QIDI PA12-CF", + "Qidi/QIDI PAHT-CF", + "Qidi/QIDI PAHT-GF", + "Qidi/QIDI PET-CF", + "Qidi/QIDI PET-GF", + "Qidi/QIDI PETG Basic", + "Qidi/QIDI PETG Rapido", + "Qidi/QIDI PETG Tough", + "Qidi/QIDI PETG Translucent", + "Qidi/QIDI PETG-CF", + "Qidi/QIDI PETG-GF", + "Qidi/QIDI PLA Basic", + "Qidi/QIDI PLA Matte Basic", + "Qidi/QIDI PLA Rapido", + "Qidi/QIDI PLA Rapido Matte", + "Qidi/QIDI PLA Rapido Metal", + "Qidi/QIDI PLA Rapido Silk", + "Qidi/QIDI PPS-CF", + "Qidi/QIDI PPS-GF", + "Qidi/QIDI Support For PAHT", + "Qidi/QIDI Support For PET/PA", + "Qidi/QIDI UltraPA-CF25", + "Qidi/QIDI WOOD Rapido", + "Qidi/Qidi Generic PA", + "Qidi/Qidi Generic PA-CF", + "Qidi/Qidi Generic PC", + "Qidi/Qidi Generic PETG", + "Qidi/Qidi Generic PETG-CF", + "Qidi/Qidi Generic PLA", + "Qidi/Qidi Generic PLA Silk", + "Qidi/Qidi Generic PLA+", + "Qidi/Qidi Generic PLA-CF", + "Qidi/Qidi PLA-CF", + "Qidi/Tinmorry PETG-ECO", + "Ratrig/RatRig BigNozzle PCTG", + "Ratrig/RatRig BigNozzle PETG", + "Ratrig/RatRig BigNozzle PLA", + "Ratrig/RatRig Generic PA", + "Ratrig/RatRig Generic PA-CF", + "Ratrig/RatRig Generic PC", + "Ratrig/RatRig Generic PCTG", + "Ratrig/RatRig Generic PETG", + "Ratrig/RatRig Generic PLA", + "Ratrig/RatRig Generic PLA-CF", + "Ratrig/RatRig PunkFil PETG", + "Ratrig/RatRig PunkFil PETG CF", + "SecKit/SecKit Generic PA", + "SecKit/SecKit Generic PA-CF", + "SecKit/SecKit Generic PC", + "SecKit/SecKit Generic PETG", + "SecKit/SecKit Generic PLA", + "SecKit/SecKit Generic PLA-CF", + "Snapmaker/Fiberon PA12-CF10 @Snapmaker U1 base", + "Snapmaker/Fiberon PA6-CF20 @Snapmaker U1 base", + "Snapmaker/Fiberon PA612-ESD @Snapmaker U1 base", + "Snapmaker/Panchroma CoPE @Snapmaker U1 base", + "Snapmaker/Panchroma PLA @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Celestial @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Galaxy @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Glow @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Luminous @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Marble @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Matte @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Metallic @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Neon @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Satin @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Silk @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Starlight @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Temp Shift @Snapmaker U1 base", + "Snapmaker/Panchroma PLA Translucent @Snapmaker U1 base", + "Snapmaker/Panchroma PLA UV Shift @Snapmaker U1 base", + "Snapmaker/PolyLite CosPLA @Snapmaker U1 base", + "Snapmaker/PolyLite PLA @Snapmaker U1 base", + "Snapmaker/PolyLite PLA @base", + "Snapmaker/PolyLite PLA Galaxy @Snapmaker U1 base", + "Snapmaker/PolyLite PLA Glow @Snapmaker U1 base", + "Snapmaker/PolyLite PLA Luminous @Snapmaker U1 base", + "Snapmaker/PolyLite PLA Neon @Snapmaker U1 base", + "Snapmaker/PolyLite PLA Pro @Snapmaker U1 base", + "Snapmaker/PolyLite PLA Pro Metallic @Snapmaker U1 base", + "Snapmaker/PolyLite PLA Starlight @Snapmaker U1 base", + "Snapmaker/PolyLite PLA Translucent @Snapmaker U1 base", + "Snapmaker/PolyTerra Dual PLA @0.2 nozzle", + "Snapmaker/PolyTerra J1 PLA", + "Snapmaker/PolyTerra J1 PLA @0.2 nozzle", + "Snapmaker/PolyTerra PLA @Snapmaker U1 base", + "Snapmaker/PolyTerra PLA Marble @Snapmaker U1 base", + "Snapmaker/PolyTerra PLA+ @Snapmaker U1 base", + "Snapmaker/Polymaker HT-PLA @Snapmaker U1 base", + "Snapmaker/Polymaker HT-PLA-GF @Snapmaker U1 base", + "Snapmaker/Polymaker PLA @Snapmaker U1 base", + "Snapmaker/Polymaker PLA Pro @Snapmaker U1 base", + "Snapmaker/Polymaker PLA Pro Metallic @Snapmaker U1 base", + "Snapmaker/Snapmaker Dual PA-CF @base", + "Snapmaker/Snapmaker Dual PET @base", + "Snapmaker/Snapmaker Dual PLA @base", + "Snapmaker/Snapmaker Dual PLA Eco @base", + "Snapmaker/Snapmaker Dual PLA Matte @base", + "Snapmaker/Snapmaker Dual PLA Metal @base", + "Snapmaker/Snapmaker Dual PLA Silk @base", + "Snapmaker/Snapmaker Dual PLA-CF @base", + "Snapmaker/Snapmaker J1 PA-CF @base", + "Snapmaker/Snapmaker J1 PET @base", + "Snapmaker/Snapmaker J1 PLA @base", + "Snapmaker/Snapmaker J1 PLA Eco @base", + "Snapmaker/Snapmaker J1 PLA Matte @base", + "Snapmaker/Snapmaker J1 PLA Metal @base", + "Snapmaker/Snapmaker J1 PLA Silk @base", + "Snapmaker/Snapmaker J1 PLA-CF @base", + "Snapmaker/Snapmaker PA-CF @U1 base", + "Snapmaker/Snapmaker PA-CF @base", + "Snapmaker/Snapmaker PET @U1 base", + "Snapmaker/Snapmaker PET @base", + "Snapmaker/Snapmaker PLA @U1 base", + "Snapmaker/Snapmaker PLA @base", + "Snapmaker/Snapmaker PLA Eco @U1 base", + "Snapmaker/Snapmaker PLA Eco @base", + "Snapmaker/Snapmaker PLA Metal @U1 base", + "Snapmaker/Snapmaker PLA Silk @U1 base", + "Snapmaker/Snapmaker PLA Silk @base", + "Snapmaker/Snapmaker PLA-CF @U1 base", + "Snapmaker/Snapmaker PLA-CF @base", + "Sovol/Generic PLA Silk @Sovol SV08 MAX", + "Sovol/Polymaker PETG @Sovol SV08 MAX", + "Sovol/SUNLU PETG @Sovol SV08 MAX", + "Sovol/Sovol SV06 ACE ABS", + "Sovol/Sovol SV06 ACE PETG", + "Sovol/Sovol SV06 ACE PLA", + "Sovol/Sovol SV06 ACE TPU", + "Sovol/Sovol SV06 Plus ACE ABS", + "Sovol/Sovol SV06 Plus ACE PETG", + "Sovol/Sovol SV06 Plus ACE PLA", + "Sovol/Sovol SV06 Plus ACE TPU", + "Sovol/Sovol SV07 PLA", + "Sovol/Sovol SV08 ABS", + "Sovol/Sovol SV08 PETG", + "Sovol/Sovol SV08 PLA", + "Sovol/Sovol SV08 PLA @SV08 0.2 nozzle", + "Sovol/Sovol SV08 TPU", + "Sovol/Sovol Zero ABS", + "Sovol/Sovol Zero PC", + "Sovol/Sovol Zero PETG", + "Sovol/Sovol Zero PETG HS Nozzle", + "Sovol/Sovol Zero PLA Basic", + "Sovol/Sovol Zero PLA Basic HS Nozzle", + "Sovol/Sovol Zero PLA Silk", + "Sovol/Sovol Zero PLA Silk HS Nozzle", + "Sovol/Sovol Zero TPU", + "Tiertime/Tiertime Generic BVOH", + "Tiertime/Tiertime Generic BVOH@300HS", + "Tiertime/Tiertime Generic EVA", + "Tiertime/Tiertime Generic EVA@300HS", + "Tiertime/Tiertime Generic HIPS", + "Tiertime/Tiertime Generic HIPS@300HS", + "Tiertime/Tiertime Generic PA", + "Tiertime/Tiertime Generic PA-CF", + "Tiertime/Tiertime Generic PA-CF@300HS", + "Tiertime/Tiertime Generic PA@300HS", + "Tiertime/Tiertime Generic PC", + "Tiertime/Tiertime Generic PC@300HS", + "Tiertime/Tiertime Generic PCTG", + "Tiertime/Tiertime Generic PCTG@300HS", + "Tiertime/Tiertime Generic PE", + "Tiertime/Tiertime Generic PE-CF", + "Tiertime/Tiertime Generic PE-CF@300HS", + "Tiertime/Tiertime Generic PE@300HS", + "Tiertime/Tiertime Generic PETG", + "Tiertime/Tiertime Generic PETG-CF", + "Tiertime/Tiertime Generic PETG-CF@300HS", + "Tiertime/Tiertime Generic PETG@300HS", + "Tiertime/Tiertime Generic PHA", + "Tiertime/Tiertime Generic PHA@300HS", + "Tiertime/Tiertime Generic PLA", + "Tiertime/Tiertime Generic PLA High Speed", + "Tiertime/Tiertime Generic PLA High Speed@300HS", + "Tiertime/Tiertime Generic PLA Silk", + "Tiertime/Tiertime Generic PLA Silk@300HS", + "Tiertime/Tiertime Generic PLA-CF", + "Tiertime/Tiertime Generic PLA-CF@300HS", + "Tiertime/Tiertime Generic PLA@300HS", + "Tiertime/Tiertime Generic PP", + "Tiertime/Tiertime Generic PP-CF", + "Tiertime/Tiertime Generic PP-CF@300HS", + "Tiertime/Tiertime Generic PP-GF", + "Tiertime/Tiertime Generic PP-GF@300HS", + "Tiertime/Tiertime Generic PP@300HS", + "Tiertime/Tiertime Generic PPA-CF", + "Tiertime/Tiertime Generic PPA-CF@300HS", + "Tiertime/Tiertime Generic PPA-GF", + "Tiertime/Tiertime Generic PPA-GF@300HS", + "Tiertime/Tiertime Generic SBS", + "Tiertime/Tiertime Generic SBS@300HS", + "Tiertime/Tiertime PA6-CF", + "Tiertime/Tiertime PA6-CF@300HS", + "Tiertime/Tiertime PC", + "Tiertime/Tiertime PC@300HS", + "Tiertime/Tiertime PET-CF", + "Tiertime/Tiertime PET-CF@300HS", + "Tiertime/Tiertime PETG", + "Tiertime/Tiertime PETG@300HS", + "Tiertime/Tiertime PLA", + "Tiertime/Tiertime PLA-CF", + "Tiertime/Tiertime PLA-CF@300HS", + "Tiertime/Tiertime PLA@300HS", + "TwoTrees/TwoTrees Generic HS PLA @SK1", + "Volumic/Désactivé", + "Volumic/PA6 CF20 (Performance)", + "Volumic/PETG ESD (Performance)", + "Volumic/PPS Carbone (Performance)", + "Volumic/Volumic NYLON Ultra", + "Volumic/Volumic NYLON Ultra (Performance)", + "Volumic/Volumic PC", + "Volumic/Volumic PC (Performance)", + "Volumic/Volumic PCTG Ultra (Performance)", + "Volumic/Volumic PETG Ultra", + "Volumic/Volumic PETG Ultra (Performance)", + "Volumic/Volumic PETG Ultra carbone", + "Volumic/Volumic PETG Ultra carbone (Performance)", + "Volumic/Volumic PLA Ultra", + "Volumic/Volumic PLA Ultra (Performance)", + "Volumic/Volumic PP Ultra", + "Volumic/Volumic PP Ultra (Performance)", + "Volumic/Volumic UNIVERSAL Ultra", + "Volumic/Volumic UNIVERSAL Ultra (Performance)", + "Vzbot/Vzbot Generic PA", + "Vzbot/Vzbot Generic PA-CF", + "Vzbot/Vzbot Generic PC", + "Vzbot/Vzbot Generic PETG", + "Vzbot/Vzbot Generic PLA", + "Vzbot/Vzbot Generic PLA-CF", + "Wanhao France/Yumi Generic PETG", + "Wanhao France/Yumi Generic PLA", + "WonderMaker/WonderMaker PET-CF", + "WonderMaker/WonderMaker PETG Basic", + "WonderMaker/WonderMaker PLA Basic", + "WonderMaker/WonderMaker PLA Marble", + "WonderMaker/WonderMaker PLA Matte", + "WonderMaker/WonderMaker PLA Metal", + "WonderMaker/WonderMaker PLA Silk", + "WonderMaker/WonderMaker PLA Wood", + "Z-Bolt/Generic PA @Z-Bolt Base", + "Z-Bolt/Generic PETG @Z-Bolt Base", + "Z-Bolt/Generic PLA @Z-Bolt Base", + "re3D/re3D Greengate rPETG", + "re3D/re3D Greengate rPETG @0.8 nozzle", + "re3D/re3D Greengate rPETG @1.75 nozzle", + "re3D/re3D PC", + "re3D/re3D PC @0.4 nozzle", + "re3D/re3D PC @0.8 nozzle", + "re3D/re3D PETG", + "re3D/re3D PETG @0.4 nozzle", + "re3D/re3D PETG @0.8 nozzle", + "re3D/re3D PLA", + "re3D/re3D PLA @0.4 nozzle", + "re3D/re3D PLA @0.8 nozzle", + "re3D/re3D rPP", + "re3D/re3D rPP @0.8 nozzle", + "re3D/re3D rPP @1.75 nozzle" + ], "id_overrides": [ "Afinia/Afinia PLA", "Afinia/Afinia PLA@HS", @@ -9446,5 +10483,9965 @@ "re3D/re3D rPP", "re3D/re3D rPP @0.8 nozzle", "re3D/re3D rPP @1.75 nozzle" - ] + ], + "triple_exceptions": [ + "Anycubic/Anycubic ABS", + "Anycubic/Anycubic ASA", + "Anycubic/Anycubic PETG", + "Anycubic/Anycubic PLA", + "Anycubic/Anycubic PLA High Speed", + "Anycubic/Anycubic PLA Silk", + "Anycubic/Anycubic PLA+", + "Anycubic/Anycubic TPU", + "Anycubic/Generic PETG", + "Creality/Creality Generic ABS", + "Creality/Creality Generic ASA", + "Creality/Creality Generic PETG", + "Creality/Creality Generic TPU", + "Creality/Generic PETG-GF", + "Flashforge/Flashforge ABS", + "Flashforge/Flashforge ASA-CF", + "Flashforge/Flashforge PET-CF", + "Flashforge/Flashforge PETG", + "Flashforge/Flashforge PPA-CF", + "Flashforge/Flashforge PPS-CF", + "Flashforge/FusRock Generic PAHT-CF", + "Flashforge/FusRock Generic PET-CF", + "Flashforge/FusRock Generic S-Multi", + "Flashforge/FusRock Generic S-PAHT", + "Prusa/Prusa Generic ASA", + "Prusa/Prusa Generic TPU", + "Prusa/Prusa Generic TPU HF", + "Prusa/Prusament PETG", + "Prusa/Prusament PLA", + "Prusa/Prusament rPLA", + "Snapmaker/PolyLite PLA", + "re3D/re3D Greengate rPETG" + ], + "triples": { + "00001": [ + [ + "Generic", + "PLA", + "Generic PLA" + ] + ], + "00002": [ + [ + "Generic", + "PLA", + "Generic PLA-Silk" + ] + ], + "00003": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "00004": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "00005": [ + [ + "Generic", + "TPU", + "Generic TPU" + ] + ], + "00006": [ + [ + "Generic", + "PLA-CF", + "Generic PLA-CF" + ] + ], + "00007": [ + [ + "Generic", + "ASA", + "Generic ASA" + ] + ], + "00008": [ + [ + "Generic", + "PA", + "Generic PA" + ] + ], + "00009": [ + [ + "Generic", + "PA-CF", + "Generic PA-CF" + ] + ], + "00010": [ + [ + "Generic", + "BVOH", + "Generic BVOH" + ] + ], + "00011": [ + [ + "Generic", + "PVA", + "Generic PVA" + ] + ], + "00012": [ + [ + "Generic", + "HIPS", + "Generic HIPS" + ] + ], + "00013": [ + [ + "Generic", + "PET-CF", + "Generic PET-CF" + ] + ], + "00014": [ + [ + "Generic", + "PETG-CF", + "Generic PETG-CF" + ] + ], + "00015": [ + [ + "Generic", + "PA6-CF", + "Generic PA6-CF" + ] + ], + "00016": [ + [ + "Generic", + "PA-CF", + "Generic PAHT-CF" + ] + ], + "00017": [ + [ + "Generic", + "PPS", + "Generic PPS" + ] + ], + "00018": [ + [ + "Generic", + "PPS-CF", + "Generic PPS-CF" + ] + ], + "00019": [ + [ + "Generic", + "PP", + "Generic PP" + ] + ], + "00020": [ + [ + "Generic", + "PET", + "Generic PET" + ] + ], + "00021": [ + [ + "Generic", + "PC", + "Generic PC" + ] + ], + "00022": [ + [ + "Generic", + "PA-CF", + "Generic PA612-CF" + ] + ], + "00023": [ + [ + "Generic", + "PA", + "Generic Support for PA" + ] + ], + "00024": [ + [ + "Generic", + "PLA", + "Generic Support for PLA" + ] + ], + "00025": [ + [ + "Generic", + "PA-CF", + "Generic PA12-CF" + ] + ], + "00026": [ + [ + "Generic", + "TPU", + "Generic TPU 64D" + ] + ], + "00027": [ + [ + "Creality", + "PETG-GF", + "Generic PETG-GF" + ], + [ + "Generic", + "PETG-GF", + "Generic PETG-GF" + ] + ], + "00031": [ + [ + "Generic", + "PP-CF", + "Generic PP-CF" + ] + ], + "00032": [ + [ + "Generic", + "PCTG", + "Generic PCTG" + ] + ], + "00033": [ + [ + "Generic", + "ASA-CF", + "Generic ASA-CF" + ] + ], + "00034": [ + [ + "Generic", + "PA-GF", + "Generic PA6-GF" + ] + ], + "00035": [ + [ + "eSUN", + "PLA", + "eSUN PLA-LW" + ] + ], + "01001": [ + [ + "Creality", + "PLA", + "Creality Hyper PLA" + ], + [ + "Creality", + "PLA", + "Hyper PLA" + ] + ], + "01002": [ + [ + "Creality", + "PLA", + "Hyper L-W PLA" + ] + ], + "01003": [ + [ + "Creality", + "PLA", + "Hyper Luminous" + ] + ], + "01004": [ + [ + "Creality", + "PLA", + "Hyper Stardust" + ] + ], + "01601": [ + [ + "Creality", + "PLA", + "Soleyin Ultra PLA" + ] + ], + "02001": [ + [ + "Creality", + "PLA-CF", + "Creality Hyper PLA-CF" + ], + [ + "Creality", + "PLA-CF", + "Hyper PLA-CF" + ] + ], + "03001": [ + [ + "Creality", + "ABS", + "Creality Hyper ABS" + ], + [ + "Creality", + "ABS", + "Hyper ABS" + ] + ], + "04001": [ + [ + "Creality", + "PLA", + "CR-PLA" + ], + [ + "Generic", + "PLA", + "Creality Generic PLA" + ] + ], + "05001": [ + [ + "Creality", + "PLA", + "CR-Silk" + ], + [ + "Creality", + "PLA", + "Creality Silk PLA" + ] + ], + "06001": [ + [ + "Creality", + "PETG", + "CR-PETG" + ], + [ + "Creality", + "PETG", + "Creality Generic PETG" + ] + ], + "06002": [ + [ + "Creality", + "PETG", + "Hyper PETG" + ] + ], + "06003": [ + [ + "Creality", + "PETG-CF", + "Hyper PETG-CF" + ] + ], + "06004": [ + [ + "Creality", + "PETG-GF", + "Hyper PETG-GF" + ] + ], + "06005": [ + [ + "Creality", + "PETG", + "Soleyin Basic PETG" + ] + ], + "07001": [ + [ + "Creality", + "ABS", + "CR-ABS" + ], + [ + "Creality", + "ABS", + "Creality Generic ABS" + ] + ], + "07002": [ + [ + "Creality", + "PC", + "Hyper PC" + ] + ], + "08001": [ + [ + "Creality", + "PLA", + "Ender-PLA" + ] + ], + "09001": [ + [ + "Creality", + "PLA", + "EN-PLA+" + ] + ], + "09002": [ + [ + "Creality", + "PLA", + "ENDER FAST PLA" + ] + ], + "10001": [ + [ + "Creality", + "TPU", + "Creality Generic TPU" + ], + [ + "Creality", + "TPU", + "HP-TPU" + ] + ], + "1009481135": [ + [ + "Snapmaker", + "PET", + "Snapmaker J1 PET" + ] + ], + "1042511226": [ + [ + "Snapmaker", + "PETG-CF", + "Snapmaker Dual PETG-CF" + ] + ], + "10425112260": [ + [ + "Snapmaker", + "PETG-CF", + "Snapmaker PETG-CF" + ] + ], + "11001": [ + [ + "Creality", + "PA", + "CR-Nylon" + ], + [ + "Creality", + "PA", + "Creality Generic PA" + ] + ], + "1172603684": [ + [ + "Snapmaker", + "PETG", + "Snapmaker J1 PETG" + ] + ], + "1181363872": [ + [ + "Snapmaker", + "PLA", + "Snapmaker Dual PLA Silk" + ] + ], + "11813638720": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA Silk" + ] + ], + "1192769348": [ + [ + "Snapmaker", + "PLA", + "Snapmaker J1 PLA Matte" + ] + ], + "12002": [ + [ + "Creality", + "PA-CF", + "Hyper PPA-CF" + ] + ], + "12003": [ + [ + "Creality", + "PA-CF", + "Hyper PAHT-CF" + ] + ], + "12004": [ + [ + "Creality", + "PA-CF", + "Hyper PA612-CF" + ] + ], + "12005": [ + [ + "Creality", + "PA-CF", + "Hyper PA6-CF" + ] + ], + "1207881278": [ + [ + "Snapmaker", + "Breakaway Support", + "Snapmaker Dual Breakaway" + ] + ], + "1210173120": [ + [ + "Snapmaker", + "PA-CF", + "Snapmaker J1 PA-CF" + ] + ], + "1223824394": [ + [ + "Snapmaker", + "ABS", + "Snapmaker J1 ABS" + ] + ], + "1247172706": [ + [ + "Snapmaker", + "ASA", + "Snapmaker Dual ASA" + ] + ], + "12471727060": [ + [ + "Snapmaker", + "ASA", + "Snapmaker ASA" + ] + ], + "13001": [ + [ + "Creality", + "PLA-CF", + "CR-PLA Carbon" + ] + ], + "1344609062": [ + [ + "Snapmaker", + "PVA", + "Snapmaker PVA" + ] + ], + "1355502217": [ + [ + "Snapmaker", + "PETG-CF", + "Snapmaker PETG-CF" + ] + ], + "1393866034": [ + [ + "Snapmaker", + "PLA", + "PolyLite PLA" + ] + ], + "14001": [ + [ + "Creality", + "PLA", + "CR-PLA Matte" + ] + ], + "1417031127": [ + [ + "Snapmaker", + "PLA", + "Snapmaker Dual PLA" + ] + ], + "14170311270": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA" + ] + ], + "141703112701": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA SnapSpeed" + ] + ], + "1417031127011": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA Lite" + ] + ], + "144877656": [ + [ + "Snapmaker", + "ASA", + "Snapmaker J1 ASA" + ] + ], + "1480063856": [ + [ + "Snapmaker", + "TPU", + "Snapmaker TPU" + ] + ], + "15001": [ + [ + "Creality", + "PLA", + "CR-PLA Fluo" + ] + ], + "1528786603": [ + [ + "Snapmaker", + "PLA", + "Snapmaker J1 PLA Silk" + ] + ], + "16001": [ + [ + "Creality", + "TPU", + "CR-TPU" + ] + ], + "1655727393": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA Silk" + ] + ], + "168223792": [ + [ + "Snapmaker", + "ABS", + "Snapmaker Dual ABS" + ] + ], + "1682237920": [ + [ + "Snapmaker", + "ABS", + "Snapmaker ABS" + ] + ], + "1695556157": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA Eco" + ] + ], + "17001": [ + [ + "Creality", + "PLA", + "CR-Wood" + ] + ], + "1702147325": [ + [ + "Snapmaker", + "PLA-CF", + "Snapmaker Dual PLA-CF" + ] + ], + "17021473250": [ + [ + "Snapmaker", + "PLA-CF", + "Snapmaker PLA-CF" + ] + ], + "18001": [ + [ + "Creality", + "PLA", + "HP Ultra PLA" + ] + ], + "1895495477": [ + [ + "Snapmaker", + "PETG", + "Snapmaker PETG" + ] + ], + "19001": [ + [ + "Creality", + "ASA", + "Creality Generic ASA" + ], + [ + "Creality", + "ASA", + "HP-ASA" + ] + ], + "200803790": [ + [ + "Snapmaker", + "PLA", + "Snapmaker Dual PLA Eco" + ] + ], + "2008037900": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA Eco" + ] + ], + "2029994346": [ + [ + "Snapmaker", + "PLA", + "Snapmaker Dual PLA Metal" + ] + ], + "20299943460": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA Metal" + ] + ], + "2128577941": [ + [ + "Snapmaker", + "PET", + "Snapmaker Dual PET" + ] + ], + "21285779410": [ + [ + "Snapmaker", + "PET", + "Snapmaker PET" + ] + ], + "2209001062": [ + [ + "Snapmaker", + "PETG", + "Snapmaker Dual PETG" + ] + ], + "22090010620": [ + [ + "Snapmaker", + "PETG", + "Snapmaker PETG" + ] + ], + "2549587591": [ + [ + "Snapmaker", + "PET", + "Snapmaker PET" + ] + ], + "2742961008": [ + [ + "Snapmaker", + "ASA", + "Snapmaker ASA" + ] + ], + "29001": [ + [ + "Creality", + "PLA", + "Hyper Marble" + ] + ], + "2971656290": [ + [ + "Snapmaker", + "TPU", + "Snapmaker Dual TPU" + ] + ], + "29716562900": [ + [ + "Snapmaker", + "TPU", + "Snapmaker TPU" + ] + ], + "297165629001": [ + [ + "Generic", + "TPU", + "Snapmaker TPU 95A" + ] + ], + "3104636980": [ + [ + "Snapmaker", + "PVA", + "Snapmaker Dual PVA" + ] + ], + "31046369800": [ + [ + "Snapmaker", + "PVA", + "Snapmaker PVA" + ] + ], + "3177068229": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA" + ] + ], + "3383257822": [ + [ + "Snapmaker", + "PLA", + "Snapmaker J1 PLA Eco" + ] + ], + "3492897526": [ + [ + "Snapmaker", + "Breakaway Support", + "Snapmaker J1 Breakaway" + ] + ], + "3493177425": [ + [ + "Snapmaker", + "PA-CF", + "Snapmaker Dual PA-CF" + ] + ], + "34931774250": [ + [ + "Snapmaker", + "PA-CF", + "Snapmaker PA-CF" + ] + ], + "3503790988": [ + [ + "Snapmaker", + "PLA", + "Snapmaker Dual PLA Matte" + ] + ], + "377675245": [ + [ + "Snapmaker", + "PLA", + "Snapmaker J1 PLA" + ] + ], + "3806593857": [ + [ + "Snapmaker", + "PLA-CF", + "Snapmaker J1 PLA-CF" + ] + ], + "3811508002": [ + [ + "Snapmaker", + "ABS", + "Snapmaker ABS" + ] + ], + "3864371306": [ + [ + "Snapmaker", + "PLA-CF", + "Snapmaker PLA-CF" + ] + ], + "4012961186": [ + [ + "Snapmaker", + "PLA", + "Snapmaker J1 PLA Metal" + ] + ], + "4092268632": [ + [ + "Snapmaker", + "TPU", + "Snapmaker J1 TPU" + ] + ], + "4227461134": [ + [ + "Snapmaker", + "PVA", + "Snapmaker J1 PVA" + ] + ], + "4235401834": [ + [ + "Snapmaker", + "PETG-CF", + "Snapmaker J1 PETG-CF" + ] + ], + "581236806": [ + [ + "Snapmaker", + "PA-CF", + "Snapmaker PA-CF" + ] + ], + "AZ01-1": [ + [ + "Aliz", + "PETG-CF", + "AliZ PETG-CF" + ] + ], + "AZ01-2": [ + [ + "Aliz", + "PETG", + "AliZ PETG-Metal" + ] + ], + "AliZ001": [ + [ + "Aliz", + "PETG", + "AliZ PETG" + ] + ], + "AliZ002": [ + [ + "Aliz", + "PLA", + "AliZ PLA" + ] + ], + "AliZ003": [ + [ + "Aliz", + "PA-CF", + "AliZ PA-CF" + ] + ], + "BSFI001": [ + [ + "Generic", + "PLA", + "Blocks Generic PLA" + ] + ], + "BSFI002": [ + [ + "Generic", + "PETG", + "Blocks Generic PETG" + ] + ], + "BSFI003": [ + [ + "Generic", + "TPU", + "Blocks Generic TPU" + ] + ], + "BSFI004": [ + [ + "Generic", + "ABS", + "Blocks Generic ABS" + ] + ], + "BSFI005": [ + [ + "Generic", + "ASA", + "Blocks Generic ASA" + ] + ], + "BSFI006": [ + [ + "Generic", + "PA", + "Blocks Generic PA" + ] + ], + "BSFI007": [ + [ + "Generic", + "PA-CF", + "Blocks Generic PA-CF" + ] + ], + "BSFI008": [ + [ + "Generic", + "PC", + "Blocks Generic PC" + ] + ], + "BSFI009": [ + [ + "Generic", + "PVA", + "Blocks Generic PVA" + ] + ], + "BSFI010": [ + [ + "Generic", + "PLA-CF", + "Blocks Generic PLA-CF" + ] + ], + "CX30DB20": [ + [ + "COEX 3D", + "TPU", + "COEX TPE 30D" + ] + ], + "CX40DB20": [ + [ + "COEX 3D", + "TPU", + "COEX TPE 40D" + ] + ], + "CX60AB17": [ + [ + "COEX 3D", + "TPU", + "COEX TPU 60A" + ] + ], + "CX60DB19": [ + [ + "COEX 3D", + "TPU", + "COEX TPE 60D" + ] + ], + "CXABPB09": [ + [ + "COEX 3D", + "ABS", + "COEX ABS PRIME" + ] + ], + "CXABSB01": [ + [ + "COEX 3D", + "ABS", + "COEX ABS" + ] + ], + "CXASAB04": [ + [ + "COEX 3D", + "ASA", + "COEX ASA PRIME" + ] + ], + "CXPACB23": [ + [ + "COEX 3D", + "PA-CF", + "COEX NYLEX PA6-CF" + ] + ], + "CXPAUB21": [ + [ + "COEX 3D", + "PA", + "COEX NYLEX UNFILLED" + ] + ], + "CXPCTB15": [ + [ + "COEX 3D", + "PCTG", + "COEX PCTG PRIME" + ] + ], + "CXPETB13": [ + [ + "COEX 3D", + "PETG", + "COEX PETG" + ] + ], + "CXPLAB02": [ + [ + "COEX 3D", + "PLA", + "COEX PLA" + ] + ], + "CXPLAB08": [ + [ + "COEX 3D", + "PLA", + "COEX PLA PRIME" + ] + ], + "CXPLSB03": [ + [ + "COEX 3D", + "PLA", + "COEX PLA+Silk" + ] + ], + "DFL99": [ + [ + "Volumic", + "PLA", + "Désactivé" + ] + ], + "DREMC001": [ + [ + "DREMC", + "PLA", + "DREMC PLA+" + ] + ], + "DREMC002": [ + [ + "DREMC", + "PETG", + "DREMC PETG" + ] + ], + "DREMC003": [ + [ + "DREMC", + "ABS", + "DREMC ABS" + ] + ], + "DREMC004": [ + [ + "DREMC", + "ABS", + "DREMC ABS+" + ] + ], + "DREMC005": [ + [ + "DREMC", + "ASA", + "DREMC ASA" + ] + ], + "DREMC006": [ + [ + "DREMC", + "PA6-CF", + "DREMC PA6-CF" + ] + ], + "DREMC007": [ + [ + "DREMC", + "PA-CF", + "DREMC PA12-CF" + ] + ], + "DREMC008": [ + [ + "DREMC", + "PET-CF", + "DREMC PET-CF" + ] + ], + "DREMC009": [ + [ + "DREMC", + "ABS-GF", + "DREMC ABS-GF" + ] + ], + "DREMC010": [ + [ + "DREMC", + "PPA-CF", + "DREMC PPA-CF" + ], + [ + "DREMC", + "TPU", + "DREMC TPU 95A" + ] + ], + "DREMC011": [ + [ + "DREMC", + "PLA", + "DREMC PLA+ HS" + ] + ], + "DREMC012": [ + [ + "DREMC", + "ASA", + "DREMC ASA CF" + ] + ], + "DREMC013": [ + [ + "DREMC", + "ASA", + "DREMC ASA GF" + ] + ], + "E1001": [ + [ + "eSUN", + "PLA", + "eSUN PLA+" + ] + ], + "E1002": [ + [ + "eSUN", + "PLA", + "eSUN PLA-Silk" + ] + ], + "E1003": [ + [ + "eSUN", + "PLA", + "eSUN PLA-Matte" + ] + ], + "E1004": [ + [ + "eSUN", + "PLA", + "eSUN PLA-Lite" + ] + ], + "E1005": [ + [ + "eSUN", + "PLA-CF", + "eSUN PLA-CF" + ] + ], + "E1006": [ + [ + "eSUN", + "PLA", + "eSUN PLA-HS" + ] + ], + "E2001": [ + [ + "eSUN", + "PETG", + "eSUN PETG" + ] + ], + "E2002": [ + [ + "eSUN", + "PETG", + "eSUN PETG+HS" + ] + ], + "E2003": [ + [ + "eSUN", + "PETG", + "eSUN PETG-Basic" + ] + ], + "E3001": [ + [ + "eSUN", + "ABS", + "eSUN ABS+" + ] + ], + "E4001": [ + [ + "eSUN", + "ASA", + "eSUN ASA+" + ] + ], + "E8001": [ + [ + "eSUN", + "PET", + "eSUN PET-Basic" + ] + ], + "EABSB00": [ + [ + "Elegoo", + "ABS", + "Elegoo ABS" + ] + ], + "EASAB00": [ + [ + "Elegoo", + "ASA", + "Elegoo ASA" + ] + ], + "EFL33": [ + [ + "Eryone", + "PP-CF", + "Eryone PP-CF" + ] + ], + "EFL40": [ + [ + "Eryone", + "PLA-CF", + "Eryone PLA-CF" + ] + ], + "EFL43": [ + [ + "Eryone", + "PETG-CF", + "Eryone PETG-CF" + ] + ], + "EFL71": [ + [ + "Eryone", + "PA", + "Eryone PA" + ] + ], + "EFL72": [ + [ + "Eryone", + "PA-CF", + "Eryone PA-CF" + ] + ], + "EFL73": [ + [ + "Eryone", + "PA-GF", + "Eryone PA-GF" + ] + ], + "EFL81": [ + [ + "Eryone", + "ABS-CF", + "Eryone ABS-CF" + ] + ], + "EFL82": [ + [ + "Eryone", + "ASA-CF", + "Eryone ASA-CF" + ] + ], + "EFL90": [ + [ + "Eryone", + "PLA", + "Eryone PLA" + ] + ], + "EFL91": [ + [ + "Eryone", + "ABS", + "Eryone ABS" + ] + ], + "EFL92": [ + [ + "Eryone", + "ASA", + "Eryone ASA" + ] + ], + "EFL93": [ + [ + "Eryone", + "PETG", + "Eryone PETG" + ] + ], + "EFL941": [ + [ + "Eryone", + "PLA Silk", + "Eryone Silk PLA" + ] + ], + "EFL95": [ + [ + "Eryone", + "TPU", + "Eryone TPU" + ] + ], + "EPAHTB00": [ + [ + "Elegoo", + "PA", + "Elegoo PAHT" + ] + ], + "EPCB00": [ + [ + "Elegoo", + "PC", + "Elegoo PC" + ] + ], + "EPETGB00": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG" + ] + ], + "EPLAB00": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA" + ] + ], + "ESN02": [ + [ + "eSUN", + "PLA-AERO", + "eSUN ePLA-LW" + ] + ], + "ESN03": [ + [ + "eSUN", + "PETG", + "eSUN PETG" + ] + ], + "ETPUB00": [ + [ + "Elegoo", + "TPU", + "Elegoo TPU" + ] + ], + "FDPLAST01": [ + [ + "FDplast", + "ABS", + "FDplast ABS" + ] + ], + "FDPLAST02": [ + [ + "FDplast", + "HIPS", + "FDplast HIPS" + ] + ], + "FDPLAST03": [ + [ + "FDplast", + "PETG", + "FDplast PETG" + ] + ], + "FDPLAST04": [ + [ + "FDplast", + "PLA", + "FDplast PLA" + ] + ], + "FDPLAST05": [ + [ + "FDplast", + "SBS", + "FDplast SBS" + ] + ], + "FDPLAST06": [ + [ + "FDplast", + "TPU", + "FDplast TPU" + ] + ], + "FFF01": [ + [ + "Flashforge", + "PLA", + "Flashforge PLA" + ] + ], + "FFF02": [ + [ + "Flashforge", + "ABS", + "Flashforge ABS" + ] + ], + "FFF03": [ + [ + "Flashforge", + "PETG", + "Flashforge PETG" + ], + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "FFG01": [ + [ + "Generic", + "PLA", + "Flashforge Generic PLA" + ] + ], + "FILAR0001": [ + [ + "FilAr", + "PLA", + "FilAr PLA Bronce" + ] + ], + "FILAR0002": [ + [ + "FilAr", + "PLA", + "FilAr PLA Gris Plata" + ] + ], + "FILAR0003": [ + [ + "FilAr", + "PLA", + "FilAr PLA Cobre" + ] + ], + "FILAR0004": [ + [ + "FilAr", + "PLA", + "FilAr PLA Titanio" + ] + ], + "FILAR0005": [ + [ + "FilAr", + "PLA", + "FilAr PLA Tabaco" + ] + ], + "FILAR0006": [ + [ + "FilAr", + "PLA", + "FilAr PLA Cafe con Leche" + ] + ], + "FILAR0007": [ + [ + "FilAr", + "PLA", + "FilAr PLA Manteca" + ] + ], + "FILAR0008": [ + [ + "FilAr", + "PLA", + "FilAr PLA Marron Oxido" + ] + ], + "FILAR0009": [ + [ + "FilAr", + "PLA", + "FilAr PLA Carpincho" + ] + ], + "FILAR0010": [ + [ + "FilAr", + "PLA", + "FilAr PLA Rosa Amaranto" + ] + ], + "FILAR0011": [ + [ + "FilAr", + "PLA", + "FilAr PLA Rosa Flamenco" + ] + ], + "FILAR0012": [ + [ + "FilAr", + "PLA", + "FilAr PLA Piel" + ] + ], + "FILAR0013": [ + [ + "FilAr", + "PLA", + "FilAr PLA Verde FilAr" + ] + ], + "FILAR0014": [ + [ + "FilAr", + "PLA", + "FilAr PLA Verde Manzana" + ] + ], + "FILAR0015": [ + [ + "FilAr", + "PLA", + "FilAr PLA Verde Pixel" + ] + ], + "FILAR0016": [ + [ + "FilAr", + "PLA", + "FilAr PLA Verde Oliva" + ] + ], + "FILAR0017": [ + [ + "FilAr", + "PLA", + "FilAr PLA Blanco Antartida" + ] + ], + "FILAR0018": [ + [ + "FilAr", + "PLA", + "FilAr PLA Blanco Calido" + ] + ], + "FILAR0019": [ + [ + "FilAr", + "PLA", + "FilAr PLA Negro Azabache" + ] + ], + "FILAR0020": [ + [ + "FilAr", + "PLA", + "FilAr PLA Naranja Tigre" + ] + ], + "FILAR0021": [ + [ + "FilAr", + "PLA", + "FilAr PLA Rojo de Carreras" + ] + ], + "FILAR0022": [ + [ + "FilAr", + "PLA", + "FilAr PLA Amarillo Lirio" + ] + ], + "FILAR0023": [ + [ + "FilAr", + "PLA", + "FilAr PLA Violeta Jacaranda" + ] + ], + "FILAR0024": [ + [ + "FilAr", + "PLA", + "FilAr PLA Gris Pizarra" + ] + ], + "FILAR0025": [ + [ + "FilAr", + "PLA", + "FilAr PLA Gris Ceniza" + ] + ], + "FILAR0026": [ + [ + "FilAr", + "PLA", + "FilAr PLA Azul Francia" + ] + ], + "FILAR0027": [ + [ + "FilAr", + "PLA", + "FilAr PLA Celeste Cielo" + ] + ], + "FILAR0028": [ + [ + "FilAr", + "PLA", + "FilAr PLA Oro" + ] + ], + "FILAR0029": [ + [ + "FilAr", + "PLA", + "FilAr PLA Dorado" + ] + ], + "FILAR0030": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Amarillo" + ] + ], + "FILAR0031": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Azul" + ] + ], + "FILAR0032": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Beige" + ] + ], + "FILAR0033": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Blanco" + ] + ], + "FILAR0034": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Bordo" + ] + ], + "FILAR0035": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Celeste Cielo" + ] + ], + "FILAR0036": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Chocolate" + ] + ], + "FILAR0037": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Gris" + ] + ], + "FILAR0038": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Marron" + ] + ], + "FILAR0039": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Naranja" + ] + ], + "FILAR0040": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Negro" + ] + ], + "FILAR0041": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Piel" + ] + ], + "FILAR0042": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Rojo" + ] + ], + "FILAR0043": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Rosa" + ] + ], + "FILAR0044": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Uva" + ] + ], + "FILAR0045": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Verde" + ] + ], + "FILAR0046": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate Violeta" + ] + ], + "FILAR0047": [ + [ + "FilAr", + "PETG", + "FilAr PETG Amarillo Lima" + ] + ], + "FILAR0048": [ + [ + "FilAr", + "PETG", + "FilAr PETG Amarillo Radiante" + ] + ], + "FILAR0049": [ + [ + "FilAr", + "PETG", + "FilAr PETG Azul Boreal" + ] + ], + "FILAR0050": [ + [ + "FilAr", + "PETG", + "FilAr PETG Azul Francia" + ] + ], + "FILAR0051": [ + [ + "FilAr", + "PETG", + "FilAr PETG Azul Imperial" + ] + ], + "FILAR0052": [ + [ + "FilAr", + "PETG", + "FilAr PETG Blanco Antartida" + ] + ], + "FILAR0053": [ + [ + "FilAr", + "PETG", + "FilAr PETG Cian" + ] + ], + "FILAR0054": [ + [ + "FilAr", + "PETG", + "FilAr PETG Coral" + ] + ], + "FILAR0055": [ + [ + "FilAr", + "PETG", + "FilAr PETG Cristal" + ] + ], + "FILAR0056": [ + [ + "FilAr", + "PETG", + "FilAr PETG Gris Ceniza" + ] + ], + "FILAR0057": [ + [ + "FilAr", + "PETG", + "FilAr PETG Gris Plata" + ] + ], + "FILAR0058": [ + [ + "FilAr", + "PETG", + "FilAr PETG Magenta" + ] + ], + "FILAR0059": [ + [ + "FilAr", + "PETG", + "FilAr PETG Negro Azabache" + ] + ], + "FILAR0060": [ + [ + "FilAr", + "PETG", + "FilAr PETG Rojo Carmesi" + ] + ], + "FILARB01": [ + [ + "FilAr", + "PLA", + "FilAr PLA" + ] + ], + "FILARB02": [ + [ + "FilAr", + "PLA", + "FilAr PLA-mate" + ] + ], + "FILARB03": [ + [ + "FilAr", + "PETG", + "FilAr PETG" + ] + ], + "FILL3D001": [ + [ + "FILL3D", + "PLA", + "FILL3D PLA Turbo" + ] + ], + "FILL3D002": [ + [ + "FILL3D", + "PLA", + "FILL3D PLA Basic" + ] + ], + "FILL3D003": [ + [ + "FILL3D", + "PETG", + "FILL3D PETG" + ] + ], + "FILL3D004": [ + [ + "FILL3D", + "PP", + "FILL3D PP" + ] + ], + "FILL3D005": [ + [ + "FILL3D", + "PP", + "FILL3D PPCF" + ] + ], + "FILL3D006": [ + [ + "FILL3D", + "PA", + "FILL3D PA" + ] + ], + "FILL3D007": [ + [ + "FILL3D", + "PETG-CF", + "FILL3D PETG CF" + ] + ], + "GABSB00": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "GASAB00": [ + [ + "Generic", + "ASA", + "Generic ASA" + ] + ], + "GFA00": [ + [ + "Afinia", + "PLA", + "Afinia PLA" + ], + [ + "Afinia", + "PLA", + "Afinia Value PLA" + ], + [ + "Orca Arena", + "PLA", + "Arena PLA Basic" + ], + [ + "Tiertime", + "PLA", + "Tiertime PLA" + ], + [ + "WonderMaker", + "PLA", + "WonderMaker PLA Basic" + ] + ], + "GFA00_01": [ + [ + "Afinia", + "PLA", + "Afinia PLA" + ], + [ + "Tiertime", + "PLA", + "Tiertime PLA" + ] + ], + "GFA01": [ + [ + "Orca Arena", + "PLA", + "Arena PLA Matte" + ], + [ + "WonderMaker", + "PLA", + "WonderMaker PLA Matte" + ] + ], + "GFA02": [ + [ + "Orca Arena", + "PLA", + "Arena PLA Metal" + ], + [ + "WonderMaker", + "PLA", + "WonderMaker PLA Metal" + ] + ], + "GFA03": [ + [ + "Orca Arena", + "PLA", + "Arena PLA Impact" + ] + ], + "GFA05": [ + [ + "Generic", + "PLA", + "Qidi Generic PLA Silk" + ], + [ + "Orca Arena", + "PLA", + "Arena PLA Silk" + ], + [ + "WonderMaker", + "PLA", + "WonderMaker PLA Silk" + ] + ], + "GFA07": [ + [ + "Orca Arena", + "PLA", + "Arena PLA Marble" + ], + [ + "WonderMaker", + "PLA", + "WonderMaker PLA Marble" + ] + ], + "GFA08": [ + [ + "Orca Arena", + "PLA", + "Arena PLA Sparkle" + ] + ], + "GFA09": [ + [ + "Orca Arena", + "PLA", + "Arena PLA Tough" + ] + ], + "GFA16": [ + [ + "WonderMaker", + "PLA", + "WonderMaker PLA Wood" + ] + ], + "GFA50": [ + [ + "Orca Arena", + "PLA-CF", + "Arena PLA-CF" + ], + [ + "Tiertime", + "PLA-CF", + "Tiertime PLA-CF" + ] + ], + "GFA50_01": [ + [ + "Tiertime", + "PLA-CF", + "Tiertime PLA-CF" + ] + ], + "GFA99": [ + [ + "Anycubic", + "ASA", + "Anycubic ASA" + ], + [ + "CoLiDo", + "PLA", + "CoLiDo Generic PLA" + ], + [ + "CoLiDo", + "PLA", + "CoLiDo PLA+" + ], + [ + "Generic", + "ASA", + "Anycubic ASA" + ] + ], + "GFASA": [ + [ + "Anycubic", + "ASA", + "Anycubic ASA" + ], + [ + "Generic", + "ASA", + "Anycubic ASA" + ] + ], + "GFB00": [ + [ + "Afinia", + "ABS", + "Afinia ABS" + ], + [ + "Afinia", + "ABS", + "Afinia ABS+" + ], + [ + "Afinia", + "ABS", + "Afinia Value ABS" + ], + [ + "Orca Arena", + "ABS", + "Arena ABS" + ], + [ + "Tiertime", + "ABS", + "Tiertime ABS" + ], + [ + "WonderMaker", + "ABS", + "WonderMaker ABS" + ] + ], + "GFB00_01": [ + [ + "Afinia", + "ABS", + "Afinia ABS" + ], + [ + "Tiertime", + "ABS", + "Tiertime ABS" + ] + ], + "GFB01": [ + [ + "Tiertime", + "ASA", + "Tiertime ASA" + ], + [ + "WonderMaker", + "ASA", + "WonderMaker ASA" + ] + ], + "GFB01_01": [ + [ + "Tiertime", + "ASA", + "Tiertime ASA" + ] + ], + "GFB98": [ + [ + "Generic", + "ASA", + "Anker Generic ASA" + ], + [ + "Generic", + "ASA", + "Anycubic Generic ASA" + ], + [ + "Generic", + "ASA", + "Artillery Generic ASA" + ], + [ + "Generic", + "ASA", + "Creality Generic ASA" + ], + [ + "Generic", + "ASA", + "FLSun Generic ASA" + ], + [ + "Generic", + "ASA", + "FLSun S1 ASA" + ], + [ + "Generic", + "ASA", + "FLSun T1 ASA" + ], + [ + "Generic", + "ASA", + "OrcaArena Generic ASA" + ], + [ + "Generic", + "ASA", + "RatRig Generic ASA" + ], + [ + "Generic", + "ASA", + "SecKit Generic ASA" + ], + [ + "Generic", + "ASA", + "Tiertime Generic ASA" + ], + [ + "Generic", + "ASA", + "Vzbot Generic ASA" + ], + [ + "Generic", + "ASA", + "fdm_filament_asa" + ] + ], + "GFB98_01": [ + [ + "Generic", + "ASA", + "Tiertime Generic ASA" + ] + ], + "GFB98_2": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "GFB98_3": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "GFB98_4": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "GFB98_5": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ], + [ + "Generic", + "ASA", + "Prusa Generic ASA HF" + ] + ], + "GFB98_6": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA HF" + ] + ], + "GFB98_7": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA HF" + ] + ], + "GFB99": [ + [ + "CoLiDo", + "ABS", + "CoLiDo ABS" + ], + [ + "CoLiDo", + "ABS", + "CoLiDo Generic ABS" + ], + [ + "FlyingBear", + "ABS", + "FlyingBear Generic ABS" + ], + [ + "Generic", + "ABS", + "Anker Generic ABS" + ], + [ + "Generic", + "ABS", + "Anycubic Generic ABS" + ], + [ + "Generic", + "ABS", + "Artillery Generic ABS" + ], + [ + "Generic", + "ABS", + "Chuanying ABS" + ], + [ + "Generic", + "ABS", + "Chuanying Generic ABS" + ], + [ + "Generic", + "ABS", + "Comgrow Generic ABS" + ], + [ + "Generic", + "ABS", + "Creality Generic ABS" + ], + [ + "Generic", + "ABS", + "FLSun Generic ABS" + ], + [ + "Generic", + "ABS", + "FLSun S1 ABS" + ], + [ + "Generic", + "ABS", + "FLSun T1 ABS" + ], + [ + "Generic", + "ABS", + "Flashforge ABS" + ], + [ + "Generic", + "ABS", + "Flashforge Generic ABS" + ], + [ + "Generic", + "ABS", + "Generic ABS" + ], + [ + "Generic", + "ABS", + "Lulzbot 2.85mm ABS" + ], + [ + "Generic", + "ABS", + "OrcaArena Generic ABS" + ], + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ], + [ + "Generic", + "ABS", + "Qidi Generic ABS" + ], + [ + "Generic", + "ABS", + "RatRig Generic ABS" + ], + [ + "Generic", + "ABS", + "SecKit Generic ABS" + ], + [ + "Generic", + "ABS", + "Tiertime Generic ABS" + ], + [ + "Generic", + "ABS", + "Vzbot Generic ABS" + ], + [ + "Generic", + "TPU", + "DeltaMaker Generic TPU" + ], + [ + "InfiMech", + "ABS", + "InfiMech Generic ABS" + ], + [ + "Volumic", + "ABS", + "Volumic ABS Ultra" + ], + [ + "Volumic", + "ABS", + "Volumic ABS Ultra (Performance)" + ] + ], + "GFB99_01": [ + [ + "Generic", + "ABS", + "Tiertime Generic ABS" + ] + ], + "GFB99_2": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "GFB99_3": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "GFB99_4": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "GFB99_5": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ], + [ + "Generic", + "ABS", + "Prusa Generic ABS HF" + ] + ], + "GFB99_6": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS HF" + ] + ], + "GFB99_7": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS HF" + ] + ], + "GFC00": [ + [ + "Orca Arena", + "PC", + "Arena PC" + ], + [ + "Tiertime", + "PC", + "Tiertime PC" + ] + ], + "GFC00_01": [ + [ + "Tiertime", + "PC", + "Tiertime PC" + ] + ], + "GFC98": [ + [ + "Generic", + "PC-CF", + "fdm_filament_pccf" + ] + ], + "GFC99": [ + [ + "FlyingBear", + "PC", + "FlyingBear Generic PC" + ], + [ + "Generic", + "PC", + "Anker Generic PC" + ], + [ + "Generic", + "PC", + "Anycubic Generic PC" + ], + [ + "Generic", + "PC", + "Creality Generic PC" + ], + [ + "Generic", + "PC", + "FLSun Generic PC" + ], + [ + "Generic", + "PC", + "Generic PC" + ], + [ + "Generic", + "PC", + "OrcaArena Generic PC" + ], + [ + "Generic", + "PC", + "Qidi Generic PC" + ], + [ + "Generic", + "PC", + "RatRig Generic PC" + ], + [ + "Generic", + "PC", + "SecKit Generic PC" + ], + [ + "Generic", + "PC", + "Tiertime Generic PC" + ], + [ + "Generic", + "PC", + "Vzbot Generic PC" + ], + [ + "Generic", + "PC", + "fdm_filament_pc" + ], + [ + "InfiMech", + "PC", + "InfiMech Generic PC" + ], + [ + "Volumic", + "PC", + "Volumic PC" + ], + [ + "Volumic", + "PC", + "Volumic PC (Performance)" + ] + ], + "GFC99_01": [ + [ + "Generic", + "PC", + "Tiertime Generic PC" + ] + ], + "GFC99_1": [ + [ + "Generic", + "PC", + "Prusa Generic PC" + ] + ], + "GFC99_2": [ + [ + "Generic", + "PC", + "Prusa Generic PC" + ] + ], + "GFC99_3": [ + [ + "Generic", + "PC", + "Prusa Generic PC" + ] + ], + "GFC99_4": [ + [ + "Generic", + "PC", + "Prusa Generic PC" + ], + [ + "Generic", + "PC", + "Prusa Generic PC HF" + ] + ], + "GFC99_5": [ + [ + "Generic", + "PC", + "Prusa Generic PC HF" + ] + ], + "GFC99_6": [ + [ + "Generic", + "PC", + "Prusa Generic PC HF" + ] + ], + "GFG00": [ + [ + "Orca Arena", + "PETG", + "Arena PETG Basic" + ], + [ + "Tiertime", + "PETG", + "Tiertime PETG" + ], + [ + "WonderMaker", + "PETG", + "WonderMaker PETG Basic" + ] + ], + "GFG00_01": [ + [ + "Tiertime", + "PETG", + "Tiertime PETG" + ] + ], + "GFG01": [ + [ + "Generic", + "PETG", + "re3D Greengate rPETG" + ], + [ + "re3D", + "PETG", + "re3D Greengate rPETG" + ] + ], + "GFG02": [ + [ + "re3D", + "PP", + "re3D rPP" + ] + ], + "GFG03": [ + [ + "re3D", + "PETG", + "re3D PETG" + ] + ], + "GFG50": [ + [ + "Orca Arena", + "PETG-CF", + "Arena PETG-CF" + ] + ], + "GFG60": [ + [ + "Polymaker", + "PETG", + "PolyLite PETG" + ] + ], + "GFG97": [ + [ + "Generic", + "PCTG", + "Tiertime Generic PCTG" + ] + ], + "GFG97-01": [ + [ + "Generic", + "PCTG", + "Tiertime Generic PCTG" + ] + ], + "GFG98": [ + [ + "Generic", + "PETG-CF", + "Anker Generic PETG-CF" + ], + [ + "Generic", + "PETG-CF", + "Qidi Generic PETG-CF" + ], + [ + "Generic", + "PETG-CF", + "Tiertime Generic PETG-CF" + ], + [ + "Orca Arena", + "PETG-CF", + "OrcaArena Generic PETG-CF" + ], + [ + "Volumic", + "PETG", + "Volumic PETG Ultra carbone" + ], + [ + "Volumic", + "PETG", + "Volumic PETG Ultra carbone (Performance)" + ] + ], + "GFG98_01": [ + [ + "Generic", + "PETG-CF", + "Tiertime Generic PETG-CF" + ] + ], + "GFG99": [ + [ + "CoLiDo", + "PETG", + "CoLiDo Generic PETG" + ], + [ + "CoLiDo", + "PETG", + "CoLiDo PETG" + ], + [ + "FlyingBear", + "PETG", + "FlyingBear Generic PETG" + ], + [ + "FlyingBear", + "PETG Basic", + "FlyingBear PETG Basic" + ], + [ + "Generic", + "PEEK", + "MM Generic PEEK" + ], + [ + "Generic", + "PETG", + "Anker Generic PETG" + ], + [ + "Generic", + "PETG", + "Anycubic Generic PETG" + ], + [ + "Generic", + "PETG", + "Artillery Generic PETG" + ], + [ + "Generic", + "PETG", + "C1 Generic PETG" + ], + [ + "Generic", + "PETG", + "Chuanying Generic PETG" + ], + [ + "Generic", + "PETG", + "Chuanying PETG" + ], + [ + "Generic", + "PETG", + "Comgrow Generic PETG" + ], + [ + "Generic", + "PETG", + "Creality Generic PETG" + ], + [ + "Generic", + "PETG", + "DeltaMaker Generic PETG" + ], + [ + "Generic", + "PETG", + "FLSun Generic PETG" + ], + [ + "Generic", + "PETG", + "FLSun S1 PETG" + ], + [ + "Generic", + "PETG", + "FLSun T1 PETG" + ], + [ + "Generic", + "PETG", + "Flashforge Generic PETG" + ], + [ + "Generic", + "PETG", + "Flashforge PETG" + ], + [ + "Generic", + "PETG", + "Generic PETG" + ], + [ + "Generic", + "PETG", + "Lulzbot 2.85mm PETG" + ], + [ + "Generic", + "PETG", + "OrcaArena Generic PETG" + ], + [ + "Generic", + "PETG", + "Qidi Generic PETG" + ], + [ + "Generic", + "PETG", + "RatRig Generic PETG" + ], + [ + "Generic", + "PETG", + "SecKit Generic PETG" + ], + [ + "Generic", + "PETG", + "Tiertime Generic PETG" + ], + [ + "Generic", + "PETG", + "Vzbot Generic PETG" + ], + [ + "Generic", + "PETG", + "Yumi Generic PETG" + ], + [ + "InfiMech", + "PETG", + "InfiMech Generic PETG" + ], + [ + "Volumic", + "PETG", + "Volumic PETG Ultra" + ], + [ + "Volumic", + "PETG", + "Volumic PETG Ultra (Performance)" + ], + [ + "Yumi", + "PETG", + "YUMI PETG" + ], + [ + "re3D", + "PC", + "re3D PC" + ] + ], + "GFG99_01": [ + [ + "Generic", + "PETG", + "Tiertime Generic PETG" + ] + ], + "GFG99_2": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "GFG99_3": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "GFG99_4": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "GFG99_5": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ], + [ + "Generic", + "PETG", + "Prusa Generic PETG HF" + ] + ], + "GFG99_6": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG HF" + ] + ], + "GFG99_7": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG HF" + ] + ], + "GFL00": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA" + ] + ], + "GFL01": [ + [ + "Polymaker", + "PLA", + "PolyTerra PLA" + ] + ], + "GFL06": [ + [ + "Polymaker", + "PETG", + "Fiberon PETG-ESD" + ] + ], + "GFL100": [ + [ + "Generic", + "PLA", + "Generic PLA" + ], + [ + "Generic", + "PLA", + "TwoTrees Generic HS PLA" + ] + ], + "GFL106": [ + [ + "Generic", + "PLA", + "Generic PLA" + ] + ], + "GFL108": [ + [ + "Generic", + "PLA", + "Generic PLA" + ] + ], + "GFL200": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "GFL204": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "GFL206": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "GFL208": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "GFL300": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "GFL304": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "GFL306": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "GFL308": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "GFL400": [ + [ + "Generic", + "PA", + "Generic PA" + ] + ], + "GFL404": [ + [ + "Generic", + "PA", + "Generic PA" + ] + ], + "GFL406": [ + [ + "Generic", + "PA", + "Generic PA" + ] + ], + "GFL408": [ + [ + "Generic", + "PA", + "Generic PA" + ] + ], + "GFL500": [ + [ + "Generic", + "ABS", + "Generic ABS HT" + ] + ], + "GFL504": [ + [ + "Generic", + "ABS", + "Generic ABS HT" + ] + ], + "GFL506": [ + [ + "Generic", + "ABS", + "Generic ABS HT" + ] + ], + "GFL508": [ + [ + "Generic", + "ABS", + "Generic ABS HT" + ] + ], + "GFL56": [ + [ + "Polymaker", + "PA-CF", + "Fiberon PA12-CF10" + ] + ], + "GFL57": [ + [ + "Polymaker", + "PA6-CF", + "Fiberon PA6-CF20" + ] + ], + "GFL62": [ + [ + "Polymaker", + "PLA", + "PolyLite CosPLA" + ] + ], + "GFL63": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Galaxy" + ] + ], + "GFL64": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Glow" + ] + ], + "GFL65": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Luminous" + ] + ], + "GFL66": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Neon" + ] + ], + "GFL67": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Starlight" + ] + ], + "GFL68": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Translucent" + ] + ], + "GFL69": [ + [ + "Polymaker", + "PLA", + "PolyTerra PLA Marble" + ] + ], + "GFL70": [ + [ + "Polymaker", + "PLA", + "PolyTerra PLA+" + ] + ], + "GFL71": [ + [ + "Polymaker", + "ASA", + "Fiberon ASA-CF08" + ] + ], + "GFL72": [ + [ + "Polymaker", + "PA-CF", + "Fiberon PA612-ESD" + ] + ], + "GFL73": [ + [ + "Polymaker", + "ABS", + "Fiberon PPS-GF20" + ] + ], + "GFL74": [ + [ + "Polymaker", + "PETG", + "PolyLite PETG Translucent" + ] + ], + "GFL75": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Pro Metallic" + ] + ], + "GFL76": [ + [ + "Polymaker", + "PETG", + "Polymaker PETG" + ] + ], + "GFL77": [ + [ + "Polymaker", + "PETG", + "Polymaker PETG Galaxy" + ] + ], + "GFL78": [ + [ + "Polymaker", + "PLA", + "Polymaker PLA" + ] + ], + "GFL79": [ + [ + "Polymaker", + "PLA", + "Polymaker PLA Pro" + ] + ], + "GFL80": [ + [ + "Polymaker", + "PLA", + "Polymaker PLA Pro Metallic" + ] + ], + "GFL94": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA Slik" + ] + ], + "GFL95": [ + [ + "Generic", + "PLA", + "Anker Generic PLA+" + ], + [ + "Generic", + "PLA", + "Tiertime Generic PLA High Speed" + ] + ], + "GFL95_01": [ + [ + "Generic", + "PLA", + "Tiertime Generic PLA High Speed" + ] + ], + "GFL96": [ + [ + "Generic", + "PLA", + "Anker Generic PLA Silk" + ], + [ + "Generic", + "PLA", + "Tiertime Generic PLA Silk" + ] + ], + "GFL96_01": [ + [ + "Generic", + "PLA", + "Tiertime Generic PLA Silk" + ] + ], + "GFL97": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA SE" + ] + ], + "GFL98": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA Glow" + ], + [ + "Generic", + "PLA-CF", + "Anker Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "Anycubic Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "Artillery Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "Creality Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "FLSun Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "OrcaArena Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "Qidi Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "RatRig Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "SecKit Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "Tiertime Generic PLA-CF" + ], + [ + "Generic", + "PLA-CF", + "Vzbot Generic PLA-CF" + ] + ], + "GFL98_01": [ + [ + "Generic", + "PLA-CF", + "Tiertime Generic PLA-CF" + ] + ], + "GFL98_1": [ + [ + "Generic", + "PLA-CF", + "Prusa Generic PLA-CF" + ] + ], + "GFL98_3": [ + [ + "Generic", + "PLA-CF", + "Prusa Generic PLA-CF" + ] + ], + "GFL98_4": [ + [ + "Generic", + "PLA-CF", + "Prusa Generic PLA-CF" + ] + ], + "GFL98_5": [ + [ + "Generic", + "PLA-CF", + "Prusa Generic PLA-CF" + ] + ], + "GFL99": [ + [ + "Co Print", + "PLA", + "CoPrint Generic PLA" + ], + [ + "FlyingBear", + "PLA", + "FlyingBear Generic PLA" + ], + [ + "Generic", + "ASA", + "Flashforge ASA" + ], + [ + "Generic", + "PLA", + "Anker Generic PLA" + ], + [ + "Generic", + "PLA", + "Anycubic Generic PLA" + ], + [ + "Generic", + "PLA", + "Artillery Generic PLA" + ], + [ + "Generic", + "PLA", + "C1 Generic PLA" + ], + [ + "Generic", + "PLA", + "Chuanying Generic PLA" + ], + [ + "Generic", + "PLA", + "Comgrow Generic PLA" + ], + [ + "Generic", + "PLA", + "Comgrow T300 PLA" + ], + [ + "Generic", + "PLA", + "Creality Generic PLA" + ], + [ + "Generic", + "PLA", + "DeltaMaker Generic PLA" + ], + [ + "Generic", + "PLA", + "Dremel Generic PLA" + ], + [ + "Generic", + "PLA", + "FLSun Generic PLA" + ], + [ + "Generic", + "PLA", + "FLSun S1 PLA Generic" + ], + [ + "Generic", + "PLA", + "FLSun T1 PLA Generic" + ], + [ + "Generic", + "PLA", + "Flashforge PLA-SILK" + ], + [ + "Generic", + "PLA", + "Generic PLA" + ], + [ + "Generic", + "PLA", + "Generic PLA High Speed" + ], + [ + "Generic", + "PLA", + "Lulzbot 2.85mm PLA" + ], + [ + "Generic", + "PLA", + "OrcaArena Generic PLA" + ], + [ + "Generic", + "PLA", + "Peopoly Generic PLA" + ], + [ + "Generic", + "PLA", + "Phrozen PLA" + ], + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ], + [ + "Generic", + "PLA", + "Qidi Generic PLA" + ], + [ + "Generic", + "PLA", + "RatRig Generic PLA" + ], + [ + "Generic", + "PLA", + "SecKit Generic PLA" + ], + [ + "Generic", + "PLA", + "Sovol SV07 PLA" + ], + [ + "Generic", + "PLA", + "Tiertime Generic PLA" + ], + [ + "Generic", + "PLA", + "Vzbot Generic PLA" + ], + [ + "Generic", + "PLA", + "Yumi Generic PLA" + ], + [ + "InfiMech", + "PLA", + "InfiMech Generic PLA" + ], + [ + "Volumic", + "PLA", + "Volumic PLA Ultra" + ], + [ + "Volumic", + "PLA", + "Volumic PLA Ultra (Performance)" + ], + [ + "re3D", + "PLA", + "re3D PLA" + ] + ], + "GFL99_01": [ + [ + "Generic", + "PLA", + "Tiertime Generic PLA" + ] + ], + "GFL99_2": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "GFL99_3": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "GFL99_4": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "GFL99_5": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ], + [ + "Generic", + "PLA", + "Prusa Generic PLA HF" + ] + ], + "GFL99_6": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA HF" + ] + ], + "GFL99_7": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA HF" + ] + ], + "GFLLK10PETG": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "GFLLK10PLA": [ + [ + "Generic", + "PLA", + "Generic PLA" + ] + ], + "GFLLK10PPETG": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "GFLLK10PPLA": [ + [ + "Generic", + "PLA", + "Generic PLA" + ] + ], + "GFN03": [ + [ + "Orca Arena", + "PA-CF", + "Arena PA-CF" + ] + ], + "GFN04": [ + [ + "Orca Arena", + "PA-CF", + "Arena PAHT-CF" + ] + ], + "GFN05": [ + [ + "Tiertime", + "PA6-CF", + "Tiertime PA6-CF" + ] + ], + "GFN05_01": [ + [ + "Tiertime", + "PA6-CF", + "Tiertime PA6-CF" + ] + ], + "GFN96": [ + [ + "Generic", + "PPA-GF", + "Tiertime Generic PPA-GF" + ] + ], + "GFN96_01": [ + [ + "Generic", + "PPA-GF", + "Tiertime Generic PPA-GF" + ] + ], + "GFN97": [ + [ + "Generic", + "PPA-CF", + "Tiertime Generic PPA-CF" + ] + ], + "GFN97_01": [ + [ + "Generic", + "PPA-CF", + "Tiertime Generic PPA-CF" + ] + ], + "GFN98": [ + [ + "FlyingBear", + "PA-CF", + "FlyingBear Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "Anker Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "Anycubic Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "Creality Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "FLSun Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "OrcaArena Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "Qidi Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "RatRig Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "SecKit Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "Tiertime Generic PA-CF" + ], + [ + "Generic", + "PA-CF", + "Vzbot Generic PA-CF" + ], + [ + "Generic", + "PA11-CF", + "fdm_filament_pa11cf" + ], + [ + "InfiMech", + "PA-CF", + "InfiMech Generic PA-CF" + ] + ], + "GFN98_02": [ + [ + "Generic", + "PA-CF", + "Tiertime Generic PA-CF" + ] + ], + "GFN98_1": [ + [ + "Generic", + "PA-CF", + "Prusa Generic PA-CF" + ] + ], + "GFN98_2": [ + [ + "Generic", + "PA-CF", + "Prusa Generic PA-CF" + ] + ], + "GFN98_3": [ + [ + "Generic", + "PA-CF", + "Prusa Generic PA-CF" + ] + ], + "GFN98_4": [ + [ + "Generic", + "PA-CF", + "Prusa Generic PA-CF" + ] + ], + "GFN99": [ + [ + "Generic", + "PA", + "Anker Generic PA" + ], + [ + "Generic", + "PA", + "Anycubic Generic PA" + ], + [ + "Generic", + "PA", + "FLSun Generic PA" + ], + [ + "Generic", + "PA", + "OrcaArena Generic PA" + ], + [ + "Generic", + "PA", + "Prusa Generic PA" + ], + [ + "Generic", + "PA", + "Qidi Generic PA" + ], + [ + "Generic", + "PA", + "RatRig Generic PA" + ], + [ + "Generic", + "PA", + "SecKit Generic PA" + ], + [ + "Generic", + "PA", + "Tiertime Generic PA" + ], + [ + "Generic", + "PA", + "Vzbot Generic PA" + ], + [ + "Volumic", + "PA", + "Volumic NYLON Ultra" + ], + [ + "Volumic", + "PA", + "Volumic NYLON Ultra (Performance)" + ] + ], + "GFN99_01": [ + [ + "Generic", + "PA", + "Tiertime Generic PA" + ] + ], + "GFN99_1": [ + [ + "Generic", + "PA", + "Prusa Generic PA" + ] + ], + "GFN99_2": [ + [ + "Generic", + "PA", + "Prusa Generic PA" + ] + ], + "GFN99_3": [ + [ + "Generic", + "PA", + "Prusa Generic PA" + ] + ], + "GFN99_4": [ + [ + "Generic", + "PA", + "Prusa Generic PA" + ] + ], + "GFOT001": [ + [ + "Overture", + "PLA", + "Overture PLA Pro" + ] + ], + "GFOT002": [ + [ + "Overture", + "PLA", + "Overture Silk PLA" + ] + ], + "GFOT003": [ + [ + "Overture", + "PLA", + "Overture Easy PLA" + ] + ], + "GFOT004": [ + [ + "Overture", + "PLA", + "Overture Rock PLA" + ] + ], + "GFOT005": [ + [ + "Overture", + "PLA", + "Overture Super PLA+" + ] + ], + "GFOT006": [ + [ + "Overture", + "PLA", + "Overture Air PLA" + ] + ], + "GFOT008": [ + [ + "Overture", + "TPU", + "Overture TPU" + ] + ], + "GFOT009": [ + [ + "Overture", + "ASA", + "Overture ASA" + ] + ], + "GFP95": [ + [ + "Generic", + "PP-GF", + "Tiertime Generic PP-GF" + ] + ], + "GFP95_01": [ + [ + "Generic", + "PP-GF", + "Tiertime Generic PP-GF" + ] + ], + "GFP96": [ + [ + "Generic", + "PP-CF", + "Tiertime Generic PP-CF" + ] + ], + "GFP96_01": [ + [ + "Generic", + "PP-CF", + "Tiertime Generic PP-CF" + ] + ], + "GFP97": [ + [ + "Generic", + "PP", + "Tiertime Generic PP" + ] + ], + "GFP97_01": [ + [ + "Generic", + "PP", + "Tiertime Generic PP" + ] + ], + "GFP98": [ + [ + "Generic", + "PE-CF", + "Tiertime Generic PE-CF" + ] + ], + "GFP98_01": [ + [ + "Generic", + "PE-CF", + "Tiertime Generic PE-CF" + ] + ], + "GFP99": [ + [ + "Generic", + "PE", + "Tiertime Generic PE" + ] + ], + "GFP99_01": [ + [ + "Generic", + "PE", + "Tiertime Generic PE" + ] + ], + "GFPA": [ + [ + "Anycubic", + "PA", + "Anycubic PA" + ] + ], + "GFPA6-CF": [ + [ + "Anycubic", + "PA6-CF", + "Anycubic PA6-CF" + ] + ], + "GFPAHT-CF": [ + [ + "Anycubic", + "PAHT-CF", + "Anycubic PAHT-CF" + ] + ], + "GFPC": [ + [ + "Anycubic", + "PC", + "Anycubic PC" + ] + ], + "GFPC-CF": [ + [ + "Anycubic", + "PC-CF", + "Anycubic PC-CF" + ] + ], + "GFPC-GF": [ + [ + "Anycubic", + "PC-GF", + "Anycubic PC-GF" + ] + ], + "GFPEBA": [ + [ + "Anycubic", + "PEBA", + "Anycubic PEBA" + ] + ], + "GFPET-CF": [ + [ + "Anycubic", + "PET-CF", + "Anycubic PET-CF" + ] + ], + "GFPETG": [ + [ + "Anycubic", + "PETG", + "Generic PETG" + ], + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "GFPETG-1": [ + [ + "Generic", + "PETG", + "Peopoly Generic PETG" + ] + ], + "GFPETG-CF": [ + [ + "Anycubic", + "PETG-CF", + "Anycubic PETG-CF" + ] + ], + "GFPLA Glow": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA Glow" + ] + ], + "GFPLA Translucent": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA Translucent" + ] + ], + "GFPLA-CF": [ + [ + "Anycubic", + "PLA-CF", + "Anycubic PLA-CF" + ] + ], + "GFPM001": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA" + ] + ], + "GFPM002": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Matte" + ] + ], + "GFPM003": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Marble" + ] + ], + "GFPM004": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Silk" + ] + ], + "GFPM005": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Satin" + ] + ], + "GFPM006": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Translucent" + ] + ], + "GFPM007": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Galaxy" + ] + ], + "GFPM008": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Celestial" + ] + ], + "GFPM009": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Starlight" + ] + ], + "GFPM010": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Glow" + ] + ], + "GFPM011": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Luminous" + ] + ], + "GFPM012": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Metallic" + ] + ], + "GFPM013": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Neon" + ] + ], + "GFPM014": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA UV Shift" + ] + ], + "GFPM015": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Temp Shift" + ] + ], + "GFPM016": [ + [ + "Polymaker", + "PLA", + "Panchroma CoPE" + ] + ], + "GFPM017": [ + [ + "Polymaker", + "PLA", + "Polymaker HT-PLA" + ] + ], + "GFPM018": [ + [ + "Polymaker", + "PLA", + "Polymaker HT-PLA-GF" + ] + ], + "GFPM019": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Pro" + ] + ], + "GFPVA": [ + [ + "Anycubic", + "PVA", + "Anycubic PVA" + ] + ], + "GFR98": [ + [ + "Generic", + "PHA", + "Tiertime Generic PHA" + ] + ], + "GFR98_01": [ + [ + "Generic", + "PHA", + "Tiertime Generic PHA" + ] + ], + "GFR99": [ + [ + "Generic", + "EVA", + "Tiertime Generic EVA" + ] + ], + "GFR99_01": [ + [ + "Generic", + "EVA", + "Tiertime Generic EVA" + ] + ], + "GFRH01": [ + [ + "Generic", + "ABS", + "fdm_filament_abs" + ] + ], + "GFRH02": [ + [ + "Generic", + "ASA", + "fdm_filament_asa" + ] + ], + "GFRH03": [ + [ + "Generic", + "PETG", + "fdm_filament_petg" + ] + ], + "GFRH04": [ + [ + "Generic", + "PLA", + "fdm_filament_pla" + ] + ], + "GFRH05": [ + [ + "Generic", + "TPU", + "fdm_filament_tpu" + ] + ], + "GFRH06": [ + [ + "Generic", + "PC-CF", + "fdm_filament_pccf" + ] + ], + "GFS00": [ + [ + "Orca Arena", + "PLA", + "Arena Support W" + ] + ], + "GFS01": [ + [ + "Orca Arena", + "PA", + "Arena Support G" + ] + ], + "GFS04": [ + [ + "Tiertime", + "PVA", + "Tiertime PVA" + ], + [ + "WonderMaker", + "PVA", + "WonderMaker PVA" + ] + ], + "GFS04_01": [ + [ + "Tiertime", + "PVA", + "Tiertime PVA" + ] + ], + "GFS97": [ + [ + "Generic", + "BVOH", + "Tiertime Generic BVOH" + ] + ], + "GFS97_01": [ + [ + "Generic", + "BVOH", + "Tiertime Generic BVOH" + ] + ], + "GFS98": [ + [ + "Generic", + "HIPS", + "Tiertime Generic HIPS" + ], + [ + "Generic", + "PVB", + "fdm_filament_pvb" + ] + ], + "GFS98_01": [ + [ + "Generic", + "HIPS", + "Tiertime Generic HIPS" + ] + ], + "GFS99": [ + [ + "Generic", + "PVA", + "Anker Generic PVA" + ], + [ + "Generic", + "PVA", + "Anycubic Generic PVA" + ], + [ + "Generic", + "PVA", + "FLSun Generic PVA" + ], + [ + "Generic", + "PVA", + "Generic PVA" + ], + [ + "Generic", + "PVA", + "OrcaArena Generic PVA" + ], + [ + "Generic", + "PVA", + "Prusa Generic PVA" + ], + [ + "Generic", + "PVA", + "Qidi Generic PVA" + ], + [ + "Generic", + "PVA", + "RatRig Generic PVA" + ], + [ + "Generic", + "PVA", + "SecKit Generic PVA" + ], + [ + "Generic", + "PVA", + "Tiertime Generic PVA" + ], + [ + "Generic", + "PVA", + "Vzbot Generic PVA" + ], + [ + "Volumic", + "PVA", + "Volumic PVA" + ], + [ + "Volumic", + "PVA", + "Volumic PVA-BVOH (Performance)" + ] + ], + "GFS99_01": [ + [ + "Generic", + "PVA", + "Tiertime Generic PVA" + ] + ], + "GFS99_1": [ + [ + "Generic", + "PVA", + "Prusa Generic PVA" + ] + ], + "GFS99_2": [ + [ + "Generic", + "PVA", + "Prusa Generic PVA" + ] + ], + "GFS99_3": [ + [ + "Generic", + "PVA", + "Prusa Generic PVA" + ] + ], + "GFS99_4": [ + [ + "Generic", + "PVA", + "Prusa Generic PVA" + ], + [ + "Generic", + "PVA", + "Prusa Generic PVA HF" + ] + ], + "GFS99_5": [ + [ + "Generic", + "PVA", + "Prusa Generic PVA HF" + ] + ], + "GFS99_6": [ + [ + "Generic", + "PVA", + "Prusa Generic PVA HF" + ] + ], + "GFSEP001": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA Premium" + ] + ], + "GFSEP002": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA Matte" + ] + ], + "GFSEP003": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA Silk" + ] + ], + "GFSEP004": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA Neon" + ] + ], + "GFSEP005": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA High Speed" + ] + ], + "GFSEP006": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA INGEO 850" + ] + ], + "GFSEP007": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA INGEO 870" + ] + ], + "GFSEP008": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA Antibacterial" + ] + ], + "GFSEP009": [ + [ + "Eolas Prints", + "PLA", + "Eolas Prints PLA Transition" + ] + ], + "GFSEP010": [ + [ + "Eolas Prints", + "PETG", + "Eolas Prints PETG" + ] + ], + "GFSEP011": [ + [ + "Eolas Prints", + "PETG", + "Eolas Prints PETG UV Resistant" + ] + ], + "GFSEP012": [ + [ + "Eolas Prints", + "PETG", + "Eolas Prints PETG Transition" + ] + ], + "GFSEP013": [ + [ + "Eolas Prints", + "TPU", + "Eolas Prints TPU Flex 93A" + ] + ], + "GFSEP014": [ + [ + "Eolas Prints", + "TPU", + "Eolas Prints TPU Flex D53" + ] + ], + "GFSEP015": [ + [ + "Eolas Prints", + "TPU", + "Eolas Prints TPU D60 UV Resistant" + ] + ], + "GFSEP016": [ + [ + "Eolas Prints", + "TPU", + "Eolas Prints TPU Transition" + ] + ], + "GFSEP017": [ + [ + "Eolas Prints", + "ABS", + "Eolas Prints ABS" + ] + ], + "GFSEP018": [ + [ + "Eolas Prints", + "ASA", + "Eolas Prints ASA" + ] + ], + "GFSL99": [ + [ + "Generic", + "ABS", + "Peopoly Generic ABS" + ] + ], + "GFSNL02": [ + [ + "SUNLU", + "PLA", + "SUNLU PLA Matte" + ] + ], + "GFSNL03": [ + [ + "SUNLU", + "PLA", + "SUNLU PLA+" + ] + ], + "GFSNL04": [ + [ + "SUNLU", + "PLA", + "SUNLU PLA+ 2.0" + ] + ], + "GFSNL05": [ + [ + "SUNLU", + "PLA", + "SUNLU Silk PLA+" + ] + ], + "GFSNL06": [ + [ + "SUNLU", + "PLA", + "SUNLU PLA Marble" + ] + ], + "GFSNL07": [ + [ + "SUNLU", + "PLA", + "SUNLU Wood PLA" + ] + ], + "GFSNL08": [ + [ + "SUNLU", + "PETG", + "SUNLU PETG" + ] + ], + "GFT01": [ + [ + "Orca Arena", + "PET-CF", + "Arena PET-CF" + ], + [ + "Tiertime", + "PET-CF", + "Tiertime PET-CF" + ], + [ + "WonderMaker", + "PET-CF", + "WonderMaker PET-CF" + ] + ], + "GFT01_01": [ + [ + "Tiertime", + "PET-CF", + "Tiertime PET-CF" + ] + ], + "GFT02": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "GFT97": [ + [ + "Generic", + "PPS", + "Tiertime Generic PPS" + ] + ], + "GFT97_01": [ + [ + "Generic", + "PPS", + "Tiertime Generic PPS" + ] + ], + "GFT98": [ + [ + "Generic", + "PPS-CF", + "Tiertime Generic PPS-CF" + ] + ], + "GFT98_01": [ + [ + "Generic", + "PPS-CF", + "Tiertime Generic PPS-CF" + ] + ], + "GFTPU 95A": [ + [ + "Anycubic", + "TPU", + "Anycubic TPU 95A" + ] + ], + "GFTPU for ACE": [ + [ + "Anycubic", + "TPU", + "Anycubic TPU for ACE" + ] + ], + "GFU01": [ + [ + "Afinia", + "TPU", + "Afinia TPU" + ], + [ + "Orca Arena", + "TPU", + "Arena TPU 95A" + ], + [ + "Tiertime", + "TPU", + "Tiertime TPU 95A" + ], + [ + "WonderMaker", + "TPU", + "WonderMaker TPU 95A" + ] + ], + "GFU01_01": [ + [ + "Afinia", + "TPU", + "Afinia TPU" + ], + [ + "Tiertime", + "TPU", + "Tiertime TPU 95A" + ] + ], + "GFU99": [ + [ + "CoLiDo", + "TPU", + "CoLiDo Generic TPU" + ], + [ + "FlyingBear", + "TPU", + "FlyingBear Generic TPU" + ], + [ + "FlyingBear", + "TPU", + "FlyingBear TPU Basic" + ], + [ + "Generic", + "FLEX", + "fdm_filament_flex" + ], + [ + "Generic", + "TPU", + "Anker Generic TPU" + ], + [ + "Generic", + "TPU", + "Anycubic Generic TPU" + ], + [ + "Generic", + "TPU", + "Artillery Generic TPU" + ], + [ + "Generic", + "TPU", + "Creality Generic TPU" + ], + [ + "Generic", + "TPU", + "FLSun Generic TPU" + ], + [ + "Generic", + "TPU", + "FLSun S1 TPU" + ], + [ + "Generic", + "TPU", + "FLSun T1 TPU" + ], + [ + "Generic", + "TPU", + "Generic TPU" + ], + [ + "Generic", + "TPU", + "OrcaArena Generic TPU" + ], + [ + "Generic", + "TPU", + "Prusa Generic TPU" + ], + [ + "Generic", + "TPU", + "Qidi Generic TPU" + ], + [ + "Generic", + "TPU", + "RatRig Generic TPU" + ], + [ + "Generic", + "TPU", + "SecKit Generic TPU" + ], + [ + "Generic", + "TPU", + "Tiertime Generic TPU" + ], + [ + "Generic", + "TPU", + "TwoTrees Generic 95A TPU" + ], + [ + "Generic", + "TPU", + "Vzbot Generic TPU" + ], + [ + "InfiMech", + "TPU", + "InfiMech Generic TPU" + ], + [ + "InfiMech", + "TPU", + "InfiMech TPU Basic" + ], + [ + "Volumic", + "TPU", + "Volumic FLEX93 Ultra" + ], + [ + "Volumic", + "TPU", + "Volumic FLEX93 Ultra (Performance)" + ], + [ + "Yumi", + "PLA", + "YUMI PLA Bowden" + ], + [ + "Yumi", + "PLA", + "YUMI PLA Direct Drive" + ] + ], + "GFU99_01": [ + [ + "Generic", + "TPU", + "Tiertime Generic TPU" + ] + ], + "GFU99_2": [ + [ + "Generic", + "FLEX", + "Prusa Generic TPU" + ] + ], + "GFU99_3": [ + [ + "Generic", + "FLEX", + "Prusa Generic TPU HF" + ] + ], + "GPAB00": [ + [ + "Generic", + "PA", + "Generic PA" + ] + ], + "GPCB00": [ + [ + "Generic", + "PC", + "Generic PC" + ] + ], + "GPETB00": [ + [ + "Generic", + "PET", + "Generic PET" + ] + ], + "GPETGB00": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "GPLAB00": [ + [ + "Generic", + "PLA", + "Generic PLA" + ] + ], + "Generic ABS @E3NG v1.2S": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "Generic ABS @MK4S": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Generic ABS @MK4S 0.6": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Generic ABS @MK4S 0.8": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Generic ABS @MK4S HF0.4": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Generic ABS @MK4S HF0.5": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Generic ABS @MK4S HF0.6": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Generic ABS @MK4S HF0.8": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Generic ASA @E3NG v1.2S": [ + [ + "Generic", + "ASA", + "Generic ASA" + ] + ], + "Generic FLEX @MK4S": [ + [ + "Generic", + "FLEX", + "Prusa Generic TPU" + ] + ], + "Generic FLEX @MK4S 0.6": [ + [ + "Generic", + "FLEX", + "Prusa Generic TPU" + ] + ], + "Generic FLEX @MK4S 0.8": [ + [ + "Generic", + "FLEX", + "Prusa Generic TPU" + ] + ], + "Generic PCCF @E3NG v1.2S": [ + [ + "Generic", + "PC-CF", + "Generic PCCF" + ] + ], + "Generic PETG @E3NG v1.2S": [ + [ + "Generic", + "PETG", + "Generic PETG" + ] + ], + "Generic PETG @MK4S": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Generic PETG @MK4S 0.6": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Generic PETG @MK4S 0.8": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Generic PETG @MK4S HF0.4": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Generic PETG @MK4S HF0.5": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Generic PETG @MK4S HF0.6": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Generic PETG @MK4S HF0.8": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Generic PLA @E3NG v1.2S": [ + [ + "Generic", + "PLA", + "Generic PLA" + ] + ], + "Generic PLA @MK4S": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Generic PLA @MK4S 0.6": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Generic PLA @MK4S 0.8": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Generic PLA @MK4S HF0.4": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Generic PLA @MK4S HF0.5": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Generic PLA @MK4S HF0.6": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Generic PLA @MK4S HF0.8": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Generic PLA Silk @MK4S": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA Silk" + ] + ], + "Generic PLA Silk @MK4S 0.6": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA Silk" + ] + ], + "Generic PLA Silk @MK4S 0.8": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA Silk" + ] + ], + "Generic TPU @E3NG v1.2S": [ + [ + "Generic", + "TPU", + "Generic TPU" + ] + ], + "IQM1": [ + [ + "iQ Materials", + "PACF Pro", + "Fiberthree PACF Pro P1" + ] + ], + "IQM1011": [ + [ + "iQ Materials", + "VXL90 Xioneer", + "VXL90 TiQ2 P2" + ] + ], + "IQM2": [ + [ + "iQ Materials", + "ABS Material4Print Natur", + "Material4Print ABS Natur P1" + ] + ], + "IQM3": [ + [ + "iQ Materials", + "PETG Polymax", + "Polymaker PETG Polymax black P1" + ] + ], + "IQM7": [ + [ + "iQ Materials", + "PACF Pro", + "Fiberthree PACF Pro P2" + ] + ], + "LHF_abs": [ + [ + "LH Stinger", + "ASA", + "LHS ABS" + ] + ], + "LHF_asa": [ + [ + "LH Stinger", + "ASA", + "LHS ASA" + ] + ], + "LHF_ftpuf": [ + [ + "LH Stinger", + "TPU", + "LHS TPU Foamy 78A" + ] + ], + "LHF_pccf": [ + [ + "LH Stinger", + "PC", + "LHS PC CF" + ] + ], + "LHF_pctg": [ + [ + "LH Stinger", + "PCTG", + "LHS PCTG" + ] + ], + "LHF_petg": [ + [ + "LH Stinger", + "PETG", + "LHS PETG" + ] + ], + "LHF_pla": [ + [ + "LH Stinger", + "PLA", + "LHS PLA" + ] + ], + "LHF_ptpu_pl": [ + [ + "LH Stinger", + "TPU", + "LHS TPU" + ] + ], + "NIT01": [ + [ + "NIT", + "ABS", + "NIT ABS" + ] + ], + "NIT02": [ + [ + "NIT", + "PLA", + "NIT PLA" + ] + ], + "NIT03": [ + [ + "NIT", + "PETG", + "NIT PETG" + ] + ], + "OEABSB00": [ + [ + "Elegoo", + "ABS", + "Elegoo ABS" + ] + ], + "OEASAB00": [ + [ + "Elegoo", + "ASA", + "Elegoo ASA" + ] + ], + "OEPAHTB0": [ + [ + "Elegoo", + "PA", + "Elegoo PAHT" + ] + ], + "OEPCB00": [ + [ + "Elegoo", + "PC", + "Elegoo PC" + ] + ], + "OEPETGB0": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG" + ] + ], + "OEPLAB00": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA" + ] + ], + "OETPUB00": [ + [ + "Elegoo", + "TPU", + "Elegoo TPU" + ] + ], + "OF09vOcG": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Galaxy" + ] + ], + "OF0Ee2jQ": [ + [ + "Snapmaker", + "TPU", + "Snapmaker Dual TPU High-Flow" + ] + ], + "OF0M6QAQ": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Galaxy" + ] + ], + "OF0SyJhk": [ + [ + "FusRock", + "PA6-CF", + "Flashforge PA6-CF" + ] + ], + "OF0TODnN": [ + [ + "Generic", + "PET-CF", + "Generic PET-CF" + ] + ], + "OF0TjJ0G": [ + [ + "QIDI", + "ASA-AERO", + "Qidi ASA-Aero" + ] + ], + "OF0UbONm": [ + [ + "Generic", + "S-PAHT", + "FusRock S-PAHT" + ] + ], + "OF0VEdLB": [ + [ + "Cubicon", + "PETG", + "Cubicon PETG" + ] + ], + "OF0Y5MLd": [ + [ + "QIDI", + "PETG-GF", + "QIDI PETG-GF" + ] + ], + "OF0wk0Pk": [ + [ + "Generic", + "PLA", + "Flashforge PLA Pro" + ] + ], + "OF0z1XpN": [ + [ + "QIDI", + "PLA", + "QIDI PLA Rapido Silk" + ] + ], + "OF102WKM": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG PRO" + ] + ], + "OF17cONC": [ + [ + "Polymaker", + "PLA", + "PolyTerra Dual PLA" + ] + ], + "OF1Zypyy": [ + [ + "QIDI", + "PETG", + "QIDI PETG Tough" + ] + ], + "OF22AxCm": [ + [ + "Generic", + "PETG", + "Polymaker PETG" + ] + ], + "OF2HQqTC": [ + [ + "Cubicon", + "PLA", + "Cubicon PLA+" + ] + ], + "OF2Psxdc": [ + [ + "Co Print", + "TPU", + "CoPrint Generic TPU" + ] + ], + "OF2UFmOo": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA" + ] + ], + "OF2tvNYA": [ + [ + "Generic", + "PLA", + "Sovol Zero PLA Silk" + ] + ], + "OF38Hyk4": [ + [ + "Generic", + "PETG", + "Flashforge PETG Pro" + ] + ], + "OF3GKs2i": [ + [ + "eSUN", + "PLA", + "eSUN PLA-Matte" + ] + ], + "OF3XeEuV": [ + [ + "QIDI", + "PET-GF", + "QIDI PET-GF" + ] + ], + "OF3YOOOs": [ + [ + "Generic", + "PETG-CF", + "Chuanying Generic PETG-CF10" + ] + ], + "OF3arl18": [ + [ + "Elegoo", + "PETG", + "Elegoo Rapid PETG" + ] + ], + "OF41QVDH": [ + [ + "Generic", + "PLA", + "Flashforge Generic PLA-Silk" + ] + ], + "OF43idK2": [ + [ + "Generic", + "PLA", + "Creality Generic PLA Matte" + ] + ], + "OF48G7He": [ + [ + "QIDI", + "PETG", + "QIDI PETG Tough 0.8 nozzle" + ] + ], + "OF4RKeng": [ + [ + "Generic", + "ASA", + "Flashforge Generic ASA" + ] + ], + "OF4fgpse": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Marble" + ] + ], + "OF4gzzE4": [ + [ + "QIDI", + "PET-CF", + "QIDI PET-CF" + ] + ], + "OF4jILJA": [ + [ + "FlyingBear", + "PC", + "FlyingBear PC" + ] + ], + "OF4m9A5w": [ + [ + "Generic", + "HIPS", + "Flashforge Generic HIPS" + ] + ], + "OF50NUZI": [ + [ + "FlyingBear", + "PLA", + "FlyingBear PLA Basic" + ] + ], + "OF57IbZT": [ + [ + "Artillery", + "PLA Basic", + "Artillery PLA Basic" + ] + ], + "OF5Bmdtg": [ + [ + "Generic", + "ABS", + "Sovol SV06 Plus ACE ABS" + ] + ], + "OF5J2axC": [ + [ + "Generic", + "PLA", + "Sovol Zero PLA Basic" + ] + ], + "OF5T1Y9R": [ + [ + "Generic", + "ABS", + "Flashforge ABS Basic" + ] + ], + "OF5UUaUM": [ + [ + "Generic", + "PLA", + "Qidi Generic PLA High Speed" + ] + ], + "OF5Ugv12": [ + [ + "Other", + "PLA", + "Other PLA" + ] + ], + "OF5kwfZu": [ + [ + "FusRock", + "PA-CF", + "FusRock Generic NexPA-CF25" + ] + ], + "OF60Lmeq": [ + [ + "Generic", + "PLA", + "Flashforge PLA Matte" + ] + ], + "OF6EOy1A": [ + [ + "Bambu Lab", + "PETG", + "Bambu PETG" + ] + ], + "OF6FM5F7": [ + [ + "Generic", + "PAHT", + "FusRock PAHT" + ] + ], + "OF6Mxzsg": [ + [ + "Generic", + "TPU", + "Generic TPU" + ] + ], + "OF6SEkeJ": [ + [ + "Generic", + "PET-CF", + "FusRock PET-CF" + ] + ], + "OF6XIeUW": [ + [ + "Generic", + "PLA-CF", + "Generic PLA-CF10" + ] + ], + "OF6XuVQE": [ + [ + "Cubicon", + "ABS", + "Cubicon ABS" + ] + ], + "OF70DKqH": [ + [ + "Snapmaker", + "TPU", + "Snapmaker TPE" + ] + ], + "OF79FFkO": [ + [ + "Elegoo", + "PC", + "Elegoo PC-FR" + ] + ], + "OF7M9vy4": [ + [ + "Generic", + "PLA", + "RatRig BigNozzle PLA" + ] + ], + "OF7cv1Ub": [ + [ + "QIDI", + "PLA-CF", + "QIDI PLA-CF" + ] + ], + "OF7f0eUZ": [ + [ + "Volumic", + "ASA", + "Volumic ASA Ultra" + ] + ], + "OF7tgL9c": [ + [ + "Generic", + "ASA", + "Chuanying Generic ASA" + ] + ], + "OF8HQmJD": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Sparkle" + ] + ], + "OF8WvPVL": [ + [ + "QIDI", + "ABS", + "QIDI ABS Rapido 0.8 nozzle" + ] + ], + "OF9RZyBz": [ + [ + "Polymaker", + "PA", + "Polymaker Generic S1" + ] + ], + "OF9qvJxV": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Translucent2" + ] + ], + "OF9sr3Vn": [ + [ + "Generic", + "PLA", + "Flashforge PLA Sparkle" + ] + ], + "OF9tjFbj": [ + [ + "Peopoly", + "PLA", + "Peopoly Lancer PLA-C" + ] + ], + "OF9vnvM8": [ + [ + "Artillery", + "PETG-CF", + "Artillery PETG-CF" + ] + ], + "OFAEqlWa": [ + [ + "Generic", + "PLA", + "Flashforge Generic PLA-SILK" + ] + ], + "OFAGqTQG": [ + [ + "Volumic", + "PP", + "Volumic PP Ultra" + ] + ], + "OFAGrVjP": [ + [ + "Generic", + "PLA", + "Flashforge PLA Silk" + ] + ], + "OFAHagT9": [ + [ + "Elegoo", + "PLA", + "Elegoo Rapid PLA+" + ] + ], + "OFAK2XjZ": [ + [ + "Other", + "ABS", + "Other ABS" + ] + ], + "OFAUDGEQ": [ + [ + "QIDI", + "PLA", + "QIDI PLA Rapido Matte" + ] + ], + "OFAUX5fB": [ + [ + "Generic", + "PETG", + "C1 Generic High Flow PETG" + ] + ], + "OFAbsMuV": [ + [ + "Snapmaker", + "TPU", + "Snapmaker Dual TPE" + ] + ], + "OFAdAYYk": [ + [ + "QIDI", + "PPS-CF", + "QIDI PPS-CF" + ] + ], + "OFAdZFD1": [ + [ + "FusRock", + "PET-GF", + "FusRock Generic PET-GF" + ] + ], + "OFAiqi5v": [ + [ + "Elegoo", + "ASA", + "Elegoo ASA-CF" + ] + ], + "OFAnyzjN": [ + [ + "HATCHBOX", + "PETG", + "HATCHBOX PETG" + ] + ], + "OFAtydxE": [ + [ + "Generic", + "S-Multi", + "FusRock S-Multi" + ] + ], + "OFBGxHgT": [ + [ + "FlyingBear", + "PLA", + "FlyingBear PLA" + ] + ], + "OFBRSZom": [ + [ + "Volumic", + "PPS-CF", + "PPS Carbone (Performance)" + ] + ], + "OFBvxnjM": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG-GF" + ] + ], + "OFC2FSsV": [ + [ + "Overture", + "PLA", + "Overture PLA" + ] + ], + "OFC5CCeo": [ + [ + "Generic", + "PETG-CF", + "Flashforge Generic PETG-CF" + ] + ], + "OFCJaXZS": [ + [ + "Generic", + "PPS", + "Flashforge PPS" + ] + ], + "OFCKK3u9": [ + [ + "FlyingBear", + "PA-CF", + "FlyingBear PA-CF" + ] + ], + "OFCgpxn2": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS HF" + ] + ], + "OFChH9xu": [ + [ + "Snapmaker", + "TPU", + "Snapmaker J1 TPE" + ] + ], + "OFD5e0sH": [ + [ + "Elegoo", + "PLA", + "Elegoo Rapid PLA+" + ] + ], + "OFDCk0cz": [ + [ + "Generic", + "PETG-CF", + "Generic PETG-CF" + ] + ], + "OFDMWB6t": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA+" + ] + ], + "OFDNiRTx": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA+" + ] + ], + "OFDkABjZ": [ + [ + "Generic", + "TPU-90A", + "Generic TPU-90A" + ] + ], + "OFDramZ2": [ + [ + "Co Print", + "PETG", + "CoPrint Generic PETG" + ] + ], + "OFE22Uba": [ + [ + "Generic", + "TPU-90A", + "Flashforge TPU-90A" + ] + ], + "OFEYKrjb": [ + [ + "Artillery", + "PETG", + "Artillery PETG" + ] + ], + "OFEdyZqS": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA Luminous" + ] + ], + "OFFIL5pD": [ + [ + "QIDI", + "PA12-CF", + "QIDI PA12-CF" + ] + ], + "OFFenwbU": [ + [ + "Generic", + "PLA", + "FLSun S1 PLA Silk" + ] + ], + "OFFmJ8aR": [ + [ + "Other", + "PLA", + "Other PLA Hyper" + ] + ], + "OFFrbsvQ": [ + [ + "Other", + "TPU", + "Other TPU" + ] + ], + "OFG8vHw8": [ + [ + "Generic", + "ASA", + "Qidi Generic ASA" + ] + ], + "OFGFZhWu": [ + [ + "Generic", + "PETG", + "Sovol SV06 ACE PETG" + ] + ], + "OFGb4hPY": [ + [ + "Anycubic", + "PEBA", + "Anycubic PEBA 95A" + ] + ], + "OFGfdP6J": [ + [ + "FusRock", + "S-Multi", + "FusRock Generic S-Multi" + ], + [ + "Generic", + "PET-CF", + "FusRock Generic S-Multi" + ] + ], + "OFHBTqyD": [ + [ + "Generic", + "PLA", + "Flashforge PLA Basic" + ] + ], + "OFHDaMut": [ + [ + "Volumic", + "PA6-CF", + "PA6 CF20 (Performance)" + ] + ], + "OFHL32M0": [ + [ + "Anycubic", + "TPU", + "Anycubic TPU" + ], + [ + "Generic", + "TPU", + "Anycubic TPU" + ] + ], + "OFHtQDC9": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Basic" + ] + ], + "OFICPchn": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Basic" + ] + ], + "OFIGHU7W": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG-CF" + ] + ], + "OFIl3ar9": [ + [ + "Generic", + "PETG", + "Sovol SV08 PETG" + ] + ], + "OFIzguhB": [ + [ + "Volumic", + "UNIV", + "Volumic UNIVERSAL Ultra" + ] + ], + "OFJIImD5": [ + [ + "Elegoo", + "PETG", + "Elegoo PET-CF" + ] + ], + "OFJfx0Cl": [ + [ + "Generic", + "PE-CF", + "Generic PE-CF" + ] + ], + "OFJsqMzx": [ + [ + "Generic", + "PLA", + "Generic PLA Matte" + ] + ], + "OFK39W9u": [ + [ + "Generic", + "PETG-CF", + "Flashforge Generic PETG-CF10" + ] + ], + "OFK820gS": [ + [ + "QIDI", + "ASA", + "QIDI ASA" + ] + ], + "OFKDnrjL": [ + [ + "Artillery", + "PLA Matte", + "Artillery PLA Matte" + ] + ], + "OFKH15Hd": [ + [ + "Generic", + "ABS", + "Sovol SV08 ABS" + ] + ], + "OFKWVi10": [ + [ + "Generic", + "PLA-CF", + "Flashforge Generic PLA-CF10" + ] + ], + "OFKZH00L": [ + [ + "FusRock", + "PET-CF", + "FusRock Generic PET-CF" + ], + [ + "Generic", + "PET-CF", + "FusRock Generic PET-CF" + ] + ], + "OFKhtNBx": [ + [ + "Snapmaker", + "TPU", + "Snapmaker J1 TPU High-Flow" + ] + ], + "OFKjlAbC": [ + [ + "QIDI", + "UltraPA-CF25", + "QIDI UltraPA-CF25" + ] + ], + "OFKzh9lx": [ + [ + "InfiMech", + "PLA", + "InfiMech PLA" + ] + ], + "OFL4BLAC": [ + [ + "iQ Materials", + "HPP4GF25", + "Grauts HPP4GF25 P1" + ] + ], + "OFL4KniM": [ + [ + "Elegoo", + "TPU", + "Elegoo Rapid TPU 95A" + ] + ], + "OFL7U2MV": [ + [ + "RatRig", + "ABS", + "RatRig PunkFil ABS" + ] + ], + "OFL7vEML": [ + [ + "Generic", + "PC", + "FlashForge PC" + ] + ], + "OFLSBS99": [ + [ + "Generic", + "SBS", + "fdm_filament_sbs" + ] + ], + "OFLeFaUO": [ + [ + "Generic", + "PVA", + "Flashforge Generic PVA" + ] + ], + "OFLhOFQk": [ + [ + "Volumic", + "ASA", + "Volumic ASA Ultra (Performance)" + ] + ], + "OFLiTjQK": [ + [ + "Other", + "PA-CF", + "Other PA-CF" + ] + ], + "OFLriDvE": [ + [ + "Generic", + "PLA", + "Creality Generic PLA High Speed" + ] + ], + "OFLxO2Oc": [ + [ + "Generic", + "PET", + "FusRock PET" + ] + ], + "OFM3PeVe": [ + [ + "FusRock", + "ASA-CF", + "Flashforge ASA-CF" + ], + [ + "Generic", + "ASA-CF", + "Flashforge ASA-CF" + ] + ], + "OFMCMXSG": [ + [ + "Generic", + "PP", + "Generic PP-GF" + ] + ], + "OFMHTnPP": [ + [ + "FusRock", + "PA-CF", + "Flashforge PA12-CF" + ] + ], + "OFMQJToc": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG-GF" + ] + ], + "OFN0X7oN": [ + [ + "QIDI", + "PLA", + "QIDI PLA Basic" + ] + ], + "OFN0pBpo": [ + [ + "Generic", + "TPU", + "Qidi Generic TPU 95A" + ] + ], + "OFNQHqAV": [ + [ + "Cubicon", + "ABS", + "Cubicon ABSk" + ] + ], + "OFNRgSch": [ + [ + "Artillery", + "PC", + "Artillery PC" + ] + ], + "OFNX5uRT": [ + [ + "QIDI", + "TPU-AERO", + "QIDI TPU-Aero" + ] + ], + "OFNbajj9": [ + [ + "Generic", + "ASA", + "Creality Generic ASA-CF" + ] + ], + "OFNvrbnf": [ + [ + "Co Print", + "ABS", + "CoPrint Generic ABS" + ] + ], + "OFNz8rrn": [ + [ + "FlyingBear", + "PLA", + "FlyingBear PLA Hyper" + ] + ], + "OFO3IOWM": [ + [ + "Volumic", + "PETG-ESD", + "PETG ESD (Performance)" + ] + ], + "OFO5R9KJ": [ + [ + "Generic", + "TPU", + "Sovol Zero TPU" + ] + ], + "OFOJUJAS": [ + [ + "Cubicon", + "PA", + "Cubicon PA-CF" + ] + ], + "OFOSzSuh": [ + [ + "Generic", + "PA-CF", + "Flashforge PA-CF" + ] + ], + "OFOW9K96": [ + [ + "QIDI", + "TPU", + "Qidi TPU 95A-HF" + ] + ], + "OFOkTA0C": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG Translucent" + ] + ], + "OFOp3Oef": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "OFOysPuE": [ + [ + "QIDI", + "TPU-GF", + "QIDI TPU-GF" + ] + ], + "OFP4poIv": [ + [ + "Generic", + "PLA", + "Sovol Zero PLA Silk HS Nozzle" + ] + ], + "OFPGelMX": [ + [ + "Volumic", + "PP", + "Volumic PP Ultra (Performance)" + ] + ], + "OFPJJxkF": [ + [ + "FusRock", + "PET-CF", + "Flashforge PET-CF" + ], + [ + "Generic", + "PET-CF", + "Flashforge PET-CF" + ] + ], + "OFPJrlC3": [ + [ + "Generic", + "PLA-CF", + "Flashforge PLA-CF" + ] + ], + "OFPeGtB1": [ + [ + "Volumic", + "UNIV", + "Volumic UNIVERSAL Ultra (Performance)" + ] + ], + "OFPokkKe": [ + [ + "QIDI", + "PETG-CF", + "QIDI PETG-CF" + ] + ], + "OFPwi9X0": [ + [ + "HATCHBOX", + "ABS", + "HATCHBOX ABS" + ] + ], + "OFQ5xAKf": [ + [ + "Generic", + "PVA", + "Chuanying Generic PVA" + ] + ], + "OFQ9dI8j": [ + [ + "Generic", + "PC", + "Prusa Generic PC HF" + ] + ], + "OFQCAYAt": [ + [ + "FusRock", + "PPA-CF", + "Flashforge PPA-CF" + ], + [ + "Generic", + "PPA-CF", + "Flashforge PPA-CF" + ] + ], + "OFQZINrX": [ + [ + "QIDI", + "ABS-GF", + "QIDI ABS-GF10" + ] + ], + "OFQhLoxN": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Wood" + ] + ], + "OFQkWxJy": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Matte" + ] + ], + "OFR8dTVW": [ + [ + "Generic", + "PLA", + "FLSun S1 PLA High Speed" + ] + ], + "OFRNU58t": [ + [ + "Generic", + "TPU", + "Flashforge TPU 95A" + ] + ], + "OFRR8LUh": [ + [ + "Generic", + "ASA", + "Generic ASA" + ] + ], + "OFRS2Ggt": [ + [ + "Generic", + "PP-CF", + "Generic PP-CF" + ] + ], + "OFRgDMDJ": [ + [ + "Generic", + "PETG", + "Flashforge PETG Transparent" + ] + ], + "OFS1ekCg": [ + [ + "Generic", + "PETG", + "RatRig BigNozzle PCTG" + ] + ], + "OFS3HrR1": [ + [ + "Other", + "PLA", + "Other PLA" + ] + ], + "OFSCm6pJ": [ + [ + "Elegoo", + "PETG", + "Elegoo PET-CF" + ] + ], + "OFSSuKRr": [ + [ + "QIDI", + "PETG", + "QIDI PETG Basic" + ] + ], + "OFSdF3B1": [ + [ + "Generic", + "PAHT-CF", + "FusRock PAHT-CF" + ] + ], + "OFSkBwxf": [ + [ + "Generic", + "ASA", + "RatRig BigNozzle ASA" + ] + ], + "OFSoZBK2": [ + [ + "Generic", + "PLA", + "Sovol SV06 Plus ACE PLA" + ] + ], + "OFSpTWdv": [ + [ + "Other", + "PA-CF", + "Other PA-CF" + ] + ], + "OFT3TqB6": [ + [ + "Elas", + "ASA", + "Elas ASA" + ] + ], + "OFTa6gPH": [ + [ + "Generic", + "PLA", + "Flashforge PLA Pro" + ] + ], + "OFTbdJyT": [ + [ + "Other", + "PC", + "Other PC" + ] + ], + "OFTbsWVg": [ + [ + "Artillery", + "TPU", + "Artillery TPU" + ] + ], + "OFToBodi": [ + [ + "QIDI", + "ABS", + "QIDI ABS Rapido Metal" + ] + ], + "OFTsGps3": [ + [ + "InfiMech", + "PC", + "InfiMech PC" + ] + ], + "OFTszIHE": [ + [ + "Generic", + "PLA", + "Creality Generic PLA Silk" + ] + ], + "OFUJw3Lx": [ + [ + "QIDI", + "PEBA", + "QIDI PEBA 95A" + ] + ], + "OFUYjPc4": [ + [ + "Generic", + "PLA", + "Dremel Generic PLA" + ] + ], + "OFUkpobY": [ + [ + "QIDI", + "PAHT-CF", + "QIDI PAHT-CF" + ] + ], + "OFUqBrgf": [ + [ + "Generic", + "TPU", + "Chuanying Generic TPU" + ] + ], + "OFV1szQh": [ + [ + "Peopoly", + "PETG", + "Peopoly Lancer PETG-C" + ] + ], + "OFV4hD1o": [ + [ + "QIDI", + "PLA", + "QIDI PLA Rapido Metal" + ] + ], + "OFVBNoov": [ + [ + "InfiMech", + "ABS", + "InfiMech ABS" + ] + ], + "OFVJXE4n": [ + [ + "Other", + "ABS", + "Other ABS" + ] + ], + "OFVLuVmD": [ + [ + "Elegoo", + "PETG", + "Elegoo Rapid PETG" + ] + ], + "OFVhG0yM": [ + [ + "QIDI", + "ABS", + "QIDI ABS Odorless" + ] + ], + "OFW8UdlP": [ + [ + "QIDI", + "ABS", + "QIDI ABS Rapido" + ] + ], + "OFWkCAdc": [ + [ + "QIDI", + "PETG", + "QIDI PETG Tough 0.2 nozzle" + ] + ], + "OFWs8KIZ": [ + [ + "Generic", + "ABS-CF", + "Flashforge ABS-CF" + ] + ], + "OFWv6JYn": [ + [ + "Generic", + "PLA", + "Flashforge PLA Metal" + ] + ], + "OFWyCd0Y": [ + [ + "QIDI", + "PA-S", + "QIDI Support For PET/PA" + ] + ], + "OFXA8vy9": [ + [ + "Artillery", + "PLA Basic+", + "Artillery PLA Basic+" + ] + ], + "OFXNO5Of": [ + [ + "QIDI", + "PLA", + "QIDI PLA Rapido 0.2 nozzle" + ] + ], + "OFXQX0tr": [ + [ + "RatRig", + "PETG-CF10", + "RatRig PunkFil PETG CF" + ] + ], + "OFXcfTEb": [ + [ + "Generic", + "PETG", + "Tinmorry PETG-ECO" + ] + ], + "OFXfgh61": [ + [ + "Generic", + "PA-CF", + "Prusa Generic PA-CF" + ] + ], + "OFXkwkn6": [ + [ + "QIDI", + "UltraPA", + "QIDI PA-Ultra" + ] + ], + "OFXmQgno": [ + [ + "eSUN", + "PLA", + "eSUN PLA-Marble" + ] + ], + "OFXv4FUi": [ + [ + "Generic", + "PLA", + "Flashforge PLA Galaxy" + ] + ], + "OFY1U4uM": [ + [ + "Generic", + "TPU", + "Sovol SV08 TPU" + ] + ], + "OFYG2Gfy": [ + [ + "Afinia", + "ABS", + "Afinia ABS+" + ] + ], + "OFYH00jV": [ + [ + "QIDI", + "ABS-GF", + "QIDI ABS-GF" + ] + ], + "OFYICbI4": [ + [ + "FusRock", + "PAHT-CF", + "FusRock Generic PAHT-CF" + ], + [ + "Generic", + "PAHT-CF", + "FusRock Generic PAHT-CF" + ] + ], + "OFYRS7GZ": [ + [ + "Generic", + "TPU-95A", + "Generic TPU-95A" + ] + ], + "OFYZs7dN": [ + [ + "Generic", + "PLA", + "Flashforge Generic HS PLA" + ] + ], + "OFYehFGr": [ + [ + "Generic", + "TPU", + "RatRig BigNozzle TPU" + ] + ], + "OFYfZgxU": [ + [ + "FlyingBear", + "TPU", + "FlyingBear TPU" + ] + ], + "OFZAkPHG": [ + [ + "QIDI", + "PPS-GF", + "QIDI PPS-GF" + ] + ], + "OFZQFvHQ": [ + [ + "Generic", + "PETG", + "Flashforge HS PETG" + ] + ], + "OFZcrV9c": [ + [ + "FusRock", + "PPS-CF", + "Flashforge PPS-CF" + ], + [ + "Generic", + "PPS-CF", + "Flashforge PPS-CF" + ] + ], + "OFZhdVnP": [ + [ + "eSUN", + "PLA", + "eSUN PLA-Basic" + ] + ], + "OFZjZ1k7": [ + [ + "Generic", + "PLA", + "Creality HF Generic PLA" + ] + ], + "OFa5cFqE": [ + [ + "QIDI", + "PAHT-S", + "QIDI Support For PAHT" + ] + ], + "OFa6L3Of": [ + [ + "QIDI", + "PC-ABS-FR", + "Qidi PC-ABS-FR" + ] + ], + "OFaKIoH2": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Sparkle" + ] + ], + "OFaKZ6JI": [ + [ + "Generic", + "PETG", + "Generic PETG Basic" + ] + ], + "OFaxQnfz": [ + [ + "Cubicon", + "PC", + "Cubicon PC" + ] + ], + "OFb3UzMZ": [ + [ + "InfiMech", + "PA-CF", + "InfiMech PA-CF" + ] + ], + "OFbK5wve": [ + [ + "Generic", + "TPU", + "Prusa Generic TPU HF" + ] + ], + "OFbS9rjH": [ + [ + "QIDI", + "PETG", + "QIDI PETG Rapido" + ] + ], + "OFbYP6Wb": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Marble" + ] + ], + "OFbgt3u3": [ + [ + "Elas", + "PLA", + "Elas PLA Basic" + ] + ], + "OFc2SnEp": [ + [ + "Artillery", + "PVA", + "Artillery PVA" + ] + ], + "OFcZZWnB": [ + [ + "Cubicon", + "ABS", + "Cubicon ABS-A100" + ] + ], + "OFceJcLf": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA" + ] + ], + "OFcqs5g7": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Translucent2" + ] + ], + "OFcsHCy4": [ + [ + "Generic", + "PLA", + "Sovol SV08 PLA" + ] + ], + "OFcy60o4": [ + [ + "Generic", + "ASA", + "Flashforge ASA Basic" + ] + ], + "OFdLGn0R": [ + [ + "Generic", + "PLA", + "Creality Generic PLA Wood" + ] + ], + "OFdNORGt": [ + [ + "Generic", + "ASA-GF", + "Flashforge ASA-GF" + ] + ], + "OFdSGkUG": [ + [ + "Anycubic", + "PETG", + "Anycubic PETG" + ], + [ + "Generic", + "PETG", + "Anycubic PETG" + ] + ], + "OFdhVJql": [ + [ + "Generic", + "PLA", + "Flashforge PLA Buint Ti" + ] + ], + "OFeFRwST": [ + [ + "Generic", + "PVA", + "Flashforge PVA" + ] + ], + "OFePPmng": [ + [ + "Snapmaker", + "ABS", + "Snapmaker J1 ABS Benchy" + ] + ], + "OFeWMAGH": [ + [ + "Generic", + "PLA", + "Flashforge HS PLA Burnt Ti" + ] + ], + "OFefhSuj": [ + [ + "InfiMech", + "PLA", + "InfiMech PLA Hyper" + ] + ], + "OFenGPBR": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG Translucent" + ] + ], + "OFeybQvK": [ + [ + "QIDI", + "ABS", + "QIDI ABS Rapido 0.2 nozzle" + ] + ], + "OFf0JIqs": [ + [ + "Eryone", + "PP", + "Eryone PP" + ] + ], + "OFf2b9wu": [ + [ + "Anycubic", + "ABS", + "Anycubic ABS" + ], + [ + "Generic", + "ABS", + "Anycubic ABS" + ] + ], + "OFfB1QKi": [ + [ + "Generic", + "PETG", + "Generic PETG HF" + ] + ], + "OFfJoIgg": [ + [ + "QIDI", + "PAHT-GF", + "QIDI PAHT-GF" + ] + ], + "OFfdxQok": [ + [ + "Generic", + "PLA", + "Generic PLA+" + ] + ], + "OFfiN1Lw": [ + [ + "Generic", + "PLA", + "Chuanying Generic HS PLA" + ] + ], + "OFfkPhrJ": [ + [ + "Generic", + "PLA", + "Creality HF Generic Speed PLA" + ] + ], + "OFfoA8kJ": [ + [ + "CoLiDo", + "PLA", + "CoLiDo PLA" + ] + ], + "OFftQOek": [ + [ + "Generic", + "PLA", + "Flashforge PLA Matte" + ] + ], + "OFg4N6ZL": [ + [ + "FlyingBear", + "PETG", + "FlyingBear PETG" + ] + ], + "OFgJX0Fc": [ + [ + "Generic", + "PPS", + "FlashForge PPS" + ] + ], + "OFgK2Xvv": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA" + ], + [ + "Generic", + "PLA", + "Anycubic PLA" + ] + ], + "OFgMGSpF": [ + [ + "Generic", + "PETG", + "Sovol SV06 Plus ACE PETG" + ] + ], + "OFgNzLVh": [ + [ + "Generic", + "ABS", + "Sovol Zero ABS" + ] + ], + "OFgSK1hL": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA High Speed" + ], + [ + "Generic", + "PLA", + "Anycubic PLA High Speed" + ] + ], + "OFglpvEE": [ + [ + "Generic", + "TPU", + "Sovol SV06 ACE TPU" + ] + ], + "OFgrGfPy": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA+" + ], + [ + "Generic", + "PLA", + "Anycubic PLA+" + ] + ], + "OFgtZjeX": [ + [ + "Snapmaker", + "ABS", + "Snapmaker ABS Benchy" + ] + ], + "OFh5GUhZ": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Wood" + ] + ], + "OFhKNHC8": [ + [ + "Generic", + "PETG", + "Generic PETG PRO" + ] + ], + "OFhzHd2Y": [ + [ + "Generic", + "HIPS", + "Flashforge HIPS" + ] + ], + "OFi1ocNv": [ + [ + "Generic", + "PA", + "Flashforge PA" + ] + ], + "OFiJYDPC": [ + [ + "HATCHBOX", + "PLA", + "HATCHBOX PLA" + ] + ], + "OFic2uNr": [ + [ + "Generic", + "PLA", + "Sovol SV06 ACE PLA" + ] + ], + "OFicxmSa": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA HF" + ] + ], + "OFidJLn8": [ + [ + "Polymaker", + "ABS", + "PolyLite ABS" + ] + ], + "OFiyGNbI": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG-CF" + ] + ], + "OFjFPhUC": [ + [ + "Generic", + "PLA", + "Prusament rPLA" + ] + ], + "OFjZw7k1": [ + [ + "Polymaker", + "PLA", + "PolyTerra J1 PLA" + ] + ], + "OFjdTQd6": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA Silk" + ], + [ + "Generic", + "PLA", + "Anycubic PLA Silk" + ] + ], + "OFjepy6j": [ + [ + "FusRock", + "S-PAHT", + "FusRock Generic S-PAHT" + ], + [ + "Generic", + "PA-CF", + "FusRock Generic S-PAHT" + ] + ], + "OFjj9Yiz": [ + [ + "Generic", + "PETG", + "RatRig BigNozzle PETG" + ] + ], + "OFjohFkg": [ + [ + "QIDI", + "PETG", + "QIDI PETG Translucent" + ] + ], + "OFjqPjjl": [ + [ + "Generic", + "PET", + "Generic PET" + ] + ], + "OFjurQ3I": [ + [ + "Generic", + "PETG", + "Sovol Zero PETG" + ] + ], + "OFk388hR": [ + [ + "Generic", + "PA6-CF", + "Generic PA6-CF" + ] + ], + "OFk6T5YH": [ + [ + "Generic", + "PETG", + "Sovol Zero PETG HS Nozzle" + ] + ], + "OFkaDtKx": [ + [ + "FusRock", + "PA-CF", + "Flashforge PA66-CF" + ] + ], + "OFkeeluv": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA HF" + ] + ], + "OFkfK62J": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Silk" + ] + ], + "OFklg4aM": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA PRO" + ] + ], + "OFkoT6jl": [ + [ + "Generic", + "PLA", + "Prusament PLA" + ] + ], + "OFlN56xW": [ + [ + "Generic", + "PLA", + "Chuanying Generic PLA-Silk" + ] + ], + "OFlNKZLP": [ + [ + "Generic", + "PLA", + "Flashforge PLA Metal" + ] + ], + "OFlbn4FY": [ + [ + "Snapmaker", + "PLA", + "Snapmaker PLA Matte" + ] + ], + "OFlxnxDx": [ + [ + "Generic", + "SBS", + "Tiertime Generic SBS" + ] + ], + "OFlzQ0IS": [ + [ + "Peopoly", + "PET-CF", + "Peopoly Lancer PET-CF" + ] + ], + "OFm1VZed": [ + [ + "Generic", + "PLA-CF", + "Flashforge Generic PLA-CF" + ] + ], + "OFmF1xL9": [ + [ + "QIDI", + "ASA-CF", + "QIDI ASA-CF" + ] + ], + "OFmKNi3f": [ + [ + "QIDI", + "PLA", + "QIDI PLA Rapido" + ] + ], + "OFmOWC74": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG PRO" + ] + ], + "OFmcJsqt": [ + [ + "InfiMech", + "TPU", + "InfiMech TPU" + ] + ], + "OFmduKVr": [ + [ + "Generic", + "PPS-CF", + "FlashForge PPS-CF" + ] + ], + "OFmeQjRP": [ + [ + "Elegoo", + "TPU", + "Elegoo Rapid TPU 95A" + ] + ], + "OFmgesco": [ + [ + "Generic", + "TPU", + "Flashforge TPU 65D" + ] + ], + "OFnLnZQo": [ + [ + "Cubicon", + "PC", + "Cubicon PC" + ] + ], + "OFnMW5jr": [ + [ + "FlyingBear", + "ABS", + "FlyingBear ABS" + ] + ], + "OFnRTkEq": [ + [ + "RatRig", + "PETG", + "RatRig PunkFil PETG" + ] + ], + "OFnY9ovF": [ + [ + "Artillery", + "PA-CF", + "Artillery PA-CF" + ] + ], + "OFnj7bPm": [ + [ + "QIDI", + "ABS", + "QIDI ABS Rapido 0.6 nozzle" + ] + ], + "OFnsWQTp": [ + [ + "QIDI", + "PLA", + "QIDI WOOD Rapido" + ] + ], + "OFnyirh9": [ + [ + "Artillery", + "PLA Silk", + "Artillery PLA Silk" + ] + ], + "OFo4VB3w": [ + [ + "Generic", + "ABS-CF", + "Generic ABS-CF" + ] + ], + "OFo9EwvK": [ + [ + "Generic", + "PETG-CF", + "Generic PETG-CF10" + ] + ], + "OFoAaHcF": [ + [ + "FusRock", + "PAHT-GF", + "FusRock Generic PAHT-GF" + ] + ], + "OFoJUGj6": [ + [ + "Generic", + "PLA", + "Flashforge HS PLA" + ] + ], + "OFoimrpo": [ + [ + "Generic", + "PETG-CF", + "Flashforge PETG-CF" + ] + ], + "OFp8lkYD": [ + [ + "FusRock", + "PPA-GF", + "Flashforge PPA-GF" + ] + ], + "OFp90N8a": [ + [ + "Artillery", + "PA", + "Artillery PA" + ] + ], + "OFpHq9gJ": [ + [ + "Generic", + "PETG", + "Prusament PETG" + ] + ], + "OFphFSUJ": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG HF" + ] + ], + "OFpwg2Qh": [ + [ + "Elegoo", + "ASA", + "Elegoo ASA-CF" + ] + ], + "OFq6IhFg": [ + [ + "Cubicon", + "PLA", + "Cubicon PLA" + ] + ], + "OFqPp2yL": [ + [ + "QIDI", + "PETG", + "QIDI PETG Tough 0.6 nozzle" + ] + ], + "OFqRWRU6": [ + [ + "Generic", + "ABS-GF", + "Flashforge ABS-GF" + ] + ], + "OFqSTJ3I": [ + [ + "Generic", + "PLA", + "OrcaArena Generic PLA Silk" + ] + ], + "OFqh1Ngp": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA-CF" + ] + ], + "OFqkcvDI": [ + [ + "Peopoly", + "ABS", + "Peopoly Lancer ABS-GF" + ] + ], + "OFqyIUDi": [ + [ + "QIDI", + "PLA-CF", + "Qidi PLA-CF" + ] + ], + "OFrbbq6E": [ + [ + "Other", + "PLA", + "Other PLA Hyper" + ] + ], + "OFrgQ1Ns": [ + [ + "Elas", + "PLA", + "Elas PLA Pro" + ] + ], + "OFs1CbB1": [ + [ + "QIDI", + "PLA", + "QIDI PLA Rapido 0.8 nozzle" + ] + ], + "OFs6lLH7": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Silk" + ] + ], + "OFsCOJv7": [ + [ + "Generic", + "TPU", + "Generic TPU" + ] + ], + "OFsKqcQW": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA PRO" + ] + ], + "OFsLkLw6": [ + [ + "Generic", + "PLA", + "Generic PLA Matte" + ] + ], + "OFsNZcid": [ + [ + "Polymaker", + "PLA", + "PolyLite Dual PLA" + ] + ], + "OFsgANe3": [ + [ + "Generic", + "PAHT-CF", + "Flashforge PAHT-CF" + ] + ], + "OFsmPFwh": [ + [ + "Generic", + "TPU", + "Sovol SV06 Plus ACE TPU" + ] + ], + "OFt1YdX8": [ + [ + "Generic", + "PLA", + "FLSun T1 PLA Silk" + ] + ], + "OFt5bvKK": [ + [ + "Artillery", + "PETG Basic", + "Artillery PETG Basic" + ] + ], + "OFt9AiN5": [ + [ + "Generic", + "PA", + "Polymaker CoPA" + ] + ], + "OFtEFtsz": [ + [ + "Polymaker", + "PA", + "Polymaker Generic CoPA" + ] + ], + "OFtJMczs": [ + [ + "Generic", + "TPU-64D", + "Flashforge TPU-64D" + ] + ], + "OFtL05hE": [ + [ + "Generic", + "PLA", + "Sovol Zero PLA Basic HS Nozzle" + ] + ], + "OFtXBjvb": [ + [ + "Artillery", + "PLA Tough", + "Artillery PLA Tough" + ] + ], + "OFtcTs1o": [ + [ + "Generic", + "ABS", + "RatRig BigNozzle ABS" + ] + ], + "OFtemzzw": [ + [ + "Generic", + "ABS", + "Sovol SV06 ACE ABS" + ] + ], + "OFtet8ui": [ + [ + "Snapmaker", + "TPU", + "Snapmaker TPU High-Flow" + ] + ], + "OFti31HS": [ + [ + "Cubicon", + "PLA", + "Cubicon PLAi21" + ] + ], + "OFtmT4Nd": [ + [ + "Artillery", + "ASA", + "Artillery ASA" + ] + ], + "OFtwA91m": [ + [ + "Generic", + "PC", + "Sovol Zero PC" + ] + ], + "OFuPC4S8": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA-CF" + ] + ], + "OFuPN3iW": [ + [ + "Other", + "PETG", + "Other PETG" + ] + ], + "OFuTFxVP": [ + [ + "Overture", + "ABS", + "Overture ABS" + ] + ], + "OFuXkYly": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Glow" + ] + ], + "OFuYvNS3": [ + [ + "Afinia", + "ABS", + "Afinia Value ABS" + ] + ], + "OFuidAEg": [ + [ + "Bambu Lab", + "ABS", + "Bambu ABS" + ] + ], + "OFuq9tqb": [ + [ + "InfiMech", + "PLA", + "InfiMech PLA Basic" + ] + ], + "OFuswXlp": [ + [ + "Generic", + "TPU", + "Generic TPU 85A" + ] + ], + "OFutwD6f": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Glow" + ] + ], + "OFuyJhLF": [ + [ + "Generic", + "PETG", + "Flashforge PETG Basic" + ] + ], + "OFv2NHLC": [ + [ + "QIDI", + "ABS-GF", + "QIDI ABS-GF25" + ] + ], + "OFv8SRZh": [ + [ + "Generic", + "PLA-CF", + "Prusa Generic PLA-CF" + ] + ], + "OFvGSn0l": [ + [ + "Generic", + "PETG", + "Creality Generic PETG-CF" + ] + ], + "OFvN3117": [ + [ + "QIDI", + "PLA", + "QIDI PLA Matte Basic" + ] + ], + "OFvOhSjO": [ + [ + "Other", + "TPU", + "Other TPU" + ] + ], + "OFvYfHPB": [ + [ + "Anycubic", + "PLA", + "Anycubic PLA Matte" + ] + ], + "OFvaXhYD": [ + [ + "InfiMech", + "PETG", + "InfiMech PETG" + ] + ], + "OFviQ9lH": [ + [ + "Generic", + "TPU", + "Flashforge Generic TPU" + ] + ], + "OFvsS7Fb": [ + [ + "Generic", + "TPU-64D", + "Generic TPU-64D" + ] + ], + "OFvw0owy": [ + [ + "Afinia", + "PLA", + "Afinia Value PLA" + ] + ], + "OFvxvkAd": [ + [ + "Artillery", + "PLA-CF", + "Artillery PLA-CF" + ] + ], + "OFw05rkF": [ + [ + "Other", + "PETG", + "Other PETG" + ] + ], + "OFw0qp7w": [ + [ + "Artillery", + "PET", + "Artillery PET" + ] + ], + "OFw1S8lL": [ + [ + "Artillery", + "ABS", + "Artillery ABS" + ] + ], + "OFw4OHWE": [ + [ + "Elas", + "PETG", + "Elas PETG Basic" + ] + ], + "OFwHPTvy": [ + [ + "Generic", + "PLA", + "Qidi Generic PLA+" + ] + ], + "OFwf0oWp": [ + [ + "Generic", + "PLA", + "Flashforge PLA Luminous" + ] + ], + "OFwyqcGn": [ + [ + "Elegoo", + "PLA", + "Elegoo PLA Matte" + ] + ], + "OFxLeRyT": [ + [ + "Generic", + "PVA", + "Prusa Generic PVA HF" + ] + ], + "OFxVzzn4": [ + [ + "Generic", + "PLA-CF", + "Chuanying Generic PLA-CF10" + ] + ], + "OFxfCy9m": [ + [ + "Volumic", + "PETG", + "Volumic PCTG Ultra (Performance)" + ] + ], + "OFxjFYxE": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG HF" + ] + ], + "OFxlch4V": [ + [ + "CoLiDo", + "PLA", + "CoLiDo PLA Silk" + ] + ], + "OFxodd2u": [ + [ + "DeltaMaker", + "PLA", + "DeltaMaker Brand PLA" + ] + ], + "OFxxa5yn": [ + [ + "Snapmaker", + "ABS", + "Snapmaker Dual ABS Benchy" + ] + ], + "OFy7oQ1g": [ + [ + "Generic", + "ASA", + "Blocks Generic ASA-CF" + ] + ], + "OFyD1Zo8": [ + [ + "Other", + "PC", + "Other PC" + ] + ], + "OFyoW5W2": [ + [ + "Generic", + "PLA", + "Flashforge PLA Color Change" + ] + ], + "OFypQeKR": [ + [ + "Generic", + "PC-CF", + "Generic PC-CF" + ] + ], + "OFyt23xJ": [ + [ + "Generic", + "PLA", + "FLSun T1 PLA High Speed" + ] + ], + "OFyxAbO4": [ + [ + "Generic", + "TPU-95A", + "Flashforge TPU-95A" + ] + ], + "OFzFbFcl": [ + [ + "Elegoo", + "PC", + "Elegoo PC-FR" + ] + ], + "OFzQQeCi": [ + [ + "Generic", + "HIPS", + "Chuanying Generic HIPS" + ] + ], + "OFzb2BLs": [ + [ + "Generic", + "PETG", + "RatRig Generic PCTG" + ] + ], + "OFziUL2X": [ + [ + "Generic", + "PLA", + "Ginger Generic PLA" + ] + ], + "OFzrWlH5": [ + [ + "Elegoo", + "PETG", + "Elegoo PETG HF" + ] + ], + "OGFA00": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Basic" + ] + ], + "OGFA01": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Matte" + ] + ], + "OGFA02": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Metal" + ] + ], + "OGFA03": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Impact" + ] + ], + "OGFA05": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Silk" + ] + ], + "OGFA06": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Silk+" + ] + ], + "OGFA07": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Marble" + ] + ], + "OGFA08": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Sparkle" + ] + ], + "OGFA09": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Tough" + ] + ], + "OGFA11": [ + [ + "Bambu Lab", + "PLA-AERO", + "Bambu PLA Aero" + ] + ], + "OGFA12": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Glow" + ] + ], + "OGFA13": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Dynamic" + ] + ], + "OGFA15": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Galaxy" + ] + ], + "OGFA16": [ + [ + "Bambu Lab", + "PLA", + "Bambu PLA Wood" + ] + ], + "OGFA50": [ + [ + "Bambu Lab", + "PLA-CF", + "Bambu PLA-CF" + ] + ], + "OGFB00": [ + [ + "Bambu Lab", + "ABS", + "Bambu ABS" + ] + ], + "OGFB01": [ + [ + "Bambu Lab", + "ASA", + "Bambu ASA" + ] + ], + "OGFB02": [ + [ + "Bambu Lab", + "ASA-AERO", + "Bambu ASA-Aero" + ] + ], + "OGFB50": [ + [ + "Bambu Lab", + "ABS-GF", + "Bambu ABS-GF" + ] + ], + "OGFB51": [ + [ + "Bambu Lab", + "ASA-CF", + "Bambu ASA-CF" + ] + ], + "OGFB60": [ + [ + "Polymaker", + "ABS", + "PolyLite ABS" + ] + ], + "OGFB61": [ + [ + "Polymaker", + "ASA", + "PolyLite ASA" + ] + ], + "OGFB98": [ + [ + "Generic", + "ASA", + "Generic ASA" + ] + ], + "OGFB99": [ + [ + "Generic", + "ABS", + "Generic ABS" + ] + ], + "OGFC00": [ + [ + "Bambu Lab", + "PC", + "Bambu PC" + ] + ], + "OGFC01": [ + [ + "Bambu Lab", + "PC", + "Bambu PC FR" + ] + ], + "OGFC99": [ + [ + "Generic", + "PC", + "Generic PC" + ], + [ + "Generic", + "PC", + "fdm_filament_pc" + ] + ], + "OGFG00": [ + [ + "Bambu Lab", + "PETG", + "Bambu PETG Basic" + ] + ], + "OGFG01": [ + [ + "Bambu Lab", + "PETG", + "Bambu PETG Translucent" + ] + ], + "OGFG02": [ + [ + "Bambu Lab", + "PETG", + "Bambu PETG HF" + ] + ], + "OGFG50": [ + [ + "Bambu Lab", + "PETG-CF", + "Bambu PETG-CF" + ] + ], + "OGFG60": [ + [ + "Polymaker", + "PETG", + "PolyLite PETG" + ] + ], + "OGFG97": [ + [ + "Generic", + "PCTG", + "fdm_filament_pctg" + ] + ], + "OGFG99": [ + [ + "Generic", + "PETG", + "Generic PETG" + ], + [ + "Generic", + "PETG", + "fdm_filament_pet" + ] + ], + "OGFGG300": [ + [ + "GreenGate3D", + "PETG", + "GreenGate3D PETG" + ] + ], + "OGFL00": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA" + ] + ], + "OGFL00-1": [ + [ + "Polymaker", + "PLA", + "PolyTerra Dual PLA" + ] + ], + "OGFL01": [ + [ + "Polymaker", + "PLA", + "PolyTerra PLA" + ] + ], + "OGFL03": [ + [ + "eSUN", + "PLA", + "eSUN PLA+" + ] + ], + "OGFL04": [ + [ + "Overture", + "PLA", + "Overture PLA" + ] + ], + "OGFL05": [ + [ + "Overture", + "PLA", + "Overture Matte PLA" + ] + ], + "OGFL06": [ + [ + "Polymaker", + "PETG", + "Fiberon PETG-ESD" + ] + ], + "OGFL50": [ + [ + "Polymaker", + "PA6-CF", + "Fiberon PA6-CF" + ] + ], + "OGFL51": [ + [ + "Polymaker", + "PA-GF", + "Fiberon PA6-GF" + ] + ], + "OGFL52": [ + [ + "Polymaker", + "PA-CF", + "Fiberon PA12-CF" + ] + ], + "OGFL53": [ + [ + "Polymaker", + "PA", + "Fiberon PA612-CF" + ] + ], + "OGFL54": [ + [ + "Polymaker", + "PET-CF", + "Fiberon PET-CF" + ] + ], + "OGFL55": [ + [ + "Polymaker", + "PETG-CF", + "Fiberon PETG-rCF" + ] + ], + "OGFL95": [ + [ + "Generic", + "PLA", + "Generic PLA High Speed" + ] + ], + "OGFL96": [ + [ + "Generic", + "PLA", + "Generic PLA Silk" + ], + [ + "Generic", + "PLA", + "fdm_filament_pla_silk" + ] + ], + "OGFL98": [ + [ + "Generic", + "PLA-CF", + "Generic PLA-CF" + ] + ], + "OGFL99": [ + [ + "Generic", + "PLA", + "fdm_filament_pla" + ] + ], + "OGFLC99": [ + [ + "Generic", + "CoPE", + "fdm_filament_cope" + ] + ], + "OGFN03": [ + [ + "Bambu Lab", + "PA-CF", + "Bambu PA-CF" + ] + ], + "OGFN04": [ + [ + "Bambu Lab", + "PA-CF", + "Bambu PAHT-CF" + ] + ], + "OGFN05": [ + [ + "Bambu Lab", + "PA6-CF", + "Bambu PA6-CF" + ] + ], + "OGFN06": [ + [ + "Bambu Lab", + "PPA-CF", + "Bambu PPA-CF" + ] + ], + "OGFN08": [ + [ + "Bambu Lab", + "PA-GF", + "Bambu PA6-GF" + ] + ], + "OGFN96": [ + [ + "Generic", + "PPA-GF", + "Generic PPA-GF" + ] + ], + "OGFN97": [ + [ + "Generic", + "PPA-CF", + "fdm_filament_ppa" + ] + ], + "OGFN98": [ + [ + "Generic", + "PA-CF", + "Generic PA-CF" + ] + ], + "OGFN99": [ + [ + "Generic", + "PA", + "Generic PA" + ], + [ + "Generic", + "PA", + "fdm_filament_pa" + ] + ], + "OGFNMK00": [ + [ + "Numakers", + "PLA", + "Numakers PLA+" + ] + ], + "OGFP97": [ + [ + "Generic", + "PP", + "fdm_filament_pp" + ] + ], + "OGFP99": [ + [ + "Generic", + "PE", + "fdm_filament_pe" + ] + ], + "OGFPM001": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA" + ] + ], + "OGFPM002": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Matte" + ] + ], + "OGFPM003": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Marble" + ] + ], + "OGFPM004": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Silk" + ] + ], + "OGFPM005": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Satin" + ] + ], + "OGFPM006": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Translucent" + ] + ], + "OGFPM007": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Galaxy" + ] + ], + "OGFPM008": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Celestial" + ] + ], + "OGFPM009": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Starlight" + ] + ], + "OGFPM010": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Glow" + ] + ], + "OGFPM011": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Luminous" + ] + ], + "OGFPM012": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Metallic" + ] + ], + "OGFPM013": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Neon" + ] + ], + "OGFPM014": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA UV Shift" + ] + ], + "OGFPM015": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Temp Shift" + ] + ], + "OGFPM016": [ + [ + "Polymaker", + "PLA", + "Panchroma CoPE" + ] + ], + "OGFPM017": [ + [ + "Polymaker", + "PLA", + "Polymaker HT-PLA" + ] + ], + "OGFPM018": [ + [ + "Polymaker", + "PLA", + "Polymaker HT-PLA-GF" + ] + ], + "OGFPM019": [ + [ + "Polymaker", + "PLA", + "PolyLite PLA Pro" + ] + ], + "OGFR00": [ + [ + "FusRock", + "ABS-GF", + "FusRock ABS-GF" + ] + ], + "OGFR98": [ + [ + "Generic", + "PHA", + "fdm_filament_pha" + ] + ], + "OGFR99": [ + [ + "Generic", + "EVA", + "fdm_filament_eva" + ] + ], + "OGFS00": [ + [ + "Bambu Lab", + "PLA", + "Bambu Support W" + ] + ], + "OGFS01": [ + [ + "Bambu Lab", + "PA", + "Bambu Support G" + ] + ], + "OGFS02": [ + [ + "Bambu Lab", + "PLA", + "Bambu Support For PLA" + ] + ], + "OGFS03": [ + [ + "Bambu Lab", + "PA", + "Bambu Support For PA/PET" + ] + ], + "OGFS04": [ + [ + "Bambu Lab", + "PVA", + "Bambu PVA" + ] + ], + "OGFS05": [ + [ + "Bambu Lab", + "PLA", + "Bambu Support For PLA/PETG" + ] + ], + "OGFS06": [ + [ + "Bambu Lab", + "ABS", + "Bambu Support for ABS" + ] + ], + "OGFS97": [ + [ + "Generic", + "BVOH", + "fdm_filament_bvoh" + ] + ], + "OGFS98": [ + [ + "Generic", + "HIPS", + "fdm_filament_hips" + ] + ], + "OGFS99": [ + [ + "Generic", + "PVA", + "Generic PVA" + ] + ], + "OGFSNL02": [ + [ + "SUNLU", + "PLA", + "SUNLU PLA Matte" + ] + ], + "OGFSNL03": [ + [ + "SUNLU", + "PLA", + "SUNLU PLA+" + ] + ], + "OGFSNL04": [ + [ + "SUNLU", + "PLA", + "SUNLU PLA+ 2.0" + ] + ], + "OGFSNL05": [ + [ + "SUNLU", + "PLA", + "SUNLU Silk PLA+" + ] + ], + "OGFSNL06": [ + [ + "SUNLU", + "PLA", + "SUNLU PLA Marble" + ] + ], + "OGFSNL07": [ + [ + "SUNLU", + "PLA", + "SUNLU Wood PLA" + ] + ], + "OGFSNL08": [ + [ + "Generic", + "PETG", + "SUNLU PETG" + ], + [ + "SUNLU", + "PETG", + "SUNLU PETG" + ] + ], + "OGFT01": [ + [ + "Bambu Lab", + "PET-CF", + "Bambu PET-CF" + ] + ], + "OGFT02": [ + [ + "Bambu Lab", + "PPS-CF", + "Bambu PPS-CF" + ] + ], + "OGFU00": [ + [ + "Bambu Lab", + "TPU", + "Bambu TPU 95A HF" + ] + ], + "OGFU01": [ + [ + "Bambu Lab", + "TPU", + "Bambu TPU 95A" + ] + ], + "OGFU99": [ + [ + "Generic", + "TPU", + "Generic TPU" + ] + ], + "OVTABS08": [ + [ + "Overture", + "ABS", + "Overture ABS Basic" + ] + ], + "P1001": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Satin" + ] + ], + "P1002": [ + [ + "Polymaker", + "PLA", + "PolySonic PLA Pro" + ] + ], + "P1003": [ + [ + "Polymaker", + "PLA", + "Panchroma PLA Matte" + ] + ], + "P1004": [ + [ + "Polymaker", + "PLA", + "PolySonic PLA" + ] + ], + "P510cfa0": [ + [ + "Cubicon", + "PLA", + "Cubicon PLA" + ] + ], + "P510cfa1": [ + [ + "Cubicon", + "PLA", + "Cubicon PLA+" + ] + ], + "P510cfa2": [ + [ + "Cubicon", + "PLA", + "Cubicon PLAi21" + ] + ], + "P510cfb0": [ + [ + "Cubicon", + "ABS", + "Cubicon ABS" + ] + ], + "P510cfb1": [ + [ + "Cubicon", + "ABS", + "Cubicon ABS-A100" + ] + ], + "P510cfb2": [ + [ + "Cubicon", + "ABS", + "Cubicon ABSk" + ] + ], + "P510cfc0": [ + [ + "Cubicon", + "PETG", + "Cubicon PETG" + ] + ], + "P510cfd0": [ + [ + "Cubicon", + "PA", + "Cubicon PA-CF" + ] + ], + "P510eff9": [ + [ + "Generic", + "PETG", + "Ginger Generic PETG" + ] + ], + "Pfcf9c4c": [ + [ + "Artillery", + "PLA", + "Artillery PLA" + ] + ], + "Prusa Generic ABS @CORE One": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Prusa Generic ABS @CORE One 0.6": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Prusa Generic ABS @CORE One 0.8": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Prusa Generic ABS @CORE One HF 0.4": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Prusa Generic ABS @CORE One HF 0.5": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Prusa Generic ABS @CORE One HF 0.6": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Prusa Generic ABS @CORE One HF 0.8": [ + [ + "Generic", + "ABS", + "Prusa Generic ABS" + ] + ], + "Prusa Generic ASA @CORE One": [ + [ + "Prusa Polymers", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusa Generic ASA @CORE One 0.6": [ + [ + "Prusa Polymers", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusa Generic ASA @CORE One 0.8": [ + [ + "Prusa Polymers", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusa Generic ASA @CORE One HF 0.4": [ + [ + "Prusa Polymers", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusa Generic ASA @CORE One HF 0.5": [ + [ + "Prusa Polymers", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusa Generic ASA @CORE One HF 0.6": [ + [ + "Prusa Polymers", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusa Generic ASA @CORE One HF 0.8": [ + [ + "Prusa Polymers", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusa Generic PETG @CORE One": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Prusa Generic PETG @CORE One 0.6": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Prusa Generic PETG @CORE One 0.8": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Prusa Generic PETG @CORE One HF 0.4": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Prusa Generic PETG @CORE One HF 0.5": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Prusa Generic PETG @CORE One HF 0.6": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Prusa Generic PETG @CORE One HF 0.8": [ + [ + "Generic", + "PETG", + "Prusa Generic PETG" + ] + ], + "Prusa Generic PLA @CORE One": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Prusa Generic PLA @CORE One 0.6": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Prusa Generic PLA @CORE One 0.8": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Prusa Generic PLA @CORE One HF 0.4": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Prusa Generic PLA @CORE One HF 0.5": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Prusa Generic PLA @CORE One HF 0.6": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Prusa Generic PLA @CORE One HF 0.8": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA" + ] + ], + "Prusa Generic PLA Silk @CORE One": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA Silk" + ] + ], + "Prusa Generic PLA Silk @CORE One 0.6": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA Silk" + ] + ], + "Prusa Generic PLA Silk @CORE One 0.8": [ + [ + "Generic", + "PLA", + "Prusa Generic PLA Silk" + ] + ], + "Prusa Generic TPU @CORE One": [ + [ + "Generic", + "FLEX", + "Prusa Generic TPU" + ] + ], + "Prusa Generic TPU @CORE One 0.6": [ + [ + "Generic", + "FLEX", + "Prusa Generic TPU" + ] + ], + "Prusa Generic TPU @CORE One 0.8": [ + [ + "Generic", + "FLEX", + "Prusa Generic TPU" + ] + ], + "Prusament ASA @CORE One": [ + [ + "Prusa Polymers", + "ASA", + "Prusament ASA" + ] + ], + "Prusament ASA @CORE One 0.6": [ + [ + "Prusa Polymers", + "ASA", + "Prusament ASA" + ] + ], + "Prusament ASA @CORE One 0.8": [ + [ + "Prusa Polymers", + "ASA", + "Prusament ASA" + ] + ], + "Prusament ASA @CORE One HF 0.4": [ + [ + "Prusa Polymers", + "ASA", + "Prusament ASA" + ] + ], + "Prusament ASA @CORE One HF 0.5": [ + [ + "Prusa Polymers", + "ASA", + "Prusament ASA" + ] + ], + "Prusament ASA @CORE One HF 0.6": [ + [ + "Prusa Polymers", + "ASA", + "Prusament ASA" + ] + ], + "Prusament ASA @CORE One HF 0.8": [ + [ + "Prusa Polymers", + "ASA", + "Prusament ASA" + ] + ], + "Prusament ASA @MK4S": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusament ASA @MK4S 0.6": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusament ASA @MK4S 0.8": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusament ASA @MK4S HF0.4": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusament ASA @MK4S HF0.5": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusament ASA @MK4S HF0.6": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusament ASA @MK4S HF0.8": [ + [ + "Generic", + "ASA", + "Prusa Generic ASA" + ] + ], + "Prusament PA-CF @CORE One": [ + [ + "Prusa Polymers", + "PA", + "Prusament PA-CF" + ] + ], + "Prusament PA-CF @CORE One 0.6": [ + [ + "Prusa Polymers", + "PA", + "Prusament PA-CF" + ] + ], + "Prusament PA-CF @CORE One 0.8": [ + [ + "Prusa Polymers", + "PA", + "Prusament PA-CF" + ] + ], + "Prusament PC Blend @CORE One": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC Blend" + ] + ], + "Prusament PC Blend @CORE One 0.6": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC Blend" + ] + ], + "Prusament PC Blend @CORE One 0.8": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC Blend" + ] + ], + "Prusament PC Blend @CORE One HF 0.4": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC Blend" + ] + ], + "Prusament PC Blend @CORE One HF 0.5": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC Blend" + ] + ], + "Prusament PC Blend @CORE One HF 0.6": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC Blend" + ] + ], + "Prusament PC Blend @CORE One HF 0.8": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC Blend" + ] + ], + "Prusament PC-CF @CORE One": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC-CF" + ] + ], + "Prusament PC-CF @CORE One 0.6": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC-CF" + ] + ], + "Prusament PC-CF @CORE One 0.8": [ + [ + "Prusa Polymers", + "PC", + "Prusament PC-CF" + ] + ], + "Prusament PETG @CORE One": [ + [ + "Prusa Polymers", + "PETG", + "Prusament PETG" + ] + ], + "Prusament PETG @CORE One 0.6": [ + [ + "Prusa Polymers", + "PETG", + "Prusament PETG" + ] + ], + "Prusament PETG @CORE One 0.8": [ + [ + "Prusa Polymers", + "PETG", + "Prusament PETG" + ] + ], + "Prusament PETG @CORE One HF 0.4": [ + [ + "Prusa Polymers", + "PETG", + "Prusament PETG" + ] + ], + "Prusament PETG @CORE One HF 0.5": [ + [ + "Prusa Polymers", + "PETG", + "Prusament PETG" + ] + ], + "Prusament PETG @CORE One HF 0.6": [ + [ + "Prusa Polymers", + "PETG", + "Prusament PETG" + ] + ], + "Prusament PETG @CORE One HF 0.8": [ + [ + "Prusa Polymers", + "PETG", + "Prusament PETG" + ] + ], + "Prusament PLA @CORE One": [ + [ + "Prusa Polymers", + "PLA", + "Prusament PLA" + ] + ], + "Prusament PLA @CORE One 0.6": [ + [ + "Prusa Polymers", + "PLA", + "Prusament PLA" + ] + ], + "Prusament PLA @CORE One 0.8": [ + [ + "Prusa Polymers", + "PLA", + "Prusament PLA" + ] + ], + "Prusament PLA @CORE One HF 0.4": [ + [ + "Prusa Polymers", + "PLA", + "Prusament PLA" + ] + ], + "Prusament PLA @CORE One HF 0.5": [ + [ + "Prusa Polymers", + "PLA", + "Prusament PLA" + ] + ], + "Prusament PLA @CORE One HF 0.6": [ + [ + "Prusa Polymers", + "PLA", + "Prusament PLA" + ] + ], + "Prusament PLA @CORE One HF 0.8": [ + [ + "Prusa Polymers", + "PLA", + "Prusament PLA" + ] + ], + "Prusament PVB @CORE One": [ + [ + "Prusa Polymers", + "PVB", + "Prusament PVB" + ] + ], + "Prusament PVB @CORE One 0.6": [ + [ + "Prusa Polymers", + "PVB", + "Prusament PVB" + ] + ], + "Prusament PVB @CORE One 0.8": [ + [ + "Prusa Polymers", + "PVB", + "Prusament PVB" + ] + ], + "Prusament rPLA @CORE One": [ + [ + "Prusa Polymers", + "PLA", + "Prusament rPLA" + ] + ], + "Prusament rPLA @CORE One 0.6": [ + [ + "Prusa Polymers", + "PLA", + "Prusament rPLA" + ] + ], + "Prusament rPLA @CORE One 0.8": [ + [ + "Prusa Polymers", + "PLA", + "Prusament rPLA" + ] + ], + "SMCFB001": [ + [ + "SeeMeCNC", + "ABS", + "SeeMeCNC ABS" + ] + ], + "SMCFG001": [ + [ + "SeeMeCNC", + "PETG", + "SeeMeCNC PETG" + ] + ], + "SMCFG002": [ + [ + "SeeMeCNC", + "PETG-CF", + "SeeMeCNC PETG-CF" + ] + ], + "SMCFL001": [ + [ + "SeeMeCNC", + "PLA", + "SeeMeCNC PLA" + ] + ], + "SMCFN001": [ + [ + "SeeMeCNC", + "PA-CF", + "SeeMeCNC PA-CF" + ] + ], + "SMCFU001": [ + [ + "SeeMeCNC", + "TPU", + "SeeMeCNC TPU" + ] + ], + "VLMNT01": [ + [ + "Valment", + "PLA", + "Valment PLA" + ] + ], + "VLMNT02": [ + [ + "Valment", + "PLA", + "Valment PLA Silk" + ] + ], + "VLMNT03": [ + [ + "Valment", + "PLA-CF", + "Valment PLA-CF" + ] + ], + "VLMNT04": [ + [ + "Valment", + "PLA", + "Valment PLA Galaxy" + ] + ] + } } diff --git a/scripts/retired_filament_ids.json b/scripts/retired_filament_ids.json deleted file mode 100644 index d71f89d9ac..0000000000 --- a/scripts/retired_filament_ids.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "retired": { - "GFABS": [ - "Anycubic/Anycubic ABS" - ], - "GFL92": [ - "Anycubic/Anycubic PLA" - ], - "GFL93": [ - "Anycubic/Anycubic PLA+" - ], - "GFPLA": [ - "Anycubic/Anycubic PLA" - ], - "GFPLA High Speed": [ - "Anycubic/Anycubic PLA High Speed" - ], - "GFPLA Matte": [ - "Anycubic/Anycubic PLA Matte" - ], - "GFPLA Silk": [ - "Anycubic/Anycubic PLA Silk" - ], - "GFPLA+": [ - "Anycubic/Anycubic PLA+" - ], - "P11851ba": [ - "Artillery/Artillery PET" - ], - "P284941e": [ - "Artillery/Artillery PETG", - "Artillery/Artillery PETG Basic" - ], - "P2fbf0c0": [ - "Artillery/Artillery TPU" - ], - "P39db358": [ - "Artillery/Artillery ABS" - ], - "P8163162": [ - "Artillery/Artillery PVA" - ], - "P87436f6": [ - "Artillery/Artillery ASA" - ] - } -} diff --git a/scripts/test_moonraker_lane_data.py b/scripts/test_moonraker_lane_data.py index 44a0a7e30e..16ed4924e0 100644 --- a/scripts/test_moonraker_lane_data.py +++ b/scripts/test_moonraker_lane_data.py @@ -5,10 +5,15 @@ Inserts/deletes/modifies random lane data in Moonraker database, then reads back and displays with colored output. """ -import requests +try: + import requests +except ImportError: + requests = None # only needed for live-printer operations, not --check-ofl-map import random import argparse import json +import os +import re import time import sys @@ -16,6 +21,11 @@ import sys DEFAULT_HOST = "192.168.88.9" DEFAULT_PORT = 7125 NAMESPACE = "lane_data" + +# Repo-relative paths for the offline generic-map check +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MOONRAKER_AGENT_CPP = os.path.join(REPO_ROOT, "src", "slic3r", "Utils", "MoonrakerPrinterAgent.cpp") +OFL_FILAMENT_DIR = os.path.join(REPO_ROOT, "resources", "profiles", "OrcaFilamentLibrary", "filament") LANE_KEYS = [f"lane{i}" for i in range(1, 9)] # lane1-lane8 MATERIALS = ["PLA", "ABS", "PETG", "ASA", "ASA Sparkle", "TPU", ""] @@ -30,6 +40,95 @@ MATERIAL_TEMPS = { "": {"nozzle": None, "bed": None}, } +def parse_cpp_type_map(): + """Extract the normalized-type -> OFL generic family table from MoonrakerPrinterAgent.cpp. + + Reads MoonrakerPrinterAgent::map_filament_type_to_generic_id's type_to_ofl_family + initializer so the check tracks the C++ normalization without a duplicated list. + """ + with open(MOONRAKER_AGENT_CPP, encoding="utf-8") as f: + src = f.read() + m = re.search(r"type_to_ofl_family\s*=\s*\{(.*?)\n\s*\};", src, re.DOTALL) + if not m: + raise RuntimeError(f"type_to_ofl_family table not found in {MOONRAKER_AGENT_CPP}") + pairs = re.findall(r'\{\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\}', m.group(1)) + if not pairs: + raise RuntimeError("type_to_ofl_family table parsed empty") + return dict(pairs) + +def load_ofl_presets(): + """Map preset name -> parsed JSON for every OrcaFilamentLibrary filament profile.""" + presets = {} + for root, _dirs, files in os.walk(OFL_FILAMENT_DIR): + for fn in files: + if not fn.endswith(".json"): + continue + try: + with open(os.path.join(root, fn), encoding="utf-8") as f: + data = json.load(f) + except (OSError, ValueError): + continue + name = data.get("name") + if isinstance(name, str) and name: + presets[name] = data + return presets + +def resolve_ofl_filament_id(presets, name): + """Follow the inherits chain (within OFL) until a filament_id is declared.""" + seen = set() + while name and name not in seen: + seen.add(name) + preset = presets.get(name) + if preset is None: + return None + fid = preset.get("filament_id") + if fid: + return fid + name = preset.get("inherits") + return None + +def check_ofl_generic_map(): + """Assert every material type the C++ normalization handles resolves to a shipped + OrcaFilamentLibrary generic preset carrying a filament_id. + + Expectations are derived from the shipped profiles, not pinned id literals, so the + check stays valid across filament_id re-mints. + """ + print("Checking C++ generic-type map against shipped OrcaFilamentLibrary presets...") + try: + type_map = parse_cpp_type_map() + except (OSError, RuntimeError) as e: + print(f" FAIL: {e}") + return False + presets = load_ofl_presets() + if not presets: + print(f" FAIL: no OFL filament profiles found under {OFL_FILAMENT_DIR}") + return False + errors = [] + for family in sorted(set(type_map.values())): + preset_name = f"Generic {family} @System" + if preset_name not in presets: + errors.append(f"{preset_name}: no such OFL preset") + continue + if str(presets[preset_name].get("instantiation", "")).lower() != "true": + errors.append(f"{preset_name}: not instantiated — the C++ runtime lookup " + f"only sees presets loaded into the PresetBundle") + continue + fid = resolve_ofl_filament_id(presets, preset_name) + if not fid: + errors.append(f"{preset_name}: no filament_id resolvable through inherits") + continue + aliases = ", ".join(sorted(t for t, fam in type_map.items() if fam == family)) + print(f" {fid:10s} {preset_name:32s} <- {aliases}") + if errors: + for e in errors: + print(f" FAIL: {e}") + print(f"OFL generic map check FAILED ({len(errors)} error(s))") + return False + print(f"OFL generic map check passed: {len(type_map)} type aliases, " + f"{len(set(type_map.values()))} OFL generic presets\n") + return True + def test_connection(host, port, api_key=None, verbose=False): """Test basic connectivity to Moonraker.""" url = f"http://{host}:{port}/server/info" @@ -404,11 +503,25 @@ def main(): help="Only read and display current lane data") parser.add_argument("--load", metavar="FILE", help="Load lane data from JSON file and overwrite printer lanes") + parser.add_argument("--check-ofl-map", action="store_true", + help="Only run the offline check that the C++ generic-type map " + "resolves against shipped OrcaFilamentLibrary presets") parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output for debugging") args = parser.parse_args() + # Offline check first: the C++ type normalization must resolve against shipped + # OFL presets (no printer needed). + if not check_ofl_generic_map(): + return 1 + if args.check_ofl_map: + return 0 + + if requests is None: + print("The 'requests' module is required for live printer operations (pip install requests).") + return 1 + print(f"\nConnecting to Moonraker at {args.host}:{args.port}...") # First test basic connectivity diff --git a/scripts/tests/test_filament_id.py b/scripts/tests/test_filament_id.py index 14d7d439b7..d60377f51e 100644 --- a/scripts/tests/test_filament_id.py +++ b/scripts/tests/test_filament_id.py @@ -34,12 +34,18 @@ def load_json_file(path): # --------------------------------------------------------------------------- def preset(name, filament_id=None, inherits=None, instantiation=True, - compatible_printers=None): + compatible_printers=None, filament_vendor=None, filament_type=None): data = {"type": "filament", "name": name} if inherits is not None: data["inherits"] = inherits if filament_id is not None: data["filament_id"] = filament_id + if filament_vendor is not None: + data["filament_vendor"] = ( + [filament_vendor] if isinstance(filament_vendor, str) else filament_vendor) + if filament_type is not None: + data["filament_type"] = ( + [filament_type] if isinstance(filament_type, str) else filament_type) data["instantiation"] = "true" if instantiation else "false" if compatible_printers is not None: data["compatible_printers"] = compatible_printers @@ -102,13 +108,18 @@ class SyntheticTree: with open(idx_path, "w", encoding="utf-8") as f: json.dump(index, f, indent=4, ensure_ascii=False) + def write_ledger(self, retired=None, hints=None): + afi.write_ledger(self.retired, {"retired": retired or {}, + "hints": hints or {}}) + # -- pipeline wrappers --------------------------------------------------- - def update_snapshot(self, allow_shared_catalog=False): + def update_snapshot(self, allow_shared_catalog=False, forget_path=None): buf = io.StringIO() with contextlib.redirect_stdout(buf): rc = afi.update_snapshot(self.profiles, self.snapshot, self.retired, - allow_shared_catalog=allow_shared_catalog) + allow_shared_catalog=allow_shared_catalog, + forget_path=forget_path) return rc, buf.getvalue() def check(self): @@ -124,17 +135,37 @@ class SyntheticTree: self.retired) return changed, errors, buf.getvalue() + def remint(self, vendors): + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + changed, errors = afi.remint_vendors(vendors, self.profiles, self.retired) + return changed, errors, buf.getvalue() + + def drop_redundant(self, vendor): + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + dropped, errors = afi.drop_redundant_ids(vendor, self.profiles) + return dropped, errors, buf.getvalue() + + def add_hint(self, pairs): + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + rc = afi.add_hints(pairs, self.profiles, self.retired) + return rc, buf.getvalue() + def make_clean_tree(): """Baseline tree: OFL base+generic, a vendor family, a clean tuned generic.""" t = SyntheticTree() t.add_vendor(OFL, [ - preset("fdm_pla", filament_id="OGFL99", instantiation=False), + preset("fdm_pla", filament_id="OGFL99", instantiation=False, + filament_vendor="Generic", filament_type="PLA"), preset("Generic PLA @System", inherits="fdm_pla", compatible_printers=[]), ]) t.add_vendor("VendorA", [ - preset("APLA @base", filament_id="AX01", instantiation=False), + preset("APLA @base", filament_id="AX01", instantiation=False, + filament_vendor="AVendor", filament_type="PLA"), preset("APLA @P1", inherits="APLA @base", compatible_printers=["P1 0.4 nozzle"]), # Correctly tuned OFL generic: keeps the OFL base name, claims a printer. @@ -163,33 +194,45 @@ class TestMint(unittest.TestCase): uuid.UUID("c4d3ff49-4c32-5534-a3e3-00894157ab97")) def test_known_vector(self): - # Hardcoded, independently computed vector: freezes prefix, input string - # layout ("filament_family//") and base62 derivation. - self.assertEqual(afi.generate_filament_id("Qidi", "HATCHBOX PLA"), "OFiJYDPC") + # Hardcoded, independently computed vectors: freeze prefix, input string + # layout ("filament_product///") and base62 tail. + self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA"), + "OF5CgdDq") + self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA", + salt=1), "OFD9mV8H") + self.assertEqual(afi.generate_filament_id("Generic", "PLA", "Generic PLA"), + "OFDSrzZ8") def test_determinism_and_format(self): - for vendor, family in [("Qidi", "HATCHBOX PLA"), ("Creality", "CR PLA"), - ("OrcaFilamentLibrary", "Generic ABS"), - ("BBL", "拓竹 PLA")]: # unicode family - a = afi.generate_filament_id(vendor, family) - b = afi.generate_filament_id(vendor, family) + for triple in [("Polymaker", "PLA", "PolyLite PLA"), + ("Creality", "PLA", "CR PLA"), + ("Generic", "ABS", "Generic ABS"), + ("拓竹", "PLA", "拓竹 PLA")]: # unicode components + a = afi.generate_filament_id(*triple) + b = afi.generate_filament_id(*triple) self.assertEqual(a, b) self.assertRegex(a, r"^OF[0-9A-Za-z]{6}$") self.assertEqual(len(a), 8) + def test_every_triple_component_changes_the_id(self): + base = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA") + self.assertNotEqual(base, afi.generate_filament_id("Other", "PLA", "PolyLite PLA")) + self.assertNotEqual(base, afi.generate_filament_id("Polymaker", "PETG", "PolyLite PLA")) + self.assertNotEqual(base, afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA Pro")) + def test_salt_changes_id(self): - base = afi.generate_filament_id("Qidi", "HATCHBOX PLA") - salted = afi.generate_filament_id("Qidi", "HATCHBOX PLA", salt=1) + base = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA") + salted = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA", salt=1) self.assertNotEqual(base, salted) self.assertRegex(salted, r"^OF[0-9A-Za-z]{6}$") def test_mint_salt_iteration(self): - v, fam = "Qidi", "HATCHBOX PLA" - taken = {afi.generate_filament_id(v, fam, s) for s in range(2)} - self.assertEqual(afi.mint_filament_id(v, fam, taken), - afi.generate_filament_id(v, fam, salt=2)) - self.assertEqual(afi.mint_filament_id(v, fam, set()), - afi.generate_filament_id(v, fam)) + triple = ("Polymaker", "PLA", "PolyLite PLA") + taken = {afi.generate_filament_id(*triple, salt=s) for s in range(2)} + self.assertEqual(afi.mint_filament_id(*triple, taken), + afi.generate_filament_id(*triple, salt=2)) + self.assertEqual(afi.mint_filament_id(*triple, set()), + afi.generate_filament_id(*triple)) class TestBaseName(unittest.TestCase): @@ -278,8 +321,74 @@ class TestResolver(unittest.TestCase): self.assertEqual(fid, "PARENT") +class TestTripleResolution(unittest.TestCase): + @staticmethod + def rec(name, inherits=None, filament_vendor=None, filament_type=None): + return {"name": name, "inherits": inherits, + "filament_vendor": filament_vendor, "filament_type": filament_type} + + def field(self, name, vendor_recs, ofl_recs, field="filament_vendor"): + fmap = {r["name"]: r for r in vendor_recs} + omap = {r["name"]: r for r in ofl_recs} + return afi.resolve_filament_field(name, field, fmap, omap) + + def test_own_value_first_element_of_list(self): + got = self.field("A", [self.rec("A", filament_vendor=["Poly", "Ignored"])], []) + self.assertEqual(got, "Poly") + + def test_plain_string_value_tolerated(self): + got = self.field("A", [self.rec("A", filament_vendor="Poly")], []) + self.assertEqual(got, "Poly") + + def test_inherited_within_vendor(self): + got = self.field( + "A", [self.rec("A", inherits="B"), self.rec("B", inherits="C"), + self.rec("C", filament_vendor=["Poly"])], []) + self.assertEqual(got, "Poly") + + def test_empty_list_keeps_walking(self): + got = self.field( + "A", [self.rec("A", inherits="B", filament_vendor=[]), + self.rec("B", filament_vendor=["Poly"])], []) + self.assertEqual(got, "Poly") + + def test_ofl_fallback(self): + got = self.field( + "A", [self.rec("A", inherits="Generic PLA @System")], + [self.rec("Generic PLA @System", inherits="fdm_pla"), + self.rec("fdm_pla", filament_vendor=["Generic"])]) + self.assertEqual(got, "Generic") + + def test_ofl_stays_in_ofl(self): + got = self.field( + "A", + [self.rec("A", inherits="ofl_entry"), + self.rec("fdm_pla", filament_vendor=["WRONG"])], + [self.rec("ofl_entry", inherits="fdm_pla"), + self.rec("fdm_pla", filament_vendor=["RIGHT"])]) + self.assertEqual(got, "RIGHT") + + def test_dead_end_retries_parent_in_ofl(self): + got = self.field( + "A", [self.rec("A", inherits="shared"), self.rec("shared")], + [self.rec("shared", filament_vendor=["Poly"])]) + self.assertEqual(got, "Poly") + + def test_missing_is_empty(self): + self.assertEqual(self.field("A", [self.rec("A")], []), "") + self.assertEqual(self.field("A", [self.rec("A", inherits="nope")], []), "") + + def test_resolve_triple(self): + recs = [self.rec("MyPLA @base", filament_vendor=["MyVendor"], + filament_type=["PLA"]), + self.rec("MyPLA @P1", inherits="MyPLA @base")] + fmap = {r["name"]: r for r in recs} + self.assertEqual(afi.resolve_triple("MyPLA @P1", fmap, {}), + ("MyVendor", "PLA", "MyPLA")) + + # --------------------------------------------------------------------------- -# reserved namespaces +# reserved namespaces / islands # --------------------------------------------------------------------------- class TestReservedSpaces(unittest.TestCase): @@ -289,9 +398,16 @@ class TestReservedSpaces(unittest.TestCase): self.assertEqual(afi.reserved_space_owner("P1234abc"), (True, None)) self.assertEqual(afi.reserved_space_owner("pAbCdEf1"), (True, None)) # case-insensitive self.assertEqual(afi.reserved_space_owner("null"), (True, None)) - self.assertEqual(afi.reserved_space_owner("OFiJYDPC"), (False, None)) + self.assertEqual(afi.reserved_space_owner("OF5CgdDq"), (False, None)) self.assertEqual(afi.reserved_space_owner("P1234abcd"), (False, None)) # 8 hex chars: not the user space + def test_island_declarations(self): + self.assertTrue(afi.is_island_declaration("BBL", "GFL99")) + self.assertTrue(afi.is_island_declaration("BBL", "OF5CgdDq")) + self.assertTrue(afi.is_island_declaration("Qidi", "QD_X4_PLA")) + self.assertFalse(afi.is_island_declaration("Qidi", "OF5CgdDq")) + self.assertFalse(afi.is_island_declaration("VendorA", "GFL99")) + # --------------------------------------------------------------------------- # checks on synthetic trees @@ -302,10 +418,12 @@ class TestChecks(SyntheticTreeCase): errors, out = self.t.check() self.assertEqual(errors, 0, out) self.assertNotIn("[ERROR]", out) + self.assertNotIn("[WARNING]", out) def test_check1_unknown_non_of_id(self): self.t.write_preset("VendorA", preset("BPLA @base", filament_id="BOGUS_9", - instantiation=False)) + instantiation=False, + filament_vendor="BV", filament_type="PLA")) self.t.write_preset("VendorA", preset("BPLA @P1", inherits="BPLA @base", compatible_printers=["P1"])) errors, out = self.t.check() @@ -328,27 +446,57 @@ class TestChecks(SyntheticTreeCase): self.assertIn("stability", out) self.assertIn('"VendorA/APLA"', out) - def test_check3_of_id_must_match_mint(self): + def test_check2_triple_change_needs_snapshot_update(self): + self.t.write_preset("VendorA", preset("APLA @base", filament_id="AX01", + instantiation=False, + filament_vendor="AVendor", + filament_type="PETG"), + register=False) + errors, out = self.t.check() + self.assertGreater(errors, 0) + self.assertIn('triple "AVendor/PETG/APLA" is not sanctioned', out) + self.assertIn('triple stability: snapshot triple "AVendor/PLA/APLA"', out) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + + def test_check3_of_id_must_match_triple_mint(self): self.t.write_preset("VendorA", preset("BNEW @base", filament_id="OFZZZZZZ", - instantiation=False)) + instantiation=False, + filament_vendor="BV", filament_type="PLA")) self.t.write_preset("VendorA", preset("BNEW @P1", inherits="BNEW @base", compatible_printers=["P1"])) errors, out = self.t.check() self.assertGreater(errors, 0) - self.assertIn("does not match its mint", out) - self.assertIn(afi.generate_filament_id("VendorA", "BNEW"), out) + self.assertIn("does not match the mint of its triple", out) + self.assertIn(afi.generate_filament_id("BV", "PLA", "BNEW"), out) def test_check3_salted_mint_is_accepted(self): - salted = afi.generate_filament_id("VendorA", "BNEW", salt=3) + salted = afi.generate_filament_id("BV", "PLA", "BNEW", salt=3) self.t.write_preset("VendorA", preset("BNEW @base", filament_id=salted, - instantiation=False)) + instantiation=False, + filament_vendor="BV", filament_type="PLA")) self.t.write_preset("VendorA", preset("BNEW @P1", inherits="BNEW @base", compatible_printers=["P1"])) _errors, out = self.t.check() # check 2 still wants a snapshot update - self.assertNotIn("does not match its mint", out) + self.assertNotIn("does not match the mint", out) + + def test_check3_grandfathered_id_triple_pair(self): + # A non-conforming (id, triple) pair sanctioned by the snapshot passes. + self.t.write_preset("VendorA", preset("CNEW @base", filament_id="OFZZZZZZ", + instantiation=False, + filament_vendor="CV", filament_type="PLA")) + self.t.write_preset("VendorA", preset("CNEW @P1", inherits="CNEW @base", + compatible_printers=["P1"])) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) def test_check4_retired_id_reuse(self): - afi.write_ledger(self.t.retired, {"retired": {"AX01": ["VendorA/APLA"]}}) + self.t.write_ledger(retired={"AX01": {"claims": ["VendorA/APLA"], + "successor": None}}) errors, out = self.t.check() self.assertGreater(errors, 0) self.assertIn("retired", out) @@ -371,6 +519,37 @@ class TestChecks(SyntheticTreeCase): self.assertGreater(errors, 0) self.assertIn("cannot shadow the OFL preset", out) + def test_check5_own_id_key_on_ofl_rider(self): + # A vendor root that declares its own id while inheriting an OFL family + # forks the family off the catalog id. + self.t.write_preset("VendorA", preset("GPLA @base", filament_id="AX77", + instantiation=False, + inherits="Generic PLA @System")) + self.t.write_preset("VendorA", preset("GPLA @P1", inherits="GPLA @base", + compatible_printers=["P1"])) + errors, out = self.t.check() + self.assertGreater(errors, 0) + self.assertIn('declares its own filament_id "AX77"', out) + + def test_check5_applies_to_non_generic_ofl_families(self): + fid = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA") + self.t.write_preset(OFL, preset("poly_pla_base", filament_id=fid, + instantiation=False, + filament_vendor="Polymaker", + filament_type="PLA")) + self.t.write_preset(OFL, preset("PolyLite PLA @System", + inherits="poly_pla_base", + compatible_printers=[])) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + self.t.write_preset("VendorA", preset("PolyX PLA @P1", + inherits="PolyLite PLA @System", + compatible_printers=["P1"])) + errors, out = self.t.check() + self.assertGreater(errors, 0) + self.assertIn("rename re-exposes the OFL preset", out) + self.assertIn("PolyX PLA @P1", out) + def test_check5_exception_is_grandfathered(self): self.t.write_preset("VendorA", preset("Tuned PLA @P1", inherits="Generic PLA @System", @@ -388,7 +567,9 @@ class TestChecks(SyntheticTreeCase): with self.subTest(fid=fid): name = f"R{fid} @base" self.t.write_preset("VendorA", preset(name, filament_id=fid, - instantiation=False)) + instantiation=False, + filament_vendor="RV", + filament_type="PLA")) self.t.write_preset("VendorA", preset(f"R{fid} @P1", inherits=name, compatible_printers=["P1"])) errors, out = self.t.check() @@ -428,6 +609,184 @@ class TestChecks(SyntheticTreeCase): self.assertIn("snapshot not found", out) +class TestCheck8(SyntheticTreeCase): + def test_8a_empty_vendor_is_hard_error(self): + fid = afi.generate_filament_id("", "PLA", "NVPLA") + self.t.write_preset("VendorA", preset("NVPLA @base", filament_id=fid, + instantiation=False, + filament_type="PLA")) + self.t.write_preset("VendorA", preset("NVPLA @P1", inherits="NVPLA @base", + compatible_printers=["P1"])) + errors, out = self.t.check() + self.assertGreater(errors, 0) + self.assertIn("resolves empty filament_vendor", out) + self.assertIn('filament_vendor "Generic"', out) + # No grandfathering: sanctioning the tree does not silence check 8a. + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + errors, out = self.t.check() + self.assertEqual(errors, 1, out) + self.assertIn("resolves empty filament_vendor", out) + + def test_8b_divergent_family_triples(self): + id1 = afi.generate_filament_id("MV", "PLA", "MPLA") + id2 = afi.generate_filament_id("MV", "PETG", "MPLA") + self.t.write_preset("VendorA", preset("MPLA @base1", filament_id=id1, + instantiation=False, + filament_vendor="MV", + filament_type="PLA")) + self.t.write_preset("VendorA", preset("MPLA @base2", filament_id=id2, + instantiation=False, + filament_vendor="MV", + filament_type="PETG")) + errors, out = self.t.check() + self.assertGreater(errors, 0) + self.assertIn("divergent triples", out) + self.assertIn("MV/PLA/MPLA", out) + self.assertIn("MV/PETG/MPLA", out) + # ... until --update-snapshot grandfathers the family in triple_exceptions. + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + self.assertIn("VendorA/MPLA", load_json_file(self.t.snapshot)["triple_exceptions"]) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + + def test_8_cross_bundle_divergence_is_warning_only(self): + fid = afi.generate_filament_id("BV", "PETG", "APLA") + self.t.add_vendor("VendorB", [ + preset("APLA @base", filament_id=fid, instantiation=False, + filament_vendor="BV", filament_type="PETG"), + preset("APLA @PB", inherits="APLA @base", + compatible_printers=["PB 0.4 nozzle"]), + ]) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + self.assertIn("[WARNING]", out) + self.assertIn("across bundles", out) + self.assertIn('"APLA"', out) + + +class TestCheck9(SyntheticTreeCase): + def test_ok_chain_null_and_live(self): + self.t.write_ledger(retired={ + "R1": {"claims": ["VendorA/Gone1"], "successor": "AX01"}, + "R2": {"claims": ["VendorA/Gone2"], "successor": "R1"}, + "R3": {"claims": ["VendorA/Gone3"], "successor": None}, + }) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + + def test_cycle_is_an_error(self): + self.t.write_ledger(retired={ + "C1": {"claims": [], "successor": "C2"}, + "C2": {"claims": [], "successor": "C1"}, + }) + errors, out = self.t.check() + self.assertEqual(errors, 2, out) + self.assertIn("cycles", out) + + def test_dead_end_is_an_error(self): + self.t.write_ledger(retired={ + "D1": {"claims": [], "successor": "OFnope99"}, + }) + errors, out = self.t.check() + self.assertEqual(errors, 1, out) + self.assertIn("dead-ends", out) + self.assertIn("OFnope99", out) + + def test_hint_key_must_be_in_island_space(self): + self.t.write_ledger(hints={"AX01": "OGFL99"}) + errors, out = self.t.check() + self.assertEqual(errors, 1, out) + self.assertIn("not in an island id space", out) + + def test_hint_key_may_be_absent_island_id(self): + # Released / never-shipped island-catalog ids are valid hint keys. + self.t.write_ledger(hints={"GFXX99": "AX01"}) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + + def test_hint_key_must_not_be_retired_too(self): + self.t.write_ledger(retired={"GFXX99": {"claims": [], "successor": "AX01"}}, + hints={"GFXX99": "AX01"}) + errors, out = self.t.check() + self.assertEqual(errors, 1, out) + self.assertIn("never both", out) + + def test_live_hint_key_with_non_island_declarer_is_an_error(self): + self.t.write_preset("VendorA", preset( + "GPLA @base", filament_id="GFZZ01", instantiation=False, + filament_vendor="Generic", filament_type="PLA")) + self.t.write_preset("VendorA", preset( + "GPLA @P1", inherits="GPLA @base", + compatible_printers=["P1 0.4 nozzle"])) + rc, _out = self.t.update_snapshot(allow_shared_catalog=True) + self.assertEqual(rc, 0) + self.t.write_ledger(hints={"GFZZ01": "AX01"}) + errors, out = self.t.check() + self.assertEqual(errors, 1, out) + self.assertIn("re-mint those declarers first", out) + + def test_island_hint_ok_and_value_must_chain_live(self): + self.t.add_vendor("BBL", [ + preset("BPLA @base", filament_id="GFB01", instantiation=False), + preset("BPLA @X1C", inherits="BPLA @base", + compatible_printers=["X1C 0.4 nozzle"]), + ]) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + self.t.write_ledger(hints={"GFB01": "AX01"}) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + # A hint chain ending on a null retirement is dead for the runtime. + self.t.write_ledger(retired={"R9": {"claims": [], "successor": None}}, + hints={"GFB01": "R9"}) + errors, out = self.t.check() + self.assertEqual(errors, 1, out) + self.assertIn("chain-terminate at a live tree id", out) + + +# --------------------------------------------------------------------------- +# successor mode rule + ledger schema migration +# --------------------------------------------------------------------------- + +class TestSuccessorMode(unittest.TestCase): + def test_majority_wins(self): + claim_ids = {"V/A": {"ID1"}, "V/B": {"ID1"}, "V/C": {"ID2"}} + succ, counts = afi.successor_by_mode(["V/A", "V/B", "V/C"], claim_ids) + self.assertEqual(succ, "ID1") + self.assertEqual(counts, {"ID1": 2, "ID2": 1}) + + def test_tie_breaks_lexicographically(self): + claim_ids = {"V/A": {"IDB"}, "V/B": {"IDA"}} + succ, counts = afi.successor_by_mode(["V/A", "V/B"], claim_ids) + self.assertEqual(succ, "IDA") + self.assertEqual(counts, {"IDA": 1, "IDB": 1}) + + def test_no_surviving_family_means_null(self): + self.assertEqual(afi.successor_by_mode(["V/GONE"], {}), (None, {})) + self.assertEqual(afi.successor_by_mode([], {"V/A": {"ID1"}}), (None, {})) + + +class TestLedgerMigration(SyntheticTreeCase): + def test_backfill_successors_via_mode_rule(self): + analysis = afi.analyze_tree(self.t.profiles) + claim_ids = afi.claim_effective_ids(analysis) + old = { + "X9": ["VendorA/APLA"], # family lives on + "Y9": ["VendorA/GONE"], # family died + "Z9": {"claims": ["kept"], "successor": "AX01"}, # already migrated + } + migrated = afi.migrate_retired_entries(old, claim_ids) + self.assertEqual(migrated["X9"], + {"claims": ["VendorA/APLA"], "successor": "AX01"}) + self.assertEqual(migrated["Y9"], + {"claims": ["VendorA/GONE"], "successor": None}) + self.assertEqual(migrated["Z9"], old["Z9"]) + + # --------------------------------------------------------------------------- # --update-snapshot # --------------------------------------------------------------------------- @@ -448,30 +807,117 @@ class TestUpdateSnapshot(SyntheticTreeCase): self.assertEqual(snap["ids"]["AX01"], ["VendorA/APLA"]) self.assertEqual(snap["ids"]["OGFL99"], ["OrcaFilamentLibrary/Generic PLA", "VendorA/Generic PLA"]) + self.assertEqual(snap["triples"]["AX01"], [["AVendor", "PLA", "APLA"]]) + self.assertEqual(snap["triples"]["OGFL99"], [["Generic", "PLA", "fdm_pla"]]) + self.assertEqual(snap["triple_exceptions"], []) - def test_vanished_id_is_retired(self): + def test_vanished_island_id_released_with_hint_not_retired(self): + self.t.write_preset("VendorA", preset( + "GPLA @base", filament_id="GFZZ01", instantiation=False, + filament_vendor="Generic", filament_type="PLA")) + self.t.write_preset("VendorA", preset( + "GPLA @P1", inherits="GPLA @base", + compatible_printers=["P1 0.4 nozzle"])) + rc, _out = self.t.update_snapshot(allow_shared_catalog=True) + self.assertEqual(rc, 0) + # The family re-mints away from the island id: released, not retired. + self.t.write_preset("VendorA", preset( + "GPLA @base", filament_id="OFaaaaaa", instantiation=False, + filament_vendor="Generic", filament_type="PLA"), register=False) + rc, out = self.t.update_snapshot() + self.assertEqual(rc, 0) + self.assertIn("released to the BBL island space", out) + ledger = afi.load_ledger(self.t.retired) + self.assertNotIn("GFZZ01", ledger["retired"]) + self.assertEqual(ledger["hints"].get("GFZZ01"), "OFaaaaaa") + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + # The island may legitimately (re)ship a released id later. + self.t.add_vendor("BBL", [ + preset("BPLA @base", filament_id="GFZZ01", instantiation=False), + preset("BPLA @X1C", inherits="BPLA @base", + compatible_printers=["X1C 0.4 nozzle"]), + ]) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + + def test_vanished_id_is_retired_with_null_successor(self): self.t.remove_preset("VendorA", "APLA @base") self.t.remove_preset("VendorA", "APLA @P1") rc, out = self.t.update_snapshot() self.assertEqual(rc, 0) self.assertIn("AX01", out) - retired = load_json_file(self.t.retired) - self.assertEqual(retired["retired"], {"AX01": ["VendorA/APLA"]}) + retired = load_json_file(self.t.retired)["retired"] + self.assertEqual(retired, + {"AX01": {"claims": ["VendorA/APLA"], "successor": None}}) snap = load_json_file(self.t.snapshot) self.assertNotIn("AX01", snap["ids"]) + self.assertNotIn("AX01", snap["triples"]) # append-only + idempotent: a second run keeps the ledger intact rc, out = self.t.update_snapshot() self.assertEqual(rc, 0) self.assertIn("nothing changed", out) - retired2 = load_json_file(self.t.retired) - self.assertEqual(retired, retired2) + self.assertEqual(retired, load_json_file(self.t.retired)["retired"]) # ... and a retired id may never be minted again - self.assertNotIn(afi.mint_filament_id("VendorA", "APLA", - set(retired["retired"])), retired["retired"]) + self.assertNotIn(afi.mint_filament_id("AVendor", "PLA", "APLA", + set(retired)), retired) + + def test_vanished_id_successor_follows_surviving_family(self): + # The family lives on under a new id (a re-mint): the mode rule finds it. + self.t.write_preset("VendorA", preset("APLA @base", filament_id="AXNEW", + instantiation=False, + filament_vendor="AVendor", + filament_type="PLA"), + register=False) + rc, out = self.t.update_snapshot() + self.assertEqual(rc, 0) + retired = load_json_file(self.t.retired)["retired"] + self.assertEqual(retired["AX01"], + {"claims": ["VendorA/APLA"], "successor": "AXNEW"}) + + def test_multi_candidate_retirement_warns_and_ties_break(self): + self.t.write_preset("VendorA", preset("SHRD @base", filament_id="SH01", + instantiation=False, + filament_vendor="SV", + filament_type="PLA")) + self.t.write_preset("VendorA", preset("CH1 @P1", inherits="SHRD @base", + compatible_printers=["P1"])) + self.t.write_preset("VendorA", preset("CH2 @P1", inherits="SHRD @base", + compatible_printers=["P1"])) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + # Split the two families onto their own roots and drop the shared id. + self.t.write_preset("VendorA", preset("CH1 @base", filament_id="SA01", + instantiation=False, + filament_vendor="SV", + filament_type="PLA")) + self.t.write_preset("VendorA", preset("CH2 @base", filament_id="SB01", + instantiation=False, + filament_vendor="SV", + filament_type="PLA")) + self.t.write_preset("VendorA", preset("CH1 @P1", inherits="CH1 @base", + compatible_printers=["P1"]), + register=False) + self.t.write_preset("VendorA", preset("CH2 @P1", inherits="CH2 @base", + compatible_printers=["P1"]), + register=False) + self.t.remove_preset("VendorA", "SHRD @base") + rc, out = self.t.update_snapshot() + self.assertEqual(rc, 0) + self.assertIn("[WARNING]", out) + self.assertIn("SB01", out) # the losing candidate is named + retired = load_json_file(self.t.retired)["retired"] + self.assertEqual(retired["SH01"]["successor"], "SA01") + self.assertEqual(retired["SH01"]["claims"], + ["VendorA/CH1", "VendorA/CH2"]) def test_refuses_new_reserved_namespace_claims(self): self.t.write_preset("VendorA", preset("CNEW @base", filament_id="GFX99", - instantiation=False)) + instantiation=False, + filament_vendor="CV", + filament_type="PLA")) self.t.write_preset("VendorA", preset("CNEW @P1", inherits="CNEW @base", compatible_printers=["P1"])) with open(self.t.snapshot, "rb") as f: @@ -487,6 +933,122 @@ class TestUpdateSnapshot(SyntheticTreeCase): self.assertEqual(snap["ids"]["GFX99"], ["VendorA/CNEW"]) +class TestForgetNeverShipped(SyntheticTreeCase): + def forget_file(self, ids): + path = os.path.join(self.t.dir, "forget.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(ids, f) + return path + + def test_refuses_when_listed_id_is_live(self): + with open(self.t.snapshot, "rb") as f: + before = f.read() + rc, out = self.t.update_snapshot(forget_path=self.forget_file(["AX01"])) + self.assertEqual(rc, 1) + self.assertIn("still in the tree", out) + with open(self.t.snapshot, "rb") as f: + self.assertEqual(f.read(), before) + + def test_forgotten_id_is_not_retired(self): + self.t.write_preset("VendorA", preset("ZN @base", filament_id="Y999", + instantiation=False, + filament_vendor="ZV", + filament_type="PLA")) + self.t.write_preset("VendorA", preset("ZN @P1", inherits="ZN @base", + compatible_printers=["P1"])) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + self.t.remove_preset("VendorA", "ZN @base") + self.t.remove_preset("VendorA", "ZN @P1") + rc, out = self.t.update_snapshot(forget_path=self.forget_file(["Y999"])) + self.assertEqual(rc, 0) + self.assertIn("forgotten (never shipped)", out) + self.assertNotIn("Y999", load_json_file(self.t.retired)["retired"]) + self.assertNotIn("Y999", load_json_file(self.t.snapshot)["ids"]) + + def test_splices_chains_through_forgotten_id(self): + self.t.write_preset("VendorA", preset("ZN @base", filament_id="Y999", + instantiation=False, + filament_vendor="ZV", + filament_type="PLA")) + self.t.write_preset("VendorA", preset("ZN @P1", inherits="ZN @base", + compatible_printers=["P1"])) + rc, _out = self.t.update_snapshot() + self.assertEqual(rc, 0) + # A shipped id already forwards to Y999, which turns out never shipped. + self.t.write_ledger(retired={ + "OLD1": {"claims": ["VendorA/ZN"], "successor": "Y999"}}) + # The family re-mints to NEWZ and Y999 vanishes. + self.t.write_preset("VendorA", preset("ZN @base", filament_id="NEWZ", + instantiation=False, + filament_vendor="ZV", + filament_type="PLA"), + register=False) + rc, out = self.t.update_snapshot(forget_path=self.forget_file(["Y999"])) + self.assertEqual(rc, 0) + self.assertIn("spliced succession", out) + retired = load_json_file(self.t.retired)["retired"] + self.assertNotIn("Y999", retired) + self.assertEqual(retired["OLD1"], + {"claims": ["VendorA/ZN"], "successor": "NEWZ"}) + + +# --------------------------------------------------------------------------- +# --add-hint +# --------------------------------------------------------------------------- + +class TestAddHint(SyntheticTreeCase): + def add_bbl(self): + self.t.add_vendor("BBL", [ + preset("BPLA @base", filament_id="GFB01", instantiation=False), + preset("BPLA @X1C", inherits="BPLA @base", + compatible_printers=["X1C 0.4 nozzle"]), + ]) + rc, _out = self.t.update_snapshot() + assert rc == 0 + + def test_adds_island_hint(self): + self.add_bbl() + rc, out = self.t.add_hint(["GFB01=AX01"]) + self.assertEqual(rc, 0, out) + self.assertEqual(load_json_file(self.t.retired)["hints"], + {"GFB01": "AX01"}) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + + def test_rejects_non_island_old(self): + rc, out = self.t.add_hint(["AX01=OGFL99"]) + self.assertEqual(rc, 1) + self.assertIn("not in an island id space", out) + self.assertEqual(afi.load_ledger(self.t.retired)["hints"], {}) + + def test_accepts_absent_island_old(self): + # Island catalogs own ids Orca never shipped (or has released). + rc, out = self.t.add_hint(["GFXX99=AX01"]) + self.assertEqual(rc, 0, out) + self.assertEqual(load_json_file(self.t.retired)["hints"], + {"GFXX99": "AX01"}) + errors, out = self.t.check() + self.assertEqual(errors, 0, out) + + def test_rejects_retired_old(self): + self.t.write_ledger(retired={"ROLD": {"claims": [], "successor": None}}) + rc, out = self.t.add_hint(["ROLD=AX01"]) + self.assertEqual(rc, 1) + self.assertIn("is retired", out) + + def test_rejects_dead_target(self): + self.add_bbl() + rc, out = self.t.add_hint(["GFB01=OFnope99"]) + self.assertEqual(rc, 1) + self.assertIn("not a live tree id", out) + + def test_rejects_malformed_pair(self): + rc, out = self.t.add_hint(["GFB01"]) + self.assertEqual(rc, 1) + self.assertIn('expects "OLD=NEW"', out) + + # --------------------------------------------------------------------------- # default run: mint + insert # --------------------------------------------------------------------------- @@ -498,21 +1060,25 @@ class TestAssign(SyntheticTreeCase): self.assertIn("nothing to do (0 files changed)", out) def test_mints_into_family_root_and_rootless_member(self): - self.t.write_preset("VendorA", preset("FNEW @base", instantiation=False)) + self.t.write_preset("VendorA", preset("FNEW @base", instantiation=False, + filament_vendor="FV", + filament_type="PLA")) self.t.write_preset("VendorA", preset("FNEW @P1", inherits="FNEW @base", compatible_printers=["P1"])) self.t.write_preset("VendorA", preset("FNEW @P2", inherits="FNEW @base", compatible_printers=["P2"])) - self.t.write_preset("VendorA", preset("GNEW @P1", compatible_printers=["P1"])) + self.t.write_preset("VendorA", preset("GNEW @P1", compatible_printers=["P1"], + filament_vendor="GV", + filament_type="PETG")) changed, errors, _out = self.t.assign() self.assertEqual(errors, 0) self.assertEqual(changed, 2) # one root + one root-less member root = load_json_file(self.t.preset_path("VendorA", "FNEW @base")) self.assertEqual(root["filament_id"], - afi.generate_filament_id("VendorA", "FNEW")) + afi.generate_filament_id("FV", "PLA", "FNEW")) member = load_json_file(self.t.preset_path("VendorA", "GNEW @P1")) self.assertEqual(member["filament_id"], - afi.generate_filament_id("VendorA", "GNEW")) + afi.generate_filament_id("GV", "PETG", "GNEW")) # variants themselves never get the key child = load_json_file(self.t.preset_path("VendorA", "FNEW @P1")) self.assertNotIn("filament_id", child) @@ -521,8 +1087,36 @@ class TestAssign(SyntheticTreeCase): self.assertEqual((changed, errors), (0, 0)) self.assertIn("nothing to do", out) + def test_refuses_family_with_incomplete_triple(self): + self.t.write_preset("VendorA", preset("ENEW @base", instantiation=False)) + self.t.write_preset("VendorA", preset("ENEW @P1", inherits="ENEW @base", + compatible_printers=["P1"])) + changed, errors, out = self.t.assign() + self.assertEqual(changed, 0) + self.assertGreater(errors, 0) + self.assertIn("resolves empty filament_vendor and filament_type", out) + self.assertIn("mint key needs both", out) + + def test_refuses_family_with_divergent_root_fields(self): + self.t.write_preset("VendorA", preset("HNEW @base1", instantiation=False, + filament_vendor="HV", + filament_type="PLA")) + self.t.write_preset("VendorA", preset("HNEW @base2", instantiation=False, + filament_vendor="HV", + filament_type="PETG")) + self.t.write_preset("VendorA", preset("HNEW @P1", inherits="HNEW @base1", + compatible_printers=["P1"])) + self.t.write_preset("VendorA", preset("HNEW @P2", inherits="HNEW @base2", + compatible_printers=["P2"])) + changed, errors, out = self.t.assign() + self.assertEqual(changed, 0) + self.assertGreater(errors, 0) + self.assertIn("divergent (filament_vendor, filament_type)", out) + def test_shared_root_between_families_is_refused(self): - self.t.write_preset("VendorA", preset("shared_base", instantiation=False)) + self.t.write_preset("VendorA", preset("shared_base", instantiation=False, + filament_vendor="SV", + filament_type="PLA")) self.t.write_preset("VendorA", preset("HNEW @P1", inherits="shared_base", compatible_printers=["P1"])) self.t.write_preset("VendorA", preset("INEW @P1", inherits="shared_base", @@ -533,6 +1127,10 @@ class TestAssign(SyntheticTreeCase): self.assertIn("shared with famil", out) +# --------------------------------------------------------------------------- +# byte-preserving profile edits +# --------------------------------------------------------------------------- + class TestInsertEditing(unittest.TestCase): CRLF_TEXT = ( '{\r\n' @@ -546,6 +1144,17 @@ class TestInsertEditing(unittest.TestCase): '\t]\r\n' '}\r\n' ) + CRLF_WITH_ID = ( + '{\r\n' + '\t"type": "filament",\r\n' + '\t"name": "JNEW @base",\r\n' + '\t"filament_id": "OFold123",\r\n' + '\t"instantiation": "false",\r\n' + '\t"filament_type": [\r\n' + '\t\t"PLA"\r\n' + '\t]\r\n' + '}\r\n' + ) def test_insert_before_instantiation_preserves_bytes(self): text, n = afi.insert_filament_id(self.CRLF_TEXT, "OFabc123") @@ -585,6 +1194,192 @@ class TestInsertEditing(unittest.TestCase): raw.replace(b'\t"filament_id": "OFabc123",\r\n', b"", 1), self.CRLF_TEXT.encode("utf-8")) + def test_replace_value_preserves_every_other_byte(self): + text, n = afi.replace_filament_id_value(self.CRLF_WITH_ID, + "OFold123", "OFnew456") + self.assertEqual(n, 1) + self.assertEqual(json.loads(text)["filament_id"], "OFnew456") + self.assertEqual(text, self.CRLF_WITH_ID.replace('"OFold123"', '"OFnew456"')) + + def test_replace_value_requires_exact_old_value(self): + _text, n = afi.replace_filament_id_value(self.CRLF_WITH_ID, + "OFwrong1", "OFnew456") + self.assertEqual(n, 0) + + def test_rewrite_filament_id_on_disk(self): + t = SyntheticTree() + self.addCleanup(t.cleanup) + t.add_vendor("VendorA", []) + path = t.preset_path("VendorA", "JNEW @base") + with open(path, "wb") as f: + f.write(self.CRLF_WITH_ID.encode("utf-8")) + afi.rewrite_filament_id(path, "OFold123", "OFnew456") + with open(path, "rb") as f: + raw = f.read() + self.assertEqual(raw, + self.CRLF_WITH_ID.replace('"OFold123"', '"OFnew456"') + .encode("utf-8")) + with self.assertRaises(RuntimeError): + afi.rewrite_filament_id(path, "OFold123", "OFxxx999") # stale old id + + def test_delete_line_with_trailing_comma(self): + text, n = afi.delete_filament_id_line(self.CRLF_WITH_ID, "OFold123") + self.assertEqual(n, 1) + self.assertNotIn("filament_id", json.loads(text)) + self.assertEqual(text, self.CRLF_WITH_ID.replace( + '\t"filament_id": "OFold123",\r\n', "", 1)) + + def test_delete_last_property_line(self): + lf_text = ('{\n "name": "K @base",\n "instantiation": "false",\n' + ' "filament_id": "OFold123"\n}\n') + text, n = afi.delete_filament_id_line(lf_text, "OFold123") + self.assertEqual(n, 1) + data = json.loads(text) + self.assertNotIn("filament_id", data) + self.assertEqual(data["instantiation"], "false") + + +# --------------------------------------------------------------------------- +# --remint +# --------------------------------------------------------------------------- + +class TestRemint(SyntheticTreeCase): + TRIPLE = ("AVendor", "PLA", "APLA") + + def test_rewrites_mismatching_declaration(self): + want = afi.generate_filament_id(*self.TRIPLE) + changed, errors, out = self.t.remint(["VendorA"]) + self.assertEqual(errors, 0, out) + self.assertEqual(changed, 1) + self.assertIn('"AX01" -> "%s"' % want, out) + root = load_json_file(self.t.preset_path("VendorA", "APLA @base")) + self.assertEqual(root["filament_id"], want) + # OFL was not part of the run + ofl_root = load_json_file(self.t.preset_path(OFL, "fdm_pla")) + self.assertEqual(ofl_root["filament_id"], "OGFL99") + # idempotent: a second run over the same vendor rewrites nothing + changed, errors, _out = self.t.remint(["VendorA"]) + self.assertEqual((changed, errors), (0, 0)) + + def test_same_triple_converges_within_run(self): + self.t.add_vendor("VendorB", [ + preset("APLA @Bbase", filament_id="BX01", instantiation=False, + filament_vendor="AVendor", filament_type="PLA"), + preset("APLA @PB", inherits="APLA @Bbase", + compatible_printers=["PB 0.4 nozzle"]), + ]) + want = afi.generate_filament_id(*self.TRIPLE) + changed, errors, _out = self.t.remint(["VendorA", "VendorB"]) + self.assertEqual(errors, 0) + self.assertEqual(changed, 2) + a = load_json_file(self.t.preset_path("VendorA", "APLA @base")) + b = load_json_file(self.t.preset_path("VendorB", "APLA @Bbase")) + self.assertEqual(a["filament_id"], want) + self.assertEqual(b["filament_id"], want) + + def test_same_triple_converges_with_existing_tree_id(self): + # Another bundle already carries the conforming id for the same triple: + # reuse is required, not blocked. + want = afi.generate_filament_id(*self.TRIPLE) + self.t.add_vendor("VendorB", [ + preset("APLA @Bbase", filament_id=want, instantiation=False, + filament_vendor="AVendor", filament_type="PLA"), + ]) + changed, errors, _out = self.t.remint(["VendorA"]) + self.assertEqual((changed, errors), (1, 0)) + a = load_json_file(self.t.preset_path("VendorA", "APLA @base")) + self.assertEqual(a["filament_id"], want) + + def test_blocked_by_retired_id(self): + want0 = afi.generate_filament_id(*self.TRIPLE) + self.t.write_ledger(retired={want0: {"claims": [], "successor": None}}) + changed, errors, _out = self.t.remint(["VendorA"]) + self.assertEqual((changed, errors), (1, 0)) + root = load_json_file(self.t.preset_path("VendorA", "APLA @base")) + self.assertEqual(root["filament_id"], + afi.generate_filament_id(*self.TRIPLE, salt=1)) + + def test_blocked_by_hint_key(self): + want0 = afi.generate_filament_id(*self.TRIPLE) + self.t.write_ledger(hints={want0: "AX01"}) + changed, errors, _out = self.t.remint(["VendorA"]) + self.assertEqual((changed, errors), (1, 0)) + root = load_json_file(self.t.preset_path("VendorA", "APLA @base")) + self.assertEqual(root["filament_id"], + afi.generate_filament_id(*self.TRIPLE, salt=1)) + + def test_blocked_by_other_triple_occurrence(self): + want0 = afi.generate_filament_id(*self.TRIPLE) + self.t.add_vendor("VendorB", [ + preset("BPLA @base", filament_id=want0, instantiation=False, + filament_vendor="BV", filament_type="PETG"), + ]) + changed, errors, _out = self.t.remint(["VendorA"]) + self.assertEqual((changed, errors), (1, 0)) + root = load_json_file(self.t.preset_path("VendorA", "APLA @base")) + self.assertEqual(root["filament_id"], + afi.generate_filament_id(*self.TRIPLE, salt=1)) + + def test_bbl_is_forbidden(self): + changed, errors, out = self.t.remint(["BBL"]) + self.assertEqual(changed, 0) + self.assertGreater(errors, 0) + self.assertIn("forbidden", out) + + +# --------------------------------------------------------------------------- +# --drop-redundant-ids +# --------------------------------------------------------------------------- + +class TestDropRedundantIds(SyntheticTreeCase): + def test_drops_only_ofl_riding_same_base_name_declarations(self): + # Redundant: same base name, rides the OFL generic, re-declares its id. + self.t.write_preset("VendorA", preset("Generic PLA @P2", + inherits="Generic PLA @System", + compatible_printers=["P2"], + filament_id="OGFL99")) + # Renamed rider: not dropped (a repoint worksheet decision, not a drop). + self.t.write_preset("VendorA", preset("Tuned PLA @P3", + inherits="Generic PLA @System", + compatible_printers=["P3"], + filament_id="OGFL99")) + dropped, errors, out = self.t.drop_redundant("VendorA") + self.assertEqual(errors, 0, out) + self.assertEqual(dropped, 1) + self.assertIn('dropped filament_id "OGFL99"', out) + self.assertIn('rides OFL "Generic PLA @System"', out) + gone = load_json_file(self.t.preset_path("VendorA", "Generic PLA @P2")) + self.assertNotIn("filament_id", gone) + kept = load_json_file(self.t.preset_path("VendorA", "Tuned PLA @P3")) + self.assertEqual(kept["filament_id"], "OGFL99") + # vendor-rooted families are untouched + root = load_json_file(self.t.preset_path("VendorA", "APLA @base")) + self.assertEqual(root["filament_id"], "AX01") + + def test_bbl_is_forbidden(self): + dropped, errors, out = self.t.drop_redundant("BBL") + self.assertEqual(dropped, 0) + self.assertGreater(errors, 0) + self.assertIn("forbidden", out) + + +# --------------------------------------------------------------------------- +# --mint CLI +# --------------------------------------------------------------------------- + +class TestMintCli(unittest.TestCase): + def test_prints_an_of_id(self): + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + rc = afi.main(["--mint", "Polymaker/PLA/PolyLite PLA"]) + self.assertEqual(rc, 0) + self.assertRegex(buf.getvalue().strip(), r"^OF[0-9A-Za-z]{6}$") + + def test_requires_exactly_three_parts(self): + with self.assertRaises(SystemExit), \ + contextlib.redirect_stderr(io.StringIO()): + afi.main(["--mint", "Vendor/Family"]) + # --------------------------------------------------------------------------- # the real tree @@ -610,9 +1405,11 @@ class TestRealTree(unittest.TestCase): class TestReviewFixes(SyntheticTreeCase): def test_update_snapshot_refuses_retired_id_reuse(self): - fid = afi.generate_filament_id("VendorA", "LONER") + fid = afi.generate_filament_id("LV", "PLA", "LONER") self.t.write_preset("VendorA", preset("LONER @base", filament_id=fid, - instantiation=False)) + instantiation=False, + filament_vendor="LV", + filament_type="PLA")) self.t.write_preset("VendorA", preset("LONER @P1", inherits="LONER @base", compatible_printers=["P1 0.4 nozzle"])) rc, _ = self.t.update_snapshot() @@ -624,7 +1421,9 @@ class TestReviewFixes(SyntheticTreeCase): self.assertIn(fid, afi.load_retired(self.t.retired)) # Resurrect the same id: --update-snapshot must refuse, not sanction. self.t.write_preset("VendorA", preset("LONER @base", filament_id=fid, - instantiation=False)) + instantiation=False, + filament_vendor="LV", + filament_type="PLA")) self.t.write_preset("VendorA", preset("LONER @P1", inherits="LONER @base", compatible_printers=["P1 0.4 nozzle"])) rc, out = self.t.update_snapshot() @@ -633,12 +1432,14 @@ class TestReviewFixes(SyntheticTreeCase): self.assertNotIn(fid, load_json_file(self.t.snapshot)["ids"]) def test_check3_skips_of_id_inherited_from_other_vendor(self): - # Post-Phase-1 world: an OFL generic carries its own minted OF id and a - # vendor tunes it correctly (same base name, non-empty printers). The - # new claim must trip only the snapshot gate, never mint conformance. - fid = afi.generate_filament_id(OFL, "Generic PLA Matte") + # An OFL family carries its own minted OF id and a vendor tunes it + # correctly (same base name, non-empty printers). The new claim must + # trip only the snapshot gate, never mint conformance. + fid = afi.generate_filament_id("Generic", "PLA", "Generic PLA Matte") self.t.write_preset(OFL, preset("Generic PLA Matte @base", filament_id=fid, - instantiation=False)) + instantiation=False, + filament_vendor="Generic", + filament_type="PLA")) self.t.write_preset(OFL, preset("Generic PLA Matte @System", inherits="Generic PLA Matte @base", compatible_printers=[])) @@ -648,7 +1449,7 @@ class TestReviewFixes(SyntheticTreeCase): inherits="Generic PLA Matte @System", compatible_printers=["P1 0.4 nozzle"])) errors, out = self.t.check() - self.assertNotIn("does not match its mint", out) + self.assertNotIn("does not match the mint", out) self.assertIn("not sanctioned", out) self.assertEqual(errors, 1, out) # After sanctioning the claim the tree is fully green again. @@ -659,10 +1460,12 @@ class TestReviewFixes(SyntheticTreeCase): def test_check7c_prints_expected_mint(self): self.t.write_preset("VendorA", preset("Orphan PLA @P1", - compatible_printers=["P1 0.4 nozzle"])) + compatible_printers=["P1 0.4 nozzle"], + filament_vendor="OV", + filament_type="PLA")) errors, out = self.t.check() self.assertGreater(errors, 0) - self.assertIn(afi.generate_filament_id("VendorA", "Orphan PLA"), out) + self.assertIn(afi.generate_filament_id("OV", "PLA", "Orphan PLA"), out) if __name__ == "__main__": diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index bc728f62e5..c3a44c25f1 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -555,6 +555,60 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::str return std::string(out, 16); } +std::string follow_filament_id_succession(const std::string& id, const std::map& forwards) +{ + auto it = forwards.find(id); + if (it == forwards.end()) + return std::string(); + std::set visited { id }; + std::string current = id; + while (it != forwards.end() && visited.insert(it->second).second) { + current = it->second; + it = forwards.find(current); + } + // it == end(): current is the live end of the chain; otherwise the ledger is + // cyclic and current is the last id reached before a repeat. + return current; +} + +const std::map& filament_id_succession_map() +{ + // Loaded once; C++11 magic-static initialization makes this thread-safe. + // Succession is a best-effort miss-path fallback, so a missing or corrupt + // ledger is not an error for the client - it just yields an empty map. + static const std::map forwards = []() { + std::map map; + const std::string path = Slic3r::resources_dir() + "/profiles/retired_filament_ids.json"; + boost::nowide::ifstream file(path.c_str()); + if (!file.is_open()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": cannot open " << path; + return map; + } + try { + nlohmann::json ledger; + file >> ledger; + if (ledger.contains("retired") && ledger["retired"].is_object()) + for (auto& [old_id, entry] : ledger["retired"].items()) + if (entry.is_object() && entry.contains("successor") && entry["successor"].is_string()) + map[old_id] = entry["successor"].get(); + if (ledger.contains("hints") && ledger["hints"].is_object()) + for (auto& [old_id, target] : ledger["hints"].items()) + if (target.is_string()) + map[old_id] = target.get(); + } catch (...) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": failed to parse " << path; + map.clear(); + } + return map; + }(); + return forwards; +} + +std::string resolve_filament_id_succession(const std::string& id) +{ + return follow_filament_id_succession(id, filament_id_succession_map()); +} + std::string Preset::get_iot_type_string(Preset::Type type) { switch (type) { diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index bbbdc6c5ff..e84f56b107 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -100,6 +100,23 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::string& type, const std::string& name); +// Succession forwarding for retired/renamed filament_ids, backed by the shipped +// ledger resources/profiles/retired_filament_ids.json. Consulted on a filament_id +// resolution miss only, so live ids always resolve natively first. +// +// Pure chain walk over `forwards` (old id -> successor). Returns the final id of the +// chain, or an empty string when `id` has no entry. Cycle-guarded: on a malformed +// cyclic ledger it stops at the last id reached before any repeat (a self-loop +// returns `id` itself). +std::string follow_filament_id_succession(const std::string& id, + const std::map& forwards); +// Forwarding map loaded once (thread-safe) from the ledger, merging +// "retired" {id -> non-null successor} and "hints" {island-owned id -> live id}. +// A missing or corrupt file yields an empty map. +const std::map& filament_id_succession_map(); +// Convenience: follow_filament_id_succession(id, filament_id_succession_map()). +std::string resolve_filament_id_succession(const std::string& id); + enum ConfigFileType { CONFIG_FILE_TYPE_UNKNOWN, diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 399443a706..a0f687c26b 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -729,6 +729,12 @@ std::optional PresetBundle::get_filament_by_filament_id(const } } } + // Miss: a retired/renamed id may forward to a live successor through the shipped + // succession ledger. resolve() returns a chain-final id, so the retry below cannot + // recurse further (a malformed cyclic ledger yields a non-final id, rejected here). + const std::string successor = resolve_filament_id_succession(filament_id); + if (!successor.empty() && resolve_filament_id_succession(successor).empty()) + return get_filament_by_filament_id(successor, printer_name); return std::nullopt; } @@ -3150,6 +3156,15 @@ 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()) { + // Retired/renamed ids forward to a live successor through the shipped succession ledger. + const std::string successor = resolve_filament_id_succession(filament_id); + if (!successor.empty()) { + iter = std::find_if(filaments.begin(), filaments.end(), + [this, &successor](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == successor; }); + warn_ambiguous_filament_id_match(filaments, iter, successor); + } + } 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); @@ -3253,6 +3268,16 @@ unsigned int PresetBundle::sync_ams_list(std::vector> filament_list = filaments.get_filament_presets(); std::set 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)->config.option("compatible_printers"); diff --git a/src/slic3r/GUI/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp index 8ed0305369..e568078851 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.cpp +++ b/src/slic3r/GUI/CaliHistoryDialog.cpp @@ -59,7 +59,7 @@ int get_colume_idx(CaliColumnType type, MachineObject* obj) return type; } -static wxString get_preset_name_by_filament_id(std::string filament_id) +static wxString get_preset_name_by_filament_id(std::string filament_id, bool follow_succession = true) { auto preset_bundle = wxGetApp().preset_bundle; auto collection = &preset_bundle->filaments; @@ -93,6 +93,12 @@ static wxString get_preset_name_by_filament_id(std::string filament_id) } } } + if (preset_name.empty() && follow_succession) { + // Retired/renamed ids forward to a live successor through the shipped succession ledger. + const std::string successor = resolve_filament_id_succession(filament_id); + if (!successor.empty()) + preset_name = get_preset_name_by_filament_id(successor, false); + } return preset_name; } diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 9d7afd13b9..963ecca4e5 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -2540,14 +2540,22 @@ std::string MachineObject::setting_id_to_type(std::string setting_id, std::strin std::string type; PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle; if (preset_bundle) { - for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { - - if (it->filament_id.compare(setting_id) == 0 && it->is_system) { - std::string display_filament_type; - it->config.get_filament_type(display_filament_type); - type = display_filament_type; - break; + auto lookup_system_type = [preset_bundle](const std::string &id) { + std::string display_filament_type; + for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { + if (it->filament_id.compare(id) == 0 && it->is_system) { + it->config.get_filament_type(display_filament_type); + break; + } } + return display_filament_type; + }; + type = lookup_system_type(setting_id); + if (type.empty()) { + // Retired/renamed ids forward to a live successor through the shipped succession ledger. + const std::string successor = resolve_filament_id_succession(setting_id); + if (!successor.empty()) + type = lookup_system_type(successor); } } @@ -5249,7 +5257,10 @@ void MachineObject::update_filament_list() for (auto it = filament_list.begin(); it != filament_list.end(); it++) { if (m_filament_list.find(it->first) != m_filament_list.end()) { - assert(it->first.size() == 8 && it->first[0] == 'P'); + // User roots may legitimately carry adopted system-shaped ids (GF*/OF*/P-hex + // system), so a non-'P' id here is expected, not an invariant violation. + if (it->first.size() != 8 || it->first[0] != 'P') + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": user-root filament_id is not user-shaped: " << it->first; if (it->second.first != m_filament_list[it->first].first) { BOOST_LOG_TRIVIAL(info) << "old min temp is not equal to new min temp and filament id: " << it->first; @@ -5311,6 +5322,17 @@ void MachineObject::update_printer_preset_name() void MachineObject::check_ams_filament_valid() { PresetBundle * preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle; + // A tray id carried by ANY system filament preset is not a dangling user-preset id + // (ten shipped P-hex system ids pass the 'P' shape gates below), so the destructive + // tray-wipe / temp-rewrite handling must never fire for it. + auto is_system_filament_id = [preset_bundle](const std::string &id) { + if (!preset_bundle) + return false; + for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) + if (it->is_system && it->filament_id == id) + return true; + return false; + }; auto printer_model = DevPrinterConfigUtil::get_printer_display_name(this->printer_type); std::map> need_checked_filament_id; for (auto &ams_pair : m_fila_system->GetAmsList()) { @@ -5332,6 +5354,8 @@ void MachineObject::check_ams_filament_valid() auto &checked_filament = data.checked_filament; for (const auto &[slot_id, curr_tray] : ams->GetTrays()) { + if (curr_tray->setting_id.size() == 8 && curr_tray->setting_id[0] == 'P' && is_system_filament_id(curr_tray->setting_id)) + continue; if (curr_tray->setting_id.size() == 8 && curr_tray->setting_id[0] == 'P' && filament_list.find(curr_tray->setting_id) == filament_list.end()) { if (checked_filament.find(curr_tray->setting_id) != checked_filament.end()) { need_checked_filament_id[nozzle_diameter_str].insert(curr_tray->setting_id); @@ -5392,6 +5416,8 @@ void MachineObject::check_ams_filament_valid() auto &data = m_nozzle_filament_data[nozzle_diameter_str]; auto &checked_filament = data.checked_filament; auto &filament_list = data.filament_list; + if (vt_tray.setting_id.size() == 8 && vt_tray.setting_id[0] == 'P' && is_system_filament_id(vt_tray.setting_id)) + continue; if (vt_tray.setting_id.size() == 8 && vt_tray.setting_id[0] == 'P' && filament_list.find(vt_tray.setting_id) == filament_list.end()) { if (checked_filament.find(vt_tray.setting_id) != checked_filament.end()) { need_checked_filament_id[nozzle_diameter_str].insert(vt_tray.setting_id); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 6475ae1c88..2a512497dc 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -537,6 +537,13 @@ bool PresetComboBox::add_ams_filaments(std::string selected, bool alias_name) } auto iter = std::find_if(filaments.begin(), filaments.end(), [&filament_id, this](auto &f) { return f.is_compatible && m_collection->get_preset_base(f) == &f && f.filament_id == filament_id; }); + if (iter == filaments.end()) { + // Retired/renamed ids forward to a live successor through the shipped succession ledger. + const std::string successor = resolve_filament_id_succession(filament_id); + if (!successor.empty()) + iter = std::find_if(filaments.begin(), filaments.end(), + [&successor, this](auto &f) { return f.is_compatible && m_collection->get_preset_base(f) == &f && f.filament_id == successor; }); + } if (iter == filaments.end()) { auto filament_type = tray.opt_string("filament_type", 0u); if (!filament_type.empty()) { diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index f1d892134b..ecb23c926c 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include namespace { @@ -609,51 +610,70 @@ std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::st { const std::string upper = trim_and_upper(filament_type); - // Map to OrcaFilamentLibrary preset IDs (compatible with all printers) - // Source: resources/profiles/OrcaFilamentLibrary/filament/ + // Normalize reported material names (trimmed, uppercased) to an OrcaFilamentLibrary + // generic family. The family's filament_id is resolved from the loaded system presets + // below rather than hardcoded, so profile id re-mints never require touching this table. + // scripts/test_moonraker_lane_data.py parses this initializer; keep the {"A", "B"} format. + static const std::map type_to_ofl_family = { + // PLA variants + {"PLA", "PLA"}, + {"PLA-CF", "PLA-CF"}, + {"PLA SILK", "PLA Silk"}, + {"PLA-SILK", "PLA Silk"}, + {"PLA HIGH SPEED", "PLA High Speed"}, + {"PLA-HS", "PLA High Speed"}, + {"PLA HS", "PLA High Speed"}, - // PLA variants - if (upper == "PLA") return "OGFL99"; - if (upper == "PLA-CF") return "OGFL98"; - if (upper == "PLA SILK" || upper == "PLA-SILK") return "OGFL96"; - if (upper == "PLA HIGH SPEED" || upper == "PLA-HS" || upper == "PLA HS") return "OGFL95"; + // ABS/ASA variants + {"ABS", "ABS"}, + {"ASA", "ASA"}, - // ABS/ASA variants - if (upper == "ABS") return "OGFB99"; - if (upper == "ASA") return "OGFB98"; + // PETG/PET variants + {"PETG", "PETG"}, + {"PET", "PETG"}, + {"PCTG", "PCTG"}, - // PETG/PET variants - if (upper == "PETG" || upper == "PET") return "OGFG99"; - if (upper == "PCTG") return "OGFG97"; + // PA/Nylon variants + {"PA", "PA"}, + {"NYLON", "PA"}, + {"PA-CF", "PA-CF"}, + {"PPA", "PPA-CF"}, + {"PPA-CF", "PPA-CF"}, + {"PPA-GF", "PPA-GF"}, - // PA/Nylon variants - if (upper == "PA" || upper == "NYLON") return "OGFN99"; - if (upper == "PA-CF") return "OGFN98"; - if (upper == "PPA" || upper == "PPA-CF") return "OGFN97"; - if (upper == "PPA-GF") return "OGFN96"; + // PC variants + {"PC", "PC"}, - // PC variants - if (upper == "PC") return "OGFC99"; + // PP/PE variants + {"PE", "PE"}, + {"PP", "PP"}, - // PP/PE variants - if (upper == "PE") return "OGFP99"; - if (upper == "PP") return "OGFP97"; + // Support materials + {"PVA", "PVA"}, + {"HIPS", "HIPS"}, + {"BVOH", "BVOH"}, - // Support materials - if (upper == "PVA") return "OGFS99"; - if (upper == "HIPS") return "OGFS98"; - if (upper == "BVOH") return "OGFS97"; + // TPU variants + {"TPU", "TPU"}, - // TPU variants - if (upper == "TPU") return "OGFU99"; + // Other materials + {"EVA", "EVA"}, + {"PHA", "PHA"}, + {"COPE", "CoPE"}, + {"SBS", "SBS"}, + }; - // Other materials - if (upper == "EVA") return "OGFR99"; - if (upper == "PHA") return "OGFR98"; - if (upper == "COPE") return "OGFLC99"; - if (upper == "SBS") return "OFLSBS99"; + auto it = type_to_ofl_family.find(upper); + if (it == type_to_ofl_family.end()) + return UNKNOWN_FILAMENT_ID; - // Unknown material + if (auto* bundle = GUI::wxGetApp().preset_bundle) { + const Preset* preset = bundle->filaments.find_preset("Generic " + it->second + " @System"); + if (preset != nullptr && preset->is_system && !preset->filament_id.empty()) + return preset->filament_id; + } + + // Unknown material, or no loaded preset data to resolve against return UNKNOWN_FILAMENT_ID; } diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index d5e6eb958e..328ffda8da 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable(${_TEST_NAME}_tests test_config.cpp test_preset_bundle_loading.cpp test_preset_setting_id.cpp + test_filament_id_succession.cpp test_elephant_foot_compensation.cpp test_geometry.cpp test_placeholder_parser.cpp diff --git a/tests/libslic3r/test_filament_id_succession.cpp b/tests/libslic3r/test_filament_id_succession.cpp new file mode 100644 index 0000000000..36268277f8 --- /dev/null +++ b/tests/libslic3r/test_filament_id_succession.cpp @@ -0,0 +1,61 @@ +#include + +#include "libslic3r/Preset.hpp" + +using namespace Slic3r; + +// Pure-function coverage of the succession chain walk (follow_filament_id_succession). +// The ledger-file loader (filament_id_succession_map) is deliberately untested here so +// the test carries no resources-dir dependency. +TEST_CASE("filament_id succession follows forwarding chains", "[Preset][filament_id]") { + SECTION("empty map returns empty") { + const std::map empty; + CHECK(follow_filament_id_succession("OFabc123", empty).empty()); + CHECK(follow_filament_id_succession("", empty).empty()); + } + + const std::map forwards = { + {"OFold001", "OFnew001"}, + {"OFchainA", "OFchainB"}, + {"OFchainB", "OFchainC"}, + {"OFchainC", "OFlive00"}, + }; + + SECTION("absent id returns empty") { + CHECK(follow_filament_id_succession("GFL99", forwards).empty()); + } + + SECTION("single hop resolves to the successor") { + CHECK(follow_filament_id_succession("OFold001", forwards) == "OFnew001"); + } + + SECTION("multi-hop chain resolves to the live end") { + CHECK(follow_filament_id_succession("OFchainA", forwards) == "OFlive00"); + CHECK(follow_filament_id_succession("OFchainB", forwards) == "OFlive00"); + CHECK(follow_filament_id_succession("OFchainC", forwards) == "OFlive00"); + } +} + +// Cycles are a ledger-validation error (script check 9) and never ship, but the client +// walk must still terminate on corrupt data: it stops at the last id reached before any +// id would repeat. +TEST_CASE("filament_id succession terminates on malformed cycles", "[Preset][filament_id]") { + SECTION("two-node cycle returns the last id before revisiting") { + const std::map cycle = {{"A", "B"}, {"B", "A"}}; + CHECK(follow_filament_id_succession("A", cycle) == "B"); + CHECK(follow_filament_id_succession("B", cycle) == "A"); + } + + SECTION("cycle entered through a tail stops before re-entering it") { + // T -> A -> B -> C -> A: stops at C, the last id reached before repeating A. + const std::map cycle = { + {"T", "A"}, {"A", "B"}, {"B", "C"}, {"C", "A"}}; + CHECK(follow_filament_id_succession("T", cycle) == "C"); + CHECK(follow_filament_id_succession("A", cycle) == "C"); + } + + SECTION("self-loop returns the id itself") { + const std::map loop = {{"A", "A"}}; + CHECK(follow_filament_id_succession("A", loop) == "A"); + } +}