mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-30 20:31:09 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77a64c300a | ||
|
|
ee117d009a | ||
|
|
8a95c6d507 | ||
|
|
e8e5ce2ba0 | ||
|
|
8d2196d4a6 | ||
|
|
4303d723a1 | ||
|
|
132086933f | ||
|
|
4d81f0c181 | ||
|
|
1e0fb1a0ae | ||
|
|
72998ddda2 | ||
|
|
4d421918ef | ||
|
|
0f48ad4157 | ||
|
|
97a7eb459f | ||
|
|
9abec76af1 | ||
|
|
8b67ee0e2f | ||
|
|
7ebcf3e1e5 | ||
|
|
d7341e981b | ||
|
|
1d30019679 |
@@ -1,86 +0,0 @@
|
||||
# DELEGATION SPECIFICATION: HARNESS-DRIVEN VALIDATION LOOP
|
||||
slug: sketch-focus-arbiter · repo: /home/tommaso/projects/apps/orca_cad · branch: cad-mainline
|
||||
|
||||
## 1. TARGET GOAL
|
||||
|
||||
**Functional Objective.** Keyboard input in the Design tab is routed by WHAT THE KEY IS, not by
|
||||
which widget the window manager decided to focus. Adopted from FreeCAD's
|
||||
`DrawSketchKeyboardManager::detectKeyboardEventHandlingMode`
|
||||
(src/Mod/Sketcher/Gui/DrawSketchKeyboardManager.cpp), which never queries focus at all:
|
||||
|
||||
- digit, `-`, `.`, `,` -> the open value field
|
||||
- Backspace / Delete -> the open value field (when one is open)
|
||||
- Enter / Return / Tab -> commit the field, control returns to the view
|
||||
- a letter -> the sketch-tool shortcut map, as today
|
||||
- Esc -> the existing CadLevel LIFO (DesignInteraction.hpp), unchanged
|
||||
- anything else -> sticky: whoever had it keeps it
|
||||
|
||||
Observable postcondition: for EVERY sketch tool that opens a value field, a value typed
|
||||
immediately after the field appears — with NO click into the field — is the value committed.
|
||||
Today the prefill is committed instead whenever the WM withholds focus.
|
||||
|
||||
**Target Files / Scope (writable).**
|
||||
src/slic3r/GUI/CAD/DesignPanel.cpp (the arbiter lives in the existing wxEVT_CHAR_HOOK)
|
||||
src/slic3r/GUI/CAD/DesignCanvas.cpp/.hpp (forwarding entry points only)
|
||||
src/slic3r/GUI/CAD/SketchInlineEditor.cpp/.hpp (accept a programmatically delivered character)
|
||||
scripts/CAD/check-gui-click-edit.py (F2P oracle — authoring exception, see §4)
|
||||
Everything else read-only. No dependency additions, no reformatting.
|
||||
|
||||
**Open Bindings.**
|
||||
- The in-canvas ImGui field on wip/in-canvas-value-field is NOT in scope. Default: the arbiter
|
||||
is implemented against the CURRENT wxFrame field on cad-mainline, because content-based
|
||||
routing makes the window's focus irrelevant either way. If it later moves in-canvas the
|
||||
arbiter is unchanged.
|
||||
- Tools whose field is opened by a toolbar button rather than a gesture (Constrain path) are
|
||||
covered by the same arbiter but are not in the F2P tool list. Default: assert them in P2P only.
|
||||
|
||||
## 2. HARNESS ENVIRONMENT & GROUND TRUTH
|
||||
|
||||
The rig container `orcacad-gui` on nativedev IS the harness. Xvfb `:11` + openbox, the app under
|
||||
test, `xdotool` for synthetic input, and an MCP socket at `/tmp/mcp.sock` that reports sketch
|
||||
state as JSON. It is a closed loop: drive input, read geometry back, assert. No window manager
|
||||
politics, no human.
|
||||
|
||||
Harness interface (ordered; each slot one invocation, one exit code):
|
||||
S1 sync docker cp <file> orcacad-gui:/OrcaSlicer/<path>
|
||||
S2 build docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer
|
||||
S3 restart docker exec orcacad-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh
|
||||
S4 F2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach
|
||||
S5 P2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py
|
||||
|
||||
**F2P.** `scripts/CAD/check-gui-click-edit.py`. For each of Line, Rectangle, Circle, Slot,
|
||||
Polygon, Ellipse and Rounded rectangle: arm the tool, draw it, and type a value that differs
|
||||
from the prefill WITHOUT clicking the field. Assert the committed value equals the typed value.
|
||||
The ladder must FAIL against unmodified cad-mainline — that is what proves it asserts something.
|
||||
|
||||
**P2P.** `scripts/CAD/check-gui-sketching.py`, the existing gesture ladder, minus anything red at
|
||||
baseline. NOTE: it calls `focus_field()` — one click into the field before typing — which is the
|
||||
workaround this whole task removes. It stays green as a regression guard; it is NOT evidence.
|
||||
|
||||
**Test Integrity Constraint.** `focus_field()` in check-gui-sketching.py must NOT be deleted to
|
||||
make things pass, and check-gui-click-edit.py must NOT be weakened. Either invalidates the run.
|
||||
|
||||
## 3. VERIFICATION COMMANDS
|
||||
1. Static: `docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer` (warnings delta only;
|
||||
this repo configures no linter — the compiler is the static gate. Absolute-zero is NOT the gate.)
|
||||
2. Harness: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach`
|
||||
3. Regression: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py`
|
||||
|
||||
## 4. CONVERGENCE LOOP — ceiling 8 iterations
|
||||
EDIT (scoped) -> EXECUTE S1..S5 -> PARSE the ladder's per-tool assertions and the [UX]/[KEYTRACE]
|
||||
lines -> PATCH from the parsed cause. On ceiling without convergence: stop, report the last diff
|
||||
and the unresolved failure set. Do not report success.
|
||||
|
||||
F2P authoring exception: check-gui-click-edit.py is writable, and must be shown RED against
|
||||
unmodified source before any source edit counts.
|
||||
|
||||
## 5. TERMINATION CRITERIA
|
||||
- [ ] S2 exits 0, and introduces no compiler warning absent from the baseline.
|
||||
- [ ] S4 ALL_PASSED — every tool commits the typed value, no click into the field.
|
||||
- [ ] S5 shows zero regressions against its recorded baseline pass count.
|
||||
- [ ] F2P proven red without the fix (source stashed, ladder re-run, must FAIL).
|
||||
|
||||
## 6. GUARDRAILS
|
||||
Zero-assumption: no completion claim without captured stdout and exit codes. Oracle supremacy:
|
||||
the ladder's verdict overrides my judgement. Blast radius: §1 files only. Baseline obligation:
|
||||
run §3 once before the first edit and record it.
|
||||
@@ -1,188 +0,0 @@
|
||||
---
|
||||
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, 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
|
||||
|
||||
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.
|
||||
|
||||
A bundle is the index `resources/profiles/<Vendor>.json` plus the folder `<Vendor>/`. 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.
|
||||
|
||||
## 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 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) |
|
||||
| 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 |
|
||||
| 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) |
|
||||
|
||||
## Rules
|
||||
|
||||
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` | `<Model>` (`Bambu Lab X1 Carbon`) | the exact string, named by each variant's `printer_model`; also the `<Model>_cover.png` stem |
|
||||
| `machine` | `<Model> <nozzle> 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` | `<lh>mm <Quality> @<target>` | the exact string when referenced or selected; `@<target>` is a label, compatibility comes from the preset's list or condition |
|
||||
| `filament` | `<Product> @<target>` | 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 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](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 "<Vendor>"
|
||||
python3 scripts/orca_profile_tool.py update-index --vendor "<Vendor>"
|
||||
python3 scripts/orca_profile_tool.py generate-id --vendor "<Vendor>"
|
||||
python3 scripts/orca_profile_tool.py check
|
||||
```
|
||||
|
||||
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 "<Vendor>" # development loop
|
||||
./scripts/check_profile.sh # full tree before the PR
|
||||
```
|
||||
|
||||
On Windows use `scripts\check_profile.bat -Vendor "<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 look
|
||||
|
||||
| Symptom | Start here |
|
||||
| --- | --- |
|
||||
| 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 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.
|
||||
@@ -1,547 +0,0 @@
|
||||
# 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 `"<extruder_type> <nozzle volume type>"`: 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 <name>"`, 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 `"<type> 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`, …) 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
|
||||
`"<extruder_type> <nozzle volume type>"` (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 "<extruder_type[i]> <nozzle volume type>"
|
||||
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<std::string>` 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-29 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<std::string>\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 (54): `activate_air_filtration`, `activate_air_filtration_during_print`, `activate_air_filtration_on_completion`, `adaptive_pressure_advance`, `adaptive_pressure_advance_bridges`, `adaptive_pressure_advance_model`, `adaptive_pressure_advance_overhangs`, `complete_print_exhaust_fan_speed`, `during_print_exhaust_fan_speed`, `enable_pressure_advance`, `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`, `pressure_advance`, `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.
|
||||
@@ -1,286 +0,0 @@
|
||||
# Filament profiles and OrcaFilamentLibrary
|
||||
|
||||
`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
|
||||
|
||||
| Contribution | Location |
|
||||
| --- | --- |
|
||||
| Generic material for all printers | `OrcaFilamentLibrary/filament/Generic <mat> @System.json` |
|
||||
| A brand's product, all printers | `OrcaFilamentLibrary/filament/<Brand>/` |
|
||||
| A brand's tune for one printer vendor | `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/` (recommended); `<PrinterVendor>/filament/<Brand>/` also works |
|
||||
| A printer vendor's tune of a generic, or its own product | `<PrinterVendor>/filament/` |
|
||||
|
||||
Both locations in the third row are supported: `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/<Name>.json`
|
||||
(the shape the wiki shows) and `<PrinterVendor>/filament/<Brand>/`. The library path is the one a
|
||||
filament brand should contribute to: `OrcaFilamentLibrary/filament/<Brand>/` is the brand's own folder,
|
||||
while a printer vendor's folder belongs to that printer vendor.
|
||||
|
||||
Library layout: `filament/base/fdm_filament_*.json` material roots, root-level
|
||||
`Generic <mat> @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
|
||||
// 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", // minted here by generate-id; every child inherits it
|
||||
"filament_vendor": ["Polymaker"], "filament_type": ["PA6-CF"], /* … */ }
|
||||
|
||||
// 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": [] }
|
||||
|
||||
// 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 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.
|
||||
|
||||
## Colour is a runtime property
|
||||
|
||||
`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-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` 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
|
||||
// <Vendor>/filament/Generic PETG @Acme One 0.4 nozzle.json
|
||||
{ "type": "filament", "name": "Generic PETG @Acme One 0.4 nozzle", "from": "system",
|
||||
"instantiation": "true", "inherits": "Generic PETG @System",
|
||||
"filament_flow_ratio": ["0.95"], "filament_max_volumetric_speed": ["10"],
|
||||
"compatible_printers": ["Acme One 0.4 nozzle"] }
|
||||
```
|
||||
|
||||
**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
|
||||
// <Vendor>/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
|
||||
|
||||
// <Vendor>/filament/Acme Aura PETG @Acme One 0.4 nozzle.json
|
||||
{ "type": "filament", "name": "Acme Aura PETG @Acme One 0.4 nozzle", "from": "system",
|
||||
"instantiation": "true", "inherits": "Acme Aura PETG @base",
|
||||
"filament_max_volumetric_speed": ["11"],
|
||||
"compatible_printers": ["Acme One 0.4 nozzle"] }
|
||||
```
|
||||
|
||||
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** (`@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: `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 preset of one product shares it
|
||||
(`<filament_vendor>/<filament_type>/<alias>`). 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, often vague hint; decide by the lists, with a judgement call on the
|
||||
name. Naming conventions differ by vendor: BBL's is the reference (`@<Vendor> <Model>` for a whole
|
||||
model, `@<Vendor> <Model> <nozzle> nozzle` for one variant, `@<Vendor>` for a vendor-wide generic), but
|
||||
others vary (`@<printer model>`, a printer serial, or Creality's `@<Model>-all`). A name never overrides
|
||||
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
|
||||
`… @<Vendor> <Model> <nozzle> nozzle`).
|
||||
2. **Model-specialized**: lists the variants of one printer model (BBL-style `… @<Vendor> <Model>`). 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 <mat> @<Vendor>`).
|
||||
|
||||
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.
|
||||
- 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 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.
|
||||
|
||||
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 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 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 hide each other; overlapping lists for the same product trip the ambiguous-match error
|
||||
above instead.
|
||||
- This is why adding `Generic PLA @<printer>` 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 <mat> @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 <mat> @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, 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.
|
||||
|
||||
- `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. 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"`
|
||||
|
||||
`"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 <key>: nil`). To leave a non-nullable key unset, 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.
|
||||
|
||||
## Tuning per nozzle and per variant
|
||||
|
||||
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` (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.
|
||||
|
||||
On a printer with extruder variants, a filament tunes these per variant too:
|
||||
`filament_max_volumetric_speed`, `filament_flow_ratio`, `nozzle_temperature`, pressure advance 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 `fan_max_speed` 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". 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 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.
|
||||
@@ -1,156 +0,0 @@
|
||||
# `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
|
||||
[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.
|
||||
|
||||
| | `setting_id` | `filament_id` |
|
||||
| --- | --- | --- |
|
||||
| Identifies | one selectable preset | one filament **product** |
|
||||
| Key hashed | `<vendor folder>/<type>/<name>` | `filament_product/<filament_vendor>/<filament_type>/<alias>`, 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` 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 |
|
||||
|
||||
`<type>` 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
|
||||
|
||||
`scripts/orca_profile_tool.py` takes a subcommand:
|
||||
|
||||
| Command | Does |
|
||||
| --- | --- |
|
||||
| `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 `<Vendor>.json` list references |
|
||||
| `update-index` | rebuilds the `*_list` sections from the files on disk |
|
||||
|
||||
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
|
||||
> part of landing a profile. Preview with `--dry-run`.
|
||||
|
||||
**Register, then mint.** The `filament_id` pass reads `<Vendor>.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.
|
||||
|
||||
- `--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
|
||||
[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 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.
|
||||
|
||||
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
|
||||
|
||||
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 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, 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 (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 library generic's id: wrong product
|
||||
```
|
||||
|
||||
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`, …) 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'))"
|
||||
# -> OF5CgdDq
|
||||
```
|
||||
|
||||
The quoting works unchanged in cmd and PowerShell; only swap `python3` for `py -3`.
|
||||
`generate_preset_setting_id('<vendor folder>', '<type>', '<name>')` is the `setting_id` equivalent.
|
||||
|
||||
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
|
||||
|
||||
The exception covers **`setting_id` assignment only**, keyed on the *folder* name `BBL`:
|
||||
|
||||
- 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
|
||||
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 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`. `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_<series>_<vendor>_<typeidx>`), 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) 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).
|
||||
@@ -1,232 +0,0 @@
|
||||
# Printer models and variants
|
||||
|
||||
Both live in `resources/profiles/<Vendor>/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`.
|
||||
|
||||
## `machine_model`: a record, not a config preset
|
||||
|
||||
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**, 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
|
||||
{
|
||||
"type": "machine_model",
|
||||
"name": "Phrozen Arco",
|
||||
"machine_tech": "FFF",
|
||||
"family": "Phrozen",
|
||||
"model_id": "Phrozen Arco",
|
||||
"nozzle_diameter": "0.4",
|
||||
"bed_model": "Phrozen Arco_buildplate_model.stl",
|
||||
"bed_texture": "Phrozen Arco_buildplate_texture.svg",
|
||||
"hotend_model": "",
|
||||
"default_materials": "Generic PLA @Phrozen Arco 0.4 nozzle"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Notes |
|
||||
| --- | --- |
|
||||
| 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** (named by the
|
||||
vendor id). Convention: `<Model>_buildplate_model.stl` and `<Model>_buildplate_texture.svg`. An
|
||||
empty string is the legal "none", and is the norm for `hotend_model`.
|
||||
|
||||
**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 `<Model>_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.
|
||||
|
||||
## `machine`: the variant
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "machine",
|
||||
"name": "Phrozen Arco 0.4 nozzle",
|
||||
"inherits": "fdm_machine_common",
|
||||
"from": "system",
|
||||
"setting_id": "lvaYKTUZr5C9jSwk",
|
||||
"instantiation": "true",
|
||||
"printer_model": "Phrozen Arco",
|
||||
"printer_variant": "0.4",
|
||||
"nozzle_diameter": ["0.4"],
|
||||
"default_print_profile": "0.20mm Standard @Phrozen Arco 0.4 nozzle",
|
||||
"default_filament_profile": ["Generic PLA @Phrozen Arco 0.4 nozzle"],
|
||||
"printable_area": ["0x0", "300x0", "300x300", "0x300"],
|
||||
"printable_height": "300"
|
||||
}
|
||||
```
|
||||
|
||||
Minimum viable key set: `type`, `name`, `from`, `instantiation`, `setting_id`, `inherits`,
|
||||
`printer_model`, `printer_variant`, `nozzle_diameter`, `printable_area`, `printable_height`,
|
||||
`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_model` and `printer_variant`
|
||||
|
||||
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 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 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.
|
||||
|
||||
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. A variant whose `printer_model` names a sibling model by mistake
|
||||
leaves its own model's size in exactly that state.
|
||||
|
||||
### Other keys
|
||||
|
||||
- `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, 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
|
||||
|
||||
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
|
||||
([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 `<Model>_cover.png` go directly in `<Vendor>/`.
|
||||
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. 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
|
||||
(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)), 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
|
||||
|
||||
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 (`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 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.
|
||||
|
||||
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`. 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 G-code is actually expanded, which means `validate_slice`:
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh --vendor "<Vendor>" validate_slice
|
||||
# Windows: scripts\check_profile.bat -Vendor "<Vendor>" validate_slice
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -1,94 +0,0 @@
|
||||
# Preset naming
|
||||
|
||||
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`
|
||||
|
||||
`<Model>`, 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 `<Model>_cover.png` and, by convention, of the bed assets
|
||||
(`<Model>_buildplate_model.stl`).
|
||||
|
||||
## `machine` (variant)
|
||||
|
||||
`<Model> <nozzle> 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
|
||||
`<Model> 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`
|
||||
|
||||
`<layer height>mm <quality> @<target>`. The quality word stays before `@` and the printer target after
|
||||
it: a printer model in the quality position leaves the tier undescribed. The `@<target>` 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`
|
||||
|
||||
`<Product> @<target>`. 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. 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
|
||||
(`<Product> @System`, empty `compatible_printers`). Not enforced, so a deviation is worth a review
|
||||
comment. The literal `Generic <mat> @System` is also load-bearing for project recovery
|
||||
([alias shadowing](filament-profiles.md#alias-shadowing)).
|
||||
- Printer tunes. BBL's shape is the reference: `@<Vendor>` (vendor-wide), `@<Vendor> <Model>` (one
|
||||
model), `@<Vendor> <Model> <nozzle> nozzle` (one variant). Other vendors differ: a bare model
|
||||
(`QIDI ABS-GF@Q2-Series`), a printer serial, Creality's `@<Model>-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)).
|
||||
- No colour in the product name: `<Product> <Colour>` presets are not authored; colour is chosen at
|
||||
runtime ([colour](filament-profiles.md#colour-is-a-runtime-property)).
|
||||
|
||||
## Bases
|
||||
|
||||
| Type | Base names |
|
||||
| --- | --- |
|
||||
| `machine` | `fdm_machine_common`, `fdm_<vendor>_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_<vendor>_<lh>_nozzle_<n>` |
|
||||
| `filament` | `fdm_filament_*` material roots, `<Product> @base` product roots |
|
||||
|
||||
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.
|
||||
|
||||
## Uniqueness
|
||||
|
||||
- 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.
|
||||
|
||||
## 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`.
|
||||
@@ -1,155 +0,0 @@
|
||||
# Process profiles
|
||||
|
||||
Processes live in `resources/profiles/<Vendor>/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
|
||||
|
||||
`"<layer height>mm <quality> @<target>"` 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:
|
||||
|
||||
| Quality | Ratio | 0.2 nozzle | 0.4 | 0.6 | 0.8 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Extra Fine | 0.2× | — | 0.08 | — | — |
|
||||
| Fine | 0.3× | 0.06 | 0.12 | 0.18 | 0.24 |
|
||||
| Optimal | 0.4× | 0.08 | 0.16 | 0.24 | 0.32 |
|
||||
| Standard | 0.5× | 0.10 | 0.20 | 0.30 | 0.40 |
|
||||
| Draft | 0.6× | 0.12 | 0.24 | 0.36 | 0.48 |
|
||||
| Extra Draft | 0.7× | 0.14 | 0.28 | 0.42 | 0.56 |
|
||||
|
||||
This is the `fdm_process_single_<lh>_nozzle_<n>` ladder; 0.4 is commonly the unsuffixed nozzle default.
|
||||
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: it need not equal any printer variant name.
|
||||
Compatibility comes from the resolved list or condition, not this label.
|
||||
|
||||
## Shape
|
||||
|
||||
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:
|
||||
`fdm_process_common` → `fdm_process_arena_common` → `fdm_process_arena_<lh>_nozzle_<n>` → leaf, where the
|
||||
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'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)).
|
||||
|
||||
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: a bundle may chain selectable processes several levels deep, so editing one
|
||||
silently changes others. Check a leaf's children before editing it.
|
||||
|
||||
## Compatibility
|
||||
|
||||
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
|
||||
legitimate for a process, and no check enforces its presence.
|
||||
|
||||
- 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.
|
||||
- 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.*/
|
||||
```
|
||||
|
||||
The `[^_a-zA-Z0-9]` exists because `PRINTER_MODEL_COREONE_L` also contains `PRINTER_MODEL_COREONE`.
|
||||
|
||||
A leaf listing a whole model family is where a newly added printer is usually forgotten.
|
||||
|
||||
## 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 `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 behaviour |
|
||||
|
||||
**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 or automatic widths, and validate their resolved
|
||||
values.
|
||||
|
||||
`min_layer_height` and `max_layer_height` are machine keys; no process file sets them.
|
||||
|
||||
### Slicing limits
|
||||
|
||||
Slicing rejects a process that breaks one of these (the message in italics):
|
||||
|
||||
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`: 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, 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**.
|
||||
|
||||
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**, 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 bundle's existing ladder.
|
||||
2. If the bundle has per-nozzle bases, add one (`fdm_process_<vendor>_<lh>_nozzle_<n>`) 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 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.
|
||||
@@ -1,277 +0,0 @@
|
||||
# Reviewing a profile change
|
||||
|
||||
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; 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` / 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 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 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/<Vendor>.json` must have its
|
||||
`version` incremented: last component, carrying `.99` into the third component. A library change means
|
||||
bumping `OrcaFilamentLibrary.json`.
|
||||
|
||||
*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` 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
|
||||
bundle had drifted, the author's PR now carries someone else's reordering; ask for it in a separate
|
||||
commit rather than reviewing it inline.
|
||||
- **A deleted selectable preset needs a successor** as in item 4. `update-index` removes its
|
||||
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
|
||||
([what normalize changes](validation.md#normalize-and-update-index-are-part-of-the-check)). Check that
|
||||
the keys it removed were meant to go.
|
||||
|
||||
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.
|
||||
|
||||
## 3. Are ids generated, not written?
|
||||
|
||||
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 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
|
||||
`setting_id` breaks preset identity. See [ids.md](ids.md).
|
||||
|
||||
## 4. Does anything disappear for existing users?
|
||||
|
||||
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 <name> for config <file>!`; 3MF-embedded
|
||||
presets are dropped with no error at all. Commit `33923464ae` reverted exactly this for Cubicon;
|
||||
`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 ([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, 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` 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. One product, one all-printer preset; colour is not a preset
|
||||
|
||||
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 `<Product> @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,
|
||||
so another compatible process can conceal a bad reference. Inspect it even after a pass.
|
||||
|
||||
## 8. Types and spellings
|
||||
|
||||
Every value a string or an array of strings; `filament_type` an array; `instantiation` the string
|
||||
`"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 6](../SKILL.md#rules)), the single most common way a profile
|
||||
edit does nothing while CI stays green.
|
||||
|
||||
## 9. Blast radius of a base edit
|
||||
|
||||
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).
|
||||
|
||||
## 10. Do the numbers make sense for the nozzle and material?
|
||||
|
||||
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.
|
||||
|
||||
## 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 `<Model>_cover.png` exist under
|
||||
`resources/profiles/<vendor folder>/`, in exact case. Nothing checks them.
|
||||
|
||||
## 13. `default_materials` and `default_filament_profile` (checked by CI)
|
||||
|
||||
`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 "<Vendor>" # py -3 on Windows
|
||||
```
|
||||
|
||||
## 14. Per-extruder vectors (not checked) and variant arrays (widths and layout checked)
|
||||
|
||||
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).
|
||||
|
||||
`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
|
||||
([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): 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.
|
||||
|
||||
## 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 (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, 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. End with a
|
||||
verdict: can it merge as it stands?
|
||||
@@ -1,254 +0,0 @@
|
||||
# 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 <dir>` to work on a
|
||||
copy of the tree):
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/orca-profiles/scripts/shared_settings.py snapshot <before.json> # before any edit
|
||||
python3 .claude/skills/orca-profiles/scripts/shared_settings.py compare <before.json> # 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_<vendor>_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_<vendor>_<family>_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_<vendor>_<model>_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_<vendor>_<family>_common` → `fdm_<vendor>_<family>_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_<vendor>_common` | strategy: seam, wall order, infill and support patterns |
|
||||
| Printer family or variant layout | `fdm_process_<vendor>_<family>_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_<vendor>_<lh>_nozzle_<n>` (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_<material>` in OrcaFilamentLibrary, shared by every bundle | material defaults |
|
||||
| Product | `<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 "<Vendor>" --type machine
|
||||
python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "<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 <key>` 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_<vendor>_<family>_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 "<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.
|
||||
@@ -1,447 +0,0 @@
|
||||
# Validating profiles
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh # everything CI runs
|
||||
./scripts/check_profile.sh --vendor "<Vendor>" # development loop
|
||||
./scripts/check_profile.sh profile_tool validate_slice # named checks only
|
||||
```
|
||||
|
||||
```bat
|
||||
scripts\check_profile.bat :: the same three, on Windows
|
||||
scripts\check_profile.bat -Vendor "<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. 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 runs even after an earlier one fails; the script exits non-zero if any failed, and writes
|
||||
`logs/<check>.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 <a dir of your own>` 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 | `<cache dir>/validator/OrcaSlicer_profile_validator` |
|
||||
| macOS | `<cache dir>/validator/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator` |
|
||||
| Windows | `<cache dir>\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 <dir>` means a live run holds `<dir>/.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, 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 <tree + fixture> -l 2` | a shipped preset name that a past release offered no longer resolving |
|
||||
|
||||
**`-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.
|
||||
|
||||
**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.
|
||||
|
||||
### `validate_custom`: the backward-compatibility gate
|
||||
|
||||
It downloads one fixture archive per past release (v1.9.0 onwards) of *generated mock* user presets: a
|
||||
`<vendor>_<preset>_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, `<Vendor>/` 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`
|
||||
|
||||
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 `fix-variant`, `generate-id`, `normalize`,
|
||||
`trim` and `update-index`; [ids.md](ids.md#the-tool) has the writing half.
|
||||
|
||||
| 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 <field> 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` |
|
||||
|
||||
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
|
||||
(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 "<V>" in <dir>`, 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: <dir> at …/<dir>.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
|
||||
command that fixes the batch.
|
||||
- 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 keys
|
||||
|
||||
`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 <key>`); `normalize`
|
||||
removes them.
|
||||
|
||||
### Default-material references
|
||||
|
||||
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 `<extruder type> <nozzle volume type>` 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
|
||||
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 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` (case-insensitive), otherwise `machine_model`. That
|
||||
heuristic cannot reliably classify shared machine bases or unusually named variants.
|
||||
|
||||
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; 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 `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` 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/<config>` and `build*/*/src/<config>`), 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, point at it with `--validator` / `-Validator`, or set
|
||||
`ORCA_PROFILE_VALIDATOR` (`$env:ORCA_PROFILE_VALIDATOR` in PowerShell).
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `-p <dir>` | profile tree (also becomes the data dir) |
|
||||
| `-l <n>` | log level; CI uses 2 |
|
||||
| `-v <Vendor>` | load only that vendor **plus** OrcaFilamentLibrary |
|
||||
| `-s` | slice sweep |
|
||||
| `-o <dir>` | 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 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
|
||||
direct run, inspect `user/` and remove only empty directories created by that run; fixtures or
|
||||
pre-existing user files may be present.
|
||||
|
||||
## Checking a copy of the tree
|
||||
|
||||
Use `--profiles DIR` on the Python tool and `-p DIR` on the validator. The wrappers' `--profiles` /
|
||||
`-ProfilesDir` passes the tree to both, so one run validates a copy fully:
|
||||
|
||||
```bash
|
||||
./scripts/check_profile.sh --profiles "<tree>"
|
||||
# Windows: scripts\check_profile.bat -ProfilesDir "<tree>"
|
||||
```
|
||||
|
||||
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
|
||||
<validator> -p <scratch>/profiles -v "<Vendor>" -s -o <gcode dir>
|
||||
```
|
||||
|
||||
Each printer's G-code is saved as `<vendor name>__<printer>.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 `<data_dir>/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 |
|
||||
| --- | --- |
|
||||
| macOS | `~/Library/Application Support/OrcaSlicer` |
|
||||
| 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.
|
||||
|
||||
## Error → remedy
|
||||
|
||||
| Message | Fix |
|
||||
| --- | --- |
|
||||
| `can not find inherits <parent> for <preset>` | 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 <name>` | nothing in the chain declares one: run `generate-id` |
|
||||
| `can not find parent <name> for config <user preset>!` | a shipped name disappeared: add `renamed_from` |
|
||||
| `Failed loading configuration file <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 <key>: nil`, `Deserializing nil into a non-nullable object`); the lines above it name the cause |
|
||||
| `Missing instantiation attribute for <name>` | key absent **or** not the string `"true"` / `"false"` |
|
||||
| `contains incorrect keys: <keys>, which were removed` | a key valid for a different preset type |
|
||||
| `defines invalid printer variant "<v>"` | not a token of the model's `nozzle_diameter` list |
|
||||
| `has printer_variant "<v>" that does not match its nozzle_diameter` | [the set comparison](machine-profiles.md#printer_model-and-printer_variant) |
|
||||
| `references unknown compatible_printers "<p>"` | 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 "<old>" (now "<new>")` | in-tree references must name the current preset; `renamed_from` does not excuse them |
|
||||
| `Filament preset "<f>" 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 <V>.json list references it, so it never loads` | `update-index`, or delete the file |
|
||||
| `[ERROR] … no <V>.json list references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` |
|
||||
| `[ERROR] … normalize would <change>` / `<V>.json: update-index would rebuild <lists>` | run that command and commit the result |
|
||||
| `[ERROR] <V> has N <type> profiles named "<name>"` | identify the intended preset and remove or rename the duplicate; use `trim --dry-run` only for deliberate unindexed-file cleanup |
|
||||
| `Duplicate key error in <file>: Duplicate key detected: <key>` | 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 <file> 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) |
|
||||
| `"<key>" has N values for variant length S at stride k, which takes M` (with ` (no <list key>, so …)` after `S`, naming where `S` came from, when the preset writes no list, and ` (it comes from <file>)` 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)) |
|
||||
| `<key> <where> 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, `<extruder type> <nozzle volume type>` 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 |
|
||||
| `<key> 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: <dir>` | 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 "<n>" in its "default_materials"` / `… "default_filament_profile"` | name an existing system (not user, not base) filament exactly; `;` separators, exact case |
|
||||
| `Missing filament profile: '<n>' referenced in <file>` | 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 "<n>" is declared by N bundles` | model names are unique across the tree; rename the new model |
|
||||
| `vendor <V>'s config version: <s> 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 "<p>" 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 "<p>" 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"**, 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
|
||||
(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 `<!-- profile-validation-comment -->`, 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 PR limited to their own `resources/profiles/<Vendor>/` 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.
|
||||
@@ -1,214 +0,0 @@
|
||||
# Vendor bundles
|
||||
|
||||
A bundle is `resources/profiles/<Vendor>.json` (the index) plus `resources/profiles/<Vendor>/`. 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`.
|
||||
|
||||
## The index
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Phrozen",
|
||||
"version": "02.04.00.03",
|
||||
"force_update": "0",
|
||||
"description": "Phrozen configurations",
|
||||
"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 the profile updater, never by the loader. `sub_path` is relative to the
|
||||
**vendor folder**.
|
||||
|
||||
| List | Holds |
|
||||
| --- | --- |
|
||||
| `machine_model_list` | `machine_model` records (the printer product) |
|
||||
| `machine_list` | printer variants **and** shared machine bases |
|
||||
| `process_list` | selectable processes **and** shared process bases |
|
||||
| `filament_list` | selectable filaments **and** shared filament bases |
|
||||
|
||||
### Three registration rules
|
||||
|
||||
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, 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 <parent> for <child>` 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, 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 misspelled key
|
||||
(`"subpath"`), is silently dropped. (A misspelled `sub_path` value is a `check` error naming the entry.)
|
||||
|
||||
`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`
|
||||
|
||||
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 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 <V>'s config version: <s>
|
||||
invalid`).
|
||||
|
||||
## Common preset keys
|
||||
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| `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"` 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
|
||||
[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 the validator gates: a missing key or any value other than the
|
||||
strings `"true"` / `"false"` is a counted error (`Missing instantiation attribute for <name>`) 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` and `include`
|
||||
|
||||
`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.
|
||||
|
||||
`"include": ["<name>", …]` (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"`: 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; 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 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*, so a neighbour's `renamed_from` is no model.
|
||||
|
||||
## Failure scopes
|
||||
|
||||
| Scope | Cause |
|
||||
| --- | --- |
|
||||
| **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 <key>`); 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 <key>`): 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 (`unexpected end of input`), not "file
|
||||
not found".
|
||||
|
||||
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: <name>
|
||||
in vendor: <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`** 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
|
||||
([naming.md](naming.md)).
|
||||
2. `resources/profiles/<Vendor>.json`: `name`, `version` (`01.00.00.00`), `force_update: "0"`,
|
||||
`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: `<Vendor>/machine/fdm_machine_common.json` and
|
||||
`<Vendor>/process/fdm_process_common.json`, both `"instantiation": "false"` with no `setting_id`. For
|
||||
a Klipper printer add your own `<Vendor>/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 `<Model>_cover.png`, all directly in `<Vendor>/`. 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
|
||||
`<the name you gave the model in machine_model_list>_cover.png`.
|
||||
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#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.
|
||||
|
||||
## `resources/profiles_template/`
|
||||
|
||||
A separate tree (`Template.json` + `Template/`) holding filament and process templates. It is **not** a
|
||||
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.
|
||||
@@ -1,241 +0,0 @@
|
||||
#!/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 <repo>/scripts; this file in <repo>/.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()
|
||||
@@ -1,12 +1,2 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Shell scripts are run by Git Bash on Windows CI, which cannot read a script
|
||||
# with CRLF line endings: it fails on the first line. Windows checkouts default
|
||||
# to core.autocrlf=true, so keep these LF whatever the platform.
|
||||
*.sh text eol=lf
|
||||
|
||||
# Batch files are read by cmd.exe, which tracks a byte offset into the file to
|
||||
# resume after `call :label`. With LF endings that offset can land wrong and the
|
||||
# label lookup fails, so keep these CRLF whatever the platform.
|
||||
*.bat text eol=crlf
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Something behaves incorrectly while Orca Slicer keeps running
|
||||
description: File a bug report
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -10,8 +10,6 @@ body:
|
||||
Please note that this is not the place to make feature requests or ask for help.
|
||||
For this, please use the [Feature request](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=feature_request.yml) issue type or you can discuss your idea on our [Discord server](https://discord.gg/P4VE9UY9gJ) with others.
|
||||
|
||||
If Orca Slicer closes on its own, freezes or stops responding, please use the [Crash report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=crash_report.yml) form instead. It asks for the logs a crash needs.
|
||||
|
||||
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
@@ -34,22 +32,14 @@ body:
|
||||
attributes:
|
||||
label: OrcaSlicer Version
|
||||
description: Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`.
|
||||
placeholder: e.g. 2.5.0
|
||||
placeholder: e.g. 1.9.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: working_version
|
||||
attributes:
|
||||
label: Regression compared to a previous version
|
||||
description: Did it work in a previous version?
|
||||
placeholder: e.g. 2.3.2
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: os_type
|
||||
attributes:
|
||||
label: "Operating System (OS)"
|
||||
description: "What OSes are you experiencing issues on?"
|
||||
description: "What OSes are you are experiencing issues on?"
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
@@ -88,7 +78,7 @@ body:
|
||||
id: reproduce_steps
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: Please describe the detailed steps to reproduce this issue
|
||||
description: Please described the detailed steps to reproduce this issue
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
@@ -110,23 +100,28 @@ body:
|
||||
description: What should happen after the above steps?
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
id: file_required
|
||||
attributes:
|
||||
value: |
|
||||
Please be sure to add the following files:
|
||||
* Please upload a ZIP archive containing the **project file** used when the problem arise. Please export it just before or after the problem occurs. Even if you did nothing and/or there is no object, export it! (We need the configurations in project file).
|
||||
You can export the project file from the application menu in `File`->`Save project as...`, then zip it
|
||||
* A **log file** for crashes and similar issues.
|
||||
You can find your log file here:
|
||||
Windows: `%APPDATA%\OrcaSlicer\log` or usually `C:\Users\<your username>\AppData\Roaming\OrcaSlicer\log`
|
||||
MacOS: `$HOME/Library/Application Support/OrcaSlicer/log`
|
||||
Linux: `$HOME/.config/OrcaSlicer/log`
|
||||
If Orca Slicer still starts, you can also reach this directory from the application menu in `Help` -> `Show Configuration Folder`
|
||||
You can zip the log directory, or just select the newest logs when this issue happens, and zip them
|
||||
- type: textarea
|
||||
id: file_uploads
|
||||
attributes:
|
||||
label: Project file & Debug log uploads
|
||||
description: |
|
||||
Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB.
|
||||
|
||||
* The **project file** used when the problem happened, zipped. Export it just before or after the problem occurs. Even if you did nothing and there is no object on the plate, export it, since we need the configuration it carries. `File` -> `Save project as...`
|
||||
* The **log folder**, zipped. `Help` -> `Show Configuration Folder` opens it, or find it at:
|
||||
* Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\<you>\AppData\Roaming\OrcaSlicer\log`
|
||||
* macOS: `$HOME/Library/Application Support/OrcaSlicer/log`
|
||||
* Linux: `$HOME/.config/OrcaSlicer/log`
|
||||
* Flatpak: `$HOME/.var/app/com.orcaslicer.OrcaSlicer/config/OrcaSlicer/log`
|
||||
* If the zip comes out over 25 MB, attach the newest logs from that folder on their own instead.
|
||||
description: Drop the project file and debug log here
|
||||
placeholder: |
|
||||
Zipped project file
|
||||
Zipped log folder
|
||||
Project File: `File` -> `Save project as...` then zip it & drop it here
|
||||
Log File: `Help` -> `Show Configuration Folder`, then zip the log directory, or just select the newest logs in `log` when this issue happens and zip them, then drop the zip file here
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
@@ -141,5 +136,7 @@ body:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
name: 💥 Crash Report
|
||||
description: Orca Slicer closes on its own, freezes or stops responding
|
||||
labels: ["crash"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for taking the time to report a crash.**
|
||||
|
||||
Use this form when Orca Slicer closes on its own, freezes, or stops responding.
|
||||
If the application stays open and only produces a wrong result, please use the [Bug report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=bug_report.yml) form instead.
|
||||
A printer whose toolhead collides with the print is also a bug report rather than a crash, since the application itself did not stop.
|
||||
|
||||
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is this crash reproducible in the latest nightly build?
|
||||
description: >
|
||||
Please verify this crash still happens in the latest nightly build first. It may already be fixed there:
|
||||
[Nightly builds](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds).
|
||||
options:
|
||||
- label: I have checked the latest nightly build and the crash is still reproducible
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this crash?
|
||||
description: Please search to see if an issue already exists for the crash you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: OrcaSlicer Version
|
||||
description: Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`.
|
||||
placeholder: e.g. 2.5.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: working_version
|
||||
attributes:
|
||||
label: Regression compared to a previous version
|
||||
description: Did it work in a previous version?
|
||||
placeholder: e.g. 2.3.2
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: os_type
|
||||
attributes:
|
||||
label: "Operating System (OS)"
|
||||
description: "What OSes are you seeing the crash on?"
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os_version
|
||||
attributes:
|
||||
label: "OS Version"
|
||||
description: "What OS version does this relate to?"
|
||||
placeholder: "i.e. OS: Windows 7/8/10/11 ..., Ubuntu 22.04/Fedora 36 ..., macOS 10.15/11.1/12.3 ..."
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: printer
|
||||
attributes:
|
||||
label: Printer
|
||||
description: Which printer was selected
|
||||
placeholder: Voron 2.4/VzBot/Prusa MK4/Bambu Lab X1 series/Bambu Lab P1P/...
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: crash_moment
|
||||
attributes:
|
||||
label: When does the crash happen?
|
||||
description: Pick the point where Orca Slicer stops working.
|
||||
options:
|
||||
- Not sure
|
||||
- On startup, before the main window appears
|
||||
- When opening or importing a project or model
|
||||
- While changing printer, filament or process settings
|
||||
- While slicing
|
||||
- In the 3D view, Preview or Assembly view
|
||||
- When exporting G-code or sending a print to the printer
|
||||
- On the Device tab, or connecting to a printer (camera, sync, login)
|
||||
- While using a specific tool, dialog or calibration
|
||||
- After resuming from sleep or changing monitors
|
||||
- When closing the application
|
||||
- No clear pattern
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: crash_frequency
|
||||
attributes:
|
||||
label: How often does it happen?
|
||||
options:
|
||||
- Not sure
|
||||
- Every time
|
||||
- Often, but not every time
|
||||
- Rarely
|
||||
- It only happened once
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: fresh_config
|
||||
attributes:
|
||||
label: Does it still crash with a fresh configuration?
|
||||
description: >
|
||||
Close Orca Slicer and rename your configuration folder (`%APPDATA%\OrcaSlicer` on Windows,
|
||||
`$HOME/Library/Application Support/OrcaSlicer` on macOS, `$HOME/.config/OrcaSlicer` on Linux),
|
||||
then start it again. Renaming keeps your settings, so you can put the folder back afterwards.
|
||||
options:
|
||||
- I have not tried this
|
||||
- Yes, it still crashes
|
||||
- No, the crash goes away
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce_steps
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: Please describe the detailed steps that lead to the crash.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. Orca Slicer closes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system_info
|
||||
attributes:
|
||||
label: Additional system information
|
||||
description: >
|
||||
Display card and driver version are worth adding for crashes on startup or in the 3D view.
|
||||
CPU and memory are worth adding for crashes while slicing.
|
||||
placeholder: |
|
||||
CPU: 11th gen Intel r core tm i7-1185g7/AMD Ryzen 7 6800h/...
|
||||
Memory: 32/16 GB...
|
||||
Display Card: NVIDIA Quadro P400/...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: file_uploads
|
||||
attributes:
|
||||
label: Project file, logs and crash report uploads
|
||||
description: |
|
||||
A crash report without logs usually cannot be acted on. Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB.
|
||||
|
||||
* The **project file** used when the crash happened, zipped. Export it just before or after the crash, even if the plate is empty, since we need the configuration it carries. `File` -> `Save project as...`
|
||||
* The whole **log folder**, zipped rather than single files picked out of it. `Help` -> `Show Configuration Folder` opens it, or find it at:
|
||||
* Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\<you>\AppData\Roaming\OrcaSlicer\log`
|
||||
* macOS: `$HOME/Library/Application Support/OrcaSlicer/log`
|
||||
* Linux: `$HOME/.config/OrcaSlicer/log`
|
||||
* Flatpak: `$HOME/.var/app/com.orcaslicer.OrcaSlicer/config/OrcaSlicer/log`
|
||||
* On Windows the crash itself is written to a separate `crash_*.log` in there, and that is the file we need most. If the zip comes out over 25 MB GitHub will refuse it, so attach the newest log and any `crash_*.log` on their own instead.
|
||||
* The **operating system crash report**, on macOS and Linux, where Orca Slicer cannot write its own crash log. It is often the only record of where it died:
|
||||
* macOS: Console.app -> Crash Reports, or `$HOME/Library/Logs/DiagnosticReports/`. The file starts with `OrcaSlicer` and ends in `.ips`. Zip it before attaching, GitHub does not accept `.ips` files.
|
||||
* Linux: run `orca-slicer` from a terminal (Flatpak: `flatpak run com.orcaslicer.OrcaSlicer`) and paste everything it prints when it dies. On systemd systems `coredumpctl info orca-slicer` gives a backtrace.
|
||||
placeholder: |
|
||||
Zipped project file
|
||||
Zipped log folder
|
||||
Zipped macOS .ips crash report, or the terminal output on Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: file_checklist
|
||||
attributes:
|
||||
label: Checklist of files to include
|
||||
options:
|
||||
- label: Log folder
|
||||
- label: Project file
|
||||
- label: Operating system crash report (macOS and Linux)
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Screenshots? References? Anything that will give us more context about the crash you are encountering!
|
||||
validations:
|
||||
required: false
|
||||
+18
-206
@@ -14,10 +14,6 @@ on:
|
||||
- 'localization/**'
|
||||
- 'resources/**'
|
||||
- ".github/workflows/build_*.yml"
|
||||
- ".github/workflows/unit_tests*.yml"
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
- 'scripts/msix/**'
|
||||
- 'tests/**'
|
||||
@@ -33,12 +29,10 @@ on:
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
- ".github/workflows/build_*.yml"
|
||||
- ".github/workflows/unit_tests*.yml"
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'build_release_vs.bat'
|
||||
- 'build_release_vs2022.bat'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
- 'scripts/msix/**'
|
||||
- 'tests/**'
|
||||
@@ -60,22 +54,6 @@ concurrency:
|
||||
|
||||
|
||||
jobs:
|
||||
# build_win.bat ships a test suite. Run it before the Windows builds.
|
||||
check_build_script:
|
||||
name: Windows build script tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: 'false'
|
||||
|
||||
# Windows PowerShell rather than pwsh: the suite drives build_win.bat
|
||||
# through cmd, and the two differ in how they quote native arguments.
|
||||
- name: Run the build script test suite
|
||||
shell: powershell
|
||||
run: .\scripts\test_build_win.ps1
|
||||
|
||||
build_linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -101,16 +79,14 @@ jobs:
|
||||
# SELF_HOSTED skips arm64 (the self-hosted Windows server is x64-only).
|
||||
matrix:
|
||||
include: ${{ fromJSON(vars.SELF_HOSTED
|
||||
&& '[{"arch":"x64","os":"orca-win-server","compiler":"clang"}]'
|
||||
|| '[{"arch":"x64","os":"windows-latest","compiler":"clang"},{"arch":"arm64","os":"windows-11-vs2026-arm","compiler":"clang"}]') }}
|
||||
needs: check_build_script
|
||||
&& '[{"arch":"x64","os":"orca-win-server"}]'
|
||||
|| '[{"arch":"x64","os":"windows-latest"},{"arch":"arm64","os":"windows-11-arm"}]') }}
|
||||
# Don't run scheduled builds on forks:
|
||||
if: ${{ !cancelled() && needs.check_build_script.result == 'success' && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
|
||||
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
|
||||
uses: ./.github/workflows/build_check_cache.yml
|
||||
with:
|
||||
os: ${{ matrix.os }}
|
||||
arch: ${{ matrix.arch }}
|
||||
compiler: ${{ matrix.compiler }}
|
||||
build-deps-only: ${{ inputs.build-deps-only || false }}
|
||||
force-build: ${{ github.event_name == 'schedule' }}
|
||||
secrets: inherit
|
||||
@@ -171,7 +147,7 @@ jobs:
|
||||
if: ${{ !cancelled() && success() && !vars.SELF_HOSTED }}
|
||||
uses: ./.github/workflows/unit_tests.yml
|
||||
with:
|
||||
os: windows-11-vs2026-arm
|
||||
os: windows-11-arm
|
||||
artifact: ${{ github.sha }}-tests-windows-arm64
|
||||
test-dir: build-arm64/tests
|
||||
unit_tests_macos_arm64:
|
||||
@@ -183,11 +159,9 @@ jobs:
|
||||
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
|
||||
artifact: ${{ github.sha }}-tests-macos-arm64
|
||||
test-dir: build/arm64/tests
|
||||
# Slice a two-colour cube through every shipped printer, and through every
|
||||
# system process/filament whose templates no printer's own slice reaches, so
|
||||
# every custom g-code and filename_format shipped is expanded (names in {if}
|
||||
# branches not taken included) - catches slicing regressions the static
|
||||
# profile checks and unit tests can't see.
|
||||
# Slice a two-colour cube through every shipped printer so all custom g-code
|
||||
# (change_filament_gcode, machine start/end, etc.) is expanded - catches
|
||||
# slicing regressions the static profile checks and unit tests can't see.
|
||||
# Profile-only PRs are covered by check_profiles.yml's nightly binary; this
|
||||
# covers src/engine PRs with the PR-built binary.
|
||||
slice_check_linux:
|
||||
@@ -211,7 +185,7 @@ jobs:
|
||||
./validator-bin/OrcaSlicer_profile_validator -p "${{ github.workspace }}/resources/profiles" -s -l 2
|
||||
publish_test_results:
|
||||
name: Publish Test Results
|
||||
needs: [unit_tests_linux_x86_64, unit_tests_linux_aarch64, unit_tests_windows_x64, unit_tests_windows_arm64, unit_tests_macos_arm64, unit_tests_flatpak_x86_64, unit_tests_flatpak_aarch64]
|
||||
needs: [unit_tests_linux_x86_64, unit_tests_linux_aarch64, unit_tests_windows_x64, unit_tests_windows_arm64, unit_tests_macos_arm64]
|
||||
if: ${{ !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -283,154 +257,38 @@ jobs:
|
||||
echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
|
||||
echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
- name: Compute the flatpak-builder cache key
|
||||
id: fp_cache_key
|
||||
run: echo "key=flatpak-builder-${{ matrix.variant.arch }}-${{ hashFiles('deps/**', 'scripts/flatpak/com.orcaslicer.OrcaSlicer.yml', 'scripts/flatpak/make_deps_tar.sh') }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
# Manage flatpak-builder cache externally so PRs restore but never upload.
|
||||
# The compiler cache under it is keyed per run below, so it is left out.
|
||||
# Manage flatpak-builder cache externally so PRs restore but never upload
|
||||
- name: Restore flatpak-builder cache
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: |
|
||||
.flatpak-builder/*
|
||||
!.flatpak-builder/ccache
|
||||
key: ${{ steps.fp_cache_key.outputs.key }}
|
||||
path: .flatpak-builder
|
||||
key: flatpak-builder-${{ matrix.variant.arch }}-${{ github.event.pull_request.base.sha }}
|
||||
restore-keys: flatpak-builder-${{ matrix.variant.arch }}-
|
||||
- name: Save/restore flatpak-builder cache
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
.flatpak-builder/*
|
||||
!.flatpak-builder/ccache
|
||||
key: ${{ steps.fp_cache_key.outputs.key }}
|
||||
path: .flatpak-builder
|
||||
key: flatpak-builder-${{ matrix.variant.arch }}-${{ github.sha }}
|
||||
restore-keys: flatpak-builder-${{ matrix.variant.arch }}-
|
||||
# Compiler cache for the OrcaSlicer module, as in build_orca.yml. Pull
|
||||
# requests only restore it; every other run (main, release branches, the
|
||||
# nightly, a dispatch) saves it. orca_deps stays on the state cache above.
|
||||
- name: Name the compiler cache leg
|
||||
run: |
|
||||
leg="Flatpak-${{ matrix.variant.arch }}"
|
||||
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
- name: Restore compiler cache
|
||||
id: ccache_restore
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: .flatpak-builder/ccache
|
||||
key: ${{ env.CCACHE_ENTRY }}
|
||||
restore-keys: ccache-${{ env.CCACHE_LEG }}-
|
||||
- name: Disable debug info for faster CI builds
|
||||
run: |
|
||||
sed -i '/^build-options:/a\ no-debuginfo: true\n strip: true' \
|
||||
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
shell: bash
|
||||
# flatpak-builder reuses a module from its cache when the definition and
|
||||
# sources are unchanged, so a re-run of the same commit would skip the
|
||||
# OrcaSlicer module and ship no test asset. A per-run value in that module's
|
||||
# env keeps it rebuilding; orca_deps stays cached, and the compiler cache
|
||||
# still serves the rebuild. run-tests on the same module builds the test
|
||||
# binaries.
|
||||
- name: Inject commit hash, run-tests and cache buster into Flatpak manifest
|
||||
env:
|
||||
flatpak_builder_cache_buster: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
- name: Inject git commit hash into Flatpak manifest
|
||||
run: |
|
||||
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n run-tests: true\n build-options:\n env:\n flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\n git_commit_hash: \"$git_commit_hash\"|}" \
|
||||
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n git_commit_hash: \"$git_commit_hash\"|}" \
|
||||
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
shell: bash
|
||||
# flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds
|
||||
# with clang, so CMake's launcher runs ccache instead; --ccache is still what
|
||||
# mounts the cache directory into the sandbox. The settings go into that
|
||||
# directory's own config file, which the sandbox reads too.
|
||||
- name: Enable compiler cache
|
||||
run: |
|
||||
printf ' %s\n' \
|
||||
'CMAKE_C_COMPILER_LAUNCHER: ccache' \
|
||||
'CMAKE_CXX_COMPILER_LAUNCHER: ccache' > "$RUNNER_TEMP/ccache-env.yml"
|
||||
sed -i "/^ git_commit_hash: /r $RUNNER_TEMP/ccache-env.yml" \
|
||||
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
grep -q '^ CMAKE_CXX_COMPILER_LAUNCHER: ccache$' scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
mkdir -p .flatpak-builder/ccache
|
||||
export CCACHE_DIR=$PWD/.flatpak-builder/ccache
|
||||
ccache --set-config=max_size=3G
|
||||
# The compiler is reinstalled every run, so its mtime means nothing.
|
||||
ccache --set-config=compiler_check=content
|
||||
# Headers a fresh checkout has just written, the few files that use
|
||||
# __DATE__ or __TIME__, and the precompiled header, whose macros ccache
|
||||
# cannot see.
|
||||
ccache --set-config=sloppiness=pch_defines,time_macros,include_file_mtime,include_file_ctime
|
||||
# Hash the includes the compiler reports instead of preprocessing every
|
||||
# miss before compiling it.
|
||||
ccache --set-config=depend_mode=true
|
||||
# The restored directory carries the previous run's counters.
|
||||
ccache -z
|
||||
shell: bash
|
||||
- name: Check the manifest keeps orca_deps cacheable
|
||||
run: ./scripts/flatpak/check_manifest_cacheable.sh
|
||||
shell: bash
|
||||
- name: Pack deps/ for the Flatpak manifest
|
||||
run: ./scripts/flatpak/make_deps_tar.sh
|
||||
shell: bash
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@master
|
||||
with:
|
||||
bundle: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
|
||||
manifest-path: scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
|
||||
# cache only turns on flatpak-builder --ccache; the caching itself is above.
|
||||
cache: true
|
||||
restore-cache: false
|
||||
save-cache: false
|
||||
cache: false
|
||||
arch: ${{ matrix.variant.arch }}
|
||||
upload-artifact: false
|
||||
keep-build-dirs: true
|
||||
# The build has just touched everything it can use, so an object untouched
|
||||
# for a week is dead, usually orphaned by a flag change.
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: |
|
||||
export CCACHE_DIR=$PWD/.flatpak-builder/ccache
|
||||
ccache --evict-older-than 7d
|
||||
ccache -s -v || ccache -s
|
||||
shell: bash
|
||||
# Save the new entry first, then drop the older ones for this leg on this
|
||||
# ref, so a failed save leaves the previous entry in place. A cancelled or
|
||||
# failed build saves too, since what it compiled is still valid; a restore
|
||||
# that did not finish does not, since the directory may be a truncated copy.
|
||||
- name: Save compiler cache
|
||||
id: ccache_save
|
||||
if: ${{ always() && steps.ccache_restore.outcome == 'success' && github.event_name != 'pull_request' }}
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: .flatpak-builder/ccache
|
||||
key: ${{ env.CCACHE_ENTRY }}
|
||||
- name: Drop older compiler cache entries
|
||||
if: ${{ always() && steps.ccache_save.outcome == 'success' }}
|
||||
# The container has no gh, so this is the list and delete over the REST API.
|
||||
# Older means a lower run id, so two runs finishing close together keep
|
||||
# the newer entry whichever of them cleans up last.
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches"
|
||||
# The save step reports success even when its tar failed, so keep
|
||||
# the older entries unless the new one is listed.
|
||||
if ! curl -sSf -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"$api?ref=$GITHUB_REF&key=$CCACHE_ENTRY" \
|
||||
| jq -e --arg entry "$CCACHE_ENTRY" 'any(.actions_caches[]; .key == $entry)' > /dev/null; then
|
||||
echo "$CCACHE_ENTRY was not saved; keeping the older entries."
|
||||
exit 0
|
||||
fi
|
||||
curl -sSf -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"$api?ref=$GITHUB_REF&key=ccache-$CCACHE_LEG-&per_page=100" \
|
||||
| jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \
|
||||
'.actions_caches[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \
|
||||
| while read -r id; do
|
||||
curl -sSf -X DELETE -H "Authorization: Bearer $GH_TOKEN" "$api/$id"
|
||||
done
|
||||
shell: bash
|
||||
- name: Upload artifacts Flatpak
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -446,49 +304,3 @@ jobs:
|
||||
asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
|
||||
# The asset is /app (the exes link it at runtime) plus the build tree
|
||||
# slimmed to what ctest needs.
|
||||
- name: Package flatpak test asset
|
||||
shell: bash
|
||||
run: |
|
||||
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* | tail -1)
|
||||
find "$d/build_flatpak" -mindepth 1 -maxdepth 1 ! -name tests -exec rm -rf {} +
|
||||
# Strip debug info (the SDK builds with -g, only the app gets stripped);
|
||||
# the bounds checks are compiled in, so a stripped exe still catches them.
|
||||
find "$d/build_flatpak/tests" -type f -perm -u+x -exec strip --strip-unneeded {} + 2>/dev/null || true
|
||||
# At runtime the tests read tests/ (TEST_DATA_DIR), scripts/, and under
|
||||
# resources/ the shipped profiles (PROFILES_DIR), the printers/ maps, and
|
||||
# the icon SVGs (NativeCommands icon names are checked against them).
|
||||
find "$d" -mindepth 1 -maxdepth 1 -type d \
|
||||
! -name tests ! -name build_flatpak ! -name scripts ! -name resources -exec rm -rf {} +
|
||||
find "$d/resources" -mindepth 1 -maxdepth 1 ! -name profiles ! -name printers ! -name images -exec rm -rf {} +
|
||||
# Only the SVGs are read; the png/ico/icns/gif assets are ~35MB of dead weight.
|
||||
find "$d/resources/images" -mindepth 1 -maxdepth 1 ! -name '*.svg' -exec rm -rf {} + 2>/dev/null || true
|
||||
tar -cf flatpak-test-asset.tar flatpak_app "$d"
|
||||
- name: Upload flatpak test asset
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ github.sha }}-flatpak-tests-${{ matrix.variant.arch }}
|
||||
path: flatpak-test-asset.tar
|
||||
retention-days: 1
|
||||
# keep-build-dirs would otherwise land in the flatpak-builder cache saved post-job.
|
||||
- name: Drop the kept build dirs before the flatpak-builder cache saves
|
||||
if: always()
|
||||
shell: bash
|
||||
run: rm -rf .flatpak-builder/build
|
||||
unit_tests_flatpak_x86_64:
|
||||
name: Flatpak x86_64
|
||||
needs: flatpak
|
||||
if: ${{ !cancelled() && success() }}
|
||||
uses: ./.github/workflows/unit_tests_flatpak.yml
|
||||
with:
|
||||
os: ubuntu-24.04
|
||||
artifact: ${{ github.sha }}-flatpak-tests-x86_64
|
||||
unit_tests_flatpak_aarch64:
|
||||
name: Flatpak aarch64
|
||||
needs: flatpak
|
||||
if: ${{ !cancelled() && success() }}
|
||||
uses: ./.github/workflows/unit_tests_flatpak.yml
|
||||
with:
|
||||
os: ubuntu-24.04-arm
|
||||
artifact: ${{ github.sha }}-flatpak-tests-aarch64
|
||||
|
||||
@@ -9,10 +9,6 @@ on:
|
||||
arch:
|
||||
required: false
|
||||
type: string
|
||||
compiler:
|
||||
required: false
|
||||
type: string
|
||||
default: msvc
|
||||
build-deps-only:
|
||||
required: false
|
||||
type: boolean
|
||||
@@ -37,12 +33,11 @@ jobs:
|
||||
- name: set outputs
|
||||
id: set_outputs
|
||||
env:
|
||||
# Anything that changes how the tree is built belongs in the key, or a job
|
||||
# restores one it cannot use. Linux amd64 passes no arch deliberately, so
|
||||
# 'linux-clang' keeps the cache it already has.
|
||||
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
|
||||
# The Windows ARM64 deps build in build-arm64, all others under build;
|
||||
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
|
||||
# Keep macOS/Windows cache keys architecture-specific. amd64 Linux passes
|
||||
# no arch (key stays 'linux-clang', preserving the existing cache);
|
||||
# aarch64 gets its own 'linux-clang-aarch64' key.
|
||||
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}', inputs.arch) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
|
||||
# ARM64 builds use the build-arm64 tree (see build_release_vs.bat); x64/other use build.
|
||||
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
|
||||
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
|
||||
run: |
|
||||
@@ -67,7 +62,6 @@ jobs:
|
||||
valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' }}
|
||||
os: ${{ inputs.os }}
|
||||
arch: ${{ inputs.arch }}
|
||||
compiler: ${{ inputs.compiler }}
|
||||
build-deps-only: ${{ inputs.build-deps-only }}
|
||||
force-build: ${{ inputs.force-build }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -16,10 +16,6 @@ on:
|
||||
arch:
|
||||
required: false
|
||||
type: string
|
||||
compiler:
|
||||
required: false
|
||||
type: string
|
||||
default: msvc
|
||||
build-deps-only:
|
||||
required: false
|
||||
type: boolean
|
||||
@@ -138,11 +134,14 @@ jobs:
|
||||
if (-not "${{ vars.SELF_HOSTED }}") {
|
||||
choco install strawberryperl
|
||||
}
|
||||
# cache-path is the install directory inside the deps build directory.
|
||||
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
|
||||
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
|
||||
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
|
||||
.\build_win.bat -d --arch ${{ inputs.arch }} --deps-dir $deps @flags
|
||||
$arch = "${{ inputs.arch }}"
|
||||
if ($arch -eq "arm64") {
|
||||
.\build_release_vs.bat deps arm64
|
||||
.\build_release_vs.bat pack arm64
|
||||
} else {
|
||||
.\build_release_vs.bat deps
|
||||
.\build_release_vs.bat pack
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Build on Mac ${{ inputs.arch }}
|
||||
@@ -150,9 +149,9 @@ jobs:
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
if [ -z "${{ vars.SELF_HOSTED }}" ]; then
|
||||
brew install automake texinfo libtool pkgconf yasm nasm
|
||||
brew install automake texinfo libtool
|
||||
fi
|
||||
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15
|
||||
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15
|
||||
(cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \
|
||||
find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +)
|
||||
|
||||
@@ -205,5 +204,4 @@ jobs:
|
||||
cache-path: ${{ inputs.cache-path }}
|
||||
os: ${{ inputs.os }}
|
||||
arch: ${{ inputs.arch }}
|
||||
compiler: ${{ inputs.compiler }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -13,10 +13,6 @@ on:
|
||||
arch:
|
||||
required: false
|
||||
type: string
|
||||
compiler:
|
||||
required: false
|
||||
type: string
|
||||
default: msvc
|
||||
macos-combine-only:
|
||||
required: false
|
||||
type: boolean
|
||||
@@ -76,69 +72,6 @@ jobs:
|
||||
if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" }
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin
|
||||
|
||||
# Compiler cache. Pushes save it, so main keeps it warm; pull requests
|
||||
# restore it and discard what they compiled. Objects are keyed on the
|
||||
# preprocessed source, the compiler and the flags, so a leg only ever
|
||||
# hits its own entries. A failed install costs the caching, not the build.
|
||||
- name: Name the compiler cache leg
|
||||
if: ${{ !inputs.macos-combine-only }}
|
||||
shell: bash
|
||||
run: |
|
||||
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
|
||||
# clang-cl refuses a precompiled header from another cl.exe build and ccache
|
||||
# does not hash that build, so each one gets its own cache. The build number
|
||||
# is read from cl.exe itself; the toolset directory keeps its name across patches.
|
||||
if [ "${{ runner.os }}" = Windows ]; then
|
||||
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
|
||||
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
|
||||
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
|
||||
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
|
||||
fi
|
||||
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
|
||||
|
||||
# The action only installs and configures ccache. Restore and save go
|
||||
# through actions/cache with one path string, since the cache service
|
||||
# only matches entries saved under the identical path and the action
|
||||
# spells it differently on Windows.
|
||||
- name: Compiler cache
|
||||
id: ccache
|
||||
if: ${{ !inputs.macos-combine-only }}
|
||||
continue-on-error: true
|
||||
uses: hendrikmuhs/ccache-action@v1.2.24
|
||||
with:
|
||||
key: ${{ env.CCACHE_LEG }}
|
||||
max-size: 3G
|
||||
restore: false
|
||||
save: false
|
||||
# ccache -s runs as its own step; no summary table per job.
|
||||
job-summary: ''
|
||||
|
||||
- name: Restore compiler cache
|
||||
id: ccache_restore
|
||||
if: ${{ steps.ccache.outcome == 'success' }}
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ${{ env.CCACHE_ENTRY }}
|
||||
restore-keys: ccache-${{ env.CCACHE_LEG }}-
|
||||
|
||||
- name: Enable compiler cache
|
||||
if: ${{ steps.ccache.outcome == 'success' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CMAKE_C_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV"
|
||||
echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV"
|
||||
# Headers a fresh checkout has just written, the few files that
|
||||
# use __DATE__ or __TIME__, and the precompiled header, whose
|
||||
# macros ccache cannot see.
|
||||
echo "CCACHE_SLOPPINESS=pch_defines,time_macros,include_file_mtime,include_file_ctime" >> "$GITHUB_ENV"
|
||||
# Hash the includes the compiler reports instead of preprocessing
|
||||
# every miss before compiling it.
|
||||
echo "CCACHE_DEPEND=1" >> "$GITHUB_ENV"
|
||||
# The restored directory carries the previous run's counters.
|
||||
ccache -z
|
||||
|
||||
- name: Get the version and date on Ubuntu and macOS
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
@@ -212,7 +145,7 @@ jobs:
|
||||
env:
|
||||
ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }}
|
||||
run: |
|
||||
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
|
||||
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
|
||||
|
||||
- name: Pack unit tests mac
|
||||
if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64'
|
||||
@@ -229,14 +162,6 @@ jobs:
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build system preset cache (macOS)
|
||||
if: runner.os == 'macOS' && !inputs.macos-combine-only
|
||||
working-directory: ${{ github.workspace }}
|
||||
shell: bash
|
||||
# The bundle was already packed from resources/, so the caches have to be
|
||||
# installed into it here; the source tree keeps its JSONs for later jobs.
|
||||
run: ./scripts/build_preset_cache.sh -b build/${{ inputs.arch }} build/${{ inputs.arch }}/OrcaSlicer/OrcaSlicer.app/Contents/Resources/profiles
|
||||
|
||||
- name: Pack macOS app bundle ${{ inputs.arch }}
|
||||
if: runner.os == 'macOS' && !inputs.macos-combine-only
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -271,7 +196,7 @@ jobs:
|
||||
if: runner.os == 'macOS' && inputs.macos-combine-only
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a universal -t 10.15
|
||||
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15
|
||||
|
||||
# Thanks to RaySajuuk, it's working now
|
||||
- name: Sign app and notary
|
||||
@@ -448,26 +373,9 @@ jobs:
|
||||
|
||||
- name: Install nsis
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
shell: pwsh
|
||||
# The Chocolatey community feed intermittently 504s, and `choco install`
|
||||
# exits 0 when package resolution fails that way. Unchecked, the job then
|
||||
# builds for ~25 minutes before `cpack -G NSIS` reports a missing makensis.
|
||||
# Retry the install and verify makensis itself, so a real failure stops here.
|
||||
run: |
|
||||
dir "C:/Program Files (x86)/Windows Kits/10/Include"
|
||||
$nsisDir = Join-Path ${env:ProgramFiles(x86)} 'NSIS'
|
||||
$makensis = Join-Path $nsisDir 'makensis.exe'
|
||||
for ($attempt = 1; $attempt -le 3 -and -not (Test-Path $makensis); $attempt++) {
|
||||
if ($attempt -gt 1) { Start-Sleep -Seconds (15 * $attempt) }
|
||||
Write-Host "::group::choco install nsis (attempt $attempt)"
|
||||
choco install nsis --yes --no-progress
|
||||
Write-Host "::endgroup::"
|
||||
}
|
||||
if (-not (Test-Path $makensis)) {
|
||||
throw "NSIS install failed: $makensis not found after 3 attempts."
|
||||
}
|
||||
& $makensis /VERSION
|
||||
$nsisDir | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
|
||||
choco install nsis
|
||||
|
||||
- name: Build slicer Win
|
||||
if: runner.os == 'Windows'
|
||||
@@ -476,22 +384,12 @@ jobs:
|
||||
# env:
|
||||
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
|
||||
# WindowsSDKVersion: '10.0.26100.0\'
|
||||
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
|
||||
# "tests" builds the unit tests too; the unit_tests_windows_* jobs run them.
|
||||
run: |
|
||||
# cache-path is the install directory inside the deps build directory.
|
||||
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/')
|
||||
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator.
|
||||
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" }
|
||||
.\build_win.bat -s --tests -i --arch ${{ inputs.arch }} --build-dir $env:BUILD_DIR --deps-dir $deps @flags
|
||||
$arch = "${{ inputs.arch }}"
|
||||
if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 tests } else { .\build_release_vs.bat slicer tests }
|
||||
shell: pwsh
|
||||
|
||||
- name: Build system preset cache (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: cmd
|
||||
# Shipped into both the already-installed tree (portable zip, MSIX) and
|
||||
# the checkout cpack re-installs from when it builds the NSIS installer.
|
||||
run: scripts\build_preset_cache.bat --prune-source "%BUILD_DIR%" "resources\profiles" "%BUILD_DIR%\OrcaSlicer\resources\profiles"
|
||||
|
||||
- name: Pack unit tests Win
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -641,20 +539,6 @@ jobs:
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build system preset cache (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
# Both were packed from resources/ before the caches existed, so the
|
||||
# AppImage is unpacked first and the caches shipped into it and into
|
||||
# the package tree; the source tree keeps its JSONs for later steps.
|
||||
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
|
||||
chmod +x "$appimage"
|
||||
"$appimage" --appimage-extract
|
||||
./scripts/build_preset_cache.sh -b build build/package/resources/profiles squashfs-root/resources/profiles
|
||||
appimagetool=$(find build -name "appimagetool.AppImage" | head -1)
|
||||
ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage"
|
||||
rm -rf squashfs-root
|
||||
# Ship the freshly-built validator so slice_check_linux (build_all.yml)
|
||||
# can slice-sweep the shipped profiles with this PR's engine. Taken from
|
||||
# the aarch64 leg so the sweep also exercises the arm build; x86_64 on
|
||||
@@ -702,18 +586,6 @@ jobs:
|
||||
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
|
||||
path: './build/src/Release/OrcaSlicer_profile_validator'
|
||||
|
||||
# generate_system_cache is what scripts/build_preset_cache.sh bakes the
|
||||
# <vendor>.opc caches with; it was already built by the "Build system
|
||||
# preset cache (Linux)" step above. The .opc format is 64-bit
|
||||
# little-endian native, i.e. identical across every platform Orca ships,
|
||||
# so only the Linux binary is published.
|
||||
- name: Upload generate_system_cache Ubuntu
|
||||
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
|
||||
path: './build/src/dev-utils/Release/generate_system_cache'
|
||||
|
||||
- name: Deploy Ubuntu release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
@@ -744,17 +616,6 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy Ubuntu generate_system_cache release
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
|
||||
release_id: 137995723
|
||||
asset_path: ./build/src/dev-utils/Release/generate_system_cache
|
||||
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
- name: Deploy orca_custom_preset_tests
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
@@ -765,48 +626,3 @@ jobs:
|
||||
asset_name: orca_custom_preset_tests.zip
|
||||
asset_content_type: application/octet-stream
|
||||
max_releases: 1
|
||||
|
||||
# The build has just touched everything it can use, so an object
|
||||
# untouched for a week is dead, usually orphaned by a flag change.
|
||||
- name: Compiler cache statistics
|
||||
if: ${{ always() && steps.ccache.outcome == 'success' }}
|
||||
shell: bash
|
||||
run: |
|
||||
ccache --evict-older-than 7d
|
||||
ccache -s -v || ccache -s
|
||||
|
||||
# Entries are immutable, so the new one is saved first and the older
|
||||
# ones for this leg on this ref are dropped afterwards: a failed save
|
||||
# leaves the previous entry in place. A cancelled or failed build saves
|
||||
# too, since what it compiled is still valid; a restore that did not
|
||||
# finish does not, since the directory may be a truncated copy.
|
||||
- name: Save compiler cache
|
||||
id: ccache_save
|
||||
if: ${{ always() && steps.ccache_restore.outcome == 'success' && github.event_name != 'pull_request' }}
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ${{ env.CCACHE_ENTRY }}
|
||||
|
||||
- name: Drop older compiler cache entries
|
||||
if: ${{ always() && steps.ccache_save.outcome == 'success' }}
|
||||
# A read-only token (fork PRs) cannot delete; that only costs storage.
|
||||
# Older means a lower run id, so two runs finishing close together keep
|
||||
# the newer entry whichever of them cleans up last.
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# The save step reports success even when its tar failed, so keep
|
||||
# the older entries unless the new one is listed.
|
||||
if ! gh cache list --ref "$GITHUB_REF" --key "$CCACHE_ENTRY" --json key \
|
||||
| jq -e --arg entry "$CCACHE_ENTRY" 'any(.[]; .key == $entry)' > /dev/null; then
|
||||
echo "$CCACHE_ENTRY was not saved; keeping the older entries."
|
||||
exit 0
|
||||
fi
|
||||
gh cache list --ref "$GITHUB_REF" --key "ccache-$CCACHE_LEG-" --limit 100 --json id,key \
|
||||
| jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \
|
||||
'.[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \
|
||||
| tr -d '\r' \
|
||||
| while read -r id; do gh cache delete "$id"; done
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
name: Check profiles
|
||||
on:
|
||||
pull_request:
|
||||
# release/* is included because pr-merge-bot.yml lets delegates merge into
|
||||
# it, and it gates on this workflow's result. Without it a delegated merge
|
||||
# into a release branch would run no profile validation at all.
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
paths:
|
||||
- 'resources/profiles/**'
|
||||
# orca_profile_tool.py also validates resources/printers/bambu_filament_ids.json, and
|
||||
# both it and its tests live in scripts/, so a PR touching only those must still run
|
||||
# this workflow.
|
||||
- 'resources/printers/**'
|
||||
- 'scripts/**'
|
||||
# orca_profile_tool.py reads the variant key sets from PrintConfig.cpp, and its
|
||||
# tests the obsolete keys, so a PR changing either must be checked against the profiles.
|
||||
- 'src/libslic3r/PrintConfig.cpp'
|
||||
- ".github/workflows/check_profiles.yml"
|
||||
|
||||
workflow_dispatch:
|
||||
@@ -32,31 +20,18 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check_profiles:
|
||||
# This job name is the check-run name pr-merge-bot.yml requires before a
|
||||
# delegated merge. Renaming it silently disables that gate.
|
||||
name: Check profiles
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Deliberately not continue-on-error, unlike every check below: if the tool itself is
|
||||
# broken, nothing it then reports about the profiles is worth reading.
|
||||
- name: Run the profile tool's own unit tests
|
||||
run: python3 -m unittest discover -s scripts/tests -t scripts
|
||||
|
||||
# What the validator below cannot see. It loads the tree the way the slicer does, so
|
||||
# it never notices a profile no <vendor>.json indexes, a preset name two files claim,
|
||||
# an id that is not the mint of its own triple, or a file that normalize and
|
||||
# update-index would still rewrite.
|
||||
# The step id is the handle the PR comment and the failure gate below use; renaming it
|
||||
# silently disables them.
|
||||
- name: Check profiles (orca_profile_tool.py)
|
||||
id: profile_tool
|
||||
- name: Run extra JSON check
|
||||
id: extra_json_check
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set +e
|
||||
python3 ./scripts/orca_profile_tool.py check 2>&1 | tee ${{ runner.temp }}/profile_tool.log
|
||||
python3 ./scripts/orca_extra_profile_check.py 2>&1 | tee ${{ runner.temp }}/extra_json_check.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
# download
|
||||
@@ -74,10 +49,8 @@ jobs:
|
||||
set +e
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
# Slice a two-colour cube through every printer, and through every system process/filament whose
|
||||
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
|
||||
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
|
||||
# invalid-flow bugs the static checks above cannot see.
|
||||
# Slice a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode)
|
||||
# is expanded - catches undefined-placeholder / invalid-flow bugs the static checks above cannot see.
|
||||
- name: validate slice (expand custom g-code)
|
||||
id: validate_slice
|
||||
continue-on-error: true
|
||||
@@ -85,14 +58,13 @@ jobs:
|
||||
set +e
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -s -l 2 2>&1 | tee ${{ runner.temp }}/validate_slice.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
# All vendors' filament_id collisions were fixed, so the duplicate-filament-subtype
|
||||
# check runs tree-wide.
|
||||
- name: validate filament subtype check
|
||||
# For now run filament subtype check only for BBL profiles until we fix other vendors' profiles.
|
||||
- name: validate filament subtype check for BBL profiles
|
||||
id: validate_filament_subtypes
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set +e
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
|
||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -v BBL -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: validate custom presets
|
||||
@@ -203,7 +175,7 @@ jobs:
|
||||
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
|
||||
|
||||
- name: Prepare comment artifact
|
||||
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
run: |
|
||||
{
|
||||
# Marker matched by check_profiles_comment.yml to delete prior comments.
|
||||
@@ -211,11 +183,11 @@ jobs:
|
||||
echo "## :x: Profile Validation Errors"
|
||||
echo ""
|
||||
|
||||
if [ "${{ steps.profile_tool.outcome }}" = "failure" ]; then
|
||||
echo "### Profile Check Failed (orca_profile_tool.py)"
|
||||
if [ "${{ steps.extra_json_check.outcome }}" = "failure" ]; then
|
||||
echo "### Extra JSON Check Failed"
|
||||
echo ""
|
||||
echo '```'
|
||||
head -c 30000 ${{ runner.temp }}/profile_tool.log || echo "No output captured"
|
||||
head -c 30000 ${{ runner.temp }}/extra_json_check.log || echo "No output captured"
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
@@ -239,7 +211,7 @@ jobs:
|
||||
fi
|
||||
|
||||
if [ "${{ steps.validate_filament_subtypes.outcome }}" = "failure" ]; then
|
||||
echo "### Filament Subtype Validation Failed"
|
||||
echo "### BBL Filament Subtype Validation Failed"
|
||||
echo ""
|
||||
echo '```'
|
||||
head -c 30000 ${{ runner.temp }}/validate_filament_subtypes.log || echo "No output captured"
|
||||
@@ -257,7 +229,7 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo '*Fix the errors above and push a new commit. To reproduce this run locally: `scripts/check_profile.sh`, or `scripts\check_profile.bat` on Windows.*'
|
||||
echo "*Please fix the above errors and push a new commit.*"
|
||||
} > ${{ runner.temp }}/profile-check-results/pr_comment.md
|
||||
|
||||
- name: Upload comment artifact
|
||||
@@ -269,8 +241,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
- name: Fail if any check failed
|
||||
if: ${{ always() && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
|
||||
run: |
|
||||
echo "One or more profile checks failed; see the step logs above."
|
||||
echo 'Reproduce the whole run locally with scripts/check_profile.sh (scripts\check_profile.bat on Windows).'
|
||||
echo "One or more profile checks failed. See above for details."
|
||||
exit 1
|
||||
|
||||
@@ -19,18 +19,11 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
# Doxygen with call graphs over all of src/ outgrows the runner's RAM;
|
||||
# replace the runner's swapfile with an 8 GB one.
|
||||
- name: Grow swap space
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo swapoff -a
|
||||
sudo rm -f /swapfile
|
||||
sudo fallocate -l 8G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
free -h
|
||||
- uses: thejerrybao/setup-swap-space@v1
|
||||
with:
|
||||
swap-space-path: /swapfile
|
||||
swap-size-gb: 8
|
||||
remove-existing-swap-files: true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
name: Daily OFL OTA Update
|
||||
|
||||
run-name: Daily OFL OTA Update [OFL barrier]
|
||||
|
||||
# This workflow is intended for creating and publishing the OrcaFilamentLibrary (OFL) OPC package to
|
||||
# https://github.com/OrcaSlicer/orcaslicer-profiles, which generates an OTA update.
|
||||
# This cronjob runs daily at 00:00 UTC every day and scans main plus every release/vX.Y.Z branch for
|
||||
# changes to resources/profiles/OrcaFilamentLibrary since that branch's own last successful run. Any
|
||||
# branch with no changes is skipped; each changed branch gets its own post_merge_profiles.yml dispatch.
|
||||
#
|
||||
# OFL has no dedicated FOLDER_MERGERS grant (it isn't merged through the PR merge-bot delegation
|
||||
# scheme), so post_merge_profiles.yml is dispatched with an explicit `vendor` input, which that
|
||||
# workflow trusts and uses to bypass the FOLDER_MERGERS check for this trigger. That same explicit-
|
||||
# vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after
|
||||
# uploading - see post_merge_profiles.yml for both sides of that contract.
|
||||
#
|
||||
# Each run captures a timestamp, dispatches the needed OFL publishers, waits for
|
||||
# all of them to finish, then clears the pending-publish table once. Changes merged
|
||||
# after that timestamp remain pending for the next run.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: write # list this workflow's past runs and dispatch post_merge_profiles.yml
|
||||
contents: read
|
||||
|
||||
env:
|
||||
VENDOR: OrcaFilamentLibrary
|
||||
|
||||
jobs:
|
||||
daily-job:
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Capture start timestamp
|
||||
id: start
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
timestamp="$(date -u +%s)"
|
||||
echo "timestamp=$timestamp" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Full history: the per-branch "since last successful run" check below
|
||||
# needs to look arbitrarily far back if a prior run failed or was skipped.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch all branches
|
||||
shell: bash
|
||||
run: git fetch origin '+refs/heads/*:refs/remotes/origin/*'
|
||||
|
||||
- name: Scan branches and publish changed OFL profiles
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SCAN_UNTIL: ${{ steps.start.outputs.timestamp }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
mapfile -t branches < <(
|
||||
gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \
|
||||
| grep -E '^(main|release/v[0-9]+\.[0-9]+\.[0-9]+)$' | sort -u
|
||||
)
|
||||
|
||||
# The cron run is the checkpoint: a successful run means every
|
||||
# dispatched branch publisher completed and the pending queue was
|
||||
# cleared. Manual or push-triggered post_merge_profiles runs are not
|
||||
# checkpoints for this scan.
|
||||
successful_cron_runs="$(gh api --method GET \
|
||||
"repos/${{ github.repository }}/actions/workflows/ofl-ota-cronjob.yml/runs" \
|
||||
-f status=success -f branch=main -f per_page=100 --paginate \
|
||||
--jq '.workflow_runs[] | select((.display_title // "") | contains("[OFL barrier]"))')"
|
||||
since="$(jq -rs 'sort_by(.run_started_at) | last.run_started_at // empty' <<< "$successful_cron_runs")"
|
||||
|
||||
for branch in "${branches[@]}"; do
|
||||
echo "::group::$branch"
|
||||
|
||||
if [ -z "$since" ]; then
|
||||
echo "No prior successful OFL cron run; checking $branch through $SCAN_UNTIL."
|
||||
changed_files="$(git log --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
|
||||
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
|
||||
| sed '/^$/d')"
|
||||
else
|
||||
changed_files="$(git log --since="$since" --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
|
||||
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
|
||||
| sed '/^$/d')"
|
||||
fi
|
||||
|
||||
if [ -n "$changed_files" ]; then
|
||||
echo "OFL changed on $branch from ${since:-the beginning} through $SCAN_UNTIL:"
|
||||
echo "$changed_files"
|
||||
changed=true
|
||||
else
|
||||
echo "No OFL changes on $branch through $SCAN_UNTIL."
|
||||
changed=false
|
||||
fi
|
||||
|
||||
if [ "$changed" = true ]; then
|
||||
dispatch_id="${GITHUB_RUN_ID}-${branch//\//-}"
|
||||
# Record successful dispatches for the barrier step below.
|
||||
# Branches whose workflow predates workflow_dispatch are skipped
|
||||
# with a warning, as they were before the barrier was added.
|
||||
if gh workflow run post_merge_profiles.yml \
|
||||
--repo "${{ github.repository }}" \
|
||||
--ref "$branch" \
|
||||
-f vendor="$VENDOR" -f auto_publish=true \
|
||||
-f ofl_cron_dispatch_id="$dispatch_id"; then
|
||||
printf '%s\t%s\n' "$branch" "$dispatch_id" >> "$RUNNER_TEMP/ofl-dispatches.tsv"
|
||||
else
|
||||
echo "::warning::skipping $branch because post_merge_profiles.yml could not be dispatched at that ref"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
- name: Wait for OFL publishers
|
||||
id: wait
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DISPATCHES_FILE: ${{ runner.temp }}/ofl-dispatches.tsv
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ ! -s "$DISPATCHES_FILE" ]; then
|
||||
echo "No OFL publisher workflows were dispatched; pending queue will not be cleared."
|
||||
echo "publishers_dispatched=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
: > "$RUNNER_TEMP/ofl-run-ids.tsv"
|
||||
while IFS=$'\t' read -r branch dispatch_id; do
|
||||
[ -n "$branch" ] || continue
|
||||
echo "Waiting for OFL publisher on $branch ($dispatch_id)"
|
||||
|
||||
run_id=""
|
||||
for _ in {1..120}; do
|
||||
runs_json="$(gh api --method GET \
|
||||
"repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \
|
||||
-f branch="$branch" -f event=workflow_dispatch -f per_page=100)"
|
||||
run_id="$(jq -r --arg marker "[OFL cron $dispatch_id]" \
|
||||
'[.workflow_runs[] | select((.display_title // "") | contains($marker))]
|
||||
| sort_by(.created_at) | last | .id // empty' <<< "$runs_json")"
|
||||
[ -n "$run_id" ] && break
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ -z "$run_id" ]; then
|
||||
echo "::error::could not find dispatched post_merge_profiles run for $branch ($dispatch_id)"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\t%s\n' "$branch" "$run_id" >> "$RUNNER_TEMP/ofl-run-ids.tsv"
|
||||
done < "$DISPATCHES_FILE"
|
||||
|
||||
all_success=true
|
||||
while IFS=$'\t' read -r branch run_id; do
|
||||
[ -n "$run_id" ] || continue
|
||||
echo "Watching OFL publisher run $run_id for $branch"
|
||||
if ! gh run watch "$run_id" --repo "${{ github.repository }}" --exit-status; then
|
||||
all_success=false
|
||||
fi
|
||||
done < "$RUNNER_TEMP/ofl-run-ids.tsv"
|
||||
|
||||
if [ "$all_success" != true ]; then
|
||||
echo "::error::one or more OFL publisher workflows failed; pending queue will not be cleared"
|
||||
exit 1
|
||||
fi
|
||||
echo "publishers_dispatched=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Clear OFL pending queue
|
||||
if: steps.wait.outputs.publishers_dispatched == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
|
||||
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
|
||||
TIMESTAMP: ${{ steps.start.outputs.timestamp }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
|
||||
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
|
||||
|
||||
resp_file="$RUNNER_TEMP/ota-pending-clear-response.json"
|
||||
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
|
||||
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear?timestamp=$TIMESTAMP" \
|
||||
-H "Authorization: Bearer $OTA_API_KEY")"
|
||||
body="$(cat "$resp_file")"
|
||||
echo "$body"
|
||||
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "::error::OTA pending-clear call failed with HTTP $status"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,263 +0,0 @@
|
||||
# Nightly parity checks from OrcaSlicer/orca-test-repo, kept out of the
|
||||
# per-build "Run external slicer regression tests" step because they take far
|
||||
# longer than that step's budget:
|
||||
# effect - the CLI override sweep's full effect stage: every landed option
|
||||
# re-sliced on its own to see whether it changes the G-code
|
||||
# harness - the GUI-vs-CLI parity harness (metrics only, never fails)
|
||||
# Both test the latest successful build_all.yml Linux AppImage from main, with
|
||||
# sources checked out at the commit that build was made from; a manual run can
|
||||
# name another branch, or pin one build by its run id. Nothing here gates a
|
||||
# build or a PR.
|
||||
name: Parity Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# build_all.yml starts at 17:00 UTC and has finished by ~20:00
|
||||
- cron: "0 21 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_repo_ref:
|
||||
description: "orca-test-repo ref to run"
|
||||
required: false
|
||||
default: "main"
|
||||
build_branch:
|
||||
description: "branch whose newest successful build_all artifact to test (a PR build is the PR merged into its base; sources are checked out at the PR head)"
|
||||
required: false
|
||||
default: "main"
|
||||
build_run_id:
|
||||
description: "build_all run id to test instead of build_branch's newest (same PR caveat)"
|
||||
required: false
|
||||
default: ""
|
||||
fixtures:
|
||||
description: "harness fixture ids, space-separated (empty = all)"
|
||||
required: false
|
||||
default: ""
|
||||
cli_presets:
|
||||
description: "harness lane C presets: flat = flatten inherits first, raw = leaf profile as-is"
|
||||
required: false
|
||||
default: "flat"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Find the build to test
|
||||
# Don't run scheduled checks on forks
|
||||
if: github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
run_id: ${{ steps.find.outputs.run_id }}
|
||||
head_sha: ${{ steps.find.outputs.head_sha }}
|
||||
steps:
|
||||
- id: find
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ inputs.build_branch || 'main' }}
|
||||
RUN_ID: ${{ inputs.build_run_id }}
|
||||
SCHEDULED: ${{ github.event_name == 'schedule' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
[[ $RUN_ID =~ ^[0-9]+$ ]] || { echo "build_run_id must be a numeric run id, got '$RUN_ID'" >&2; exit 1; }
|
||||
# a pinned build is read directly, not through a search; it must come
|
||||
# from this repository, because the later jobs check out its commit here
|
||||
found=$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID" --jq \
|
||||
'select(.path == ".github/workflows/build_all.yml" and .conclusion == "success"
|
||||
and .head_repository.full_name == env.GH_REPO)
|
||||
| "\(.id) \(.head_sha) \(.created_at)"')
|
||||
[ -n "$found" ] || { echo "run $RUN_ID is not a successful build_all run of $GH_REPO" >&2; exit 1; }
|
||||
else
|
||||
# GitHub serves filtered run listings (branch=, status=, head_sha=, ...)
|
||||
# from a search index that has returned weeks-old results, while the
|
||||
# unfiltered listing stays current, so list unfiltered and filter here.
|
||||
# The repository check keeps out fork PRs whose branch has the same
|
||||
# name. A feature branch is normally built only for its PR, and a PR
|
||||
# build compiles the PR merged into its base rather than head_sha, so
|
||||
# a build of the branch itself (push or dispatch) is preferred when
|
||||
# the same page has one.
|
||||
pick='([.workflow_runs[] | select(.head_branch == env.BRANCH and .conclusion == "success"
|
||||
and .head_repository.full_name == env.GH_REPO)]
|
||||
| map(select(.event != "pull_request"))[0] // .[0])
|
||||
| select(.) | "\(.id) \(.head_sha) \(.created_at)"'
|
||||
# a page of 100 runs spans about a day and a half; a manual run may
|
||||
# target a branch that last built weeks ago
|
||||
pages=3
|
||||
if [ "$SCHEDULED" != true ]; then pages=20; fi
|
||||
found=""
|
||||
for page in $(seq "$pages"); do
|
||||
found=$(gh api "repos/$GH_REPO/actions/workflows/build_all.yml/runs?per_page=100&page=$page" --jq "$pick")
|
||||
if [ -n "$found" ]; then break; fi
|
||||
done
|
||||
[ -n "$found" ] || { echo "no successful $BRANCH build among the last $((pages * 100)) build_all runs; pass build_run_id to test an older one" >&2; exit 1; }
|
||||
fi
|
||||
read -r run_id head_sha created <<< "$found"
|
||||
# the nightly fails rather than report on a stale build
|
||||
if [ "$SCHEDULED" = true ] && [ $(( $(date +%s) - $(date -d "$created" +%s) )) -gt 172800 ]; then
|
||||
echo "newest $BRANCH build $run_id is from $created, over 48 hours old" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'run_id=%s\nhead_sha=%s\n' "$run_id" "$head_sha" >> "$GITHUB_OUTPUT"
|
||||
cat "$GITHUB_OUTPUT"
|
||||
echo "Testing build [$run_id](https://github.com/$GH_REPO/actions/runs/$run_id) of \`$head_sha\`, built $created" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
effect:
|
||||
name: Override sweep effect stage (shard ${{ matrix.shard }})
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# orca-test-repo's parity/effect_routing.json holds a 2-way split,
|
||||
# ~12.5 min a shard on this runner
|
||||
shard: [0, 1]
|
||||
steps:
|
||||
- &checkout-suite
|
||||
name: Check out the test suite
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: OrcaSlicer/orca-test-repo
|
||||
ref: ${{ inputs.test_repo_ref || 'main' }}
|
||||
path: orca-test-repo
|
||||
|
||||
# The AppImage ships only packed preset caches, so profiles and the CLI
|
||||
# option surface come from the sources the build was made from
|
||||
- &checkout-slicer
|
||||
name: Check out OrcaSlicer at the build's commit
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.build.outputs.head_sha }}
|
||||
path: slicer
|
||||
lfs: 'false'
|
||||
|
||||
- &extract-appimage
|
||||
name: Download and extract the Linux AppImage
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh run download "${{ needs.build.outputs.run_id }}" --dir appimage \
|
||||
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
|
||||
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
|
||||
[ -n "$appimage" ] || { echo "no x86_64 AppImage in run ${{ needs.build.outputs.run_id }}"; exit 1; }
|
||||
chmod +x "$appimage"
|
||||
"$appimage" --appimage-extract > /dev/null
|
||||
# The bare binary cannot find the AppImage's bundled libraries; AppRun
|
||||
# sets them up and execs it, so exit codes and signals pass through
|
||||
[ -x squashfs-root/AppRun ] || { echo "no AppRun in the AppImage"; exit 1; }
|
||||
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
|
||||
echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install the AppImage's host runtime dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install suite dependencies
|
||||
run: pip install -r orca-test-repo/requirements.txt
|
||||
|
||||
- name: Run the override sweep with the full effect stage
|
||||
id: run
|
||||
continue-on-error: true
|
||||
working-directory: orca-test-repo
|
||||
run: |
|
||||
set -o pipefail
|
||||
# -rA keeps the per-stage summaries, which pytest otherwise swallows
|
||||
# for passing tests
|
||||
python -m pytest test_cli_overrides.py -c pytest.ini -v -rA \
|
||||
--effect-full --effect-shard ${{ matrix.shard }}/2 \
|
||||
--orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \
|
||||
2>&1 | tee ../sweep.log
|
||||
|
||||
- name: Publish job summary
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo "## Override sweep effect stage, shard ${{ matrix.shard }}/2"
|
||||
echo "Build ${{ needs.build.outputs.head_sha }} (run ${{ needs.build.outputs.run_id }})"
|
||||
echo '```'
|
||||
grep -E "\[override sweep" sweep.log || echo "no stage summaries, see the log"
|
||||
grep -E "^=+ .*(passed|failed)" sweep.log | tail -1 || true
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload the override report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: override-report-shard${{ matrix.shard }}
|
||||
path: |
|
||||
orca-test-repo/.pytest_cache/override_report.json
|
||||
sweep.log
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
# The sweep step continues on error so the summary and report still get
|
||||
# published; this puts the failure back on the job
|
||||
- name: Fail the job if the sweep failed
|
||||
if: steps.run.outcome == 'failure'
|
||||
run: |
|
||||
echo "the override sweep failed, see the job summary and the uploaded report" >&2
|
||||
exit 1
|
||||
|
||||
harness:
|
||||
name: GUI-vs-CLI parity harness
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- *checkout-suite
|
||||
- *checkout-slicer
|
||||
- *extract-appimage
|
||||
|
||||
- name: Install display tooling and the AppImage's host runtime
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
xvfb xdotool imagemagick openbox mesa-utils \
|
||||
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
|
||||
|
||||
- name: Run the parity harness
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fixtures=()
|
||||
for f in ${{ inputs.fixtures || '' }}; do
|
||||
fixtures+=(--fixture "$f")
|
||||
done
|
||||
# 2 GUI displays: ~1.5 cores peak / ~1.9 GB on this 4-vCPU runner,
|
||||
# and each fixture is fully isolated, so results match a serial run
|
||||
python3 orca-test-repo/parity/run_parity.py \
|
||||
--slicer-root "$ORCA_SOURCE" --bin "$ORCA_BIN" \
|
||||
--cli-presets "${{ inputs.cli_presets || 'flat' }}" \
|
||||
--gui-workers 2 --out "$PWD/parity-out" "${fixtures[@]}"
|
||||
|
||||
- name: Publish job summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f parity-out/report.md ]; then
|
||||
cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "the harness produced no report, see the log" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Drop per-lane datadirs before upload
|
||||
if: always()
|
||||
run: rm -rf parity-out/*/seed parity-out/*/datadir-* || true
|
||||
|
||||
- name: Upload the scorecard and evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: parity-scorecard
|
||||
path: parity-out/
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
@@ -1,464 +0,0 @@
|
||||
name: Post-merge profiles
|
||||
|
||||
run-name: >-
|
||||
Post-merge profiles${{
|
||||
inputs.ofl_cron_dispatch_id != '' &&
|
||||
inputs.vendor == 'OrcaFilamentLibrary' &&
|
||||
(inputs.auto_publish == true || inputs.auto_publish == 'true') &&
|
||||
format(' [OFL cron {0}]', inputs.ofl_cron_dispatch_id) ||
|
||||
''
|
||||
}}
|
||||
|
||||
# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a
|
||||
# profile change lands on main or a release branch, rebuild the affected vendors'
|
||||
# binary preset caches (<vendor>.opc) and publish each as a versioned ZIP asset on
|
||||
# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA
|
||||
# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and
|
||||
# only then does it become a live OTA update - this workflow does none of that
|
||||
# last part (no changelog, no R2, no webhook).
|
||||
#
|
||||
# A workflow_dispatch carrying a `vendor` input (e.g. the daily OFL cron - OFL has
|
||||
# no FOLDER_MERGERS grant, since it isn't merged through the PR merge-bot delegation
|
||||
# scheme) publishes that vendor directly and skips the FOLDER_MERGERS check below.
|
||||
# workflow_dispatch is already a trusted, explicit trigger, unlike the automatic
|
||||
# push-diff path the FOLDER_MERGERS check exists to gate.
|
||||
#
|
||||
# Separately, an ordinary push whose diff touches an OrcaFilamentLibrary company
|
||||
# folder (resources/profiles/OrcaFilamentLibrary/filament/<Company>/**) records
|
||||
# that PR as pending via POST /api/v1/ota/ofl/pending, regardless of whether
|
||||
# OrcaFilamentLibrary as a whole is authorized to publish in this same run - a
|
||||
# partner's OTA Manager dashboard should see a merged PR immediately, well
|
||||
# before the daily cron actually builds and publishes it.
|
||||
#
|
||||
# Asset contract expected by OrcaCloud's release scanner:
|
||||
# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$
|
||||
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.opc)
|
||||
#
|
||||
# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# once v2.5.0 stable is released, this will be removed, so nightly won't receive OTA updates.
|
||||
- main
|
||||
# release/vX.Y.Z point-release branches only, not the release/vX.Y working
|
||||
# branch profile PRs land on first - "v*.*.*" requires two literal dots,
|
||||
# which release/vX.Y (one dot) doesn't have.
|
||||
- release/v*.*.*
|
||||
paths:
|
||||
- 'resources/profiles/**'
|
||||
- '.github/workflows/post_merge_profiles.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
vendor:
|
||||
description: >-
|
||||
Publish only this vendor, bypassing the FOLDER_MERGERS grant check.
|
||||
For trusted explicit dispatches only (e.g. the OFL nightly cron).
|
||||
Leave empty to fall back to diffing the triggering commit.
|
||||
required: false
|
||||
type: string
|
||||
auto_publish:
|
||||
description: >-
|
||||
After publishing, also call the OTA auto-publish API to go live
|
||||
immediately, skipping the human changelog/Publish step. Separate
|
||||
from `vendor` on purpose: a maintainer can dispatch with just
|
||||
`vendor` set to rebuild/republish an asset without it going live.
|
||||
Only the OFL nightly cron should set this to true.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
ofl_cron_dispatch_id:
|
||||
description: >-
|
||||
Unique marker supplied by the trusted OFL daily cron so it can find
|
||||
and wait for this dispatched workflow run.
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read # commits/{sha}/pulls lookup in the OFL-pending step
|
||||
|
||||
# One run per branch; let a run finish rather than cancel it, since it publishes.
|
||||
concurrency:
|
||||
group: post-merge-profiles-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
# generate_system_cache is published to this repo's own nightly-builds release
|
||||
# by build_orca.yml's Linux leg. The job guard pins github.repository to
|
||||
# OrcaSlicer/OrcaSlicer, so this resolves there.
|
||||
TOOL_REPO: ${{ github.repository }}
|
||||
TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly
|
||||
# Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo.
|
||||
PROFILES_OWNER: OrcaSlicer
|
||||
PROFILES_REPO: orcaslicer-profiles
|
||||
|
||||
jobs:
|
||||
publish_profile_caches:
|
||||
name: Publish profile caches
|
||||
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
|
||||
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
|
||||
# merge bot. Keep this environment free of protection rules so this
|
||||
# push-triggered job does not wait for a reviewer.
|
||||
environment: merge-delegation
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Enough history to reach github.event.before for the changed-vendor
|
||||
# diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the
|
||||
# step below. fetch-depth: 0 would clone all of OrcaSlicer's history.
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Resolve changed vendors
|
||||
id: vendors
|
||||
shell: bash
|
||||
env:
|
||||
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
|
||||
DISPATCH_VENDOR: ${{ github.event_name == 'workflow_dispatch' && inputs.vendor || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# A vendor has a manifest plus either a preset directory or a version
|
||||
# field; this drops non-vendor files such as blacklist.json. Shared by
|
||||
# both the explicit-dispatch path below and the push-diff path further
|
||||
# down, so the definition of "valid vendor" can't drift between them.
|
||||
is_valid_vendor() {
|
||||
local v="$1"
|
||||
local json="resources/profiles/$v.json"
|
||||
[ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }
|
||||
}
|
||||
|
||||
# Explicit vendor dispatch (e.g. the OFL cron): trust the caller and
|
||||
# skip both the git-diff detection and the FOLDER_MERGERS check below.
|
||||
if [ -n "$DISPATCH_VENDOR" ]; then
|
||||
v="$DISPATCH_VENDOR"
|
||||
# Becomes part of the release asset filename and the OTA API's
|
||||
# payload; keep it to the same charset every real vendor name uses.
|
||||
if ! [[ "$v" =~ ^[A-Za-z0-9]+$ ]]; then
|
||||
echo "::error::vendor '$v' must be alphanumeric"
|
||||
exit 1
|
||||
fi
|
||||
if ! is_valid_vendor "$v"; then
|
||||
echo "::error::vendor '$v' has no resources/profiles/$v.json with a profile directory or version field"
|
||||
exit 1
|
||||
fi
|
||||
echo "vendors=$v" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
base='${{ github.event.before }}'
|
||||
head='${{ github.sha }}'
|
||||
# Zero SHA (branch created / force push) or manual dispatch: fall back
|
||||
# to this commit's own diff.
|
||||
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
base="$head^"
|
||||
fi
|
||||
# Exposed so the OFL-pending step below can reuse this exact diff
|
||||
# range instead of re-deriving it (and drifting from this logic).
|
||||
echo "base=$base" >> "$GITHUB_OUTPUT"
|
||||
echo "head=$head" >> "$GITHUB_OUTPUT"
|
||||
mapfile -t candidates < <(
|
||||
git diff --name-only "$base" "$head" -- resources/profiles \
|
||||
| sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
vendors=()
|
||||
for v in "${candidates[@]:-}"; do
|
||||
[ -n "$v" ] || continue
|
||||
if is_valid_vendor "$v"; then
|
||||
vendors+=("$v")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#vendors[@]}" -eq 0 ]; then
|
||||
echo "vendors=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# A vendor is eligible only when both the profile directory and its
|
||||
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
|
||||
# grant. The account part is intentionally ignored here: this is a
|
||||
# post-merge safety check, not an authorization check for a command.
|
||||
# An ineligible vendor (e.g. OrcaFilamentLibrary, which has no grant)
|
||||
# is dropped on its own - it never blocks other vendors in the same
|
||||
# push from publishing.
|
||||
grants=()
|
||||
while IFS= read -r raw_line; do
|
||||
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
[ -n "$line" ] || continue
|
||||
[[ "$line" == \#* ]] && continue
|
||||
[[ "$line" == *:* ]] || continue
|
||||
|
||||
grant="${line#*:}"
|
||||
grant="${grant#"${grant%%[![:space:]]*}"}"
|
||||
grant="${grant%"${grant##*[![:space:]]}"}"
|
||||
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
|
||||
grants+=("$grant")
|
||||
done <<< "${FOLDER_MERGERS:-}"
|
||||
|
||||
is_granted() {
|
||||
local path="$1"
|
||||
local grant
|
||||
for grant in "${grants[@]:-}"; do
|
||||
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
authorized=()
|
||||
unauthorized=()
|
||||
for v in "${vendors[@]}"; do
|
||||
if is_granted "resources/profiles/$v" && is_granted "resources/profiles/$v.json"; then
|
||||
authorized+=("$v")
|
||||
else
|
||||
unauthorized+=("$v")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#unauthorized[@]}" -ne 0 ]; then
|
||||
echo "::warning::skipping vendor(s) with no FOLDER_MERGERS grant (no asset built or published for them this run): ${unauthorized[*]}"
|
||||
fi
|
||||
|
||||
echo "vendors=${authorized[*]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Resolve Orca version
|
||||
id: orca
|
||||
# Unconditional: needed both by the vendor-publish pipeline below (only
|
||||
# when vendors is non-empty) and by the OFL-pending step at the end
|
||||
# (which runs whenever OFL itself changed, even if vendors ends up
|
||||
# empty because OFL has no FOLDER_MERGERS grant). Cheap and harmless
|
||||
# to always resolve - version.inc is present on every commit.
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)"
|
||||
[ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; }
|
||||
orca_ver="${raw%%-*}"
|
||||
if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1
|
||||
fi
|
||||
# release_tag is what the desktop client sends as orca_version and what
|
||||
# OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix.
|
||||
echo "release_tag=$raw" >> "$GITHUB_OUTPUT"
|
||||
echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate profile versions
|
||||
id: pver
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: > "$RUNNER_TEMP/pver.tsv"
|
||||
for v in ${{ steps.vendors.outputs.vendors }}; do
|
||||
pv="$(jq -r '.version // empty' "resources/profiles/$v.json")"
|
||||
if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::vendor $v version '${pv:-<none>}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv"
|
||||
done
|
||||
|
||||
- name: Download generate_system_cache
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
env:
|
||||
# gh (with the default token) rather than an unauthenticated curl: keeps
|
||||
# working if TOOL_REPO is ever private and avoids anonymous rate limits.
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release download nightly-builds --repo "$TOOL_REPO" \
|
||||
--pattern "$TOOL_ASSET" --output generate_system_cache --clobber
|
||||
chmod +x generate_system_cache
|
||||
|
||||
- name: Build caches and package assets
|
||||
id: pkg
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# One timestamp for the whole run so a multi-vendor merge groups together.
|
||||
ts="$(date -u +%Y%m%d%H%M)"
|
||||
orca_ver='${{ steps.orca.outputs.orca_ver }}'
|
||||
out="$RUNNER_TEMP/assets"
|
||||
mkdir -p "$out"
|
||||
|
||||
for v in ${{ steps.vendors.outputs.vendors }}; do
|
||||
./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2
|
||||
opc="resources/profiles/$v.opc"
|
||||
[ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; }
|
||||
pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")"
|
||||
name="${orca_ver}_${v}_${pv}_${ts}.zip"
|
||||
( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" )
|
||||
done
|
||||
echo "dir=$out" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Mint profiles-repo token
|
||||
id: token
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.PROFILES_APP_ID }}
|
||||
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
|
||||
owner: ${{ env.PROFILES_OWNER }}
|
||||
repositories: ${{ env.PROFILES_REPO }}
|
||||
|
||||
- name: Publish assets to profiles release
|
||||
if: steps.vendors.outputs.vendors != ''
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
RELEASE_TAG: ${{ steps.orca.outputs.release_tag }}
|
||||
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repo="$PROFILES_OWNER/$PROFILES_REPO"
|
||||
if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then
|
||||
echo "Creating release $RELEASE_TAG on $repo"
|
||||
gh release create "$RELEASE_TAG" --repo "$repo" \
|
||||
--title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \
|
||||
--latest=false
|
||||
fi
|
||||
# Asset names are timestamp-unique; a clash means a bug, so don't --clobber.
|
||||
gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip
|
||||
|
||||
{
|
||||
echo "### Published to \`$repo\` release \`$RELEASE_TAG\`"
|
||||
for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Notify OTA auto-publish
|
||||
# Gated on auto_publish specifically, not just "vendor was dispatched":
|
||||
# a maintainer manually dispatching with vendor=OrcaFilamentLibrary (e.g.
|
||||
# to rebuild/republish an asset while debugging) must not silently go
|
||||
# live. Only a caller that explicitly opts in with auto_publish=true
|
||||
# (the OFL nightly cron) skips the human changelog/Publish step.
|
||||
if: >-
|
||||
steps.vendors.outputs.vendors != '' && github.event_name == 'workflow_dispatch'
|
||||
&& (inputs.auto_publish == true || inputs.auto_publish == 'true')
|
||||
shell: bash
|
||||
env:
|
||||
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
|
||||
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
|
||||
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
|
||||
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
|
||||
|
||||
mapfile -t zip_files < <(cd "$ASSET_DIR" && ls -1 *.zip)
|
||||
filenames_json="$(printf '%s\n' "${zip_files[@]}" | jq -R . | jq -s .)"
|
||||
payload="$(jq -n --argjson filenames "$filenames_json" '{filenames: $filenames}')"
|
||||
|
||||
resp_file="$RUNNER_TEMP/ota-auto-publish-response.json"
|
||||
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
|
||||
"${OTA_API_BASE_URL%/}/api/v1/ota/auto-publish" \
|
||||
-H "Authorization: Bearer $OTA_API_KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$payload")"
|
||||
body="$(cat "$resp_file")"
|
||||
echo "$body"
|
||||
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "::error::OTA auto-publish call failed with HTTP $status"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A 200 can still carry per-file "error" results (e.g. NOT_FOUND); the
|
||||
# asset is already safely published to the profiles release above, but
|
||||
# it never went live, so treat that as a failure worth surfacing loudly.
|
||||
error_count="$(jq '[.results[] | select(.status == "error")] | length' <<< "$body")"
|
||||
if [ "$error_count" != "0" ]; then
|
||||
jq -r '.results[] | select(.status == "error") | "::error::\(.filename): \(.code) - \(.message)"' <<< "$body"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Record OFL pending changes
|
||||
# A real merge, never the cron's explicit-vendor dispatch (that's
|
||||
# automation publishing, not a new merge to report). This covers two
|
||||
# trigger shapes: an ordinary push, and a vendor-less workflow_dispatch
|
||||
# - the latter is exactly what pr-merge-bot.yml's re-dispatch after a
|
||||
# successful /bot merge looks like (a GITHUB_TOKEN-authored merge fires
|
||||
# no push event at all, which is why that re-dispatch exists). Both
|
||||
# land in the same diff-fallback path in "Resolve changed vendors", so
|
||||
# base/head/orca_ver are already correctly populated either way - only
|
||||
# this condition needs widening.
|
||||
# Placed last in the job on purpose: a failure here must never block
|
||||
# the vendor-publish pipeline above, which a step failing earlier in
|
||||
# the job would do (subsequent steps without always() get skipped).
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'workflow_dispatch' && !inputs.vendor)
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
|
||||
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base='${{ steps.vendors.outputs.base }}'
|
||||
head='${{ steps.vendors.outputs.head }}'
|
||||
orca_ver='${{ steps.orca.outputs.orca_ver }}'
|
||||
|
||||
# Only real vendor subdirectories under filament/, e.g.
|
||||
# .../filament/Qidi/x.json -> "Qidi". This naturally excludes loose
|
||||
# top-level files (.../filament/Generic PLA @System.json - no further
|
||||
# slash to match) and is further filtered below to drop "base", the
|
||||
# shared @base/@System inheritance folder, not a partner company.
|
||||
mapfile -t ofl_companies < <(
|
||||
git diff --name-only "$base" "$head" -- resources/profiles/OrcaFilamentLibrary/filament \
|
||||
| sed -nE 's#^resources/profiles/OrcaFilamentLibrary/filament/([^/]+)/.*#\1#p' \
|
||||
| grep -vx 'base' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
if [ "${#ofl_companies[@]}" -eq 0 ]; then
|
||||
echo "No OFL company folders changed in this push."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
|
||||
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
|
||||
|
||||
# The head commit's own merged PR, not a per-commit walk: this
|
||||
# assumes the ordinary one-PR-per-push shape every other merge path
|
||||
# in this repo already assumes (pr-merge-bot.yml's re-dispatch logic
|
||||
# does the same). A merge commit's parents don't matter here - this
|
||||
# API call works the same regardless of merge strategy.
|
||||
pr_json="$(gh api "repos/${{ github.repository }}/commits/$head/pulls" \
|
||||
--jq '[.[] | select(.merged_at != null)] | sort_by(.merged_at) | last // empty')"
|
||||
|
||||
if [ -z "$pr_json" ]; then
|
||||
echo "::warning::push $head touches OFL compan(y/ies) (${ofl_companies[*]}) but has no associated merged PR; skipping pending record(s)"
|
||||
exit 0
|
||||
fi
|
||||
pr_number="$(jq -r '.number' <<< "$pr_json")"
|
||||
pr_url="$(jq -r '.html_url' <<< "$pr_json")"
|
||||
pr_title="$(jq -r '.title' <<< "$pr_json")"
|
||||
|
||||
for company in "${ofl_companies[@]}"; do
|
||||
payload="$(jq -n --arg vendor "$company" --arg ver "$orca_ver" --argjson pr "$pr_number" \
|
||||
--arg url "$pr_url" --arg title "$pr_title" \
|
||||
'{vendor: $vendor, orcaSlicerVersion: $ver, prNumber: $pr, prUrl: $url, prTitle: $title}')"
|
||||
|
||||
resp_file="$RUNNER_TEMP/ofl-pending-$company.json"
|
||||
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
|
||||
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending" \
|
||||
-H "Authorization: Bearer $OTA_API_KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$payload")"
|
||||
body="$(cat "$resp_file")"
|
||||
echo "$body"
|
||||
|
||||
if [ "$status" != "200" ]; then
|
||||
echo "::error::OFL pending record failed for vendor=$company (PR #$pr_number): HTTP $status"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -32,22 +32,13 @@ jobs:
|
||||
}
|
||||
|
||||
const allowedLabels = [
|
||||
// kind of change
|
||||
'crash',
|
||||
'bug-fix',
|
||||
'SECURITY',
|
||||
'enhancement',
|
||||
'QoL',
|
||||
'optimization',
|
||||
// area
|
||||
'UI/UX',
|
||||
'profile',
|
||||
'Localization',
|
||||
// infrastructure
|
||||
'build',
|
||||
'test',
|
||||
'dependencies',
|
||||
'documentation'
|
||||
'profile',
|
||||
'QoL',
|
||||
'UI/UX',
|
||||
'dependencies'
|
||||
];
|
||||
const pr = context.payload.pull_request;
|
||||
const labelsList = `${allowedLabels
|
||||
@@ -191,22 +182,13 @@ jobs:
|
||||
}
|
||||
|
||||
const allowedLabels = [
|
||||
// kind of change
|
||||
'crash',
|
||||
'bug-fix',
|
||||
'SECURITY',
|
||||
'enhancement',
|
||||
'QoL',
|
||||
'optimization',
|
||||
// area
|
||||
'UI/UX',
|
||||
'profile',
|
||||
'Localization',
|
||||
// infrastructure
|
||||
'build',
|
||||
'test',
|
||||
'dependencies',
|
||||
'documentation'
|
||||
'profile',
|
||||
'QoL',
|
||||
'UI/UX',
|
||||
'dependencies'
|
||||
];
|
||||
|
||||
const issue = context.payload.issue;
|
||||
|
||||
@@ -1,933 +0,0 @@
|
||||
name: PR Merge Bot
|
||||
|
||||
# Merges a pull request on request from a delegated vendor profile maintainer.
|
||||
# The merge is performed by this workflow's GITHUB_TOKEN, so a delegate needs no
|
||||
# repository access.
|
||||
#
|
||||
# Commands, posted as a comment on the PR:
|
||||
# /bot merge squash-merge the PR
|
||||
# /bot merge --dry-run report the verdict without merging
|
||||
#
|
||||
# Merges only when the commenter holds a grant covering every changed path, the
|
||||
# PR targets main or release/*, and CI is green on the head commit. Otherwise it
|
||||
# comments naming the files that fell outside the grant.
|
||||
#
|
||||
# When a PR touching resources/profiles/** is opened, two labels are applied
|
||||
# independently of the merge command:
|
||||
# profile every changed path is inside resources/profiles/
|
||||
# orca profile partner the PR author holds a grant covering every changed
|
||||
# path, plus a one-time comment explaining /bot merge
|
||||
# Neither label changes what the merge command checks.
|
||||
#
|
||||
# Grants come from the FOLDER_MERGERS variable in the `merge-delegation`
|
||||
# environment: one per line, `account: path`, `#` comments and blank lines
|
||||
# allowed. Paths may contain spaces. A vendor takes two grants, the folder and
|
||||
# its sibling bundle JSON:
|
||||
#
|
||||
# # Acme profiles
|
||||
# vendor-maintainer: resources/profiles/Acme/
|
||||
# vendor-maintainer: resources/profiles/Acme.json
|
||||
#
|
||||
# Edit the grant list (environment scope, so admin only):
|
||||
# gh variable set FOLDER_MERGERS --env merge-delegation --body "$(cat folder-mergers.txt)"
|
||||
# gh variable get FOLDER_MERGERS --env merge-delegation
|
||||
#
|
||||
# Stop all merging without touching this file:
|
||||
# gh variable set MERGE_BOT_DRY_RUN --body true
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
# Labels profile PRs on open, without waiting for a /bot merge command.
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
paths:
|
||||
- 'resources/profiles/**'
|
||||
|
||||
# One merge attempt per PR at a time, so two quick comments cannot race.
|
||||
# Labels run under their own group, so a queued label run is not replaced by
|
||||
# a merge run for the same PR.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.issue.number || github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
merge:
|
||||
# Skips the job unless a PR comment mentions the command.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'issue_comment'
|
||||
&& github.event.issue.pull_request != null
|
||||
&& contains(github.event.comment.body, '/bot merge')
|
||||
permissions:
|
||||
contents: write # pulls.merge
|
||||
pull-requests: write # pulls.merge
|
||||
issues: write # feedback comment + reactions
|
||||
actions: write # re-dispatch build_all.yml after the merge
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# Supplies FOLDER_MERGERS. Must carry no protection rules, or every
|
||||
# delegated merge and partner label run would wait for a human reviewer.
|
||||
environment: merge-delegation
|
||||
steps:
|
||||
- name: Merge PR on behalf of a folder delegate
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
# Read as env vars, never interpolated into the script body.
|
||||
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
|
||||
MERGE_BOT_DRY_RUN: ${{ vars.MERGE_BOT_DRY_RUN }}
|
||||
with:
|
||||
script: |
|
||||
function isPermissionDenied(error) {
|
||||
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const MARKER = '<!-- pr-merge-bot -->';
|
||||
// No grant may reach outside this root.
|
||||
const DELEGATABLE_ROOT = 'resources/profiles/';
|
||||
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
|
||||
const MERGE_METHOD = 'squash';
|
||||
const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml
|
||||
const LISTFILES_CAP = 3000;
|
||||
const MAX_REPORTED_FILES = 12;
|
||||
const MERGEABLE_ATTEMPTS = 5;
|
||||
const MERGEABLE_DELAY_MS = 2000;
|
||||
const OK_CONCLUSIONS = new Set(['success', 'neutral', 'skipped']);
|
||||
const REGULAR_FILE_MODES = new Set(['100644', '100755']);
|
||||
|
||||
// Paths refused whatever the grants say. Checked before grants, so
|
||||
// delegating a new root means removing it from this list too.
|
||||
const DENIED_PATTERNS = [
|
||||
/^\.github\//,
|
||||
/(^|\/)\.git(attributes|modules|ignore|config)$/,
|
||||
/^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//,
|
||||
/(^|\/)cmakelists\.txt$/,
|
||||
/\.cmake$/,
|
||||
/^build_[^/]*\.(?:sh|bat)$/,
|
||||
/^version\.inc$/,
|
||||
// Executables, including those inside the delegatable root.
|
||||
/\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/
|
||||
];
|
||||
|
||||
function parseGrants(raw) {
|
||||
// GitHub login: 1-39 chars, alphanumerics with single interior hyphens.
|
||||
const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
||||
const grantsByLogin = new Map();
|
||||
const problems = [];
|
||||
|
||||
(raw || '').split(/\r?\n/).forEach((rawLine, index) => {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Splits on the first colon only, so paths may contain ':' and spaces.
|
||||
const separator = line.indexOf(':');
|
||||
if (separator === -1) {
|
||||
problems.push(`line ${index + 1}: expected \`account: path\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const login = line.slice(0, separator).trim().replace(/^@/, '');
|
||||
const path = line.slice(separator + 1).trim().replace(/\/+$/, '');
|
||||
|
||||
if (!loginPattern.test(login)) {
|
||||
problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`);
|
||||
return;
|
||||
}
|
||||
if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) {
|
||||
problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`);
|
||||
return;
|
||||
}
|
||||
// Rejects anything outside the root, and the bare root itself.
|
||||
if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) {
|
||||
problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = login.toLowerCase();
|
||||
grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path));
|
||||
});
|
||||
|
||||
return { grantsByLogin, problems };
|
||||
}
|
||||
|
||||
function isDenied(path) {
|
||||
if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalized = path.normalize('NFKC').toLowerCase();
|
||||
return DENIED_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
// Byte-exact match on directory boundaries, so a grant of
|
||||
// `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`.
|
||||
function isGranted(path, grants) {
|
||||
return grants.some((grant) => path === grant || path.startsWith(`${grant}/`));
|
||||
}
|
||||
|
||||
// Both endpoints of a rename; both must satisfy the grant.
|
||||
function pathsFor(file) {
|
||||
return [file.filename, file.previous_filename].filter(Boolean);
|
||||
}
|
||||
|
||||
function formatList(items) {
|
||||
const unique = [...new Set(items)];
|
||||
const shown = unique.slice(0, MAX_REPORTED_FILES).map((item) => `- \`${item}\``);
|
||||
if (unique.length > MAX_REPORTED_FILES) {
|
||||
shown.push(`- …and ${unique.length - MAX_REPORTED_FILES} more`);
|
||||
}
|
||||
return shown.join('\n');
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const issue = context.payload.issue;
|
||||
const comment = context.payload.comment;
|
||||
|
||||
if (!issue.pull_request) {
|
||||
core.info('Ignoring comment that is not on a pull request.');
|
||||
return;
|
||||
}
|
||||
// Ignores a comment whose sender is not its author.
|
||||
if (context.payload.action !== 'created' || context.payload.sender.login !== comment.user.login) {
|
||||
core.warning('Ignoring comment whose sender does not match its author.');
|
||||
return;
|
||||
}
|
||||
if (comment.user.type !== 'User') {
|
||||
core.info('Ignoring bot-authored command.');
|
||||
return;
|
||||
}
|
||||
|
||||
const commandLine = (comment.body || '')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => /^\/bot\s+merge\b/i.test(line));
|
||||
|
||||
if (!commandLine) {
|
||||
core.info('No /bot merge command found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const commenter = comment.user.login;
|
||||
const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS);
|
||||
const grants = grantsByLogin.get(commenter.toLowerCase()) || [];
|
||||
|
||||
for (const problem of problems) {
|
||||
core.warning(`FOLDER_MERGERS ${problem}`);
|
||||
}
|
||||
|
||||
// Says nothing to accounts with no grant, so it cannot be used to spam.
|
||||
if (!grants.length) {
|
||||
core.info(`Ignoring /bot merge from @${commenter}: not listed in FOLDER_MERGERS.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Warns instead of failing when the token cannot post feedback.
|
||||
async function bestEffort(call, warning) {
|
||||
try {
|
||||
await call();
|
||||
} catch (error) {
|
||||
if (isPermissionDenied(error)) {
|
||||
core.warning(warning);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const react = (content) => bestEffort(
|
||||
() => github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content }),
|
||||
`Cannot add the "${content}" reaction because the token cannot write.`);
|
||||
|
||||
const say = (body) => bestEffort(
|
||||
() => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n${body}` }),
|
||||
'Cannot post a comment because the token cannot write comments.');
|
||||
|
||||
// Declines the command: warns in the log, reacts, explains on the PR.
|
||||
async function refuse(reason) {
|
||||
const configNote = problems.length
|
||||
? `\n\n\`FOLDER_MERGERS\` also has problems a maintainer needs to fix:\n${problems.map((problem) => `- ${problem}`).join('\n')}`
|
||||
: '';
|
||||
const grantsNote = `\n\n<details><summary>Your current grants</summary>\n\n${formatList(grants)}\n\n</details>`;
|
||||
|
||||
core.warning(`Refused /bot merge from @${commenter}: ${reason}`);
|
||||
await react('-1');
|
||||
await say(`@${commenter} I can't merge this PR: ${reason}${configNote}${grantsNote}`);
|
||||
}
|
||||
|
||||
await react('eyes');
|
||||
|
||||
const args = (commandLine.match(/^\/bot\s+merge\s*(.*)$/i)[1] || '').trim().split(/\s+/).filter(Boolean);
|
||||
const unknownArgs = args.filter((arg) => arg.toLowerCase() !== '--dry-run');
|
||||
const dryRun = String(process.env.MERGE_BOT_DRY_RUN || '').toLowerCase() === 'true'
|
||||
|| unknownArgs.length !== args.length;
|
||||
|
||||
if (unknownArgs.length) {
|
||||
return refuse(
|
||||
`I don't understand ${unknownArgs.map((arg) => `\`${arg}\``).join(', ')}. ` +
|
||||
'Usage: `/bot merge` or `/bot merge --dry-run`.'
|
||||
);
|
||||
}
|
||||
|
||||
// Refuses everything while the grant list is malformed.
|
||||
if (problems.length) {
|
||||
return refuse(
|
||||
'the `FOLDER_MERGERS` grant list has malformed lines, so I refuse every merge until it is fixed.'
|
||||
);
|
||||
}
|
||||
|
||||
let { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue.number
|
||||
});
|
||||
|
||||
if (pr.merged) {
|
||||
return refuse('it is already merged.');
|
||||
}
|
||||
if (pr.state !== 'open') {
|
||||
return refuse(`its state is \`${pr.state}\`, not \`open\`.`);
|
||||
}
|
||||
if (pr.draft) {
|
||||
return refuse('it is still a draft. Mark it ready for review first.');
|
||||
}
|
||||
if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) {
|
||||
return refuse(`it targets \`${pr.base.ref}\`. Delegated merges are only allowed into \`main\` and \`release/*\`.`);
|
||||
}
|
||||
|
||||
// ---- folder scope ----
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
if (!files.length) {
|
||||
return refuse('it changes no files, so there is nothing to verify or merge.');
|
||||
}
|
||||
// Refuses when the file list is truncated or disagrees with the PR.
|
||||
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
|
||||
return refuse(
|
||||
`it reports ${pr.changed_files} changed files but the API listed ${files.length}, ` +
|
||||
'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.'
|
||||
);
|
||||
}
|
||||
|
||||
const deniedFiles = [];
|
||||
const outsideFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
for (const path of pathsFor(file)) {
|
||||
if (isDenied(path)) {
|
||||
deniedFiles.push(path);
|
||||
} else if (!isGranted(path, grants)) {
|
||||
outsideFiles.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deniedFiles.length) {
|
||||
core.error(`@${commenter} attempted a delegated merge touching protected paths: ${deniedFiles.join(', ')}`);
|
||||
return refuse(
|
||||
'it touches paths that are never delegatable, whatever the grants say ' +
|
||||
`(CI, build, scripts or executable files):\n\n${formatList(deniedFiles)}\n\nA maintainer should look at this before it goes any further.`
|
||||
);
|
||||
}
|
||||
if (outsideFiles.length) {
|
||||
return refuse(
|
||||
`${outsideFiles.length} changed path(s) fall outside your grants:\n\n${formatList(outsideFiles)}\n\n` +
|
||||
'A vendor needs both grants: `resources/profiles/<Vendor>/` **and** `resources/profiles/<Vendor>.json`.'
|
||||
);
|
||||
}
|
||||
|
||||
// ---- file modes: rejects symlinks and submodules ----
|
||||
// Fetches the delegatable subtree only; listFiles does not report modes.
|
||||
const headSha = pr.head.sha;
|
||||
const { data: tree } = await github.rest.git.getTree({
|
||||
owner,
|
||||
repo,
|
||||
tree_sha: `${headSha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`,
|
||||
recursive: 'true'
|
||||
});
|
||||
|
||||
if (tree.truncated) {
|
||||
return refuse('the git tree is too large to verify file modes. A maintainer must merge this one.');
|
||||
}
|
||||
|
||||
// Entry paths are subtree-relative.
|
||||
const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode]));
|
||||
const irregularFiles = files
|
||||
.filter((file) => file.status !== 'removed')
|
||||
.map((file) => [file.filename, modesByPath.get(file.filename)])
|
||||
.filter(([, mode]) => !REGULAR_FILE_MODES.has(mode))
|
||||
.map(([path, mode]) => `${path} (mode ${mode || 'missing'})`);
|
||||
|
||||
if (irregularFiles.length) {
|
||||
core.error(`@${commenter} attempted a delegated merge with non-regular files: ${irregularFiles.join(', ')}`);
|
||||
return refuse(
|
||||
`it adds symlinks, submodules or files I cannot verify:\n\n${formatList(irregularFiles)}\n\nA maintainer should look at this before it goes any further.`
|
||||
);
|
||||
}
|
||||
|
||||
// ---- mergeability: waits for GitHub to compute it ----
|
||||
for (let attempt = 0; pr.mergeable === null && attempt < MERGEABLE_ATTEMPTS; attempt += 1) {
|
||||
core.info(`Mergeability not computed yet; retrying in ${MERGEABLE_DELAY_MS}ms.`);
|
||||
await sleep(MERGEABLE_DELAY_MS);
|
||||
({ data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number
|
||||
}));
|
||||
}
|
||||
|
||||
if (pr.mergeable === null) {
|
||||
return refuse('GitHub is still working out whether it can be merged. Try `/bot merge` again in a minute.');
|
||||
}
|
||||
if (!pr.mergeable) {
|
||||
return refuse(`it is not mergeable (\`${pr.mergeable_state}\`) - most likely a conflict with \`${pr.base.ref}\`.`);
|
||||
}
|
||||
|
||||
// ---- CI on the head commit ----
|
||||
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
|
||||
owner,
|
||||
repo,
|
||||
ref: headSha,
|
||||
filter: 'latest',
|
||||
per_page: 100
|
||||
});
|
||||
const pendingChecks = checkRuns.filter((run) => run.status !== 'completed');
|
||||
const failedChecks = checkRuns.filter((run) => run.status === 'completed' && !OK_CONCLUSIONS.has(run.conclusion));
|
||||
|
||||
if (pendingChecks.length) {
|
||||
return refuse(
|
||||
`${pendingChecks.length} check(s) are still running on \`${headSha.slice(0, 7)}\`:\n\n` +
|
||||
`${formatList(pendingChecks.map((run) => run.name))}\n\nRe-run \`/bot merge\` once they finish.`
|
||||
);
|
||||
}
|
||||
if (failedChecks.length) {
|
||||
return refuse(
|
||||
`${failedChecks.length} check(s) are not green on \`${headSha.slice(0, 7)}\`:\n\n` +
|
||||
formatList(failedChecks.map((run) => `${run.name} (${run.conclusion})`))
|
||||
);
|
||||
}
|
||||
|
||||
const { data: combined } = await github.rest.repos.getCombinedStatusForRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: headSha
|
||||
});
|
||||
// total_count 0 only means there are no legacy statuses.
|
||||
if (combined.total_count > 0 && combined.state !== 'success') {
|
||||
return refuse(
|
||||
`the combined commit status on \`${headSha.slice(0, 7)}\` is \`${combined.state}\`:\n\n` +
|
||||
formatList(combined.statuses.filter((status) => status.state !== 'success')
|
||||
.map((status) => `${status.context} (${status.state})`))
|
||||
);
|
||||
}
|
||||
|
||||
// Requires the check to have actually run, not merely to have not failed.
|
||||
const requiredCheck = checkRuns.find((run) =>
|
||||
run.name === REQUIRED_CHECK &&
|
||||
run.app && run.app.slug === 'github-actions' &&
|
||||
run.status === 'completed' && OK_CONCLUSIONS.has(run.conclusion));
|
||||
|
||||
if (!requiredCheck) {
|
||||
return refuse(
|
||||
`the \`${REQUIRED_CHECK}\` check has not succeeded on \`${headSha.slice(0, 7)}\`. ` +
|
||||
'If it never ran, a maintainer needs to approve the workflow run first.'
|
||||
);
|
||||
}
|
||||
|
||||
const scopeSummary = `${files.length} file(s), all within:\n${formatList(grants)}`;
|
||||
|
||||
if (dryRun) {
|
||||
core.info('Dry run: every gate passed, not merging.');
|
||||
await react('+1');
|
||||
await say(
|
||||
`@${commenter} **dry run** - this PR passes every gate and I *would* squash-merge it ` +
|
||||
`at \`${headSha.slice(0, 7)}\`.\n\nVerified scope: ${scopeSummary}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- re-validate, then merge ----
|
||||
// An unchanged head SHA means the verified file list still holds.
|
||||
const { data: fresh } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
|
||||
if (fresh.head.sha !== headSha || fresh.base.ref !== pr.base.ref || fresh.state !== 'open' || fresh.merged || fresh.draft) {
|
||||
return refuse('it changed while I was checking it. Nothing was merged - re-run `/bot merge`.');
|
||||
}
|
||||
|
||||
let merged;
|
||||
try {
|
||||
// Pinned to the verified head: a moved head fails with 409.
|
||||
({ data: merged } = await github.rest.pulls.merge({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
sha: headSha,
|
||||
merge_method: MERGE_METHOD,
|
||||
commit_title: `${pr.title} (#${pr.number})`,
|
||||
commit_message:
|
||||
`Merged by /bot merge on behalf of @${commenter} (id ${comment.user.id}).\n` +
|
||||
`Grants: ${grants.join(', ')}\nHead: ${headSha}\n`
|
||||
}));
|
||||
} catch (error) {
|
||||
const hint = {
|
||||
403: 'the workflow token cannot write to the repository.',
|
||||
405: 'GitHub refused the merge - branch protection, a required review or check, a newly added CODEOWNERS file, or squash merging being disabled.',
|
||||
409: `the head commit moved after I verified it (was \`${headSha.slice(0, 7)}\`).`,
|
||||
422: 'GitHub rejected the merge as invalid.'
|
||||
}[error.status];
|
||||
|
||||
if (!hint) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await refuse(`${hint}\n\n> ${error.message}\n\nNothing was merged.`);
|
||||
core.setFailed(`Delegated merge failed: ${error.status} ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Merged #${pr.number} as ${merged.sha}.`);
|
||||
await react('rocket');
|
||||
await say(
|
||||
`@${commenter} squash-merged into \`${pr.base.ref}\` as ${merged.sha}.\n\nVerified scope: ${scopeSummary}`
|
||||
);
|
||||
|
||||
// ---- re-kick the build ----
|
||||
// A GITHUB_TOKEN merge fires no push event, so build_all.yml would
|
||||
// otherwise never see these files.
|
||||
try {
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'build_all.yml',
|
||||
ref: pr.base.ref
|
||||
});
|
||||
core.info(`Dispatched build_all.yml on ${pr.base.ref}.`);
|
||||
} catch (error) {
|
||||
core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`);
|
||||
}
|
||||
// ---- re-kick the profile publish ----
|
||||
// Same reason as above: post_merge_profiles.yml is push-triggered, so a
|
||||
// GITHUB_TOKEN merge never starts it. workflow_dispatch skips the paths:
|
||||
// filter, so only dispatch when the PR actually touched profiles.
|
||||
const touchesProfiles = files.some((file) =>
|
||||
[file.filename, file.previous_filename]
|
||||
.filter(Boolean)
|
||||
.some((p) => p.startsWith('resources/profiles/'))
|
||||
);
|
||||
if (touchesProfiles) {
|
||||
try {
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'post_merge_profiles.yml',
|
||||
ref: pr.base.ref
|
||||
});
|
||||
core.info(`Dispatched post_merge_profiles.yml on ${pr.base.ref}.`);
|
||||
} catch (error) {
|
||||
core.warning(`Merged successfully, but dispatching post_merge_profiles.yml failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
label-profile:
|
||||
# Independent of the merge rules: any PR that changes only files inside
|
||||
# resources/profiles/ is labeled `profile`.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'pull_request_target'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Label profile-only PRs
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
function isPermissionDenied(error) {
|
||||
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
||||
}
|
||||
|
||||
const PROFILE_ROOT = 'resources/profiles/';
|
||||
const LABEL = 'profile';
|
||||
const LISTFILES_CAP = 3000;
|
||||
const ATTEMPTS = 3;
|
||||
|
||||
function profileOnlyProblem(pr, files) {
|
||||
if (!files.length) {
|
||||
return 'PR changes no files; not labeling.';
|
||||
}
|
||||
// A truncated list, or a count that disagrees with the PR, cannot
|
||||
// prove "only profile files".
|
||||
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
|
||||
return `PR reports ${pr.changed_files} changed files but the API listed ${files.length}; not labeling.`;
|
||||
}
|
||||
|
||||
// Both endpoints of a rename count, so a move out of the profile
|
||||
// root is not mistaken for a profile-only change.
|
||||
const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
|
||||
const outside = paths.filter((path) => !path.startsWith(PROFILE_ROOT));
|
||||
|
||||
if (outside.length) {
|
||||
return `${outside.length} changed path(s) fall outside ${PROFILE_ROOT}; not labeling.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const number = context.payload.pull_request.number;
|
||||
|
||||
// The event payload is frozen at `opened`; listFiles is not. Read
|
||||
// fresh PR metadata and retry if either side of the diff changes.
|
||||
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
||||
|
||||
if (pr.state !== 'open') {
|
||||
core.info(`PR is ${pr.state}; not labeling.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
const problem = profileOnlyProblem(pr, files);
|
||||
|
||||
const { data: after } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
||||
if (
|
||||
after.state !== 'open' ||
|
||||
after.head.sha !== pr.head.sha ||
|
||||
after.base.ref !== pr.base.ref ||
|
||||
after.base.sha !== pr.base.sha
|
||||
) {
|
||||
core.info('PR changed while listing files; retrying.');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (problem) {
|
||||
core.info(problem);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
labels: [LABEL]
|
||||
});
|
||||
core.info(`Applied the "${LABEL}" label.`);
|
||||
} catch (error) {
|
||||
if (isPermissionDenied(error)) {
|
||||
core.warning(`Cannot add the "${LABEL}" label because the token cannot write.`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
core.warning('PR kept changing during verification; not labeling.');
|
||||
|
||||
label-profile-partner:
|
||||
# Labels a profile PR whose author holds a grant covering every changed
|
||||
# path, and explains the /bot merge command to them once.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'pull_request_target'
|
||||
permissions:
|
||||
contents: read # delegatable subtree, for file modes
|
||||
pull-requests: write
|
||||
issues: write # label + comment
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# Supplies FOLDER_MERGERS. Must carry no protection rules, or every
|
||||
# qualifying PR open would wait for a human reviewer.
|
||||
environment: merge-delegation
|
||||
steps:
|
||||
- name: Label profile PRs from delegated maintainers
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
# Read as an env var, never interpolated into the script body.
|
||||
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
|
||||
with:
|
||||
script: |
|
||||
function isPermissionDenied(error) {
|
||||
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
||||
}
|
||||
|
||||
// Never prints the grant list: this job posts public comments and
|
||||
// its logs are public too.
|
||||
async function bestEffort(call, warning) {
|
||||
try {
|
||||
await call();
|
||||
} catch (error) {
|
||||
if (isPermissionDenied(error)) {
|
||||
core.warning(warning);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const MARKER = '<!-- profile-partner-bot -->';
|
||||
const LABEL = 'orca profile partner';
|
||||
const ATTEMPTS = 3;
|
||||
|
||||
// ---- scope rules, mirrored from the merge job above ----
|
||||
// Change both together: these decide whether a delegate could merge.
|
||||
const DELEGATABLE_ROOT = 'resources/profiles/';
|
||||
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
|
||||
const LISTFILES_CAP = 3000;
|
||||
const REGULAR_FILE_MODES = new Set(['100644', '100755']);
|
||||
|
||||
const DENIED_PATTERNS = [
|
||||
/^\.github\//,
|
||||
/(^|\/)\.git(attributes|modules|ignore|config)$/,
|
||||
/^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//,
|
||||
/(^|\/)cmakelists\.txt$/,
|
||||
/\.cmake$/,
|
||||
/^build_[^/]*\.(?:sh|bat)$/,
|
||||
/^version\.inc$/,
|
||||
// Executables, including those inside the delegatable root.
|
||||
/\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/
|
||||
];
|
||||
|
||||
function parseGrants(raw) {
|
||||
// GitHub login: 1-39 chars, alphanumerics with single interior hyphens.
|
||||
const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
||||
const grantsByLogin = new Map();
|
||||
const problems = [];
|
||||
|
||||
(raw || '').split(/\r?\n/).forEach((rawLine, index) => {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Splits on the first colon only, so paths may contain ':' and spaces.
|
||||
const separator = line.indexOf(':');
|
||||
if (separator === -1) {
|
||||
problems.push(`line ${index + 1}: expected \`account: path\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const login = line.slice(0, separator).trim().replace(/^@/, '');
|
||||
const path = line.slice(separator + 1).trim().replace(/\/+$/, '');
|
||||
|
||||
if (!loginPattern.test(login)) {
|
||||
problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`);
|
||||
return;
|
||||
}
|
||||
if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) {
|
||||
problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`);
|
||||
return;
|
||||
}
|
||||
// Rejects anything outside the root, and the bare root itself.
|
||||
if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) {
|
||||
problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = login.toLowerCase();
|
||||
grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path));
|
||||
});
|
||||
|
||||
return { grantsByLogin, problems };
|
||||
}
|
||||
|
||||
function isDenied(path) {
|
||||
if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalized = path.normalize('NFKC').toLowerCase();
|
||||
return DENIED_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
// Byte-exact match on directory boundaries, so a grant of
|
||||
// `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`.
|
||||
function isGranted(path, grants) {
|
||||
return grants.some((grant) => path === grant || path.startsWith(`${grant}/`));
|
||||
}
|
||||
|
||||
// Both endpoints of a rename; both must satisfy the grant.
|
||||
function pathsFor(file) {
|
||||
return [file.filename, file.previous_filename].filter(Boolean);
|
||||
}
|
||||
// ---- end mirrored rules ----
|
||||
|
||||
function scopeProblem(pr, files, grants) {
|
||||
if (!files.length) {
|
||||
return 'PR changes no files; not labeling.';
|
||||
}
|
||||
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
|
||||
return `PR reports ${pr.changed_files} changed files but the API listed ${files.length}; not labeling.`;
|
||||
}
|
||||
|
||||
let outsideCount = 0;
|
||||
for (const file of files) {
|
||||
for (const path of pathsFor(file)) {
|
||||
if (isDenied(path) || !isGranted(path, grants)) {
|
||||
outsideCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (outsideCount) {
|
||||
return `PR has ${outsideCount} path(s) outside @${author}'s grants; not labeling.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- file modes: rejects symlinks and submodules ----
|
||||
function modeProblem(files, tree) {
|
||||
if (tree.truncated) {
|
||||
return 'The profile tree is too large to verify file modes; not labeling.';
|
||||
}
|
||||
|
||||
const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode]));
|
||||
const hasIrregularFile = files.some((file) =>
|
||||
file.status !== 'removed' && !REGULAR_FILE_MODES.has(modesByPath.get(file.filename)));
|
||||
|
||||
if (hasIrregularFile) {
|
||||
return 'PR adds symlinks, submodules or files whose modes cannot be verified; not labeling.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const number = context.payload.pull_request.number;
|
||||
const author = context.payload.pull_request.user.login;
|
||||
|
||||
const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS);
|
||||
|
||||
// Only the count: the malformed lines may name grant holders.
|
||||
if (problems.length) {
|
||||
core.warning(`FOLDER_MERGERS has ${problems.length} malformed line(s); not labeling.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const grants = grantsByLogin.get(author.toLowerCase()) || [];
|
||||
// Says nothing to accounts with no grant, so it cannot be used to spam.
|
||||
if (!grants.length) {
|
||||
core.info(`Ignoring PR from @${author}: not listed in FOLDER_MERGERS.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read current PR metadata for the file list and head tree. Retry
|
||||
// if either side of the diff changes during verification.
|
||||
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
||||
|
||||
if (pr.state !== 'open') {
|
||||
core.info(`PR is ${pr.state}; not labeling.`);
|
||||
return;
|
||||
}
|
||||
if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) {
|
||||
core.info(`PR targets "${pr.base.ref}", not main or release/*; not labeling.`);
|
||||
return;
|
||||
}
|
||||
// Checked before listing files, so a PR too large to list is
|
||||
// rejected in one call.
|
||||
if (pr.changed_files >= LISTFILES_CAP) {
|
||||
core.info(`PR changes ${pr.changed_files} files, more than the API can list; not labeling.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
const scopeIssue = scopeProblem(pr, files, grants);
|
||||
|
||||
let modeIssue = null;
|
||||
if (!scopeIssue) {
|
||||
const { data: tree } = await github.rest.git.getTree({
|
||||
owner,
|
||||
repo,
|
||||
tree_sha: `${pr.head.sha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`,
|
||||
recursive: 'true'
|
||||
});
|
||||
modeIssue = modeProblem(files, tree);
|
||||
}
|
||||
|
||||
const { data: after } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
||||
if (
|
||||
after.state !== 'open' ||
|
||||
after.head.sha !== pr.head.sha ||
|
||||
after.base.ref !== pr.base.ref ||
|
||||
after.base.sha !== pr.base.sha
|
||||
) {
|
||||
core.info('PR changed while verifying; retrying.');
|
||||
continue;
|
||||
}
|
||||
|
||||
const problem = scopeIssue || modeIssue;
|
||||
if (problem) {
|
||||
core.info(problem);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- label + one-time comment ----
|
||||
await bestEffort(
|
||||
() => github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [LABEL] }),
|
||||
`Cannot add the "${LABEL}" label because the token cannot write.`);
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
if (comments.some((comment) => (comment.body || '').includes(MARKER))) {
|
||||
core.info('Partner notice already present; skipping comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
await bestEffort(
|
||||
() => github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body:
|
||||
`${MARKER}\n` +
|
||||
`Hi @${author}, this profile PR is covered by your delegated merge grant.\n\n` +
|
||||
`Once it is ready for review and CI is green, you can merge it yourself:\n\n` +
|
||||
`- \`/bot merge\` - squash-merge into \`main\` or \`release/*\`\n` +
|
||||
`- \`/bot merge --dry-run\` - report the verdict without merging\n\n` +
|
||||
`The bot re-checks the scope, the file modes and the \`Check profiles\` check at merge time.`
|
||||
}),
|
||||
'Cannot post the partner notice because the token cannot write comments.');
|
||||
|
||||
core.info(`Applied the "${LABEL}" label and posted the /bot merge notice.`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.warning('PR kept changing during verification; not labeling.');
|
||||
@@ -44,14 +44,6 @@ jobs:
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
# run_unit_tests.sh installs the plugin tests' numpy with the uv the build stages
|
||||
# beside them; the Windows arm64 build bundles none, so put one on PATH there.
|
||||
- name: Install uv
|
||||
if: runner.os == 'Windows' && runner.arch == 'ARM64'
|
||||
uses: astral-sh/setup-uv@v10.2.0
|
||||
with:
|
||||
version: "0.11.21" # ORCA_UV_VERSION in CMakeLists.txt
|
||||
enable-cache: false
|
||||
- uses: lukka/get-cmake@latest
|
||||
with:
|
||||
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
|
||||
@@ -62,10 +54,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
tar -xvf build_tests.tar
|
||||
# Every platform builds with a multi-config generator (build_linux.sh uses Ninja
|
||||
# Multi-Config), so ctest needs the config: without it, plain add_test() tests
|
||||
# lose their labels and report "Not Run".
|
||||
scripts/run_unit_tests.sh "${{ inputs.test-dir }}" Release
|
||||
# Multi-config generators (Windows/macOS) need a config; Linux is single-config.
|
||||
scripts/run_unit_tests.sh "${{ inputs.test-dir }}" "${{ runner.os != 'Linux' && 'Release' || '' }}"
|
||||
- name: Upload Test Logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Flatpak Unit Tests
|
||||
|
||||
# Run the flatpak build's test asset inside the sandbox, once per arch. The
|
||||
# GNOME SDK's _GLIBCXX_ASSERTIONS gives a bounds-checked STL that catches
|
||||
# out-of-bounds reads no other test leg does.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
os:
|
||||
required: true
|
||||
type: string
|
||||
artifact:
|
||||
description: Test asset uploaded by the flatpak build leg
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
unit_tests_flatpak:
|
||||
name: Flatpak Unit Tests
|
||||
runs-on: ${{ inputs.os }}
|
||||
container:
|
||||
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50
|
||||
options: --privileged
|
||||
steps:
|
||||
- name: Restore test asset
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
- name: Run unit tests (bounds-checked sandbox)
|
||||
timeout-minutes: 20
|
||||
shell: bash
|
||||
run: |
|
||||
tar -xf flatpak-test-asset.tar
|
||||
# Recreate the stable module symlink so /run/build/OrcaSlicer resolves.
|
||||
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* | tail -1)
|
||||
ln -sfn "$(basename "$d")" .flatpak-builder/build/OrcaSlicer
|
||||
# The runtime + SDK + the llvm extension the app metadata references,
|
||||
# which `flatpak build` mounts; best-effort, the image may have them.
|
||||
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak install --user -y --noninteractive flathub \
|
||||
org.gnome.Platform//50 org.gnome.Sdk//50 org.freedesktop.Sdk.Extension.llvm21//25.08 || true
|
||||
# `flatpak build` uses bwrap (no rofiles-fuse, which this container
|
||||
# rejects); bind-mount the build tree so the baked TEST_DATA_DIR resolves.
|
||||
flatpak build --die-with-parent --share=network \
|
||||
--bind-mount=/run/build="$PWD/.flatpak-builder/build" \
|
||||
flatpak_app \
|
||||
bash -c 'cd /run/build/OrcaSlicer && scripts/run_unit_tests.sh build_flatpak/tests'
|
||||
- name: Collect test results
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* 2>/dev/null | tail -1 || true)
|
||||
[ -n "$d" ] && [ -f "$d/ctest_results.xml" ] && cp "$d/ctest_results.xml" ctest_results.xml || true
|
||||
- name: Upload Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-${{ inputs.artifact }}
|
||||
path: ctest_results.xml
|
||||
retention-days: 5
|
||||
if-no-files-found: warn
|
||||
- name: Delete Test Asset
|
||||
if: success()
|
||||
uses: geekyeggo/delete-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
failOnError: false
|
||||
@@ -1,9 +1,7 @@
|
||||
Build
|
||||
Build.bat
|
||||
/build*/
|
||||
/out/
|
||||
CMakeLists.txt.user
|
||||
CMakeUserPresets.json
|
||||
**/CMakeLists.txt.autosave
|
||||
deps/build*
|
||||
MYMETA.json
|
||||
@@ -51,7 +49,3 @@ internal_docs/
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.opc
|
||||
/.test/
|
||||
docs/superpowers/
|
||||
ctest_results.xml
|
||||
|
||||
@@ -20,18 +20,11 @@ cmake --build . --config %build_type% --target ALL_BUILD -- -m
|
||||
Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow.
|
||||
|
||||
```bash
|
||||
cd build && ctest -C Release --output-on-failure # all tests
|
||||
ctest --test-dir ./tests/libslic3r -C Release # individual suite
|
||||
ctest --test-dir ./tests/fff_print -C Release
|
||||
cd build && ctest --output-on-failure # all tests
|
||||
ctest --test-dir ./tests/libslic3r # individual suite
|
||||
ctest --test-dir ./tests/fff_print
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- Docs live in `docs/`; the high-level design of a subsystem goes in `docs/HLSD/<subsystem>.md`.
|
||||
- Describe the design as it stands — what the subsystem does, why it exists, and the constraints that shape it. Not the route that got there: no phases, task lists, status markers, or "before/after this PR" framing.
|
||||
- Planning and investigation output (brainstorms, superpowers design and plan docs) stays in `docs/superpowers/`, which is gitignored. Never commit it.
|
||||
- Write a doc only when the design is not evident from the code, and when a change invalidates an existing one, update it in the same PR.
|
||||
|
||||
## Code Style
|
||||
|
||||
- C++17, selective C++20. PascalCase classes, snake_case functions/variables
|
||||
@@ -63,17 +56,16 @@ ctest --test-dir ./tests/fff_print -C Release
|
||||
- Add helper functions or utilities only when existing code cannot reasonably be reused. Avoid duplication.
|
||||
- Keep code concise and clear. Manually simplify AI generated bloated codes before review.
|
||||
- Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults.
|
||||
- For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.json` was bumped.
|
||||
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) for that language.
|
||||
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) for that language.
|
||||
|
||||
## Localization & translations
|
||||
|
||||
Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`.
|
||||
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_guide.md) for the human-facing version of these principles.
|
||||
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_guide.md) for the human-facing version of these principles.
|
||||
|
||||
### Terminology
|
||||
|
||||
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
|
||||
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
|
||||
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync.
|
||||
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
|
||||
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
|
||||
|
||||
+142
-345
@@ -4,10 +4,6 @@ endif()
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
if(POLICY CMP0177)
|
||||
cmake_policy(SET CMP0177 NEW)
|
||||
endif()
|
||||
|
||||
|
||||
# The following line used to be in tests/CMakeLists.txt
|
||||
# Having it there causes rebuilds of all targets on any CMakeLists.txt change under tests/
|
||||
@@ -56,23 +52,13 @@ You can do this in Environment Variables settings.
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3
|
||||
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||
endif ()
|
||||
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
endif ()
|
||||
|
||||
# Keep MSVC's default /W3 out of CMAKE_<LANG>_FLAGS so it can be applied to our own
|
||||
# targets only. Silencing a bundled target would otherwise override a warning level,
|
||||
# which cl reports as D9025 for every file it compiles.
|
||||
if (POLICY CMP0092)
|
||||
cmake_policy(SET CMP0092 NEW)
|
||||
endif ()
|
||||
|
||||
# project() reads this, so set it first.
|
||||
set(CMAKE_USER_MAKE_RULES_OVERRIDE "${CMAKE_CURRENT_LIST_DIR}/cmake/modules/ClangClShowIncludes.cmake")
|
||||
|
||||
project(OrcaSlicer)
|
||||
|
||||
# Backward compatibility for old CMake versions
|
||||
@@ -102,6 +88,33 @@ else ()
|
||||
add_compile_definitions("$<$<CONFIG:Release>:WXINSPECTOR_DISABLE>")
|
||||
endif ()
|
||||
|
||||
find_package(Git)
|
||||
if(DEFINED ENV{git_commit_hash} AND NOT "$ENV{git_commit_hash}" STREQUAL "")
|
||||
message(STATUS "Specified git commit hash: $ENV{git_commit_hash}")
|
||||
if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||
# Convert the given hash to short hash
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --short "$ENV{git_commit_hash}"
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
else()
|
||||
# No .git directory (e.g., Flatpak sandbox) — truncate directly
|
||||
string(SUBSTRING "$ENV{git_commit_hash}" 0 7 GIT_COMMIT_HASH)
|
||||
endif()
|
||||
add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")
|
||||
elseif(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||
# Check current Git commit hash
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} log -1 --format=%h
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{SLIC3R_STATIC})
|
||||
set(SLIC3R_STATIC_INITIAL $ENV{SLIC3R_STATIC})
|
||||
else()
|
||||
@@ -110,15 +123,11 @@ endif()
|
||||
|
||||
option(SLIC3R_STATIC "Compile OrcaSlicer with static libraries (Boost, TBB)" ${SLIC3R_STATIC_INITIAL})
|
||||
option(SLIC3R_GUI "Compile OrcaSlicer with GUI components (OpenGL, wxWidgets)" 1)
|
||||
option(SLIC3R_CAD "Compile OrcaSlicer with the parametric Design/CAD tab (needs OCCT ModelingAlgorithms)" 1)
|
||||
option(SLIC3R_FHS "Assume OrcaSlicer is to be installed in a FHS directory structure" 0)
|
||||
option(SLIC3R_PROFILE "Compile OrcaSlicer with an invasive Shiny profiler" 0)
|
||||
option(SLIC3R_PCH "Use precompiled headers" 1)
|
||||
option(SLIC3R_WARNINGS "Emit compiler warnings for OrcaSlicer sources" 1)
|
||||
option(SLIC3R_BUNDLED_WARNINGS "Emit compiler warnings for bundled third-party sources" 0)
|
||||
option(SLIC3R_MSVC_COMPILE_PARALLEL "Compile on Visual Studio in parallel" 1)
|
||||
option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1)
|
||||
option(SLIC3R_RELATIVE_DEBUG_PATHS "Record a relative compilation directory in debug info (clang-cl)" 0)
|
||||
option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0)
|
||||
|
||||
# Python stubgen module
|
||||
@@ -279,14 +288,7 @@ if (APPLE)
|
||||
endif()
|
||||
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
|
||||
|
||||
# The macOS CI jobs build with Ninja (build_release_macos.sh -x), so the Xcode generator
|
||||
# is not covered. Xcode adds -Wshorten-64-to-32 by default ("Implicit Conversion to 32 Bit
|
||||
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
|
||||
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
|
||||
|
||||
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
endif ()
|
||||
|
||||
# Proposal for C++ unit tests and sandboxes
|
||||
@@ -317,10 +319,6 @@ if (SLIC3R_GUI)
|
||||
add_definitions(-DSLIC3R_GUI)
|
||||
endif ()
|
||||
|
||||
if (SLIC3R_CAD)
|
||||
add_definitions(-DSLIC3R_CAD)
|
||||
endif ()
|
||||
|
||||
if(SLIC3R_DESKTOP_INTEGRATION)
|
||||
add_definitions(-DSLIC3R_DESKTOP_INTEGRATION)
|
||||
endif ()
|
||||
@@ -337,47 +335,23 @@ if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||
|
||||
# clang-cl can interpret SYSTEM header paths if -imsvc is used
|
||||
set(CMAKE_INCLUDE_SYSTEM_FLAG_CXX "-imsvc")
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall \
|
||||
-Wno-old-style-cast -Wno-reserved-id-macro -Wno-c++98-compat-pedantic")
|
||||
else ()
|
||||
set(IS_CLANG_CL FALSE)
|
||||
endif ()
|
||||
|
||||
if (MSVC)
|
||||
# CMP0092 only applies when the cache is created; an existing tree keeps its /W3,
|
||||
# which a silenced bundled target would then override (D9025, once per file).
|
||||
string(REGEX REPLACE "/W[0-4]" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
|
||||
string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
|
||||
# /MP only matters for the VS generators, where CMake turns it into the
|
||||
# MultiProcessorCompilation property. Ninja parallelises on its own, and
|
||||
# clang-cl warns "argument unused" if the flag reaches it.
|
||||
if (SLIC3R_MSVC_COMPILE_PARALLEL AND CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
if (SLIC3R_MSVC_COMPILE_PARALLEL AND NOT IS_CLANG_CL)
|
||||
add_compile_options(/MP)
|
||||
endif ()
|
||||
|
||||
# Parse lambdas the way the standard says, as clang and GCC already do. Without it
|
||||
# MSVC keeps its legacy lambda processor under /std:c++17 and rejects reading a
|
||||
# constexpr constant inside a lambda that does not capture it (C3493), which no
|
||||
# other compiler requires. Implied by /std:c++20 and /permissive-, so it is only
|
||||
# needed while we are on C++17. clang-cl is conforming already and does not take
|
||||
# the flag. Requires VS2019 16.8 or newer.
|
||||
if (NOT IS_CLANG_CL)
|
||||
# cl.exe only warns (D9002) about an unknown /Zc: option, so without this the
|
||||
# flag would be dropped and the first lambda reading a constexpr constant would
|
||||
# fail with C3493 far from the cause.
|
||||
if (MSVC_VERSION LESS 1928)
|
||||
message(FATAL_ERROR "Visual Studio 2019 16.8 (MSVC 19.28) or newer is required; detected MSVC ${MSVC_VERSION}.")
|
||||
endif ()
|
||||
add_compile_options(/Zc:lambda)
|
||||
endif ()
|
||||
# /bigobj (Increase Number of Sections in .Obj file)
|
||||
add_compile_options(-bigobj)
|
||||
# error C3859: virtual memory range for PCH exceeded; please recompile with a command line option of '-Zm90' or greater
|
||||
# Generate symbols at every build target, even for the release.
|
||||
# -Zm520 fixes error C3859 but forces the compiler to pre-allocate that memory for every translation unit regardless
|
||||
# combining /Zi with /FS frees up a significant amount of memory pressure across all parallel compile jobs and makes /MP faster overall.
|
||||
if (SLIC3R_MSVC_PDB)
|
||||
add_compile_options(/Zi /FS)
|
||||
endif ()
|
||||
add_compile_options(-bigobj /Zi /FS)
|
||||
# Disable STL4007: Many result_type typedefs and all argument_type, first_argument_type, and second_argument_type typedefs are deprecated in C++17.
|
||||
#FIXME Remove this line after eigen library adapts to the new C++17 adaptor rules.
|
||||
add_compile_options(-D_SILENCE_CXX17_ADAPTOR_TYPEDEFS_DEPRECATION_WARNING)
|
||||
@@ -395,16 +369,6 @@ if (MSVC)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /LTCG")
|
||||
endif ()
|
||||
|
||||
# Without this every object names its build directory and two worktrees never
|
||||
# share cache entries. The linker still writes absolute paths into the PDB.
|
||||
if (SLIC3R_RELATIVE_DEBUG_PATHS)
|
||||
if (IS_CLANG_CL)
|
||||
add_compile_options(-ffile-compilation-dir=.)
|
||||
else ()
|
||||
message(WARNING "SLIC3R_RELATIVE_DEBUG_PATHS is only implemented for clang-cl")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (${CMAKE_CXX_COMPILER_ID} STREQUAL "AppleClang" AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER 15)
|
||||
add_compile_definitions(BOOST_NO_CXX98_FUNCTION_BASE _HAS_AUTO_PTR_ETC=0)
|
||||
endif()
|
||||
@@ -559,103 +523,63 @@ if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUXX)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fext-numeric-literals" )
|
||||
endif()
|
||||
|
||||
if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang"))
|
||||
if (IS_CLANG_CL)
|
||||
# clang-cl reads -Wall as MSVC /Wall, which clang maps to -Weverything. /W4 is
|
||||
# its -Wall -Wextra and, unlike /clang:-Wall, is ordered with the -Wno-* below
|
||||
# instead of after them. The -Wextra-only warnings are dropped again so the set
|
||||
# matches what -Wall gives the GNU/Clang builds.
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4" )
|
||||
add_compile_options(-Wno-unused-parameter -Wno-ignored-qualifiers -Wno-missing-field-initializers)
|
||||
elseif (NOT MINGW)
|
||||
if (NOT MSVC AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang"))
|
||||
if (NOT MINGW)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall" )
|
||||
endif ()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder" )
|
||||
|
||||
# Every warning is an error unless it appears in one of the two lists below.
|
||||
# disabled - never wanted. Off everywhere, so it never warns or errors.
|
||||
# demoted - wanted, not cleared yet. Still warns, does not error.
|
||||
|
||||
# Disabled.
|
||||
set(warnings_disabled
|
||||
reorder # members initialised in an order we chose
|
||||
sign-compare # signed/unsigned comparisons throughout
|
||||
misleading-indentation # false positives on mixed tabs and spaces
|
||||
switch # unhandled enum value in a switch
|
||||
unused-function # commented-out or conditionally compiled code
|
||||
unused-variable # commented-out or conditionally compiled code
|
||||
unused-but-set-variable # commented-out or conditionally compiled code
|
||||
unused-label # commented-out or conditionally compiled code
|
||||
unused-local-typedefs # commented-out or conditionally compiled code
|
||||
)
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
list(APPEND warnings_disabled deprecated-declarations) # legacy OpenGL calls
|
||||
endif ()
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 6.0)
|
||||
list(APPEND warnings_disabled ignored-attributes) # from Eigen headers marked SYSTEM
|
||||
endif ()
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
list(APPEND warnings_disabled unknown-pragmas) # igl pragmas, GCC bug 66943
|
||||
endif ()
|
||||
foreach (w IN LISTS warnings_disabled)
|
||||
add_compile_options(-Wno-${w})
|
||||
endforeach ()
|
||||
|
||||
# GCC is not built in CI, so don't throw errors CI won't catch.
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# On GCC and Clang, no return from a non-void function is a warning only. Here, we make it an error.
|
||||
add_compile_options(-Werror=return-type)
|
||||
else ()
|
||||
# Turn everything else into an error. Dependency headers are exempt because the
|
||||
# SYSTEM include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their
|
||||
# diagnostics out.
|
||||
add_compile_options(-Werror)
|
||||
endif ()
|
||||
|
||||
# Demoted. Remove a name once its category is cleared on every compiler.
|
||||
set(warnings_demoted)
|
||||
if (APPLE)
|
||||
list(APPEND warnings_demoted
|
||||
# MacDarkMode.mm makes two calls to AppKit's private titlebarViewController
|
||||
# and one to a wxWidgets category on NSTableColumn whose header is not
|
||||
# imported. Clearing it means declaring the private selectors ourselves, which
|
||||
# needs a macOS build to verify.
|
||||
objc-method-access
|
||||
)
|
||||
endif ()
|
||||
if (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
|
||||
list(APPEND warnings_demoted
|
||||
# About two dozen GetProcAddress casts, most in the vendored dark_mode.hpp,
|
||||
# retype FARPROC to a real signature. The __stdcall typedefs are identical to
|
||||
# FARPROC on x64, so only arm64 reports them. Clearing them is a separate
|
||||
# sweep.
|
||||
cast-function-type-mismatch
|
||||
)
|
||||
endif ()
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
list(APPEND warnings_demoted
|
||||
# enum-constexpr-conversion is a Clang warning that defaults to an error,
|
||||
# present through clang 20 and gone in clang 21.
|
||||
enum-constexpr-conversion
|
||||
)
|
||||
endif ()
|
||||
# Since some portions of code are just commented out or put under conditional compilation, there are
|
||||
# a bunch of warning related to unused functions and variables. Suppress those warnings to not pollute
|
||||
# compilers diagnostics output with warnings we not going to look at
|
||||
add_compile_options(-Wno-unused-function -Wno-unused-variable -Wno-unused-but-set-variable -Wno-unused-label -Wno-unused-local-typedefs)
|
||||
|
||||
# The list mixes names not every compiler has, so add each exception only where the
|
||||
# compiler knows the warning. Probe with the positive -W<name>, which an unknown
|
||||
# warning fails on both compilers (GCC errors, Clang reports unknown-warning-option).
|
||||
# An option that takes a =N argument rejects the bare -W<name>, so fall back to
|
||||
# -W<name>=1 and demote with the trailing =.
|
||||
# Ignore signed/unsigned comparison warnings
|
||||
add_compile_options(-Wno-sign-compare)
|
||||
|
||||
# The mismatch of tabs and spaces throughout the project can sometimes
|
||||
# cause this warning to appear even though the indentation is fine.
|
||||
# Some includes also cause the warning
|
||||
add_compile_options(-Wno-misleading-indentation)
|
||||
|
||||
# Disable warning if enum value does not have a corresponding case in switch statement
|
||||
add_compile_options(-Wno-switch)
|
||||
|
||||
# removes LOTS of extraneous Eigen warnings (GCC only supports it since 6.1)
|
||||
# https://eigen.tuxfamily.org/bz/show_bug.cgi?id=1221
|
||||
if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 6.0)
|
||||
add_compile_options(-Wno-ignored-attributes) # Tamas: Eigen include dirs are marked as SYSTEM
|
||||
endif()
|
||||
|
||||
# Clang reports legacy OpenGL calls as deprecated. Turn off the warning for now
|
||||
# to reduce the clutter, we know about this one. It should be reenabled after
|
||||
# we finally get rid of the deprecated code.
|
||||
if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
|
||||
add_compile_options(-Wno-deprecated-declarations)
|
||||
endif()
|
||||
|
||||
if((${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "AppleClang") AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER 15)
|
||||
include(CheckCXXCompilerFlag)
|
||||
foreach (category IN LISTS warnings_demoted)
|
||||
string(MAKE_C_IDENTIFIER "ORCA_HAS_W_${category}" _orca_has_w)
|
||||
check_cxx_compiler_flag("-W${category}" ${_orca_has_w})
|
||||
if (${_orca_has_w})
|
||||
add_compile_options(-Wno-error=${category})
|
||||
else ()
|
||||
check_cxx_compiler_flag("-W${category}=1" ${_orca_has_w}_arg)
|
||||
if (${${_orca_has_w}_arg})
|
||||
add_compile_options(-Wno-error=${category}=)
|
||||
endif ()
|
||||
endif ()
|
||||
endforeach ()
|
||||
check_cxx_compiler_flag(-Wno-error=enum-constexpr-conversion HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV)
|
||||
if(HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV)
|
||||
add_compile_options(-Wno-error=enum-constexpr-conversion)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
#GCC generates loads of -Wunknown-pragmas when compiling igl. The fix is not easy due to a bug in gcc, see
|
||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66943 or
|
||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431
|
||||
# We will turn the warning of for GCC for now:
|
||||
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
# GCC generates loads of -Wunknown-pragmas when compiling igl. The fix is not easy due to a bug in gcc, see
|
||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66943 or
|
||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431
|
||||
# We will turn the warning of for GCC for now:
|
||||
add_compile_options(-Wno-unknown-pragmas)
|
||||
endif()
|
||||
|
||||
# Compress the debug info with zstd to save space in Flatpak CI builds
|
||||
if(FLATPAK)
|
||||
@@ -665,6 +589,10 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 14)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=template-id-cdtor" )
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
if (SLIC3R_ASAN)
|
||||
@@ -834,37 +762,6 @@ find_package(OpenSSL REQUIRED)
|
||||
find_package(CURL REQUIRED)
|
||||
find_package(Freetype REQUIRED)
|
||||
|
||||
if (SLIC3R_GUI)
|
||||
# LibDataChannel's installed export references its bundled dependencies,
|
||||
# but does not install their CMake targets. Recreate those targets from
|
||||
# the same dependency prefix before loading the LibDataChannel config.
|
||||
if (NOT TARGET Usrsctp::usrsctp)
|
||||
find_library(_ORCA_USRSCTP_LIBRARY NAMES usrsctp
|
||||
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
|
||||
if (_ORCA_USRSCTP_LIBRARY)
|
||||
add_library(Usrsctp::usrsctp UNKNOWN IMPORTED GLOBAL)
|
||||
set_target_properties(Usrsctp::usrsctp PROPERTIES
|
||||
IMPORTED_LOCATION "${_ORCA_USRSCTP_LIBRARY}"
|
||||
IMPORTED_LINK_INTERFACE_LANGUAGES C
|
||||
INTERFACE_LINK_LIBRARIES "Threads::Threads")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT TARGET LibJuice::LibJuice)
|
||||
find_library(_ORCA_LIBJUICE_LIBRARY NAMES juice
|
||||
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
|
||||
if (_ORCA_LIBJUICE_LIBRARY)
|
||||
add_library(LibJuice::LibJuice UNKNOWN IMPORTED GLOBAL)
|
||||
set_target_properties(LibJuice::LibJuice PROPERTIES
|
||||
IMPORTED_LOCATION "${_ORCA_LIBJUICE_LIBRARY}"
|
||||
IMPORTED_LINK_INTERFACE_LANGUAGES C
|
||||
INTERFACE_LINK_LIBRARIES "Threads::Threads")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(LibDataChannel CONFIG REQUIRED)
|
||||
endif()
|
||||
|
||||
|
||||
add_library(libcurl INTERFACE)
|
||||
target_link_libraries(libcurl INTERFACE CURL::libcurl)
|
||||
@@ -1120,157 +1017,77 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
|
||||
${TOP_LEVEL_PROJECT_DIR}/deps/WebView2/lib/win-${_arch}/WebView2Loader.dll
|
||||
DESTINATION ${_out_dir})
|
||||
|
||||
# Stage the OCCT toolkits libslic3r links (published as OCCT_LIBS), not whatever the
|
||||
# deps prefix happens to hold, and fail the configure if one of them is missing.
|
||||
if (NOT OCCT_LIBS)
|
||||
message(FATAL_ERROR "OCCT_LIBS is not set; libslic3r must be configured first.")
|
||||
endif ()
|
||||
set(_occt_bin "${CMAKE_PREFIX_PATH}/bin/occt")
|
||||
set(_occt_dlls "")
|
||||
set(_occt_staged "")
|
||||
set(_missing_occt "")
|
||||
foreach (_tk IN LISTS OCCT_LIBS)
|
||||
if (EXISTS "${_occt_bin}/${_tk}.dll")
|
||||
list(APPEND _occt_dlls "${_occt_bin}/${_tk}.dll")
|
||||
list(APPEND _occt_staged "${_out_dir}/${_tk}.dll")
|
||||
else ()
|
||||
list(APPEND _missing_occt "${_tk}.dll")
|
||||
endif ()
|
||||
endforeach ()
|
||||
if (_missing_occt)
|
||||
message(FATAL_ERROR
|
||||
"OCCT DLLs missing from ${_occt_bin}/: ${_missing_occt}\n"
|
||||
"Rebuild the dependencies (build_release_vs2022.bat deps) with the same "
|
||||
"SLIC3R_CAD setting as this project.")
|
||||
endif ()
|
||||
file(COPY ${_occt_dlls}
|
||||
file(COPY ${CMAKE_PREFIX_PATH}/bin/occt/TKBO.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKBRep.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKCAF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKCDF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKernel.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKG2d.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKG3d.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKGeomAlgo.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKGeomBase.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKHLR.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKLCAF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKMath.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKMesh.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKPrim.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKService.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKShHealing.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKSTEP.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKSTEP209.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKSTEPAttr.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKSTEPBase.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKTopAlgo.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKV3d.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKVCAF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKXCAF.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/freetype.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/avformat-61.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
|
||||
${CMAKE_PREFIX_PATH}/bin/avutil-59.dll
|
||||
DESTINATION ${_out_dir})
|
||||
|
||||
set(_dll_list
|
||||
set(${output_dlls}
|
||||
${_out_dir}/libgmp-10.dll
|
||||
${_out_dir}/libmpfr-4.dll
|
||||
${_out_dir}/WebView2Loader.dll
|
||||
|
||||
${_out_dir}/TKBO.dll
|
||||
${_out_dir}/TKBRep.dll
|
||||
${_out_dir}/TKCAF.dll
|
||||
${_out_dir}/TKCDF.dll
|
||||
${_out_dir}/TKernel.dll
|
||||
${_out_dir}/TKG2d.dll
|
||||
${_out_dir}/TKG3d.dll
|
||||
${_out_dir}/TKGeomAlgo.dll
|
||||
${_out_dir}/TKGeomBase.dll
|
||||
${_out_dir}/TKHLR.dll
|
||||
${_out_dir}/TKLCAF.dll
|
||||
${_out_dir}/TKMath.dll
|
||||
${_out_dir}/TKMesh.dll
|
||||
${_out_dir}/TKPrim.dll
|
||||
${_out_dir}/TKService.dll
|
||||
${_out_dir}/TKShHealing.dll
|
||||
${_out_dir}/TKSTEP.dll
|
||||
${_out_dir}/TKSTEP209.dll
|
||||
${_out_dir}/TKSTEPAttr.dll
|
||||
${_out_dir}/TKSTEPBase.dll
|
||||
${_out_dir}/TKTopAlgo.dll
|
||||
${_out_dir}/TKV3d.dll
|
||||
${_out_dir}/TKVCAF.dll
|
||||
${_out_dir}/TKXCAF.dll
|
||||
${_out_dir}/TKXDESTEP.dll
|
||||
${_out_dir}/TKXSBase.dll
|
||||
|
||||
${_out_dir}/freetype.dll
|
||||
${_out_dir}/avformat-61.dll
|
||||
${_out_dir}/avcodec-61.dll
|
||||
${_out_dir}/swresample-5.dll
|
||||
${_out_dir}/swscale-8.dll
|
||||
${_out_dir}/avutil-59.dll
|
||||
)
|
||||
list(APPEND _dll_list ${_occt_staged})
|
||||
set(${output_dlls} ${_dll_list} PARENT_SCOPE)
|
||||
|
||||
endfunction()
|
||||
|
||||
function(orcaslicer_copy_sos target config postfix output_sos)
|
||||
|
||||
get_property(_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
get_target_property(_alt_out_dir ${target} RUNTIME_OUTPUT_DIRECTORY)
|
||||
|
||||
if (_alt_out_dir)
|
||||
set(_out_dir "${_alt_out_dir}")
|
||||
elseif (_is_multi)
|
||||
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}/${config}")
|
||||
else ()
|
||||
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
endif ()
|
||||
|
||||
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavformat.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61
|
||||
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61.1.100
|
||||
${CMAKE_PREFIX_PATH}/lib/libavcodec.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
|
||||
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
|
||||
${CMAKE_PREFIX_PATH}/lib/libavutil.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libavutil.so.59
|
||||
${CMAKE_PREFIX_PATH}/lib/libavutil.so.59.8.100
|
||||
${CMAKE_PREFIX_PATH}/lib/libswscale.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libswscale.so.8
|
||||
${CMAKE_PREFIX_PATH}/lib/libswscale.so.8.1.100
|
||||
${CMAKE_PREFIX_PATH}/lib/libswresample.so
|
||||
${CMAKE_PREFIX_PATH}/lib/libswresample.so.5
|
||||
${CMAKE_PREFIX_PATH}/lib/libswresample.so.5.1.100
|
||||
DESTINATION ${_out_dir})
|
||||
|
||||
set(${output_sos}
|
||||
${_out_dir}/libavformat.so
|
||||
${_out_dir}/libavformat.so.61
|
||||
${_out_dir}/libavformat.so.61.1.100
|
||||
${_out_dir}/libavcodec.so
|
||||
${_out_dir}/libavcodec.so.61
|
||||
${_out_dir}/libavcodec.so.61.3.100
|
||||
${_out_dir}/libavutil.so
|
||||
${_out_dir}/libavutil.so.59
|
||||
${_out_dir}/libavutil.so.59.8.100
|
||||
${_out_dir}/libswscale.so
|
||||
${_out_dir}/libswscale.so.8
|
||||
${_out_dir}/libswscale.so.8.1.100
|
||||
${_out_dir}/libswresample.so
|
||||
${_out_dir}/libswresample.so.5
|
||||
${_out_dir}/libswresample.so.5.1.100
|
||||
PARENT_SCOPE
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# Bundled sources set their own warning flags, and a plain -Wall there means /Wall
|
||||
# (= -Weverything) under clang-cl. Target options are applied after the ones a target
|
||||
# set on itself, so these win. Targets are discovered rather than listed so a newly
|
||||
# bundled library needs no maintenance here.
|
||||
function(orcaslicer_silence_third_party_warnings _dir)
|
||||
get_property(_subdirs DIRECTORY "${_dir}" PROPERTY SUBDIRECTORIES)
|
||||
foreach (_subdir IN LISTS _subdirs)
|
||||
orcaslicer_silence_third_party_warnings("${_subdir}")
|
||||
endforeach ()
|
||||
get_property(_targets DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS)
|
||||
foreach (_target IN LISTS _targets)
|
||||
get_target_property(_type ${_target} TYPE)
|
||||
if (NOT _type STREQUAL "INTERFACE_LIBRARY" AND NOT _type STREQUAL "UTILITY")
|
||||
if (MSVC AND NOT IS_CLANG_CL)
|
||||
# Drop any level the target set for itself, or -w overrides it and cl
|
||||
# reports D9025 once per file.
|
||||
get_target_property(_opts ${_target} COMPILE_OPTIONS)
|
||||
if (_opts)
|
||||
string(REGEX REPLACE "/W[0-4]|/Wall" "" _opts "${_opts}")
|
||||
string(REGEX REPLACE ";;+" ";" _opts "${_opts}")
|
||||
set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "${_opts}")
|
||||
endif ()
|
||||
# CMake maps a level into the VS generator's WarningLevel element, while a
|
||||
# bare -w stays on the command line and trips D9025 there, once per file.
|
||||
target_compile_options(${_target} PRIVATE /W0)
|
||||
else ()
|
||||
target_compile_options(${_target} PRIVATE -w)
|
||||
endif ()
|
||||
endif ()
|
||||
endforeach ()
|
||||
endfunction()
|
||||
|
||||
|
||||
# libslic3r, OrcaSlicer GUI and the OrcaSlicer executable.
|
||||
add_subdirectory(deps_src)
|
||||
|
||||
if (NOT SLIC3R_BUNDLED_WARNINGS)
|
||||
orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/deps_src")
|
||||
endif ()
|
||||
|
||||
# Warning level for the targets added below: our sources, plus glad and libvgcode,
|
||||
# which are vendored but live under src/. The deps_src libraries were configured just
|
||||
# above. CMP0092 left MSVC without a default level, so it is set here.
|
||||
if (NOT SLIC3R_WARNINGS)
|
||||
add_compile_options(-w)
|
||||
elseif (MSVC AND NOT IS_CLANG_CL)
|
||||
# /we4715 is C4715, no return from a non-void function, an error on the GNU/Clang
|
||||
# builds under -Werror. MSVC is not in that model, so this stays a single promoted
|
||||
# warning.
|
||||
add_compile_options(/W3 /we4715)
|
||||
endif ()
|
||||
|
||||
add_subdirectory(src)
|
||||
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT OrcaSlicer_app_gui)
|
||||
|
||||
@@ -1282,10 +1099,6 @@ endif()
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
if (NOT SLIC3R_BUNDLED_WARNINGS)
|
||||
# Catch2 is vendored under tests/ and sets its own warning flags too.
|
||||
orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/tests/catch2")
|
||||
endif ()
|
||||
endif()
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
@@ -1330,22 +1143,6 @@ else ()
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(LIBRARY_FILES
|
||||
${LIBDIR_BIN}/libavformat.so.61
|
||||
${LIBDIR_BIN}/libavformat.so.61.1.100
|
||||
${LIBDIR_BIN}/libavcodec.so.61
|
||||
${LIBDIR_BIN}/libavcodec.so.61.3.100
|
||||
${LIBDIR_BIN}/libavutil.so.59
|
||||
${LIBDIR_BIN}/libavutil.so.59.8.100
|
||||
${LIBDIR_BIN}/libswresample.so.5
|
||||
${LIBDIR_BIN}/libswresample.so.5.1.100
|
||||
${LIBDIR_BIN}/libswscale.so.8
|
||||
${LIBDIR_BIN}/libswscale.so.8.1.100
|
||||
)
|
||||
install(FILES ${LIBRARY_FILES} DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
|
||||
endif ()
|
||||
|
||||
install(FILES ${CMAKE_SOURCE_DIR}/LICENSE.txt DESTINATION ".")
|
||||
configure_file(${LIBDIR}/dev-utils/platform/unix/fhs.hpp.in ${LIBDIR_BIN}/dev-utils/platform/unix/fhs.hpp)
|
||||
|
||||
|
||||
@@ -260,9 +260,6 @@ if [[ ! -f "./scripts/flatpak/com.orcaslicer.OrcaSlicer.yml" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Packing deps/ for the manifest...${NC}"
|
||||
./scripts/flatpak/make_deps_tar.sh
|
||||
|
||||
# Build the Flatpak
|
||||
echo -e "${YELLOW}Building Flatpak package...${NC}"
|
||||
echo -e "This may take a while (30+ minutes depending on your system)..."
|
||||
|
||||
@@ -567,8 +567,6 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
|
||||
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer
|
||||
echo "Building OrcaSlicer_profile_validator .."
|
||||
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer_profile_validator
|
||||
echo "Building generate_system_cache ..."
|
||||
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target generate_system_cache
|
||||
./scripts/run_gettext.sh
|
||||
fi
|
||||
if [[ -n "${BUILD_TESTS}" ]] ; then
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
set WP=%CD%
|
||||
|
||||
set debug=OFF
|
||||
set debuginfo=OFF
|
||||
if "%1"=="debug" set debug=ON
|
||||
if "%2"=="debug" set debug=ON
|
||||
if "%1"=="debuginfo" set debuginfo=ON
|
||||
if "%2"=="debuginfo" set debuginfo=ON
|
||||
if "%debug%"=="ON" (
|
||||
set build_type=Debug
|
||||
set build_dir=build-dbg
|
||||
) else (
|
||||
if "%debuginfo%"=="ON" (
|
||||
set build_type=RelWithDebInfo
|
||||
set build_dir=build-dbginfo
|
||||
) else (
|
||||
set build_type=Release
|
||||
set build_dir=build
|
||||
)
|
||||
)
|
||||
echo build type set to %build_type%
|
||||
|
||||
cd deps
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
set DEPS=%CD%/OrcaSlicer_dep
|
||||
set "SIG_FLAG="
|
||||
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
|
||||
if "%1"=="slicer" (
|
||||
GOTO :slicer
|
||||
)
|
||||
echo "building deps.."
|
||||
|
||||
echo cmake ../ -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake ../ -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target deps -- -m
|
||||
|
||||
if "%1"=="deps" exit /b 0
|
||||
|
||||
:slicer
|
||||
echo "building Orca Slicer..."
|
||||
cd %WP%
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
|
||||
echo cmake .. -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake .. -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type% %SIG_FLAG%
|
||||
cmake --build . --config %build_type% --target ALL_BUILD -- -m
|
||||
cd ..
|
||||
call scripts/run_gettext.bat
|
||||
cd %build_dir%
|
||||
cmake --build . --target install --config %build_type%
|
||||
@@ -4,7 +4,7 @@ set -e
|
||||
set -o pipefail
|
||||
SECONDS=0
|
||||
|
||||
while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
while getopts ":dpa:snt:xbc:i:1Tuh" opt; do
|
||||
case "${opt}" in
|
||||
d )
|
||||
export BUILD_TARGET="deps"
|
||||
@@ -38,8 +38,8 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
i )
|
||||
export CMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH:+$CMAKE_IGNORE_PREFIX_PATH;}$OPTARG"
|
||||
;;
|
||||
j )
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL="$OPTARG"
|
||||
1 )
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL=1
|
||||
;;
|
||||
T )
|
||||
export BUILD_TESTS="1"
|
||||
@@ -53,12 +53,12 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
echo " -s: Build slicer only"
|
||||
echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)"
|
||||
echo " -n: Nightly build"
|
||||
echo " -t: Specify minimum version of the target platform, default is 12.0"
|
||||
echo " -t: Specify minimum version of the target platform, default is 11.3"
|
||||
echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode"
|
||||
echo " -b: Build without reconfiguring CMake"
|
||||
echo " -c: Set CMake build configuration, default is Release"
|
||||
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
|
||||
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
|
||||
echo " -1: Use single job for building"
|
||||
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
|
||||
exit 0
|
||||
;;
|
||||
@@ -95,7 +95,7 @@ if [ -z "$DEPS_CMAKE_GENERATOR" ]; then
|
||||
fi
|
||||
|
||||
if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then
|
||||
export OSX_DEPLOYMENT_TARGET="12.0"
|
||||
export OSX_DEPLOYMENT_TARGET="11.3"
|
||||
fi
|
||||
|
||||
if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
@REM OrcaSlicer build script for Windows with VS auto-detect
|
||||
@echo off
|
||||
set WP=%CD%
|
||||
set _START_TIME=%TIME%
|
||||
|
||||
@REM Default target architecture to the host CPU arch; override by passing
|
||||
@REM "x64" or "arm64" as an argument. PROCESSOR_ARCHITEW6432 covers a 32-bit
|
||||
@REM shell running on a 64-bit OS, where PROCESSOR_ARCHITECTURE reads "x86".
|
||||
set arch=x64
|
||||
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set arch=ARM64
|
||||
if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" set arch=ARM64
|
||||
if /I "%1"=="arm64" set arch=ARM64
|
||||
if /I "%2"=="arm64" set arch=ARM64
|
||||
if /I "%1"=="x64" set arch=x64
|
||||
if /I "%2"=="x64" set arch=x64
|
||||
|
||||
@REM Check for Ninja Multi-Config option (-x)
|
||||
set USE_NINJA=0
|
||||
for %%a in (%*) do (
|
||||
if "%%a"=="-x" set USE_NINJA=1
|
||||
)
|
||||
|
||||
@REM Check for unit-tests option ("tests")
|
||||
set BUILD_TESTS=OFF
|
||||
for %%a in (%*) do (
|
||||
if /I "%%a"=="tests" set BUILD_TESTS=ON
|
||||
)
|
||||
|
||||
if "%USE_NINJA%"=="1" (
|
||||
echo Using Ninja Multi-Config generator
|
||||
set CMAKE_GENERATOR="Ninja Multi-Config"
|
||||
set VS_VERSION=Ninja
|
||||
goto :generator_ready
|
||||
)
|
||||
|
||||
@REM Detect Visual Studio version using msbuild
|
||||
echo Detecting Visual Studio version using msbuild...
|
||||
|
||||
@REM Try to get MSBuild version - the output format varies by VS version
|
||||
set VS_MAJOR=
|
||||
for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do (
|
||||
for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a
|
||||
set MSBUILD_OUTPUT=%%i
|
||||
goto :version_found
|
||||
)
|
||||
|
||||
@REM Alternative method for newer MSBuild versions
|
||||
if "%VS_MAJOR%"=="" (
|
||||
for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do (
|
||||
for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a
|
||||
set MSBUILD_OUTPUT=%%i
|
||||
goto :version_found
|
||||
)
|
||||
)
|
||||
|
||||
:version_found
|
||||
echo MSBuild version detected: %MSBUILD_OUTPUT%
|
||||
echo Major version: %VS_MAJOR%
|
||||
|
||||
if "%VS_MAJOR%"=="" (
|
||||
echo Error: Could not determine Visual Studio version from msbuild
|
||||
echo Please ensure Visual Studio and MSBuild are properly installed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%VS_MAJOR%"=="16" (
|
||||
set VS_VERSION=2019
|
||||
set CMAKE_GENERATOR="Visual Studio 16 2019"
|
||||
) else if "%VS_MAJOR%"=="17" (
|
||||
set VS_VERSION=2022
|
||||
set CMAKE_GENERATOR="Visual Studio 17 2022"
|
||||
) else if "%VS_MAJOR%"=="18" (
|
||||
set VS_VERSION=2026
|
||||
set CMAKE_GENERATOR="Visual Studio 18 2026"
|
||||
) else (
|
||||
echo Error: Unsupported Visual Studio version: %VS_MAJOR%
|
||||
echo Supported versions: VS2019 (16.x^), VS2022 (17.x^), VS2026 (18.x^)
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Detected Visual Studio %VS_VERSION% (version %VS_MAJOR%)
|
||||
echo Using CMake generator: %CMAKE_GENERATOR%
|
||||
|
||||
:generator_ready
|
||||
|
||||
@REM Pack deps
|
||||
if "%1"=="pack" (
|
||||
setlocal ENABLEDELAYEDEXPANSION
|
||||
cd %WP%/deps/build
|
||||
if "%arch%"=="ARM64" cd %WP%/deps/build-arm64
|
||||
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do set build_date=%%c%%b%%a
|
||||
echo packing deps: OrcaSlicer_dep_win-!arch!_!build_date!_vs!VS_VERSION!.zip
|
||||
|
||||
%WP%/tools/7z.exe a OrcaSlicer_dep_win-!arch!_!build_date!_vs!VS_VERSION!.zip OrcaSlicer_dep
|
||||
goto :done
|
||||
)
|
||||
|
||||
set debug=OFF
|
||||
set debuginfo=OFF
|
||||
if "%1"=="debug" set debug=ON
|
||||
if "%2"=="debug" set debug=ON
|
||||
if "%1"=="debuginfo" set debuginfo=ON
|
||||
if "%2"=="debuginfo" set debuginfo=ON
|
||||
if "%debug%"=="ON" (
|
||||
set build_type=Debug
|
||||
set build_dir=build-dbg
|
||||
) else (
|
||||
if "%debuginfo%"=="ON" (
|
||||
set build_type=RelWithDebInfo
|
||||
set build_dir=build-dbginfo
|
||||
) else (
|
||||
set build_type=Release
|
||||
set build_dir=build
|
||||
)
|
||||
)
|
||||
if "%arch%"=="ARM64" set build_dir=%build_dir%-arm64
|
||||
echo build type set to %build_type%, arch=%arch%
|
||||
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
cd deps
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
set "SIG_FLAG="
|
||||
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
|
||||
|
||||
if "%1"=="slicer" (
|
||||
GOTO :slicer
|
||||
)
|
||||
echo "building deps.."
|
||||
|
||||
echo on
|
||||
REM Set minimum CMake policy to avoid <3.5 errors
|
||||
set CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
if "%USE_NINJA%"=="1" (
|
||||
cmake ../ -G %CMAKE_GENERATOR% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target deps
|
||||
) else (
|
||||
cmake ../ -G %CMAKE_GENERATOR% -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target deps -- -m
|
||||
)
|
||||
@echo off
|
||||
|
||||
if "%1"=="deps" goto :done
|
||||
|
||||
:slicer
|
||||
echo "building Orca Slicer..."
|
||||
cd %WP%
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
|
||||
echo on
|
||||
set CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
if "%USE_NINJA%"=="1" (
|
||||
cmake .. -G %CMAKE_GENERATOR% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target ALL_BUILD
|
||||
) else (
|
||||
cmake .. -G %CMAKE_GENERATOR% -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target ALL_BUILD -- -m
|
||||
)
|
||||
@echo off
|
||||
cd ..
|
||||
call scripts/run_gettext.bat
|
||||
cd %build_dir%
|
||||
cmake --build . --target install --config %build_type%
|
||||
|
||||
:done
|
||||
@echo off
|
||||
for /f "tokens=1-3 delims=:.," %%a in ("%_START_TIME: =0%") do set /a "_start_s=%%a*3600+%%b*60+%%c"
|
||||
for /f "tokens=1-3 delims=:.," %%a in ("%TIME: =0%") do set /a "_end_s=%%a*3600+%%b*60+%%c"
|
||||
set /a "_elapsed=_end_s - _start_s"
|
||||
if %_elapsed% lss 0 set /a "_elapsed+=86400"
|
||||
set /a "_hours=_elapsed / 3600"
|
||||
set /a "_remainder=_elapsed - _hours * 3600"
|
||||
set /a "_mins=_remainder / 60"
|
||||
set /a "_secs=_remainder - _mins * 60"
|
||||
echo.
|
||||
echo Build completed in %_hours%h %_mins%m %_secs%s
|
||||
@@ -0,0 +1,80 @@
|
||||
@REM OrcaSlicer build script for Windows
|
||||
@echo off
|
||||
set WP=%CD%
|
||||
|
||||
@REM Pack deps
|
||||
if "%1"=="pack" (
|
||||
setlocal ENABLEDELAYEDEXPANSION
|
||||
cd %WP%/deps/build
|
||||
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do set build_date=%%c%%b%%a
|
||||
echo packing deps: OrcaSlicer_dep_win64_!build_date!_vs2022.zip
|
||||
|
||||
%WP%/tools/7z.exe a OrcaSlicer_dep_win64_!build_date!_vs2022.zip OrcaSlicer_dep
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
set debug=OFF
|
||||
set debuginfo=OFF
|
||||
@REM Default target architecture to the host CPU arch; override with x64/arm64 arg.
|
||||
set arch=x64
|
||||
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set arch=ARM64
|
||||
if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" set arch=ARM64
|
||||
if "%1"=="debug" set debug=ON
|
||||
if "%2"=="debug" set debug=ON
|
||||
if "%1"=="debuginfo" set debuginfo=ON
|
||||
if "%2"=="debuginfo" set debuginfo=ON
|
||||
if /I "%1"=="arm64" set arch=ARM64
|
||||
if /I "%2"=="arm64" set arch=ARM64
|
||||
if /I "%1"=="x64" set arch=x64
|
||||
if /I "%2"=="x64" set arch=x64
|
||||
if "%debug%"=="ON" (
|
||||
set build_type=Debug
|
||||
set build_dir=build-dbg
|
||||
) else (
|
||||
if "%debuginfo%"=="ON" (
|
||||
set build_type=RelWithDebInfo
|
||||
set build_dir=build-dbginfo
|
||||
) else (
|
||||
set build_type=Release
|
||||
set build_dir=build
|
||||
)
|
||||
)
|
||||
if "%arch%"=="ARM64" set build_dir=%build_dir%-arm64
|
||||
echo build type set to %build_type%, arch=%arch%
|
||||
|
||||
setlocal DISABLEDELAYEDEXPANSION
|
||||
cd deps
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
set "SIG_FLAG="
|
||||
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
|
||||
|
||||
if "%1"=="slicer" (
|
||||
GOTO :slicer
|
||||
)
|
||||
echo "building deps.."
|
||||
|
||||
echo on
|
||||
REM Set minimum CMake policy to avoid <3.5 errors
|
||||
set CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
cmake ../ -G "Visual Studio 17 2022" -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target deps -- -m
|
||||
@echo off
|
||||
|
||||
if "%1"=="deps" exit /b 0
|
||||
|
||||
:slicer
|
||||
echo "building Orca Slicer..."
|
||||
cd %WP%
|
||||
mkdir %build_dir%
|
||||
cd %build_dir%
|
||||
|
||||
echo on
|
||||
set CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
cmake .. -G "Visual Studio 17 2022" -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type%
|
||||
cmake --build . --config %build_type% --target ALL_BUILD -- -m
|
||||
@echo off
|
||||
cd ..
|
||||
call scripts/run_gettext.bat
|
||||
cd %build_dir%
|
||||
cmake --build . --target install --config %build_type%
|
||||
-1396
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
|
||||
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
|
||||
# no headers for that object. ccache reproduces /showIncludes output on a hit.
|
||||
foreach (_lang C CXX)
|
||||
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
|
||||
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
|
||||
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
|
||||
endif ()
|
||||
endforeach ()
|
||||
@@ -256,13 +256,6 @@ function(add_precompiled_header _target _input)
|
||||
message(STATUS "Adding precompiled header ${_input} to target ${_target}.")
|
||||
target_precompile_headers(${_target} PRIVATE ${_input})
|
||||
|
||||
# Clang records the modification time of every input in the precompiled
|
||||
# header, which makes it differ between two checkouts of the same source
|
||||
# and defeats a compiler cache. The build system already rebuilds the
|
||||
# header when an input changes.
|
||||
target_compile_options(${_target} PRIVATE
|
||||
"$<$<CXX_COMPILER_ID:Clang,AppleClang>:SHELL:-Xclang -fno-pch-timestamp>")
|
||||
|
||||
get_target_property(_sources ${_target} SOURCES)
|
||||
list(FILTER _sources INCLUDE REGEX ".*\\.mm?")
|
||||
|
||||
|
||||
Vendored
-43
@@ -1,43 +0,0 @@
|
||||
if(CMAKE_VERSION VERSION_LESS 3.22)
|
||||
set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.3.1.tar.gz")
|
||||
set(_assimp_hash "SHA256=a07666be71afe1ad4bc008c2336b7c688aca391271188eb9108d0c6db1be53f1")
|
||||
else()
|
||||
set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz")
|
||||
set(_assimp_hash "SHA256=66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb")
|
||||
endif()
|
||||
|
||||
# Assimp's bundled zlib (contrib/zlib) is too old to compile against the modern
|
||||
# macOS SDK: its zutil.h takes the classic-Mac branch under TARGET_OS_MAC and
|
||||
# does `#define fdopen(fd,mode) NULL`, which then clobbers the SDK's real
|
||||
# `fdopen` prototype in <stdio.h> and breaks the build. On macOS use the system
|
||||
# zlib (already found by find_package(ZLIB) in deps-unix-common) instead.
|
||||
if(APPLE)
|
||||
set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=OFF")
|
||||
else()
|
||||
set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=ON")
|
||||
endif()
|
||||
|
||||
orcaslicer_add_cmake_project(Assimp
|
||||
URL ${_assimp_url}
|
||||
URL_HASH ${_assimp_hash}
|
||||
CMAKE_ARGS
|
||||
# Assimp's ccache support sets the global RULE_LAUNCH_COMPILE, which breaks
|
||||
# the Ninja RC rule. The superbuild forwards CMAKE_<LANG>_COMPILER_LAUNCHER.
|
||||
-DASSIMP_BUILD_USE_CCACHE=OFF
|
||||
-DASSIMP_BUILD_TESTS=OFF
|
||||
-DASSIMP_BUILD_SAMPLES=OFF
|
||||
-DASSIMP_BUILD_ASSIMP_TOOLS=OFF
|
||||
-DASSIMP_INSTALL_PDB=OFF
|
||||
-DASSIMP_NO_EXPORT=ON
|
||||
-DASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT=OFF
|
||||
-DASSIMP_BUILD_GLTF_IMPORTER=ON
|
||||
-DASSIMP_BUILD_OBJ_IMPORTER=ON
|
||||
-DASSIMP_BUILD_FBX_IMPORTER=ON
|
||||
${_assimp_build_zlib}
|
||||
-DASSIMP_WARNINGS_AS_ERRORS=OFF
|
||||
-DBUILD_WITH_STATIC_CRT=OFF
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
add_debug_dep(dep_Assimp)
|
||||
endif ()
|
||||
Vendored
-16
@@ -24,20 +24,6 @@ if (MSVC AND DEP_DEBUG)
|
||||
set(_options "FORWARD_CONFIG")
|
||||
endif ()
|
||||
|
||||
# Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API
|
||||
# takes volatile long*; cl compiles that with a warning, clang errors out.
|
||||
set(_boost_c_flags_line "")
|
||||
set(_boost_cxx_flags_line "")
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types")
|
||||
# The Visual Studio generator applies only the link language's flags to a
|
||||
# project, and boost_container links as C++, so its C file never sees
|
||||
# CMAKE_C_FLAGS. The C++ flags reach every file; keep CMake's defaults.
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
set(_boost_cxx_flags_line "-DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS} -Wno-incompatible-pointer-types")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(Boost
|
||||
${_options}
|
||||
URL "https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.gz"
|
||||
@@ -52,8 +38,6 @@ orcaslicer_add_cmake_project(Boost
|
||||
"${_context_abi_line}"
|
||||
"${_context_arch_line}"
|
||||
"${_context_impl_line}"
|
||||
"${_boost_c_flags_line}"
|
||||
"${_boost_cxx_flags_line}"
|
||||
)
|
||||
|
||||
set(DEP_Boost_DEPENDS ZLIB)
|
||||
Vendored
+8
-55
@@ -26,9 +26,9 @@ endif()
|
||||
|
||||
cmake_minimum_required(VERSION 3.2)
|
||||
if (APPLE)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
|
||||
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3
|
||||
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE)
|
||||
endif ()
|
||||
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
|
||||
@@ -38,14 +38,6 @@ if(POLICY CMP0135) # DOWNLOAD_EXTRACT_TIMESTAMP
|
||||
cmake_policy(SET CMP0135 NEW)
|
||||
endif()
|
||||
|
||||
# project() reads this, so set it first. scripts/flatpak/make_deps_tar.sh packs deps/
|
||||
# without cmake/, so the file is missing in a Flatpak build.
|
||||
set(_rules_override "${CMAKE_CURRENT_LIST_DIR}/../cmake/modules/ClangClShowIncludes.cmake")
|
||||
if (EXISTS "${_rules_override}")
|
||||
set(CMAKE_USER_MAKE_RULES_OVERRIDE "${_rules_override}")
|
||||
endif ()
|
||||
unset(_rules_override)
|
||||
|
||||
project(OrcaSlicer-deps)
|
||||
|
||||
# Backward compatibility for old CMake versions
|
||||
@@ -63,7 +55,6 @@ endif ()
|
||||
|
||||
set(DEP_DOWNLOAD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/DL_CACHE CACHE PATH "Path for downloaded source packages.")
|
||||
set(FLATPAK FALSE CACHE BOOL "Toggles various build settings for flatpak, like /usr/local in DESTDIR or not building wxwidgets")
|
||||
option(SLIC3R_CAD "Build the SolveSpace solver and OCCT ModelingAlgorithms module the parametric Design/CAD tab needs. Must match the main project's SLIC3R_CAD." ON)
|
||||
|
||||
if ("${DESTDIR}" STREQUAL "" OR "${DESTDIR}" STREQUAL "${AUTOGENERATED_DESTDIR}")
|
||||
if (LINUX AND (NOT DEFINED USE_OLD_DESTDIR_PREV OR USE_OLD_DESTDIR_PREV) AND EXISTS "${CMAKE_BINARY_DIR}/destdir/usr/local" AND NOT EXISTS "${CMAKE_BINARY_DIR}/OrcaSlicer_dep/usr/local")
|
||||
@@ -164,18 +155,10 @@ if (NOT _is_multi AND NOT CMAKE_BUILD_TYPE)
|
||||
endif ()
|
||||
|
||||
function(orcaslicer_add_cmake_project projectname)
|
||||
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND;SOURCE_DIR" "CMAKE_ARGS" ${ARGN})
|
||||
|
||||
# MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the
|
||||
# generator. A non-Visual-Studio superbuild passes its own generator down, and with
|
||||
# it the CMAKE_C_COMPILER / CMAKE_CXX_COMPILER forwarded below.
|
||||
set(_dep_msvc_gen FALSE)
|
||||
if (MSVC AND CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
set(_dep_msvc_gen TRUE)
|
||||
endif ()
|
||||
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN})
|
||||
|
||||
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
|
||||
if (_is_multi OR _dep_msvc_gen)
|
||||
if (_is_multi OR MSVC)
|
||||
if (P_ARGS_FORWARD_CONFIG)
|
||||
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
|
||||
elseif (ORCA_INCLUDE_DEBUG_INFO AND NOT DEP_DEBUG)
|
||||
@@ -191,37 +174,26 @@ function(orcaslicer_add_cmake_project projectname)
|
||||
set(_target_config "Release")
|
||||
endif()
|
||||
|
||||
if (_dep_msvc_gen)
|
||||
if (MSVC)
|
||||
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}")
|
||||
# The toolset picks the compiler here, not the CMAKE_<LANG>_COMPILER
|
||||
# forwarded below, so without it a clang-cl superbuild builds with cl.
|
||||
if (CMAKE_GENERATOR_TOOLSET)
|
||||
list(APPEND _gen CMAKE_GENERATOR_TOOLSET "${CMAKE_GENERATOR_TOOLSET}")
|
||||
endif ()
|
||||
else()
|
||||
set(_gen "")
|
||||
endif()
|
||||
|
||||
if ($ENV{CMAKE_BUILD_PARALLEL_LEVEL})
|
||||
set(_build_j "") # assume environment will control --build parallel setting
|
||||
elseif(_dep_msvc_gen)
|
||||
elseif(MSVC)
|
||||
set(_build_j "/m")
|
||||
else()
|
||||
set(_build_j "-j${NPROC}")
|
||||
endif ()
|
||||
|
||||
set(_source_dir_arg "")
|
||||
if (P_ARGS_SOURCE_DIR)
|
||||
set(_source_dir_arg SOURCE_DIR ${P_ARGS_SOURCE_DIR})
|
||||
endif ()
|
||||
|
||||
if (NOT IS_CROSS_COMPILE OR NOT APPLE)
|
||||
ExternalProject_Add(
|
||||
dep_${projectname}
|
||||
EXCLUDE_FROM_ALL ON
|
||||
INSTALL_DIR ${DESTDIR}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
|
||||
${_source_dir_arg}
|
||||
${_gen}
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
@@ -234,7 +206,6 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
|
||||
-DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER}
|
||||
-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER}
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER}
|
||||
-DCMAKE_USER_MAKE_RULES_OVERRIDE:STRING=${CMAKE_USER_MAKE_RULES_OVERRIDE}
|
||||
-DCMAKE_TOOLCHAIN_FILE:STRING=${CMAKE_TOOLCHAIN_FILE}
|
||||
-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS}
|
||||
-DCMAKE_SHARED_LINKER_FLAGS:STRING=${CMAKE_SHARED_LINKER_FLAGS}
|
||||
@@ -255,14 +226,12 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
|
||||
# note for future devs: shared libs may actually create a size reduction
|
||||
# but orcaslicer_deps tends to get really funny regarding linking after that (notably boost)
|
||||
# so, as much as I would like to use that, it's not happening
|
||||
if (NOT P_ARGS_SOURCE_DIR)
|
||||
ExternalProject_Add_Step(dep_${projectname} free_download_space
|
||||
DEPENDEES download # do after download
|
||||
COMMENT "Freeing Space: Removing source archive"
|
||||
WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf ${projectname}
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -r ${projectname}
|
||||
)
|
||||
endif ()
|
||||
ExternalProject_Add_Step(dep_${projectname} free_build_space
|
||||
DEPENDEES install # do after install
|
||||
COMMENT "Freeing Space: Removing source and build files"
|
||||
@@ -276,7 +245,6 @@ else()
|
||||
EXCLUDE_FROM_ALL ON
|
||||
INSTALL_DIR ${DESTDIR}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
|
||||
${_source_dir_arg}
|
||||
${_gen}
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
@@ -285,7 +253,6 @@ else()
|
||||
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
|
||||
-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER}
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER}
|
||||
-DCMAKE_USER_MAKE_RULES_OVERRIDE:STRING=${CMAKE_USER_MAKE_RULES_OVERRIDE}
|
||||
-DBUILD_SHARED_LIBS:BOOL=OFF
|
||||
${_cmake_osx_arch}
|
||||
"${_configs_line}"
|
||||
@@ -383,11 +350,6 @@ include(GLEW/GLEW.cmake)
|
||||
|
||||
include(GLFW/GLFW.cmake)
|
||||
include(OpenCSG/OpenCSG.cmake)
|
||||
set(SLVS_PKG "")
|
||||
if (SLIC3R_CAD)
|
||||
include(SLVS/SLVS.cmake)
|
||||
set(SLVS_PKG dep_SLVS)
|
||||
endif ()
|
||||
|
||||
include(TBB/TBB.cmake)
|
||||
|
||||
@@ -405,6 +367,7 @@ include(libnoise/libnoise.cmake)
|
||||
|
||||
include(Draco/Draco.cmake)
|
||||
|
||||
|
||||
# I *think* 1.1 is used for *just* md5 hashing?
|
||||
# 3.1 has everything in the right place, but the md5 funcs used are deprecated
|
||||
# a grep across the repo shows it is used for other things
|
||||
@@ -415,12 +378,6 @@ if(NOT OPENSSL_FOUND)
|
||||
set(OPENSSL_PKG dep_OpenSSL)
|
||||
endif()
|
||||
|
||||
include(FFMPEG/FFMPEG.cmake)
|
||||
include(Assimp/Assimp.cmake)
|
||||
|
||||
include(DataChannel/DataChannel.cmake)
|
||||
set(DATACHANNEL_PKG dep_DataChannel)
|
||||
|
||||
# we don't want to load a "wrong" openssl when loading curl
|
||||
# so, just don't even bother
|
||||
# ...i think this is how it works? change if wrong
|
||||
@@ -479,7 +436,6 @@ set(_dep_list
|
||||
dep_NLopt
|
||||
dep_OpenVDB
|
||||
dep_OpenCSG
|
||||
${SLVS_PKG}
|
||||
dep_OpenCV
|
||||
dep_Eigen
|
||||
dep_CGAL
|
||||
@@ -492,9 +448,6 @@ set(_dep_list
|
||||
dep_libnoise
|
||||
dep_python3
|
||||
dep_wxInspector
|
||||
dep_FFMPEG
|
||||
dep_Assimp
|
||||
${DATACHANNEL_PKG}
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
|
||||
Vendored
-14
@@ -56,18 +56,6 @@ else()
|
||||
set(_curl_static ON)
|
||||
endif()
|
||||
|
||||
# curl 7.75's configure probes and code rely on C laxness cl allows but clang
|
||||
# errors on (implicit function declarations, int* vs u_long* in ioctlsocket),
|
||||
# which flips probe results and misconfigures nonblock.c into the AmigaOS
|
||||
# IoctlSocket branch. Relax both diagnostics so the probes behave like cl, and
|
||||
# pin the camel-case probes off since they only "pass" by implicit declaration.
|
||||
set(_curl_c_flags_line "")
|
||||
set(_curl_probe_overrides "")
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(_curl_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-implicit-function-declaration -Wno-incompatible-pointer-types")
|
||||
set(_curl_probe_overrides -DHAVE_IOCTLSOCKET_CAMEL=0 -DHAVE_IOCTLSOCKET_CAMEL_FIONBIO=0)
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(CURL
|
||||
# GIT_REPOSITORY https://github.com/curl/curl.git
|
||||
# GIT_TAG curl-7_75_0
|
||||
@@ -81,8 +69,6 @@ orcaslicer_add_cmake_project(CURL
|
||||
-DBUILD_CURL_EXE:BOOL=OFF
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
-DCURL_STATICLIB=${_curl_static}
|
||||
"${_curl_c_flags_line}"
|
||||
${_curl_probe_overrides}
|
||||
${_curl_platform_flags}
|
||||
)
|
||||
|
||||
|
||||
Vendored
-37
@@ -1,37 +0,0 @@
|
||||
# libdatachannel is the native ICE/DTLS/SCTP implementation used by the
|
||||
# GUI WebRTC camera controller. Keep the source revision fixed: the signaling
|
||||
# protocol is evolving independently of this transport dependency.
|
||||
#
|
||||
# It vendors plog, usrsctp and libjuice as git submodules, which a plain
|
||||
# GitHub tag tarball does not include. The flatpak sandbox has no network
|
||||
# access during the build, so there the manifest itself clones the repo
|
||||
# (submodules and all) into the dependency download directory before the
|
||||
# sandbox closes. ExternalProject_Add is pointed at that existing checkout
|
||||
# instead of being given its own network-dependent download method.
|
||||
if (FLATPAK)
|
||||
set(_datachannel_source
|
||||
SOURCE_DIR ${DEP_DOWNLOAD_DIR}/DataChannel
|
||||
)
|
||||
else()
|
||||
set(_datachannel_source
|
||||
GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git
|
||||
GIT_TAG v0.24.5
|
||||
GIT_SHALLOW ON
|
||||
GIT_SUBMODULES_RECURSE ON
|
||||
)
|
||||
endif()
|
||||
|
||||
orcaslicer_add_cmake_project(DataChannel
|
||||
DEPENDS ${OPENSSL_PKG}
|
||||
CMAKE_ARGS
|
||||
-DNO_EXAMPLES=ON
|
||||
-DNO_TESTS=ON
|
||||
-DNO_WEBSOCKET=ON
|
||||
-DNO_MEDIA=ON
|
||||
-DUSE_NICE=OFF
|
||||
-DUSE_SYSTEM_JUICE=OFF
|
||||
-DUSE_SYSTEM_USRSCTP=OFF
|
||||
-DOPENSSL_ROOT_DIR:PATH=${DESTDIR}
|
||||
-DOPENSSL_USE_STATIC_LIBS=ON
|
||||
${_datachannel_source}
|
||||
)
|
||||
Vendored
-3
@@ -7,7 +7,4 @@ orcaslicer_add_cmake_project(Draco
|
||||
${_options}
|
||||
URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip
|
||||
URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
|
||||
CMAKE_ARGS
|
||||
# The encoder and decoder tools duplicate draco.lib; see deps-windows.cmake.
|
||||
"${DEP_LLD_FORCE_MULTIPLE}"
|
||||
)
|
||||
Vendored
-15
@@ -7,20 +7,5 @@ orcaslicer_add_cmake_project(Eigen
|
||||
URL https://gitlab.com/libeigen/eigen/-/archive/5.0.1/eigen-5.0.1.zip
|
||||
URL_HASH SHA256=0dbb1f9e3aaad66f352c03227d8c983f6f0b49e0b07e71a7300f4abcc01aee12
|
||||
CMAKE_ARGS "${_eigen_extra_flags}"
|
||||
# Only the headers are consumed here. Everything below builds nothing we
|
||||
# use, and all three enable_language(Fortran): test/CMakeLists.txt:9,
|
||||
# lapack/CMakeLists.txt:6 and blas/testing/CMakeLists.txt:2. They default
|
||||
# to ON because the dependency configures as its own top-level project.
|
||||
#
|
||||
# Whether that probe is harmless depends on what CMake finds. The Visual
|
||||
# Studio generator supports no Fortran, so it finds nothing; clang-cl sits
|
||||
# next to the LLVM toolset's flang, which works. MSVC with Ninja finds
|
||||
# Strawberry Perl's MinGW gfortran instead, which the deps build already
|
||||
# requires for OpenSSL, and hands it the MSVC-style /machine:x64 that
|
||||
# MinGW's ld reads as a missing input file. The configure dies there and
|
||||
# takes the rest of the superbuild with it.
|
||||
-DEIGEN_BUILD_TESTING=OFF
|
||||
-DEIGEN_BUILD_BLAS=OFF
|
||||
-DEIGEN_BUILD_LAPACK=OFF
|
||||
DEPENDS dep_Boost dep_GMP dep_MPFR
|
||||
)
|
||||
|
||||
Vendored
-103
@@ -1,103 +0,0 @@
|
||||
set(_conf_cmd ./configure)
|
||||
|
||||
set(_ffmpeg_depends)
|
||||
set(_ffmpeg_configure_command ${_conf_cmd})
|
||||
if (TARGET dep_OpenSSL)
|
||||
set(_ffmpeg_depends DEPENDS dep_OpenSSL)
|
||||
set(_ffmpeg_configure_command
|
||||
${CMAKE_COMMAND} -E env
|
||||
"PKG_CONFIG_PATH=${DESTDIR}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}"
|
||||
${_conf_cmd}
|
||||
)
|
||||
endif()
|
||||
|
||||
if (MSVC)
|
||||
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
|
||||
|
||||
set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-winarm64-orca-shared-7.0.zip")
|
||||
set(PREBUILD_HASH_arm64 "da480cbb39680056de824c57ec4dc3bd577b479ebbc310ff1f9dc55cf014b4c1")
|
||||
set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-win64-orca-shared-7.0.zip")
|
||||
set(PREBUILD_HASH_x64 "85da19daf198f5548259d8aabb349db84997a3f6e6886d8d7764114add9c6dae")
|
||||
|
||||
ExternalProject_Add(dep_FFMPEG
|
||||
${_ffmpeg_depends}
|
||||
URL ${PREBUILD_URL_${DEPS_ARCH}}
|
||||
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/bin" "${DESTDIR}/bin"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/lib" "${DESTDIR}/lib"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/include" "${DESTDIR}/include"
|
||||
)
|
||||
|
||||
else ()
|
||||
set(_openssl_cmd --enable-openssl)
|
||||
|
||||
if (APPLE)
|
||||
set(_minos_cmd
|
||||
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
|
||||
"--extra-ldflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
|
||||
)
|
||||
# Static FFmpeg: nothing to bundle into the .app, no rpath handling.
|
||||
# Disable the VideoToolbox/AudioToolbox HW-accel paths: the player decodes
|
||||
# in software (swscale), and the auto-detected HW objects would drag in
|
||||
# system frameworks that the static libs would then depend on.
|
||||
set(_link_cmd --enable-static --disable-shared --disable-videotoolbox --disable-audiotoolbox)
|
||||
if (IS_CROSS_COMPILE)
|
||||
set(_cross_cmd --enable-cross-compile)
|
||||
set(_pic_cmd --enable-pic)
|
||||
if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64")
|
||||
set(_arch_cmd --arch=arm64)
|
||||
set(_cc_cmd "--cc=clang -arch arm64")
|
||||
else()
|
||||
set(_arch_cmd --arch=x86_64)
|
||||
set(_cc_cmd "--cc=clang -arch x86_64")
|
||||
endif()
|
||||
endif()
|
||||
else ()
|
||||
set(_link_cmd --enable-shared)
|
||||
endif ()
|
||||
|
||||
set(_build_j -j)
|
||||
if(DEFINED ENV{CMAKE_BUILD_PARALLEL_LEVEL})
|
||||
set(_build_j "-j$ENV{CMAKE_BUILD_PARALLEL_LEVEL}")
|
||||
endif()
|
||||
|
||||
ExternalProject_Add(dep_FFMPEG
|
||||
${_ffmpeg_depends}
|
||||
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
|
||||
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
|
||||
CONFIGURE_COMMAND ${_ffmpeg_configure_command}
|
||||
${_cross_cmd}
|
||||
${_pic_cmd}
|
||||
${_arch_cmd}
|
||||
${_cc_cmd}
|
||||
"--prefix=${DESTDIR}"
|
||||
${_link_cmd}
|
||||
${_minos_cmd}
|
||||
${_openssl_cmd}
|
||||
--disable-doc
|
||||
--enable-small
|
||||
--disable-outdevs
|
||||
--disable-filters
|
||||
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
|
||||
--disable-protocols
|
||||
--enable-protocol=file,fd,pipe,http,https,rtp,tcp,udp
|
||||
--disable-muxers
|
||||
--enable-muxer=rtp
|
||||
--disable-encoders
|
||||
--disable-decoders
|
||||
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
|
||||
--disable-demuxers
|
||||
--enable-demuxer=h264,mp3,mov,mpjpeg,rtsp,sdp
|
||||
--disable-zlib
|
||||
--disable-avdevice
|
||||
BUILD_IN_SOURCE ON
|
||||
BUILD_COMMAND make ${_build_j}
|
||||
INSTALL_COMMAND make install
|
||||
)
|
||||
|
||||
endif()
|
||||
Vendored
+3
-3
@@ -5,7 +5,7 @@
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
@@ -17,7 +17,7 @@
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
@@ -26,7 +26,7 @@
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
|
||||
Vendored
-2
@@ -8,8 +8,6 @@ orcaslicer_add_cmake_project(NLopt
|
||||
-DNLOPT_GUILE:BOOL=OFF
|
||||
-DNLOPT_SWIG:BOOL=OFF
|
||||
-DNLOPT_TESTS:BOOL=OFF
|
||||
# testopt is built regardless of NLOPT_TESTS; see deps-windows.cmake.
|
||||
"${DEP_LLD_FORCE_MULTIPLE}"
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
|
||||
Vendored
-43
@@ -1,20 +1,3 @@
|
||||
diff --git a/adm/cmake/occt_defs_flags.cmake b/adm/cmake/occt_defs_flags.cmake
|
||||
index 00000000..00000001 100644
|
||||
--- a/adm/cmake/occt_defs_flags.cmake
|
||||
+++ b/adm/cmake/occt_defs_flags.cmake
|
||||
@@ -134,7 +134,11 @@
|
||||
set (CMAKE_CXX_FLAGS "-std=c++0x ${CMAKE_CXX_FLAGS}")
|
||||
endif()
|
||||
# Optimize size of binaries
|
||||
- set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}")
|
||||
+ # clang-cl reports the Clang compiler ID, and OCCT builds shared on Windows,
|
||||
+ # where the MSVC-style linker gets this flag as an argument it does not know.
|
||||
+ if (NOT WIN32)
|
||||
+ set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}")
|
||||
+ endif()
|
||||
elseif(MINGW)
|
||||
add_definitions(-D_WIN32_WINNT=0x0601)
|
||||
# _WIN32_WINNT=0x0601 (use Windows 7 SDK)
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index d98acc0f..28eb8eb4 100644
|
||||
--- a/CMakeLists.txt
|
||||
@@ -185,32 +168,6 @@ index d98acc0f..28eb8eb4 100644
|
||||
endforeach()
|
||||
|
||||
if (BUILD_SAMPLES_QT)
|
||||
diff --git a/adm/cmake/occt_macros.cmake b/adm/cmake/occt_macros.cmake
|
||||
index 224c96b1..8c94a1c5 100644
|
||||
--- a/adm/cmake/occt_macros.cmake
|
||||
+++ b/adm/cmake/occt_macros.cmake
|
||||
@@ -608,7 +608,7 @@ macro (OCCT_INSERT_CODE_FOR_TARGET)
|
||||
install(CODE "if (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$\")
|
||||
set (OCCT_INSTALL_BIN_LETTER \"\")
|
||||
elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ww][Ii][Tt][Hh][Dd][Ee][Bb][Ii][Nn][Ff][Oo])$\")
|
||||
- set (OCCT_INSTALL_BIN_LETTER \"i\")
|
||||
+ set (OCCT_INSTALL_BIN_LETTER \"\")
|
||||
elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Dd][Ee][Bb][Uu][Gg])$\")
|
||||
set (OCCT_INSTALL_BIN_LETTER \"d\")
|
||||
endif()")
|
||||
diff --git a/adm/cmake/occt_toolkit.cmake b/adm/cmake/occt_toolkit.cmake
|
||||
index 550e0e2f..7ac1a3b8 100644
|
||||
--- a/adm/cmake/occt_toolkit.cmake
|
||||
+++ b/adm/cmake/occt_toolkit.cmake
|
||||
@@ -241,7 +241,7 @@
|
||||
else()
|
||||
set (aReleasePdbConf)
|
||||
endif()
|
||||
- install (FILES ${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin\${OCCT_INSTALL_BIN_LETTER}/${PROJECT_NAME}.pdb
|
||||
+ install (FILES $<TARGET_PDB_FILE:${PROJECT_NAME}>
|
||||
CONFIGURATIONS Debug ${aReleasePdbConf} RelWithDebInfo
|
||||
DESTINATION "${INSTALL_DIR_BIN}\${OCCT_INSTALL_BIN_LETTER}")
|
||||
endif()
|
||||
diff --git a/src/Font/Font_FTFont.cxx b/src/Font/Font_FTFont.cxx
|
||||
index 5ae9899f..0a17372b 100644
|
||||
--- a/src/Font/Font_FTFont.cxx
|
||||
|
||||
Vendored
+1
-24
@@ -1,31 +1,9 @@
|
||||
# clang-cl cannot emit IGESAppli_GeneralModule.cxx on ARM64
|
||||
# (llvm/llvm-project#62081). cl and clang-cl share an ABI.
|
||||
set(_occt_compiler_args "")
|
||||
if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||
set(_occt_compiler_args -DCMAKE_C_COMPILER:STRING=cl -DCMAKE_CXX_COMPILER:STRING=cl)
|
||||
endif ()
|
||||
|
||||
if(WIN32)
|
||||
set(library_build_type "Shared")
|
||||
else()
|
||||
set(library_build_type "Static")
|
||||
endif()
|
||||
|
||||
# SLIC3R_CAD (declared in deps/CMakeLists.txt) builds OCCT's ModelingAlgorithms module
|
||||
# (fillet/offset/loft), whose only consumer is the parametric Design/CAD tab. With it OFF
|
||||
# the deps prefix matches upstream exactly.
|
||||
#
|
||||
# With it ON the delta is THREE toolkits, not two: TKFillet (7.40 MiB archive, used via
|
||||
# BRepFilletAPI), TKOffset (5.38 MiB, used via BRepOffsetAPI) and TKFeat (4.42 MiB), which
|
||||
# nothing here references but which the module flag builds anyway -- it is all-or-nothing
|
||||
# per module. The module's other nine toolkits are built either way, because DataExchange
|
||||
# (the STEP path upstream already ships) depends on them.
|
||||
#
|
||||
# On macOS/Linux OCCT links statically, so an unreferenced toolkit costs build time and no
|
||||
# shipped bytes. The Windows figure is a real DLL cost and has NOT been measured -- an
|
||||
# earlier "3.77 MiB, Windows only" note here covered only two of the three toolkits and is
|
||||
# not a number to quote. See docs/HLSD/design-tab.md.
|
||||
|
||||
if (IN_GIT_REPO)
|
||||
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
|
||||
endif ()
|
||||
@@ -50,10 +28,9 @@ orcaslicer_add_cmake_project(OCCT
|
||||
#-DBUILD_MODULE_DataExchange=OFF
|
||||
-DBUILD_MODULE_Draw=OFF
|
||||
-DBUILD_MODULE_FoundationClasses=OFF
|
||||
-DBUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD}
|
||||
-DBUILD_MODULE_ModelingAlgorithms=OFF
|
||||
-DBUILD_MODULE_ModelingData=OFF
|
||||
-DBUILD_MODULE_Visualization=OFF
|
||||
${_occt_compiler_args}
|
||||
)
|
||||
|
||||
# add_dependencies(dep_OCCT ${FREETYPE_PKG})
|
||||
|
||||
Vendored
-8
@@ -10,13 +10,6 @@ else ()
|
||||
set(_options "")
|
||||
endif ()
|
||||
|
||||
# carotene is OpenCV's ARM NEON HAL. It uses M_PI without _USE_MATH_DEFINES
|
||||
# and does not compile with clang-cl.
|
||||
set(_disable_carotene "")
|
||||
if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||
set(_disable_carotene "-DWITH_CAROTENE=OFF")
|
||||
endif ()
|
||||
|
||||
if (IN_GIT_REPO)
|
||||
set(OpenCV_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OpenCV-prefix/src/dep_OpenCV)
|
||||
endif ()
|
||||
@@ -90,6 +83,5 @@ orcaslicer_add_cmake_project(OpenCV
|
||||
-DWITH_PROTOBUF=OFF
|
||||
-DWITH_WIN32UI=OFF
|
||||
-DHAVE_WIN32UI=FALSE
|
||||
${_disable_carotene}
|
||||
)
|
||||
|
||||
|
||||
Vendored
+4
-20
@@ -6,7 +6,7 @@ if(DEFINED OPENSSL_ARCH)
|
||||
set(_cross_arch ${OPENSSL_ARCH})
|
||||
else()
|
||||
if(WIN32)
|
||||
if("${DEPS_ARCH}" STREQUAL "arm64")
|
||||
if("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "ARM64")
|
||||
set(_cross_arch "VC-WIN64-ARM")
|
||||
else()
|
||||
set(_cross_arch "VC-WIN64A")
|
||||
@@ -17,20 +17,10 @@ else()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS)
|
||||
# OpenSSL's perl Configure honors the CC environment variable, but the
|
||||
# VC-WIN64A makefile only works with cl (an unquoted clang-cl path with
|
||||
# spaces, e.g. exported by CLion, silently produces no .obj files and the
|
||||
# lib step fails with LNK1181). Pin the upstream toolchain.
|
||||
# Keep rc.exe resolved from the MSVC developer environment as well. The
|
||||
# absolute Windows SDK path contains spaces and OpenSSL 1.1.1 writes it to
|
||||
# the generated nmake file without quoting, which skips .res generation.
|
||||
# /FS serializes access to OpenSSL's shared generated PDB when cl is
|
||||
# driven through nmake from a Ninja configure step.
|
||||
set(_conf_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} perl Configure )
|
||||
set(_conf_cmd perl Configure )
|
||||
set(_cross_comp_prefix_line "")
|
||||
set(_make_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake)
|
||||
set(_install_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake install_sw )
|
||||
set(_make_cmd nmake)
|
||||
set(_install_cmd nmake install_sw )
|
||||
else()
|
||||
if(APPLE)
|
||||
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
|
||||
@@ -80,12 +70,6 @@ ExternalProject_Add(dep_OpenSSL
|
||||
INSTALL_COMMAND ${_install_cmd}
|
||||
)
|
||||
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
# OpenSSL builds with cl, but MSBuild runs nmake in this project's toolset
|
||||
# environment, and ClangCL's puts clang's headers first. Use the default.
|
||||
set_target_properties(dep_OpenSSL PROPERTIES VS_PLATFORM_TOOLSET "$(DefaultPlatformToolset)")
|
||||
endif ()
|
||||
|
||||
ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
|
||||
DEPENDEES install
|
||||
|
||||
|
||||
Vendored
-4
@@ -1,10 +1,6 @@
|
||||
if (APPLE)
|
||||
# Only disable NEON extension for Apple ARM builds, leave it enabled for Raspberry PI.
|
||||
set(_disable_neon_extension "-DPNG_ARM_NEON=off")
|
||||
elseif ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||
# libpng's CMake ignores PNG_ARM_NEON on Windows ARM64 and skips the NEON
|
||||
# sources, but pngpriv.h enables NEON anyway.
|
||||
set(_disable_neon_extension "-DCMAKE_C_FLAGS=/DWIN32 /D_WINDOWS /DPNG_ARM_NEON_OPT=0")
|
||||
else ()
|
||||
set(_disable_neon_extension "")
|
||||
endif ()
|
||||
|
||||
Vendored
-65
@@ -1,65 +0,0 @@
|
||||
# Replaces the upstream SolveSpaceLib CMakeLists, which builds a demo executable and
|
||||
# has no install rules. The sources themselves are used verbatim.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
project(SLVS VERSION 3.0)
|
||||
|
||||
add_library(slvs
|
||||
libslvs/constrainteq.cpp
|
||||
libslvs/entity.cpp
|
||||
libslvs/expr.cpp
|
||||
libslvs/system.cpp
|
||||
libslvs/util.cpp
|
||||
libslvs/platform/unixutil.cpp
|
||||
libslvs/lib.cpp
|
||||
libslvs/SolveSpaceSystem.cpp)
|
||||
|
||||
target_compile_features(slvs PUBLIC cxx_std_11)
|
||||
|
||||
# LIBRARY strips the solver core out of the SolveSpace application it was extracted from.
|
||||
target_compile_definitions(slvs PRIVATE -DLIBRARY)
|
||||
if (MSVC)
|
||||
target_compile_definitions(slvs PRIVATE -D_CRT_SECURE_NO_WARNINGS -D_SCL_SECURE_NO_WARNINGS)
|
||||
endif ()
|
||||
|
||||
target_include_directories(slvs
|
||||
PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/libslvs/include>
|
||||
PRIVATE ${PROJECT_SOURCE_DIR}/libslvs)
|
||||
|
||||
# libslic3r is linked into shared targets, so this has to be position independent.
|
||||
set_target_properties(slvs PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# 2018 code, predating the project's warning settings; it is not ours to clean up.
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
target_compile_options(slvs PRIVATE -w -fno-strict-aliasing)
|
||||
endif ()
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY AnyNewerVersion)
|
||||
|
||||
install(TARGETS slvs
|
||||
EXPORT ${PROJECT_NAME}Targets
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
|
||||
set(ConfigPackageLocation ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
|
||||
|
||||
install(EXPORT ${PROJECT_NAME}Targets
|
||||
FILE "${PROJECT_NAME}Config.cmake"
|
||||
NAMESPACE ${PROJECT_NAME}::
|
||||
DESTINATION ${ConfigPackageLocation})
|
||||
|
||||
install(FILES
|
||||
${PROJECT_SOURCE_DIR}/libslvs/include/slvs.h
|
||||
${PROJECT_SOURCE_DIR}/libslvs/include/SolveSpaceSystem.h
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
|
||||
DESTINATION ${ConfigPackageLocation})
|
||||
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
# libslvs — the geometric constraint solver behind the Design tab's sketch constraints.
|
||||
# Extraction of solvespace.com's libslvs, taken verbatim from JacobStoren/SolveSpaceLib;
|
||||
# only the CMakeLists is ours, because upstream's builds a demo and installs nothing.
|
||||
# GPLv3, compatible with this fork's licence. Self-contained: no external dependencies.
|
||||
orcaslicer_add_cmake_project(SLVS
|
||||
URL https://github.com/JacobStoren/SolveSpaceLib/archive/4d8704523e4bf212fadf5189f92484244f670fea.zip
|
||||
URL_HASH SHA256=1c4bdde9c3c6ef20ea4b50b73601de56769f2eb131b36927d7c6489f102e6c30
|
||||
PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in ./CMakeLists.txt
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
add_debug_dep(dep_SLVS)
|
||||
endif ()
|
||||
Vendored
-98
@@ -1,98 +0,0 @@
|
||||
# Copyright (c) 2020-2021 Intel Corporation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set(TBB_LINK_DEF_FILE_FLAG ${CMAKE_LINK_DEF_FILE_FLAG})
|
||||
set(TBB_DEF_FILE_PREFIX win${TBB_ARCH})
|
||||
|
||||
# Workaround for CMake issue https://gitlab.kitware.com/cmake/cmake/issues/18317.
|
||||
# TODO: consider use of CMP0092 CMake policy.
|
||||
string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
|
||||
set(TBB_WARNING_LEVEL $<$<BOOL:${TBB_STRICT}>:/W4> $<$<BOOL:${TBB_STRICT}>:/WX>)
|
||||
|
||||
# Warning suppression C4324: structure was padded due to alignment specifier
|
||||
set(TBB_WARNING_SUPPRESS /wd4324)
|
||||
set(TBB_TEST_COMPILE_FLAGS /bigobj)
|
||||
|
||||
if (MSVC_VERSION LESS_EQUAL 1900)
|
||||
# Warning suppression C4503 for VS2015 and earlier:
|
||||
# decorated name length exceeded, name was truncated.
|
||||
# More info can be found at
|
||||
# https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-1-c4503
|
||||
set(TBB_TEST_COMPILE_FLAGS ${TBB_TEST_COMPILE_FLAGS} /wd4503)
|
||||
endif()
|
||||
|
||||
set(TBB_LIB_COMPILE_FLAGS -D_CRT_SECURE_NO_WARNINGS /GS)
|
||||
set(TBB_COMMON_COMPILE_FLAGS /volatile:iso /FS /EHsc)
|
||||
|
||||
# Ignore /WX set through add_compile_options() or added to CMAKE_CXX_FLAGS if TBB_STRICT is disabled.
|
||||
if (NOT TBB_STRICT AND COMMAND tbb_remove_compile_flag)
|
||||
tbb_remove_compile_flag(/WX)
|
||||
endif()
|
||||
|
||||
if (WINDOWS_STORE OR TBB_WINDOWS_DRIVER)
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D_WIN32_WINNT=0x0A00)
|
||||
set(TBB_COMMON_LINK_FLAGS -NODEFAULTLIB:kernel32.lib -INCREMENTAL:NO)
|
||||
set(TBB_COMMON_LINK_LIBS OneCore.lib)
|
||||
endif()
|
||||
|
||||
if (WINDOWS_STORE)
|
||||
if (NOT CMAKE_SYSTEM_VERSION EQUAL 10.0)
|
||||
message(FATAL_ERROR "CMAKE_SYSTEM_VERSION must be equal to 10.0")
|
||||
endif()
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /ZW /ZW:nostdlib)
|
||||
# CMake define this extra lib, remove it for this build type
|
||||
string(REGEX REPLACE "WindowsApp.lib" "" CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES}")
|
||||
|
||||
if (TBB_NO_APPCONTAINER)
|
||||
set(TBB_LIB_LINK_FLAGS ${TBB_LIB_LINK_FLAGS} -APPCONTAINER:NO)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (TBB_WINDOWS_DRIVER)
|
||||
# Since this is universal driver disable this variable
|
||||
set(CMAKE_SYSTEM_PROCESSOR "")
|
||||
# CMake define list additional libs, remove it for this build type
|
||||
set(CMAKE_CXX_STANDARD_LIBRARIES "")
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D _UNICODE /DUNICODE /DWINAPI_FAMILY=WINAPI_FAMILY_APP /D__WRL_NO_DEFAULT_LIB__)
|
||||
endif()
|
||||
|
||||
if (NOT DEFINED TBB_ENABLE_IPO)
|
||||
if (DEFINED CMAKE_INTERPROCEDURAL_OPTIMIZATION)
|
||||
set(TBB_ENABLE_IPO ${CMAKE_INTERPROCEDURAL_OPTIMIZATION})
|
||||
else()
|
||||
set(TBB_ENABLE_IPO ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (TBB_ENABLE_IPO)
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)")
|
||||
if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)")
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg)
|
||||
endif()
|
||||
set(TBB_OPENMP_NO_LINK_FLAG TRUE)
|
||||
set(TBB_IPO_COMPILE_FLAGS $<$<NOT:$<CONFIG:Debug>>:-flto>)
|
||||
else()
|
||||
set(TBB_IPO_COMPILE_FLAGS $<$<NOT:$<CONFIG:Debug>>:/GL>)
|
||||
set(TBB_IPO_LINK_FLAGS $<$<NOT:$<CONFIG:Debug>>:-LTCG> $<$<NOT:$<CONFIG:Debug>>:-INCREMENTAL:NO>)
|
||||
endif()
|
||||
else()
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)" AND CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)")
|
||||
set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg)
|
||||
endif()
|
||||
set(TBB_IPO_COMPILE_FLAGS "")
|
||||
set(TBB_IPO_LINK_FLAGS "")
|
||||
endif()
|
||||
|
||||
set(TBB_OPENMP_FLAG /openmp)
|
||||
Vendored
+1
-6
@@ -1,6 +1,4 @@
|
||||
if (MSVC)
|
||||
set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/MSVC.cmake ./cmake/compilers/MSVC.cmake)
|
||||
elseif (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
if (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/GNU.cmake ./cmake/compilers/GNU.cmake)
|
||||
else()
|
||||
set(_patch_command "")
|
||||
@@ -15,9 +13,6 @@ orcaslicer_add_cmake_project(
|
||||
-DTBB_BUILD_SHARED=OFF
|
||||
-DTBB_BUILD_TESTS=OFF
|
||||
-DTBB_TEST=OFF
|
||||
-DTBB_DISABLE_HWLOC_AUTOMATIC_SEARCH=ON
|
||||
-DTBB_ENABLE_IPO=OFF
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
-DCMAKE_DEBUG_POSTFIX=_debug
|
||||
)
|
||||
|
||||
Vendored
-9
@@ -42,15 +42,6 @@ else ()
|
||||
message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}")
|
||||
endif ()
|
||||
|
||||
# Draco's tools and NLopt's testopt compile sources that are also in their
|
||||
# static library. MSBuild passes the library before the objects and lld-link
|
||||
# resolves as it goes, so the library's copy wins and the object then reads as
|
||||
# a duplicate. Nothing uses those executables, so let lld keep the first one.
|
||||
set(DEP_LLD_FORCE_MULTIPLE "")
|
||||
if (CMAKE_GENERATOR MATCHES "Visual Studio" AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(DEP_LLD_FORCE_MULTIPLE "-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
|
||||
endif ()
|
||||
|
||||
if (${DEP_DEBUG})
|
||||
set(DEP_BOOST_DEBUG "debug")
|
||||
else ()
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Compiles Objects/unicodectype.c without optimisation. VS 2026's ARM64 code
|
||||
generator needs about 27 GB for _PyUnicode_ToNumeric, a switch with 1951
|
||||
cases. CPython has the same workaround (python/cpython#153668). -->
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Update="..\Objects\unicodectype.c">
|
||||
<Optimization>Disabled</Optimization>
|
||||
<WholeProgramOptimization>false</WholeProgramOptimization>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Vendored
+8
-20
@@ -15,13 +15,7 @@ if(WIN32)
|
||||
# See https://github.com/python/cpython/issues/153438
|
||||
# Patch from https://github.com/python/cpython/pull/153608
|
||||
# This patch has not been merged to 3.12 yet so we need to apply it manually
|
||||
#
|
||||
# Without core.autocrlf=false the patched find_python.bat comes out LF and
|
||||
# cmd.exe cannot find its goto labels.
|
||||
set(_patch_cmd git init
|
||||
&& ${GIT_EXECUTABLE} -c core.autocrlf=false apply --verbose
|
||||
--ignore-space-change --whitespace=fix
|
||||
${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch)
|
||||
set(_patch_cmd git init && ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch)
|
||||
|
||||
if(MSVC_VERSION EQUAL 1800)
|
||||
set(_python_platform_toolset v120)
|
||||
@@ -59,9 +53,12 @@ if(WIN32)
|
||||
set(_python_pcbuild_output_dir win32)
|
||||
endif()
|
||||
|
||||
# pybind11 undefines _DEBUG around Python.h so a debug build links the
|
||||
# release python3xx.lib; Py_DEBUG could not load release plugin modules.
|
||||
set(_python_pcbuild_config Release)
|
||||
set(_python_layout_debug OFF)
|
||||
if(DEFINED DEP_DEBUG AND DEP_DEBUG)
|
||||
set(_python_pcbuild_config Debug)
|
||||
set(_python_layout_debug ON)
|
||||
endif()
|
||||
|
||||
# CPython's PCbuild needs a 64-bit-hosted toolchain: find_msbuild.bat picks the
|
||||
# 32-bit Bin\MSBuild.exe, whose x86 cl.exe/link.exe run out of address space
|
||||
@@ -88,18 +85,8 @@ if(WIN32)
|
||||
list(APPEND _python_env_args "PreferredToolArchitecture=${_python_tool_arch}")
|
||||
endif()
|
||||
|
||||
# MSBuild reads extra switches from PCbuild/msbuild.rsp.
|
||||
set(_python_rsp "/p:PlatformToolset=${_python_platform_toolset}\n")
|
||||
# VS 2026's ARM64 code generator needs about 27 GB for one function in
|
||||
# Objects/unicodectype.c (python/cpython#153668); the property sheet compiles
|
||||
# that file without optimisation.
|
||||
if(_python_pcbuild_platform STREQUAL "ARM64")
|
||||
file(TO_NATIVE_PATH "${CMAKE_CURRENT_LIST_DIR}/arm64-unicodectype.props" _python_arm64_props)
|
||||
string(APPEND _python_rsp "/p:ForceImportAfterCppTargets=\"${_python_arm64_props}\"\n")
|
||||
endif()
|
||||
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/python3-msbuild.rsp" "${_python_rsp}")
|
||||
set(_conf_cmd
|
||||
${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_BINARY_DIR}/python3-msbuild.rsp" <SOURCE_DIR>/PCbuild/msbuild.rsp
|
||||
cmd /c "echo /p:PlatformToolset=${_python_platform_toolset}>PCbuild\\msbuild.rsp"
|
||||
)
|
||||
set(_build_cmd
|
||||
${CMAKE_COMMAND} -E env ${_python_env_args}
|
||||
@@ -114,6 +101,7 @@ if(WIN32)
|
||||
-DPYTHON_BUILD_DIR=<SOURCE_DIR>/PCbuild/${_python_pcbuild_output_dir}
|
||||
-DPYTHON_DEST_DIR=${DESTDIR}/libpython
|
||||
-DPYTHON_LAYOUT_ARCH=${_python_layout_arch}
|
||||
-DPYTHON_DEBUG=${_python_layout_debug}
|
||||
-P ${CMAKE_CURRENT_LIST_DIR}/stage_windows.cmake
|
||||
)
|
||||
elseif(APPLE)
|
||||
|
||||
Vendored
+15
-1
@@ -9,6 +9,9 @@ foreach(_var PYTHON_SOURCE_DIR PYTHON_BUILD_DIR PYTHON_DEST_DIR PYTHON_LAYOUT_AR
|
||||
endforeach()
|
||||
|
||||
set(_python_exe "${PYTHON_BUILD_DIR}/python.exe")
|
||||
if(PYTHON_DEBUG)
|
||||
set(_python_exe "${PYTHON_BUILD_DIR}/python_d.exe")
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${_python_exe}")
|
||||
message(FATAL_ERROR "Built Python executable not found: ${_python_exe}")
|
||||
@@ -46,10 +49,21 @@ endif()
|
||||
set(_required_files
|
||||
"${PYTHON_DEST_DIR}/Lib/encodings/__init__.py"
|
||||
"${PYTHON_DEST_DIR}/include/Python.h"
|
||||
)
|
||||
|
||||
if(PYTHON_DEBUG)
|
||||
list(APPEND _required_files
|
||||
"${PYTHON_DEST_DIR}/python_d.exe"
|
||||
"${PYTHON_DEST_DIR}/python${_python_abi}_d.dll"
|
||||
"${PYTHON_DEST_DIR}/libs/python${_python_abi}_d.lib"
|
||||
)
|
||||
else()
|
||||
list(APPEND _required_files
|
||||
"${PYTHON_DEST_DIR}/python.exe"
|
||||
"${PYTHON_DEST_DIR}/python${_python_abi}.dll"
|
||||
"${PYTHON_DEST_DIR}/libs/python${_python_abi}.lib"
|
||||
)
|
||||
)
|
||||
endif()
|
||||
|
||||
foreach(_required_file IN LISTS _required_files)
|
||||
if(NOT EXISTS "${_required_file}")
|
||||
|
||||
Vendored
-24
@@ -1,26 +1,3 @@
|
||||
# wxInspector finds wxWidgets through CMake's FindwxWidgets module, which only
|
||||
# searches lib/vc*_lib because _WX_TOOL is hardcoded to "vc". A superbuild driven
|
||||
# by clang-cl installs wxWidgets into lib/clang_x64_lib, so hand the module the
|
||||
# directory wxWidgets actually used, derived the same way wxWidgetsConfig.cmake
|
||||
# derives it.
|
||||
set(_wxinspector_wx_hints "")
|
||||
if (MSVC)
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(_wx_compiler_prefix "clang")
|
||||
else ()
|
||||
set(_wx_compiler_prefix "vc")
|
||||
endif ()
|
||||
set(_wx_arch_suffix "")
|
||||
if (CMAKE_GENERATOR_PLATFORM AND NOT CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
|
||||
string(TOLOWER "_${CMAKE_GENERATOR_PLATFORM}" _wx_arch_suffix)
|
||||
elseif (CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(_wx_arch_suffix "_x64")
|
||||
endif ()
|
||||
set(_wxinspector_wx_hints
|
||||
"-DwxWidgets_ROOT_DIR=${DESTDIR}"
|
||||
"-DwxWidgets_LIB_DIR=${DESTDIR}/lib/${_wx_compiler_prefix}${_wx_arch_suffix}_lib")
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(
|
||||
wxInspector
|
||||
URL https://github.com/Noisyfox/wxInspector/archive/refs/tags/v1.0.0.zip
|
||||
@@ -29,7 +6,6 @@ orcaslicer_add_cmake_project(
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_CXX_FLAGS="-DwxDEBUG_LEVEL=0"
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
${_wxinspector_wx_hints}
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
diff --git a/src/osx/cocoa/colour.mm b/src/osx/cocoa/colour.mm
|
||||
index 31515d146f..86b33e94a2 100644
|
||||
--- a/src/osx/cocoa/colour.mm
|
||||
+++ b/src/osx/cocoa/colour.mm
|
||||
@@ -125,3 +125,3 @@
|
||||
wxOSXEffectiveAppearanceSetter helper;
|
||||
- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] )
|
||||
+ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] )
|
||||
return [colRGBA redComponent];
|
||||
@@ -134,3 +134,3 @@
|
||||
wxOSXEffectiveAppearanceSetter helper;
|
||||
- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] )
|
||||
+ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] )
|
||||
return [colRGBA greenComponent];
|
||||
@@ -143,3 +143,3 @@
|
||||
wxOSXEffectiveAppearanceSetter helper;
|
||||
- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] )
|
||||
+ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] )
|
||||
return [colRGBA blueComponent];
|
||||
@@ -152,3 +152,3 @@
|
||||
wxOSXEffectiveAppearanceSetter helper;
|
||||
- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] )
|
||||
+ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] )
|
||||
return [colRGBA alphaComponent];
|
||||
@@ -160,3 +160,3 @@
|
||||
{
|
||||
- return [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] != nil;
|
||||
+ return [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] != nil;
|
||||
}
|
||||
Vendored
-10
@@ -21,22 +21,12 @@ else ()
|
||||
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
|
||||
endif ()
|
||||
|
||||
set(_wx_patch_command "")
|
||||
if (APPLE)
|
||||
set(_wx_patch_command
|
||||
${GIT_EXECUTABLE} checkout -f -- src/osx/cocoa/colour.mm
|
||||
COMMAND ${GIT_EXECUTABLE} apply --verbose
|
||||
${CMAKE_CURRENT_LIST_DIR}/0001-macos-use-srgb-colour-components.patch
|
||||
)
|
||||
endif ()
|
||||
|
||||
orcaslicer_add_cmake_project(
|
||||
wxWidgets
|
||||
GIT_REPOSITORY "https://github.com/SoftFever/Orca-deps-wxWidgets"
|
||||
GIT_TAG v3.3.2
|
||||
GIT_SHALLOW ON
|
||||
GIT_SUBMODULES 3rdparty/catch 3rdparty/pcre 3rdparty/libwebp
|
||||
PATCH_COMMAND ${_wx_patch_command}
|
||||
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
|
||||
CMAKE_ARGS
|
||||
-DwxBUILD_PRECOMP=ON
|
||||
|
||||
@@ -162,7 +162,7 @@ static bool stl_read(stl_file *stl, FILE *fp, int first_facet, bool first, Impor
|
||||
rewind(fp);
|
||||
try{
|
||||
char solid_name[256];
|
||||
int res_solid = fscanf(fp, " solid %255[^\n]", solid_name);
|
||||
int res_solid = fscanf(fp, " solid %[^\n]", solid_name);
|
||||
if (res_solid == 1) {
|
||||
char* mw_position = strstr(solid_name, "MW");
|
||||
if (mw_position != NULL) {
|
||||
@@ -170,7 +170,7 @@ static bool stl_read(stl_file *stl, FILE *fp, int first_facet, bool first, Impor
|
||||
char version_str[16];
|
||||
char model_id_str[128];
|
||||
char country_code_str[16];
|
||||
int num_values = sscanf(mw_position + 3, "%15s %127s %15s", version_str, model_id_str, country_code_str);
|
||||
int num_values = sscanf(mw_position + 3, "%s %s %s", version_str, model_id_str, country_code_str);
|
||||
if (num_values == 3) {
|
||||
if (strcmp(version_str, "1.0") == 0) {
|
||||
model_id = model_id_str;
|
||||
|
||||
@@ -37,11 +37,7 @@ target_include_directories(Clipper2
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
if (MSVC AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
target_compile_options(Clipper2 PRIVATE /W4 /WX)
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
target_compile_options(Clipper2 PRIVATE /W4)
|
||||
endif()
|
||||
else()
|
||||
target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
target_link_libraries(Clipper2 PUBLIC -lm)
|
||||
|
||||
@@ -2856,7 +2856,6 @@ const ImWchar* ImFontAtlas::GetGlyphRangesDefault()
|
||||
{
|
||||
0x0020, 0x00FF, // Basic Latin + Latin Supplement
|
||||
0x2000, 0x206F, // General Punctuation
|
||||
0x2103, 0x2103, // ℃ Celsius symbol
|
||||
0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana
|
||||
0x31F0, 0x31FF, // Katakana Phonetic Extensions
|
||||
0xFF00, 0xFFEF, // Half-width characters
|
||||
|
||||
@@ -11,8 +11,6 @@ add_library(miniz_static STATIC
|
||||
|
||||
if(${CMAKE_C_COMPILER_ID} STREQUAL "GNU")
|
||||
target_compile_definitions(miniz_static PRIVATE _GNU_SOURCE)
|
||||
elseif (CMAKE_C_COMPILER_ID STREQUAL "Clang" AND CMAKE_C_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
target_compile_options(miniz_static PRIVATE /clang:-Wno-error=incompatible-pointer-types)
|
||||
endif()
|
||||
|
||||
target_link_libraries(miniz INTERFACE miniz_static)
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
# Deferred Page Construction: High Level Design
|
||||
|
||||
## Why it exists
|
||||
|
||||
The main window is a notebook of tabs, and only one of them is on screen when the first
|
||||
frame appears. Others are opened later in the session, some never, and some only exist
|
||||
for certain printers. Building every tab before the first frame makes each startup pay for
|
||||
tabs the user may never open.
|
||||
|
||||
This subsystem builds a tab the first time it is shown, and builds the rest in small units
|
||||
while the user is idle after startup. Startup pays only for what the first frame shows,
|
||||
the other tabs are usually ready before anyone opens them, and a click that lands in the
|
||||
middle of the idle build waits for one unit at most. Work that is not a tab, such as a
|
||||
dialog or the 3D view's GL resources, uses the same machinery.
|
||||
|
||||
## The parts
|
||||
|
||||
The parts are independent. A holder can hold anything a factory makes, a placeholder page
|
||||
is a holder with a widget, a staged object can live outside a holder, and the scheduler
|
||||
knows none of them; it runs tasks, which the main window makes from the holders.
|
||||
|
||||
### The holder: `Lazy<T>`
|
||||
|
||||
A holder keeps one object and the factory that makes it. The rest of the app reads the
|
||||
object if it exists, makes sure it exists now when about to show or navigate to it, or runs
|
||||
something once it exists. A type with one instance in the app gets these as statics
|
||||
through `LazyInstance<T>`, so callers need no reference to the main window, and all of them
|
||||
are harmless while no holder exists.
|
||||
|
||||
The holder guarantees that callers see the object only once it is completely built. A
|
||||
build cannot re-enter itself, so a nested request finds nothing yet, and a factory that
|
||||
returns null or a unit that throws leaves the holder and the scheduler able to carry on.
|
||||
The holder does not own the object; its wx parent does, as for any window. The holder has
|
||||
no wx dependency and is unit-tested.
|
||||
|
||||
### The placeholder page: `LazyPage<Panel>`
|
||||
|
||||
The notebook needs a page object for a tab to exist and for tabs to be inserted and
|
||||
removed by pointer, and the placeholder is that object. It builds the real panel inside
|
||||
itself the first time it is shown and forwards showing and hiding afterwards, so a panel's
|
||||
own show handling stays its activation hook. Nothing builds while the main window is
|
||||
hidden; the window's first show builds the start page. A page that is out of the book is
|
||||
not prebuilt. A panel built while its page is hidden stays hidden, and gets the theming
|
||||
the window applied before the panel existed.
|
||||
|
||||
### Staged construction: `StagedBuild`
|
||||
|
||||
A constructor too big to be one unit builds a skeleton and queues the rest as steps, which
|
||||
run one per unit. A child panel's steps can be forwarded to its parent, and the parent is
|
||||
complete only once the child is. Nothing may use what a step builds before the last step
|
||||
has run, so staged panels follow these constraints:
|
||||
|
||||
- members created in steps start out null, so a partly built panel can be destroyed;
|
||||
- timers, event handlers and destructors that touch step content check that the panel is
|
||||
complete first;
|
||||
- nothing takes focus while off screen, since a unit may run while the user is typing
|
||||
elsewhere;
|
||||
- a widget added by a step keeps its place in the sizer through an empty slot the skeleton
|
||||
creates.
|
||||
|
||||
### The scheduler: `IdleScheduler` and `PrebuildQueue`
|
||||
|
||||
The queue holds tasks in order, and a slice runs units of the first pending task until
|
||||
the task finishes, the time budget is spent, or input arrives. It has no wx dependency and
|
||||
is unit-tested with a fake clock. A task whose work goes away, such as a tab removed from
|
||||
the book, is skipped, and becomes pending again if the work comes back.
|
||||
|
||||
A slice runs only once the user has been idle for a short quiet time, and each slice is its
|
||||
own timer message, so paint, timers and input queued in between are handled before the
|
||||
next slice. Posting slices as pending events would not do that, because wx drains every
|
||||
pending event before the next native message. On GTK a timer that is always due starves
|
||||
the lower-priority sources that repaint and deliver posted events, so slices are a few
|
||||
milliseconds apart. On Windows a slice also waits while the native queue holds input,
|
||||
not counting mouse moves, which Windows synthesizes whenever a window appears under the
|
||||
cursor. A slice never runs inside a `wxYield()`, where it would build pages in the middle of
|
||||
the code that yielded. A unit cannot be interrupted, so the largest unit bounds how long a
|
||||
click can wait.
|
||||
When nothing is pending the timer stops and the subsystem costs nothing.
|
||||
|
||||
The main window owns the scheduler because it owns what the tasks build, and clearing the
|
||||
queue with the window keeps a task from outliving its object. Each owner provides its own
|
||||
tasks, such as a tab, a dialog, the Prepare tab's settings page one option group at a
|
||||
time, the Prepare page's layout at the size the book gives its pages, or the 3D view's GL
|
||||
resources.
|
||||
|
||||
### The 3D view's GL resources
|
||||
|
||||
OpenGL is loaded on the Prepare tab's canvas. When the start page is not Prepare, loading
|
||||
it is an idle task that makes the context current on the hidden canvas, so the start page
|
||||
paints first and Prepare never appears. Loading it on a shown canvas under `Freeze()` holds
|
||||
back the start page's paint, and on GTK `Freeze()` cannot hide the canvas, which is a
|
||||
native child window or a Wayland subsurface drawn outside GTK. A hidden Windows child
|
||||
window keeps its device context, macOS attaches the context to a hidden view, and GTK
|
||||
creates the canvas's surface when the widget is realized, so on GTK the task realizes the
|
||||
canvas first. If the context cannot be made current, the canvas's first render loads the
|
||||
resources.
|
||||
|
||||
## Rules
|
||||
|
||||
**Before the first frame.** Only the start page and the Prepare tab's plater are built
|
||||
before the first frame. Everything else goes through a holder.
|
||||
|
||||
**Reaching a lazy object.** Callers use the type's statics. Reading it if built is for
|
||||
things the object can live without, such as a rescale, a color change or a status refresh.
|
||||
Making sure it exists is for navigating to it or showing it. Running something once it is
|
||||
built is for state it would not fetch for itself on construction. A panel that pulls its
|
||||
state when constructed only ever needs to be read if built.
|
||||
|
||||
**Unit size.** A unit should fit in one slice on a fast machine. A constructor over that is
|
||||
staged, and a single widget over it is accepted unless the widget itself can be split.
|
||||
|
||||
**Order.** Tasks run cheapest and most likely to be opened first. Each holder's order is
|
||||
given where it is created, with gaps so a new task fits between its neighbors. A negative
|
||||
order is never prebuilt, for something few sessions open that costs more to build unasked
|
||||
than it saves.
|
||||
|
||||
## Adopting it
|
||||
|
||||
A lazy tab needs a panel type deriving from `LazyInstance`, a placeholder page the main
|
||||
window creates once with its order and registers for the idle build, and every use of the
|
||||
panel outside the main window going through the statics. Its constructor has to cope with
|
||||
the main window already existing and the user being busy elsewhere, so it takes no focus
|
||||
while off screen, and it does all of its own setup, since the main window does nothing to a
|
||||
panel after creating it.
|
||||
|
||||
To stage a heavy constructor, keep the skeleton in the constructor, move the rest into
|
||||
steps in its original order, and follow the staged-construction constraints. Measure the
|
||||
units; a step that is still one big widget is split inside the widget or accepted.
|
||||
|
||||
## Verifying
|
||||
|
||||
The log lists the queue when it is registered, reports each completed task at info level
|
||||
and each slice and unit at debug level, and reports every on-demand build with its units
|
||||
and time. A task's completion line counts only the slice it finished in. A run from the
|
||||
configured start page should show every registered task complete in order, with no unit
|
||||
longer than intended. A click on a tab during the idle build should show an on-demand
|
||||
build for what was left, with the slices resuming once the user is idle again.
|
||||
@@ -1,213 +0,0 @@
|
||||
# Design tab — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
The Design tab is a parametric CAD environment inside the slicer: sketch, constrain, build
|
||||
solid features, commit the result to the plate. It exists because the alternative is a round
|
||||
trip through an external CAD application, and that round trip discards design intent at both
|
||||
ends — a part edited after slicing comes back as a mesh rather than as the feature history
|
||||
that produced it. Keeping the model in the project means a dimension can be changed after the
|
||||
part has been sliced, with the nozzle diameter, the build volume and the material already
|
||||
known.
|
||||
|
||||
Its coupling to the rest of the application is deliberately narrow. It adds no stage to the
|
||||
slicing pipeline and touches neither the preset system nor `Tab`. It reaches the rest of Orca
|
||||
in two places: **Commit to Plate**, which hands finished solids to Prepare as ordinary model
|
||||
objects, and one optional 3MF archive entry that carries the recipe. Everything else is
|
||||
contained in `src/libslic3r/CAD/` and `src/slic3r/GUI/CAD/`.
|
||||
|
||||
The user-facing manual lives in the wiki
|
||||
([Design Tab](https://www.orcaslicer.com/wiki/design_tab)), not here. This document covers the
|
||||
parts of the design that the code does not make evident.
|
||||
|
||||
## The model is a recipe
|
||||
|
||||
`CadDocument` holds an ordered list of `CadFeature` and nothing else that matters. Bodies,
|
||||
meshes and display geometry are **derived**: `recompute()` replays the feature list from the
|
||||
start and rebuilds them. Editing a dimension set twenty features ago is therefore an ordinary
|
||||
edit — everything downstream is rebuilt by the same replay that built it the first time.
|
||||
|
||||
Two consequences follow from deriving rather than storing:
|
||||
|
||||
- **Undo is a snapshot of `features` alone.** The caller calls `checkpoint()` before the
|
||||
mutations that make up one user action; undo restores that snapshot and recomputes. Because
|
||||
everything else is derived, one checkpoint is exactly one `Ctrl+Z` step and the restored
|
||||
state is exact rather than approximately reconstructed. The tab keeps this stack itself; it
|
||||
is not Orca's project snapshot system, which operates on `Model` objects the Design tab does
|
||||
not own until Commit.
|
||||
- **Face and edge ids are session-scoped.** They are indices into `TopExp::MapShapes`, so a
|
||||
rebuild invalidates every one of them. `CadDocument::topo_generation` is bumped on every
|
||||
rebuild so a holder of an id can discover that it is stale instead of silently addressing a
|
||||
different edge. The counter is deliberately not serialized: an id means nothing outside the
|
||||
run that produced it.
|
||||
|
||||
## Geometry kernel and dependency surface
|
||||
|
||||
The kernel is OCCT, which OrcaSlicer **already** links — `Format/STEP.cpp`, `Format/svg.cpp`
|
||||
and `Shape/TextShape.cpp` use it upstream. The Design tab adds no third-party dependency; it
|
||||
widens the existing OCCT build by one module flag in `deps/OCCT/OCCT.cmake`:
|
||||
|
||||
```cmake
|
||||
-DBUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD}
|
||||
```
|
||||
|
||||
Most of that module's twelve toolkits were already being built, because `DataExchange` — the
|
||||
STEP path upstream ships — depends on them. The delta is `TKFillet` (used through
|
||||
`BRepFilletAPI`), `TKOffset` (`BRepOffsetAPI`) and `TKFeat`, which nothing here references but
|
||||
which the module flag builds anyway, because OCCT's module flags are all-or-nothing. On macOS
|
||||
and Linux OCCT links statically, so an unreferenced toolkit costs build time and no shipped
|
||||
bytes; on Windows OCCT builds shared, so the cost there is real DLL bytes. That Windows figure
|
||||
has not been measured, and `OCCT.cmake` says so rather than carrying a number that was derived
|
||||
from an incomplete toolkit list.
|
||||
|
||||
On Windows the packaging step asserts that every linked OCCT toolkit has a shipped DLL and
|
||||
fails the configure with the name of any that is missing, because the alternative failure — a
|
||||
deps prefix built with a different `SLIC3R_CAD` setting than the app — otherwise surfaces as a
|
||||
missing DLL at first launch.
|
||||
|
||||
## The sketch constraint solver
|
||||
|
||||
`src/libslic3r/slvs/` is a vendored subset of SolveSpace's `libslvs`: self-contained, no
|
||||
external dependencies, **GPL-3.0**, with its `LICENSE` preserved verbatim in the directory.
|
||||
`SketchSolver.cpp` is its only consumer and drives every sketch constraint in the tab.
|
||||
|
||||
OrcaSlicer is AGPL-3.0. GPLv3 §13 permits combining a GPLv3 work with an AGPLv3 work and
|
||||
AGPLv3 §13 grants the converse, so the combined work is distributable under AGPL-3.0 with the
|
||||
solver's GPLv3 terms preserved. The solver is vendored rather than fetched as a dependency
|
||||
because it is a pinned subset with no build system of its own; the cost of that choice is
|
||||
upstream-sync burden, paid deliberately to keep `deps/` unchanged.
|
||||
|
||||
## The SLIC3R_CAD gate
|
||||
|
||||
`SLIC3R_CAD` (default ON) compiles the tab and selects the OCCT module flag above. With it OFF
|
||||
the tab is not built and the deps prefix matches upstream exactly. The gate is cheap because
|
||||
the hooks the Design tab adds to shared GUI code — chiefly the `m_design_sketch_tool` member
|
||||
and the render, mouse and key hooks in `GLCanvas3D` — are null-guarded on the path they extend,
|
||||
so removing the tab removes behaviour rather than requiring the host code to be rewritten.
|
||||
|
||||
The flag has to agree between the dependencies and the application; that is what the DLL
|
||||
assertion above is checking.
|
||||
|
||||
## Project persistence
|
||||
|
||||
A project stores the recipe as one optional archive entry, `Metadata/orca_cad.bin`, backed by
|
||||
a single `std::string cad_recipe` on `Model`. The entry is written only when the string is
|
||||
non-empty, and readers that do not know it ignore it, so projects that contain no CAD model are
|
||||
byte-identical to what upstream would have written and older readers are unaffected.
|
||||
|
||||
The blob is a cereal binary archive whose layout is the field order of `CadFeature`'s
|
||||
save/load. That makes the format the one irreversible decision in the subsystem, and the rules
|
||||
that keep it survivable are:
|
||||
|
||||
- **Append only, never reorder.** Enums serialize positionally as their underlying integer, so
|
||||
inserting a value in the middle of `SketchConstraintType` or `CadFeatureType` reinterprets
|
||||
every constraint in every saved project. New fields go at the end.
|
||||
- **Features are length-framed.** Since v5 each feature is a length-prefixed, self-contained
|
||||
cereal stream, so a reader can skip a feature written by a newer build and stop cleanly on an
|
||||
older one. This is what makes appending a field a non-breaking change from here on. v4 and
|
||||
earlier still open through the pre-framing flat path; v1 is deliberately not loadable and has
|
||||
no migration path.
|
||||
- **A newer stamp is refused, not guessed at.** `deserialize_recipe` rejects a blob whose
|
||||
version exceeds `ORCA_CAD_RECIPE_VERSION` with a message naming both versions.
|
||||
- **The rules are held by fixtures, not by discipline.** `tests/data/cad_recipe_v{3,4,5}.bin`
|
||||
are checked-in blobs from the builds that wrote them, and the tests that load them fail if a
|
||||
field is reordered — which the in-memory round-trip test cannot detect. A regeneration test
|
||||
(`[.regen]`, not run by default) produces a fresh fixture when a new version is stamped.
|
||||
|
||||
`Import` features embed the imported solid as an OCCT BRep string inside the recipe rather than
|
||||
referencing the source file, so a project opens without the STEP or mesh it was built from.
|
||||
The cost is that saved projects are coupled to an OCCT BRep revision.
|
||||
|
||||
## The interaction contract
|
||||
|
||||
Three inputs carry the whole modelling loop — left click, right click and `Esc` — and the
|
||||
contract between them is stated in code rather than spread across handlers.
|
||||
|
||||
`DesignInteraction.hpp` defines a four-level LIFO stack whose enum value *is* the depth, so
|
||||
"which level does this press belong to" is a comparison:
|
||||
|
||||
| Level | Holds | One `Esc` press |
|
||||
| --- | --- | --- |
|
||||
| `Transient` | a value field or a popup menu | closes it; the tool stays armed |
|
||||
| `Gesture` | an uncommitted delta — an entity being drawn, a body being dragged | reverts it; committed work is untouched |
|
||||
| `Tool` | a feature card, an armed sketch tool, a constrain session | exits it; drawn entities survive |
|
||||
| `Idle` | nothing transient | clears the selection; leaves a sketch session only if it is empty |
|
||||
|
||||
`cad_escape_level()` is a `constexpr` free function over a POD of four booleans rather than a
|
||||
method on the panel, so the ordering that is the entire contract is checkable without a window,
|
||||
a GL context or an event loop — five `static_assert`s in the header do exactly that at compile
|
||||
time.
|
||||
|
||||
**The strict invariant: no level of `Esc` deletes a feature, discards a sketch that holds
|
||||
geometry, or rolls history back.** Destroying work needs a gesture that says so — `Del` on an
|
||||
explicit selection, the sketch ribbon's Cancel, which asks first, or `Ctrl+Z`. A sketch
|
||||
*session* is deliberately not a `Tool` level; it is the environment the `Idle` level lives in,
|
||||
which makes the destructive path unrepresentable rather than merely unlikely.
|
||||
|
||||
Right-click is read at button-up against two independent budgets — 3 px of drift and 200 ms —
|
||||
because drift alone still popped a menu at the end of a slow, careful orbit. The raycast uses
|
||||
the press position, not the release. An armed sketch tool that already consumed the right
|
||||
button (to terminate a chain, say) declines to also open a menu, through a read-and-clear flag.
|
||||
Past either budget the event is navigation, and navigation does not transition the state
|
||||
machine.
|
||||
|
||||
Entering a sketch changes three things at once so the mode is legible: a banner above the
|
||||
canvas (a sibling of the canvas, not a child over it — on GTK a child window over a
|
||||
`wxGLCanvas` is a native window and does not reliably stack over GL), the printer bed muted so
|
||||
a plate grid is never read as a sketch grid, and `N` to look normal to the plane. Code that
|
||||
changes any of the three belongs with a change to this section.
|
||||
|
||||
## The offer is generated, not hand-written
|
||||
|
||||
Right-clicking geometry opens the *offer*: eight families in a fixed order, each verb at a
|
||||
permanent row index, verbs that do not apply shown disabled **in place with their reason**
|
||||
rather than removed. The invariant is that a verb's row index is identical in every selection
|
||||
where it appears and that adding a verb never moves an existing one — the hand learns the
|
||||
position, so the menu is never re-sorted, compacted or adaptively ordered.
|
||||
|
||||
An invariant across 92 verbs and 20 selection kinds does not survive by review, so the map
|
||||
exists once, as data: `scripts/CAD/tool_atlas.json` carries every verb with its row, key, icon,
|
||||
accepted selections, preconditions and refusal string, and `scripts/CAD/gen_offer_table.py`
|
||||
emits `src/slic3r/GUI/CAD/DesignOffer.hpp` from it. The header is checked in and never
|
||||
hand-edited; `scripts/CAD/run-all-checks.sh` runs the generator with `--check` as its first
|
||||
rung, which is what makes "GENERATED — DO NOT EDIT" a fact rather than a request. The generator
|
||||
also refuses an atlas with a duplicate verb id, since `mcp_run_verb` resolves a verb by id and
|
||||
would make the second one unreachable.
|
||||
|
||||
The atlas and its generator sit in `scripts/CAD/` rather than in `docs/`: they are build inputs
|
||||
for a checked-in header, not documentation.
|
||||
|
||||
## Automation surface
|
||||
|
||||
`McpControl` exposes the document over JSON-RPC when `ORCA_CAD_MCP` is set in the environment,
|
||||
with `tools/orca_cad_mcp_bridge.py` as the client side. It describes the scene, queries
|
||||
topology, measures, and runs the same verbs the offer does — it re-implements nothing, so a
|
||||
scripted action and a clicked one cannot diverge. It is off unless the variable is set.
|
||||
|
||||
## Where the code lives
|
||||
|
||||
| Path | Role |
|
||||
| --- | --- |
|
||||
| `src/libslic3r/CAD/CadDocument.*` | the feature recipe, its replay, undo and serialization |
|
||||
| `src/libslic3r/CAD/GeometryEngine.*` | OCCT wrapper — faces, edges, booleans, healing |
|
||||
| `src/libslic3r/CAD/SketchEngine.*` | profile → wire → solid |
|
||||
| `src/libslic3r/CAD/SketchSolver.*` | constraint solving, over the vendored solver |
|
||||
| `src/libslic3r/slvs/` | vendored 2D constraint solver (GPLv3) |
|
||||
| `src/slic3r/GUI/CAD/DesignPanel.*` | the tab: toolbar, feature cards, tree, key maps |
|
||||
| `src/slic3r/GUI/CAD/DesignCanvas.*` | viewport integration |
|
||||
| `src/slic3r/GUI/CAD/DesignSketchTool.*` | in-canvas sketching |
|
||||
| `src/slic3r/GUI/CAD/DesignInteraction.hpp` | the Esc level contract |
|
||||
| `src/slic3r/GUI/CAD/DesignOffer.hpp` | generated offer table |
|
||||
| `scripts/CAD/tool_atlas.json` | source of truth for the offer |
|
||||
|
||||
## Verification
|
||||
|
||||
The kernel is covered by Catch2 suites in `tests/libslic3r/` (`test_caddocument`,
|
||||
`test_sketchconstraints`, `test_sketchedit`, `test_sketchimport`, `test_sketchinference`,
|
||||
`test_sketchprofile`, `test_slvs_constraints`), which need no display;
|
||||
`scripts/CAD/run-kernel-tests.sh` builds only `libslic3r_tests` and runs them headless.
|
||||
|
||||
The GUI half is not covered by CI, which has no OpenGL canvas or synthetic input: the ladders
|
||||
in `scripts/CAD/` drive a running application in a local rig instead, and
|
||||
`scripts/CAD/run-all-checks.sh` is the gate that runs all of them. A green kernel run says
|
||||
nothing about the viewport, so the two are reported separately rather than as one number.
|
||||
@@ -1,487 +0,0 @@
|
||||
# Filament IDs (`filament_id`)
|
||||
|
||||
`filament_id` identifies one **filament product**: one named spool product = one id, shared by
|
||||
all of that product's per-printer / per-nozzle variants, in every profile bundle that ships it.
|
||||
Devices use it to match a physical spool or tray to a filament preset. It is never per-color,
|
||||
per-printer, per-nozzle, or per-preset (per-preset identity is `setting_id`), and it is never
|
||||
per-bundle either — PolyLite PLA carries the same id whether the preset lives in the
|
||||
OrcaFilamentLibrary (OFL), Qidi, or Snapmaker bundle. The granularity is the name on the spool,
|
||||
not the brand behind it: `AAA PLA Lite` and `AAA PLA Pro` are two filaments with two ids, not
|
||||
variants of one.
|
||||
|
||||
**How it is generated:** an id is computed, never invented. `scripts/orca_profile_tool.py`
|
||||
mints it as a deterministic hash of the product's identity — the triple
|
||||
`(filament_vendor, filament_type, filament name)`, where the filament name is the preset name
|
||||
with its `@...` variant suffix stripped — producing an 8-character `OF*` code that is the
|
||||
same for that product in every bundle, in every PR, on every machine. For example, Polymaker's
|
||||
PolyLite PLA presets (`PolyLite PLA @base`, `PolyLite PLA@Q2-Series`, …) resolve
|
||||
`filament_vendor` `Polymaker`, `filament_type` `PLA`, and filament name `PolyLite PLA`; hashing
|
||||
`filament_product/Polymaker/PLA/PolyLite PLA` yields `OF5CgdDq`, and that is the id the
|
||||
OrcaFilamentLibrary, OrcaArena, Qidi, and Snapmaker bundles all arrive at independently
|
||||
(derivation details in the Minting section).
|
||||
|
||||
**How it is used:** at runtime the id is the join key between hardware and profiles.
|
||||
When a printer reports what a tray holds (Bambu AMS, Qidi box, Creality CFS,
|
||||
Klipper, Snapmaker), OrcaSlicer matches the reported id against the filament presets
|
||||
compatible with that printer to select the right profile; other features — tray display
|
||||
names, support-material detection, vitrification warnings, multi-nozzle filament grouping —
|
||||
look up material properties by id alone. An id that changes is not forwarded anywhere: a
|
||||
tray or record still holding the old value falls back to matching by material type until the
|
||||
user re-selects the filament, so identity changes are made deliberately and rarely.
|
||||
|
||||
This page is the rule for authoring `filament_id` in system profiles
|
||||
(`resources/profiles/**`). CI enforces everything below; the short version is:
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Never write a `filament_id` value by hand.** A new filament gets its id from
|
||||
> `python scripts/orca_profile_tool.py generate-id`; one already in the tree has one — inherit it.
|
||||
|
||||
## The design
|
||||
|
||||
Because several consumers match **globally by id alone, first hit wins** (see the next
|
||||
section), any two materials sharing one id feed wrong data somewhere — a wrong tray name, a
|
||||
wrong support-material flag, a wrong nozzle grouping — and inside one printer a duplicated id
|
||||
makes AMS spool matching a coin toss. Hand-written ids produce such collisions constantly, so
|
||||
the system is built to make them impossible: an id is a pure hash of the product's identity —
|
||||
no registry to maintain, no next-free-number ceremony, no way for two concurrent PRs to race
|
||||
for the same number, and no way to get it wrong by hand, because you never write it by hand.
|
||||
CI holds every id in the tree to that rule, so the profiles themselves are the whole record of
|
||||
which products exist and which bundles ship them.
|
||||
|
||||
## Who consumes the id
|
||||
|
||||
The canonical consumer is tray-to-preset matching: a device reports a tray material id
|
||||
(`tray_info_idx`), and the shared matching pipeline (`PresetBundle::sync_ams_list` and
|
||||
friends) resolves it to a preset. The matcher is printer-scoped and first-match-wins:
|
||||
scanning only compatible root presets — system roots plus user-made custom filaments, which
|
||||
are user roots carrying their own `P*` ids; a preset derived from another resolves through
|
||||
its root and never matches directly — it picks the first one whose `filament_id` equals the
|
||||
tray's. On a miss it falls back by filament type: a system `Generic <type>` preset
|
||||
(matched by name, then by type similarity), else the slot's previous selection, else any compatible system generic or,
|
||||
failing that, any compatible system preset, else the slot is skipped — every fallback
|
||||
selection surfaces a user-visible notice.
|
||||
|
||||
Today only the Bambu AMS integration follows this pattern end to end — the device itself
|
||||
reports the id, `BBLPrinterAgent` translates it out of Bambu's catalog into ours, and the
|
||||
pipeline does all the matching. The other device integrations still synthesize a preset id
|
||||
client-side in their agents (by type, brand, or color lookups against the loaded presets)
|
||||
before the pipeline runs; they are intended to converge on the same pattern, with the
|
||||
device-reported tray material id flowing through the shared matcher.
|
||||
|
||||
| Ecosystem | Where the tray id comes from today |
|
||||
| --- | --- |
|
||||
| Bambu AMS | the device itself (RFID / user tray setting), in Bambu's own `GF*` catalog; `BBLPrinterAgent` rewrites it into our id before the matcher sees it (see [The Bambu catalog map](#the-bambu-catalog-map)) |
|
||||
| Qidi box | composed at runtime as `QD_<series>_<vendor>_<typeidx>` — vendor and type indices from the device's per-slot saved variables, the series digit inferred client-side from the printer model/name. No preset carries a `QD_*` value, so the slot currently resolves by filament type; mapping the composed id onto the filament's minted id belongs in the agent |
|
||||
| Creality CFS | runtime brand/type scoring returns the winning preset's id |
|
||||
| Klipper (AFC / Happy Hare) | runtime lookup by filament type |
|
||||
| Snapmaker | runtime color/vendor/type match |
|
||||
|
||||
Tray-to-preset matching is printer-scoped, but **several consumers match globally by id alone,
|
||||
first hit wins**: tray display names, `filament_is_support`, vitrification warnings, and
|
||||
multi-nozzle filament grouping in the slicing pipeline (`FilamentGroup::try_merge_filaments`
|
||||
merges plate slots sharing one `(filament_id, color)` pair, with matching
|
||||
extruder-printability, onto one nozzle group; the engine is implemented but no grouping path
|
||||
calls it yet).
|
||||
Two *different* materials sharing one id
|
||||
feed wrong data to those consumers even when the presets live in different vendors — so
|
||||
cross-material id sharing is never safe. Within one printer, duplicate ids break AMS matching:
|
||||
the matcher picks whichever preset loads first (it now logs an "Ambiguous AMS filament match"
|
||||
warning, but the pick is still arbitrary) and the tray-edit dialog, which lists one entry per
|
||||
id, hides the second preset entirely. The profile validator's `-f` check
|
||||
(`check_filament_subtypes` → `PresetBundle::check_duplicate_filament_subtypes`) rejects this
|
||||
per printer, and CI runs it tree-wide.
|
||||
|
||||
Two more consumer-side facts worth knowing:
|
||||
|
||||
- The machine-facing dialogs (AMS tray edit, AMS dry control, calibration history, extrusion
|
||||
calibration) offer the filaments a connected printer can use by the same compatibility rule
|
||||
the plater uses (an empty `compatible_printers` means *every* printer). Alias shadowing
|
||||
still applies: a vendor's same-name profile supersedes the library generic. That is what
|
||||
puts Orca Filament Library materials in those lists — deduplicated to one entry per
|
||||
`filament_id` in the AMS and calibration-history dialogs, while extrusion calibration
|
||||
deliberately lists every matching preset by full name.
|
||||
- The id is load-bearing at startup: an instantiated system filament (one marked
|
||||
`"instantiation": "true"` — see the structure rules) that resolves **no**
|
||||
`filament_id` anywhere in its `inherits` chain is a hard load error in the C++ loader
|
||||
(`Can not find filament_id for <name>`) that discards the entire vendor bundle (for the
|
||||
OrcaFilamentLibrary itself the failure is messier: library presets loaded before the
|
||||
failing one survive, and every vendor bundle whose filaments inherit from the library is
|
||||
then discarded for want of a base). CI's structure check catches this before it ships.
|
||||
|
||||
## Do I need a new id? The one-question test
|
||||
|
||||
> **Would a user consider this a different spool product than anything already in the tree?**
|
||||
|
||||
Different polymer, different sub-brand (Basic / Matte / Silk / HF), fiber-filled sibling, or a
|
||||
second selectable diameter → **new filament, new id**. The same spool tuned for another printer
|
||||
or nozzle → **join the existing filament** (keep its base name and inherit it; no id
|
||||
key needed). Tuning a generic material → **join the OrcaFilamentLibrary filament** (inherit
|
||||
`Generic X @System` and keep the `Generic X` base name; no id key needed).
|
||||
|
||||
| Situation | id |
|
||||
| --- | --- |
|
||||
| Per-printer / per-nozzle variant of an existing material | same id (inherit it) |
|
||||
| Sub-brand or product line (PLA vs PLA Matte vs PLA Silk vs PLA HF) | new id each |
|
||||
| Color | never a new id |
|
||||
| Second diameter of the same product (1.75 + 2.85) | sibling filament, new id |
|
||||
| "High-speed" tuned for a *different printer model* | same id (it is a printer variant) |
|
||||
| "High-speed" selectable *alongside* the normal preset on one printer | new name, so a new id (it is a product line) |
|
||||
|
||||
## Structure rules
|
||||
|
||||
1. **Every preset carries the id of its own product, wherever it gets it from.** The id is a
|
||||
function of the preset's own triple (rule 5), and `inherits` carries settings, never
|
||||
identity. So a preset may declare the key itself or inherit it from any ancestor — a
|
||||
`<Filament> @base` root, a real (instantiated) preset of the same filament, an
|
||||
OrcaFilamentLibrary preset — and CI checks one thing: the id it ends up with equals the
|
||||
mint of *its* triple. The usual shape is one `@base` root (`"instantiation": "false"`)
|
||||
declaring the key and the per-printer variants inheriting it; a filament may have several
|
||||
roots — Qidi's PolyLite PLA has four per-series roots (`PolyLite PLA@Q2-Series`,
|
||||
`@Q2C-Series`, `@X-Max 4-Series`, `@X-Plus 5-Series`) — which then all declare the identical
|
||||
id. A branded filament that borrows a generic's settings (`Flashforge ABS Basic @FF C5`
|
||||
inherits `Generic ABS @System`) declares its own id, because its triple is its own.
|
||||
2. **The filament name is the base name**: the preset name with everything from the first
|
||||
(optionally space-preceded) `@` stripped. `MyBrand PLA @Orca 3D Fuse1` and `MyBrand PLA@HS`
|
||||
are both the filament `MyBrand PLA`.
|
||||
3. **Within one filament, variants' `compatible_printers` are pairwise disjoint** — per printer,
|
||||
at most one compatible instantiated preset per id, or AMS matching turns ambiguous. The
|
||||
C++ validator's `-f` check enforces this, tree-wide in CI. Since one product carries one id
|
||||
and cannot be split onto two, this rule is the *only* remedy for such an ambiguity: narrow
|
||||
the `compatible_printers`, or retire the preset that duplicates another.
|
||||
4. **Generics belong to OrcaFilamentLibrary.** A vendor tuning a generic material inherits
|
||||
`Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library
|
||||
preset on your printers, and it is what makes its triple — and so its id — the library's)
|
||||
and sets a non-empty `compatible_printers` — e.g. `Generic PLA @Sovol SV08 MAX` inherits
|
||||
`Generic PLA @System` and lists three Sovol nozzles. Renaming such a preset makes it a
|
||||
different product by rule 5, so it then needs its own id.
|
||||
5. **Ids follow the product identity.** The id is a pure function of the product triple
|
||||
`(filament_vendor, filament_type, filament name)`, so correcting any of them re-mints the id
|
||||
**by design**, applied by `generate-id` (preview with `--dry-run`, confine with `--vendor`);
|
||||
the exact sequence is in the FAQ. Nothing forwards
|
||||
the old value, so anything outside the tree that stored it — a device tray, a calibration
|
||||
record, a saved project — falls back to matching by filament type until the user re-selects
|
||||
the filament. Re-mint deliberately, and only to fix a genuinely wrong identity.
|
||||
(`renamed_from` still gates preset-*name* compatibility, as before.)
|
||||
|
||||
## Minting — nobody invents ids
|
||||
|
||||
New ids are deterministic, computed exactly like the `setting_id` precedent
|
||||
(the `setting_id` half of `scripts/orca_profile_tool.py generate-id`):
|
||||
|
||||
```text
|
||||
FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id")
|
||||
= c4d3ff49-4c32-5534-a3e3-00894157ab97
|
||||
filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
|
||||
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
|
||||
```
|
||||
|
||||
`base62_6` is the low 6 base62 digits (alphabet `0-9A-Za-z`) of the UUID taken as a big-endian
|
||||
integer, most-significant digit first; with the `OF` prefix the full id is 8 chars, within the
|
||||
AMS length limit. The triple comes from the root preset's *flattened* config:
|
||||
`<filament_vendor>` is the filament
|
||||
**manufacturer** (`"Polymaker"`, or `"Generic"` for generics — never the printer brand),
|
||||
`<filament_type>` the material type, `<filament_name>` the root's base name; the two config
|
||||
values are inheritable list options and the first element counts.
|
||||
|
||||
Content-addressing on that triple is what makes the whole system converge. The key contains no
|
||||
bundle name, so the same product mints the same id in every bundle — moving a filament into
|
||||
OrcaFilamentLibrary never changes its id, and two vendors independently shipping the same
|
||||
product arrive at the same id without coordinating. `Polymaker/PLA/PolyLite PLA` mints
|
||||
`OF5CgdDq`, and that one id is declared by the OrcaFilamentLibrary, OrcaArena, Qidi, and
|
||||
Snapmaker bundles alike; the OFL generic `Generic/PLA/Generic PLA` mints `OFDSrzZ8`, claimed
|
||||
by 35 bundles — most by independent declarations converging on the same mint, the rest
|
||||
purely through inheritance from the OFL preset.
|
||||
|
||||
Nothing but the triple feeds the mint — not the rest of the tree, not what another preset of
|
||||
the product happens to carry. Determined triple, determined id: one product
|
||||
carries one id and there is no second acceptable value for it, so any other value on a preset
|
||||
is a mismatch `check` reports and `generate-id` pulls back. Two *different* products whose
|
||||
triples mint the same base62 value would be a collision (a roughly 36-bit id space against a
|
||||
few thousand products); nothing salts past it: `check` reports it naming both products,
|
||||
`generate-id` refuses to write it, and the remedy is a rename so their triples differ. Where
|
||||
two presets of one product would be AMS-ambiguous on a printer, the fix is likewise in the
|
||||
profiles — make their `compatible_printers` disjoint (structure rule 3), retire the redundant
|
||||
preset, or, if they really are different products, give them different names so their triples
|
||||
differ. Never a second id for one triple.
|
||||
|
||||
Workflow for a new filament:
|
||||
|
||||
```bash
|
||||
# 1. Author the filament with NO filament_id key anywhere.
|
||||
python scripts/orca_profile_tool.py generate-id --dry-run # 2. preview the ids — writes nothing
|
||||
python scripts/orca_profile_tool.py generate-id # 3. apply them to the profile file(s)
|
||||
python scripts/orca_profile_tool.py check # 4. validate — everything CI checks
|
||||
```
|
||||
|
||||
`generate-id` makes every filament's id equal the mint of its own
|
||||
`(filament_vendor, filament_type, filament name)` triple: it inserts one where an instantiated
|
||||
filament resolves none, and re-derives one that does not match. A preset that *inherits* a
|
||||
mismatching id is the one case left to the author — check 2b names it, and the fix is to inherit
|
||||
a preset of the same filament or to give the preset its own key. A declaration is left alone
|
||||
exactly when it already equals the one id its triple mints, and a collision (check 2d) is
|
||||
reported and left unwritten. The same run assigns
|
||||
`generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process
|
||||
and machine preset of every vendor except BBL, which keeps its authoritative `G*` ids, strips
|
||||
`setting_id` from base profiles, and fixes the misspelled `settings_id` key — dropped, or, for
|
||||
BBL, whose ids have no formula to fall back on, restored under the correct name. It is idempotent and
|
||||
byte-preserving (indentation, BOM, and line endings intact, every edited file re-parsed to fail
|
||||
loudly), and a no-op on a tree that already passes `check`.
|
||||
|
||||
- `--filament-id` limits the run to `filament_id`.
|
||||
- `--setting-id` limits the run to `setting_id`. The two exclude each other; pass neither to
|
||||
write both.
|
||||
- `--vendor VENDOR` confines the run to that bundle; repeatable. The id is a function of the
|
||||
triple alone, so a narrowed run writes exactly what a full one would; `check` reports
|
||||
whatever it left outside.
|
||||
- `--dry-run` reports what the run would do and writes nothing, so
|
||||
`generate-id --dry-run --vendor <Vendor>` previews just that bundle.
|
||||
- `--profiles DIR` points the tooling at a different profile tree (default
|
||||
`resources/profiles`).
|
||||
|
||||
The tool's other commands maintain the tree around the ids: `fix` normalises profile files,
|
||||
`trim` drops files no `<vendor>.json` list references, and `update-index` rebuilds those lists.
|
||||
They do not touch ids; `--help` documents them.
|
||||
|
||||
**Identity fixes need no separate command.** `generate-id` re-derives an id that no longer matches
|
||||
its triple exactly the way it fills in a missing one, so a rename or a `filament_vendor` /
|
||||
`filament_type` correction is just: fix the config and run `generate-id` (confine it with
|
||||
`--vendor`, preview it with `--dry-run`).
|
||||
|
||||
If you skip the tooling, CI fails and prints the remedy: the expected id for your filament and
|
||||
the instruction to run `python scripts/orca_profile_tool.py generate-id`.
|
||||
|
||||
## Ids other systems compose
|
||||
|
||||
Every filament profile carries a minted id, with no exceptions and no spellings held back for
|
||||
anyone. There is therefore no reserved namespace to respect and no bundle that owns one: an id
|
||||
some other system composes for its own purposes is simply not the mint of a triple, so it
|
||||
cannot be a system profile's `filament_id`, and the format check rejects it for that reason
|
||||
alone — same error, same remedy, whoever wrote it.
|
||||
|
||||
Three such spaces exist around us, and are worth recognising so nobody mistakes one for an id
|
||||
to copy into a profile:
|
||||
|
||||
- **Bambu's `GF*` catalog.** Bambu's device / RFID / cloud catalog is external and opaque. Every
|
||||
BBL filament mints an `OF` id from its triple like every other vendor's, and the
|
||||
correspondence to Bambu's catalog ids lives in one generated file the app applies at the
|
||||
printer boundary — the next section. Note that `GF` is a *prefix*, not a namespace the tree
|
||||
avoids: BBL's authoritative `setting_id` values include `GF`-prefixed ones, and
|
||||
`resources/profiles/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_*` protocol ids.** The Qidi box composes `QD_<series>_<vendor>_<typeidx>` at
|
||||
runtime (slot vendor and type indices reported by the device, the series digit inferred
|
||||
client-side from the printer model/name). Qidi presets carry ordinary minted `OF*` ids
|
||||
(generics share the OFL ids), so a composed id matches no preset and the slot falls back to
|
||||
filament type; translating it to the filament's id belongs in `QidiPrinterAgent`. Treating
|
||||
per-series protocol ids as preset ids would put one product under five ids
|
||||
(`QIDI PLA Rapido` would be `QD_0_1_1` through `QD_4_1_1`) — exactly the fragmentation the
|
||||
mint rule removes.
|
||||
- **`P` + 7 hex chars, and `"null"`.** What `CreatePresetsDialog.cpp` gives a filament a *user*
|
||||
creates. Those are user presets, not system profiles, and the two never meet in the tree.
|
||||
|
||||
## The Bambu catalog map
|
||||
|
||||
Bambu's printers, its AMS and its cloud know only Bambu's own catalog ids. Our profiles carry
|
||||
minted `OF` ids like every other vendor's, so one generated file records the correspondence and
|
||||
the app applies it **only where an id crosses to or from a Bambu printer**.
|
||||
|
||||
**The file** is `resources/printers/bambu_filament_ids.json` — a header plus one row per
|
||||
catalogued product, keyed by our id:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "https://github.com/bambulab/BambuStudio",
|
||||
"bambustudio_commit": "66e405477",
|
||||
"generated": "2026-09-04",
|
||||
"filaments": {
|
||||
"OFhuaUQB": { "bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It ships in `resources/printers/`, next to `filaments_blacklist.json` — deliberately not in
|
||||
`resources/profiles/`, where the loader reads every top-level `.json` as a vendor index. It
|
||||
holds 100 rows today, one per product BambuStudio ships, and the correspondence is
|
||||
one-to-one in both directions.
|
||||
|
||||
**It is generated, never hand-edited.** `python scripts/update_bambu_filament_ids.py` rebuilds
|
||||
it from **BambuStudio's own shipped BBL bundle** — a sparse shallow clone of upstream `master`,
|
||||
or `--bambustudio-dir <a BambuStudio resources/profiles checkout>`. Our BBL bundle is a fork of
|
||||
Bambu's, tuned and extended independently, so it is not the source of truth for Bambu's ids.
|
||||
A row's key is the id the product's `(filament_vendor, filament_type, filament name)` triple
|
||||
mints — the same id any bundle of ours carries for it, since the id is a function of the triple
|
||||
alone; the row of a product we do not ship sits inert until some bundle claims that triple —
|
||||
`OFdyfQvU` / `GFG03`, "Bambu PETG Matte", is such a row today.
|
||||
|
||||
**Regenerate it in the same commit as every BBL profile sync**, and read the drift report it
|
||||
prints. Two lines, both informational, neither blocking the write:
|
||||
|
||||
```text
|
||||
upstream ships 'Bambu PETG Matte' (Bambu Lab/PETG), we ship nothing with that identity
|
||||
Orca BBL filaments with no row: 135 Orca-only product(s)
|
||||
```
|
||||
|
||||
The first names each upstream product our BBL bundle has no same-identity filament for —
|
||||
sometimes a genuinely missing product, sometimes a name drift a follow-up rename would
|
||||
converge. The second counts our own BBL filaments that matched no row: 135 of 234 today, of
|
||||
which 109 send an `OF` id on the wire and 26 already rode `OF` ids inherited from the
|
||||
OrcaFilamentLibrary. **135 is the number to expect at every regeneration** — 109 was the
|
||||
one-off size of the transition and stopped being computable from the tree once the BBL bundle
|
||||
was re-minted, so do not "fix" the report to print it.
|
||||
|
||||
**Check 4** lives in `check_filament_ids`, so profile CI runs it alongside the other three. It
|
||||
holds the file to its contract: it parses, carries `source` / `bambustudio_commit` /
|
||||
`generated`, keys only `OF`-format ids, maps each Bambu id at most once, and — for every row
|
||||
whose key the tree actually claims — agrees with the tree on that id's `(vendor, type, name)`
|
||||
triple. A row for a product we do not ship is skipped, not an error. The remedy it prints is
|
||||
always the same: regenerate the map and commit the diff for review.
|
||||
|
||||
### The runtime rule: swap on hit
|
||||
|
||||
Outbound, our id with a row becomes Bambu's; inbound, Bambu's id with a row becomes ours.
|
||||
Everything else is forwarded untouched — an `OF` id with no row, a Bambu id for a product we do
|
||||
not ship, a `P`-hex user id, `"null"`, an empty string. Translation is confined to the
|
||||
boundary: nothing between the boundaries ever holds a Bambu id.
|
||||
|
||||
Translating one value is a capability of the printer agent: `IPrinterAgent` declares
|
||||
`to_orca_filament_id` and `from_orca_filament_id` returning their argument, and `BBLPrinterAgent`
|
||||
overrides them with Bambu's map, so an agent whose printers already speak our ids inherits the
|
||||
identity default and translates nothing. `NetworkAgent` forwards both to the live agent, so the
|
||||
comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated
|
||||
while no agent is live. Whole documents are Bambu's business alone:
|
||||
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under
|
||||
`tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries
|
||||
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
|
||||
file degrades to identity with a log line rather than failing.
|
||||
|
||||
| Boundary | Where it translates |
|
||||
| --- | --- |
|
||||
| Everything the agent sends | `BBLPrinterAgent::send_message` and `send_message_to_printer`, plus `PrintParams::ams_mapping_info` in `dispatch_start` — the funnel all five `start_*` calls share |
|
||||
| Everything the agent receives | `set_on_message_fn` and `set_on_local_message_fn` wrap their callback, so `MachineObject::parse_json` and everything downstream see our ids only |
|
||||
| 3mf export | `Plater::export_3mf` writes Bambu's ids into `slice_info.config`, gated on `preset_bundle.is_bbl_vendor()` — the printer reads that file and knows only its own catalog, and no other vendor's export is affected. The CLI has its own writer in `OrcaSlicer.cpp`; it does the same, gated on the `printer_model` prefix that already decides `Print::is_BBL_printer()` for that run |
|
||||
| Project ingest | `Plater::priv::load_files` reverse-maps the project's `filament_ids` before the bundle ingests them, so a project saved by an older Orca or by BambuStudio still resolves the same presets |
|
||||
| Prints from the printer's SD card | `SelectMachineDialog::update_print_required_data` reverse-maps each plate's slice-info ids as it adopts the plates, so the AMS mapping dialog pairs them with trays |
|
||||
| Bambu-specific comparisons | `CalibUtils.cpp`, `DeviceManager.cpp`, `DeviceCore/DevFilaSystem.cpp`, `DeviceCore/DevFilaBlackList.cpp`, `SelectMachine.cpp`, `AMSDryControl.cpp`, `AMSMaterialsSetting.cpp`, `PresetComboBoxes.cpp`, `ColorDecomposeSupport.cpp` |
|
||||
|
||||
That last row is the rule to follow when a new Bambu-specific behaviour is added: **translate
|
||||
the value you are about to compare, never the table you compare it against.** The shipped data
|
||||
those sites read is Bambu's and stays verbatim — `white_fila_ids` in
|
||||
`resources/printers/filaments_blacklist.json`, the calibration id lists in
|
||||
`resources/printers/<model>.json`, `fila_id` in
|
||||
`resources/profiles/BBL/filament/filaments_color_codes.json`.
|
||||
|
||||
`tests/slic3rutils/test_bambu_filament_ids.cpp` covers the lookups, the payload rewrite and the
|
||||
Bambu-specific rules. `orcaslicer_discover_tests` registers a Catch2 tag as a CTest **label**,
|
||||
not as part of the test name, so `-R` matches nothing here and the filter is `-L`:
|
||||
|
||||
```bash
|
||||
ctest --test-dir <build dir>/tests/slic3rutils -L BambuFilamentIds
|
||||
```
|
||||
|
||||
### Three places the map deliberately does not reach
|
||||
|
||||
The map and its lookups live in the GUI library, which libslic3r cannot link against and which a
|
||||
GUI-less build does not link at all. Three consequences are known and documented; none is worth
|
||||
pulling the map down into libslic3r for.
|
||||
|
||||
- **The support display type in `PrintConfig.cpp`.** `DynamicPrintConfig::get_filament_type`
|
||||
picks `PLA-S` / `Sup.PLA` and `PA-S` / `Sup.PA` for a support filament by testing
|
||||
`filament_id` against `GFS00` and `GFS01`, and otherwise falls back on `filament_type` — a
|
||||
fallback that returns those same two pairs for `"PLA"` and `"PA"`. Bambu Support W inherits
|
||||
`fdm_filament_pla` and Bambu Support G inherits `fdm_filament_pa`, so with their `OF` ids the
|
||||
fallback produces exactly what the id branches produced. (The only config that ever carries a
|
||||
singular `filament_id` key is the AMS tray config built in `Plater.cpp`, and that one never
|
||||
reaches this function.) These two lines are the only mention of a Bambu id anywhere in
|
||||
libslic3r, and they need no change.
|
||||
- **Config imports.** `PresetBundle::import_presets` (File ▸ Import ▸ Import Configs, for
|
||||
`.json` / `.zip` / `.orca_filament` / `.orca_printer` / `.orca_bundle`) and
|
||||
`PresetBundle::load_config_file` (the CLI's `--load-settings` of a G-code file with an
|
||||
embedded config) both parse inside libslic3r, out of the GUI's reach, so a Bambu id carried
|
||||
in such a file lands on the imported preset untranslated. The effect is bounded: that preset
|
||||
does not auto-match an AMS tray while the stale id is live, and the id does not survive
|
||||
being saved — `Preset::save` writes a `filament_id` key only for a preset whose `inherits` is
|
||||
empty, and on the next load an inheriting preset takes its parent's id. A known gap, and not
|
||||
a regression: nothing forwarded a stale id before either.
|
||||
- **A build configured without the GUI.** `target_link_libraries(OrcaSlicer libslic3r_gui)` sits
|
||||
inside `if (SLIC3R_GUI)` in `src/CMakeLists.txt`, so the lookups are not linkable when the GUI
|
||||
is off. The CLI's 3mf writer in `src/OrcaSlicer.cpp` therefore guards its translation with
|
||||
`#ifdef SLIC3R_GUI`, and a 3mf that such a build slices for a Bambu printer carries our `OF`
|
||||
ids in `slice_info.config` rather than Bambu's. Every shipped build enables the GUI, so this
|
||||
reaches only a purpose-built GUI-less binary.
|
||||
|
||||
One more thing worth recording before it is rediscovered:
|
||||
`SyncAmsInfoDialog::update_print_required_data` is a structural twin of the SD-card function
|
||||
above and carries no translation. It has no callers today and its plate list is only ever read
|
||||
for `printer_model_id`, so it is not a live gap — but wiring it up without adding the reverse
|
||||
map would silently reproduce the bug.
|
||||
|
||||
## How CI enforces this
|
||||
|
||||
Profile CI (`check_profiles.yml`) runs `check_filament_ids()` tree-wide via
|
||||
`scripts/orca_profile_tool.py check`. Every check judges the tree against the rules on this
|
||||
page and nothing else — there is no recorded id state to match and no grandfather list of any
|
||||
kind.
|
||||
|
||||
The checks, in brief:
|
||||
|
||||
- **Format** — every id occurring in the tree is `OF` + 6 base62 chars. No exceptions, not
|
||||
even BBL.
|
||||
- **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the
|
||||
one id its declarer's own triple mints, with no second acceptable value; the id an
|
||||
instantiated preset *inherits* must equal the mint of *its* own triple, however it inherits
|
||||
it (a root, a real filament, a library preset — structure rule 1); and every instantiated
|
||||
system filament must resolve an effective id at all (recall: an id-less one is a hard load
|
||||
error in C++ that discards the whole vendor bundle); and no two products mint one id (a
|
||||
base62 collision, resolved by renaming one of them). The errors print the expected id.
|
||||
- **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and
|
||||
`filament_type` (generics use `"Generic"`), and all declarers of one filament within a
|
||||
bundle must agree on the triple.
|
||||
- **Bambu catalog map** — `resources/printers/bambu_filament_ids.json` parses, carries its
|
||||
`source` / `bambustudio_commit` / `generated` header, keys only `OF`-format ids, maps each
|
||||
Bambu id at most once, and agrees with the tree on the triple of every row whose key the
|
||||
tree claims. See [The Bambu catalog map](#the-bambu-catalog-map); the remedy is always to
|
||||
regenerate, never to hand-edit.
|
||||
|
||||
A profile that declares an id no triple mints — a Bambu catalog id, a composed Qidi one, a
|
||||
hand-typed value, whatever its vendor — fails the format check. For a Bambu-cataloged product
|
||||
the catalog map is where the correspondence belongs. Two products sharing one id are caught by
|
||||
the identity check whether the id is declared or inherited.
|
||||
|
||||
The same `check` run holds every declared id to the AMS 8-character limit, tree-wide and for
|
||||
every vendor alike, scoped to the presets a vendor's index actually references (a file the index
|
||||
never loads cannot break AMS matching).
|
||||
|
||||
Complementing the Python checks, CI also runs the C++ profile validator with `-f`
|
||||
(`check_filament_subtypes`): it loads the bundle exactly as the app does and flags any printer
|
||||
for which two or more compatible filament presets share one `filament_id` — the runtime-shaped
|
||||
ambiguity check behind structure rule 3.
|
||||
|
||||
## FAQ
|
||||
|
||||
- **A new color of an existing product?** Never a new id — colors are not filaments.
|
||||
- **A second diameter (1.75 mm and 2.85 mm) of the same product?** A sibling filament with its
|
||||
own id: two diameters are separately selectable spool products.
|
||||
- **A high-speed tune of an existing material for another printer model?** Same filament:
|
||||
keep the base name and inherit its root; no id key needed.
|
||||
- **A tuned generic ("our profile for Generic PLA")?** Inherit `Generic PLA @System`, keep the
|
||||
`Generic PLA` base name, set `compatible_printers`; no id key needed.
|
||||
- **A branded filament that borrows a generic's settings?** Fine — inherit `Generic X @System`
|
||||
(or any real filament) for the settings and declare the id of your own filament; run
|
||||
`python scripts/orca_profile_tool.py generate-id` to mint it. Inheritance never changes the id.
|
||||
- **I need to fix a filament's `filament_vendor` or `filament_type`.** Fix the config, run
|
||||
`generate-id --vendor <Vendor>` (preview with `--dry-run`), and commit the result. The id
|
||||
re-derives from the corrected identity, and
|
||||
nothing forwards the old value, so a tray or record still holding it falls back to matching by
|
||||
filament type.
|
||||
- **I need to rename a filament.** Rename the presets (adding `renamed_from`, which keeps the
|
||||
preset *name* resolving), then `generate-id --vendor <Vendor>` (preview with `--dry-run`). The
|
||||
id follows the new filament name; as with any identity fix, the old id
|
||||
is not forwarded.
|
||||
- **Can I reuse a `QD_*` id for a Qidi profile?** No — it is not a mint, so it is not a
|
||||
`filament_id`. Those values are composed by the box at runtime, and no preset carries one.
|
||||
Author Qidi filaments like any other vendor's.
|
||||
- **CI says my filament needs an id.** Run `python scripts/orca_profile_tool.py generate-id` and
|
||||
commit the result. Do not type an id by hand.
|
||||
|
||||
For general profile authoring, see the profile development guide on the
|
||||
[OrcaSlicer wiki](https://www.orcaslicer.com/wiki).
|
||||
@@ -1,132 +0,0 @@
|
||||
# Keyboard Shortcuts
|
||||
|
||||
## Why it exists
|
||||
|
||||
Key events arrive in several windows (the main frame's char hook, the 3D canvases, the
|
||||
gizmo manager and the object list), and the same keys are shown again in menu labels,
|
||||
toolbar tooltips, gizmo names and the shortcuts dialog. The registry is the one table all
|
||||
of them read. Each binding is defined once; dispatchers look key events up there, labels
|
||||
are derived from it, and a change the user makes updates all of them.
|
||||
|
||||
## Data model
|
||||
|
||||
`KeyChord` (`src/slic3r/GUI/KeyChord.hpp`) is one key press: the key code as
|
||||
`wxEVT_KEY_DOWN` reports it, plus the `wxMOD_*` modifiers held with it. It has two
|
||||
text forms. The canonical one (`Ctrl+Shift+S`) is platform-neutral and doubles as the wx
|
||||
accelerator string and the config format. The display one uses translated modifier
|
||||
names and the command and option glyphs on macOS. `KeyChord::from_event()` turns any wx
|
||||
key event into the same key code and modifiers, so a chord recorded in the dialog is
|
||||
equal to the chord a dispatcher builds from the key press.
|
||||
|
||||
`Shortcut` is the enum of every user-facing binding. `shortcut_table` in
|
||||
`src/slic3r/GUI/Shortcuts.cpp` gives each one a config key, a description, a context
|
||||
mask, a default chord, a `repeatable` flag and a `modifier_variants` flag, in the order
|
||||
the dialog lists them; a `static_assert` keeps the table and the enum in step.
|
||||
|
||||
`ShortcutRegistry` overlays the user's overrides on the defaults and keeps a
|
||||
chord-to-shortcut index for lookups. It reads and writes the `shortcuts` section of
|
||||
`AppConfig`. Only overrides are stored, so a default can change between releases
|
||||
without touching anyone's config; `none` records a shortcut the user unbound.
|
||||
|
||||
## Contexts
|
||||
|
||||
A key press is looked up in the context of the window that received it.
|
||||
|
||||
| Context | Dispatcher | Examples |
|
||||
|--------------|-----------------------------------------------------------|-------------------------------|
|
||||
| `Global` | `MainFrame`'s `wxEVT_CHAR_HOOK`, before any child sees it | New project, camera views |
|
||||
| `Plater` | `GLCanvas3D` of the 3D and assembly views | Arrange, gizmo activation |
|
||||
| `Preview` | `GLCanvas3D` of the G-code preview | One-layer mode, jump to layer |
|
||||
| `ObjectList` | the object list | Copy, delete, auto drop |
|
||||
| `Painting` | `GLGizmosManager` while a painting gizmo is open | Circle, sphere, fill tools |
|
||||
|
||||
A shortcut can belong to several contexts, which is how copy and paste are a single
|
||||
binding for the canvas and the object list. Two shortcuts can share a chord when their
|
||||
contexts do not overlap; `C` is the cut gizmo in the 3D view, the G-code window in the
|
||||
preview and the circle tool while painting. A Global chord is dispatched before every
|
||||
other context, so the dialog treats it as conflicting with all of them.
|
||||
|
||||
A Global shortcut has to include Ctrl or Alt or use a key that types nothing, since a
|
||||
bare printable key in the frame hook would swallow that character in every text field.
|
||||
The dialog refuses such chords and `ShortcutRegistry::load()` drops them from the config.
|
||||
Space counts as typing. The speed dial's default is the one bare Space, and
|
||||
`MainFrame` leaves it to a focused control that uses Space itself (text fields, buttons,
|
||||
combo boxes), so it opens the dial from the canvases and the tab strip only.
|
||||
|
||||
## Which event a chord matches
|
||||
|
||||
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
|
||||
on the keyboard layout: the key labelled `Q` on an AZERTY keyboard and the key in the
|
||||
same position under a Cyrillic layout both report `Q`. Numpad keys fold onto their main
|
||||
keyboard equivalents, so `Ctrl+1` and `Ctrl+Numpad 1` are one binding.
|
||||
|
||||
Punctuation matches on `wxEVT_CHAR`, because only the char event knows which character
|
||||
a key produced under the active layout. `+` is Shift and `=` on a US keyboard and a key
|
||||
of its own on a German one, and the binding means the character in both cases. The
|
||||
canvas looks a key up on key-down first and, when nothing matched, once more on the char
|
||||
event, for punctuation chords only. The dialog records chords the same way: a printable
|
||||
non-alphanumeric key pressed with nothing but Shift is taken from the char event that
|
||||
follows.
|
||||
|
||||
wxGTK does not report key auto-repeat, so the canvases share one record of the keys
|
||||
seen going down and swallow the repeats of every shortcut not marked `repeatable`. Zoom
|
||||
and undo repeat, for example; a toggle such as Tab does not. The record is shared because
|
||||
a shortcut can move the focus to another canvas while its key is still held; a key
|
||||
released while no canvas had the focus is dropped on the next press.
|
||||
|
||||
A few shortcuts have `modifier_variants`: Shift or Ctrl added to their binding selects a
|
||||
step of the same action (1 mm and camera-space moves of the selection, five-step slider
|
||||
moves). Only a binding without Shift or Ctrl of its own has steps, so no two bindings
|
||||
share one. `ShortcutRegistry::match()` looks the exact chord up first and only then, when
|
||||
nothing is bound to it, looks for such a shortcut whose binding is the chord minus those
|
||||
modifiers, reporting which were added; a binding on Ctrl+Shift+key therefore wins over
|
||||
the combined step. The Shift and Ctrl steps themselves are reserved. `step_owner()` names
|
||||
the shortcut they belong to, the capture dialog refuses to assign them, and
|
||||
`conflicts()` reports exact chords only. A binding made before its key became a stepping
|
||||
key keeps its chord and shadows that one step. A move or rotation of the selection
|
||||
started from the keyboard runs until the key that started it is released, or the
|
||||
canvas loses focus, so a held key is one undo step.
|
||||
|
||||
## Labels
|
||||
|
||||
Menu labels, toolbar tooltips, gizmo names, the context menu and the shortcuts dialog
|
||||
read the registry, so a rebinding shows up in all of them. Each tracked menu item keeps
|
||||
its base label; `MainFrame::update_shortcut_labels()` appends the current binding
|
||||
again after an edit, which also installs the new wx accelerator.
|
||||
|
||||
A chord that is unsafe as a menu accelerator, meaning a bare printable key, is appended
|
||||
to the label as plain text so the menu cannot take it away from text fields. The macOS
|
||||
edit menu shows its clipboard and undo entries that way, because a system-menu key
|
||||
equivalent for Cmd+C would run instead of the text field's own copy.
|
||||
|
||||
On macOS the object list receives no key events at all, so its bindings are installed as
|
||||
a `wxAcceleratorTable`, regenerated from the registry after each edit.
|
||||
|
||||
## Editing
|
||||
|
||||
The shortcuts dialog has a page per context, each opening with a line that says when its
|
||||
keys apply. A page lists the shortcuts under the headings of `section_table`, with the
|
||||
fixed keys that cannot change (mouse buttons, the step modifiers, Esc, the digit keys
|
||||
that pick a filament) sorted into the same sections. The mouse drag rows describe the
|
||||
camera actions chosen in Preferences; their button opens Preferences > Control with that
|
||||
option scrolled into view and focused, instead of editing a key.
|
||||
Editing a row opens a capture dialog that records the next chord, names the shortcuts it
|
||||
would take the chord from, and on confirmation unbinds those and binds this one.
|
||||
Resetting a row asks the same question when its default is now held by another
|
||||
shortcut, so a reset cannot leave two shortcuts on one chord. Each change is written to
|
||||
the config at once and pushed to the menus, tooltips and accelerator tables through
|
||||
`GUI_App::on_shortcuts_changed()`. The dialog opens from the Help menu and Preferences >
|
||||
Control on the Global page, and from the `?` key on the page of the view that received it.
|
||||
|
||||
## Adding a shortcut
|
||||
|
||||
1. Add the enum value to `Shortcut` and its row to `shortcut_table`, in the position
|
||||
the dialog should list it; the row's section heading is the `section_table` entry
|
||||
above it, so a new section needs an entry there too. Pick a default that does not
|
||||
collide inside its contexts; the `[Shortcuts]` tests check every default against the
|
||||
others.
|
||||
2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`,
|
||||
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's
|
||||
`on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor.
|
||||
3. Where the UI shows the key, ask the registry (`display()` for tooltips,
|
||||
`accelerator()` for menu labels); no label holds a literal key name.
|
||||
@@ -1,129 +0,0 @@
|
||||
# Multiline infill — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
`fill_multiline` prints every sparse infill wall as N adjacent lines instead of
|
||||
one, so a wall is `d1 = N * spacing` thick. Only internal sparse infill uses it.
|
||||
Each pattern first builds its single-line centerlines at N times the usual line
|
||||
spacing (so the density holds), and `multiline_fill()` then replaces each
|
||||
centerline by the lines of that wall: the centerline itself when N is odd, and
|
||||
closed outlines around it at every `spacing` out to `d1 / 2`. The outlines are
|
||||
clipped to the fill region contracted by half a line width, then connected like
|
||||
any other infill.
|
||||
|
||||
Outlines of centerlines that cross each other overlap at every crossing, which
|
||||
over-extrudes the wall intersections. The line-crossing patterns Grid,
|
||||
Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross
|
||||
(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and
|
||||
Support Cubic (`FillAdaptive`); the other patterns outline their usual
|
||||
centerlines.
|
||||
|
||||
## Non-crossing centerlines
|
||||
|
||||
The crossing lines are resolved into x-monotone paths, the levels of the line
|
||||
arrangement: walking along x, the k-th path is always the k-th line from the
|
||||
bottom. At every crossing, the two paths bounce off each other instead of
|
||||
passing through. Adjacent paths meet only at crossings, so their outlines touch
|
||||
there and nowhere overlap.
|
||||
|
||||
Where two paths meet, each is cut short by a line perpendicular to the bisector
|
||||
of its bend, `d1 / 2` from the crossing. The two cut segments are parallel and
|
||||
`d1` apart, so the outermost lines of the two walls sit exactly `spacing` apart,
|
||||
like the lines inside a wall. Where three lines meet at one point, the middle
|
||||
path runs straight through and the outer two are cut `d1` from it.
|
||||
|
||||
Each pattern builds its rows along x in a rotated frame. Grid lines run at ±45°
|
||||
there, and its rows are trapezoid waves that transpose on alternate layers. The three families of Triangles, Tri-hexagon and
|
||||
Cubic run at 0°, 60° and 120°. Those rows rotate by 120° every layer about a
|
||||
3-fold center of the arrangement, so each family takes every role in turn.
|
||||
The pattern is phased on fixed positions, so it lines up across layers and
|
||||
across the regions of one layer. Rounding the corners with
|
||||
`sparse_infill_smooth_factor` happens before `multiline_fill()`.
|
||||
|
||||
Each region builds only the rows over its bounding box in that frame, and
|
||||
outlines only the centerlines within `d1 / 2` of it, the ones whose outlines
|
||||
reach it. Every row is monotone along its direction, so each outline is started
|
||||
on the cap at the first end of its centerline, outside the region, and clipping
|
||||
to the region cuts it only where it crosses the boundary.
|
||||
|
||||
## Cubic
|
||||
|
||||
Single-line Cubic draws the three families at the same spacing `h` and shifts
|
||||
them with z: by `+dx`, `-dx` and `+dx`, `dx = z / sqrt(2)`. The multiline paths
|
||||
follow the same lines. In the frame where one family is horizontal, the other two
|
||||
cross in rows `h` apart, alternating by half a period, at height
|
||||
`tau = -3 * dx (mod h)` above the horizontal line below them. The crossings split
|
||||
every band between horizontal lines into up-pointing triangles of height `tau`,
|
||||
down-pointing triangles of height `h - tau`, and hexagons. At `tau = 0` (and `h`)
|
||||
all three families meet at common points, as in Triangles. At `tau = h / 2` the
|
||||
triangles are equal, as in Tri-hexagon. The origin of that frame is always a
|
||||
3-fold center, whatever z is, so the per-layer rotation keeps the lines in place.
|
||||
|
||||
Each band holds two paths that touch at its crossings: the upper one takes the
|
||||
V below the crossing and runs along the top horizontal line, and the lower one
|
||||
takes the inverted V above it and runs along the bottom line. Both are the same function
|
||||
of `tau`, the lower one mirrored with `h - tau`. `cubic_upper_level()` builds one
|
||||
period of the upper path as the lower envelope of five lines, clipped from below:
|
||||
|
||||
- the two slanted lines through the crossings,
|
||||
- the horizontal line, lowered when the triangle above it is less than `1.5 * d1` high,
|
||||
- the two chamfers where the path turns onto and off the horizontal line, `d1 / 2`
|
||||
from those crossings,
|
||||
- the flat cut into the V at the crossing.
|
||||
|
||||
The cut height `clamp(tau - d1 / 2, 0, h - d1) + d1` is what makes the pattern
|
||||
continuous in z. While both triangles are at least `1.5 * d1` high, every
|
||||
crossing is a pair of bends `d1 / 2` from it, as in Tri-hexagon. When a triangle
|
||||
is thinner, its three paths stack like a triple crossing. The path through it
|
||||
flattens toward its base line and lies on it once the triangle is under `d1 / 2`
|
||||
high, and the paths beside it are pushed `d1` away. The layout thus reaches the
|
||||
Triangles one where the families meet. Adjacent paths stay at least `d1` apart
|
||||
at every `tau` and at every density up to 100%.
|
||||
|
||||
## Adaptive Cubic
|
||||
|
||||
Adaptive Cubic and Support Cubic take their lines from an octree of cubes
|
||||
standing on a corner. On each layer every cube cuts its three mid-planes into
|
||||
segments of the same three 60° families as Cubic, but the pattern is not
|
||||
periodic. Smaller cubes near the surface add finer lines, and a finer line ends
|
||||
where it meets the wall of its coarser cube, so the lines form crossings and
|
||||
T-junctions. `FillAdaptive::multiline_paths()` builds the paths from these
|
||||
segments directly, for each fill region and within `4 * d1` of it.
|
||||
|
||||
At a crossing the two paths bounce as in Cubic. At a T-junction the through line
|
||||
runs straight on and the path of the ending line stops there. Every path still
|
||||
runs left to right in the frame where one family is horizontal, and that family
|
||||
rotates with the layer.
|
||||
|
||||
Every line of every cube size lies on one fine lattice, so crossings closer than
|
||||
a few `d1` are the corners of one small triangle of that lattice, as in Cubic.
|
||||
The cuts follow the Cubic rules without a closed formula:
|
||||
|
||||
- The two bends of a crossing are cut `d1` apart, `d1 / 2` each, perpendicular
|
||||
to their bisector, so their walls touch. A cut goes no further than the path
|
||||
end, and the other bend takes the rest of `d1`.
|
||||
- At the tip of a small triangle, between the two slanted families, a cut also
|
||||
goes no further than the neighbouring bend turning the other way, and the
|
||||
path beyond that bend is kept a wall away from it. The bends onto the
|
||||
horizontal family are not limited this way: pushing their paths apart would
|
||||
open gaps between walls that should touch.
|
||||
- A cut moves the path only where the cut line lies beyond it, near its bend.
|
||||
The sharp bends between the two slanted families are cut after the bends onto
|
||||
the horizontal family, so the tip of a small triangle wins, as in Cubic.
|
||||
- A path stopping at a T-junction is trimmed until it is `d1` less half a line
|
||||
spacing from every other path, so that its end overlaps the wall it stops on
|
||||
by half a line and bonds to it. The paths are trimmed one at a time against
|
||||
the others as already trimmed, so two ends facing each other meet instead of
|
||||
both backing off. A path stopping on the line of another is trimmed before
|
||||
that one, so it gives way and the other still reaches the line it stops on. A
|
||||
second round trims every path again from its full length, so an end grows
|
||||
back where the ends it gave way to were trimmed later, and a last round only
|
||||
shortens them, keeping them that far apart. Paths shorter than `d1` are left
|
||||
out.
|
||||
- A line that ends on another less than `2 * d1` past a crossing stops at that
|
||||
crossing instead, the shorter one where both do. The path along such a stub
|
||||
would be trimmed away, leaving a hole between the walls that were cut to
|
||||
touch it.
|
||||
|
||||
Short paths enclosed by coarser lines still print as closed outlines, but most
|
||||
paths run on across several cells.
|
||||
@@ -1,239 +0,0 @@
|
||||
# Precise Seam — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
Precise Seam places the seam where a helper volume intersects the external
|
||||
wall. The user attaches a mesh to an object as a Precise Seam modifier, and on
|
||||
every layer the seam placer reads the modifier's slice to decide where the seam
|
||||
of each external perimeter may, must or must not go. The same mesh keeps
|
||||
working after the model changes, so the seam does not have to be repainted
|
||||
after every design revision, and a swept helper body can guide the seam along
|
||||
any path.
|
||||
|
||||
The modifier is non-printing geometry. It does not take part in slicing, region
|
||||
assignment, filament selection or brim adhesion. It affects only seam
|
||||
placement, which runs during G-code export.
|
||||
|
||||
## Volume types and priority
|
||||
|
||||
Precise Seam adds six `ModelVolumeType` values after `SUPPORT_ENFORCER`. The
|
||||
strong types come first and the weak types follow. `is_precise_seam()`,
|
||||
`is_precise_seam_strong()` and `is_precise_seam_weak()` are range checks that
|
||||
depend on this order.
|
||||
|
||||
| Type | Group | Effect on the perimeter |
|
||||
| --- | --- | --- |
|
||||
| `PRECISE_SEAM_CENTER` | strong | seam at the arc-length midpoint of the intersection |
|
||||
| `PRECISE_SEAM_LEFT` | strong | seam at the first point of the intersection |
|
||||
| `PRECISE_SEAM_RIGHT` | strong | seam at the last point of the intersection |
|
||||
| `PRECISE_SEAM_ENFORCED` | weak | intersection marked as enforced |
|
||||
| `PRECISE_SEAM_BLOCKED` | weak | intersection marked as blocked |
|
||||
| `PRECISE_SEAM_NEUTRAL` | weak | intersection reset to neutral |
|
||||
|
||||
A strong modifier fixes one point. A weak modifier only changes the
|
||||
enforced/blocked type of seam candidates, and the configured seam position then
|
||||
chooses among them. First and last are taken along the perimeter made
|
||||
counter-clockwise seen from above. On an outer wall seen from outside, Left is
|
||||
the left end of the intersection. On the wall of a hole seen from inside the
|
||||
hole, the two ends are swapped.
|
||||
|
||||
The order of volumes in the object is the priority order, highest first.
|
||||
`ModelObject::sort_volumes()` keeps every strong modifier before every weak one
|
||||
and preserves the user's order within each group. The object list lets the user
|
||||
drag a modifier only within its own group. A type change that crosses a group
|
||||
boundary moves the volume to the end of its new group, where it has the lowest
|
||||
priority. Strong modifiers are tried in this order, and the first one that
|
||||
yields a seam on a perimeter wins. Weak modifiers are applied from the lowest
|
||||
priority to the highest, so the highest one overwrites any overlapping zone.
|
||||
|
||||
## Model storage and 3MF compatibility
|
||||
|
||||
Projects must stay readable by earlier releases, and the modifier must not
|
||||
change a print there. Both 3MF writers therefore store a Precise Seam volume as
|
||||
an ordinary parameter modifier: `modifier_part` in the Bambu-format part
|
||||
subtype, and `ParameterModifier` together with the legacy `modifier` flag in
|
||||
the Prusa-format volume metadata. The seam mode is written separately under
|
||||
`precise_seam_type`, using the names from `ModelVolume::type_to_string()`
|
||||
(`precise_seam_center` and so on).
|
||||
|
||||
On load, the mode applies after all other volume metadata, regardless of XML
|
||||
key order, and only when the base type is a modifier. Missing or unknown modes
|
||||
leave an ordinary modifier. Seam metadata on any other base type is ignored.
|
||||
Files that stored the seam mode directly as the volume type still load.
|
||||
|
||||
A Precise Seam volume keeps any per-volume settings it had as a part or
|
||||
modifier, but they are inactive and the object list shows no settings item for
|
||||
it. The writers prefix these keys with `precise_seam_config:`, so an earlier
|
||||
reader drops them as unknown options. The volume therefore loads there as a
|
||||
modifier without settings and has no effect on the print. The current reader
|
||||
restores the keys only when the volume ends up as a Precise Seam type, so the
|
||||
settings return when the user changes the type back. Configuration values are
|
||||
XML-escaped in both writers, for every volume type.
|
||||
|
||||
## Print invalidation
|
||||
|
||||
`Print::apply()` compares the Precise Seam volumes of each object by type, ID
|
||||
and transformation. Adding, removing, moving, reordering or retyping one
|
||||
cancels background processing and invalidates only `psGCodeExport`; the sliced
|
||||
layers are kept. `model_volume_list_update_supports_and_seams()` then brings
|
||||
the support and Precise Seam volumes of the print's model copy in line with the
|
||||
new model in one pass. A volume may switch between the two families, since
|
||||
neither affects slicing. A conversion to or from a part or ordinary modifier
|
||||
changes the solid and modifier volume lists and reslices as before.
|
||||
|
||||
## Modifier slices
|
||||
|
||||
`SeamPlacer::init()` collects the Precise Seam volumes of each object once:
|
||||
strong ones in priority order and weak ones reversed. It slices each volume
|
||||
separately with `PrintObject::slice_single_volume()`, which shares
|
||||
`slice_modifier_volumes()` with support blockers and enforcers but does not
|
||||
merge volumes, so each keeps its own priority. The result is cached per volume
|
||||
and indexed by object layer; `Layer::id()` includes raft layers, which are
|
||||
subtracted. Seam candidates are then gathered in parallel over the layers and
|
||||
read the cache without locking.
|
||||
|
||||
Objects without Precise Seam volumes follow the unchanged seam placement path.
|
||||
For objects that have them, perimeter extraction also removes consecutive
|
||||
duplicate points and the repeated closing point of each extrusion loop.
|
||||
Zero-length edges at path junctions would otherwise prevent point insertion
|
||||
there. Distinct visits to one point of a self-touching contour are kept.
|
||||
|
||||
## Finding the wall segment
|
||||
|
||||
The seam placer works on the external perimeter loops of each layer, both
|
||||
outer contours and holes, each made counter-clockwise. For every modifier
|
||||
polygon on the layer that overlaps the perimeter's bounding box, the region
|
||||
enclosed by the perimeter is clipped against the modifier polygon. The boundary
|
||||
of each intersection polygon alternates between runs that follow the perimeter
|
||||
and runs that follow the modifier outline. The wall segment is the longest
|
||||
continuous run of intersection vertices that lie on the perimeter, measured in
|
||||
vertices.
|
||||
|
||||
The fast path first finds an intersection vertex that exactly matches a
|
||||
perimeter vertex. It then walks forward and backward, expecting the adjacent
|
||||
perimeter vertex and falling back to projection when Clipper has merged or
|
||||
split collinear edges. A vertex counts as on the perimeter when its projection
|
||||
is within about 1.6 nm, which covers Clipper's rounding. If no vertex matches
|
||||
exactly, or every vertex lies on the perimeter, the general path projects all
|
||||
vertices. When every vertex is on the perimeter, the edge midpoints are checked
|
||||
instead: a modifier chord can join two perimeter vertices directly, and the
|
||||
chords split the vertex ring into runs. If no edge leaves the perimeter, the
|
||||
perimeter lies entirely inside the modifier.
|
||||
|
||||
`Polygon::point_projection()` optionally reports the edge that holds the
|
||||
projection, and every point of the segment keeps the index of its perimeter
|
||||
edge. New points are inserted on that edge. A point within 1 µm of an existing
|
||||
vertex snaps to that vertex instead.
|
||||
|
||||
## Strong modifiers
|
||||
|
||||
For a strong modifier, the target is the first point, the last point or the
|
||||
arc-length midpoint of the segment. The midpoint is projected back onto the
|
||||
original perimeter, because Clipper may have merged several perimeter edges
|
||||
into one segment edge. The target is inserted into the perimeter, and a helper
|
||||
point is inserted 1 µm before and after it. Strong modifiers are tried in
|
||||
priority order, the first valid intersection decides the seam, and weak
|
||||
modifiers are not processed for that perimeter.
|
||||
|
||||
When candidates are built, the inserted point is the only enforced candidate
|
||||
and becomes the central enforcer; every other candidate is blocked. The seam
|
||||
position modes then pick that point: Aligned and Aligned Back prefer the central
|
||||
enforcer, while Back, Random and Nearest rank enforced candidates above blocked
|
||||
ones. Alignment and random placement can still move the final position along an
|
||||
edge. After alignment, `restore_precise_seam_positions()` writes the exact point
|
||||
and its index back into every perimeter that has a strong seam. Inner walls take
|
||||
their seam from the external seam as usual, including staggering.
|
||||
|
||||
## Weak modifiers
|
||||
|
||||
Weak modifiers produce one segment per intersection polygon, so one modifier can
|
||||
mark several zones on one perimeter. All segment boundaries are inserted into
|
||||
the perimeter in order of decreasing arc length. Each insertion then leaves the
|
||||
indices of the pending, shorter ones unchanged; a point on the closing edge is
|
||||
appended rather than inserted at index zero. A helper point is added 1 µm
|
||||
outside each boundary. Random placement picks a position along the edge that
|
||||
follows a candidate. These helpers keep that edge 1 µm long at each boundary, so
|
||||
a zone cannot extend or intrude further than that. Boundaries that coincide
|
||||
share their helper points.
|
||||
|
||||
The zone types are then resolved in priority order, and the edges of enforced
|
||||
zones are subdivided into steps of at most
|
||||
`SeamPlacer::enforcer_oversampling_distance` (0.2 mm). The middle candidate of
|
||||
the longest enforced patch is therefore close to the geometric middle of the
|
||||
zone. That patch is measured in candidates, across the closing edge, regardless
|
||||
of where the contour starts; the same rule applies to painted seams.
|
||||
|
||||
Candidates first receive their type from seam painting. The weak zones then
|
||||
overwrite it, lowest priority first. Blocked and Enforced zones therefore take
|
||||
precedence over painting, and Neutral clears painting inside its zone.
|
||||
|
||||
## Unsupported geometry and warnings
|
||||
|
||||
Some modifier shapes cannot be resolved to one seam or one zone per crossing.
|
||||
They are detected cheaply and reported rather than guessed:
|
||||
|
||||
- A strong modifier that crosses a perimeter in more than one place uses only
|
||||
its first valid segment. The other crossings are ignored.
|
||||
- A modifier that crosses the whole region enclosed by the perimeter is
|
||||
detected when the modifier outline minus that region leaves more than one
|
||||
piece, none of them a hole. Its intersection holds two wall runs, and only
|
||||
one of them is used.
|
||||
- A modifier whose slice has a hole on a layer, found as a clockwise polygon in
|
||||
the flattened slice, is skipped on that layer. The flattened slice no longer
|
||||
records which hole belongs to which contour.
|
||||
- A perimeter that lies entirely inside a modifier is ignored by that modifier.
|
||||
|
||||
The conditions are atomic flags shared by all layers and objects. After all
|
||||
objects are processed, `SeamPlacer::init()` issues at most one non-critical
|
||||
warning with the ID `SlicingPreciseSeamWarning`. The warning is a single line
|
||||
that lists every cause found, because the export warnings dialog shows only the
|
||||
first line of each warning. Repeated warning events replace this notification
|
||||
instead of appending text to it.
|
||||
|
||||
## User interface
|
||||
|
||||
- *Add Precise Seam* in the object menu creates a Center modifier from a
|
||||
primitive or a loaded mesh. Text and SVG volumes cannot become Precise Seam
|
||||
modifiers: the menu does not offer them, and `ObjectList::set_volume_type()`
|
||||
refuses the change.
|
||||
- *Change Type* has a single *Precise Seam* entry. It converts other volumes to
|
||||
Center and keeps the mode of volumes that are already Precise Seam. The
|
||||
*Precise Seam Type* submenu appears only when every selected item is a
|
||||
Precise Seam volume, including settings rows that resolve to one. It sets the
|
||||
chosen mode on all selected volumes.
|
||||
- Each mode has its own icon in the object list and its own color in the 3D
|
||||
view, at 60% opacity: warm oranges for the strong modes, and green, red and
|
||||
gray for Enforced, Blocked and Neutral.
|
||||
- Object list drops map visible rows to volume indices while skipping hidden
|
||||
cut connectors, and they refresh the row-to-volume map of the object.
|
||||
- Precise Seam volumes have no filament, block pasting into SLA, and are exposed
|
||||
to Python plugins as `ModelVolumeType` values plus the `is_precise_seam*()`
|
||||
methods.
|
||||
|
||||
## Implementation and verification
|
||||
|
||||
- [PreciseSeam.cpp](../../src/libslic3r/GCode/PreciseSeam.cpp) implements segment
|
||||
detection, point insertion, weak-zone resolution and position restoration.
|
||||
[SeamPlacer.cpp](../../src/libslic3r/GCode/SeamPlacer.cpp) integrates it into
|
||||
candidate gathering and issues the warning.
|
||||
- [Model.hpp](../../src/libslic3r/Model.hpp) defines the types and their order,
|
||||
[PrintApply.cpp](../../src/libslic3r/PrintApply.cpp) handles invalidation, and
|
||||
[PrintObjectSlice.cpp](../../src/libslic3r/PrintObjectSlice.cpp) slices the
|
||||
modifiers. [bbs_3mf.cpp](../../src/libslic3r/Format/bbs_3mf.cpp) and
|
||||
[3mf.cpp](../../src/libslic3r/Format/3mf.cpp) store them.
|
||||
- [GUI_Factories.cpp](../../src/slic3r/GUI/GUI_Factories.cpp) and
|
||||
[GUI_ObjectList.cpp](../../src/slic3r/GUI/GUI_ObjectList.cpp) provide the menus,
|
||||
type changes and ordering.
|
||||
- [Precise Seam tests](../../tests/fff_print/test_precise_seam.cpp) cover the
|
||||
strong positions, including a midpoint on an existing vertex or the closing
|
||||
edge. They also cover shared and coincident weak boundaries, every warning,
|
||||
and the priority order.
|
||||
- [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) cover
|
||||
enforced-patch selection independent of the contour start, fully painted
|
||||
contours, duplicate removal, and `Print::apply()` synchronization through
|
||||
type changes and restored model snapshots.
|
||||
- [3MF tests](../../tests/libslic3r/test_precise_seam_3mf.cpp) cover the round
|
||||
trip of every mode and of inactive settings, attribute escaping, and which
|
||||
metadata combinations restore a seam mode.
|
||||
[Plugin tests](../../tests/slic3rutils/test_precise_seam_plugin.cpp) cover the
|
||||
Python bindings.
|
||||
@@ -1,432 +0,0 @@
|
||||
# System Preset Cache — High Level Design
|
||||
|
||||
## Why it exists
|
||||
|
||||
OrcaSlicer ships tens of thousands of system preset JSON files. Every launch used to
|
||||
parse all of them: read each vendor profile, walk its machine, process and filament
|
||||
sub-files, resolve inheritance, and build the preset collections from scratch. That
|
||||
parse dominated startup, and it produced the same result every time, because system
|
||||
presets only change when the app is updated or a profile update is installed.
|
||||
|
||||
The preset cache replaces that parse with a read. Each vendor's presets are serialized
|
||||
once — at build time, in CI — into a single binary file the app reads in one pass. The
|
||||
read replaces the file walk and the JSON parsing, which is where the time went;
|
||||
resolving inheritance and registering the presets still runs at load, through the same
|
||||
code the JSON path uses, so the result is the parse's result without the parse.
|
||||
|
||||
The cache is **only ever an optimization**. Every rule below exists to guarantee that a
|
||||
cache is either provably equivalent to parsing the JSONs, or rejected. There is no
|
||||
"mostly right" cache.
|
||||
|
||||
## The unit is one vendor
|
||||
|
||||
A cache covers exactly one vendor. `BBL.opc` sits beside `BBL.json` and holds
|
||||
everything `BBL.json` and the `BBL/` sub-file tree would have produced.
|
||||
|
||||
Per-vendor granularity is what makes the system practical:
|
||||
|
||||
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
|
||||
vendors keep theirs — even when the bumped vendor is the shared Orca filament
|
||||
library everyone else inherits from.
|
||||
- The setup wizard loads its vendors through the same routine as startup, so it
|
||||
gets the same speedup without a second code path.
|
||||
- A vendor with no cache, or a broken one, costs only that vendor a parse.
|
||||
|
||||
A cache holds *system* presets only. User presets, project settings and modified
|
||||
presets are never serialized — they have their own storage and their own lifecycle.
|
||||
|
||||
## Where the files live
|
||||
|
||||
| Location | Contents on a shipped build | Role |
|
||||
|---|---|---|
|
||||
| `resources/profiles/` | `<vendor>.opc` alone — the profile and its preset JSONs both pruned | What the app ships with; what installing copies from, and the only thing it is read for |
|
||||
| `<data_dir>/system/` | `<vendor>.opc` alone, or `<vendor>.json` + `<vendor>/` after an update | What the user has installed |
|
||||
| `<data_dir>/system/` (dev build) | `<vendor>.json` + `<vendor>/` + `<vendor>.opc` written at runtime | A developer tree caches as it parses |
|
||||
| `<data_dir>/cache/wizard_profile_data.json` | The wizard's derived vendor catalog plus the stamps it was built from | Written and read by the setup wizard only; never shipped (see "The wizard's profile-data cache") |
|
||||
|
||||
Two forms of the same vendor therefore exist, and the system's central rule is that
|
||||
**a vendor's cache is the whole of it**. Where a cache ships or is installed, no profile
|
||||
and no preset JSONs sit beside it: the cache carries the presets, the vendor profile,
|
||||
and the version stamp that says which release it came from. A vendor is "installed" if
|
||||
either form is present *and usable*, and its installed version is read from whichever
|
||||
form a load would serve.
|
||||
|
||||
What stays beside the caches in `resources/profiles/` is everything that is not a
|
||||
preset: each vendor's directory of printer thumbnails, cover images, bed models and
|
||||
hotend meshes, which are read from disk by path and were never part of the cache. Files
|
||||
that are not vendors at all, `blacklist.json` chief among them, are untouched.
|
||||
|
||||
The alternative — shipping both and treating the cache as a sidecar — was rejected. It
|
||||
doubles the installed size, and it creates a class of bug where the two disagree and
|
||||
the app's behavior depends on which one a given code path happened to read.
|
||||
|
||||
## What a cache file is
|
||||
|
||||
A fixed-size header followed by one binary stream.
|
||||
|
||||
The header carries a magic number, the cache format version, the payload size and a
|
||||
CRC32 of the payload. It exists so that a truncated download, a half-written file or a
|
||||
file from an entirely different program is rejected in microseconds, before anything
|
||||
tries to interpret it.
|
||||
|
||||
The payload opens with the stamps that decide whether the cache may be used at all —
|
||||
format version, vendor name, vendor version — then a dictionary, and then the vendor's
|
||||
data: its vendor profile, three lists of preset entries (process, filament, machine),
|
||||
and the count of errors the original parse hit.
|
||||
|
||||
Each entry is one preset **in source form**: what its JSON sub-file states and nothing
|
||||
that resolving it derives — the preset's own config diff, the name of the preset it
|
||||
inherits, the names of the presets it includes, and the parse metadata (name, sub-path,
|
||||
description, instantiation, setting and filament ids, renames). Non-instantiated base
|
||||
presets are stored too; the children that inherit from or include them cannot resolve
|
||||
without them.
|
||||
|
||||
**The payload names its own keys.** The dictionary holds the distinct `opt_key`s the
|
||||
file uses, the `ConfigOptionType` each was written as, and the distinct enum *value
|
||||
names*; an option in an entry's config is then a `uint16` index into that dictionary
|
||||
plus its value. Names are written once per file rather than once per occurrence, and a
|
||||
reader resolves the dictionary against this build's `print_config_def` once, after
|
||||
which reading an option is a vector index.
|
||||
|
||||
This is what makes the cache survive config-schema drift. The alternative — keying an
|
||||
option by its `serialization_key_ordinal`, the position `ConfigDef::add` assigns by
|
||||
declaration order at static init — cannot: inserting one option into the middle of
|
||||
`PrintConfig.cpp` shifts every later ordinal, and the lookup on the way back in then
|
||||
*succeeds on the wrong option*, silently, wherever the two share a type. Because a
|
||||
name-keyed payload instead drops the individual options this build cannot place, the
|
||||
file as a whole stays readable, and there is no schema fingerprint — no checksum over
|
||||
the option schema that would reject every cache on every release. An option this build
|
||||
no longer defines, or now defines with a different type, gets exactly what it gets from
|
||||
a JSON profile: read, dropped, and the rest of the preset loads.
|
||||
|
||||
The ordinal-keyed cereal hooks in `PrintConfig.hpp` are untouched — they are also the
|
||||
undo/redo wire format, where the process cannot change underneath them. The cache has
|
||||
its own serialization in `PresetCacheFormat.{hpp,cpp}`.
|
||||
|
||||
Three deliberate choices in the layout:
|
||||
|
||||
- **Stamps come first**, so the question "what version is this vendor installed at?"
|
||||
can be answered by reading the first kilobyte. The updater asks that question for
|
||||
every vendor on every launch; reading tens of megabytes to answer it would give back
|
||||
the startup time the cache saved. The dictionary sits behind them, ahead of the
|
||||
entries, so a reader that does go on resolves it once and then indexes.
|
||||
- **Nothing inherited is baked in.** A filament preset that inherits from the shared
|
||||
library is stored as its own diff plus its parent's name, and the parent is looked up
|
||||
when the entry is installed, against whatever library is loaded then. A cache
|
||||
therefore carries no other vendor's values, and no other vendor's update — the
|
||||
library's included — can make it stale.
|
||||
- **Nothing derived is stored.** Default presets, flattened configs, aliases and
|
||||
lookup maps are all reconstructed at load by the same code the JSON path runs, and
|
||||
state that path never fills (obsolete-preset lists) is not stored either. This keeps
|
||||
the cache a record of the vendor's data, not a memory image of the program's state.
|
||||
|
||||
## When a cache may be used
|
||||
|
||||
A cache is accepted only if every gate below passes. Any failure means "parse the
|
||||
JSONs instead" — never a hard error, never a partial load.
|
||||
|
||||
**1. Integrity.** Magic number, a declared body size that is exactly the rest of the
|
||||
file, CRC32 over the payload. The size is checked against the file's real length before
|
||||
anything is allocated on the strength of it, so an eight-byte field in an unauthenticated
|
||||
file cannot ask for a gigabyte.
|
||||
|
||||
**2. Cache format version.** A single integer bumped by hand whenever the binary layout
|
||||
changes in a way nothing else would catch: reordering or retyping a hand-written
|
||||
serialized field, or changing what the cache's own stamps mean. Config-schema drift is
|
||||
explicitly *not* such a change — the dictionary handles it — so this no longer moves
|
||||
every release.
|
||||
|
||||
**3. Vendor identity and version.** The cache names the vendor it holds and the profile
|
||||
version it was built from. It is accepted only if that version is at least as new as
|
||||
the profile now on disk. Where no profile sits beside the cache — the shipped,
|
||||
cache-only form — the comparison is skipped, because nothing on disk can be newer than
|
||||
a cache that is the installation.
|
||||
|
||||
**4. Every entry installs.** Entries are installed as they are read, and an entry that
|
||||
cannot be — typically one that inherits a parent the currently loaded filament library
|
||||
no longer provides — rejects the whole cache, never just the entry. A partial vendor is
|
||||
not a vendor.
|
||||
|
||||
There is deliberately no stamp for the shared filament library. A cache stores its
|
||||
filaments' inheritance by name and resolves it at load, so a library update changes
|
||||
what a cache load *produces*, never whether the cache is *valid* — the same file yields
|
||||
the updated result. This matters most on a shipped build, where a vendor is its cache
|
||||
and nothing else: a profile update that delivered only the library would otherwise have
|
||||
stranded every other vendor with a cache it invalidated and no JSONs to fall back on.
|
||||
|
||||
A vendor profile with no parsable version is never cached and never served from a
|
||||
cache. There would be no way to tell later whether the cache had gone stale, and a
|
||||
cache nothing can invalidate is worse than no cache.
|
||||
|
||||
## How a vendor is loaded
|
||||
|
||||
Vendors load in a fixed order, because filament inheritance crosses exactly one
|
||||
boundary: any vendor's filament may inherit from the shared Orca filament library,
|
||||
and nothing else reaches across vendors — an `include` is always vendor-local. Only
|
||||
installing a vendor's presets crosses it; reading the vendor, from its cache or its
|
||||
JSONs, needs nothing from the library. So every other vendor is read while the library
|
||||
loads, each is installed against it as soon as both are done, and the results are
|
||||
merged in a stable order:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
lib["1 · OrcaFilamentLibrary loaded;<br/>meanwhile every other vendor read<br/>from its cache or its JSONs"] --> par["2 · every other vendor installed<br/>in parallel, each into its own bundle,<br/>filaments resolving against the library"] --> merge["3 · bundles merged into one,<br/>in one pass per collection,<br/>in stable vendor order"]
|
||||
```
|
||||
|
||||
`PresetBundle::load_vendors` runs these steps for startup and for the setup wizard,
|
||||
which hand it the vendors to load and the directory each is installed in.
|
||||
|
||||
Whether a vendor comes from its cache or from a parse changes nothing in that
|
||||
order — both produce the same bundle, so cached and parsed vendors mix freely in
|
||||
one startup.
|
||||
|
||||
**A vendor is loaded from where it is installed and nowhere else.** For startup that
|
||||
is `<data_dir>/system/`; resources reaches the app by being *installed* into that
|
||||
directory first, never by being loaded from. (The setup wizard is the one caller with
|
||||
a different notion of "where": it also shows vendors the user has not installed, and
|
||||
loads those from `resources/profiles` — see "The wizard's profile-data cache".) There
|
||||
is one lookup tier and one parse source:
|
||||
|
||||
```
|
||||
load vendor V from <data_dir>/system:
|
||||
system/V.opc passes CACHE_VERSION + size + CRC + vendor name + version gate?
|
||||
yes -> serve from it
|
||||
no -> parse system/V.json, then write system/V.opc back
|
||||
```
|
||||
|
||||
The same decision drawn out — "the gates" are the four acceptance checks above:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
start["load vendor V from a directory dir<br/>— normally <data_dir>/system/"]
|
||||
start --> stamp["installed version = version of dir/V.json<br/>— or ∞ with no profile there,<br/>the cache then being the installation"]
|
||||
stamp --> g1{"dir/V.opc<br/>passes all four gates?"}
|
||||
g1 -- "yes" --> hit(["served from the<br/>installed cache"])
|
||||
g1 -- "no" --> pd["parse the JSONs in dir"]
|
||||
pd --> ver{"profile version<br/>parsable?"}
|
||||
ver -- "yes" --> save(["loaded; dir/V.opc written back —<br/>the next load takes the top path"])
|
||||
ver -- "no" --> raw(["loaded, never cached"])
|
||||
```
|
||||
|
||||
A second tier into `resources/profiles/` used to sit between those two, and a parse
|
||||
fallback to the same place behind them. Both existed only because an installed cache
|
||||
died on every app upgrade, when the schema fingerprint rejected it; with the fingerprint
|
||||
gone there is nothing for them to rescue. They also had a cost: on a developer tree the
|
||||
shipped cache answered first, so the profile in `<data_dir>/system/` was never parsed
|
||||
and its cache was never written back.
|
||||
|
||||
Serving from a cache is not a memory-image restore. The entries are deserialized and
|
||||
then installed by `install_vendor`, the routine the JSON path hands the vendor's entries
|
||||
to once it has parsed the sub-files: inheritance resolved against the presets installed
|
||||
before them and the currently loaded filament library, includes layered in, configs
|
||||
flattened onto the collection defaults, validated and registered. An `include` layers
|
||||
what the included base states, between the parent and the preset's own keys: the base's
|
||||
diff against the
|
||||
default, taken when the base itself was installed and before the per-variant padding
|
||||
`inherits` sees, so only what a template sets reaches the presets including it. The two
|
||||
paths share everything below the parse, which is what makes a cache-loaded bundle
|
||||
indistinguishable from a JSON-loaded one by construction rather than by test coverage.
|
||||
Installation also rebuilds each preset's file path from the local data directory, so a
|
||||
shipped cache never carries the generating machine's paths.
|
||||
|
||||
Installing an entry is split in two. `resolve_vendor_preset` flattens it, reading only
|
||||
what is registered under the names it inherits and includes, and `commit_vendor_preset`
|
||||
registers it, the only step that writes anything shared. Entries resolve across threads
|
||||
in runs and commit in the order the vendor lists them. A run ends before an entry that
|
||||
inherits or includes one already in it, since that one's commit registers what the
|
||||
entry resolves against, so no entry in a run reads what another in it registers. An
|
||||
entry's parse messages are held until it commits. The bundle, the log's parse and
|
||||
install messages and the error count therefore come out as parsing and installing one
|
||||
entry at a time would leave them, whatever the listing order.
|
||||
|
||||
App upgrades work because a cache normally survives one. Only a deliberate
|
||||
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
|
||||
install time rather than at load: a vendor whose cache this build cannot read counts
|
||||
as **not installed**, so the updater lays down a working copy on the next launch (see
|
||||
below). A vendor that still has its profile JSONs beside the cache is simply parsed
|
||||
and re-cached.
|
||||
|
||||
If a parse does happen and the vendor's profile carries a version, the app writes the
|
||||
cache back beside where it looked for the vendor. That is how a developer build warms
|
||||
itself up on second launch, and how a vendor delivered by a profile update becomes
|
||||
cached without waiting for the next release.
|
||||
|
||||
## The wizard's profile-data cache
|
||||
|
||||
The setup wizard's printer and filament pages want every vendor in one bundle — the
|
||||
installed ones *and* the shipped ones the user has not installed yet, because the
|
||||
wizard is where installing is chosen. Its set therefore spans two directories:
|
||||
`<data_dir>/system/` for installed vendors (shadowing resources on a name collision),
|
||||
`resources/profiles` for the rest, each vendor loaded from its own directory.
|
||||
|
||||
What the wizard actually consumes from that bundle is one derived JSON — the model /
|
||||
machine / filament / process catalog its web pages render — and that JSON is a pure
|
||||
function of the vendor set: each vendor's name and version, in load order. A profile
|
||||
change requires a version bump, so name and version determine a vendor's content
|
||||
wherever its copy sits; which directory served it is deliberately **not** stamped,
|
||||
and installing or removing a copy at an unchanged version leaves the cache valid. So
|
||||
the wizard caches the *derived JSON*, not another form of the inputs:
|
||||
`<data_dir>/cache/wizard_profile_data.json` holds the stamp list and the catalog. On
|
||||
open, the wizard computes the current stamps (one version peek per vendor) and, when
|
||||
they match, serves the catalog from the file — no bundle built, no preset installed.
|
||||
Caching bundle inputs instead was tried and measured: rebuilding the bundle from
|
||||
per-vendor caches costs over a second of preset installation whatever feeds it, so
|
||||
only skipping the rebuild entirely wins.
|
||||
|
||||
Any change to the set — a vendor added, removed or updated, or its cache-only
|
||||
`.opc` replaced by a newer one — changes the stamps and retires the whole file;
|
||||
the wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load
|
||||
startup uses (per-vendor caches serving where they cover), and writes the catalog
|
||||
back. When a vendor fails to load, the filament library included, that open falls
|
||||
back to the wizard's own scan of the vendor JSONs, as when no bundle can be built,
|
||||
and writes nothing. Selections, region and per-open decorations are applied
|
||||
downstream of the cache either way, so a served catalog is indistinguishable from a
|
||||
rebuilt one. Nothing ships this file and the updater never touches it; it is a
|
||||
locally written artifact, re-derived whenever stale, written through a temp file and
|
||||
rename so half a cache is never readable.
|
||||
|
||||
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
|
||||
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor
|
||||
cache file must not sit in that directory. Relatedly, the stamp reader is hardened:
|
||||
`read_cache_stamps` validates the cache version before reading anything
|
||||
variable-length and bounds the stamp strings' lengths, so a reader pointed at a
|
||||
foreign or damaged `.opc` rejects it cleanly instead of aborting on a garbage
|
||||
64-bit allocation.
|
||||
|
||||
## How a vendor is installed
|
||||
|
||||
Installing copies from `resources/profiles/` into `<data_dir>/system/`. A shipped build
|
||||
offers only a cache and a source tree only JSONs, but a partially-generated tree can
|
||||
have both, at different versions, so the installer picks the form that ships at the
|
||||
**newer version** and installs only that one:
|
||||
|
||||
- Cache newer or equal, and readable → copy the `.opc`, verify the *copy* is one this
|
||||
build can read, and only then delete any profile and vendor directory a previous
|
||||
install left behind, so nothing can shadow it.
|
||||
- Profile newer, or the cache unreadable or absent → copy the profile and the vendor's
|
||||
preset JSONs exactly as the app did before caches existed, and delete any stale `.opc`
|
||||
once the profile is safely in place.
|
||||
|
||||
One vendor that cannot be installed is one vendor missing, not a reason to leave the
|
||||
rest uninstalled: the installer skips it, records the failure, and carries on with the
|
||||
batch. A vendor whose cache arrives unreadable falls back to installing its profile,
|
||||
which is decided by reading the copy rather than by the kilobyte peek that chose the
|
||||
form.
|
||||
|
||||
**"Installed" means present and usable.** Where the cache is the whole of a vendor's
|
||||
installation, a `.opc` this build cannot read is not an installation — counted as one,
|
||||
the vendor would be stranded with nothing to load and the updater would never repair
|
||||
it. The installed version is likewise whichever form a load would actually serve: the
|
||||
cache's stamp while it covers the profile beside it, the profile's own version once it
|
||||
does not.
|
||||
|
||||
The result is that only one form of a vendor is ever present, and it is the newest one
|
||||
the build has. This matters most for the update check, which compares what is installed
|
||||
against what installing *would* lay down: if those two disagreed about which form
|
||||
counts, a vendor could reinstall on every launch forever, or silently never update.
|
||||
|
||||
Profile updates delivered over the air always arrive as JSONs, and they win — an
|
||||
updated vendor's real profile lands in the data directory, the installed cache beside it
|
||||
is older and gets rejected, and the vendor is parsed and re-cached. An update that touches only
|
||||
the filament library needs nothing more: every other vendor's cache stays valid and
|
||||
simply resolves against the new library on its next load.
|
||||
|
||||
## How the caches are produced
|
||||
|
||||
Cache generation is a build step, not something a user ever runs.
|
||||
|
||||
One script per platform does the whole job, and CI calls it once on each. It builds a
|
||||
small dev-utility that loads a profiles directory exactly as the app would, with cache
|
||||
writing enabled, dropping a `<vendor>.opc` beside every vendor profile it parses; then
|
||||
it copies those caches into each packaged application it was pointed at and deletes
|
||||
every preset JSON they replace — the vendor's own profile included. Only a vendor that
|
||||
actually has a cache is pruned, so a vendor the generator skipped keeps its JSONs and is
|
||||
simply parsed at startup.
|
||||
|
||||
Caches are generated into the checkout's own `resources/profiles`, because that is what
|
||||
cpack re-installs from when it builds the NSIS installer — so that directory is also a
|
||||
prune target in CI. Pruning it deletes the checkout's preset JSONs, which is a packaging
|
||||
step, not something a build should do to a working tree by surprise: the Windows script
|
||||
refuses that target unless given `--prune-source`, and CI passes it.
|
||||
|
||||
Generation runs after the build, in the same job, so the caches ship with a build that
|
||||
can read them.
|
||||
|
||||
The flatpak differs only in where the script is called from. Nothing outside
|
||||
flatpak-builder ever builds it, so there is no packaged tree for the workflow to point
|
||||
the script at afterwards: the manifest runs it as a build step instead, against the
|
||||
profiles the install has already copied into `/app`.
|
||||
|
||||
## Behavior when things go wrong
|
||||
|
||||
The system is designed so that no cache problem is fatal:
|
||||
|
||||
- **Corrupt, truncated or foreign file** — rejected at the header, vendor parsed. A
|
||||
cache is written to a temp file beside its target and moved into place, so a write
|
||||
that dies partway leaves the previous cache intact rather than a truncated one.
|
||||
- **An option this build no longer has, or now types differently** — that option alone
|
||||
is dropped, exactly as a JSON profile's would be. The preset and the file load.
|
||||
- **Cache from a build with a different cache layout** — rejected on `CACHE_VERSION`.
|
||||
A vendor with JSONs beside it is parsed and re-cached; a cache-only vendor reads as
|
||||
not installed and the updater reinstalls it.
|
||||
- **Stale cache** — rejected on the vendor version stamp, vendor parsed and re-cached.
|
||||
- **Failure part-way through loading** — a deserialization error, or any entry that
|
||||
fails to install — rejects the whole cache, and the bundle is reset to a clean state
|
||||
before falling back, so a half-loaded cache can never leak into the parsed result.
|
||||
- **A vendor that can be neither read nor parsed** — logged, and left out. The setup
|
||||
wizard drops that vendor from its list and opens with the rest; startup records the
|
||||
error alongside the vendors that did load. One broken vendor never takes the app down.
|
||||
|
||||
The one genuine limit: on a shipped build a vendor is its cache and nothing else, so a
|
||||
rejected cache has nothing to fall back to for that vendor. This is by design — the
|
||||
alternative is shipping every preset twice — and it is why the acceptance gates are
|
||||
conservative and why CI generates the caches with the same build that ships them. The
|
||||
recovery path is a profile update, which delivers real JSONs.
|
||||
|
||||
It also means nothing may quietly assume a `<vendor>.json` exists. Discovery, version
|
||||
checks and the update decision all read whichever form is present, and a code path that
|
||||
enumerates only `*.json` will find no vendors at all in a packaged build.
|
||||
|
||||
## Maintenance rules
|
||||
|
||||
- **Adding, removing, retyping or reordering a config option** needs nothing. The
|
||||
payload names its keys and its enum values, so an option a cache carries and this
|
||||
build does not is dropped; one this build has and the cache does not is simply
|
||||
absent, as it would be from a JSON that predates it.
|
||||
- **Changing a hand-written `serialize()`** — `VendorProfile` or its nested types — or
|
||||
the `CachedPreset` field list — written and read by `visit_entry` in
|
||||
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
|
||||
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
|
||||
- **Adding a kind of reference between presets**, as `inherits` and `include` are:
|
||||
parse the names into `CachedPreset` (a field change, so `CACHE_VERSION` is bumped),
|
||||
have `install_vendor_entries` end a run before an entry that names one already in it
|
||||
and retain what the names point at, look them up only in `resolve_vendor_preset`, and
|
||||
register what they point at only in `commit_vendor_preset`. The listing-order test in
|
||||
`test_vendor_cache.cpp` fails for a kind the runs do not check once its fixture uses
|
||||
it.
|
||||
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
|
||||
65535 options and one cache at most 65535 distinct enum value names.
|
||||
`CacheDictionary::save` throws past that, which surfaces when CI generates the
|
||||
caches rather than on a user's machine.
|
||||
- **Bumping `CACHE_VERSION` is safe without a resources fallback** because
|
||||
`is_vendor_installed` means *present and usable*: cache-only vendors read as not
|
||||
installed after a bump, and the updater reinstalls them from resources.
|
||||
- **Bumping a vendor profile's version** invalidates that vendor's cache and nothing
|
||||
else — the filament library's included. Other vendors' caches resolve against the
|
||||
new library the next time they load.
|
||||
- **Caches are never committed.** They are build artifacts, generated per build,
|
||||
ignored by git.
|
||||
|
||||
## Where this lives in the tree
|
||||
|
||||
| Area | Files |
|
||||
|---|---|
|
||||
| Everything about the bytes on disk — the dictionary, one config's wire format, the file framing and stamps, entry serialization, `VendorCacheFile` save/load/peeks | `src/libslic3r/PresetCacheFormat.{hpp,cpp}` |
|
||||
| Serve-or-parse decision, installing cache entries into a bundle, cache write-back | `src/libslic3r/PresetBundle.{hpp,cpp}` |
|
||||
| Vendor profile serialization | `src/libslic3r/Preset.hpp` |
|
||||
| Vendor discovery, installed/shipped versions, installation | `src/libslic3r/utils.cpp` (declared in `Utils.hpp`) |
|
||||
| Update and reinstall decisions | `src/slic3r/Utils/PresetUpdater.cpp` |
|
||||
| Setup wizard and printer-selection dialog | `src/slic3r/GUI/ConfigWizard.cpp`, `src/slic3r/GUI/WebGuideDialog.cpp` |
|
||||
| Generator tool | `src/dev-utils/generate_system_cache.cpp` |
|
||||
| Build and packaging script | `scripts/build_preset_cache.{sh,bat}` |
|
||||
| Tests | `tests/libslic3r/test_vendor_cache.cpp` |
|
||||
@@ -1,197 +0,0 @@
|
||||
# Prime tower sparse layers — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
A prime tower exists to absorb filament changes, but it is planned on every
|
||||
object layer below the topmost change, not only on the layers that purge. The
|
||||
layers in between carry no filament change and print nothing but a block of the
|
||||
tower's own footprint to keep its top level. They are called sparse layers, and
|
||||
on a print with few changes they are most of the tower: they cost time, filament
|
||||
and a travel to the tower on every layer.
|
||||
|
||||
Two settings trade that cost against something else. `wipe_tower_no_sparse_layers`
|
||||
drops them, which sinks the tower below the model. `wipe_tower_sparse_layers_combination`
|
||||
merges runs of them into fewer, thicker layers, which keeps the tower level with
|
||||
the model. Both are off by default, and with both off the tower prints one layer
|
||||
per object layer as it always has.
|
||||
|
||||
The decisions belong to tower planning and G-code emission. They do not change
|
||||
sliced object geometry, but they do change the emitted G-code, the filament and
|
||||
time estimates, and — for the compacted case — whether a plate is printable at
|
||||
all. Changing either setting invalidates the tower step.
|
||||
|
||||
## What a sparse layer is
|
||||
|
||||
`ToolOrdering::fill_wipe_tower_partitions` counts the filament changes per layer
|
||||
and propagates that count downwards, so every layer below the topmost change is
|
||||
marked as carrying a tower. It then fills any gap between two tower layers, so
|
||||
the tower is continuous from the bed to its last purge. `wipe_tower_layer_height`
|
||||
is the distance from the previous tower layer, which is the object's layer height
|
||||
whenever the tower prints on every layer.
|
||||
|
||||
`Print::_make_wipe_tower` plans one tower layer per such object layer. A layer
|
||||
whose only call keeps the current filament leaves no toolchange in the plan, and
|
||||
the layer it generates is a single result whose initial and new tool are equal.
|
||||
That is what `wipe_tower_layer_is_sparse` recognises, and it is the unit both
|
||||
settings work on.
|
||||
|
||||
The plan stays one entry per tower layer in every case. The G-code emitter walks
|
||||
`WipeTowerData::tool_changes` by layer index, advancing once per object layer
|
||||
that carries a tower, so a planner that removed entries would silently shift
|
||||
every later layer onto the wrong tower geometry. Layers that print nothing are
|
||||
therefore still planned and still generated; they are marked, and the emitter
|
||||
drops them.
|
||||
|
||||
## Shared rules
|
||||
|
||||
Tower planning, G-code emission and the plate validation all have to agree about
|
||||
which layers print and where. They ask one set of free functions, declared beside
|
||||
the tower classes, rather than each re-deriving the answer from the raw options:
|
||||
|
||||
- `wipe_tower_sparse_layers_skipped` — whether sparse layers are really dropped.
|
||||
Smooth timelapse and clumping detection park the nozzle on the tower every
|
||||
layer, so with either of them on no layer is ever dropped and the option reads
|
||||
as off everywhere.
|
||||
- `wipe_tower_sparse_layers_combined` — whether runs are really merged. The same
|
||||
two rule it out, and so does `wipe_tower_no_sparse_layers`: dropping the layers
|
||||
outright is the stronger answer to the same problem, so the two settings are
|
||||
exclusive and the GUI greys out the second while the first is on.
|
||||
- `wipe_tower_layer_is_sparse`, `wipe_tower_layer_is_combined_away` — per-layer
|
||||
questions the emitter asks about generated results.
|
||||
- `compute_compacted_wipe_tower_z` — the tower's print z per planned layer when
|
||||
it is compacted.
|
||||
- `combine_sparse_wipe_tower_layers` and its `combine_sparse_wipe_tower_plan`
|
||||
wrapper — the merge rule, applied to either generator's plan.
|
||||
|
||||
Both tower generators are driven through these. `WipeTower` (Type 1, the block
|
||||
tower) and `WipeTower2` (Type 2, the default) keep separate plans with the same
|
||||
per-layer shape — print z, layer height, toolchanges, and a `combined_away` flag
|
||||
— so one template covers both.
|
||||
|
||||
## Dropping sparse layers
|
||||
|
||||
With `wipe_tower_no_sparse_layers`, the tower only grows on layers that carry a
|
||||
real change. It therefore falls one layer height behind the object for every
|
||||
sparse layer, and by the top of a tall print it can sit far below the model. The
|
||||
nozzle has to reach down to it at each purge.
|
||||
|
||||
`compute_compacted_wipe_tower_z` derives that z once, from the generated results,
|
||||
so the emitter and the validator cannot disagree. Emission descends to it, but
|
||||
only once the nozzle is parked over the tower: descending while still over the
|
||||
model would drive the nozzle into the print, so a descent that would do that is
|
||||
deferred until after the travel to the tower. Extrusions emitted without an
|
||||
explicit z — the nozzle-change wipe in particular — are pulled down to the
|
||||
compacted z for the same reason.
|
||||
|
||||
Reaching down is only safe if nothing tall stands near the tower. `Print.hpp`
|
||||
carries the clearance rule: a keep-out zone grown from the tower's footprint by
|
||||
the spiral z-hop envelope, and a per-object limit on how high an object may rise
|
||||
near it, tiered by the nozzle cone, the head body, the rod and the lid. The same
|
||||
rule serves the precise check on real extrusions, the pre-slice estimate that
|
||||
feeds the plater, and the outlines the plater draws while an object is dragged,
|
||||
so that the ring the user sees touches the object's outline exactly when the
|
||||
check trips.
|
||||
|
||||
## Merging sparse layers
|
||||
|
||||
With `wipe_tower_sparse_layers_combination`, no layer is dropped and nothing is
|
||||
compacted: the tower keeps following the object, and the nozzle never descends.
|
||||
Instead a run of consecutive sparse layers prints once, on the run's last layer,
|
||||
at the accumulated height of everything it covers — the same way infill
|
||||
combination merges sparse infill. The layers below it in the run print nothing.
|
||||
|
||||
`combine_sparse_wipe_tower_plan` runs before the tower's depths are planned,
|
||||
because the heights it rewrites feed the extrusion flow of every later pass. It
|
||||
raises `height` in place on the layer that prints a run and sets `combined_away`
|
||||
on the rest; generation then proceeds unchanged, and the flag is copied onto the
|
||||
results so the emitter can drop them.
|
||||
|
||||
Four constraints shape the rule:
|
||||
|
||||
- **Whole layers only.** A tower layer is entered at the object's z, so a merged
|
||||
layer has to end on an object layer boundary. The merged height is therefore a
|
||||
sum of whole layer heights, never a clamped value.
|
||||
- **The nozzle's maximum layer height.** A run stops growing as soon as one more
|
||||
layer would pass `max_layer_height` for the nozzle printing it — three quarters
|
||||
of the nozzle diameter when that is left at 0, as elsewhere in slicing. The cap
|
||||
is read through the filament-to-nozzle map, since `max_layer_height` is per
|
||||
nozzle while the tower indexes filaments. This is what makes the setting inert
|
||||
at common layer heights: two 0.2 mm layers are 0.4 mm and do not fit under a
|
||||
0.3 mm maximum, so nothing merges until the layer height is 0.15 mm or below,
|
||||
or the maximum is raised.
|
||||
- **A filament change purges at its own z.** A layer with a real change can
|
||||
neither be merged away nor absorb the run below it, so a run always ends on its
|
||||
own last sparse layer and the change above it is untouched.
|
||||
- **The first layer stays on the bed.** It carries the brim and is never merged.
|
||||
|
||||
A run holds one filament throughout — that is what makes it sparse — so the cap
|
||||
is uniform across it, and the tower reserves depth only for the purges above a
|
||||
layer, so a run has one footprint and the merged layer covers exactly the area
|
||||
the layers it replaces would have.
|
||||
|
||||
## Emission and accounting
|
||||
|
||||
`WipeTowerIntegration` drops a layer whose results are marked, for both settings,
|
||||
through the same `ignore_sparse` path in `tool_change` and
|
||||
`is_empty_wipe_tower_gcode`. A dropped layer emits no travel to the tower and no
|
||||
extrusion.
|
||||
|
||||
Filament used is accumulated by the generators while they write, so a layer that
|
||||
will be dropped must not be charged. Type 1 asks `layer_is_printed` at each of
|
||||
its accumulation points; Type 2 guards the equivalent block in `finish_layer`,
|
||||
which also stops a merged-away layer from adding height of its own — the layer
|
||||
that prints the run carries all of it.
|
||||
|
||||
A merged layer is the only case where the tower's layer height differs from the
|
||||
object layer it sits on, and therefore the only case where the height the
|
||||
exporter already emitted for that layer is wrong for the tower. Both generators
|
||||
do declare a height, but each hardcodes a tag dialect — the block tower forces
|
||||
the BBL tag, the other writes the compatible one — while the G-code processor
|
||||
reads only the tag its printer uses. On a non-BBL printer with a Type 1 tower the
|
||||
declaration is dropped, and the merged layer is drawn and costed as a thin one.
|
||||
`WipeTowerIntegration::tower_height_tag` therefore declares it at export time,
|
||||
where the printer is known, and only when the tower's own G-code does not already
|
||||
carry the tag that will be read. The object's height returns on the next object
|
||||
path, because emission forces the processor role to the tower on any layer that
|
||||
carries one.
|
||||
|
||||
## Constraints
|
||||
|
||||
A layer that prints nothing prints nothing at all, including any interface work
|
||||
the tower planner scheduled there. The Type 1 block planner marks a layer as a
|
||||
contact layer when a filament category stops or starts being used relative to the
|
||||
layer below, and a sparse layer immediately above a change qualifies. Merging a
|
||||
run, like dropping its layers, replaces that interface with the run's single
|
||||
layer. Both settings are off by default for this among other reasons.
|
||||
|
||||
Neither setting changes what the tower is for. A plate that needs a tower on
|
||||
every layer — smooth timelapse, clumping detection — gets one, and the settings
|
||||
read as off rather than compacting or merging in one place and not another.
|
||||
|
||||
## Implementation and verification
|
||||
|
||||
- [WipeTower.hpp](../../src/libslic3r/GCode/WipeTower.hpp) declares the shared
|
||||
rules and the plan-merging template;
|
||||
[WipeTower.cpp](../../src/libslic3r/GCode/WipeTower.cpp) implements them and
|
||||
the Type 1 tower, [WipeTower2.cpp](../../src/libslic3r/GCode/WipeTower2.cpp)
|
||||
the Type 2 tower.
|
||||
- [ToolOrdering.cpp](../../src/libslic3r/GCode/ToolOrdering.cpp) decides which
|
||||
layers carry a tower at all, and
|
||||
[Print.cpp](../../src/libslic3r/Print.cpp) plans it and runs the clearance
|
||||
check whose rule lives in [Print.hpp](../../src/libslic3r/Print.hpp).
|
||||
- [GCode.cpp](../../src/libslic3r/GCode.cpp) emits the tower, drops the layers
|
||||
that print nothing, and declares a merged layer's height;
|
||||
[PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines the settings and
|
||||
[ConfigManipulation.cpp](../../src/slic3r/GUI/ConfigManipulation.cpp) their
|
||||
mutual exclusion.
|
||||
- [GLCanvas3D.cpp](../../src/slic3r/GUI/GLCanvas3D.cpp) and
|
||||
[PartPlate.cpp](../../src/slic3r/GUI/PartPlate.cpp) draw the compacted tower's
|
||||
keep-out outlines live while the user drags.
|
||||
- [Rule tests](../../tests/libslic3r/test_wipe_tower.cpp) cover the gating of
|
||||
both settings, the per-layer predicates, the compacted z, the merge rule's run
|
||||
flushing, height conservation, the nozzle cap and the first-layer exemption,
|
||||
and the clearance geometry the plater draws.
|
||||
- [Slicing tests](../../tests/fff_print/test_wipe_tower.cpp) slice a real print
|
||||
and check that a run folds, that the tower still covers the object exactly
|
||||
once, that a run too thin for the cap is left alone, and that a merged layer
|
||||
declares its height in the tag the printer's processor reads.
|
||||
@@ -1,240 +0,0 @@
|
||||
# Printer agents
|
||||
|
||||
Printer agents isolate printer-specific communication from the rest of OrcaSlicer. The GUI and
|
||||
`DeviceManager` operate on a shared set of printer operations and device state; a selected printer
|
||||
agent implements those operations for a particular printer ecosystem. The agent boundary allows
|
||||
Bambu, Moonraker-based printers, built-in integrations, and Python-provided integrations to use the
|
||||
same application workflow without making the GUI understand every printer protocol.
|
||||
|
||||
The current boundary is an adapter boundary around the existing application contract. In particular,
|
||||
some request fields and message payloads still use the Bambu-shaped representation that existing
|
||||
`MachineObject` and `DeviceManager` code consumes. The printer agent is responsible for translating
|
||||
that representation into the protocol spoken by its printer. This is an intentional compatibility
|
||||
constraint of the current design; the interface is not yet a neutral printer protocol.
|
||||
|
||||
The v1 dialect migration path is deliberately narrow. `DeviceManager` currently speaks the Bambu JSON
|
||||
dialect because that is the payload shape already used throughout the command and state workflow. The
|
||||
v1 `OrcaPrinterAgent` also accepts that Bambu dialect. Its transport path places the small translation
|
||||
needed for the target printer at `deliver_to_sink`, keeping the compatibility code at the edge rather
|
||||
than spreading it through `DeviceManager` or the agent interface.
|
||||
|
||||
The eventual direction is for `DeviceManager` to produce an Orca JSON dialect. The Bambu agent will then
|
||||
own the translation from Orca JSON to Bambu's protocol, while `OrcaPrinterAgent` can forward the Orca
|
||||
payload directly to its sink. The v1 translation at `deliver_to_sink` can then be removed without
|
||||
changing `DeviceManager`, the command callers, or the rest of the agent workflow.
|
||||
|
||||
## Components
|
||||
|
||||
The system has four relevant layers:
|
||||
|
||||
```text
|
||||
GUI / DeviceManager / MachineObject
|
||||
|
|
||||
NetworkAgent
|
||||
/ \
|
||||
IPrinterAgent ICloudServiceAgent
|
||||
| |
|
||||
printer protocol authentication and cloud services
|
||||
```
|
||||
|
||||
### `DeviceManager` and `MachineObject`
|
||||
|
||||
`DeviceManager` owns the application-facing printer workflow. It maintains `MachineObject` instances,
|
||||
updates their state, filters devices for the active printer agent, and initiates operations such as
|
||||
homing, temperature changes, printing, subscriptions, and camera playback.
|
||||
|
||||
`MachineObject` remains the shared state model used by the GUI. It does not contain the implementation
|
||||
of a printer protocol. When a device is discovered or returned by a cloud query, the device is tagged
|
||||
with the active `printer_agent_id`. Device lists and selected-machine operations use that tag to avoid
|
||||
sending an operation through an agent that does not own the device.
|
||||
|
||||
### `NetworkAgent`
|
||||
|
||||
`NetworkAgent` is the façade used by the GUI and `DeviceManager`. It owns:
|
||||
|
||||
- the currently selected `IPrinterAgent`;
|
||||
- the registered cloud-service instances, indexed by provider;
|
||||
- callbacks shared by the active printer agent and the application;
|
||||
- the forwarding methods for printer commands and cloud operations.
|
||||
|
||||
There is one active printer agent for the currently selected printer preset. Switching the preset
|
||||
increments the machine-list generation, disconnects the old printer agent, removes its callbacks, and
|
||||
installs the newly selected agent. The façade then forwards printer operations to that agent.
|
||||
|
||||
Cloud operations are selected separately using a provider key. `NetworkAgent` forwards a cloud request
|
||||
to the matching `ICloudServiceAgent`, and forwards cloud camera operations with a device ID. The
|
||||
printer agent receives a cloud-agent pointer through `set_cloud_agent()` when it is created, allowing
|
||||
printer communication to obtain cloud tokens without depending on a concrete cloud implementation.
|
||||
|
||||
### `IPrinterAgent`
|
||||
|
||||
`IPrinterAgent` is the printer-facing contract. It covers:
|
||||
|
||||
- cloud-relay and direct-LAN message delivery;
|
||||
- LAN connection, discovery, binding, and certificates;
|
||||
- printer subscriptions and callbacks;
|
||||
- print operations;
|
||||
- filament synchronization;
|
||||
- camera capability and local camera URL reporting;
|
||||
- printer command methods.
|
||||
|
||||
Concrete built-in implementations include the Bambu wrapper, the native Orca/Moonraker path, and
|
||||
other printer-agent implementations registered by the application. A printer agent may use either
|
||||
the cloud agent, a direct LAN connection, or both.
|
||||
|
||||
### `ICloudServiceAgent`
|
||||
|
||||
`ICloudServiceAgent` owns authentication and services provided by a cloud backend. It covers login
|
||||
state, tokens, user and printer lists, settings synchronization, model services, cloud messages, and
|
||||
cloud camera operations.
|
||||
|
||||
Cloud camera operations are device-scoped:
|
||||
|
||||
- `get_camera_url(dev_id, callback)` obtains a stream URL for one device;
|
||||
- `create_camera_signaling_channel(dev_id)` creates signaling for one device where the provider
|
||||
supports it.
|
||||
|
||||
This is separate from the local camera URL exposed by `IPrinterAgent`, which is currently scoped to
|
||||
the active printer agent because a normal LAN agent represents one physical printer connection.
|
||||
|
||||
## Agent registration and selection
|
||||
|
||||
`NetworkAgentFactory` maintains the printer-agent registry. Each registry entry contains an agent ID,
|
||||
a display name, and a factory function. Built-in agents register during application initialization.
|
||||
Python printer-agent capabilities register dynamically and contribute an agent ID and factory entry.
|
||||
|
||||
The selected printer preset contains the printer-agent choice. If no explicit choice is stored, the
|
||||
application preserves the existing default behavior: Bambu presets select the Bambu agent and other
|
||||
presets select the native Orca agent. When a preset is changed, `GUI_App` resolves the effective agent
|
||||
ID, obtains the corresponding cloud agent, creates the printer agent through the registry, and installs
|
||||
it in `NetworkAgent`.
|
||||
|
||||
The registry rejects conflicting agent IDs. This matters for Python plugins because an agent ID is the
|
||||
stable identity used by presets and device ownership; two enabled plugin capabilities must not claim
|
||||
the same ID.
|
||||
|
||||
## Message and command flow
|
||||
|
||||
There are two low-level message paths:
|
||||
|
||||
- `send_message()` publishes a command through the printer's cloud relay;
|
||||
- `send_message_to_printer()` sends a command directly to the printer over the LAN path.
|
||||
|
||||
Both paths accept a JSON string, quality-of-service and flag values, and return the existing network
|
||||
status code domain. The agent owns the conversion from that JSON contract to its native transport.
|
||||
|
||||
The typed `command_*` methods are the application-facing convenience layer. The five generic defaults
|
||||
currently implemented by `IPrinterAgent` construct the existing JSON dialect and route through the
|
||||
same message path:
|
||||
|
||||
| Method | Default operation |
|
||||
| --- | --- |
|
||||
| `command_xyz_abs()` | Send `G90` for absolute positioning |
|
||||
| `command_auto_leveling()` | Send `G29` for bed leveling |
|
||||
| `command_go_home()` | Use the supported homing operation or send `G28` |
|
||||
| `command_set_bed()` | Use the supported bed control or send `M140` |
|
||||
| `command_set_nozzle()` | Send `M104` for nozzle temperature |
|
||||
|
||||
These are compatibility defaults for common printer workflows, not a guarantee that every firmware
|
||||
implements every command identically. An agent can override a method when its protocol needs another
|
||||
operation. For example, a Klipper configuration may use `BED_MESH_CALIBRATE` instead of `G29`.
|
||||
|
||||
The remaining common command methods default to `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` because their
|
||||
existing behavior is vendor-specific or has no portable implementation:
|
||||
|
||||
- AMS RFID refresh;
|
||||
- AMS calibration;
|
||||
- AMS tray selection;
|
||||
- camera start;
|
||||
- axis control.
|
||||
|
||||
The methods remain on the common interface so an agent that supports them can override them explicitly.
|
||||
`sequence_id` remains part of the command contract because `DeviceManager` creates and tracks it as
|
||||
the command ID.
|
||||
|
||||
## Device ownership and stale responses
|
||||
|
||||
Printer-agent ownership is represented by `printer_agent_id` on device records and `MachineObject`
|
||||
instances. The active agent ID is attached when a device is discovered, returned by a cloud list, or
|
||||
reused after a preset switch. Local-machine configuration also persists the agent ID so a saved LAN
|
||||
device is not silently reused by an unrelated agent.
|
||||
|
||||
Cloud printer-list responses carry three pieces of request context added by `NetworkAgent`:
|
||||
|
||||
```text
|
||||
provider cloud provider used for the request
|
||||
agent_id active printer agent when the request was made
|
||||
generation machine-list generation when the request was made
|
||||
```
|
||||
|
||||
`DeviceManager` accepts the response only when those values still match the current provider, active
|
||||
agent, and generation. This prevents a slow response from the previous preset or provider from
|
||||
repopulating the current device list.
|
||||
|
||||
The provider mapping is currently selected by `GUI_App`: the Bambu agent maps to the Bambu cloud
|
||||
provider and other agents map to the Orca cloud provider. The generation check protects that existing
|
||||
selection from races; it does not make cloud-provider ownership intrinsic to an agent. Cloud-printer
|
||||
ownership and the broader Orca cloud services are therefore still separate architectural concerns.
|
||||
|
||||
## Python printer agents
|
||||
|
||||
`PrinterAgentPluginCapability` implements `IPrinterAgent` directly. The live capability object is
|
||||
registered with `NetworkAgentFactory` and handed out as the printer agent when its agent ID is selected.
|
||||
The plugin receives the selected `ICloudServiceAgent` through `set_cloud_agent()` just like a built-in
|
||||
printer agent.
|
||||
|
||||
Python plugins must implement the core communication and lifecycle methods required by the interface,
|
||||
including agent metadata, printer connection, discovery callbacks, and the two message-send methods.
|
||||
Methods that are meaningful only to a particular printer are optional overrides where the C++ base
|
||||
class provides a default.
|
||||
|
||||
All ten `command_*` methods are available in the Python binding and in the trampoline. Their override
|
||||
status is intentionally optional:
|
||||
|
||||
- the five generic commands use the C++ default when Python does not override them;
|
||||
- the five vendor-specific commands return `NOT_SUPPORTED` unless Python supplies an implementation;
|
||||
- a Python implementation can replace either behavior for its own protocol.
|
||||
|
||||
The Python camera binding exposes HTTP, HTTPS, RTSP, and HTTP-snapshot modes. WebRTC remains a
|
||||
built-in C++ camera mode, but is not exposed as a Python mode because the current Python capability
|
||||
does not provide the corresponding cloud signaling-channel contract.
|
||||
|
||||
## Camera playback boundary
|
||||
|
||||
The camera stream mode describes how a stream is obtained; it does not by itself define ownership of
|
||||
the wxWidgets view that renders it. `MediaPlayCtrl` selects and tears down the active backend, while
|
||||
the wx parent owns the child window or renderer. This is important because a web view, native media
|
||||
control, and frame-based/WebRTC renderer have different wx window-lifetime requirements.
|
||||
|
||||
Cloud URL and signaling requests are routed through `NetworkAgent` to the cloud provider selected for
|
||||
the device. Local URL requests are routed to the active printer agent. The distinction keeps cloud
|
||||
account services device-scoped while preserving the current one-LAN-agent/one-printer model.
|
||||
|
||||
## Compatibility constraints
|
||||
|
||||
The printer-agent boundary intentionally preserves several existing application contracts:
|
||||
|
||||
- Bambu-shaped JSON is still the shared command representation;
|
||||
- existing network status codes are reused, with Orca-specific unsupported/capability errors added
|
||||
in the Orca-reserved range;
|
||||
- `MachineObject` remains the shared device-state model;
|
||||
- preset and local-machine data retain compatibility with the existing agent-selection behavior;
|
||||
- Python plugins use the existing capability and pybind11 registration system.
|
||||
|
||||
The agent abstraction is therefore responsible for containing vendor differences, not for pretending
|
||||
that all vendor protocols are identical. The planned Orca JSON dialect is the protocol-neutral command
|
||||
model for the `DeviceManager`/agent boundary. Once it is introduced, Bambu-specific translation remains
|
||||
inside the Bambu agent and the Orca agent's v1 sink adapter can be removed as a self-contained cleanup.
|
||||
|
||||
## Main implementation locations
|
||||
|
||||
- [`IPrinterAgent`](../../src/slic3r/Utils/IPrinterAgent.hpp) — printer-agent contract and generic command defaults
|
||||
- [`ICloudServiceAgent`](../../src/slic3r/Utils/ICloudServiceAgent.hpp) — cloud service and per-device
|
||||
cloud camera contract
|
||||
- [`NetworkAgent`](../../src/slic3r/Utils/NetworkAgent.hpp) — façade and dispatch between active agents
|
||||
- [`NetworkAgentFactory`](../../src/slic3r/Utils/NetworkAgentFactory.hpp) — built-in and Python agent registry
|
||||
- [`DeviceManager`](../../src/slic3r/GUI/DeviceCore/DevManager.cpp) — device ownership, filtering, and
|
||||
stale-response checks
|
||||
- [`PrinterAgentPluginCapability`](../../src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp)
|
||||
— Python bindings
|
||||
- [`MediaPlayCtrl`](../../src/slic3r/GUI/MediaPlayCtrl.cpp) — camera backend selection and playback lifecycle
|
||||
@@ -1,170 +0,0 @@
|
||||
# Wipe inward — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
Wipe inward reduces reheating of fresh plastic and visible seam artifacts by
|
||||
moving the hot nozzle toward adjacent printed material during the external-wall
|
||||
wipe. Wipe marks are especially visible at layer heights below 0.1 mm.
|
||||
The option applies only to wipes after external walls, including walls around
|
||||
holes. It does not offset wipes after inner walls, infill or supports. For an
|
||||
outer contour the move is inward; for a hole it is away from the hole, toward
|
||||
the surrounding material. The path must remain supported by material that is
|
||||
already present when the wipe executes.
|
||||
|
||||
The operation belongs to G-code generation. It uses extrusion paths, their actual
|
||||
widths and their print order. Changing its settings invalidates G-code export
|
||||
while preserving the sliced geometry.
|
||||
|
||||
## Settings and eligibility
|
||||
|
||||
`wipe_inward` defaults to disabled and requires Wipe while retracting to be
|
||||
enabled for the active filament. `wipe_inward_distance` defaults to 50% of the
|
||||
actual external-wall extrusion width; it also accepts an absolute distance in
|
||||
millimeters. Using the path width makes Auto width and Arachne's variable widths
|
||||
meaningful. The effective offset is limited by that width and the spacing to the
|
||||
adjacent wall. A zero distance disables the offset.
|
||||
|
||||
Only external perimeters with a suitable, previously printed inner perimeter
|
||||
are eligible. A configured wall count alone cannot establish eligibility:
|
||||
the local geometry may contain fewer walls, and walls scheduled later do not
|
||||
provide support. Outer/Inner wall order therefore normally retains the regular
|
||||
wipe path.
|
||||
|
||||
Retraction and pressure advance calibrations disable inward wiping so it cannot
|
||||
mask the behavior being measured. The calibration settings turn it off, and
|
||||
G-code generation enforces this even if a profile or object override enables it.
|
||||
|
||||
## Path selection and support
|
||||
|
||||
The planner identifies an adjacent inner perimeter on the material side of the
|
||||
outgoing wall. Contour winding and the distinction between outer contours and
|
||||
holes establish a preferred direction; local printed geometry resolves ambiguous
|
||||
or self-touching contours.
|
||||
|
||||
Candidate paths offset or translate the portion needed for the configured wipe
|
||||
distance. A wide seam gap can prevent a supported forward path; following the
|
||||
incoming printed wall backwards is also a candidate. If translating that wall
|
||||
cannot provide a complete wipe around a curve, the planner tries an offset of
|
||||
the reversed wall. Direction checks allow coordinate-rounding error at a
|
||||
perpendicular entry, while rejecting actual backtracking. The planner checks the
|
||||
complete executable path, including its connector from the nozzle position,
|
||||
against the current and earlier printed perimeters. Nearby endpoints alone do
|
||||
not establish support across a gap.
|
||||
|
||||
Each region accumulates its printed perimeter prefix once, in extrusion order.
|
||||
Every entity contributes its geometry only after it is printed, and the prefix
|
||||
is discarded when the region ends. This collection is skipped when inward wiping
|
||||
is disabled or its configured distance is zero. A mixed inner-wall loop remains
|
||||
an eligible target even when its first path is an overhang: ordinary inner-wall
|
||||
paths elsewhere in the loop identify it. Likewise, an external loop with an
|
||||
overhanging start remains eligible when other segments identify the external
|
||||
wall. It is available for support checks but is not an inner-wall target.
|
||||
Candidate-specific support filtering and AABB trees are built only for eligible
|
||||
external loops, then reused across their candidate paths.
|
||||
|
||||
Material-side validation applies with or without a seam gap. Along each
|
||||
candidate, local wall normals point toward the adjacent printed inner wall;
|
||||
samples on the opposite side are rejected even when they remain close enough
|
||||
to the external wall to pass the support check. This uses the open wall geometry
|
||||
without treating it as a closed polygon. Full paths at a zero-gap seam also
|
||||
retain clearance from the external wall after their initial connector. At a
|
||||
clipped corner, another branch can be closer than the requested offset, so
|
||||
material-side and support checks apply without that additional clearance rule.
|
||||
|
||||
An accepted candidate replaces the stored wipe path as a whole. A short direct
|
||||
inward move is also eligible when longer candidates fail validation. It may
|
||||
waive full wall clearance, but must pass the material-side check. Its initial
|
||||
direction is checked from the actual nozzle position after any loop pre-move;
|
||||
the original wall endpoint is retained separately for intersection checks. It takes
|
||||
priority over the alternate offset when the preferred and translated paths
|
||||
are unusable. A longer reversed path may replace the selected candidate only
|
||||
when its distance to the target inner wall is no worse within tolerance.
|
||||
|
||||
## Fallback to the regular wipe
|
||||
|
||||
The original wipe path is retained when:
|
||||
|
||||
- No suitable adjacent inner wall has already been printed near the seam. This
|
||||
includes single-wall areas, locally missing inner walls and normally Outer/Inner
|
||||
wall order. A distant wall or a wall on the air side does not qualify.
|
||||
- The requested or available offset, or the configured wipe distance, is zero
|
||||
or too small at the geometry's coordinate precision.
|
||||
- Degenerate geometry prevents construction of a usable candidate, or all
|
||||
candidates fail the checks for printed support, direction, wall clearance or
|
||||
the connector from the actual nozzle position. This can occur at tight corners,
|
||||
narrow features or seam gaps.
|
||||
|
||||
Corners and seam gaps do not automatically trigger fallback: an offset,
|
||||
translated, reversed or short direct inward path may still be valid. The regular
|
||||
wipe is retained only when no candidate is accepted.
|
||||
|
||||
Fallback uses the path and retraction rules for `wipe_inward` disabled.
|
||||
Wipe while retracting must still be enabled for a wipe to occur; `wipe_on_loops`
|
||||
remains controlled by its own setting.
|
||||
|
||||
## Interaction with Wipe on loop
|
||||
|
||||
`wipe_on_loops` is an independent option that makes a short move before leaving
|
||||
an external loop. It can operate with `wipe_inward` disabled. When both options
|
||||
are enabled, its destination is the starting position for the inward wipe.
|
||||
|
||||
The loop move samples the outgoing and incoming paths by distance across path
|
||||
boundaries. The sampling distance is bounded by the nozzle diameter and one
|
||||
quarter of the total path length. It samples the outgoing path at up to 20% of
|
||||
the nozzle diameter and rotates that point around the seam through one third
|
||||
of the material-side corner angle. For a closed square outer contour, this
|
||||
produces a move of 20% of the nozzle diameter at 30 degrees into the corner.
|
||||
Coincident samples or degenerate angles suppress the move.
|
||||
|
||||
The nozzle position stored by G-code generation must match the emitted loop
|
||||
move. Both travel planning and wipe execution depend on this position, including
|
||||
when Wipe inward is disabled.
|
||||
|
||||
With a seam gap, a loop move may advance past the inward offset's original entry.
|
||||
If that alone makes the connector backtrack, the entry advances to the nozzle's
|
||||
projection on the offset. The planner extends the source as needed to preserve
|
||||
the configured wipe length and validates the new connector and complete path.
|
||||
Joins that already backtrack across the seam gap are not adjusted this way.
|
||||
|
||||
## Execution and retraction
|
||||
|
||||
The stored wipe path uses a sentinel first point. Execution starts from the
|
||||
actual nozzle position and proceeds to the second stored point. Path selection,
|
||||
support validation and wipe-length calculation must all use this same executable
|
||||
geometry, especially after a Wipe on loop move.
|
||||
|
||||
An accepted inward path executes at the end of the external loop, after any
|
||||
Wipe on loop move, without retracting filament. It consumes the stored path and
|
||||
updates the nozzle position before travel planning. A short travel to the next
|
||||
wall cannot discard this wipe or force a retraction or Z-hop. Subsequent travel
|
||||
uses the normal minimum-travel threshold and retraction/lift settings from the
|
||||
new position. The regular wipe, including fallback, remains deferred until a
|
||||
normal retraction uses it.
|
||||
|
||||
Retraction is divided into portions before, during and after wiping. The amount
|
||||
that can be retracted during the wipe depends on its executable length, wipe
|
||||
speed and the active filament's retraction speed. Fractional retraction speeds
|
||||
are retained in this calculation. For a 2 mm wipe at 100 mm/s and a retraction
|
||||
speed of 25.5 mm/s, the wipe can retract 0.51 mm. With a total retraction of 0.8 mm
|
||||
and both before/after percentages set to zero, the remaining 0.29 mm is retracted
|
||||
before wiping. This split applies to regular deferred wipes, including fallback;
|
||||
an accepted inward wipe executes separately without retraction.
|
||||
|
||||
## Implementation and verification
|
||||
|
||||
- [GCode.cpp](../../src/libslic3r/GCode.cpp) integrates path selection, nozzle
|
||||
position and retraction; [Print.cpp](../../src/libslic3r/Print.cpp) controls
|
||||
invalidation, and [PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines
|
||||
the settings.
|
||||
- [WipePathHelpers](../../src/libslic3r/GCode/WipePathHelpers.hpp) implements path
|
||||
sampling, offset selection and support checks.
|
||||
- [Geometry tests](../../tests/libslic3r/test_wipe_path.cpp) cover support,
|
||||
degenerate paths, contour and hole orientations, and exact loop-move geometry
|
||||
across path subdivisions.
|
||||
- [FFF tests](../../tests/fff_print/test_wipe.cpp) cover emitted trajectories,
|
||||
fallback, minimum-travel retraction and Z-hop rules, and export invalidation.
|
||||
With Wipe inward disabled, they check the loop move's direction and magnitude
|
||||
for Classic and Arachne, the subsequent wipe's start and length, and fractional
|
||||
retraction splitting in absolute and relative E modes.
|
||||
Loop-move checks use reserved role/wipe markers and extrusion state, and run
|
||||
with human-readable G-code comments both enabled and disabled.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Move `wxInspectable` into `DPIAware` — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Move `wxInspector::wxInspectable` from individual leaf classes into the common `DPIAware<P>` template so every DPIAware widget is automatically inspectable and gets the inspector keyboard shortcut.
|
||||
|
||||
**Architecture:** `DPIAware<P>` gains `wxInspector::wxInspectable` as a second base class and calls `SetupInspectorAccelerator(this)` in its constructor. `DPIDialog` and `MainFrame` drop their now-redundant `wxInspectable` inheritance and `SetupInspectorAccelerator` calls.
|
||||
|
||||
**Tech Stack:** C++17, wxWidgets, wxInspector
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Build with `D:\VisualStudio\2026\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe`
|
||||
- Use `--config RelWithDebInfo` for all builds
|
||||
- Cross-platform: must compile on Windows, macOS, and Linux
|
||||
- Match existing code style: PascalCase classes, `#pragma once`
|
||||
- Do NOT commit files under `.superpowers/`
|
||||
- Do NOT commit `task.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Move `wxInspectable` and `SetupInspectorAccelerator` into `DPIAware<P>`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/slic3r/GUI/GUI_Utils.hpp:92` (DPIAware template — add wxInspectable base + SetupInspectorAccelerator call)
|
||||
- Modify: `src/slic3r/GUI/GUI_Utils.hpp:276` (DPIDialog — drop wxInspectable + SetupInspectorAccelerator)
|
||||
- Modify: `src/slic3r/GUI/MainFrame.hpp:96` (MainFrame — drop wxInspectable)
|
||||
- Modify: `src/slic3r/GUI/MainFrame.cpp:304` (MainFrame constructor — drop SetupInspectorAccelerator)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Nothing (standalone refactor)
|
||||
- Produces: All DPIAware widgets automatically inherit `wxInspector::wxInspectable` and get Ctrl+Shift+I accelerator
|
||||
|
||||
- [ ] **Step 1: Add `wxInspectable` to `DPIAware<P>` and call `SetupInspectorAccelerator`**
|
||||
|
||||
In `src/slic3r/GUI/GUI_Utils.hpp`, line 92, change the base class:
|
||||
|
||||
```cpp
|
||||
// Before:
|
||||
template<class P> class DPIAware : public P
|
||||
// After:
|
||||
template<class P> class DPIAware : public P, public wxInspector::wxInspectable
|
||||
```
|
||||
|
||||
In the constructor body of `DPIAware<P>`, after `this->CenterOnParent();` (currently line 110), add:
|
||||
|
||||
```cpp
|
||||
SetupInspectorAccelerator(this);
|
||||
```
|
||||
|
||||
(`<wx/inspector/inspector.h>` is already included at line 23.)
|
||||
|
||||
- [ ] **Step 2: Remove redundant `wxInspectable` and `SetupInspectorAccelerator` from `DPIDialog`**
|
||||
|
||||
In `src/slic3r/GUI/GUI_Utils.hpp`, line 276, change:
|
||||
|
||||
```cpp
|
||||
// Before:
|
||||
class DPIDialog : public DPIAware<wxDialog>, public wxInspector::wxInspectable
|
||||
// After:
|
||||
class DPIDialog : public DPIAware<wxDialog>
|
||||
```
|
||||
|
||||
In the `DPIDialog` constructor body, remove the `SetupInspectorAccelerator(this);` line (currently line 286). The rest of the constructor stays.
|
||||
|
||||
- [ ] **Step 3: Remove redundant `wxInspectable` from `MainFrame`**
|
||||
|
||||
In `src/slic3r/GUI/MainFrame.hpp`, line 96, change:
|
||||
|
||||
```cpp
|
||||
// Before:
|
||||
class MainFrame : public DPIFrame, public wxInspector::wxInspectable
|
||||
// After:
|
||||
class MainFrame : public DPIFrame
|
||||
```
|
||||
|
||||
`MainFrame` now gets `wxInspectable` through `DPIFrame` → `DPIAware<wxFrame>`.
|
||||
|
||||
- [ ] **Step 4: Remove redundant `SetupInspectorAccelerator` from `MainFrame` constructor**
|
||||
|
||||
In `src/slic3r/GUI/MainFrame.cpp`, line 304, remove the line:
|
||||
|
||||
```cpp
|
||||
SetupInspectorAccelerator(this);
|
||||
```
|
||||
|
||||
It is now called automatically by the `DPIAware<wxFrame>` constructor.
|
||||
|
||||
- [ ] **Step 5: Build to verify compilation**
|
||||
|
||||
```powershell
|
||||
$cmakePath = "D:\VisualStudio\2026\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
|
||||
& $cmakePath --build . --config RelWithDebInfo --target ALL_BUILD -- -m
|
||||
```
|
||||
|
||||
Expected: Build succeeds with zero new errors or warnings.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/GUI/GUI_Utils.hpp src/slic3r/GUI/MainFrame.hpp src/slic3r/GUI/MainFrame.cpp
|
||||
git commit -m "refactor: move wxInspectable and SetupInspectorAccelerator into DPIAware
|
||||
|
||||
DPIAware<P> now inherits wxInspector::wxInspectable and calls
|
||||
SetupInspectorAccelerator in its constructor, making all DPIAware
|
||||
widgets automatically appear in the inspector tree with the
|
||||
Ctrl+Shift+I shortcut. Remove redundant wxInspectable inheritance
|
||||
and SetupInspectorAccelerator calls from DPIDialog and MainFrame.
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
@@ -0,0 +1,753 @@
|
||||
# wxInspector Plugins for OrcaSlicer — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build two wxInspector plugins (DPIAware + CustomWidgets) that expose OrcaSlicer custom control properties in the inspector's property grid.
|
||||
|
||||
**Architecture:** Two plugins in a shared folder under `src/slic3r/Utils/wxInspectorPlugins/`. DPIAwarePlugin uses `dynamic_cast<DPIFrame*>/<DPIDialog*>` for detection; CustomWidgetsPlugin uses per-type `dynamic_cast`. Both registered as static singletons via a single inline function in `Registration.hpp`, called from `MainFrame` constructor.
|
||||
|
||||
**Tech Stack:** C++17, wxWidgets, wxInspector plugin API (`wx/inspector/plugin.h`, `wx/inspector/inspector.h`), OrcaSlicer custom widget headers
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Plugins placed under `src/slic3r/Utils/wxInspectorPlugins/`
|
||||
- Build with `D:\VisualStudio\2026\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe`
|
||||
- Minimal source changes: only trivial (one-line) getters/setters added to existing classes
|
||||
- Cross-platform: must compile on Windows, macOS, and Linux
|
||||
- Match existing code style: PascalCase classes, snake_case functions, `#pragma once`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add getters/setters to existing Orca widget headers
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/slic3r/GUI/GUI_Utils.hpp` (DPIAware template — add 4 methods)
|
||||
- Modify: `src/slic3r/GUI/Widgets/Button.hpp` (add 3 getters)
|
||||
- Modify: `src/slic3r/GUI/Widgets/CheckBox.hpp` (add 1 getter)
|
||||
- Modify: `src/slic3r/GUI/Widgets/TextInput.hpp` (add 1 getter)
|
||||
- Modify: `src/slic3r/GUI/Widgets/LabeledStaticBox.hpp` (add 4 getter declarations)
|
||||
- Modify: `src/slic3r/GUI/Widgets/LabeledStaticBox.cpp` (add 4 getter implementations)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Nothing (prerequisite for all other tasks)
|
||||
- Produces:
|
||||
- `DPIAware<P>::set_scale_factor(float)`, `DPIAware<P>::set_prev_scale_factor(float)`, `DPIAware<P>::set_em_unit(int)`, `DPIAware<P>::force_rescale() const`
|
||||
- `Button::GetStyle()`, `Button::GetType()`, `Button::IsSelected()`
|
||||
- `CheckBox::IsHalfChecked()`
|
||||
- `TextInput::GetCornerRadius()`
|
||||
- `LabeledStaticBox::GetCornerRadius()`, `LabeledStaticBox::GetBorderWidth()`, `LabeledStaticBox::GetBorderColor()`, `LabeledStaticBox::GetScale()`
|
||||
|
||||
- [ ] **Step 1: Add DPIAware setters/getter in GUI_Utils.hpp**
|
||||
|
||||
After line 184 (`float prev_scale_factor() const { return m_prev_scale_factor; }`), add:
|
||||
|
||||
```cpp
|
||||
void set_scale_factor(float v) { m_scale_factor = v; }
|
||||
void set_prev_scale_factor(float v) { m_prev_scale_factor = v; }
|
||||
void set_em_unit(int v) { m_em_unit = v; }
|
||||
bool force_rescale() const { return m_force_rescale; }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add Button getters in Button.hpp**
|
||||
|
||||
After line 79 (`void SetSelected(bool selected = true) { m_selected = selected; }`), add:
|
||||
|
||||
```cpp
|
||||
ButtonStyle GetStyle() const { return m_style; }
|
||||
ButtonType GetType() const { return m_type; }
|
||||
bool IsSelected() const { return m_selected; }
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add CheckBox getter in CheckBox.hpp**
|
||||
|
||||
After line 16 (`void SetHalfChecked(bool value = true);`), add:
|
||||
|
||||
```cpp
|
||||
bool IsHalfChecked() const { return m_half_checked; }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add TextInput getter in TextInput.hpp**
|
||||
|
||||
After line 44 (`void SetCornerRadius(double radius);`), add:
|
||||
|
||||
```cpp
|
||||
int GetCornerRadius() const { return static_cast<int>(radius); }
|
||||
```
|
||||
|
||||
(Note: `radius` is inherited from `StaticBox` which has it as a protected `double` member.)
|
||||
|
||||
- [ ] **Step 5: Add LabeledStaticBox getter declarations in LabeledStaticBox.hpp**
|
||||
|
||||
After line 46 (`bool Enable(bool enable) override;`), add:
|
||||
|
||||
```cpp
|
||||
int GetCornerRadius() const { return m_radius; }
|
||||
int GetBorderWidth() const { return m_border_width; }
|
||||
StateColor GetBorderColor() const { return border_color; }
|
||||
float GetScale() const { return m_scale; }
|
||||
```
|
||||
|
||||
(Note: all of `m_radius`, `m_border_width`, `border_color`, `m_scale` are protected members, accessible to inline methods.)
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/GUI/GUI_Utils.hpp src/slic3r/GUI/Widgets/Button.hpp src/slic3r/GUI/Widgets/CheckBox.hpp src/slic3r/GUI/Widgets/TextInput.hpp src/slic3r/GUI/Widgets/LabeledStaticBox.hpp
|
||||
git commit -m "feat: add getters/setters for wxInspector plugin access
|
||||
|
||||
Add minimal public accessors to DPIAware (set_scale_factor,
|
||||
set_prev_scale_factor, set_em_unit, force_rescale), Button
|
||||
(GetStyle, GetType, IsSelected), CheckBox (IsHalfChecked),
|
||||
TextInput (GetCornerRadius), and LabeledStaticBox
|
||||
(GetCornerRadius, GetBorderWidth, GetBorderColor, GetScale)."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create Registration helper header
|
||||
|
||||
**Files:**
|
||||
- Create: `src/slic3r/Utils/wxInspectorPlugins/Registration.hpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Nothing (forward-declares plugin classes)
|
||||
- Produces: `RegisterOrcaInspectorPlugins()`
|
||||
|
||||
- [ ] **Step 1: Create directory**
|
||||
|
||||
```bash
|
||||
mkdir -p src/slic3r/Utils/wxInspectorPlugins
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write Registration.hpp**
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
namespace wxInspector {
|
||||
class wxInspectorPlugin;
|
||||
void RegisterPlugin(wxInspectorPlugin* plugin);
|
||||
}
|
||||
|
||||
// Forward declare our plugins
|
||||
class DPIAwarePlugin;
|
||||
class CustomWidgetsPlugin;
|
||||
|
||||
inline void RegisterOrcaInspectorPlugins()
|
||||
{
|
||||
static DPIAwarePlugin dpiaware;
|
||||
static CustomWidgetsPlugin customWidgets;
|
||||
wxInspector::RegisterPlugin(&dpiaware);
|
||||
wxInspector::RegisterPlugin(&customWidgets);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/Utils/wxInspectorPlugins/Registration.hpp
|
||||
git commit -m "feat: add wxInspector plugin registration helper
|
||||
|
||||
Add RegisterOrcaInspectorPlugins() inline function that creates
|
||||
and registers the DPIAwarePlugin and CustomWidgetsPlugin as
|
||||
static instances (matching wxInspector's built-in pattern)."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create DPIAwarePlugin
|
||||
|
||||
**Files:**
|
||||
- Create: `src/slic3r/Utils/wxInspectorPlugins/DPIAwarePlugin.hpp`
|
||||
- Create: `src/slic3r/Utils/wxInspectorPlugins/DPIAwarePlugin.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 (DPIAware getters/setters), Task 2 (registration pattern)
|
||||
- Produces: `class DPIAwarePlugin : public wxInspector::wxInspectorPlugin`
|
||||
|
||||
- [ ] **Step 1: Write DPIAwarePlugin.hpp**
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include <wx/inspector/plugin.h>
|
||||
|
||||
class DPIAwarePlugin : public wxInspector::wxInspectorPlugin
|
||||
{
|
||||
public:
|
||||
wxString GetName() const override;
|
||||
|
||||
bool CanProvideProperties(wxClassInfo* info) override;
|
||||
|
||||
wxVector<wxInspector::PropertyDef> GetProperties(
|
||||
wxInspector::InspectableObject& obj) override;
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write DPIAwarePlugin.cpp**
|
||||
|
||||
```cpp
|
||||
#include "DPIAwarePlugin.hpp"
|
||||
|
||||
#include "slic3r/GUI/GUI_Utils.hpp" // DPIFrame, DPIDialog, DPIAware<P>
|
||||
|
||||
#include <wx/window.h>
|
||||
|
||||
namespace {
|
||||
|
||||
template<typename T>
|
||||
void addDPIProps(T* dpi, wxVector<wxInspector::PropertyDef>& props)
|
||||
{
|
||||
using namespace wxInspector;
|
||||
|
||||
props.push_back({"Scale Factor", "DPI Scaling", PropertyType::String,
|
||||
wxString::Format("%.2f", dpi->scale_factor()), false, {},
|
||||
[dpi]() { return wxString::Format("%.2f", dpi->scale_factor()); },
|
||||
[dpi](const wxString& v) {
|
||||
double val;
|
||||
if (wxSscanf(v, "%lf", &val) != 1) return false;
|
||||
dpi->set_scale_factor((float) val);
|
||||
return true;
|
||||
}});
|
||||
|
||||
props.push_back({"Prev Scale Factor", "DPI Scaling", PropertyType::String,
|
||||
wxString::Format("%.2f", dpi->prev_scale_factor()), false, {},
|
||||
[dpi]() { return wxString::Format("%.2f", dpi->prev_scale_factor()); },
|
||||
[dpi](const wxString& v) {
|
||||
double val;
|
||||
if (wxSscanf(v, "%lf", &val) != 1) return false;
|
||||
dpi->set_prev_scale_factor((float) val);
|
||||
return true;
|
||||
}});
|
||||
|
||||
props.push_back({"EM Unit", "DPI Scaling", PropertyType::Integer,
|
||||
wxString::Format("%d", dpi->em_unit()), false, {},
|
||||
[dpi]() { return wxString::Format("%d", dpi->em_unit()); },
|
||||
[dpi](const wxString& v) {
|
||||
long val;
|
||||
if (!v.ToLong(&val)) return false;
|
||||
dpi->set_em_unit((int) val);
|
||||
return true;
|
||||
}});
|
||||
|
||||
props.push_back({"Normal Font", "DPI Scaling", PropertyType::ReadOnly,
|
||||
dpi->normal_font().GetNativeFontInfoDesc(), true, {},
|
||||
[dpi]() { return dpi->normal_font().GetNativeFontInfoDesc(); },
|
||||
nullptr});
|
||||
|
||||
props.push_back({"Force Rescale", "DPI Scaling", PropertyType::Boolean,
|
||||
dpi->force_rescale() ? "true" : "false", true, {},
|
||||
[dpi]() { return dpi->force_rescale() ? "true" : "false"; },
|
||||
nullptr});
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
wxString DPIAwarePlugin::GetName() const
|
||||
{
|
||||
return "OrcaDPIAware";
|
||||
}
|
||||
|
||||
bool DPIAwarePlugin::CanProvideProperties(wxClassInfo* info)
|
||||
{
|
||||
return info->IsKindOf(CLASSINFO(wxWindow));
|
||||
}
|
||||
|
||||
wxVector<wxInspector::PropertyDef> DPIAwarePlugin::GetProperties(
|
||||
wxInspector::InspectableObject& obj)
|
||||
{
|
||||
wxVector<wxInspector::PropertyDef> props;
|
||||
wxWindow* win = obj.AsWindow();
|
||||
if (!win) return props;
|
||||
|
||||
if (auto* frame = dynamic_cast<DPIFrame*>(win)) {
|
||||
addDPIProps(frame, props);
|
||||
} else if (auto* dlg = dynamic_cast<DPIDialog*>(win)) {
|
||||
addDPIProps(dlg, props);
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/Utils/wxInspectorPlugins/DPIAwarePlugin.hpp src/slic3r/Utils/wxInspectorPlugins/DPIAwarePlugin.cpp
|
||||
git commit -m "feat: add DPIAware wxInspector plugin
|
||||
|
||||
Exposes DPI scaling properties (scale_factor, prev_scale_factor,
|
||||
em_unit, normal_font, force_rescale) on DPIFrame and DPIDialog
|
||||
widgets. Uses dynamic_cast for detection and a template helper
|
||||
to capture the correct static type for lambda accessors."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Create CustomWidgetsPlugin
|
||||
|
||||
**Files:**
|
||||
- Create: `src/slic3r/Utils/wxInspectorPlugins/CustomWidgetsPlugin.hpp`
|
||||
- Create: `src/slic3r/Utils/wxInspectorPlugins/CustomWidgetsPlugin.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 (all widget getters), Task 2 (registration pattern)
|
||||
- Produces: `class CustomWidgetsPlugin : public wxInspector::wxInspectorPlugin`
|
||||
|
||||
- [ ] **Step 1: Write CustomWidgetsPlugin.hpp**
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include <wx/inspector/plugin.h>
|
||||
|
||||
class CustomWidgetsPlugin : public wxInspector::wxInspectorPlugin
|
||||
{
|
||||
public:
|
||||
wxString GetName() const override;
|
||||
|
||||
bool CanProvideProperties(wxClassInfo* info) override;
|
||||
|
||||
wxVector<wxInspector::PropertyDef> GetProperties(
|
||||
wxInspector::InspectableObject& obj) override;
|
||||
|
||||
private:
|
||||
void addButtonProps(class Button* btn,
|
||||
wxVector<wxInspector::PropertyDef>& props);
|
||||
void addCheckBoxProps(class CheckBox* cb,
|
||||
wxVector<wxInspector::PropertyDef>& props);
|
||||
void addTextInputProps(class TextInput* ti,
|
||||
wxVector<wxInspector::PropertyDef>& props);
|
||||
void addSwitchButtonProps(class SwitchButton* sb,
|
||||
wxVector<wxInspector::PropertyDef>& props);
|
||||
void addProgressBarProps(class ProgressBar* pb,
|
||||
wxVector<wxInspector::PropertyDef>& props);
|
||||
void addLabelProps(class Label* lbl,
|
||||
wxVector<wxInspector::PropertyDef>& props);
|
||||
void addLabeledStaticBoxProps(class LabeledStaticBox* lsb,
|
||||
wxVector<wxInspector::PropertyDef>& props);
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write CustomWidgetsPlugin.cpp — includes and GetName/CanProvideProperties**
|
||||
|
||||
```cpp
|
||||
#include "CustomWidgetsPlugin.hpp"
|
||||
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/Widgets/CheckBox.hpp"
|
||||
#include "slic3r/GUI/Widgets/TextInput.hpp"
|
||||
#include "slic3r/GUI/Widgets/SwitchButton.hpp"
|
||||
#include "slic3r/GUI/Widgets/ProgressBar.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include "slic3r/GUI/Widgets/LabeledStaticBox.hpp"
|
||||
|
||||
#include <wx/window.h>
|
||||
#include <wx/tglbtn.h>
|
||||
|
||||
wxString CustomWidgetsPlugin::GetName() const
|
||||
{
|
||||
return "OrcaCustomWidgets";
|
||||
}
|
||||
|
||||
bool CustomWidgetsPlugin::CanProvideProperties(wxClassInfo* info)
|
||||
{
|
||||
return info->IsKindOf(CLASSINFO(wxWindow));
|
||||
}
|
||||
|
||||
wxVector<wxInspector::PropertyDef> CustomWidgetsPlugin::GetProperties(
|
||||
wxInspector::InspectableObject& obj)
|
||||
{
|
||||
wxVector<wxInspector::PropertyDef> props;
|
||||
wxWindow* win = obj.AsWindow();
|
||||
if (!win) return props;
|
||||
|
||||
if (auto* btn = dynamic_cast<Button*>(win))
|
||||
addButtonProps(btn, props);
|
||||
if (auto* cb = dynamic_cast<CheckBox*>(win))
|
||||
addCheckBoxProps(cb, props);
|
||||
if (auto* ti = dynamic_cast<TextInput*>(win))
|
||||
addTextInputProps(ti, props);
|
||||
if (auto* sb = dynamic_cast<SwitchButton*>(win))
|
||||
addSwitchButtonProps(sb, props);
|
||||
if (auto* pb = dynamic_cast<ProgressBar*>(win))
|
||||
addProgressBarProps(pb, props);
|
||||
if (auto* lbl = dynamic_cast<Label*>(win))
|
||||
addLabelProps(lbl, props);
|
||||
if (auto* lsb = dynamic_cast<LabeledStaticBox*>(win))
|
||||
addLabeledStaticBoxProps(lsb, props);
|
||||
|
||||
return props;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write CustomWidgetsPlugin.cpp — addButtonProps**
|
||||
|
||||
```cpp
|
||||
void CustomWidgetsPlugin::addButtonProps(Button* btn,
|
||||
wxVector<wxInspector::PropertyDef>& props)
|
||||
{
|
||||
using namespace wxInspector;
|
||||
|
||||
wxVector<wxString> styleChoices;
|
||||
styleChoices.push_back("Regular");
|
||||
styleChoices.push_back("Confirm");
|
||||
styleChoices.push_back("Alert");
|
||||
styleChoices.push_back("Disabled");
|
||||
|
||||
auto styleToStr = [](ButtonStyle s) -> wxString {
|
||||
switch (s) {
|
||||
case ButtonStyle::Regular: return "Regular";
|
||||
case ButtonStyle::Confirm: return "Confirm";
|
||||
case ButtonStyle::Alert: return "Alert";
|
||||
case ButtonStyle::Disabled: return "Disabled";
|
||||
}
|
||||
return "Regular";
|
||||
};
|
||||
|
||||
props.push_back({"Button Style", "Orca Button", PropertyType::Choice,
|
||||
styleToStr(btn->GetStyle()), false, styleChoices,
|
||||
[btn, styleToStr]() { return styleToStr(btn->GetStyle()); },
|
||||
[btn](const wxString& v) {
|
||||
ButtonStyle s = ButtonStyle::Regular;
|
||||
if (v == "Confirm") s = ButtonStyle::Confirm;
|
||||
else if (v == "Alert") s = ButtonStyle::Alert;
|
||||
else if (v == "Disabled") s = ButtonStyle::Disabled;
|
||||
btn->SetStyle(s, btn->GetType());
|
||||
return true;
|
||||
}});
|
||||
|
||||
wxVector<wxString> typeChoices;
|
||||
typeChoices.push_back("Compact");
|
||||
typeChoices.push_back("Window");
|
||||
typeChoices.push_back("Choice");
|
||||
typeChoices.push_back("Parameter");
|
||||
typeChoices.push_back("Icon");
|
||||
typeChoices.push_back("Expanded");
|
||||
|
||||
auto typeToStr = [](ButtonType t) -> wxString {
|
||||
switch (t) {
|
||||
case ButtonType::Compact: return "Compact";
|
||||
case ButtonType::Window: return "Window";
|
||||
case ButtonType::Choice: return "Choice";
|
||||
case ButtonType::Parameter: return "Parameter";
|
||||
case ButtonType::Icon: return "Icon";
|
||||
case ButtonType::Expanded: return "Expanded";
|
||||
}
|
||||
return "Compact";
|
||||
};
|
||||
|
||||
props.push_back({"Button Type", "Orca Button", PropertyType::Choice,
|
||||
typeToStr(btn->GetType()), false, typeChoices,
|
||||
[btn, typeToStr]() { return typeToStr(btn->GetType()); },
|
||||
[btn](const wxString& v) {
|
||||
ButtonType t = ButtonType::Compact;
|
||||
if (v == "Window") t = ButtonType::Window;
|
||||
else if (v == "Choice") t = ButtonType::Choice;
|
||||
else if (v == "Parameter") t = ButtonType::Parameter;
|
||||
else if (v == "Icon") t = ButtonType::Icon;
|
||||
else if (v == "Expanded") t = ButtonType::Expanded;
|
||||
btn->SetStyle(btn->GetStyle(), t);
|
||||
return true;
|
||||
}});
|
||||
|
||||
props.push_back({"Selected", "Orca Button", PropertyType::Boolean,
|
||||
btn->IsSelected() ? "true" : "false", false, {},
|
||||
[btn]() { return btn->IsSelected() ? "true" : "false"; },
|
||||
[btn](const wxString& v) {
|
||||
btn->SetSelected(v == "true");
|
||||
btn->Refresh();
|
||||
return true;
|
||||
}});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write CustomWidgetsPlugin.cpp — addCheckBoxProps**
|
||||
|
||||
```cpp
|
||||
void CustomWidgetsPlugin::addCheckBoxProps(CheckBox* cb,
|
||||
wxVector<wxInspector::PropertyDef>& props)
|
||||
{
|
||||
using namespace wxInspector;
|
||||
|
||||
props.push_back({"Half Checked", "Orca CheckBox", PropertyType::Boolean,
|
||||
cb->IsHalfChecked() ? "true" : "false", false, {},
|
||||
[cb]() { return cb->IsHalfChecked() ? "true" : "false"; },
|
||||
[cb](const wxString& v) {
|
||||
cb->SetHalfChecked(v == "true");
|
||||
return true;
|
||||
}});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Write CustomWidgetsPlugin.cpp — addTextInputProps**
|
||||
|
||||
```cpp
|
||||
void CustomWidgetsPlugin::addTextInputProps(TextInput* ti,
|
||||
wxVector<wxInspector::PropertyDef>& props)
|
||||
{
|
||||
using namespace wxInspector;
|
||||
|
||||
props.push_back({"Label", "Orca TextInput", PropertyType::String,
|
||||
ti->GetLabel(), false, {},
|
||||
[ti]() { return ti->GetLabel(); },
|
||||
[ti](const wxString& v) { ti->SetLabel(v); return true; }});
|
||||
|
||||
props.push_back({"Text Value", "Orca TextInput", PropertyType::String,
|
||||
ti->GetTextCtrl()->GetValue(), false, {},
|
||||
[ti]() { return ti->GetTextCtrl()->GetValue(); },
|
||||
[ti](const wxString& v) { ti->GetTextCtrl()->SetValue(v); return true; }});
|
||||
|
||||
props.push_back({"Corner Radius", "Orca TextInput", PropertyType::Integer,
|
||||
wxString::Format("%d", ti->GetCornerRadius()), false, {},
|
||||
[ti]() { return wxString::Format("%d", ti->GetCornerRadius()); },
|
||||
[ti](const wxString& v) {
|
||||
long val;
|
||||
if (!v.ToLong(&val)) return false;
|
||||
ti->SetCornerRadius((double) val);
|
||||
ti->Refresh();
|
||||
return true;
|
||||
}});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Write CustomWidgetsPlugin.cpp — addSwitchButtonProps**
|
||||
|
||||
```cpp
|
||||
void CustomWidgetsPlugin::addSwitchButtonProps(SwitchButton* sb,
|
||||
wxVector<wxInspector::PropertyDef>& props)
|
||||
{
|
||||
using namespace wxInspector;
|
||||
|
||||
props.push_back({"Value", "Orca SwitchButton", PropertyType::Boolean,
|
||||
sb->GetValue() ? "true" : "false", false, {},
|
||||
[sb]() { return sb->GetValue() ? "true" : "false"; },
|
||||
[sb](const wxString& v) {
|
||||
sb->SetValue(v == "true");
|
||||
return true;
|
||||
}});
|
||||
}
|
||||
```
|
||||
|
||||
(Note: `GetValue()` and `SetValue()` are inherited from `wxBitmapToggleButton` → `wxToggleButton`.)
|
||||
|
||||
- [ ] **Step 7: Write CustomWidgetsPlugin.cpp — addProgressBarProps**
|
||||
|
||||
```cpp
|
||||
void CustomWidgetsPlugin::addProgressBarProps(ProgressBar* pb,
|
||||
wxVector<wxInspector::PropertyDef>& props)
|
||||
{
|
||||
using namespace wxInspector;
|
||||
|
||||
props.push_back({"Proportion", "Orca ProgressBar", PropertyType::String,
|
||||
wxString::Format("%.2f", pb->m_proportion), false, {},
|
||||
[pb]() { return wxString::Format("%.2f", pb->m_proportion); },
|
||||
[pb](const wxString& v) {
|
||||
double val;
|
||||
if (wxSscanf(v, "%lf", &val) != 1) return false;
|
||||
pb->m_proportion = val;
|
||||
pb->Refresh();
|
||||
return true;
|
||||
}});
|
||||
|
||||
props.push_back({"Show Number", "Orca ProgressBar", PropertyType::Boolean,
|
||||
pb->m_shownumber ? "true" : "false", false, {},
|
||||
[pb]() { return pb->m_shownumber ? "true" : "false"; },
|
||||
[pb](const wxString& v) {
|
||||
pb->m_shownumber = (v == "true");
|
||||
pb->Refresh();
|
||||
return true;
|
||||
}});
|
||||
}
|
||||
```
|
||||
|
||||
(Note: `m_proportion` and `m_shownumber` are public members on `ProgressBar`.)
|
||||
|
||||
- [ ] **Step 8: Write CustomWidgetsPlugin.cpp — addLabelProps**
|
||||
|
||||
```cpp
|
||||
void CustomWidgetsPlugin::addLabelProps(Label* lbl,
|
||||
wxVector<wxInspector::PropertyDef>& props)
|
||||
{
|
||||
using namespace wxInspector;
|
||||
|
||||
bool isHyperlink = (lbl->GetWindowStyleFlag() & 0x0020) != 0; // LB_HYPERLINK
|
||||
|
||||
props.push_back({"Is Hyperlink", "Orca Label", PropertyType::Boolean,
|
||||
isHyperlink ? "true" : "false", true, {},
|
||||
[lbl]() {
|
||||
return (lbl->GetWindowStyleFlag() & 0x0020) ? "true" : "false";
|
||||
},
|
||||
nullptr});
|
||||
|
||||
props.push_back({"Font Point Size", "Orca Label", PropertyType::ReadOnly,
|
||||
wxString::Format("%d", lbl->GetFont().GetPointSize()), true, {},
|
||||
[lbl]() {
|
||||
return wxString::Format("%d", lbl->GetFont().GetPointSize());
|
||||
},
|
||||
nullptr});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Write CustomWidgetsPlugin.cpp — addLabeledStaticBoxProps**
|
||||
|
||||
```cpp
|
||||
void CustomWidgetsPlugin::addLabeledStaticBoxProps(LabeledStaticBox* lsb,
|
||||
wxVector<wxInspector::PropertyDef>& props)
|
||||
{
|
||||
using namespace wxInspector;
|
||||
|
||||
props.push_back({"Corner Radius", "LabeledStaticBox", PropertyType::Integer,
|
||||
wxString::Format("%d", lsb->GetCornerRadius()), false, {},
|
||||
[lsb]() { return wxString::Format("%d", lsb->GetCornerRadius()); },
|
||||
[lsb](const wxString& v) {
|
||||
long val;
|
||||
if (!v.ToLong(&val)) return false;
|
||||
lsb->SetCornerRadius((int) val);
|
||||
return true;
|
||||
}});
|
||||
|
||||
props.push_back({"Border Width", "LabeledStaticBox", PropertyType::Integer,
|
||||
wxString::Format("%d", lsb->GetBorderWidth()), false, {},
|
||||
[lsb]() { return wxString::Format("%d", lsb->GetBorderWidth()); },
|
||||
[lsb](const wxString& v) {
|
||||
long val;
|
||||
if (!v.ToLong(&val)) return false;
|
||||
lsb->SetBorderWidth((int) val);
|
||||
return true;
|
||||
}});
|
||||
|
||||
// Border Color: display as hex string
|
||||
wxColour bc = lsb->GetBorderColor().colorForStates(0);
|
||||
props.push_back({"Border Color", "LabeledStaticBox", PropertyType::String,
|
||||
bc.GetAsString(wxC2S_HTML_SYNTAX), false, {},
|
||||
[lsb]() {
|
||||
return lsb->GetBorderColor()
|
||||
.colorForStates(0)
|
||||
.GetAsString(wxC2S_HTML_SYNTAX);
|
||||
},
|
||||
[lsb](const wxString& v) {
|
||||
wxColour c(v);
|
||||
if (!c.IsOk()) return false;
|
||||
lsb->SetBorderColor(StateColor(c));
|
||||
return true;
|
||||
}});
|
||||
|
||||
props.push_back({"Scale", "LabeledStaticBox", PropertyType::ReadOnly,
|
||||
wxString::Format("%.2f", lsb->GetScale()), true, {},
|
||||
[lsb]() { return wxString::Format("%.2f", lsb->GetScale()); },
|
||||
nullptr});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 10: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/Utils/wxInspectorPlugins/CustomWidgetsPlugin.hpp src/slic3r/Utils/wxInspectorPlugins/CustomWidgetsPlugin.cpp
|
||||
git commit -m "feat: add OrcaCustomWidgets wxInspector plugin
|
||||
|
||||
Exposes Orca-specific properties on 7 widget types:
|
||||
- Button: Style, Type, Selected
|
||||
- CheckBox: Half Checked
|
||||
- TextInput: Label, Text Value, Corner Radius
|
||||
- SwitchButton: Value
|
||||
- ProgressBar: Proportion, Show Number
|
||||
- Label: Is Hyperlink, Font Point Size
|
||||
- LabeledStaticBox: Corner Radius, Border Width, Border Color, Scale
|
||||
|
||||
Each widget type uses dynamic_cast for safe detection."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Wire plugins into MainFrame and CMakeLists
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/slic3r/GUI/MainFrame.cpp` (add include + registration call)
|
||||
- Modify: `src/slic3r/CMakeLists.txt` (add 4 source files)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tasks 1-4 (all plugins and registration helper)
|
||||
- Produces: Registered plugins available at runtime, buildable project
|
||||
|
||||
- [ ] **Step 1: Add include in MainFrame.cpp**
|
||||
|
||||
After the existing includes (around line 30, near the other Utils includes), add:
|
||||
|
||||
```cpp
|
||||
#include "slic3r/Utils/wxInspectorPlugins/Registration.hpp"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add registration call in MainFrame constructor**
|
||||
|
||||
After `SetupInspectorAccelerator(this);` (currently line ~303), add:
|
||||
|
||||
```cpp
|
||||
RegisterOrcaInspectorPlugins();
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add source files to CMakeLists.txt**
|
||||
|
||||
Find the `SLIC3R_GUI_SOURCES` list in `src/slic3r/CMakeLists.txt`. After the existing `Utils/*.cpp` entries (around line 650-754), add:
|
||||
|
||||
```cmake
|
||||
Utils/wxInspectorPlugins/DPIAwarePlugin.hpp
|
||||
Utils/wxInspectorPlugins/DPIAwarePlugin.cpp
|
||||
Utils/wxInspectorPlugins/CustomWidgetsPlugin.hpp
|
||||
Utils/wxInspectorPlugins/CustomWidgetsPlugin.cpp
|
||||
Utils/wxInspectorPlugins/Registration.hpp
|
||||
```
|
||||
|
||||
(Note: Add all 5 files — 2 .hpp + 2 .cpp + 1 Registration.hpp. wxWidgets cmake needs headers listed too for the resource system.)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/GUI/MainFrame.cpp src/slic3r/CMakeLists.txt
|
||||
git commit -m "feat: wire wxInspector plugins into MainFrame and build
|
||||
|
||||
- Call RegisterOrcaInspectorPlugins() after SetupInspectorAccelerator
|
||||
- Add all plugin source files to SLIC3R_GUI_SOURCES"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Build and verify
|
||||
|
||||
**Files:**
|
||||
- None modified (verification only)
|
||||
|
||||
- [ ] **Step 1: Configure the build**
|
||||
|
||||
```powershell
|
||||
$cmakePath = "D:\VisualStudio\2026\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
|
||||
& $cmakePath --build . --config Debug --target ALL_BUILD -- -m
|
||||
```
|
||||
|
||||
Expected: Build succeeds with zero errors and zero warnings from our new files.
|
||||
|
||||
- [ ] **Step 2: Fix any compilation errors**
|
||||
|
||||
If the build fails:
|
||||
- Check that `#include` paths resolve (the `slic3r/GUI/…` relative paths use `src/` as the include root — verify this is set up in CMake via `include_directories`)
|
||||
- Check that `ButtonStyle` and `ButtonType` enums are visible (they're defined in `Button.hpp`)
|
||||
- Check that `StateColor` constructor from `wxColour` is valid (it has `StateColor(wxColour const&)`)
|
||||
- Check that `LabeledStaticBox::GetBorderColor()` returns by value (StateColor copy is fine)
|
||||
- On macOS: static box margin removal call needs `#ifdef __WXOSX__` guard
|
||||
|
||||
- [ ] **Step 3: Launch OrcaSlicer and verify inspector**
|
||||
|
||||
Launch the built OrcaSlicer, press Ctrl+Shift+I to open the inspector:
|
||||
1. Select the MainFrame in the tree — verify "DPI Scaling" category appears with Scale Factor, Prev Scale Factor, EM Unit, Normal Font, Force Rescale
|
||||
2. Select an Orca Button — verify "Orca Button" category appears
|
||||
3. Select an Orca CheckBox — verify "Orca CheckBox" category appears
|
||||
4. Edit a property value (e.g., Scale Factor) — verify the setter applies correctly
|
||||
5. Select a LabeledStaticBox — verify corner radius, border width, border color, scale appear
|
||||
|
||||
- [ ] **Step 5: Commit (if fixes were needed) or mark complete**
|
||||
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
|
||||
If clean: verification complete. If changes were made: `git add` and commit with fix message.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Move `wxInspectable` into `DPIAware` — Design Spec
|
||||
|
||||
Date: 2026-07-23
|
||||
Branch: `dev/layout-inspector`
|
||||
|
||||
## Overview
|
||||
|
||||
Move the `wxInspector::wxInspectable` base class from individual leaf classes (`DPIDialog`, `MainFrame`) into the common `DPIAware<P>` template. This makes every DPIAware widget automatically visible in the inspector tree without requiring each subclass to opt in.
|
||||
|
||||
## Motivation
|
||||
|
||||
Currently, only `DPIDialog` and `MainFrame` explicitly inherit `wxInspectable`. `DPIFrame` (which `MainFrame` inherits from) does not — `MainFrame` adds it manually. This means:
|
||||
|
||||
- Any `DPIAware<T>` widget that isn't `DPIDialog` or `MainFrame` is invisible in the inspector tree
|
||||
- `DPIFrame` subclasses (`BaseTransparentDPIFrame`, `ImageDPIFrame`, `ModelMallDialog`, `MediaFileFrame`, `SecondaryCheckDialog`, `PrintErrorDialog`, etc.) don't appear
|
||||
- Adding a new DPIAware widget type requires remembering to also inherit `wxInspectable`
|
||||
|
||||
Moving `wxInspectable` to `DPIAware` fixes this for all current and future DPIAware widgets at once.
|
||||
|
||||
## Design
|
||||
|
||||
### Change 1: `GUI_Utils.hpp` — `DPIAware<P>`
|
||||
|
||||
Add `wxInspector::wxInspectable` as a second base class, and call `SetupInspectorAccelerator(this)` in the constructor (after `this->CenterOnParent()`):
|
||||
|
||||
```cpp
|
||||
// Before:
|
||||
template<class P> class DPIAware : public P
|
||||
|
||||
// After:
|
||||
template<class P> class DPIAware : public P, public wxInspector::wxInspectable
|
||||
```
|
||||
|
||||
Add in the constructor body (after `this->CenterOnParent()` at line 110):
|
||||
```cpp
|
||||
SetupInspectorAccelerator(this);
|
||||
```
|
||||
|
||||
This gives every `DPIAware<T>` widget both inspectability and the Ctrl+Shift+I keyboard shortcut automatically. `#include <wx/inspector/inspector.h>` is already present in the file.
|
||||
|
||||
### Change 2: `GUI_Utils.hpp` — `DPIDialog`
|
||||
|
||||
Remove the now-redundant `wxInspector::wxInspectable` and the `SetupInspectorAccelerator(this)` call:
|
||||
|
||||
```cpp
|
||||
// Before:
|
||||
class DPIDialog : public DPIAware<wxDialog>, public wxInspector::wxInspectable
|
||||
// ...
|
||||
SetupInspectorAccelerator(this);
|
||||
|
||||
// After:
|
||||
class DPIDialog : public DPIAware<wxDialog>
|
||||
// (SetupInspectorAccelerator call removed — now done in DPIAware constructor)
|
||||
```
|
||||
|
||||
`DPIDialog` gets `wxInspectable` and the accelerator through `DPIAware<wxDialog>` now.
|
||||
|
||||
### Change 3: `MainFrame.hpp` — `MainFrame`
|
||||
|
||||
Remove the now-redundant `wxInspector::wxInspectable`:
|
||||
|
||||
```cpp
|
||||
// Before:
|
||||
class MainFrame : public DPIFrame, public wxInspector::wxInspectable
|
||||
|
||||
// After:
|
||||
class MainFrame : public DPIFrame
|
||||
```
|
||||
|
||||
`MainFrame` gets `wxInspectable` through `DPIFrame` → `DPIAware<wxFrame>`.
|
||||
|
||||
### Change 4: `MainFrame.cpp` — `MainFrame` constructor
|
||||
|
||||
Remove the now-redundant `SetupInspectorAccelerator(this)` call (line 304). It will be called automatically by the `DPIAware` constructor.
|
||||
|
||||
## Impact
|
||||
|
||||
| Widget | Before | After |
|
||||
|--------|--------|-------|
|
||||
| `DPIDialog` subclasses (~80) | ✓ inspectable | ✓ inspectable (transitive) |
|
||||
| `MainFrame` | ✓ inspectable | ✓ inspectable (transitive) |
|
||||
| `DPIFrame` subclasses (8 others) | ✗ invisible | ✓ inspectable |
|
||||
| Future `DPIAware<T>` | ✗ invisible | ✓ inspectable |
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/slic3r/GUI/GUI_Utils.hpp` | `DPIAware<P>` gains `wxInspector::wxInspectable` + `SetupInspectorAccelerator(this)` call; `DPIDialog` drops redundant `wxInspector::wxInspectable` and `SetupInspectorAccelerator(this)` |
|
||||
| `src/slic3r/GUI/MainFrame.hpp` | `MainFrame` drops redundant `wxInspector::wxInspectable` |
|
||||
| `src/slic3r/GUI/MainFrame.cpp` | Remove redundant `SetupInspectorAccelerator(this)` from MainFrame constructor |
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- The `DPIAwarePlugin` detection logic (`dynamic_cast<DPIFrame*>` / `dynamic_cast<DPIDialog*>`) is unchanged
|
||||
- No new DPI properties — this is purely about tree visibility and accelerator setup
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Multiple inheritance**: `DPIAware<P>` already has a vtable (virtual destructor). Adding `wxInspectable` adds a second base but no additional data members. The `wxInspector::wxInspectable` class is expected to be a lightweight marker interface.
|
||||
- **Build**: No new includes needed; `<wx/inspector/inspector.h>` is already included in `GUI_Utils.hpp`.
|
||||
- **Cross-platform**: The change is standard C++ multiple inheritance — no platform-specific concerns.
|
||||
@@ -0,0 +1,244 @@
|
||||
# wxInspector Plugins for OrcaSlicer Custom Controls — Design Spec
|
||||
|
||||
Date: 2026-07-23
|
||||
Branch: `dev/layout-inspector`
|
||||
|
||||
## Overview
|
||||
|
||||
Create wxInspector plugins that expose OrcaSlicer's custom widget properties in the inspector's property grid. Without these plugins, the inspector shows only generic wxWidgets properties — missing all DPI-awareness data, custom styling, and Orca-specific control state.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **DPIAware properties** — Inspect and update `scale_factor`, `prev_scale_factor`, `em_unit`, and `normal_font` on any DPIAware-derived widget
|
||||
2. **Custom widget properties** — Surface Orca-specific properties on `Button`, `CheckBox`, `TextInput`, `SwitchButton`, `ProgressBar`, `Label`, and `LabeledStaticBox`
|
||||
3. **Minimal source changes** — Only add trivial (one-line) getters/setters to existing classes; no architectural refactoring of Orca's widget hierarchy
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Custom inspector panels or AUI tabs (use the existing property grid and method invoker)
|
||||
- Python-plugin integration (this is C++ wxInspector, not Orca's Python plugin system)
|
||||
- Event logging customization (the built-in event logger already works)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Two Plugins
|
||||
|
||||
| Plugin | Class | Files |
|
||||
|--------|-------|-------|
|
||||
| DPIAware plugin | `DPIAwarePlugin` | `DPIAwarePlugin.hpp`, `DPIAwarePlugin.cpp` |
|
||||
| Custom widgets plugin | `CustomWidgetsPlugin` | `CustomWidgetsPlugin.hpp`, `CustomWidgetsPlugin.cpp` |
|
||||
| Registration helper | inline function | `Registration.hpp` |
|
||||
|
||||
All files live under `src/slic3r/Utils/wxInspectorPlugins/`.
|
||||
|
||||
### Plugin Detection Strategy
|
||||
|
||||
**DPIAware plugin**: Uses `dynamic_cast<DPIFrame*>` and `dynamic_cast<DPIDialog*>` as detection gates. `DPIFrame` = `DPIAware<wxFrame>`, `DPIDialog` = `DPIAware<wxDialog>`. Since these are concrete typedefs, `dynamic_cast` works at runtime. This covers `MainFrame`, `SettingsDialog`, and all 8 calibration dialogs (which inherit `DPIDialog`).
|
||||
|
||||
**Custom widgets plugin**: Gates broadly on `CLASSINFO(wxWindow)`, then uses per-type `dynamic_cast` inside `GetProperties` to check each Orca-specific type. Only matching types append properties.
|
||||
|
||||
### Registration
|
||||
|
||||
A single `RegisterOrcaInspectorPlugins()` inline function in `Registration.hpp` creates both plugins as function-local statics (matching the wxInspector built-in provider pattern) and registers them via `wxInspector::RegisterPlugin()`.
|
||||
|
||||
Called once from `MainFrame::MainFrame()` after `SetupInspectorAccelerator(this)`.
|
||||
|
||||
### Why Separate Plugins?
|
||||
|
||||
- DPIAware is a C++ template concept (not a wxClassInfo-isKindOf check), so it needs its own detection logic
|
||||
- Custom widgets use standard wxClassInfo-based detection, matching the built-in provider pattern
|
||||
- Two focused files are easier to review and maintain than one monolithic plugin
|
||||
- Compile-time failure isolation: if a widget header changes, only one plugin breaks
|
||||
|
||||
## DPIAware Plugin — Property Specification
|
||||
|
||||
### Source Changes (GUI_Utils.hpp)
|
||||
|
||||
Four one-liner methods added to the `DPIAware<P>` template class (public section):
|
||||
|
||||
```cpp
|
||||
float scale_factor() const { return m_scale_factor; } // already exists
|
||||
float prev_scale_factor() const { return m_prev_scale_factor; } // already exists
|
||||
int em_unit() const { return m_em_unit; } // already exists
|
||||
void set_scale_factor(float v) { m_scale_factor = v; } // NEW
|
||||
void set_prev_scale_factor(float v) { m_prev_scale_factor = v; } // NEW
|
||||
void set_em_unit(int v) { m_em_unit = v; } // NEW
|
||||
bool force_rescale() const { return m_force_rescale; } // NEW
|
||||
// m_normal_font getter already exists: normal_font()
|
||||
```
|
||||
|
||||
### Detection
|
||||
|
||||
```cpp
|
||||
bool CanProvideProperties(wxClassInfo* info) override {
|
||||
// Gated in GetProperties via dynamic_cast on the window itself
|
||||
return info->IsKindOf(CLASSINFO(wxWindow));
|
||||
}
|
||||
```
|
||||
|
||||
In `GetProperties`:
|
||||
```cpp
|
||||
auto* win = obj.AsWindow();
|
||||
bool isDPI = dynamic_cast<DPIFrame*>(win) || dynamic_cast<DPIDialog*>(win);
|
||||
if (!isDPI) return props;
|
||||
```
|
||||
|
||||
### Property Table (category: "DPI Scaling")
|
||||
|
||||
| Name | Type | Editable | Getter | Setter |
|
||||
|------|------|----------|--------|--------|
|
||||
| Scale Factor | String (float) | Yes | `dpi->scale_factor()` | `dpi->set_scale_factor(v)` |
|
||||
| Prev Scale Factor | String (float) | Yes | `dpi->prev_scale_factor()` | `dpi->set_prev_scale_factor(v)` |
|
||||
| EM Unit | Integer | Yes | `dpi->em_unit()` | `dpi->set_em_unit(v)` |
|
||||
| Normal Font | ReadOnly | No | `dpi->normal_font().GetNativeFontInfoDesc()` | — |
|
||||
| Force Rescale | Boolean (ReadOnly) | No | `dpi->force_rescale()` | — |
|
||||
|
||||
**Note on setters**: The setters simply store values. They do NOT trigger a widget rescale/layout. To see the effect of a changed scale factor, use the inspector's Methods panel to call `Layout()` or resize the window — which triggers the DPI_CHANGED event path naturally.
|
||||
|
||||
## Custom Widgets Plugin — Property Specification
|
||||
|
||||
All properties are appended to the built-in wxWindow properties. Each widget type is independently detected via `dynamic_cast`.
|
||||
|
||||
### Detection gates (in `GetProperties`)
|
||||
|
||||
```cpp
|
||||
auto* win = obj.AsWindow();
|
||||
if (auto* btn = dynamic_cast<Button*>(win)) { addButtonProperties(btn, props); }
|
||||
if (auto* cb = dynamic_cast<CheckBox*>(win)) { addCheckBoxProperties(cb, props); }
|
||||
if (auto* ti = dynamic_cast<TextInput*>(win)) { addTextInputProperties(ti, props); }
|
||||
if (auto* sb = dynamic_cast<SwitchButton*>(win)) { addSwitchButtonProperties(sb, props); }
|
||||
if (auto* pb = dynamic_cast<ProgressBar*>(win)) { addProgressBarProperties(pb, props); }
|
||||
if (auto* lbl = dynamic_cast<Label*>(win)) { addLabelProperties(lbl, props); }
|
||||
if (auto* lsb = dynamic_cast<LabeledStaticBox*>(win)) { addLabeledStaticBoxProperties(lsb, props); }
|
||||
```
|
||||
|
||||
### Orca Button (`Button`) — category: "Orca Button"
|
||||
|
||||
| Name | Type | Editable | Getter | Setter |
|
||||
|------|------|----------|--------|--------|
|
||||
| Button Style | Choice | Yes | enum→string | string→enum |
|
||||
| Button Type | Choice | Yes | enum→string | string→enum |
|
||||
| Selected | Boolean | Yes | `m_selected` (needs getter) | `SetSelected(v)` |
|
||||
| Active Icon | ReadOnly | No | icon name string | — |
|
||||
| Inactive Icon | ReadOnly | No | icon name string | — |
|
||||
|
||||
Choices for Button Style: `Regular`, `Confirm`, `Alert`, `Disabled`
|
||||
Choices for Button Type: `Compact`, `Window`, `Choice`, `Parameter`, `Icon`, `Expanded`
|
||||
|
||||
**Source changes needed**: Button's `m_selected` is private. Add one-liner getter:
|
||||
```cpp
|
||||
bool IsSelected() const { return m_selected; }
|
||||
```
|
||||
|
||||
### Orca CheckBox (`CheckBox`) — category: "Orca CheckBox"
|
||||
|
||||
| Name | Type | Editable | Getter | Setter |
|
||||
|------|------|----------|--------|--------|
|
||||
| Half Checked | Boolean | Yes | `m_half_checked` (needs getter) | `SetHalfChecked(v)` |
|
||||
|
||||
**Source changes needed**: `m_half_checked` is private. Add one-liner getter:
|
||||
```cpp
|
||||
bool IsHalfChecked() const { return m_half_checked; }
|
||||
```
|
||||
|
||||
### Orca TextInput (`TextInput`) — category: "Orca TextInput"
|
||||
|
||||
| Name | Type | Editable | Getter | Setter |
|
||||
|------|------|----------|--------|--------|
|
||||
| Label | String | Yes | `GetLabel()` (inherited from wxWindow) | `SetLabel(v)` (exists) |
|
||||
| Text Value | String | Yes | `GetTextCtrl()->GetValue()` (GetTextCtrl is public) | `GetTextCtrl()->SetValue(v)` |
|
||||
| Corner Radius | Integer | Yes | `GetCornerRadius()` (NEW) | `SetCornerRadius(v)` (exists) |
|
||||
|
||||
**Source changes needed**: Add one getter to `TextInput`:
|
||||
```cpp
|
||||
int GetCornerRadius() const { return static_cast<int>(radius); }
|
||||
```
|
||||
(`radius` is inherited from StaticBox. `SetCornerRadius(double)` already exists. `GetTextCtrl()` is already public.)
|
||||
|
||||
### Orca SwitchButton (`SwitchButton`) — category: "Orca SwitchButton"
|
||||
|
||||
| Name | Type | Editable | Getter | Setter |
|
||||
|------|------|----------|--------|--------|
|
||||
| Value | Boolean | Yes | existing getter | existing setter |
|
||||
|
||||
### Orca ProgressBar (`ProgressBar`) — category: "Orca ProgressBar"
|
||||
|
||||
| Name | Type | Editable | Getter | Setter |
|
||||
|------|------|----------|--------|--------|
|
||||
| Proportion | Float (0-1) | Yes | `pb->m_proportion` (public member) | `pb->m_proportion = v` |
|
||||
| Show Number | Boolean | Yes | `pb->m_shownumber` (public member) | `pb->m_shownumber = v` |
|
||||
|
||||
**No source changes needed**: `m_proportion` and `m_shownumber` are already public members. `SetValue(int)` and `SetProgress(int)` already exist as public methods.
|
||||
|
||||
### Orca Label (`Label`) — category: "Orca Label"
|
||||
|
||||
| Name | Type | Editable | Getter | Setter |
|
||||
|------|------|----------|--------|--------|
|
||||
| Is Hyperlink | Boolean | No | existing flag check | — |
|
||||
| Font Size | ReadOnly | No | `GetFont().GetPointSize()` | — |
|
||||
|
||||
### LabeledStaticBox — category: "LabeledStaticBox"
|
||||
|
||||
| Name | Type | Editable | Getter | Setter |
|
||||
|------|------|----------|--------|--------|
|
||||
| Corner Radius | Integer | Yes | `GetCornerRadius()` (NEW) | `SetCornerRadius(v)` (exists) |
|
||||
| Border Width | Integer | Yes | `GetBorderWidth()` (NEW) | `SetBorderWidth(v)` (exists) |
|
||||
| Border Color | String (hex) | Yes | `GetBorderColor()` (NEW) | `SetBorderColor(v)` (exists) |
|
||||
| Scale | Float (ReadOnly) | No | `m_scale` (protected, needs getter) | — |
|
||||
|
||||
**Source changes needed**: Four one-liner getters added to `LabeledStaticBox`:
|
||||
```cpp
|
||||
int GetCornerRadius() const { return m_radius; }
|
||||
int GetBorderWidth() const { return m_border_width; }
|
||||
StateColor GetBorderColor() const { return border_color; }
|
||||
float GetScale() const { return m_scale; }
|
||||
```
|
||||
|
||||
## Files Modified (Existing Code)
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/slic3r/GUI/GUI_Utils.hpp` | +4 methods in `DPIAware<P>`: `set_scale_factor()`, `set_prev_scale_factor()`, `set_em_unit()`, `force_rescale()` |
|
||||
| `src/slic3r/GUI/Widgets/LabeledStaticBox.hpp` | +4 getter declarations: `GetCornerRadius()`, `GetBorderWidth()`, `GetBorderColor()`, `GetScale()` |
|
||||
| `src/slic3r/GUI/Widgets/LabeledStaticBox.cpp` | +4 getter implementations |
|
||||
| `src/slic3r/GUI/Widgets/Button.hpp` | +1 getter: `IsSelected()` |
|
||||
| `src/slic3r/GUI/Widgets/CheckBox.hpp` | +1 getter: `IsHalfChecked()` |
|
||||
| `src/slic3r/GUI/Widgets/TextInput.hpp` | +1 getter: `GetCornerRadius()` |
|
||||
| `src/slic3r/GUI/Widgets/ProgressBar.hpp` | None (public members are used directly) |
|
||||
| `src/slic3r/GUI/MainFrame.cpp` | +1 `#include`, +1 call to `RegisterOrcaInspectorPlugins()` |
|
||||
| `src/slic3r/CMakeLists.txt` | +4 entries in `SLIC3R_GUI_SOURCES` (the .cpp plugin files) |
|
||||
|
||||
## Files Created
|
||||
|
||||
```
|
||||
src/slic3r/Utils/wxInspectorPlugins/
|
||||
├── DPIAwarePlugin.hpp
|
||||
├── DPIAwarePlugin.cpp
|
||||
├── CustomWidgetsPlugin.hpp
|
||||
├── CustomWidgetsPlugin.cpp
|
||||
└── Registration.hpp
|
||||
```
|
||||
|
||||
## Build & Linking
|
||||
|
||||
The `wxInspector` dependency is already wired:
|
||||
- `deps/wxInspector/wxInspector.cmake` fetches and builds wxInspector
|
||||
- `src/CMakeLists.txt` lines 92-93 link `wxInspector::wxInspector` into `wxWidgets_LIBRARIES`
|
||||
- The plugin files only need `#include <wx/inspector/plugin.h>` and `#include <wx/inspector/inspector.h>` — both available from the installed dependency
|
||||
|
||||
No new CMake dependencies needed. Only the new source files need listing in `SLIC3R_GUI_SOURCES`.
|
||||
|
||||
## Error Handling & Edge Cases
|
||||
|
||||
- **Stale pointers**: Plugin lambdas capture raw pointers, regenerated on every `GetProperties` call (matching wxInspector's built-in provider pattern). Pointers live only until the next tree selection.
|
||||
- **Widget destruction**: If a widget is destroyed while the inspector is showing its properties, `InspectableObject::IsValid()` returns false and properties are not displayed. The inspector won't show stale data.
|
||||
- **Invalid property values**: Setters use `sscanf` / `ToLong` with validation (matching built-in patterns). Bogus input is rejected — setter returns `false`, property grid shows error state.
|
||||
- **DPI drift**: Setting `scale_factor` without triggering rescale means displayed sizes don't match the new factor. This is acceptable — the inspector is a developer tool; operators know to call `Layout()` after making changes.
|
||||
- **Missing widget type**: If a `dynamic_cast` fails for all types, only built-in wxWindow properties are shown. No crash, no error — just reduced info.
|
||||
|
||||
## Future Work (Out of Scope)
|
||||
|
||||
- **StateColor visualization**: `StateColor` is a multi-value type (maps bitmask states to colors). A full solution would need a custom property editor (e.g., a table showing each state→color pair). Keep it simple for now.
|
||||
- **ScalableBitmap display**: Could show the bitmap as an inline thumbnail. Complex property editor work — deferred.
|
||||
- **More widget types**: `SwitchBoard`, `MultiSwitchButton`, `StepCtrl`, `FanControl`, `DropDown`, `ComboBox`, `AMS*` widgets could all benefit. Add as needed.
|
||||
- **Property refresh on tree selection**: Currently properties are static snapshots. A "refresh" button or auto-poll could keep values current for rapidly-changing widgets (progress bars, etc.). The built-in wxInspector already provides a tree-refresh button.
|
||||
+338
-3526
File diff suppressed because it is too large
Load Diff
+429
-3954
File diff suppressed because it is too large
Load Diff
+432
-3960
File diff suppressed because it is too large
Load Diff
+418
-3943
File diff suppressed because it is too large
Load Diff
+338
-3526
File diff suppressed because it is too large
Load Diff
+408
-3895
File diff suppressed because it is too large
Load Diff
+453
-3951
File diff suppressed because it is too large
Load Diff
+459
-3984
File diff suppressed because it is too large
Load Diff
+417
-3945
File diff suppressed because it is too large
Load Diff
+425
-3950
File diff suppressed because it is too large
Load Diff
+449
-3976
File diff suppressed because it is too large
Load Diff
+432
-3959
File diff suppressed because it is too large
Load Diff
@@ -125,7 +125,6 @@ src/slic3r/GUI/ThermalPreconditioningDialog.hpp
|
||||
src/slic3r/GUI/Jobs/SLAImportJob.cpp
|
||||
src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp
|
||||
src/slic3r/GUI/AboutDialog.cpp
|
||||
src/slic3r/GUI/ActionRegistry.cpp
|
||||
src/slic3r/GUI/AMSMaterialsSetting.cpp
|
||||
src/slic3r/GUI/ExtrusionCalibration.cpp
|
||||
src/slic3r/GUI/AmsMappingPopup.cpp
|
||||
@@ -137,7 +136,6 @@ src/slic3r/GUI/BackgroundSlicingProcess.cpp
|
||||
src/slic3r/GUI/BedShapeDialog.cpp
|
||||
src/slic3r/GUI/BedShapeDialog.hpp
|
||||
src/slic3r/GUI/ConfigManipulation.cpp
|
||||
src/slic3r/GUI/ConfigValueFormatter.cpp
|
||||
src/slic3r/GUI/DeviceManager.cpp
|
||||
src/slic3r/GUI/DeviceErrorDialog.cpp
|
||||
src/slic3r/GUI/ExtraRenderers.cpp
|
||||
@@ -160,7 +158,6 @@ src/slic3r/GUI/SelectMachinePop.cpp
|
||||
src/slic3r/GUI/StatusPanel.cpp
|
||||
src/slic3r/GUI/Monitor.cpp
|
||||
src/slic3r/GUI/MsgDialog.cpp
|
||||
src/slic3r/GUI/NativeCommands.cpp
|
||||
src/slic3r/GUI/NotificationManager.hpp
|
||||
src/slic3r/GUI/NotificationManager.cpp
|
||||
src/slic3r/GUI/ObjectDataViewModel.cpp
|
||||
@@ -178,11 +175,8 @@ src/slic3r/GUI/ProgressStatusBar.cpp
|
||||
src/slic3r/GUI/PlateSettingsDialog.cpp
|
||||
src/slic3r/GUI/PrivacyUpdateDialog.cpp
|
||||
src/slic3r/GUI/PublishDialog.cpp
|
||||
src/slic3r/GUI/PublishSettingsDialog.cpp
|
||||
src/slic3r/GUI/SavePresetDialog.cpp
|
||||
src/slic3r/GUI/Search.cpp
|
||||
src/slic3r/GUI/SettingsIndex.cpp
|
||||
src/slic3r/GUI/SpeedDialDialog.cpp
|
||||
src/slic3r/GUI/Selection.cpp
|
||||
src/slic3r/GUI/SelectMachine.cpp
|
||||
src/slic3r/GUI/PrePrintChecker.cpp
|
||||
@@ -199,6 +193,7 @@ src/slic3r/GUI/ObjColorDialog.cpp
|
||||
src/slic3r/GUI/SyncAmsInfoDialog.cpp
|
||||
src/slic3r/GUI/WipeTowerDialog.cpp
|
||||
src/slic3r/GUI/wxExtensions.cpp
|
||||
src/slic3r/GUI/wxMediaCtrl2.cpp
|
||||
src/slic3r/GUI/WebUserLoginDialog.cpp
|
||||
src/slic3r/GUI/WebGuideDialog.cpp
|
||||
src/slic3r/GUI/KBShortcutsDialog.hpp
|
||||
@@ -213,7 +208,6 @@ src/slic3r/Utils/Process.cpp
|
||||
src/libslic3r/GCode.cpp
|
||||
src/libslic3r/GCodeWriter.cpp
|
||||
src/libslic3r/GCode/ToolOrdering.cpp
|
||||
src/libslic3r/GCode/SeamPlacer.cpp
|
||||
src/libslic3r/ExtrusionEntity.cpp
|
||||
src/libslic3r/Flow.cpp
|
||||
src/libslic3r/Format/AMF.cpp
|
||||
@@ -251,7 +245,6 @@ src/slic3r/GUI/TroubleshootDialog.cpp
|
||||
src/slic3r/Utils/3DPrinterOS.cpp
|
||||
src/slic3r/Utils/AstroBox.cpp
|
||||
src/slic3r/Utils/Duet.cpp
|
||||
src/slic3r/Utils/UltiMaker.cpp
|
||||
src/slic3r/Utils/FlashAir.cpp
|
||||
src/slic3r/Utils/MKS.cpp
|
||||
src/slic3r/Utils/Moonraker.cpp
|
||||
@@ -297,9 +290,3 @@ src/slic3r/GUI/PrinterWebViewHandler.cpp
|
||||
src/slic3r/GUI/AMSDryControl.cpp
|
||||
src/slic3r/GUI/AMSDryControl.hpp
|
||||
src/libslic3r/PresetBundle.cpp
|
||||
src/slic3r/GUI/CAD/DesignPanel.cpp
|
||||
src/slic3r/GUI/CAD/SketchInlineEditor.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp
|
||||
src/slic3r/GUI/KeyChord.cpp
|
||||
src/slic3r/GUI/Shortcuts.cpp
|
||||
|
||||
+441
-3942
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user