diff --git a/.agents b/.agents new file mode 120000 index 0000000000..c8161850a4 --- /dev/null +++ b/.agents @@ -0,0 +1 @@ +.claude \ No newline at end of file diff --git a/.claude/skills/orca-profiles/SKILL.md b/.claude/skills/orca-profiles/SKILL.md index 3b2130660f..40ab8c9927 100644 --- a/.claude/skills/orca-profiles/SKILL.md +++ b/.claude/skills/orca-profiles/SKILL.md @@ -1,78 +1,127 @@ --- name: orca-profiles -description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job. +description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id, and moving settings that sibling presets repeat onto shared bases after fix-variant or while drafting. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job. --- # OrcaSlicer system profiles -A bundle is `resources/profiles/.json` plus `/`. The vendor id is the -filename stem, not the index's display `name`. The index is the loader's only entry point: -unindexed presets never load. `OrcaFilamentLibrary` is the shared filament bundle; -`blacklist.json` is data, not a bundle. +This skill describes how OrcaSlicer system profiles are drafted and shaped: the rules, equations and +patterns a profile follows. Use it to draft new profiles, modify existing ones, fix profile issues and +review profile changes. -## Choose the reference for the task +A bundle is the index `resources/profiles/.json` plus the folder `/`. The vendor id is +the filename stem (`BBL`), not the index's display `name` (`Bambulab`). The index is the loader's only +entry point: an unindexed preset never loads. `OrcaFilamentLibrary` is the shared filament bundle, +loaded first; `blacklist.json` is data, not a bundle. -Read the relevant reference before editing; load others only when the task crosses those areas. -Paths below are relative to this skill. Commands run from the repository root. +## References + +Read the reference for the task before editing; load others only when the task crosses into them. +Paths below are relative to this skill. Commands run from the repository root; on Windows use `py -3` +for `python3`. | Task | Read | | --- | --- | -| Add or tune a filament, brand or material; fix compatibility / alias shadowing | [filament-profiles.md](references/filament-profiles.md) | -| Add a printer or nozzle; change models, variants, assets or extruder vectors | [machine-profiles.md](references/machine-profiles.md) | +| Add or tune a filament, brand or material; fix compatibility, alias shadowing or overlapping coverage | [filament-profiles.md](references/filament-profiles.md) | +| Add a printer or nozzle; change models, variants, assets or per-extruder vectors | [machine-profiles.md](references/machine-profiles.md) | +| Add or tune extruder variants (`extruder_type` Direct Drive / Bowden × nozzle volume type Standard / High Flow / TPU High Flow / E3D High Flow / Extra High Flow variants) on a printer, process or filament | [extruder-variants.md](references/extruder-variants.md) | | Add a quality tier or tune a process | [process-profiles.md](references/process-profiles.md) | -| Create a vendor bundle; diagnose loading or inheritance; migrate preset names | [vendor-bundle.md](references/vendor-bundle.md) | -| Name a preset; check what a name must equal | [naming.md](references/naming.md) | +| Draft several presets, or clean up after `fix-variant`: which base each shared value belongs on, when a new base pays off, proving nothing loads differently | [shared-bases.md](references/shared-bases.md) | +| Name a preset; check what a name must equal; base names, uniqueness, filenames | [naming.md](references/naming.md) | +| Create a vendor bundle; index, `version`, `inherits`, `include`; migrate preset names; diagnose why a bundle fails to load | [vendor-bundle.md](references/vendor-bundle.md) | | Change ids; diagnose AMS identity | [ids.md](references/ids.md), then `docs/HLSD/filament_id.md` for identity changes | -| Review a profile diff | [review-checklist.md](references/review-checklist.md) | | Run checks, interpret failures, test another tree or verify in the app | [validation.md](references/validation.md) | +| Review a profile diff | [review-checklist.md](references/review-checklist.md) | -## Golden rules +## Rules -1. **Bump every changed bundle's `version`**, including `OrcaFilamentLibrary.json` when affected. - Increment the last component; carry `.99` into the third component (`02.04.00.99` → - `02.04.01.00`). The updater requires a strictly newer version. CI does not check this. -2. **Register every preset, bases included, parents before children.** `update-index` generates - the four `*_list` arrays; `check` requires its output. Index names must equal file `name` fields. -3. **Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New - presets normally omit them until `generate-id`; bases must have no `setting_id`. - BBL's authoritative `setting_id` and a wrongly inherited `filament_id` need the explicit - handling in [ids.md](references/ids.md). -4. **Load failures can discard a whole vendor bundle.** Broken `inherits`, missing indexed files, - duplicate names, invalid model/variant references and unresolved filament ids affect more than - the edited preset. Inheritance stays within a bundle, except filaments may inherit the library. -5. **Preserve shipped selectable names.** Renaming, deleting or changing `instantiation` from - `"true"` to `"false"` needs `renamed_from` on a selectable successor. It is a `;`-separated string; - update in-tree references too. See [migration rules](references/vendor-bundle.md#renamed_from). -6. **Compatibility uses exact printer variant names.** Every instantiated non-library filament - needs a non-empty `compatible_printers` in its own file. Library fallbacks may omit it; - library printer-specific tunes use a non-empty list. One variant may be claimed by only one - profile per filament product (`filament_id`); an overlap is resolved by moving the variant to the - most specific preset, which is preferred over deleting a profile. See - [one variant, one profile](references/filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product). -7. **Preset values are strings or arrays of strings.** Use `"instantiation": "false"`, not `false`. - Model `nozzle_diameter` is a `;`-separated string; machine `nozzle_diameter` is an array. - Wrong types can abort loading; see [failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius). -8. **Verify setting keys against the code.** Unknown keys are silently discarded. Check - `PrintConfig.cpp` definitions and `PrintConfigDef::handle_legacy`; neighbours can contain dead - keys. `normalize` removes known obsolete keys, but does not detect arbitrary misspellings. -9. **Run the full profile checks before reporting completion.** A vendor-scoped pass is only a - development loop. Review also covers version bumps, assets, non-default processes and hardware - tuning that CI cannot establish. -10. **One all-printer preset per product; color is a runtime property, never a preset.** Never ship - presets that differ only by color — CI accepts them, so this is a review call. See - [color is a runtime property](references/filament-profiles.md#color-is-a-runtime-property). +1. **Bump the `version` of every bundle you change**, `OrcaFilamentLibrary.json` included when affected. + Increment the last component and carry `.99` into the third (`02.04.00.99` → `02.04.01.00`). The + updater installs only a strictly newer version, and CI does not check the bump. +2. **Register every preset, bases included, parents before children.** `update-index` writes the four + `*_list` arrays from the files on disk; `check` fails unless the index equals its output. Each index + entry's `name` must equal the file's `name`. +3. **Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New presets + normally omit them until `generate-id`; bases carry no `setting_id`. BBL's own `setting_id`s and a wrongly + inherited `filament_id` need the explicit handling in [ids.md](references/ids.md). +4. **A name is an identity; preserve shipped selectable names.** Every reference (`inherits`, + `compatible_printers`, `default_*`, `printer_model`) is the exact, case-sensitive `name`. Renaming or + deleting a shipped selectable preset, or flipping its `instantiation` from `"true"` to `"false"`, + needs `renamed_from` (a `;`-separated string) on a selectable successor + ([migration rules](references/vendor-bundle.md#renamed_from)); update in-tree references too. +5. **Values are strings or arrays of strings.** `"instantiation": "false"`, never `false`. A + `machine_model`'s `nozzle_diameter` is a `;`-separated string; a `machine`'s is an array. Custom + G-code is one string. Wrong types can abort loading of the bundle or of every vendor + ([failure scopes](references/vendor-bundle.md#failure-scopes)). +6. **Unknown keys are dropped silently.** Confirm every new key exists in + `src/libslic3r/PrintConfig.cpp`; a key a neighbouring file writes is no evidence it exists. `check` rejects, + and `normalize` removes, known obsolete keys, but neither detects an arbitrary misspelling. A key + missing from the definitions may be a legacy name the loader still renames + (`tool_change_gcode` → `change_filament_gcode`) or whose value it rewrites (`DirectDrive` → + `Direct Drive`); check `PrintConfigDef::handle_legacy` before removing one, and write the current name + in new edits. +7. **Write overrides only.** Inherit the bundle's bases and restate just what differs; follow the + bundle's existing layering and style, except that a new filament prefers the library's bases. A + value that every preset of a group shares goes on the group's base + ([shared bases](references/shared-bases.md)). +8. **One load error can discard a whole vendor bundle**: an unresolved `inherits`, a missing indexed + file, two selectable presets with one name, an unknown `printer_model` or `printer_variant`, a + filament with no resolvable `filament_id`, `nil` in a non-nullable key. `inherits` and `include` + resolve only inside the bundle, except that filaments may inherit from OrcaFilamentLibrary. +9. **Filament compatibility names exact printer variants.** Every instantiated filament outside the + library writes a non-empty `compatible_printers` in its own file. Library fallbacks may omit it; + library printer-specific tunes use a non-empty list. One variant may be claimed by only one preset + per filament product (`filament_id`); an overlap is resolved by moving the variant to the most + specific preset, which is preferred over deleting a preset + ([one variant, one profile](references/filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)). +10. **One all-printer preset per product; colour is a runtime property, never a preset.** Never ship + presets that differ only by colour. CI accepts them, so this is a review call + ([colour](references/filament-profiles.md#colour-is-a-runtime-property)). +11. **Write a variant key at full width or not at all.** A key in the four variant sets holds exactly + `N` values in the selectable preset that writes it, `N = S × k` in the + [sizing equation](references/extruder-variants.md#sizing-equation): one per variant, a (normal, + silent) pair per variant for the `machine_max_*` limits; one value is not "the same for every + variant". Profiles must be correct as written: `check` judges each selectable preset by the + equation, never by what the loader pads or cuts, and `check --strict` also holds what a preset + inherits to its own width, as BBL writes it. A base is never judged on its own: its array widths + count, under `--strict`, where they reach a preset, while the id and layout rules judge the + composed preset, inherited values included, without it. Declare the variant layout on a + multi-extruder printer whose extruders need different values + ([widths](references/extruder-variants.md#widths)). +12. **Run the full checks before reporting completion.** A `--vendor` run is only a development loop. + Review also covers version bumps, assets, non-default processes and hardware tuning, which CI cannot + establish. + +## Names + +| Type | Shape | What the loader uses | +| --- | --- | --- | +| `machine_model` | `` (`Bambu Lab X1 Carbon`) | the exact string, named by each variant's `printer_model`; also the `_cover.png` stem | +| `machine` | ` nozzle` | the exact string, named by `compatible_printers`; `printer_variant` holds the nozzle token (`0.4`, [rules](references/machine-profiles.md#printer_model-and-printer_variant)) | +| `process` | `mm @` | the exact string when referenced or selected; `@` is a label, compatibility comes from the preset's list or condition | +| `filament` | ` @` | text before the first `@` is the alias (shadowing, `filament_id`); the rest is a label, with reserved targets `@base` and `@System` | + +The shapes are convention; `check` enforces only uniqueness. Per-type conventions, base names and +filename rules are in [naming.md](references/naming.md). ## Creating or modifying a profile -1. **Inspect the diff and neighbouring presets.** Read their `name`, parent chain and children; - edits to a base or a leaf with descendants propagate. Match the bundle's structure and write +1. **Inspect the diff and the neighbouring presets.** Read their `name`, parent chain and children: + edits to a base, or to a leaf that others inherit, propagate. Match the bundle's structure and write only overrides. New files use tab indentation, LF and a trailing newline; preserve unrelated - formatting in existing files. Match filename case exactly and use cross-platform names. -2. **Author explicit metadata.** Set `type` yourself, especially for `machine` vs `machine_model`. - Use `"from": "system"` and string `instantiation` on config presets. Omit ids on new presets - unless [ids.md](references/ids.md) requires special handling; retain them on existing ones. - Complete compatibility, defaults, assets and any rename migration using the task reference. -3. **Bump the version**, then run the authoring commands in order for each affected bundle: + formatting in existing files. Match filename case exactly and use + [cross-platform names](references/naming.md#filenames-and-paths). +2. **Author explicit metadata.** Set `type` yourself (`machine` vs `machine_model` especially), and use + `"from": "system"` and a string `instantiation` on config presets. Omit ids on new presets unless + [ids.md](references/ids.md) requires special handling; retain them on existing ones. Complete + compatibility, defaults, assets and any rename migration using the task reference. +3. **Put shared values on shared bases** when drafting several presets, and after `fix-variant`, which + widens an array in every preset that writes it and moves nothing. Each value goes on the base of the + level that determines it, a new base only where it pays for itself, and a restructure must leave + every selectable preset loading what it loaded: `snapshot` before the edit, `compare` after it + ([shared-bases.md](references/shared-bases.md)). +4. **Bump the version**, then run the authoring commands in order for each affected bundle, reading + every diff and resolving every error before moving on: ```bash python3 scripts/orca_profile_tool.py normalize --vendor "" @@ -81,46 +130,59 @@ Paths below are relative to this skill. Commands run from the repository root. python3 scripts/orca_profile_tool.py check ``` - Writing commands support `--dry-run`. Inspect their diffs: `normalize` changes content and can - reformat entire files. Stop and resolve command errors before proceeding. - - **Do not use `trim` in this workflow:** it can delete newly authored, unindexed profiles. - Do not use `normalize --force` for routine edits. -4. **Validate:** + Writing commands accept `--dry-run`. `normalize` changes content and can reformat entire files; + rerun `update-index` after any change to `inherits` or `include`, since it orders by them. + **Do not use `trim` in this workflow:** it deletes unindexed files, including one you just added. Do + not use `normalize --force` for routine edits. An error in a bundle you did not touch predates your + change: confirm it on a clean checkout and report it rather than fixing it in the same change. +5. **Validate:** ```bash ./scripts/check_profile.sh --vendor "" # development loop ./scripts/check_profile.sh # full tree before the PR ``` - On Windows use `py -3` instead of `python3`, and `scripts\check_profile.bat -Vendor ""` - / `scripts\check_profile.bat`. Logs land in a per-user cache dir (see - [validation.md](references/validation.md)). - Id checks remain tree-wide under `--vendor`; filament-only bundles skip the default slice check. - See [validation.md](references/validation.md) for flags, coverage and error remedies. -5. **Verify the changed behavior.** Slice newly added non-default processes explicitly, and - [test in the app](references/validation.md#testing-in-the-app) for selection or UI behavior. - Report checks actually run, failures/skips and any hardware tuning still unverified. + On Windows use `scripts\check_profile.bat -Vendor ""` / `scripts\check_profile.bat`. Logs + land in a per-user cache dir ([validation.md](references/validation.md)). Id checks stay tree-wide + under `--vendor`, and filament-only bundles skip the default slice check. Under `--vendor` read only + `profile_tool` and `validate_slice`: the other three checks fail on library presets that name other + vendors' printers ([why](references/validation.md#the-five-checks)). +6. **Verify the changed behaviour.** Slice newly added non-default processes and filaments + [explicitly](references/validation.md#checking-a-copy-of-the-tree), and + [test in the app](references/validation.md#testing-in-the-app) for selection or UI behaviour. Report + the checks actually run, their failures and skips, and any hardware tuning still unverified. -## Symptom → first reference +## Symptom → first look | Symptom | Start here | | --- | --- | -| A vendor disappears | Loader log / `validate_system`; [bundle failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius) | -| A setting has no effect | Key spelling/type, `handle_legacy`, or a config key placed on a `machine_model` | -| A preset exists but is not selectable | Index registration, `instantiation`, installation and compatibility | -| A filament is missing, duplicated, or matches the wrong spool | [Compatibility and alias shadowing](references/filament-profiles.md#compatible_printers); [ids](references/ids.md) | -| Presets differ only by color, or an all-printer library preset lacks `@System` | [Color is a runtime property](references/filament-profiles.md#color-is-a-runtime-property) | -| A bed temperature is ignored | [Plate-specific temperature keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) | -| A change is absent from the running app | Version bump and [installed profile location](references/validation.md#testing-in-the-app) | -| A check fails | [Error → remedy](references/validation.md#error--remedy) | +| A vendor disappears | the app's log or the `validate_system` log; [failure scopes](references/vendor-bundle.md#failure-scopes) | +| A setting has no effect | key spelling or a legacy name (`PrintConfigDef::handle_legacy`), value type, or a config key placed on a `machine_model` | +| A preset exists but is not selectable | index registration, `instantiation`, whether it is installed (chosen in the setup wizard, or listed in the model's `default_materials`), compatibility | +| A filament is missing, duplicated, or matches the wrong spool | [compatibility and alias shadowing](references/filament-profiles.md#compatible_printers), [ids](references/ids.md) | +| Presets differ only by colour, or an all-printer library preset lacks `@System` | [colour is a runtime property](references/filament-profiles.md#colour-is-a-runtime-property) | +| High Flow (or a second extruder) slices with Standard (or extruder 1) values; a variant switch is missing; a variant's tuned values never arrive | [extruder variants](references/extruder-variants.md#variant-strings), [widths](references/extruder-variants.md#widths), [variant names](references/validation.md#variant-names) | +| Values land on the wrong extruder or mode after a variant was added | [inserting a variant](references/extruder-variants.md#adding-a-variant-inserts-its-values-at-its-variant-index), [padding and composition](references/extruder-variants.md#padding-truncation-and-composition) | +| A resolved value matches neither the file nor its `inherits` parent; an array is not the width the file wrote, or a child got only a base's first value | [composition order and `include`](references/vendor-bundle.md#inherits-and-include) | +| A bed temperature is ignored | [the twelve plate keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) | +| A change is absent from the running app | version bump and [installed profile location](references/validation.md#testing-in-the-app) | +| A check fails | [error → remedy](references/validation.md#error--remedy) | ## Source of truth -When guidance and behavior disagree, inspect the current checkout: -`scripts/orca_profile_tool.py` for tooling and flags; `src/libslic3r/Preset*.cpp` for loading and -compatibility; `src/libslic3r/PrintConfig.cpp` for setting types and legacy handling; -`src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml` for -validation coverage. `docs/HLSD/filament_id.md` defines filament identity. The -[profile development guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/how_to_create_profiles.md) +When this skill and the checkout disagree, the checkout wins: `scripts/orca_profile_tool.py` for the +tool and its flags; `src/libslic3r/Preset*.cpp` for loading and compatibility; +`src/libslic3r/PrintConfig.cpp` for keys, types, nullable options, the variant key sets and legacy +handling; `src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml` +for validation coverage; `docs/HLSD/filament_id.md` for filament identity. The wiki's +[profile guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/how_to_create_profiles.md) is a tutorial; confirm loader and CLI details against these sources. + +## Editing this skill + +The skill describes what a profile must be, not what the shipped tree currently is. State rules, +equations, patterns and profile shapes; never inventories of shipped defects, counts, dated +measurements or lists of which vendors do what. Test each sentence: if editing the profiles alone, +with the engine and tool unchanged, could make it false, state the rule behind it or give a generic +example instead. Example files named as models to copy, and commit hashes cited as the reason for a +rule, are fine. diff --git a/.claude/skills/orca-profiles/references/extruder-variants.md b/.claude/skills/orca-profiles/references/extruder-variants.md new file mode 100644 index 0000000000..322d79d56f --- /dev/null +++ b/.claude/skills/orca-profiles/references/extruder-variants.md @@ -0,0 +1,548 @@ +# Extruder variants + +Use this when a printer's hotend or extruder can be in more than one hardware configuration that +needs different settings: a Standard and a High Flow nozzle, a TPU High Flow, E3D High Flow or Extra High Flow nozzle, or +a Direct Drive and a Bowden extruder on one machine. Variants let one printer, process and filament preset carry a +separate value per configuration; the user picks the configuration in the sidebar, and slicing uses +the matching values. They are not for nozzle **diameter**: that stays one `machine` preset per +`printer_variant`. + +## Variant strings + +- A **variant string** is `" "`: the extruder's `extruder_type` + value, a space, and its nozzle volume type (the project's `nozzle_volume_type`, seeded by the + printer preset's `default_nozzle_volume_type`), e.g. `"Direct Drive High Flow"`. +- A **variant** is one entry of a preset's variant list, named by its variant string (plus an extruder + id for printer and process lists). A variant-aware key holds **one value per variant** (a + (normal, silent) pair for the `machine_max_*` limits), and each preset declares its variants in + that list. Slicing picks, for each extruder, the variant whose + variant string equals the current `extruder_type` + nozzle volume type. +- Matching is an **exact string compare** of the whole string (plus the extruder id for printer and + process lists) against the preset's own resolved list, in any order. With no match the **first + variant** is used, silently: variant index 0 for printer and process keys (extruder 1's first + variant, whichever extruder asks), the filament's own first variant for filament keys. Printer and + process keys are matched only on a printer with several extruders or whose + `extruder_variant_list` offers several variant strings (`support_different_extruders`); on any + other printer their variant index 0 is read whatever it names. Nothing rejects a profile for a + variant mismatch; mistakes surface only as wrong values in the G-code. +- Variant index and array length follow [Widths](#widths). + +The complete enum is `s_keys_map_ExtruderType` and `s_keys_map_NozzleVolumeType` in +`src/libslic3r/PrintConfig.cpp` (`grep -A5 s_keys_map_NozzleVolumeType` there to confirm): + +| Part | Values | +| --- | --- | +| Extruder type | `Direct Drive`, `Bowden` | +| Nozzle volume type | `Standard`, `High Flow`, `TPU High Flow`, `E3D High Flow`, `Extra High Flow` (`Hybrid` exists but is runtime-only) | + +So the ten legal variant strings are the two extruder types × the five writable nozzle volume types, +and this table is the whole test of legality: a string's presence in a shipped profile is no evidence +for it. `Hybrid` (an extruder with several sub-nozzles) is never a variant: a filament on it reads the +variant of its own nozzle volume type from the project's `filament_volume_map`, printer and process +keys get one variant per nozzle volume type the extruder holds (`extruder_nozzle_stats`), and any +other lookup reads `Standard`. Never write it in a variant string. The same per-filament and +per-type reading applies on any extruder once `extruder_nozzle_stats` lists more nozzle volume +types than there are extruders. + +Every other string (a nozzle volume type name from another slicer, a typo, a variant copied from a +shipped profile) is a **dead variant**: nothing selects it, and since lookup is by string it does not +shift the variants beside it; it still counts toward the variant length when arrays are sized. +`orca_profile_tool.py check` reports it as an error in every bundle, BBL included +([variant names](validation.md#variant-names)). Legacy names are errors too: in the four variant +lists, `default_nozzle_volume_type` and `nozzle_volume_type`, the loader still rewrites `Normal` → +`Standard` and `Big Traffic` → `High Flow`, so a ported `Direct Drive Normal` variant would load, but +`check` rejects the spelling and names the enum name to write. `DirectDrive` is rewritten only in +`extruder_type`, so `DirectDrive Standard` in a variant list is dead. Write the enum names; +`normalize` does not convert them. + +### A nozzle the enum does not name + +The nozzle volume types are fixed by the engine, and a profile cannot add one. A nozzle the table does +not name needs the type added in code first, which is outside profile work; until then any string +for it is a dead variant. Once the engine has the type, its variant string is the enum name after +the extruder type, e.g. `"Direct Drive "`, and `check` accepts it with no tool change, since it +reads the enum from `PrintConfig.cpp`. Existing arrays are unaffected +([slice time](#slice-time-and-existing-users)). + +## When to use variants + +| Hardware | Do | +| --- | --- | +| One extruder, one nozzle type | Nothing. Without a variant list the printer has one variant ([variant length](#widths)) and no printer-key lookup takes place; a filament with several variants still gets the one for the printer's variant string. | +| Bowden-only printer | Nothing either. A list-less printer matches no string, so every printer-key lookup reads variant index 0 whether that variant is called `"Bowden Standard"` or `"Direct Drive Standard"`; naming it is cosmetic while it is the printer's only variant. A multi-extruder Bowden printer that declares the layout writes `"Bowden Standard"` variants: `"Direct Drive Standard"` entries match none of its extruders, so every extruder reads variant index 0 (and `check` reports them under [Printer rule 3](#printer-machine)). A filament with a `"Bowden Standard"` variant does get that variant there. | +| Nozzle types the user swaps (Standard / High Flow / TPU High Flow / E3D High Flow / Extra High Flow) | The printer lists them as variants; tune the keys that really differ per nozzle volume type. | +| Extruders of different types on one machine | One `extruder_type` per extruder, each extruder listing its own variants. | +| Several extruders that need different values in a variant key (retraction, z-hop, `nozzle_volume`, the `machine_max_*` limits) | Declare the layout even with a single nozzle volume type: `extruder_variant_list` with one `" Standard"` per extruder, and the flattened pair. Without it the arrays still hold one value per extruder ([variant length](#widths)), but the loader keeps only extruder 1's value of a list-less printer's arrays, so every extruder prints with it. | + +A preset without variant keys keeps working on a variant printer: its single variant is applied to +every extruder. So adding variants to a printer does not break existing processes or library +filaments; it only makes per-variant tuning possible. + +## Printer (`machine`) + +```json +"extruder_type": ["Direct Drive", "Direct Drive"], +"extruder_variant_list": ["Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow,Direct Drive TPU High Flow"], +"printer_extruder_id": ["1", "1", "2", "2", "2"], +"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow", "Direct Drive TPU High Flow"], +"default_nozzle_volume_type": ["Standard", "Standard"] +``` + +Rules: + +1. `extruder_variant_list` has **one entry per extruder**; each entry is the `,`-joined variants that + extruder supports. It is the per-extruder menu the sidebar offers. It is in no + [variant set](#the-four-key-sets), so the variant-length resize leaves it alone. Extruder 1's + first variant, variant index 0 of the flattened pair, is the fallback of every extruder whose + variant is missing when the arrays are collapsed for slicing + ([slice time](#slice-time-and-existing-users)); it is not necessarily the configuration the user + sees, which is `default_nozzle_volume_type` (rule 4). +2. `printer_extruder_variant` is that list **flattened** extruder-major, one entry per variant, and + `printer_extruder_id` gives each entry its 1-based extruder. These two size and address every + variant. Write all three keys and keep them in agreement. At load with + `single_extruder_multi_material` off, and in the app when the printer tab loads a printer with a + different number of extruders, the pair is rebuilt from `extruder_variant_list` (one + `Direct Drive Standard` per extruder when the list is absent) and the variant arrays are resized to + the rebuilt pair, padded with their first value or cut. The resize skips the `machine_max_*` limits + and `hotend_heating_rate` / `hotend_cooling_rate`: they keep their width, and an extruder beyond it + reads their first value, so extruder 2 and up of a list-less printer take extruder 1's normal limit + as their silent one too. With the three in agreement that changes nothing; a pair written without + the list is replaced. A listed variant the pair lacks is a menu choice that reads variant index 0. + - The pair without `extruder_variant_list` slices, but the sidebar offers no variant switch and + the app cannot add variants to a list-less process: nothing is lost while every extruder + has exactly one variant, and every further variant is unreachable. + - A missing or one-value `printer_extruder_id` is extruder 1 at every index + ([the id trap](#padding-truncation-and-composition)) unless the load-time rebuild above + replaces the pair (`single_extruder_multi_material` off). + - `check` reports against this rule ([variant arrays](validation.md#variant-arrays)). Errors: a + pair that is not the flattening, an id array that does not give each entry its extruder, a list + of more than one variant without the pair, and a pair without the list that puts several variants + on one extruder. A pair without the list and one variant per extruder is a warning where the + load-time rebuild would replace it (`single_extruder_multi_material` off, and a pair other than + one `Direct Drive Standard` per extruder), and passes otherwise. +3. Every variant string in an entry must start with that extruder's `extruder_type`. +4. `default_nozzle_volume_type` has one value per extruder and must name a nozzle volume type that + extruder's variants list; it seeds the sidebar. The live choice is `nozzle_volume_type` in the + project config, never a preset key. +5. Size every array by the set its key belongs to (the three sets are listed in full in + [The four key sets](#the-four-key-sets); the lengths are worked through in the + [sizing equation](#sizing-equation)): exactly the length below in each selectable preset that + writes it, or leave the key out and the preset takes what reaches it, the default or its base's + array. One value is no + shorthand for "the same for every variant"; write the value for every variant: + +| Set | Length | Keys | +| --- | --- | --- | +| `printer_extruder_options` | extruders (`E`) | the 8 per-extruder keys outside the variant scheme (`extruder_type`, `nozzle_diameter`, `default_nozzle_volume_type`, …) | +| `printer_options_with_variant_1` | variant length (`S`) | the full list below; not guessable from names | +| `printer_options_with_variant_2` | 2 × variant length | the 16 `machine_max_*` limits, at [stride 2](#widths): a (normal, silent) pair per variant | + +Put the variant layout on the shared base of all printers that share the hardware, and let the +nozzle-diameter siblings inherit it, restating only the variant arrays whose values change. `check` +does not judge a base on its own; its arrays count where they reach a selectable preset, under +`check --strict`. The loader stores a base at its **own** `printer_extruder_variant`, one variant +when it writes none whatever its extruder count, and cuts a wider array to its first values before +any child inherits it; so a multi-extruder base whose extruders need different values declares the +layout itself ([composition](#padding-truncation-and-composition)). When a variant array can move from +the presets to a base is in [shared-bases.md](shared-bases.md#variant-arrays-on-a-base). + +## Process + +1. `print_extruder_variant` + `print_extruder_id` list the (extruder id, variant string) pairs of the + process's variants. A variant is found by that pair, never by position: what is **required** is + that every pair a compatible printer (by list or condition) can select is present, in any order. A + missing pair reads the process's variant index 0 for that extruder, an extra pair is a variant + nothing selects, and neither is reported. The exception is a single-extruder printer whose + `extruder_variant_list` offers one variant string: no pair is matched there and variant index 0 + is read, so a process shared with such a printer lists that printer's pair first. **Mirroring** the printer's `printer_extruder_variant` + + `printer_extruder_id` entry for entry is the convention; follow it, so the arrays compare by eye, + but a different order with every pair present is a nit, not a defect. A process shared by printers + whose pairs differ falls back to variant index 0 on the pairs it lacks; give each layout its own base. +2. Every key in `print_options_with_variant` ([the full list](#the-four-key-sets), which is not + "every speed") has exactly one value per variant, or is left out. `print_extruder_id` needs its + value per variant too: one value pads to extruder 1 everywhere. `check` holds a + `print_extruder_id` that reaches the preset, written or inherited, to one entry per variant on any + printer, without `--strict`, and warns when it is absent and a variant repeats. +3. Only add process variants if speeds or accelerations really differ per nozzle volume type or + extruder. Otherwise omit the variant keys: a one-variant process needs no list, because match and + no-match both read variant index 0, and that variant is copied to every extruder at slice time. +4. Put the lists on the process base for that printer layout, so leaves stay small. + +## Filament + +```json +"filament_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow"], +"filament_max_volumetric_speed": ["21", "29"], +"filament_flow_ratio": ["0.98", "0.98"], +"filament_retraction_length": ["nil", "0.4"] +``` + +1. `filament_extruder_variant` lists variants **without extruder ids**: a filament's High Flow variant + is used on whichever extruder is in High Flow. Its entries must be distinct, since the lookup + returns the first equal string and a repeated entry is a variant nothing selects. There is no + filament id key: `filament_extruder_id` exists only as a G-code placeholder, not as a filament + option (its option and its set entry are commented out in `PrintConfig.cpp`), so a filament file that writes it loses the key as unknown. + `filament_extruder_compatibility` is unrelated to variants (it says which extruders the filament + may be loaded into). +2. Every key in `filament_options_with_variant` ([the full list](#the-four-key-sets)) that reaches the + preset, whether written, included or inherited, is resized to its variant count: write it at exactly + that width, or leave it out. Keys outside the set + (`filament_type`, plate temperatures, `fan_max_speed`, `slow_down_min_speed`, `pressure_advance`, + …) are never addressed by variant index; the preset contributes their first value however wide a + file writes them. +3. Cover every variant the material is meant to print on across its `compatible_printers`. Leave out + a variant deliberately when the material should not be tuned for it (e.g. a TPU High Flow variant + only on TPU filaments); an extruder reporting that variant string then reads the first variant. +4. Order the variants like the printer's, Standard first, so the first-variant fallback is the + conservative one. +5. Tune what really differs: `filament_max_volumetric_speed` is the usual difference between nozzle + volume types, then flow ratio, temperature and retraction. Use measured values; never copy the + Standard value into the High Flow variant and call it tuned. +6. `nil` is legal per variant in the nullable override keys, occupies one entry like any value, and + keeps the printer's value for that variant only. +7. Declare a multi-variant list with the arrays it sizes: on the filament, in its own file or in a + template it pulls in with [`include`](vendor-bundle.md#inherits-and-include). The filament may be + compatible with printers that list fewer variants, or none: each printer takes the filament's variant + for its own variant string, else the filament's first variant. A one-variant list on a shared base + is harmless, because one variant is the width a list-less preset has anyway; and a list-less base is + stored at that width, so a wider array on it reaches its children as its first value only. + A key such a base writes reaches a multi-variant child as one value, which the loader spreads over + the child's variants; `check --strict` reports it at the child, which restates it at its own width. + +## Widths + +Every variant key is a flat array addressed by **variant index**: index `n` belongs to entry `n` of +the preset's own variant list (`printer_extruder_variant`, `print_extruder_variant` or +`filament_extruder_variant`). The index is found by exact compare of the string +`" "` (plus the 1-based extruder id for printer and process lists), +and the value is read at `index × stride`: + +| Stride | Keys | Layout | +| --- | --- | --- | +| 1 | `printer_options_with_variant_1`, `print_options_with_variant`, `filament_options_with_variant` | `[variant 0, variant 1, …]` | +| 2 | `printer_options_with_variant_2` (the `machine_max_*` limits) | `[variant 0 normal, variant 0 silent, variant 1 normal, variant 1 silent, …]` | + +So a variant array is `variant length × stride` long. The **variant length** is the number of +entries in the preset's variant list, counted on the preset's config **after** `include` and +`inherits` are applied, dead variants included. An inherited array arrives already resized to the +base's own variant length, an included one at the width its file wrote +([composition](#padding-truncation-and-composition)): + +| Preset | Variant length | +| --- | --- | +| `machine` | `len(printer_extruder_variant)`; without it, the variants `extruder_variant_list` offers; without both, **one per extruder** (`len(nozzle_diameter)`), since the list's default is one `Direct Drive Standard` per extruder. `extruders_count` is a printer-tab field, not a preset key. The loader honours that default only halfway for a system preset: it sizes the composed preset by the one-entry default `printer_extruder_variant`, cutting every variant array to its first value, and only then (with `single_extruder_multi_material` off) rebuilds the pair to one variant per extruder ([Printer rule 2](#printer-machine)) and pads the arrays with that value. The rule's width is still one per extruder; to give extruders different values, declare the layout. | +| `process` | `len(print_extruder_variant)`; without one, 1 | +| `filament` | `len(filament_extruder_variant)`; without one, 1 | + +The per-extruder keys in `printer_extruder_options` ([listed with the sets](#the-four-key-sets)) are +outside this scheme and stay one value per extruder. Keys outside [the four sets](#the-four-key-sets) +are never variant-resized or addressed by variant index, however wide a shipped file writes them; a +per-extruder vector holds `len(nozzle_diameter)` values, and a shorter one acts as padded with its +first value ([machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers)). + +### Sizing equation + +For a key in one of the four variant sets: + +``` +E = extruders = len(nozzle_diameter) = len(extruder_type) + = len(default_nozzle_volume_type) = len(extruder_variant_list) +V_i = variants listed for extruder i = ","-separated entries of extruder_variant_list[i], + each " " +S = variant length = V_1 + V_2 + … + V_E + = len(printer_extruder_variant) = len(printer_extruder_id) +k = stride = 2 for printer_options_with_variant_2, else 1 + +N = values the key holds, one per variant: + machine (printer_options_with_variant_1, _2) = S × k + process (print_options_with_variant) = len(print_extruder_variant) = len(print_extruder_id) + filament (filament_options_with_variant) = len(filament_extruder_variant) +values[s × k + m] = variant index s, mode m (m = 0 normal, m = 1 silent; only m = 0 at stride 1) +variant index s = (printer_extruder_id[s], printer_extruder_variant[s]) +``` + +`printer_extruder_variant` is not sized by the equation; it defines `S`: it is `extruder_variant_list` +flattened extruder by extruder, and `printer_extruder_id[s]` is the extruder that index `s` came from. +A process that mirrors the printer's pairs, as [Process rule 1](#process) asks, has `N = S`; a +filament lists each variant string it is tuned for once, with no extruder id, so its `N` is its own +and independent of any one printer: it serves every printer in its `compatible_printers`, and a +variant string no extruder of a printer reports is simply never read there (a two-extruder printer +with `S = 7` serves a filament whose `N` is 3, and the filament keeps its 3 on a printer that offers +two variant strings). The +pair is what the loader reads, so the equality with the sum holds when the three keys agree, as +[Printer rule 2](#printer-machine) requires. + +A selectable preset that writes a variant key writes it at its own `N`; any other width, one value +included, is an error. A preset that leaves a key out takes what reaches it, the default or an array +it inherits or includes, which the loader resizes to the preset's `N`; `check --strict` holds that +array to the preset's `N` too. A base is not judged on its own: its arrays count only where they reach +a preset that does not override them. BBL is the model for strict: every printer-specific machine, +process and filament declares its layout and restates every variant key at its own `N`, even where all +the values are the same, keys Orca added to the sets included. +Without the lists, `extruder_variant_list` defaults to one +`Direct Drive Standard` per extruder, so a machine has `V_i = 1` and `S = E`, and a process or filament +has `N = 1`; the loader cuts a list-less machine to its first variant and, with +`single_extruder_multi_material` off, widens it again with that value ([variant length](#widths)). The per-extruder keys outside the sets +(`printer_extruder_options` plus `extruder_offset` and `extruder_colour`) and `extruder_variant_list` +itself hold `E` values. + +### Sizing examples + +**One extruder, two nozzle volume types**: `E = 1`, `V_1 = 2`, so `S = 2`; stride-1 keys hold 2 +values, `machine_max_*` hold 4: + +```json +"nozzle_diameter": ["0.4"], +"extruder_type": ["Direct Drive"], +"extruder_variant_list": ["Direct Drive Standard,Direct Drive High Flow"], +"printer_extruder_id": ["1", "1"], +"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow"], +"default_nozzle_volume_type": ["Standard"], +"retraction_length": ["0.8", "1.0"], +"machine_max_speed_x": ["500", "200", "600", "250"] +``` + +The matching process lists the same two pairs (`print_extruder_id` `["1", "1"]`, +`print_extruder_variant` as above) and holds 2 values per key, e.g. `outer_wall_speed` +`["200", "260"]`; a filament for it lists `["Direct Drive Standard", "Direct Drive High Flow"]` and +holds 2 values per key, e.g. `filament_max_volumetric_speed` `["16", "24"]`. + +**Four extruders, one nozzle volume type each** (a tool changer): `E = 4`, every `V_i = 1`, so +`S = 4`; stride-1 keys hold 4 values, `machine_max_*` hold 8: + +```json +"nozzle_diameter": ["0.4", "0.4", "0.6", "0.4"], +"extruder_type": ["Direct Drive", "Direct Drive", "Direct Drive", "Direct Drive"], +"extruder_variant_list": ["Direct Drive Standard", "Direct Drive Standard", + "Direct Drive Standard", "Direct Drive Standard"], +"printer_extruder_id": ["1", "2", "3", "4"], +"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive Standard", + "Direct Drive Standard", "Direct Drive Standard"], +"default_nozzle_volume_type": ["Standard", "Standard", "Standard", "Standard"], +"retraction_length": ["0.8", "0.8", "1.2", "0.8"], +"machine_max_speed_x": ["500", "200", "500", "200", "500", "200", "500", "200"] +``` + +The (normal, silent) pair is repeated per extruder: `["500", "200"]` would be width 2 against +`S × k = 8` (the loader pads it to `500, 200, 500, 500, …`) and `["500"]` width 1; both are errors. The +process mirrors the four pairs (`print_extruder_id` `["1", "2", "3", "4"]`) with 4 values per key, or +omits the variant keys altogether when nothing differs per extruder (then one value per key). A +filament for it lists only `["Direct Drive Standard"]`: one entry, so one value per key. Drop the +layout from this printer and the widths stay the same, since the default list gives `S = E = 4`; but +the loader then keeps only the first variant ([variant length](#widths)): `retraction_length` +becomes `0.8` on every extruder and extruder 3 loses its `1.2`. + +### Adding a variant inserts its values at its variant index + +Indexes run extruder-major: extruder 1's variants in `extruder_variant_list` order, then extruder +2's. A new variant's values go in at its index, not at the end. Giving extruder 1 of a two-extruder +printer a High Flow option, when only extruder 2 had one: + +| Key | Before | After | +| --- | --- | --- | +| `extruder_variant_list` | `["Direct Drive Standard", "Direct Drive Standard,Direct Drive High Flow"]` | `["Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"]` | +| `printer_extruder_id` | `["1", "2", "2"]` | `["1", "1", "2", "2"]` | +| `printer_extruder_variant` | `[Standard, Standard, High Flow]` | `[Standard, High Flow, Standard, High Flow]` (full strings in the file) | +| `retraction_length` (stride 1) | `["0.8", "1.0", "1.2"]` | `["0.8", "?", "1.0", "1.2"]`: one value at index 1 | +| `machine_max_speed_x` (stride 2) | `["500", "200", "600", "250", "700", "300"]` | `["500", "200", "?", "?", "600", "250", "700", "300"]`: a (normal, silent) pair at position 2 | +| `print_extruder_id` / `print_extruder_variant` / `outer_wall_speed` | mirror the printer | the same insertion at index 1 | +| `filament_extruder_variant` `[Standard, High Flow]` | — | unchanged: filament variants carry no extruder id, so the existing High Flow variant now serves both extruders | + +Every `?` is a measured value for that nozzle, on the machine limits as much as on retraction. +Removing or renaming a variant shifts the later values the same way in reverse; a variant string that +no longer matches the enum is simply a variant nothing selects. + +### Padding, truncation and composition + +**Every key in the set widens, whether or not a file restates it.** At load each variant key of the +composed config is resized to the length of the preset's own `*_extruder_variant` (the one-entry +default when it writes none) × stride: a short array is **padded by repeating its first value**, a +long one is truncated to its first values, without a word from the loader or the validator. The rule +does not follow from this padding: a file writes a variant key at **exactly `variant length × stride`** +or not at all. Any other length, one value included (which the loader spreads over every variant, at +stride 2 over normal *and* silent alike), is a mistake the loader hides and +`orca_profile_tool.py check` reports as an error in the selectable preset that writes it, even when +every value is the same; `check --strict` also reports an array that reaches a selectable preset at +another width +([variant arrays](validation.md#variant-arrays)). + +The loader sizes a list-less preset of any type to one variant at this step, a list-less machine +included: a two-extruder machine without a layout that writes `retraction_length` `["0.8", "0.9"]`, the +width the equation asks for, stores `["0.8"]`, which the pair rebuild and the slice-time collapse hand +to both extruders. A 3-value array on a preset of variant length 2 keeps its first two; at length 4 it +becomes `[a, b, c, a]`. + +Composition hands down widths in two ways: + +- **`inherits` hands down the parent's resized arrays.** A base is stored after its own resize, at + the length of its own `*_extruder_variant` (one variant for a base of any type that writes none, + whatever its extruder count), so an array wider than the base's list is cut to its first values + before any child sees it, and a child that adds variants gets those first values padded. Widen an + array only on a preset whose own resolved list already has the entries. `check` judges selectable + presets only, at the width each file wrote: an array a base's own resize cuts is not seen (a review + item), and an inherited array of another width than a child's list is left to the loader's resize + unless `check --strict`, which asks the child to restate it at its own width. +- **`include` hands down the template's diff at its pre-resize width.** The template contributes every + key where its composed config differs from the built-in defaults, taken before its own resize, so the + arrays it writes arrive at the width its file wrote, and the includer's own list sizes them. A key the + template sets to the built-in default is not passed on + ([`include`](vendor-bundle.md#inherits-and-include)). + +The id keys are the trap in this padding: `printer_extruder_id` and `print_extruder_id` are members +with default `[1]`, so a missing or one-value id array beside a longer variant list is padded to +extruder 1 at every index. That is right on a single-extruder printer and wrong on a multi-extruder +one, where every variant is then addressed as extruder 1's. A machine escapes it only where the +load-time pair rebuild of [Printer rule 2](#printer-machine) runs (`single_extruder_multi_material` +off); a process's `print_extruder_id` is never rebuilt at load. `check` +holds an id array that reaches a selectable preset, written or inherited, to one entry per variant on +any printer; `fix-variant` never pads an id array or a variant list, since those address the +variants rather than fill them. + +Consequences: + +- A base that gains a variant silently pads every descendant that restates a variant array at the old + width, and a short `machine_max_*` array copies variant 0's *normal* limit into the silent entries + too; a descendant that restates nothing inherits the widened array and needs no edit. Extend, in one + change: the printer base and each nozzle-diameter sibling that restates a variant array, the process + bases that mirror the printer's variants, and the filaments that should cover the variant. +- A one-value override is reported, and the loader spreads it over **all** variants, overwriting the + ones that should differ. +- A process or filament that omits the new variant is not padded; the variant resolves to its index 0. + +### Slice time and existing users + +**Slice time collapses variants to extruders.** When printer, process and filaments are combined on a +printer with several extruders or several variant strings, each printer and process variant key is +re-gathered to one entry per extruder (× stride) in extruder order, using each extruder's live nozzle +volume type, or to one entry per nozzle volume type for an extruder that holds several (Hybrid, or +`extruder_nozzle_stats` listing more types than there are extruders); on any other printer they are +not re-gathered and variant index 0 is read. Filament keys are re-gathered to one entry per filament, +on a printer with a single variant too once a filament has several variants, and under a dynamic +nozzle map to one entry per variant each filament prints through. Custom G-code and the +`machine_max_*` limits therefore index by extruder or filament, never by variant index; variant order +matters only inside the preset. An extruder with no matching variant reads variant index 0, extruder +1's first variant, whichever extruder it is; under layered nozzle grouping, `get_config_index_base` +reads the collapsed arrays and falls back to the same extruder's first entry instead. + +**Existing user presets and projects follow the variant string, not the position.** A user preset +stores every variant array it changed (nullable keys as per-variant diffs, `nil` where equal to the +parent). On load each parent variant takes the child's value for the variant with the same extruder id +and variant string; variants the parent gained keep the parent's value, and a child whose arrays the +parent cannot map keeps its own. Per-object process overrides are remapped when a printer change +alters the extruder count or the length of `printer_extruder_variant`, by variant string alone (no +extruder id; of several matching values the smallest wins), and a single-value override applies to +every variant. Adding or reordering variants in a shipped preset is therefore safe for existing +users; renaming a variant, or moving it to another extruder id, loses their values for it. + +**A new nozzle volume type changes no array.** Adding one to the code widens nothing until a profile +lists the new variant string; until then every existing array keeps its length and meaning. + +## The four key sets + +Membership is literal (four `std::set` initializers in `src/libslic3r/PrintConfig.cpp`) +and **not guessable from names**: `ironing_speed`, `skirt_speed`, `wipe_speed`, `scarf_joint_speed`, +`small_support_perimeter_speed` and `wipe_tower_max_purge_speed` are process speeds outside the set, +while every process `*_acceleration` and `*_jerk` key is inside, and so are +`small_perimeter_threshold`, `top_solid_infill_flow_ratio` and `slowdown_for_curled_perimeters`; +`filament_flush_temp` is in and `filament_flush_temp_fast` out; `use_firmware_retraction` is out and +`travel_slope` and `retract_lift_enforce` in. The three variant-list keys and the two id +keys are members of their own set (the list sizes itself, a no-op; the id keys are padded like any +other member, [the id trap](#padding-truncation-and-composition)), while `extruder_variant_list` is in +no set: the variant-length resize leaves it alone, and only the pair rebuild of +[Printer rule 2](#printer-machine) pads it. The per-extruder `printer_extruder_options` is listed +last for contrast; it is not a variant set. + +`check` and `fix-variant` read the four sets from `PrintConfig.cpp` on every run, so they follow the +engine. The lists below are from the 2026-09-25 checkout; regenerate them from the repository root +before relying on them (the recipe strips comments, since an initializer can carry a commented-out entry): + +```bash +python3 - <<'EOF' +import re +src = open('src/libslic3r/PrintConfig.cpp', encoding='utf-8', errors='replace').read() +for name in ['printer_options_with_variant_1', 'printer_options_with_variant_2', + 'print_options_with_variant', 'filament_options_with_variant', + 'printer_extruder_options']: + body = re.search(r'std::set\s+' + name + r'\s*=\s*\{(.*?)\};', src, re.S).group(1) + body = re.sub(r'/\*.*?\*/', '', body, flags=re.S) + body = re.sub(r'//[^\n]*', '', body) + print(name, sorted(set(re.findall(r'"([^"]+)"', body)))) +EOF +``` + +**`printer_options_with_variant_1`**, machine, stride 1 (27): `deretraction_speed`, `hotend_cooling_rate`, `hotend_heating_rate`, `long_retractions_when_cut`, `nozzle_flush_dataset`, `nozzle_type`, `nozzle_volume`, `printer_extruder_id`, `printer_extruder_variant`, `retract_after_wipe`, `retract_before_wipe`, `retract_length_toolchange`, `retract_lift_above`, `retract_lift_below`, `retract_lift_enforce`, `retract_restart_extra`, `retract_restart_extra_toolchange`, `retract_when_changing_layer`, `retraction_distances_when_cut`, `retraction_length`, `retraction_minimum_travel`, `retraction_speed`, `travel_slope`, `wipe`, `wipe_distance`, `z_hop`, `z_hop_types` + +**`printer_options_with_variant_2`**, machine, stride 2 (16): `machine_max_acceleration_e`, `machine_max_acceleration_extruding`, `machine_max_acceleration_retracting`, `machine_max_acceleration_travel`, `machine_max_acceleration_x`, `machine_max_acceleration_y`, `machine_max_acceleration_z`, `machine_max_jerk_e`, `machine_max_jerk_x`, `machine_max_jerk_y`, `machine_max_jerk_z`, `machine_max_junction_deviation`, `machine_max_speed_e`, `machine_max_speed_x`, `machine_max_speed_y`, `machine_max_speed_z` + +**`print_options_with_variant`**, process, stride 1 (45): `bridge_acceleration`, `bridge_speed`, `default_acceleration`, `default_jerk`, `default_junction_deviation`, `enable_overhang_speed`, `gap_infill_speed`, `infill_jerk`, `initial_layer_acceleration`, `initial_layer_infill_speed`, `initial_layer_jerk`, `initial_layer_speed`, `initial_layer_travel_acceleration`, `initial_layer_travel_jerk`, `initial_layer_travel_speed`, `inner_wall_acceleration`, `inner_wall_jerk`, `inner_wall_speed`, `internal_bridge_speed`, `internal_solid_infill_acceleration`, `internal_solid_infill_speed`, `outer_wall_acceleration`, `outer_wall_jerk`, `outer_wall_speed`, `overhang_1_4_speed`, `overhang_2_4_speed`, `overhang_3_4_speed`, `overhang_4_4_speed`, `print_extruder_id`, `print_extruder_variant`, `slowdown_for_curled_perimeters`, `small_perimeter_speed`, `small_perimeter_threshold`, `sparse_infill_acceleration`, `sparse_infill_speed`, `support_interface_speed`, `support_speed`, `top_solid_infill_flow_ratio`, `top_surface_acceleration`, `top_surface_jerk`, `top_surface_speed`, `travel_acceleration`, `travel_jerk`, `travel_speed`, `travel_speed_z` + +**`filament_options_with_variant`**, filament, stride 1 (48): `activate_air_filtration`, `activate_air_filtration_during_print`, `activate_air_filtration_on_completion`, `complete_print_exhaust_fan_speed`, `during_print_exhaust_fan_speed`, `filament_adaptive_volumetric_speed`, `filament_cooling_before_tower`, `filament_deretraction_speed`, `filament_extruder_variant`, `filament_flow_ratio`, `filament_flush_temp`, `filament_flush_volumetric_speed`, `filament_ironing_flow`, `filament_ironing_inset`, `filament_ironing_spacing`, `filament_ironing_speed`, `filament_long_retractions_when_cut`, `filament_max_volumetric_speed`, `filament_pre_cooling_temperature`, `filament_pre_cooling_temperature_nc`, `filament_preheat_temperature_delta`, `filament_ramming_travel_time`, `filament_ramming_travel_time_nc`, `filament_ramming_volumetric_speed`, `filament_ramming_volumetric_speed_nc`, `filament_retract_after_wipe`, `filament_retract_before_wipe`, `filament_retract_length_nc`, `filament_retract_length_toolchange`, `filament_retract_lift_above`, `filament_retract_lift_below`, `filament_retract_lift_enforce`, `filament_retract_restart_extra`, `filament_retract_restart_extra_toolchange`, `filament_retract_when_changing_layer`, `filament_retraction_distances_when_cut`, `filament_retraction_length`, `filament_retraction_minimum_travel`, `filament_retraction_speed`, `filament_wipe`, `filament_wipe_distance`, `filament_z_hop`, `filament_z_hop_types`, `long_retractions_when_ec`, `nozzle_temperature`, `nozzle_temperature_initial_layer`, `retraction_distances_when_ec`, `volumetric_speed_coefficients` + +**`printer_extruder_options`**, machine, one value per extruder, not a variant set (8): +`default_nozzle_volume_type`, `extruder_max_nozzle_count`, `extruder_printable_area`, +`extruder_printable_height`, `extruder_type`, `max_layer_height`, `min_layer_height`, +`nozzle_diameter`. These are never addressed by variant index; `extruder_offset`, `extruder_colour` and +`extruder_variant_list` are per extruder too. A shorter array acts as padded with its first value, and +entries beyond the extruder count are never read +([per-extruder vectors](machine-profiles.md#multi-extruder-idex-and-tool-changers)). + +## Checking and testing + +Checklist for a new variant profile set: + +1. Decide the variants per extruder from the real hardware; pick legal strings only. +2. Printer base: `extruder_type`, `extruder_variant_list`, flattened `printer_extruder_variant` + + `printer_extruder_id`, `default_nozzle_volume_type`; every variant array at variant length, every + `machine_max_*` at 2 × variant length as (normal, silent) pairs, even where the values are the + same, values in variant order. +3. A process base per variant layout, or no variant keys at all. +4. Filaments for the printer: a variant list covering the intended variants, every variant key at that + width, measured values per nozzle volume type. +5. Run the usual authoring commands and full checks. `check` holds each array of steps 2–4 to the + width of the selectable preset that writes it, `check --strict` also to every selectable preset it + reaches, and + the printer's layout keys through every selectable preset + ([variant arrays](validation.md#variant-arrays)); `check_variant_names` holds every variant string, + `extruder_type`, `nozzle_volume_type` and `default_nozzle_volume_type` to the enums + ([variant names](validation.md#variant-names)). The choice of variants, the variant order and + the measured values are not checked. +6. In the app, for each nozzle volume type in the sidebar combo: slice and confirm the G-code uses that + variant's values (e.g. volumetric speed limit, retraction). `validate_slice` only slices the default + nozzle volume type. + +To review rather than author, run the same list against the diff. `check` catches a wrong array length, +a layout key out of step, and a variant string the enums cannot build. The failures no check catches: a +new variant appended instead of inserted at its index, an array widened on a base whose list is +shorter (cut before any child inherits it), a process lacking a pair its printer can select, and a High +Flow variant copied from Standard. + +### UI facts to design around + +- The single-extruder sidebar shows a **nozzle volume type combo** (tooltip `Flow`, in place of the + nozzle-diameter selector) only when `extruder_variant_list` offers more than one distinct variant + string (`support_different_extruders`); four extruders listing `Direct Drive Standard` each show + none. +- The two-extruder sidebar's per-extruder nozzle volume type combos are shown for BBL printers only. + Another vendor's multi-extruder printer falls back to the single-extruder layout: one combo (for the + first extruder) when the variants differ, otherwise the nozzle-diameter selector, so the other + extruders' nozzle volume type stays at `default_nozzle_volume_type`. +- High Flow is hidden from the combo when `printer_variant` is `0.2` or the printer model is + `Bambu Lab X1E`, and E3D High Flow unless `printer_variant` is `0.4` or `0.6`. A variant listed on + another nozzle diameter is never selectable there. +- The filament tab shows a variant switch built from the filament's own `filament_extruder_variant`; + a multi-variant filament whose tab shows none did not resolve its variant list through `include` or + `inherits`. The printer and process tabs show one entry per extruder instead, labelled with that + extruder's live nozzle volume type (two for Hybrid), and only on two-extruder printers whose + `extruder_variant_list` offers more than one variant string; elsewhere they edit the variant of the + nozzle volume type selected in the sidebar (an X1C shows no switch). + +### Worked examples in the tree + +`BBL/machine/fdm_bbl_3dp_001_common.json` (one extruder), `fdm_bbl_3dp_002_common.json` (two +extruders), `Bambu Lab X1 Carbon 0.4 nozzle.json` (Standard + High Flow), `Bambu Lab H2D 0.4 +nozzle.json` (extruders with different variant sets), `Bambu Lab X2D 0.4 nozzle.json` (Direct Drive + +Bowden), with their `@BBL` processes and filaments. The H2D and X2D examples also carry +`E3D High Flow` variants. BBL's multi-variant filament lists come from `fdm_filament_template_*` +presets that the printer-specific filaments pull in with `include`, not from their `inherits` chain. diff --git a/.claude/skills/orca-profiles/references/filament-profiles.md b/.claude/skills/orca-profiles/references/filament-profiles.md index 873e2a6510..bc80176eaa 100644 --- a/.claude/skills/orca-profiles/references/filament-profiles.md +++ b/.claude/skills/orca-profiles/references/filament-profiles.md @@ -1,8 +1,8 @@ # Filament profiles and OrcaFilamentLibrary -`OrcaFilamentLibrary` is the filament-only bundle the loader reads **first**; its config map -becomes the base bundle, so any vendor may inherit a library preset by name. It is the only cross-bundle -parent — vendor-to-vendor inheritance always fails. +`OrcaFilamentLibrary` is the filament-only bundle the loader reads **first**, so any vendor's filament may +inherit a library preset by name. It is the only cross-bundle parent: vendor-to-vendor inheritance +always fails. ## Where a filament goes @@ -10,63 +10,65 @@ parent — vendor-to-vendor inheritance always fails. | --- | --- | | Generic material for all printers | `OrcaFilamentLibrary/filament/Generic @System.json` | | A brand's product, all printers | `OrcaFilamentLibrary/filament//` | -| A brand's tune for one printer | `OrcaFilamentLibrary/filament///` — recommended; `/filament//` also works | -| A printer vendor's tune of a generic or its own product | `/filament/` | +| A brand's tune for one printer vendor | `OrcaFilamentLibrary/filament///` (recommended); `/filament//` also works | +| A printer vendor's tune of a generic, or its own product | `/filament/` | -Both locations for the last-but-one row are supported: `OrcaFilamentLibrary/filament///.json` +Both locations in the third row are supported: `OrcaFilamentLibrary/filament///.json` (the shape the wiki shows) and `/filament//`. The library path is the one a -filament vendor should contribute to — `OrcaFilamentLibrary/filament//` is the brand's own -folder, while a printer vendor's folder belongs to that printer vendor. Brand tunes do ship under -printer vendors' folders today (Polymaker and SUNLU among others). +filament brand should contribute to: `OrcaFilamentLibrary/filament//` is the brand's own folder, +while a printer vendor's folder belongs to that printer vendor. -Library layout: `filament/base/fdm_filament_*.json` type roots, root-level `Generic @System.json` -generics, and one subfolder per brand, which may nest printer-specific tunes one level deeper. Adding -a brand means adding a folder here; the folder name is a directory label only — `filament_vendor` inside the JSON is the real vendor string. +Library layout: `filament/base/fdm_filament_*.json` material roots, root-level +`Generic @System.json` generics, and one subfolder per brand, which may nest printer-specific +tunes one level deeper. Adding a brand means adding a folder here; the folder name is a directory label +only, and `filament_vendor` inside the JSON is the real vendor string. ## The three-part shape ```jsonc -// Fiberon PA6-CF @base.json — the product root, holds identity + material values +// OrcaFilamentLibrary/filament/Polymaker/Fiberon PA6-CF @base.json — the product root: identity + material values { "type": "filament", "name": "Fiberon PA6-CF @base", "from": "system", "instantiation": "false", "inherits": "fdm_filament_pa", - "filament_id": "OFkOviHk", // generated here; variants inherit it + "filament_id": "OFkOviHk", // minted here by generate-id; every child inherits it "filament_vendor": ["Polymaker"], "filament_type": ["PA6-CF"], /* … */ } -// Fiberon PA6-CF @System.json — the selectable shim, 7 keys +// OrcaFilamentLibrary/filament/Polymaker/Fiberon PA6-CF @System.json — the selectable all-printer shim, 7 keys { "type": "filament", "name": "Fiberon PA6-CF @System", "from": "system", "instantiation": "true", "inherits": "Fiberon PA6-CF @base", "setting_id": "…", "compatible_printers": [] } -// /filament/Polymaker/Fiberon PA6-CF @BBL X1C.json — a printer tune -{ … "inherits": "Fiberon PA6-CF @base", "filament_max_volumetric_speed": ["14"], +// BBL/filament/Polymaker/Fiberon PA6-CF @BBL X1C.json — a printer tune (BBL keeps its own copy of the @base) +{ …, "inherits": "Fiberon PA6-CF @base", "filament_max_volumetric_speed": ["14"], "compatible_printers": ["Bambu Lab X1 Carbon 0.4 nozzle", …] } ``` -- `@base` is the convention for a root. A base carries **no** `setting_id`, no `compatible_printers`, no - `filament_settings_id`. Only the `setting_id` half is enforced, and nothing violates it; the other two - are unchecked and plenty of bases still carry them. Do not copy that from a neighbouring file. -- Every `@System` must be `"instantiation": "true"`. DREMC ships `@System` presets set to `"false"`, - which therefore ship but can never be selected; no check catches it. -- A duplicated brand `@base` across bundles is normal and intentional (`Fiberon PA6-CF @base` exists in - both the library and BBL with the same id, differing only in MVS) — bases never enter the preset - collection, so there is no duplicate-name error. -- You may inherit from an instantiated preset as well as from a base; it is common. +- `@base` is the convention for a root; a root is really `instantiation: "false"`. A base carries + **no** `setting_id`, no `compatible_printers` and no `filament_settings_id`. Only the `setting_id` + half is enforced; the other two are unchecked, so a neighbouring base that carries them is no model. +- Every `@System` shim must be `"instantiation": "true"`; one set to `"false"` would ship but could + never be selected, and no check catches it. The shim exists only for products in the library. +- `filament_cost`, `filament_density`, `filament_type` and `filament_vendor` belong on the root and + should not appear in a printer tune. +- A brand `@base` duplicated across bundles is legal (a vendor bundle may keep its own copy of a + library product root, with the same id): bases never become selectable presets and the + duplicate-name error covers only those, so there is none. +- You may inherit from an instantiated preset as well as from a base. -## Color is a runtime property +## Colour is a runtime property -`filament_id` identifies a product, not a color; filament sync/AMS reads the color from the spool at -runtime. A product ships one all-printer preset and the color is chosen at runtime — never a sibling -preset that differs only by color. A material family (PLA vs PLA Matte vs PLA Silk) is a new product; a -color is not. A printer tune keeps the product alias and does not multiply per color either. +`filament_id` identifies a product, not a colour; filament sync and AMS read the colour from the spool +at runtime. A product ships one all-printer preset and the colour is chosen at runtime, never a sibling +preset that differs only by colour. A material family (PLA vs PLA Matte vs PLA Silk) is a new product; +a colour is not. A printer tune keeps the product alias and does not multiply per colour either. -CI does not catch this — per-color presets pass `check` — so it is a review call. +CI does not catch this (per-colour presets pass `check`), so it is a review call. ## The two most common contributions -**A printer vendor tuning a generic.** Keep the `Generic X` base name so the alias shadows the library -preset on your printers, inherit `Generic X @System`, declare **no** `filament_id` (inheriting the -library's is correct — the product really is the library's generic), and give it a non-empty -`compatible_printers` in its own body: +**A printer vendor tuning a generic.** Keep the `Generic X` alias so it shadows the library preset on +your printers, inherit `Generic X @System`, declare **no** `filament_id` (inheriting the library's is +correct: the product really is the library's generic), and give it a non-empty `compatible_printers` in +its own file: ```jsonc // /filament/Generic PETG @Acme One 0.4 nozzle.json @@ -76,13 +78,14 @@ library's is correct — the product really is the library's generic), and give "compatible_printers": ["Acme One 0.4 nozzle"] } ``` -**A printer vendor's own branded product.** Give it a `@base` root so `generate-id` can mint the id (see -[ids.md](ids.md) — inheriting `Generic X @System` directly makes the id unfixable by the tool), then one -instantiated leaf per printer in the same bundle. No `@System` shim: that is only for a product entering -OrcaFilamentLibrary. +**A printer vendor's own branded product.** Give it a `@base` root on a material base so `generate-id` +can mint the id, then one instantiated leaf per printer in the same bundle. Inheriting +`Generic X @System` directly gives the product the generic's id, which the tool cannot fix +([ids.md](ids.md#what-generate-id-does-and-does-not-fix)). No `@System` shim: that is only for a product +entering OrcaFilamentLibrary. ```jsonc -// /filament/Acme Aura PETG @base.json — instantiation false, no setting_id +// /filament/Acme Aura PETG @base.json — instantiation false, no setting_id { "type": "filament", "name": "Acme Aura PETG @base", "from": "system", "instantiation": "false", "inherits": "fdm_filament_pet", "filament_vendor": ["Acme"], "filament_type": ["PETG"] } // filament_id minted here @@ -94,174 +97,190 @@ OrcaFilamentLibrary. "compatible_printers": ["Acme One 0.4 nozzle"] } ``` -Omit `filament_settings_id` from new presets — it is runtime bookkeeping the app rewrites to the preset +Omit `filament_settings_id` from new presets: it is runtime bookkeeping the app rewrites to the preset name. +To offer either kind by default, add its name to each model's `default_materials`; put it first in the +machine's `default_filament_profile` only if it should be the preselected filament +([machine keys](machine-profiles.md#other-keys)). + ## `compatible_printers` -- **Library fallbacks:** empty `[]` or absent, so they are offered on all printers except where - [alias shadowing](#alias-shadowing) supplies a printer-specific tune. -- **Library printer-specific tunes:** non-empty, listing exact printer **variant** names. These can +- **Library fallbacks** (`@System`): empty `[]` or absent, so they are offered on all printers except + where [alias shadowing](#alias-shadowing) supplies a printer-specific tune. +- **Library printer-specific tunes**: non-empty, listing exact printer **variant** names. These supersede a same-alias fallback just like a tune in a printer vendor's bundle. -- **Instantiated filaments in every other vendor:** non-empty, listing exact printer **variant** names. - Enforced twice but not identically: the C++ `has_errors` reads the *flattened* config, so an inherited list satisfies it, - while the Python check reads the file's **own** key. Write the list in the file itself. This is the - most common filament CI failure. -- Emptying it to "make it apply everywhere" fails that check *and* creates a duplicate-`filament_id` - collision against the library generic on every printer. -- Copying a base's full printer list onto a nozzle-specific variant produces duplicate combobox entries — - a real shipped bug twice over. +- **Instantiated filaments in every other vendor**: non-empty, listing exact printer **variant** names. + Enforced twice, but not identically: `validate_system` reads the resolved config, so an inherited list + satisfies it, while `check` reads the file's **own** key. Write the list in the file itself. This is + the most common filament CI failure. +- Emptying it to "make it apply everywhere" fails that check *and* collides with the library generic's + `filament_id` on every printer. +- Copying a base's full printer list onto a nozzle-specific tune produces duplicate combobox entries: a + real shipped bug twice over. ## Overlapping coverage: one variant, one profile per product -`filament_id` is the **product** key, not the preset key — every variant of one product shares it -(`//`). So if one printer variant appears in the -`compatible_printers` of two presets of that product, the slicer cannot tell them apart at AMS match time. -The C++ validator reports `Ambiguous AMS filament match: N presets share filament_id "X" … printer "Y"`. -`orca_profile_tool.py check` does **not** see it and passes. Resolve the overlap by **specificity**: keep -the variant on the most specific profile and remove it from every more general one. Deleting a profile is -the least preferred fix — moving coverage keeps the tune that users rely on. +`filament_id` is the **product** key, not the preset key: every preset of one product shares it +(`//`). So if one printer variant appears in the +`compatible_printers` of two presets of that product, the slicer cannot tell them apart at AMS match +time. `validate_system` reports `Ambiguous AMS filament match: N filament presets share filament_id "X" +and are all compatible with printer "Y"`; `orca_profile_tool.py check` does **not** see it and passes. +Resolve the overlap by **specificity**: keep the variant on the most specific profile and remove it from +every more general one. Deleting a profile is the least preferred fix: moving coverage keeps the tune +that users rely on. -Judge specificity from the profile's `compatible_printers` — how many variants it actually covers — and -use the name only as a secondary, easily-vague hint; decide by the lists, with a best judgement call on -the name. Naming conventions differ by vendor: BBL's is the reference (`@ ` for a whole +Judge specificity from the profile's `compatible_printers` (how many variants it actually covers) and +use the name only as a secondary, often vague hint; decide by the lists, with a judgement call on the +name. Naming conventions differ by vendor: BBL's is the reference (`@ ` for a whole model, `@ nozzle` for one variant, `@` for a vendor-wide generic), but others vary (`@`, a printer serial, or Creality's `@-all`). A name never overrides -the list — see [preset naming](naming.md) for the shapes. +the list; see [preset naming](naming.md#filament) for the shapes. Specificity, most to least: -1. **Variant-specialized** — lists a single printer variant (BBL-style - `... @ nozzle`). -2. **Model-specialized** — lists the variants of one printer model (BBL-style `... @ `). - It should cover every variant of its model, not only the nozzle it was authored for. -3. **Family / series** — lists variants spanning a printer family or series. -4. **Generic / catch-all** — vendor-wide, covering many unrelated models (often the bare +1. **Variant-specialized**: lists a single printer variant (BBL-style + `… @ nozzle`). +2. **Model-specialized**: lists the variants of one printer model (BBL-style `… @ `). It + should cover every variant of its model, not only the nozzle it was authored for. +3. **Family / series**: lists variants spanning a printer family or series. +4. **Generic / catch-all**: vendor-wide, covering many unrelated models (often the bare `Generic @`). Rules: - A model-specialized profile is extended to **all** variants of its model, and each variant it thereby - starts covering is removed from the family and generic profiles that also listed it — including variants - that had no overlap before. Apply it per nozzle, not just 0.4. + starts covering is removed from the family and generic profiles that also listed it, including + variants that had no overlap before. Apply it per nozzle, not just 0.4. - Apply it **per product**: trim only the material that has a specialized profile from the generic; a material whose product has no specialized profile keeps the variant in the generic. - Never strip coverage a variant has nowhere else to get. If a variant has no variant-level specialized - profile, the next level down keeps it; when the model has specialized profiles, the model-level one wins - over the family/generic. -- Moving coverage is preferred over deleting. If a profile must be deleted, remove the more general - one, not the specialized profile that carries the tune. -- After moving coverage, repoint the affected `default_filament_profile` (machine) and clean the model's - `default_materials`: they should name the most specific profile that covers the variant, and should not - keep generic entries that no longer cover the model. This rule applies equally when adding or fixing - defaults. + profile, the next level down keeps it; when the model has specialized profiles, the model-level one + wins over the family and generic ones. +- Moving coverage is preferred over deleting. If a profile must be deleted, remove the more general one, + not the specialized profile that carries the tune. +- After moving coverage, repoint the affected machine's `default_filament_profile` and clean the model's + `default_materials`: they should name the most specific profile that covers the variant, and should + not keep generic entries that no longer cover the model. This rule applies equally when adding or + fixing defaults. -Multiple profiles of one product with **disjoint** `compatible_printers` is the intended end state. +Several profiles of one product with **disjoint** `compatible_printers` is the intended end state. Adding coverage to the specialized profile and removing it from the generic is the preferred direction. -**Detection caveat:** `orca_profile_tool.py check` is blind to this; only the C++ validator behind the full -`./scripts/check_profile.sh` reports it (`validate_system`). Always confirm with that, not the vendor-scoped -loop. +**Detection caveat:** `orca_profile_tool.py check` is blind to this; only the validator behind the full +`./scripts/check_profile.sh` reports it (`validate_system`). Always confirm with that, not the +vendor-scoped loop. ## Alias shadowing -A printer-specific filament in either the library or a vendor bundle supersedes the library fallback -on the printers it lists. The matching key is the **alias**: the preset name up to the **first** `@`, -right-trimmed (no `@` → the whole name). So -`QIDI ABS-GF@Q2-Series` aliases to `QIDI ABS-GF`. +A printer-specific filament in either the library or a vendor bundle supersedes the library fallback on +the printers it lists. The matching key is the **alias**: the preset name up to the **first** `@`, +right-trimmed (no `@` → the whole name). So `QIDI ABS-GF@Q2-Series` aliases to `QIDI ABS-GF`. -A library preset with an empty `compatible_printers` collects, into `m_excluded_from`, every printer named -by any same-alias preset that *has* a non-empty list, and is then hidden on those printers. +A library preset with an empty `compatible_printers` is hidden on every printer that a same-alias preset +lists in a non-empty `compatible_printers`, whether that preset is in the library or in a vendor bundle +(a printer matches by its own name or its parent's). Two consequences: -- **Only an unrestricted library fallback can be shadowed.** Two printer-specific presets sharing - an alias do not exclude each other — overlapping lists for the same product trip the - duplicate-`filament_id` check instead. -- This is why adding `Generic PLA @` to a vendor silently removes the library `Generic PLA` - from that printer. Intended — and the reason a vendor tuning a generic must **keep the `Generic X` - base name**. +- **Only an unrestricted library fallback can be shadowed.** Two printer-specific presets sharing an + alias do not hide each other; overlapping lists for the same product trip the ambiguous-match error + above instead. +- This is why adding `Generic PLA @` to a vendor silently removes the library + `Generic PLA @System` from that printer. That is intended, and the reason a vendor tuning a generic + must **keep the `Generic X` alias**. -The literal spelling `Generic @System` is load-bearing beyond shadowing: `find_preset2` rewrites an -unresolved name containing "Generic" into that form and retries against the library, which is how 3MF -and project recovery works. +The literal spelling `Generic @System` is load-bearing beyond shadowing: when a user preset, an +imported preset or a 3MF project names a parent that no longer resolves and contains `Generic`, the +loader rewrites the name into `Generic @System` and retries. Only the library ships those names, +so keep them. ## `filament_id`, `filament_vendor`, `filament_type` -`filament_id` is minted from the triple `(filament_vendor, filament_type, name-before-first-@)`. -`filament_vendor` and `filament_type` are therefore **identity, not decoration** — editing either -re-mints the id. Read `docs/HLSD/filament_id.md` before changing any of them, and see -[ids.md](ids.md) for the tooling. +`filament_id` is minted from the triple `(filament_vendor, filament_type, alias)`. `filament_vendor` and +`filament_type` are therefore **identity, not decoration**: editing either, or the alias, re-mints the +id. Read `docs/HLSD/filament_id.md` before changing any of them, and see [ids.md](ids.md) for the +tooling. -A filament with no resolvable `filament_id` anywhere in its `inherits` chain is a **hard load error** that -discards the vendor bundle. The id inherits across bundles, so a vendor's `Generic ABS @X` inheriting -`Generic ABS @System` gets the library's id for free; a vendor's own product must resolve its own. +A filament with no resolvable `filament_id` anywhere in its `inherits` chain is a **hard load error** +that discards the vendor bundle. The id inherits across bundles, so a vendor's `Generic ABS @X` +inheriting `Generic ABS @System` gets the library's id for free; a vendor's own product must resolve its +own. -- `filament_type` **must be a JSON array** — the one vector key the Python check enforces. A scalar - `"PP"` once hung the filament/printer selection UI. +- `filament_type` **must be a JSON array**: the one vector key `check` rejects as a scalar outright. A + scalar `"PP"` once hung the filament and printer selection UI. - It is an **open** enum: an unlisted value is accepted silently and falls back to 190–300 °C defaults - and adhesion 1.0. Off-list values do ship. Prefer a value from `MaterialType::all()` in + and adhesion 1.0. Prefer a value from `MaterialType::all()` in `src/libslic3r/MaterialType.cpp`, or add a row there. - Generics use `filament_vendor: ["Generic"]`, which `fdm_filament_common` already defaults to. ## `"nil"` -Legal in any key whose `ConfigOptionDef` is `nullable`. In a filament preset that is most of the -`filament_*` family, plus `long_retractions_when_ec` and `retraction_distances_when_ec`. About half are -the extruder overrides (`filament_retraction_length`, `filament_z_hop`, `filament_wipe`, -`filament_retract_*`, `filament_retraction_speed`, `filament_deretraction_speed`, -`filament_retraction_minimum_travel`, `filament_wipe_distance`, `filament_long_retractions_when_cut`, -`filament_retraction_distances_when_cut`, …), where `nil` means *keep the printer/extruder's own value*. -The rest are ordinary nullable options (`filament_flow_ratio`, `filament_flush_temp`, -`filament_adaptive_volumetric_speed`, …) where it means *unset*. +`"nil"` is legal only in an option defined as nullable (`add_nullable`, or `nullable = true`, in +`src/libslic3r/PrintConfig.cpp`). Anywhere else it fails the file, and with it the **whole bundle** +(`Failed loading configuration file`, after `Deserializing nil into a non-nullable object` or +`Invalid value provided for parameter : nil`). To leave a non-nullable key unset, omit it; do not +write `nil`. -Anywhere else it throws `Deserializing nil into a non-nullable object`. To not set a non-nullable key, -omit it — do not write `nil`. +In filament presets a minority of the `filament_*` keys are nullable, plus `long_retractions_when_ec` +and `retraction_distances_when_ec`. About half of them are the extruder overrides (`filament_retraction_length`, +`filament_z_hop`, `filament_wipe`, `filament_retract_*`, `filament_retraction_speed`, +`filament_deretraction_speed`, `filament_retraction_minimum_travel`, `filament_wipe_distance`, +`filament_long_retractions_when_cut`, `filament_retraction_distances_when_cut`, …), where `nil` means +*keep the printer's or extruder's own value*. The rest are ordinary nullable options +(`filament_flow_ratio`, `filament_flush_temp`, `filament_adaptive_volumetric_speed`, …), where it means +*unset*. Check the option's definition before writing `nil` anywhere else. -## What to review per nozzle +## Tuning per nozzle and per variant -Across `@X` / `@X 0.N nozzle` sibling pairs the keys that differ, most often first, are +Between a product's `@X` and `@X 0.N nozzle` tunes the keys that usually differ, most often first, are `filament_max_volumetric_speed`, `filament_retraction_length`, `slow_down_min_speed`, -`filament_flow_ratio`, `slow_down_layer_time`, `nozzle_temperature` and `pressure_advance`. -`filament_cost`, `filament_density`, `filament_type` and `filament_vendor` belong on the `@base` and -should not appear in a printer tune. +`filament_flow_ratio`, `slow_down_layer_time`, `nozzle_temperature` and `pressure_advance` (switched on +by `enable_pressure_advance`). Use measured values for the material, hotend, extruder and nozzle combination. Neither maximum -volumetric speed nor pressure advance has a universal nozzle-only lookup table. When cloning a -0.4 preset for a 0.2 nozzle, explicitly revisit flow limits; do not infer a pressure-advance value -or a required direction of change from diameter alone. +volumetric speed nor pressure advance has a universal nozzle-only lookup table. When cloning a 0.4 +preset for a 0.2 nozzle, explicitly revisit flow limits; do not infer a pressure-advance value, or a +required direction of change, from diameter alone. -## Style - -Overrides, not full copies: a typical instantiated filament preset carries around a dozen non-meta keys, -and a library leaf two or three. Presets that restate fifty-plus keys from their parent do still ship — -Phrozen's single filament preset is that style — but they are the pattern to move away from, not to -copy. Commit `6943b6ddc3` is the stated model (flip true bases to `instantiation: "false"`, strip -`compatible_printers`/`setting_id`/`filament_settings_id`, add `renamed_from` on the survivor). - -Prefer the library's `fdm_filament_*` bases over a vendor-local copy. Phrozen's local -`fdm_filament_common` has drifted from the library's. - -Canonical key order, written by `orca_profile_tool.py normalize` when it rewrites a file: `type`, `name`, -`renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, `instantiation`, then everything else in -the order you wrote it. Not enforced — a file that leads with `compatible_printers` passes `check`. - -**Every vector-typed (`co*s`) key must be a JSON array.** Only `filament_type` is an outright error, but -`normalize` silently arrayifies five more (`filament_cost`, `filament_density`, -`temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`) and `check` fails when -it would. Every other vector key is on you — including `filament_start_gcode`, `filament_end_gcode`, -`filament_extruder_variant`, `compatible_printers` and the plate temperatures. +On a printer with extruder variants, a filament tunes these per variant too: +`filament_max_volumetric_speed`, `filament_flow_ratio`, `nozzle_temperature` and the retraction +overrides carry one value per variant of `filament_extruder_variant` (Standard, High Flow, …). The +exact key set is [`filament_options_with_variant`](extruder-variants.md#the-four-key-sets); +`slow_down_min_speed` and `pressure_advance` are not in it. Keep every such array at exactly that width, even where the +setting does not differ per variant, and measure the High Flow variant rather than copying Standard +([extruder-variants.md](extruder-variants.md#filament)). ## Bed temperature is twelve keys, not one -There is no single "bed temperature". Which plate key applies depends on `curr_bed_type`, whose six -selectable values (`btPC`, `btEP`, `btPEI`, `btPTE`, `btPCT`, `btSuperTack`; `btDefault` maps to no key) -`get_bed_temp_key()` turns into `cool_plate_temp`, `eng_plate_temp`, `hot_plate_temp`, -`textured_plate_temp`, `textured_cool_plate_temp` and `supertack_plate_temp` — each with an -`*_initial_layer` twin. +There is no single "bed temperature". The plate type selected for the printer (`Cool Plate`, +`Engineering Plate`, `High Temp Plate`, `Textured PEI Plate`, `Textured Cool Plate`, `Supertack Plate`) +picks one of six keys, each with an `_initial_layer` twin: `cool_plate_temp`, `eng_plate_temp`, +`hot_plate_temp`, `textured_plate_temp`, `textured_cool_plate_temp` and `supertack_plate_temp`. -`textured_cool_plate_temp` is the one most often forgotten. A printer with `support_multi_bed_types` off -hides the selector, and the printer preset's -`default_bed_type` decides which plate is selected for it, but `curr_bed_type` can still hold a stale -value carried over from another printer — so set every plate the printer plausibly has, as the sibling -presets in the bundle do. +`textured_cool_plate_temp` is the one most often forgotten. A non-BBL printer with +`support_multi_bed_types` off hides the plate selector and uses the printer preset's `default_bed_type` +(High Temp Plate, `hot_plate_temp`, when unset or invalid), but a loaded project or a CLI config can +still carry another plate. So set every plate the printer plausibly has, as the sibling presets in the +bundle do. + +## Style + +- Overrides, not full copies: an instantiated filament preset carries around a dozen non-meta keys, + and a library `@System` shim two or three. A preset that restates fifty-plus keys from its parent is + the pattern to move away from, not to copy. Commit `6943b6ddc3` is the stated model for converting such presets: flip true + bases to `instantiation: "false"`, strip their `compatible_printers`, `setting_id` and + `filament_settings_id`, and add `renamed_from` on the surviving selectable preset. A value every + printer tune of a product shares goes on the product's `@base` + ([shared bases](shared-bases.md#levels)). +- Prefer the library's `fdm_filament_*` bases over a vendor-local copy; for a new preset, even in a + bundle whose older presets use one: a local copy drifts from the library's. +- Canonical key order, written by `orca_profile_tool.py normalize` when it rewrites a file: `type`, + `name`, `renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, `instantiation`, then + everything else in the order you wrote it. Not enforced on its own: a file that leads with + `compatible_printers` passes `check`. +- **Every vector-typed (`co…s`) key must be a JSON array.** Only a scalar `filament_type` is an outright + error; `normalize` silently arrayifies five more (`filament_cost`, `filament_density`, + `temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`), and `check` fails + when it would. Every other vector key is on you, including `filament_start_gcode`, + `filament_end_gcode`, `filament_extruder_variant`, `compatible_printers` and the plate temperatures. diff --git a/.claude/skills/orca-profiles/references/ids.md b/.claude/skills/orca-profiles/references/ids.md index b6a09e9a2f..214a3d719c 100644 --- a/.claude/skills/orca-profiles/references/ids.md +++ b/.claude/skills/orca-profiles/references/ids.md @@ -1,76 +1,78 @@ # `setting_id` and `filament_id` Orca-generated ids are deterministic hashes of identity. **Never invent an id or copy a sibling's -`setting_id`.** Use `scripts/orca_profile_tool.py`; the two special cases are +`setting_id`.** +Use `scripts/orca_profile_tool.py`; the two special cases are [a wrongly inherited filament id](#what-generate-id-does-and-does-not-fix) and [BBL's authoritative setting ids](#bbls-exception-precisely). -`docs/HLSD/filament_id.md` is the authoritative design document for `filament_id` — the id landscape, the -checks CI runs, and the Bambu catalog map. This page is the tooling half. +`docs/HLSD/filament_id.md` is the authoritative design document for `filament_id`: the id landscape, +the checks CI runs, and the Bambu catalog map. This page is the tooling half. | | `setting_id` | `filament_id` | | --- | --- | --- | | Identifies | one selectable preset | one filament **product** | -| Key hashed | `//` | `filament_product///` | -| Shape | 16 base62 chars | `OF` + 6 base62 chars | +| Key hashed | `//` | `filament_product///`, using the resolved (inherited) first values and the name up to the first `@`, right-trimmed | +| Shape | 16 base62 characters | `OF` + 6 base62 characters | | Required on | every `instantiation: "true"` preset | every **instantiated** filament, own or inherited | -| Forbidden on | bases (`instantiation != "true"`) | — (a base is exactly where it belongs) | -| Scope | globally unique across the tree | shared by every variant of the product, in every bundle | +| Forbidden on | bases (`instantiation` not `"true"`) | — (a product's root base is exactly where it belongs) | +| Scope | unique across the whole tree | shared by every preset of the product, in every bundle | -`` is `machine` / `process` / `filament` — the vendor is the **folder** name (`BBL`), not the -display name (`Bambulab`). Renaming a preset changes its `setting_id`; renaming a filament, or editing -its `filament_vendor` or `filament_type`, also changes its `filament_id`. +`` is `machine`, `process` or `filament`, and the vendor is the **folder** name (`BBL`), not the +display name (`Bambulab`). Renaming a preset changes its `setting_id`; renaming a filament's alias, or +editing its `filament_vendor` or `filament_type`, also changes its `filament_id`, and the old id is not +forwarded. ## The tool -Use `scripts/orca_profile_tool.py` with a subcommand: +`scripts/orca_profile_tool.py` takes a subcommand: | Command | Does | | --- | --- | -| `check` | everything CI's `profile_tool` step runs — see [validation.md](validation.md) | +| `check` | everything CI's `profile_tool` step runs; see [validation.md](validation.md#orca_profile_toolpy-check) | | `generate-id` | writes `setting_id` and `filament_id` | | `normalize` | rewrites profile files into their canonical shape | -| `trim` | deletes profile files no `.json` list references | +| `trim` | deletes profile files no `.json` list references | | `update-index` | rebuilds the `*_list` sections from the files on disk | -The order after adding, renaming or deleting files — each step feeds the next, so it is not -interchangeable — is `normalize` → `update-index` → `generate-id` → `check`. -The [authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands. +The order after adding, renaming or deleting files is `normalize` → `update-index` → `generate-id` → +`check`. Each step feeds the next, so it is not interchangeable. The +[authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands. -> **`trim` deletes.** It removes every profile file the index does not list — including the one you just -> added and have not registered yet. Register first, or skip `trim` entirely; it is a cleanup sweep, not +> **`trim` deletes.** It removes every profile file the index does not list, including the one you just +> added and have not registered yet. Register first, or skip `trim` entirely: it is a cleanup sweep, not > part of landing a profile. Preview with `--dry-run`. **Register, then mint.** The `filament_id` pass reads `.json`'s `filament_list`, not the filesystem (the `setting_id` pass walks the filesystem, so a bundle whose index has not landed yet is -still assignable). A new filament file is therefore invisible to `generate-id`'s filament_id pass until -it is registered — its `setting_id` is written regardless. +still assignable). A new filament file is therefore invisible to `generate-id`'s `filament_id` pass +until it is registered; its `setting_id` is written regardless. -- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`) - and writes nothing. -- `--filament-id` / `--setting-id` narrow `generate-id`; they exclude each other, and passing neither - writes both. -- `--vendor` is repeatable and narrows **only what is written** — the id is a function of the triple +- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`) and + writes nothing. +- `--filament-id` / `--setting-id` narrow `generate-id` to one pass; they exclude each other, and + passing neither writes both. +- `--vendor` is repeatable and narrows **only what is written**: an id is a function of its own key alone, so a narrowed run writes exactly what a full run would. An unknown vendor exits 1 before any write. `--vendor` on `check` narrows the per-vendor checks only; the `setting_id` and `filament_id` passes stay tree-wide. -- `--profiles DIR` points any command at another tree — see +- `--profiles DIR` points any command at another tree; see [Checking a copy of the tree](validation.md#checking-a-copy-of-the-tree). - `--profile-type` narrows `normalize`, `trim` and `update-index` to `machine_model`, `process`, `filament` or `machine`. - Exit codes: 0 clean, 1 errors found (`generate-id` still writes what it could), 2 argparse misuse. - Output is ANSI-coloured; searching for the literal `[ERROR]` still works. -`generate-id` is **idempotent and byte-preserving** — BOM and CRLF kept, one key line touched per pass. -A legitimate `generate-id` diff is one or two changed lines per file: a new instantiated filament gets -both a `filament_id` and a `setting_id`, and a BBL file with a misspelled `settings_id` has that line -dropped and its value restored under the right key. `normalize` is the opposite by design — it rewrites -whole files into canonical shape — which is why `check` demands it already be a no-op. Some bundles have -CRLF committed (OrcaFilamentLibrary, Anycubic and RH3D among them), so a `normalize` pass there rewrites -every line — read the diff before committing it. +`generate-id` is **idempotent and byte-preserving**: BOM and CRLF are kept, and each pass touches only +its one key line. A legitimate `generate-id` diff is one or two changed lines per file: a new instantiated +filament gets both a `filament_id` and a `setting_id`. `normalize` is the opposite by design (it +rewrites whole files into canonical shape), which is why `check` demands it already be a no-op. A file +committed with CRLF line endings changes on every line under `normalize`; read the diff before +committing it. -On a clean tree `check` and `generate-id --dry-run` both exit 0 with zero findings. That is the -baseline to restore before opening a PR. +Exit 1 from `generate-id` does not mean nothing was written: it writes every id it can and reports the +rest, so read the diff before rerunning. On a clean tree `check` and `generate-id --dry-run` both exit 0 +with zero findings; that is the baseline to restore before opening a PR. ## What `generate-id` does and does not fix @@ -78,35 +80,37 @@ Writes: - a `setting_id` into any instantiated preset that lacks one, or whose value does not match the formula; - strips a `setting_id` from a base; -- deletes the misspelled `settings_id` key; +- deletes the misspelled key `settings_id`, moving its value to `setting_id` only on an instantiated BBL + preset that lacks one (everywhere else the old value is discarded and a fresh id minted); - a `filament_id` into the id-less **root(s)** of an instantiated filament that resolves none; - rewrites a **declared** `filament_id` that is not the mint of its own triple. Refuses to write (reports only): a base62 collision between two products, an empty `filament_vendor` or -`filament_type`, a broken `inherits` chain, roots of one filament resolving divergent `(vendor, type)` -pairs. +`filament_type`, a broken `inherits` chain, and roots of one filament resolving different +`(filament_vendor, filament_type)` pairs. -**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b, and it is the trap -most likely to bite. It happens when a branded filament inherits a generic for its settings: +**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b (the label `docs/HLSD/filament_id.md` and the tool use), and it is the trap most likely to bite. +It happens when a branded filament inherits a generic for its settings: ```jsonc { "name": "Phrozen Aura PETG @Phrozen Arco 0.4 nozzle", - "inherits": "Generic PETG @System" } // resolves the OFL generic's id — wrong product + "inherits": "Generic PETG @System" } // resolves the library generic's id: wrong product ``` -The preset resolves *an* id, so `generate-id` neither inserts nor rewrites, and `check` fails with +The preset resolves *an* id, so `generate-id` neither inserts nor rewrites one, and `check` fails with `inherits filament_id "X" but its own triple "V/T/N" mints "Y"`. Two fixes, in order of preference: 1. **Give the product a `@base` root** inheriting a material base (`fdm_filament_pet`, - `fdm_filament_pla`, …). No `fdm_filament_*` base carries a `filament_id`, so the filament now resolves - none and `generate-id` mints it for you. This is also the shape the rest of the tree uses. -2. **Declare the tool-computed key on the preset itself.** Use the expected value reported by `check` - or compute it with the function below; this is not a manually chosen id. Make sure the preset - resolves the right `filament_vendor` and `filament_type` first — with - neither set, the triple resolves through the generic parent and the branded product is minted - under vendor `Generic`. If you need the id before the file exists: + `fdm_filament_pla`, …) with its own `filament_vendor` and `filament_type`. No `fdm_filament_*` base + carries a `filament_id`, so the filament now resolves none and `generate-id` mints it on the root. + This is the product-root shape ([the three-part shape](filament-profiles.md#the-three-part-shape)). +2. **Declare the tool-computed id on the preset itself.** Use the expected value `check` reports, or + compute it with the function below; this is not a manually chosen id. First make sure the preset + resolves the right `filament_vendor` and `filament_type`: with neither set, the triple resolves + through the generic parent and the branded product is minted under vendor `Generic`. If you need the + id before the file exists: ```bash python3 -c "import sys; sys.path.insert(0,'scripts'); from orca_profile_tool import generate_filament_id as g; print(g('Polymaker','PLA','PolyLite PLA'))" @@ -114,36 +118,39 @@ Two fixes, in order of preference: ``` The quoting works unchanged in cmd and PowerShell; only swap `python3` for `py -3`. + `generate_preset_setting_id('', '', '')` is the `setting_id` equivalent. - The `setting_id` equivalent is `generate_preset_setting_id('', '', '')`. +A vendor's tune of a generic that keeps the `Generic X` alias is not this case: inheriting the +generic's id is correct there, because the product really is the library generic +([filament-profiles.md](filament-profiles.md#the-two-most-common-contributions)). ## BBL's exception, precisely -`RESERVED_VENDORS = {"BBL"}` covers **`setting_id` assignment only**, keyed on the *folder* name: +The exception covers **`setting_id` assignment only**, keyed on the *folder* name `BBL`: -- The tool never mints or replaces a BBL `setting_id`. A new instantiated BBL preset with no - `setting_id` therefore **cannot be fixed by the tool**, yet the presence rule still applies to it — - carry over Bambu's authoritative id by hand. +- The tool never mints or replaces a `setting_id` in `BBL/`: those are Bambu's own ids. A new + instantiated BBL preset with no `setting_id` therefore **cannot be fixed by the tool**, yet the + presence rule still applies to it: carry over Bambu's authoritative id by hand. - BBL is not exempt from anything else: bases still get their `setting_id` stripped, ids must still be - globally unique, and BBL `filament_id`s are minted like everyone else's — every one of them is an - `OF*`. + unique across the tree, and BBL `filament_id`s are minted like everyone else's, as `OF…` ids. ## Ids other systems compose -No id from another system is the mint of a triple, so `check` rejects it like any other bad id — same -error, same remedy, whoever wrote it. Three such spaces exist near the tree; recognise them so you do -not copy one into a profile: +No id from another system is the mint of a triple, so `check` rejects one used as a `filament_id` like +any other bad id: same error, same remedy, whoever wrote it. Three such spaces exist near the tree; +recognise them so you do not copy one into a profile: -- **Bambu's `GF*` catalog** — external and opaque, correlated to Orca's ids by the generated - `resources/printers/bambu_filament_ids.json`. `GF` is a *prefix*, not a spelling the tree avoids: most - BBL `setting_id`s start with `G`, and `blacklist.json` and - `BBL/filament/filaments_color_codes.json` both reference Bambu catalog ids by design. The rule is - about `filament_id` and nothing else. -- **Qidi's `QD_*`** — composed at runtime by the box (`QD___`), not a preset id. -- **`P` + 7 hex, and `"null"`** — what `CreatePresetsDialog.cpp` gives a *user*-created filament. +- **Bambu's `GF…` catalog**: external and opaque, correlated to Orca's ids by the generated + `resources/printers/bambu_filament_ids.json`. `blacklist.json` and + `BBL/filament/filaments_color_codes.json` reference Bambu catalog ids by design. The rule is about + `filament_id` and nothing else: every BBL `setting_id` starts with `G`, and that is Bambu's own + preset id, not a leaked catalog id. +- **Qidi's `QD_…`**: composed at runtime by the printer's filament box + (`QD___`), not a preset id. +- **`P` + 7 hex digits, and `"null"`**: what the app gives a *user*-created filament. ## Tests -`python3 -m unittest discover -s scripts/tests -t scripts` (`py -3 -m …` on Windows). Note the -`-t scripts` argument; without it the imports fail. CI runs them as the first, non-`continue-on-error` -step of the profile job — see [validation.md](validation.md#ci). +`python3 -m unittest discover -s scripts/tests -t scripts` (`py -3 -m …` on Windows) runs the tool's +unit tests. Note the `-t scripts` argument; without it the imports fail. CI runs them as the first, +non-`continue-on-error` step of the profile job; see [validation.md](validation.md#ci). diff --git a/.claude/skills/orca-profiles/references/machine-profiles.md b/.claude/skills/orca-profiles/references/machine-profiles.md index 01071af6b1..de338f14ee 100644 --- a/.claude/skills/orca-profiles/references/machine-profiles.md +++ b/.claude/skills/orca-profiles/references/machine-profiles.md @@ -1,23 +1,23 @@ # Printer models and variants -Both live in `resources/profiles//machine/*.json`; models go in `machine_model_list`, variants -and shared bases in `machine_list`. Every one of them is registered. Some vendors (Elegoo, Eryone, -InfiMech, FlyingBear) nest a further subfolder under `machine/`, so recurse rather than globbing -`machine/*.json`. +Both live in `resources/profiles//machine/`; models go in `machine_model_list`, variants and +shared bases in `machine_list`. Every one of them is registered. A bundle may nest further subfolders +under `machine/`, so recurse rather than globbing `machine/*.json`. -## A `machine_model` is not a config preset +## `machine_model`: a record, not a config preset -It is parsed by a hand-written key switch, and only these keys are stored (`version` and `url` are -matched and discarded): +The loader reads a fixed set of keys from a `machine_model` and stores only these (`version` and `url` +are recognised and discarded): `name`, `model_id`, `nozzle_diameter`, `machine_tech`, `family`, `bed_model`, `bed_texture`, `hotend_model`, `default_materials`, `not_support_bed_type`, `image_bed_type`, `bottom_texture_end_name`, `bottom_texture_rect`, `bottom_texture_rect_longer`, `middle_texture_rect`, `use_double_extruder_default_texture`. -**Everything else is silently dropped.** Only `name` and `nozzle_diameter` are required. Dead keys ship -on real models today — `url`, `default_bed_type`, even a `desciption` typo — so a neighbour carrying a -key is no evidence it does anything. Printer config options belong on the `machine` preset, never here. +**Everything else is silently dropped**, a printer config key such as `default_bed_type` or a +misspelling included, so a neighbour carrying a key is no evidence it does anything. Printer config options belong on the `machine` preset, never here. The loader drops a model +silently if its index entry has no name or its `nozzle_diameter` yields no sizes; `check` also requires +the file's own `name`. ```json { @@ -36,28 +36,28 @@ key is no evidence it does anything. Printer config options belong on the `machi | Field | Notes | | --- | --- | -| identity | **the `name` of the `machine_model_list` entry**, which is what a variant's `printer_model` must equal. `check_name_consistency` forces it to equal the file's `name`, so they coincide. | -| `model_id` | a *separate* cloud/device printer type. Optional, and not required to be unique. Not the model's identity. Changing it changes device matching. | -| `machine_tech` | only `starts_with("SL")` means SLA; everything else is FFF. Write `FFF`; a few models write `FGF`, which is a label with no effect. | -| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (Qidi writes `0.4;0.2;0.6;0.8` to put the default first). This list is the authoritative set of legal `printer_variant` values. | -| `default_materials` | `;`-separated filament **preset names**. Used to preselect in the wizard *and* by `PresetBundle::load_installed_filaments` to auto-install a printer's filaments on first run, so a dangling entry costs a real user a filament. Not `,`; case-sensitive (`@System`). `check` fails on a dangling name here or in `default_filament_profile`. | +| identity | **the `name` of the `machine_model_list` entry**, which is what a variant's `printer_model` must equal. `check` forces it to equal the file's `name`, so they coincide. | +| `model_id` | a *separate* cloud/device printer type. Optional, and not required to be unique. Not the model's identity; changing it changes device matching. | +| `machine_tech` | only a value starting with `SL` means SLA; everything else is FFF. Write `FFF`; `FGF` behaves as FFF. | +| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (`0.4;0.2;0.6;0.8` puts the default first). This list is the authoritative set of legal `printer_variant` values. | +| `default_materials` | `;`-separated filament **preset names**, not `,`; case-sensitive (`@System`); order is ignored. Used to preselect filaments in the setup wizard *and* to install a printer's filaments on first run, so a dangling entry costs a real user a filament. Every name must exist (`check` fails on a name matching no filament file, here or in `default_filament_profile`), and every variant of the model needs at least one entry compatible with it (`validate_system`). | | `family` | a wizard grouping label only; give every model one. | ### Assets -`bed_model`, `bed_texture` and `hotend_model` are paths relative to the **vendor folder** (by id). -Majority convention: `_buildplate_model.stl` and `_buildplate_texture.svg`. An empty string -is the legal "none", and is the norm for `hotend_model`. +`bed_model`, `bed_texture` and `hotend_model` are paths relative to the **vendor folder** (named by the +vendor id). Convention: `_buildplate_model.stl` and `_buildplate_texture.svg`. An +empty string is the legal "none", and is the norm for `hotend_model`. -**Nothing checks that the file exists.** A missing `hotend_model` falls back to -`resources/profiles/hotend.stl`; a missing `bed_model`/`bed_texture` just renders nothing. Broken -references already ship. Verify by hand. +**Nothing checks that the files exist.** A missing `hotend_model` falls back to +`resources/profiles/hotend.stl`; a missing `bed_model` makes the bed render as a generic custom bed, and a missing +`bed_texture` renders no texture. +Verify by hand, in exact case. -Every model also has a `_cover.png` in the vendor folder — treat it as required, not optional. -240×240 is the cap `scripts/optimize_cover_images.py` enforces and the size most covers already use. -A missing cover degrades to a placeholder in both the wizard and the sidebar. +Every model also has a `_cover.png` in the vendor folder; treat it as required, not optional. +240×240 is the cap `scripts/optimize_cover_images.py` enforces. A missing cover degrades to a placeholder in both the wizard and the sidebar. -## The `machine` variant +## `machine`: the variant ```json { @@ -79,116 +79,154 @@ A missing cover degrades to a placeholder in both the wizard and the sidebar. Minimum viable key set: `type`, `name`, `from`, `instantiation`, `setting_id`, `inherits`, `printer_model`, `printer_variant`, `nozzle_diameter`, `printable_area`, `printable_height`, -`default_print_profile`. The four keys without which the preset will not load at all are `name`, -`instantiation`, `printer_model` and `printer_variant`; `default_filament_profile` is an array -(`["Generic PLA @System"]`) and the model's `default_materials` a `;`-separated string. Unlike a -`machine_model`, a `machine` **is** config-loaded, so a key belonging to another preset type is a -reported error (a misspelled key is still silent). +`default_print_profile`. `default_filament_profile` is optional; when written it +is an array (`["Generic PLA @System"]`), while the model's `default_materials` is a `;`-separated +string. Unlike a `machine_model`, a `machine` **is** a config preset, so a key belonging to another +preset type is a reported error and is removed; a misspelled key is still dropped silently. -### `printer_variant` — three hard rules +### `printer_model` and `printer_variant` -1. Non-empty, and an exact member of the model's `;`-separated `nozzle_diameter` list. -2. `printer_model` non-empty and naming a model of this vendor. -3. In validation mode, for instantiated presets only: split `printer_variant` on `+`, each token must - start with a number (a trailing non-numeric suffix such as `HF` is ignored), and the resulting **set** - must equal `set(nozzle_diameter)`. +1. `printer_model` is non-empty and names a model of this bundle exactly. +2. `printer_variant` is non-empty and an exact token of that model's `;`-separated `nozzle_diameter` + list. +3. For instantiated presets, when validating: split `printer_variant` on `+`; each token must start + with a number (a trailing non-numeric suffix such as `HF` is ignored), and the resulting **set** must + equal the set of `nozzle_diameter` values. -Rules 1 and 2 are loader-enforced — failing either drops the preset *and* the whole bundle. Rule 3 only -raises a validation error: the preset still loads, but the validator exits non-zero. +Rules 1 and 2 are loader-enforced: failing either discards the whole bundle. Rule 3 only raises a +validation error: the preset still loads, but the validator exits non-zero. -`nozzle_diameter` lists one entry **per physical nozzle**; `printer_variant` lists the **distinct** -diameters joined with `+`. Snapmaker U1 is the worked case: `["0.4","0.4","0.6","0.6"]` against -`"0.4+0.6"` — it passes because the comparison is on sets. +`nozzle_diameter` lists one entry **per extruder**; `printer_variant` lists the **distinct** diameters +joined with `+`: `["0.4","0.4","0.6","0.6"]` against `"0.4+0.6"` passes because the comparison is on +sets. -The conventional values are `0.2`, `0.25`, `0.4`, `0.5`, `0.6`, `0.8` and `1.0`. Suffixed forms -(`0.4HF`, `0.6HF`, `0.8HF`, `0.4HS`) are Flashforge-only and the `+` form is rare. A variant is **not** -required to be unique within a model — Volumic ships `EXO42 IDRE`, `… COPY MODE` and `… MIRROR MODE` all -at `0.4` under the one model `EXO42 IDRE`. +Write `printer_variant` as a bare diameter matching the model's list, with no unit. The conventional +values are `0.2`, `0.25`, `0.4`, `0.5`, `0.6`, `0.8` and `1.0`; a suffixed form (`0.4HF`, `0.4HS`) or +the `+` form is legal under rule 3. A `printer_variant` is **not** required to be unique within a +model: an IDEX model's normal, `COPY MODE` and `MIRROR MODE` presets can all be `0.4`. The converse is **unchecked**: a nozzle size in the model's list with no matching variant is offered in -the wizard and resolves to nothing. `Wanhao France`'s `D12 500 PRO M2 DIRECT` ships that bug today. +the wizard and resolves to nothing. A variant whose `printer_model` names a sibling model by mistake +leaves its own model's size in exactly that state. -### Other fields worth knowing +### Other keys -- `default_print_profile` is a **scalar**, matched by exact preset name. Not a `;` list. The named +- `default_print_profile` is a **scalar**, matched by exact preset name; not a `;` list. The named process must be compatible with this printer through its resolved list or condition. `validate_slice` attempts to select it and rejects generic Default fallbacks, but compatibility - updates can choose another compatible preset. Check the exact default reference yourself. -- `default_filament_profile` is an **array**, one name per element. -- `printable_area` is an array of `"XxY"` strings — four points for a rectangle, one per segment for a - delta or circular bed. -- `gcode_flavor` is usually set once in the base; `klipper`, `marlin`, `marlin2` and `reprapfirmware` - cover nearly every shipped printer. -- `printer_settings_id` is junk — most files carrying it disagree with their own name. Do not copy it - when cloning a bundle. -- `min_layer_height` / `max_layer_height` are **machine** keys (per extruder), never process keys. + updates can choose another compatible preset, and `check` does not resolve the name. Check the exact + default reference yourself. +- `default_filament_profile` is an **array**, one name per element. Entry 0 is the filament preselected + when the printer is chosen; entry *i* is the preferred replacement when filament *i* is incompatible, + and any listed name outranks an unlisted one. The validator checks every entry. The list of a + printer's filaments is the model's `default_materials`: a new filament goes into `default_materials`; + put it first in `default_filament_profile` only if it should become the preselected one. +- `printable_area` is an array of `"XxY"` strings: four points for a rectangle; a delta or other circular bed + is a polygon with one point per segment. +- A non-BBL printer shows the plate selector only with `support_multi_bed_types` `"1"`; otherwise it + uses its `default_bed_type` (a plate name such as `"Textured PEI Plate"`; High Temp Plate when unset). + Filaments still set every plate + ([twelve keys](filament-profiles.md#bed-temperature-is-twelve-keys-not-one)). +- `gcode_flavor` is usually set once in the base; the common values are `klipper`, `marlin`, `marlin2` + and `reprapfirmware`. +- `printer_settings_id` does nothing in a preset file: the app replaces it with the selected preset's + name before slicing. Omit it, and do not copy it when cloning a bundle. +- `min_layer_height` / `max_layer_height` are **machine** keys (one per extruder), never process keys. ## Bases -Nearly every machine-bearing vendor registers a base literally named `fdm_machine_common`, and Klipper -vendors add `fdm_klipper_common` on top of it. Two levels is the usual depth. +The conventional machine root is a base named `fdm_machine_common`, with `fdm_klipper_common` on top +of it for Klipper printers. Which values a hardware +family or model base holds, and when adding one pays off, is in [shared-bases.md](shared-bases.md). **There is no leading-underscore convention for bases.** ## Adding a printer to an existing bundle -1. Choose the names first — model, variant(s), process(es); everything else references them. +1. Choose the names first: model, variant(s), process(es); everything else references them + ([naming.md](naming.md)). 2. Add the model (`machine_model_list`) and one `machine` variant per nozzle; the minimum key sets are above. Bed assets and `_cover.png` go directly in `/`. 3. Add at least one process per variant naming it in `compatible_printers` ([process-profiles.md](process-profiles.md#adding-a-quality-tier-or-a-nozzles-processes)). -4. Register everything (or run `update-index`), bump the version, run the id tool, validate. +4. Add the variants to the filaments they should offer, and set the model's `default_materials` so every + variant has a compatible entry. +5. Register everything (`update-index`), bump the version, run the id tool, validate: the + [authoring workflow](../SKILL.md#creating-or-modifying-a-profile). ## Adding a nozzle variant 1. Extend the model's `nozzle_diameter` (`"0.4"` → `"0.4;0.6"`). -2. Add the variant preset. Either inherit the shared base (the usual choice) or the 0.4 sibling (Elegoo, - BBL, Prusa and Qidi do this — smaller diff, but the sibling's edits now reach this file too). -3. Override what actually changes with nozzle: `nozzle_diameter`, `printer_variant`, - `default_print_profile`, `default_filament_profile`, `min_layer_height`/`max_layer_height`, and +2. Add the variant preset. Either inherit the shared base (the usual choice), or the 0.4 sibling + (a smaller diff, but the sibling's edits now reach this file too). Follow the bundle. +3. Override what actually changes with the nozzle: `nozzle_diameter`, `printer_variant`, + `default_print_profile`, `default_filament_profile`, `min_layer_height` / `max_layer_height`, and retraction if the vendor tunes it. -4. Add at least one process for the new nozzle — see [process-profiles.md](process-profiles.md). +4. Add at least one process for the new nozzle (see [process-profiles.md](process-profiles.md)), and + extend the filaments' `compatible_printers` so at least one `default_materials` entry covers the new + variant. 5. Register both, bump the version, run the id tool, validate. -## Multi-extruder, IDEX and tool-changers +## Multi-extruder, IDEX and tool changers -Per-extruder vectors are **silently resized** to the nozzle count, with no error. Padding repeats the -**first** value, not the last — `["0.4","0.6"]` on a 4-nozzle machine becomes `0.4, 0.6, 0.4, 0.4`. -Longer vectors are truncated. +Per-extruder vectors hold one value per extruder (`len(nozzle_diameter)`), and a wrong length raises no +error: a short vector acts as padded with its **first** value, not the last (`extruder_offset` +`["0x0","50x0"]` on a 4-extruder machine reads as `0x0, 50x0, 0x0, 0x0`), and entries beyond the +extruder count are never read. -Note the two sizing families: the plain per-extruder keys (`extruder_offset`, `extruder_colour`, -`extruder_printable_height`, `min_layer_height`, `max_layer_height`, `nozzle_diameter`) are sized to the -extruder count, while `printer_options_with_variant_1` (`retraction_length`, `z_hop`, `wipe`, -`nozzle_type`, the rest of the retraction family) is sized to `printer_extruder_variant` instead. +Note the two sizing families. The plain per-extruder keys (`printer_extruder_options`: `extruder_type`, +`nozzle_diameter`, `default_nozzle_volume_type`, `extruder_printable_height`, `min_layer_height`, +`max_layer_height`, …, given in full with the [key sets](extruder-variants.md#the-four-key-sets), plus +`extruder_offset` and `extruder_colour`) hold one value per extruder. The variant sets +(`retraction_length`, `z_hop`, `wipe`, `nozzle_type`, the `machine_max_*` limits at stride 2; +[the full lists](extruder-variants.md#the-four-key-sets)) are sized to the variant length: +`len(printer_extruder_variant)`, or one variant per extruder when the resolved preset writes no layout, +since `extruder_variant_list` defaults to one `Direct Drive Standard` per extruder +([widths](extruder-variants.md#widths)). `extruders_count` is a printer-tab field, not a preset key; the +loader drops it. - Give **one entry per extruder** for ordinary per-extruder vectors such as `extruder_offset`, - `extruder_colour`, `min_layer_height` and `max_layer_height`; size the variant-dependent family - to `printer_extruder_variant` instead. - A single `["0x0"]` `extruder_offset` on a dual or multi-tool machine — which already ships — pads every - toolhead to the same offset, so the offset never applies. -- Overriding `nozzle_diameter` to a different count without re-stating every per-extruder vector is the - other half of the trap — `Snapmaker U1 (0.4+0.6 nozzle)` inherits 5-entry vectors against 4 nozzles. + `extruder_colour`, `min_layer_height` and `max_layer_height`. Size the variant sets to the variant + length × stride, one value per variant (per extruder when there is no layout; a (normal, silent) + pair for the `machine_max_*` limits), even where the values are the same; the loader keeps only the first value of a list-less printer's variant arrays, so declare + the layout when the extruders differ. A single `["0x0"]` `extruder_offset` on a dual or + multi-extruder machine pads every extruder to the same offset, so the offset never applies. +- Overriding `nozzle_diameter` to a different count without restating every per-extruder vector is the + other half of the trap: a 4-extruder preset on a 5-extruder base inherits 5-entry vectors against 4 + extruders. + The reverse is silent too: a base is stored resized to its own `printer_extruder_variant` (one variant + when it writes none, whatever its extruder count), so a wider variant array on it reaches the children + as its first value padded ([composition](extruder-variants.md#padding-truncation-and-composition)). + `check` does not judge the base on its own; where its extruders need different values, declare the + layout on the base. -Copy targets: `Custom/machine/fdm_toolchanger_common.json` + `Custom/machine/MyToolChanger 0.4 -nozzle.json` (a clean minimal variant on a base that gives every vector five entries), and -`Ratrig/machine/RatRig V-Core 4 IDEX 300 0.4 nozzle.json` for IDEX. The BBL extruder-variant machinery -(`extruder_variant_list`, `printer_extruder_id`, `default_nozzle_volume_type`) is used by a handful of -vendors — do not copy it into a new bundle (`nozzle_volume_type` itself is not a machine-preset key). +Structure to copy: `Custom/machine/fdm_toolchanger_common.json` + `Custom/machine/MyToolChanger 0.4 +nozzle.json` (a minimal variant on a base that gives the per-extruder vectors five entries), and +`Ratrig/machine/RatRig V-Core 4 IDEX 300 0.4 nozzle.json` for IDEX. Take the structure from them and +the widths from the [sizing equation](extruder-variants.md#sizing-equation). Both are list-less, so +every variant array holds one value per extruder and the loader keeps only the first: fine while every +extruder shares the same retraction and limits. Add the extruder-variant layout +(`extruder_variant_list`, `printer_extruder_variant` / `printer_extruder_id`, +`default_nozzle_volume_type`) when the hardware has swappable nozzle volume types or mixed extruder +types, or as soon as one extruder needs its own value in a variant key; how to author it, and the +matching process and filament variants, is in [extruder-variants.md](extruder-variants.md). +(`nozzle_volume_type` itself is not a machine-preset key.) ## Custom G-code The keys are `machine_start_gcode`, `machine_end_gcode`, `change_filament_gcode`, -`machine_pause_gcode`, `before_layer_change_gcode` and `layer_change_gcode`. Both a single string with -embedded `\n` and a JSON array of lines are legal and both are in use — do not convert one into the -other. Conditionals are `{if …}` / `{elsif …}` / `{else}` / `{endif}`; `{elsif}` is rare but real (Qidi's -`layer_change_gcode` uses it). +`machine_pause_gcode`, `before_layer_change_gcode` and `layer_change_gcode`. Each is one string with +embedded `\n`. Never split G-code into a JSON array of lines: the loader joins array elements with `,` +into a single line (a one-element array is equivalent to the string): a two-element +`machine_start_gcode` becomes one line, `PRINT_START …,SET_PRESSURE_ADVANCE ADVANCE=0.046`. +Conditionals are `{if …}` / `{elsif …}` / `{else}` / `{endif}`. -Placeholder errors only surface when the config is actually expanded, which means `validate_slice`: +Placeholder errors only surface when the G-code is actually expanded, which means `validate_slice`: ```bash ./scripts/check_profile.sh --vendor "" validate_slice # Windows: scripts\check_profile.bat -Vendor "" validate_slice ``` -What the sweep covers is in [validation.md](validation.md#validate_slice); no `CP TOOLCHANGE START` in -the output means `change_filament_gcode` never expanded. +What the sweep covers is in [validation.md](validation.md#validate_slice); a printer whose output has no +`CP TOOLCHANGE START` fails it, because its `change_filament_gcode` never expanded. diff --git a/.claude/skills/orca-profiles/references/naming.md b/.claude/skills/orca-profiles/references/naming.md index cad82ebb6b..e2fd5a4d88 100644 --- a/.claude/skills/orca-profiles/references/naming.md +++ b/.claude/skills/orca-profiles/references/naming.md @@ -1,90 +1,94 @@ # Preset naming -A preset's `name` is the loader's key, not decoration. The index registers it; `inherits`, -`compatible_printers` and the `default_*` keys reference it by the exact string; `renamed_from` depends -on it; and `setting_id` / `filament_id` hash it (see [ids.md](ids.md)). Two presets of one type in a -bundle may not share a name (`check_preset_name_uniqueness`). Treat a name change as an identity change, -not a relabel. - -Naming is convention only where the loader does not parse it. What the loader actually acts on: - -| Type | Shape | Acted on | -| --- | --- | --- | -| `machine_model` | `` | the exact string, named by a variant's `printer_model` | -| `machine` | ` nozzle` | the exact string, named by `compatible_printers`; `printer_variant` must equal a nozzle diameter | -| `process` | `mm @` | the exact string when referenced or selected; the `@` half is a label | -| `filament` | ` @` | text before the first `@` is the **alias**, used for shadowing; the rest is a label | +A preset's `name` is its identity, not decoration. The index registers it; `inherits`, +`compatible_printers`, `printer_model` and the `default_*` keys reference it by the exact, +case-sensitive string; `renamed_from` migrates it; `setting_id` and `filament_id` hash it +([ids.md](ids.md)). Treat a name change as an identity change that needs +[migration](vendor-bundle.md#renamed_from), not a relabel. Which part of each name the loader acts on +is the table in [SKILL.md](../SKILL.md#names); this page holds the conventions. ## `machine_model` -`` — the vendor-prefixed model name (`Bambu Lab X1 Carbon`, `Creality K1`, `Prusa CORE One`). A -variant names it verbatim in `printer_model`; a mismatch makes the variant invalid. `check_name_consistency` -forces the index entry to equal the file's `name`, and the variant's `printer_model` targets this string -([A `machine_model` is not a config preset](machine-profiles.md#a-machine_model-is-not-a-config-preset)). -It is also the `_cover.png` and bed-asset stem. +``, vendor-prefixed: `Bambu Lab X1 Carbon`, `Creality K1`, `Prusa CORE One`. Each variant's +`printer_model` names it verbatim (a mismatch discards the bundle), and its index entry equals the +file's `name`. It is also the stem of `_cover.png` and, by convention, of the bed assets +(`_buildplate_model.stl`). ## `machine` (variant) -` nozzle` is near-universal (`Bambu Lab X1 Carbon 0.4 nozzle`). `printer_variant` holds -the bare nozzle (`0.4`) and must be an exact member of the model's `nozzle_diameter` list — the hard -rules are in [The `machine` variant](machine-profiles.md#the-machine-variant). Casing varies -(`nozzle` / `Nozzle`): match the bundle, not this page. A variant that is not nozzle-specific (a special -toolhead, a multi-material build) may drop the suffix — still an exact reference. Bases are named -`fdm_machine_common` / `fdm__common`. +` nozzle` is near-universal (`Bambu Lab X1 Carbon 0.4 nozzle`). Casing varies +(`nozzle` / `Nozzle`): match the bundle, not this page. A variant that is not nozzle-specific (a +special toolhead, a multi-material build, IDEX copy and mirror modes such as +` COPY MODE (0.4 nozzle)`) may drop or reshape the suffix; it is still an exact reference. `printer_variant` +holds the nozzle token: `0.4`, a suffixed `0.4HF`, or `0.4+0.6` for mixed nozzles +([rules](machine-profiles.md#printer_model-and-printer_variant)). ## `process` -`mm @` — [process-profiles.md](process-profiles.md#naming) has the -quality ladder and the `fdm_process_*` base names. The quality label stays before `@` and the printer -target after it: a printer model in the quality slot leaves the tier undescribed. The `@` is a -human label, not a reference: it usually does not equal a real variant, and compatibility comes from the -resolved `compatible_printers` list or condition. +`mm @`. The quality word stays before `@` and the printer target after +it: a printer model in the quality position leaves the tier undescribed. The `@` is a label, +and need not equal any variant name; compatibility comes from `compatible_printers` or the +condition. The quality ladder and per-nozzle labels are in +[process-profiles.md](process-profiles.md#naming). ## `filament` -` @`. The product half is what `filament_id` hashes and what survives as the **alias** up -to the first `@`; the target half is a label except for reserved forms: +` @`. The product half, up to the first `@` and right-trimmed, is the **alias**: +shadowing matches on it and `filament_id` hashes it. The target half is a label, except for the +reserved forms: -- `@base` — a non-instantiated product root. `@base` is convention; a base is really identified by - `instantiation: "false"` and no `setting_id` ([the three-part shape](filament-profiles.md#the-three-part-shape)). -- `@System` — the OrcaFilamentLibrary selectable shim, and the convention for an all-printer product - (` @System`, empty `compatible_printers`); not enforced, so a deviation is worth a review - comment. The literal `Generic @System` is load-bearing for 3MF/project recovery, beyond the - alias rule ([alias shadowing](filament-profiles.md#alias-shadowing)). -- `@`, `@ `, `@ nozzle` — printer tunes, BBL's shape. - Other vendors differ (a bare model, a printer serial, Creality's `@-all`). Specificity is judged - from `compatible_printers`, not the name +- `@base`: a non-instantiated product root. Convention only; a base is really + `instantiation: "false"` without `setting_id` + ([the three-part shape](filament-profiles.md#the-three-part-shape)). +- `@System`: the OrcaFilamentLibrary selectable shim, and the convention for an all-printer product + (` @System`, empty `compatible_printers`). Not enforced, so a deviation is worth a review + comment. The literal `Generic @System` is also load-bearing for project recovery + ([alias shadowing](filament-profiles.md#alias-shadowing)). +- Printer tunes. BBL's shape is the reference: `@` (vendor-wide), `@ ` (one + model), `@ nozzle` (one variant). Other vendors differ: a bare model + (`QIDI ABS-GF@Q2-Series`), a printer serial, Creality's `@-all`. Judge specificity from + `compatible_printers`, never from the name ([one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)). -- Color is not part of the product name: ` ` presets are not authored; the color is - chosen at runtime - ([color is a runtime property](filament-profiles.md#color-is-a-runtime-property)). +- No colour in the product name: ` ` presets are not authored; colour is chosen at + runtime ([colour](filament-profiles.md#colour-is-a-runtime-property)). -## Checking names - -Check every newly added profile's `name` against its type and role: model, selectable preset or base. -Apply the same checks to an intentional name change. The human-readable naming shapes are not enforced -by `check`: inspect the added or renamed profiles in the diff, using neighbouring names as context and -following the bundle's established style where the type-specific conventions allow variation. - -For bases (`instantiation: "false"`), use the type-specific conventions: +## Bases | Type | Base names | | --- | --- | -| `machine` | `fdm_machine_common`, `fdm__common`, or an established machine-family base name | -| `process` | `fdm_process_*`, including shared roots and per-layer-height / per-nozzle bases such as `fdm_process_single_0.20` | -| `filament` | `fdm_filament_*` material roots or ` @base` product roots | +| `machine` | `fdm_machine_common`, `fdm__common`, `fdm_klipper_common`, or an established machine-family base | +| `process` | `fdm_process_*`: shared roots and per-layer-height or per-nozzle bases such as `fdm_process_single_0.20` or `fdm_process___nozzle_` | +| `filament` | `fdm_filament_*` material roots, ` @base` product roots | -Shared base names across bundles are intentional, including product roots such as `Fiberon PA6-CF @base`. -Investigate a newly authored base that retains an unrelated selectable preset's name from a copy. +There is no leading-underscore convention. Base names repeat across bundles by design: +every bundle may have its own `fdm_process_common`, and a product root such as `Fiberon PA6-CF @base` +can exist in both the library and a vendor. Investigate a newly authored base that kept an unrelated +selectable preset's name from a copy. -**Name uniqueness is checked by CI.** `check_preset_name_uniqueness` checks type + name within each -bundle. `check_machine_model_name_uniqueness` checks model names across the entire tree, even with -`--vendor`: `Preset::get_printer_type` matches `printer_model` against all vendors' models and returns -the first match, so a duplicate makes lookup depend on vendor order. Both run as part of -`python3 scripts/orca_profile_tool.py check`. +## Uniqueness -## Not the same as the filename +- Type + name is unique within a bundle, indexed or not; `check` enforces it. +- `machine_model` names are unique across the whole tree; `check` enforces it even with `--vendor`. + Printer-type lookup matches `printer_model` against every vendor's models and takes the first, so a + duplicate makes it depend on vendor order. +- At load, two selectable presets with one name discard the bundle, and a duplicate across vendors + is a validator error. Two bases with one name, or a base and a selectable preset, load silently and + the first in the index wins; an unindexed twin is therefore one `sub_path` edit away from becoming + the parent every child resolves to. -The loader keys off `name`, and a filename that disagrees usually still loads. Index `name` must equal the -file's `name`, and the filename should match `sub_path`; a mismatch that differs only in case breaks -another platform ([cross-platform paths](validation.md#cross-platform-paths)). +## Filenames and paths + +The loader keys off `name`, and a filename that disagrees usually still loads, but keep the filename +equal to the `name` and to the index `sub_path`. Match the exact case of every `sub_path` and asset +filename: Linux filesystems distinguish case even when a macOS or Windows checkout does not, and +preset-name references are case-sensitive on every platform. Avoid Windows-invalid characters +(`< > : " | ? *`), reserved device names such as `CON` and `NUL` (with any extension), and trailing +spaces or dots in a path component; a space right before `.json` is not a trailing space. + +## Checking names + +Check the `name` of every newly added profile, and every intentional rename, against its type and role +(model, selectable preset or base). `check` does not enforce the shapes on this page: inspect the +added or renamed presets in the diff, use neighbouring names as context, and follow the bundle's +established style where the conventions allow variation. Preserve shipped names during ordinary +tuning; renaming a shipped selectable preset needs `renamed_from`. diff --git a/.claude/skills/orca-profiles/references/process-profiles.md b/.claude/skills/orca-profiles/references/process-profiles.md index 98f31bac1a..785c9ee358 100644 --- a/.claude/skills/orca-profiles/references/process-profiles.md +++ b/.claude/skills/orca-profiles/references/process-profiles.md @@ -1,14 +1,15 @@ # Process profiles -Processes live in `resources/profiles//process/` — selectable leaves and shared bases alike, and +Processes live in `resources/profiles//process/`, selectable leaves and shared bases alike, and every one of them is registered in `process_list`. There are no global processes shared across vendors. ## Naming -`"mm @"` — near-universal, so match it. +`"mm @"` is near-universal, so match it: the quality word before `@`, +the printer label after it ([naming.md](naming.md#process)). -Follow the bundle's existing quality vocabulary. BBL's common ladder relates the quality word to -the layer-height / nozzle ratio; it is a naming convention, not a loader constraint: +Follow the bundle's existing quality vocabulary. BBL's common ladder relates the quality word to the +layer height / nozzle ratio; it is a naming convention, not a loader constraint: | Quality | Ratio | 0.2 nozzle | 0.4 | 0.6 | 0.8 | | --- | --- | --- | --- | --- | --- | @@ -20,49 +21,50 @@ the layer-height / nozzle ratio; it is a naming convention, not a loader constra | Extra Draft | 0.7× | 0.14 | 0.28 | 0.42 | 0.56 | This is the `fdm_process_single__nozzle_` ladder; 0.4 is commonly the unsuffixed nozzle default. -Match neighbouring names rather than renaming shipped tiers to fit the table. +Newer BBL printers add High Quality, Balanced Quality and Strength tiers. Match neighbouring names rather +than renaming shipped tiers to fit the table. On a model with several nozzles, processes for the other +nozzles usually carry the nozzle in the label (`0.30mm Standard @BBL X1C 0.6 nozzle`); follow the bundle. -The `@target` is a human label, not a reference: most do not equal any real printer variant name. +The `@target` is a human label, not a reference: it need not equal any printer variant name. Compatibility comes from the resolved list or condition, not this label. ## Shape -A selectable leaf's only truly universal keys are `type`, `setting_id`, `name` and `instantiation`; -`inherits` and `from` are near-universal — plus compatibility. No slicing key is universal; even -`layer_height` is more often inherited than restated. A base has `type`, `name`, `instantiation`, almost -always `from`, and **no** `setting_id`. +A selectable leaf has `type`, `setting_id`, `name` and `instantiation`, normally `inherits` and +`from`, plus compatibility; its slicing keys, `layer_height` included, normally come from its bases. A +base has `type`, `name`, `instantiation`, `from`, and **no** `setting_id`. -**Target shape: a 7-key leaf.** `OrcaArena` is the cleanest model — +**Target shape: a 7-key leaf.** `OrcaArena` is the cleanest model: `fdm_process_common` → `fdm_process_arena_common` → `fdm_process_arena__nozzle_` → leaf, where the -leaf carries only `type`, `name`, `inherits`, `from`, `setting_id`, `instantiation`, +leaf carries only `type`, `name`, `inherits`, `from`, `setting_id`, `instantiation` and `compatible_printers`, and the per-nozzle base holds the layer height and all eight line widths. +[shared-bases.md](shared-bases.md#levels) says which level each process setting belongs to. -BBL, WonderMaker and Z-Bolt are uniform in *layering* — every leaf inherits a base, names its printers -directly and holds no layer height of its own — but not in key count. Imitate BBL's layering, not its -content: its leaves carry doubled `print_extruder_variant` arrays that no single-variant vendor needs. +BBL's *layering* is a model too (every leaf inherits a base, names its printers directly and holds no +layer height of its own), but not its content: its leaves carry multi-variant `print_extruder_variant` +arrays that no single-variant vendor needs ([extruder-variants.md](extruder-variants.md#process)). -Nearly every vendor ships its own `fdm_process_common` as the inherits-less root. Those files are not -identical; copying another vendor's version into a new bundle is normal. +A bundle has its own `fdm_process_common` as the inherits-less root, since a process inherits only +inside its bundle; starting a new bundle's from another vendor's copy is fine. -Beware leaf-inherits-leaf: Prusa chains several levels deep through sibling leaves, and Elegoo and -Flashforge do it too, so editing one selectable process silently changes others. Check a leaf's children -before editing it. +Beware leaf-inherits-leaf: a bundle may chain selectable processes several levels deep, so editing one +silently changes others. Check a leaf's children before editing it. ## Compatibility -Most leaves set `compatible_printers` directly; some inherit it from a base, and Prusa's fall through to +A leaf sets `compatible_printers` directly, inherits it from a base, or falls through to `compatible_printers_condition`. After resolving `inherits`, **every selectable process has one or the -other** — that is the invariant to review against. Unlike filaments, inheriting `compatible_printers` is +other**: that is the invariant to review against. Unlike filaments, inheriting `compatible_printers` is legitimate for a process, and no check enforces its presence. -- A non-empty `compatible_printers` makes `compatible_printers_condition` **dead code**. Use one or - the other. -- A condition that fails to parse means *compatible with everything* — a warning, not an error. A typo +- A non-empty `compatible_printers` makes `compatible_printers_condition` **dead**. Use one or the + other. +- A condition that fails to parse means *compatible with everything*: a warning, not an error. A typo widens compatibility instead of narrowing it. -- Matching is `boost::regex` **`regex_match`** — a full-string match, which is why every shipped - condition wraps its keyword in `.*`. Because it is boost rather than `std`, `.` also spans the newlines - inside `printer_notes`. -- A `printer_notes` keyword that prefixes another model's keyword matches both. Prusa guards it: +- A regex in a condition must match the **whole** string, so wrap the keyword in `.*`; `.` also spans + the newlines inside `printer_notes`. +- A `printer_notes` keyword that prefixes another model's keyword matches both. Guard it with a + character class after the keyword, and combine terms with `and`: ``` printer_notes=~/.*PRINTER_MODEL_COREONE[^_a-zA-Z0-9].*/ and nozzle_diameter[0]==0.4 and printer_notes=~/.*HF_NOZZLE.*/ @@ -70,76 +72,84 @@ legitimate for a process, and no check enforces its presence. The `[^_a-zA-Z0-9]` exists because `PRINTER_MODEL_COREONE_L` also contains `PRINTER_MODEL_COREONE`. -`compatible_printers` is almost always one element. A leaf listing a whole model family is where a newly -added printer is usually forgotten. +A leaf listing a whole model family is where a newly added printer is usually forgotten. -## What to review per nozzle +## Values to review per nozzle | Key group | Review | | --- | --- | | `line_width` and per-region widths | resolved widths suit the nozzle and layer height | -| `layer_height`, `initial_layer_print_height` | within the printer's limits | +| `layer_height`, `initial_layer_print_height` | within the printer's `min_layer_height` / `max_layer_height` | | print speeds | consistent with flow limits and hardware tuning | -| shell layers, wall loops, accelerations, support Z distances | preserve the intended thickness, motion and support behavior | +| shell layers, wall loops, accelerations, support Z distances | preserve the intended thickness, motion and support behaviour | -**A common starting pattern is nozzle + 0.02 mm**: 0.22 / 0.42 / 0.62 / 0.82 / 1.02. In that pattern, at 0.4, -`inner_wall_line_width`, `sparse_infill_line_width`, `skin_infill_line_width` and +**A common starting pattern is line width = nozzle + 0.02 mm**: 0.22 / 0.42 / 0.62 / 0.82 / 1.02. In +that pattern, at 0.4, `inner_wall_line_width`, `sparse_infill_line_width`, `skin_infill_line_width` and `skeleton_infill_line_width` widen to 0.45 and `initial_layer_line_width` to 0.5; at 0.2, `initial_layer_line_width` widens to 0.25. Also derived, and easily missed: -`ironing_inset = line_width / 2` (0.11 / 0.21 / 0.31 / 0.41). -These are examples, not required values; preserve intentional vendor tuning and percentage/automatic -widths, and validate their resolved values. +`ironing_inset = line_width / 2` (0.11 / 0.21 / 0.31 / 0.41). These are examples, not required values; +preserve intentional vendor tuning and percentage or automatic widths, and validate their resolved +values. -`min_layer_height` and `max_layer_height` are machine keys — no process file sets them. +`min_layer_height` and `max_layer_height` are machine keys; no process file sets them. -## Slice-time content checks +### Slicing limits -`Print::validate()` enforces four rules at slice time: +Slicing rejects a process that breaks one of these (the message in italics): -1. `initial_layer_print_height` ≤ min `nozzle_diameter` -2. `layer_height` ≤ min `nozzle_diameter` — *"Layer height cannot exceed nozzle diameter."* -3. `line_width` and the seven per-region widths (inner/outer wall, sparse infill, internal solid infill, - top surface, skin, skeleton) > `layer_height` — *"Line width too small"*. `support_line_width` only - when the object has support or a raft; `initial_layer_line_width` is never checked. -4. every width ≤ 5 × max `nozzle_diameter` — *"Line width too large"* +1. `initial_layer_print_height` ≤ the smallest `nozzle_diameter` (with a raft, the nozzle of the raft's + first-layer extruder). +2. `layer_height` ≤ the smallest `nozzle_diameter`: *"Layer height cannot exceed nozzle diameter."* +3. `line_width` and the seven per-region widths (inner and outer wall, sparse infill, internal solid + infill, top surface, skin, skeleton) > `layer_height`: *"Line width too small"*. + `support_line_width` is checked only when the object has support or a raft; + `initial_layer_line_width` is never checked. A width that resolves to 0 (automatic) is skipped. +4. Every width ≤ 5 × the largest `nozzle_diameter`: *"Line width too large"*. -Two further rules cover `bridge_line_width` (≤ nozzle diameter; > `layer_height` unless `thick_bridges` -and `thick_internal_bridges` are both on). The sweep starts from printer defaults rather than -enumerating every process. **A new non-default process gets no dedicated slice coverage in CI.** +Two further rules cover `bridge_line_width`: it must not exceed the nozzle diameter, and must exceed +`layer_height` unless `thick_bridges` and `thick_internal_bridges` are both on. The slice sweep starts +from printer defaults rather than enumerating every process: **a new non-default process gets no +dedicated slice coverage in CI.** ## What CI checks on a process Structure, not content: `process_list` name consistency **and** index coverage the other way, two files claiming one process name, the `extruder_clearance_radius` / `extruder_clearance_max_radius` conflict -pair, duplicate JSON keys, a file `normalize` would rewrite, and the five `setting_id` rules (the fifth -rejects the misspelled key `settings_id`). `compatible_printers` presence is checked for **filaments -only**. +pair, duplicate JSON keys, a file `normalize` would rewrite, the five `setting_id` rules (present on +selectable presets, absent from bases, equal to the formula outside `BBL/`, unique across the tree, and +no misspelled key `settings_id`), and the variant arrays: every array of `print_options_with_variant` +exactly `variant length × stride` wide in each selectable process that writes it, and a written +`print_extruder_id` one entry per variant +([variant arrays](validation.md#variant-arrays)). `compatible_printers` presence is checked for +**filaments only**. -Note the C++ loader derives a missing `setting_id` on the fly, so the validator will not fail a process -without one — only `orca_profile_tool.py check` catches it. Running the validator alone gives a false -all-clear. +The loader derives a missing `setting_id` on the fly, so the validator accepts a process without one; +only `orca_profile_tool.py check` catches it. +Running the validator alone gives a false all-clear. ## Silent failures specific to processes -- **Unknown or misspelled keys are discarded with no error and no warning.** They ship all over the - process tree, both plain typos (`inital_layer_height`, `tree_support_bramch_diameter_angle`, - `sparse_infill_patter`) and keys copied from other slicers that Orca never defined. -- Keys on the tool's `OBSOLETE_KEYS` list (`adaptive_layer_height`, `overhang_totally_speed`, …) are - rejected by `check`'s normalization pass across preset types; `normalize` removes them. - The additional per-key obsolete warnings read `filament/` only. -- A dangling `compatible_printers` inside an `instantiation: "false"` base is invisible to - `check_preset_references`: a base never becomes a `Preset` at all (its config goes into `config_maps` - and the loader returns early), so it is in no collection for the check to walk. -- Orphan bases that nothing inherits are scattered through the tree — usually the leftover of a - half-finished nozzle addition. +- **Unknown or misspelled keys are discarded with no error and no warning**, both plain typos + (`inital_layer_height`, `tree_support_bramch_diameter_angle`, `sparse_infill_patter`) and keys + copied from other slicers that Orca never defined. +- Keys on the tool's obsolete list (`adaptive_layer_height`, `overhang_totally_speed`, …) are rejected + by `check`'s normalization pass across preset types; `normalize` removes them. The additional per-key + obsolete warnings read `filament/` only. +- A dangling `compatible_printers` inside an `instantiation: "false"` base is reported only through a + selectable child that inherits it unchanged; it goes unreported when every child overrides the list, + or when the base has no instantiated children. +- Nothing flags an orphan base that nothing inherits, usually the leftover of a half-finished nozzle + addition. ## Adding a quality tier or a nozzle's processes -1. Choose the layer height and quality label using the vendor's existing ladder. -2. If the vendor has per-nozzle bases, add one (`fdm_process___nozzle_`) with the layer +1. Choose the layer height and quality label using the bundle's existing ladder. +2. If the bundle has per-nozzle bases, add one (`fdm_process___nozzle_`) with the layer height, nozzle-appropriate line widths, `initial_layer_print_height` and `ironing_inset`. 3. Add the leaf: 7 keys, `compatible_printers` naming the exact printer variant(s). -4. Register both in `process_list`, parent first. Bump the version, run the id tool, validate. -5. Slice this process explicitly with its intended printer; the sweep gives non-default tiers no - dedicated coverage. If it is a printer's `default_print_profile`, verify the exact name and - resolved compatibility too — the sweep may fall back or select another compatible process. +4. Register both in `process_list`, parent first, bump the version, run the id tool and validate: the + [authoring workflow](../SKILL.md#creating-or-modifying-a-profile). +5. Slice this process explicitly with its intended printer + ([on a copy of the tree](validation.md#checking-a-copy-of-the-tree)); the sweep gives non-default + tiers no dedicated coverage. If it is a printer's `default_print_profile`, verify the exact name and + resolved compatibility too: the sweep may fall back or select another compatible process. diff --git a/.claude/skills/orca-profiles/references/review-checklist.md b/.claude/skills/orca-profiles/references/review-checklist.md index 1d49cf2524..9037e364b4 100644 --- a/.claude/skills/orca-profiles/references/review-checklist.md +++ b/.claude/skills/orca-profiles/references/review-checklist.md @@ -1,37 +1,43 @@ # Reviewing a profile change -Start with delivery, identity and backward compatibility, then check the affected preset types. -The table highlights gaps that need human review. What CI *does* run: -[validation.md](validation.md). +Run `./scripts/check_profile.sh` on the applied diff first ([validation.md](validation.md) says what CI +runs), then work through the items below: delivery, identity and backward compatibility first, then the +affected preset types. The table lists the gaps CI cannot see, so only a reviewer catches them. | Not checked by CI | Consequence | | --- | --- | -| The `version` bump | The change never reaches an upgrading user | -| A misspelled setting key | Setting silently has no effect | +| The `version` bump | The change never reaches an upgrading user; an absent `version` hides the vendor from the setup wizard | +| A misspelled setting key | The setting silently has no effect | | A filename Windows cannot check out, or one that differs from its `sub_path` only in case | Works on the author's machine, breaks the bundle on another platform | -| `bed_model` / `bed_texture` / `hotend_model` pointing at a missing asset | Bed renders as Custom, hotend falls back to the generic model | +| `bed_model` / `bed_texture` / `hotend_model` / cover pointing at a missing asset | A missing bed model renders a generic custom bed and a missing texture renders none, the hotend falls back to the generic model, the cover shows a placeholder | | A nozzle size in a model's list with no matching variant | The size is offered and resolves to nothing | | A non-default process | `validate_slice` gives non-default quality tiers no dedicated coverage | | Whether the intended default survived compatibility selection | The sweep can select a different compatible preset | -| A dangling `compatible_printers` inside an `instantiation: "false"` base | A base never becomes a `Preset`, so the reference check never sees it (a bad `inherits` in a base *is* caught) | +| A dangling `compatible_printers` inside a base whose children all override it (or that has no instantiated children) | The reference check walks resolved selectable presets, so it reports a base's list only through a child that inherits it unchanged (a bad `inherits` in a base *is* caught) | +| A base nothing inherits | Dead weight, usually the leftover of an unfinished nozzle addition | | A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name | -| A preset differentiated only by color, or an all-printer library preset without `@System` | Per-color presets split one product across ids and the selector fills with near-duplicates; CI stays green | -| Per-extruder vector length on a multi-nozzle printer | Silently padded (with the **first** value) or truncated | -| A name that ignores its type's convention — a model in a `process` quality slot, or an unrelated target label left in a copied preset | The selector misrepresents the preset's quality or intended printer | +| A preset differentiated only by colour, or an all-printer library preset without `@System` | Per-colour presets split one product across several ids and the selector fills with near-duplicates; CI stays green | +| Plate temperatures for plates the printer has | The user's plate reads an unset or inherited temperature | +| Per-extruder vector length on a multi-extruder printer | Silently padded (with the **first** value) or truncated | +| A new variant appended instead of inserted at its variant index, a variant array widened on a base with a shorter list, or a variant a filament/process lacks | Values shift onto the wrong extruder, are cut before any child inherits them, or resolve to the first variant: High Flow silently gets Standard values | +| A name that ignores its type's convention: a printer model in a `process` quality position, or an unrelated target label or base name left in a copied preset | The selector misrepresents the preset's quality or intended printer | +| Values: temperatures, speeds, widths, pressure advance | A wrong value prints wrong while CI stays green | ## 1. Was the vendor `version` bumped? For **every** bundle whose folder the diff touches, `resources/profiles/.json` must have its -`version` incremented — last component, carrying `.99` into the third component. A library change -means bumping `OrcaFilamentLibrary.json`. +`version` incremented: last component, carrying `.99` into the third component. A library change means +bumping `OrcaFilamentLibrary.json`. -*Why:* nothing in CI checks it, and `PresetUpdater` reinstalls only when `vendor_ver < resource_ver` — -without a bump the change reaches neither an upgrading user nor the author's own running app. +*Why:* nothing in CI checks it, and the app reinstalls a bundled profile set only when its version is +newer than the installed one: without a bump the change reaches neither an upgrading user nor the +author's own running app. Without any `version` the vendor vanishes from the setup wizard, and neither +`check` nor the validator reports it. ## 2. Was the index rebuilt, and does the diff contain only this change? -`check` now fails on an unregistered file, on an index `update-index` would reorder, and on a file -`normalize` would rewrite — so a PR that skipped them arrives red, and you do not have to spot the +`check` fails on an unregistered file, on an index `update-index` would reorder, and on a file +`normalize` would rewrite, so a PR that skipped them arrives red and you do not have to spot the omission yourself. Three things are still yours: - **The index diff belongs to this change.** `update-index` rewrites whole `*_list` sections. If the @@ -41,11 +47,15 @@ omission yourself. Three things are still yours: registration; `validate_custom` detects the break only for names covered by released fixtures. - **`normalize` edits content, not just layout.** It drops `version` and `is_custom_defined` from preset files, removes obsolete keys, deletes six print-speed keys from filament profiles, and resolves - `extruder_clearance_radius` against `extruder_clearance_max_radius` by keeping the larger. - Check that the keys it removed were meant to go. + `extruder_clearance_radius` against `extruder_clearance_max_radius` by keeping the larger + ([what normalize changes](validation.md#normalize-and-update-index-are-part-of-the-check)). Check that + the keys it removed were meant to go. -Obsolete keys fail `check`'s normalization pass and should be removed with `normalize`. -`check` also reports per-key obsolete warnings for filament profiles in the selected vendors. +Index order is dependency order, not alphabetical: parents and include templates before the presets +that use them, then, in name order, the entries that neither depend on nor are depended on by another +entry of their own list (such as a leaf filament whose only parent is in the library), and any entry on +a dependency cycle. Judge a hand-placed entry only by +`update-index --dry-run`: if it reports nothing to rebuild, the position is not a finding. *Why:* the index is the loader's only entry point. Out-of-order entries fail with `can not find inherits` and take the whole vendor bundle down; an unindexed file gets reviewed, merged and never loads. @@ -55,8 +65,8 @@ and take the whole vendor bundle down; an unindexed file gets reviewed, merged a No hand-typed or copied `setting_id` / `filament_id`. Instantiated presets have a `setting_id`; bases do not. `check` enforces all of that; what it cannot tell you is whether the identity *should* have moved. -A rewritten or removed `filament_id` means a product's identity moved — a rename, or an edited -`filament_vendor` / `filament_type` — and the old id is not forwarded anywhere. Confirm that was +A rewritten or removed `filament_id` means a product's identity moved (a renamed alias, or an edited +`filament_vendor` / `filament_type`), and the old id is not forwarded anywhere. Confirm that was intended, and that a new id is not a rename in disguise. *Why:* a duplicate `filament_id` on one printer makes AMS spool matching a coin toss; a copied @@ -64,136 +74,204 @@ intended, and that a new id is not a rename in disguise. ## 4. Does anything disappear for existing users? -A rename, a deletion, or a flip of `"instantiation": "true"` → `"false"` on a shipped preset removes the -name from the preset collection. It needs `renamed_from` on a successor — and only one preset may claim a -given old name. The claimed old name must **not** still be a live preset; the redirect is inert if it is. +A rename, a deletion, or a flip of `"instantiation": "true"` → `"false"` on a shipped selectable preset +removes the name from the preset collection. It needs `renamed_from` on a selectable successor +([renamed_from](vendor-bundle.md#renamed_from)), and only one preset may claim a given old name. The +claimed old name must **not** still be a live preset; the redirect is inert if it is. *Why:* user presets inheriting it die with `can not find parent for config !`; 3MF-embedded presets are dropped with no error at all. Commit `33923464ae` reverted exactly this for Cubicon; -`6943b6ddc3` redid it correctly. CI's `validate_custom` catches the shipped-name case — but not an inert -`renamed_from`. +`6943b6ddc3` redid it correctly with `renamed_from`. CI's `validate_custom` catches the shipped-name +case, but not an inert `renamed_from`. ## 5. Is `compatible_printers` right? -Exact printer **variant** names, non-empty on every instantiated filament outside OrcaFilamentLibrary -and written in the preset's own file — golden rule 6, with the flattened-vs-own-key trap in -[filament-profiles.md](filament-profiles.md#compatible_printers). Watch for a nozzle-specific variant that -inherited or copied the base's full printer list, and for two presets of one product with overlapping -lists — duplicate combobox entries and an ambiguous AMS match. +Exact printer **variant** names, non-empty on every instantiated filament outside OrcaFilamentLibrary, +and written in the preset's own file ([SKILL.md rule 9](../SKILL.md#rules); the resolved-vs-own-key trap +is in [filament-profiles.md](filament-profiles.md#compatible_printers)). A `machine_model` name instead +of a variant name is the usual mistake: `check` passes it, the validator reports +`references unknown compatible_printers`. Watch for a nozzle-specific tune that inherited or copied the +base's full printer list, and for two presets of one product with overlapping lists: duplicate combobox +entries and an ambiguous AMS match. Two presets of one product (`filament_id`) must not share a variant. Resolve it by specificity: move the -variant to the most specific preset and remove it from the more general ones — preferred over deleting a -profile. Then repoint the machine's `default_filament_profile` and the model's `default_materials` at the -profile that now covers it. See +variant to the most specific preset and remove it from the more general ones, which is preferred over +deleting a profile. Then repoint the machine's `default_filament_profile` and the model's +`default_materials` at the profile that now covers it. See [one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product). -*Why:* real shipped bugs twice (`b7b3418baf` "showing up everywhere", `ff83aa41ef` duplicate Flashforge -entries). The Python `check` passes on an overlap; only the full `check_profile.sh` (`validate_system`) -reports `Ambiguous AMS filament match`. +*Why:* real shipped bugs twice (`b7b3418baf` filaments "showing up everywhere", `ff83aa41ef` duplicate +Flashforge entries). The Python `check` passes on an overlap; only the full `check_profile.sh` +(`validate_system`) reports `Ambiguous AMS filament match`. -## 6. Model ↔ variant ↔ process consistency +## 6. One product, one all-printer preset; colour is not a preset -- New nozzle size → the model's `nozzle_diameter` list extended, a variant with a matching - `printer_variant`, and at least one process listing that variant. -- `default_print_profile` is one exact name (not a `;` list), and that process's resolved - compatibility list or condition includes this printer. +No presets that differ only by colour: `filament_id` identifies a product, and the colour comes from the +spool at runtime. An all-printer library product is a ` @System` shim with an empty +`compatible_printers` ([colour](filament-profiles.md#colour-is-a-runtime-property)). + +*Why:* per-colour presets pass every check, so this is a review call. + +## 7. Model ↔ variant ↔ process consistency + +- A new nozzle size → the model's `nozzle_diameter` list extended, a variant with a matching + `printer_variant`, and at least one process listing that variant. Every size in the model's list has a + variant (unchecked). +- `default_print_profile` is one exact name (not a `;` list), and that process's resolved compatibility + list or condition includes this printer. - `default_filament_profile` is an array of names that exist. +- Each variant of the model has at least one compatible entry in the model's `default_materials`. -*Why:* an unlisted `printer_variant` is a hard bundle-load failure. Default process selection is -weaker: the sweep attempts the named default, then updates compatibility and rejects generic Default -fallbacks. Another compatible process can conceal a bad reference, so inspect it even after a pass. +*Why:* an unlisted `printer_variant` is a hard bundle-load failure. Default process selection is weaker: +the sweep attempts the named default, then updates compatibility and rejects generic Default fallbacks, +so another compatible process can conceal a bad reference. Inspect it even after a pass. -## 7. Types and spellings +## 8. Types and spellings Every value a string or an array of strings; `filament_type` an array; `instantiation` the string -`"true"`/`"false"` — golden rule 7. Check index metadata and model `nozzle_diameter` especially; -wrong types there can abort loading for **every** vendor. +`"true"` / `"false"`; custom G-code one string, never an array of lines ([SKILL.md rule 5](../SKILL.md#rules)). +Check index metadata and model `nozzle_diameter` especially: wrong types there can abort loading for +**every** vendor. The part only a reviewer can do: check new setting keys against `src/libslic3r/PrintConfig.cpp`. A -misspelled key is silently discarded (rule 8), the single most common way a profile edit does nothing -while CI stays green. +misspelled key is silently discarded ([rule 6](../SKILL.md#rules)), the single most common way a profile +edit does nothing while CI stays green. -## 8. Blast radius of a base edit +## 9. Blast radius of a base edit -A change to `fdm_*_common.json` reaches every child at once. Ask which presets it touches — several -reverts in this repo are exactly this (`41d1b0d3c8`, `dc491166a8`). Also check whether the edited leaf has -children of its own: Prusa, Flashforge and Elegoo all chain leaf-inherits-leaf several levels deep. +A change to `fdm_*_common.json` or any other base reaches every child at once. Ask which presets it +touches: several reverts in this repo are exactly this (`41d1b0d3c8`, `dc491166a8`). Also check whether +the edited leaf has children of its own: a bundle may chain leaf-inherits-leaf several levels deep. A +newly added base that nothing inherits is dead weight, and a dangling +`compatible_printers` inside a base is reported only through a child that inherits it unchanged. A diff +that only moves values between presets and bases must leave every selectable preset loading what it +loaded: ask for the `compare` result ([shared bases](shared-bases.md#nothing-loads-differently)), and +check each new base against the [balance rules](shared-bases.md#balance). -## 9. Do the numbers make sense for the nozzle? +## 10. Do the numbers make sense for the nozzle and material? -Check resolved widths and layer heights against the nozzle, and flow limits / pressure advance -against the actual hardware and material. The patterns in [process-profiles.md](process-profiles.md) -are examples, not mandatory values; [filament-profiles.md](filament-profiles.md) explains what to -revisit for a nozzle change. A cloned preset's unchanged MVS needs particular scrutiny. +Check resolved widths and layer heights against the nozzle, temperatures against the material (PLA +values under an ASA name print wrong), and flow limits / pressure advance against the actual hardware +and material. The patterns in [process-profiles.md](process-profiles.md#values-to-review-per-nozzle) +are examples, not mandatory values; [filament-profiles.md](filament-profiles.md#tuning-per-nozzle-and-per-variant) +explains what to revisit for a nozzle change. A cloned preset's unchanged volumetric speed needs +particular scrutiny. Settings tuned for real hardware cannot be verified by reading the diff. Say so rather than approving numbers nobody measured. -## 10. Asset references (not checked anywhere) +## 11. Plate temperatures + +A filament sets the plate temperature for every plate the printer plausibly has, as its siblings do; +`textured_cool_plate_temp` is the one most often forgotten +([twelve keys](filament-profiles.md#bed-temperature-is-twelve-keys-not-one)). + +## 12. Asset references (not checked anywhere) `bed_model`, `bed_texture`, `hotend_model` and `_cover.png` exist under -`resources/profiles//`. Broken references already ship; nothing checks them. +`resources/profiles//`, in exact case. Nothing checks them. -## 11. `default_materials` (checked by CI) +## 13. `default_materials` and `default_filament_profile` (checked by CI) -`check` fails on a `default_materials` / `default_filament_profile` name that resolves to no system -filament, so a dangling entry no longer reaches review. When compatibility moves between profiles of a -product, the machine's `default_filament_profile` and the model's `default_materials` must be repointed at -the most specific profile that still covers the variant, dropping generic entries that no longer apply — -the same [specificity rule](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product) -applies when adding or fixing defaults. Scope the run while working on one vendor: +`check` fails on a `default_materials` / `default_filament_profile` name that matches no filament file, +and `validate_system` on a variant with no compatible system filament in `default_materials`, so a +dangling entry no longer reaches review. When compatibility moves between profiles of a product, the +machine's `default_filament_profile` and the model's `default_materials` must be repointed at the most +specific profile that still covers the variant, dropping generic entries that no longer apply; the same +[specificity rule](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product) applies +when adding or fixing defaults. Scope the run while working on one vendor: ```bash python3 scripts/orca_profile_tool.py check --vendor "" # py -3 on Windows ``` -## 12. Per-extruder vector lengths (not checked) +## 14. Per-extruder vectors (not checked) and variant arrays (widths and layout checked) -One entry per extruder for the plain per-extruder vectors; the `printer_options_with_variant_1` keys are -sized to `printer_extruder_variant` instead. A wrong length is silently padded — repeating the **first** -value, not the last — or truncated. The two sizing families and the worked cases are in +One entry per extruder for the plain per-extruder vectors; the variant sets are sized to the variant +length, `len(printer_extruder_variant)` or one per extruder when the resolved preset has no layout (a +per-extruder difference in those keys still needs the layout, since the loader keeps only the +first value of a list-less printer's arrays). A wrong length is silently padded, repeating the **first** +value, not the last, or truncated. The two sizing families and the worked cases are in [machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers). -## 13. Non-default processes get no slice coverage +`check` reports a variant array of any type that is not exactly the `variant length × stride` width +of the selectable preset that writes it, one value included, as an error whatever the values; +`check --strict` also reports one that reaches a selectable preset at another width. A base is not +judged on its own. On a machine it +also reports layout keys that disagree: `printer_extruder_variant` / `printer_extruder_id` not the +flattening of `extruder_variant_list`, a variant without its extruder's `extruder_type` prefix, a +`default_nozzle_volume_type` the extruder does not list, a list of several variants without the pair, a +pair without the list that puts several variants on one extruder. On a machine or a process, an id +array, written or inherited, whose length differs from its variant list is an error; the id and +layout rules judge the composed preset without `--strict`. Two layout findings are warnings: a +pair without the list that `single_extruder_multi_material` off would replace at load, and a process +without `print_extruder_id` whose variants repeat. The full rules are in +[variant arrays](validation.md#variant-arrays). `check_variant_names` separately holds every variant +string, `extruder_type`, `nozzle_volume_type` and `default_nozzle_volume_type` to the engine's enums +in every bundle, BBL included, failing on a dead variant, a legacy spelling and a variant list that +names one variant twice ([variant names](validation.md#variant-names)). + +On a multi-variant printer, still check by hand: + +- that a new variant's values sit at its variant index (machine limits as a (normal, silent) pair at + `2 × index`) in every file of the chain that restates the key, `include` templates included; +- that each process lists every (extruder id, variant) pair its printers can select; +- that no array was widened on a base with a shorter variant list: the loader cuts it before the + children inherit it ([composition](extruder-variants.md#padding-truncation-and-composition)), and + `check`, even `--strict`, composes without that cut, so it passes; +- that only keys in [the four sets](extruder-variants.md#the-four-key-sets) carry per-variant values; +- that the High Flow variants carry measured values rather than copies + ([checking and testing](extruder-variants.md#checking-and-testing)). + +## 15. Non-default processes get no slice coverage `validate_slice` starts from printer defaults; it does not enumerate every process. Slice a new or -changed non-default tier explicitly with its intended printer. - -## 14. Housekeeping worth a nit, not a block - -`"from"` other than `"system"` (the preset-bundle loader ignores it, though the CLI's config-file loader -rejects anything but `system`/`user`/`User`), `printer_settings_id` copied from another -vendor, and a filename that disagrees with the preset's `name` (common; the loader keys off `name`). - -## 15. Cross-platform filenames and paths (not checked) - -Check for Windows-invalid characters, reserved device names, trailing path-component spaces/dots, -and case mismatches in `sub_path` or asset paths. See [cross-platform paths](validation.md#cross-platform-paths). +changed non-default tier explicitly with its intended printer +([on a copy of the tree](validation.md#checking-a-copy-of-the-tree)). ## 16. Do the preset names follow the conventions? -Check **every newly added profile and intentional name change**, including models and bases, -against [the naming conventions](naming.md#checking-names). Preserve shipped names during -ordinary tuning; renaming a shipped selectable preset requires the migration in item 4. +Check **every newly added profile and intentional name change**, including models and bases, against +[the naming conventions](naming.md#checking-names): no printer model in a process quality position, no +target label or base name left over from a copied preset, the bundle's established style. Preserve +shipped names during ordinary tuning; renaming a shipped selectable preset requires the migration in +item 4. -*Why:* CI checks name uniqueness, but does not enforce the naming conventions. Catch naming -mistakes before the names ship and existing projects depend on them. +*Why:* CI checks name uniqueness, but does not enforce the naming conventions. Catch naming mistakes +before the names ship and existing projects depend on them. + +## 17. Cross-platform filenames and paths (not checked) + +Check for Windows-invalid characters, reserved device names, trailing path-component spaces or dots, +and case mismatches between `sub_path` or asset references and the files on disk. See +[filenames and paths](naming.md#filenames-and-paths). + +## 18. Housekeeping worth a nit, not a block + +`"from"` other than `"system"` (the bundle loader ignores it, though loading the file as a CLI config +rejects anything but `system` / `user` / `User`), `printer_settings_id` copied from another vendor, +redundant overrides that restate the parent's value, siblings that each repeat a value their base could +hold ([shared bases](shared-bases.md)), and a filename that disagrees with the preset's +`name` (the loader keys off `name`). --- ## Reporting the review -A finding is: **one defect**, its file, what breaks at runtime or in CI, and the fix. Split independent -defects into separate findings even when they live in one file — five id problems in one bullet get one -fix and four survivors. +A finding is **one defect**: its file (or quoted lines), what breaks at runtime or in CI, and the fix. +Split independent defects into separate findings even when they live in one file: five id problems in +one bullet get one fix and four survivors. Say which findings `check` or the validator reports and which +only a reader catches: a missing version bump, a misspelled key and wrong temperatures pass CI, so a +contributor who only reruns the tools fixes what CI flags and resubmits the rest. Severity discriminates only if it is earned: | Severity | Means | | --- | --- | | blocker | the bundle fails to load, or a preset is unreachable at runtime | -| major | CI fails, or existing users lose a preset | -| minor | wrong-but-working: dead keys, `from`, naming, redundant overrides | +| major | CI fails, existing users lose a preset, or a value prints wrong while CI stays green (PLA temperatures under an ASA name; a misspelled key whose intended value differs from the inherited one) | +| minor | wrong but working: redundant or dead keys that change nothing, `from`, naming | -Compute every number and id (`orca_profile_tool.py`, a scripted count) or omit it — one invented count -makes a reader stop trusting the right ones. Report a command's result only if you ran it. +Compute every number and id (`orca_profile_tool.py`, a scripted count) or omit it: one invented count +makes a reader stop trusting the right ones. Report a command's result only if you ran it. End with a +verdict: can it merge as it stands? diff --git a/.claude/skills/orca-profiles/references/shared-bases.md b/.claude/skills/orca-profiles/references/shared-bases.md new file mode 100644 index 0000000000..500cd34006 --- /dev/null +++ b/.claude/skills/orca-profiles/references/shared-bases.md @@ -0,0 +1,254 @@ +# Shared bases + +Use this when drafting several presets at once, after `fix-variant`, or when sibling presets repeat the +same values. Each value is written once, on the base of the group it is true for, and a selectable +preset holds its identity and what makes it different. The work has two halves: choosing the groups, +which is judgment, and moving the values, which must leave every preset loading exactly what it loaded +before. + +## When + +- **Drafting** a printer family, a quality ladder or a product line: place each setting at its + [level](#levels) before writing any preset, then write the presets as overrides + ([Rule 7](../SKILL.md#rules)). +- **After `fix-variant`.** It resizes each array in the selectable preset that writes it and never + moves or deletes a value ([variant arrays](validation.md#variant-arrays)). A family whose presets all + wrote one value now repeats the widened array in every preset, and a preset that restated what it + inherits now restates it wider. +- **Converting full copies**: a machine that inherits nothing, or a filament that restates fifty-plus + keys, is the style to move away from ([filament style](filament-profiles.md#style)). + +Out of scope: + +- **BBL.** Its profiles are synced from BambuStudio: a restructure is overwritten by the next sync and + makes every later sync diff unreadable. Fix BBL values in the file that holds them. +- **OrcaFilamentLibrary bases for one vendor's values.** A library base reaches every bundle's + filaments. Put a vendor's shared values on a base in its own bundle (a product `@base`, or a vendor + base that inherits the library's), and change a library base only for a value true of every filament + below it in every bundle. +- **Families the change does not touch.** Restructure the presets you are already changing; a + bundle-wide pass is a change of its own. Commit a restructure without any value change, so `compare` + alone verifies it. + +## Nothing loads differently + +A restructure changes where values are written, never what a selectable preset loads. Presets keep +their `name` and `setting_id`, and user presets and projects refer to a system preset by name and store +their own changes against what it loads, so an unchanged load changes nothing for users. Prove it with +the bundled helper, from the repository root (every subcommand takes `--profiles ` to work on a +copy of the tree): + +```bash +python3 .claude/skills/orca-profiles/scripts/shared_settings.py snapshot # before any edit +python3 .claude/skills/orca-profiles/scripts/shared_settings.py compare # after: "0 difference(s)", exit 0 +``` + +`snapshot` records every selectable preset of every bundle as the loader stores it: the parent's stored +config, each `include` at the width its file wrote, the preset's own keys, then every variant array +resized to the preset's own variant list ([composition](vendor-bundle.md#inherits-and-include)). It +models the step `check` does not see: a base is stored after its own resize, so an array wider than a +base's list reaches its children cut. It also leaves out what the loader reads from each file and never +hands down (`name`, `type`, `from`, `instantiation`, `inherits`, `include`, `setting_id`, +`renamed_from`, `description`, `version`, `url`, `is_custom_defined`), so those keys never move to a +base. Nor does it record `print_settings_id`, `printer_settings_id` or `filament_settings_id`: the app +replaces them with the selected presets' names before slicing, so a file's value never counts. Delete +them from the presets you restructure rather than moving them to a base. + +It does not know the built-in defaults, so `compare` lists a key written on one side only (no file of +the preset's chain writes it on the other) apart from the differences, and exits 1 for either. A +difference is a value the edit changed: undo it, or make it a separate, deliberate change. A one-sided +key is no change only when its written value is the option's default in `PrintConfig.cpp`: confirm +each, as when a preset that loaded the default by leaving a key out must now write it +([Balance 5](#balance)). A value equal to the default needs writing nowhere when no base above writes +another: delete it from the presets rather than moving it, and confirm the one-sided keys. + +## Levels + +A base stands for a level of the vendor's catalogue, and a setting lives at the level that determines +it. The test for a shared value: if it had to change for one preset of the group, should it change for +all of them? If yes, it is the group's and goes on the group's base. A value that is only equal today +(two unrelated printers with the same acceleration) stays in each preset: a base built on coincidence +is later edited for one preset and silently changes the others. For a default with exceptions +([Balance 5](#balance)), ask the question of the presets that inherit the default. + +The tables give each setting's usual level; the test decides for a given bundle: where each toolhead +(Bowden or Direct Drive) has its own default filament, `default_filament_profile` follows the +toolhead, not the nozzle. Levels run +coarse to fine, and a chain need not visit every level: each preset or base inherits the next coarser +level that has a base. + +| Machine level | Base | Settings it determines | +| --- | --- | --- | +| Vendor | `fdm_machine_common`, `fdm__common` | the vendor's defaults for every printer | +| Firmware | `fdm_klipper_common`, `fdm_marlin_common` | `gcode_flavor`, G-code that calls the firmware's macros (layer change, pause, filament change), `host_type`, `print_host`, the thumbnail format | +| Hardware family: models that share a frame, motion system, toolhead or extruder layout | `fdm___common` (`fdm_qidi_x3_common`, `fdm_machine_eryone_ER20_common`) | the `machine_max_*` limits, `extruder_clearance_*`, the toolhead's retraction where every nozzle shares it, `z_hop` and wipe, fitted hardware such as `auxiliary_fan`, the extruder count and per-extruder vectors, the variant layout ([Printer rule 2](extruder-variants.md#printer-machine)) | +| Model: one `machine_model`, which for an IDEX printer includes its mode | the default-nozzle preset where the bundle hangs its other nozzle presets off it; otherwise `fdm___common` | `printable_area`, `printable_height`, `bed_exclude_area`, the model's start G-code, a COPY or MIRROR mode's settings | +| Nozzle: the selectable preset | none | `printer_model` and `printer_variant`, which name the preset's model and nozzle and stay in every preset as the tree writes them; `nozzle_diameter`, `min_layer_height`, `max_layer_height`, `default_print_profile`, `default_filament_profile`, retraction the vendor tunes per nozzle | + +A family may split once more, into toolhead or revision groups that exist only within it: +`fdm___common` → `fdm___mk1_common`. A split that crosses another axis is not a +level: when every controller comes with every toolhead, a toolhead base under each controller base +repeats the same values in each ([Balance 6](#balance)). + +| Process level | Base | Settings it determines | +| --- | --- | --- | +| Vendor | `fdm_process_common`, `fdm_process__common` | strategy: seam, wall order, infill and support patterns | +| Printer family or variant layout | `fdm_process___common` (`fdm_process_arena_common`, BBL's `fdm_process_dual_common`) | speeds, accelerations and jerk of that motion system; the variant layout ([Process rule 4](extruder-variants.md#process)) | +| Layer height × nozzle | `fdm_process___nozzle_` (BBL's `fdm_process_single_0.20`) | `layer_height`, line widths, shell layers, speeds scaled to the layer | +| Quality × printer: the selectable preset | none | `compatible_printers`, and what is unique to that combination | + +| Filament level | Base | Settings it determines | +| --- | --- | --- | +| Material | `fdm_filament_` in OrcaFilamentLibrary, shared by every bundle | material defaults | +| Product | ` @base` | `filament_id` (minted here, [ids](ids.md)), `filament_vendor`, `filament_type`, density, cost, the product's temperatures and cooling | +| Printer or nozzle tune: the selectable preset | none | `compatible_printers` (always in its own file, [Rule 9](../SKILL.md#rules)), volumetric speed, flow ratio, pressure advance and retraction measured on that printer | + +**Equal where they must differ is a copy.** A key that follows a finer level, such as the layer-height +limits and line widths that follow the nozzle diameter or `printable_area` that follows the bed, never +moves above that level. When presets that differ in it carry the same value, the value was copied: leave +it in the presets and report it (the [worked example](#worked-example-an-idex-family) has one). + +## Balance + +1. **One group, one base; use the existing one first.** A base is the home of the presets below it, + whatever its name. In a bundle whose presets are all one family, the vendor base is the family base, + so the family's values go there. In a bundle that hangs the other nozzles off the default-nozzle preset, that preset is the model's home. + Never create a base whose presets are exactly its parent's. Where two existing bases already serve + the same presets (a copied `fdm_machine_common` above the vendor's own base), the finer one is the + home, and merging the pair is a change of its own. Moving a key into an existing home adds no file. + A base value that no preset below it loads is dead: replace it with the group's value when there is + one; otherwise leave it and report it, since a future preset would inherit it. +2. **A new base must stand for a level and pay for itself.** Its file costs five metadata keys and an + index entry, so create it only when it takes `k` keys off `n` selectable presets with + `(n − 1) × k > 6`, where a custom G-code value counts as one key per G-code line: what it removes + must outnumber what it adds. A model with two nozzles that share three short keys keeps them in both. +3. **No ad-hoc bases:** never a base for presets that merely agree (the ones with 0.8 mm retraction), + and never a base with one preset below it. +4. **Keep every selectable preset within four ancestors**, selectable parents included. When a level would push a preset past that, fold it into the level above or leave + its keys in the presets. +5. **A default with exceptions.** A value that only some presets below a shared base load moves to that + base, whichever axis it follows, when two conditions hold. More presets load it than any other value + (on a tie the key stays in the presets), and `w − a > 1`, where `w` presets drop their copy and `a` + presets that take the key from the base and load another value, the built-in default included, must + now write theirs. Presets that write another value keep it and are unaffected. The base then holds + the group's default, and the exceptions stay visible in their own files. +6. **One chain; the other axes stay in the presets.** `inherits` follows one axis. When presets vary + along several (bed size × controller × toolhead), first fill the existing homes by + rules 1 and 5. Then give new bases to the axis whose bases pay most: sum rule 2's count over its + bases, less the keys a re-parented preset must now write because it no longer inherits them from its + old parent; on a tie, follow the layering the bundle already has. Leave the other axes' keys in the + presets, and never repeat one axis's bases under each group of another. Outside BBL, whose synced + presets need theirs, add no `include` template for a second axis: the loader reads `include` only + since #15869 (2026-09-25), and an app that predates it ignores the key, so the template's settings + never reach the preset. + +Name a new base after its level ([base names](naming.md#bases)). The name must be unique in its bundle +and must not equal a selectable preset's: two such presets load silently and the first in the index wins +([uniqueness](naming.md#uniqueness)). + +## Restated values + +`candidates` lists every key a file writes that it would load unchanged without writing it. Delete it +when the value is what the presets below the base that supplies it share: more of them load it, +written or inherited, than any other value. A family that restates machine limits, clearances and +G-code every other printer of the bundle loads from `fdm_klipper_common` drops its copies. When most +presets below that base load another value, the match is a coincidence: keep the key, and move it to +the level of the presets that share it. Keys of the nozzle level stay in the preset in +either case. + +After a restructure the report still lists keys that are right where they are: the nozzle-level keys +each preset keeps, and arrays a list-less multi-extruder base writes at its presets' width for +`check --strict`. + +## Variant arrays on a base + +- The loader stores a base with its variant arrays resized to the base's own variant list, one variant + when it writes none, so an array on a narrower base reaches its presets as its first value, padded. + Move a variant array to a base only when the base declares the presets' variant list (and, for a + machine or process, their ids), moving the layout keys with it as + [Printer rule 2](extruder-variants.md#printer-machine) and [Process rule 4](extruder-variants.md#process) + ask; or when the presets load its first value anyway: every value is equal, or the presets are + list-less machines, which the loader cuts to one variant. `compare` catches a cut. +- Write the array on the base at the width of the presets it serves, their `N` + ([sizing equation](extruder-variants.md#sizing-equation)), so `check --strict` judges the right width + where it reaches them. When the presets below a base need different widths (single- and + dual-extruder models on one base), leave the array in the presets, or on bases that each serve one + width. +- On a list-less multi-extruder family base, declare the extruder count: `nozzle_diameter` and the other + per-extruder vectors at one entry per extruder. The base then has its presets' width, and + `fix-variant --strict` writes an array that reaches the presets at another width into the base once, + instead of into every preset. +- Deleting a restated variant array leaves the preset on the inherited array. Plain `check` accepts + that; `check --strict` reports it when the inherited width differs, which is why a bundle held to + `--strict` restates the array at each preset's width. + +## Procedure + +1. **Snapshot** the tree before any edit, `fix-variant` included. After `fix-variant`, run `compare`: a + difference is a value its padding changed (it repeats the last value, the loader the first). Set that + value deliberately, then snapshot again as the baseline for the restructure. +2. **List the candidates:** + + ```bash + python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "" --type machine + python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "" --type machine --group-by printer_model + ``` + + It prints the [restated values](#restated-values), then, per base, the keys every selectable preset + below it loads with one value and how many of those presets write it themselves, and under + `default with exceptions` the values that pass [Balance 5](#balance), with `w` and `a`. With + `--group-by ` it groups the selectable presets by that key's value instead and names each + group's nearest common base, where a new base would go: `printer_model` for models, `gcode_flavor` + for firmware, `extruder_type` or `default_filament_profile` for toolheads, `filament_id` for + filament products, `layer_height` for processes. The report is evidence, not a plan: it cannot tell + a shared value from a coincidence or a copy. +3. **Decide each key** by [Levels](#levels), [Balance](#balance) and + [Restated values](#restated-values): delete the restatements of shared values, move group values up + to the group's home, and create only the bases that pay. +4. **Edit.** Each new base gets `"type"`, `"name"`, `"from": "system"`, `"instantiation": "false"`, no + `setting_id`, and `inherits` set to the presets' old parent. Point the presets' `inherits` at it and + delete the moved keys from them. Bump the version, then run `normalize`, `update-index` (it orders + parents first) and `generate-id --dry-run`, which must write nothing: bases take no id and presets + keep theirs. +5. **In a bundle held to `--strict`, run `fix-variant --strict` now**, so it writes into the new bases. +6. **Verify.** `compare` prints `0 difference(s)`, and every one-sided key it lists is a default. + `check` reports no error it did not report before, and neither does `check --strict` where the bundle + passes it. Then run the [authoring checks](../SKILL.md#creating-or-modifying-a-profile). +7. **Report** each base added (name, level, presets below it, keys it holds), the keys left in presets + and why, the copies found, and the `compare` result. + +## Worked example: an IDEX family + +A Klipper bundle's IDEX family has 36 selectable machines (bed size 300, 400 or 500 × normal, COPY or +MIRROR mode × 0.4, 0.5, 0.6 or 0.8 nozzle) that all inherit `fdm_klipper_common` directly and write 49 +keys each. `fix-variant` has widened their retraction arrays to two values and their machine limits +to four. + +- **Restated.** All 36 restate 10 values that every other printer of the bundle loads from + `fdm_klipper_common`: three machine limits, the three clearances, wipe, `retract_before_wipe`, and the + layer-change and pause G-code. Delete them. +- **Family.** All 36 load one value for 24 more keys: the other machine limits, `extruder_offset`, + retraction, `z_hop`, `single_extruder_multi_material`, `manual_filament_change`, the remaining G-code + except the start G-code, and thumbnails. A new family base, `fdm___idex_common`, + holds them, plus `nozzle_diameter` `["0.4", "0.4"]` for the extruder count: at least + `(36 − 1) × 24 = 840`. +- **Model.** Each `printer_model` (a bed size in one mode) has four nozzle presets that share + `printable_area`, `printable_height` and a three-line `machine_start_gcode`: `(4 − 1) × 5 = 15`, so + one base per model, nine in all. The family does not hang its other nozzles off a default-nozzle + preset, so the model level here is a base. +- **A coincidence.** The twelve 500 presets' `printable_height` 500 equals `fdm_klipper_common`'s, but + every preset of the bundle writes its own height and 300 is the most common. The 500 is the base's + leftover, not a shared value, so it stays on the model bases. +- **A copy.** In COPY and MIRROR mode, every nozzle of a model carries the 0.4 nozzle's + `min_layer_height`, `max_layer_height` and `retract_lift_below`: 0.06, 0.3 and 0.2 on the 0.8 nozzle, + where normal mode has 0.12, 0.5 and 0.3. `candidates --group-by printer_model` lists the first and + last as shared by the model, and `max_layer_height` as restated from `fdm_klipper_common`, whose value + is also 0.3. They follow the nozzle, so they stay in the presets and are reported for tuning. +- **Nozzle.** Each preset keeps `printer_model`, `printer_variant`, `nozzle_diameter`, + `min_layer_height`, `max_layer_height` and `retract_lift_below` beside its metadata. +- **Result.** 36 full presets become 36 short ones on 10 new bases. `compare` reports 0 differences, + `check --vendor ""` passes as before, and `generate-id --dry-run` writes nothing. + `check --strict` reports more errors than before, because the deleted variant arrays now reach the + presets at `fdm_klipper_common`'s one value. `fix-variant --strict` writes those arrays into the + family base alone, after which `check --strict` passes for the family and `compare` still reports 0 + differences. diff --git a/.claude/skills/orca-profiles/references/validation.md b/.claude/skills/orca-profiles/references/validation.md index c23b555f49..b5c647f839 100644 --- a/.claude/skills/orca-profiles/references/validation.md +++ b/.claude/skills/orca-profiles/references/validation.md @@ -1,8 +1,8 @@ # Validating profiles ```bash -./scripts/check_profile.sh # everything CI runs -./scripts/check_profile.sh --vendor "" # fast loop +./scripts/check_profile.sh # everything CI runs +./scripts/check_profile.sh --vendor "" # development loop ./scripts/check_profile.sh profile_tool validate_slice # named checks only ``` @@ -12,153 +12,316 @@ scripts\check_profile.bat -Vendor "" scripts\check_profile.bat profile_tool validate_slice ``` -`check_profile.bat` is a shim around `check_profile.ps1` — same checks, same order, same logs; -the flags take PowerShell spellings (`-Vendor`, `-ProfilesDir`, `-Validator`, `-Download`, `-Refresh`, -`-WorkDir`, `-LogLevel`) and positional check names are unchanged. `-p`, `-v` and `-l` are aliases, so -`-v Elegoo -l 2` reads the same on both platforms. It passes `-ExecutionPolicy Bypass` because a +`check_profile.bat` is a shim around `check_profile.ps1`: same checks, same order, same logs. Its flags +take PowerShell spellings (`-Vendor`, `-ProfilesDir`, `-Validator`, `-Download`, `-Refresh`, +`-WorkDir`, `-LogLevel`), and positional check names are unchanged. `-p`, `-v` and `-l` are aliases, +so `-v Elegoo -l 2` reads the same on both platforms. It passes `-ExecutionPolicy Bypass` because a default Windows client refuses to run a checked-out `.ps1` at all. The `.ps1` finds Python itself, probing `py -3`, then `python`, then `python3`; run the tool by hand with `py -3` for the same reason. -Every check in the run happens even after an earlier one fails; the script exits non-zero if any did, and writes -`logs/.log` plus, on failure, `pr_comment.md` under a per-user cache dir — the same report CI posts on the PR. -That dir is `~/Library/Caches/orca-profile-check` on macOS, `${XDG_CACHE_HOME:-~/.cache}/orca-profile-check` on Linux -and `%LOCALAPPDATA%\orca-profile-check` on Windows; it is named apart from OrcaSlicer's own per-user dirs and sits -outside the checkout, so every worktree shares one copy. `--work-dir` / `-WorkDir` overrides it. A stale `.lock` -there after a crash must be removed by hand. +Every check runs even after an earlier one fails; the script exits non-zero if any failed, and writes +`logs/.log` plus, on failure, `pr_comment.md` (the same report CI posts on the PR) under a +per-user cache dir: + +| Platform | Cache dir | +| --- | --- | +| macOS | `~/Library/Caches/orca-profile-check` | +| Linux | `${XDG_CACHE_HOME:-~/.cache}/orca-profile-check` | +| Windows | `%LOCALAPPDATA%\orca-profile-check` | + +It is named apart from OrcaSlicer's own per-user dirs and sits outside the checkout, so every worktree +shares one copy and each run overwrites its `logs/`. `--work-dir` / `-WorkDir` overrides it. + +When other worktrees or agents may run checks too, pass `--work-dir ` from the start +and capture the console output yourself: the shared `logs/` can belong to another run by the time you +read them. With `--work-dir`, also pass `--validator` pointing at the cached nightly, so the new dir +does not download it again: + +| Platform | Cached validator | +| --- | --- | +| Linux | `/validator/OrcaSlicer_profile_validator` | +| macOS | `/validator/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator` | +| Windows | `\validator\OrcaSlicer_profile_validator.exe` | + +Copying the cached `profile-fixtures/` into the new dir reuses the fixture archives; the fixture +`manifest.json` is still downloaded on every run, so `validate_custom` needs the network either way. +`another run is using ` means a live run holds `/.lock`: leave it and use your own +`--work-dir`. Only a `.lock` with no `check_profile` process alive is a crash leftover; delete it by +hand. ## The five checks | Check | Command it runs | Catches | | --- | --- | --- | -| `profile_tool` | `python3 scripts/orca_profile_tool.py check` | index coverage **both ways**, preset-name collisions, files `normalize`/`update-index` would still rewrite, duplicate JSON keys, filament `compatible_printers`, `filament_type` array, conflict keys, id length, **all `setting_id` and `filament_id` rules** | -| `validate_system` | `validator -p resources/profiles -l 2` | load errors, missing filament `compatible_printers`, dangling `inherits`/`compatible_*`, duplicate `filament_id` per printer | -| `validate_slice` | `validator -p … -s -l 2` | custom G-code expansion, unresolvable printer defaults | -| `validate_filament_subtypes` | `validator -p … -l 2 -f` | nothing extra — see below | -| `validate_custom` | `validator -p -l 2` | a shipped preset name that a past release offered no longer resolving | +| `profile_tool` | `python3 scripts/orca_profile_tool.py check` | index coverage **both ways**, preset-name collisions, files `normalize` / `update-index` would still rewrite, duplicate JSON keys, filament `compatible_printers`, `filament_type` array, conflict keys, variant strings and array widths and layout keys, dangling `default_materials`, id length, **all `setting_id` and `filament_id` rules** ([below](#orca_profile_toolpy-check)) | +| `validate_system` | `validator -p resources/profiles -l 2` | load errors, missing filament `compatible_printers`, dangling `inherits` / `compatible_*`, duplicate `filament_id` per printer (`Ambiguous AMS filament match`), printer defaults that name no compatible system filament | +| `validate_slice` | `validator -p … -s -l 2` | custom G-code expansion and unresolvable printer defaults, by slicing | +| `validate_filament_subtypes` | `validator -p … -l 2 -f` | nothing extra; see below | +| `validate_custom` | `validator -p -l 2` | a shipped preset name that a past release offered no longer resolving | -**`-f` is a no-op.** It is declared `po::bool_switch()->default_value(true)`, so the duplicate-`filament_id` -check runs whether or not you pass it — `validate_system` already fails on duplicates. The binary's own -`--help` ("Off unless this flag is present") does not reflect that default. +**`-f` is a no-op.** It defaults to on, so the duplicate-`filament_id` check runs whether or not you +pass it, and `validate_system` already fails on duplicates. The binary's own `--help` ("Off unless this +flag is present") does not reflect that default. -### `validate_custom` — the backward-compatibility gate +**A `--vendor` run reads differently from CI.** The validator's `-v` loads that vendor plus +OrcaFilamentLibrary and nothing else, so every library tune whose `compatible_printers` names another +vendor's printers fails `validate_system`, `validate_filament_subtypes` and each `validate_custom` +fixture with thousands of `references unknown compatible_printers "Bambu Lab …"` lines. Under +`--vendor`, `profile_tool` and `validate_slice` are the meaningful results; for the other three, filter +the log for your vendor's files and treat only those lines as findings. The unscoped run is the CI +result; run it before the PR. -Downloads one fixture archive per past release (v1.9.0 onwards) of *generated mock* user presets — -a `__orca_test` copy of every system preset that -release shipped, cut with the validator's own `-g 1` mode — unpacks each over a copy of the current tree -and loads it. Each entry holds only `inherits` plus a canned diff, so the one failure it adds over -`validate_system` is a shipped preset name disappearing. (The whole current tree sits under each fixture, -so every `validate_system` error fails it too.) This is what makes a rename or an -`instantiation` flip a CI failure rather than just a user complaint, and the reason `renamed_from` is -mandatory. +### `validate_custom`: the backward-compatibility gate + +It downloads one fixture archive per past release (v1.9.0 onwards) of *generated mock* user presets: a +`__orca_test` copy of every system preset that release shipped, cut with the +validator's own `-g 1` mode. It unpacks each over a copy of the current tree and loads it. Each entry +holds only `inherits` plus a canned diff, so the one failure it adds over `validate_system` is a shipped +preset name disappearing. This is what makes a rename, a deletion or an `instantiation` flip a CI +failure rather than just a user complaint, and the reason `renamed_from` is mandatory. + +The whole current tree sits under each fixture, so every `validate_system` error fails +`validate_custom` too: fix `validate_system` first. Under `--vendor` it copies only the top-level index +files, `/` and `OrcaFilamentLibrary/`, and picks fixtures by the index's display `name` +(`Bambulab` for `BBL`), not the file stem; fixture presets without that prefix are covered only by an +unscoped run, and it warns `validate_custom checked nothing` when none match. ### `validate_slice` -Slices a two-colour cube on every instantiable printer in the tree, sequentially, forcing the prime tower. -It selects `default_print_profile` and the first `default_filament_profile`, then updates compatibility; -that update can select a different compatible preset. Confirm the intended defaults yourself rather -than treating a passing sweep as proof that those exact presets were sliced. -A printer fails if it cannot be selected, falls back to a Default preset, throws, produces no g-code, or -emits no `CP TOOLCHANGE START`. It cannot be scoped to a filament-only vendor -(`No instantiable printer presets found for vendor OrcaFilamentLibrary`); `check_profile.sh` records it -as SKIP for a vendor with no `machine/` folder. +It slices a two-colour cube on every instantiable printer in the tree, sequentially, forcing the prime +tower. It selects `default_print_profile` and the first `default_filament_profile`, then updates +compatibility; that update can select a different compatible preset. Confirm the intended defaults +yourself rather than treating a passing sweep as proof that those exact presets were sliced. + +A printer fails if it cannot be selected, falls back to a Default preset, throws, produces no G-code, +or emits no `CP TOOLCHANGE START` (`change_filament_gcode` never expanded). Non-default processes and +filaments get no dedicated coverage; [slice them on a copy](#checking-a-copy-of-the-tree). In the +default set, a bundle without a `machine/` folder is recorded as SKIP; naming `validate_slice` +explicitly for it fails (`No instantiable printer presets found for vendor OrcaFilamentLibrary`). The +validator logs `[error]` lines that do not fail a check (such as `could not found extruder_type`); only +each check's PASS or FAIL counts. ## `orca_profile_tool.py check` -`check` is one subcommand of the tool that also owns -`generate-id`, `normalize`, `trim` and `update-index`; see [ids.md](ids.md) for the writing half. +`check` is one subcommand of the tool that also owns `fix-variant`, `generate-id`, `normalize`, +`trim` and `update-index`; [ids.md](ids.md#the-tool) has the writing half. -| Per vendor | Catches | -| --- | --- | -| `check_preset_name_uniqueness` | two files in one bundle claiming one type + name — indexed or not | -| `check_index_coverage` | a file on disk that no `*_list` references (**an error, not a warning**) | -| `check_name_consistency` | an index entry whose `name` disagrees with the file, or whose `sub_path` is missing | -| `check_normalized` | a file `normalize` would rewrite, and an index `update-index` would rebuild | -| `check_filament_compatible_printers` | an instantiated non-library filament with no `compatible_printers` of its own | -| `check_conflict_keys` | `extruder_clearance_radius` alongside `extruder_clearance_max_radius` | -| `check_vector_type_keys` | a vector option written as a scalar (`"filament_type": "PLA"`) | -| `check_filament_id_length` | a declared `filament_id` longer than 8 characters | -| `check_machine_default_materials` | every `default_materials` / `default_filament_profile` name resolves | -| `check_obsolete_keys` | per-key warnings for ignored options; **filament files only** | +| Catches | Scope | Function in the tool | +| --- | --- | --- | +| two files in one bundle claiming one type + name, indexed or not | per vendor | `check_preset_name_uniqueness` | +| a file on disk that no `*_list` references (**an error, not a warning**) | per vendor | `check_index_coverage` | +| an index entry whose `name` disagrees with the file, or whose `sub_path` is missing | per vendor | `check_name_consistency` | +| a file `normalize` would rewrite, an index `update-index` would rebuild | per vendor | `check_normalized` | +| duplicate JSON keys in a file | every file read | the JSON loader | +| an instantiated non-library filament with no non-empty `compatible_printers` of its own | per vendor | `check_filament_compatible_printers` | +| `extruder_clearance_radius` alongside `extruder_clearance_max_radius` | per vendor | `check_conflict_keys` | +| a scalar `filament_type` (`"filament_type": "PLA"`); the five other filament vectors `normalize` arrayifies surface as `normalize would convert to an array` | per vendor | `check_vector_type_keys`, `check_normalized` | +| a variant string the two enums cannot build (a dead variant, a legacy spelling included), a variant list naming one variant twice, an `extruder_type`, `nozzle_volume_type` or `default_nozzle_volume_type` that is not an enum name ([variant names](#variant-names)) | per vendor | `check_variant_names` | +| a variant array not exactly `variant length × stride` wide in a selectable preset that writes it (with `--strict`, also in one it reaches); machine variant layout keys that disagree; a process id array that does not pair each variant ([variant arrays](#variant-arrays)) | per vendor | `check_variant_arrays` | +| a declared `filament_id` longer than 8 characters | per vendor | `check_filament_id_length` | +| a `default_materials` name, or a `default_filament_profile` name, matching no filament file ([below](#default-material-references)) | per vendor | `check_machine_default_materials` | +| per-key warnings for ignored options, **filament files only** | per vendor | `check_obsolete_keys` | +| `setting_id` uniqueness, every `filament_id` rule, and `machine_model` names duplicated across bundles | **tree-wide, ignoring `--vendor` entirely** | `check_setting_id_uniqueness`, `check_filament_ids`, `check_machine_model_name_uniqueness` | -Tree-wide, **ignoring `--vendor` entirely**: `check_setting_id_uniqueness` and `check_filament_ids`. So a -vendor-scoped run can and does fail on another vendor's files — and it saves seconds, not minutes. +Because the id and model-name checks stay tree-wide, a vendor-scoped run can and does fail on another +vendor's files, and it saves seconds, not minutes. Unscoped, the per-vendor pass covers every bundle. The only exclusion is the stray `user/` directory -(see below); `OrcaFilamentLibrary` is held to the same rules as any vendor, its sole exemption being -that a library filament may leave `compatible_printers` empty — exactly what -`check_filament_compatible_printers` allows. `check_normalized` covers every bundle with an index. +(below); OrcaFilamentLibrary is held to the same rules as any vendor, its sole exemption being that a +library filament may leave `compatible_printers` empty. `check_normalized` covers every bundle with an +index. Notes that matter: -- Exit codes: **0** clean, **1** errors found, **2** argparse misuse. Warnings never change the exit code. -- A nonexistent `--vendor` is a hard error — `[ERROR] unknown vendor "" in `, exit 1. -- `--vendor ""` means all vendors; `check_profile.sh` relies on that. `--vendor` is repeatable. -- A **stray directory** under `resources/profiles/` still gets counted as a vendor by the per-vendor pass - and warned about (`No profiles found for vendor: at …/.json`, and the "Checked vendors" count - goes up by one). The one exception is `user/`, the validator's data dir, which an unscoped `check` - skips by name; `--vendor user` still checks and warns about it. Warnings never change the exit code. - `normalize`, `trim` and `update-index` ignore strays too — they define a bundle as *a directory with a +- Exit codes: **0** clean, **1** errors found, **2** argparse misuse. Warnings never change the exit + code. +- A nonexistent `--vendor` is a hard error: `[ERROR] unknown vendor "" in `, exit 1. +- `--vendor ""` means all vendors; `check_profile.sh` relies on that. `--vendor` is repeatable + (`check --vendor A --vendor OrcaFilamentLibrary`); `check_profile.sh` takes one. +- A **stray directory** under `resources/profiles/` still gets counted as a vendor by the per-vendor + pass and warned about (`No profiles found for vendor: at …/.json`, and the "Checked + vendors" count goes up by one): usually an emptied folder, or a `user/` left by a direct validator + run. An unscoped `check` skips `user/` by name; `--vendor user` still checks and warns about it. + `normalize`, `trim` and `update-index` ignore strays too: they define a bundle as *a directory with a matching index file*. - Each remedy is printed once for the whole run, not once per file, as a `[WARNING]` under the errors - ("2 unreferenced file(s) above: delete them, or run … update-index"). Read those lines: they name the + (`2 unreferenced file(s) above: delete them, or run … update-index`). Read those lines: they name the command that fixes the batch. -- The trailing summary always suggests `normalize`. That is right for the shape errors and misleading for - everything else — an id error needs `generate-id`, a dangling `default_materials` needs a human. +- When there are errors or warnings, the trailing summary suggests `normalize`. That is right for the + shape errors and misleading for everything else: an id error needs `generate-id`, a dangling + `default_materials` needs a human. +- Other options: `--dry-run` on every writing command, `--profiles DIR` to point any command at another + tree, `--profile-type` to narrow `normalize`, `trim` and `update-index` to one type + ([ids.md](ids.md#the-tool)). - `resources/profiles/check_unused_setting_id.py` is a legacy BBL-only diagnostic, not part of profile CI. Use `orca_profile_tool.py check` for current id validation. -### Obsolete-key diagnostics +### Obsolete keys -`check` always reports per-key warnings for obsolete options in filament profiles. -The normalization check also rejects obsolete keys across preset types; `normalize` removes them. +`check` always reports per-key warnings for obsolete options in filament profiles. Its normalization +check also rejects obsolete keys across all preset types (`normalize would remove `); `normalize` +removes them. ### Default-material references -The materials check finds `default_materials` / `default_filament_profile` entries naming a preset -that does not exist. The three authoring errors it surfaces are `,` instead of `;`, wrong case -(`@system`), and a whole `;`-joined string stuffed into one array element. +The materials check finds `default_materials` / `default_filament_profile` entries naming a preset that +does not exist. It reads each `machine/` file's own key (a model's `default_materials`, a variant's +`default_filament_profile`; a file that writes both is checked on `default_materials` only) and accepts +any `name` found in the vendor's or OrcaFilamentLibrary's `filament/` files, bases and unindexed files +included. The three authoring errors it surfaces are `,` instead of `;`, wrong case (`@system`), and a +whole `;`-joined string stuffed into one array element. Only the validator (`validate_system`) requires +an instantiated system filament that is compatible with each variant. + +### Variant arrays + +`check_variant_arrays` composes every selectable preset the loader's way (the parent, then each +`include` in order, then the file's own keys; a filament's `inherits` may fall through to +OrcaFilamentLibrary) and holds each key of [the four variant sets](extruder-variants.md#the-four-key-sets) +that the preset writes itself to exactly its `variant length × stride` +([widths](extruder-variants.md#widths)). The variant length is the length of the composed preset's +own `*_extruder_variant` list; without one, a machine's is the number of variants its +`extruder_variant_list` offers, else its extruder count (the list's default is one +`Direct Drive Standard` per extruder), and a process's or filament's is 1. Any other width is an +error, one value included and even when every value is the same. A key the preset does not write +takes what reaches it, the default or an array it inherits or includes, which the loader resizes; it +is not checked. A base is not judged on its own: what it writes counts only where it reaches a preset +that does not override it. + +`check --strict` also holds every selectable preset to its own width for each key that reaches it, +so a preset whose variants differ from those of the file its array comes from restates the array +(the error names that file). That is BBL's practice and the target for new printer-specific presets; +CI runs `check` without `--strict`. + +An id array that reaches a selectable preset, `printer_extruder_id` or `print_extruder_id`, written or +inherited, must have one entry per entry of its variant list on any printer. This rule and the +machine layout rules below judge the composed preset without `--strict`, whichever file writes the +keys. Beside a written variant list this rule reports it instead of the width +rule, so it is reported once. A process that +lists variants without `print_extruder_id` gets a **warning** when a variant repeats (every entry then +reads as extruder 1, so the repeated variant is unreachable), nothing otherwise. + +On a machine the layout keys are held to [Printer rules 1–4](extruder-variants.md#printer-machine), +every failure an error unless marked: + +- With `extruder_variant_list` written: the list has one entry per extruder, as many as + `nozzle_diameter`; every variant starts with its extruder's `extruder_type`; + `default_nozzle_volume_type` names a nozzle volume type that extruder lists; + `printer_extruder_variant` is the list flattened extruder-major and `printer_extruder_id` gives each + entry its 1-based extruder (an id array left out reads as extruder 1 everywhere, which passes when + those are the flattening's ids). Without the pair, the list may offer one variant in total. +- With the pair written and no `extruder_variant_list`: one variant per extruder at most. A pair that + `single_extruder_multi_material` off would replace with the default at load is a **warning**; a pair + the rebuild would leave as it is passes. + +It does **not** see a base's own resize: it composes at the width each file wrote, so an array wider +than a base's list, which the loader cuts before any child inherits it, passes even with `--strict` +([composition](extruder-variants.md#padding-truncation-and-composition)). Nor does it see per-extruder +vectors outside the sets (`extruder_offset`, `printer_extruder_options`, …), which no variant list +sizes. What a variant holds (a High Flow variant copied from Standard, a variant inserted at the wrong +index) is review work +([item 14](review-checklist.md#14-per-extruder-vectors-not-checked-and-variant-arrays-widths-and-layout-checked)); +whether it is a name the engine can select at all is `check_variant_names`'. + +`python3 scripts/orca_profile_tool.py fix-variant` resizes every array the width rule rejects in the +selectable preset that writes it, leaves bases alone and adds no key: extra values are dropped, missing ones +repeat the last value (the last normal/silent pair at stride 2; a lone value fills normal and silent +alike). It leaves the variant lists and id arrays to you, since they address the variants rather than +fill them. `fix-variant --strict` then also writes each key that reaches a selectable preset at +another width: into the most general file on the way down to the preset whose own width is the +preset's and whose selectable presets taking it all need that width, else into the preset itself. +Presets of every bundle count towards that agreement; `--vendor` limits the files written and +`--dry-run` previews. The loader pads with the first value where `fix-variant` repeats the last, so a +padded array need not load as before, and +trimming deletes values: when the extra values were meant as per-extruder or per-variant values, +declare the variant layout instead ([Printer rule 2](extruder-variants.md#printer-machine)) and keep +them. `fix-variant` moves no value, so a family whose presets all wrote one value repeats the widened +array in each; put it on the family's base afterwards ([shared bases](shared-bases.md)). + +### Variant names + +`check_variant_names` reads the four list keys plus `extruder_type`, `nozzle_volume_type` and +`default_nozzle_volume_type` of every preset the bundle's index references, bases included, and holds +each entry to the names the engine's two enum maps define (`s_keys_map_ExtruderType`, +`s_keys_map_NozzleVolumeType`, read from `PrintConfig.cpp` on every run). The bundle's own files are +judged, not the composed config: a bad string is the writing file's error, once. Every finding is an +error, and no bundle is exempt: BBL, whose bundle is imported from BambuStudio, is held to OrcaSlicer's +enums like any other. + +- A variant string outside ` ` is a **dead variant**: it still + counts toward the variant length the arrays are sized by, so the values written for it silently never + reach the G-code. `Hybrid` too, which is runtime-only, and an empty entry. A name BambuStudio's enum + has and OrcaSlicer's lacks is dead here as well, and passes with no tool change once the engine gains + that nozzle volume type. +- A legacy name the loader still rewrites in these keys (`Normal` → `Standard`, `Big Traffic` → + `High Flow`) is an error that names the enum name to write; the profile has to spell the enum + name. `DirectDrive` is only rewritten in `extruder_type`, so a variant string carrying it is dead. +- A variant list naming one variant twice is an error: the lookup returns the first equal string, so + the repeat is unreachable and its value sits at an index no extruder reads. A filament list takes + strings, `extruder_variant_list` takes them per extruder, and a process takes `(extruder id, variant)` + pairs — one string on two extruders is two pairs, not a repeat. +- An `extruder_type`, `nozzle_volume_type` or `default_nozzle_volume_type` value that is not an enum + name is an error: they are enum options, so an unknown value fails the validator's load of the + whole bundle, while the app silently loads the option's default instead. A legacy spelling + (`DirectDrive`, `Normal`, `Big Traffic`) and `Hybrid`, an enum value no profile writes, are errors + too. + +The variant *order*, the choice of variants, and the values themselves are not checked. ### `normalize` and `update-index` are part of the check -`check` fails when either command would still change something, so they are not optional polish — the +`check` fails when either command would still change something, so they are not optional polish: the file that gets reviewed has to be the file that ships. What `normalize` changes is narrow and fixed: -adds a missing `type`, deletes a `version` or `is_custom_defined` key from a *preset* file, deletes six -print-speed keys from filament profiles (`initial_layer_print_speed`, `outer_wall_speed`, -`inner_wall_speed`, `infill_speed`, `top_surface_speed`, `travel_speed`), deletes the -obsolete keys in `PrintConfigDef::handle_legacy`'s `ignore` set across preset types, resolves the -`extruder_clearance_*` conflict pair by keeping the larger, arrayifies five filament options besides -`filament_type`, and hoists `type`, `name`, `renamed_from`, `inherits`, `from`, `setting_id`, -`filament_id`, `instantiation` to the front. A file it changes is then rewritten whole — tab-indented, -LF, one trailing newline, keys reordered. + +- adds a missing `type`; +- deletes a `version` or `is_custom_defined` key from a *preset* file; +- deletes six print-speed keys from filament profiles (`initial_layer_print_speed`, `outer_wall_speed`, + `inner_wall_speed`, `infill_speed`, `top_surface_speed`, `travel_speed`); +- deletes the obsolete keys the loader ignores (the `ignore` set in `PrintConfigDef::handle_legacy`), + across preset types; +- resolves the `extruder_clearance_*` conflict pair by keeping the larger; +- arrayifies six filament options (`filament_type`, `filament_cost`, `filament_density`, + `temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`); +- hoists `type`, `name`, `renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, + `instantiation` to the front. + +A file it changes is then rewritten whole: tab-indented, LF, one trailing newline, keys reordered. A +file committed with CRLF line endings therefore changes on every line; read the diff before committing +it. **Set `type` explicitly when authoring.** For a file in `machine/` without it, normalization guesses -`machine` only if its name contains `nozzle`, otherwise `machine_model`. That heuristic cannot -reliably classify shared machine bases or unusually named variants. +`machine` only if its name contains `nozzle` (case-insensitive), otherwise `machine_model`. That +heuristic cannot reliably classify shared machine bases or unusually named variants. -The Python obsolete-key set is checked against the C++ source by a unit test. Active options -and legacy aliases that the loader migrates (such as `extruder_type` and -`extruder_clearance_max_radius`) are preserved. +The tool's obsolete-key set is checked against the loader's ignore list by a unit test. Active options +are preserved, including live keys whose *values* the loader rewrites (`extruder_type`: `DirectDrive` → +`Direct Drive`; the variant-string keys: `Normal` / `Big Traffic` → `Standard` / `High Flow`), and so are +legacy key names the loader migrates (such as `extruder_clearance_max_radius`). Two things it therefore does **not** enforce: - **Formatting and key order on their own.** A file with none of those problems is skipped entirely, so 4-space indent, a missing trailing newline, and a file that leads with `compatible_printers` all pass - `check`. They stay latent until something else trips `normalize` and the whole file reformats inside an - unrelated diff. (`normalize --force` rewrites every file, which is not something to run on a shipped - bundle.) + `check`. They stay latent until something else trips `normalize` and the whole file reformats inside + an unrelated diff. (`normalize --force` rewrites every file; do not run it on a shipped bundle.) - **A misspelled setting key.** `inital_layer_height` and `sparse_infill_densiti` pass `check` cleanly. - Verify new keys against `PrintConfig.cpp` and `PrintConfigDef::handle_legacy`. + Verify new keys against `src/libslic3r/PrintConfig.cpp` and the loader's legacy handling + (`PrintConfigDef::handle_legacy`: renamed keys, rewritten values and ignored keys). + +`check` does not catch a dangling `default_print_profile` either; check that name by hand. ## The validator binary -Built from `src/dev-utils/OrcaSlicer_profile_validator.cpp` (`-DORCA_TOOLS=ON`). -Both scripts find a local build under `build*/` — `check_profile.sh` tries Release, RelWithDebInfo, then -Debug, and `check_profile.ps1` adds MinSizeRel — else they download the nightly into the -`validator` subdirectory of the per-user cache dir (see above). Pass `--download` / `-Download` to -match CI exactly, since a stale local build is used silently. Windows looks for +Built from `src/dev-utils/OrcaSlicer_profile_validator.cpp` with `-DORCA_TOOLS=ON`. Both scripts use a +local build under `build*/` when one exists, else they download the nightly into the `validator` +subdirectory of the cache dir. `check_profile.sh` searches Release, then RelWithDebInfo, then Debug +(each under `build*/src/` and `build*/*/src/`), then single-config `build*/src`, and +prefers a host-architecture build tree; `check_profile.ps1` tries Release, RelWithDebInfo, MinSizeRel, +then Debug. A stale local build is used silently; `--download` / `-Download` skips local builds and uses the +nightly. The download and the fixtures stay cached until `--refresh` / `-Refresh`, so +`--download --refresh` (`-Download -Refresh`) matches CI exactly. Windows looks for `OrcaSlicer_profile_validator.exe`. -If your build lives somewhere else entirely, point at it with `--validator` / `-Validator`, or set +If your build lives somewhere else, point at it with `--validator` / `-Validator`, or set `ORCA_PROFILE_VALIDATOR` (`$env:ORCA_PROFILE_VALIDATOR` in PowerShell). | Flag | Meaning | @@ -167,11 +330,12 @@ If your build lives somewhere else entirely, point at it with `--validator` / `- | `-l ` | log level; CI uses 2 | | `-v ` | load only that vendor **plus** OrcaFilamentLibrary | | `-s` | slice sweep | +| `-o ` | with `-s`, save each printer's G-code there | | `-f` | no-op (see above) | | `-g 1` | regenerate user-preset fixtures; takes a value, and wipes the user preset dir first | -On ARM64 Linux the nightly is x86-64 only — the script warns and downloads anyway, producing a binary -that will not run. Build it locally instead. +On ARM64 Linux the nightly is x86-64 only: the script warns and downloads anyway, producing a binary +that will not run. Build it locally with `-DORCA_TOOLS=ON` and pass `--validator`. Running the validator directly uses the profile tree as its data directory and can create `user/` there. Prefer the wrappers, which stash existing user presets and restore them afterward. After a @@ -185,16 +349,27 @@ Use `--profiles DIR` on the Python tool and `-p DIR` on the validator. The wrapp ```bash ./scripts/check_profile.sh --profiles "" +# Windows: scripts\check_profile.bat -ProfilesDir "" ``` -On Windows use `scripts\check_profile.bat -ProfilesDir ""`. +To slice a process or filament that is not a printer's default, or to read the G-code, work on a copy: +copy `resources/profiles` and `resources/info` into one scratch dir (the validator reads `info/` next to +the tree), point the printer's `default_print_profile` or first `default_filament_profile` at the preset +in the copy, and run the validator with `-o`: + +```bash + -p /profiles -v "" -s -o +``` + +Each printer's G-code is saved as `__.gcode`; its embedded config +(`; print_settings_id = …`, `; filament_settings_id = …`) shows what was actually sliced. ## Testing in the app -Editing this checkout's `resources/profiles` does not update a separately installed application. -Test with a build using the edited resources and a bumped bundle version; the updater installs newer -bundles under `/system/`, and the preset cache also depends on the bundle version. -Use Help ▸ Show Configuration Folder to locate the active data directory: +Editing this checkout's `resources/profiles` does not update a separately installed application. Test +with a build using the edited resources and a bumped bundle version: the updater installs newer bundles +under `/system/`, and the preset cache also depends on the bundle version. Use Help ▸ Show +Configuration Folder to locate the active data directory: | Platform | Default data directory | | --- | --- | @@ -202,58 +377,71 @@ Use Help ▸ Show Configuration Folder to locate the active data directory: | Linux | `$XDG_CONFIG_HOME/OrcaSlicer`, or `~/.config/OrcaSlicer` when unset | | Windows | `%APPDATA%\OrcaSlicer` | -A portable `data_dir` next to the executable takes precedence. Use a separate test configuration -for a clean-install check; preserve the normal configuration and user presets. - -## Cross-platform paths - -Match the exact case of each `sub_path` and asset filename; Linux filesystems commonly distinguish -case even when a macOS or Windows checkout does not. Preset-name references are case-sensitive -on every platform. Avoid Windows-invalid characters (`< > : " | ? *`), reserved device names -such as `CON` / `NUL` (including with extensions), and trailing spaces or dots in path components. -Keep stems tidy too, but a space immediately before `.json` is not a trailing path-component space. +A portable `data_dir` next to the executable takes precedence. Use a separate test configuration for a +clean-install check; preserve the normal configuration and user presets. ## Error → remedy | Message | Fix | | --- | --- | -| `can not find inherits for ` | parent missing, unregistered, or listed **after** the child | -| `can not find filament_id for ` | nothing in the chain declares one — run `generate-id` | -| `can not find parent for config !` | a shipped name disappeared — add `renamed_from` | -| `Missing instantiation attribute for ` | key absent **or** not the string `"true"`/`"false"` | +| `can not find inherits for ` | parent missing, unregistered, misspelled, or listed **after** the child | +| `can not find include` | the template is misspelled, registered after the includer, or selectable | +| `can not find filament_id for ` | nothing in the chain declares one: run `generate-id` | +| `can not find parent for config !` | a shipped name disappeared: add `renamed_from` | +| `Failed loading configuration file ` | that file could not be loaded and the whole bundle was discarded: a JSON error, or a value its option cannot take, such as `nil` in a non-nullable key (`Invalid value provided for parameter : nil`, `Deserializing nil into a non-nullable object`); the lines above it name the cause | +| `Missing instantiation attribute for ` | key absent **or** not the string `"true"` / `"false"` | | `contains incorrect keys: , which were removed` | a key valid for a different preset type | -| `defines invalid printer variant ""` | not in the model's `nozzle_diameter` list | -| `has printer_variant "" that does not match its nozzle_diameter` | the set comparison in [machine-profiles.md](machine-profiles.md) | -| `references unknown compatible_printers "

