diff --git a/docs/HLSD/preset-cache.md b/docs/HLSD/preset-cache.md index 6e693dbd6f..666e452f1e 100644 --- a/docs/HLSD/preset-cache.md +++ b/docs/HLSD/preset-cache.md @@ -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 that resolving it derives — the preset's own config diff, the name of the preset it -inherits, and the parse metadata (name, sub-path, description, instantiation, setting -and filament ids, renames). Non-instantiated base presets are stored too; the children -that inherit from them cannot resolve without them. +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 @@ -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 boundary: any vendor's filament may inherit from the shared Orca filament library, -and nothing else reaches across vendors. The library therefore goes first, alone; -every other vendor follows in parallel, resolving against it; and the results are -merged in a stable order: +and nothing else reaches across vendors — an `include` is always vendor-local. The +library therefore goes first, alone; every other vendor follows in parallel, resolving +against it; and the results are merged in a stable order: ```mermaid flowchart LR @@ -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 then installed one by one — inheritance resolved against the presets installed before -them and the currently loaded filament library, configs flattened onto the collection -defaults, validated and registered — by the same function the JSON path calls straight -after parsing a sub-file. The two paths share everything below the parse, which is what -makes a cache-loaded bundle indistinguishable from a JSON-loaded one by construction -rather than by test coverage. Installation also rebuilds each preset's file path from -the local data directory, so a shipped cache never carries the generating machine's -paths. +them and the currently loaded filament library, includes layered in, configs flattened +onto the collection defaults, validated and registered — by the same function the JSON +path calls straight after parsing a sub-file. 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. 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 diff --git a/resources/profiles/BBL.json b/resources/profiles/BBL.json index 3b52c1e22e..44354ce026 100644 --- a/resources/profiles/BBL.json +++ b/resources/profiles/BBL.json @@ -1,7 +1,7 @@ { "name": "Bambulab", "url": "http://www.bambulab.com/Parameters/vendor/BBL.json", - "version": "02.08.00.09", + "version": "02.08.00.10", "force_update": "0", "description": "the initial version of BBL configurations", "machine_model_list": [ @@ -67,6 +67,22 @@ "name": "fdm_filament_common", "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", "sub_path": "filament/fdm_filament_abs.json" @@ -8246,261 +8262,9 @@ { "name": "Generic TPU @BBL H2S", "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": [ - { - "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", "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", "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": [ diff --git a/scripts/orca_profile_tool.py b/scripts/orca_profile_tool.py index c41c2ba7fc..29863f8d1a 100755 --- a/scripts/orca_profile_tool.py +++ b/scripts/orca_profile_tool.py @@ -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 `inherits` in the vendor map with OFL base-bundle fallback. Values are list 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: seen = set() @@ -887,8 +888,8 @@ def _vendor_json_files(vendor_path): def check_preset_name_uniqueness(profiles_dir, vendor): """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 - (PresetBundle.cpp load_subfiles), and std::map::emplace keeps the first + The loader resolves "inherits" and "include" through per-type maps of the bundle's + 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 wins is nothing but index order. An unindexed twin counts too - it is one 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 first, keys the slicer no longer reads gone, filament options that are vectors - written as vectors - and a .json's canonical lists, ordered parents-first - so the loader resolves every "inherits" in one pass. Running them over a - contributed bundle has to be a no-op; where it would not be, the file that was - reviewed is not the file that ships, and the next maintainer to run normalize - carries an unrelated diff into their own change. + written as vectors - and a .json's canonical lists, ordered + dependencies-first so the loader resolves every "inherits" and "include" in one + pass. Running them over a contributed bundle has to be a no-op; where it would + not be, the file that was reviewed is not the file that ships, and the next + 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 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 is reported and kept: never delete what could not be read. - An unindexed file that some surviving profile names in "inherits" is kept too, - and reported, UNLESS an indexed profile already carries that name: "inherits" is - resolved by preset name, so the indexed one is the parent every child actually + An unindexed file that some surviving profile names in "inherits" or "include" is + kept too, and reported, UNLESS an indexed profile already carries that name: both + 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 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 @@ -2069,7 +2070,7 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None, listed.add(posixpath.normpath(sub_path.replace("\\", "/"))) 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 for sub in subs: 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): continue if sub_path in listed or profile.get("type") not in PROFILE_TYPES: - if profile.get("inherits"): - inherited.add(profile["inherits"]) + inherited.update(profile_dependencies(profile)) if sub_path in listed and profile.get("name"): provided[profile["name"]] = sub_path continue @@ -2104,8 +2104,7 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None, for path, profile in rescued.items(): del candidates[path] kept[path] = profile - if profile.get("inherits"): - inherited.add(profile["inherits"]) + inherited.update(profile_dependencies(profile)) for path in sorted(kept): 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 # --------------------------------------------------------------------------- -def topological_sort(profiles): - """Order index entries parents-first, so the loader resolves inherits in one pass. +def profile_dependencies(profile): + """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) in_degree = defaultdict(int) by_name = {p["name"]: p for p in profiles} all_names = set(by_name) - placed = set() for profile in profiles: - parent = profile.get("inherits") child = profile["name"] - if parent in all_names: - graph[parent].append(child) - in_degree[child] += 1 - in_degree.setdefault(parent, 0) - placed.add(child) - placed.add(parent) + for parent in profile_dependencies(profile): + if parent in all_names: + graph[parent].append(child) + in_degree[child] += 1 + in_degree.setdefault(parent, 0) queue = sorted(name for name, degree in in_degree.items() if degree == 0) result = [] while queue: current = queue.pop(0) result.append(by_name[current]) - placed.add(current) for child in sorted(graph[current]): in_degree[child] -= 1 if in_degree[child] == 0: 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 @@ -2225,15 +2232,15 @@ def build_index_sections(profiles_dir, vendor, profile_types=None): "name": name, "sub_path": os.path.relpath(path, vendor_dir).replace(os.sep, "/"), } - if profile.get("inherits"): - entry["inherits"] = profile["inherits"] + for key in ("inherits", "include"): + if profile.get(key): + entry[key] = profile[key] by_name[name].append(entry["sub_path"]) entries.append(entry) - sorted_entries = topological_sort(entries) - for entry in sorted_entries: - entry.pop("inherits", None) # ordering input only, not part of the index - sections[profile_type + "_list"] = sorted_entries + # inherits/include were ordering input only; the index holds name and sub_path + sections[profile_type + "_list"] = [{"name": e["name"], "sub_path": e["sub_path"]} + for e in topological_sort(entries)] for rel, found in sorted(unplaceable.items()): 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], "regenerate the *_list sections of .json", "Rebuild the *_list sections of each .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" "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" diff --git a/scripts/tests/test_profile_tool.py b/scripts/tests/test_profile_tool.py index f9a7634471..5359b1158b 100644 --- a/scripts/tests/test_profile_tool.py +++ b/scripts/tests/test_profile_tool.py @@ -411,6 +411,26 @@ class TestUpdateIndex(TreeCase): for entry in self.t.read_index("V")["filament_list"]: 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): self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"}) self.t.write("V", "filament/B.json", {"name": "B"}) diff --git a/src/dev-utils/CMakeLists.txt b/src/dev-utils/CMakeLists.txt index 2cfce6a7c5..e718da9e83 100644 --- a/src/dev-utils/CMakeLists.txt +++ b/src/dev-utils/CMakeLists.txt @@ -28,6 +28,12 @@ if (ORCA_TOOLS) target_link_libraries(generate_system_cache libslic3r boost_headeronly) 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() # Function that adds source file encoding check to a target diff --git a/src/dev-utils/profile_include_dump.cpp b/src/dev-utils/profile_include_dump.cpp new file mode 100644 index 0000000000..f2486dedfc --- /dev/null +++ b/src/dev-utils/profile_include_dump.cpp @@ -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 +#include +#include "nlohmann/json.hpp" + +#include +#include +#include +#include +#include +#include + +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 includes; + std::vector keys; +}; + +// Sub-files by name, per section of the vendor index: `inherits` and `include` +// both resolve within a section. +using Sections = std::map>; + +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 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()); + // A file without a name goes by its name in the index. + Entry &entry = sections[section][file.value("name", item.at("name").get())]; + 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()); + else + for (const json &name : *include) + entry.includes.push_back(name.get()); + } + 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 §ion) +{ + 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 §ions, std::ostream &out) +{ + size_t dumped = 0; + for (const auto &[section, entries] : sections) { + std::vector 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 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 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) : "") << "\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 .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 -o [-v ] [-l ] [-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; +} diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index c8d816b3a4..42d6970ad1 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -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)) { 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)) { key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value()); } else { diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index c66518e29d..5b32d90ac6 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -62,6 +62,7 @@ #define ORCA_JSON_KEY_UPDATE_TIME "updated_time" #define ORCA_JSON_KEY_CREATED_TIME "created_time" #define BBL_JSON_KEY_INHERITS "inherits" +#define BBL_JSON_KEY_INCLUDES "include" #define BBL_JSON_KEY_INSTANTIATION "instantiation" #define BBL_JSON_KEY_NOZZLE_DIAMETER "nozzle_diameter" #define BBL_JSON_KEY_PRINTER_TECH "machine_tech" diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index dc731b63c4..c8fe00b17c 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -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 // cache-loaded bundle cannot come out different from a JSON-loaded 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 // success. std::string PresetBundle::load_vendor_preset( @@ -6607,9 +6608,10 @@ std::string PresetBundle::load_vendor_preset( const PresetBundle* base_bundle, LoadConfigBundleAttributes flags, ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, - std::map& config_maps, std::map& filament_id_maps, + std::map& config_maps, std::map& include_maps, + std::map& filament_id_maps, PresetCollection* presets_collection, size_t& count, bool is_from_lib, - const std::set* retain_configs) + const std::set* retain_configs, const std::set* retain_includes) { const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; @@ -6652,14 +6654,30 @@ std::string PresetBundle::load_vendor_preset( return reason; } } - else { - if (presets_collection->type() == Preset::TYPE_PRINTER) - default_config = &presets_collection->default_preset_for(entry.config_src).config; - else - default_config = &presets_collection->default_preset().config; - } + else + default_config = &presets_collection->default_preset_for(entry.config_src).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); + // 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); if (entry.instantiation == "false" && "Template" != vendor_name) { // Report configuration fields, which are misplaced into a wrong group. @@ -7118,6 +7136,15 @@ std::pair PresetBundle::load_vendor_configs_ } entry.name = key_values[BBL_JSON_KEY_NAME]; 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()) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << entry.name; @@ -7146,6 +7173,20 @@ std::pair PresetBundle::load_vendor_configs_ 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()); + 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 (!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 @@ -7162,7 +7203,7 @@ std::pair PresetBundle::load_vendor_configs_ return reason; }; - std::map configs; + std::map configs, include_maps; std::map filament_id_maps; // 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 @@ -7179,6 +7220,7 @@ std::pair PresetBundle::load_vendor_configs_ auto load_subfiles = [&](std::vector>& subfiles, std::vector& entries, const char* kind, bool is_from_lib = false) { configs.clear(); + include_maps.clear(); filament_id_maps.clear(); for (auto& subfile : subfiles) { CachedPreset entry; @@ -7186,8 +7228,8 @@ std::pair PresetBundle::load_vendor_configs_ if (reason.empty()) { const int errors_before_install = m_errors; reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags, - substitution_context, substitutions, configs, filament_id_maps, presets, - presets_loaded, is_from_lib); + substitution_context, substitutions, configs, include_maps, filament_id_maps, + presets, presets_loaded, is_from_lib); install_errors += m_errors - errors_before_install; } 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. ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent }; PresetsConfigSubstitutions substitutions; - std::map configs; + std::map configs, include_maps; std::map filament_id_maps; const std::string path = boost::filesystem::path(cache_path).parent_path().string(); size_t count = 0; auto install_entries = [&](const std::vector& entries, PresetCollection* presets, bool is_from_lib) { configs.clear(); + include_maps.clear(); filament_id_maps.clear(); - // Only configs of presets that other entries inherit are ever looked - // up again; registering just those skips one full config copy for - // every leaf preset. The library's filaments are all retained — they - // become the m_config_maps other vendors resolve against. - std::set inherited; - for (const CachedPreset& entry : entries) + // Only configs of presets that other entries inherit or include are + // ever looked up again; registering just those skips one full config + // copy for every leaf preset. The library's filaments are all retained + // — they become the m_config_maps other vendors resolve against. + std::set inherited, included; + for (const CachedPreset& entry : entries) { if (! entry.inherits.empty()) inherited.insert(entry.inherits); + included.insert(entry.includes.begin(), entry.includes.end()); + } const std::set* retain_configs = is_from_lib ? nullptr : &inherited; for (const CachedPreset& entry : entries) { const std::string reason = load_vendor_preset(entry, path, vendor_name, 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()) throw std::runtime_error("entry " + entry.name + " failed to install: " + reason); } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 5a8f34e706..741f6069a4 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -634,22 +634,24 @@ private: // 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); - // Load one source-form preset entry into this bundle: resolve `inherits`, - // flatten, validate and register the preset. Returns the reason loading - // failed, empty on success. See the definition for the sharing contract - // between the JSON parse and the cache load. - // retain_configs, when non-null, names the only presets registered into - // config_maps (a full config copy each). The cache load passes the names its - // entries inherit — the only ones ever looked up again; the JSON parse - // retains all, not knowing what later subfiles inherit. + // Load one source-form preset entry into this bundle: resolve `inherits` + // and `include`, flatten, validate and register the preset. Returns the + // reason loading failed, empty on success. See the definition for the + // sharing contract between the JSON parse and the cache load. + // retain_configs / retain_includes, when non-null, name the only presets + // registered into config_maps / include_maps (a config copy each). The + // cache load passes the names its entries inherit / include — the only + // ones ever looked up again; the JSON parse retains all, not knowing what + // later subfiles name. std::string load_vendor_preset(const CachedPreset& entry, const std::string& path, const std::string& vendor_name, const PresetBundle* base_bundle, LoadConfigBundleAttributes flags, ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, - std::map& config_maps, std::map& filament_id_maps, + std::map& config_maps, std::map& include_maps, + std::map& filament_id_maps, PresetCollection* presets_collection, size_t& count, bool is_from_lib, - const std::set* retain_configs = nullptr); + const std::set* retain_configs = nullptr, const std::set* retain_includes = nullptr); // Clear every collection's m_printer_hold_alias, which reset() leaves alone. void clear_printer_hold_aliases(); diff --git a/src/libslic3r/PresetCacheFormat.cpp b/src/libslic3r/PresetCacheFormat.cpp index accebba7b4..1eca878fe2 100644 --- a/src/libslic3r/PresetCacheFormat.cpp +++ b/src/libslic3r/PresetCacheFormat.cpp @@ -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 // 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. -constexpr uint32_t CACHE_VERSION = 1; +constexpr uint32_t CACHE_VERSION = 2; // 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 @@ -325,7 +325,7 @@ void visit_entry(Archive& ar, Entry& e, ConfigFn&& config) { ar(e.name, e.sub_path); 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 diff --git a/src/libslic3r/PresetCacheFormat.hpp b/src/libslic3r/PresetCacheFormat.hpp index b200ec9911..2c90c07882 100644 --- a/src/libslic3r/PresetCacheFormat.hpp +++ b/src/libslic3r/PresetCacheFormat.hpp @@ -107,12 +107,13 @@ void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, con // comes after. void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict); -// One preset as its JSON subfile states it: the config diff, the name of the -// preset it inherits, and the parse metadata — everything the parse phase of -// load_vendor_configs_from_json extracts and nothing it derives. Inheritance -// is resolved when the entry is installed, against whatever filament library -// is loaded then, so a cache carries no other vendor's values and no other -// vendor's update can make it stale. +// One preset as its JSON subfile states it: the config diff, the names of the +// preset it inherits and the presets it includes, and the parse metadata — +// everything the parse phase of load_vendor_configs_from_json extracts and +// nothing it derives. Inheritance and includes are resolved when the entry is +// installed, against whatever filament library is loaded then, so a cache +// 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 // field below in this order — once, for the save, the load and the name peek alike. struct CachedPreset @@ -121,6 +122,7 @@ struct CachedPreset std::string sub_path; // path under the vendor's directory DynamicPrintConfig config_src; // the preset's own diff, nothing inherited std::string inherits; + std::vector includes; // layered under config_src, in this order std::string description; std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error std::string setting_id; diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 208bbc6cf0..0da7b7bafe 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -490,6 +490,24 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[ CHECK(reloaded.option("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 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]") { DynamicPrintConfig config; config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); diff --git a/tests/libslic3r/test_vendor_cache.cpp b/tests/libslic3r/test_vendor_cache.cpp index 85e100f4d4..93d2b2e87a 100644 --- a/tests/libslic3r/test_vendor_cache.cpp +++ b/tests/libslic3r/test_vendor_cache.cpp @@ -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 -// base filament with an instantiated child inheriting it, a second standalone -// filament carrying explicit metadata, and one machine preset with a rename — so -// the equivalence test below sees every CachedPreset field populated. -void write_full_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version) +// base filament with an instantiated child that inherits it and includes a +// dual-extruder template, a second standalone filament carrying explicit +// metadata, and one machine preset with a rename that includes a G-code template +// — 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 / "filament"); 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()) << R"({"version":")" << version << R"(","name":")" << vendor << R"(",)" << 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"("filament_list":[)" - << 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"}],)" - << R"("machine_list":[{"name":")" << vendor << R"( 0.4 nozzle","sub_path":"machine/printer.json"}]})"; + << (templates_last ? filament_presets + "," + filament_template : filament_template + "," + filament_presets) + << R"(],"machine_list":[)" + << (templates_last ? machine_presets + "," + machine_template : machine_template + "," + machine_presets) + << "]}"; std::ofstream((dir / vendor / "machine" / "model.json").string()) << R"({"type":"machine_model","name":"Test Model","nozzle_diameter":"0.4"})"; std::ofstream((dir / vendor / "process" / "standard.json").string()) << R"({"type":"process","name":"0.20mm Standard @)" << vendor << 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()) << 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()) << R"({"type":"filament","name":")" << vendor << R"( PLA @0.4","from":"system","instantiation":"true","filament_id":"GFA00","filament_cost":"20",)" << R"("setting_id":"GFSA04","description":"Test PLA description"})"; std::ofstream((dir / vendor / "filament" / "silk.json").string()) << 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()) << R"({"type":"machine","name":")" << vendor << 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"})"; } @@ -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); REQUIRE(pr != nullptr); CHECK(pr->renamed_from == std::vector{"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]") @@ -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()); } +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("filament_max_volumetric_speed"); + REQUIRE(speed != nullptr); + CHECK(speed->values == std::vector{20., 22.}); // the include wins over the parent + const auto* flush = silk->config.option("filament_flush_temp"); + REQUIRE(flush != nullptr); + CHECK(flush->values == std::vector{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("activate_air_filtration"); + REQUIRE(air != nullptr); + CHECK(air->values == std::vector{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("filament_max_volumetric_speed")->values == std::vector{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); +}