diff --git a/.claude/skills/orca-profiles/references/extruder-variants.md b/.claude/skills/orca-profiles/references/extruder-variants.md index 322d79d56f..5059fd5c6b 100644 --- a/.claude/skills/orca-profiles/references/extruder-variants.md +++ b/.claude/skills/orca-profiles/references/extruder-variants.md @@ -189,8 +189,7 @@ the presets to a base is in [shared-bases.md](shared-bases.md#variant-arrays-on- 2. Every key in `filament_options_with_variant` ([the full list](#the-four-key-sets)) that reaches the preset, whether written, included or inherited, is resized to its variant count: write it at exactly that width, or leave it out. Keys outside the set - (`filament_type`, plate temperatures, `fan_max_speed`, `slow_down_min_speed`, `pressure_advance`, - …) are never addressed by variant index; the preset contributes their first value however wide a + (`filament_type`, plate temperatures, `fan_max_speed`, `slow_down_min_speed`, …) are never addressed by variant index; the preset contributes their first value however wide a file writes them. 3. Cover every variant the material is meant to print on across its `compatible_printers`. Leave out a variant deliberately when the material should not be tuned for it (e.g. a TPU High Flow variant @@ -455,7 +454,7 @@ no set: the variant-length resize leaves it alone, and only the pair rebuild of last for contrast; it is not a variant set. `check` and `fix-variant` read the four sets from `PrintConfig.cpp` on every run, so they follow the -engine. The lists below are from the 2026-09-25 checkout; regenerate them from the repository root +engine. The lists below are from the 2026-09-29 checkout; regenerate them from the repository root before relying on them (the recipe strips comments, since an initializer can carry a commented-out entry): ```bash @@ -478,7 +477,7 @@ EOF **`print_options_with_variant`**, process, stride 1 (45): `bridge_acceleration`, `bridge_speed`, `default_acceleration`, `default_jerk`, `default_junction_deviation`, `enable_overhang_speed`, `gap_infill_speed`, `infill_jerk`, `initial_layer_acceleration`, `initial_layer_infill_speed`, `initial_layer_jerk`, `initial_layer_speed`, `initial_layer_travel_acceleration`, `initial_layer_travel_jerk`, `initial_layer_travel_speed`, `inner_wall_acceleration`, `inner_wall_jerk`, `inner_wall_speed`, `internal_bridge_speed`, `internal_solid_infill_acceleration`, `internal_solid_infill_speed`, `outer_wall_acceleration`, `outer_wall_jerk`, `outer_wall_speed`, `overhang_1_4_speed`, `overhang_2_4_speed`, `overhang_3_4_speed`, `overhang_4_4_speed`, `print_extruder_id`, `print_extruder_variant`, `slowdown_for_curled_perimeters`, `small_perimeter_speed`, `small_perimeter_threshold`, `sparse_infill_acceleration`, `sparse_infill_speed`, `support_interface_speed`, `support_speed`, `top_solid_infill_flow_ratio`, `top_surface_acceleration`, `top_surface_jerk`, `top_surface_speed`, `travel_acceleration`, `travel_jerk`, `travel_speed`, `travel_speed_z` -**`filament_options_with_variant`**, filament, stride 1 (48): `activate_air_filtration`, `activate_air_filtration_during_print`, `activate_air_filtration_on_completion`, `complete_print_exhaust_fan_speed`, `during_print_exhaust_fan_speed`, `filament_adaptive_volumetric_speed`, `filament_cooling_before_tower`, `filament_deretraction_speed`, `filament_extruder_variant`, `filament_flow_ratio`, `filament_flush_temp`, `filament_flush_volumetric_speed`, `filament_ironing_flow`, `filament_ironing_inset`, `filament_ironing_spacing`, `filament_ironing_speed`, `filament_long_retractions_when_cut`, `filament_max_volumetric_speed`, `filament_pre_cooling_temperature`, `filament_pre_cooling_temperature_nc`, `filament_preheat_temperature_delta`, `filament_ramming_travel_time`, `filament_ramming_travel_time_nc`, `filament_ramming_volumetric_speed`, `filament_ramming_volumetric_speed_nc`, `filament_retract_after_wipe`, `filament_retract_before_wipe`, `filament_retract_length_nc`, `filament_retract_length_toolchange`, `filament_retract_lift_above`, `filament_retract_lift_below`, `filament_retract_lift_enforce`, `filament_retract_restart_extra`, `filament_retract_restart_extra_toolchange`, `filament_retract_when_changing_layer`, `filament_retraction_distances_when_cut`, `filament_retraction_length`, `filament_retraction_minimum_travel`, `filament_retraction_speed`, `filament_wipe`, `filament_wipe_distance`, `filament_z_hop`, `filament_z_hop_types`, `long_retractions_when_ec`, `nozzle_temperature`, `nozzle_temperature_initial_layer`, `retraction_distances_when_ec`, `volumetric_speed_coefficients` +**`filament_options_with_variant`**, filament, stride 1 (54): `activate_air_filtration`, `activate_air_filtration_during_print`, `activate_air_filtration_on_completion`, `adaptive_pressure_advance`, `adaptive_pressure_advance_bridges`, `adaptive_pressure_advance_model`, `adaptive_pressure_advance_overhangs`, `complete_print_exhaust_fan_speed`, `during_print_exhaust_fan_speed`, `enable_pressure_advance`, `filament_adaptive_volumetric_speed`, `filament_cooling_before_tower`, `filament_deretraction_speed`, `filament_extruder_variant`, `filament_flow_ratio`, `filament_flush_temp`, `filament_flush_volumetric_speed`, `filament_ironing_flow`, `filament_ironing_inset`, `filament_ironing_spacing`, `filament_ironing_speed`, `filament_long_retractions_when_cut`, `filament_max_volumetric_speed`, `filament_pre_cooling_temperature`, `filament_pre_cooling_temperature_nc`, `filament_preheat_temperature_delta`, `filament_ramming_travel_time`, `filament_ramming_travel_time_nc`, `filament_ramming_volumetric_speed`, `filament_ramming_volumetric_speed_nc`, `filament_retract_after_wipe`, `filament_retract_before_wipe`, `filament_retract_length_nc`, `filament_retract_length_toolchange`, `filament_retract_lift_above`, `filament_retract_lift_below`, `filament_retract_lift_enforce`, `filament_retract_restart_extra`, `filament_retract_restart_extra_toolchange`, `filament_retract_when_changing_layer`, `filament_retraction_distances_when_cut`, `filament_retraction_length`, `filament_retraction_minimum_travel`, `filament_retraction_speed`, `filament_wipe`, `filament_wipe_distance`, `filament_z_hop`, `filament_z_hop_types`, `long_retractions_when_ec`, `nozzle_temperature`, `nozzle_temperature_initial_layer`, `pressure_advance`, `retraction_distances_when_ec`, `volumetric_speed_coefficients` **`printer_extruder_options`**, machine, one value per extruder, not a variant set (8): `default_nozzle_volume_type`, `extruder_max_nozzle_count`, `extruder_printable_area`, diff --git a/.claude/skills/orca-profiles/references/filament-profiles.md b/.claude/skills/orca-profiles/references/filament-profiles.md index bc80176eaa..0ec5eca24e 100644 --- a/.claude/skills/orca-profiles/references/filament-profiles.md +++ b/.claude/skills/orca-profiles/references/filament-profiles.md @@ -244,10 +244,10 @@ preset for a 0.2 nozzle, explicitly revisit flow limits; do not infer a pressure required direction of change, from diameter alone. On a printer with extruder variants, a filament tunes these per variant too: -`filament_max_volumetric_speed`, `filament_flow_ratio`, `nozzle_temperature` and the retraction -overrides carry one value per variant of `filament_extruder_variant` (Standard, High Flow, …). The +`filament_max_volumetric_speed`, `filament_flow_ratio`, `nozzle_temperature`, pressure advance and the +retraction overrides carry one value per variant of `filament_extruder_variant` (Standard, High Flow, …). The exact key set is [`filament_options_with_variant`](extruder-variants.md#the-four-key-sets); -`slow_down_min_speed` and `pressure_advance` are not in it. Keep every such array at exactly that width, even where the +`slow_down_min_speed` and `fan_max_speed` are not in it. Keep every such array at exactly that width, even where the setting does not differ per variant, and measure the High Flow variant rather than copying Standard ([extruder-variants.md](extruder-variants.md#filament)). diff --git a/.github/workflows/ofl-ota-cronjob.yml b/.github/workflows/ofl-ota-cronjob.yml index 9d027190ae..501508eafa 100644 --- a/.github/workflows/ofl-ota-cronjob.yml +++ b/.github/workflows/ofl-ota-cronjob.yml @@ -146,7 +146,7 @@ jobs: "repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \ -f branch="$branch" -f event=workflow_dispatch -f per_page=100)" run_id="$(jq -r --arg marker "[OFL cron $dispatch_id]" \ - '[.workflow_runs[] | select((.display_title // "") | contains($marker))] \ + '[.workflow_runs[] | select((.display_title // "") | contains($marker))] | sort_by(.created_at) | last | .id // empty' <<< "$runs_json")" [ -n "$run_id" ] && break sleep 5 diff --git a/docs/HLSD/preset-cache.md b/docs/HLSD/preset-cache.md index 666e452f1e..aa39d522a0 100644 --- a/docs/HLSD/preset-cache.md +++ b/docs/HLSD/preset-cache.md @@ -28,8 +28,8 @@ Per-vendor granularity is what makes the system practical: - A vendor whose profile is bumped invalidates only its own cache. The other 60-odd vendors keep theirs — even when the bumped vendor is the shared Orca filament library everyone else inherits from. -- The setup wizard, which loads vendors one at a time, gets the same speedup as - startup without a second code path. +- The setup wizard loads its vendors through the same routine as startup, so it + gets the same speedup without a second code path. - A vendor with no cache, or a broken one, costs only that vendor a parse. A cache holds *system* presets only. User presets, project settings and modified @@ -162,15 +162,20 @@ 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 — 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: +and nothing else reaches across vendors — an `include` is always vendor-local. Only +installing a vendor's presets crosses it; reading the vendor, from its cache or its +JSONs, needs nothing from the library. So every other vendor is read while the library +loads, each is installed against it as soon as both are done, and the results are +merged in a stable order: ```mermaid flowchart LR - lib["1 · OrcaFilamentLibrary
loaded first, synchronously"] --> par["2 · every other vendor in parallel,
each into its own bundle, filaments
resolving against the loaded library"] --> merge["3 · bundles merged into one,
sequentially, in stable vendor order"] + lib["1 · OrcaFilamentLibrary loaded;
meanwhile every other vendor read
from its cache or its JSONs"] --> par["2 · every other vendor installed
in parallel, each into its own bundle,
filaments resolving against the library"] --> merge["3 · bundles merged into one,
in one pass per collection,
in stable vendor order"] ``` +`PresetBundle::load_vendors` runs these steps for startup and for the setup wizard, +which hand it the vendors to load and the directory each is installed in. + Whether a vendor comes from its cache or from a parse changes nothing in that order — both produce the same bundle, so cached and parsed vendors mix freely in one startup. @@ -211,11 +216,12 @@ shipped cache answered first, so the profile in `/system/` was never p 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, 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 +then installed by `install_vendor`, the routine the JSON path hands the vendor's entries +to once it has parsed the sub-files: inheritance resolved against the presets installed +before them and the currently loaded filament library, includes layered in, configs +flattened onto the collection defaults, validated and registered. An `include` layers +what the included base states, between the parent and the preset's own keys: the base's +diff against the default, taken when the base itself was installed and before the per-variant padding `inherits` sees, so only what a template sets reaches the presets including it. The two paths share everything below the parse, which is what makes a cache-loaded bundle @@ -223,6 +229,16 @@ indistinguishable from a JSON-loaded one by construction rather than by test cov Installation also rebuilds each preset's file path from the local data directory, so a shipped cache never carries the generating machine's paths. +Installing an entry is split in two. `resolve_vendor_preset` flattens it, reading only +what is registered under the names it inherits and includes, and `commit_vendor_preset` +registers it, the only step that writes anything shared. Entries resolve across threads +in runs and commit in the order the vendor lists them. A run ends before an entry that +inherits or includes one already in it, since that one's commit registers what the +entry resolves against, so no entry in a run reads what another in it registers. An +entry's parse messages are held until it commits. The bundle, the log's parse and +install messages and the error count therefore come out as parsing and installing one +entry at a time would leave them, whatever the listing order. + App upgrades work because a cache normally survives one. Only a deliberate `CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at install time rather than at load: a vendor whose cache this build cannot read counts @@ -254,17 +270,20 @@ the wizard caches the *derived JSON*, not another form of the inputs: open, the wizard computes the current stamps (one version peek per vendor) and, when they match, serves the catalog from the file — no bundle built, no preset installed. Caching bundle inputs instead was tried and measured: rebuilding the bundle from -per-vendor caches costs ~2 s of preset installation whatever feeds it, so only -skipping the rebuild entirely wins. +per-vendor caches costs over a second of preset installation whatever feeds it, so +only skipping the rebuild entirely wins. Any change to the set — a vendor added, removed or updated, or its cache-only `.opc` replaced by a newer one — changes the stamps and retires the whole file; -the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where -they cover) and writes the catalog back. Selections, region and per-open decorations -are applied downstream of the cache either way, so a served catalog is -indistinguishable from a rebuilt one. Nothing ships this file and the updater never -touches it; it is a locally written artifact, re-derived whenever stale, written -through a temp file and rename so half a cache is never readable. +the wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load +startup uses (per-vendor caches serving where they cover), and writes the catalog +back. When a vendor fails to load, the filament library included, that open falls +back to the wizard's own scan of the vendor JSONs, as when no bundle can be built, +and writes nothing. Selections, region and per-open decorations are applied +downstream of the cache either way, so a served catalog is indistinguishable from a +rebuilt one. Nothing ships this file and the updater never touches it; it is a +locally written artifact, re-derived whenever stale, written through a temp file and +rename so half a cache is never readable. The cache lives under `/cache/`, not beside the vendors: everything that scans `/system/` treats any `.opc` there as a vendor, so a non-vendor @@ -378,6 +397,13 @@ enumerates only `*.json` will find no vendors at all in a packaged build. the `CachedPreset` field list — written and read by `visit_entry` in `PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand. +- **Adding a kind of reference between presets**, as `inherits` and `include` are: + parse the names into `CachedPreset` (a field change, so `CACHE_VERSION` is bumped), + have `install_vendor_entries` end a run before an entry that names one already in it + and retain what the names point at, look them up only in `resolve_vendor_preset`, and + register what they point at only in `commit_vendor_preset`. The listing-order test in + `test_vendor_cache.cpp` fails for a kind the runs do not check once its fixture uses + it. - **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most 65535 options and one cache at most 65535 distinct enum value names. `CacheDictionary::save` throws past that, which surfaces when CI generates the diff --git a/resources/profiles/OrcaFilamentLibrary.json b/resources/profiles/OrcaFilamentLibrary.json index 084a12993f..ebf4b7b928 100644 --- a/resources/profiles/OrcaFilamentLibrary.json +++ b/resources/profiles/OrcaFilamentLibrary.json @@ -1,6 +1,6 @@ { "name": "OrcaFilamentLibrary", - "version": "02.04.00.15", + "version": "02.04.00.16", "force_update": "0", "description": "Orca Filament Library", "filament_list": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth ABS rABS.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth ABS rABS.json index 4d06956b11..b9ee6494c5 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth ABS rABS.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth ABS rABS.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura FDA.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura FDA.json index 7bc7435c74..8532e514a3 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura FDA.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura FDA.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura.json index b33951c7ba..c7690e1178 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA Adura.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA-CF Adura X.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA-CF Adura X.json index 9f938e51e9..7dab608a6f 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA-CF Adura X.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA-CF Adura X.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA6 Addlantis.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA6 Addlantis.json index 352bd80ac6..6e068507f1 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA6 Addlantis.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PA6 Addlantis.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PC BLend HT LCF.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PC BLend HT LCF.json index 839c055ebf..de13a67d83 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PC BLend HT LCF.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PC BLend HT LCF.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Base.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Base.json index f2e5824c14..b1120899d7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Base.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG ESD.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG ESD.json index 1074484973..cc7012c75d 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG ESD.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG ESD.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Economy.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Economy.json index 3747f7a86f..48119c7b04 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Economy.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Economy.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Flame v0.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Flame v0.json index 7dea674787..0650202cea 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Flame v0.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG Flame v0.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG PRO Matte.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG PRO Matte.json index 8dc175d8e1..978db741a7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG PRO Matte.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG PRO Matte.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "1" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG rPETG Matte.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG rPETG Matte.json index 0454a13f5c..8514e62315 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG rPETG Matte.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG rPETG Matte.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG-CF Rigid X.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG-CF Rigid X.json index 024faed63d..d2789e2d15 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG-CF Rigid X.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PETG-CF Rigid X.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "0" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA E-PLA.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA E-PLA.json index 1f6892b815..b92f8c27b5 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA E-PLA.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA E-PLA.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Economy.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Economy.json index ccacef16f4..de0cdb9f05 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Economy.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Economy.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA HT-PLA PRO Matte.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA HT-PLA PRO Matte.json index aa466f3c3e..e8e2fca28a 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA HT-PLA PRO Matte.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA HT-PLA PRO Matte.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Premium Silk.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Premium Silk.json index f794753c4a..f3cb324a87 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Premium Silk.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Premium Silk.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Textura.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Textura.json index 73cfea81a4..9b49ebac26 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Textura.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Textura.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Wood.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Wood.json index 5d64f416b7..e29534a5ad 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Wood.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA Wood.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "0" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json index d752b3adaf..98a98c4536 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA.json index 2deaf8d323..def3487467 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA X-PLA.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json index 5ef8ccc6b6..ef614b39ae 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PVDF Adamant S1.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PVDF Adamant S1.json index 608d67a5bd..e0a99b8b05 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PVDF Adamant S1.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth PVDF Adamant S1.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU EasyFlex.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU EasyFlex.json index 25f2c310db..a1d9deab44 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU EasyFlex.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU EasyFlex.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 85A.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 85A.json index a922a91ba1..e944d203a9 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 85A.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 85A.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json index 80c80328e8..d0e272d902 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json @@ -111,6 +111,7 @@ "1" ], "enable_pressure_advance": [ + "0", "0" ], "eng_plate_temp": [ @@ -316,6 +317,7 @@ "2" ], "pressure_advance": [ + "0.02", "0.02" ], "reduce_fan_stop_start_freq": [ diff --git a/resources/web/model/model.js b/resources/web/model/model.js index b401bea75b..3d7c2fc8ad 100644 --- a/resources/web/model/model.js +++ b/resources/web/model/model.js @@ -214,9 +214,9 @@ function ShowModelInfo( pModel ) SendWXDebugInfo("Model Name: "+sModelName); - $('#ModelName').html(sModelName); + $('#ModelName').text(sModelName); $('#ModelName').attr('title',sModelName); - $('#ModelAuthorName').html(sModelAuthor); + $('#ModelAuthorName').text(sModelAuthor); switch(UploadType) { @@ -268,7 +268,7 @@ function ShowModelInfo( pModel ) break; } - $('#Model_Desc').html( html_decode(sModelDesc) ); + $('#Model_Desc').empty().append( SanitizeDescHtml( html_decode(sModelDesc) ) ); let ModelPreviewList=pModel.preview_img; let TotalPreview=ModelPreviewList.length; @@ -281,16 +281,15 @@ function ShowModelInfo( pModel ) if(TotalPreview>0) { - let htmlPreview=''; + $('#ModelPreviewList').empty(); for(let pn=0;pn'; + $('#ModelPreviewList').append( $('
').append( $('').attr('src',FTmpPath) ) ); } - $('#ModelPreviewList').html(htmlPreview); $('#Model_Preview_Image').viewer({ title: false, fullsreen: false, @@ -410,7 +409,8 @@ function ConstructFileHtml( ID, pItem ) { let fTotal=pItem.length; - let strHtml=''; + let pBoard=$('#'+ID+' .FileListBoard'); + pBoard.empty(); for( let f=0;f').attr('src',ImgPath); + let pMenu=$('
'); if( strClass!='ImageIcon' ) { - strHtml+='
'+ - '
'+ - '
'+ - '
'+tName+'
'+ - '
'+ - '
'+ - '
'; + pMenu.on('click', function(){ OnClickOpenFile(tPath); }); } else { ImgID++; let TmpImgID="AF"+ImgID; - strHtml+='
'+ - '
'+ - '
'+ - '
'+tName+'
'+ - '
'+ - '
'+ - '
'; + pIconImg.attr('id',TmpImgID); + pMenu.on('click', function(){ OnClickOpenImage(TmpImgID); }); } + + let pFileItem=$('
'); + pFileItem.append( $('
').addClass(strClass).append(pIconImg) ); + pFileItem.append( $('
').append( $('
').text(tName).attr('title',tName) ) ); + pFileItem.append( pMenu ); + pBoard.append( pFileItem ); } - $('#'+ID+' .FileListBoard').html(strHtml); - if( fTotal>0 ) $('#'+ID).show(); } +// Descriptions are untrusted 3MF metadata that may carry rich-text HTML (e.g. from MakerWorld). +// Rebuild them from an inert parse, keeping only plain formatting tags and http(s) links and images. +var DescAllowedTags=['P','BR','B','STRONG','I','EM','U','S','STRIKE','DEL','INS','SUB','SUP','SMALL','MARK', + 'Q','ABBR','KBD','WBR','H1','H2','H3','H4','H5','H6','UL','OL','LI','DL','DT','DD','BLOCKQUOTE','PRE','CODE', + 'HR','SPAN','DIV','FIGURE','FIGCAPTION','TABLE','CAPTION','THEAD','TBODY','TFOOT','TR','TH','TD','A','IMG']; +// Plain attributes kept per tag; the numeric ones must be plain non-negative integers. +var DescAllowedAttrs={'IMG':['alt','title','width','height'],'TD':['colspan','rowspan'],'TH':['colspan','rowspan'],'OL':['start']}; +var DescNumericAttrs=['width','height','colspan','rowspan','start']; +// Dropped together with their content; any other unknown tag is unwrapped to its children. +var DescDroppedTags=['SCRIPT','STYLE','TEMPLATE','NOSCRIPT','TEXTAREA','TITLE','IFRAME','FRAME','OBJECT','EMBED','SVG','MATH']; + +function IsHttpUrl( strUrl ) +{ + // The scheme must be written as is, so nothing the URL parser would strip can precede or split it. + if( typeof strUrl!='string' || !/^https?:/i.test(strUrl) ) + return false; + try + { + let sProtocol=new URL(strUrl).protocol; + return sProtocol=='http:' || sProtocol=='https:'; + } + catch(e) + { + return false; + } +} + +// Images load as soon as the page opens, so only https sources are kept: no plain-http requests to the local network. +function IsHttpsUrl( strUrl ) +{ + return IsHttpUrl(strUrl) && new URL(strUrl).protocol=='https:'; +} + +// Embedded YouTube players become a plain link to the video. +function GetYouTubeEmbedUrl( pNode ) +{ + let sSrc=pNode.getAttribute('src'); + if( !IsHttpUrl(sSrc) ) + return null; + let pUrl=new URL(sSrc); + return ( pUrl.origin=='https://www.youtube.com' && pUrl.pathname.indexOf('/embed/')==0 ) ? pUrl.href : null; +} + +function CopyDescNodes( pSrc, pDst ) +{ + for( let pNode=pSrc.firstChild;pNode!=null;pNode=pNode.nextSibling ) + { + if( pNode.nodeType==Node.TEXT_NODE ) + { + pDst.appendChild( document.createTextNode(pNode.nodeValue) ); + continue; + } + if( pNode.nodeType!=Node.ELEMENT_NODE ) + continue; + + let sTag=pNode.nodeName.toUpperCase(); + if( sTag=='IFRAME' ) + { + let sVideoUrl=GetYouTubeEmbedUrl(pNode); + if( sVideoUrl!=null ) + { + let pLink=document.createElement('A'); + pLink.setAttribute('href',sVideoUrl); + pLink.textContent=sVideoUrl; + pDst.appendChild(pLink); + } + continue; + } + if( $.inArray(sTag,DescDroppedTags)>=0 ) + continue; + if( $.inArray(sTag,DescAllowedTags)<0 ) + { + CopyDescNodes(pNode,pDst); + continue; + } + + let pElem=document.createElement(sTag); + if( sTag=='A' && IsHttpUrl(pNode.getAttribute('href')) ) + pElem.setAttribute('href',pNode.getAttribute('href')); + else if( sTag=='IMG' ) + { + if( !IsHttpsUrl(pNode.getAttribute('src')) ) + continue; + pElem.setAttribute('src',pNode.getAttribute('src')); + } + $.each( DescAllowedAttrs[sTag]||[], function(i,sAttr){ + let sValue=pNode.getAttribute(sAttr); + if( sValue!=null && ( $.inArray(sAttr,DescNumericAttrs)<0 || /^\d+$/.test(sValue) ) ) + pElem.setAttribute(sAttr,sValue); + }); + CopyDescNodes(pNode,pElem); + pDst.appendChild(pElem); + } +} + +function SanitizeDescHtml( strHtml ) +{ + let pFragment=document.createDocumentFragment(); + // A DOMParser document is inert: it runs no scripts and loads no resources. + let pDoc=new DOMParser().parseFromString(strHtml,'text/html'); + if( pDoc && pDoc.body ) + CopyDescNodes(pDoc.body,pFragment); + return pFragment; +} + function ShowProfilelInfo( pProfile ) { //==========Profile Info========== @@ -483,10 +583,10 @@ function ShowProfilelInfo( pProfile ) let sProfileAuthor=decodeURIComponent(pProfile.author); let sProfileDesc=decodeURIComponent(pProfile.description); - $('#ProfileName').html(sProfileName); - $('#ProfileAuthor').html(sProfileAuthor); + $('#ProfileName').text(sProfileName); + $('#ProfileAuthor').text(sProfileAuthor); - $('#Profile_Desc').html( html_decode(sProfileDesc) ); + $('#Profile_Desc').empty().append( SanitizeDescHtml( html_decode(sProfileDesc) ) ); let ProfilePreviewList=pProfile.preview_img; let TotalPreview=ProfilePreviewList.length; @@ -499,15 +599,14 @@ function ShowProfilelInfo( pProfile ) if(TotalPreview>0) { - let htmlPreview=''; + $('#ProfilePreviewList').empty(); for(let pn=0;pn'; + $('#ProfilePreviewList').append( $('
').append( $('').attr('src',FTmpPath) ) ); } - $('#ProfilePreviewList').html(htmlPreview); $('#Profile_Preview_Image').viewer({ title: false, fullsreen: false, diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 33411e2f0d..1939988261 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -3476,6 +3476,14 @@ int CLI::run(int argc, char **argv) } new_variant_counts = old_variant_counts; //filament_variant_count = old_variant_counts; + //ORCA: lay the per-variant options out one value per variant of the current filaments before each + // loaded filament replaces its own variants, including an option only a loaded filament + // defines, which otherwise starts as a single default value and never reaches the others. + for (const DynamicPrintConfig &config : load_filaments_config) + for (const std::string &opt_key : filament_options_with_variant) + if (opt_key != "filament_extruder_variant" && config.has(opt_key)) + m_print_config.option(opt_key, true); + normalize_filament_values_to_variants(m_print_config); for (int index = 0; index < load_filaments_config.size(); index++) { DynamicPrintConfig& config = load_filaments_config[index]; int filament_index = load_filaments_index[index]; @@ -3647,6 +3655,25 @@ int CLI::run(int argc, char **argv) } } + //ORCA: a per-variant option the loaded filament does not define keeps the values of the + // variants the filament already had, and a variant new to it takes its first one's. + const int old_start = old_start_indice[filament_index - 1]; + std::vector kept_variant_indice = new_variant_indice; + for (int &i : kept_variant_indice) + if (i < 0) + i = old_start; + for (const std::string &opt_key : filament_options_with_variant) { + if (config.has(opt_key)) + continue; + auto *opt_vec_dst = dynamic_cast(m_print_config.option(opt_key)); + if (opt_vec_dst == nullptr || opt_vec_dst->size() < size_t(old_start + old_variant_count)) + continue; + // set_with_restore_2() pads its source in place + std::unique_ptr old_values(opt_vec_dst->clone()); + opt_vec_dst->set_with_restore_2(static_cast(old_values.get()), kept_variant_indice, old_start, + old_variant_count, true); + } + //update the old index if (old_variant_count != new_variant_count) { @@ -4081,6 +4108,9 @@ int CLI::run(int argc, char **argv) if (printer_technology == ptFFF) { fff_print_config.apply(m_print_config, true); m_print_config.apply(fff_print_config, true); + //ORCA: an option no preset or project defines has just come in as its single default value, and a + // command line override may hold one value per filament. + normalize_filament_values_to_variants(m_print_config); } else { boost::nowide::cerr << "invalid printer_technology " << std::endl; record_exit_reson(outfile_dir, CLI_INVALID_PRINTER_TECH, 0, cli_errors[CLI_INVALID_PRINTER_TECH], sliced_info); diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index cdcd9b4dec..d67b48c981 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -511,6 +511,10 @@ void AppConfig::set_defaults() set_bool("enable_high_low_temp_mixed_printing", false); } + if (get("remember_print_action").empty()) { + set_bool("remember_print_action", false); + } + if (get("ignore_ext_filament_in_filament_map").empty()){ set_bool("ignore_ext_filament_in_filament_map", false); } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index b27f58e699..1857412466 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -355,6 +355,7 @@ set(lisbslic3r_sources Optimize/Optimizer.hpp Orient.cpp Orient.hpp + ParallelResolve.hpp ParameterUtils.cpp ParameterUtils.hpp pchheader.cpp diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 42d6970ad1..427f6e66c9 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -849,6 +849,19 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC return std::move(substitutions_ctxt.substitutions); } +// Case-insensitive compare of a JSON key against a fixed ASCII one, without +// boost::iequals, whose std::locale() takes a lock the whole process shares in the +// MSVC runtime. +static bool ascii_iequals(const std::string &key, const char *literal) +{ + auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; }; + size_t i = 0; + for (; i < key.size() && literal[i] != '\0'; ++ i) + if (lower(key[i]) != lower(literal[i])) + return false; + return i == key.size() && literal[i] == '\0'; +} + int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map& key_values, std::string& reason) { json j; @@ -916,44 +929,44 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex } //parse the json elements for (auto it = j.begin(); it != j.end(); it++) { - if (boost::iequals(it.key(),BBL_JSON_KEY_VERSION)) { + if (ascii_iequals(it.key(), BBL_JSON_KEY_VERSION)) { key_values.emplace(BBL_JSON_KEY_VERSION, it.value()); } - else if (boost::iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) { //skip it } - else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_NAME)) { key_values.emplace(BBL_JSON_KEY_NAME, it.value()); if (it.value() == "project_settings") is_project_settings = true; } - else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_URL)) { key_values.emplace(BBL_JSON_KEY_URL, it.value()); } - else if (boost::iequals(it.key(), BBL_JSON_KEY_TYPE)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_TYPE)) { key_values.emplace(BBL_JSON_KEY_TYPE, it.value()); } - else if (boost::iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) { key_values.emplace(BBL_JSON_KEY_SETTING_ID, it.value()); } - else if (boost::iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) { key_values.emplace(BBL_JSON_KEY_FILAMENT_ID, it.value()); } - else if (boost::iequals(it.key(), BBL_JSON_KEY_FROM)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_FROM)) { key_values.emplace(BBL_JSON_KEY_FROM, it.value()); } - else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) { key_values.emplace(BBL_JSON_KEY_DESCRIPTION, it.value()); } - else if (boost::iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) { + else if (ascii_iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) { key_values.emplace(BBL_JSON_KEY_INSTANTIATION, it.value()); } - else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) { + else if (!load_inherits_to_config && ascii_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)) { + else if (!load_inherits_to_config && ascii_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 (ascii_iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) { key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value()); } else { t_config_option_key opt_key = it.key(); diff --git a/src/libslic3r/Format/OBJ.cpp b/src/libslic3r/Format/OBJ.cpp index de91efb64d..8242f2c0c9 100644 --- a/src/libslic3r/Format/OBJ.cpp +++ b/src/libslic3r/Format/OBJ.cpp @@ -141,7 +141,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s its.indices.emplace_back(indices[0], indices[1], indices[2]); int face_index =its.indices.size() - 1; RGBA face_color; - auto set_face_color = [&uvs, &data, &mtl_data, &obj_info, &face_color](int face_index, const std::string mtl_name) { + auto set_face_color = [&uvs, &data, &mtl_data, &obj_info, &face_color](int face_index, const std::string mtl_name, const std::array &corners) { if (mtl_data.new_mtl_unmap.find(mtl_name) != mtl_data.new_mtl_unmap.end()) { bool is_merge_ka_kd = true; for (size_t n = 0; n < 3; n++) { @@ -174,7 +174,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s return Vec2f::Zero(); return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]); }; - std::array uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])}; + std::array uv_array{uv_at(uvs[corners[0]]), uv_at(uvs[corners[1]]), uv_at(uvs[corners[2]])}; obj_info.uvs.emplace_back(uv_array); } obj_info.face_colors.emplace_back(face_color); @@ -185,27 +185,27 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s } } }; - auto set_face_color_by_mtl = [&data, &set_face_color](int face_index) { + auto set_face_color_by_mtl = [&data, &set_face_color](int face_index, const std::array &corners) { if (data.usemtls.size() == 1) { - set_face_color(face_index, data.usemtls[0].name); + set_face_color(face_index, data.usemtls[0].name, corners); } else { for (size_t k = 0; k < data.usemtls.size(); k++) { auto mtl = data.usemtls[k]; if (face_index >= mtl.face_start && face_index <= mtl.face_end) { - set_face_color(face_index, data.usemtls[k].name); + set_face_color(face_index, data.usemtls[k].name, corners); break; } } } }; if (exist_mtl) { - set_face_color_by_mtl(face_index); + set_face_color_by_mtl(face_index, {0, 1, 2}); } if (cnt == 4) { its.indices.emplace_back(indices[0], indices[2], indices[3]); int face_index = its.indices.size() - 1; if (exist_mtl) { - set_face_color_by_mtl(face_index); + set_face_color_by_mtl(face_index, {0, 2, 3}); } } } @@ -217,8 +217,12 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s message = _L("This OBJ file couldn't be read because it's empty."); return false; } - if (meshptr->volume() < 0) + if (meshptr->volume() < 0) { meshptr->flip_triangles(); + // Flipping swaps corners 1 and 2 of every face, so the UVs have to follow. + for (std::array &uv : obj_info.uvs) + std::swap(uv[1], uv[2]); + } // Hand the parsed material table back so callers can build a TexturedMesh from it. if (out_mtl) *out_mtl = mtl_data; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8bbeb05514..90cca3f084 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1497,12 +1497,7 @@ static std::vector get_path_of_change_filament(const Print& print) check_add_eol(toolchange_gcode_str); // SoftFever: set new PA for new filament - if (gcodegen.config().enable_pressure_advance.get_at(new_filament_id)) { - gcode += gcodegen.writer().set_pressure_advance(gcodegen.config().pressure_advance.get_at(new_filament_id)); - // Orca: Adaptive PA - // Reset Adaptive PA processor last PA value - gcodegen.m_pa_processor->resetPreviousPA(gcodegen.config().pressure_advance.get_at(new_filament_id)); - } + gcode += gcodegen.set_filament_pressure_advance(new_filament_id); // A phony move to the end position at the wipe tower. gcodegen.writer().travel_to_xy((end_pos + plate_origin_2d).cast()); @@ -1826,12 +1821,8 @@ static std::vector get_path_of_change_filament(const Print& print) check_add_eol(toolchange_gcode_str); // SoftFever: set new PA for new filament - if (new_extruder_id != -1 && gcodegen.config().enable_pressure_advance.get_at(new_extruder_id)) { - gcode += gcodegen.writer().set_pressure_advance(gcodegen.config().pressure_advance.get_at(new_extruder_id)); - // Orca: Adaptive PA - // Reset Adaptive PA processor last PA value - gcodegen.m_pa_processor->resetPreviousPA(gcodegen.config().pressure_advance.get_at(new_extruder_id)); - } + if (new_extruder_id != -1) + gcode += gcodegen.set_filament_pressure_advance(new_extruder_id); // A phony move to the end position at the wipe tower. gcodegen.writer().travel_to_xy((end_pos + plate_origin_2d).cast()); @@ -3232,7 +3223,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_cooling_buffer->set_current_extruder(initial_extruder_id, extruder_id); // Orca: Initialise AdaptivePA processor filter - m_pa_processor = std::make_unique(*this, tool_ordering.all_extruders()); + m_pa_processor = std::make_unique(*this); // Update output variables after the extruders were initialized. m_placeholder_parser_integration.init(m_writer); @@ -3797,12 +3788,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } } // Orca: add missing PA settings for initial filament - if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) { - file.write(m_writer.set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); - // Orca: Adaptive PA - // Reset Adaptive PA processor last PA value - m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(initial_non_support_extruder_id)); - } + file.write(set_filament_pressure_advance(initial_non_support_extruder_id)); } //flush FanMover buffer to avoid modifying the start gcode if it's manual. @@ -4188,6 +4174,28 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato print.throw_if_canceled(); + // Some firmwares only scan the last N lines for the time estimate, so the stats are written + // after the config block. + if (!is_bbl_printers && !skip_config_block) { + file.write("; CONFIG_BLOCK_START\n"); + std::string full_config; + append_full_config(print, full_config); + if (!full_config.empty()) + file.write(full_config); + + // SoftFever: write compatiple info + int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type); + file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature); + file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str()); + file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0)); + file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value); + + //SF TODO +// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0); + + file.write("; CONFIG_BLOCK_END\n\n"); + } + // Get filament stats. file.write(DoExport::update_print_stats_and_format_filament_stats( // Const inputs @@ -4214,26 +4222,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder) .c_str()); file.write("\n"); - if (!skip_config_block) { - file.write("; CONFIG_BLOCK_START\n"); - std::string full_config; - append_full_config(print, full_config); - if (!full_config.empty()) - file.write(full_config); - - // SoftFever: write compatiple info - int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type); - file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature); - file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str()); - file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0)); - file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value); - - //SF TODO -// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0); - - file.write("; CONFIG_BLOCK_END\n\n"); - } // !skip_config_block - } file.write("\n"); @@ -8390,12 +8378,16 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Orca: Dynamic PA // If adaptive PA is enabled, by default evaluate PA on all extrusion moves + // The PA settings and the PA_CHANGE tags use the filament config index of the filament's extruder variant + const unsigned int pa_config_index = (unsigned int) get_filament_config_index(m_writer.filament()->id()); + const bool adaptive_pa = m_config.adaptive_pressure_advance.get_at(pa_config_index) && m_config.enable_pressure_advance.get_at(pa_config_index); + const bool adaptive_pa_overhangs = adaptive_pa && m_config.adaptive_pressure_advance_overhangs.get_at(pa_config_index); bool is_pa_calib = m_curr_print->calib_mode() == CalibMode::Calib_PA_Line || m_curr_print->calib_mode() == CalibMode::Calib_PA_Pattern || m_curr_print->calib_mode() == CalibMode::Calib_PA_Tower; bool evaluate_adaptive_pa = false; bool role_change = (m_last_extrusion_role != path.role()); - if (!is_pa_calib && FILAMENT_CONFIG(adaptive_pressure_advance) && FILAMENT_CONFIG(enable_pressure_advance)) { + if (!is_pa_calib && adaptive_pa) { evaluate_adaptive_pa = true; // If we have already emmited a PA change because the m_multi_flow_segment_path_pa_set is set // skip re-issuing the PA change tag. @@ -8478,7 +8470,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Post processor flag generation code segment when option to emit only at role changes is enabled // Variables published to the post processor: // 1) Tag to trigger a PA evaluation (because a role change was identified and the user has requested dynamic PA adjustments) - // 2) Current extruder ID (to identify the PA model for the currently used extruder) + // 2) Current filament config index (to identify the PA model for the currently used filament and its extruder variant) // 3) mm3_per_mm value (to then multiply by the final model print speed after slowdown for cooling is applied) // 4) the current acceleration (to pass to the model for evaluation) // 5) whether this is an external perimeter (for future use) @@ -8489,7 +8481,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (m_multi_flow_segment_path_average_mm3_per_mm > 0) { sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + pa_config_index, m_multi_flow_segment_path_average_mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8502,7 +8494,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // to issue a zero flow PA change command for this sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + pa_config_index, _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8610,11 +8602,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // or a flow change, so emit the flag to evaluate PA for the upcomming extrusion // Emit tag before new speed is set so the post processor reads the next speed immediately and uses it. // Dont emit tag if it has just already been emitted from a role change above - if(_mm3_per_mm >0 && - FILAMENT_CONFIG(adaptive_pressure_advance) && - FILAMENT_CONFIG(enable_pressure_advance) && - FILAMENT_CONFIG(adaptive_pressure_advance_overhangs) && - !evaluate_adaptive_pa){ + if(_mm3_per_mm >0 && adaptive_pa_overhangs && !evaluate_adaptive_pa){ if(writer().get_current_speed() > F){ // Ramping down speed - use overhang logic where the minimum speed is used between current and upcoming extrusion if(m_config.gcode_comments){ sprintf(buf, "; Ramp down-non-variable\n"); @@ -8622,7 +8610,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + pa_config_index, _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8637,7 +8625,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + pa_config_index, _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8843,10 +8831,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // ORCA: Adaptive PA code segment when adjusting PA within the same feature // There is a speed change or flow change so emit the flag to evaluate PA for the upcomming extrusion // Emit tag before new speed is set so the post processor reads the next speed immediately and uses it. - if(_mm3_per_mm >0 && - EXTRUDER_CONFIG(adaptive_pressure_advance) && - EXTRUDER_CONFIG(enable_pressure_advance) && - EXTRUDER_CONFIG(adaptive_pressure_advance_overhangs) ){ + if(_mm3_per_mm >0 && adaptive_pa_overhangs){ if(last_set_speed > new_speed){ // Ramping down speed - use overhang logic where the minimum speed is used between current and upcoming extrusion if(m_config.gcode_comments) { sprintf(buf, "; Ramp up-variable\n"); @@ -8854,7 +8839,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + pa_config_index, _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -8869,7 +8854,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + pa_config_index, _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -9422,33 +9407,29 @@ void GCode::update_placeholder_parser_with_variant_params() if (num_filaments == 0) return; - // Helpers: remap config arrays from variant index space to filament_id index space. + // Helper: remap config arrays from variant index space to filament_id index space. // After remapping, gcode templates can use param[filament_id] directly. - auto remap_floats_by_filament = [&](const auto &src) { - std::vector dst(num_filaments); - for (size_t i = 0; i < num_filaments; ++i) - dst[i] = src.get_at(get_filament_config_index(i)); - return dst; - }; - auto remap_ints_by_filament = [&](const auto &src) { - std::vector dst(num_filaments); + auto remap_by_filament = [&](const auto &src) { + std::decay_t dst(num_filaments); for (size_t i = 0; i < num_filaments; ++i) dst[i] = src.get_at(get_filament_config_index(i)); return dst; }; // --- filament_options_with_variant: gcode indexes by filament_id --- - this->placeholder_parser().set("filament_max_volumetric_speed", new ConfigOptionFloats(remap_floats_by_filament(m_config.filament_max_volumetric_speed))); - this->placeholder_parser().set("filament_pre_cooling_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.filament_pre_cooling_temperature))); - this->placeholder_parser().set("filament_pre_cooling_temperature_nc", new ConfigOptionInts(remap_ints_by_filament(m_config.filament_pre_cooling_temperature_nc))); - this->placeholder_parser().set("filament_cooling_before_tower", new ConfigOptionFloats(remap_floats_by_filament(m_config.filament_cooling_before_tower))); - this->placeholder_parser().set("nozzle_temperature_initial_layer", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature_initial_layer))); - this->placeholder_parser().set("nozzle_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature))); + this->placeholder_parser().set("filament_max_volumetric_speed", new ConfigOptionFloats(remap_by_filament(m_config.filament_max_volumetric_speed))); + this->placeholder_parser().set("filament_pre_cooling_temperature", new ConfigOptionInts(remap_by_filament(m_config.filament_pre_cooling_temperature))); + this->placeholder_parser().set("filament_pre_cooling_temperature_nc", new ConfigOptionInts(remap_by_filament(m_config.filament_pre_cooling_temperature_nc))); + this->placeholder_parser().set("filament_cooling_before_tower", new ConfigOptionFloats(remap_by_filament(m_config.filament_cooling_before_tower))); + this->placeholder_parser().set("nozzle_temperature_initial_layer", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature_initial_layer))); + this->placeholder_parser().set("nozzle_temperature", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature))); // first_layer_temperature is a legacy alias of nozzle_temperature_initial_layer - this->placeholder_parser().set("first_layer_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature_initial_layer))); + this->placeholder_parser().set("first_layer_temperature", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature_initial_layer))); + this->placeholder_parser().set("pressure_advance", new ConfigOptionFloats(remap_by_filament(m_config.pressure_advance))); + this->placeholder_parser().set("enable_pressure_advance", new ConfigOptionBools(remap_by_filament(m_config.enable_pressure_advance))); // --- printer_options_with_variant_1: in m_config these are already merged as filament-indexed --- - this->placeholder_parser().set("retraction_distances_when_cut", new ConfigOptionFloats(remap_floats_by_filament(m_config.retraction_distances_when_cut))); + this->placeholder_parser().set("retraction_distances_when_cut", new ConfigOptionFloats(remap_by_filament(m_config.retraction_distances_when_cut))); // hotend_cooling_rate / hotend_heating_rate: gcode uses [filament_map[x]-1] (extruder_id), no remap needed // --- filament_map: per-layer dynamic, sync from m_config to placeholder_parser --- @@ -9458,10 +9439,10 @@ void GCode::update_placeholder_parser_with_variant_params() { // Fast purge mode uses filament_flush_temp_fast; Default is inert. bool use_fast_flush = m_config.prime_volume_mode == PrimeVolumeMode::pvmFast; - auto flush_v_speed = remap_floats_by_filament(m_config.filament_flush_volumetric_speed); - auto filament_max_v = remap_floats_by_filament(m_config.filament_max_volumetric_speed); - auto flush_temps = remap_ints_by_filament(use_fast_flush ? m_config.filament_flush_temp_fast - : m_config.filament_flush_temp); + auto flush_v_speed = remap_by_filament(m_config.filament_flush_volumetric_speed); + auto filament_max_v = remap_by_filament(m_config.filament_max_volumetric_speed); + auto flush_temps = remap_by_filament(use_fast_flush ? m_config.filament_flush_temp_fast + : m_config.filament_flush_temp); for (size_t i = 0; i < num_filaments; ++i) { if (flush_v_speed[i] == 0) flush_v_speed[i] = filament_max_v[i]; @@ -9513,12 +9494,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config); check_add_eol(gcode); } - if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); - // Orca: Adaptive PA - // Reset Adaptive PA processor last PA value - m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); - } + gcode += set_filament_pressure_advance(new_filament_id); gcode += m_writer.toolchange(new_filament_id, new_extruder_id); if (Extruder *fil = m_writer.filament()) @@ -9909,18 +9885,24 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo if (m_ooze_prevention.enable && !defer_temp_wait) gcode += m_ooze_prevention.post_toolchange(*this); - if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); - // Orca: Adaptive PA - // Reset Adaptive PA processor last PA value - m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); - } + gcode += set_filament_pressure_advance(new_filament_id); //Orca: tool changer or IDEX's firmware may change Z position, so we set it to unknown/undefined m_last_pos_defined = false; return gcode; } +std::string GCode::set_filament_pressure_advance(unsigned int filament_id) +{ + const size_t fi = get_filament_config_index(filament_id); + if (!m_config.enable_pressure_advance.get_at(fi)) + return {}; + // Orca: Adaptive PA + // Reset Adaptive PA processor last PA value + m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(fi)); + return m_writer.set_pressure_advance(m_config.pressure_advance.get_at(fi)); +} + inline std::string polygon_to_string(const Polygon &polygon, Print *print, bool is_print_space = false) { std::ostringstream gcode; gcode << "["; diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 41986cc1d6..bded77d9d1 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -281,6 +281,8 @@ public: // extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract. std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); } std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false); + // Sets the pressure advance of the filament's extruder variant, if enabled for it. + std::string set_filament_pressure_advance(unsigned int filament_id); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); diff --git a/src/libslic3r/GCode/AdaptivePAProcessor.cpp b/src/libslic3r/GCode/AdaptivePAProcessor.cpp index 80d6b034ad..8145db3b9f 100644 --- a/src/libslic3r/GCode/AdaptivePAProcessor.cpp +++ b/src/libslic3r/GCode/AdaptivePAProcessor.cpp @@ -21,38 +21,30 @@ namespace Slic3r { * * @param gcodegen A reference to the GCode object that generates the G-code. */ -AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen, const std::vector &tools_used) +AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen) : m_gcodegen(gcodegen), m_config(gcodegen.config()), m_last_predicted_pa(0.0), m_max_next_feedrate(0.0), m_next_feedrate(0.0), m_current_feedrate(0.0), - m_last_extruder_id(-1), + m_last_config_index(-1), m_pa_change_pattern(R"(; PA_CHANGE:T(\d+) MM3MM:([0-9]*\.[0-9]+) ACCEL:(\d+) BR:(\d+) RC:(\d+) OV:(\d+))"), m_g1_f_pattern(R"(G1 F([0-9]+))") { - // Constructor body can be used for further initialization if necessary - for (unsigned int tool : tools_used) { - // Only enable model for the tool if both PA and adaptive PA options are enabled - if(m_config.adaptive_pressure_advance.get_at(tool) && m_config.enable_pressure_advance.get_at(tool)){ - auto interpolator = std::make_unique(); - // Get calibration values from extruder - std::string pa_calibration_values = m_config.adaptive_pressure_advance_model.get_at(tool); - // Setup the model and store it in the tool-interpolation model map - interpolator->parseAndSetData(pa_calibration_values); - m_AdaptivePAInterpolators[tool] = std::move(interpolator); - } - } } -// Method to get the interpolator for a specific tool ID -AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int tool_id) { - auto it = m_AdaptivePAInterpolators.find(tool_id); - if (it != m_AdaptivePAInterpolators.end()) { - return it->second.get(); +// Method to get the interpolator for a specific filament config index. +// The model is built the first time an index is requested, as the indices in use depend on +// the extruder variant each filament prints with. +AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int config_index) { + auto [it, inserted] = m_AdaptivePAInterpolators.try_emplace(config_index); + // Only enable model for the index if both PA and adaptive PA options are enabled + if (inserted && m_config.adaptive_pressure_advance.get_at(config_index) && m_config.enable_pressure_advance.get_at(config_index)) { + it->second = std::make_unique(); + it->second->parseAndSetData(m_config.adaptive_pressure_advance_model.get_at(config_index)); } - return nullptr; // Handle the case where the tool_id was not found + return it->second.get(); } /** @@ -108,16 +100,17 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) { // the PA for that material is set. As no tag below will be found for this extruder, the original PA is retained. if (line.find("; PA_CHANGE") == 0) { // prune lines quickly before running regex check as regex is more expensive to run if (std::regex_search(line, m_match, m_pa_change_pattern)) { - int extruder_id = std::stoi(m_match[1].str()); + // The tag carries the filament config index, which selects the PA settings of the filament's extruder variant + int config_index = std::stoi(m_match[1].str()); mm3mm_value = std::stod(m_match[2].str()); accel_value = std::stod(m_match[3].str()); int isBridge = std::stoi(m_match[4].str()); int roleChange = std::stoi(m_match[5].str()); int isOverhang = std::stoi(m_match[6].str()); - // Check if the extruder ID has changed - bool extruder_changed = (extruder_id != m_last_extruder_id); - m_last_extruder_id = extruder_id; + // Check if the filament config index has changed + bool config_index_changed = (config_index != m_last_config_index); + m_last_config_index = config_index; // Save the PA_CHANGE line to output later after finding feedrate pa_change_line = line; @@ -212,23 +205,23 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) { // Calculate the predicted PA using the upcomming feature maximum feedrate // Get the interpolator for the active tool - AdaptivePAInterpolator* interpolator = getInterpolator(m_last_extruder_id); + AdaptivePAInterpolator* interpolator = getInterpolator(m_last_config_index); double predicted_pa = 0; double adaptive_PA_speed = 0; if(!interpolator){ // Tool not found in the interpolator map // Tool not found in the PA interpolator to tool map - predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; + predicted_pa = m_config.enable_pressure_advance.get_at(m_last_config_index) ? m_config.pressure_advance.get_at(m_last_config_index) : 0; if(m_config.gcode_comments) output << "; APA: Tool doesnt have APA enabled\n"; - } else if (!interpolator->isInitialised() || (!m_config.adaptive_pressure_advance.get_at(m_last_extruder_id)) ) + } else if (!interpolator->isInitialised() || (!m_config.adaptive_pressure_advance.get_at(m_last_config_index)) ) // Check if the model is not initialised by the constructor for the active extruder // Also check that adaptive PA is enabled for that extruder. This should not be needed // as the PA change flag should not be set upstream (in the GCode.cpp file) if adaptive PA is disabled // however check for robustness sake. { // Model failed or adaptive pressure advance not enabled - use default value from m_config - predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; + predicted_pa = m_config.enable_pressure_advance.get_at(m_last_config_index) ? m_config.pressure_advance.get_at(m_last_config_index) : 0; if(m_config.gcode_comments) output << "; APA: Interpolator setup failed, using default pressure advance\n"; } else { // Model setup succeeded // Proceed to identify the print speed to use to calculate the adaptive PA value @@ -249,18 +242,18 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) { predicted_pa = (*interpolator)(mm3mm_value * adaptive_PA_speed, accel_value); // This is a bridge, use the dedicated PA setting. - if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id) > EPSILON) - predicted_pa = m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id); + if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_config_index) > EPSILON) + predicted_pa = m_config.adaptive_pressure_advance_bridges.get_at(m_last_config_index); if (predicted_pa < 0) { // If extrapolation fails, fall back to the default PA for the extruder. - predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; + predicted_pa = m_config.enable_pressure_advance.get_at(m_last_config_index) ? m_config.pressure_advance.get_at(m_last_config_index) : 0; if(m_config.gcode_comments) output << "; APA: Interpolation failed, using fallback pressure advance value\n"; } } if(m_config.gcode_comments) { // Output debug GCode comments output << pa_change_line << '\n'; // Output PA change command tag - if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id) > EPSILON) + if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_config_index) > EPSILON) output << "; APA Model Override (bridge)\n"; output << "; APA Current Speed: " << std::to_string(m_current_feedrate) << "\n"; output << "; APA Next Speed: " << std::to_string(m_next_feedrate) << "\n"; @@ -269,7 +262,7 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) { output << "; APA Flow rate: " << std::to_string(mm3mm_value * m_max_next_feedrate) << "\n"; output << "; APA Prev PA: " << std::to_string(m_last_predicted_pa) << " New PA: " << std::to_string(predicted_pa) << "\n"; } - if (extruder_changed || std::fabs(predicted_pa - m_last_predicted_pa) > EPSILON) { + if (config_index_changed || std::fabs(predicted_pa - m_last_predicted_pa) > EPSILON) { output << m_gcodegen.writer().set_pressure_advance(predicted_pa); // Use m_writer to set pressure advance m_last_predicted_pa = predicted_pa; // Update the last predicted PA value } diff --git a/src/libslic3r/GCode/AdaptivePAProcessor.hpp b/src/libslic3r/GCode/AdaptivePAProcessor.hpp index 33d4c26d24..01aa2f1104 100644 --- a/src/libslic3r/GCode/AdaptivePAProcessor.hpp +++ b/src/libslic3r/GCode/AdaptivePAProcessor.hpp @@ -11,7 +11,6 @@ #include #include #include -#include #include "AdaptivePAInterpolator.hpp" namespace Slic3r { @@ -33,7 +32,7 @@ public: * * @param gcodegen A reference to the GCode object that generates the G-code. */ - AdaptivePAProcessor(GCode &gcodegen, const std::vector &tools_used); + AdaptivePAProcessor(GCode &gcodegen); /** * @brief Processes a layer of G-code and applies adaptive pressure advance. @@ -70,28 +69,25 @@ public: private: GCode &m_gcodegen; ///< Reference to the GCode object. - std::unordered_map> m_AdaptivePAInterpolators; ///< Map between Interpolator objects and tool ID's + std::unordered_map> m_AdaptivePAInterpolators; ///< Map between Interpolator objects and filament config indices (null when adaptive PA is off) const PrintConfig &m_config; ///< Reference to the print configuration. double m_last_predicted_pa; ///< Last predicted pressure advance value. double m_max_next_feedrate; ///< Maximum feed rate (speed) for the upcomming island. If no speed is found, the previous island speed is used. double m_next_feedrate; ///< First feed rate (speed) for the upcomming island. double m_current_feedrate; ///< Current, latest feedrate. - int m_last_extruder_id; ///< Last used extruder ID. + int m_last_config_index; ///< Filament config index of the last PA_CHANGE tag. std::regex m_pa_change_pattern; ///< Regular expression to detect PA_CHANGE pattern. std::regex m_g1_f_pattern; ///< Regular expression to detect G1 F pattern. std::smatch m_match; ///< Match results for regular expressions. /** - * @brief Get the PA interpolator attached to the specified tool ID. + * @brief Get the PA interpolator attached to the specified filament config index. * - * This method manually sets the adaptive PA internally held value. - * Call this when changing tools or in any other case where the internally assumed last PA value may be incorrect - * - * @param An integer with the tool ID for which the PA interpolation model is to be returned. - * @return The Adaptive PA Interpolator object corresponding to that tool. + * @param config_index The filament config index (one per filament and extruder variant) for which the PA interpolation model is to be returned. + * @return The Adaptive PA Interpolator object corresponding to that index, or nullptr when adaptive PA is off for it. */ - AdaptivePAInterpolator* getInterpolator(unsigned int tool_id); + AdaptivePAInterpolator* getInterpolator(unsigned int config_index); }; } // namespace Slic3r diff --git a/src/libslic3r/LocalesUtils.cpp b/src/libslic3r/LocalesUtils.cpp index e727b29b09..9c81511dc5 100644 --- a/src/libslic3r/LocalesUtils.cpp +++ b/src/libslic3r/LocalesUtils.cpp @@ -13,8 +13,19 @@ namespace Slic3r { +// How many setters this thread holds, so the ones nested in another can skip +// setlocale, which takes a lock the whole process shares on Windows. +static thread_local int s_numeric_locale_depth = 0; + CNumericLocalesSetter::CNumericLocalesSetter() { + // Nested in another setter on this thread, whose "C" the separator check + // confirms is still set. + if (s_numeric_locale_depth > 0 && is_decimal_separator_point()) { + m_nested = true; + ++ s_numeric_locale_depth; + return; + } #ifdef _WIN32 _configthreadlocale(_ENABLE_PER_THREAD_LOCALE); m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr); @@ -29,12 +40,17 @@ CNumericLocalesSetter::CNumericLocalesSetter() m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale); uselocale(m_new_locale); #endif + // Counted last, since the destructor does not run for a constructor that throws. + ++ s_numeric_locale_depth; } CNumericLocalesSetter::~CNumericLocalesSetter() { + -- s_numeric_locale_depth; + if (m_nested) + return; #ifdef _WIN32 std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data()); #else diff --git a/src/libslic3r/LocalesUtils.hpp b/src/libslic3r/LocalesUtils.hpp index f63c3572f7..d7e59f5ded 100644 --- a/src/libslic3r/LocalesUtils.hpp +++ b/src/libslic3r/LocalesUtils.hpp @@ -19,8 +19,13 @@ class CNumericLocalesSetter { public: CNumericLocalesSetter(); ~CNumericLocalesSetter(); + // A copy would restore the locale twice, and count down once more than up. + CNumericLocalesSetter(const CNumericLocalesSetter&) = delete; + CNumericLocalesSetter& operator=(const CNumericLocalesSetter&) = delete; private: + // Inside another setter on this thread, which does the setting and restoring. + bool m_nested { false }; #ifdef _WIN32 std::string m_orig_numeric_locale; #else diff --git a/src/libslic3r/ParallelResolve.hpp b/src/libslic3r/ParallelResolve.hpp new file mode 100644 index 0000000000..e913be728a --- /dev/null +++ b/src/libslic3r/ParallelResolve.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "Exception.hpp" + +namespace Slic3r { + +// A batch of resolved presets, each a whole config, is what resolve_then_commit adds +// to peak memory, so it stays far below a vendor's preset count and above any core count. +inline constexpr size_t resolve_batch_size = 64; + +// Resolve `count` items that do not depend on each other and install them one at +// a time. +// +// `resolve(i)` runs on any thread and must read only, since the items are +// resolved side by side. `commit(i, resolved)` is called for every item in index +// order, on the calling thread, and is where shared state is written. +// +// The items are worked through in batches, so what is resolved and held at once +// does not grow with `count`. An exception from either callable propagates after +// the batches before it have been committed. A cancellation of the caller's task +// group, which stops a batch partway without an exception, throws RuntimeError +// before that batch commits. +// +// `ChunkSetup`, when given, is constructed once for each piece of a batch TBB hands +// out, for per-thread state a resolve would otherwise set up per item, such as the +// C numeric locale, whose setting takes a lock the whole process shares. +template +void resolve_then_commit(size_t count, Resolve resolve, Commit commit) +{ + using Resolved = std::invoke_result_t; + + std::vector resolved(std::min(count, resolve_batch_size)); + for (size_t first = 0; first < count; first += resolve_batch_size) { + const size_t last = std::min(first + resolve_batch_size, count); + // Isolated, so a thread waiting on the batch runs none of the caller's other + // tasks before finishing it. + tbb::this_task_arena::isolate([&] { + tbb::parallel_for(tbb::blocked_range(first, last), + [&](const tbb::blocked_range& range) { + ChunkSetup setup; + (void) setup; + for (size_t i = range.begin(); i < range.end(); ++ i) + resolved[i - first] = resolve(i); + }); + }); + if (tbb::is_current_task_group_canceling()) + throw RuntimeError("resolve_then_commit: canceled before the batch was resolved"); + for (size_t i = first; i < last; ++ i) + commit(i, std::move(resolved[i - first])); + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9af8969490..e8437f7c0b 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -5,6 +5,8 @@ #include "Preset.hpp" #include "PresetBundle.hpp" #include "AppConfig.hpp" +#include "LocalesUtils.hpp" +#include "ParallelResolve.hpp" #ifdef _MSC_VER #define WIN32_LEAN_AND_MEAN @@ -506,10 +508,11 @@ void Preset::normalize(DynamicPrintConfig &config) handle_legacy_sla(config); } -std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config) +std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config, + const DynamicPrintConfig *added) { std::string incorrect_keys; - for (const std::string &key : config.keys()) + for (const std::string &key : (added != nullptr ? *added : config).keys()) if (! default_config.has(key)) { if (incorrect_keys.empty()) incorrect_keys = key; @@ -621,7 +624,6 @@ void Preset::load_info(const std::string& file) } else if (v.first.compare("base_id") == 0) { this->base_id = v.second.get_value(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << file << " and base_id: " << this->base_id; if (this->base_id.compare("null") == 0) this->base_id.clear(); } @@ -1669,6 +1671,169 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con return get_preset_canonical_name(parsed.bare, origin); } +PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset( + const boost::filesystem::path &file, const std::string &canonical_name, + const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule, + const std::string &extruder_id_name, const std::string &extruder_variant_name, + std::set *key_set1, std::set *key_set2) const +{ + UserPresetLoad out; + out.preset = Preset(m_type, canonical_name, false); + Preset &preset = out.preset; + preset.bundle_id = load_origin.bundle_id; + preset.file = file.string(); + // Load the preset file, apply preset values on top of defaults. + try { + fs::path idx_path(preset.file); + idx_path.replace_extension(".info"); + if (fs::exists(idx_path)) { + out.info_file = idx_path.string(); + preset.load_info(out.info_file); + } + DynamicPrintConfig config; + //BBS: change to json format + //ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); + std::map key_values; + std::string reason; + ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason); + if (! config_substitutions.empty()) + out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) }); + if (!reason.empty()) { + out.discard_file = true; + out.errors.push_back((boost::format("parse config %1% failed") % preset.file).str()); + return out; + } + + std::string version_str = key_values[BBL_JSON_KEY_VERSION]; + boost::optional version = Semver::parse(version_str); + if (!version) return out; + preset.version = *version; + + if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end()) + preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID]; + if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end()) + preset.description = key_values[BBL_JSON_KEY_DESCRIPTION]; + if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end()) + preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false"; + + //Orca: find and use the inherit config as the base + const Preset* inherit_preset = nullptr; + ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS); + + // check inherits_config + if (inherits_config) { + ConfigOptionString * option_str = dynamic_cast (inherits_config); + std::string inherits_value = option_str->value; + // Orca: try to find if the parent preset has been renamed + inherit_preset = this->find_preset2(inherits_value); + Preset::normalize_inherits(config, inherit_preset); + } + const Preset& default_preset = this->default_preset_for(config); + if (inherit_preset) { + preset.config = inherit_preset->config; + preset.filament_id = inherit_preset->filament_id; + extend_default_config_length(config, false, {}); + preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); + } + else { + auto inherits_config2 = dynamic_cast(inherits_config); + if ((inherits_config2 && !inherits_config2->value.empty())) { + out.errors.push_back((boost::format("can not find parent %1% for config %2%!") % inherits_config2->value % preset.file).str()); + return out; + } + // We support custom root preset now + // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. + preset.config = default_preset.config; + preset.config.apply(std::move(config)); + extend_default_config_length(preset.config, true, default_preset.config); + } + + Preset::normalize(preset.config); + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config); + if (!incorrect_keys.empty()) + out.errors.push_back("Error in a preset file: The preset \"" + preset.file + + "\" contains the following incorrect keys: " + incorrect_keys + ", which were removed"); + + if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) { + auto compatible_printers = dynamic_cast(preset.config.option("compatible_printers", true)); + if (compatible_printers && compatible_printers->values.empty()) { + size_t at_pos = canonical_name.find('@'); + if (at_pos != std::string::npos && at_pos + 1 < canonical_name.length()) { + compatible_printers->values.push_back(canonical_name.substr(at_pos + 1)); + out.save_compatible_printers = true; + } + } + } + + preset.loaded = true; + out.complete = true; + } catch (const std::ifstream::failure &err) { + out.discard_file = true; + out.errors.push_back((boost::format("The user-config cannot be loaded: %1%. Reason: %2%") % preset.file % err.what()).str()); + //throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what()); + } catch (const std::runtime_error &err) { + out.discard_file = true; + out.errors.push_back((boost::format("Failed loading the user-config file: %1%. Reason: %2%") % preset.file % err.what()).str()); + //throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what()); + } + out.install = true; + return out; +} + +void PresetCollection::commit_user_preset(UserPresetLoad &&loaded, std::deque &presets_loaded, + PresetsConfigSubstitutions &substitutions, + const std::function &preset_loaded_fn, + bool read_only) +{ + Preset &preset = loaded.preset; + if (! loaded.info_file.empty()) + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << loaded.info_file << " and base_id: " << preset.base_id; + append(substitutions, std::move(loaded.substitutions)); + for (const std::string &error : loaded.errors) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << error; + } + if (loaded.discard_file && !read_only) { + fs::path file_path(loaded.preset.file); + if (fs::exists(file_path)) + fs::remove(file_path); + file_path.replace_extension(".info"); + if (fs::exists(file_path)) + fs::remove(file_path); + } + if (!loaded.install) + return; + + if (loaded.complete) { + if (loaded.save_compatible_printers) { + // A filesystem error from the rewrite is counted, and the preset still loads. + try { + if (!read_only) + preset.save(nullptr); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name; + } catch (const std::runtime_error &err) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " can not write compatible_printers back to " << preset.file << ": " << err.what(); + } + } + //BBS: add some workaround for previous incorrect settings + if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id)) + preset.setting_id.clear(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << preset.name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible; + // add alias for custom filament preset + set_custom_preset_alias(preset); + } + + if (preset_loaded_fn != nullptr) + preset_loaded_fn(preset); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name; + presets_loaded.emplace_back(std::move(preset)); +} + // Load all presets found in dir_path. // Throws an exception on error. void PresetCollection::load_presets( @@ -1706,6 +1871,8 @@ void PresetCollection::load_presets( std::set *key_set1 = nullptr, *key_set2 = nullptr; Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + struct UserPresetFile { fs::path path; std::string canonical_name; }; + std::vector files; //BBS: change to json format for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { @@ -1721,149 +1888,26 @@ void PresetCollection::load_presets( BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name; continue; } + files.push_back({ dir_entry.path(), std::move(canonical_name) }); + } + } + + resolve_then_commit(files.size(), + [&](size_t i) { + return this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule, + extruder_id_name, extruder_variant_name, key_set1, key_set2); + }, + [&](size_t, UserPresetLoad &&loaded) { + // Committing can remove an unreadable preset's file, and a filesystem error + // there is reported without stopping the rest of the directory. try { - Preset preset(m_type, canonical_name, false); - preset.bundle_id = resolved_origin.bundle_id; - preset.file = dir_entry.path().string(); - // Load the preset file, apply preset values on top of defaults. - try { - fs::path idx_path(preset.file); - idx_path.replace_extension(".info"); - if (fs::exists(idx_path)) { - preset.load_info(idx_path.string()); - } - DynamicPrintConfig config; - //BBS: change to json format - //ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); - std::map key_values; - std::string reason; - ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason); - if (! config_substitutions.empty()) - substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) }); - if (!reason.empty()) { - fs::path file_path(preset.file); - if (!read_only && fs::exists(file_path)) - fs::remove(file_path); - file_path.replace_extension(".info"); - if (!read_only && fs::exists(file_path)) - fs::remove(file_path); - BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file; - ++m_errors; - continue; - } - - std::string version_str = key_values[BBL_JSON_KEY_VERSION]; - boost::optional version = Semver::parse(version_str); - if (!version) continue; - preset.version = *version; - - if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end()) - preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID]; - if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end()) - preset.description = key_values[BBL_JSON_KEY_DESCRIPTION]; - if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end()) - preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false"; - - //Orca: find and use the inherit config as the base - Preset* inherit_preset = nullptr; - ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS); - - // check inherits_config - if (inherits_config) { - ConfigOptionString * option_str = dynamic_cast (inherits_config); - std::string inherits_value = option_str->value; - // Orca: try to find if the parent preset has been renamed - inherit_preset = this->find_preset2(inherits_value); - Preset::normalize_inherits(config, inherit_preset); - } else { - ; - } - const Preset& default_preset = this->default_preset_for(config); - if (inherit_preset) { - preset.config = inherit_preset->config; - preset.filament_id = inherit_preset->filament_id; - extend_default_config_length(config, false, {}); - preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); - } - else { - auto inherits_config2 = dynamic_cast(inherits_config); - if ((inherits_config2 && !inherits_config2->value.empty())) { - BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%inherits_config2->value %preset.file; - ++m_errors; - continue; - } - // We support custom root preset now - // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. - preset.config = default_preset.config; - preset.config.apply(std::move(config)); - extend_default_config_length(preset.config, true, default_preset.config); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id; - - Preset::normalize(preset.config); - // Report configuration fields, which are misplaced into a wrong group. - std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config); - if (!incorrect_keys.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) - << "Error in a preset file: The preset \"" << preset.file - << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; - } - - if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) { - auto compatible_printers = dynamic_cast(preset.config.option("compatible_printers", true)); - if (compatible_printers && compatible_printers->values.empty()) { - size_t at_pos = name.find('@'); - if (at_pos != std::string::npos && at_pos + 1 < name.length()) { - compatible_printers->values.push_back(name.substr(at_pos + 1)); - if (!read_only) - preset.save(nullptr); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name; - } - } - } - - preset.loaded = true; - //BBS: add some workaround for previous incorrect settings - if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id)) - preset.setting_id.clear(); - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible; - // add alias for custom filament preset - set_custom_preset_alias(preset); - } catch (const std::ifstream::failure &err) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what(); - fs::path file_path(preset.file); - if (!read_only && fs::exists(file_path)) - fs::remove(file_path); - file_path.replace_extension(".info"); - if (!read_only && fs::exists(file_path)) - fs::remove(file_path); - //throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what()); - } catch (const std::runtime_error &err) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what(); - //throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what()); - fs::path file_path(preset.file); - if (!read_only && fs::exists(file_path)) - fs::remove(file_path); - file_path.replace_extension(".info"); - if (!read_only && fs::exists(file_path)) - fs::remove(file_path); - } - - if (preset_loaded_fn != nullptr) - preset_loaded_fn(preset); - - presets_loaded.emplace_back(preset); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name; + this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, read_only); } catch (const std::runtime_error &err) { errors_cummulative += err.what(); errors_cummulative += "\n"; } - } - } + }); + if (presets_loaded.size() > 0) m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end())); sort_presets(); @@ -2827,6 +2871,18 @@ std::pair PresetCollection::load_external_preset( return std::make_pair(&preset, false); } +Preset& PresetCollection::append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config) +{ + lock(); + Preset &preset = m_presets.emplace_back(m_type, name, false); + preset.file = std::move(path); + preset.config = std::move(config); + preset.loaded = true; + preset.is_dirty = false; + unlock(); + return preset; +} + Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version) { lock(); @@ -3846,28 +3902,53 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name) return false; } -// Merge one vendor's presets with the other vendor's presets, report duplicates. -std::vector PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors) +std::vector> PresetCollection::merge_presets(const std::vector &others, const VendorMap &new_vendors) { - std::vector duplicates; - for (Preset &preset : other.m_presets) { - if (preset.is_default || preset.is_external) - continue; - Preset key(m_type, preset.name); - auto it = (m_type == Preset::TYPE_FILAMENT) - ? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less) - : std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key); - if (it == m_presets.end() || it->name != preset.name) { + auto less = [this](const Preset &a, const Preset &b) { + return m_type == Preset::TYPE_FILAMENT ? filament_preset_less(a, b) : a < b; + }; + struct Incoming { Preset *preset; size_t source; }; + auto incoming_less = [&less](const Incoming &a, const Incoming &b) { return less(*a.preset, *b.preset); }; + // Each of `others` is sorted, so its presets form one sorted run. + std::vector incoming; + std::vector run_ends { 0 }; + for (size_t source = 0; source < others.size(); ++ source) { + for (Preset &preset : others[source]->m_presets) + if (! preset.is_default && ! preset.is_external) + incoming.push_back({ &preset, source }); + assert(std::is_sorted(incoming.begin() + run_ends.back(), incoming.end(), incoming_less)); + run_ends.push_back(incoming.size()); + } + // Merged pairwise and stably, so equal names stay in the order of `others`. + const size_t runs = others.size(); + for (size_t width = 1; width < runs; width *= 2) + for (size_t i = 0; i + width < runs; i += 2 * width) + std::inplace_merge(incoming.begin() + run_ends[i], incoming.begin() + run_ends[i + width], + incoming.begin() + run_ends[std::min(i + 2 * width, runs)], incoming_less); + + std::vector> duplicates(others.size()); + std::deque merged; + auto own = m_presets.begin() + m_num_default_presets; + std::move(m_presets.begin(), own, std::back_inserter(merged)); + // On equal names this collection's preset is kept, else the earliest of `others`, + // and each repeat is listed under the collection it came from. + for (auto next = incoming.begin(); own != m_presets.end() || next != incoming.end();) { + if (next == incoming.end() || (own != m_presets.end() && ! less(*next->preset, *own))) + merged.emplace_back(std::move(*own ++)); + else { + Preset &preset = *(next ++)->preset; if (preset.vendor != nullptr) { // Re-assign a pointer to the vendor structure in the new PresetBundle. auto it = new_vendors.find(preset.vendor->id); assert(it != new_vendors.end()); preset.vendor = &it->second; } - m_presets.emplace(it, std::move(preset)); - } else - duplicates.emplace_back(std::move(preset.name)); + merged.emplace_back(std::move(preset)); + } + for (; next != incoming.end() && next->preset->name == merged.back().name; ++ next) + duplicates[next->source].emplace_back(next->preset->name); } + m_presets = std::move(merged); return duplicates; } diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 5b32d90ac6..329e835afe 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -445,7 +445,10 @@ public: static std::string remove_suffix_modified(const std::string& name); static void normalize(DynamicPrintConfig &config); // Report configuration fields, which are misplaced into a wrong group, remove them from the config. - static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config); + // `added`, when given, is the diff applied over a copy of default_config, and only + // its keys are checked, since no other key can be missing from default_config. + static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config, + const DynamicPrintConfig *added = nullptr); // BBS: move constructor to public Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {} @@ -801,6 +804,8 @@ public: // Return number of presets including the "- default -" preset. size_t size() const { return m_presets.size(); } bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; } + // How many presets this collection refused or repaired while loading. + int error_count() const { return m_errors; } // For Print / Filament presets, disable those, which are not compatible with the printer. template @@ -874,8 +879,10 @@ protected: // This is a temporary state, which shall be fixed immediately by the following step. bool select_preset_by_name_strict(const std::string &name); - // Merge one vendor's presets with the other vendor's presets, report duplicates. - std::vector merge_presets(PresetCollection &&other, const VendorMap &new_vendors); + // Move the presets of `others` into this collection in one pass. A name this + // collection or an earlier one of `others` already has is left out, and reported + // in the list of the collection that repeats it. + std::vector> merge_presets(const std::vector &others, const VendorMap &new_vendors); // Update m_map_alias_to_profile_name from loaded system profiles. void update_map_alias_to_profile_name(); @@ -890,6 +897,43 @@ protected: void set_custom_preset_alias(Preset &preset); private: + // One preset file read and flattened against the presets already in this + // collection, before anything the collection shares has been touched. + struct UserPresetLoad + { + Preset preset; + // Joins the collection. A file that threw partway still joins it, without + // the steps that did not run. + bool install { false }; + // The whole of the load ran, so the preset is ready to be aliased. + bool complete { false }; + // A filament preset that named no compatible printer and was given one from + // its name, which commit writes back to its file. + bool save_compatible_printers { false }; + // Unreadable, so commit removes it and its .info file. + bool discard_file { false }; + // The .info file read beside the preset, which commit logs. + std::string info_file; + // Counted and logged by commit, in the order the directory listed the files. + std::vector errors; + PresetsConfigSubstitutions substitutions; + }; + + // Read and flatten one preset file. It reads only, and resolves against the presets + // loaded before this pass, never another file of the same pass, so the files of a + // pass are independent of each other. + UserPresetLoad resolve_user_preset(const boost::filesystem::path &file, const std::string &canonical_name, + const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule, + const std::string &extruder_id_name, const std::string &extruder_variant_name, + std::set *key_set1, std::set *key_set2) const; + + // Install one resolved preset. The collection, its alias maps, the error count + // and the preset files on disk are touched here and only here. + void commit_user_preset(UserPresetLoad &&loaded, std::deque &presets_loaded, + PresetsConfigSubstitutions &substitutions, + const std::function &preset_loaded_fn, + bool read_only); + std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const; // Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group. @@ -901,6 +945,10 @@ private: return a.name < b.name; } + // Append a preset without keeping the collection sorted, for a caller installing + // many at once; find_preset() is unusable until sort_presets() runs. + Preset& append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config); + // Sort presets: filament presets use generic-first ordering, others sort alphabetically. void sort_presets() { if (m_type == Preset::TYPE_FILAMENT) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index b2cc67f970..c5b78a8e0f 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5,6 +5,7 @@ #include "PresetBundle.hpp" +#include "ParallelResolve.hpp" #include "PresetCacheFormat.hpp" #include "PrintConfig.hpp" #include "PublishSettings.hpp" @@ -19,6 +20,7 @@ #include #include +#include #include #include #include @@ -37,6 +39,8 @@ #include #include #include +#include +#include // Mark string for localization and translate. #define L(s) Slic3r::I18N::translate(s) @@ -2501,35 +2505,129 @@ std::pair PresetBundle::load_system_pre if (validation_mode) dir = (boost::filesystem::path(data_dir())).make_preferred(); - const auto load_t0 = std::chrono::steady_clock::now(); - // The vendors below are loaded whole and against each other — the filament // library first, then every other vendor with it as the base — so each parse // is complete enough to be worth caching. m_generate_vendor_caches = allow_cache && (m_generate_vendor_caches || !validation_mode); + // Sorted, so any duplicate-preset warning comes out in the same order on every run. + std::vector vendors; + for (const std::string& name : vendor_names_in(dir)) + if (name == ORCA_FILAMENT_LIBRARY || !(validation_mode && !vendor_to_validate.empty() && name != vendor_to_validate)) + vendors.push_back({ name, dir }); + auto result = this->load_vendors(vendors, compatibility_rule, allow_cache); + + this->update_system_maps(); + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%result.second; + return result; +} + +std::pair PresetBundle::load_vendors(const std::vector& vendors, + ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache, const std::atomic* cancel, + std::vector* failed) +{ + const auto load_t0 = std::chrono::steady_clock::now(); + auto canceled = [cancel] { return cancel != nullptr && cancel->load(); }; + PresetsConfigSubstitutions substitutions; std::string errors_cummulative; bool first = true; - // Sorted, so any duplicate-preset warning below comes out in the same order on - // every run. - const std::set vendor_names = vendor_names_in(dir); - // Separate ORCA_FILAMENT_LIBRARY from other vendors. It must be loaded - // first because other vendors' filaments may inherit from it via the - // `base_bundle` lookup in parse_subfile. The remaining vendors are - // independent (no cross-vendor inheritance) and can be loaded in parallel. - std::string orca_lib_vendor; - std::vector other_vendors; - other_vendors.reserve(vendor_names.size()); - for (auto& vn : vendor_names) { - if (vn == ORCA_FILAMENT_LIBRARY) - orca_lib_vendor = vn; - else if (!(validation_mode && !vendor_to_validate.empty() && vn != vendor_to_validate)) - other_vendors.push_back(vn); + // Other vendors inherit from nothing but the library, and only installing them + // looks it up, so each is read while it loads and installed once both are done. + const VendorSource* orca_lib = nullptr; + std::vector other_vendors; + other_vendors.reserve(vendors.size()); + for (const VendorSource& vendor : vendors) { + if (vendor.name == ORCA_FILAMENT_LIBRARY) + orca_lib = &vendor; + else + other_vendors.push_back(&vendor); } - // Step 1: Load ORCA_FILAMENT_LIBRARY into `this` synchronously. - if (! orca_lib_vendor.empty()) { + // One of the other vendors, loaded into a PresetBundle of its own. + struct VendorLoad + { + const VendorSource* source { nullptr }; + std::unique_ptr bundle; + VendorRead read; + PresetsConfigSubstitutions substitutions; + std::string error; + // Counts the vendor's read and the library load; the second to finish installs it. + std::atomic ready { 0 }; + }; + std::vector loads(other_vendors.size()); + for (size_t i = 0; i < other_vendors.size(); ++i) + loads[i].source = other_vendors[i]; + + // Started slowest first, since the load ends when the slowest vendor does. + std::vector by_cost(loads.size()); + std::iota(by_cost.begin(), by_cost.end(), size_t(0)); + // A parse from JSON costs far more than any cache load, so parses go first, and + // within each group the bigger file, the cache or the .json that lists what + // to parse. A cache older than its profile is ordered as a parse. + const bool reads_caches = allow_cache && ! validation_mode; + std::vector> vendor_costs(loads.size()); + for (size_t i = 0; i < loads.size(); ++i) { + const VendorSource& vendor = *loads[i].source; + const boost::filesystem::path cache = vendor.dir / (vendor.name + ".opc"); + const boost::filesystem::path profile = vendor.dir / (vendor.name + ".json"); + boost::system::error_code ec, profile_ec; + const uintmax_t cache_size = reads_caches ? boost::filesystem::file_size(cache, ec) : 0; + if (reads_caches && ! ec) { + const std::time_t profile_time = boost::filesystem::last_write_time(profile, profile_ec); + if (profile_ec || profile_time <= boost::filesystem::last_write_time(cache, ec)) { + vendor_costs[i] = { false, cache_size }; + continue; + } + } + const uintmax_t profile_size = boost::filesystem::file_size(profile, profile_ec); + vendor_costs[i] = { true, profile_ec ? 0 : profile_size }; + } + std::stable_sort(by_cost.begin(), by_cost.end(), + [&](size_t a, size_t b) { return vendor_costs[a] > vendor_costs[b]; }); + + // A vendor that failed to read, or that is still to install when `cancel` is + // set, is left out. + auto install = [&](VendorLoad& load) { + if (load.bundle && ! canceled()) { + try { + load.substitutions = load.bundle->install_vendor_read(std::move(load.read), this).first; + } catch (const std::runtime_error &err) { + load.error = err.what(); + load.bundle.reset(); + } + } else + load.bundle.reset(); + load.read = VendorRead(); + }; + tbb::task_group group; + group.run([&] { + // An auto_partitioner splits the range only while a worker is asking for work, + // which at this size left every vendor on the calling thread. + tbb::parallel_for(tbb::blocked_range(0, loads.size(), 1), + [&](const tbb::blocked_range& range) { + for (size_t k = range.begin(); k < range.end(); ++k) { + VendorLoad& load = loads[by_cost[k]]; + if (! canceled()) { + try { + auto bundle = std::make_unique(); + bundle->set_is_validation_mode(validation_mode); + bundle->set_generate_vendor_caches(m_generate_vendor_caches); + load.read = bundle->read_vendor(load.source->dir.string(), load.source->name, + PresetBundle::LoadSystem, compatibility_rule, allow_cache); + load.bundle = std::move(bundle); + } catch (const std::runtime_error &err) { + load.error = err.what(); + } + } + if (++ load.ready == 2) + install(load); + } + }, tbb::simple_partitioner()); + }); + if (orca_lib != nullptr && ! canceled()) { try { // Match a fresh launch before parsing: hold aliases and the error // counter survive reset(), and would otherwise carry prior-cycle @@ -2537,66 +2635,55 @@ std::pair PresetBundle::load_system_pre this->clear_printer_hold_aliases(); this->m_errors = 0; append(substitutions, this->load_vendor_configs_from_json( - dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule, nullptr, allow_cache).first); + orca_lib->dir.string(), orca_lib->name, PresetBundle::LoadSystem, compatibility_rule, nullptr, allow_cache).first); first = false; } catch (const std::runtime_error &err) { if (validation_mode) throw err; errors_cummulative += err.what(); errors_cummulative += "\n"; + if (failed != nullptr) + failed->push_back(orca_lib->name); } } + for (size_t k : by_cost) + if (++ loads[k].ready == 2) + group.run([&install, &load = loads[k]] { install(load); }); + group.wait(); - // Step 2: Load remaining vendors in parallel. Each gets its own - // PresetBundle and uses `this` (which contains ORCA_FILAMENT_LIBRARY) - // as the base_bundle for cross-bundle inheritance lookups. - std::vector> parallel_bundles(other_vendors.size()); - std::vector parallel_substitutions(other_vendors.size()); - std::vector parallel_errors(other_vendors.size()); - - tbb::parallel_for(tbb::blocked_range(0, other_vendors.size()), - [&](const tbb::blocked_range& range) { - for (size_t i = range.begin(); i < range.end(); ++i) { - auto bundle = std::make_unique(); - bundle->set_is_validation_mode(validation_mode); - bundle->set_generate_vendor_caches(m_generate_vendor_caches); - try { - auto result = bundle->load_vendor_configs_from_json( - dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this, allow_cache); - parallel_substitutions[i] = std::move(result.first); - parallel_bundles[i] = std::move(bundle); - } catch (const std::runtime_error &err) { - parallel_errors[i] = err.what(); - } - } - }); - - // Step 3: Sequentially merge the parallel-loaded bundles into `this`. - // The merge order is the original vendor order so any duplicate-warning - // output stays stable across runs. - for (size_t i = 0; i < other_vendors.size(); ++i) { - if (!parallel_errors[i].empty()) { + // Merged in the original vendor order, so any duplicate-warning output stays + // stable across runs. + std::vector bundles; + for (VendorLoad& load : loads) + if (load.bundle) + bundles.push_back(load.bundle.get()); + const std::vector> duplicates = this->merge_presets(bundles); + size_t merged = 0; + for (VendorLoad& load : loads) { + if (! load.error.empty()) { if (validation_mode) - throw std::runtime_error(parallel_errors[i]); - errors_cummulative += parallel_errors[i]; + throw std::runtime_error(load.error); + errors_cummulative += load.error; errors_cummulative += "\n"; + if (failed != nullptr) + failed->push_back(load.source->name); continue; } - if (!parallel_bundles[i]) + if (! load.bundle) continue; - const std::string& vendor_name = other_vendors[i]; - append(substitutions, std::move(parallel_substitutions[i])); - std::vector duplicates = this->merge_presets(std::move(*parallel_bundles[i])); + const std::string& vendor_name = load.source->name; + const std::vector& vendor_duplicates = duplicates[merged ++]; + append(substitutions, std::move(load.substitutions)); first = false; - if (!duplicates.empty()) { + if (!vendor_duplicates.empty()) { errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; - for (size_t j = 0; j < duplicates.size(); ++j) { - if (j > 0) + for (size_t k = 0; k < vendor_duplicates.size(); ++k) { + if (k > 0) errors_cummulative += ", "; - errors_cummulative += duplicates[j]; + errors_cummulative += vendor_duplicates[k]; ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Found duplicated preset: " + duplicates[j] + " in vendor: " + vendor_name + ": "; + BOOST_LOG_TRIVIAL(error) << "Found duplicated preset: " + vendor_duplicates[k] + " in vendor: " + vendor_name + ": "; } } } @@ -2606,14 +2693,9 @@ std::pair PresetBundle::load_system_pre this->reset(false); } - this->update_system_maps(); - const auto load_ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - load_t0).count(); - BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << vendor_names.size() << " vendor(s) loaded in " << load_ms << " ms"; - - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative; + BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << vendors.size() << " vendor(s) loaded in " << load_ms << " ms"; return std::make_pair(std::move(substitutions), errors_cummulative); } @@ -2682,7 +2764,7 @@ std::pair PresetBundle::load_system_fil // Report duplicate profiles. PresetBundle other; append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); - std::vector duplicates = this->merge_presets(std::move(other)); + std::vector duplicates = std::move(this->merge_presets({ &other }).front()); if (!duplicates.empty()) { errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; for (size_t i = 0; i < duplicates.size(); ++i) { @@ -2728,26 +2810,33 @@ VendorProfile PresetBundle::get_custom_vendor_models() const return vendor; } -// Merge one vendor's presets with the other vendor's presets, report duplicates. -std::vector PresetBundle::merge_presets(PresetBundle &&other) +std::vector> PresetBundle::merge_presets(const std::vector &others) { - this->vendors.insert(other.vendors.begin(), other.vendors.end()); - std::vector duplicate_prints = this->prints .merge_presets(std::move(other.prints), this->vendors); - std::vector duplicate_sla_prints = this->sla_prints .merge_presets(std::move(other.sla_prints), this->vendors); - std::vector duplicate_filaments = this->filaments .merge_presets(std::move(other.filaments), this->vendors); - std::vector duplicate_sla_materials = this->sla_materials.merge_presets(std::move(other.sla_materials), this->vendors); - std::vector duplicate_printers = this->printers .merge_presets(std::move(other.printers), this->vendors); - append(this->obsolete_presets.prints, std::move(other.obsolete_presets.prints)); - append(this->obsolete_presets.sla_prints, std::move(other.obsolete_presets.sla_prints)); - append(this->obsolete_presets.filaments, std::move(other.obsolete_presets.filaments)); - append(this->obsolete_presets.sla_materials, std::move(other.obsolete_presets.sla_materials)); - append(this->obsolete_presets.printers, std::move(other.obsolete_presets.printers)); - append(duplicate_prints, std::move(duplicate_sla_prints)); - append(duplicate_prints, std::move(duplicate_filaments)); - append(duplicate_prints, std::move(duplicate_sla_materials)); - append(duplicate_prints, std::move(duplicate_printers)); - m_errors += other.m_errors; - return duplicate_prints; + for (PresetBundle *other : others) + this->vendors.insert(other->vendors.begin(), other->vendors.end()); + std::vector> duplicates(others.size()); + auto merge = [&](auto collection) { + std::vector other_collections; + for (PresetBundle *other : others) + other_collections.push_back(&(other->*collection)); + std::vector> collection_duplicates = (this->*collection).merge_presets(other_collections, this->vendors); + for (size_t i = 0; i < others.size(); ++ i) + append(duplicates[i], std::move(collection_duplicates[i])); + }; + merge(&PresetBundle::prints); + merge(&PresetBundle::sla_prints); + merge(&PresetBundle::filaments); + merge(&PresetBundle::sla_materials); + merge(&PresetBundle::printers); + for (PresetBundle *other : others) { + append(this->obsolete_presets.prints, std::move(other->obsolete_presets.prints)); + append(this->obsolete_presets.sla_prints, std::move(other->obsolete_presets.sla_prints)); + append(this->obsolete_presets.filaments, std::move(other->obsolete_presets.filaments)); + append(this->obsolete_presets.sla_materials, std::move(other->obsolete_presets.sla_materials)); + append(this->obsolete_presets.printers, std::move(other->obsolete_presets.printers)); + m_errors += other->m_errors; + } + return duplicates; } void PresetBundle::update_system_maps() @@ -6598,38 +6687,38 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": finished"); } -// Orca: load one source-form preset entry — parsed from its JSON subfile just -// 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, 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( - 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& 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_includes) +static ConfigurationError failed_loading_error(const std::string& file, const std::string& dir) { - const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); - const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; + return ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % file % dir).str()); +} + +void PresetBundle::log_errors(const std::vector& errors) +{ + for (const std::string& error : errors) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << error; + } +} + +PresetBundle::PresetInstall PresetBundle::resolve_vendor_preset(const CachedPreset& entry, const VendorInstall& install) const +{ + const std::string& path = install.path; + const std::string& vendor_name = install.vendor_name; + const PresetBundle* base_bundle = install.base_bundle; + const PresetCollection& presets_collection = *install.presets; + const VendorProfile& current_vendor_profile = *install.vendor_profile; const std::string& preset_name = entry.name; - std::string alias_name, filament_id = entry.filament_id; - std::vector renamed_from = entry.renamed_from; - DynamicPrintConfig config; const DynamicPrintConfig* default_config = nullptr; - std::string reason; + const bool retain = install.is_from_lib || install.inherited.count(preset_name) != 0; + + PresetInstall out; + out.filament_id = entry.filament_id; + out.renamed_from = entry.renamed_from; //check whether it inherits other preset or not if (! entry.inherits.empty()) { - auto it2 = config_maps.find(entry.inherits); - if (it2 != config_maps.end()) + auto it2 = install.config_maps.find(entry.inherits); + if (it2 != install.config_maps.end()) default_config = &(it2->second); if (default_config == nullptr && base_bundle != nullptr) { auto base_it2 = base_bundle->m_config_maps.find(entry.inherits); @@ -6637,115 +6726,98 @@ std::string PresetBundle::load_vendor_preset( default_config = &(base_it2->second); } if (default_config != nullptr) { - if (filament_id.empty() && (presets_collection->type() == Preset::TYPE_FILAMENT)) { - auto filament_id_map_iter = filament_id_maps.find(entry.inherits); - if (filament_id_map_iter != filament_id_maps.end()) { - filament_id = filament_id_map_iter->second; + if (out.filament_id.empty() && (presets_collection.type() == Preset::TYPE_FILAMENT)) { + auto filament_id_map_iter = install.filament_id_maps.find(entry.inherits); + if (filament_id_map_iter != install.filament_id_maps.end()) { + out.filament_id = filament_id_map_iter->second; } - if (filament_id.empty() && base_bundle != nullptr) { + if (out.filament_id.empty() && base_bundle != nullptr) { auto base_filament_id_map_iter = base_bundle->m_filament_id_maps.find(entry.inherits); if (base_filament_id_map_iter != base_bundle->m_filament_id_maps.end()) { - filament_id = base_filament_id_map_iter->second; + out.filament_id = base_filament_id_map_iter->second; } } } } else { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << entry.inherits << " for " << preset_name; - // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); - reason = "Can not find inherits: " + entry.inherits; - return reason; + out.errors.push_back(std::string(__FUNCTION__) + ": can not find inherits " + entry.inherits + " for " + preset_name); + out.reason = "Can not find inherits: " + entry.inherits; + return out; } } else - default_config = &presets_collection->default_preset_for(entry.config_src).config; - config = *default_config; + default_config = &presets_collection.default_preset_for(entry.config_src).config; + out.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; + auto it = install.include_maps.find(name); + if (it == install.include_maps.end()) { + out.errors.push_back(std::string(__FUNCTION__) + ": can not find include " + name + " for " + preset_name); continue; } - config.apply(it->second); + out.config.apply(it->second); } - config.apply(entry.config_src); + out.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)); + if (entry.instantiation == "false" && install.included.count(preset_name) != 0) { + out.included.emplace(); + out.included->apply_only(out.config, out.config.diff(presets_collection.default_preset_for(out.config).config)); } - extend_default_config_length(config, true, *default_config); + extend_default_config_length(out.config, true, *default_config); + // Report configuration fields, which are misplaced into a wrong group, before + // Preset::normalize derives keys from them. An include diff holds only keys of the + // collection default, so only the entry's own keys can be missing from default_config. + std::string incorrect_keys = Preset::remove_invalid_keys(out.config, *default_config, &entry.config_src); + if (!incorrect_keys.empty()) + out.errors.push_back(std::string(__FUNCTION__) + ": The config " + path + "/" + vendor_name + "/" + entry.sub_path + + " contains incorrect keys: " + incorrect_keys + ", which were removed"); if (entry.instantiation == "false" && "Template" != vendor_name) { - // Report configuration fields, which are misplaced into a wrong group. - std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); - if (!incorrect_keys.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys - << ", which were removed"; - } - - if (retain_configs == nullptr || retain_configs->count(preset_name) != 0) - config_maps.emplace(preset_name, std::move(config)); - if ((presets_collection->type() == Preset::TYPE_FILAMENT) && (!filament_id.empty())) - filament_id_maps.emplace(preset_name, filament_id); - return reason; + out.config_only = true; + if (retain) + out.retained = std::move(out.config); + return out; } - if (config.has("alias")) - alias_name = (dynamic_cast(config.option("alias")))->value; - Preset::normalize(config); + if (out.config.has("alias")) + out.alias = (dynamic_cast(out.config.option("alias")))->value; - // Report configuration fields, which are misplaced into a wrong group. - std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); - if (!incorrect_keys.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys - << ", which were removed"; - } + Preset::normalize(out.config); - if (presets_collection->type() == Preset::TYPE_PRINTER) { + if (presets_collection.type() == Preset::TYPE_PRINTER) { // Filter out printer presets, which are not mentioned in the vendor profile. // These presets are considered not installed. - auto printer_model = config.opt_string("printer_model"); + auto printer_model = out.config.opt_string("printer_model"); if (printer_model.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines no printer model, it will be ignored."; - reason = std::string("can not find printer_model"); - return reason; + out.errors.push_back("Error in a Vendor Config Bundle \"" + path + "\": The printer preset \"" + + preset_name + "\" defines no printer model, it will be ignored."); + out.reason = std::string("can not find printer_model"); + return out; } - auto printer_variant = config.opt_string("printer_variant"); + auto printer_variant = out.config.opt_string("printer_variant"); if (printer_variant.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines no printer variant, it will be ignored."; - reason = std::string("can not find printer_variant"); - return reason; + out.errors.push_back("Error in a Vendor Config Bundle \"" + path + "\": The printer preset \"" + + preset_name + "\" defines no printer variant, it will be ignored."); + out.reason = std::string("can not find printer_variant"); + return out; } - auto it_model = std::find_if(current_vendor_profile->models.cbegin(), current_vendor_profile->models.cend(), + auto it_model = std::find_if(current_vendor_profile.models.cbegin(), current_vendor_profile.models.cend(), [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } ); - if (it_model == current_vendor_profile->models.end()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; - reason = std::string("can not find printer model in vendor profile"); - return reason; + if (it_model == current_vendor_profile.models.end()) { + out.errors.push_back("Error in a Vendor Config Bundle \"" + path + "\": The printer preset \"" + + preset_name + "\" defines invalid printer model \"" + printer_model + "\", it will be ignored."); + out.reason = std::string("can not find printer model in vendor profile"); + return out; } auto it_variant = it_model->variant(printer_variant); if (it_variant == nullptr) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; - reason = std::string("can not find printer_variant in vendor profile"); - return reason; + out.errors.push_back("Error in a Vendor Config Bundle \"" + path + "\": The printer preset \"" + + preset_name + "\" defines invalid printer variant \"" + printer_variant + "\", it will be ignored."); + out.reason = std::string("can not find printer_variant in vendor profile"); + return out; } // An instantiation printer profile's nozzle_diameter must match the numeric (diameter) // prefix of its printer_variant: "0.4" -> {0.4}, "0.8HF" -> {0.8} (a trailing @@ -6756,7 +6828,7 @@ std::string PresetBundle::load_vendor_preset( // validated, not variant uniqueness. Validation-only so the app keeps loading existing // profiles unchanged. if (validation_mode && entry.instantiation == "true") { - const auto *nd = config.option("nozzle_diameter"); + const auto *nd = out.config.option("nozzle_diameter"); std::set nozzles, variant_nozzles; if (nd != nullptr) nozzles.insert(nd->values.begin(), nd->values.end()); @@ -6771,34 +6843,71 @@ std::string PresetBundle::load_vendor_preset( variant_nozzles.insert(d); } if (!variant_ok || variant_nozzles != nozzles) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" has printer_variant \"" << printer_variant << - "\" that does not match its nozzle_diameter \"" << (nd ? nd->serialize() : std::string()) << "\". " + out.errors.push_back("Error in a Vendor Config Bundle \"" + path + "\": The printer preset \"" + + preset_name + "\" has printer_variant \"" + printer_variant + + "\" that does not match its nozzle_diameter \"" + (nd ? nd->serialize() : std::string()) + "\". " "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " - "nozzle order (e.g. \"0.4+0.6\")."; + "nozzle order (e.g. \"0.4+0.6\")."); } } } - const Preset *preset_existing = presets_collection->find_preset(preset_name, false); - if (preset_existing != nullptr) { + + // Derive the profile logical name aka alias from the preset name if the alias was not stated explicitely. + if (out.alias.empty()) { + size_t end_pos = preset_name.find_first_of("@"); + if (end_pos != std::string::npos) { + out.alias = preset_name.substr(0, end_pos); + if (out.renamed_from.empty()) + // Add the preset name with the '@' character removed into the "renamed_from" list. + out.renamed_from.emplace_back(out.alias + preset_name.substr(end_pos + 1)); + boost::trim_right(out.alias); + } + } + + out.file_path = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / vendor_name / entry.sub_path).make_preferred().string(); + if (validation_mode) + out.file_path = (boost::filesystem::path(data_dir()) / vendor_name / entry.sub_path).make_preferred().string(); + if (m_preserve_vendor_source_paths) + out.file_path = (boost::filesystem::path(path) / vendor_name / entry.sub_path).make_preferred().string(); + if (retain) + out.retained = out.config; + return out; +} + +std::string PresetBundle::commit_vendor_preset(const CachedPreset& entry, PresetInstall&& resolved, + ConfigSubstitutions&& substitutions, VendorInstall& install) +{ + const std::string& path = install.path; + const std::string& vendor_name = install.vendor_name; + PresetCollection* presets_collection = install.presets; + const VendorProfile* current_vendor_profile = install.vendor_profile; + const std::string& preset_name = entry.name; + + log_errors(resolved.errors); + if (resolved.included) + install.include_maps.emplace(preset_name, std::move(*resolved.included)); + if (! resolved.reason.empty()) + return resolved.reason; + + if (resolved.config_only) { + if (resolved.retained) + install.config_maps.emplace(preset_name, std::move(*resolved.retained)); + if ((presets_collection->type() == Preset::TYPE_FILAMENT) && (!resolved.filament_id.empty())) + install.filament_id_maps.emplace(preset_name, resolved.filament_id); + return std::string(); + } + + if (! install.installed_names.insert(preset_name).second) { ++m_errors; BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name << "\" has already been loaded from another Config Bundle."; - reason = std::string("duplicated defines"); - return reason; + return std::string("duplicated defines"); } - auto file_path = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / vendor_name / entry.sub_path).make_preferred(); - if (validation_mode) - file_path = (boost::filesystem::path(data_dir()) / vendor_name / entry.sub_path).make_preferred(); - if (m_preserve_vendor_source_paths) - file_path = (boost::filesystem::path(path) / vendor_name / entry.sub_path).make_preferred(); - // Load the preset into the list of presets, save it to disk. - Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); - if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { + Preset &loaded = presets_collection->append_preset(std::move(resolved.file_path), preset_name, std::move(resolved.config)); + if (install.flags.has(LoadConfigBundleAttribute::LoadSystem)) { loaded.is_system = true; loaded.vendor = current_vendor_profile; loaded.version = current_vendor_profile->config_version; @@ -6811,51 +6920,126 @@ std::string PresetBundle::load_vendor_preset( if (loaded.setting_id.empty() && entry.instantiation == "true") loaded.setting_id = generate_preset_setting_id( vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); - loaded.filament_id = filament_id; - loaded.m_from_orca_filament_lib = is_from_lib; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; + loaded.filament_id = resolved.filament_id; + loaded.m_from_orca_filament_lib = install.is_from_lib; if (presets_collection->type() == Preset::TYPE_FILAMENT) { - if (filament_id.empty() && "Template" != vendor_name) { + if (resolved.filament_id.empty() && "Template" != vendor_name) { ++m_errors; BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find filament_id for " << preset_name; //throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); - reason = "Can not find filament_id for " + preset_name; - return reason; + return "Can not find filament_id for " + preset_name; } else { - filament_id_maps.emplace(preset_name, filament_id); + install.filament_id_maps.emplace(preset_name, resolved.filament_id); } } } - // Derive the profile logical name aka alias from the preset name if the alias was not stated explicitely. - if (alias_name.empty()) { - size_t end_pos = preset_name.find_first_of("@"); - if (end_pos != std::string::npos) { - alias_name = preset_name.substr(0, end_pos); - if (renamed_from.empty()) - // Add the preset name with the '@' character removed into the "renamed_from" list. - renamed_from.emplace_back(alias_name + preset_name.substr(end_pos + 1)); - boost::trim_right(alias_name); - } - } - if (alias_name.empty()) + if (resolved.alias.empty()) loaded.alias = preset_name; else { - loaded.alias = std::move(alias_name); + loaded.alias = std::move(resolved.alias); filaments.set_printer_hold_alias(loaded.alias, loaded); } - loaded.renamed_from = std::move(renamed_from); - if (! substitution_context.empty()) - substitutions.push_back({ + loaded.renamed_from = std::move(resolved.renamed_from); + if (! substitutions.empty()) + install.substitutions->push_back({ preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, - std::string(), std::move(substitution_context.substitutions) }); - if (retain_configs == nullptr || retain_configs->count(preset_name) != 0) - config_maps.emplace(preset_name, loaded.config); - ++count; + std::string(), std::move(substitutions) }); + if (resolved.retained) + install.config_maps.emplace(preset_name, std::move(*resolved.retained)); + ++install.count; //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %subfile; - return reason; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ", got preset " << loaded.name << ", filament_id " << loaded.filament_id + << ", from " << path << "/" << vendor_name << "/" << entry.sub_path; + return std::string(); +} + +void PresetBundle::install_vendor_entries(VendorInstall& install, const std::vector& entries, + std::vector* parsed) +{ + assert(parsed == nullptr || parsed->size() == entries.size()); + for (const CachedPreset& entry : entries) { + if (! entry.inherits.empty()) + install.inherited.insert(entry.inherits); + install.included.insert(entry.includes.begin(), entry.includes.end()); + } + // The names an appended preset may not repeat, since the collection cannot be + // searched until it is sorted again. + for (const Preset& preset : install.presets->m_presets) + install.installed_names.insert(preset.name); + // Held until the collection is sorted again, also when an entry throws, so a reader + // holding the lock never finds it unsorted. + install.presets->lock(); + ScopeGuard sort_on_exit([&install] { + install.presets->sort_presets(); + install.presets->unlock(); + }); + + auto resolve = [&](size_t i) { return resolve_vendor_preset(entries[i], install); }; + auto commit = [&](size_t i, PresetInstall&& resolved) { + ConfigSubstitutions substitutions; + if (parsed != nullptr) { + log_errors((*parsed)[i].errors); + for (const std::string& warning : (*parsed)[i].warnings) + BOOST_LOG_TRIVIAL(error) << warning; + substitutions = std::move((*parsed)[i].substitutions); + } + const std::string reason = commit_vendor_preset(entries[i], std::move(resolved), std::move(substitutions), install); + if (! reason.empty()) { + ++m_errors; + const std::string subfile_path = install.path + "/" + install.vendor_name + "/" + entries[i].sub_path; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not install " << entries[i].name << " from " << subfile_path << ": " << reason; + throw failed_loading_error(subfile_path, install.path); + } + }; + // A run of entries resolves together and commits in listing order, and ends + // before an entry that inherits or includes one already in it. So no entry + // resolves against a registration from its own run, and the result matches + // installing one at a time. + std::unordered_set run_names; + size_t run_begin = 0; + auto install_run = [&](size_t run_end) { + resolve_then_commit(run_end - run_begin, + [&](size_t k) { return resolve(run_begin + k); }, + [&](size_t k, PresetInstall&& resolved) { commit(run_begin + k, std::move(resolved)); }); + run_names.clear(); + run_begin = run_end; + }; + auto in_run = [&](const std::string& name) { return run_names.count(name) != 0; }; + for (size_t i = 0; i < entries.size(); ++ i) { + const CachedPreset& entry = entries[i]; + if (in_run(entry.inherits) || std::any_of(entry.includes.begin(), entry.includes.end(), in_run)) + install_run(i); + if (install.inherited.count(entry.name) != 0 || install.included.count(entry.name) != 0) + run_names.insert(entry.name); + } + install_run(entries.size()); +} + +size_t PresetBundle::install_vendor(const std::string& path, const std::string& vendor_name, const PresetBundle* base_bundle, + LoadConfigBundleAttributes flags, const VendorCacheData& entries, + VendorParse* parsed, bool complete, PresetsConfigSubstitutions& substitutions) +{ + const bool is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; + const VendorProfile* vendor_profile = &this->vendors.at(vendor_name); + size_t count = 0; + auto install_collection = [&](const std::vector& list, std::vector* list_parsed, + PresetCollection* presets, bool is_from_lib) { + VendorInstall install { path, vendor_name, vendor_profile, base_bundle, flags, presets, is_from_lib, &substitutions }; + install_vendor_entries(install, list, list_parsed); + count += install.count; + return install; + }; + install_collection(entries.process_entries, parsed ? &parsed->process_entries : nullptr, &this->prints, false); + VendorInstall filaments = install_collection(entries.filament_entries, parsed ? &parsed->filament_entries : nullptr, + &this->filaments, is_orca_lib); + if (is_orca_lib && complete) { + m_config_maps = std::move(filaments.config_maps); + m_filament_id_maps = std::move(filaments.filament_id_maps); + } + install_collection(entries.machine_entries, parsed ? &parsed->machine_entries : nullptr, &this->printers, false); + return count; } //BBS: Load a config bundle file from json @@ -6863,12 +7047,14 @@ std::pair PresetBundle::load_vendor_configs_ const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle, bool allow_cache) { - // Enable substitutions for user config bundle, throw an exception when loading a system profile. - ConfigSubstitutionContext substitution_context { compatibility_rule }; - PresetsConfigSubstitutions substitutions; - // Errors already on this bundle when the load began; the cache stamp below - // counts only what this parse adds. - const int errors_at_entry = m_errors; + return this->install_vendor_read(this->read_vendor(dir, vendor_name, flags, compatibility_rule, allow_cache), base_bundle); +} + +PresetBundle::VendorRead PresetBundle::read_vendor(const std::string& dir, const std::string& vendor_name, + LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache) +{ + VendorRead read { dir, vendor_name, flags, compatibility_rule }; + read.errors_at_entry = m_errors; //BBS: add config related logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%")%dir.c_str()%compatibility_rule; @@ -6878,16 +7064,34 @@ std::pair PresetBundle::load_vendor_configs_ // Orca: only a whole-vendor load has a cache — the vendor-only and filament-only // scans want a slice of one. Validation reads the JSONs whatever is cached. - const boost::filesystem::path dir_path(dir); - const bool cacheable = allow_cache && flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); - if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) { - size_t presets_loaded = 0; - for (const PresetCollection* coll : std::initializer_list{ - &this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, &this->printers }) - presets_loaded += coll->m_presets.size() - coll->m_num_default_presets; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", %1% served from its preset cache, %2% presets")%vendor_name%presets_loaded; - return std::make_pair(std::move(substitutions), presets_loaded); + read.cacheable = allow_cache && flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); + if (read.cacheable && ! validation_mode) { + // A vendor is loaded from where it is installed and nowhere else; resources + // reaches the app by being installed into `dir` first. The cache there is + // judged against the profile beside it — or, where the cache is the whole + // of the installation, against nothing, since nothing on disk can then be + // newer than it. That state is Semver::inf(), which no real profile carries. + const boost::filesystem::path dir_path(dir); + const boost::filesystem::path profile = dir_path / (vendor_name + ".json"); + const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) : Semver::inf(); + read.cache_path = (dir_path / (vendor_name + ".opc")).string(); + read.from_cache = VendorCacheFile::load(read.cache_path, vendor_name, version, read.data); + if (read.from_cache) + return read; } + this->parse_vendor_json(read); + return read; +} + +void PresetBundle::parse_vendor_json(VendorRead& read) +{ + // Starts over from what read_vendor was asked for, since a cache that could + // not be installed leaves its own state behind. + read = VendorRead { std::move(read.dir), std::move(read.vendor_name), read.flags, read.compatibility_rule, + read.errors_at_entry, read.cacheable }; + const std::string& dir = read.dir; + const std::string& vendor_name = read.vendor_name; + LoadConfigBundleAttributes flags = read.flags; // 1) load the vroot json and construct the vendor profile VendorProfile vendor_profile(vendor_name); @@ -7108,21 +7312,21 @@ std::pair PresetBundle::load_vendor_configs_ BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", loaded vendor profile, name %1%, id %2%, version %3%")%vendor_profile.name%vendor_profile.id%vendor_profile.config_version.to_string(); - if (flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) - return std::make_pair(PresetsConfigSubstitutions{}, 0); + if (flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) { + read.vendor_only = true; + return; + } // 3) paste the process/filament/print configs - PresetCollection *presets = nullptr; - size_t presets_loaded = 0; // Parse one subfile into a source-form entry — everything the JSON states, - // nothing resolved. Loading the entry (load_vendor_preset) is the - // same code whether the entry was parsed just now or deserialized from the + // nothing resolved. Installing the entries (install_vendor) is the + // same code whether they were parsed just now or deserialized from the // vendor's cache. - auto parse_subfile = [this, dir, vendor_name]( + auto parse_subfile = [&dir, &vendor_name]( ConfigSubstitutionContext& substitution_context, const std::pair& subfile_iter, - CachedPreset& entry) -> std::string { + CachedPreset& entry, std::vector& errors, std::vector& warnings) -> std::string { std::string subfile = dir + "/" + vendor_name + "/" + subfile_iter.second; std::string reason; @@ -7134,8 +7338,7 @@ std::pair PresetBundle::load_vendor_configs_ entry.sub_path = subfile_iter.second; entry.config_src.load_from_json(subfile, substitution_context, false, key_values, reason); if (!reason.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": load config file "< PresetBundle::load_vendor_configs_ 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; - ++m_errors; - } + errors.push_back(std::string(__FUNCTION__) + ": Missing instantiation attribute for " + entry.name); entry.instantiation = key_values[BBL_JSON_KEY_INSTANTIATION]; - if(entry.instantiation != "false" && entry.instantiation != "true"){ - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << entry.name; - ++m_errors; - } + if(entry.instantiation != "false" && entry.instantiation != "true") + errors.push_back(std::string(__FUNCTION__) + ": Missing instantiation attribute for " + entry.name); auto setting_it = key_values.find(BBL_JSON_KEY_SETTING_ID); if (setting_it != key_values.end()) entry.setting_id = setting_it->second; @@ -7171,8 +7369,7 @@ std::pair PresetBundle::load_vendor_configs_ // An `inherits` key naming nothing can never resolve; fail it // here so install can key off the empty string as "no inherits". if (entry.inherits.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << entry.inherits << " for " << entry.name; + errors.push_back(std::string(__FUNCTION__) + ": can not find inherits " + entry.inherits + " for " + entry.name); reason = "Can not find inherits: " + entry.inherits; return reason; } @@ -7185,96 +7382,108 @@ std::pair PresetBundle::load_vendor_configs_ 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; - } + else + errors.push_back(std::string(__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 - << "\" contains invalid \"renamed_from\" key, which is being ignored."; - } + if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], entry.renamed_from)) + warnings.push_back("Error in a Config \"" + dir + "\": The preset \"" + entry.name + + "\" contains invalid \"renamed_from\" key, which is being ignored."); } } catch(nlohmann::detail::parse_error &err) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); + errors.push_back(std::string(__FUNCTION__) + ": parse " + subfile + " got a nlohmann::detail::parse_error, reason = " + err.what()); reason = std::string("json parse error") + err.what(); return reason; } return reason; }; - 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 // where the vendor was looked for, even when the profile came from resources, // and stamped with the version that profile claims — a profile without one // cannot be judged for staleness later, and a cache nothing can invalidate is // worse than none. - const bool will_cache = cacheable && m_generate_vendor_caches && vendor_profile.config_version.valid(); - VendorCacheData cache_data; + read.will_cache = read.cacheable && m_generate_vendor_caches && vendor_profile.config_version.valid(); + read.version = vendor_profile.config_version.to_string(); + // Enable substitutions for user config bundle, throw an exception when loading a system profile. + ConfigSubstitutionContext substitution_context { read.compatibility_rule }; + // Parsed up to the first sub-file that fails, and the ones before it are + // installed before that failure is raised, since the filament library and a + // filament-only scan keep what a throwing load installed. + auto parse_subfiles = [&](const std::vector>& subfiles, + std::vector& entries, std::vector& entries_parsed, const char* kind) { + for (const auto& subfile : subfiles) { + CachedPreset entry; + std::vector errors, warnings; + read.reason = parse_subfile(substitution_context, subfile, entry, errors, warnings); + if (! read.reason.empty()) { + read.failed_subfile = subfile.second; + read.failed_kind = kind; + read.failed_errors = std::move(errors); + return false; + } + entries.emplace_back(std::move(entry)); + entries_parsed.push_back({ std::move(substitution_context.substitutions), std::move(errors), std::move(warnings) }); + } + return true; + }; + // One setter for every sub-file, so the ones load_from_json makes have + // nothing to set. + CNumericLocalesSetter locales_setter; + if (parse_subfiles(process_subfiles, read.data.process_entries, read.parsed.process_entries, "process") && + parse_subfiles(filament_subfiles, read.data.filament_entries, read.parsed.filament_entries, "filament")) + parse_subfiles(machine_subfiles, read.data.machine_entries, read.parsed.machine_entries, "printer"); +} + +std::pair PresetBundle::install_vendor_read(VendorRead&& read, const PresetBundle* base_bundle) +{ + const std::string& dir = read.dir; + const std::string& vendor_name = read.vendor_name; + PresetsConfigSubstitutions substitutions; + if (read.from_cache) { + if (this->install_vendor_cache(read.cache_path, vendor_name, std::move(read.data), base_bundle)) { + size_t presets_loaded = 0; + for (const PresetCollection* coll : std::initializer_list{ + &this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, &this->printers }) + presets_loaded += coll->m_presets.size() - coll->m_num_default_presets; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", %1% served from its preset cache, %2% presets")%vendor_name%presets_loaded; + return std::make_pair(std::move(substitutions), presets_loaded); + } + this->parse_vendor_json(read); + } + if (read.vendor_only) + return std::make_pair(PresetsConfigSubstitutions{}, 0); + + int parse_errors = 0; + for (const std::vector* list : { &read.parsed.process_entries, &read.parsed.filament_entries, &read.parsed.machine_entries }) + for (const EntryParse& entry_parsed : *list) + parse_errors += int(entry_parsed.errors.size()); + const int errors_before_install = m_errors; + const size_t presets_loaded = install_vendor(dir, vendor_name, base_bundle, read.flags, read.data, &read.parsed, + read.reason.empty(), substitutions); // Errors added by install are counted apart: a cache load runs install again, // so the parse_errors stamped into the cache must hold only what a cache load // will not recount. - int install_errors = 0; - 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; - std::string reason = parse_subfile(substitution_context, subfile, entry); - 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, include_maps, filament_id_maps, - presets, presets_loaded, is_from_lib); - install_errors += m_errors - errors_before_install; - } - if (!reason.empty()) { - ++m_errors; - //parse error - std::string subfile_path = dir + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse %1% setting from %2%") % kind % subfile_path; - throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % dir).str()); - } - if (will_cache) - entries.emplace_back(std::move(entry)); - } - }; - - // The section order below — process, filaments (with the ORCA-lib map copy), - // printers — is mirrored by load_vendor_cache's install loops; keep the two - // in lockstep. - //3.1) paste the process - presets = &this->prints; - load_subfiles(process_subfiles, cache_data.process_entries, "process"); - - //3.2) paste the filaments - presets = &this->filaments; - const auto is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; - load_subfiles(filament_subfiles, cache_data.filament_entries, "filament", is_orca_lib); - if (is_orca_lib) { - m_config_maps = configs; - m_filament_id_maps = filament_id_maps; + const int install_errors = m_errors - errors_before_install - parse_errors; + if (! read.reason.empty()) { + log_errors(read.failed_errors); + ++m_errors; + //parse error + std::string subfile_path = dir + "/" + vendor_name + "/" + read.failed_subfile; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse %1% setting from %2%") % read.failed_kind % subfile_path; + throw failed_loading_error(subfile_path, dir); } - //3.3) paste the printers - presets = &this->printers; - load_subfiles(machine_subfiles, cache_data.machine_entries, "printer"); - - if (will_cache) { + if (read.will_cache) { // Clamped: the count is a difference of three tallies, and a stamp that // wrapped would be added to every future load of this vendor. - cache_data.parse_errors = uint64_t(std::max(0, m_errors - errors_at_entry - install_errors)); - cache_data.vendors = this->vendors; - if (! VendorCacheFile::save((dir_path / (vendor_name + ".opc")).string(), vendor_name, - vendor_profile.config_version.to_string(), cache_data)) + read.data.parse_errors = uint64_t(std::max(0, m_errors - read.errors_at_entry - install_errors)); + read.data.vendors = this->vendors; + if (! VendorCacheFile::save((boost::filesystem::path(dir) / (vendor_name + ".opc")).string(), vendor_name, + read.version, read.data)) BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache for " << vendor_name; } @@ -7989,82 +8198,42 @@ bool BundleMetadata::save_to_json(const std::string& path) const // ---- Per-vendor preset cache: install into this bundle ------------------- // The file format itself lives in PresetCacheFormat.cpp (VendorCacheFile). -bool PresetBundle::load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle) -{ - // A vendor is loaded from where it is installed and nowhere else; resources - // reaches the app by being installed into `dir` first. The cache there is - // judged against the profile beside it — or, where the cache is the whole - // of the installation, against nothing, since nothing on disk can then be - // newer than it. That state is Semver::inf(), which no real profile carries. - const boost::filesystem::path profile = dir / (vendor_name + ".json"); - const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) - : Semver::inf(); - return this->load_vendor_cache((dir / (vendor_name + ".opc")).string(), vendor_name, version, base_bundle); -} - bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name, const Semver& expected_vendor_version, const PresetBundle* base_bundle) { - // What this bundle had counted before the cache was tried. The caller - // measures its own parse against this same baseline, so a rejection must - // put it back rather than reset it to zero. - const int errors_at_entry = this->m_errors; // Read and validated before this bundle is touched: a rejected file leaves // no state to roll back. VendorCacheData data; if (! VendorCacheFile::load(cache_path, expected_vendor_name, expected_vendor_version, data)) return false; + // VendorCacheFile::load checked the names match. + return this->install_vendor_cache(cache_path, expected_vendor_name, std::move(data), base_bundle); +} + +bool PresetBundle::install_vendor_cache(const std::string& cache_path, const std::string& vendor_name, VendorCacheData&& data, + const PresetBundle* base_bundle) +{ + // What this bundle had counted before the cache was tried. The caller + // measures its own parse against this same baseline, so a rejection must + // put it back rather than reset it to zero. + const int errors_at_entry = this->m_errors; try { - const std::string& vendor_name = expected_vendor_name; // VendorCacheFile::load checked they match this->vendors = std::move(data.vendors); // What the parse counted before install took over; install recounts its // own below, so m_errors comes out as a JSON parse would leave it. m_errors += int(data.parse_errors); - // Install the entries exactly as load_vendor_configs_from_json installs - // them straight after parsing — same code, same order. The substitution - // context stays empty (the entries were substituted when they were - // parsed), so no substitutions are reported, as before. - ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent }; + // Stays empty, since the entries were substituted when they were parsed. PresetsConfigSubstitutions substitutions; - 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 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, 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); - } - }; - install_entries(data.process_entries, &this->prints, false); - const bool is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; - install_entries(data.filament_entries, &this->filaments, is_orca_lib); - if (is_orca_lib) { - m_config_maps = configs; - m_filament_id_maps = filament_id_maps; - } - install_entries(data.machine_entries, &this->printers, false); + install_vendor(boost::filesystem::path(cache_path).parent_path().string(), vendor_name, base_bundle, + LoadConfigBundleAttribute::LoadSystem, data, nullptr, true, substitutions); return true; } catch (const std::exception& e) { + // A cancellation of the caller's task group stops the install without + // anything being wrong with the cache. + if (tbb::is_current_task_group_canceling()) + throw; BOOST_LOG_TRIVIAL(warning) << "PresetBundle: rejecting vendor cache " << cache_path << ": " << e.what(); // Restore a clean state so the caller can fall back to the JSON parse. this->reset(false); diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 741f6069a4..0bf1bbd5a5 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -622,36 +623,180 @@ public: // default_filament_profile must resolve to a system filament. bool check_printer_default_materials() const; - // Merge one vendor's presets with the other vendor's presets, report duplicates. - // Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a - // bundle out of several per-vendor caches loaded into separate PresetBundle instances. - std::vector merge_presets(PresetBundle &&other); + // One vendor to load, and the directory it is installed in. + struct VendorSource + { + std::string name; + boost::filesystem::path dir; + }; + + // Load `vendors` into this bundle, the Orca filament library directly and every + // other vendor in parallel into a bundle of its own that inherits from it, merged + // in the order given. A vendor that cannot be loaded has its error added to the + // returned text, or thrown in validation mode, and its name to `failed`; it is + // left out, except for the library, which keeps what it installed before the + // failure. Once `cancel` is set, no further vendor starts loading. + std::pair load_vendors(const std::vector& vendors, + ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache, + const std::atomic* cancel = nullptr, std::vector* failed = nullptr); private: - // Load one vendor from the preset cache installed in `dir`, judged against - // the vendor profile there. False, with this bundle left clean, when there - // is no usable cache and the vendor has to be parsed. This is how - // 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); + // Move the presets and vendor profiles of `others` into this bundle, in one pass + // over each collection. A preset whose name this bundle or an earlier one of + // `others` already has is left out and listed under the bundle that repeats it. + std::vector> merge_presets(const std::vector &others); - // 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& 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_includes = nullptr); + // What parsing one entry's JSON sub-file reported. Its errors and warnings are + // logged when the entry installs, so they come out in listing order with the + // entry's install errors, as parsing and installing one entry at a time leaves them. + struct EntryParse + { + ConfigSubstitutions substitutions; + // Counted in the bundle's error count. + std::vector errors; + std::vector warnings; + }; + + // An EntryParse for each entry of the VendorCacheData list of the same name. + struct VendorParse + { + std::vector process_entries; + std::vector filament_entries; + std::vector machine_entries; + }; + + // One vendor read from its cache or its JSONs by read_vendor, for + // install_vendor_read to install. + struct VendorRead + { + std::string dir; + std::string vendor_name; + LoadConfigBundleAttributes flags; + ForwardCompatibilitySubstitutionRule compatibility_rule; + // The errors this bundle had counted before the read, which the cache stamp leaves out. + int errors_at_entry { 0 }; + // A whole-vendor load, which can read a cache and write one. + bool cacheable { false }; + // Read from the cache at cache_path, which install can still reject. + bool from_cache { false }; + std::string cache_path; + // Only the vendor profile was asked for. + bool vendor_only { false }; + VendorCacheData data; + // What each entry's JSON parse reported, and whether and with which version + // the cache is written once the entries install. + VendorParse parsed; + bool will_cache { false }; + std::string version; + // The sub-file the parse stopped at, its kind, why, and the errors it reported. + std::string reason; + std::string failed_subfile; + const char* failed_kind { nullptr }; + std::vector failed_errors; + }; + + // Read a vendor into this bundle's vendor profiles and `data`, from its cache + // when one covers it, else from its JSONs; nothing is installed. Throws + // ConfigurationError when the vendor's own JSON cannot be parsed. + VendorRead read_vendor(const std::string& dir, const std::string& vendor_name, LoadConfigBundleAttributes flags, + ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache); + + // Parse the vendor's JSONs into `read`, up to the first sub-file that fails. + void parse_vendor_json(VendorRead& read); + + // Install what read_vendor read, against base_bundle's filament library. A cache + // that cannot be installed is replaced by a parse of the JSONs. Throws + // ConfigurationError at the first entry that cannot be installed, or after + // installing the entries before a sub-file that could not be parsed. + std::pair install_vendor_read(VendorRead&& read, const PresetBundle* base_bundle); + + // Install a cache's entries. False, with this bundle left clean, when one of them + // cannot be installed. + bool install_vendor_cache(const std::string& cache_path, const std::string& vendor_name, VendorCacheData&& data, + const PresetBundle* base_bundle); + + // Log and count errors reported by a resolve or a parse. + void log_errors(const std::vector& errors); + + // The state of installing one collection of one vendor, which + // resolve_vendor_preset reads through a const reference and only + // commit_vendor_preset writes. + struct VendorInstall + { + // The directory holding /, whose sub-paths the entries name. + std::string path; + std::string vendor_name; + const VendorProfile* vendor_profile; + const PresetBundle* base_bundle; + LoadConfigBundleAttributes flags; + PresetCollection* presets; + // The Orca filament library, which keeps every config for other vendors to + // resolve against. + bool is_from_lib; + PresetsConfigSubstitutions* substitutions; + // The names some entry inherits / includes, the only ones whose configs / + // include diffs are looked up again. + std::set inherited; + std::set included; + std::map config_maps; + std::map include_maps; + std::map filament_id_maps; + std::unordered_set installed_names; + size_t count { 0 }; + }; + + // Install a vendor's source-form entries, parsed from its JSON or read from its + // cache: processes, then filaments, then printers. Both loads go through here, + // so a cache-loaded bundle cannot come out different from a JSON-loaded one. + // `parsed` is given for entries parsed just now. `complete` says the entries + // are the vendor's whole lists; only then are the filament library's configs + // and filament ids left in m_config_maps and m_filament_id_maps. Returns the + // number of presets installed, and throws ConfigurationError at the first + // entry that cannot be installed. + size_t install_vendor(const std::string& path, const std::string& vendor_name, const PresetBundle* base_bundle, + LoadConfigBundleAttributes flags, const VendorCacheData& entries, + VendorParse* parsed, bool complete, PresetsConfigSubstitutions& substitutions); + + // Install one collection's entries in the order they are listed. + void install_vendor_entries(VendorInstall& install, const std::vector& entries, + std::vector* parsed); + + // One entry flattened against the preset it inherits, before anything this + // bundle shares has been touched. + struct PresetInstall + { + DynamicPrintConfig config; + std::string file_path; + // Empty when the preset is its own alias. + std::string alias; + std::string filament_id; + std::vector renamed_from; + // Reported by commit, so resolving entries together leaves the log and + // the error count as one entry at a time produces them. + std::vector errors; + // What a base states for the presets that include it, when it is retained. + std::optional included; + // The config kept for the entries that inherit this one, or for other + // vendors when this is the filament library. + std::optional retained; + // Not instantiated, so it contributes a config and no preset. + bool config_only { false }; + // Non-empty when the entry is rejected, and says why. + std::string reason; + }; + + // Flatten one entry against the config it inherits, from this collection's + // config_maps or base_bundle's filament library, with the include diffs it + // names layered in. It looks up nothing but the names the entry inherits and + // includes, and writes nothing. + PresetInstall resolve_vendor_preset(const CachedPreset& entry, const VendorInstall& install) const; + + // Install a resolved entry. The collections, the maps in `install` and the + // error count are touched here and only here, one entry at a time. + // Presets are appended, so a repeated name is caught with `installed_names`, + // and the collection is sorted once every entry is in. + std::string commit_vendor_preset(const CachedPreset& entry, PresetInstall&& resolved, + ConfigSubstitutions&& substitutions, VendorInstall& install); // Clear every collection's m_printer_hold_alias, which reset() leaves alone. void clear_printer_hold_aliases(); diff --git a/src/libslic3r/PresetCacheFormat.hpp b/src/libslic3r/PresetCacheFormat.hpp index 2c90c07882..52c40bea21 100644 --- a/src/libslic3r/PresetCacheFormat.hpp +++ b/src/libslic3r/PresetCacheFormat.hpp @@ -109,7 +109,7 @@ void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict); // 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 +// everything PresetBundle::parse_vendor_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 diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index b708b759b2..be4e217d5c 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2439,12 +2439,18 @@ StringObjectException Print::validate(std::vector *warnin if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter) warn(L("The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences."), "precise_outer_wall"); - // check adaptive pressure advance model - for (unsigned int extruder_id : extruders) { - if (m_config.adaptive_pressure_advance.get_at(extruder_id) && - m_config.enable_pressure_advance.get_at(extruder_id)) { - - const std::string pa_model = m_config.adaptive_pressure_advance_model.get_at(extruder_id); + // check adaptive pressure advance model of every extruder variant column of the used filaments + const std::vector &self_index = m_config.filament_self_index.values; + const size_t pa_columns = std::max(m_config.adaptive_pressure_advance_model.size(), size_t(extruders.back()) + 1); + for (size_t column = 0; column < pa_columns; ++column) { + // filament_self_index maps a column to its filament once the filament arrays hold one column per variant + const unsigned int filament_id = self_index.size() == pa_columns ? self_index[column] - 1 : column; + if (!std::binary_search(extruders.begin(), extruders.end(), filament_id)) + continue; + if (m_config.adaptive_pressure_advance.get_at(column) && + m_config.enable_pressure_advance.get_at(column)) { + + const std::string pa_model = m_config.adaptive_pressure_advance_model.get_at(column); if (!pa_model.empty()) { std::string validation_error = AdaptivePAProcessor::validate_adaptive_pa_model(pa_model); if (!validation_error.empty()) { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 440f3e761a..94220e31fd 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -9403,6 +9403,10 @@ void PrintConfigDef::handle_legacy_composite(DynamicPrintConfig &config) } config.set_key_value("wiping_volumes_use_custom_matrix", new ConfigOptionBool(custom)); } + + // Orca: a config saved before a key joined filament_options_with_variant stores it once per filament + // rather than once per filament variant, and one exported by an older CLI may store a single value. + normalize_filament_values_to_variants(config); } const PrintConfigDef print_config_def; @@ -9505,6 +9509,13 @@ std::set filament_options_with_variant = { "filament_ironing_spacing", "filament_ironing_inset", "filament_ironing_speed", + // Orca: pressure advance + "enable_pressure_advance", + "pressure_advance", + "adaptive_pressure_advance", + "adaptive_pressure_advance_model", + "adaptive_pressure_advance_overhangs", + "adaptive_pressure_advance_bridges", "activate_air_filtration", "activate_air_filtration_during_print", "activate_air_filtration_on_completion", @@ -10712,6 +10723,25 @@ void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVect target.set_to_index(&source, indices, stride); } +void normalize_filament_values_to_variants(DynamicPrintConfig &config) +{ + const auto *self_index = config.option("filament_self_index"); + if (self_index == nullptr || self_index->empty()) + return; + const int filament_count = *std::max_element(self_index->values.begin(), self_index->values.end()); + if (filament_count <= 0 || size_t(filament_count) >= self_index->size()) + return; + for (const std::string &key : filament_options_with_variant) { + auto *opt = dynamic_cast(config.option(key)); + if (opt == nullptr || (opt->size() != size_t(filament_count) && opt->size() != 1)) + continue; + std::unique_ptr per_filament(opt->clone()); + // set_at() takes the first value for a filament past the end of a single-value vector + for (size_t variant = 0; variant < self_index->size(); ++variant) + opt->set_at(per_filament.get(), variant, self_index->values[variant] - 1); + } +} + //used for object/region config //use the smallest of multiple to single @@ -12565,6 +12595,7 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef() new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool."); new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone."); + new_def("curr_bed_type", coString, "Current bed type", "Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate')."); } PrintStatisticsConfigDef::PrintStatisticsConfigDef() diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 8b61280979..1e91f2e451 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -884,6 +884,11 @@ extern std::set empty_options; void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source, const std::vector &variant_index, int stride = 1); +// Orca: lays every filament_options_with_variant option out one value per filament variant, as +// filament_self_index maps the variants to filaments. An option holding one value per filament, or a +// single value, gives every variant of a filament that filament's value; other lengths are left alone. +void normalize_filament_values_to_variants(DynamicPrintConfig &config); + extern std::set filament_dev_options; extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector variant_index, std::set& key_set1, int stride = 1); diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index ff14e563fb..c85d33eed2 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -256,10 +256,19 @@ extern bool is_gallery_file(const std::string& path, char const* type); extern bool is_shapes_dir(const std::string& dir); //BBS: add json support extern bool is_json_file(const std::string& path); -// True if rel_path is relative, has no ".." component and, joined to root, still resolves inside it. +// True if rel_path is relative, has no ".." component or embedded NUL and, joined to root, still resolves inside it. // Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS // is rejected on all of them. extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root); +// True if a symlink stored at link_rel_path (relative to root) with this target stays inside root: the target +// must be relative, and joined to the link's directory it must pass is_path_within_root. +extern bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root); +// True if path names an entry strictly inside root: it must be spelled with root as its prefix, +// and must still resolve inside root once symlinks are followed. +extern bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root); +// True if a file with this name is of a type that the desktop opens as plain content, so it cannot run code. +// Anything unknown is not safe. +extern bool is_safe_to_open_file_name(const std::string &file_name); // Orca: custom protocal support utils inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); } diff --git a/src/libslic3r/miniz_extension.cpp b/src/libslic3r/miniz_extension.cpp index 53a4a8e9de..4d9b528267 100644 --- a/src/libslic3r/miniz_extension.cpp +++ b/src/libslic3r/miniz_extension.cpp @@ -4,6 +4,9 @@ #include "miniz_extension.hpp" #include "Utils.hpp" +#include +#include + #if defined(_MSC_VER) || defined(__MINGW64__) #include "boost/nowide/cstdio.hpp" #endif @@ -115,6 +118,66 @@ std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_ return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename); } +bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir) +{ + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, zip_path_utf8)) { + BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for " << zip_path_utf8; + return false; + } + + const mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + mz_zip_archive_file_stat stat; + + // Validate every entry first so an archive with a single escaping entry leaves no partial output behind. + const boost::filesystem::path root(dest_dir); + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat) && !is_path_within_root(stat.m_filename, root)) { + BOOST_LOG_TRIVIAL(error) << "Unzip: rejecting " << zip_path_utf8 << ", entry " << stat.m_filename << " resolves outside " << dest_dir; + close_zip_reader(&archive); + return false; + } + } + + for (mz_uint i = 0; i < num_entries; ++i) { + if (!mz_zip_reader_file_stat(&archive, i, &stat)) { + BOOST_LOG_TRIVIAL(warning) << "Unzip: read file stat failed"; + continue; + } + const std::string dest_file = dest_dir + "/" + stat.m_filename; + try { + if (stat.m_is_directory) { + const boost::filesystem::path dest_path(dest_file); + if (!boost::filesystem::exists(dest_path)) + boost::filesystem::create_directories(dest_path); + continue; + } + if (stat.m_uncomp_size == 0) { + BOOST_LOG_TRIVIAL(warning) << "Unzip: invalid size for file " << stat.m_filename; + continue; + } + // Replace a symlink at the destination rather than writing through it. + const boost::filesystem::path dest_path(dest_file); + if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path))) + boost::filesystem::remove(dest_path); + if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) { + BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed"; + close_zip_reader(&archive); + return false; + } + BOOST_LOG_TRIVIAL(info) << "Unzip: successfully extract file " << stat.m_file_index << " to " << dest_file; + } catch (const std::exception &e) { + close_zip_reader(&archive); + BOOST_LOG_TRIVIAL(error) << "Unzip: archive read exception: " << e.what(); + return false; + } + } + close_zip_reader(&archive); + return true; +} + MZ_Archive::MZ_Archive() { mz_zip_zero_struct(&arch); diff --git a/src/libslic3r/miniz_extension.hpp b/src/libslic3r/miniz_extension.hpp index 1a1c96689f..97aa91b93f 100644 --- a/src/libslic3r/miniz_extension.hpp +++ b/src/libslic3r/miniz_extension.hpp @@ -11,6 +11,8 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname_utf8); bool close_zip_reader(mz_zip_archive *zip); bool close_zip_writer(mz_zip_archive *zip); std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat); +// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir. +bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir); class MZ_Archive { public: diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 44fad7dc25..f56ce6a7af 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -70,6 +70,7 @@ #include #include +#include #include #include #include @@ -1093,6 +1094,9 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p auto is_separator = [](char c) { return c == '/' || c == '\\'; }; if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':')) return false; + // The filesystem calls stop at a NUL, so they would act on a shorter path than the one checked here. + if (rel_path.find('\0') != std::string::npos) + return false; for (size_t start = 0; start <= rel_path.size();) { size_t end = start; while (end < rel_path.size() && !is_separator(rel_path[end])) @@ -1103,7 +1107,10 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p } // Resolve against the canonical root so a symlink inside it cannot lead back out. try { - const std::string root_str = boost::filesystem::weakly_canonical(root).string(); + std::string root_str = boost::filesystem::weakly_canonical(root).string(); + // A trailing separator on root would otherwise fail the prefix match below for every path. + while (!root_str.empty() && (root_str.back() == '/' || root_str.back() == boost::filesystem::path::preferred_separator)) + root_str.pop_back(); const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string(); return full_str.compare(0, root_str.size(), root_str) == 0 && (full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator); @@ -1112,6 +1119,37 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p } } +bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root) +{ + if (target.empty() || target.front() == '/' || target.front() == '\\' || (target.size() > 1 && target[1] == ':')) + return false; + // A relative target without ".." only descends from the link's directory, so no chain of such links can leave root. + const size_t sep = link_rel_path.find_last_of("/\\"); + return is_path_within_root((sep == std::string::npos ? std::string() : link_rel_path.substr(0, sep + 1)) + target, root); +} + +bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root) +{ + const boost::filesystem::path rel = path.lexically_relative(root); + return !rel.empty() && rel != "." && is_path_within_root(rel.string(), root); +} + +bool is_safe_to_open_file_name(const std::string &file_name) +{ + // Formats that cannot carry macros or scripts. Legacy and OpenDocument office files, HTML and SVG are left out on purpose. + static const std::vector safe_extensions = { + "jpg", "jpeg", "jfif", "pjpeg", "pjp", "png", "gif", "bmp", "webp", "tif", "tiff", + "pdf", "txt", "md", "csv", "docx", "xlsx", "pptx", + "stl", "obj", "3mf", "amf", "ply", "step", "stp", "iges", "igs", "dxf", + "mp4", "mov", "webm"}; + // The name must end in the extension itself: Windows drops trailing dots and spaces and reads ':' as a stream separator. + const size_t dot = file_name.find_last_of('.'); + if (dot == std::string::npos || file_name.find_first_of("/\\:") != std::string::npos) + return false; + const std::string extension = boost::algorithm::to_lower_copy(file_name.substr(dot + 1)); + return std::find(safe_extensions.begin(), safe_extensions.end(), extension) != safe_extensions.end(); +} + bool is_img_file(const std::string &path) { return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg"); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index dc6bbba163..361c2f15a7 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -1035,14 +1035,9 @@ if (UNIX AND NOT APPLE) find_package(GTK${SLIC3R_GTK} REQUIRED) pkg_check_modules(LIBSECRET REQUIRED libsecret-1) pkg_check_modules(webkit2gtk REQUIRED webkit2gtk-4.1) - if (FLATPAK) - # I don't know why this is needed, but for whatever reason slic3r isn't - # linking to X11 and webkit2gtk. force it. - find_package(X11 REQUIRED) - target_link_libraries(libslic3r_gui ${X11_LIBRARIES} ${webkit2gtk_LIBRARIES}) - endif() + find_package(X11 REQUIRED) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS} ${webkit2gtk_INCLUDE_DIRS}) - target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES}) + target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES} ${webkit2gtk_LIBRARIES} ${X11_LIBRARIES}) # Propagate GDK backend detection results as compile definitions so that # LinuxDisplayBackend.cpp can include the right GDK headers. diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp index 444daa4f91..67998b59d9 100644 --- a/src/slic3r/GUI/Auxiliary.cpp +++ b/src/slic3r/GUI/Auxiliary.cpp @@ -428,7 +428,7 @@ void AuFile::on_dclick(wxMouseEvent &evt) if (m_type == AddFileButton) return; else - wxLaunchDefaultApplication(m_file_path.wstring(), 0); + desktop_open_project_attachment(this, m_file_path); } void AuFile::on_mouse_left_up(wxMouseEvent &evt) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 5a58ec7b6c..cc687ff84a 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -171,14 +171,16 @@ void ConfigManipulation::check_adaptive_pressure_advance_model(DynamicPrintConfi return; const auto* model = config->option("adaptive_pressure_advance_model"); - if (model == nullptr || model->values.empty()) + if (model == nullptr) return; - std::string raw_model; - for (const std::string& chunk : model->values) - raw_model += chunk; - - std::string error = AdaptivePAProcessor::validate_adaptive_pa_model(raw_model); + // Each extruder variant holds its own model. + std::string error; + for (const std::string& variant_model : model->values) { + error = AdaptivePAProcessor::validate_adaptive_pa_model(variant_model); + if (!error.empty()) + break; + } if (!error.empty()) { wxString msg_text = _L("Adaptive Pressure Advance model validation failed:\n"); msg_text += from_u8(error); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index ffa067308c..01ff6de714 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -6546,8 +6546,11 @@ void GLCanvas3D::render_thumbnail_internal(ThumbnailData& thumbnail_data, const // glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f)); BOOST_LOG_TRIVIAL(info) << boost::format("render_thumbnail: finished"); - // Puts the canvas viewport back in place of the thumbnail one set above. - wxGetApp().plater()->get_camera().apply_viewport(); + // Puts the canvas viewport back in place of the thumbnail one set above. The CLI renders + // thumbnails with no application and no plater, so there is no canvas viewport to restore. + if (wxTheApp != nullptr) + if (Plater *plater = wxGetApp().plater(); plater != nullptr) + plater->get_camera().apply_viewport(); } void GLCanvas3D::render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 78a511c90c..d7bd96f178 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -14,6 +14,9 @@ #include #include +#include +#include + #if __APPLE__ #import #elif _WIN32 @@ -27,11 +30,13 @@ #include "AboutDialog.hpp" #include "MsgDialog.hpp" +#include "Plater.hpp" #include "format.hpp" #include "WebUserLoginDialog.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/Utils.hpp" namespace Slic3r { @@ -531,6 +536,11 @@ boost::filesystem::path into_path(const wxString &str) return boost::filesystem::path(str.wx_str()); } +wxString file_url_from_path(const boost::filesystem::path &path) +{ + return wxFileSystem::FileNameToURL(wxFileName(from_path(path))); +} + void about() { AboutDialog dlg; @@ -635,4 +645,26 @@ void desktop_open_any_folder( const std::string& path ) } +bool desktop_open_project_attachment(wxWindow *parent, const boost::filesystem::path &path) +{ + // The auxiliary path is UTF-8, which is what boost::filesystem reads a narrow string as. + const boost::filesystem::path aux_root(wxGetApp().plater()->model().get_auxiliary_file_temp_path()); + boost::system::error_code ec; + if (!is_absolute_path_within_root(path, aux_root) || !boost::filesystem::is_regular_file(path, ec)) + return false; + + // Attachments come with the project and carry no download mark, so the desktop would open them without a warning. + if (!is_safe_to_open_file_name(path.filename().string())) { + MessageDialog dlg(parent, + wxString::Format(_L("\"%s\" is not a plain document, image or model file. Opening it may run it as a " + "program or script on this computer.\n\n" + "Only open attachments from projects you trust. Open it anyway?"), + from_path(path.filename())), + _L("Open attachment"), wxICON_WARNING | wxYES_NO); + if (dlg.ShowModal() != wxID_YES) + return false; + } + return wxLaunchDefaultApplication(from_path(path), 0); +} + } } diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index db882b79cf..a5bbb35f6f 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -76,6 +76,9 @@ std::string into_u8(const wxString &str); wxString from_path(const boost::filesystem::path &path); // boost path from wxString boost::filesystem::path into_path(const wxString &str); +// file:// URL of a local path, percent-encoded so characters such as '#', '%' and '?' stay part of the path. +// Append any query or fragment to the result. +wxString file_url_from_path(const boost::filesystem::path &path); // Display an About dialog extern void about(); @@ -83,6 +86,9 @@ extern void about(); extern void desktop_open_datadir_folder(); // Ask the destop to open one folder extern void desktop_open_any_folder(const std::string& path); +// Ask the desktop to open a file from the project's auxiliary directory, after a confirmation +// unless its type is known to be plain content. Returns false if the file was not opened. +extern bool desktop_open_project_attachment(wxWindow *parent, const boost::filesystem::path &path); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index a41001f9e0..72849dd78e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1512,11 +1512,33 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size()); dest_file = decode(extra.substr(0, n), stat.m_filename); } + if (!is_path_within_root(dest_file, plugin_folder)) { + BOOST_LOG_TRIVIAL(error) << "[install_plugin] entry " << dest_file << " resolves outside " << plugin_folder.string(); + close_zip_reader(&archive); + if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); } + return InstallStatusUnzipFailed; + } auto dest_path = plugin_folder / dest_file; - boost::filesystem::create_directories(dest_path.parent_path()); std::string dest_zip_file = encode_path(dest_path.string().c_str()); +#ifndef WIN32 + // Validate a symlink's target before anything at the destination is replaced. + const bool is_link = S_ISLNK(stat.m_external_attr >> 16); + std::string link; + if (is_link) { + link.assign(stat.m_uncomp_size, 0); + if (!mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0) || + !is_symlink_target_within_root(dest_file, link, plugin_folder)) { + BOOST_LOG_TRIVIAL(error) << "[install_plugin] link " << dest_file << " -> " << link << " is unreadable or resolves outside " << plugin_folder.string(); + close_zip_reader(&archive); + if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); } + return InstallStatusUnzipFailed; + } + } +#endif try { - if (fs::exists(dest_path)) { + boost::filesystem::create_directories(dest_path.parent_path()); + // symlink_status so that an existing symlink, dangling or not, is replaced rather than written through. + if (fs::exists(fs::symlink_status(dest_path))) { boost::system::error_code ec; fs::remove(dest_path, ec); if (ec) { @@ -1544,9 +1566,8 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP } mz_bool res = 0; #ifndef WIN32 - if (S_ISLNK(stat.m_external_attr >> 16)) { - std::string link(stat.m_uncomp_size + 1, 0); - res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0); + if (is_link) { + res = 1; try { boost::filesystem::create_symlink(link, dest_path); } catch (const std::exception &e) { diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index a57dbf0b97..343d95d3ff 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1997,6 +1997,126 @@ bool MainFrame::can_reslice() const return (m_plater != nullptr) && !m_plater->model().objects.empty(); } +namespace { +// Orca: stable key persisted by the "remember last print action" preference. Reordering PrintSelectType +// must not silently remap a saved preference, so the key never derives from the enum value. +const char* print_select_type_key(MainFrame::PrintSelectType type) +{ + switch (type) { + case MainFrame::ePrintAll: return "print_all"; + case MainFrame::ePrintPlate: return "print_plate"; + case MainFrame::eExportSlicedFile: return "export_sliced_file"; + case MainFrame::eExportAllSlicedFile: return "export_all_sliced_file"; + case MainFrame::eExportGcode: return "export_gcode"; + case MainFrame::eSendGcode: return "send_gcode"; + case MainFrame::eSendToPrinter: return "send_to_printer"; + case MainFrame::eSendToPrinterAll: return "send_to_printer_all"; + case MainFrame::ePrintMultiMachine: return "print_multi_machine"; + case MainFrame::eUploadGcode: break; // Orca: no dropdown entry, never selectable + } + return ""; +} + +// Orca: single source for the print button and print dropdown labels +wxString print_select_type_label(MainFrame::PrintSelectType type) +{ + switch (type) { + case MainFrame::ePrintAll: return _L("Print all"); + case MainFrame::ePrintPlate: return _L("Print plate"); + case MainFrame::eExportSlicedFile: return _L("Export plate sliced file"); + case MainFrame::eExportAllSlicedFile: return _L("Export all sliced file"); + case MainFrame::eExportGcode: return _L("Export G-code file"); + case MainFrame::eSendGcode: return _L_CONTEXT("Print", "Verb"); + case MainFrame::eSendToPrinter: return _L("Send"); + case MainFrame::eSendToPrinterAll: return _L("Send all"); + case MainFrame::ePrintMultiMachine: return _L("Send to Multi-device"); + case MainFrame::eUploadGcode: break; // Orca: no dropdown entry, never selectable + } + return _L("Print plate"); +} +} // namespace + +std::vector MainFrame::available_print_actions() const +{ + std::vector actions; + const auto preset_bundle = wxGetApp().preset_bundle; + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + + if (preset_bundle && !preset_bundle->is_bbl_vendor() && !use_printer_agents) { + // ThirdParty actions + actions.push_back(eSendGcode); + // Orca: when the printer accepts a .gcode.3mf (the "Support 3MF as gcode" option), + // also offer exporting the sliced .gcode.3mf bundle + const auto* use_3mf_opt = preset_bundle->printers.get_edited_preset().config.option("use_3mf"); + if (use_3mf_opt != nullptr && use_3mf_opt->value) + actions.push_back(eExportSlicedFile); + actions.push_back(eExportGcode); + return actions; + } + + // Orca Slicer actions + bool support_send = true; + bool support_print_all = true; + if (preset_bundle && !preset_bundle->use_bbl_network() && !use_printer_agents) { + support_send = false; // All 3rd print hosts do not have the send options + + const auto& cfg = preset_bundle->printers.get_edited_preset().config; + const auto* host_type_opt = cfg.option>("host_type"); + // Only simply print support uploading all plates + support_print_all = host_type_opt != nullptr && host_type_opt->value == PrintHostType::htSimplyPrint; + } + + actions.push_back(ePrintPlate); + if (support_print_all) + actions.push_back(ePrintAll); + if (support_send) { + actions.push_back(eSendToPrinter); + actions.push_back(eSendToPrinterAll); + } + if (enable_multi_machine) + actions.push_back(ePrintMultiMachine); + actions.push_back(eExportSlicedFile); + actions.push_back(eExportAllSlicedFile); + actions.push_back(eExportGcode); + return actions; +} + +void MainFrame::select_print_action(PrintSelectType select_type) +{ + m_print_btn->SetLabel(print_select_type_label(select_type)); + m_print_select = select_type; + remember_print_select(select_type); + m_print_enable = get_enable_print_status(); + m_print_btn->Enable(m_print_enable); + this->Layout(); + fit_tab_labels(); // ORCA on label change +} + +void MainFrame::remember_print_select(PrintSelectType select_type) +{ + if (!wxGetApp().app_config->get_bool("remember_print_action")) + return; + // AppConfig is marked dirty here and flushed by the regular autosave + wxGetApp().app_config->set("last_print_action", print_select_type_key(select_type)); +} + +bool MainFrame::get_remembered_print_select(PrintSelectType& out) const +{ + if (!wxGetApp().app_config->get_bool("remember_print_action")) + return false; + const std::string saved = wxGetApp().app_config->get("last_print_action"); + if (saved.empty()) + return false; + // Orca: only restore an action the current printer actually offers in the dropdown + for (PrintSelectType type : available_print_actions()) { + if (saved == print_select_type_key(type)) { + out = type; + return true; + } + } + return false; +} + wxBoxSizer* MainFrame::create_side_tools() { enable_multi_machine = wxGetApp().is_enable_multi_machine(); @@ -2016,6 +2136,14 @@ wxBoxSizer* MainFrame::create_side_tools() m_print_btn = new SideButton(print_panel, _L("Print plate"), ""); m_print_option_btn = new SideButton(print_panel, "", "sidebutton_dropdown", 0, 14); + // Orca: restore the last used print/export action if the user opted to remember it + PrintSelectType remembered_print_select; + if (get_remembered_print_select(remembered_print_select)) { + m_print_select = remembered_print_select; + m_print_btn->SetLabel(print_select_type_label(remembered_print_select)); + fit_tab_labels(); // ORCA on label change + } + auto slice_sizer = new wxBoxSizer(wxHORIZONTAL); slice_sizer->Add(m_slice_option_btn, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(1)); slice_sizer->Add(m_slice_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(1)); @@ -2178,193 +2306,15 @@ wxBoxSizer* MainFrame::create_side_tools() m_print_option_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { SidePopup* p = new SidePopup(this); - - if (wxGetApp().preset_bundle - && !wxGetApp().preset_bundle->is_bbl_vendor() - && !wxGetApp().app_config->get_bool("use_printer_agents")) { - // ThirdParty Buttons - SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); - export_gcode_btn->SetCornerRadius(0); - export_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Export G-code file")); - m_print_select = eExportGcode; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change + for (PrintSelectType type : available_print_actions()) { + SideButton* btn = new SideButton(p, print_select_type_label(type), ""); + btn->SetCornerRadius(0); + btn->Bind(wxEVT_BUTTON, [this, p, type](wxCommandEvent&) { + select_print_action(type); p->Dismiss(); }); - - // upload and print - SideButton* send_gcode_btn = new SideButton(p, _L_CONTEXT("Print", "Verb"), ""); - send_gcode_btn->SetCornerRadius(0); - send_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L_CONTEXT("Print", "Verb")); - m_print_select = eSendGcode; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - - p->append_button(send_gcode_btn); - - // Orca: when the printer accepts a .gcode.3mf (the "Support 3MF as gcode" option), - // also offer exporting the sliced .gcode.3mf bundle - const auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; - const auto* use_3mf_opt = printer_config.option("use_3mf"); - if (use_3mf_opt != nullptr && use_3mf_opt->value) { - SideButton* export_sliced_file_btn = new SideButton(p, _L("Export plate sliced file"), ""); - export_sliced_file_btn->SetCornerRadius(0); - export_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Export plate sliced file")); - m_print_select = eExportSlicedFile; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - p->append_button(export_sliced_file_btn); - } - - p->append_button(export_gcode_btn); + p->append_button(btn); } - else { - //Orca Slicer Buttons - SideButton* print_plate_btn = new SideButton(p, _L("Print plate"), ""); - print_plate_btn->SetCornerRadius(0); - - SideButton* send_to_printer_btn = new SideButton(p, _L("Send"), ""); - send_to_printer_btn->SetCornerRadius(0); - - SideButton* export_sliced_file_btn = new SideButton(p, _L("Export plate sliced file"), ""); - export_sliced_file_btn->SetCornerRadius(0); - - SideButton* export_all_sliced_file_btn = new SideButton(p, _L("Export all sliced file"), ""); - export_all_sliced_file_btn->SetCornerRadius(0); - - print_plate_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Print plate")); - m_print_select = ePrintPlate; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - - SideButton* print_all_btn = new SideButton(p, _L("Print all"), ""); - print_all_btn->SetCornerRadius(0); - print_all_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Print all")); - m_print_select = ePrintAll; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - - send_to_printer_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Send")); - m_print_select = eSendToPrinter; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - - SideButton* send_to_printer_all_btn = new SideButton(p, _L("Send all"), ""); - send_to_printer_all_btn->SetCornerRadius(0); - send_to_printer_all_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Send all")); - m_print_select = eSendToPrinterAll; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - - export_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Export plate sliced file")); - m_print_select = eExportSlicedFile; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - - export_all_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Export all sliced file")); - m_print_select = eExportAllSlicedFile; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - - bool support_send = true; - bool support_print_all = true; - - const auto preset_bundle = wxGetApp().preset_bundle; - if (preset_bundle) { - if (preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { - // BBL network support everything - } else { - support_send = false; // All 3rd print hosts do not have the send options - - auto cfg = preset_bundle->printers.get_edited_preset().config; - const auto host_type = cfg.option>("host_type")->value; - - // Only simply print support uploading all plates - support_print_all = host_type == PrintHostType::htSimplyPrint; - } - } - - p->append_button(print_plate_btn); - if (support_print_all) { - p->append_button(print_all_btn); - } - if (support_send) { - p->append_button(send_to_printer_btn); - p->append_button(send_to_printer_all_btn); - } - if (enable_multi_machine) { - SideButton* print_multi_machine_btn = new SideButton(p, _L("Send to Multi-device"), ""); - print_multi_machine_btn->SetCornerRadius(0); - print_multi_machine_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Send to Multi-device")); - m_print_select = ePrintMultiMachine; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - p->append_button(print_multi_machine_btn); - } - p->append_button(export_sliced_file_btn); - p->append_button(export_all_sliced_file_btn); - SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); - export_gcode_btn->SetCornerRadius(0); - export_gcode_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { - m_print_btn->SetLabel(_L("Export G-code file")); - m_print_select = eExportGcode; - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - fit_tab_labels(); // ORCA on label change - p->Dismiss(); - }); - p->append_button(export_gcode_btn); - } - p->Popup(m_print_btn); } ); @@ -4224,38 +4174,22 @@ void MainFrame::on_config_changed(DynamicPrintConfig* config) const void MainFrame::set_print_button_to_default(PrintSelectType select_type) { - if (select_type == PrintSelectType::ePrintPlate) { - m_print_btn->SetLabel(_L("Print plate")); - m_print_select = ePrintPlate; - if (m_print_enable) - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - } else if (select_type == PrintSelectType::eSendGcode) { - m_print_btn->SetLabel(_L_CONTEXT("Print", "Verb")); - m_print_select = eSendGcode; - if (m_print_enable) - m_print_enable = get_enable_print_status() && can_send_gcode(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - } else if (select_type == PrintSelectType::eExportGcode) { - m_print_btn->SetLabel(_L("Export G-code file")); - m_print_select = eExportGcode; - if (m_print_enable) - m_print_enable = get_enable_print_status() && can_send_gcode(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - } else if (select_type == PrintSelectType::eExportSlicedFile) { - m_print_btn->SetLabel(_L("Export plate sliced file")); - m_print_select = eExportSlicedFile; - if (m_print_enable) - m_print_enable = get_enable_print_status(); - m_print_btn->Enable(m_print_enable); - this->Layout(); - } else { - // unsupport - return; - } + // Orca: keep the user's remembered print/export action instead of resetting it to the computed + // default. get_remembered_print_select() already rejects anything this printer does not offer. + PrintSelectType remembered; + if (get_remembered_print_select(remembered)) + select_type = remembered; + + if (select_type == eUploadGcode) + return; // unsupported: no dropdown entry exists for this action + + m_print_btn->SetLabel(print_select_type_label(select_type)); + m_print_select = select_type; + // get_enable_print_status() already applies can_send_gcode() to the actions that need it + if (m_print_enable) + m_print_enable = get_enable_print_status(); + m_print_btn->Enable(m_print_enable); + this->Layout(); } void MainFrame::add_to_recent_projects(const wxString& filename) diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index dce12c3fc1..0033f41a18 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -13,6 +13,7 @@ #include #include +#include #include "GUI_Utils.hpp" #include "Event.hpp" @@ -430,6 +431,14 @@ public: // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig void on_config_changed(DynamicPrintConfig* cfg) const ; void set_print_button_to_default(PrintSelectType select_type); + // Orca: the print/export actions the current printer offers, in the order the dropdown lists them. + // The dropdown is built from this, and a remembered action is only restored if it appears here. + std::vector available_print_actions() const; + // Orca: apply an action picked from the print dropdown to the print button + void select_print_action(PrintSelectType select_type); + // Orca: remember the user's preferred print/export action across sessions (see "remember_print_action" preference) + void remember_print_select(PrintSelectType select_type); + bool get_remembered_print_select(PrintSelectType& out) const; bool can_save() const; bool can_save_as() const; diff --git a/src/slic3r/GUI/MarkdownTip.cpp b/src/slic3r/GUI/MarkdownTip.cpp index 7ffeae9e36..a22f4da87f 100644 --- a/src/slic3r/GUI/MarkdownTip.cpp +++ b/src/slic3r/GUI/MarkdownTip.cpp @@ -98,10 +98,7 @@ void MarkdownTip::LoadStyle() ph /= "tooltip/styled.html"; _data_dir = false; } - auto url = ph.string(); - std::replace(url.begin(), url.end(), '\\', '/'); - url = "file:///" + url; - _tipView->LoadURL(from_u8(url)); + _tipView->LoadURL(file_url_from_path(ph)); _lastTip.clear(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 1b23642bbe..1290729509 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -287,6 +287,30 @@ static void set_config_values(DynamicPrintConfig *config, const std::string &key } } +// Orca: a calibration print sets pressure advance explicitly to 0 on each extruder variant that +// has it disabled, so the value stored in the printer does not skew the result. Variants with +// pressure advance enabled keep their own value. +static void zero_pressure_advance_where_disabled(DynamicPrintConfig *filament_config) +{ + auto enable_pa = filament_config->option("enable_pressure_advance"); + auto pa = filament_config->option("pressure_advance"); + auto adaptive_pa = filament_config->option("adaptive_pressure_advance"); + if (!enable_pa || !pa || !adaptive_pa || pa->empty() || adaptive_pa->empty()) + return; + // All variant keys share the variant count; widen a short one rather than index past it. + if (pa->size() < enable_pa->size()) + pa->resize(enable_pa->size()); + if (adaptive_pa->size() < enable_pa->size()) + adaptive_pa->resize(enable_pa->size()); + for (size_t variant = 0; variant < enable_pa->size(); ++variant) { + if (enable_pa->get_at(variant)) + continue; + enable_pa->values[variant] = true; + pa->values[variant] = 0.0; + adaptive_pa->values[variant] = false; + } +} + bool Plater::has_illegal_filename_characters(const wxString& wxs_name) { std::string name = into_u8(wxs_name); @@ -3673,7 +3697,7 @@ void Sidebar::update_all_preset_comboboxes() wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; if(url.empty()) - url = wxString::Format("file://%s/web/orca/missing_connection.html", from_u8(resources_dir())); + url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/orca/missing_connection.html"); else { const auto host_type = cfg.option>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) @@ -17120,11 +17144,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) set_config_values(print_config, "default_jerk", 0); } - if (!filament_config->option("enable_pressure_advance")->get_at(0)) { - set_config_values(filament_config, "enable_pressure_advance", true); - set_config_values(filament_config, "pressure_advance", 0.0); - set_config_values(filament_config, "adaptive_pressure_advance", false); - } + zero_pressure_advance_where_disabled(filament_config); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); @@ -17186,11 +17206,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) set_config_values(print_config, "default_jerk", 0); } - if (!filament_config->option("enable_pressure_advance")->get_at(0)) { - set_config_values(filament_config, "enable_pressure_advance", true); - set_config_values(filament_config, "pressure_advance", 0.0); - set_config_values(filament_config, "adaptive_pressure_advance", false); - } + zero_pressure_advance_where_disabled(filament_config); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); @@ -17252,11 +17268,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) set_config_values(print_config, "default_jerk", 0); } - if (!filament_config->option("enable_pressure_advance")->get_at(0)) { - set_config_values(filament_config, "enable_pressure_advance", true); - set_config_values(filament_config, "pressure_advance", 0.0); - set_config_values(filament_config, "adaptive_pressure_advance", false); - } + zero_pressure_advance_where_disabled(filament_config); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true}); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 055449c35d..d9aedec79b 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1830,7 +1830,10 @@ void PreferencesDialog::create_items() auto item_mix_print_high_low_temperature = create_item_checkbox(_L("Remove mixed temperature restriction"), _L("With this option enabled, you can print materials with a large temperature difference together."), "enable_high_low_temp_mixed_printing"); g_sizer->Add(item_mix_print_high_low_temperature); - + + auto item_remember_print_action = create_item_checkbox(_L("Remember last print action"), _L("If enabled, OrcaSlicer will remember the last selected option in the print button's dropdown (e.g. Print, Export plate sliced file, Export G-code file) and use it as the default on next startup."), "remember_print_action"); + g_sizer->Add(item_remember_print_action); + //// CONTROL > Camera g_sizer->Add(create_item_title(_L("Camera")), 1, wxEXPAND); diff --git a/src/slic3r/GUI/PrivacyUpdateDialog.cpp b/src/slic3r/GUI/PrivacyUpdateDialog.cpp index c417767a42..c396e85bfe 100644 --- a/src/slic3r/GUI/PrivacyUpdateDialog.cpp +++ b/src/slic3r/GUI/PrivacyUpdateDialog.cpp @@ -57,9 +57,7 @@ PrivacyUpdateDialog::PrivacyUpdateDialog(wxWindow* parent, wxWindowID id, const fs::path ph(resources_dir()); ph /= "tooltip/privacyupdate.html"; - m_host_url = ph.string(); - std::replace(m_host_url.begin(), m_host_url.end(), '\\', '/'); - m_host_url = "file:///" + m_host_url; + m_host_url = into_u8(file_url_from_path(ph)); m_vebview_release_note->LoadURL(from_u8(m_host_url)); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp index 8d87939afc..127400c8b1 100644 --- a/src/slic3r/GUI/Project.cpp +++ b/src/slic3r/GUI/Project.cpp @@ -45,10 +45,10 @@ const std::vector license_list = { ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style) { SetBackgroundColour(*wxWHITE); - m_project_home_url = wxString::Format("file://%s/web/model/index.html", from_u8(resources_dir())); + m_project_home_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/model/index.html"); wxString strlang = wxGetApp().current_language_code_safe(); if (strlang != "") - m_project_home_url = wxString::Format("file://%s/web/model/index.html?lang=%s", from_u8(resources_dir()), strlang); + m_project_home_url += "?lang=" + strlang; wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); @@ -293,10 +293,8 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt) if (!accessory_path.empty()) { std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString()); fs::path path(decode_path); - - if (fs::exists(path)) { - wxLaunchDefaultApplication(path.wstring(), 0); - } + if (!desktop_open_project_attachment(this, path)) + BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: not opening " << decode_path; } } else if (strCmd == "request_3mf_info") { diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index c8338dc79a..58b3a40ca9 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -510,7 +510,7 @@ void UpdateVersionDialog::update_version_info(wxString release_note, wxString ve out_buf->append(text, size); }, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0); html_source.append(""); - m_vebview_release_note->LoadURL("file://" + (boost::filesystem::path (resources_dir()) / "web/guide/0/index.html").string()); + m_vebview_release_note->LoadURL(file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/0/index.html")); SetMinSize(GetSize()); SetMaxSize(GetSize()); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index b5b93fca17..c824c16d5d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4415,15 +4415,15 @@ void TabFilament::build() optgroup->append_single_option_line("pellet_flow_coefficient", "printer_basic_information_advanced#pellet-modded-printer"); optgroup->append_single_option_line("filament_flow_ratio", "material_flow_ratio_and_pressure_advance#flow-ratio", 0); - optgroup->append_single_option_line("enable_pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance"); - optgroup->append_single_option_line("pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance"); + optgroup->append_single_option_line("enable_pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance", 0); + optgroup->append_single_option_line("pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance", 0); // Orca: adaptive pressure advance and calibration model - optgroup->append_single_option_line("adaptive_pressure_advance", "material_flow_ratio_and_pressure_advance#enable-adaptive-pressure-advance-beta"); - optgroup->append_single_option_line("adaptive_pressure_advance_overhangs", "material_flow_ratio_and_pressure_advance#enable-adaptive-pressure-advance-for-overhangs-beta"); - optgroup->append_single_option_line("adaptive_pressure_advance_bridges", "material_flow_ratio_and_pressure_advance#pressure-advance-for-bridges"); + optgroup->append_single_option_line("adaptive_pressure_advance", "material_flow_ratio_and_pressure_advance#enable-adaptive-pressure-advance-beta", 0); + optgroup->append_single_option_line("adaptive_pressure_advance_overhangs", "material_flow_ratio_and_pressure_advance#enable-adaptive-pressure-advance-for-overhangs-beta", 0); + optgroup->append_single_option_line("adaptive_pressure_advance_bridges", "material_flow_ratio_and_pressure_advance#pressure-advance-for-bridges", 0); - Option option = optgroup->get_option("adaptive_pressure_advance_model"); + Option option = optgroup->get_option("adaptive_pressure_advance_model", 0); option.opt.full_width = true; option.opt.is_code = true; option.opt.height = 15; @@ -4432,7 +4432,7 @@ void TabFilament::build() DynamicPrintConfig& filament_config = m_preset_bundle->filaments.get_edited_preset().config; update_dirty(); - if (opt_key == "adaptive_pressure_advance_model") + if (opt_key.substr(0, opt_key.find('#')) == "adaptive_pressure_advance_model") m_config_manipulation.check_adaptive_pressure_advance_model(&filament_config); on_value_change(opt_key, value); @@ -4829,8 +4829,11 @@ void TabFilament::toggle_options() } if (m_active_page->title() == L("Filament")) { - bool pa = m_config->opt_bool("enable_pressure_advance", 0); - toggle_option("pressure_advance", pa); + const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0; + const unsigned int variant_idx = (unsigned int) std::max(selection, 0); + + bool pa = m_config->opt_bool("enable_pressure_advance", variant_idx); + toggle_option("pressure_advance", pa, 0); //Orca: Enable the plates that should be visible when multi bed support is enabled or a BBL printer is selected; otherwise, enable only the plate visible for the selected bed type. DynamicConfig& proj_cfg = m_preset_bundle->project_config; @@ -4858,12 +4861,12 @@ void TabFilament::toggle_options() // Orca: adaptive pressure advance and calibration model // If PA is not enabled, disable adaptive pressure advance and hide the model section // If adaptive PA is not enabled, hide the adaptive PA model section - toggle_option("adaptive_pressure_advance", pa); - toggle_option("adaptive_pressure_advance_overhangs", pa); - bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", 0); - toggle_line("adaptive_pressure_advance_overhangs", has_adaptive_pa && pa); - toggle_line("adaptive_pressure_advance_model", has_adaptive_pa && pa); - toggle_line("adaptive_pressure_advance_bridges", has_adaptive_pa && pa); + toggle_option("adaptive_pressure_advance", pa, 0); + toggle_option("adaptive_pressure_advance_overhangs", pa, 0); + bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", variant_idx); + toggle_line("adaptive_pressure_advance_overhangs", has_adaptive_pa && pa, 0); + toggle_line("adaptive_pressure_advance_model", has_adaptive_pa && pa, 0); + toggle_line("adaptive_pressure_advance_bridges", has_adaptive_pa && pa, 0); bool is_pellet_printer = printer_cfg.opt_bool("pellet_modded_printer"); toggle_line("pellet_flow_coefficient", is_pellet_printer); @@ -4871,8 +4874,6 @@ void TabFilament::toggle_options() toggle_line("activate_chamber_temp_control", printer_cfg.opt_bool("support_chamber_temp_control")); - const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0; - const unsigned int variant_idx = (unsigned int) std::max(selection, 0); std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", variant_idx); bool enable_fit = volumetric_speed_cos != "0 0 0 0 0 0"; toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_idx); @@ -5205,7 +5206,6 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("adaptive_bed_mesh_margin", "printer_basic_information_adaptive_bed_mesh#mesh-margin"); optgroup = page->new_optgroup(L("Accessory"), "param_accessory"); - optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type", 0); optgroup->append_single_option_line("nozzle_hrc", "printer_basic_information_accessory#nozzle-hrc"); optgroup->append_single_option_line("auxiliary_fan", "printer_basic_information_accessory#auxiliary-part-cooling-fan"); optgroup->append_single_option_line("fan_direction"); @@ -5720,6 +5720,7 @@ if (is_marlin_flavor) auto optgroup = page->new_optgroup(L("Basic information"), L"param_information", -1, true); optgroup->append_single_option_line("nozzle_diameter", "printer_extruder_basic_information#nozzle-diameter", extruder_idx); + optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type", extruder_idx); //optgroup->append_single_option_line("nozzle_volume_type", "", extruder_idx); optgroup->append_single_option_line("nozzle_volume", "printer_extruder_basic_information#nozzle-volume", extruder_idx); @@ -5731,7 +5732,7 @@ if (is_marlin_flavor) optgroup->m_on_change = [this, extruder_idx](const t_config_option_key& opt_key, boost::any value) { bool is_SEMM = m_config->opt_bool("single_extruder_multi_material"); - if (is_SEMM && m_extruders_count > 1 && opt_key.find_first_of("nozzle_diameter") != std::string::npos) + if (is_SEMM && m_extruders_count > 1 && boost::starts_with(opt_key, "nozzle_diameter")) { SuppressBackgroundProcessingUpdate sbpu; const double new_nd = boost::any_cast(value); diff --git a/src/slic3r/GUI/UserManager.cpp b/src/slic3r/GUI/UserManager.cpp index e874c2158d..f181f3e568 100644 --- a/src/slic3r/GUI/UserManager.cpp +++ b/src/slic3r/GUI/UserManager.cpp @@ -31,14 +31,15 @@ int UserManager::parse_json(std::string payload) { bool restored_json = false; json j; - json j_pre = json::parse(payload); - if (j_pre.empty()) { - return -1; - } //bind/unbind try { + json j_pre = json::parse(payload); + if (j_pre.empty()) { + return -1; + } + if (j_pre.contains("bind")) { if (j_pre["bind"].contains("command")) { diff --git a/src/slic3r/GUI/WebDownPluginDlg.cpp b/src/slic3r/GUI/WebDownPluginDlg.cpp index dca17110f9..6358582831 100644 --- a/src/slic3r/GUI/WebDownPluginDlg.cpp +++ b/src/slic3r/GUI/WebDownPluginDlg.cpp @@ -38,9 +38,7 @@ DownPluginFrame::DownPluginFrame(GUI_App *pGUI) : wxDialog((wxWindow *) (pGUI->m // set the frame icon wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL); - wxString TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/6/index.html").make_preferred().string()); - - TargetUrl = "file://" + TargetUrl; + wxString TargetUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/6/index.html"); // Create the webview m_browser = WebView::CreateWebView(this, TargetUrl); diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 9cea8bec28..5370509587 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -218,37 +218,38 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load) m_page = startpage; BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, load=%1%, start_page=%2%")%load%int(startpage); //wxLogMessage("GUIDE: webpage_1 %s", (boost::filesystem::path(resources_dir()) / "web\\guide\\1\\index.html").make_preferred().string().c_str() ); - wxString TargetUrl = from_u8( (boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string() ); + const wxString guide_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/0/index.html"); + wxString TargetUrl = guide_url + "?target=1"; //wxLogMessage("GUIDE: webpage_2 %s", TargetUrl.mb_str()); if (startpage == BBL_WELCOME){ SetTitle(_L("Setup Wizard")); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string()); + TargetUrl = guide_url + "?target=1"; } else if (startpage == BBL_REGION) { SetTitle(_L("Setup Wizard")); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=11").make_preferred().string()); + TargetUrl = guide_url + "?target=11"; } else if (startpage == BBL_MODELS) { SetTitle(_L("Setup Wizard")); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); + TargetUrl = guide_url + "?target=21"; } else if (startpage == BBL_FILAMENTS) { SetTitle(_L("Setup Wizard")); int nSize = m_ProfileJson["model"].size(); if (nSize>0) - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=22").make_preferred().string()); + TargetUrl = guide_url + "?target=22"; else - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); + TargetUrl = guide_url + "?target=21"; } else if (startpage == BBL_FILAMENT_ONLY) { SetTitle(""); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=23").make_preferred().string()); + TargetUrl = guide_url + "?target=23"; } else if (startpage == BBL_MODELS_ONLY) { SetTitle(""); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=24").make_preferred().string()); + TargetUrl = guide_url + "?target=24"; } else { SetTitle(_L("Setup Wizard")); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); + TargetUrl = guide_url + "?target=21"; } wxString strlang = wxGetApp().current_language_code_safe(); @@ -256,7 +257,6 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load) if (strlang != "") TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang); - TargetUrl = "file://" + TargetUrl; if (load) load_url(TargetUrl); @@ -1284,7 +1284,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r entry["vendor"] = vp.id; entry["nozzle_diameter"] = nozzle_str; entry["materials"] = materials_str; - entry["cover"] = cover_path.string(); + entry["cover"] = into_u8(file_url_from_path(cover_path)); entry["nozzle_selected"] = ""; entry["sub_path"] = ""; m_ProfileJson["model"].push_back(entry); @@ -1405,23 +1405,26 @@ bool GuideFrame::BuildProfileDataFromVendors() // is served from the shipped profiles. Each is stamped by name and // version alone: a profile change requires a version bump, so those two // determine content wherever the vendor's copy sits. - struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; }; - std::vector ordered; - auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) { + std::vector ordered; + json stamps = json::array(); + auto add_vendor = [&ordered, &stamps](const std::string& name, const boost::filesystem::path& dir) { // The version a load from `dir` would serve: the profile's where one // exists (a cache is only served while it covers the profile beside // it), the cache's own stamp where the cache is the whole vendor. // A profile without a version (blacklist.json) carries no presets // and is passed over. const boost::filesystem::path profile = dir / (name + ".json"); + std::string version; if (boost::filesystem::exists(profile)) { const Semver v = get_version_from_json(profile.string()); - if (v.valid()) - ordered.push_back({name, dir, v.to_string()}); + if (! v.valid()) + return; + version = v.to_string(); } else { - ordered.push_back({name, dir, - VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)}); + version = VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name); } + ordered.push_back({name, dir}); + stamps.push_back({name, version}); }; const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY); if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end()) @@ -1431,9 +1434,6 @@ bool GuideFrame::BuildProfileDataFromVendors() add_vendor(name, dir); if (ordered.empty()) return false; - json stamps = json::array(); - for (const VendorSource& v : ordered) - stamps.push_back({v.name, v.version}); // What this function derives is a pure function of that stamped set, so // the derived JSON is cached whole: a fresh cache makes an open one @@ -1461,26 +1461,18 @@ bool GuideFrame::BuildProfileDataFromVendors() } // Each vendor comes from its preset cache where one covers it, which is - // what makes this worth doing instead of the scan below; loading into a - // bundle per vendor keeps the install order the startup path has. - PresetBundle bundle; - auto load_vendor = [](PresetBundle& into, const std::string& vendor, - const boost::filesystem::path& dir, const PresetBundle* base) { - into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem, - ForwardCompatibilitySubstitutionRule::EnableSilent, base); - }; - for (const VendorSource& v : ordered) { - if (*m_cancel_token) - return false; // as in the scan below: a vendor without a cache is parsed, and that takes time - if (v.name == filament_library) { - load_vendor(bundle, v.name, v.dir, nullptr); - } else { - PresetBundle tmp; - load_vendor(tmp, v.name, v.dir, &bundle); - bundle.merge_presets(std::move(tmp)); - } - } - if (bundle.vendors.empty()) + // what makes this worth doing instead of the scan below. + PresetBundle bundle; + std::vector failed; + const std::string errors = bundle.load_vendors(ordered, ForwardCompatibilitySubstitutionRule::EnableSilent, + /*allow_cache=*/true, m_cancel_token.get(), &failed).second; + if (*m_cancel_token || bundle.vendors.empty()) + return false; + if (! errors.empty()) + BOOST_LOG_TRIVIAL(warning) << "GuideFrame: loading the vendors reported: " << errors; + // A vendor that failed to load sends this open to the scan below, which lists + // what it can read of every vendor. + if (! failed.empty()) return false; if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false)) return false; @@ -1732,7 +1724,7 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file) .make_preferred(); } - OneModel["cover"] = cover_path.string(); + OneModel["cover"] = into_u8(file_url_from_path(cover_path)); OneModel["nozzle_selected"] = ""; diff --git a/src/slic3r/GUI/WebUserLoginDialog.cpp b/src/slic3r/GUI/WebUserLoginDialog.cpp index 829e7ef106..72860e3bdc 100644 --- a/src/slic3r/GUI/WebUserLoginDialog.cpp +++ b/src/slic3r/GUI/WebUserLoginDialog.cpp @@ -599,7 +599,7 @@ void ZUserLogin::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt)) bool ZUserLogin::ShowErrorPage() { - wxString ErrortUrl = from_u8((boost::filesystem::path(resources_dir()) / "web\\login\\error.html").make_preferred().string()); + wxString ErrortUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/login/error.html"); load_url(ErrortUrl); return true; diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index f20d24163c..a98b196eb1 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -36,10 +36,10 @@ namespace GUI { WebViewPanel::WebViewPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) { - wxString url = wxString::Format("file://%s/web/homepage/index.html", from_u8(resources_dir())); + wxString url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/homepage/index.html"); wxString strlang = wxGetApp().current_language_code_safe(); if (strlang != "") - url = wxString::Format("file://%s/web/homepage/index.html?lang=%s", from_u8(resources_dir()), strlang); + url += "?lang=" + strlang; wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index 42659431f0..616b8b8779 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -1,5 +1,6 @@ #include "MultiNozzleSync.hpp" +#include "../GUI.hpp" #include "../GUI_App.hpp" #include "../I18N.hpp" #include "../Plater.hpp" @@ -21,8 +22,6 @@ #include #include -#include -#include #include #include @@ -641,8 +640,7 @@ NozzleListTable::NozzleListTable(wxWindow* parent) : wxPanel(parent,wxID_ANY,wxD m_web_view->AddScriptMessageHandler("nozzleListTable"); m_web_view->EnableContextMenu(false); fs::path filepath = fs::path(resources_dir()) / "web/flush/NozzleListTable.html"; - wxFileName fn(wxString::FromUTF8(filepath.string())); - wxString url = wxFileSystem::FileNameToURL(fn); + wxString url = file_url_from_path(filepath); m_web_view->LoadURL(url); auto sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/Widgets/WebHosting.cpp b/src/slic3r/GUI/Widgets/WebHosting.cpp index a1b0df496a..b6af6d3a39 100644 --- a/src/slic3r/GUI/Widgets/WebHosting.cpp +++ b/src/slic3r/GUI/Widgets/WebHosting.cpp @@ -48,20 +48,19 @@ constexpr char ORCA_BRIDGE_JS[] = R"JS( wxString bootstrap_url() { - return wxString("file://") + from_u8((boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE).make_preferred().string()); + return file_url_from_path(boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE); } wxString content_base_url() { - const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string(); - return wxString("file://") + from_u8(dir) + "/"; + return file_url_from_path(boost::filesystem::path(resources_dir()) / "web") + "/"; } bool is_content_url(const wxString& url) { - // The web view reports the URL it parsed, which escapes anything the resources path holds - // (a space, a non-ASCII character), while content_base_url() is the raw path. - return wxURI::Unescape(url.BeforeFirst('#')) == content_base_url(); + // The web view reports the URL it parsed, which may escape the resources path differently + // from content_base_url(). + return wxURI::Unescape(url.BeforeFirst('#')) == wxURI::Unescape(content_base_url()); } const char* orca_bridge_script() { return ORCA_BRIDGE_JS; } diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp index e4e3d2878a..3c14ab37e1 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp @@ -192,7 +192,7 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path, wxString WebViewHostDialog::build_resource_url(const std::string& resource_path) const { - wxString target_url = from_u8((boost::filesystem::path(resources_dir()) / resource_path).make_preferred().string()); + wxString target_url = file_url_from_path(boost::filesystem::path(resources_dir()) / resource_path); if (append_language_to_url()) { const wxString lang = wxGetApp().current_language_code_safe(); @@ -202,7 +202,7 @@ wxString WebViewHostDialog::build_resource_url(const std::string& resource_path) } } - return wxString("file://") + target_url; + return target_url; } void WebViewHostDialog::load_url(const wxString& url) diff --git a/src/slic3r/GUI/WipeTowerDialog.cpp b/src/slic3r/GUI/WipeTowerDialog.cpp index 9a473c7c8a..126f090a0b 100644 --- a/src/slic3r/GUI/WipeTowerDialog.cpp +++ b/src/slic3r/GUI/WipeTowerDialog.cpp @@ -464,7 +464,7 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) : wxString filepath_str = from_path(filepath); wxFileName fn(filepath_str); if(fn.FileExists()) { - wxString url = wxFileSystem::FileNameToURL(fn); + wxString url = file_url_from_path(filepath); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "File exists and load url " << url.ToStdString(); m_webview->LoadURL(url); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "Successfully loaded url: " << url.ToStdString(); diff --git a/src/slic3r/Utils/Duet.cpp b/src/slic3r/Utils/Duet.cpp index 37db6b6c33..e26a493131 100644 --- a/src/slic3r/Utils/Duet.cpp +++ b/src/slic3r/Utils/Duet.cpp @@ -274,13 +274,4 @@ bool Duet::start_print(wxString &msg, const std::string &filename, ConnectionTyp return res; } -int Duet::get_err_code_from_body(const std::string &body) const -{ - pt::ptree root; - std::istringstream iss (body); // wrap returned json to istringstream - pt::read_json(iss, root); - - return root.get("err", 0); -} - } diff --git a/src/slic3r/Utils/Duet.hpp b/src/slic3r/Utils/Duet.hpp index 2a91aa8536..7808c51fde 100644 --- a/src/slic3r/Utils/Duet.hpp +++ b/src/slic3r/Utils/Duet.hpp @@ -40,7 +40,6 @@ private: ConnectionType connect(wxString &msg) const; void disconnect(ConnectionType connectionType) const; bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const; - int get_err_code_from_body(const std::string &body) const; }; } diff --git a/src/slic3r/Utils/ESP3D.cpp b/src/slic3r/Utils/ESP3D.cpp index 6de41ebe55..5c10862118 100644 --- a/src/slic3r/Utils/ESP3D.cpp +++ b/src/slic3r/Utils/ESP3D.cpp @@ -146,15 +146,6 @@ bool ESP3D::start_print(wxString& msg, const std::string& filename) const return ret; } -int ESP3D::get_err_code_from_body(const std::string& body) const -{ - pt::ptree root; - std::istringstream iss(body); // wrap returned json to istringstream - pt::read_json(iss, root); - - return root.get("err", 0); -} - // ESP3D only accepts 8.3 filenames else it crashes marlin and other undefined behaviour std::string ESP3D::get_short_name(const std::string& filename) const { diff --git a/src/slic3r/Utils/ESP3D.hpp b/src/slic3r/Utils/ESP3D.hpp index 7ac3c66f48..44d206d238 100644 --- a/src/slic3r/Utils/ESP3D.hpp +++ b/src/slic3r/Utils/ESP3D.hpp @@ -33,7 +33,6 @@ private: std::string m_console_port; bool start_print(wxString& msg, const std::string& filename) const; - int get_err_code_from_body(const std::string& body) const; std::string get_short_name(const std::string& filename) const; std::string format_command(const std::string& path, const std::string& arg, const std::string& val) const; }; diff --git a/src/slic3r/Utils/ElegooLink.cpp b/src/slic3r/Utils/ElegooLink.cpp index 03b7bdc6c7..64ff4a9455 100644 --- a/src/slic3r/Utils/ElegooLink.cpp +++ b/src/slic3r/Utils/ElegooLink.cpp @@ -340,9 +340,7 @@ namespace Slic3r { if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2) return fallback_webui; - std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html"; - std::replace(web_path.begin(), web_path.end(), '\\', '/'); - web_path = "file://" + web_path; + std::string web_path = GUI::into_u8(GUI::file_url_from_path(boost::filesystem::path(resources_dir()) / "web/elegoolink/lan_service_web/index.html")); const std::string token = get_cc2_token(config->opt_string("printhost_apikey")); const std::string host_ip = Http::get_host_header_value(host); diff --git a/src/slic3r/Utils/Flashforge.cpp b/src/slic3r/Utils/Flashforge.cpp index 88b3eb3f69..a042a889ae 100644 --- a/src/slic3r/Utils/Flashforge.cpp +++ b/src/slic3r/Utils/Flashforge.cpp @@ -510,12 +510,22 @@ bool Flashforge::fetch_material_slots(std::vector& slots if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg)) return false; - const auto parsed = json::parse(body, nullptr, false, true); - if (parsed.is_discarded()) { + if (!parse_material_slots(body, slots, supports_material_station)) { msg = _(L("Flashforge returned an invalid JSON response.")); return false; } + return true; +} + +bool Flashforge::parse_material_slots(const std::string& body, std::vector& slots, bool* supports_material_station) +{ + slots.clear(); + + const auto parsed = json::parse(body, nullptr, false, true); + if (parsed.is_discarded()) + return false; + const auto& detail = parsed.contains("detail") ? parsed["detail"] : parsed; const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] : detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json(); @@ -542,12 +552,21 @@ bool Flashforge::fetch_material_slots(std::vector& slots if (supports_material_station != nullptr) *supports_material_station = reports_material_station; + // Fields are read leniently: firmware may send numbers as strings or flags as numbers. for (const auto& slot : slot_infos) { + if (!slot.is_object()) + continue; FlashforgeMaterialSlot info; - info.slot_id = slot.value("slotId", static_cast(slots.size()) + 1); - info.has_filament = slot.value("hasFilament", false); - info.material_name = slot.value("materialName", std::string()); - info.material_color = slot.value("materialColor", std::string()); + info.slot_id = static_cast(slots.size()) + 1; + if (const auto it = slot.find("slotId"); it != slot.end()) + try_parse_json_int(*it, info.slot_id); + int has_filament = 0; + if (const auto it = slot.find("hasFilament"); it != slot.end() && try_parse_json_int(*it, has_filament)) + info.has_filament = has_filament != 0; + if (const auto it = slot.find("materialName"); it != slot.end() && it->is_string()) + info.material_name = it->get(); + if (const auto it = slot.find("materialColor"); it != slot.end() && it->is_string()) + info.material_color = it->get(); slots.emplace_back(std::move(info)); } @@ -670,13 +689,4 @@ std::string Flashforge::extract_host_name() const return out; } -int Flashforge::get_err_code_from_body(const std::string& body) const -{ - pt::ptree root; - std::istringstream iss(body); // wrap returned json to istringstream - pt::read_json(iss, root); - - return root.get("err", 0); -} - } // namespace Slic3r diff --git a/src/slic3r/Utils/Flashforge.hpp b/src/slic3r/Utils/Flashforge.hpp index ea7acdcb11..237cf4c009 100644 --- a/src/slic3r/Utils/Flashforge.hpp +++ b/src/slic3r/Utils/Flashforge.hpp @@ -45,6 +45,8 @@ public: PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } std::string get_host() const override { return m_host; } bool fetch_material_slots(std::vector& slots, bool* supports_material_station, wxString& msg) const; + // Parses a local API "detail" reply. Returns false when the body is not valid JSON. + static bool parse_material_slots(const std::string& body, std::vector& slots, bool* supports_material_station); static bool discover_printers(std::vector& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3); private: @@ -68,7 +70,6 @@ private: bool request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const; std::string make_http_url(const std::string& path) const; std::string extract_host_name() const; - int get_err_code_from_body(const std::string &body) const; bool connect(wxString& msg) const; bool start_print(wxString& msg, const std::string& filename) const; }; diff --git a/src/slic3r/Utils/MKS.cpp b/src/slic3r/Utils/MKS.cpp index c4aa4c7262..ced7af9c9f 100644 --- a/src/slic3r/Utils/MKS.cpp +++ b/src/slic3r/Utils/MKS.cpp @@ -141,13 +141,4 @@ bool MKS::start_print(wxString& msg, const std::string& filename) const return ret; } -int MKS::get_err_code_from_body(const std::string& body) const -{ - pt::ptree root; - std::istringstream iss(body); // wrap returned json to istringstream - pt::read_json(iss, root); - - return root.get("err", 0); -} - } // Slic3r diff --git a/src/slic3r/Utils/MKS.hpp b/src/slic3r/Utils/MKS.hpp index 79143fdd9a..01cdafc405 100644 --- a/src/slic3r/Utils/MKS.hpp +++ b/src/slic3r/Utils/MKS.hpp @@ -34,7 +34,6 @@ private: std::string get_upload_url(const std::string& filename) const; bool start_print(wxString& msg, const std::string& filename) const; - int get_err_code_from_body(const std::string& body) const; }; } diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index b8379697d6..5bbe779970 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -339,62 +339,8 @@ bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &targe //BBS: refine preset update logic bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path) { - bool res = true; - std::string file_path = source_path.string(); - std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string(); - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - - if (!open_zip_reader(&archive, file_path)) - { - BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for "< #include #include +#include #include #include #include #include +#include +#include #include #include @@ -172,6 +175,20 @@ std::string moonraker_error_reason(const std::string &body) } // namespace +int PrintHost::get_err_code_from_body(const std::string &body) +{ + boost::property_tree::ptree root; + std::istringstream iss(body); + try { + boost::property_tree::read_json(iss, root); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "PrintHost: response is not valid JSON: " << ex.what(); + return -1; + } + + return root.get("err", 0); +} + wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const { if (status != 0) { @@ -415,12 +432,10 @@ void PrintHostJobQueue::priv::remove_source() source_to_remove.clear(); } -void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) +bool PrintHostJobQueue::upload_job(PrintHostJob &job, PrintHost::ProgressFn progress_fn, PrintHost::ErrorFn error_fn, PrintHost::InfoFn info_fn) { - emit_progress(0); // Indicate the upload is starting - // Captured before upload_data is moved into upload() below. - const std::string upload_filename = the_job.upload_data.source_path.filename().string(); + const std::string upload_filename = job.upload_data.source_path.filename().string(); { LifecycleEventContext ctx; @@ -429,19 +444,37 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx); } - bool success = the_job.printhost->upload(std::move(the_job.upload_data), - [this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); }, - [this](wxString error) { this->error_fn(std::move(error)); }, - [this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); } - ); + bool success = false; + std::string error; + // A throwing upload must not stop the worker, or later jobs would stay queued forever. + try { + success = job.printhost->upload(std::move(job.upload_data), std::move(progress_fn), error_fn, std::move(info_fn)); + } catch (const std::exception &e) { + error = e.what(); + error_fn(error); + } { LifecycleEventContext ctx; ctx.name = upload_filename; ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error; + ctx.msg = error; fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx); } + return success; +} + +void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) +{ + emit_progress(0); // Indicate the upload is starting + + bool success = PrintHostJobQueue::upload_job(the_job, + [this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); }, + [this](wxString error) { this->error_fn(std::move(error)); }, + [this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); } + ); + if (success) { emit_progress(100); if (the_job.switch_to_device_tab) { diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index 87a8df8934..cf197ede07 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -87,6 +87,8 @@ public: static PrintHost* get_print_host(DynamicPrintConfig *config); static std::string get_print_host_webui(DynamicPrintConfig *config); + // Reads the "err" field of a JSON reply, 0 when absent. Returns -1 when the body is not valid JSON. + static int get_err_code_from_body(const std::string &body); //Support for cloud webui login virtual bool is_cloud() const { return false; } @@ -150,6 +152,10 @@ public: void enqueue(PrintHostJob job); void cancel(size_t id); + // Uploads the job, firing UploadStarted and a matching UploadFinished. An exception thrown by + // the upload is reported through error_fn and makes the upload fail. + static bool upload_job(PrintHostJob &job, PrintHost::ProgressFn progress_fn, PrintHost::ErrorFn error_fn, PrintHost::InfoFn info_fn); + private: struct priv; std::shared_ptr p; diff --git a/src/slic3r/Utils/UltiMaker.cpp b/src/slic3r/Utils/UltiMaker.cpp index 0376578db0..badb69d209 100644 --- a/src/slic3r/Utils/UltiMaker.cpp +++ b/src/slic3r/Utils/UltiMaker.cpp @@ -654,13 +654,4 @@ bool UltiMaker::start_print(wxString &msg, const std::string &filename, Connecti return res; } -int UltiMaker::get_err_code_from_body(const std::string &body) const -{ - pt::ptree root; - std::istringstream iss (body); // wrap returned json to istringstream - pt::read_json(iss, root); - - return root.get("err", 0); -} - } diff --git a/src/slic3r/Utils/UltiMaker.hpp b/src/slic3r/Utils/UltiMaker.hpp index b32b87434f..c1f9ced304 100644 --- a/src/slic3r/Utils/UltiMaker.hpp +++ b/src/slic3r/Utils/UltiMaker.hpp @@ -64,7 +64,6 @@ private: void set_auth(Http& http) const; void disconnect(ConnectionType connectionType) const; bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType) const; - int get_err_code_from_body(const std::string &body) const; }; } diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index 2a73dcac83..c39efaccf5 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -768,3 +768,128 @@ TEST_CASE("Each filament prints with its variant of the extruder's variant strin CHECK(gcode.find("; nozzle_temperature = " + resolved + "\n") != std::string::npos); } } + +// An adaptive pressure advance model predicting the same pressure advance at every flow and acceleration. +static std::string constant_pressure_advance_model(const std::string &pa) +{ + return pa + ",1,1000\n" + pa + ",500,1000\n" + pa + ",1,100000\n" + pa + ",500,100000"; +} + +// The pressure advance values a Klipper G-code sets. +static std::set pressure_advance_values(const std::string &gcode) +{ + const std::string token = "SET_PRESSURE_ADVANCE ADVANCE="; + std::set values; + std::istringstream stream(gcode); + for (std::string line; std::getline(stream, line);) + if (line.rfind(token, 0) == 0) + values.insert(line.substr(token.size(), line.find(';') - token.size())); + return values; +} + +// Pressure advance, and the adaptive pressure advance model, are tuned per extruder variant like the +// other filament variant settings. +TEST_CASE("Each filament sets the pressure advance of its extruder variant", "[MultiFilament]") +{ + auto [nozzle_volume_type, filament, pressure_advance, adaptive_pressure_advance] = GENERATE(table({ + { nvtStandard, 1, "0.021", "0.012" }, + { nvtHighFlow, 1, "0.037", "0.034" }, + { nvtHighFlow, 2, "0.043", "0.056" }, // filament 2 defines no High Flow variant + })); + const bool adaptive = GENERATE(false, true); + DYNAMIC_SECTION(get_nozzle_volume_type_string(nozzle_volume_type) << " nozzle, filament " << filament << (adaptive ? ", adaptive" : "")) { + DynamicPrintConfig config = multifilament_config(2, { + { "gcode_flavor", "klipper" }, + { "extruder_variant_list", "Direct Drive Standard,Direct Drive High Flow" }, + // filament 1 defines Standard (0.021) and High Flow (0.037), filament 2 Standard (0.043) + { "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard" }, + { "filament_self_index", "1,1,2" }, + { "enable_pressure_advance", "1,1,1" }, + { "pressure_advance", "0.021,0.037,0.043" }, + { "adaptive_pressure_advance", adaptive ? "1,1,1" : "0,0,0" }, + { "sparse_infill_filament_id", filament }, + { "internal_solid_filament_id", filament }, + { "top_surface_filament_id", filament }, + { "bottom_surface_filament_id", filament }, + { "outer_wall_filament_id", filament }, + { "inner_wall_filament_id", filament }, + { "enable_prime_tower", 0 }, + { "skirt_loops", 0 }, + { "brim_type", "no_brim" }, + // custom G-code indexes the per-filament arrays by filament + { "machine_start_gcode", "; start pressure advance {pressure_advance[initial_extruder]}" }, + }); + config.option("adaptive_pressure_advance_model")->values = { + constant_pressure_advance_model("0.012"), constant_pressure_advance_model("0.034"), constant_pressure_advance_model("0.056") }; + config.option("nozzle_volume_type", true)->values = { nozzle_volume_type }; + const std::string gcode = slice({ cube(20) }, config); + + // The toolchange sets the variant's pressure advance; with adaptive pressure advance, the + // prediction of the variant's model then replaces it. + std::set expected{ pressure_advance }; + if (adaptive) + expected.insert(adaptive_pressure_advance); + CHECK(pressure_advance_values(gcode) == expected); + CHECK(gcode.find("; start pressure advance " + pressure_advance + "\n") != std::string::npos); + } +} + +// On a printer with two extruders, a filament takes the pressure advance of the variant of the extruder +// it is mapped to, whichever filament and extruder that is. +TEST_CASE("Each filament sets the pressure advance of its extruder variant on a two-extruder printer", "[MultiFilament]") +{ + auto [filament, extruder, pressure_advance, adaptive_pressure_advance] = GENERATE(table({ + { 1, 1, "0.021", "0.012" }, + { 1, 2, "0.037", "0.034" }, + { 2, 1, "0.043", "0.056" }, + { 2, 2, "0.049", "0.078" }, + })); + const bool adaptive = GENERATE(false, true); + DYNAMIC_SECTION("filament " << filament << " on extruder " << extruder << (adaptive ? ", adaptive" : "")) { + // the other filament goes on the other extruder + const std::string filament_map = filament == 1 ? std::to_string(extruder) + "," + std::to_string(3 - extruder) : + std::to_string(3 - extruder) + "," + std::to_string(extruder); + DynamicPrintConfig config = multifilament_config(2, { + { "gcode_flavor", "klipper" }, + { "single_extruder_multi_material", 0 }, + { "nozzle_diameter", "0.4,0.4" }, + { "extruder_printable_height", "0,0" }, + // extruder 1 has a Standard nozzle, extruder 2 a High Flow one + { "printer_extruder_id", "1,2" }, + { "printer_extruder_variant", "Direct Drive Standard;Direct Drive High Flow" }, + { "extruder_variant_list", "Direct Drive Standard;Direct Drive High Flow" }, + { "filament_map", filament_map }, + // both filaments define Standard and High Flow + { "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard;Direct Drive High Flow" }, + { "filament_self_index", "1,1,2,2" }, + { "enable_pressure_advance", "1,1,1,1" }, + { "pressure_advance", "0.021,0.037,0.043,0.049" }, + { "adaptive_pressure_advance", adaptive ? "1,1,1,1" : "0,0,0,0" }, + { "sparse_infill_filament_id", filament }, + { "internal_solid_filament_id", filament }, + { "top_surface_filament_id", filament }, + { "bottom_surface_filament_id", filament }, + { "outer_wall_filament_id", filament }, + { "inner_wall_filament_id", filament }, + { "enable_prime_tower", 0 }, + { "skirt_loops", 0 }, + { "brim_type", "no_brim" }, + // custom G-code indexes the per-filament arrays by filament + { "machine_start_gcode", "; start pressure advance {pressure_advance[initial_extruder]}" }, + }); + config.option("adaptive_pressure_advance_model")->values = { + constant_pressure_advance_model("0.012"), constant_pressure_advance_model("0.034"), + constant_pressure_advance_model("0.056"), constant_pressure_advance_model("0.078") }; + config.option("extruder_type", true)->values = { etDirectDrive, etDirectDrive }; + config.option("nozzle_volume_type", true)->values = { nvtStandard, nvtHighFlow }; + // keep the mapping above rather than grouping the filaments automatically + config.option>("filament_map_mode", true)->value = fmmManual; + const std::string gcode = slice({ cube(20) }, config); + + std::set expected{ pressure_advance }; + if (adaptive) + expected.insert(adaptive_pressure_advance); + CHECK(pressure_advance_values(gcode) == expected); + CHECK(gcode.find("; start pressure advance " + pressure_advance + "\n") != std::string::npos); + } +} diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 32cd608b43..a28669b8d4 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -489,6 +489,25 @@ TEST_CASE("gcode_skip_config_block omits the resolved-settings comment block", " CHECK(gcode.find("; EXECUTABLE_BLOCK_START") != std::string::npos); } +// Some firmwares only scan the last N lines of the file for "estimated printing time", so it +// must stay close to EOF regardless of the resolved-settings config block's size. +TEST_CASE("The estimated printing time comment stays near the end of the file", "[Print]") +{ + const std::string gcode = slice({ cube(20) }, {}); + const size_t config_block_end = gcode.find("; CONFIG_BLOCK_END"); + const size_t filament_stats = gcode.find("; filament used [mm]"); + const size_t time_comment = gcode.find("estimated printing time"); + REQUIRE(config_block_end != std::string::npos); + REQUIRE(filament_stats != std::string::npos); + REQUIRE(time_comment != std::string::npos); + CHECK(filament_stats > config_block_end); + CHECK(time_comment > filament_stats); + + const size_t line_start = gcode.rfind('\n', time_comment) + 1; + const size_t trailing_lines = std::count(gcode.begin() + line_start, gcode.end(), '\n'); + CHECK(trailing_lines <= 5); +} + // Custom G-code templates substitute placeholders during export. TEST_CASE("Custom G-code placeholders are substituted", "[Print]") { diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index fcc257f14d..3f2938d568 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -16,8 +16,10 @@ add_executable(${_TEST_NAME}_tests test_clipper_utils.cpp test_config.cpp test_config_variant_expansion.cpp + test_locales_utils.cpp test_drc.cpp test_toolordering_nozzle_group.cpp + test_parallel_resolve.cpp test_preset_bundle_loading.cpp test_preset_setting_id.cpp test_preset_diff.cpp @@ -44,6 +46,7 @@ add_executable(${_TEST_NAME}_tests test_lay_on_face.cpp test_model.cpp test_utils.cpp + test_miniz_extension.cpp test_timeutils.cpp test_voronoi.cpp test_wipe_tower_estimate.cpp diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index e603808b54..de19e8219c 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -717,3 +717,15 @@ TEST_CASE("get_index_for_extruder scales the variant column by the requested str REQUIRE(col0_stride2 == 0); REQUIRE(col1_stride2 == 2); } + +// A per-variant filament option read with a single value gives it to every filament variant. A project +// exported by an older CLI holds a single value for an option no loaded preset defined, such as +// filament_ironing_flow. +TEST_CASE("A per-variant filament option read with a single value gives it to every filament variant", "[Config]") +{ + // filament 1 defines Standard and High Flow, filament 2 Standard + DynamicPrintConfig config; + config.option("filament_self_index", true)->values = {1, 1, 2}; + config.load_from_ini_string("pressure_advance = 0.021", ForwardCompatibilitySubstitutionRule::Disable); + REQUIRE(config.option("pressure_advance")->values == std::vector({0.021, 0.021, 0.021})); +} diff --git a/tests/libslic3r/test_locales_utils.cpp b/tests/libslic3r/test_locales_utils.cpp new file mode 100644 index 0000000000..6f1528649d --- /dev/null +++ b/tests/libslic3r/test_locales_utils.cpp @@ -0,0 +1,69 @@ +#include + +#include + +#include "libslic3r/LocalesUtils.hpp" + +using namespace Slic3r; + +namespace { + +// Switches this thread's numeric locale to one whose decimal separator is a comma, +// when the system has one installed. +struct CommaNumericLocale +{ +#ifdef _WIN32 + bool apply() + { + for (const char* name : { "de-DE", "German_Germany.1252" }) + if (std::setlocale(LC_NUMERIC, name) != nullptr) + return true; + return false; + } +#else + locale_t locale { (locale_t) 0 }; + bool apply() + { + for (const char* name : { "de_DE.UTF-8", "de_DE.utf8", "de_DE" }) + if ((locale = newlocale(LC_NUMERIC_MASK, name, (locale_t) 0)) != (locale_t) 0) { + uselocale(locale); + return true; + } + return false; + } + // Freed once the setters around apply() have put the thread's own locale back. + ~CommaNumericLocale() + { + if (locale != (locale_t) 0) + freelocale(locale); + } +#endif +}; + +} // namespace + +TEST_CASE("a setter nested in another leaves the C locale in place for the outer one", "[LocalesUtils]") +{ + CNumericLocalesSetter outer; + { + CNumericLocalesSetter inner; + CHECK(is_decimal_separator_point()); + } + CHECK(is_decimal_separator_point()); +} + +TEST_CASE("a setter nested in another sets C again when the locale changed between them", "[LocalesUtils]") +{ + CommaNumericLocale comma; + { + CNumericLocalesSetter outer; + if (! comma.apply()) + SKIP("no locale with a comma decimal separator is installed"); + REQUIRE_FALSE(is_decimal_separator_point()); + { + CNumericLocalesSetter inner; + CHECK(is_decimal_separator_point()); + } + CHECK_FALSE(is_decimal_separator_point()); + } +} diff --git a/tests/libslic3r/test_miniz_extension.cpp b/tests/libslic3r/test_miniz_extension.cpp new file mode 100644 index 0000000000..ff3274a886 --- /dev/null +++ b/tests/libslic3r/test_miniz_extension.cpp @@ -0,0 +1,194 @@ +#include + +#include "libslic3r/miniz_extension.hpp" + +#include "test_utils.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +using namespace Slic3r; +namespace fs = boost::filesystem; + +namespace { + +void write_zip(const fs::path &zip_file, const std::vector> &entries) +{ + mz_zip_archive zip; + mz_zip_zero_struct(&zip); + REQUIRE(open_zip_writer(&zip, zip_file.string())); + for (const auto &[name, content] : entries) + REQUIRE(mz_zip_writer_add_mem(&zip, name.c_str(), content.data(), content.size(), MZ_DEFAULT_COMPRESSION)); + REQUIRE(mz_zip_writer_finalize_archive(&zip)); + REQUIRE(close_zip_writer(&zip)); +} + +// miniz refuses to write a name starting with '/', so write a placeholder of the same length and patch it in place. +void rename_entry(const fs::path &zip_file, const std::string &from, const std::string &to) +{ + REQUIRE(from.size() == to.size()); + std::string bytes; + { + std::ifstream in(zip_file.string(), std::ios::binary); + bytes.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); + } + size_t count = 0; + for (size_t pos = bytes.find(from); pos != std::string::npos; pos = bytes.find(from, pos + to.size()), ++count) + bytes.replace(pos, from.size(), to); + // Once in the local header and once in the central directory. + REQUIRE(count == 2); + std::ofstream out(zip_file.string(), std::ios::binary | std::ios::trunc); + out << bytes; +} + +std::vector list_dir(const fs::path &dir) +{ + std::vector names; + for (const fs::directory_entry &entry : fs::directory_iterator(dir)) + names.push_back(entry.path().filename().string()); + std::sort(names.begin(), names.end()); + return names; +} + +std::string read_file(const fs::path &file) +{ + std::ifstream in(file.string(), std::ios::binary); + return std::string(std::istreambuf_iterator(in), std::istreambuf_iterator()); +} + +} // namespace + +TEST_CASE("Confined extraction writes a well-formed archive under the target directory", "[MinizExtension]") +{ + ScopedTemporaryDir tmp; + const fs::path zip_file = tmp.path() / "bundle.zip"; + const fs::path target = tmp.path() / "cache"; + fs::create_directories(target); + write_zip(zip_file, {{"vendor/", ""}, {"vendor/machine/", ""}, {"vendor.json", "{\"a\":1}"}, {"vendor/machine/printer.json", "{\"b\":2}"}}); + + REQUIRE(extract_archive_confined(zip_file.string(), target.string())); + CHECK(fs::is_directory(target / "vendor")); + CHECK(read_file(target / "vendor.json") == "{\"a\":1}"); + CHECK(read_file(target / "vendor" / "machine" / "printer.json") == "{\"b\":2}"); +} + +TEST_CASE("Confined extraction rejects an archive with an entry outside the target directory", "[MinizExtension]") +{ + ScopedTemporaryDir tmp; + const fs::path zip_file = tmp.path() / "bundle.zip"; + const fs::path target = tmp.path() / "cache"; + fs::create_directories(target); + + const std::string escaping_entry = GENERATE(std::string("../escape.txt"), std::string("..\\escape.txt"), + std::string("sub/../../escape.txt"), std::string("C:/escape.txt"), + std::string("C:escape.txt"), std::string("\\escape.txt")); + // The normal entry comes first so a per-entry check would already have written it. + write_zip(zip_file, {{"normal.json", "{}"}, {escaping_entry, "escaped"}}); + + CAPTURE(escaping_entry); + CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string())); + CHECK_FALSE(fs::exists(tmp.path() / "escape.txt")); + CHECK(fs::is_empty(target)); +} + +TEST_CASE("Confined extraction rejects an archive with an absolute entry name", "[MinizExtension]") +{ + ScopedTemporaryDir tmp; + const fs::path zip_file = tmp.path() / "bundle.zip"; + const fs::path target = tmp.path() / "cache"; + fs::create_directories(target); + + const std::string absolute = (tmp.path() / "escape.txt").generic_string(); + const std::string placeholder = "#" + absolute.substr(1); + write_zip(zip_file, {{"normal.json", "{}"}, {placeholder, "escaped"}}); + rename_entry(zip_file, placeholder, absolute); + + CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string())); + CHECK_FALSE(fs::exists(tmp.path() / "escape.txt")); + CHECK(fs::is_empty(target)); +} + +TEST_CASE("Confined extraction rejects a directory entry outside the target directory", "[MinizExtension]") +{ + ScopedTemporaryDir tmp; + const fs::path zip_file = tmp.path() / "bundle.zip"; + const fs::path target = tmp.path() / "cache"; + fs::create_directories(target); + write_zip(zip_file, {{"vendor/", ""}, {"../outside/", ""}}); + + CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string())); + CHECK_FALSE(fs::exists(tmp.path() / "outside")); + CHECK(fs::is_empty(target)); +} + +TEST_CASE("Confined extraction validates zero-size entries like any other", "[MinizExtension]") +{ + ScopedTemporaryDir tmp; + const fs::path zip_file = tmp.path() / "bundle.zip"; + const fs::path target = tmp.path() / "cache"; + fs::create_directories(target); + + SECTION("an empty file inside the target does not fail the archive") { + write_zip(zip_file, {{"empty.json", ""}, {"vendor.json", "{}"}}); + CHECK(extract_archive_confined(zip_file.string(), target.string())); + CHECK(read_file(target / "vendor.json") == "{}"); + } + SECTION("an empty file outside the target rejects the archive") { + write_zip(zip_file, {{"vendor.json", "{}"}, {"../escape.txt", ""}}); + CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string())); + CHECK_FALSE(fs::exists(tmp.path() / "escape.txt")); + CHECK(fs::is_empty(target)); + } +} + +TEST_CASE("Confined extraction writes nothing outside the target for Windows-specific name forms", "[MinizExtension]") +{ + ScopedTemporaryDir tmp; + const fs::path zip_file = tmp.path() / "bundle.zip"; + const fs::path target = tmp.path() / "cache"; + fs::create_directories(target); + + // Windows strips trailing dots and spaces and maps device names; whether these extract depends on the + // platform, but none of them may land beside the target. + const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("..."), std::string(".. "), + std::string(".. /escape.txt"), std::string(".../escape.txt"), std::string("CON"), + std::string("sub/NUL.txt"), std::string("C:escape.txt")); + write_zip(zip_file, {{name, "payload"}}); + + CAPTURE(name); + extract_archive_confined(zip_file.string(), target.string()); + CHECK(list_dir(tmp.path()) == std::vector{"bundle.zip", "cache"}); +} + +#ifndef _WIN32 +TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]") +{ + ScopedTemporaryDir tmp; + const fs::path zip_file = tmp.path() / "bundle.zip"; + const fs::path target = tmp.path() / "cache"; + const fs::path outside = tmp.path() / "outside"; + fs::create_directories(target); + fs::create_directories(outside); + write_zip(zip_file, {{"vendor.json", "{\"a\":1}"}}); + + SECTION("a dangling symlink") { + fs::create_symlink(outside / "vendor.json", target / "vendor.json"); + CHECK(extract_archive_confined(zip_file.string(), target.string())); + CHECK_FALSE(fs::exists(outside / "vendor.json")); + CHECK_FALSE(fs::is_symlink(fs::symlink_status(target / "vendor.json"))); + CHECK(read_file(target / "vendor.json") == "{\"a\":1}"); + } + SECTION("a symlink to an existing file") { + { std::ofstream((outside / "vendor.json").string()) << "original"; } + fs::create_symlink(outside / "vendor.json", target / "vendor.json"); + extract_archive_confined(zip_file.string(), target.string()); + CHECK(read_file(outside / "vendor.json") == "original"); + } +} +#endif diff --git a/tests/libslic3r/test_obj.cpp b/tests/libslic3r/test_obj.cpp index d198a69c8c..2cbf57ff04 100644 --- a/tests/libslic3r/test_obj.cpp +++ b/tests/libslic3r/test_obj.cpp @@ -19,9 +19,8 @@ struct LoadedObj ObjInfo info; }; -// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1). -// Only the first face and the vt lines are varied; the other three faces reference vt 1. -LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::string &vts = "vt 0.25 0.5\nvt 0.75 1\n") +// Loads an OBJ made of the given lines, with a material library that defines material "a". +LoadedObj load_textured_obj(const std::string &body) { ScopedTemporaryFile obj(".obj"); ScopedTemporaryFile mtl(".mtl"); @@ -31,12 +30,7 @@ LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::st } { boost::nowide::ofstream out(obj.string()); - out << "mtllib " << mtl.path().filename().string() << "\n" - << "v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\n" - << vts - << "usemtl a\n" - << first_face << "\n" - << "f 1/1 2/1 4/1\nf 1/1 4/1 3/1\nf 2/1 3/1 4/1\n"; + out << "mtllib " << mtl.path().filename().string() << "\n" << body; } LoadedObj loaded; std::string message; @@ -44,6 +38,21 @@ LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::st return loaded; } +// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1). +// Only the first face and the vt lines are varied; the other three faces reference vt 1. +LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::string &vts = "vt 0.25 0.5\nvt 0.75 1\n") +{ + return load_textured_obj("v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\n" + vts + "usemtl a\n" + first_face + "\n" + + "f 1/1 2/1 4/1\nf 1/1 4/1 3/1\nf 2/1 3/1 4/1\n"); +} + +// Texture coordinate n is (n / 10, n / 20), so a UV identifies the vt it came from. +void check_uv_is_vt(const Vec2f &uv, int vt) +{ + CHECK_THAT(uv.x(), WithinAbs(vt / 10., 1e-6)); + CHECK_THAT(uv.y(), WithinAbs(vt / 20., 1e-6)); +} + } // namespace TEST_CASE("An out-of-range texture index falls back to a zero UV and keeps the geometry", "[OBJ][Regression]") @@ -120,3 +129,39 @@ TEST_CASE("A texture coordinate with w does not shift the indices of the ones af CHECK_THAT(uv[2].x(), WithinAbs(0.75, 1e-6)); CHECK_THAT(uv[2].y(), WithinAbs(1., 1e-6)); } + +TEST_CASE("Both triangles of a quad take the texture coordinates of their own corners", "[OBJ][Regression]") +{ + const LoadedObj loaded = load_textured_obj("v 0 0 0\nv 10 0 0\nv 10 10 0\nv 0 10 0\n" + "vt 0.1 0.05\nvt 0.2 0.1\nvt 0.3 0.15\nvt 0.4 0.2\n" + "usemtl a\n" + "f 1/1 2/2 3/3 4/4\n"); + + REQUIRE(loaded.ok); + REQUIRE(loaded.mesh.facets_count() == 2); + REQUIRE(loaded.info.uvs.size() == 2); + check_uv_is_vt(loaded.info.uvs[0][0], 1); + check_uv_is_vt(loaded.info.uvs[0][1], 2); + check_uv_is_vt(loaded.info.uvs[0][2], 3); + check_uv_is_vt(loaded.info.uvs[1][0], 1); + check_uv_is_vt(loaded.info.uvs[1][1], 3); + check_uv_is_vt(loaded.info.uvs[1][2], 4); +} + +TEST_CASE("Texture coordinates follow the corners of a mesh that is flipped on load", "[OBJ][Regression]") +{ + // The faces wind inwards, so the loader flips them. Vertex n uses vt n. + const LoadedObj loaded = load_textured_obj("v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\n" + "vt 0.1 0.05\nvt 0.2 0.1\nvt 0.3 0.15\nvt 0.4 0.2\n" + "usemtl a\n" + "f 1/1 2/2 3/3\nf 1/1 4/4 2/2\nf 1/1 3/3 4/4\nf 2/2 4/4 3/3\n"); + + REQUIRE(loaded.ok); + const indexed_triangle_set &its = loaded.mesh.its; + CHECK(its_volume(its) > 0.f); + REQUIRE(its.indices.size() == 4); + REQUIRE(loaded.info.uvs.size() == 4); + for (size_t face = 0; face < its.indices.size(); ++face) + for (int corner = 0; corner < 3; ++corner) + check_uv_is_vt(loaded.info.uvs[face][corner], its.indices[face][corner] + 1); +} diff --git a/tests/libslic3r/test_parallel_resolve.cpp b/tests/libslic3r/test_parallel_resolve.cpp new file mode 100644 index 0000000000..76811f1475 --- /dev/null +++ b/tests/libslic3r/test_parallel_resolve.cpp @@ -0,0 +1,122 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "libslic3r/ParallelResolve.hpp" + +using namespace Slic3r; + +namespace { + +thread_local int t_live_setups = 0; + +// Counts how many are alive on the constructing thread. +struct CountingSetup +{ + CountingSetup() { ++ t_live_setups; } + ~CountingSetup() { -- t_live_setups; } +}; + +std::vector first_indices(size_t count) +{ + std::vector indices(count); + std::iota(indices.begin(), indices.end(), size_t(0)); + return indices; +} + +} // namespace + +TEST_CASE("every item resolves once and commits in index order on the calling thread", "[ParallelResolve]") +{ + const size_t count = 200; + std::vector resolves(count, 0); + std::vector committed, values; + std::vector on_caller; + const std::thread::id caller = std::this_thread::get_id(); + resolve_then_commit(count, + [&](size_t i) { ++ resolves[i]; return i * 3; }, + [&](size_t i, size_t resolved) { + committed.push_back(i); + values.push_back(resolved); + on_caller.push_back(std::this_thread::get_id() == caller); + }); + + CHECK(committed == first_indices(count)); + for (size_t i = 0; i < count; ++ i) { + CHECK(resolves[i] == 1); + CHECK(values[i] == i * 3); + CHECK(on_caller[i]); + } +} + +TEST_CASE("every item resolves inside one chunk setup", "[ParallelResolve]") +{ + const size_t count = 200; + std::vector live(count, 0); + resolve_then_commit(count, + [&](size_t i) { live[i] = t_live_setups; return 0; }, + [](size_t, int) {}); + + for (size_t i = 0; i < count; ++ i) + CHECK(live[i] == 1); +} + +TEST_CASE("an exception from resolve leaves the batches before it committed", "[ParallelResolve]") +{ + const size_t count = 200, fails_at = 150; + REQUIRE(fails_at >= resolve_batch_size); + std::vector committed; + CHECK_THROWS_AS(resolve_then_commit(count, + [&](size_t i) { + if (i == fails_at) + throw std::runtime_error("resolve failed"); + return i; + }, + [&](size_t i, size_t) { committed.push_back(i); }), + std::runtime_error); + + CHECK(committed == first_indices(fails_at / resolve_batch_size * resolve_batch_size)); +} + +TEST_CASE("an exception from commit stops at the item that threw", "[ParallelResolve]") +{ + const size_t count = 200, fails_at = 90; + std::vector committed; + CHECK_THROWS_AS(resolve_then_commit(count, + [](size_t i) { return i; }, + [&](size_t i, size_t) { + if (i == fails_at) + throw std::runtime_error("commit failed"); + committed.push_back(i); + }), + std::runtime_error); + + CHECK(committed == first_indices(fails_at)); +} + +TEST_CASE("a canceled task group stops before committing an item it did not resolve", "[ParallelResolve]") +{ + std::vector committed; + bool threw = false; + tbb::task_group_context context; + tbb::parallel_for(tbb::blocked_range(0, 1), [&](const tbb::blocked_range&) { + context.cancel_group_execution(); + try { + resolve_then_commit(200, + [](size_t i) { return i + 1; }, + [&](size_t, size_t resolved) { committed.push_back(resolved); }); + } catch (const std::runtime_error&) { + threw = true; + } + }, context); + + CHECK(threw); + CHECK(committed.empty()); +} diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 6ec6ba9b1e..49f0f90214 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -59,6 +59,16 @@ void write_preset_with_inherits(const DynamicPrintConfig &default_config, const config.save_to_json(file.string(), name, "User", "1.0.0"); } +// A user preset file stating nothing but the preset it inherits, so every value it +// ends up with came from resolving that parent. +void write_minimal_child(const fs::path &file, const std::string &name, const std::string &inherits) +{ + fs::create_directories(file.parent_path()); + std::ofstream(file.string()) + << R"({"type":"process","name":")" << name << R"(","from":"User","version":"1.0.0","inherits":")" + << inherits << R"("})"; +} + // Add an in-memory preset (no file) with the given inherits value (empty => root preset). Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {}) { @@ -261,6 +271,162 @@ TEST_CASE("Selected printer uses its default or saved bed type", "[Preset][Bundl CHECK(app_config.get_printer_setting("Test Printer", "curr_bed_type") == std::to_string(static_cast(expected_bed_type))); } +TEST_CASE("A directory of user presets loads with each one resolved against its parent", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestCollection coll; + + Preset &parent = add_inmemory_preset(coll, "Parent Process"); + parent.config.option("layer_height", true)->value = 0.24; + parent.is_system = true; + + constexpr int children = 400; + for (int i = 0; i < children; ++ i) + write_minimal_child(temp_dir.path() / PRESET_PRINT_NAME / ("Child " + std::to_string(i) + ".json"), + "Child " + std::to_string(i), "Parent Process"); + + PresetsConfigSubstitutions substitutions; + coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions, + ForwardCompatibilitySubstitutionRule::Disable); + + CHECK(coll.size() == size_t(children) + 2); // the children, the default preset and the parent + CHECK(coll.error_count() == 0); + for (int i = 0; i < children; ++ i) { + const Preset *child = coll.find_preset("Child " + std::to_string(i)); + REQUIRE(child != nullptr); + CHECK(child->inherits() == "Parent Process"); + CHECK(child->alias == "Child " + std::to_string(i)); + CHECK(child->loaded); + REQUIRE(child->config.option("layer_height") != nullptr); + CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-9)); + } +} + +TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + + auto seed_directory = [&]() { + for (int i = 0; i < 200; ++ i) + write_minimal_child(temp_dir.path() / PRESET_PRINT_NAME / ("Child " + std::to_string(i) + ".json"), + "Child " + std::to_string(i), "Parent Process"); + }; + + std::vector> names_per_run; + for (int run = 0; run < 3; ++ run) { + RenameTestCollection coll; + Preset &parent = add_inmemory_preset(coll, "Parent Process"); + parent.is_system = true; + if (run == 0) + seed_directory(); + + PresetsConfigSubstitutions substitutions; + coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions, + ForwardCompatibilitySubstitutionRule::Disable); + + std::vector names; + for (auto it = coll.begin(); it != coll.end(); ++ it) + names.push_back(it->name + "|" + it->alias + "|" + it->inherits()); + names_per_run.push_back(std::move(names)); + } + + REQUIRE(names_per_run[0].size() > 200); + CHECK(names_per_run[1] == names_per_run[0]); + CHECK(names_per_run[2] == names_per_run[0]); +} + +TEST_CASE("An unreadable user preset is counted and removed while the rest still load", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestCollection coll; + const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME; + + for (int i = 0; i < 20; ++ i) + write_preset_with_inherits(coll.default_preset().config, dir / ("Good " + std::to_string(i) + ".json"), + "Good " + std::to_string(i), std::string()); + fs::create_directories(dir); + std::ofstream((dir / "Broken.json").string()) << "{not-json"; + + PresetsConfigSubstitutions substitutions; + coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + CHECK(coll.error_count() == 1); + CHECK(coll.find_preset("Broken") == nullptr); + CHECK_FALSE(fs::exists(dir / "Broken.json")); + for (int i = 0; i < 20; ++ i) + CHECK(coll.find_preset("Good " + std::to_string(i)) != nullptr); +} + +TEST_CASE("A user filament naming no compatible printer gets the one after its @, in memory and on disk", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + PresetBundle bundle; + const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json"; + REQUIRE(bundle.filaments.default_preset().config.option("compatible_printers")->values.empty()); + write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string()); + + PresetsConfigSubstitutions substitutions; + bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + const std::vector expected { "Test Printer" }; + const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer"); + REQUIRE(preset != nullptr); + CHECK(preset->config.option("compatible_printers")->values == expected); + + DynamicPrintConfig saved; + std::map key_values; + std::string reason; + saved.load_from_json(file.string(), ForwardCompatibilitySubstitutionRule::EnableSilent, key_values, reason); + REQUIRE(reason.empty()); + REQUIRE(saved.option("compatible_printers") != nullptr); + CHECK(saved.option("compatible_printers")->values == expected); +} + +TEST_CASE("A user preset's setting id equal to its base id is dropped in memory, not in the .info written back", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + PresetBundle bundle; + const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json"; + write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string()); + fs::path info = file; + info.replace_extension(".info"); + std::ofstream(info.string()) << "sync_info = \nuser_id = \nsetting_id = PFUS1\nbase_id = PFUS1\nupdated_time = 0\n"; + + PresetsConfigSubstitutions substitutions; + bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer"); + REQUIRE(preset != nullptr); + CHECK(preset->setting_id.empty()); + CHECK(preset->base_id == "PFUS1"); + Preset reloaded(Preset::TYPE_FILAMENT, "My PLA @Test Printer"); + reloaded.load_info(info.string()); + CHECK(reloaded.setting_id == "PFUS1"); +} + +TEST_CASE("A user preset that is not loaded still reports its substituted values", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestCollection coll; + const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME; + fs::create_directories(dir); + std::ofstream((dir / "Orphan.json").string()) + << R"({"type":"process","name":"Orphan","from":"User","version":"1.0.0","inherits":"No Such Parent",)" + << R"("wall_generator":"no_such_generator"})"; + + PresetsConfigSubstitutions substitutions; + coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions, + ForwardCompatibilitySubstitutionRule::Enable); + + CHECK(coll.find_preset("Orphan") == nullptr); + CHECK(coll.error_count() == 1); + REQUIRE(substitutions.size() == 1); + CHECK(substitutions.front().preset_name == "Orphan"); +} + TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]") { RenameTestCollection coll; @@ -5482,3 +5648,39 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape")); } } + +// A project saved before a key joined filament_options_with_variant stores it once per filament, +// while the keys that were already per variant store it once per filament variant. Loading such a +// project gives every variant of a filament that filament's value. +TEST_CASE("A project saved with pressure advance per filament applies it to every variant of the filament", "[Preset][Bundle]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; + config.opt("filament_diameter")->values = { 1.75, 1.75 }; + config.option("filament_settings_id", true)->values = { "Project PLA", "Project PETG" }; + // A multi-variant printer: full_print_config() leaves the list out, and the loader splits the + // variant keys per filament only when the project carries it. + config.option("extruder_variant_list", true)->values = { "Direct Drive Standard,Direct Drive High Flow" }; + // filament 1 defines Standard and High Flow, filament 2 Standard + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard" }; + config.opt("filament_self_index")->values = { 1, 1, 2 }; + config.opt("filament_flow_ratio")->values = { 0.95, 0.96, 0.97 }; // one per filament variant + // One per filament, read through the loader that project files go through. + config.load_from_ini_string("pressure_advance = 0.021,0.043", ForwardCompatibilitySubstitutionRule::Disable); + // The CLI slices the config as loaded. + check_double_vector(config.opt("pressure_advance")->values, { 0.021, 0.021, 0.043 }); + check_double_vector(config.opt("filament_flow_ratio")->values, { 0.95, 0.96, 0.97 }); + // The GUI normalizes the config before load; mirror that so only the production path runs. + Preset::normalize(config); + + PresetBundle bundle; + bundle.load_config_model("test.3mf", std::move(config)); + + REQUIRE(bundle.filament_presets.size() == 2); + const DynamicPrintConfig &pla = bundle.filaments.find_preset(bundle.filament_presets[0], false, true)->config; + const DynamicPrintConfig &petg = bundle.filaments.find_preset(bundle.filament_presets[1], false, true)->config; + check_double_vector(pla.opt("pressure_advance")->values, { 0.021, 0.021 }); + check_double_vector(petg.opt("pressure_advance")->values, { 0.043 }); + check_double_vector(pla.opt("filament_flow_ratio")->values, { 0.95, 0.96 }); + check_double_vector(petg.opt("filament_flow_ratio")->values, { 0.97 }); +} diff --git a/tests/libslic3r/test_utils.cpp b/tests/libslic3r/test_utils.cpp index 2111173326..6256e5f044 100644 --- a/tests/libslic3r/test_utils.cpp +++ b/tests/libslic3r/test_utils.cpp @@ -243,3 +243,147 @@ TEST_CASE("find_unused_filename gives up after 999 versions", "[Utils]") { REQUIRE_FALSE(find_unused_filename(dir.path(), "model.3mf", {}, name)); CHECK(name == "model(999).3mf"); } + +TEST_CASE("is_path_within_root accepts a root given with a trailing separator", "[utils]") { + ScopedTemporaryDir tmp; + const std::string root = tmp.path().string(); + const std::string with_separator = GENERATE_COPY(root + "/", root + std::string(1, static_cast(boost::filesystem::path::preferred_separator))); + + CAPTURE(with_separator); + CHECK(is_path_within_root("vendor.json", with_separator)); + CHECK(is_path_within_root("vendor/machine/printer.json", with_separator)); + CHECK_FALSE(is_path_within_root("../vendor.json", with_separator)); +} + +TEST_CASE("is_path_within_root treats Windows-specific name forms the same on every platform", "[utils]") { + ScopedTemporaryDir tmp; + + SECTION("names ending in dots or spaces stay inside the root") { + const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("dir./file.json"), std::string("dir /file.json")); + CAPTURE(name); + CHECK(is_path_within_root(name, tmp.path())); + } + SECTION("drive-relative names are rejected") { + const std::string name = GENERATE(std::string("C:x"), std::string("c:x/y.json"), std::string("C:")); + CAPTURE(name); + CHECK_FALSE(is_path_within_root(name, tmp.path())); + } +} + +TEST_CASE("is_path_within_root rejects a name with an embedded NUL", "[utils]") { + ScopedTemporaryDir tmp; + // The filesystem calls stop at the NUL, so they would act on a different path than the one checked. + const std::string name = GENERATE(std::string("..\0", 3), std::string("..\0x/file.json", 14), std::string("sub/..\0x", 8), + std::string("file.json\0", 10), std::string("\0file.json", 10)); + CAPTURE(name.size()); + CHECK_FALSE(is_path_within_root(name, tmp.path())); +} + +TEST_CASE("is_symlink_target_within_root accepts relative targets that stay inside the root", "[utils]") { + ScopedTemporaryDir tmp; + const auto [link, target] = GENERATE(std::make_pair(std::string("Versions/Current"), std::string("A")), + std::make_pair(std::string("Foo.framework/Foo"), std::string("Versions/Current/Foo")), + std::make_pair(std::string("libfoo.so"), std::string("libfoo.so.1")), + std::make_pair(std::string("a/b/link"), std::string("c/d"))); + CAPTURE(link, target); + CHECK(is_symlink_target_within_root(link, target, tmp.path())); +} + +TEST_CASE("is_symlink_target_within_root rejects absolute targets and targets that climb out", "[utils]") { + ScopedTemporaryDir tmp; + const std::string outside = (tmp.path().parent_path() / "outside").generic_string(); + const auto [link, target] = GENERATE_COPY(std::make_pair(std::string("sub/link"), outside), + std::make_pair(std::string("sub/link"), std::string("/etc/passwd")), + std::make_pair(std::string("sub/link"), std::string("\\outside")), + std::make_pair(std::string("sub/link"), std::string("C:/outside")), + std::make_pair(std::string("sub/link"), std::string("C:outside")), + std::make_pair(std::string("sub/link"), std::string("")), + std::make_pair(std::string("link"), std::string("..")), + std::make_pair(std::string("link"), std::string("../outside")), + std::make_pair(std::string("sub/link"), std::string("../../outside")), + std::make_pair(std::string("sub/link"), std::string("x/../../../outside")), + std::make_pair(std::string("sub/link"), std::string("..\\..\\outside")), + // symlink() stops at the NUL, so this target would be created as "..". + std::make_pair(std::string("link"), std::string("..\0", 3))); + CAPTURE(link, target); + CHECK_FALSE(is_symlink_target_within_root(link, target, tmp.path())); +} + +#ifndef _WIN32 +TEST_CASE("is_symlink_target_within_root rejects a target that passes through a symlink leading out", "[utils]") { + ScopedTemporaryDir tmp; + const boost::filesystem::path root = tmp.path() / "root"; + const boost::filesystem::path outside = tmp.path() / "outside"; + boost::filesystem::create_directories(root); + boost::filesystem::create_directories(outside); + boost::filesystem::create_symlink(outside, root / "out"); + + CHECK_FALSE(is_symlink_target_within_root("link", "out/lib.so", root)); + CHECK(is_symlink_target_within_root("link", "in/lib.so", root)); +} +#endif + +TEST_CASE("is_absolute_path_within_root accepts only entries inside the root", "[utils]") { + namespace fs = boost::filesystem; + ScopedTemporaryDir outer; + const fs::path root = outer.path() / "Auxiliaries"; + fs::create_directories(root / "Others"); + const fs::path inside = root / "Others" / "note.txt"; + const fs::path outside = outer.path() / "secret.txt"; + std::ofstream(inside.string()) << "inside"; + std::ofstream(outside.string()) << "outside"; + + SECTION("a file inside the root") { + REQUIRE(is_absolute_path_within_root(inside, root)); + } + SECTION("a path inside the root whose file does not exist yet") { + REQUIRE(is_absolute_path_within_root(root / "Others" / "missing.txt", root)); + } + SECTION("the root itself") { + REQUIRE_FALSE(is_absolute_path_within_root(root, root)); + } + SECTION("a parent-directory escape spelled under the root") { + REQUIRE_FALSE(is_absolute_path_within_root(root / "Others" / ".." / ".." / "secret.txt", root)); + } + SECTION("an absolute path elsewhere") { + REQUIRE_FALSE(is_absolute_path_within_root(outside, root)); + } + SECTION("a sibling directory sharing the root's name as a prefix") { + const fs::path sibling = outer.path() / "Auxiliaries2" / "note.txt"; + REQUIRE_FALSE(is_absolute_path_within_root(sibling, root)); + } + SECTION("a relative path") { + REQUIRE_FALSE(is_absolute_path_within_root(fs::path("Others") / "note.txt", root)); + } + SECTION("an empty path") { + REQUIRE_FALSE(is_absolute_path_within_root(fs::path(), root)); + } +#ifndef _WIN32 + // Creating symlinks on Windows needs elevated rights or developer mode. + SECTION("a symlink inside the root that points outside") { + const fs::path link = root / "Others" / "link.txt"; + fs::create_symlink(outside, link); + REQUIRE_FALSE(is_absolute_path_within_root(link, root)); + } +#endif +} + +TEST_CASE("is_safe_to_open_file_name accepts plain documents, images and models", "[utils]") { + const std::string safe = GENERATE(as{}, + "Manual.pdf", "BOM.xlsx", "BOM.csv", "guide.docx", "notes.txt", "README.md", "photo.JPG", "render.png", + "assembly.step", "part.stl", "project.3mf", "drawing.dxf", "build.mp4", "setup.exe.pdf", ".pdf"); + INFO(safe); + CHECK(is_safe_to_open_file_name(safe)); +} + +TEST_CASE("is_safe_to_open_file_name rejects programs and anything it does not know", "[utils]") { + const std::string unsafe = GENERATE(as{}, + "setup.exe", "SETUP.EXE", "Manual.pdf.exe", "run.bat", "shortcut.lnk", "site.url", "script.ps1", "help.chm", + "tool.jar", "script.py", "Install.command", "install.sh", "launcher.desktop", "Printer.AppImage", + // Documents that can carry macros or scripts. + "BOM.xls", "BOM.xlsm", "guide.doc", "guide.docm", "sheet.ods", "page.html", "logo.svg", "bundle.zip", + // No extension, an unknown one, or a name the desktop would read differently. + "readme", "pdf", "data.xyz", "", "...", "Manual.pdf.", "Manual.pdf ", "setup.exe:note.txt", "dir.pdf/readme"); + INFO(unsafe); + CHECK_FALSE(is_safe_to_open_file_name(unsafe)); +} diff --git a/tests/libslic3r/test_vendor_cache.cpp b/tests/libslic3r/test_vendor_cache.cpp index 93d2b2e87a..585595ace9 100644 --- a/tests/libslic3r/test_vendor_cache.cpp +++ b/tests/libslic3r/test_vendor_cache.cpp @@ -3,12 +3,21 @@ #include #include #include +#include +#include #include #include #include +#include +#include +#include #include #include +#include +#include +#include + #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetCacheFormat.hpp" #include "libslic3r/Preset.hpp" @@ -52,6 +61,27 @@ void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std << R"(","from":"system","instantiation":"true","layer_height":"0.2"})"; } +// A vendor whose process list is `processes`, each a preset name and the text of +// its sub-file, listed in that order. +void write_process_vendor(const fs::path& dir, const std::string& vendor, + const std::vector>& processes) +{ + fs::create_directories(dir / vendor / "process"); + std::ofstream index((dir / (vendor + ".json")).string()); + index << R"({"version":"1.0.0","name":")" << vendor << R"(","process_list":[)"; + for (size_t i = 0; i < processes.size(); ++ i) { + const std::string sub_path = "process/p" + std::to_string(i) + ".json"; + index << (i ? "," : "") << R"({"name":")" << processes[i].first << R"(","sub_path":")" << sub_path << R"("})"; + std::ofstream((dir / vendor / sub_path).string()) << processes[i].second; + } + index << "]}"; +} + +std::string process_json(const std::string& name, const std::string& extra = std::string()) +{ + return R"({"type":"process","name":")" + name + R"(","from":"system",)" + extra + R"("layer_height":"0.2"})"; +} + // A small but complete vendor: one machine model, one process, a non-instantiated // base filament with an instantiated child that inherits it and includes a // dual-extruder template, a second standalone filament carrying explicit @@ -128,6 +158,80 @@ void write_lib_tree(const fs::path& dir, const std::string& version, const std:: << R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})"; } +struct FixtureEntry { + std::string name, sub_path; + std::vector deps; // indices of the entries this one inherits or includes +}; + +// Filaments with two levels of inheritance, a template to include, and `leaves` +// presets that inherit either level, some including the template. Only the +// subfiles are written; write_filament_list lists them. +std::vector write_layered_filaments(const fs::path& dir, const std::string& vendor, int leaves) +{ + fs::create_directories(dir / vendor / "filament"); + std::vector entries { + { vendor + " Base PLA", "filament/base.json", {} }, + { vendor + " Mid PLA", "filament/mid.json", {0} }, + { vendor + " dual template", "filament/template.json", {} }, + }; + std::ofstream((dir / vendor / "filament" / "base.json").string()) + << R"({"type":"filament","name":")" << entries[0].name << R"(","from":"system","instantiation":"false",)" + << R"("filament_id":"GFA_base","filament_cost":"42","filament_max_volumetric_speed":["12"]})"; + std::ofstream((dir / vendor / "filament" / "mid.json").string()) + << R"({"type":"filament","name":")" << entries[1].name << R"(","from":"system","instantiation":"false",)" + << R"("inherits":")" << entries[0].name << R"(","filament_flow_ratio":"0.95"})"; + std::ofstream((dir / vendor / "filament" / "template.json").string()) + << R"({"type":"filament","name":")" << entries[2].name << R"(","from":"system","instantiation":"false",)" + << R"("filament_extruder_variant":["Direct Drive Standard","Direct Drive High Flow"],)" + << R"("filament_max_volumetric_speed":["20","22"]})"; + for (int i = 0; i < leaves; ++ i) { + const size_t parent = i % 2; + const bool include = i % 3 == 0; + entries.push_back({ vendor + " PLA " + std::to_string(i) + " @0.4", "filament/leaf" + std::to_string(i) + ".json", + include ? std::vector{parent, 2} : std::vector{parent} }); + std::ofstream((dir / vendor / entries.back().sub_path).string()) + << R"({"type":"filament","name":")" << entries.back().name << R"(","from":"system","instantiation":"true",)" + << R"("inherits":")" << entries[parent].name << R"(",)" + << (include ? R"("include":[")" + entries[2].name + R"("],)" : std::string()) + << R"("nozzle_temperature":[")" << (200 + i % 40) << R"("]})"; + } + return entries; +} + +// The vendor profile for write_layered_filaments, listing its entries in `order`, +// or as they were written when `order` is empty. +void write_filament_list(const fs::path& dir, const std::string& vendor, const std::vector& entries, + std::vector order = {}) +{ + if (order.empty()) { + order.resize(entries.size()); + std::iota(order.begin(), order.end(), 0); + } + std::ofstream f((dir / (vendor + ".json")).string()); + f << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)"; + for (size_t k = 0; k < order.size(); ++ k) + f << (k ? "," : "") << R"({"name":")" << entries[order[k]].name << R"(","sub_path":")" + << entries[order[k]].sub_path << R"("})"; + f << "]}"; +} + +// A random order of `entries` that lists each one after everything it depends on. +std::vector shuffled_after_dependencies(const std::vector& entries, std::mt19937& rng) +{ + std::vector order; + std::vector listed(entries.size(), false); + while (order.size() < entries.size()) { + std::vector ready; + for (size_t i = 0; i < entries.size(); ++ i) + if (! listed[i] && std::all_of(entries[i].deps.begin(), entries[i].deps.end(), [&](size_t d) { return listed[d]; })) + ready.push_back(i); + const size_t next = ready[std::uniform_int_distribution(0, ready.size() - 1)(rng)]; + listed[next] = true; + order.push_back(next); + } + return order; +} + // A vendor whose one filament inherits the library's base and states nothing of // its own — everything it shows comes from the library it is resolved against. void write_vendor_with_lib_filament(const fs::path& dir, const std::string& vendor, const std::string& version) @@ -624,6 +728,299 @@ TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", " CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // through the include } +TEST_CASE("a wide vendor loads from its cache exactly as it loads from JSON", "[VendorCache]") +{ + InstallDirs dirs; + constexpr int leaves = 400; + write_filament_list(dirs.system, "Acme", write_layered_filaments(dirs.system, "Acme", leaves)); + + PresetBundle from_json; + from_json.set_generate_vendor_caches(true); + from_json.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(dirs.system / "Acme.opc")); + + fs::remove_all(dirs.system / "Acme"); + PresetBundle from_cache; + from_cache.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + auto a = presets_for(from_json.filaments, "Acme"); + auto b = presets_for(from_cache.filaments, "Acme"); + REQUIRE(a.size() == size_t(leaves)); + REQUIRE(b.size() == a.size()); + for (size_t i = 0; i < a.size(); ++ i) { + CHECK(a[i]->name == b[i]->name); + CHECK(preset_deep_equal(*a[i], *b[i])); + } + CHECK(from_cache.error_count() == from_json.error_count()); + // The two-level chain resolved, so the leaf has the middle profile's flow ratio + // and the base profile's filament id, neither of which it states. + const Preset* leaf = from_cache.filaments.find_preset("Acme PLA 7 @0.4", false); + REQUIRE(leaf != nullptr); + CHECK(leaf->filament_id == "GFA_base"); + const auto* flow = leaf->config.option("filament_flow_ratio"); + REQUIRE(flow != nullptr); + CHECK_THAT(flow->values.front(), WithinAbs(0.95, 1e-9)); +} + +TEST_CASE("presets install the same in any order that lists each after what it depends on", "[VendorCache]") +{ + InstallDirs dirs; + const std::vector entries = write_layered_filaments(dirs.system, "Acme", 150); + auto load = [&](const std::vector& order) { + write_filament_list(dirs.system, "Acme", entries, order); + auto bundle = std::make_unique(); + bundle->load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + return bundle; + }; + const auto reference = load({}); + CHECK(reference->error_count() == 0); + const auto expected = presets_for(reference->filaments, "Acme"); + REQUIRE(expected.size() == 150); + + // Every seed checks against one reference load, which GENERATE would repeat per seed. + for (unsigned seed = 0; seed < 8; ++ seed) { + CAPTURE(seed); + std::mt19937 rng(seed); + const auto shuffled = load(shuffled_after_dependencies(entries, rng)); + CHECK(shuffled->error_count() == 0); + const auto actual = presets_for(shuffled->filaments, "Acme"); + REQUIRE(actual.size() == expected.size()); + for (size_t i = 0; i < actual.size(); ++ i) + CHECK(preset_deep_equal(*actual[i], *expected[i])); + } +} + +TEST_CASE("a key misplaced into a vendor preset is reported and removed", "[VendorCache]") +{ + InstallDirs dirs; + write_process_vendor(dirs.system, "Acme", { + { "Acme template", R"({"type":"process","name":"Acme template","from":"system","instantiation":"false","wall_loops":"5","filament_density":"1.2"})" }, + { "Acme base", R"({"type":"process","name":"Acme base","from":"system","instantiation":"false","filament_cost":"5"})" }, + { "0.20mm Standard @Acme", process_json("0.20mm Standard @Acme", + R"("instantiation":"true","inherits":"Acme base","include":["Acme template"],"nozzle_temperature":["210"],)") } }); + PresetBundle bundle; + bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + CHECK(bundle.error_count() == 3); + const Preset* preset = bundle.prints.find_preset("0.20mm Standard @Acme", false); + REQUIRE(preset != nullptr); + CHECK_FALSE(preset->config.has("filament_cost")); + CHECK_FALSE(preset->config.has("filament_density")); + CHECK_FALSE(preset->config.has("nozzle_temperature")); + CHECK(preset->config.opt_int("wall_loops") == 5); +} + +TEST_CASE("each vendor loads from the directory it is listed with", "[VendorCache]") +{ + InstallDirs dirs; + write_vendor_tree(dirs.system, "Acme", "1.0.0"); + write_vendor_tree(dirs.profiles, "Zeta", "2.0.0"); + PresetBundle bundle; + bundle.load_vendors({ { "Acme", dirs.system }, { "Zeta", dirs.profiles } }, + ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false); + + CHECK(bundle.vendors.count("Acme") == 1); + CHECK(bundle.vendors.count("Zeta") == 1); + CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr); + CHECK(bundle.prints.find_preset("0.20mm Standard @Zeta", false) != nullptr); +} + +TEST_CASE("a vendor that fails to load is left out, reported, and the others still load", "[VendorCache]") +{ + InstallDirs dirs; + write_vendor_tree(dirs.system, "Acme", "1.0.0"); + write_process_vendor(dirs.system, "Broken", { { "Broken A", "{not-json" } }); + PresetBundle bundle; + std::vector failed; + const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { "Broken", dirs.system } }, + ForwardCompatibilitySubstitutionRule::EnableSilent, + /*allow_cache=*/false, nullptr, &failed).second; + + CHECK(bundle.vendors.count("Acme") == 1); + CHECK(bundle.vendors.count("Broken") == 0); + CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr); + CHECK(errors.find("Broken") != std::string::npos); + CHECK(failed == std::vector{ "Broken" }); +} + +TEST_CASE("a filament library that fails partway is reported, and vendors inheriting from it are left out", "[VendorCache]") +{ + InstallDirs dirs; + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + write_lib_tree(dirs.system, "1.0.0", "20"); + std::ofstream((dirs.system / (lib + ".json")).string()) + << R"({"version":"1.0.0","name":")" << lib << R"(","filament_list":[)" + << R"({"name":"Generic PLA","sub_path":"filament/generic_pla.json"},)" + << R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json"}]})"; + std::ofstream((dirs.system / lib / "filament" / "generic_petg.json").string()) << "{not-json"; + write_vendor_with_lib_filament(dirs.system, "Acme", "1.0.0"); + write_vendor_tree(dirs.system, "Zeta", "1.0.0"); + + PresetBundle bundle; + std::vector failed; + const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { lib, dirs.system }, { "Zeta", dirs.system } }, + ForwardCompatibilitySubstitutionRule::EnableSilent, + /*allow_cache=*/false, nullptr, &failed).second; + + CHECK(failed == std::vector{ lib, "Acme" }); + CHECK(errors.find("generic_petg.json") != std::string::npos); + CHECK(bundle.vendors.count(lib) == 1); + CHECK(bundle.vendors.count("Acme") == 0); + CHECK(bundle.filaments.find_preset("Acme PLA @0.4", false) == nullptr); + CHECK(bundle.vendors.count("Zeta") == 1); + CHECK(bundle.prints.find_preset("0.20mm Standard @Zeta", false) != nullptr); +} + +TEST_CASE("a vendor whose profile cannot be read is left out, reported, and the others still load", "[VendorCache]") +{ + InstallDirs dirs; + write_vendor_tree(dirs.system, "Acme", "1.0.0"); + std::ofstream((dirs.system / "Broken.json").string()) << "{not-json"; + PresetBundle bundle; + const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { "Broken", dirs.system } }, + ForwardCompatibilitySubstitutionRule::EnableSilent, + /*allow_cache=*/false).second; + + CHECK(bundle.vendors.count("Acme") == 1); + CHECK(bundle.vendors.count("Broken") == 0); + CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr); + CHECK(errors.find("Broken.json") != std::string::npos); +} + +TEST_CASE("a canceled vendor load starts no vendor", "[VendorCache]") +{ + InstallDirs dirs; + write_vendor_tree(dirs.system, "Acme", "1.0.0"); + write_vendor_tree(dirs.system, "Zeta", "1.0.0"); + const std::atomic cancel { true }; + PresetBundle bundle; + bundle.load_vendors({ { "Acme", dirs.system }, { "Zeta", dirs.system } }, + ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false, &cancel); + + CHECK(bundle.vendors.empty()); + CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) == nullptr); +} + +TEST_CASE("a preset two vendors both define is kept from the first listed and reported under the others", "[VendorCache]") +{ + InstallDirs dirs; + for (const std::string vendor : { "Acme", "Mira", "Zeta" }) + write_process_vendor(dirs.system, vendor, { + { "Shared", process_json("Shared", R"("instantiation":"true",)") }, + { "Own @" + vendor, process_json("Own @" + vendor, R"("instantiation":"true",)") } }); + PresetBundle bundle; + const std::string errors = bundle.load_vendors({ { "Mira", dirs.system }, { "Acme", dirs.system }, { "Zeta", dirs.system } }, + ForwardCompatibilitySubstitutionRule::EnableSilent, + /*allow_cache=*/false).second; + + const Preset* shared = bundle.prints.find_preset("Shared", false); + REQUIRE(shared != nullptr); + REQUIRE(shared->vendor != nullptr); + CHECK(shared->vendor->id == "Mira"); + CHECK(errors.find("vendor Mira") == std::string::npos); + CHECK(errors.find("Found duplicated settings in vendor Acme's json file lists: Shared") != std::string::npos); + CHECK(errors.find("Found duplicated settings in vendor Zeta's json file lists: Shared") != std::string::npos); + CHECK(bundle.error_count() == 2); + for (const std::string vendor : { "Acme", "Mira", "Zeta" }) { + const Preset* own = bundle.prints.find_preset("Own @" + vendor, false); + REQUIRE(own != nullptr); + CHECK(own->vendor == &bundle.vendors.at(vendor)); + } +} + +TEST_CASE("filaments merged from several vendors come out generic first, then by name", "[VendorCache]") +{ + InstallDirs dirs; + auto write_filaments = [&](const std::string& vendor, const std::vector& names) { + fs::create_directories(dirs.system / vendor / "filament"); + std::ofstream index((dirs.system / (vendor + ".json")).string()); + index << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)"; + for (size_t i = 0; i < names.size(); ++ i) { + const std::string sub_path = "filament/f" + std::to_string(i) + ".json"; + index << (i ? "," : "") << R"({"name":")" << names[i] << R"(","sub_path":")" << sub_path << R"("})"; + std::ofstream((dirs.system / vendor / sub_path).string()) + << R"({"type":"filament","name":")" << names[i] << R"(","from":"system","instantiation":"true",)" + << R"("filament_id":"GF)" << vendor << i << R"("})"; + } + index << "]}"; + }; + write_filaments("Zeta", { "Zeta PLA @0.4", "Generic PETG @Zeta" }); + write_filaments("Acme", { "Acme PLA @0.4", "Generic PLA @Acme" }); + PresetBundle bundle; + bundle.load_vendors({ { "Zeta", dirs.system }, { "Acme", dirs.system } }, + ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false); + + std::vector names; + for (const Preset& preset : bundle.filaments.get_presets()) + if (! preset.is_default) + names.push_back(preset.name); + CHECK(names == std::vector{ "Generic PETG @Zeta", "Generic PLA @Acme", "Acme PLA @0.4", "Zeta PLA @0.4" }); + for (const std::string& name : names) + CHECK(bundle.filaments.find_preset(name, false) != nullptr); +} + +TEST_CASE("a vendor read while the filament library loads resolves against it, from JSON and from its cache", "[VendorCache]") +{ + InstallDirs dirs; + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + write_lib_tree(dirs.system, "1.0.0", "20"); + write_vendor_with_lib_filament(dirs.system, "Acme", "1.0.0"); + auto load = [&] { + auto bundle = std::make_unique(); + bundle->set_generate_vendor_caches(true); + bundle->load_vendors({ { "Acme", dirs.system }, { lib, dirs.system } }, + ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/true); + return bundle; + }; + + const auto from_json = load(); + REQUIRE(fs::exists(dirs.system / "Acme.opc")); + fs::remove_all(dirs.system / "Acme"); + const auto from_cache = load(); + + for (const PresetBundle* bundle : { from_json.get(), from_cache.get() }) { + const Preset* pla = bundle->filaments.find_preset("Acme PLA @0.4", false); + REQUIRE(pla != nullptr); + CHECK(pla->filament_id == "GFL99"); + CHECK_THAT(pla->config.option("filament_cost")->values.front(), WithinAbs(20., 1e-9)); + CHECK(bundle->error_count() == 0); + } +} + +TEST_CASE("repeated cache loads of one vendor produce the same presets", "[VendorCache]") +{ + InstallDirs dirs; + write_filament_list(dirs.system, "Acme", write_layered_filaments(dirs.system, "Acme", 400)); + + PresetBundle seed; + seed.set_generate_vendor_caches(true); + seed.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(dirs.system / "Acme.opc")); + fs::remove_all(dirs.system / "Acme"); + + std::vector loads(3); + for (PresetBundle& bundle : loads) + bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + auto first = presets_for(loads[0].filaments, "Acme"); + REQUIRE(!first.empty()); + for (size_t run = 1; run < loads.size(); ++ run) { + auto again = presets_for(loads[run].filaments, "Acme"); + REQUIRE(again.size() == first.size()); + for (size_t i = 0; i < first.size(); ++ i) { + CHECK(first[i]->name == again[i]->name); + CHECK(preset_deep_equal(*first[i], *again[i])); + } + CHECK(loads[run].error_count() == loads[0].error_count()); + } +} + TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]") { TempDir tmp; @@ -866,6 +1263,27 @@ TEST_CASE("a vendor installed as its cache alone still loads after a library upd CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9)); } +TEST_CASE("a cache install stopped by a canceled task group throws instead of rejecting the cache", "[VendorCache]") +{ + TempDir tmp; + const std::string cache = (tmp.path / "Acme.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Acme"), "Acme", "1.0.0", {filament_entry("Acme PLA @0.4")})); + + PresetBundle bundle; + bool threw = false; + tbb::task_group_context context; + tbb::parallel_for(tbb::blocked_range(0, 1), [&](const tbb::blocked_range&) { + context.cancel_group_execution(); + try { + bundle.load_vendor_cache(cache, "Acme", Semver(1, 0, 0)); + } catch (const std::runtime_error&) { + threw = true; + } + }, context); + + CHECK(threw); +} + TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSONs", "[VendorCache]") { TempDir tmp; @@ -893,6 +1311,12 @@ TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSON user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(presets_loaded == 1); CHECK(out.vendors.at("Acme").name == "Acme"); // the profile's name, not the cache's + + // Through load_vendors, which reads the cache before installing it. + PresetBundle several; + several.load_vendors({ { "Acme", user } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/true); + CHECK(several.vendors.at("Acme").name == "Acme"); + CHECK(several.prints.find_preset("0.20mm Standard @Acme", false) != nullptr); } TEST_CASE("a profile with no usable version is never served from cache", "[VendorCache]") @@ -1691,6 +2115,31 @@ TEST_CASE("an include listed after the preset that names it is an error, and the CHECK(silk->config.option("filament_max_volumetric_speed")->values == std::vector{12.}); } +TEST_CASE("an include listed after the preset that names it is missing from the cache load too", "[VendorCache]") +{ + InstallDirs dirs; + write_full_vendor_tree(dirs.system, "Acme", "1.0.0", /*templates_last=*/true); + PresetBundle from_json; + from_json.set_generate_vendor_caches(true); + from_json.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(dirs.system / "Acme.opc")); + + fs::remove_all(dirs.system / "Acme"); + PresetBundle from_cache; + from_cache.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(from_cache.error_count() == 2); + const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false); + REQUIRE(pr != nullptr); + CHECK(pr->config.opt_string("machine_start_gcode") != "G28 ; template"); + const Preset* silk = from_cache.filaments.find_preset("Acme Silk PLA @0.4", false); + REQUIRE(silk != nullptr); + const auto& speed = silk->config.option("filament_max_volumetric_speed")->values; + REQUIRE(speed.size() == 1); + CHECK_THAT(speed.front(), WithinAbs(12., 1e-9)); +} + TEST_CASE("a G-code template that states no instantiation is included, not loaded as a preset", "[VendorCache]") { InstallDirs dirs; @@ -1712,3 +2161,35 @@ TEST_CASE("a G-code template that states no instantiation is included, not loade CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); CHECK(presets_for(bundle.printers, "Acme").size() == 1); } + +TEST_CASE("a sub-file that fails to parse leaves the ones listed before it installed", "[VendorCache]") +{ + InstallDirs dirs; + write_process_vendor(dirs.system, "Acme", { + { "Acme A", process_json("Acme A", R"("instantiation":"true",)") }, + { "Acme B", "{not-json" }, + { "Acme C", process_json("Acme C", R"("instantiation":"true",)") } }); + PresetBundle bundle; + CHECK_THROWS_AS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent), + ConfigurationError); + CHECK(bundle.prints.find_preset("Acme A", false) != nullptr); + CHECK(bundle.prints.find_preset("Acme C", false) == nullptr); +} + +TEST_CASE("a load that fails to install partway counts no errors of the sub-files after it", "[VendorCache]") +{ + InstallDirs dirs; + // B names a parent nobody defines. C states no instantiation, an error of its own. + write_process_vendor(dirs.system, "Acme", { + { "Acme A", process_json("Acme A", R"("instantiation":"true",)") }, + { "Acme B", process_json("Acme B", R"("instantiation":"true","inherits":"Nobody",)") }, + { "Acme C", process_json("Acme C") } }); + PresetBundle bundle; + CHECK_THROWS_AS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent), + ConfigurationError); + // B's missing parent and B's failed install. + CHECK(bundle.error_count() == 2); + CHECK(bundle.prints.find_preset("Acme A", false) != nullptr); +} diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index be796e814b..a3863134d5 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -27,6 +27,8 @@ add_executable(${_TEST_NAME}_tests test_plugin_cloud_metadata.cpp test_plugin_audit.cpp test_shortcuts.cpp + test_file_url.cpp + test_user_manager.cpp ../fff_print/test_helpers.cpp ) diff --git a/tests/slic3rutils/test_file_url.cpp b/tests/slic3rutils/test_file_url.cpp new file mode 100644 index 0000000000..484067b57e --- /dev/null +++ b/tests/slic3rutils/test_file_url.cpp @@ -0,0 +1,74 @@ +#include +#include + +#include "slic3r/GUI/GUI.hpp" + +#include + +#include + +using namespace Slic3r::GUI; + +#ifndef _WIN32 +TEST_CASE("A file URL keeps characters special to URLs in its path", "[FileUrl]") +{ + const std::string path = GENERATE(as{}, + "/opt/test#dir/resources/web/homepage/index.html", + "/opt/test%20x/resources/web/homepage/index.html", + "/opt/Orca Slicer/resources/web/homepage/index.html", + "/opt/what?/resources/web/homepage/index.html", + "/home/Jos\xC3\xA9/\xE8\xB5\x84\xE6\xBA\x90/resources/web/homepage/index.html"); + + const wxString url = file_url_from_path(boost::filesystem::path(path)); + CAPTURE(path, url.utf8_string()); + + // WebView::CreateWebView() and WebView::LoadUrl() re-parse the URL before loading it. + const wxURI uri(wxURI(url).BuildURI()); + CHECK(uri.GetScheme() == "file"); + CHECK(!uri.HasQuery()); + CHECK(!uri.HasFragment()); + CHECK(wxURI::Unescape(uri.GetPath()).utf8_string() == path); +} + +TEST_CASE("A file URL of a plain path is the path behind file://", "[FileUrl]") +{ + const std::string path = "/opt/OrcaSlicer/resources/web/homepage/index.html"; + CHECK(file_url_from_path(boost::filesystem::path(path)) == "file://" + path); +} + +TEST_CASE("A query appended to a file URL stays separate from its path", "[FileUrl]") +{ + const std::string path = "/opt/test#dir%20x/resources/web/guide/0/index.html"; + const wxURI uri(file_url_from_path(boost::filesystem::path(path)) + "?target=21&lang=de"); + CHECK(wxURI::Unescape(uri.GetPath()).utf8_string() == path); + CHECK(uri.GetQuery() == "target=21&lang=de"); + CHECK(!uri.HasFragment()); +} +#else +TEST_CASE("A file URL of a Windows path has a drive letter and forward slashes", "[FileUrl]") +{ + const auto [path, url] = GENERATE(table({ + { L"C:\\Program Files\\OrcaSlicer\\resources\\web\\homepage\\index.html", + "file:///C:/Program%20Files/OrcaSlicer/resources/web/homepage/index.html" }, + { L"D:\\#OneDrive\\OrcaSlicer\\resources\\web\\guide\\0\\index.html", + "file:///D:/%23OneDrive/OrcaSlicer/resources/web/guide/0/index.html" }, + { L"D:\\100%\\OrcaSlicer\\resources\\web\\homepage\\index.html", + "file:///D:/100%25/OrcaSlicer/resources/web/homepage/index.html" }, + // Callers join the resources directory with a forward-slash relative path. + { L"D:\\#OneDrive\\OrcaSlicer\\resources/web/homepage/index.html", + "file:///D:/%23OneDrive/OrcaSlicer/resources/web/homepage/index.html" }, + { L"\\\\server\\share\\OrcaSlicer\\resources\\web\\homepage\\index.html", + "file://server/share/OrcaSlicer/resources/web/homepage/index.html" }, + })); + CHECK(file_url_from_path(boost::filesystem::path(path)).utf8_string() == url); +} + +TEST_CASE("A query appended to a Windows file URL stays separate from its path", "[FileUrl]") +{ + const wxURI uri(file_url_from_path(boost::filesystem::path(L"D:\\#OneDrive\\OrcaSlicer\\resources\\web\\guide\\0\\index.html")) + + "?target=21&lang=de"); + CHECK(wxURI::Unescape(uri.GetPath()) == "/D:/#OneDrive/OrcaSlicer/resources/web/guide/0/index.html"); + CHECK(uri.GetQuery() == "target=21&lang=de"); + CHECK(!uri.HasFragment()); +} +#endif diff --git a/tests/slic3rutils/test_plugin_host_api.cpp b/tests/slic3rutils/test_plugin_host_api.cpp index e02864f703..0283bb5744 100644 --- a/tests/slic3rutils/test_plugin_host_api.cpp +++ b/tests/slic3rutils/test_plugin_host_api.cpp @@ -16,6 +16,8 @@ #include +#include + namespace py = pybind11; namespace { @@ -210,23 +212,33 @@ TEST_CASE("A reloaded plugin page is recognised by its base URL, fragment aside" { using namespace Slic3r::GUI::web_hosting; - // A resources path holding a space, which the web view reports escaped. + // A resources path holding a space, which the web view may report escaped differently. const Slic3r::ScopedResourcesDir resources("web content check"); // The swapped-in page, then after an in-page anchor and a reload. CHECK(is_content_url(content_base_url())); CHECK(is_content_url(content_base_url() + "#tab2")); - wxString escaped = content_base_url(); - escaped.Replace(" ", "%20"); - REQUIRE(escaped != content_base_url()); - CHECK(is_content_url(escaped)); - CHECK(is_content_url(escaped + "#tab2")); + const wxString unescaped = wxURI::Unescape(content_base_url()); + REQUIRE(unescaped != content_base_url()); + CHECK(is_content_url(unescaped)); + CHECK(is_content_url(unescaped + "#tab2")); // A page the plugin linked to keeps its own URL and must be left alone. CHECK_FALSE(is_content_url(content_base_url() + "guide.html")); CHECK_FALSE(is_content_url("https://example.com/")); CHECK_FALSE(is_content_url("")); } +TEST_CASE("A reloaded plugin page is recognised when the resources path holds a '#'", "[PluginHost]") +{ + using namespace Slic3r::GUI::web_hosting; + + const Slic3r::ScopedResourcesDir resources("web#content check"); + + CHECK(is_content_url(content_base_url())); + CHECK(is_content_url(content_base_url() + "#tab2")); + CHECK_FALSE(is_content_url(content_base_url() + "guide.html")); +} + TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]") { using Catch::Matchers::WithinAbs; diff --git a/tests/slic3rutils/test_printhost.cpp b/tests/slic3rutils/test_printhost.cpp index 557d4a5c89..c4df56ebb6 100644 --- a/tests/slic3rutils/test_printhost.cpp +++ b/tests/slic3rutils/test_printhost.cpp @@ -1,8 +1,15 @@ #include +#include +#include +#include +#include + #include +#include "libslic3r/LifecycleEvents.hpp" #include "slic3r/Utils/PrintHost.hpp" +#include "slic3r/Utils/Flashforge.hpp" using namespace Slic3r; @@ -24,6 +31,41 @@ public: std::string get_host() const override { return {}; } }; +class ThrowingPrintHost : public TestPrintHost +{ +public: + bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { throw std::runtime_error("reply could not be read"); } +}; + +struct UploadEvents +{ + std::vector events; + std::vector codes; + std::vector errors; + bool uploaded{false}; + + explicit UploadEvents(std::unique_ptr host) + { + set_lifecycle_hook_fn([this](LifecycleEvent event, const LifecycleEventContext& ctx) { + events.push_back(event); + codes.push_back(ctx.code); + }); + + PrintHostJob job; + job.printhost = std::move(host); + job.upload_data.source_path = "plate.gcode"; + try { + uploaded = PrintHostJobQueue::upload_job(job, [](Http::Progress, bool&) {}, + [this](wxString error) { errors.push_back(error.ToStdString()); }, + [](wxString, wxString) {}); + } catch (...) { + set_lifecycle_hook_fn(nullptr); + throw; + } + set_lifecycle_hook_fn(nullptr); + } +}; + std::string format_error(const std::string& body, const std::string& error, unsigned status) { return TestPrintHost().format_error(body, error, status).ToStdString(); @@ -46,6 +88,14 @@ std::string moonraker_error(int code, const std::string& message, const std::str constexpr const char* k_busy_file_403 = R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON"; +// Replies a print host can send instead of JSON: a proxy or login page, nothing, a cut-off body. +const std::vector non_json_replies = { + "proxy login required", + "", + "{\"err\":", + "{\"detail\":{\"matlStationInfo\":{\"slotInfos\":[{\"slotId\":1,", +}; + } // namespace TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]") @@ -211,3 +261,114 @@ TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", " CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect"); } } + +TEST_CASE("Print host error code is read from a JSON reply", "[PrintHost]") +{ + CHECK(PrintHost::get_err_code_from_body(R"({"err":0})") == 0); + CHECK(PrintHost::get_err_code_from_body(R"({"err":2})") == 2); + CHECK(PrintHost::get_err_code_from_body(R"({"status":"ok"})") == 0); +} + +TEST_CASE("Print host error code reports a reply that is not JSON as an error", "[PrintHost]") +{ + const std::string body = GENERATE(from_range(non_json_replies)); + int err = 0; + REQUIRE_NOTHROW(err = PrintHost::get_err_code_from_body(body)); + CHECK(err != 0); +} + +TEST_CASE("Print host error code tolerates a wrongly typed err field", "[PrintHost]") +{ + const std::string body = GENERATE(as{}, R"({"err":"busy"})", R"({"err":{"code":1}})", R"([1,2])"); + CHECK_NOTHROW(PrintHost::get_err_code_from_body(body)); +} + +TEST_CASE("Flashforge material slots are read from a well-formed reply", "[PrintHost][Flashforge]") +{ + const std::string body = R"({"code":0,"detail":{"hasMatlStation":true,"matlStationInfo":{"slotCnt":2,"slotInfos":[ + {"slotId":1,"hasFilament":true,"materialName":"PLA","materialColor":"#FFFFFF"}, + {"slotId":2,"hasFilament":false,"materialName":"","materialColor":""}]}}})"; + + std::vector slots; + bool supports_station = false; + REQUIRE(Flashforge::parse_material_slots(body, slots, &supports_station)); + CHECK(supports_station); + REQUIRE(slots.size() == 2); + CHECK(slots[0].slot_id == 1); + CHECK(slots[0].has_filament); + CHECK(slots[0].material_name == "PLA"); + CHECK(slots[0].material_color == "#FFFFFF"); + CHECK(slots[1].slot_id == 2); + CHECK_FALSE(slots[1].has_filament); +} + +TEST_CASE("Flashforge material slots accept numbers as strings and flags as numbers", "[PrintHost][Flashforge]") +{ + const std::string body = R"({"detail":{"matlStationInfo":{"slotInfos":[ + {"slotId":"3","hasFilament":1,"materialName":null,"materialColor":7}]}}})"; + + std::vector slots; + REQUIRE_NOTHROW(Flashforge::parse_material_slots(body, slots, nullptr)); + REQUIRE(slots.size() == 1); + CHECK(slots[0].slot_id == 3); + CHECK(slots[0].has_filament); + CHECK(slots[0].material_name.empty()); + CHECK(slots[0].material_color.empty()); +} + +TEST_CASE("Flashforge material slots skip entries that are not objects", "[PrintHost][Flashforge]") +{ + const std::string body = R"({"detail":{"matlStationInfo":{"slotInfos":[5,"slot",null,[], + {"slotId":4,"hasFilament":true,"materialName":"PETG"}]}}})"; + + std::vector slots; + REQUIRE_NOTHROW(Flashforge::parse_material_slots(body, slots, nullptr)); + REQUIRE(slots.size() == 1); + CHECK(slots[0].slot_id == 4); + CHECK(slots[0].material_name == "PETG"); +} + +TEST_CASE("Flashforge material slots tolerate slot info that is not a list", "[PrintHost][Flashforge]") +{ + const std::string body = GENERATE(as{}, + R"({"detail":{"matlStationInfo":{"slotInfos":5}}})", + R"({"detail":{"matlStationInfo":{"slotInfos":"none"}}})", + R"({"detail":{"matlStationInfo":7}})", + R"({"detail":"offline"})"); + + std::vector slots; + bool ok = false; + REQUIRE_NOTHROW(ok = Flashforge::parse_material_slots(body, slots, nullptr)); + CHECK(ok); + CHECK(slots.empty()); +} + +TEST_CASE("Flashforge material slots reject a reply that is not JSON", "[PrintHost][Flashforge]") +{ + const std::string body = GENERATE(from_range(non_json_replies)); + std::vector slots; + bool ok = true; + REQUIRE_NOTHROW(ok = Flashforge::parse_material_slots(body, slots, nullptr)); + CHECK_FALSE(ok); + CHECK(slots.empty()); +} + +TEST_CASE("An upload that throws still finishes with an error", "[PrintHost][LifecycleEvents]") +{ + UploadEvents run(std::make_unique()); + + CHECK_FALSE(run.uploaded); + CHECK(run.errors == std::vector{"reply could not be read"}); + CHECK(run.events == std::vector{LifecycleEvent::UploadStarted, LifecycleEvent::UploadFinished}); + CHECK(run.codes == std::vector{LifecycleEvtCode::Ok, LifecycleEvtCode::Error}); +} + +TEST_CASE("A successful upload finishes without an error", "[PrintHost][LifecycleEvents]") +{ + UploadEvents run(std::make_unique()); + + CHECK(run.uploaded); + CHECK(run.errors.empty()); + CHECK(run.events == std::vector{LifecycleEvent::UploadStarted, LifecycleEvent::UploadFinished}); + CHECK(run.codes == std::vector{LifecycleEvtCode::Ok, LifecycleEvtCode::Ok}); +} diff --git a/tests/slic3rutils/test_user_manager.cpp b/tests/slic3rutils/test_user_manager.cpp new file mode 100644 index 0000000000..cfccebe1cc --- /dev/null +++ b/tests/slic3rutils/test_user_manager.cpp @@ -0,0 +1,23 @@ +#include + +#include + +#include "slic3r/GUI/UserManager.hpp" + +using namespace Slic3r; + +TEST_CASE("User message that is not JSON is rejected without throwing", "[UserManager]") +{ + const std::string payload = GENERATE(as{}, "not json", "", "", "{\"bind\":"); + UserManager manager; + int result = 0; + REQUIRE_NOTHROW(result = manager.parse_json(payload)); + CHECK(result == -1); +} + +TEST_CASE("User message without a successful bind is ignored", "[UserManager]") +{ + const std::string payload = GENERATE(as{}, "{}", R"({"bind":{"command":"unbind"}})", R"({"bind":"bind"})", "[1]"); + UserManager manager; + CHECK(manager.parse_json(payload) == -1); +}