Compare commits

..

3 Commits

Author SHA1 Message Date
Ian Chua
64d04a75f3 feat: additional events 2026-08-24 11:36:37 +08:00
Ian Chua
bf22ef2a82 Merge branch 'main' into feat/plugin-lifecycle-evts 2026-08-20 12:21:50 +08:00
Ian Chua
dfd3444ae7 feat: initial draft of lifecycle events API for plugins 2026-08-19 19:20:31 +08:00
3657 changed files with 4531 additions and 36429 deletions

5
.gitattributes vendored
View File

@@ -1,7 +1,2 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Shell scripts are run by Git Bash on Windows CI, which cannot read a script
# with CRLF line endings: it fails on the first line. Windows checkouts default
# to core.autocrlf=true, so keep these LF whatever the platform.
*.sh text eol=lf

View File

@@ -20,8 +20,6 @@
> Please describe the tests that you have conducted to verify the changes made in this PR.
-->
- [ ] New filament/material profiles: I wrote **no** `filament_id` key by hand — I ran `python scripts/assign_filament_ids.py` and `--update-snapshot` and committed both diffs (see `doc/developer-reference/filament_id.md`)
<!--
> A guide for users on how to download the artifacts from this PR.
-->

View File

@@ -14,7 +14,6 @@ on:
- 'localization/**'
- 'resources/**'
- ".github/workflows/build_*.yml"
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
@@ -34,7 +33,6 @@ on:
- 'build_release_vs.bat'
- 'build_release_vs2022.bat'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'

View File