"` | the printer was renamed or deleted; fix the reference | +| `defines invalid printer variant ""` | not a token of the model's `nozzle_diameter` list | +| `has printer_variant "" that does not match its nozzle_diameter` | [the set comparison](machine-profiles.md#printer_model-and-printer_variant) | +| `references unknown compatible_printers "

"` | the printer was renamed or deleted, or a `machine_model` name was used instead of a variant name: fix the reference. Under `--vendor`, usually another vendor's printer ([why](#the-five-checks)) | | `references renamed compatible_printers "" (now "")` | in-tree references must name the current preset; `renamed_from` does not excuse them | -| `Filament preset "" is missing compatible_printers setting` | non-library filaments need a non-empty list in their **own** file — the flattened-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) | -| `Ambiguous AMS filament match: N presets share filament_id "X" … printer "Y"` | make the lists disjoint by [specificity](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product) — the specialized profile keeps the variant, the general ones drop it; prefer this over deleting a profile — or fix an `inherits` pointing at another material's `@base`. `orca_profile_tool.py check` does not catch this; only `validate_system` here does | -| `Layer height cannot exceed nozzle diameter.` / `Line width too small` | `Print::validate()` flow rules | +| `Filament preset "" is missing compatible_printers setting` | non-library filaments need a non-empty list in their **own** file; the resolved-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) | +| `Ambiguous AMS filament match: N filament presets share filament_id "X" and are all compatible with printer "Y"` | make the lists disjoint by [specificity](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product): the specialized profile keeps the variant, the general ones drop it; prefer this over deleting a profile. Or fix an `inherits` pointing at another product's `@base`. `orca_profile_tool.py check` does not catch this; only `validate_system` does | +| `Layer height cannot exceed nozzle diameter.` / `Line width too small` / `Line width too large` | the [slicing limits](process-profiles.md#values-to-review-per-nozzle) | | `[ERROR] … no .json list references it, so it never loads` | `update-index`, or delete the file | -| `[ERROR] … references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` | +| `[ERROR] … no .json list references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` | | `[ERROR] … normalize would ` / `.json: update-index would rebuild ` | run that command and commit the result | | `[ERROR] has N profiles named ""` | identify the intended preset and remove or rename the duplicate; use `trim --dry-run` only for deliberate unindexed-file cleanup | -| `[ERROR] … must not have a setting_id` / `is missing a setting_id` | `generate-id --setting-id` | -| `inherits filament_id "X" but its own triple … mints "Y"` | `generate-id` will **not** fix this — see [ids.md](ids.md) | -| `vendor 's config version: invalid` | the `version` string is not Semver-parseable | -| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or model; see [failure scopes](vendor-bundle.md#failure-modes-ranked-by-blast-radius) | -| `Printer "

