Load profile includes so synced Bambu Lab presets get their G-code back

This commit is contained in:
SoftFever
2026-09-24 13:22:37 +08:00
parent d51ad889fd
commit 2b602943a5
14 changed files with 768 additions and 350 deletions
+17 -13
View File
@@ -76,9 +76,10 @@ 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 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 that resolving it derives — the preset's own config diff, the name of the preset it
inherits, and the parse metadata (name, sub-path, description, instantiation, setting inherits, the names of the presets it includes, and the parse metadata (name, sub-path,
and filament ids, renames). Non-instantiated base presets are stored too; the children description, instantiation, setting and filament ids, renames). Non-instantiated base
that inherit from them cannot resolve without them. 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 **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 file uses, the `ConfigOptionType` each was written as, and the distinct enum *value
@@ -161,9 +162,9 @@ cache nothing can invalidate is worse than no cache.
Vendors load in a fixed order, because filament inheritance crosses exactly one 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, boundary: any vendor's filament may inherit from the shared Orca filament library,
and nothing else reaches across vendors. The library therefore goes first, alone; and nothing else reaches across vendors — an `include` is always vendor-local. The
every other vendor follows in parallel, resolving against it; and the results are library therefore goes first, alone; every other vendor follows in parallel, resolving
merged in a stable order: against it; and the results are merged in a stable order:
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -211,13 +212,16 @@ and its cache was never written back.
Serving from a cache is not a memory-image restore. The entries are deserialized and Serving from a cache is not a memory-image restore. The entries are deserialized and
then installed one by one — inheritance resolved against the presets installed before then installed one by one — inheritance resolved against the presets installed before
them and the currently loaded filament library, configs flattened onto the collection them and the currently loaded filament library, includes layered in, configs flattened
defaults, validated and registered — by the same function the JSON path calls straight onto the collection defaults, validated and registered — by the same function the JSON
after parsing a sub-file. The two paths share everything below the parse, which is what path calls straight after parsing a sub-file. An `include` layers what the included
makes a cache-loaded bundle indistinguishable from a JSON-loaded one by construction base states, between the parent and the preset's own keys: the base's diff against the
rather than by test coverage. Installation also rebuilds each preset's file path from default, taken when the base itself was installed and before the per-variant padding
the local data directory, so a shipped cache never carries the generating machine's `inherits` sees, so only what a template sets reaches the presets including it. The two
paths. paths share everything below the parse, which is what makes a cache-loaded bundle
indistinguishable from a JSON-loaded one by construction rather than by test coverage.
Installation also rebuilds each preset's file path from the local data directory, so a
shipped cache never carries the generating machine's paths.
App upgrades work because a cache normally survives one. Only a deliberate 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 `CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
+253 -253
View File
@@ -1,7 +1,7 @@
{ {
"name": "Bambulab", "name": "Bambulab",
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json", "url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
"version": "02.08.00.09", "version": "02.08.00.10",
"force_update": "0", "force_update": "0",
"description": "the initial version of BBL configurations", "description": "the initial version of BBL configurations",
"machine_model_list": [ "machine_model_list": [
@@ -67,6 +67,22 @@
"name": "fdm_filament_common", "name": "fdm_filament_common",
"sub_path": "filament/fdm_filament_common.json" "sub_path": "filament/fdm_filament_common.json"
}, },
{
"name": "fdm_filament_template_direct_bowden",
"sub_path": "filament/fdm_filament_template_direct_bowden.json"
},
{
"name": "fdm_filament_template_direct_bowden_e3d",
"sub_path": "filament/fdm_filament_template_direct_bowden_e3d.json"
},
{
"name": "fdm_filament_template_direct_dual",
"sub_path": "filament/fdm_filament_template_direct_dual.json"
},
{
"name": "fdm_filament_template_direct_dual_e3d",
"sub_path": "filament/fdm_filament_template_direct_dual_e3d.json"
},
{ {
"name": "fdm_filament_abs", "name": "fdm_filament_abs",
"sub_path": "filament/fdm_filament_abs.json" "sub_path": "filament/fdm_filament_abs.json"
@@ -8246,261 +8262,9 @@
{ {
"name": "Generic TPU @BBL H2S", "name": "Generic TPU @BBL H2S",
"sub_path": "filament/Generic TPU @BBL H2S.json" "sub_path": "filament/Generic TPU @BBL H2S.json"
},
{
"name": "fdm_filament_template_direct_bowden",
"sub_path": "filament/fdm_filament_template_direct_bowden.json"
},
{
"name": "fdm_filament_template_direct_bowden_e3d",
"sub_path": "filament/fdm_filament_template_direct_bowden_e3d.json"
},
{
"name": "fdm_filament_template_direct_dual",
"sub_path": "filament/fdm_filament_template_direct_dual.json"
},
{
"name": "fdm_filament_template_direct_dual_e3d",
"sub_path": "filament/fdm_filament_template_direct_dual_e3d.json"
} }
], ],
"machine_list": [ "machine_list": [
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_bbl_3dp_001_common",
"sub_path": "machine/fdm_bbl_3dp_001_common.json"
},
{
"name": "fdm_bbl_3dp_002_common",
"sub_path": "machine/fdm_bbl_3dp_002_common.json"
},
{
"name": "Bambu Lab A1 0.4 nozzle",
"sub_path": "machine/Bambu Lab A1 0.4 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.4 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.4 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.4 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.4 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.4 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.4 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.4 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.4 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.4 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.4 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.4 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.4 nozzle.json"
},
{
"name": "Bambu Lab A1 0.2 nozzle",
"sub_path": "machine/Bambu Lab A1 0.2 nozzle.json"
},
{
"name": "Bambu Lab A1 0.6 nozzle",
"sub_path": "machine/Bambu Lab A1 0.6 nozzle.json"
},
{
"name": "Bambu Lab A1 0.8 nozzle",
"sub_path": "machine/Bambu Lab A1 0.8 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.2 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.2 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.6 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.6 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.8 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.8 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.2 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.2 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.6 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.6 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.8 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.8 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.2 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.2 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.6 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.6 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.8 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.8 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.2 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.2 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.6 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.6 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.8 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.8 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.2 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.2 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.6 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.6 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.8 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.8 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.2 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.2 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.6 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.6 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.8 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.8 nozzle.json"
},
{ {
"name": "Bambu Lab A1 0.4 nozzle template change_filament_gcode", "name": "Bambu Lab A1 0.4 nozzle template change_filament_gcode",
"sub_path": "machine/Bambu Lab A1 0.4 nozzle template change_filament_gcode.json" "sub_path": "machine/Bambu Lab A1 0.4 nozzle template change_filament_gcode.json"
@@ -8856,6 +8620,242 @@
{ {
"name": "Bambu Lab X2D 0.4 nozzle template time_lapse_gcode", "name": "Bambu Lab X2D 0.4 nozzle template time_lapse_gcode",
"sub_path": "machine/Bambu Lab X2D 0.4 nozzle template time_lapse_gcode.json" "sub_path": "machine/Bambu Lab X2D 0.4 nozzle template time_lapse_gcode.json"
},
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_bbl_3dp_001_common",
"sub_path": "machine/fdm_bbl_3dp_001_common.json"
},
{
"name": "fdm_bbl_3dp_002_common",
"sub_path": "machine/fdm_bbl_3dp_002_common.json"
},
{
"name": "Bambu Lab A1 0.4 nozzle",
"sub_path": "machine/Bambu Lab A1 0.4 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.4 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.4 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.4 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.4 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.4 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.4 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.4 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.4 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.4 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.4 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.4 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.4 nozzle.json"
},
{
"name": "Bambu Lab A1 0.2 nozzle",
"sub_path": "machine/Bambu Lab A1 0.2 nozzle.json"
},
{
"name": "Bambu Lab A1 0.6 nozzle",
"sub_path": "machine/Bambu Lab A1 0.6 nozzle.json"
},
{
"name": "Bambu Lab A1 0.8 nozzle",
"sub_path": "machine/Bambu Lab A1 0.8 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.2 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.2 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.6 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.6 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.8 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.8 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.2 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.2 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.6 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.6 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.8 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.8 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.2 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.2 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.6 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.6 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.8 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.8 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.2 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.2 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.6 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.6 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.8 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.8 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.2 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.2 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.6 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.6 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.8 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.8 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.2 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.2 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.6 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.6 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.8 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.8 nozzle.json"
} }
], ],
"process_list": [ "process_list": [
+44 -36
View File
@@ -437,6 +437,7 @@ def resolve_filament_field(name, field, filaments, ofl_filaments, seen=None, in_
the same hop semantics as resolve_filament_id: own value, else walk the same hop semantics as resolve_filament_id: own value, else walk
`inherits` in the vendor map with OFL base-bundle fallback. Values are list `inherits` in the vendor map with OFL base-bundle fallback. Values are list
options — the first element counts; "" when the chain never defines one. options — the first element counts; "" when the chain never defines one.
Templates pulled in by `include` are not consulted: none states either field.
""" """
if seen is None: if seen is None:
seen = set() seen = set()
@@ -887,8 +888,8 @@ def _vendor_json_files(vendor_path):
def check_preset_name_uniqueness(profiles_dir, vendor): def check_preset_name_uniqueness(profiles_dir, vendor):
"""No two profiles in a bundle may share a type and a name, indexed or not. """No two profiles in a bundle may share a type and a name, indexed or not.
The loader resolves "inherits" through a per-type map of the bundle's profiles The loader resolves "inherits" and "include" through per-type maps of the bundle's
(PresetBundle.cpp load_subfiles), and std::map::emplace keeps the first profiles (PresetBundle.cpp load_subfiles), and std::map::emplace keeps the first
insertion: a second file claiming the name is silently dropped, and which one insertion: a second file claiming the name is silently dropped, and which one
wins is nothing but index order. An unindexed twin counts too - it is one wins is nothing but index order. An unindexed twin counts too - it is one
sub_path edit away from deciding that silently. sub_path edit away from deciding that silently.
@@ -1287,11 +1288,11 @@ def check_normalized(profiles_dir, vendor):
Those two commands define a profile file's canonical shape - identifying keys Those two commands define a profile file's canonical shape - identifying keys
first, keys the slicer no longer reads gone, filament options that are vectors first, keys the slicer no longer reads gone, filament options that are vectors
written as vectors - and a <vendor>.json's canonical lists, ordered parents-first written as vectors - and a <vendor>.json's canonical lists, ordered
so the loader resolves every "inherits" in one pass. Running them over a dependencies-first so the loader resolves every "inherits" and "include" in one
contributed bundle has to be a no-op; where it would not be, the file that was pass. Running them over a contributed bundle has to be a no-op; where it would
reviewed is not the file that ships, and the next maintainer to run normalize not be, the file that was reviewed is not the file that ships, and the next
carries an unrelated diff into their own change. maintainer to run normalize carries an unrelated diff into their own change.
It asks the normalize and update-index sections below rather than restating what It asks the normalize and update-index sections below rather than restating what
they do, because a second definition of normal is free to drift from the one that they do, because a second definition of normal is free to drift from the one that
@@ -2034,9 +2035,9 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
cli_config.json carry no "type" - all of them stay. A file that cannot be parsed cli_config.json carry no "type" - all of them stay. A file that cannot be parsed
is reported and kept: never delete what could not be read. is reported and kept: never delete what could not be read.
An unindexed file that some surviving profile names in "inherits" is kept too, An unindexed file that some surviving profile names in "inherits" or "include" is
and reported, UNLESS an indexed profile already carries that name: "inherits" is kept too, and reported, UNLESS an indexed profile already carries that name: both
resolved by preset name, so the indexed one is the parent every child actually are resolved by preset name, so the indexed one is the parent every child actually
gets, and the unindexed file is a stale copy the loader never reaches. Where no gets, and the unindexed file is a stale copy the loader never reaches. Where no
indexed profile provides the name the inheriting preset really is broken, and indexed profile provides the name the inheriting preset really is broken, and
deleting the file would destroy the only record of the settings it was written deleting the file would destroy the only record of the settings it was written
@@ -2069,7 +2070,7 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
listed.add(posixpath.normpath(sub_path.replace("\\", "/"))) listed.add(posixpath.normpath(sub_path.replace("\\", "/")))
candidates = {} # path -> profile, for every unindexed preset candidates = {} # path -> profile, for every unindexed preset
inherited = set() # every name the files that stay claim as a parent inherited = set() # every name the files that stay claim as a parent or include
provided = {} # name -> sub_path, for the profiles the loader can see provided = {} # name -> sub_path, for the profiles the loader can see
for sub in subs: for sub in subs:
for path in _walk_json(os.path.join(vendor_dir, sub)): for path in _walk_json(os.path.join(vendor_dir, sub)):
@@ -2084,8 +2085,7 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
if not isinstance(profile, dict): if not isinstance(profile, dict):
continue continue
if sub_path in listed or profile.get("type") not in PROFILE_TYPES: if sub_path in listed or profile.get("type") not in PROFILE_TYPES:
if profile.get("inherits"): inherited.update(profile_dependencies(profile))
inherited.add(profile["inherits"])
if sub_path in listed and profile.get("name"): if sub_path in listed and profile.get("name"):
provided[profile["name"]] = sub_path provided[profile["name"]] = sub_path
continue continue
@@ -2104,8 +2104,7 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
for path, profile in rescued.items(): for path, profile in rescued.items():
del candidates[path] del candidates[path]
kept[path] = profile kept[path] = profile
if profile.get("inherits"): inherited.update(profile_dependencies(profile))
inherited.add(profile["inherits"])
for path in sorted(kept): for path in sorted(kept):
print_warning(f"{_rel(path, profiles_dir)}: not indexed by {vendor}.json but " print_warning(f"{_rel(path, profiles_dir)}: not indexed by {vendor}.json but "
@@ -2142,40 +2141,48 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
# update-index # update-index
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def topological_sort(profiles): def profile_dependencies(profile):
"""Order index entries parents-first, so the loader resolves inherits in one pass. """The names a profile needs loaded before it: its parent and every include."""
include = profile.get("include") or []
if isinstance(include, str):
include = [include]
return [name for name in [profile.get("inherits"), *include] if name]
Entries whose parent is not in the same section keep their own (sorted) order at
the end; the loader finds those parents through the base bundle instead. def topological_sort(profiles):
"""Order index entries dependencies-first, so the loader resolves every
"inherits" and "include" in one pass.
Entries that neither depend on nor are depended on by another in the same section
go at the end in name order; the loader finds their parents, if any, through the
base bundle instead. Every entry on a dependency cycle, which no order can satisfy,
goes there too.
""" """
graph = defaultdict(list) graph = defaultdict(list)
in_degree = defaultdict(int) in_degree = defaultdict(int)
by_name = {p["name"]: p for p in profiles} by_name = {p["name"]: p for p in profiles}
all_names = set(by_name) all_names = set(by_name)
placed = set()
for profile in profiles: for profile in profiles:
parent = profile.get("inherits")
child = profile["name"] child = profile["name"]
if parent in all_names: for parent in profile_dependencies(profile):
graph[parent].append(child) if parent in all_names:
in_degree[child] += 1 graph[parent].append(child)
in_degree.setdefault(parent, 0) in_degree[child] += 1
placed.add(child) in_degree.setdefault(parent, 0)
placed.add(parent)
queue = sorted(name for name, degree in in_degree.items() if degree == 0) queue = sorted(name for name, degree in in_degree.items() if degree == 0)
result = [] result = []
while queue: while queue:
current = queue.pop(0) current = queue.pop(0)
result.append(by_name[current]) result.append(by_name[current])
placed.add(current)
for child in sorted(graph[current]): for child in sorted(graph[current]):
in_degree[child] -= 1 in_degree[child] -= 1
if in_degree[child] == 0: if in_degree[child] == 0:
queue.append(child) queue.append(child)
result.extend(by_name[name] for name in sorted(all_names - placed)) ordered = {p["name"] for p in result}
result.extend(by_name[name] for name in sorted(all_names - ordered))
return result return result
@@ -2225,15 +2232,15 @@ def build_index_sections(profiles_dir, vendor, profile_types=None):
"name": name, "name": name,
"sub_path": os.path.relpath(path, vendor_dir).replace(os.sep, "/"), "sub_path": os.path.relpath(path, vendor_dir).replace(os.sep, "/"),
} }
if profile.get("inherits"): for key in ("inherits", "include"):
entry["inherits"] = profile["inherits"] if profile.get(key):
entry[key] = profile[key]
by_name[name].append(entry["sub_path"]) by_name[name].append(entry["sub_path"])
entries.append(entry) entries.append(entry)
sorted_entries = topological_sort(entries) # inherits/include were ordering input only; the index holds name and sub_path
for entry in sorted_entries: sections[profile_type + "_list"] = [{"name": e["name"], "sub_path": e["sub_path"]}
entry.pop("inherits", None) # ordering input only, not part of the index for e in topological_sort(entries)]
sections[profile_type + "_list"] = sorted_entries
for rel, found in sorted(unplaceable.items()): for rel, found in sorted(unplaceable.items()):
problems.append(f'{rel}: type {found!r} is not one of {list(PROFILE_TYPES)}, so it ' problems.append(f'{rel}: type {found!r} is not one of {list(PROFILE_TYPES)}, so it '
@@ -2418,7 +2425,8 @@ def build_parser():
add("update-index", [vendor_opt, type_opt, dry_run_opt, profiles_opt], add("update-index", [vendor_opt, type_opt, dry_run_opt, profiles_opt],
"regenerate the *_list sections of <vendor>.json", "regenerate the *_list sections of <vendor>.json",
"Rebuild the *_list sections of each <vendor>.json from the files on disk,\n" "Rebuild the *_list sections of each <vendor>.json from the files on disk,\n"
"ordered parents-first so the loader resolves inherits in one pass.\n" "ordered dependencies-first so the loader resolves inherits and include\n"
"in one pass.\n"
"\n" "\n"
"A profile is indexed under the section its own \"type\" names, so run\n" "A profile is indexed under the section its own \"type\" names, so run\n"
"normalize first: it is what writes a missing type. Two files claiming one\n" "normalize first: it is what writes a missing type. Two files claiming one\n"
+20
View File
@@ -411,6 +411,26 @@ class TestUpdateIndex(TreeCase):
for entry in self.t.read_index("V")["filament_list"]: for entry in self.t.read_index("V")["filament_list"]:
self.assertEqual(sorted(entry), ["name", "sub_path"]) self.assertEqual(sorted(entry), ["name", "sub_path"])
def test_include_targets_are_listed_before_their_users(self):
# The loader resolves include like inherits: in one pass over the list, so
# a template must be listed before every preset that includes it - even
# though a template has no parent of its own to order it by.
self.t.write("V", "machine/P.json", {"type": "machine", "name": "P",
"include": ["T start", "T end"]})
self.t.write("V", "machine/T start.json", {"type": "machine", "name": "T start"})
self.t.write("V", "machine/T end.json", {"type": "machine", "name": "T end"})
self.t.write("V", "filament/F.json", {"type": "filament", "name": "F",
"inherits": "B", "include": "S"})
self.t.write("V", "filament/B.json", {"type": "filament", "name": "B"})
self.t.write("V", "filament/S.json", {"type": "filament", "name": "S"})
rc, out = self.run_command("update-index")
self.assertEqual(rc, 0, out)
machines = [e["name"] for e in self.t.read_index("V")["machine_list"]]
self.assertEqual(machines, ["T end", "T start", "P"])
filaments = [e["name"] for e in self.t.read_index("V")["filament_list"]]
self.assertLess(filaments.index("B"), filaments.index("F"))
self.assertLess(filaments.index("S"), filaments.index("F"))
def test_a_profile_with_no_usable_type_is_reported_not_dropped(self): def test_a_profile_with_no_usable_type_is_reported_not_dropped(self):
self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"}) self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"})
self.t.write("V", "filament/B.json", {"name": "B"}) self.t.write("V", "filament/B.json", {"name": "B"})
+6
View File
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
target_link_libraries(generate_system_cache libslic3r boost_headeronly) target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS}) target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
# profile_include_dump: prints what included templates contribute to a vendor's presets,
# to diff against the same tool built in BambuStudio. Built only on request.
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
target_link_libraries(profile_include_dump libslic3r boost_headeronly)
target_compile_definitions(profile_include_dump PRIVATE ${_DEV_DEFS})
endif() endif()
# Function that adds source file encoding check to a target # Function that adds source file encoding check to a target
+215
View File
@@ -0,0 +1,215 @@
// Prints what the profile `include` key contributes to one vendor's system
// presets, so that the dump from OrcaSlicer and the one from BambuStudio can be
// diffed: the two copies of this file differ only in how they load the vendor.
// For every preset whose values pass through an included template, from its
// own file or an ancestor's, it prints the templates those files name, in the
// order they apply, then the loaded value of each key the templates set. Every
// line starts with the preset, so a plain diff names the preset of each
// difference.
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include "nlohmann/json.hpp"
#include <algorithm>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
using namespace Slic3r;
namespace fs = boost::filesystem;
using nlohmann::json;
namespace {
// What a sub-file states about where its values come from, and what it sets.
struct Entry
{
std::string inherits;
std::vector<std::string> includes;
std::vector<std::string> keys;
};
// Sub-files by name, per section of the vendor index: `inherits` and `include`
// both resolve within a section.
using Sections = std::map<std::string, std::map<std::string, Entry>>;
json read_json(const fs::path &path)
{
boost::nowide::ifstream in(path.string());
if (!in)
throw std::runtime_error("Cannot read " + path.string());
return json::parse(in);
}
Sections read_vendor(const fs::path &dir, const std::string &vendor)
{
// Keys that name and place a sub-file rather than set a value.
static const std::set<std::string> metadata = {"name", "type", "from", "instantiation", "inherits", "include"};
Sections sections;
const json index = read_json(dir / (vendor + ".json"));
for (const char *section : {"process_list", "filament_list", "machine_list"}) {
const auto list = index.find(section);
if (list == index.end())
continue;
for (const json &item : *list) {
const json file = read_json(dir / vendor / item.at("sub_path").get<std::string>());
// A file without a name goes by its name in the index.
Entry &entry = sections[section][file.value("name", item.at("name").get<std::string>())];
entry.inherits = file.value("inherits", "");
if (const auto include = file.find("include"); include != file.end()) {
if (include->is_string())
entry.includes.push_back(include->get<std::string>());
else
for (const json &name : *include)
entry.includes.push_back(name.get<std::string>());
}
for (auto it = file.begin(); it != file.end(); ++it)
if (metadata.count(it.key()) == 0)
entry.keys.push_back(it.key());
}
}
return sections;
}
const PresetCollection &presets_of(const PresetBundle &bundle, const std::string &section)
{
if (section == "process_list")
return bundle.prints;
if (section == "filament_list")
return bundle.filaments;
return bundle.printers;
}
size_t dump(const PresetBundle &bundle, const Sections &sections, std::ostream &out)
{
size_t dumped = 0;
for (const auto &[section, entries] : sections) {
std::vector<const Preset *> presets;
for (const Preset &preset : presets_of(bundle, section).get_presets())
if (preset.is_system)
presets.push_back(&preset);
std::sort(presets.begin(), presets.end(), [](const Preset *a, const Preset *b) { return a->name < b->name; });
for (const Preset *preset : presets) {
// The preset and its ancestors, root first: the order their includes apply in.
std::vector<const Entry *> lineage;
for (auto it = entries.find(preset->name); it != entries.end() && lineage.size() <= entries.size();
it = entries.find(it->second.inherits))
lineage.insert(lineage.begin(), &it->second);
std::string templates;
std::set<std::string> keys;
for (const Entry *entry : lineage)
for (const std::string &name : entry->includes) {
templates += (templates.empty() ? "" : "; ") + name;
if (const auto it = entries.find(name); it != entries.end())
keys.insert(it->second.keys.begin(), it->second.keys.end());
}
if (templates.empty())
continue;
const std::string prefix = section.substr(0, section.find('_')) + " | " + preset->name + " | ";
out << prefix << "include = " << templates << "\n";
for (const std::string &key : keys)
out << prefix << key << " = " << (preset->config.has(key) ? preset->config.opt_serialize(key) : "<absent>") << "\n";
++dumped;
}
}
return dumped;
}
// Orca: load the vendor as the app does, against the filament library when the
// directory has one: parsed from its JSON files or, with from_cache, from the
// preset cache the app reads on every launch after the first.
void load_vendor(PresetBundle &bundle, const std::string &dir, const std::string &vendor, bool from_cache)
{
const auto rule = ForwardCompatibilitySubstitutionRule::EnableSilent;
PresetBundle library;
const PresetBundle *base = nullptr;
if (fs::is_regular_file(fs::path(dir) / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json"))) {
library.load_vendor_configs_from_json(dir, PresetBundle::ORCA_FILAMENT_LIBRARY, PresetBundle::LoadSystem, rule, nullptr, false);
base = &library;
}
if (!from_cache) {
bundle.load_vendor_configs_from_json(dir, vendor, PresetBundle::LoadSystem, rule, base, false);
return;
}
// Parse once to write <vendor>.opc beside the profile, then load that
// cache into a clean bundle. Any cache already there goes first, so that a
// failed write cannot leave it to be loaded; one that was not there before
// goes again afterwards.
const fs::path cache = fs::path(dir) / (vendor + ".opc");
const bool had_cache = fs::exists(cache);
fs::remove(cache);
PresetBundle parsed;
parsed.set_is_validation_mode(true); // parse the JSON: validation never serves a cache
parsed.set_generate_vendor_caches(true);
parsed.load_vendor_configs_from_json(dir, vendor, PresetBundle::LoadSystem, rule, base);
const bool loaded = bundle.load_vendor_cache(cache.string(), vendor, parsed.vendors.at(vendor).config_version, base);
if (!had_cache)
fs::remove(cache);
if (!loaded)
throw std::runtime_error("The preset cache " + cache.string() + " was not written or was rejected");
}
} // namespace
int main(int argc, char *argv[])
{
const char *usage = "Usage: profile_include_dump -p <profiles dir> -o <dump file> [-v <vendor>] [-l <log level>] [-c]\n"
" -v vendor to dump, BBL if omitted\n"
" -l log level, 0 (fatal) to 5 (trace); 1 if omitted\n"
" -c load the presets from the vendor's preset cache, generated first, not the JSON\n";
std::string dir, output, vendor = "BBL";
unsigned log_level = 1;
bool from_cache = false;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "-c") {
from_cache = true;
continue;
}
if (i + 1 == argc) {
std::cerr << usage;
return 1;
}
const std::string value = argv[++i];
if (arg == "-p")
dir = value;
else if (arg == "-o")
output = value;
else if (arg == "-v")
vendor = value;
else if (arg == "-l" && value.size() == 1 && value[0] >= '0' && value[0] <= '5')
log_level = unsigned(value[0] - '0');
else {
std::cerr << usage;
return 1;
}
}
if (dir.empty() || output.empty() || !fs::is_directory(dir)) {
std::cerr << usage;
return 1;
}
set_logging_level(log_level);
try {
PresetBundle bundle;
load_vendor(bundle, dir, vendor, from_cache);
boost::nowide::ofstream out(output);
if (!out)
throw std::runtime_error("Cannot write " + output);
const size_t dumped = dump(bundle, read_vendor(dir, vendor), out);
out.close();
if (!out)
throw std::runtime_error("Failed writing " + output);
std::cerr << "Dumped " << dumped << " " << vendor << " presets that include a template\n";
} catch (const std::exception &ex) {
std::cerr << ex.what() << "\n";
return 1;
}
return 0;
}
+3
View File
@@ -950,6 +950,9 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
} }
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) { else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value()); key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
}
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
} else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) { } else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value()); key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
} else { } else {
+1
View File
@@ -62,6 +62,7 @@
#define ORCA_JSON_KEY_UPDATE_TIME "updated_time" #define ORCA_JSON_KEY_UPDATE_TIME "updated_time"
#define ORCA_JSON_KEY_CREATED_TIME "created_time" #define ORCA_JSON_KEY_CREATED_TIME "created_time"
#define BBL_JSON_KEY_INHERITS "inherits" #define BBL_JSON_KEY_INHERITS "inherits"
#define BBL_JSON_KEY_INCLUDES "include"
#define BBL_JSON_KEY_INSTANTIATION "instantiation" #define BBL_JSON_KEY_INSTANTIATION "instantiation"
#define BBL_JSON_KEY_NOZZLE_DIAMETER "nozzle_diameter" #define BBL_JSON_KEY_NOZZLE_DIAMETER "nozzle_diameter"
#define BBL_JSON_KEY_PRINTER_TECH "machine_tech" #define BBL_JSON_KEY_PRINTER_TECH "machine_tech"
+65 -20
View File
@@ -6598,7 +6598,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// now, or deserialized from the vendor's cache; the code is shared so a // now, or deserialized from the vendor's cache; the code is shared so a
// cache-loaded bundle cannot come out different from a JSON-loaded one. // cache-loaded bundle cannot come out different from a JSON-loaded one.
// Resolves `inherits` against the presets loaded before this one // Resolves `inherits` against the presets loaded before this one
// (config_maps) or against base_bundle's filament library, flattens, validates // (config_maps) or against base_bundle's filament library, layers each
// `include` (include_maps) under the preset's own keys, flattens, validates
// and registers the preset. Returns the reason loading failed, empty on // and registers the preset. Returns the reason loading failed, empty on
// success. // success.
std::string PresetBundle::load_vendor_preset( std::string PresetBundle::load_vendor_preset(
@@ -6607,9 +6608,10 @@ std::string PresetBundle::load_vendor_preset(
const PresetBundle* base_bundle, const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags, LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps, std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib, PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs) const std::set<std::string>* retain_configs, const std::set<std::string>* retain_includes)
{ {
const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name);
const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path;
@@ -6652,14 +6654,30 @@ std::string PresetBundle::load_vendor_preset(
return reason; return reason;
} }
} }
else { else
if (presets_collection->type() == Preset::TYPE_PRINTER) default_config = &presets_collection->default_preset_for(entry.config_src).config;
default_config = &presets_collection->default_preset_for(entry.config_src).config;
else
default_config = &presets_collection->default_preset().config;
}
config = *default_config; config = *default_config;
// Layer each included preset's own keys over the parent, in the order listed;
// this preset's own keys go on top.
for (const std::string& name : entry.includes) {
auto it = include_maps.find(name);
if (it == include_maps.end()) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find include " << name << " for " << preset_name;
continue;
}
config.apply(it->second);
}
config.apply(entry.config_src); config.apply(entry.config_src);
// Record what a base states, its diff against the default, for the presets
// that include it. It is taken before extend_default_config_length pads every
// per-variant key to the base's variant count: the padded defaults would
// otherwise override the values each includer inherits.
if (entry.instantiation == "false" && (retain_includes == nullptr || retain_includes->count(preset_name) != 0)) {
DynamicPrintConfig included;
included.apply_only(config, config.diff(presets_collection->default_preset_for(config).config));
include_maps.emplace(preset_name, std::move(included));
}
extend_default_config_length(config, true, *default_config); extend_default_config_length(config, true, *default_config);
if (entry.instantiation == "false" && "Template" != vendor_name) { if (entry.instantiation == "false" && "Template" != vendor_name) {
// Report configuration fields, which are misplaced into a wrong group. // Report configuration fields, which are misplaced into a wrong group.
@@ -7118,6 +7136,15 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
} }
entry.name = key_values[BBL_JSON_KEY_NAME]; entry.name = key_values[BBL_JSON_KEY_NAME];
entry.description = key_values[BBL_JSON_KEY_DESCRIPTION]; entry.description = key_values[BBL_JSON_KEY_DESCRIPTION];
// A file that states no instantiation and is named as G-code, or has no
// name, is a template that is only there to be included. A nameless one
// goes by its name in the vendor index.
if (auto it = key_values.find(BBL_JSON_KEY_INSTANTIATION);
(it == key_values.end() || it->second.empty()) && (entry.name.empty() || entry.name.find("gcode") != std::string::npos)) {
key_values[BBL_JSON_KEY_INSTANTIATION] = "false";
if (entry.name.empty())
entry.name = subfile_iter.first;
}
if(key_values.find(BBL_JSON_KEY_INSTANTIATION) == key_values.end()) if(key_values.find(BBL_JSON_KEY_INSTANTIATION) == key_values.end())
{ {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << entry.name; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << entry.name;
@@ -7146,6 +7173,20 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
return reason; return reason;
} }
} }
if (auto it = key_values.find(BBL_JSON_KEY_INCLUDES); it != key_values.end()) {
// An array of names, or one bare name; load_from_json kept the JSON text.
nlohmann::json includes = nlohmann::json::parse(it->second);
if (!includes.is_array())
includes = nlohmann::json::array({std::move(includes)});
for (const auto& name : includes) {
if (name.is_string())
entry.includes.push_back(name.get<std::string>());
else {
++m_errors;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid include " << name.dump() << " for " << entry.name;
}
}
}
if (key_values.find(ORCA_JSON_KEY_RENAMED_FROM) != key_values.end()) { if (key_values.find(ORCA_JSON_KEY_RENAMED_FROM) != key_values.end()) {
if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], entry.renamed_from)) { if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], entry.renamed_from)) {
BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << dir << "\": The preset \"" << entry.name BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << dir << "\": The preset \"" << entry.name
@@ -7162,7 +7203,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
return reason; return reason;
}; };
std::map<std::string, DynamicPrintConfig> configs; std::map<std::string, DynamicPrintConfig> configs, include_maps;
std::map<std::string, std::string> filament_id_maps; std::map<std::string, std::string> filament_id_maps;
// Orca: whether to (re)write the vendor's cache after this parse, leaving it // Orca: whether to (re)write the vendor's cache after this parse, leaving it
// in step with the profile so the next run reads it instead. It is written // in step with the profile so the next run reads it instead. It is written
@@ -7179,6 +7220,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
auto load_subfiles = [&](std::vector<std::pair<std::string, std::string>>& subfiles, auto load_subfiles = [&](std::vector<std::pair<std::string, std::string>>& subfiles,
std::vector<CachedPreset>& entries, const char* kind, bool is_from_lib = false) { std::vector<CachedPreset>& entries, const char* kind, bool is_from_lib = false) {
configs.clear(); configs.clear();
include_maps.clear();
filament_id_maps.clear(); filament_id_maps.clear();
for (auto& subfile : subfiles) { for (auto& subfile : subfiles) {
CachedPreset entry; CachedPreset entry;
@@ -7186,8 +7228,8 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
if (reason.empty()) { if (reason.empty()) {
const int errors_before_install = m_errors; const int errors_before_install = m_errors;
reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags, reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags,
substitution_context, substitutions, configs, filament_id_maps, presets, substitution_context, substitutions, configs, include_maps, filament_id_maps,
presets_loaded, is_from_lib); presets, presets_loaded, is_from_lib);
install_errors += m_errors - errors_before_install; install_errors += m_errors - errors_before_install;
} }
if (!reason.empty()) { if (!reason.empty()) {
@@ -7982,26 +8024,29 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::s
// parsed), so no substitutions are reported, as before. // parsed), so no substitutions are reported, as before.
ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent }; ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent };
PresetsConfigSubstitutions substitutions; PresetsConfigSubstitutions substitutions;
std::map<std::string, DynamicPrintConfig> configs; std::map<std::string, DynamicPrintConfig> configs, include_maps;
std::map<std::string, std::string> filament_id_maps; std::map<std::string, std::string> filament_id_maps;
const std::string path = boost::filesystem::path(cache_path).parent_path().string(); const std::string path = boost::filesystem::path(cache_path).parent_path().string();
size_t count = 0; size_t count = 0;
auto install_entries = [&](const std::vector<CachedPreset>& entries, PresetCollection* presets, bool is_from_lib) { auto install_entries = [&](const std::vector<CachedPreset>& entries, PresetCollection* presets, bool is_from_lib) {
configs.clear(); configs.clear();
include_maps.clear();
filament_id_maps.clear(); filament_id_maps.clear();
// Only configs of presets that other entries inherit are ever looked // Only configs of presets that other entries inherit or include are
// up again; registering just those skips one full config copy for // ever looked up again; registering just those skips one full config
// every leaf preset. The library's filaments are all retained — they // copy for every leaf preset. The library's filaments are all retained
// become the m_config_maps other vendors resolve against. // — they become the m_config_maps other vendors resolve against.
std::set<std::string> inherited; std::set<std::string> inherited, included;
for (const CachedPreset& entry : entries) for (const CachedPreset& entry : entries) {
if (! entry.inherits.empty()) if (! entry.inherits.empty())
inherited.insert(entry.inherits); inherited.insert(entry.inherits);
included.insert(entry.includes.begin(), entry.includes.end());
}
const std::set<std::string>* retain_configs = is_from_lib ? nullptr : &inherited; const std::set<std::string>* retain_configs = is_from_lib ? nullptr : &inherited;
for (const CachedPreset& entry : entries) { for (const CachedPreset& entry : entries) {
const std::string reason = load_vendor_preset(entry, path, vendor_name, const std::string reason = load_vendor_preset(entry, path, vendor_name,
base_bundle, LoadConfigBundleAttribute::LoadSystem, substitution_context, substitutions, base_bundle, LoadConfigBundleAttribute::LoadSystem, substitution_context, substitutions,
configs, filament_id_maps, presets, count, is_from_lib, retain_configs); configs, include_maps, filament_id_maps, presets, count, is_from_lib, retain_configs, &included);
if (! reason.empty()) if (! reason.empty())
throw std::runtime_error("entry " + entry.name + " failed to install: " + reason); throw std::runtime_error("entry " + entry.name + " failed to install: " + reason);
} }
+12 -10
View File
@@ -634,22 +634,24 @@ private:
// load_vendor_configs_from_json reads a cache. // load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle); bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// Load one source-form preset entry into this bundle: resolve `inherits`, // Load one source-form preset entry into this bundle: resolve `inherits`
// flatten, validate and register the preset. Returns the reason loading // and `include`, flatten, validate and register the preset. Returns the
// failed, empty on success. See the definition for the sharing contract // reason loading failed, empty on success. See the definition for the
// between the JSON parse and the cache load. // sharing contract between the JSON parse and the cache load.
// retain_configs, when non-null, names the only presets registered into // retain_configs / retain_includes, when non-null, name the only presets
// config_maps (a full config copy each). The cache load passes the names its // registered into config_maps / include_maps (a config copy each). The
// entries inherit — the only ones ever looked up again; the JSON parse // cache load passes the names its entries inherit / include — the only
// retains all, not knowing what later subfiles inherit. // ones ever looked up again; the JSON parse retains all, not knowing what
// later subfiles name.
std::string load_vendor_preset(const CachedPreset& entry, std::string load_vendor_preset(const CachedPreset& entry,
const std::string& path, const std::string& vendor_name, const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle, const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags, LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps, std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib, PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs = nullptr); const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
// Clear every collection's m_printer_hold_alias, which reset() leaves alone. // Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases(); void clear_printer_hold_aliases();
+2 -2
View File
@@ -254,7 +254,7 @@ constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
// save_entries below), or a change to the cache's own layout or the // save_entries below), or a change to the cache's own layout or the
// meaning of its stamps. Option-schema drift is NOT such a change — the // meaning of its stamps. Option-schema drift is NOT such a change — the
// dictionary handles it, which is why this no longer moves every release. // dictionary handles it, which is why this no longer moves every release.
constexpr uint32_t CACHE_VERSION = 1; constexpr uint32_t CACHE_VERSION = 2;
// A stamp-string read that refuses an absurd length before allocating anything. // A stamp-string read that refuses an absurd length before allocating anything.
// The stamps are read from files named from the outside (peek_version is // The stamps are read from files named from the outside (peek_version is
@@ -325,7 +325,7 @@ void visit_entry(Archive& ar, Entry& e, ConfigFn&& config)
{ {
ar(e.name, e.sub_path); ar(e.name, e.sub_path);
config(); config();
ar(e.inherits, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from); ar(e.inherits, e.includes, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from);
} }
// The count comes from a file that has already passed magic and CRC, but a // The count comes from a file that has already passed magic and CRC, but a
+8 -6
View File
@@ -107,12 +107,13 @@ void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, con
// comes after. // comes after.
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict); void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
// One preset as its JSON subfile states it: the config diff, the name of the // One preset as its JSON subfile states it: the config diff, the names of the
// preset it inherits, and the parse metadata — everything the parse phase of // preset it inherits and the presets it includes, and the parse metadata —
// load_vendor_configs_from_json extracts and nothing it derives. Inheritance // everything the parse phase of load_vendor_configs_from_json extracts and
// is resolved when the entry is installed, against whatever filament library // nothing it derives. Inheritance and includes are resolved when the entry is
// is loaded then, so a cache carries no other vendor's values and no other // installed, against whatever filament library is loaded then, so a cache
// vendor's update can make it stale. // carries no other vendor's values and no other vendor's update can make it
// stale.
// Written and read by visit_entry in PresetCacheFormat.cpp, which lists every // Written and read by visit_entry in PresetCacheFormat.cpp, which lists every
// field below in this order — once, for the save, the load and the name peek alike. // field below in this order — once, for the save, the load and the name peek alike.
struct CachedPreset struct CachedPreset
@@ -121,6 +122,7 @@ struct CachedPreset
std::string sub_path; // path under the vendor's directory std::string sub_path; // path under the vendor's directory
DynamicPrintConfig config_src; // the preset's own diff, nothing inherited DynamicPrintConfig config_src; // the preset's own diff, nothing inherited
std::string inherits; std::string inherits;
std::vector<std::string> includes; // layered under config_src, in this order
std::string description; std::string description;
std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error
std::string setting_id; std::string setting_id;
+18
View File
@@ -490,6 +490,24 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs); CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
} }
TEST_CASE("load_from_json hands a preset's include list to the caller instead of the config", "[Config]") {
ScopedTemporaryFile tmp(".json");
{
boost::nowide::ofstream ofs(tmp.string());
ofs << R"({"type":"machine","name":"P","instantiation":"true","include":["T start","T end"],"machine_end_gcode":"M84"})";
}
DynamicPrintConfig config;
ConfigSubstitutionContext substitutions(ForwardCompatibilitySubstitutionRule::Disable);
std::map<std::string, std::string> key_values;
std::string reason;
REQUIRE(config.load_from_json(tmp.string(), substitutions, false, key_values, reason) == 0);
CHECK(reason.empty());
CHECK(key_values["include"] == R"(["T start","T end"])");
CHECK_FALSE(config.has("include"));
CHECK(substitutions.unrecogized_keys.empty());
CHECK(config.opt_string("machine_end_gcode") == "M84");
}
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") { TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
DynamicPrintConfig config; DynamicPrintConfig config;
config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
+104 -10
View File
@@ -53,41 +53,64 @@ void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std
} }
// A small but complete vendor: one machine model, one process, a non-instantiated // A small but complete vendor: one machine model, one process, a non-instantiated
// base filament with an instantiated child inheriting it, a second standalone // base filament with an instantiated child that inherits it and includes a
// filament carrying explicit metadata, and one machine preset with a rename — so // dual-extruder template, a second standalone filament carrying explicit
// the equivalence test below sees every CachedPreset field populated. // metadata, and one machine preset with a rename that includes a G-code template
void write_full_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version) // — so the equivalence test below sees every CachedPreset field populated. The
// templates are listed before the presets that include them, as update-index
// orders them, or after them when `templates_last` asks for the broken order.
void write_full_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version,
bool templates_last = false)
{ {
fs::create_directories(dir / vendor / "process"); fs::create_directories(dir / vendor / "process");
fs::create_directories(dir / vendor / "filament"); fs::create_directories(dir / vendor / "filament");
fs::create_directories(dir / vendor / "machine"); fs::create_directories(dir / vendor / "machine");
const std::string filament_template = R"({"name":")" + vendor + R"( dual template","sub_path":"filament/template.json"})";
const std::string filament_presets = R"({"name":")" + vendor + R"( Base PLA","sub_path":"filament/base.json"},)"
R"({"name":")" + vendor + R"( PLA @0.4","sub_path":"filament/pla.json"},)"
R"({"name":")" + vendor + R"( Silk PLA @0.4","sub_path":"filament/silk.json"})";
const std::string machine_template = R"({"name":")" + vendor + R"( 0.4 template machine_start_gcode","sub_path":"machine/start.json"})";
const std::string machine_presets = R"({"name":")" + vendor + R"( 0.4 nozzle","sub_path":"machine/printer.json"})";
std::ofstream((dir / (vendor + ".json")).string()) std::ofstream((dir / (vendor + ".json")).string())
<< R"({"version":")" << version << R"(","name":")" << vendor << R"(",)" << R"({"version":")" << version << R"(","name":")" << vendor << R"(",)"
<< R"("machine_model_list":[{"name":"Test Model","sub_path":"machine/model.json"}],)" << R"("machine_model_list":[{"name":"Test Model","sub_path":"machine/model.json"}],)"
<< R"("process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}],)" << R"("process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}],)"
<< R"("filament_list":[)" << R"("filament_list":[)"
<< R"({"name":")" << vendor << R"( Base PLA","sub_path":"filament/base.json"},)" << (templates_last ? filament_presets + "," + filament_template : filament_template + "," + filament_presets)
<< R"({"name":")" << vendor << R"( PLA @0.4","sub_path":"filament/pla.json"},)" << R"(],"machine_list":[)"
<< R"({"name":")" << vendor << R"( Silk PLA @0.4","sub_path":"filament/silk.json"}],)" << (templates_last ? machine_presets + "," + machine_template : machine_template + "," + machine_presets)
<< R"("machine_list":[{"name":")" << vendor << R"( 0.4 nozzle","sub_path":"machine/printer.json"}]})"; << "]}";
std::ofstream((dir / vendor / "machine" / "model.json").string()) std::ofstream((dir / vendor / "machine" / "model.json").string())
<< R"({"type":"machine_model","name":"Test Model","nozzle_diameter":"0.4"})"; << R"({"type":"machine_model","name":"Test Model","nozzle_diameter":"0.4"})";
std::ofstream((dir / vendor / "process" / "standard.json").string()) std::ofstream((dir / vendor / "process" / "standard.json").string())
<< R"({"type":"process","name":"0.20mm Standard @)" << vendor << R"({"type":"process","name":"0.20mm Standard @)" << vendor
<< R"(","from":"system","instantiation":"true","layer_height":"0.2"})"; << R"(","from":"system","instantiation":"true","layer_height":"0.2"})";
// The base sets two per-variant keys; the template restates one of them for
// two variants and adds a third; the child restates the third.
std::ofstream((dir / vendor / "filament" / "base.json").string()) std::ofstream((dir / vendor / "filament" / "base.json").string())
<< R"({"type":"filament","name":")" << vendor << R"({"type":"filament","name":")" << vendor
<< R"( Base PLA","from":"system","instantiation":"false","filament_id":"GFA_base","filament_cost":"42"})"; << R"( Base PLA","from":"system","instantiation":"false","filament_id":"GFA_base","filament_cost":"42",)"
<< R"("activate_air_filtration":["1"],"filament_max_volumetric_speed":["12"]})";
std::ofstream((dir / vendor / "filament" / "template.json").string())
<< R"({"type":"filament","name":")" << vendor << R"( dual template","from":"system","instantiation":"false",)"
<< R"("filament_extruder_variant":["Direct Drive Standard","Direct Drive High Flow"],)"
<< R"("filament_max_volumetric_speed":["20","22"],"filament_flush_temp":["0","0"]})";
std::ofstream((dir / vendor / "filament" / "pla.json").string()) std::ofstream((dir / vendor / "filament" / "pla.json").string())
<< R"({"type":"filament","name":")" << vendor << R"({"type":"filament","name":")" << vendor
<< R"( PLA @0.4","from":"system","instantiation":"true","filament_id":"GFA00","filament_cost":"20",)" << R"( PLA @0.4","from":"system","instantiation":"true","filament_id":"GFA00","filament_cost":"20",)"
<< R"("setting_id":"GFSA04","description":"Test PLA description"})"; << R"("setting_id":"GFSA04","description":"Test PLA description"})";
std::ofstream((dir / vendor / "filament" / "silk.json").string()) std::ofstream((dir / vendor / "filament" / "silk.json").string())
<< R"({"type":"filament","name":")" << vendor << R"({"type":"filament","name":")" << vendor
<< R"( Silk PLA @0.4","from":"system","instantiation":"true","inherits":")" << vendor << R"( Base PLA"})"; << R"( Silk PLA @0.4","from":"system","instantiation":"true","inherits":")" << vendor << R"( Base PLA",)"
<< R"("include":[")" << vendor << R"( dual template"],"filament_flush_temp":["5","5"]})";
// The template states two G-codes; the printer restates one of them.
std::ofstream((dir / vendor / "machine" / "start.json").string())
<< R"({"type":"machine","name":")" << vendor << R"( 0.4 template machine_start_gcode","from":"system",)"
<< R"("instantiation":"false","machine_start_gcode":"G28 ; template","machine_end_gcode":"M84 ; template"})";
std::ofstream((dir / vendor / "machine" / "printer.json").string()) std::ofstream((dir / vendor / "machine" / "printer.json").string())
<< R"({"type":"machine","name":")" << vendor << R"({"type":"machine","name":")" << vendor
<< R"( 0.4 nozzle","from":"system","instantiation":"true","printer_model":"Test Model","printer_variant":"0.4",)" << R"( 0.4 nozzle","from":"system","instantiation":"true","printer_model":"Test Model","printer_variant":"0.4",)"
<< R"("include":[")" << vendor << R"( 0.4 template machine_start_gcode"],"machine_end_gcode":"M84 ; own",)"
<< R"("renamed_from":")" << vendor << R"( old 0.4 nozzle"})"; << R"("renamed_from":")" << vendor << R"( old 0.4 nozzle"})";
} }
@@ -598,6 +621,7 @@ TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", "
const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false); const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr); REQUIRE(pr != nullptr);
CHECK(pr->renamed_from == std::vector<std::string>{"Acme old 0.4 nozzle"}); CHECK(pr->renamed_from == std::vector<std::string>{"Acme old 0.4 nozzle"});
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // through the include
} }
TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]") TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]")
@@ -1618,3 +1642,73 @@ TEST_CASE("a stamp string with an absurd length is rejected, not allocated", "[V
CHECK(VendorCacheFile::peek_version(cache, "Evil").empty()); CHECK(VendorCacheFile::peek_version(cache, "Evil").empty());
} }
TEST_CASE("an included template's keys land on the preset, between the parent's and its own", "[VendorCache]")
{
InstallDirs dirs;
write_full_vendor_tree(dirs.system, "Acme", "1.0.0");
PresetBundle bundle;
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(bundle.error_count() == 0);
const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr);
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // from the include
CHECK(pr->config.opt_string("machine_end_gcode") == "M84 ; own"); // the preset's own key wins
CHECK(presets_for(bundle.printers, "Acme").size() == 1); // the template is no preset
const Preset* silk = bundle.filaments.find_preset("Acme Silk PLA @0.4", false);
REQUIRE(silk != nullptr);
const auto* speed = silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed");
REQUIRE(speed != nullptr);
CHECK(speed->values == std::vector<double>{20., 22.}); // the include wins over the parent
const auto* flush = silk->config.option<ConfigOptionInts>("filament_flush_temp");
REQUIRE(flush != nullptr);
CHECK(flush->values == std::vector<int>{5, 5}); // the preset's own key wins
// A per-variant key the template never mentions keeps the parent's value.
// The loader pads every base to its variant count; an include taken from the
// padded copy would carry the padded default [0,0] over the parent's 1.
const auto* air = silk->config.option<ConfigOptionBools>("activate_air_filtration");
REQUIRE(air != nullptr);
CHECK(air->values == std::vector<unsigned char>{1, 1});
CHECK(presets_for(bundle.filaments, "Acme").size() == 2); // the template is no preset
}
TEST_CASE("an include listed after the preset that names it is an error, and the preset loads without it", "[VendorCache]")
{
InstallDirs dirs;
write_full_vendor_tree(dirs.system, "Acme", "1.0.0", /*templates_last=*/true);
PresetBundle bundle;
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
// One error per unresolved include: the printer's and the filament's.
CHECK(bundle.error_count() == 2);
const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr);
CHECK(pr->config.opt_string("machine_start_gcode") != "G28 ; template");
const Preset* silk = bundle.filaments.find_preset("Acme Silk PLA @0.4", false);
REQUIRE(silk != nullptr);
CHECK(silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>{12.});
}
TEST_CASE("a G-code template that states no instantiation is included, not loaded as a preset", "[VendorCache]")
{
InstallDirs dirs;
write_full_vendor_tree(dirs.system, "Acme", "1.0.0");
const fs::path start = dirs.system / "Acme" / "machine" / "start.json";
SECTION("named as G-code") {
std::ofstream(start.string()) << R"({"type":"machine","name":"Acme 0.4 template machine_start_gcode","from":"system",)"
<< R"("machine_start_gcode":"G28 ; template"})";
}
SECTION("not named, so included by its name in the vendor index") {
std::ofstream(start.string()) << R"({"type":"machine","from":"system","machine_start_gcode":"G28 ; template"})";
}
PresetBundle bundle;
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(bundle.error_count() == 0);
const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr);
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template");
CHECK(presets_for(bundle.printers, "Acme").size() == 1);
}