@@ -162,14 +162,6 @@ jobs:
retention-days: 5
if-no-files-found: error
- name: Build system preset cache (macOS)
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
shell: bash
# The bundle was already packed from resources/, so the caches have to be
# installed into it here; the source tree keeps its JSONs for later jobs.
run: ./scripts/build_preset_cache.sh -b build/${{ inputs.arch }} build/${{ inputs.arch }}/OrcaSlicer/OrcaSlicer.app/Contents/Resources/profiles
- name: Pack macOS app bundle ${{ inputs.arch }}
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
@@ -398,13 +390,6 @@ jobs:
if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 tests } else { .\build_release_vs.bat slicer tests }
shell: pwsh
- name: Build system preset cache (Windows)
if: runner.os == 'Windows'
shell: cmd
# Shipped into both the already-installed tree (portable zip, MSIX) and
# the checkout cpack re-installs from when it builds the NSIS installer.
run: scripts\build_preset_cache.bat --prune-source "%BUILD_DIR%" "resources\profiles" "%BUILD_DIR%\OrcaSlicer\resources\profiles"
- name: Pack unit tests Win
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
@@ -554,20 +539,6 @@ jobs:
retention-days: 5
if-no-files-found: error
- name: Build system preset cache (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
# Both were packed from resources/ before the caches existed, so the
# AppImage is unpacked first and the caches shipped into it and into
# the package tree; the source tree keeps its JSONs for later steps.
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
chmod +x "$appimage"
"$appimage" --appimage-extract
./scripts/build_preset_cache.sh -b build build/package/resources/profiles squashfs-root/resources/profiles
appimagetool=$(find build -name "appimagetool.AppImage" | head -1)
ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage"
rm -rf squashfs-root
# Ship the freshly-built validator so slice_check_linux (build_all.yml)
# can slice-sweep the shipped profiles with this PR's engine. Taken from
# the aarch64 leg so the sweep also exercises the arm build; x86_64 on

View File

@@ -64,14 +64,13 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -s -l 2 2>&1 | tee ${{ runner.temp }}/validate_slice.log
exit ${PIPESTATUS[0]}
# All vendors' filament_id collisions were fixed (see scripts/filament_id_snapshot.json),
# so the duplicate-filament-subtype check runs tree-wide.
- name: validate filament subtype check
# For now run filament subtype check only for BBL profiles until we fix other vendors' profiles.
- name: validate filament subtype check for BBL profiles
id: validate_filament_subtypes
continue-on-error: true
run: |
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -v BBL -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
exit ${PIPESTATUS[0]}
- name: validate custom presets
@@ -218,7 +217,7 @@ jobs:
fi
if [ "${{ steps.validate_filament_subtypes.outcome }}" = "failure" ]; then
echo "### Filament Subtype Validation Failed"
echo "### BBL Filament Subtype Validation Failed"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_filament_subtypes.log || echo "No output captured"

1
.gitignore vendored
View File

@@ -49,4 +49,3 @@ internal_docs/
# Python bytecode
__pycache__/
*.pyc
*.opc

View File

@@ -567,8 +567,6 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer
echo "Building OrcaSlicer_profile_validator .."
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer_profile_validator
echo "Building generate_system_cache ..."
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target generate_system_cache
./scripts/run_gettext.sh
fi
if [[ -n "${BUILD_TESTS}" ]] ; then

View File

@@ -1,211 +0,0 @@
# Filament IDs (`filament_id`)
`filament_id` identifies a **material family**: one commercial product line = one id, shared by
all of that material's per-printer / per-nozzle variants. Devices use it to match a physical
spool or tray to a filament preset. It is never per-color, per-printer, per-nozzle, or
per-preset (per-preset identity is `setting_id`).
This page is the rule for authoring `filament_id` in system profiles
(`resources/profiles/**`). CI enforces everything below; the short version is:
> [!IMPORTANT]
> **Never write a `filament_id` value by hand.** New families get their id from
> `python scripts/assign_filament_ids.py`; existing families already have one — inherit it.
## Who consumes the id
Every device integration funnels a tray material id (`tray_info_idx`) through the same
matching pipeline (`PresetBundle::sync_ams_list` and friends):
| Ecosystem | Where the id comes from |
| --- | --- |
| Bambu AMS | device side (RFID / user tray setting) — the `GF*` catalog |
| Qidi box | built from device enums (`QD_*`); needs an exactly matching visible preset |
| Creality CFS | runtime brand/type scoring returns the current preset's id |
| Klipper (AFC / Happy Hare) | runtime lookup by filament type |
| Snapmaker | runtime color/vendor/type match |
Tray-to-preset matching is printer-scoped, but **several consumers match globally by id alone,
first hit wins**: tray display names, `filament_is_support`, vitrification warnings, and
multi-nozzle filament grouping in the slicing pipeline. Two *different* materials sharing one
id feed wrong data to those consumers even when the presets live in different vendors — so
cross-material id sharing is never safe. Within one printer, duplicate ids silently break AMS
matching (first match wins, the tray-edit dialog hides the second preset); the profile
validator's `-f` check rejects this.
## Do I need a new id? The one-question test
> **Would a user consider this a different spool product than anything already in the tree?**
Different polymer, different sub-brand (Basic / Matte / Silk / HF), fiber-filled sibling, or a
second selectable diameter → **new family, new id**. The same spool tuned for another printer
or nozzle → **join the existing family** (inherit its `@base`, write no id key). Tuning a
generic material → **join the OrcaFilamentLibrary family** (inherit `Generic X @System`, keep
the `Generic X` base name, write no id key).
| Situation | id |
| --- | --- |
| Per-printer / per-nozzle variant of an existing material | same id (inherit, never write the key) |
| Sub-brand or product line (PLA vs PLA Matte vs PLA Silk vs PLA HF) | new id each |
| Color | never a new id |
| Second diameter selectable on the same printer (1.75 + 2.85) | sibling family, new id |
| "High-speed" tuned for a *different printer model* | same id (it is a printer variant) |
| "High-speed" selectable *alongside* the normal preset on one printer | new id (it is a product line) |
## Structure rules
1. **Only family roots carry the key.** Root presets (any preset *not* marked
`"instantiation": "true"`, typically `<Family> @base` with `"instantiation": "false"`)
declare `filament_id`; instantiated variants inherit a root and never
write the key. A family may have several roots (e.g. per-series bases) — all of them must
declare the *identical* id.
2. **The family name is the base name**: the preset name with everything from the first
(optionally space-preceded) `@` stripped. `MyBrand PLA @Orca 3D Fuse1` and `MyBrand PLA@HS`
both belong to family `MyBrand PLA`.
3. **Within a family, variants' `compatible_printers` are pairwise disjoint** — per printer
preset, at most one compatible instantiated preset per id. The C++ validator (`-f`)
enforces this.
4. **Generics belong to OrcaFilamentLibrary.** A vendor tuning a generic material inherits
`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 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
New ids are deterministic, computed exactly like the `setting_id` precedent
(`scripts/assign_vendor_setting_ids.py`):
```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_product/<filament_vendor>/<filament_type>/<family_name>") )
```
`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. The triple
comes from the family root's *flattened* config: `<filament_vendor>` is the filament
**manufacturer** (`"Polymaker"`, or `"Generic"` for generics — never the printer brand),
`<filament_type>` the material type, `<family_name>` 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:
```bash
# 1. Author the family with NO filament_id key anywhere.
python scripts/assign_filament_ids.py # 2. mint + insert ids into the family root(s)
python scripts/assign_filament_ids.py --update-snapshot # 3. record the new claims in the ledger
python scripts/assign_filament_ids.py --check # 4. verify — the same checks CI runs
# 5. Commit the profile edits together with scripts/filament_id_snapshot.json.
```
`--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 (a declaration already equal to a salt iteration
of its own triple is conformant and left alone — deliberate salt splits keeping two
presets of one product apart for per-printer AMS matching survive),
`--drop-redundant-ids VENDOR` deletes declarations
that merely re-declare an inherited OFL id, `--add-hint "OLD=NEW"` records a cross-island
succession hint, and `--retire "OLD=NEW"` records succession for a shipped non-island id
that vanished while another declarer kept it alive (lineage the automatic claim vote can no
longer see).
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
the resulting diff.
## Reserved namespaces — never mint or hand-write into
| Space | Owner | Rule |
| --- | --- | --- |
| `GF*` | Bambu AMS/RFID catalog | BBL vendor only; byte-copies elsewhere only where the snapshot already sanctions them |
| `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 | `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": <id|null>}` — 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 <type>` fallback.
The file is append-only and maintained exclusively by `--update-snapshot` / `--add-hint` /
`--retire`.
## How CI enforces this
Profile CI (`check_profiles.yml``scripts/orca_extra_profile_check.py`) runs
`check_filament_ids()` tree-wide. Its ground truth is
**`scripts/filament_id_snapshot.json` — the sanctioned state**: the id state derived from the
tree must equal the snapshot exactly, in both directions. Any change to the id landscape
therefore surfaces as a diff to that file, and **that snapshot diff is what maintainers review
and gate in a PR**. Never edit the snapshot by hand — `--update-snapshot` regenerates it
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 **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** — 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
drift; every instantiated system filament must resolve an effective id through its
`inherits` chain (an id-less one is a hard load error in C++ that discards the whole vendor
bundle).
Sharing a **reserved-catalog** id with a new family or vendor (e.g. shipping a Bambu-cataloged
product under another vendor with its authentic `GF*` id) is refused by `--update-snapshot`
unless you pass `--allow-shared-catalog` — and it still lands in the snapshot diff for
maintainer review. Any other new sharing of an existing id is caught by the mint-conformance
check instead.
## FAQ
- **A new color of an existing product?** Never a new id — colors are not families.
- **A second diameter (1.75 mm and 2.85 mm) of the same product?** A sibling family with its
own id: two diameters are separately selectable spool products.
- **A high-speed tune of an existing material for another printer model?** Same family:
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 (or fix its `filament_vendor`/`filament_type`).** Add
`renamed_from` for the name, run `--remint <Vendor>` 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.
For general profile authoring, see the profile development guide on the
[OrcaSlicer wiki](https://www.orcaslicer.com/wiki).

View File

@@ -1,402 +0,0 @@
# System Preset Cache — High Level Design
## Why it exists
OrcaSlicer ships tens of thousands of system preset JSON files. Every launch used to
parse all of them: read each vendor profile, walk its machine, process and filament
sub-files, resolve inheritance, and build the preset collections from scratch. That
parse dominated startup, and it produced the same result every time, because system
presets only change when the app is updated or a profile update is installed.
The preset cache replaces that parse with a read. Each vendor's presets are serialized
once — at build time, in CI — into a single binary file the app reads in one pass. The
read replaces the file walk and the JSON parsing, which is where the time went;
resolving inheritance and registering the presets still runs at load, through the same
code the JSON path uses, so the result is the parse's result without the parse.
The cache is **only ever an optimization**. Every rule below exists to guarantee that a
cache is either provably equivalent to parsing the JSONs, or rejected. There is no
"mostly right" cache.
## The unit is one vendor
A cache covers exactly one vendor. `BBL.opc` sits beside `BBL.json` and holds
everything `BBL.json` and the `BBL/` sub-file tree would have produced.
Per-vendor granularity is what makes the system practical:
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
vendors keep theirs — even when the bumped vendor is the shared Orca filament
library everyone else inherits from.
- The setup wizard, which loads vendors one at a time, gets the same speedup as
startup without a second code path.
- A vendor with no cache, or a broken one, costs only that vendor a parse.
A cache holds *system* presets only. User presets, project settings and modified
presets are never serialized — they have their own storage and their own lifecycle.
## Where the files live
| Location | Contents on a shipped build | Role |
|---|---|---|
| `resources/profiles/` | `<vendor>.opc` alone — the profile and its preset JSONs both pruned | What the app ships with; what installing copies from, and the only thing it is read for |
| `<data_dir>/system/` | `<vendor>.opc` alone, or `<vendor>.json` + `<vendor>/` after an update | What the user has installed |
| `<data_dir>/system/` (dev build) | `<vendor>.json` + `<vendor>/` + `<vendor>.opc` written at runtime | A developer tree caches as it parses |
| `<data_dir>/cache/wizard_profile_data.json` | The wizard's derived vendor catalog plus the stamps it was built from | Written and read by the setup wizard only; never shipped (see "The wizard's profile-data cache") |
Two forms of the same vendor therefore exist, and the system's central rule is that
**a vendor's cache is the whole of it**. Where a cache ships or is installed, no profile
and no preset JSONs sit beside it: the cache carries the presets, the vendor profile,
and the version stamp that says which release it came from. A vendor is "installed" if
either form is present *and usable*, and its installed version is read from whichever
form a load would serve.
What stays beside the caches in `resources/profiles/` is everything that is not a
preset: each vendor's directory of printer thumbnails, cover images, bed models and
hotend meshes, which are read from disk by path and were never part of the cache. Files
that are not vendors at all, `blacklist.json` chief among them, are untouched.
The alternative — shipping both and treating the cache as a sidecar — was rejected. It
doubles the installed size, and it creates a class of bug where the two disagree and
the app's behavior depends on which one a given code path happened to read.
## What a cache file is
A fixed-size header followed by one binary stream.
The header carries a magic number, the cache format version, the payload size and a
CRC32 of the payload. It exists so that a truncated download, a half-written file or a
file from an entirely different program is rejected in microseconds, before anything
tries to interpret it.
The payload opens with the stamps that decide whether the cache may be used at all —
format version, vendor name, vendor version — then a dictionary, and then the vendor's
data: its vendor profile, three lists of preset entries (process, filament, machine),
and the count of errors the original parse hit.
Each entry is one preset **in source form**: what its JSON sub-file states and nothing
that resolving it derives — the preset's own config diff, the name of the preset it
inherits, and the parse metadata (name, sub-path, description, instantiation, setting
and filament ids, renames). Non-instantiated base presets are stored too; the children
that inherit from them cannot resolve without them.
**The payload names its own keys.** The dictionary holds the distinct `opt_key`s the
file uses, the `ConfigOptionType` each was written as, and the distinct enum *value
names*; an option in an entry's config is then a `uint16` index into that dictionary
plus its value. Names are written once per file rather than once per occurrence, and a
reader resolves the dictionary against this build's `print_config_def` once, after
which reading an option is a vector index.
This is what makes the cache survive config-schema drift. The alternative — keying an
option by its `serialization_key_ordinal`, the position `ConfigDef::add` assigns by
declaration order at static init — cannot: inserting one option into the middle of
`PrintConfig.cpp` shifts every later ordinal, and the lookup on the way back in then
*succeeds on the wrong option*, silently, wherever the two share a type. Because a
name-keyed payload instead drops the individual options this build cannot place, the
file as a whole stays readable, and there is no schema fingerprint — no checksum over
the option schema that would reject every cache on every release. An option this build
no longer defines, or now defines with a different type, gets exactly what it gets from
a JSON profile: read, dropped, and the rest of the preset loads.
The ordinal-keyed cereal hooks in `PrintConfig.hpp` are untouched — they are also the
undo/redo wire format, where the process cannot change underneath them. The cache has
its own serialization in `PresetCacheFormat.{hpp,cpp}`.
Three deliberate choices in the layout:
- **Stamps come first**, so the question "what version is this vendor installed at?"
can be answered by reading the first kilobyte. The updater asks that question for
every vendor on every launch; reading tens of megabytes to answer it would give back
the startup time the cache saved. The dictionary sits behind them, ahead of the
entries, so a reader that does go on resolves it once and then indexes.
- **Nothing inherited is baked in.** A filament preset that inherits from the shared
library is stored as its own diff plus its parent's name, and the parent is looked up
when the entry is installed, against whatever library is loaded then. A cache
therefore carries no other vendor's values, and no other vendor's update — the
library's included — can make it stale.
- **Nothing derived is stored.** Default presets, flattened configs, aliases and
lookup maps are all reconstructed at load by the same code the JSON path runs, and
state that path never fills (obsolete-preset lists) is not stored either. This keeps
the cache a record of the vendor's data, not a memory image of the program's state.
## When a cache may be used
A cache is accepted only if every gate below passes. Any failure means "parse the
JSONs instead" — never a hard error, never a partial load.
**1. Integrity.** Magic number, a declared body size that is exactly the rest of the
file, CRC32 over the payload. The size is checked against the file's real length before
anything is allocated on the strength of it, so an eight-byte field in an unauthenticated
file cannot ask for a gigabyte.
**2. Cache format version.** A single integer bumped by hand whenever the binary layout
changes in a way nothing else would catch: reordering or retyping a hand-written
serialized field, or changing what the cache's own stamps mean. Config-schema drift is
explicitly *not* such a change — the dictionary handles it — so this no longer moves
every release.
**3. Vendor identity and version.** The cache names the vendor it holds and the profile
version it was built from. It is accepted only if that version is at least as new as
the profile now on disk. Where no profile sits beside the cache — the shipped,
cache-only form — the comparison is skipped, because nothing on disk can be newer than
a cache that is the installation.
**4. Every entry installs.** Entries are installed as they are read, and an entry that
cannot be — typically one that inherits a parent the currently loaded filament library
no longer provides — rejects the whole cache, never just the entry. A partial vendor is
not a vendor.
There is deliberately no stamp for the shared filament library. A cache stores its
filaments' inheritance by name and resolves it at load, so a library update changes
what a cache load *produces*, never whether the cache is *valid* — the same file yields
the updated result. This matters most on a shipped build, where a vendor is its cache
and nothing else: a profile update that delivered only the library would otherwise have
stranded every other vendor with a cache it invalidated and no JSONs to fall back on.
A vendor profile with no parsable version is never cached and never served from a
cache. There would be no way to tell later whether the cache had gone stale, and a
cache nothing can invalidate is worse than no cache.
## How a vendor is loaded
Vendors load in a fixed order, because filament inheritance crosses exactly one
boundary: any vendor's filament may inherit from the shared Orca filament library,
and nothing else reaches across vendors. The library therefore goes first, alone;
every other vendor follows in parallel, resolving against it; and the results are
merged in a stable order:
```mermaid
flowchart LR
lib["1 · OrcaFilamentLibrary<br/>loaded first, synchronously"] --> par["2 · every other vendor in parallel,<br/>each into its own bundle, filaments<br/>resolving against the loaded library"] --> merge["3 · bundles merged into one,<br/>sequentially, in stable vendor order"]
```
Whether a vendor comes from its cache or from a parse changes nothing in that
order — both produce the same bundle, so cached and parsed vendors mix freely in
one startup.
**A vendor is loaded from where it is installed and nowhere else.** For startup that
is `<data_dir>/system/`; resources reaches the app by being *installed* into that
directory first, never by being loaded from. (The setup wizard is the one caller with
a different notion of "where": it also shows vendors the user has not installed, and
loads those from `resources/profiles` — see "The wizard's profile-data cache".) There
is one lookup tier and one parse source:
```
load vendor V from <data_dir>/system:
system/V.opc passes CACHE_VERSION + size + CRC + vendor name + version gate?
yes -> serve from it
no -> parse system/V.json, then write system/V.opc back
```
The same decision drawn out — "the gates" are the four acceptance checks above:
```mermaid
flowchart TB
start["load vendor V from a directory dir<br/>— normally &lt;data_dir&gt;/system/"]
start --> stamp["installed version = version of dir/V.json<br/>— or ∞ with no profile there,<br/>the cache then being the installation"]
stamp --> g1{"dir/V.opc<br/>passes all four gates?"}
g1 -- "yes" --> hit(["served from the<br/>installed cache"])
g1 -- "no" --> pd["parse the JSONs in dir"]
pd --> ver{"profile version<br/>parsable?"}
ver -- "yes" --> save(["loaded; dir/V.opc written back —<br/>the next load takes the top path"])
ver -- "no" --> raw(["loaded, never cached"])
```
A second tier into `resources/profiles/` used to sit between those two, and a parse
fallback to the same place behind them. Both existed only because an installed cache
died on every app upgrade, when the schema fingerprint rejected it; with the fingerprint
gone there is nothing for them to rescue. They also had a cost: on a developer tree the
shipped cache answered first, so the profile in `<data_dir>/system/` was never parsed
and its cache was never written back.
Serving from a cache is not a memory-image restore. The entries are deserialized and
then installed one by one — inheritance resolved against the presets installed before
them and the currently loaded filament library, configs flattened onto the collection
defaults, validated and registered — by the same function the JSON path calls straight
after parsing a sub-file. The two paths share everything below the parse, which is what
makes a cache-loaded bundle indistinguishable from a JSON-loaded one by construction
rather than by test coverage. Installation also rebuilds each preset's file path from
the local data directory, so a shipped cache never carries the generating machine's
paths.
App upgrades work because a cache normally survives one. Only a deliberate
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
install time rather than at load: a vendor whose cache this build cannot read counts
as **not installed**, so the updater lays down a working copy on the next launch (see
below). A vendor that still has its profile JSONs beside the cache is simply parsed
and re-cached.
If a parse does happen and the vendor's profile carries a version, the app writes the
cache back beside where it looked for the vendor. That is how a developer build warms
itself up on second launch, and how a vendor delivered by a profile update becomes
cached without waiting for the next release.
## The wizard's profile-data cache
The setup wizard's printer and filament pages want every vendor in one bundle — the
installed ones *and* the shipped ones the user has not installed yet, because the
wizard is where installing is chosen. Its set therefore spans two directories:
`<data_dir>/system/` for installed vendors (shadowing resources on a name collision),
`resources/profiles` for the rest, each vendor loaded from its own directory.
What the wizard actually consumes from that bundle is one derived JSON — the model /
machine / filament / process catalog its web pages render — and that JSON is a pure
function of the vendor set: each vendor's name and version, in load order. A profile
change requires a version bump, so name and version determine a vendor's content
wherever its copy sits; which directory served it is deliberately **not** stamped,
and installing or removing a copy at an unchanged version leaves the cache valid. So
the wizard caches the *derived JSON*, not another form of the inputs:
`<data_dir>/cache/wizard_profile_data.json` holds the stamp list and the catalog. On
open, the wizard computes the current stamps (one version peek per vendor) and, when
they match, serves the catalog from the file — no bundle built, no preset installed.
Caching bundle inputs instead was tried and measured: rebuilding the bundle from
per-vendor caches costs ~2 s of preset installation whatever feeds it, so only
skipping the rebuild entirely wins.
Any change to the set — a vendor added, removed or updated, or its cache-only
`.opc` replaced by a newer one — changes the stamps and retires the whole file;
the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where
they cover) and writes the catalog back. Selections, region and per-open decorations
are applied downstream of the cache either way, so a served catalog is
indistinguishable from a rebuilt one. Nothing ships this file and the updater never
touches it; it is a locally written artifact, re-derived whenever stale, written
through a temp file and rename so half a cache is never readable.
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor
cache file must not sit in that directory. Relatedly, the stamp reader is hardened:
`read_cache_stamps` validates the cache version before reading anything
variable-length and bounds the stamp strings' lengths, so a reader pointed at a
foreign or damaged `.opc` rejects it cleanly instead of aborting on a garbage
64-bit allocation.
## How a vendor is installed
Installing copies from `resources/profiles/` into `<data_dir>/system/`. A shipped build
offers only a cache and a source tree only JSONs, but a partially-generated tree can
have both, at different versions, so the installer picks the form that ships at the
**newer version** and installs only that one:
- Cache newer or equal, and readable → copy the `.opc`, verify the *copy* is one this
build can read, and only then delete any profile and vendor directory a previous
install left behind, so nothing can shadow it.
- Profile newer, or the cache unreadable or absent → copy the profile and the vendor's
preset JSONs exactly as the app did before caches existed, and delete any stale `.opc`
once the profile is safely in place.
One vendor that cannot be installed is one vendor missing, not a reason to leave the
rest uninstalled: the installer skips it, records the failure, and carries on with the
batch. A vendor whose cache arrives unreadable falls back to installing its profile,
which is decided by reading the copy rather than by the kilobyte peek that chose the
form.
**"Installed" means present and usable.** Where the cache is the whole of a vendor's
installation, a `.opc` this build cannot read is not an installation — counted as one,
the vendor would be stranded with nothing to load and the updater would never repair
it. The installed version is likewise whichever form a load would actually serve: the
cache's stamp while it covers the profile beside it, the profile's own version once it
does not.
The result is that only one form of a vendor is ever present, and it is the newest one
the build has. This matters most for the update check, which compares what is installed
against what installing *would* lay down: if those two disagreed about which form
counts, a vendor could reinstall on every launch forever, or silently never update.
Profile updates delivered over the air always arrive as JSONs, and they win — an
updated vendor's real profile lands in the data directory, the installed cache beside it
is older and gets rejected, and the vendor is parsed and re-cached. An update that touches only
the filament library needs nothing more: every other vendor's cache stays valid and
simply resolves against the new library on its next load.
## How the caches are produced
Cache generation is a build step, not something a user ever runs.
One script per platform does the whole job, and CI calls it once on each. It builds a
small dev-utility that loads a profiles directory exactly as the app would, with cache
writing enabled, dropping a `<vendor>.opc` beside every vendor profile it parses; then
it copies those caches into each packaged application it was pointed at and deletes
every preset JSON they replace — the vendor's own profile included. Only a vendor that
actually has a cache is pruned, so a vendor the generator skipped keeps its JSONs and is
simply parsed at startup.
Caches are generated into the checkout's own `resources/profiles`, because that is what
cpack re-installs from when it builds the NSIS installer — so that directory is also a
prune target in CI. Pruning it deletes the checkout's preset JSONs, which is a packaging
step, not something a build should do to a working tree by surprise: the Windows script
refuses that target unless given `--prune-source`, and CI passes it.
Generation runs after the build, in the same job, so the caches ship with a build that
can read them.
The flatpak differs only in where the script is called from. Nothing outside
flatpak-builder ever builds it, so there is no packaged tree for the workflow to point
the script at afterwards: the manifest runs it as a build step instead, against the
profiles the install has already copied into `/app`.
## Behavior when things go wrong
The system is designed so that no cache problem is fatal:
- **Corrupt, truncated or foreign file** — rejected at the header, vendor parsed. A
cache is written to a temp file beside its target and moved into place, so a write
that dies partway leaves the previous cache intact rather than a truncated one.
- **An option this build no longer has, or now types differently** — that option alone
is dropped, exactly as a JSON profile's would be. The preset and the file load.
- **Cache from a build with a different cache layout** — rejected on `CACHE_VERSION`.
A vendor with JSONs beside it is parsed and re-cached; a cache-only vendor reads as
not installed and the updater reinstalls it.
- **Stale cache** — rejected on the vendor version stamp, vendor parsed and re-cached.
- **Failure part-way through loading** — a deserialization error, or any entry that
fails to install — rejects the whole cache, and the bundle is reset to a clean state
before falling back, so a half-loaded cache can never leak into the parsed result.
- **A vendor that can be neither read nor parsed** — logged, and left out. The setup
wizard drops that vendor from its list and opens with the rest; startup records the
error alongside the vendors that did load. One broken vendor never takes the app down.
The one genuine limit: on a shipped build a vendor is its cache and nothing else, so a
rejected cache has nothing to fall back to for that vendor. This is by design — the
alternative is shipping every preset twice — and it is why the acceptance gates are
conservative and why CI generates the caches with the same build that ships them. The
recovery path is a profile update, which delivers real JSONs.
It also means nothing may quietly assume a `<vendor>.json` exists. Discovery, version
checks and the update decision all read whichever form is present, and a code path that
enumerates only `*.json` will find no vendors at all in a packaged build.
## Maintenance rules
- **Adding, removing, retyping or reordering a config option** needs nothing. The
payload names its keys and its enum values, so an option a cache carries and this
build does not is dropped; one this build has and the cache does not is simply
absent, as it would be from a JSON that predates it.
- **Changing a hand-written `serialize()`** — `VendorProfile` or its nested types — or
the `CachedPreset` field list — written and read by `visit_entry` in
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
65535 options and one cache at most 65535 distinct enum value names.
`CacheDictionary::save` throws past that, which surfaces when CI generates the
caches rather than on a user's machine.
- **Bumping `CACHE_VERSION` is safe without a resources fallback** because
`is_vendor_installed` means *present and usable*: cache-only vendors read as not
installed after a bump, and the updater reinstalls them from resources.
- **Bumping a vendor profile's version** invalidates that vendor's cache and nothing
else — the filament library's included. Other vendors' caches resolve against the
new library the next time they load.
- **Caches are never committed.** They are build artifacts, generated per build,
ignored by git.
## Where this lives in the tree
| Area | Files |
|---|---|
| Everything about the bytes on disk — the dictionary, one config's wire format, the file framing and stamps, entry serialization, `VendorCacheFile` save/load/peeks | `src/libslic3r/PresetCacheFormat.{hpp,cpp}` |
| Serve-or-parse decision, installing cache entries into a bundle, cache write-back | `src/libslic3r/PresetBundle.{hpp,cpp}` |
| Vendor profile serialization | `src/libslic3r/Preset.hpp` |
| Vendor discovery, installed/shipped versions, installation | `src/libslic3r/utils.cpp` (declared in `Utils.hpp`) |
| Update and reinstall decisions | `src/slic3r/Utils/PresetUpdater.cpp` |
| Setup wizard and printer-selection dialog | `src/slic3r/GUI/ConfigWizard.cpp`, `src/slic3r/GUI/WebGuideDialog.cpp` |
| Generator tool | `src/dev-utils/generate_system_cache.cpp` |
| Build and packaging script | `scripts/build_preset_cache.{sh,bat}` |
| Tests | `tests/libslic3r/test_vendor_cache.cpp` |

File diff suppressed because it is too large Load Diff

View File

@@ -1,397 +0,0 @@
# filament_id: generation rule + system-profile fix plan
Follow-up to PR #14459 (commit `c2e91cb8`, validator `-f` / `check_duplicate_filament_subtypes`).
Goal 1: a filament_id generation rule for all vendors. Goal 2: an error-pattern taxonomy and a
migration plan that removes every ambiguous filament_id, without touching Bambu (BBL) profiles.
All numbers below are reproducible: a loader-faithful audit script re-derives the validator's
output **exactly** (1256/1256 printer-level errors, 356 logical collision groups, 30 vendors).
Every code claim was verified against source with `file:line` references.
---
## 0. Executive summary
- `filament_id` is a **material-family id**: one id per commercial product line, shared by all
of that material's per-printer/per-nozzle variants. Matching is always `(filament_id +
printer compatibility)`; the invariant from PR #14459 is *per printer preset, at most one
compatible instantiated filament preset per id*.
- Every modern device ecosystem funnels through this id — not just Bambu AMS: Qidi box,
Creality CFS, Klipper AFC/Happy Hare, Snapmaker all emit/consume `tray_info_idx`
(see §1). Several consumers match **globally, without printer scoping**, so two *different
materials* sharing one id is unsafe even across vendors.
- **Proposed rule (§3):** deterministic, script-minted ids — `OF` + 6 base62 chars from
`uuid5(vendor + family)`, declared **only on family-root (`@base`) presets**; variants inherit.
Nobody ever invents an id by hand; CI prints the expected id when one is missing. Existing
unambiguous ids are grandfathered and frozen; `GF*` (Bambu), `QD_*` (Qidi device protocol),
and `P<7-hex>` (user custom presets) are reserved namespaces that must never be minted into.
This was selected by an adversarial design/judge process over a registry-grammar alternative
and a structure-only alternative, then stress-tested; amendments from that stress test are
folded in below.
- **Migration (§5):** fresh-never-reused ids only, family-atomic, names never changed. This is
provably safe: user presets re-derive `filament_id` from their parent on every load
(`Preset.cpp:1658-1682`), 3mf resolves presets by name+config (`Preset.cpp:2490-2576`), and
Klipper/Creality/Snapmaker derive tray ids at runtime. One PR per vendor; CI's `-f` scope
ratchets per vendor until tree-wide.
---
## 1. How filament_id actually works (verified)
### Consumers and scoping
All device integrations converge on one pipeline: device/agent sets `tray_info_idx`
`DevAmsTray.setting_id``Sidebar::build_filament_ams_list` (`Plater.cpp:3423-3493`) →
`PresetBundle::sync_ams_list` / `get_ams_cobox_infos` (`PresetBundle.cpp:3112-3308`) match it
against filament presets.
| Ecosystem | Where the id comes from | Effect of changing a system id |
|---|---|---|
| BBL AMS | device-side (RFID / user tray setting), `DeviceManager.cpp:3823+` | breaks matching — **frozen by mandate** |
| Qidi box | built from device enums: `"QD_" + series + "_" + vendor + "_" + type_idx`, `QidiPrinterAgent.cpp:146-152`; needs an exactly-matching visible preset | breaks matching — **`QD_*` ids are a frozen device contract** |
| Creality CFS | runtime brand/type scoring returns current preset's id (`CrealityPrintAgent.cpp:46-118`) | invisible |
| Klipper (AFC / Happy Hare) | runtime `filament_id_by_type` (`MoonrakerPrinterAgent.cpp:808,936`) | invisible |
| Snapmaker | runtime color/vendor/type match (`SnapmakerPrinterAgent.cpp:22-64`) | invisible |
Matching is printer-scoped (`is_compatible`) in the AMS sync paths and all printer agents — this
is what makes the per-printer invariant sufficient there. But several consumers match
**globally by id alone**, first match wins:
- `get_filament_by_filament_id("")` — tray display name, `filament_is_support`,
`temperature_vitrification` warnings (`PresetBundle.cpp:690-733`; callers
`DevFilaBlackList.cpp:70`, `Plater.cpp:3453`, `SelectMachine.cpp:3560,4623`). The code
comment at `:695` states the assumption outright: an id maps to ONE material globally.
- `MachineObject::setting_id_to_type` (`DeviceManager.cpp:2538`), calibration-history name
lookup (`CaliHistoryDialog.cpp:62`), custom-filament cloud grouping (`Preset.cpp:2839`).
- The slicing pipeline itself: multi-nozzle filament grouping merges project filaments whose
`(filament_id, color)` match (`FilamentGroup.cpp:513-528` via `ToolOrdering.cpp:1164`).
**Consequence:** within one printer, duplicate ids break AMS matching (silent first-wins,
`find_if` at `PresetBundle.cpp:3132/3233`; the AMS tray-edit dialog even *hides* the second
preset, `AMSMaterialsSetting.cpp:894-897`). Across vendors, the same id on *different
materials* feeds wrong name/type/vitrification data to the global consumers and can merge
different materials into one nozzle group. Same id on the *same* material (e.g. `GFL99` =
Generic PLA in 29 vendors) is comparatively benign — those attributes agree.
### Identity machinery
- **Effective id resolution** (`PresetBundle.cpp:4842-5080`): own `filament_id` key → vendor
`filament_id_maps[inherits]` (file order in the vendor index is load-bearing) →
OrcaFilamentLibrary base-bundle map. An instantiated system filament that resolves *no* id is
a hard load error that discards the whole vendor bundle (`:5072`, throw at `:5141-5147`) —
so "missing id" cannot ship; what looked like 26 id-less Flashforge presets actually resolve
to OFL's `OGFL99`/`OGFG99` through the base-bundle fallback.
- **Two family-identity systems exist**: `filament_id` (device matching) and `alias` (name
before `" @"`). OFL shadowing is keyed on **alias**: `update_library_profile_excluded_from`
(`Preset.cpp:3684-3714`) hides an OFL preset (empty `compatible_printers` = compatible with
everything, `Preset.cpp:837`) on printers claimed by a same-alias vendor preset. There is
**no id-based shadowing**. A vendor preset that tunes an OFL generic but renames it
re-exposes the OFL preset and creates a live duplicate. The rule below aligns the two
systems: one family = one alias = one id.
- **User-custom id space**: user-created filaments get `"P" + md5(name)[0:7]` (8 chars,
`CreatePresetsDialog.cpp:533`), or *reuse a system id* when the base name matches an existing
preset (`:510-528`). `"null"` is used as a sentinel. Root user presets persist their id
forever; inheriting user presets re-derive it on every load.
## 2. The id landscape today
**Bambu's grammar** (derived from all 1970 BBL instantiated presets; BBL is internally clean —
181 id definitions, 0 duplicates):
- Classic `GF<LETTER><NN>`: letter = family (A Bambu-PLA, B ABS/ASA, C PC, G PETG/PCTG,
L third-party+generic PLA, N PA/PPA, P PP/PE, R misc, S support, T PET/PPS, U TPU).
Numbers: 00-49 branded ascending, 50-59 fiber-filled, 60-70 partner block, **95-99 generic
tier descending** (99 = the family's plain generic).
- Brand partners `GF<BRANDCODE><NNN>`: GFPM Polymaker, GFOT Overture, GFSNL SUNLU, GFNMK
Numakers. One id per product line; never per color, never per printer/nozzle/diameter.
- Structural rule: the id lives on the material's `@base`; every variant inherits it.
- Hardcoded in C++: `GFS00/GFS01` support check (`DeviceManager.cpp:4739`), per-family PA
defaults `GFU01/03/04` (`CalibUtils.cpp:54-75`) — `GF*` is Bambu's space, byte-frozen.
**Everything else is ad-hoc, invented by individual contributors and imitated** (full history
in §7): OFL's `O`-prefix mirrors (`OGFA00`; introduced 2025-03-31, commit `8c4a65e3e1`),
Tiertime/Afinia `GFx##_##` per-printer-line suffixes, SeeMeCNC per-nozzle ids, LH `LHF_pla`,
LONGER 11-char pseudo-GF ids, Anycubic ids **with spaces** (`"GFPLA Silk"`), Prusa ids that are
entire preset names (36 chars), and mass copy-paste of `GFL99/GFB99/GFG99` onto everything
(Qidi alone stamped `GFB99` into **317 files** across all materials). The only guidance that
ever existed was "≤ 8 chars" — enforced for BBL only (`orca_extra_profile_check.py:292,320`),
and the (now removed) profile wiki's own examples *taught* id copy-pasting.
**The damage, quantified** (audit reproduces validator 1256/1256):
| Ledger | Count |
|---|---|
| Within-vendor logical collision groups (validator `-f`) | **356** across 30 vendors (1256 printer-level errors) |
| OFL×vendor same-id groups (validator blind spot) | 42 — of which **10 are live** (alias mismatch defeats shadowing); 32 already neutralized by alias shadowing |
| OFL-internal: one id, several materials, visible on every printer | **16 ids** (e.g. `OEPLAB00` = 14 distinct Elegoo PLA products; `OGFL06` = eSUN PLA-Marble *and* Fiberon PETG-ESD) |
| Cross-vendor semantic collisions (same id, different materials) | **67 ids** (e.g. `GFU99` also covers a PEBA; Anycubic minted `GFL95` "Matte" ≠ Bambu `GFL95` "High Speed") |
Worst vendors by groups: Qidi 97, Flashforge 69, Elegoo 37, Prusa 31, Cubicon 17,
Anycubic 14, InfiMech 14, Snapmaker 11, Artillery 8, Creality 8, FlyingBear 8.
---
## 3. The rule (proposal)
Selected by a 3-design / 2-judge adversarial process (deterministic-mint won over
registry-grammar and structure-only on ambiguity-prevention, contributor simplicity, and
enforceability), then hardened by three adversarial review passes. This section is written as
the future authoring doc.
### 3.1 The one-question test
> **Would a user consider this a different spool product than anything already in the tree?**
> Different polymer, different sub-brand (Basic / Matte / Silk / HF), fiber-filled sibling, or
> a second selectable diameter → **new family, new id**. The same spool tuned for another
> printer or nozzle → **join the existing family** (inherit its `@base`, no id key). Tuning a
> generic material → **join the OFL family** (inherit the `Generic X @System` preset, keep the
> `Generic X` base name, add no id).
Same id / new id at a glance:
| Situation | id |
|---|---|
| Per-printer / per-nozzle variant of an existing material | same id (inherit, never write the key) |
| Sub-brand or product line (PLA vs PLA Matte vs PLA Silk vs PLA HF) | new id each |
| Color | never a new id |
| Second diameter selectable on the same printer (1.75 + 2.85) | sibling family, new id |
| "High-speed" tuned for a *different printer model* | same id (it's a printer variant) |
| "High-speed" selectable *alongside* the normal preset on one printer | new id (it's a product line) |
### 3.2 Structure
1. **One family = one root.** Each material family has root preset(s) (`instantiation:false`,
typically `<Family> @base`) and only roots carry the `filament_id` key. Instantiated
variants inherit a root and never write `filament_id`. (A family MAY have several roots —
e.g. Qidi's per-series bases — but they must all declare the *identical* id.)
2. **Family identity is declared, not name-derived.** Default: the family name is the
instantiated presets' base name (name with `/\s?@.*$/` stripped — note *optional* space,
because `Afinia PLA@HS`-style names exist). When vendor naming makes that ambiguous, the
root declares an explicit `"filament_family"` key that overrides derivation; tooling errors
loudly when a root's derived family differs from its children's.
3. **Within a family, variants' `compatible_printers` are pairwise disjoint** — that *is* the
PR #14459 invariant, enforced by the validator.
4. **Generics belong to OFL.** A vendor tuning `Generic PLA` inherits
`Generic PLA @System`, keeps the `Generic PLA` base name/alias (so alias shadowing excludes
the OFL preset on those printers, `Preset.cpp:3684`), sets non-empty `compatible_printers`,
and writes no id. 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`). No id is ever recycled for a different material — stale ids live on in
user root presets and old 3mfs, and a recycled id would silently match the wrong material.
### 3.3 Minting — nobody invents ids
New family ids are computed, exactly like the `setting_id` precedent
(`scripts/assign_vendor_setting_ids.py` / `Slic3r::generate_preset_setting_id`):
```
filament_id = "OF" + base62_6( uuid5( NAMESPACE, "filament_family/<vendor>/<family>" ) )
```
8 chars total (satisfies the AMS length limit), same base62 derivation and a dedicated
namespace constant. On the astronomically rare collision with an existing id, the minter salts
the input (`.../1`, `/2`, …) until free; the result is simply frozen in the file.
- **Script path:** author commits the family with *no id anywhere*;
`python scripts/assign_filament_ids.py` inserts the minted id into the root(s). Idempotent;
never rewrites a valid existing id. A `--mint "<Vendor>/<Family>"` one-shot prints the id
without touching the tree.
- **No-script path:** CI fails with the exact line to paste:
`family "MyBrand PLA" (vendor X) needs filament_id "OFq3xT9k" in "MyBrand PLA @base.json"`.
**Reserved namespaces — never mint or hand-write into:**
| Space | Owner | Status |
|---|---|---|
| `GF*` | Bambu AMS/RFID catalog | byte-copies of authentic Bambu ids only, and only where a checked-in `shared_catalog` list sanctions the family (BBL bundle; OFL mirrors; byte-matching families in other vendors, e.g. Snapmaker's Fiberon) |
| `QD_*` | Qidi device protocol | frozen; Qidi-only; exempt from family-shape checks |
| `P[0-9A-Fa-f]{7}`, `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp:533`) | never emitted for system presets (reserve case-insensitively) |
| everything already shipped | grandfather snapshot | frozen as-is (§5) |
Trade-off accepted: minted ids are opaque (`OFq3xT9k` carries no "PLA" mnemonic — the family
name in the same file provides that). The judges preferred this over a Bambu-style extended
grammar because it removes the "who allocates the next number" ceremony, cannot race between
concurrent PRs, and needs no registry maintenance. If mnemonic ids are strongly preferred, the
runner-up design (`<NSCODE><FAMLETTER><SEQ>` + registry file) is documented in the workflow
records; everything else in this plan is unchanged under either format.
### 3.4 What CI enforces (all vendors, ratcheted)
Extend `scripts/orca_extra_profile_check.py` (it imports the same mint function; setting_id
precedent) and the C++ validator:
1. **Format**: an id is valid iff `OF[0-9A-Za-z]{6}` **or** in the grandfather snapshot **or**
vendor==BBL **or** `QD_*` in Qidi. No whitespace/ASCII/length checks needed outside the
grandfather set — new ids are minted, and the grandfather set is closed.
2. **Uniqueness ratchet**: id→families multimap computed tree-wide; no id may acquire a family
claim not recorded in the snapshot (snapshot legalizes today's benign `GFL99`-style sharing;
new multi-claims are errors; `shared_catalog` entries are the sanctioned exception).
3. **Structure ratchet**: `filament_id` key only on roots; every instantiated filament must
resolve an effective id via the simulated loader walk; all members of one family resolve the
same id; **a preset may not declare an id different from its inherited effective id** (the
`Generic SBS` drift bug class). Pre-existing violations are snapshot-frozen; new ones error.
4. **Mint conformance**: an id new relative to the snapshot must equal the mint (or a salt
iteration); the error message prints the expected value.
5. **Stability**: an `(id, family)` pair on main may not change or vanish, *following
`renamed_from` chains* (so an honest rename passes), unless listed in a maintainer-gated
migrations file. Retired ids go to an append-only `retired_ids` ledger; a retired id may
never be defined again for any family.
6. **Alias hygiene for tuned generics**: a preset inheriting an OFL `Generic * @System` (with
no own id anywhere in its vendor chain) must keep the OFL base name and have non-empty
`compatible_printers` — error message names the rename as the cause.
7. **C++ validator**: extend `check_duplicate_filament_subtypes` (`PresetBundle.cpp:5654`) to
include OFL presets in every vendor's per-printer check, *minus* alias-excluded ones —
`m_excluded_from` is already populated in the validator context (`update_system_maps` at
`PresetBundle.cpp:2302`), so the 32 shadowed pairs won't false-positive and the 10 live ones
will be caught. Run `-f` per vendor in CI, widening as vendors are cleaned (§5).
8. **Runtime backstop** (one-line change): log a warning when the AMS-sync `find_if`
(`PresetBundle.cpp:3132/3233`) finds 2+ compatible presets for one id — the only layer that
can see side-loaded/forked bundles.
---
## 4. Error-pattern taxonomy (goal 2) — with counts and fixes
356 groups were classified by 37 agents reading the actual profiles, spot-checked
independently (1 substantive disagreement in 14 samples). Counts below fold the spot-check
corrections in. **Fix rule for all patterns: replacement id values are always freshly minted
`OF*`; the "which preset keeps the id" decision uses the precedence _Bambu-catalog material >
OFL generic family > family that historically introduced the id_.**
| # | Pattern | Groups | Fix |
|---|---|---|---|
| P1 | `copy_paste_id` — different materials share an id verbatim (task's error 1). Qidi's `GFB99`×317-files epidemic; Anycubic's three id "eras"; Peopoly `GFSL99` on ABS | ~167 | impostor families get minted ids on their roots; owner keeps the id |
| P2 | `wrong_inherits` / id-less product lines — a variant inherits another family's root (PR #14459's Panchroma case) or a product line never got its own root (Prusa HF; Flashforge's ~140-preset `FFG01` umbrella) | ~87 | create per-family roots with minted ids; re-point `inherits`; **never** delete the id-less shadow/base files — convert them (they carry real config: Flashforge `fdm_filament_pla` differs materially from OFL's) |
| P3 | `generic_family_overlap` — vendor-*branded* preset rides a generic family id via inheritance (task's error 2, generalized). Includes the 10 live OFL duplicates | ~68 | branded presets get minted family ids; true generic tunings instead adopt the OFL family *with matching alias* (rule 3.2.4); Sovol is the elegant case — just **delete** its wrong own-id lines and let OFL ids flow through inheritance |
| P4 | `overclaim_compat` — same material, broader variant claims a printer that a dedicated variant covers (the BBL H2DP pattern fixed in #14459) | ~24 | trim `compatible_printers` of the broader preset (Dremel, Cubicon `@base`s that are also instantiated, Wanhao France Bowden/Direct) |
| P5 | template-carried id — id declared on a shared settings template (`fdm_filament_*`, `fdm_filament_common`) so every family inheriting it collapses (Prusa, Ginger Additive, Snapmaker TPU base) | inside P1/P2 counts | move ids off templates onto family roots |
| P6 | OFL-internal collisions — 16 ids spanning several materials, visible on every printer (Elegoo blocks, Elas `OGFA00`×3, `OGFL06` polymer mismatch, `Generic PETG HF/PETG-CF @System` missing own ids) | 16 ids | fix inside OFL first (it's the base bundle every vendor resolves against) |
| P7 | alias-mismatch re-exposure — vendor tunes a generic under a different name, OFL preset resurfaces (Snapmaker `PolyTerra J1 PLA` vs OFL `PolyTerra PLA`) | 10 live | rename-to-alias where it's genuinely the same family, else mint |
| P8 | per-variant ids — no ambiguity, but family semantics broken: every variant has its own id (Prusa name-ids, SeeMeCNC nozzle suffixes, Afinia/Tiertime `_##`, iQ) so device matching can't identify the material across nozzles | ~210 presets | grandfather (they're unambiguous); converge opportunistically; document as anti-pattern |
| P9 | format violations — spaces (`"GFPLA Silk"`), >8 chars (LONGER, SeeMeCNC, LH), GF-shaped inventions (Anycubic `GFL93-97`, CoLiDo `GFA99`) | in the above | fixed as a byproduct of re-minting; snapshot freezes the unambiguous rest |
| P10 | cross-vendor semantic collisions — 67 ids meaning different materials in different vendors (dangerous via the global unscoped consumers, §1) | 67 ids | mostly eliminated by P1-P3 re-minting; the remaining same-material generic sharing is legalized by the snapshot |
| P11 | deliberate coexistence & data hygiene — Snapmaker "Benchy" demo presets (gated by `compatible_prints`, which the id check can't see) and a self-collision from duplicate `compatible_printers` entries | 4 | give demo presets own minted ids; dedupe list entries; add a lint for duplicate array entries |
New patterns beyond the two in the task (goal 2.3): P5-P11.
---
## 5. Migration plan
### Safety foundation (verified, §1/§7)
Safe: fresh never-used ids, family-consistent; trims of `compatible_printers`; inherits
re-pointing; OFL id changes (children re-derive). Unsafe: touching `BBL`/`QD_*`; recycling or
swapping ids; splitting a family's id; **deleting or renaming preset names** (user presets
whose `inherits` no longer resolves are dropped at load, `Preset.cpp:1687-1691`) — if a name
must go, `renamed_from` coverage is mandatory.
### Phases
0. **Land the rule + tooling first** (no profile changes): `scripts/assign_filament_ids.py`
(mint + insert + `--mint`), the extended `orca_extra_profile_check.py` checks in
snapshot-ratchet mode, the C++ validator OFL cross-check, the runtime warning, the rule doc.
Generate `filament_id_snapshot.json` (id→families multimap over main) and empty
`retired_ids.json` — both checked in.
1. **OFL first** (it's the base bundle every vendor resolves against): fix the 16 internal
collisions (Elas/eSUN/DREMC copy-pastes get mints; `Generic PETG HF/PETG-CF/PP-CF/PP-GF/
PE-CF/PLA Matte @System` get their own family roots+ids instead of collapsing into their
parent generic), keeping every current effective id that is unambiguous.
2. **Per-vendor PRs, worst-first**: Qidi → Flashforge → Elegoo → Prusa → Cubicon → Anycubic →
InfiMech → Snapmaker → the long tail (22 vendors, mostly 1-8 one-line fixes). Each PR flips
that vendor into CI's `-f` scope (`check_profiles.yml` currently `-v BBL -f`; append
vendors as they reach zero; when all are in, drop `-v` and run tree-wide).
3. **Delete the ratchet allowlists** once tree-wide zero holds; checks become hard rules for
everything born after the snapshot.
### Migration-script contract (from the adversarial pass — important)
- Consumes from the classification only: group membership, fix category, keep-id precedence,
inherits-repoint targets. **All replacement id values are recomputed via the mint** — id
literals in analysis notes (e.g. `GFA00_02`, `GFS98`, `GFG96`) are legacy-culture artifacts
and are ignored with a warning; assert no emitted id matches `^(GF|QD_|P[0-9A-Fa-f]{7}$)`.
- **Family-atomic**: re-idding any preset re-ids every same-family sibling in the same commit,
even siblings outside the collision group (FlyingBear `GFB99 @S1` vs `@Ghost7`); Prusa is
family-atomic per material (its HF/CF families span frozen `_N`-suffix ids — freeze what's
unambiguous, mint once per family for the colliding members).
- **Diff bound**: a vendor migration PR may only touch collision-group files + same-family
siblings of re-idded presets; every id that is per-printer-unambiguous today stays
byte-identical.
- **Config-equivalence gate**: dump every instantiated preset's flattened effective config on
main and on the PR head; the diff must be empty except `filament_id`/`inherits`/
`compatible_printers` edits the plan prescribes (this is what makes the Flashforge
shadow-file conversions safe: each id-less `fdm_filament_*` shadow becomes a named vendor
family root carrying its config byte-for-byte, children re-pointed, then the shadow name
retired).
- **No deletions**: redundant presets (Flashforge's byte-identical `Generic X`/`Flashforge X`
twins) are re-minted, not dropped; consolidation with `renamed_from` is a separate,
human-reviewed cleanup.
- Expected user impact: none for inheriting user presets, 3mfs, Klipper/Creality/Snapmaker
sync. Residue: user *root* presets that copied an old system id keep it forever
(AMS auto-match falls back to generic-by-type — low severity, unavoidable from the repo).
### Vendor-specific notes (from classification)
- **Qidi (97)**: three profile generations. `QD_*` generics are correct and frozen; the fix is
the brand families (Bambu/HATCHBOX/Overture/PolyLite/Tinmorry/QIDI-brand) that all carry
`GFB99/GFG99/GFL99`. Multi-root families are the norm (`...@Q2-Series` / `@Q2C-Series` /
`@X-Max 4-Series` bases) — same mint lands in every series root of one family.
- **Flashforge (69)**: two umbrellas (`FFG01` ~140 presets; `GFB99/GFG99/GFL99` G3U-era) +
OFL-riding branded presets + shadow-file conversion (above).
- **Elegoo (37)**: single mistake — every commercial variant inherits the material-class
`@base` (`E<MAT>B00`); mint one id per product line (Silk/Matte/PRO/Rapid/…), roots exist.
- **Prusa (31)**: HF product lines need their own roots; ids move off `fdm_filament_*`
templates; frozen name-shaped and `_N` ids stay.
- **Sovol (6)**: delete the wrong own-id lines; correct ids flow from OFL by inheritance.
- **Cubicon (17)**: `@base` presets are themselves instantiated + over-claiming; 9 file edits.
---
## 6. Concrete work items (PR-sized)
1. `scripts/assign_filament_ids.py` + mint function + tests (incl. the 14-group regression set
from the adversarial pass). *(new)*
2. `orca_extra_profile_check.py`: checks §3.4-1..6 in ratchet mode + snapshot/ledger files;
drop the BBL/OFL-only gate at `:292` and the OFL skip at `:601`. *(extend)*
3. C++ validator: OFL-aware `check_duplicate_filament_subtypes`; runtime ambiguity warning at
the two `find_if` sites. *(small)*
4. Rule documentation: `doc/developer-reference/filament_id.md` (recreate the path; wiki
cross-link) + profile-PR template checkbox ("new materials: no filament_id key anywhere; CI
prints the minted id"). *(new)*
5. OFL migration PR (phase 1).
6. Per-vendor migration PRs (phase 2), each widening CI `-f` scope.
The audit tooling from this analysis (loader-faithful resolver; reproduces the validator
1256/1256) is in this session's scratchpad (`audit_filament_ids.py`) and is the natural seed
for items 1-2.
## 7. Evidence & methodology
- Validator ground truth: `OrcaSlicer_profile_validator -f` tree-wide → 1256 errors; audit
script reproduces exactly (356 logical groups after dedup by (vendor, id, preset-set)).
- Code analysis: 5 parallel agents over runtime consumers, persistence/migration surface,
loader semantics, Bambu grammar, and convention history — all claims carry `file:line`.
- Classification: 37 agents (one per vendor chunk) reading actual profile JSONs; 14-sample
independent re-derivation found 1 substantive error (a Flashforge group mislabeled
`missing_id`; corrected — the loader resolves those ids from OFL).
- Design: 3 independent designs → 2 judges (both chose the deterministic mint; scores 58/50/56
and 60/54/56) → 3 adversarial attackers (35 scenarios; every `breaks` finding is folded into
§3.2-3.4/§5 as an amendment: declared families, multi-root support, ratchet-not-absolute
checks, no-deletion rule, shadow-file conversion, classification-id-literal quarantine,
diameter siblings, case-insensitive P-hex reservation, fork guidance).
- Key history: `OGF*` born 2025-03-31 (`8c4a65e3e1`, no PR); `_##` suffix born PR #9739;
8-char check born PR #9574; wiki (with the id-copy-paste example) removed 2025-11-24
(`f0d79b99eb`).
## 8. Open decisions for maintainers
1. **Id format**: opaque deterministic `OF*` mint (recommended, judges 2/2) vs mnemonic
registry grammar (runner-up). Everything else in the plan is format-agnostic.
2. **Multi-vendor brands** (Snapmaker ships Fiberon with authentic `GF*` ids): sanction via
`shared_catalog` (recommended, low churn) vs hoisting those families into OFL.
3. **Benchy-style demo presets**: mint ids per demo preset (recommended) vs teaching the
validator `compatible_prints` gating.
4. **Alias alignment**: fix the 10 live alias-mismatch OFL duplicates by rename-to-alias
(better long-term, needs `renamed_from`) vs minting vendor ids (safer, more ids).
5. **Where the rule doc lives**: in-repo `doc/` (recommended — CI messages need a stable link)
vs wiki-only.

View File

@@ -1,314 +0,0 @@
# filament_id plan v2: the `get_filament_id` (P+md5) proposal, validated
> **Status note:** the verdict in §0-§4 and §7-§8 stands. The work items in §5 are
> superseded by `filament_id_plan_v3.md` (OFL-as-catalog, content-addressed triple key,
> succession ledger): W1/W3 are carried into v3.0, W2/W5 carry unchanged, W4 is dropped.
Follow-up to `filament_id_plan.md` (v1, implemented on `feature/filament_id`: deterministic
`OF*` mint + snapshot ledger + 30-vendor migration, tree-wide validator `-f` = 0).
**Proposal under review:** mint system `filament_id`s with the scheme of
`static std::string get_filament_id(std::string vendor_typr_serial)`
(`src/slic3r/GUI/CreatePresetsDialog.cpp:487-552`) — Bambu's user-custom-filament id
allocator — so system ids are "compatible with Bambu AMS sync".
Every claim below was verified in source (`file:line`), against upstream BambuStudio, or in
primary online sources (URLs in §8). Verdict first, evidence after.
---
## 0. Executive summary — the verdict
**Do not re-mint system ids as `P+md5(name)`. Keep the v1 `OF*` mint.** Three facts decide it:
1. **The AMS/device path never validates id *shape*.** `tray_info_idx` is an opaque string
end-to-end: sent verbatim (`DeviceManager.cpp:1642`), parsed verbatim
(`DevFilaSystem.cpp:512-514`), and firmware persists arbitrary bytes — an A1 stored and
echoed a *corrupted* id `Pde2\xea58c` (bambulab/BambuStudio#5436). A P-shaped system id
is not "more acceptable" to the device than an `OF*` one.
2. **P-shape buys zero recognition.** The printer resolves tray ids only against its built-in
`GF*` catalog plus the *account's* cloud custom-filament cache (official Bambu wiki); ids
it cannot resolve round-trip as `?`. Orca can never upload system presets into that cache
(`Preset.cpp:2071` gates cloud upload on `is_user()`), so an Orca system `P` id shows `?`
exactly like an `OF` id. The public catalog id space is 100% `GF*` (ha-bambulab
`filaments.json`: 86/86 ids).
3. **P-shape is the one shape the codebase punishes.** The *only* id-shape dispatch in the
entire tree is `MachineObject::check_ams_filament_valid` + `update_filament_list`
(`DeviceManager.cpp:5335/5352/5395/5410`, gate `setting_id.size()==8 && [0]=='P'`), a
user-custom lifecycle reconciler that **remotely wipes an AMS tray**
(`command_ams_filament_settings(..., "", "", white, "", 0, 0)` at `:5345`) or rewrites its
temps (`:5361-5367`) when a P-shaped tray id drops out of the user-root preset list
(`:5219`, armed at `:5268`). Name-keyed minting makes system id == user-custom id *by
design* (same name → same id), so "user deletes their now-redundant custom after the
system preset ships" becomes a remote tray wipe. `GF*` (wrong length) and `OF*` (wrong
first char) are structurally immune.
The proposal's *real* value lives in two places, and v2 captures both without the re-mint:
- **The adopt step** (the function's first branch, `:509-511/:526-528`): same base name →
same id. In-app, this already gives Orca-local convergence today — a user custom named
like a system family adopts the system id *whatever its shape*. At authoring time, the
same semantics = the v1 `shared_catalog` mechanism; §5/W4 extends it (curated, optional)
to let byte-authentic Bambu-catalog families carry authentic `GF*` ids.
- **The hash step belongs to user space.** `P+md5(name)` is Bambu's allocator for *user*
presets; the community is already asking for it there (OrcaSlicer PR #13315, unique ids
for inherited user presets). §5/W5 endorses that lane. System profiles stay out of the
`P` namespace — and v2 adds client hardening (W1) because ten *already-shipped* system
P-hex ids (Cubicon `P510cf**`×8, Ginger `P510eff9`, Artillery `Pfcf9c4c`) sit inside the
wipe-gate's shape match today.
Judge panel (3 independent lenses over 4 variants; §7): keep-`OF*` won 23/40 aggregate
(device-ecosystem 6, identity-semantics 8, migration-enforceability 9) vs full Bambu
emulation 11, hash-only name-keyed 12, vendor-scoped P-hash 12. The single dissent
(device-ecosystem, favoring adoption) is honored by W4; its own verdict on the shape was
"adopt the semantics, not the costume".
---
## 1. What the proposed generator actually is (verified)
`get_filament_id(vendor_typr_serial)` (`CreatePresetsDialog.cpp:487-552`), called with
`"<vendor> <type> <serial>"` (`:1128`, `PLA-AERO``"PLA Aero"`), and per-preset with the
display base name when cloning presets for a user-created printer
(`Preset.cpp:2785-2803``:2798-2800`):
1. **Adopt:** scan a temp bundle of *every* vendor bundle on disk
(`PresetBundle::load_system_filaments_json`, `PresetBundle.cpp:2342-2391`) plus the
user's presets, then the live bundle (`Preset.cpp:2831-2842`); if any preset's base name
(`name.substr(0, first('@') - 1)`) equals the input and its id ≠ `"null"`, **return that
id** — which can be `GF*`, `QD_*`, `OF*`, or another user's P-hex.
2. **Mint:** `"P" + md5(input)[0:7]`, lowercase hex over the raw UTF-8 bytes (`:533`,
`:480`).
3. **Collide:** if the id is held by a *different* name, re-hash with a local wall-clock
salt (`md5(input + get_curr_time())`, `:547`) — non-deterministic by construction.
Upstream parity: the function and its caller input construction are **byte-identical** in
BambuStudio master (`CreatePresetsDialog.cpp` L434-499 / L1061; diff empty, fetched
2026-07-03). So identical dialog inputs in BBS and Orca yield identical ids — the kernel of
the proposal's compatibility claim.
Reproduction limits (why "exactly the approach" cannot be ported to system minting anyway):
- The base-name parser truncates names lacking a space before `@` (`"Afinia PLA@HS"`
`"Afinia PL"`); 48 shipped presets hit this, and the *clone* path parses differently
(`" @"` two-char, `Preset.cpp:2792`), so the app itself is internally inconsistent.
- 597 presets (361 whole families, e.g. all of Anker) contain no `@` and are invisible to
the adopt scan and its collision map (`:505-508`).
- The collision salt is wall-clock time — unreproducible across machines, so id values
cannot be CI-verified from names; a ledger would still be the sole source of truth.
- Shipped P-hex ids don't round-trip from names today: Cubicon `P510cfb0`
`P+md5("Cubicon ABS")` = `Pc624b68` (verified) — so even Bambu-lineage P ids are not
name-derivable in practice.
## 2. What Bambu AMS sync actually consumes (verified)
- Send: `command_ams_filament_settings` puts the preset's `filament_id` verbatim into
`tray_info_idx` (`DeviceManager.cpp:1642`); cloud vs LAN differ only in transport
(`:2502-2536`). No client- or firmware-side rejection path exists; the ack parse stores
whatever the printer echoes (`:3808-3860`).
- Resolve (slicer side): tray id → first compatible root preset with equal `filament_id`
system *and* user roots, no shape filter (`PresetBundle.cpp:3151`, `:3252-3254`); misses
fall back by `"Generic <type>"` name, then similarity, then keep-previous
(`:3157-3169`, `:3260-3305`). `setting_id_to_type` resolves against `is_system` presets
only (`DeviceManager.cpp:2538-2559`) — a system preset resolves *because it is system*,
never because of its id shape.
- Resolve (device side): built-in `GF*` catalog + the account's cloud custom cache;
custom-filament sync is **cloud-only** (not LAN), and an id the printer has no data for
displays `?` until opened once on the printer screen (official wiki, §8). Third-party
consumers see customs as "unknown" (ha-bambulab #466/#540).
- Since Jan 2025, AMS configuration is gated behind Bambu-signed clients on new firmware
(Developer Mode / older firmware exempt) — any AMS-sync benefit for Orca is conditional
on that regardless of id scheme (Bambu authorization blog, §8).
- Orca already ships non-`GF`/non-`P` shapes to Bambu trays in the field: OFL presets are
compatible with every printer (`PresetBundle.cpp:5681-5684`) and carry `OGF*`/`OFL*` ids
(e.g. `OFLSBS99`, hardcoded in `MoonrakerPrinterAgent.cpp:654`). The premise "the
ecosystem has only ever seen `GF*`/P-hex" is already false, with no observed rejection.
**Cross-client reality check.** The name-keyed benefit ("a BBS-created custom named like an
Orca system preset lands on the same id, so Orca's sync matches the system preset") is
mechanically true but narrow: it needs byte-exact names (including the `PLA Aero` mapping
and single spacing), the unsalted hash path, cloud mode, and permissive firmware. The
symmetric case already works in Orca *without* any re-mint: an Orca user creating a custom
named like a system family **adopts the system id** via the in-app adopt branch, so local
custom ↔ system ↔ tray matching is shape-independent today.
## 3. The disqualifying finding, in mechanism form
`update_filament_list` (`DeviceManager.cpp:5208-5272`) snapshots `{filament_id → temps}`
over **user root presets only** (`preset.is_user() && preset.inherits() == ""`, `:5219`);
ids that vanish or change temps between snapshots are armed into `checked_filament`
(`:5268`). `check_ams_filament_valid` (`:5311-5445`) then, for every AMS/virtual tray whose
`setting_id` matches `size()==8 && [0]=='P'`:
- id armed and **not** in the current user-root list → remotely **clears the tray**
(`:5335-5349`, `:5395-5408`);
- id armed and in the list with unequal temps → **rewrites tray temps from the user
preset**, ignoring same-id system presets (`:5352-5374`; `PresetBundle.cpp:3719` skips
non-user presets; unguarded `find()->second` at `:3717`).
Failure scenario under the proposal: system `"PolyLite PLA"` ships with
`P+md5("PolyLite PLA")`. A user's same-named custom (BBS- or Orca-created — same id by
design) is on an AMS tray. The user deletes the now-redundant custom. Next status refresh:
the id is armed, absent from user roots → Orca wipes the tray, even though the system
preset still resolves that id perfectly. Runs continuously (`StatusPanel.cpp:3275-3277`).
Two corollaries independent of the proposal (→ W1):
- The ten shipped P-hex **system** ids (Cubicon/Ginger/Artillery) already pass the shape
gate; only the "id must transit a user root" arming condition protects them, and the
in-app adopt branch can create exactly that transit today.
- `assert(it->first.size() == 8 && it->first[0] == 'P')` (`:5252`) is violable today: a
user custom named `"Bambu PLA Basic"` adopts `GFA00` onto a user root and aborts debug
builds.
## 4. Identity costs the re-mint would add (quantified, tree dry-run)
- 1148 distinct family base names; **198** appear in 2+ vendors; **159** groups carry ≥2
distinct ids today and would silently merge under name-keying (Generic PETG alone: 15
ids today). 112 involve BBL; 82 are `GF↔OGF` mirror pairs.
- **4** cross-vendor groups share a name across materially different `filament_type`
(Generic PLA Silk, Generic PETG-CF, Generic PA6-CF, Generic PE-CF) — one merged id would
feed wrong type/name/vitrification to the global unscoped consumers
(`PresetBundle.cpp:690-733`; `DeviceManager.cpp:2538`; `FilamentGroup.cpp:513-519`) (→ W3).
- 33 deliberately-split families on the branch would re-merge (25 Elegoo-vs-OFL pairs, 7
FlyingBear-vs-InfiMech "Other *" pairs, the salted Cubicon PC pair) — undoing v1
decisions that the fixture gate forced.
- Renames become id migrations: name-keyed ids re-key on any marketing rename; the ledger
can freeze them, but then name-derivability — the scheme's selling point — dies family
by family. The `OF*` key (`vendor/family`) survives display-name churn.
- Entropy drops 35.7 → 28 bits (still 0 collisions at today's 1148 names; ~0.75 expected
at 20k). Re-mint surface: 391 unshipped ids across 1327 files, 13 hardcoded tests, the
reserved-namespace policy inversion, and a retired-ledger rebuild.
## 5. Work items (delta from `feature/filament_id` HEAD)
**W1 — client hardening (C++, small, ships with the migration PR train).**
a. Skip the tray-reset and temp-rewrite in `check_ams_filament_valid` when **any
`is_system` filament preset carries the tray's id** — no compatibility filter,
mirroring the semantics of the global consumers (`get_filament_by_filament_id`,
`setting_id_to_type`). This protects the ten shipped P-hex system ids and any BBS
custom colliding with them, while ids resolving only to user presets (or to nothing)
keep today's cleanup behavior.
b. Relax the debug assert `DeviceManager.cpp:5252` to a log (user roots legitimately
carry adopted `GF*`/`OF*` ids).
c. Guard `PresetBundle.cpp:3717` (`find()` unchecked before `->second`).
d. Regression-check that AMS sync never clears trays whose id it merely cannot resolve
(OrcaSlicer#4431 class): the keep-previous fallbacks at `PresetBundle.cpp:3163-3169`,
`:3296-3305` must cover the UI paths.
**W2 — tooling + doc guardrails.**
a. Document `^OF[0-9A-Za-z]{6}$` as the system-mint namespace. **Do not add it to
`reserved_space_owner`** (`scripts/assign_filament_ids.py:394-402`): check 6 exempts
only snapshot-grandfathered claims and the `--update-snapshot` refusal gate
(`:596-621`) would then reject every *future* mint. The space is already enforced —
check 3 (mint conformance, `:488-509`) errors on any OF-shaped id that does not equal
its own family's mint (covered by `test_check3_of_id_must_match_mint`), and the
snapshot diff is the human gate. Add one doc paragraph + a test asserting a foreign
vendor claiming another family's OF id fails check 3.
b. `doc/developer-reference/filament_id.md`: add a "why not P-hex" section citing the
`DeviceManager.cpp:5335` gate and the account-cache resolution model — community
pressure toward `P+md5` exists (PR #13315) and will recur.
c. Document that user roots may legitimately hold adopted system ids (incl. what that
means for cloud upload, `Preset.cpp:1874-1876`), matching BBS behavior.
**W3 — data hygiene: the 4 same-name-different-type groups.** Audit each (they confuse
name-based fallback matching and any future interop even under vendor-scoped ids):
verify whether the divergent `filament_type` is a data bug (e.g. OFL `Generic PETG-CF`
resolving `PETG`, OFL `Generic PE-CF` resolving `PE`, Flashforge `Generic PLA Silk`
mixing `SILK`+`PLA`, Creality-vs-Elegoo `Generic PA6-CF` as `PA-CF`/`PA6-CF`) or
intentional; fix by type alignment or rename with evidence, gated by
config-equivalence (only `filament_type` diffs as prescribed) + full validator suite.
**W4 — optional, curated GF adoption (the proposal's adopt step, done safely).** Extend the
v1 `--allow-shared-catalog` mechanism into an explicit per-family adoption worksheet: a
family may carry a byte-authentic Bambu catalog id iff (i) it is verifiably the same
commercial product as the BBL family (vendor evidence + equal `filament_type`), (ii)
validator `-f` stays 0 tree-wide (alias shadowing covers BBL/OFL overlaps), (iii) the
fixture gate passes, (iv) the snapshot diff records the adoption. Candidates: the 13
actionable BBL-overlapping families minted `OF*` in v1 (e.g. Qidi `Bambu ABS``GFB00`,
`PolyLite PLA``GFL00`, `Overture PLA``GFL04`, `PolyLite ABS``GFB60`; `OFLSBS99` is
shipped and frozen; the two type-hazard OFL families are excluded until W3 lands).
**Honest benefit statement:** these presets are compatible only with non-Bambu printers,
so no Bambu device resolves their ids today; the payoff is one-product-one-id coherence
for the global consumers, project portability, and readiness for W6. Recommended seed:
the four Qidi brand families; generics deferred.
**W5 — user-preset lane (where the proposal's hash belongs).** Support unique ids for
inherited user presets (the PR #13315 pain: children share the parent's id, so AMS always
resolves the generic parent). `P + md5(preset name)` matching
`CreatePresetsDialog::get_filament_id` is correct *in user space*: adopt-first against
existing ids, then hash; ids frozen per preset after mint. Constraints: never emitted
into system profiles (W2a makes that mechanical); W1 must land first so shared-id
lifecycles can't wipe trays.
**W6 — recorded future option:** SoftFever's "upload OrcaSlicer's filament database
(especially the filament ID) to the printer" (PR #12724 comment). If pursued, Orca ids
become first-class device-visible artifacts and the P-mimicry question is permanently
moot; the `OF*` scheme is the *better* citizen there (collision-free vendor-scoped ids,
no user-custom masquerade).
Unchanged from v1: mint rule, snapshot/retired ledgers (no OF retirement — nothing is
re-minted), CI wiring, migration commits. `filament_id_plan.md` remains the implemented
baseline; this document records the v2 decision and its follow-on work.
## 6. Gates (per work item)
Same battery as v1, all local: `python scripts/orca_extra_profile_check.py` exit 0;
`assign_filament_ids.py --check` exit 0 (46+ unit tests green; new tests for W2a
reservation and, when implemented, W4 adoption sanctioning); validator `-l 2` exit 0,
`-f` tree-wide exit 0, `-r` BBL+Qidi exit 0; custom-preset fixture overlays (the W1/W4
changes touch exactly the preset-visibility machinery the fixtures exist to protect);
config-equivalence: flattened effective configs differ only in prescribed keys
(`filament_id` for W4, `filament_type` for W3, none for W1/W2). W1 additionally needs a
manual AMS smoke test on a live Bambu printer (tray set/clear round-trip) — a release
checklist item, not CI (the workflows have no hardware runners).
## 7. Scheme comparison (judge panel record)
| Variant | Device lens | Identity lens | Migration lens | Σ |
|---|---|---|---|---|
| V1 full Bambu emulation (adopt incl. `GF*` + P+md5 + salt) | 7 | 2 | 2 | 11 |
| V2 hash-only name-keyed `P+md5(base name)` | 4 | 4 | 4 | 12 |
| V3 vendor-scoped P-hash (`P+md5(vendor/family)`) | 2 | 5 | 5 | 12 |
| **V4 keep `OF*` uuid5 vendor-scoped (chosen)** | 6 | 8 | 9 | **23** |
All three judges answered the pivotal question the same way: the P-hex **shape** confers no
device/cloud benefit (resolution is by value against catalog + account cache; the path is
otherwise shape-agnostic) and is the only shape with a destructive client-side gate. V3 is
strictly dominated (all of the re-mint cost, none of the name-keyed benefit). V1's genuine
half — GF adoption — is captured by W4 at zero re-mint cost.
## 8. Evidence index
Code (this branch): `CreatePresetsDialog.cpp:487-552,1128,2798-2800` (generator + callers);
`DeviceManager.cpp:1642` (verbatim send), `:2538-2559` (system-only type resolve),
`:5208-5272` (user-root snapshot, `:5219`, `:5252` assert, `:5268` arming), `:5311-5445`
(P-shape gate + tray wipe `:5335/:5345`, temp rewrite `:5352-5374`, virtual tray
`:5395-5435`); `DevFilaSystem.cpp:512-524`; `PresetBundle.cpp:690-733,3116-3128,3151,
3252-3254,3709-3767,5674-5753`; `Preset.cpp:1874-1876,2071,2785-2842`;
`AMSMaterialsSetting.cpp:886,894-897`; `WebGuideDialog.cpp:67`;
`PresetComboBoxes.cpp:1952-1966`; `MoonrakerPrinterAgent.cpp:654`;
`scripts/assign_filament_ids.py:69,71-72,118-144,394-402,596-621`.
Online (fetched 2026-07-03): BambuStudio master `CreatePresetsDialog.cpp` L434-499
(byte-identical generator); wiki.bambulab.com `create-filament` (custom filaments AMS-able
from firmware 1.6.6; only dialog-created presets sync) and `custom-filament-issue`
(cloud-only sync; unknown ids show `?`); blog.bambulab.com authorization-control (Jan-2025
AMS-config gating); bambulab/BambuStudio#5436 (firmware persists corrupted id);
OrcaSlicer #4431 (tray clobber to `?`), #3874 (cloud-only confirmation), PR #14459
(the `-f` invariant), PR #14423 + PR #12724 + PR #13315 (maintainer statements / community
P+md5 pressure); greghesp/ha-bambulab `filaments.json` (86/86 `GF*`) + #466/#540.
Method: 6 parallel evidence agents (generator semantics; AMS/device path; exhaustive
shape-dispatch sweep; upstream/online; tree-wide dry-run over 5892 presets / 1455 families;
branch change inventory) → 3-judge panel (device-ecosystem, identity-semantics,
migration-enforceability lenses). Dry-run artifacts: session scratchpad
`phex_dryrun_{summary,details,followup}.json`.
## 9. Open decisions for maintainers
1. **W4 scope**: none / 4 Qidi brand families (recommended) / + generic-tier re-adoption.
2. **W5 timing**: implement in-repo vs review upstream PR #13315 with the W1 hardening as a
prerequisite either way.
3. **W6**: pursue the filament-database-upload design (makes Orca ids device-visible and
ends the mimicry debate for good).

View File

@@ -1,213 +0,0 @@
# filament_id plan v3: OFL as the catalog, content-addressed product ids
Supersedes §5 of `filament_id_plan_v2.md` (whose P+md5 verdict stands unchanged) and
revises the mint rule of `filament_id_plan.md` (v1, implemented on `feature/filament_id`).
Decision driver: v1's mint key scoped families to the *profile bundle* (printer brand), so
one commercial product tuned in N bundles carries N ids — e.g. PolyLite PLA ships in five
bundles (`BBL`, `OrcaFilamentLibrary`, `OrcaArena`, `Qidi`, `Snapmaker`), all with
`filament_vendor: "Polymaker"`, under up to five different ids (`GFL00`, `OFceJcLf`, …).
That contradicts what `filament_id` means: one id per commercial product line.
## 0. The architecture
Three tiers, two frozen islands:
1. **BBL island — untouched.** Bambu profiles keep their `GF*` catalog ids byte-for-byte
(device/RFID/cloud contract). No convergence between GF and OFL ids is attempted: the
same product may permanently carry `GFL00` on the BBL side and an `OF*` id everywhere
else. This is a deliberate trade for simplicity; v2's curated-GF-adoption (old W4) is
dropped.
2. **`QD_*` island — untouched.** Qidi's box builds `QD_<series>_<vendor>_<type>` from
device enums and requires exactly-matching presets (`QidiPrinterAgent.cpp:146-152`).
Qidi presets carrying `QD_*` are exempt from re-minting, forever.
3. **Everything else converges on OrcaFilamentLibrary.** OFL is the product catalog: a
material family's id is declared once, on its OFL family root. Vendor bundles carry
only per-printer *specializations* of OFL families — same base name, non-empty
`compatible_printers`, **no `filament_id` key** — which (a) alias-shadow the OFL preset
on the printers they claim (`Preset.cpp:3684-3714`: "Generic PLA @Qidi Q2 0.4mm nozzle"
hides "Generic PLA @System" on that machine) and (b) resolve the OFL family's id
through the loader's inherits/base-bundle walk (`PresetBundle.cpp:4842-5080`). Products
OFL does not (yet) carry mint their id in the vendor bundle **with the same rule**;
because the key is bundle-independent (§1), later hoisting the family into OFL never
changes its id.
Convergence therefore happens by *single declaration point*, not by copying ids across
bundles — no adoption registry, no cross-bundle id claims to curate.
## 1. The mint rule
```
triple = (filament_vendor, filament_type, family_name) # from the family ROOT's flattened config
key = "filament_product/<filament_vendor>/<filament_type>/<family_name>"
id = "OF" + base62_6( uuid5( FILAMENT_ID_NAMESPACE, key ) ) # namespace unchanged:
# c4d3ff49-4c32-5534-a3e3-00894157ab97
```
- **Triple resolution.** All three values come from the family root preset's *flattened
effective config* (`filament_vendor` and `filament_type` are inheritable list options —
take the first element; family_name = the root's base name, `\s?@.*` stripped once).
For own-key-layout families (no root), each declaring preset's flattened config is used
and CI requires all declarers of one family to agree on the triple. A root that resolves
an empty `filament_vendor` or `filament_type` is a CI error (generics use `"Generic"`).
Values enter the key verbatim (UTF-8, no case folding).
- **Bundle-independent by design.** The key contains no bundle name, so the same product
yields the same id whether minted in OFL or in a vendor bundle; migrating a family into
OFL (§0.3) is id-stable. Two bundles independently adding the same triple converge
automatically — and that is correct, because equal (vendor, type, name) *is* the
definition of "same product" here. The known look-alike hazards stay separated by the
type component: the 4 same-name-different-type groups (Flashforge `Generic PLA Silk` as
SILK, OFL `Generic PETG-CF` as PETG, `Generic PA6-CF`, `Generic PE-CF`) hash apart until
their type bugs are fixed (§5, W3) and converge automatically after — self-healing that
pure name-keying (rejected in v2) could not provide.
- **Format and salt unchanged from v1**: 8 chars, `OF` + 6 base62; deterministic `/1`,
`/2`… salt past any taken or retired id (the O+7 widening was evaluated and rejected:
62⁶ expects 1.9×10⁻⁵ collisions at today's 1,455 families, and `^O.{7}$` would sweep 35
legacy ids into the conformance gate vs. one today).
- **Content-addressed, deliberately.** If any triple component changes — a family rename,
a `filament_vendor` correction, a `filament_type` fix — the id changes with it. This
*replaces* v1's "ids are immutable once shipped" with "ids are derivable from the
product identity, and identity changes are migrations" — made safe by the succession
ledger (§2). `renamed_from` still gates preset-name compatibility as before.
- CLI: `--mint "Polymaker/PLA/PolyLite PLA"` prints without touching the tree; running
the script plain inserts missing ids; CI errors print the expected id.
## 2. The succession ledger (the amendment that makes this safe)
Shipped ids are referenced outside the tree: AFC/Klipper lane data, Bambu AMS trays
holding OFL-only materials, on-device PA-calibration records, user-root preset copies,
3mf `slice_info`. Retiring an id without a forwarding pointer downgrades all of those to
`Generic <type>` fallbacks. Therefore:
- **Schema.** `scripts/retired_filament_ids.json` entries become objects:
`{"retired": {"OGFL99": {"claims": [...], "successor": "OFxxxxxx"}}}`. Append-only as
before; a successor may itself be retired later (chains allowed, cycle-checked, and
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 → <OFL Generic PLA id>`): consulted only when no live
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 <type>` name fallback, in:
`PresetBundle::get_filament_by_filament_id` (covers `DevFilaBlackList`, `SelectMachine`
warnings, `Plater` tray configs in one place), the AMS sync match predicates
(`PresetBundle.cpp:3151`, `:3252-3254` miss paths at `:3157-3169`/`:3260-3305`),
`PresetComboBoxes::add_ams_filaments`, `MachineObject::setting_id_to_type`
(`DeviceManager.cpp:2545` miss branch), calibration-history name lookup
(`CaliHistoryDialog.cpp:62`), and the #14423 Moonraker lane matching when it lands.
With this in place the OFL re-mint is near-residue-free and every future
content-addressed rename stays safe.
- **Kill the hardcoded generic map.** `MoonrakerPrinterAgent::map_filament_type_to_generic_id`
(`MoonrakerPrinterAgent.cpp:608-658`) hardcodes 23 OFL ids (`OGFL99``OFLSBS99`).
Replace it with a runtime lookup of the OFL generic preset by name ("Generic PLA
@System" → its current id), removing the code↔profile lockstep permanently. (This also
releases `OFLSBS99`, v2's one frozen OF-shaped legacy id, for normal re-minting.)
## 3. Validation rule changes
- **Check 3 (mint conformance)** becomes a pure function of the root's triple: a non-BBL,
non-`QD_*` id must equal `mint(triple)` ± salt, or be snapshot-grandfathered (the
grandfather set shrinks to ≈ nothing for non-BBL once migration completes).
- **Check 5 (alias hygiene) generalizes and becomes load-bearing.** For *every*
OFL-carried family (not just `Generic * @System`): a vendor specialization must keep the
OFL base name (else the OFL preset un-shadows and creates a live per-printer duplicate —
v1's P7 pattern) and non-empty `compatible_printers`, and must not declare an id. The
C++ validator's OFL-aware `-f` (`PresetBundle.cpp:5674-5753`) already enforces the
runtime consequence; the script check names the rename as the cause.
- **New check 8 (triple integrity):** every id-declaring family resolves a complete,
family-consistent triple; roots missing `filament_vendor`/`filament_type` error.
- **New check 9 (succession integrity):** every retired entry's successor chain ends at a
live tree id (or a documented cross-island hint target); no cycles; retired ids absent
from the tree.
- Snapshot mechanism, reserved spaces (`GF*`→BBL, `QD_*`→Qidi, `P`-hex/`null`→user-custom;
v2's do-not-reserve-`OF*` note stands), and check 1/2/4/7 are unchanged.
## 4. Migration phases
**v3.0 — tooling + prerequisites (no profile changes).** New mint + triple resolver
(reuse the flatten machinery), succession schema migration + C++ lookup helper wired into
the §2 miss paths, Moonraker map → runtime lookup, checks 3/5/8/9, tests. Carry v2's W1
client hardening (skip tray-wipe/temp-rewrite when a system preset holds the id; relax
the `DeviceManager.cpp:5252` assert; guard the `PresetBundle.cpp:3717` deref) — it ships
first regardless. **W3 type fixes land here**, *before* any re-mint: type is now a key
component, so minting before fixing OFL `Generic PETG-CF`/`Generic PE-CF` would re-id
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 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). Implementation notes
(v3.1, executed): OFL generic ids relocate from the fdm_filament_* template bases onto
the product-named "Generic X @System" presets first, so their triples carry the product
name; `fdm_filament_pc` keeps a (transitional) declaration because 7 Prusa presets
inherit it directly — the v3.2 Prusa worksheet re-homes them and deletes it.
**v3.2 — vendor bundles, worksheet-per-vendor (v1 machinery).** Three sub-cases:
(a) the **391 unshipped v1 `OF*` mints** re-derive under the triple key — no succession
entries (they never shipped; a documented one-time `--forget-never-shipped <list>` drops
them from the ledger lineage instead of retiring them, since the ledger's rationale —
ids live on in user presets and 3mfs — cannot apply to unreleased ids);
(b) **true generic tunings** riding copied legacy ids (`GFL99`-class) drop their own id
and re-point to the OFL generic family (the v1 Sovol pattern) — their old ids get
cross-island hints where BBL owns them, succession entries otherwise;
(c) remaining **shipped legacy ids** (numeric, name-shaped, pseudo-GF, the 57
multi-vendor GF residue, the 10 P-hex — everything non-BBL/non-`QD_*`, ~800 ids)
re-mint with succession entries. Retiring the 10 P-hex system ids also removes the last
system ids from `check_ams_filament_valid`'s destructive P-gate.
**v3.3 — ongoing consolidation (optional, per-vendor, id-stable).** Hoist vendor-unique
products into OFL where a family is genuinely multi-vendor material; thanks to the
bundle-independent key this never changes ids, so it can proceed opportunistically.
## 5. Gates (every phase)
`python scripts/orca_extra_profile_check.py` exit 0; `assign_filament_ids.py --check`
exit 0; unit tests green (existing 46 + new triple/succession/adoption tests); validator
`-l 2` exit 0, `-f` tree-wide exit 0, `-r` BBL+Qidi exit 0; custom-preset fixture
archives; config-equivalence — flattened effective configs differ only in `filament_id`
(re-mints), `inherits`/`compatible_printers` (re-points, as prescribed per worksheet), and
the W3 `filament_type` corrections. New for v3: a C++ test that a retired id resolves
through the succession chain in the sync miss path, and a Moonraker test that the generic
map lookup matches the shipped OFL presets. Manual AMS smoke test (tray set → old-id
resolve → clear) stays a release-checklist item.
## 6. Accepted costs (explicit)
- **GF ↔ OFL divergence is permanent** for products in both catalogs (PolyLite PLA ≠
`GFL00` outside BBL). The forward-looking fix is PR #12724-style filament-database
upload, where Orca ids become first-class device artifacts.
- **Renames/type-fixes re-id families** (by design); the succession ledger absorbs the
device/user residue, but each one is still a ledger entry and a snapshot diff to review.
- **User roots** keep whatever id they copied at creation; with succession lookup in
`get_filament_by_filament_id` they now *resolve* instead of dangling — strictly better
than v1's accepted residue.
- **One-time field transition**: devices holding pre-v3 ids (AFC lanes, AMS trays, cali
records) resolve via succession on updated clients; *older* Orca versions and
BambuStudio never resolved OFL-only ids anyway (`?`/generic fallback — unchanged for
them).
## 7. Open decisions
1. Whether v3.2(b)'s cross-island hints (`GFL99` → OFL generic) are wanted at launch or
deferred (pure-miss-path feature; zero risk to BBL installs, small review surface).
2. v3.3 pacing: per-vendor PRs opportunistically vs. a dedicated consolidation train.
3. Whether to fold v2's W5 (P+md5 ids for inherited *user* presets, PR #13315) into v3.0's
C++ work or keep it a separate PR (recommended: separate; W1 is its only prerequisite).
## 8. Evidence
Carried from v2 §8 (all re-verified this session), plus: `filament_vendor` audit — 49
distinct strings tree-wide, `"Polymaker"` byte-consistent across all five PolyLite-PLA
bundles; multi-vendor different-family GF residue = 57 ids; `MoonrakerPrinterAgent.cpp:
608-658` = 23 hardcoded OFL ids; mint examples verified live (`Qidi/PolyLite PLA`
`OFceJcLf` under the v1 key — the fragmentation this plan removes; salt determinism
`OF8afiMO`). Alias shadowing and the OFL id fallback verified at `Preset.cpp:3684-3714`
and `PresetBundle.cpp:4842-5080` during the v1 audit; per-printer `-f` semantics at
`PresetBundle.cpp:5674-5753`.

View File

@@ -1,224 +0,0 @@
# filament_id plan v4: dissolve the Qidi `QD_*` island
Supersedes §0.2 of `filament_id_plan_v3.md` (the "`QD_*` island — untouched, forever"
decision) and amends every v3 section that carved out `QD_*`. Everything else in v3 —
the mint rule, the succession ledger, the BBL island, checks 19 — stands as
implemented (v3.0v3.2, all gates green at commit `f7c1b290fd`).
Decision driver: the island contradicts the catalog architecture v3 built. Qidi presets
carry 204 `QD_<series>_<vendor>_<typeidx>` ids over 264 declarations (49 families,
measured 2026-08-21) — one commercial product carries up to *five* ids (one per printer
series: `QIDI PLA Rapido` = `QD_0_1_1``QD_4_1_1`), which is exactly the
fragmentation v3 exists to remove. The island was frozen because Qidi's filament box
composes these ids from device enums and requires exactly-matching presets
(`QidiPrinterAgent.cpp:146-152`). But v3.0 shipped the machinery that makes freezing
unnecessary: the succession ledger already translates retired ids on resolution miss.
`QD_*` stops being a *preset id space* and becomes a *device protocol namespace*,
translated once at the agent edge.
## 0. The architecture change
Two tiers, **one** frozen island:
1. **BBL island — untouched, unchanged.** All v3 reasoning holds (device/RFID/cloud
contract is external and opaque).
2. **Everything else converges on OFL product ids — now including Qidi.** All 204
`QD_*` ids re-mint from their family triples and gain **retired** ledger entries
(not cross-island hints: after dissolution there is no island to own them, and
check 4's "never again" is exactly the guard we want against upstream re-adding
them). The box keeps working because `QidiPrinterAgent` resolves its composed
`QD_*` id through `resolve_filament_id_succession()` on miss — the same mechanism
every other retired id already uses.
Why retire rather than hint (the one real design choice here): hints exist for ids a
*foreign catalog* owns and may legitimately re-ship (`GF*`). Post-dissolution, nothing
may ever re-ship a `QD_*` id in the profile tree — the device composes them at
runtime, the tree translates them. Retired entries make CI enforce that permanently
(`--update-snapshot` refuses resurrections, check 4 refuses occurrences); hints would
permit re-shipping, which is now always a regression. The QD→family mapping is
strictly 1:1 (verified: no `QD_*` id is claimed by more than one family), so the
mode-rule successor is unambiguous for every entry.
## 1. What the dissolution consists of
Three independent work packages, ordered for bisectability:
- **(A) Re-converge the drifted tree** — prerequisite, not Qidi-specific. The
2026-07 main merge (`f3fa3a34bd`) brought upstream vendor updates in pre-v3 style:
`--check` currently exits with **243 errors** (123 unsanctioned new ids, 46 new
instantiated own-key presets, 31 override drifts, 18 reserved-space claims, 5
hint-keys re-declared — Qidi re-added `GFB99`/`GFG99`/`GFL99`, Snapmaker re-added
`GFG96`/`GFU99` and even four *retired* ids incl. `OGFL99`, plus Snapmaker U1
triple errors and a GreenGate3D rename). This is the standing WF-B maintenance
pass; the dissolution's gates cannot go green on a red base.
- **(B) C++ succession hook in the Qidi agent** — safe to land before any data
changes (pure miss-path: while `QD_*` presets still exist, the hook never fires).
- **(C) The dissolution proper** — tooling flip + Qidi data fixes + re-mint +
snapshot/ledger regeneration, one vendor worksheet in the v3.2 mold.
## 2. Runtime translation (work package B)
`QidiPrinterAgent.cpp:183-192` currently: compose `setting_id` → keep it if a visible
base preset declares it → else degrade to `filament_id_by_type(tray_type)` (i.e. every
QIDI-brand box slot silently becomes Generic once the ids re-mint). Insert the
succession walk between those two steps:
```cpp
} else if (!setting_id.empty() && has_visible_base_preset(bundle->filaments, setting_id)) {
tray.tray_info_idx = setting_id;
} else {
// Retired QD_* protocol ids forward to their minted successors via the shipped ledger.
const std::string successor = setting_id.empty() ? std::string()
: resolve_filament_id_succession(setting_id);
if (!successor.empty() && has_visible_base_preset(bundle->filaments, successor))
tray.tray_info_idx = successor;
else
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
}
```
`resolve_filament_id_succession` is `Preset.hpp:119` (loads once, cycle-guarded,
empty-safe) — no new includes needed beyond what the file already reaches through
`PresetBundle`. This one hook covers both composition paths: the numeric-series
`build_setting_id` lambda *and* the non-numeric fallback
`map_filament_type_to_setting_id` (`:325-342`), whose four hardcoded returns
(`QD_1_0_1`/`_11`/`_41`/`_50` = Generic PLA/ABS/PETG/TPU 95A) become ledger keys in
package C. Keep that function as-is but extend its comment: the returned ids are
retired ledger keys by design, and `scripts/tests/test_filament_id.py` parses the
initializer (see §4 tests) — the Moonraker treatment (`MoonrakerPrinterAgent.cpp:
619-627`) of replacing the table with name lookups was considered and not taken: the
table already routes through the same ledger as the composed ids, and two translation
mechanisms in one agent is worse than one.
Tests (same commit):
- `tests/libslic3r/test_filament_id_succession.cpp`: add a section asserting a
`QD_`-shaped key forwards like any other (`{"QD_2_1_11", "OFnew001"}` resolves to
`"OFnew001"`) — pins that the walk is prefix-agnostic.
- `scripts/tests/test_filament_id.py`: new test parsing the four `QD_` literals out of
`QidiPrinterAgent.cpp::map_filament_type_to_setting_id` (mirror the parser in
`scripts/test_moonraker_lane_data.py`) and asserting each is a ledger key whose
chain terminates at a live tree id. **Add it marked expected-fail/skipped until
package C lands, then flip it on** — it is the permanent code↔ledger lockstep guard.
## 3. Tooling and validation flip (work package C, first commit)
All in `scripts/assign_filament_ids.py`; every touched line measured 2026-08-21:
- `is_island_declaration` (`:327-329`) → `return vendor == "BBL"`. This single change
pulls every `QD_*` declarer into the triple bookkeeping (`:385`) and thus into
checks 3 and 8, into `--remint`'s domain (`:1464`), and out of the hint-key
tolerances (`:877`, `:1100`).
- `reserved_space_owner` (`:596-604`): `QD_*` returns `(True, None)` — reserved,
ownerless, exactly like the P-hex/user-custom space. Consequences, all wanted:
check 6 refuses any future vendor claim; the `--update-snapshot` sanction gate
(`:938-963`) refuses new `QD_*` ids outright; the vanish path (`:994`) routes
`QD_*` to **retired** (owner `None` ≠ island), not released-with-hint. Update the
two message sites that render `owner is None` as "reserved for user-custom presets"
(`:628` docstring, `:958`, and check 6's copy) to name the space generically or
special-case `QD_*` ("Qidi device protocol; dissolved island — retired, never
declarable").
- Check 1 (`:667-668`): delete the `vendor == "Qidi" and fid.startswith("QD_")`
exemption.
- Module docstring (`:27-45`): rewrite the `QD_*` bullet — reserved space stays
listed, but as "device protocol namespace, translated via the succession ledger;
dissolved as a catalog island in v4, may never be declared".
- `scripts/tests/test_filament_id.py`: update the three island assertions —
`reserved_space_owner("QD_X4_PLA")``(True, None)` (`:403`),
`is_island_declaration("Qidi", "QD_X4_PLA")``False` (`:413`), and the
reserved-space message case (`:570`).
No changes to the ledger schema, the C++ checks, `--retire` (post-flip it accepts
`QD_*` olds automatically — they are non-island now), or the validator: check 3's
"non-BBL, non-`QD_*`" phrasing in v3 §3 was always implemented as "non-island", so the
flip *is* the spec change.
## 4. Migration phases
**v4.0 — re-converge the drifted tree (package A).** Per-vendor WF-B worksheets over
the 243 errors: Snapmaker U1 (triple divergence `Generic|Snapmaker|snapmaker`, empty
vendors, four resurrected retired ids — these force re-mints since retirement is
permanent), BBL/addnorth `GF_AN*` overrides (BBL island: grandfather via snapshot,
they are BBL-internal), Qidi/Snapmaker re-declared hint keys (re-mint those declarers,
the check's own prescription), GreenGate3D rename, then `--update-snapshot` (new
upstream `QD_*` ids sanction cleanly — the island is still intact in this phase) and
the full v3 §5 gate battery. Bump `version` in every touched
`resources/profiles/<Vendor>.json`. **Do not start v4.2 until `--check` exits 0.**
**v4.1 — the agent hook (package B).** §2 as written; `libslic3r_tests` +
`scripts/tests/test_filament_id.py` green; behavior-neutral by construction (no `QD_*`
ledger entries exist yet).
**v4.2 — the dissolution (package C).** One Qidi worksheet, v3.2 machinery:
1. Tooling flip commit (§3). `--check` now reports the Qidi island as
non-conformant — expected, red only between commits of this phase.
2. Data fixes, before any re-mint (the v3 "W3 lands first" lesson — type/vendor are
key components): add `filament_vendor: ["QIDI"]` at each QIDI-brand family's
inheritance apex so all declarers resolve it (39 of 49 families currently resolve
none — check 8a would refuse the mint). Verify the six Generic families
(`Generic PLA/ABS/PETG/PC/TPU 95A/PLA Silk`) resolve triples identical to their
OFL counterparts (vendor `Generic`, OFL's `filament_type`) so they *converge onto
the OFL ids* by triple math — the whole point; any mismatch is a W3-style data fix
here, not a fork. No `inherits`/`compatible_printers` re-pointing anywhere: the
series intermediates (`Generic PLA@Q2-Series` etc., all `instantiation: false`)
simply keep declarations whose values become the OFL ids.
3. `python scripts/assign_filament_ids.py --remint Qidi` — rewrites all 264
declarations in place to their family mints (same triple ⇒ same id across a
family's series intermediates and per-nozzle declarers; convergence with OFL ids
is legal by design, `want_id` already permits same-triple collisions `:1447`).
4. `--update-snapshot` — retires every vanished `QD_*` id with its mode-rule
successor. **Audit the ledger diff: all ~204 new entries must be `QD_*`→non-null.**
A null successor (possible only for a declared-only id with zero instantiated
claims) gets an explicit `--retire "QD_x=OFy"` with the family's minted id.
5. Un-skip the §2 lockstep test. Bump `resources/profiles/Qidi.json` version. Full
gate battery (§5).
Known residue, accepted: `QIDI PC-ABS-FR` (series 12) vs `QIDI PC/ABS-FR`
(series 34) are two preset-name families for one product → two ids. Renames were
ruled out in v3 (`renamed_from` rejected for migrations); if upstream ever unifies the
name, content-addressing re-ids and the ledger absorbs it — self-healing, no action
now.
**v4.3 — OFL consolidation (unchanged).** v3.3 stays optional and id-stable; QIDI-brand
families are vendor-unique and stay in the Qidi bundle.
## 5. Gates (per phase, delta from v3 §5)
Unchanged battery: `orca_extra_profile_check.py` exit 0, `assign_filament_ids.py
--check` exit 0, `scripts/tests/test_filament_id.py` all green, `libslic3r_tests`
green, validator `-l 2` tree-wide + `-f` tree-wide + `-v Qidi` exit 0, custom-preset
fixture archives (v4.2 touches no preset visibility, but they are cheap insurance —
run them for v4.0, which touches instantiation-adjacent upstream drift), flatten
config-equivalence. New for v4.2: equivalence diff may contain **only** `filament_id`
value changes (QD→OF) and the added `filament_vendor` keys on QIDI-brand families;
ledger diff audit per §4.4; the map-literal lockstep test. Manual release-checklist
item: Qidi box smoke test — slot holding a QIDI-brand material must surface the brand
preset (not Generic) on an updated client, via the memory-documented local Klipper
test rig.
## 6. Accepted costs (explicit, new relative to v3)
- **Every Qidi box slot resolves through the ledger miss-path forever** (one hash-map
walk per slot per status poll — negligible, and structurally identical to how
every retired id already resolves). The QD→OF translation table is maintained by
the retirement machinery, not by hand.
- **Older Orca clients** (pre-v4 profiles) paired with re-minted profile trees lose
QIDI-brand slot matching (they look up `QD_*` and fall back generic-by-type — the
degradation the hook removes for updated clients). Same one-time field-transition
shape v3 §6 already accepted for AMS/AFC ids.
- **Two ids for PC-ABS-FR** until upstream unifies the family name (§4 residue).
## 7. Evidence
Measured on this tree 2026-08-21 unless cited to v3: 204 distinct `QD_*` ids / 264
declaring presets / 49 families, QD→family strictly 1:1, per-series id sets
(`QD_0..4_1_1` = `QIDI PLA Rapido` etc.); 39 families resolve no `filament_vendor`;
declarations live on `instantiation:false` series intermediates (Q2/Q2C/X5/X4) and
per-nozzle X-Plus-4 presets; composition + miss-fallback at
`QidiPrinterAgent.cpp:146-152, 183-192`, hardcoded fallback table `:325-342`;
succession helpers `Preset.hpp:109-119`; ledger = 575 retired + 194 hints, zero `QD_*`
entries; snapshot holds 160 of the 204 (the 44 newcomers are post-merge drift);
`--check` = 243 errors, categorized in §1(A); island exemption mechanics at
`assign_filament_ids.py:327-329, 385, 596-604, 667, 877, 994, 1464, 1514`; `--remint`
same-triple convergence guard `:1447-1453`; retirement-permanence gate `:965-976`.

View File

@@ -1,6 +1,6 @@
{
"type": "filament",
"filament_id": "OFLfywkp",
"filament_id": "GFB00_01",
"setting_id": "wAJTMxtCY7EoavRi",
"name": "Afinia ABS+@HS",
"from": "system",

View File

@@ -1,6 +1,6 @@
{
"type": "filament",
"filament_id": "OFV5wEMe",
"filament_id": "GFB00_01",
"setting_id": "qCDnb2iBaz4hd4vX",
"name": "Afinia ABS@HS",
"from": "system",

View File

@@ -1,6 +1,6 @@
{
"type": "filament",
"filament_id": "OF9HCdyQ",
"filament_id": "GFA00_01",
"setting_id": "N3sCgjdjvp6FTtw9",
"name": "Afinia PLA@HS",
"from": "system",

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_tpu",
"from": "system",
"setting_id": "zUqTgAEbqTN1EdRl",
"filament_id": "OFDJU6R3",
"filament_id": "GFU01_01",
"instantiation": "true",
"filament_vendor": [
"Afinia"

View File

@@ -1,6 +1,6 @@
{
"type": "filament",
"filament_id": "OFXi59OX",
"filament_id": "GFB00_01",
"setting_id": "OxIiEYjbhEvSykaQ",
"name": "Afinia Value ABS@HS",
"from": "system",

View File

@@ -1,6 +1,6 @@
{
"type": "filament",
"filament_id": "OFgHh8ly",
"filament_id": "GFA00_01",
"setting_id": "BASsUdyvElEVJ9AA",
"name": "Afinia Value PLA@HS",
"from": "system",

View File

@@ -3,6 +3,6 @@
"name": "Anker Generic ABS @base",
"inherits": "fdm_filament_abs",
"from": "system",
"filament_id": "OF3P0BQA",
"filament_id": "GFB99",
"instantiation": "false"
}

View File

@@ -3,6 +3,6 @@
"name": "Anker Generic ASA @base",
"inherits": "fdm_filament_asa",
"from": "system",
"filament_id": "OFRiyes1",
"filament_id": "GFB98",
"instantiation": "false"
}

View File

@@ -3,6 +3,6 @@
"name": "Anker Generic PA @base",
"inherits": "fdm_filament_pa",
"from": "system",
"filament_id": "OFMgJQCZ",
"filament_id": "GFN99",
"instantiation": "false"
}

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PA-CF @base",
"inherits": "fdm_filament_pa",
"from": "system",
"filament_id": "OF7uE5KB",
"filament_id": "GFN98",
"instantiation": "false",
"filament_type": [
"PA-CF"

View File

@@ -3,6 +3,6 @@
"name": "Anker Generic PC @base",
"inherits": "fdm_filament_pc",
"from": "system",
"filament_id": "OFGPJxnt",
"filament_id": "GFC99",
"instantiation": "false"
}

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PETG @base",
"inherits": "fdm_filament_pet",
"from": "system",
"filament_id": "OFgvOmM2",
"filament_id": "GFG99",
"instantiation": "false",
"filament_type": [
"PETG"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PETG-CF @base",
"inherits": "fdm_filament_pet",
"from": "system",
"filament_id": "OF1daEUW",
"filament_id": "GFG98",
"instantiation": "false",
"filament_type": [
"PETG-CF"

View File

@@ -3,6 +3,6 @@
"name": "Anker Generic PLA @base",
"inherits": "fdm_filament_pla",
"from": "system",
"filament_id": "OFndmfJ1",
"filament_id": "GFL99",
"instantiation": "false"
}

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PLA Silk @base",
"inherits": "fdm_filament_pla",
"from": "system",
"filament_id": "OFXyyfX6",
"filament_id": "GFL96",
"instantiation": "false",
"filament_cost": [
"20"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PLA+ @base",
"inherits": "fdm_filament_pla",
"from": "system",
"filament_id": "OFIYTy8S",
"filament_id": "GFL95",
"instantiation": "false",
"filament_cost": [
"25"

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic PLA-CF @base",
"inherits": "fdm_filament_pla",
"from": "system",
"filament_id": "OFB0a3hT",
"filament_id": "GFL98",
"instantiation": "false",
"filament_type": [
"PLA-CF"

View File

@@ -3,6 +3,6 @@
"name": "Anker Generic PVA @base",
"inherits": "fdm_filament_pva",
"from": "system",
"filament_id": "OFsCbVbN",
"filament_id": "GFS99",
"instantiation": "false"
}

View File

@@ -3,7 +3,7 @@
"name": "Anker Generic TPU @base",
"inherits": "fdm_filament_tpu",
"from": "system",
"filament_id": "OF6tKa1Z",
"filament_id": "GFU99",
"instantiation": "false",
"filament_retraction_speed": "90",
"filament_deretraction_speed": "50",

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "RDI26qeSRSTe0PDg",
"filament_id": "OF223rZv",
"filament_id": "GFB99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra 3 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "Moc1cylP6mmSQjlb",
"filament_id": "OF223rZv",
"filament_id": "GFB99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra 3 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "2xgfnUKyOUQwYWPa",
"filament_id": "OF223rZv",
"filament_id": "GFB99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra 3 Max 0.8 nozzle"

View File

@@ -4,14 +4,11 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "NOiAgxA0BbvLhR1v",
"filament_id": "OF223rZv",
"filament_id": "GFB99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra S1 0.4 nozzle"
],
"filament_vendor": [
"Anycubic"
],
"filament_flow_ratio": [
"0.95"
],

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "MDWaiSjl89E6Edsn",
"filament_id": "OF223rZv",
"filament_id": "GFABS",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra S1 Max 0.25 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "wZjVYc7dBDSKRusJ",
"filament_id": "OF223rZv",
"filament_id": "GFABS",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "AbB6JtoPRsNgmxyy",
"filament_id": "OF223rZv",
"filament_id": "GFABS",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "exX565xnrLPkYkvu",
"filament_id": "OF223rZv",
"filament_id": "GFABS",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "KeEk3yQZrKmJY9Q0",
"filament_id": "OF223rZv",
"filament_id": "GFABS",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ABS @Anycubic Kobra X 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "Anycubic Generic ASA",
"from": "system",
"setting_id": "U66DGcpZeZ24VfPe",
"filament_id": "OF0yFLkY",
"filament_id": "GFA99",
"instantiation": "true",
"filament_vendor": [
"Anycubic"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "06j8OTuYw3Sh9BxL",
"filament_id": "OF0yFLkY",
"filament_id": "GFA99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ASA @Anycubic Kobra 3 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "s2XJm0JghCSUDzhh",
"filament_id": "OF0yFLkY",
"filament_id": "GFA99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ASA @Anycubic Kobra 3 Max 0.8 nozzle"

View File

@@ -4,14 +4,11 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "rf792xZNR7tRGKNg",
"filament_id": "OF0yFLkY",
"filament_id": "GFA99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ASA @Anycubic Kobra S1 0.4 nozzle"
],
"filament_vendor": [
"Anycubic"
],
"filament_flow_ratio": [
"0.98"
],

View File

@@ -4,14 +4,11 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "wqK6AVf1mNkIOmnt",
"filament_id": "OF0yFLkY",
"filament_id": "GFASA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ASA @Anycubic Kobra S1 Max 0.25 nozzle"
],
"filament_vendor": [
"Anycubic"
],
"filament_type": [
"ASA"
],

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "aMgpooFosBBvlKKR",
"filament_id": "OF0yFLkY",
"filament_id": "GFASA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ASA @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "Isk7NkdH7GxsEZkS",
"filament_id": "OF0yFLkY",
"filament_id": "GFASA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ASA @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "Ew0Lg6u3e5Xj19Bz",
"filament_id": "OF0yFLkY",
"filament_id": "GFASA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ASA @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "PEg6aLXcyUc70p9F",
"filament_id": "OF0yFLkY",
"filament_id": "GFASA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic ASA @Anycubic Kobra X 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_abs",
"from": "system",
"setting_id": "az6wmXeWSCKcUEsK",
"filament_id": "OF5Zk4e1",
"filament_id": "GFB99",
"instantiation": "true",
"filament_flow_ratio": [
"0.926"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_asa",
"from": "system",
"setting_id": "qKcef8t7nj8nBjPu",
"filament_id": "OFI80Hct",
"filament_id": "GFB98",
"instantiation": "true",
"filament_flow_ratio": [
"0.93"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "j2PXsJyI9YyoE8lc",
"filament_id": "OFPdhDlK",
"filament_id": "GFN98",
"instantiation": "true",
"filament_type": [
"PA-CF"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "xzI6EgoKa3DFYyoZ",
"filament_id": "OFB1Tv1E",
"filament_id": "GFN99",
"instantiation": "true",
"nozzle_temperature_initial_layer": [
"280"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "3zgBtEkj1fQpDkxO",
"filament_id": "OFWY6vBh",
"filament_id": "GFC99",
"instantiation": "true",
"filament_max_volumetric_speed": [
"12"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "CEd987NP90bPdkW7",
"filament_id": "OFWzr7m0",
"filament_id": "GFG99",
"instantiation": "true",
"reduce_fan_stop_start_freq": [
"1"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pla",
"from": "system",
"setting_id": "9gFgyEJelaCIVwAH",
"filament_id": "OFNcdCrm",
"filament_id": "GFL98",
"instantiation": "true",
"filament_flow_ratio": [
"0.95"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pla",
"from": "system",
"setting_id": "2MiX54GMiZjFzwBI",
"filament_id": "OFKyCgaK",
"filament_id": "GFL99",
"instantiation": "true",
"filament_flow_ratio": [
"0.98"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pva",
"from": "system",
"setting_id": "I1GMbOsMYktU7EbM",
"filament_id": "OFSGbf2I",
"filament_id": "GFS99",
"instantiation": "true",
"filament_flow_ratio": [
"0.95"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_tpu",
"from": "system",
"setting_id": "xrbLc7y1AInpumtv",
"filament_id": "OFGKLoUm",
"filament_id": "GFU99",
"instantiation": "true",
"filament_max_volumetric_speed": [
"3.2"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "hcL5OHthGzApICTk",
"filament_id": "OFzS7zt4",
"filament_id": "GFPA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PA @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "OJbFCKoG5DtVGT9J",
"filament_id": "OFzS7zt4",
"filament_id": "GFPA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PA @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "6mMIVw8iheDn1OuV",
"filament_id": "OFzS7zt4",
"filament_id": "GFPA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PA @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "epl6B2WNjV1dxG5Z",
"filament_id": "OFQe7sAx",
"filament_id": "GFPA6-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PA6-CF @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "eMUXd3PFKHl41TIq",
"filament_id": "OFQe7sAx",
"filament_id": "GFPA6-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PA6-CF @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "y9XYrUJrUYKcBhCI",
"filament_id": "OFQe7sAx",
"filament_id": "GFPA6-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PA6-CF @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "nO6m8EaBKREWUi3f",
"filament_id": "OFaEuvNF",
"filament_id": "GFPAHT-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PAHT-CF @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "ebKput7Vyn4IQez1",
"filament_id": "OFaEuvNF",
"filament_id": "GFPAHT-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PAHT-CF @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pa",
"from": "system",
"setting_id": "jzrflGdwy9U8kl1c",
"filament_id": "OFaEuvNF",
"filament_id": "GFPAHT-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PAHT-CF @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "7F0I28djSlDopbIp",
"filament_id": "OFZtkHRq",
"filament_id": "GFPC",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "wF10XxL2tNXh8C1p",
"filament_id": "OFZtkHRq",
"filament_id": "GFPC",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "t3VfcIKMk0ELiPP7",
"filament_id": "OFZtkHRq",
"filament_id": "GFPC",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "dOOXRvGwMe3NeUj0",
"filament_id": "OFeIyhUx",
"filament_id": "GFPC-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC-CF @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "H0IECyfsMaCMdefe",
"filament_id": "OFeIyhUx",
"filament_id": "GFPC-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC-CF @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "7nu7lbiAHMeFBIJc",
"filament_id": "OFeIyhUx",
"filament_id": "GFPC-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC-CF @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "puSuSfNyKlYlmLjL",
"filament_id": "OFxJRuVF",
"filament_id": "GFPC-GF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC-GF @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "1Qdmjm0MNJKU7aCD",
"filament_id": "OFxJRuVF",
"filament_id": "GFPC-GF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC-GF @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pc",
"from": "system",
"setting_id": "Z0qp7SdwN9luxLFO",
"filament_id": "OFxJRuVF",
"filament_id": "GFPC-GF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PC-GF @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_tpu",
"from": "system",
"setting_id": "gmni9H2Du5nxBqQu",
"filament_id": "OFUaHEO9",
"filament_id": "GFU99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PEBA 95A @Anycubic Kobra 3 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_tpu",
"from": "system",
"setting_id": "zCqTTjac3ydSAw3q",
"filament_id": "OFjLAYgO",
"filament_id": "GFPEBA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PEBA @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_tpu",
"from": "system",
"setting_id": "1bCJcP2RcDlvIqEo",
"filament_id": "OFjLAYgO",
"filament_id": "GFPEBA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PEBA @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_tpu",
"from": "system",
"setting_id": "5v6coSKjAl220Cnq",
"filament_id": "OFjLAYgO",
"filament_id": "GFPEBA",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PEBA @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "saRQ24o8KHgc1YnM",
"filament_id": "OFRhCTUg",
"filament_id": "GFPET-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PET-CF @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "hYFR2CtiXab9KZUp",
"filament_id": "OFRhCTUg",
"filament_id": "GFPET-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PET-CF @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "RCaf3c84JsWkuJzd",
"filament_id": "OFRhCTUg",
"filament_id": "GFPET-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PET-CF @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "Anycubic Generic PETG",
"from": "system",
"setting_id": "qe2wYLf7CEMjmfxj",
"filament_id": "OFo6n2pB",
"filament_id": "GFG99",
"instantiation": "true",
"filament_vendor": [
"Anycubic"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "uNktFymJ3RbGa7k0",
"filament_id": "OFo6n2pB",
"filament_id": "GFG99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra 3 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "EaW663aw4torYXG8",
"filament_id": "OFo6n2pB",
"filament_id": "GFG99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra 3 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "lgVEMmBHMen0sWsI",
"filament_id": "OFo6n2pB",
"filament_id": "GFG99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra 3 Max 0.8 nozzle"

View File

@@ -4,14 +4,11 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "uLRMTzyJIara4Hr2",
"filament_id": "OFo6n2pB",
"filament_id": "GFG99",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra S1 0.4 nozzle"
],
"filament_vendor": [
"Anycubic"
],
"filament_flow_ratio": [
"0.96"
],

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "FAyFkKpV5w6G5dU2",
"filament_id": "OFo6n2pB",
"filament_id": "GFPETG",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra S1 Max 0.25 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "duduwoswLHk9I9sY",
"filament_id": "OFo6n2pB",
"filament_id": "GFPETG",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "b3GU7ytZPEYNNyne",
"filament_id": "OFo6n2pB",
"filament_id": "GFPETG",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "SrHKJh6prBzMz4Se",
"filament_id": "OFo6n2pB",
"filament_id": "GFPETG",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "HyTi93UGno6uAJEO",
"filament_id": "OFo6n2pB",
"filament_id": "GFPETG",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG @Anycubic Kobra X 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "ajFzrQq9AvGtZCui",
"filament_id": "OF9UJOGF",
"filament_id": "GFPETG-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG-CF @Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "VOV59iBwqwMtmi48",
"filament_id": "OF9UJOGF",
"filament_id": "GFPETG-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG-CF @Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "fdm_filament_pet",
"from": "system",
"setting_id": "L6GIF36jbXf5NVOw",
"filament_id": "OF9UJOGF",
"filament_id": "GFPETG-CF",
"instantiation": "true",
"filament_settings_id": [
"Anycubic PETG-CF @Anycubic Kobra S1 Max 0.8 nozzle"

View File

@@ -4,7 +4,7 @@
"inherits": "Anycubic Generic PLA",
"from": "system",
"setting_id": "XIq1GeZdHqOl9Gtr",
"filament_id": "OFmjN2bc",
"filament_id": "GFL92",
"instantiation": "true",
"filament_vendor": [
"Anycubic"

Some files were not shown because too many files have changed in this diff Show More