" fell back to a default preset` | final process or filament selection is a generic Default preset; check named defaults, visibility and available compatible presets. An incompatible default may instead be replaced without this error | -| `Printer "

" sliced but the filament change never fired` | `change_filament_gcode` never expanded | +| `Duplicate key error in : Duplicate key detected: ` | a key written twice in one file; keep the intended one | +| `… must not have a setting_id` / `… is missing a setting_id` / `setting_id "X" in does not match the expected "Y" …` | `generate-id --setting-id` (by hand in `BBL/`, see [ids.md](ids.md#bbls-exception-precisely)) | +| `filament_id "X" declared by … does not match the mint of its triple …` | `generate-id --filament-id`; if the id should not be declared here at all, remove it so the preset inherits its root's | +| `inherits filament_id "X" but its own triple "V/T/N" mints "Y"` | `generate-id` will **not** fix this; see [ids.md](ids.md#what-generate-id-does-and-does-not-fix) | +| `"" has N values for variant length S at stride k, which takes M` (with ` (no , so …)` after `S`, naming where `S` came from, when the preset writes no list, and ` (it comes from )` at the end under `--strict` for an array the preset does not write) | exactly `S × k` values in variant order, or leave the key out ([widths](extruder-variants.md#widths)); on a list-less multi-extruder printer whose extruders differ, declare the layout; `fix-variant` cuts or pads to that width ([variant arrays](#variant-arrays)); an `S` you did not expect means the variant list did not resolve through `include` or `inherits` | +| `printer_extruder_variant […] is not extruder_variant_list flattened extruder-major […]` / `printer_extruder_id […] does not give each entry of printer_extruder_variant its 1-based extruder […]` / `printer_extruder_id has N entries for the M entries of printer_extruder_variant` | write the pair as the flattening of the list, ids in step ([Printer rule 2](extruder-variants.md#printer-machine)) | +| `extruder_variant_list has N entries for M extruder(s)` / `extruder_variant_list entry i "…" holds a variant that does not start with extruder i's extruder_type` / `default_nozzle_volume_type "…" is not a nozzle volume type extruder i lists` | [Printer rules 1, 3 and 4](extruder-variants.md#printer-machine) | +| `extruder_variant_list offers N variants but the preset writes no printer_extruder_variant/printer_extruder_id` / `printer_extruder_variant lists several variants for extruder N but the preset writes no extruder_variant_list` / `[WARNING] … with single_extruder_multi_material off the loader replaces printer_extruder_variant …` | write all three layout keys ([Printer rule 2](extruder-variants.md#printer-machine)) | +| `print_extruder_id has N entries for the M entries of print_extruder_variant` / `[WARNING] … print_extruder_variant repeats a variant but print_extruder_id is absent` | one id per variant entry, mirroring the printer's pairs ([Process rule 1](extruder-variants.md#process)) | +| ` holds "…", which no extruder can select: "…" is not a nozzle volume type the enum has (…)` / `… it does not start with an extruder type the enum has (…)` / `… is empty` / `… Hybrid names the sub-nozzles of one hybrid extruder at runtime` | write a legal variant string, ` ` from the two enums ([variant names](#variant-names), [variant strings](extruder-variants.md#variant-strings)); in every bundle, BBL included | +| `… holds the legacy variant "…"` / `extruder_type spells the legacy name "…"` / `nozzle_volume_type spells the legacy name "…"` / `default_nozzle_volume_type spells the legacy name "…"` | write the enum name the message gives: the loader still rewrites the legacy one, but `check` rejects it | +| ` lists "…" N times` / `print_extruder_variant lists the pair (extruder N, "…") N times` | drop the repeat and its value from every variant array: the lookup returns the first equal string | +| `extruder_type "…" is not one of (…)` / `nozzle_volume_type "…" is not one of (…)` / `default_nozzle_volume_type "…" is not one of (…)` / `… names "Hybrid", which the engine computes for a hybrid extruder at runtime` | they are enum options, so an unknown value fails the validator's load of the whole bundle and silently becomes the default in the app; use `s_keys_map_ExtruderType` / `s_keys_map_NozzleVolumeType`, `Hybrid` excepted ([variant strings](extruder-variants.md#variant-strings)) | +| `[WARNING] No profiles found for vendor:

` | a directory with no matching index (an emptied folder, or a `user/` left by a direct validator run); remove it | +| `… has no compatible system filament in its model's "default_materials"` | add a system filament preset compatible with that variant to the model's `default_materials` | +| `… names the unknown system filament "" in its "default_materials"` / `… "default_filament_profile"` | name an existing system (not user, not base) filament exactly; `;` separators, exact case | +| `Missing filament profile: '' referenced in ` | the same, caught by `check`; usual causes are `,` instead of `;`, wrong case (`@system`), or a `;`-joined list packed into one array element | +| `machine_model name "" is declared by N bundles` | model names are unique across the tree; rename the new model | +| `vendor 's config version: invalid` | the `version` string does not parse; write `MM.mm.pp.bb` | +| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or a model; see [failure scopes](vendor-bundle.md#failure-scopes) | +| `Printer "

" fell back to a default preset` | the final process or filament selection is a generic Default preset: check the named defaults, their visibility and that compatible presets exist. An incompatible default may instead be replaced without this error | +| `Printer "

" sliced but the filament change never fired (no CP TOOLCHANGE START)` | `change_filament_gcode` never expanded | ## CI -`.github/workflows/check_profiles.yml`, job **"Check profiles"**, on `pull_request` into `main` or -`release/*`, paths `resources/profiles/**`, `resources/printers/**`, `scripts/**` and the workflow itself. -There is no push trigger — a direct push to main runs no profile validation. +`.github/workflows/check_profiles.yml`, job **"Check profiles"**, runs on `pull_request` into `main` or +`release/*` touching `resources/profiles/**`, `resources/printers/**`, `scripts/**`, +`src/libslic3r/PrintConfig.cpp` (where the tool reads the variant key sets) or the workflow itself. +There is no push trigger: a direct push to main runs no profile validation. -The job opens with `python3 -m unittest discover -s scripts/tests -t scripts`, the tool's own unit -tests. That step is deliberately **not** `continue-on-error`: a broken tool makes everything it then says -about the profiles worthless. Every check after it is `continue-on-error` with a final gate, so one run -reports all five results. On failure a second workflow posts or replaces a single PR comment marked -``, with each failing log truncated to 30 KB; it deletes the comment -once the run is green. +The job opens with `python3 -m unittest discover -s scripts/tests -t scripts`, the tool's own unit tests +(run them locally after changing `scripts/`, with `py -3` on Windows, and keep `-t scripts` or the +imports fail). That step is deliberately **not** `continue-on-error`: a broken tool makes everything it +then says about the profiles worthless. Every check after it is `continue-on-error` with a final gate, +so one run reports all five results. On failure a second workflow posts or replaces a single PR comment +marked ``, holding the start of each failing log (30 KB per check, +12 KB per `validate_custom` fixture; reproduce locally for the full list); it deletes the comment once +the run is green. The job name is also the required check for the delegated-merge bot, which lets a vendor maintainer -self-merge a `resources/profiles//` PR with no human review — so whatever CI does not check -is what ships unreviewed. Its denied patterns refuse `^scripts/` and any `.py`, so a PR that touches the -tooling always needs a maintainer. +self-merge a PR limited to their own `resources/profiles//` folder with no human review, so +whatever CI does not check is what ships unreviewed. Its denied patterns refuse `^scripts/` and any +`.py`, so a PR that touches the tooling always needs a maintainer. diff --git a/.claude/skills/orca-profiles/references/vendor-bundle.md b/.claude/skills/orca-profiles/references/vendor-bundle.md index 29aac13331..d6c7710469 100644 --- a/.claude/skills/orca-profiles/references/vendor-bundle.md +++ b/.claude/skills/orca-profiles/references/vendor-bundle.md @@ -1,7 +1,7 @@ -# The vendor bundle and the loader +# Vendor bundles -A bundle is `resources/profiles/.json` (the index) plus `resources/profiles//`. -The **vendor id is the filename stem**, not the `name` inside — several differ (`BBL.json` is named +A bundle is `resources/profiles/.json` (the index) plus `resources/profiles//`. The +**vendor id is the filename stem**, not the `name` inside; the two may differ (`BBL.json` is named "Bambulab"). Asset paths and the `setting_id` formula use the id; the `validate_custom` fixture prefix uses the `name`. @@ -13,16 +13,16 @@ uses the `name`. "version": "02.04.00.03", "force_update": "0", "description": "Phrozen configurations", - "machine_model_list": [ { "name": "...", "sub_path": "machine/....json" } ], - "machine_list": [ ... ], - "process_list": [ ... ], - "filament_list": [ ... ] + "machine_model_list": [ { "name": "Phrozen Arco", "sub_path": "machine/Phrozen Arco.json" } ], + "machine_list": [ … ], + "process_list": [ … ], + "filament_list": [ … ] } ``` -The loader reads `name`, `version`, `url` and the four `*_list` arrays. -`description` is only logged. `force_update` is read by `PresetUpdater`, never by the loader. -`sub_path` is relative to the **vendor folder**. +The loader reads `name`, `version`, `url` and the four `*_list` arrays. `description` is only logged; +`force_update` is read by the profile updater, never by the loader. `sub_path` is relative to the +**vendor folder**. | List | Holds | | --- | --- | @@ -35,141 +35,180 @@ The loader reads `name`, `version`, `url` and the four `*_list` arrays. 1. **Everything is registered, bases included.** Every preset file on disk has exactly one entry in the matching list, and no unindexed preset file is left in the tree. -2. **Parents before children.** `inherits` resolves against a per-kind map filled as the list is walked - (`configs.clear()` then process, filaments, printers). A parent listed after its child produces - `can not find inherits for ` and the bundle is discarded. -3. **The index entry's `name` must equal the `name` inside the sub_path file.** `check_name_consistency` - walks the index looking for the files; `check_index_coverage` walks the files looking for them in the - index. The `renamed_from` escape hatch `check_name_consistency`'s docstring promises is commented out. +2. **Parents before children, includes before includers.** The lists load processes first, then + filaments, then printers, each in index order, and `inherits` and `include` resolve only against + presets of that type already loaded from it. A parent + listed after its child produces `can not find inherits for ` and the bundle is + discarded; an include listed after its includer is `can not find include`, a counted error that + leaves the includer without those keys. +3. **The index entry's `name` equals the `name` inside the `sub_path` file.** `renamed_from` does not + excuse a mismatch. -All three are `check` errors now, and `update-index` writes an index that satisfies all three from the -files on disk — including the parents-first ordering, by topological sort. Hand-editing the index is +All three are `check` errors, and `update-index` writes an index that satisfies all three from the +files on disk, including the dependency ordering (parents and templates before the presets that use +them, then, in name order, entries that neither depend on nor are depended on by another entry of their own +list, and any entry on a dependency cycle). Hand-editing the index is fine for a one-line addition, but the committed result must equal what `update-index` writes, because `check` compares them. -The loader itself reports none of this: an unregistered file, or an entry with a typo'd key -(`"subpath"`), is silently dropped. (A typo'd `sub_path` is a `check` error naming the entry.) +The loader itself reports none of this: an unregistered file, or an entry with a misspelled key +(`"subpath"`), is silently dropped. (A misspelled `sub_path` value is a `check` error naming the entry.) -`BBL/cli_config.json` and `BBL/filament/filaments_color_codes.json` are auxiliary data loaded by path, -not presets. The tool's `NON_PROFILE_FILES` excludes these basenames from preset maintenance. +`BBL/cli_config.json` and, in `BBL/filament/`, `filaments_color_codes.json`, `filament_id_map.json`, +`filament_name_map.json` and `support_recommended_params.json` are auxiliary data files read by path, +not presets. The last three carry a `type` key and look like presets; the tool excludes all five from +preset maintenance. ## `version` -Parsed by a four-component Semver where the 4th is folded in as `patch = patch*100 + value`. Write it -zero-padded, `MM.mm.pp.bb`; a couple of bundles drop a component or the padding, but do not imitate them. +Four components, `MM.mm.pp.bb`, compared as a version number in which the fourth is folded into the +third (`patch × 100 + build`). Write all four components, zero-padded. -- **Bump the version for every bundle the PR touches.** `PresetUpdater` installs bundled resources - only when their version is newer than the installed version; the `.opc` preset cache is also - versioned. Nothing in profile CI checks the bump. -- **Keep the last component ≤ 99.** `02.04.00.100` and `02.04.01.00` both parse to `2.4.100`. A bundle - that reaches `.99` carries into the third component (`02.03.02.99` → `02.03.03.00`). -- An **absent** version is worse than a stale one: the validator still passes, but `Semver::valid()` - excludes `0.0.0`, so the vendor is dropped from the configuration wizard entirely and the preset cache - is disabled for it. An *unparseable* version is not silent — it throws and discards the whole bundle - (see the failure table below). +- **Bump the version for every bundle the change touches.** The app installs bundled profiles only + when their version is newer than the installed one, and the `.opc` preset cache is also keyed on + the version. Nothing in profile CI checks the bump. +- **Keep the last component ≤ 99.** `02.04.00.100` and `02.04.01.00` both read as + `2.4.100`. A bundle that + reaches `.99` carries into the third component (`02.03.02.99` → `02.03.03.00`). +- An **absent** version is worse than a stale one: it reads as `0.0.0`, which is not a valid version. + `check` and the validator still pass, but the vendor is dropped from the setup wizard entirely and gets no preset cache. Confirm the key exists. An + *unparseable* version is not silent: it discards the whole bundle (`vendor 's config version: + invalid`). ## Common preset keys | Key | Value | | --- | --- | -| `type` | `machine_model` / `machine` / `process` / `filament` | -| `name` | the preset name; the filename is *not* authoritative | -| `inherits` | the parent's exact `name` — no path, no `.json` | +| `type` | `machine_model`, `machine`, `process` or `filament` | +| `name` | the preset name, the identity every reference uses; the filename is *not* authoritative | +| `inherits` | the parent's exact `name`: no path, no `.json` | +| `include` | a template's exact `name`, or an array of them, layered under this preset's own keys ([below](#inherits-and-include)) | | `instantiation` | the **string** `"true"` (selectable) or `"false"` (base) | -| `from` | `"system"` by convention; the vendor loader never reads it | -| `setting_id` | required on instantiated presets, forbidden on bases — generated | -| `renamed_from` | `;`-separated list of old names this preset supersedes | +| `from` | `"system"` for shipped presets | +| `setting_id` | generated; required on instantiated presets, forbidden on bases | +| `renamed_from` | `;`-separated old names this preset supersedes ([below](#renamed_from)) | These are config-preset keys; `machine_model` records have their own -[schema](machine-profiles.md#a-machine_model-is-not-a-config-preset). Keep `from` as `"system"` -for shipped presets. The vendor loader ignores it, but the CLI config-file loader accepts only -`system`, `user` or `User` and handles their inheritance differently. +[key set](machine-profiles.md#machine_model-a-record-not-a-config-preset). Keep `from` as `"system"`: +the bundle loader ignores it, but loading the file as a CLI config accepts only `system`, `user` or +`User` and handles their inheritance differently. -`instantiation` is the one metadata key that is hard-gated: a missing key or any value other than the -strings `"true"`/`"false"` is an error (`Missing instantiation attribute for `). A JSON boolean -`true` fails harder — it throws inside `load_from_json` and takes the **whole vendor bundle** down. +`instantiation` is the one metadata key the validator gates: a missing key or any value other than the +strings `"true"` / `"false"` is a counted error (`Missing instantiation attribute for `) that fails +the validator, though the preset still loads and is treated as selectable. A file with no +`instantiation` whose name contains `gcode`, or that has no `name`, silently becomes an include-only +template. A JSON boolean `true` fails harder: it takes the **whole vendor bundle** down. -### `inherits` +## `inherits` and `include` -Resolution is an exact-name lookup **within the same bundle**, plus one exception: filaments may inherit -from `OrcaFilamentLibrary`, which is loaded first and becomes the base bundle. Vendor-to-vendor -inheritance always fails. You can inherit from an instantiated preset as well as from a base; it is -common. +`inherits` resolves by exact name **within the same bundle**, plus one exception: filaments may inherit +from OrcaFilamentLibrary, which is loaded first. Vendor-to-vendor inheritance always fails, and an +unresolved `inherits` discards the bundle. You can inherit from an instantiated preset as well as from a +base. -### `renamed_from` +`"include": ["", …]` (or one bare name) pulls in `instantiation: "false"` presets of the same type +from the same bundle (never the library), registered before the includer. It shares a block of keys +between presets that do not share a parent: a variant layout, a G-code template. Only `"false"` presets +can be included, so a name that resolves to nothing (misspelled, registered after the includer, or a +selectable preset) is a counted error (`can not find include`) and the preset loads without it. + +**How a preset's config is composed:** start from the parent's stored config (a root starts from the +built-in defaults), apply each preset named in `include` in the order listed, then the preset's own +keys. Later layers win, so precedence is own keys > later includes > earlier includes > the `inherits` +chain. Only then is every variant key of the composed config resized to its variant length +([widths](extruder-variants.md#widths)), and keys of another preset type removed. The two routes hand +down different widths: + +- **`inherits` hands down the resized config.** A base is stored *after* its own resize, at the length + of its own `*_extruder_variant` (one variant for a base of any type that writes none, whatever its + extruder count). A child therefore inherits the base's arrays at the base's width: an array wider than + that is cut to its first values before any child sees it, and a child that adds variants gets those + first values padded. So widen an array only on a preset whose own variant list already has the + entries; a wide array on a narrow base is silently lost at load, and `check`, which composes at the + width each file wrote and judges selectable presets only, misses that cut. +- **`include` hands down the template's diff, at its pre-resize width.** An included preset contributes + every key where its own composed config (its parent, its own includes and its own keys) differs from + the built-in defaults, taken *before* its resize. So keys the template inherits are passed on too, + arrays it writes arrive at the width its file wrote, and a key it sets to the built-in default value + is not passed on at all, so it cannot override what the includer inherited. Resizing happens on the + includer, not on the template. + +## `renamed_from` One JSON string, `;`-separated for several old names. -- Write `"A;B"`, never `"A ; B"` — an unquoted item keeps its trailing space and can never match. +- Write `"A;B"`, never `"A ; B"`: a space after a `;` is skipped, but a space before it stays part of + the name (`"A "`), which can never match. - When `renamed_from` is **absent** and the name contains `@`, the loader auto-adds the `@`-removed form (`X @Y` → `X Y`) as a rename alias. Declaring an explicit `renamed_from` **suppresses** that, so a - preset that needs both the `@`-removed form and a real old name must list both. No shipped profile - currently does, which means any preset that gained a `renamed_from` quietly lost its `X Y` alias. + preset that needs both the `@`-removed form and a real old name must list both; a preset that gains + a `renamed_from` without it quietly loses its `X Y` alias. - It rescues names stored **outside** the tree: user presets and 3MF projects. It does **not** rescue - in-tree `inherits` (exact lookup), it does **not** satisfy `check_name_consistency`, the validator - reports an in-tree reference that only resolves through it (`references renamed compatible_printers - "OLD" (now "NEW")`), and `machine_model` records never read it at all. -- Only one preset may claim a given old name — two that do is a counted error + in-tree `inherits` (exact lookup), it does **not** satisfy the index-name rule, the validator reports + an in-tree reference that only resolves through it (`references renamed compatible_printers "OLD" + (now "NEW")`), and `machine_model` records never read it at all. +- Only one preset may claim a given old name; two that do is a counted error (`… was marked as renamed from "Y" … as well`). But the redirect is **inert while a live preset still - carries that name**, and nothing checks *that*; Z-Bolt ships a folder of such dead entries. + carries that name**, and nothing checks *that*, so a neighbour's `renamed_from` is no model. -## Failure modes, ranked by blast radius +## Failure scopes | Scope | Cause | | --- | --- | -| **All vendors, zero system profiles** | a non-string `version`, `name` or `url` at the top level of a vendor index (`"version": 2`), or non-string `nozzle_diameter` on a model — `nlohmann::type_error` escapes the per-vendor `std::runtime_error` catch | -| **The whole vendor bundle** | unparseable `version`; index JSON parse error; a `sub_path` file missing or unparseable; unresolvable `inherits`; duplicate preset name within the vendor; empty/unknown `printer_model` or `printer_variant`; a filament resolving no `filament_id` | -| **One preset** | `instantiation` missing or a wrong string; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for `) | -| **Logged, not counted** | a raw JSON number in a preset — `invalid json type for `, the value is dropped and the exit code stays 0 | -| **Nothing reported by the loader** | unregistered file; misspelled setting key; missing bed/hotend asset. Only the first of those is a `check` error; the other two reach users | +| **Every vendor except OrcaFilamentLibrary, and all user presets** | a non-string where the index or a `machine_model` expects a string (`"version": 2` at the top level of an index, a numeric `name` or `url`, a non-string `nozzle_diameter` or other model key): `[json.exception.type_error.302] type must be string`, and the validator reports `Validation failed` | +| **The whole vendor bundle** | index JSON parse error; unparseable `version`; a listed file missing or unparseable; a value its option cannot take, such as `nil` in a non-nullable key (`Failed loading configuration file`); unresolved `inherits`; two selectable presets with one name; empty or unknown `printer_model` / `printer_variant`; a filament resolving no `filament_id`; a JSON boolean `instantiation` | +| **A counted error; the preset still loads** | `instantiation` missing or not `"true"` / `"false"`; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for `); an `include` naming nothing usable (`can not find include`, loads without it) | +| **The rest of the file, logged only** | an array with a non-string element (`[0.4]`, `invalid json array`): that key and every key after it in the file are dropped, and no error is counted | +| **One value, logged only** | a raw JSON number in a preset (`invalid json type for `): the value is dropped and the exit code stays 0 | +| **Nothing reported by the loader** | unregistered file; two bases with one name, or a base and a selectable preset with one name (the first in the index wins); misspelled setting key; missing bed, hotend or cover asset. `check` catches the first two; the others reach users | -Deleting a file the index still lists surfaces as a *parse error* on line 1, not "file not found" — the -loader `ifstream`s the missing path and nlohmann reports `unexpected end of input`. +Deleting a file the index still lists surfaces as a *parse error* on line 1 (`unexpected end of input`), not "file +not found". -Preset names are a **single global namespace across every vendor**: a duplicate within one vendor is a -hard bundle failure, a duplicate across vendors is reported as `Found duplicated preset: in -vendor: ` and still counts as an error. `check_preset_name_uniqueness` catches the within-bundle -case earlier and more precisely — including an *unindexed* twin, which is one `sub_path` edit away from -silently becoming the parent every child resolves to (`std::map::emplace` keeps the first insertion, so -index order decides). Base names, by contrast, repeat across bundles by design: `fdm_process_common` -exists in nearly all of them. +Selectable preset names are a **single namespace across every vendor**: a duplicate within one vendor +is a hard bundle failure, and a duplicate across vendors is reported as `Found duplicated preset: +in vendor: ` and still counts as an error. `check` catches the within-bundle case earlier and +more precisely, bases included, and including an *unindexed* twin, which is one `sub_path` edit away +from silently becoming the parent every child resolves to (the first registered preset of a name wins, +so index order decides). Base names, by contrast, repeat across bundles by design: +every bundle may have its own `fdm_process_common` ([uniqueness](naming.md#uniqueness)). ## Starting a whole new vendor bundle -Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab` or `M3D`** are -the minimal shape — a shared machine base, the model, one variant, a shared process base, two -processes, and an empty `filament_list` that takes the library generics. Do *not* start from `Phrozen`: -it carries local `fdm_filament_*` copies that have drifted from the library, and a filament preset that -restates most of its parent — the style this skill advises against. +Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab`** is the +minimal shape: a shared machine base, the model, one variant, a shared process base, two processes, and +an empty `filament_list`, so the printer takes the library generics. Do *not* start from a bundle that +carries local `fdm_filament_*` copies, which drift from the library, or filament presets that restate +most of their parent, the style this skill advises against. Write the machine files **last**, so you only visit them once: -1. **Choose the names first** — model, variant(s), process(es). Everything else references them. +1. **Choose the names first**: model, variant(s), process(es). Everything else references them + ([naming.md](naming.md)). 2. `resources/profiles/.json`: `name`, `version` (`01.00.00.00`), `force_update: "0"`, - `description`, and all four `*_list` arrays (an empty `filament_list` is fine). -3. The shared bases — `/machine/fdm_machine_common.json` and - `/process/fdm_process_common.json`, both `"instantiation": "false"` with no `setting_id`. - For a Klipper printer add your own `/machine/fdm_klipper_common.json` inheriting the machine + `description`, and all four `*_list` arrays (empty is fine: `update-index` fills them once the files + exist, so this step only needs the bundle metadata to be right). +3. The shared bases: `/machine/fdm_machine_common.json` and + `/process/fdm_process_common.json`, both `"instantiation": "false"` with no `setting_id`. For + a Klipper printer add your own `/machine/fdm_klipper_common.json` inheriting the machine base; there is no shared one, because a `machine` preset can only inherit inside its own bundle. 4. One selectable process per variant, each naming its variant in `compatible_printers`. 5. Bed assets and `_cover.png`, all directly in `/`. None of them is needed for the - bundle to load, and nothing in CI checks them — but the bed files are inert unless the `machine_model` - names them in `bed_model` / `bed_texture`, and the cover is found by convention as + bundle to load, and nothing in CI checks them; but the bed files are inert unless the + `machine_model` names them in `bed_model` / `bed_texture`, and the cover is found by convention as `_cover.png`. -6. The `machine_model` record and the `machine` variants, now that every value they reference exists — +6. The `machine_model` record and the `machine` variants, now that every value they reference exists; the minimum key sets and the `default_*` shapes are in - [machine-profiles.md](machine-profiles.md#the-machine-variant). -7. Run the tool and validate — follow + [machine-profiles.md](machine-profiles.md#machine-the-variant). +7. Run the tool and validate: follow [Creating or modifying a profile](../SKILL.md#creating-or-modifying-a-profile). `generate-id` is not optional for a new bundle: the validator loads presets that have no `setting_id`, but `check` fails - every one of them. `update-index` will fill the four `*_list` arrays for you once the files exist, so - step 2 only needs the bundle metadata to be right. + every one of them. ## `resources/profiles_template/` A separate tree (`Template.json` + `Template/`) holding filament and process templates. It is **not** a -scaffold for shipped profiles — `CreatePresetsDialog.cpp` reads it for the in-app "create a custom -printer/filament" wizard, so editing it changes what users get when they create a custom preset. -`check_profile.sh`'s validator checks default to `resources/profiles` (redirectable with `-p`), and so -does `orca_profile_tool.py` (redirectable with `--profiles`); -neither covers this tree. +scaffold for shipped profiles: the app's "create a custom printer / filament" dialog reads it, so +editing it changes what users get when they create a custom preset. `check_profile.sh`'s validator +checks default to `resources/profiles` (redirectable with `-p`), and so does `orca_profile_tool.py` +(redirectable with `--profiles`); neither covers this tree. diff --git a/.claude/skills/orca-profiles/scripts/shared_settings.py b/.claude/skills/orca-profiles/scripts/shared_settings.py new file mode 100644 index 0000000000..261fe19da4 --- /dev/null +++ b/.claude/skills/orca-profiles/scripts/shared_settings.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Find settings to move onto shared bases, and prove a move changed nothing. + + snapshot OUT.json write every selectable preset's config as the loader stores it + compare BEFORE.json report every value that differs from the snapshot; exit 1 if any + candidates --vendor V restated values; per base, the settings its presets all share and the + defaults with exceptions that would pay + +Every subcommand takes --profiles DIR (default resources/profiles). + +Configs are composed the loader's way: the parent's stored config, then each include at +the width its file wrote, then the preset's own keys, and every variant key resized to the +preset's own variant list (one variant without one), padded with its first value or cut. +A base is stored after that resize, so a variant array wider than a base's list reaches its +children cut. Not modelled: the built-in defaults. A key no file in a preset's chain writes +loads its default, so compare reports a key written on one side only separately: it is no +change when the written value is the option's default in PrintConfig.cpp. Nor is it modelled +that an include template does not pass on a key equal to the default. Reads +scripts/orca_profile_tool.py. +""" +import argparse +import json +import os +import sys +from collections import Counter, defaultdict + +# The profile tool lives in /scripts; this file in /.claude/skills/orca-profiles/scripts. +sys.path[:0] = [os.path.join(os.getcwd(), "scripts"), + os.path.join(os.path.dirname(os.path.abspath(__file__)), *[os.pardir] * 4, "scripts")] +import orca_profile_tool as tool # noqa: E402 + +TYPES = ("machine", "process", "filament") +# Keys the loader reads from each file as metadata; neither inherits nor include passes them on. +PER_FILE = {"type", "name", "from", "instantiation", "setting_id", "renamed_from", "description", + "inherits", "include", "version", "url", "is_custom_defined"} +# Keys the app replaces with the selected presets' names before slicing: a file's value never counts. +REPLACED = {"print_settings_id", "printer_settings_id", "filament_settings_id"} +# What a restructure changes by design, or what never reaches a slice. +MOVED_BY_DESIGN = {"inherits", "include"} | REPLACED +# Keys that stay in their own file: metadata, identity, and each preset's compatibility. +NEVER_SHARED = PER_FILE | REPLACED | {"filament_id", "compatible_printers", "compatible_prints", + "printer_variant", "printer_model"} + + +class Tree: + def __init__(self, profiles_dir): + self.dir = profiles_dir + self.scheme = tool._variant_scheme() + self.vendors = tool.list_vendor_names(profiles_dir) + self.bundles = {v: tool.load_vendor_configs(profiles_dir, v) for v in self.vendors} + self.cache = {} + + def lookup(self, vendor, ptype, name, in_ofl=False): + """(vendor the name resolves in, (rel, data)); filaments fall back to the library.""" + if not in_ofl and name in self.bundles[vendor][ptype]: + return vendor, self.bundles[vendor][ptype][name] + if ptype == "filament" and tool.OFL in self.bundles and name in self.bundles[tool.OFL][ptype]: + return tool.OFL, self.bundles[tool.OFL][ptype][name] + return None, None + + def composed(self, vendor, ptype, name, drop=None, seen=frozenset()): + """Config before the preset's own resize: parent stored, includes, own keys.""" + owner, found = self.lookup(vendor, ptype, name, vendor == tool.OFL) + if found is None or (owner, name) in seen: + return {} + seen = seen | {(owner, name)} + data = found[1] + config = {} + if data.get("inherits"): + config.update(self.stored(owner, ptype, data["inherits"], seen)) + include = data.get("include") or [] + for included in [include] if isinstance(include, str) else include: + if included in self.bundles[owner][ptype]: + config.update(self.composed(owner, ptype, included, seen=seen)) + config = {k: v for k, v in config.items() if k not in PER_FILE} + config.update((k, v) for k, v in data.items() if k != drop) + return config + + def stored(self, vendor, ptype, name, seen=frozenset()): + key = (vendor, ptype, name) + if key not in self.cache: + self.cache[key] = self.resize(ptype, self.composed(vendor, ptype, name, seen=seen)) + return self.cache[key] + + def resize(self, ptype, config): + list_key, strides = self.scheme[ptype] + length = len(tool._as_list(config[list_key])) if list_key in config else 1 + out = dict(config) + for key, stride in strides.items(): + if key in out: + values = tool._as_list(out[key]) + need = length * stride + out[key] = values[:need] + values[:1] * (need - len(values)) + return out + + def presets(self, vendors=None, ptypes=TYPES): + for vendor in vendors or self.vendors: + for ptype in ptypes: + for name, (rel, data) in sorted(self.bundles[vendor][ptype].items()): + yield vendor, ptype, name, rel, data + + +def snapshot(tree): + return {f"{vendor}/{ptype}/{name}": {k: v for k, v in tree.stored(vendor, ptype, name).items() + if k not in MOVED_BY_DESIGN} + for vendor, ptype, name, _rel, data in tree.presets() + if data.get("instantiation") == "true"} + + +def compare(before, after): + changed, one_sided = 0, [] + for preset in sorted(before.keys() | after.keys()): + old, new = before.get(preset), after.get(preset) + if old is None or new is None: + print(f"{preset}: {'added' if old is None else 'removed'}") + changed += 1 + continue + for key in sorted(old.keys() | new.keys()): + if key not in old or key not in new: + one_sided.append(f"{preset}: {key} " + f"{json.dumps(old[key]) if key in old else '(built-in default)'} -> " + f"{json.dumps(new[key]) if key in new else '(built-in default)'}") + elif old[key] != new[key]: + print(f"{preset}: {key} {json.dumps(old[key])} -> {json.dumps(new[key])}") + changed += 1 + for line in one_sided: + print(line) + print(f"{changed} difference(s)") + if one_sided: + print(f"{len(one_sided)} key(s) written on one side only: each is a difference unless the " + f"written value is the option's default in src/libslic3r/PrintConfig.cpp") + return changed + len(one_sided) + + +def candidates(tree, vendor, ptypes, group_by): + for ptype in ptypes: + entries = {name: data for _v, _t, name, _rel, data in tree.presets([vendor], (ptype,))} + selectable = [n for n, d in entries.items() if d.get("instantiation") == "true"] + stored = {n: tree.stored(vendor, ptype, n) for n in entries} + list_key = tree.scheme[ptype][0] + + # Restated: a key a file writes that it would inherit unchanged without writing it. + restated = {} + for name, data in entries.items(): + restated[name] = sorted( + k for k in data if k not in NEVER_SHARED and k != list_key + and (data.get("inherits") or data.get("include")) + and tree.resize(ptype, tree.composed(vendor, ptype, name, drop=k)).get(k) == stored[name][k]) + if restated[name]: + print(f"{vendor}/{ptype} {name}: restates what it inherits: {', '.join(restated[name])}") + + # Groups: every preset with selectable presets below it, or the --group-by values. + chain = {n: [] for n in selectable} + for name in selectable: + node = entries[name].get("inherits") + while node in entries and node not in chain[name]: + chain[name].append(node) + node = entries[node].get("inherits") + groups = defaultdict(list) + for name in selectable: + if group_by: + groups[json.dumps(stored[name].get(group_by))].append(name) + else: + for base in chain[name]: + groups[base].append(name) + printed = {} + for label, members in sorted(groups.items(), key=lambda g: -len(g[1])): + if len(members) < 2 or label == "null": + continue + if frozenset(members) in printed: + print(f"\n{vendor}/{ptype} {label}: the same presets as {printed[frozenset(members)]}") + continue + common = [b for b in chain[members[0]] if all(b in chain[m] for m in members[1:])] + home = common[0] if group_by and common else None if group_by else label + + def below(m): + """m and the files between it and the group's home.""" + return [m] + chain[m][:chain[m].index(home)] if home in chain[m] else [m] + shared, defaults = [], [] + for key in sorted(set().union(*(entries[m].keys() for m in members)) - NEVER_SHARED): + loaded = [json.dumps(stored[m].get(key)) for m in members] + counts = Counter(loaded).most_common(2) + if len(counts) == 1: + writers = sum(key in entries[m] and key not in restated[m] for m in members) + if writers >= 2: + shared.append(f"{key} ({writers} write it)") + continue + (top, held), (_, runner_up) = counts + if held == runner_up or top == "null" or home is None: + continue + # Balance 5: w presets drop their copy; a presets that take the key from the home + # (no file on their way to it writes it) and load another value must write theirs. + w = sum(key in entries[m] and v == top for m, v in zip(members, loaded)) + a = sum(v != top and not any(key in entries[f] for f in below(m)) + for m, v in zip(members, loaded)) + if w - a > 1: + shown = top if len(top) <= 40 else top[:37] + "..." + defaults.append(f"{key} = {shown}: {held} load it, {w} write it, " + f"{a} would have to write their own") + if shared or defaults: + where = f"; nearest common base {common[0]}" if group_by and common else "" + title = f"{group_by} = {label}" if group_by else label + print(f"\n{vendor}/{ptype} {title}: {len(members)} presets{where}") + printed[frozenset(members)] = title + for line in shared: + print(f" {line}") + if defaults: + print(" default with exceptions:") + for line in defaults: + print(f" {line}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + profiles = argparse.ArgumentParser(add_help=False) + profiles.add_argument("--profiles", default=os.path.join("resources", "profiles"), + help="profiles directory (default: resources/profiles)") + sub = parser.add_subparsers(dest="command", required=True) + sub.add_parser("snapshot", parents=[profiles]).add_argument("out") + sub.add_parser("compare", parents=[profiles]).add_argument("before") + cand = sub.add_parser("candidates", parents=[profiles]) + cand.add_argument("--vendor", required=True) + cand.add_argument("--type", choices=TYPES, action="append") + cand.add_argument("--group-by", help="group selectable presets by this key's value instead of by " + "base: printer_model, gcode_flavor, extruder_type, filament_id, layer_height, ...") + args = parser.parse_args() + tree = Tree(args.profiles) + if args.command == "snapshot": + presets = snapshot(tree) + with open(args.out, "w", encoding="utf-8") as f: + json.dump(presets, f, sort_keys=True) + print(f"{len(presets)} selectable presets written to {args.out}") + elif args.command == "compare": + with open(args.before, encoding="utf-8") as f: + sys.exit(1 if compare(json.load(f), snapshot(tree)) else 0) + else: + candidates(tree, args.vendor, args.type or TYPES, args.group_by) + + +if __name__ == "__main__": + main()