Compare commits

..
Author SHA1 Message Date
789f848694 Write the estimated printing time comment after the config block, not before (#15897)
Co-authored-by: Fernando Marino <f.marino@rheagroup.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-09-29 19:18:15 -03:00
Rodrigo FaselliandIan Bassi 3a0694dce6 Remember last print action (#15774)
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
2026-09-29 19:17:45 -03:00
Kris AustinandRodrigo Faselli f5679ad343 perf: load presets in parallel, cutting preset load time by over 60% (#15943)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-09-29 18:13:44 -03:00
HanifKoh 2769b12ce7 Give OBJ Quad and Flipped Faces the Texture Coordinates of Their Own Corners (#15977)
load_obj emits the second triangle of a quad from corners 0, 2 and 3,
but read its texture coordinates from corners 0, 1 and 2, so half of
every textured quad sampled the wrong part of the texture. The corner
indices are now passed down to where the coordinates are read.

A mesh with inward-facing triangles is flipped after loading, which
swaps corners 1 and 2 of every face. The texture coordinates were left
as they were. They are now swapped along with the corners.
2026-09-30 03:13:29 +08:00
HanifKoh 203bc63f35 Escape Project Metadata in the Project Page and Restrict Accessory Opening (#15956)
* Escape Project Metadata in the Project Page and Restrict Accessory Opening

The Project page rendered the model and profile name, author, description
and accessory file names from the 3MF as live HTML. Names, authors and file
names are now set as text, and the file list is built from DOM nodes with
bound click handlers instead of concatenated markup. Descriptions can
legitimately carry rich-text HTML, so they are rebuilt from an inert
DOMParser document, keeping only plain formatting tags, http(s) links and
http(s) images, with every other attribute dropped.

Opening an accessory from the page now only launches regular files that
lie inside the project's extracted auxiliary directory. The containment
check is a new libslic3r helper, is_absolute_path_within_root, built on
is_path_within_root so symlinks leading out of the root are rejected too.

* Tighten Project Page Description Rendering and Keep More Formatting

Link and image URLs in descriptions must now start with an http or https
scheme as written and parse as such with the URL parser. Preview images are
built as DOM nodes like the file list, and accessory names show their full
text as a tooltip.

Descriptions keep more plain formatting: del, ins, figure, figcaption, dl,
dt, dd, caption, q, abbr, kbd and wbr, plus alt, title, width and height on
images, colspan and rowspan on table cells and start on ordered lists.
Numeric attributes must be plain integers. Embedded YouTube players become
a link to the video.

* Confirm Before Opening Program Attachments and Load Only HTTPS Images

Opening a project attachment whose type runs as a program or script
(executables, installers, shortcuts, shell and PowerShell scripts, macOS
command files and apps, Linux desktop entries) now asks for confirmation
first. The check lives in libslic3r as is_executable_file_name and ignores
the trailing dots and spaces Windows strips from file names.

Images in project descriptions are kept only when they load over https,
so opening the Project tab no longer issues plain-http requests.

* Open Project Attachments Through One Guarded Helper

The Edit Project Info view launched attachments directly, without the
checks the project page has. Both now call
desktop_open_project_attachment, which checks that the file is inside
the auxiliary directory, asks for confirmation where needed and then
opens it.

The auxiliary root was built through encode_path, which returns code
page bytes on Windows, while boost::filesystem reads a narrow string as
UTF-8. With a non-ASCII temporary directory the root never matched and
no attachment opened. It is now built from the UTF-8 path directly.

The list of program extensions could not be kept complete and let
unknown types open without a prompt. It is replaced by
is_safe_to_open_file_name, a list of plain document, image, model and
video types that open directly. Everything else asks first.
2026-09-30 00:39:30 +08:00
HanifKoh e40030cf81 Stop Malformed Network Responses from Crashing the App (#15947)
* Stop Malformed Network Responses from Crashing the App

Duet, MKS and UltiMaker parsed print host replies with boost read_json
inside the HTTP completion callback with no try, so an HTML or truncated
reply threw out of the Physical Printer Test button and terminated the app,
or killed the upload queue thread. The five identical copies of the parser
(ESP3D's and Flashforge's were unused) are replaced by one shared
PrintHost::get_err_code_from_body that reports a non-JSON reply as an error.
The upload queue now catches a failing job per job, so one bad upload no
longer leaves later jobs queued forever.

Flashforge read material station slots with nlohmann value(), which throws
on off-type fields or non-object entries. The parsing moves into
Flashforge::parse_material_slots, which reads fields leniently with the
existing try_parse_json_int and skips bad entries.

UserManager::parse_json parsed the payload before its try block; the parse
now happens inside it.

* Keep UploadFinished Paired with UploadStarted When an Upload Throws

The exception from a throwing upload was caught around perform_job, so
the UploadFinished lifecycle event was skipped and plugins saw an
upload start that never finished.

The catch now sits around the upload call. The error is reported
through the job's error callback and UploadFinished is fired with an
error code, as for any other failed upload. The worker keeps running
for the next job. The started, upload and finished sequence moved to
PrintHostJobQueue::upload_job so it can be tested without the dialog.
2026-09-30 00:39:01 +08:00
HanifKoh ba468c842d Confine Updater and Plugin Archive Extraction to the Target Directory (#15957)
* Confine Updater Archive Extraction to the Target Directory

The preset updater extracted downloaded archives by appending each entry
name to the cache directory, and the network plugin installer did the same
for the plugin folder, without checking that the result stays inside it.

Move the updater's extraction into libslic3r as extract_archive_confined,
which validates every entry with is_path_within_root before writing
anything and fails the whole archive if one entry resolves outside the
target. The plugin installer now rejects such an entry the same way. Well
formed archives extract exactly as before.

* Harden Archive Extraction Against Symlinks

The plugin installer now creates a symlink entry only when its target is
relative and, joined to the link's own directory, passes
is_path_within_root, via the new is_symlink_target_within_root helper.
Before writing any entry it checks the destination with symlink_status, so
an existing symlink, dangling or not, is replaced rather than followed, and
it creates parent directories inside the existing error handling.
extract_archive_confined replaces a symlink at a destination file the same
way.

is_path_within_root now ignores a trailing separator on the root, which
previously made every path fail the check.

* Validate Plugin Symlink Targets Before Replacing Existing Files

A symlink entry's target is now read and checked before anything already
at its destination is removed or renamed aside, so an archive rejected
for its link target leaves the installed plugin files in place.

* Reject Paths with an Embedded NUL When Confining Extraction

is_path_within_root compared each component with "..", so a name such
as "..\0" passed the check. The filesystem calls stop at the NUL and
act on a shorter path than the one that was checked: a symlink target
read from a plugin archive as raw bytes was created as "..", pointing
out of the plugin directory.

A path containing a NUL is now rejected before anything touches the
filesystem, which covers every caller, including entry names taken from
the Unicode Path extra field.
2026-09-29 23:40:12 +08:00
SoftFever dd9b5dc0d4 Tune pressure advance separately for each extruder variant (#15986)
* Tune pressure advance separately for each extruder variant

Pressure advance, adaptive pressure advance and its model can now take a
different value for each extruder variant of a filament, such as Standard and
High Flow nozzles, like the other per-variant filament settings. Projects
saved with one value per filament apply it to every variant of that filament,
and the addnorth BBL filaments in the Orca Filament Library are updated to the
per-variant layout.

* Move Nozzle type to each extruder's settings page

Editing other settings on an Extruder page of a single-extruder
multi-material printer no longer triggers the nozzle diameter prompt.

* Fix command-line slicing when a filament leaves out a per-variant setting
2026-09-29 23:23:08 +08:00
HanifKoh e68694dbaf Percent-Encode Local File URLs for Embedded Web Pages (#15961)
* Percent-Encode Local File URLs for Embedded Web Pages

The Home tab, setup wizard, Project tab and other embedded pages were
loaded from file:// URLs built by pasting the resources path into a
string. A '#', '%' or '?' in the install path was then read as a URL
fragment, escape or query, so the pages failed to load, for example a
portable install under D:\#OneDrive showed a directory listing instead
of the setup wizard.

Add file_url_from_path(), built on wxFileSystem::FileNameToURL, and use
it wherever a local page or image URL is built from a path. Queries such
as ?lang= are appended after the path is encoded. The wizard's printer
cover images are passed to the page as file URLs too.

* Encode the Login Error Page URL and Cover More Windows Path Forms

The login dialog's error page was still loaded from a raw resources path;
it now uses file_url_from_path like the other local pages.

The Windows file URL tests now also cover a resources path joined with a
forward-slash relative path, as the callers build them, and a UNC path.

* Build the Flush Dialog Page URLs with the Shared Helper

WipingDialog and NozzleListTable still called
wxFileSystem::FileNameToURL directly. They now go through
file_url_from_path like every other local page, so the URLs are built
in one place.

Adds a test for a resources directory with a '#' in its name, which the
plugin page check did not recognise before.
2026-09-29 22:30:56 +08:00
SoftFever 1504bd7153 Fix command-line slicing when a filament leaves out a per-variant setting 2026-09-29 21:09:20 +08:00
SoftFever d87fe1b290 Merge branch 'main' into feature/pa_per_extruder_variant 2026-09-29 19:15:17 +08:00
Kris Austin 72cfe71b81 fix: link webkit2gtk and X11 on every Linux build, not only Flatpak (#15972)
libslic3r_gui calls webkit_* directly, and OrcaSlicer.cpp and libspnav
call Xlib, but both libraries were only linked when FLATPAK was set.
The default build links because the bundled static wxWidgets lists
them in wx-config. A shared wxWidgets does not, so any build against
one, like the Flatpak build or a distro package, fails with undefined
webkit_* and X* symbols.
2026-09-29 08:04:48 -03:00
SoftFever 50eea48408 Move Nozzle type to each extruder's settings page
Editing other settings on an Extruder page of a single-extruder
multi-material printer no longer triggers the nozzle diameter prompt.
2026-09-29 17:52:19 +08:00
SoftFever ef0c656932 Tune pressure advance separately for each extruder variant
Pressure advance, adaptive pressure advance and its model can now take a
different value for each extruder variant of a filament, such as Standard and
High Flow nozzles, like the other per-variant filament settings. Projects
saved with one value per filament apply it to every variant of that filament,
and the addnorth BBL filaments in the Orca Filament Library are updated to the
per-variant layout.
2026-09-29 17:52:13 +08:00
115 changed files with 3805 additions and 1309 deletions
@@ -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 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 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 that width, or leave it out. Keys outside the set
(`filament_type`, plate temperatures, `fan_max_speed`, `slow_down_min_speed`, `pressure_advance`, (`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
…) are never addressed by variant index; the preset contributes their first value however wide a
file writes them. file writes them.
3. Cover every variant the material is meant to print on across its `compatible_printers`. Leave out 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 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. 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 `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): before relying on them (the recipe strips comments, since an initializer can carry a commented-out entry):
```bash ```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` **`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): **`printer_extruder_options`**, machine, one value per extruder, not a variant set (8):
`default_nozzle_volume_type`, `extruder_max_nozzle_count`, `extruder_printable_area`, `default_nozzle_volume_type`, `extruder_max_nozzle_count`, `extruder_printable_area`,
@@ -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. required direction of change, from diameter alone.
On a printer with extruder variants, a filament tunes these per variant too: 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 `filament_max_volumetric_speed`, `filament_flow_ratio`, `nozzle_temperature`, pressure advance and the
overrides carry one value per variant of `filament_extruder_variant` (Standard, High Flow, …). 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); 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 setting does not differ per variant, and measure the High Flow variant rather than copying Standard
([extruder-variants.md](extruder-variants.md#filament)). ([extruder-variants.md](extruder-variants.md#filament)).
+45 -19
View File
@@ -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 - 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 vendors keep theirs — even when the bumped vendor is the shared Orca filament
library everyone else inherits from. library everyone else inherits from.
- The setup wizard, which loads vendors one at a time, gets the same speedup as - The setup wizard loads its vendors through the same routine as startup, so it
startup without a second code path. 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 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 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 Vendors load in a fixed order, because filament inheritance crosses exactly one
boundary: any vendor's filament may inherit from the shared Orca filament library, boundary: any vendor's filament may inherit from the shared Orca filament library,
and nothing else reaches across vendors — an `include` is always vendor-local. The and nothing else reaches across vendors — an `include` is always vendor-local. Only
library therefore goes first, alone; every other vendor follows in parallel, resolving installing a vendor's presets crosses it; reading the vendor, from its cache or its
against it; and the results are merged in a stable order: 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 ```mermaid
flowchart LR flowchart LR
lib["1 · OrcaFilamentLibrary<br/>loaded first, synchronously"] --> par["2 · every other vendor in parallel,<br/>each into its own bundle, filaments<br/>resolving against the loaded library"] --> merge["3 · bundles merged into one,<br/>sequentially, in stable vendor order"] lib["1 · OrcaFilamentLibrary loaded;<br/>meanwhile every other vendor read<br/>from its cache or its JSONs"] --> par["2 · every other vendor installed<br/>in parallel, each into its own bundle,<br/>filaments resolving against the library"] --> merge["3 · bundles merged into one,<br/>in one pass per collection,<br/>in stable vendor order"]
``` ```
`PresetBundle::load_vendors` runs these steps for startup and for the setup wizard,
which hand it the vendors to load and the directory each is installed in.
Whether a vendor comes from its cache or from a parse changes nothing in that 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 order — both produce the same bundle, so cached and parsed vendors mix freely in
one startup. one startup.
@@ -211,11 +216,12 @@ shipped cache answered first, so the profile in `<data_dir>/system/` was never p
and its cache was never written back. and its cache was never written back.
Serving from a cache is not a memory-image restore. The entries are deserialized and Serving from a cache is not a memory-image restore. The entries are deserialized and
then installed one by one — inheritance resolved against the presets installed before then installed by `install_vendor`, the routine the JSON path hands the vendor's entries
them and the currently loaded filament library, includes layered in, configs flattened to once it has parsed the sub-files: inheritance resolved against the presets installed
onto the collection defaults, validated and registered — by the same function the JSON before them and the currently loaded filament library, includes layered in, configs
path calls straight after parsing a sub-file. An `include` layers what the included flattened onto the collection defaults, validated and registered. An `include` layers
base states, between the parent and the preset's own keys: the base's diff against the 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 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 `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 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 Installation also rebuilds each preset's file path from the local data directory, so a
shipped cache never carries the generating machine's paths. 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 App upgrades work because a cache normally survives one. Only a deliberate
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at `CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
install time rather than at load: a vendor whose cache this build cannot read counts 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 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. 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 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 per-vendor caches costs over a second of preset installation whatever feeds it, so
skipping the rebuild entirely wins. only skipping the rebuild entirely wins.
Any change to the set — a vendor added, removed or updated, or its cache-only 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; `.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 the wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load
they cover) and writes the catalog back. Selections, region and per-open decorations startup uses (per-vendor caches serving where they cover), and writes the catalog
are applied downstream of the cache either way, so a served catalog is back. When a vendor fails to load, the filament library included, that open falls
indistinguishable from a rebuilt one. Nothing ships this file and the updater never back to the wizard's own scan of the vendor JSONs, as when no bundle can be built,
touches it; it is a locally written artifact, re-derived whenever stale, written and writes nothing. Selections, region and per-open decorations are applied
through a temp file and rename so half a cache is never readable. downstream of the cache either way, so a served catalog is indistinguishable from a
rebuilt one. Nothing ships this file and the updater never touches it; it is a
locally written artifact, re-derived whenever stale, written through a temp file and
rename so half a cache is never readable.
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor scans `<data_dir>/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 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 `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. 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 - **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. 65535 options and one cache at most 65535 distinct enum value names.
`CacheDictionary::save` throws past that, which surfaces when CI generates the `CacheDictionary::save` throws past that, which surfaces when CI generates the
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "OrcaFilamentLibrary", "name": "OrcaFilamentLibrary",
"version": "02.04.00.15", "version": "02.04.00.16",
"force_update": "0", "force_update": "0",
"description": "Orca Filament Library", "description": "Orca Filament Library",
"filament_list": [ "filament_list": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"1" "1"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"0" "0"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"0" "0"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
@@ -111,6 +111,7 @@
"1" "1"
], ],
"enable_pressure_advance": [ "enable_pressure_advance": [
"0",
"0" "0"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
@@ -316,6 +317,7 @@
"2" "2"
], ],
"pressure_advance": [ "pressure_advance": [
"0.02",
"0.02" "0.02"
], ],
"reduce_fan_stop_start_freq": [ "reduce_fan_stop_start_freq": [
+129 -30
View File
@@ -214,9 +214,9 @@ function ShowModelInfo( pModel )
SendWXDebugInfo("Model Name: "+sModelName); SendWXDebugInfo("Model Name: "+sModelName);
$('#ModelName').html(sModelName); $('#ModelName').text(sModelName);
$('#ModelName').attr('title',sModelName); $('#ModelName').attr('title',sModelName);
$('#ModelAuthorName').html(sModelAuthor); $('#ModelAuthorName').text(sModelAuthor);
switch(UploadType) switch(UploadType)
{ {
@@ -268,7 +268,7 @@ function ShowModelInfo( pModel )
break; break;
} }
$('#Model_Desc').html( html_decode(sModelDesc) ); $('#Model_Desc').empty().append( SanitizeDescHtml( html_decode(sModelDesc) ) );
let ModelPreviewList=pModel.preview_img; let ModelPreviewList=pModel.preview_img;
let TotalPreview=ModelPreviewList.length; let TotalPreview=ModelPreviewList.length;
@@ -281,16 +281,15 @@ function ShowModelInfo( pModel )
if(TotalPreview>0) if(TotalPreview>0)
{ {
let htmlPreview=''; $('#ModelPreviewList').empty();
for(let pn=0;pn<TotalPreview;pn++) for(let pn=0;pn<TotalPreview;pn++)
{ {
//let FTmpPath=decodeURIComponent(ModelPreviewList[pn]); //let FTmpPath=decodeURIComponent(ModelPreviewList[pn]);
let FTmpPath=ModelPreviewList[pn]['filepath']; let FTmpPath=ModelPreviewList[pn]['filepath'];
htmlPreview+='<div class="swiper-slide"><img class="Model_PrevImg" src="'+FTmpPath+'" /></div>'; $('#ModelPreviewList').append( $('<div class="swiper-slide"></div>').append( $('<img class="Model_PrevImg" />').attr('src',FTmpPath) ) );
} }
$('#ModelPreviewList').html(htmlPreview);
$('#Model_Preview_Image').viewer({ $('#Model_Preview_Image').viewer({
title: false, title: false,
fullsreen: false, fullsreen: false,
@@ -410,7 +409,8 @@ function ConstructFileHtml( ID, pItem )
{ {
let fTotal=pItem.length; let fTotal=pItem.length;
let strHtml=''; let pBoard=$('#'+ID+' .FileListBoard');
pBoard.empty();
for( let f=0;f<fTotal;f++ ) for( let f=0;f<fTotal;f++ )
{ {
let pOne=pItem[f]; let pOne=pItem[f];
@@ -443,39 +443,139 @@ function ConstructFileHtml( ID, pItem )
ImgPath='img/default.png'; ImgPath='img/default.png';
} }
//Add html //Add html. File names come from the 3MF, so build the nodes rather than concatenating markup.
let pIconImg=$('<img />').attr('src',ImgPath);
let pMenu=$('<div class="FileMenu"><img src="img/s.svg" /></div>');
if( strClass!='ImageIcon' ) if( strClass!='ImageIcon' )
{ {
strHtml+='<div class="FileItem">'+ pMenu.on('click', function(){ OnClickOpenFile(tPath); });
' <div class="'+strClass+'"><img src="'+ImgPath+'" /></div>'+
' <div class="FileText">'+
' <div class="FileName">'+tName+'</div>'+
' </div>'+
' <div class="FileMenu" onClick="OnClickOpenFile(\''+tPath+'\')"><img src="img/s.svg" /></div>'+
'</div>';
} }
else else
{ {
ImgID++; ImgID++;
let TmpImgID="AF"+ImgID; let TmpImgID="AF"+ImgID;
strHtml+='<div class="FileItem">'+ pIconImg.attr('id',TmpImgID);
' <div class="'+strClass+'"><img id="'+TmpImgID+'" src="'+ImgPath+'" /></div>'+ pMenu.on('click', function(){ OnClickOpenImage(TmpImgID); });
' <div class="FileText">'+
' <div class="FileName">'+tName+'</div>'+
' </div>'+
' <div class="FileMenu" onClick="OnClickOpenImage(\''+TmpImgID+'\')"><img src="img/s.svg" /></div>'+
'</div>';
} }
}
$('#'+ID+' .FileListBoard').html(strHtml); let pFileItem=$('<div class="FileItem"></div>');
pFileItem.append( $('<div></div>').addClass(strClass).append(pIconImg) );
pFileItem.append( $('<div class="FileText"></div>').append( $('<div class="FileName"></div>').text(tName).attr('title',tName) ) );
pFileItem.append( pMenu );
pBoard.append( pFileItem );
}
if( fTotal>0 ) if( fTotal>0 )
$('#'+ID).show(); $('#'+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 ) function ShowProfilelInfo( pProfile )
{ {
//==========Profile Info========== //==========Profile Info==========
@@ -483,10 +583,10 @@ function ShowProfilelInfo( pProfile )
let sProfileAuthor=decodeURIComponent(pProfile.author); let sProfileAuthor=decodeURIComponent(pProfile.author);
let sProfileDesc=decodeURIComponent(pProfile.description); let sProfileDesc=decodeURIComponent(pProfile.description);
$('#ProfileName').html(sProfileName); $('#ProfileName').text(sProfileName);
$('#ProfileAuthor').html(sProfileAuthor); $('#ProfileAuthor').text(sProfileAuthor);
$('#Profile_Desc').html( html_decode(sProfileDesc) ); $('#Profile_Desc').empty().append( SanitizeDescHtml( html_decode(sProfileDesc) ) );
let ProfilePreviewList=pProfile.preview_img; let ProfilePreviewList=pProfile.preview_img;
let TotalPreview=ProfilePreviewList.length; let TotalPreview=ProfilePreviewList.length;
@@ -499,15 +599,14 @@ function ShowProfilelInfo( pProfile )
if(TotalPreview>0) if(TotalPreview>0)
{ {
let htmlPreview=''; $('#ProfilePreviewList').empty();
for(let pn=0;pn<TotalPreview;pn++) for(let pn=0;pn<TotalPreview;pn++)
{ {
let FTmpPath=ProfilePreviewList[pn]['filepath']; let FTmpPath=ProfilePreviewList[pn]['filepath'];
htmlPreview+='<div class="swiper-slide"><img class="Model_PrevImg" src="'+FTmpPath+'" /></div>'; $('#ProfilePreviewList').append( $('<div class="swiper-slide"></div>').append( $('<img class="Model_PrevImg" />').attr('src',FTmpPath) ) );
} }
$('#ProfilePreviewList').html(htmlPreview);
$('#Profile_Preview_Image').viewer({ $('#Profile_Preview_Image').viewer({
title: false, title: false,
fullsreen: false, fullsreen: false,
+30
View File
@@ -3476,6 +3476,14 @@ int CLI::run(int argc, char **argv)
} }
new_variant_counts = old_variant_counts; new_variant_counts = old_variant_counts;
//filament_variant_count = 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++) { for (int index = 0; index < load_filaments_config.size(); index++) {
DynamicPrintConfig& config = load_filaments_config[index]; DynamicPrintConfig& config = load_filaments_config[index];
int filament_index = load_filaments_index[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<int> 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<ConfigOptionVectorBase *>(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<ConfigOption> old_values(opt_vec_dst->clone());
opt_vec_dst->set_with_restore_2(static_cast<ConfigOptionVectorBase *>(old_values.get()), kept_variant_indice, old_start,
old_variant_count, true);
}
//update the old index //update the old index
if (old_variant_count != new_variant_count) if (old_variant_count != new_variant_count)
{ {
@@ -4081,6 +4108,9 @@ int CLI::run(int argc, char **argv)
if (printer_technology == ptFFF) { if (printer_technology == ptFFF) {
fff_print_config.apply(m_print_config, true); fff_print_config.apply(m_print_config, true);
m_print_config.apply(fff_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 { } else {
boost::nowide::cerr << "invalid printer_technology " << std::endl; 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); record_exit_reson(outfile_dir, CLI_INVALID_PRINTER_TECH, 0, cli_errors[CLI_INVALID_PRINTER_TECH], sliced_info);
+4
View File
@@ -511,6 +511,10 @@ void AppConfig::set_defaults()
set_bool("enable_high_low_temp_mixed_printing", false); 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()){ if (get("ignore_ext_filament_in_filament_map").empty()){
set_bool("ignore_ext_filament_in_filament_map", false); set_bool("ignore_ext_filament_in_filament_map", false);
} }
+1
View File
@@ -355,6 +355,7 @@ set(lisbslic3r_sources
Optimize/Optimizer.hpp Optimize/Optimizer.hpp
Orient.cpp Orient.cpp
Orient.hpp Orient.hpp
ParallelResolve.hpp
ParameterUtils.cpp ParameterUtils.cpp
ParameterUtils.hpp ParameterUtils.hpp
pchheader.cpp pchheader.cpp
+26 -13
View File
@@ -849,6 +849,19 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC
return std::move(substitutions_ctxt.substitutions); 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<std::string, std::string>& key_values, std::string& reason) int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
{ {
json j; json j;
@@ -916,44 +929,44 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
} }
//parse the json elements //parse the json elements
for (auto it = j.begin(); it != j.end(); it++) { 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()); 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 //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()); key_values.emplace(BBL_JSON_KEY_NAME, it.value());
if (it.value() == "project_settings") if (it.value() == "project_settings")
is_project_settings = true; 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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()); key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
} else { } else {
t_config_option_key opt_key = it.key(); t_config_option_key opt_key = it.key();
+12 -8
View File
@@ -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]); its.indices.emplace_back(indices[0], indices[1], indices[2]);
int face_index =its.indices.size() - 1; int face_index =its.indices.size() - 1;
RGBA face_color; 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<int, 3> &corners) {
if (mtl_data.new_mtl_unmap.find(mtl_name) != mtl_data.new_mtl_unmap.end()) { if (mtl_data.new_mtl_unmap.find(mtl_name) != mtl_data.new_mtl_unmap.end()) {
bool is_merge_ka_kd = true; bool is_merge_ka_kd = true;
for (size_t n = 0; n < 3; n++) { 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::Zero();
return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]); return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]);
}; };
std::array<Vec2f, 3> uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])}; std::array<Vec2f, 3> 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.uvs.emplace_back(uv_array);
} }
obj_info.face_colors.emplace_back(face_color); 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<int, 3> &corners) {
if (data.usemtls.size() == 1) { 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 { } else {
for (size_t k = 0; k < data.usemtls.size(); k++) { for (size_t k = 0; k < data.usemtls.size(); k++) {
auto mtl = data.usemtls[k]; auto mtl = data.usemtls[k];
if (face_index >= mtl.face_start && face_index <= mtl.face_end) { 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; break;
} }
} }
} }
}; };
if (exist_mtl) { if (exist_mtl) {
set_face_color_by_mtl(face_index); set_face_color_by_mtl(face_index, {0, 1, 2});
} }
if (cnt == 4) { if (cnt == 4) {
its.indices.emplace_back(indices[0], indices[2], indices[3]); its.indices.emplace_back(indices[0], indices[2], indices[3]);
int face_index = its.indices.size() - 1; int face_index = its.indices.size() - 1;
if (exist_mtl) { 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."); message = _L("This OBJ file couldn't be read because it's empty.");
return false; return false;
} }
if (meshptr->volume() < 0) if (meshptr->volume() < 0) {
meshptr->flip_triangles(); meshptr->flip_triangles();
// Flipping swaps corners 1 and 2 of every face, so the UVs have to follow.
for (std::array<Vec2f, 3> &uv : obj_info.uvs)
std::swap(uv[1], uv[2]);
}
// Hand the parsed material table back so callers can build a TexturedMesh from it. // Hand the parsed material table back so callers can build a TexturedMesh from it.
if (out_mtl) if (out_mtl)
*out_mtl = mtl_data; *out_mtl = mtl_data;
+71 -89
View File
@@ -1497,12 +1497,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
check_add_eol(toolchange_gcode_str); check_add_eol(toolchange_gcode_str);
// SoftFever: set new PA for new filament // SoftFever: set new PA for new filament
if (gcodegen.config().enable_pressure_advance.get_at(new_filament_id)) { gcode += gcodegen.set_filament_pressure_advance(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));
}
// A phony move to the end position at the wipe tower. // A phony move to the end position at the wipe tower.
gcodegen.writer().travel_to_xy((end_pos + plate_origin_2d).cast<double>()); gcodegen.writer().travel_to_xy((end_pos + plate_origin_2d).cast<double>());
@@ -1826,12 +1821,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
check_add_eol(toolchange_gcode_str); check_add_eol(toolchange_gcode_str);
// SoftFever: set new PA for new filament // SoftFever: set new PA for new filament
if (new_extruder_id != -1 && gcodegen.config().enable_pressure_advance.get_at(new_extruder_id)) { if (new_extruder_id != -1)
gcode += gcodegen.writer().set_pressure_advance(gcodegen.config().pressure_advance.get_at(new_extruder_id)); gcode += gcodegen.set_filament_pressure_advance(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));
}
// A phony move to the end position at the wipe tower. // A phony move to the end position at the wipe tower.
gcodegen.writer().travel_to_xy((end_pos + plate_origin_2d).cast<double>()); gcodegen.writer().travel_to_xy((end_pos + plate_origin_2d).cast<double>());
@@ -3232,7 +3223,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_cooling_buffer->set_current_extruder(initial_extruder_id, extruder_id); m_cooling_buffer->set_current_extruder(initial_extruder_id, extruder_id);
// Orca: Initialise AdaptivePA processor filter // Orca: Initialise AdaptivePA processor filter
m_pa_processor = std::make_unique<AdaptivePAProcessor>(*this, tool_ordering.all_extruders()); m_pa_processor = std::make_unique<AdaptivePAProcessor>(*this);
// Update output variables after the extruders were initialized. // Update output variables after the extruders were initialized.
m_placeholder_parser_integration.init(m_writer); 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 // Orca: add missing PA settings for initial filament
if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) { file.write(set_filament_pressure_advance(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));
}
} }
//flush FanMover buffer to avoid modifying the start gcode if it's manual. //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(); 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. // Get filament stats.
file.write(DoExport::update_print_stats_and_format_filament_stats( file.write(DoExport::update_print_stats_and_format_filament_stats(
// Const inputs // Const inputs
@@ -4214,26 +4222,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder) GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder)
.c_str()); .c_str());
file.write("\n"); 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"); file.write("\n");
@@ -8390,12 +8378,16 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
// Orca: Dynamic PA // Orca: Dynamic PA
// If adaptive PA is enabled, by default evaluate PA on all extrusion moves // 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 || 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_Pattern ||
m_curr_print->calib_mode() == CalibMode::Calib_PA_Tower; m_curr_print->calib_mode() == CalibMode::Calib_PA_Tower;
bool evaluate_adaptive_pa = false; bool evaluate_adaptive_pa = false;
bool role_change = (m_last_extrusion_role != path.role()); 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; evaluate_adaptive_pa = true;
// If we have already emmited a PA change because the m_multi_flow_segment_path_pa_set is set // 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. // 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 // Post processor flag generation code segment when option to emit only at role changes is enabled
// Variables published to the post processor: // 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) // 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) // 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) // 4) the current acceleration (to pass to the model for evaluation)
// 5) whether this is an external perimeter (for future use) // 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) { 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", sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), 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, m_multi_flow_segment_path_average_mm3_per_mm,
acceleration_i, acceleration_i,
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), ((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 // 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", sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
m_writer.filament()->id(), pa_config_index,
_mm3_per_mm, _mm3_per_mm,
acceleration_i, acceleration_i,
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), ((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 // 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. // 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 // Dont emit tag if it has just already been emitted from a role change above
if(_mm3_per_mm >0 && if(_mm3_per_mm >0 && adaptive_pa_overhangs && !evaluate_adaptive_pa){
FILAMENT_CONFIG(adaptive_pressure_advance) &&
FILAMENT_CONFIG(enable_pressure_advance) &&
FILAMENT_CONFIG(adaptive_pressure_advance_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(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){ if(m_config.gcode_comments){
sprintf(buf, "; Ramp down-non-variable\n"); 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", sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
m_writer.filament()->id(), pa_config_index,
_mm3_per_mm, _mm3_per_mm,
acceleration_i, acceleration_i,
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), ((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", sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
m_writer.filament()->id(), pa_config_index,
_mm3_per_mm, _mm3_per_mm,
acceleration_i, acceleration_i,
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), ((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 // 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 // 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. // Emit tag before new speed is set so the post processor reads the next speed immediately and uses it.
if(_mm3_per_mm >0 && if(_mm3_per_mm >0 && adaptive_pa_overhangs){
EXTRUDER_CONFIG(adaptive_pressure_advance) &&
EXTRUDER_CONFIG(enable_pressure_advance) &&
EXTRUDER_CONFIG(adaptive_pressure_advance_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(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) { if(m_config.gcode_comments) {
sprintf(buf, "; Ramp up-variable\n"); 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", sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
m_writer.filament()->id(), pa_config_index,
_mm3_per_mm, _mm3_per_mm,
acceleration_i, acceleration_i,
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), ((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", sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n",
GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(),
m_writer.filament()->id(), pa_config_index,
_mm3_per_mm, _mm3_per_mm,
acceleration_i, acceleration_i,
((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)),
@@ -9422,33 +9407,29 @@ void GCode::update_placeholder_parser_with_variant_params()
if (num_filaments == 0) if (num_filaments == 0)
return; 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. // After remapping, gcode templates can use param[filament_id] directly.
auto remap_floats_by_filament = [&](const auto &src) { auto remap_by_filament = [&](const auto &src) {
std::vector<double> dst(num_filaments); std::decay_t<decltype(src.values)> 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<int> dst(num_filaments);
for (size_t i = 0; i < num_filaments; ++i) for (size_t i = 0; i < num_filaments; ++i)
dst[i] = src.get_at(get_filament_config_index(i)); dst[i] = src.get_at(get_filament_config_index(i));
return dst; return dst;
}; };
// --- filament_options_with_variant: gcode indexes by filament_id --- // --- 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_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_ints_by_filament(m_config.filament_pre_cooling_temperature))); 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_ints_by_filament(m_config.filament_pre_cooling_temperature_nc))); 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_floats_by_filament(m_config.filament_cooling_before_tower))); 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_ints_by_filament(m_config.nozzle_temperature_initial_layer))); 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_ints_by_filament(m_config.nozzle_temperature))); 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 // 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 --- // --- 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 // 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 --- // --- 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. // Fast purge mode uses filament_flush_temp_fast; Default is inert.
bool use_fast_flush = m_config.prime_volume_mode == PrimeVolumeMode::pvmFast; 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 flush_v_speed = remap_by_filament(m_config.filament_flush_volumetric_speed);
auto filament_max_v = remap_floats_by_filament(m_config.filament_max_volumetric_speed); auto filament_max_v = remap_by_filament(m_config.filament_max_volumetric_speed);
auto flush_temps = remap_ints_by_filament(use_fast_flush ? m_config.filament_flush_temp_fast auto flush_temps = remap_by_filament(use_fast_flush ? m_config.filament_flush_temp_fast
: m_config.filament_flush_temp); : m_config.filament_flush_temp);
for (size_t i = 0; i < num_filaments; ++i) { for (size_t i = 0; i < num_filaments; ++i) {
if (flush_v_speed[i] == 0) if (flush_v_speed[i] == 0)
flush_v_speed[i] = filament_max_v[i]; 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); gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config);
check_add_eol(gcode); check_add_eol(gcode);
} }
if (m_config.enable_pressure_advance.get_at(new_filament_id)) { gcode += set_filament_pressure_advance(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 += m_writer.toolchange(new_filament_id, new_extruder_id); gcode += m_writer.toolchange(new_filament_id, new_extruder_id);
if (Extruder *fil = m_writer.filament()) 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) if (m_ooze_prevention.enable && !defer_temp_wait)
gcode += m_ooze_prevention.post_toolchange(*this); gcode += m_ooze_prevention.post_toolchange(*this);
if (m_config.enable_pressure_advance.get_at(new_filament_id)) { gcode += set_filament_pressure_advance(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));
}
//Orca: tool changer or IDEX's firmware may change Z position, so we set it to unknown/undefined //Orca: tool changer or IDEX's firmware may change Z position, so we set it to unknown/undefined
m_last_pos_defined = false; m_last_pos_defined = false;
return gcode; 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) { inline std::string polygon_to_string(const Polygon &polygon, Print *print, bool is_print_space = false) {
std::ostringstream gcode; std::ostringstream gcode;
gcode << "["; gcode << "[";
+2
View File
@@ -281,6 +281,8 @@ public:
// extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract. // 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 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); 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(); bool is_BBL_Printer();
WipeTowerType wipe_tower_type(); WipeTowerType wipe_tower_type();
+26 -33
View File
@@ -21,38 +21,30 @@ namespace Slic3r {
* *
* @param gcodegen A reference to the GCode object that generates the G-code. * @param gcodegen A reference to the GCode object that generates the G-code.
*/ */
AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen, const std::vector<unsigned int> &tools_used) AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen)
: m_gcodegen(gcodegen), : m_gcodegen(gcodegen),
m_config(gcodegen.config()), m_config(gcodegen.config()),
m_last_predicted_pa(0.0), m_last_predicted_pa(0.0),
m_max_next_feedrate(0.0), m_max_next_feedrate(0.0),
m_next_feedrate(0.0), m_next_feedrate(0.0),
m_current_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_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]+))") 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<AdaptivePAInterpolator>();
// 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 // Method to get the interpolator for a specific filament config index.
AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int tool_id) { // The model is built the first time an index is requested, as the indices in use depend on
auto it = m_AdaptivePAInterpolators.find(tool_id); // the extruder variant each filament prints with.
if (it != m_AdaptivePAInterpolators.end()) { AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int config_index) {
return it->second.get(); 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<AdaptivePAInterpolator>();
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. // 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 (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)) { 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()); mm3mm_value = std::stod(m_match[2].str());
accel_value = std::stod(m_match[3].str()); accel_value = std::stod(m_match[3].str());
int isBridge = std::stoi(m_match[4].str()); int isBridge = std::stoi(m_match[4].str());
int roleChange = std::stoi(m_match[5].str()); int roleChange = std::stoi(m_match[5].str());
int isOverhang = std::stoi(m_match[6].str()); int isOverhang = std::stoi(m_match[6].str());
// Check if the extruder ID has changed // Check if the filament config index has changed
bool extruder_changed = (extruder_id != m_last_extruder_id); bool config_index_changed = (config_index != m_last_config_index);
m_last_extruder_id = extruder_id; m_last_config_index = config_index;
// Save the PA_CHANGE line to output later after finding feedrate // Save the PA_CHANGE line to output later after finding feedrate
pa_change_line = line; 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 // Calculate the predicted PA using the upcomming feature maximum feedrate
// Get the interpolator for the active tool // 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 predicted_pa = 0;
double adaptive_PA_speed = 0; double adaptive_PA_speed = 0;
if(!interpolator){ // Tool not found in the interpolator map if(!interpolator){ // Tool not found in the interpolator map
// Tool not found in the PA interpolator to tool 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"; 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 // 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 // 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 // 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. // however check for robustness sake.
{ {
// Model failed or adaptive pressure advance not enabled - use default value from m_config // 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"; if(m_config.gcode_comments) output << "; APA: Interpolator setup failed, using default pressure advance\n";
} else { // Model setup succeeded } else { // Model setup succeeded
// Proceed to identify the print speed to use to calculate the adaptive PA value // 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); predicted_pa = (*interpolator)(mm3mm_value * adaptive_PA_speed, accel_value);
// This is a bridge, use the dedicated PA setting. // This is a bridge, use the dedicated PA setting.
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)
predicted_pa = m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id); 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. 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 << "; APA: Interpolation failed, using fallback pressure advance value\n";
} }
} }
if(m_config.gcode_comments) { if(m_config.gcode_comments) {
// Output debug GCode comments // Output debug GCode comments
output << pa_change_line << '\n'; // Output PA change command tag 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 Model Override (bridge)\n";
output << "; APA Current Speed: " << std::to_string(m_current_feedrate) << "\n"; output << "; APA Current Speed: " << std::to_string(m_current_feedrate) << "\n";
output << "; APA Next Speed: " << std::to_string(m_next_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 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"; 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 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 m_last_predicted_pa = predicted_pa; // Update the last predicted PA value
} }
+7 -11
View File
@@ -11,7 +11,6 @@
#include <regex> #include <regex>
#include <memory> #include <memory>
#include <map> #include <map>
#include <vector>
#include "AdaptivePAInterpolator.hpp" #include "AdaptivePAInterpolator.hpp"
namespace Slic3r { namespace Slic3r {
@@ -33,7 +32,7 @@ public:
* *
* @param gcodegen A reference to the GCode object that generates the G-code. * @param gcodegen A reference to the GCode object that generates the G-code.
*/ */
AdaptivePAProcessor(GCode &gcodegen, const std::vector<unsigned int> &tools_used); AdaptivePAProcessor(GCode &gcodegen);
/** /**
* @brief Processes a layer of G-code and applies adaptive pressure advance. * @brief Processes a layer of G-code and applies adaptive pressure advance.
@@ -70,28 +69,25 @@ public:
private: private:
GCode &m_gcodegen; ///< Reference to the GCode object. GCode &m_gcodegen; ///< Reference to the GCode object.
std::unordered_map<unsigned int, std::unique_ptr<AdaptivePAInterpolator>> m_AdaptivePAInterpolators; ///< Map between Interpolator objects and tool ID's std::unordered_map<unsigned int, std::unique_ptr<AdaptivePAInterpolator>> 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. const PrintConfig &m_config; ///< Reference to the print configuration.
double m_last_predicted_pa; ///< Last predicted pressure advance value. 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_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_next_feedrate; ///< First feed rate (speed) for the upcomming island.
double m_current_feedrate; ///< Current, latest feedrate. 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_pa_change_pattern; ///< Regular expression to detect PA_CHANGE pattern.
std::regex m_g1_f_pattern; ///< Regular expression to detect G1 F pattern. std::regex m_g1_f_pattern; ///< Regular expression to detect G1 F pattern.
std::smatch m_match; ///< Match results for regular expressions. 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. * @param config_index The filament config index (one per filament and extruder variant) for which the PA interpolation model is to be returned.
* Call this when changing tools or in any other case where the internally assumed last PA value may be incorrect * @return The Adaptive PA Interpolator object corresponding to that index, or nullptr when adaptive PA is off for it.
*
* @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.
*/ */
AdaptivePAInterpolator* getInterpolator(unsigned int tool_id); AdaptivePAInterpolator* getInterpolator(unsigned int config_index);
}; };
} // namespace Slic3r } // namespace Slic3r
+16
View File
@@ -13,8 +13,19 @@
namespace Slic3r { 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() 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 #ifdef _WIN32
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE); _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr); 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); m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale);
uselocale(m_new_locale); uselocale(m_new_locale);
#endif #endif
// Counted last, since the destructor does not run for a constructor that throws.
++ s_numeric_locale_depth;
} }
CNumericLocalesSetter::~CNumericLocalesSetter() CNumericLocalesSetter::~CNumericLocalesSetter()
{ {
-- s_numeric_locale_depth;
if (m_nested)
return;
#ifdef _WIN32 #ifdef _WIN32
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data()); std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
#else #else
+5
View File
@@ -19,8 +19,13 @@ class CNumericLocalesSetter {
public: public:
CNumericLocalesSetter(); CNumericLocalesSetter();
~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: private:
// Inside another setter on this thread, which does the setting and restoring.
bool m_nested { false };
#ifdef _WIN32 #ifdef _WIN32
std::string m_orig_numeric_locale; std::string m_orig_numeric_locale;
#else #else
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/task_arena.h>
#include <tbb/task_group.h>
#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<class ChunkSetup = std::monostate, class Resolve, class Commit>
void resolve_then_commit(size_t count, Resolve resolve, Commit commit)
{
using Resolved = std::invoke_result_t<Resolve&, size_t>;
std::vector<Resolved> 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<size_t>(first, last),
[&](const tbb::blocked_range<size_t>& 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
+236 -155
View File
@@ -5,6 +5,8 @@
#include "Preset.hpp" #include "Preset.hpp"
#include "PresetBundle.hpp" #include "PresetBundle.hpp"
#include "AppConfig.hpp" #include "AppConfig.hpp"
#include "LocalesUtils.hpp"
#include "ParallelResolve.hpp"
#ifdef _MSC_VER #ifdef _MSC_VER
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
@@ -506,10 +508,11 @@ void Preset::normalize(DynamicPrintConfig &config)
handle_legacy_sla(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; 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 (! default_config.has(key)) {
if (incorrect_keys.empty()) if (incorrect_keys.empty())
incorrect_keys = key; incorrect_keys = key;
@@ -621,7 +624,6 @@ void Preset::load_info(const std::string& file)
} }
else if (v.first.compare("base_id") == 0) { else if (v.first.compare("base_id") == 0) {
this->base_id = v.second.get_value<std::string>(); this->base_id = v.second.get_value<std::string>();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << file << " and base_id: " << this->base_id;
if (this->base_id.compare("null") == 0) if (this->base_id.compare("null") == 0)
this->base_id.clear(); 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); 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<std::string> *key_set1, std::set<std::string> *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<std::string, std::string> 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<Semver> 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<ConfigOptionString *> (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<ConfigOptionString *>(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<ConfigOptionStrings *>(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<Preset> &presets_loaded,
PresetsConfigSubstitutions &substitutions,
const std::function<void(Preset&)> &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. // Load all presets found in dir_path.
// Throws an exception on error. // Throws an exception on error.
void PresetCollection::load_presets( void PresetCollection::load_presets(
@@ -1706,6 +1871,8 @@ void PresetCollection::load_presets(
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr; std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); 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<UserPresetFile> files;
//BBS: change to json format //BBS: change to json format
for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) 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; BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
continue; continue;
} }
files.push_back({ dir_entry.path(), std::move(canonical_name) });
}
}
resolve_then_commit<CNumericLocalesSetter>(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 { try {
Preset preset(m_type, canonical_name, false); this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, read_only);
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<std::string, std::string> 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<Semver> 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<ConfigOptionString *> (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<ConfigOptionString *>(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<ConfigOptionStrings *>(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;
} catch (const std::runtime_error &err) { } catch (const std::runtime_error &err) {
errors_cummulative += err.what(); errors_cummulative += err.what();
errors_cummulative += "\n"; errors_cummulative += "\n";
} }
} });
}
if (presets_loaded.size() > 0) 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())); m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
sort_presets(); sort_presets();
@@ -2827,6 +2871,18 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
return std::make_pair(&preset, false); 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) Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version)
{ {
lock(); lock();
@@ -3846,28 +3902,53 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name)
return false; return false;
} }
// Merge one vendor's presets with the other vendor's presets, report duplicates. std::vector<std::vector<std::string>> PresetCollection::merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors)
std::vector<std::string> PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors)
{ {
std::vector<std::string> duplicates; auto less = [this](const Preset &a, const Preset &b) {
for (Preset &preset : other.m_presets) { return m_type == Preset::TYPE_FILAMENT ? filament_preset_less(a, b) : a < b;
if (preset.is_default || preset.is_external) };
continue; struct Incoming { Preset *preset; size_t source; };
Preset key(m_type, preset.name); auto incoming_less = [&less](const Incoming &a, const Incoming &b) { return less(*a.preset, *b.preset); };
auto it = (m_type == Preset::TYPE_FILAMENT) // Each of `others` is sorted, so its presets form one sorted run.
? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less) std::vector<Incoming> incoming;
: std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key); std::vector<size_t> run_ends { 0 };
if (it == m_presets.end() || it->name != preset.name) { 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<std::vector<std::string>> duplicates(others.size());
std::deque<Preset> 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) { if (preset.vendor != nullptr) {
// Re-assign a pointer to the vendor structure in the new PresetBundle. // Re-assign a pointer to the vendor structure in the new PresetBundle.
auto it = new_vendors.find(preset.vendor->id); auto it = new_vendors.find(preset.vendor->id);
assert(it != new_vendors.end()); assert(it != new_vendors.end());
preset.vendor = &it->second; preset.vendor = &it->second;
} }
m_presets.emplace(it, std::move(preset)); merged.emplace_back(std::move(preset));
} else }
duplicates.emplace_back(std::move(preset.name)); 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; return duplicates;
} }
+51 -3
View File
@@ -445,7 +445,10 @@ public:
static std::string remove_suffix_modified(const std::string& name); static std::string remove_suffix_modified(const std::string& name);
static void normalize(DynamicPrintConfig &config); static void normalize(DynamicPrintConfig &config);
// Report configuration fields, which are misplaced into a wrong group, remove them from the 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 // BBS: move constructor to public
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {} 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. // Return number of presets including the "- default -" preset.
size_t size() const { return m_presets.size(); } size_t size() const { return m_presets.size(); }
bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; } 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. // For Print / Filament presets, disable those, which are not compatible with the printer.
template<typename PreferedCondition> template<typename PreferedCondition>
@@ -874,8 +879,10 @@ protected:
// This is a temporary state, which shall be fixed immediately by the following step. // This is a temporary state, which shall be fixed immediately by the following step.
bool select_preset_by_name_strict(const std::string &name); bool select_preset_by_name_strict(const std::string &name);
// Merge one vendor's presets with the other vendor's presets, report duplicates. // Move the presets of `others` into this collection in one pass. A name this
std::vector<std::string> merge_presets(PresetCollection &&other, const VendorMap &new_vendors); // 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<std::vector<std::string>> merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors);
// Update m_map_alias_to_profile_name from loaded system profiles. // Update m_map_alias_to_profile_name from loaded system profiles.
void update_map_alias_to_profile_name(); void update_map_alias_to_profile_name();
@@ -890,6 +897,43 @@ protected:
void set_custom_preset_alias(Preset &preset); void set_custom_preset_alias(Preset &preset);
private: 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<std::string> 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<std::string> *key_set1, std::set<std::string> *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<Preset> &presets_loaded,
PresetsConfigSubstitutions &substitutions,
const std::function<void(Preset&)> &preset_loaded_fn,
bool read_only);
std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const; 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. // Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
@@ -901,6 +945,10 @@ private:
return a.name < b.name; 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. // Sort presets: filament presets use generic-first ordering, others sort alphabetically.
void sort_presets() { void sort_presets() {
if (m_type == Preset::TYPE_FILAMENT) if (m_type == Preset::TYPE_FILAMENT)
File diff suppressed because it is too large Load Diff
+172 -27
View File
@@ -15,6 +15,7 @@
#include <unordered_map> #include <unordered_map>
#include <optional> #include <optional>
#include <array> #include <array>
#include <atomic>
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
#include <unordered_set> #include <unordered_set>
@@ -622,36 +623,180 @@ public:
// default_filament_profile must resolve to a system filament. // default_filament_profile must resolve to a system filament.
bool check_printer_default_materials() const; bool check_printer_default_materials() const;
// Merge one vendor's presets with the other vendor's presets, report duplicates. // One vendor to load, and the directory it is installed in.
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a struct VendorSource
// bundle out of several per-vendor caches loaded into separate PresetBundle instances. {
std::vector<std::string> merge_presets(PresetBundle &&other); 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<PresetsConfigSubstitutions, std::string> load_vendors(const std::vector<VendorSource>& vendors,
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache,
const std::atomic<bool>* cancel = nullptr, std::vector<std::string>* failed = nullptr);
private: private:
// Load one vendor from the preset cache installed in `dir`, judged against // Move the presets and vendor profiles of `others` into this bundle, in one pass
// the vendor profile there. False, with this bundle left clean, when there // over each collection. A preset whose name this bundle or an earlier one of
// is no usable cache and the vendor has to be parsed. This is how // `others` already has is left out and listed under the bundle that repeats it.
// load_vendor_configs_from_json reads a cache. std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetBundle*> &others);
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// Load one source-form preset entry into this bundle: resolve `inherits` // What parsing one entry's JSON sub-file reported. Its errors and warnings are
// and `include`, flatten, validate and register the preset. Returns the // logged when the entry installs, so they come out in listing order with the
// reason loading failed, empty on success. See the definition for the // entry's install errors, as parsing and installing one entry at a time leaves them.
// sharing contract between the JSON parse and the cache load. struct EntryParse
// retain_configs / retain_includes, when non-null, name the only presets {
// registered into config_maps / include_maps (a config copy each). The ConfigSubstitutions substitutions;
// cache load passes the names its entries inherit / include — the only // Counted in the bundle's error count.
// ones ever looked up again; the JSON parse retains all, not knowing what std::vector<std::string> errors;
// later subfiles name. std::vector<std::string> warnings;
std::string load_vendor_preset(const CachedPreset& entry, };
const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle, // An EntryParse for each entry of the VendorCacheData list of the same name.
LoadConfigBundleAttributes flags, struct VendorParse
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, {
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps, std::vector<EntryParse> process_entries;
std::map<std::string, std::string>& filament_id_maps, std::vector<EntryParse> filament_entries;
PresetCollection* presets_collection, size_t& count, bool is_from_lib, std::vector<EntryParse> machine_entries;
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr); };
// 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<std::string> 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<PresetsConfigSubstitutions, size_t> 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<std::string>& 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 <vendor_name>/, 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<std::string> inherited;
std::set<std::string> included;
std::map<std::string, DynamicPrintConfig> config_maps;
std::map<std::string, DynamicPrintConfig> include_maps;
std::map<std::string, std::string> filament_id_maps;
std::unordered_set<std::string> 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<CachedPreset>& entries,
std::vector<EntryParse>* 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<std::string> 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<std::string> errors;
// What a base states for the presets that include it, when it is retained.
std::optional<DynamicPrintConfig> included;
// The config kept for the entries that inherit this one, or for other
// vendors when this is the filament library.
std::optional<DynamicPrintConfig> 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. // Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases(); void clear_printer_hold_aliases();
+1 -1
View File
@@ -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 // 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 — // 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 // nothing it derives. Inheritance and includes are resolved when the entry is
// installed, against whatever filament library is loaded then, so a cache // 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 // carries no other vendor's values and no other vendor's update can make it
+11 -5
View File
@@ -2439,12 +2439,18 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter) 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"); 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 // check adaptive pressure advance model of every extruder variant column of the used filaments
for (unsigned int extruder_id : extruders) { const std::vector<int> &self_index = m_config.filament_self_index.values;
if (m_config.adaptive_pressure_advance.get_at(extruder_id) && const size_t pa_columns = std::max(m_config.adaptive_pressure_advance_model.size(), size_t(extruders.back()) + 1);
m_config.enable_pressure_advance.get_at(extruder_id)) { 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(extruder_id); const std::string pa_model = m_config.adaptive_pressure_advance_model.get_at(column);
if (!pa_model.empty()) { if (!pa_model.empty()) {
std::string validation_error = AdaptivePAProcessor::validate_adaptive_pa_model(pa_model); std::string validation_error = AdaptivePAProcessor::validate_adaptive_pa_model(pa_model);
if (!validation_error.empty()) { if (!validation_error.empty()) {
+30
View File
@@ -9403,6 +9403,10 @@ void PrintConfigDef::handle_legacy_composite(DynamicPrintConfig &config)
} }
config.set_key_value("wiping_volumes_use_custom_matrix", new ConfigOptionBool(custom)); 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; const PrintConfigDef print_config_def;
@@ -9505,6 +9509,13 @@ std::set<std::string> filament_options_with_variant = {
"filament_ironing_spacing", "filament_ironing_spacing",
"filament_ironing_inset", "filament_ironing_inset",
"filament_ironing_speed", "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",
"activate_air_filtration_during_print", "activate_air_filtration_during_print",
"activate_air_filtration_on_completion", "activate_air_filtration_on_completion",
@@ -10712,6 +10723,25 @@ void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVect
target.set_to_index(&source, indices, stride); target.set_to_index(&source, indices, stride);
} }
void normalize_filament_values_to_variants(DynamicPrintConfig &config)
{
const auto *self_index = config.option<ConfigOptionInts>("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<ConfigOptionVectorBase *>(config.option(key));
if (opt == nullptr || (opt->size() != size_t(filament_count) && opt->size() != 1))
continue;
std::unique_ptr<ConfigOption> 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 //used for object/region config
//use the smallest of multiple to single //use the smallest of multiple to single
+5
View File
@@ -884,6 +884,11 @@ extern std::set<std::string> empty_options;
void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source, void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source,
const std::vector<int> &variant_index, int stride = 1); const std::vector<int> &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<std::string> filament_dev_options; extern std::set<std::string> filament_dev_options;
extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1); extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1);
+10 -1
View File
@@ -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); extern bool is_shapes_dir(const std::string& dir);
//BBS: add json support //BBS: add json support
extern bool is_json_file(const std::string& path); 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 // Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
// is rejected on all of them. // is rejected on all of them.
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root); 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 // Orca: custom protocal support utils
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); } inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
+63
View File
@@ -4,6 +4,9 @@
#include "miniz_extension.hpp" #include "miniz_extension.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#if defined(_MSC_VER) || defined(__MINGW64__) #if defined(_MSC_VER) || defined(__MINGW64__)
#include "boost/nowide/cstdio.hpp" #include "boost/nowide/cstdio.hpp"
#endif #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); 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_Archive::MZ_Archive()
{ {
mz_zip_zero_struct(&arch); mz_zip_zero_struct(&arch);
+2
View File
@@ -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_reader(mz_zip_archive *zip);
bool close_zip_writer(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); 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 { class MZ_Archive {
public: public:
+39 -1
View File
@@ -70,6 +70,7 @@
#include <boost/shared_ptr.hpp> #include <boost/shared_ptr.hpp>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
@@ -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 == '\\'; }; auto is_separator = [](char c) { return c == '/' || c == '\\'; };
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':')) if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
return false; 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();) { for (size_t start = 0; start <= rel_path.size();) {
size_t end = start; size_t end = start;
while (end < rel_path.size() && !is_separator(rel_path[end])) 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. // Resolve against the canonical root so a symlink inside it cannot lead back out.
try { 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(); const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
return full_str.compare(0, root_str.size(), root_str) == 0 && 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); (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<std::string> 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) bool is_img_file(const std::string &path)
{ {
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg"); return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
+2 -7
View File
@@ -1019,14 +1019,9 @@ if (UNIX AND NOT APPLE)
find_package(GTK${SLIC3R_GTK} REQUIRED) find_package(GTK${SLIC3R_GTK} REQUIRED)
pkg_check_modules(LIBSECRET REQUIRED libsecret-1) pkg_check_modules(LIBSECRET REQUIRED libsecret-1)
pkg_check_modules(webkit2gtk REQUIRED webkit2gtk-4.1) pkg_check_modules(webkit2gtk REQUIRED webkit2gtk-4.1)
if (FLATPAK) find_package(X11 REQUIRED)
# 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()
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS} ${webkit2gtk_INCLUDE_DIRS}) 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 # Propagate GDK backend detection results as compile definitions so that
# LinuxDisplayBackend.cpp can include the right GDK headers. # LinuxDisplayBackend.cpp can include the right GDK headers.
+1 -1
View File
@@ -428,7 +428,7 @@ void AuFile::on_dclick(wxMouseEvent &evt)
if (m_type == AddFileButton) if (m_type == AddFileButton)
return; return;
else else
wxLaunchDefaultApplication(m_file_path.wstring(), 0); desktop_open_project_attachment(this, m_file_path);
} }
void AuFile::on_mouse_left_up(wxMouseEvent &evt) void AuFile::on_mouse_left_up(wxMouseEvent &evt)
+8 -6
View File
@@ -171,14 +171,16 @@ void ConfigManipulation::check_adaptive_pressure_advance_model(DynamicPrintConfi
return; return;
const auto* model = config->option<ConfigOptionStrings>("adaptive_pressure_advance_model"); const auto* model = config->option<ConfigOptionStrings>("adaptive_pressure_advance_model");
if (model == nullptr || model->values.empty()) if (model == nullptr)
return; return;
std::string raw_model; // Each extruder variant holds its own model.
for (const std::string& chunk : model->values) std::string error;
raw_model += chunk; for (const std::string& variant_model : model->values) {
error = AdaptivePAProcessor::validate_adaptive_pa_model(variant_model);
std::string error = AdaptivePAProcessor::validate_adaptive_pa_model(raw_model); if (!error.empty())
break;
}
if (!error.empty()) { if (!error.empty()) {
wxString msg_text = _L("Adaptive Pressure Advance model validation failed:\n"); wxString msg_text = _L("Adaptive Pressure Advance model validation failed:\n");
msg_text += from_u8(error); msg_text += from_u8(error);
+32
View File
@@ -14,6 +14,9 @@
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/any.hpp> #include <boost/any.hpp>
#include <wx/filename.h>
#include <wx/filesys.h>
#if __APPLE__ #if __APPLE__
#import <IOKit/pwr_mgt/IOPMLib.h> #import <IOKit/pwr_mgt/IOPMLib.h>
#elif _WIN32 #elif _WIN32
@@ -27,11 +30,13 @@
#include "AboutDialog.hpp" #include "AboutDialog.hpp"
#include "MsgDialog.hpp" #include "MsgDialog.hpp"
#include "Plater.hpp"
#include "format.hpp" #include "format.hpp"
#include "WebUserLoginDialog.hpp" #include "WebUserLoginDialog.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/Utils.hpp"
namespace Slic3r { namespace Slic3r {
@@ -531,6 +536,11 @@ boost::filesystem::path into_path(const wxString &str)
return boost::filesystem::path(str.wx_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() void about()
{ {
AboutDialog dlg; 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);
}
} } } }
+6
View File
@@ -76,6 +76,9 @@ std::string into_u8(const wxString &str);
wxString from_path(const boost::filesystem::path &path); wxString from_path(const boost::filesystem::path &path);
// boost path from wxString // boost path from wxString
boost::filesystem::path into_path(const wxString &str); 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 // Display an About dialog
extern void about(); extern void about();
@@ -83,6 +86,9 @@ extern void about();
extern void desktop_open_datadir_folder(); extern void desktop_open_datadir_folder();
// Ask the destop to open one folder // Ask the destop to open one folder
extern void desktop_open_any_folder(const std::string& path); 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 GUI
} // namespace Slic3r } // namespace Slic3r
+26 -5
View File
@@ -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()); 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); 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; 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()); 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 { 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; boost::system::error_code ec;
fs::remove(dest_path, ec); fs::remove(dest_path, ec);
if (ec) { if (ec) {
@@ -1544,9 +1566,8 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
} }
mz_bool res = 0; mz_bool res = 0;
#ifndef WIN32 #ifndef WIN32
if (S_ISLNK(stat.m_external_attr >> 16)) { if (is_link) {
std::string link(stat.m_uncomp_size + 1, 0); res = 1;
res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0);
try { try {
boost::filesystem::create_symlink(link, dest_path); boost::filesystem::create_symlink(link, dest_path);
} catch (const std::exception &e) { } catch (const std::exception &e) {
+150 -216
View File
@@ -1997,6 +1997,126 @@ bool MainFrame::can_reslice() const
return (m_plater != nullptr) && !m_plater->model().objects.empty(); 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::PrintSelectType> MainFrame::available_print_actions() const
{
std::vector<PrintSelectType> 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<ConfigOptionBool>("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<ConfigOptionEnum<PrintHostType>>("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() wxBoxSizer* MainFrame::create_side_tools()
{ {
enable_multi_machine = wxGetApp().is_enable_multi_machine(); 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_btn = new SideButton(print_panel, _L("Print plate"), "");
m_print_option_btn = new SideButton(print_panel, "", "sidebutton_dropdown", 0, 14); 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); 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_option_btn, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(1));
slice_sizer->Add(m_slice_btn, 0, wxLEFT | 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) m_print_option_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event)
{ {
SidePopup* p = new SidePopup(this); SidePopup* p = new SidePopup(this);
for (PrintSelectType type : available_print_actions()) {
if (wxGetApp().preset_bundle SideButton* btn = new SideButton(p, print_select_type_label(type), "");
&& !wxGetApp().preset_bundle->is_bbl_vendor() btn->SetCornerRadius(0);
&& !wxGetApp().app_config->get_bool("use_printer_agents")) { btn->Bind(wxEVT_BUTTON, [this, p, type](wxCommandEvent&) {
// ThirdParty Buttons select_print_action(type);
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->Dismiss();
}); });
p->append_button(btn);
// 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<ConfigOptionBool>("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);
} }
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<ConfigOptionEnum<PrintHostType>>("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); 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) void MainFrame::set_print_button_to_default(PrintSelectType select_type)
{ {
if (select_type == PrintSelectType::ePrintPlate) { // Orca: keep the user's remembered print/export action instead of resetting it to the computed
m_print_btn->SetLabel(_L("Print plate")); // default. get_remembered_print_select() already rejects anything this printer does not offer.
m_print_select = ePrintPlate; PrintSelectType remembered;
if (m_print_enable) if (get_remembered_print_select(remembered))
m_print_enable = get_enable_print_status(); select_type = remembered;
m_print_btn->Enable(m_print_enable);
this->Layout(); if (select_type == eUploadGcode)
} else if (select_type == PrintSelectType::eSendGcode) { return; // unsupported: no dropdown entry exists for this action
m_print_btn->SetLabel(_L_CONTEXT("Print", "Verb"));
m_print_select = eSendGcode; m_print_btn->SetLabel(print_select_type_label(select_type));
if (m_print_enable) m_print_select = select_type;
m_print_enable = get_enable_print_status() && can_send_gcode(); // get_enable_print_status() already applies can_send_gcode() to the actions that need it
m_print_btn->Enable(m_print_enable); if (m_print_enable)
this->Layout(); m_print_enable = get_enable_print_status();
} else if (select_type == PrintSelectType::eExportGcode) { m_print_btn->Enable(m_print_enable);
m_print_btn->SetLabel(_L("Export G-code file")); this->Layout();
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;
}
} }
void MainFrame::add_to_recent_projects(const wxString& filename) void MainFrame::add_to_recent_projects(const wxString& filename)
+9
View File
@@ -13,6 +13,7 @@
#include <string> #include <string>
#include <map> #include <map>
#include <vector>
#include "GUI_Utils.hpp" #include "GUI_Utils.hpp"
#include "Event.hpp" #include "Event.hpp"
@@ -430,6 +431,14 @@ public:
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
void on_config_changed(DynamicPrintConfig* cfg) const ; void on_config_changed(DynamicPrintConfig* cfg) const ;
void set_print_button_to_default(PrintSelectType select_type); 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<PrintSelectType> 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() const;
bool can_save_as() const; bool can_save_as() const;
+1 -4
View File
@@ -98,10 +98,7 @@ void MarkdownTip::LoadStyle()
ph /= "tooltip/styled.html"; ph /= "tooltip/styled.html";
_data_dir = false; _data_dir = false;
} }
auto url = ph.string(); _tipView->LoadURL(file_url_from_path(ph));
std::replace(url.begin(), url.end(), '\\', '/');
url = "file:///" + url;
_tipView->LoadURL(from_u8(url));
_lastTip.clear(); _lastTip.clear();
} }
+28 -16
View File
@@ -286,6 +286,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<ConfigOptionBools>("enable_pressure_advance");
auto pa = filament_config->option<ConfigOptionFloats>("pressure_advance");
auto adaptive_pa = filament_config->option<ConfigOptionBools>("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) bool Plater::has_illegal_filename_characters(const wxString& wxs_name)
{ {
std::string name = into_u8(wxs_name); std::string name = into_u8(wxs_name);
@@ -3672,7 +3696,7 @@ void Sidebar::update_all_preset_comboboxes()
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
wxString apikey; wxString apikey;
if(url.empty()) 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 { else {
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value; const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint))
@@ -17064,11 +17088,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0); set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0);
} }
if (!filament_config->option<ConfigOptionBools>("enable_pressure_advance")->get_at(0)) { zero_pressure_advance_where_disabled(filament_config);
set_config_values<bool, ConfigOptionBools>(filament_config, "enable_pressure_advance", true);
set_config_values<double, ConfigOptionFloatsNullable>(filament_config, "pressure_advance", 0.0);
set_config_values<bool, ConfigOptionBools>(filament_config, "adaptive_pressure_advance", false);
}
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false});
@@ -17130,11 +17150,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0); set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0);
} }
if (!filament_config->option<ConfigOptionBools>("enable_pressure_advance")->get_at(0)) { zero_pressure_advance_where_disabled(filament_config);
set_config_values<bool, ConfigOptionBools>(filament_config, "enable_pressure_advance", true);
set_config_values<double, ConfigOptionFloatsNullable>(filament_config, "pressure_advance", 0.0);
set_config_values<bool, ConfigOptionBools>(filament_config, "adaptive_pressure_advance", false);
}
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false});
@@ -17196,11 +17212,7 @@ void Plater::Calib_Cornering(const Calib_Params& params)
set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0); set_config_values<double, ConfigOptionFloatsNullable>(print_config, "default_jerk", 0);
} }
if (!filament_config->option<ConfigOptionBools>("enable_pressure_advance")->get_at(0)) { zero_pressure_advance_where_disabled(filament_config);
set_config_values<bool, ConfigOptionBools>(filament_config, "enable_pressure_advance", true);
set_config_values<double, ConfigOptionFloatsNullable>(filament_config, "pressure_advance", 0.0);
set_config_values<bool, ConfigOptionBools>(filament_config, "adaptive_pressure_advance", false);
}
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true});
+3
View File
@@ -1831,6 +1831,9 @@ 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"); 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); 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 //// CONTROL > Camera
g_sizer->Add(create_item_title(_L("Camera")), 1, wxEXPAND); g_sizer->Add(create_item_title(_L("Camera")), 1, wxEXPAND);
+1 -3
View File
@@ -57,9 +57,7 @@ PrivacyUpdateDialog::PrivacyUpdateDialog(wxWindow* parent, wxWindowID id, const
fs::path ph(resources_dir()); fs::path ph(resources_dir());
ph /= "tooltip/privacyupdate.html"; ph /= "tooltip/privacyupdate.html";
m_host_url = ph.string(); m_host_url = into_u8(file_url_from_path(ph));
std::replace(m_host_url.begin(), m_host_url.end(), '\\', '/');
m_host_url = "file:///" + m_host_url;
m_vebview_release_note->LoadURL(from_u8(m_host_url)); m_vebview_release_note->LoadURL(from_u8(m_host_url));
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
+4 -6
View File
@@ -45,10 +45,10 @@ const std::vector<std::string> license_list = {
ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style) ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style)
{ {
SetBackgroundColour(*wxWHITE); 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(); wxString strlang = wxGetApp().current_language_code_safe();
if (strlang != "") 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); wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
@@ -293,10 +293,8 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
if (!accessory_path.empty()) { if (!accessory_path.empty()) {
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString()); std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
fs::path path(decode_path); fs::path path(decode_path);
if (!desktop_open_project_attachment(this, path))
if (fs::exists(path)) { BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: not opening " << decode_path;
wxLaunchDefaultApplication(path.wstring(), 0);
}
} }
} }
else if (strCmd == "request_3mf_info") { else if (strCmd == "request_3mf_info") {
+1 -1
View File
@@ -510,7 +510,7 @@ void UpdateVersionDialog::update_version_info(wxString release_note, wxString ve
out_buf->append(text, size); out_buf->append(text, size);
}, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0); }, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0);
html_source.append("</body></html>"); html_source.append("</body></html>");
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()); SetMinSize(GetSize());
SetMaxSize(GetSize()); SetMaxSize(GetSize());
+20 -19
View File
@@ -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("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("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("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"); optgroup->append_single_option_line("pressure_advance", "material_flow_ratio_and_pressure_advance#pressure-advance", 0);
// Orca: adaptive pressure advance and calibration model // 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", "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"); 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"); 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.full_width = true;
option.opt.is_code = true; option.opt.is_code = true;
option.opt.height = 15; option.opt.height = 15;
@@ -4432,7 +4432,7 @@ void TabFilament::build()
DynamicPrintConfig& filament_config = m_preset_bundle->filaments.get_edited_preset().config; DynamicPrintConfig& filament_config = m_preset_bundle->filaments.get_edited_preset().config;
update_dirty(); 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); m_config_manipulation.check_adaptive_pressure_advance_model(&filament_config);
on_value_change(opt_key, value); on_value_change(opt_key, value);
@@ -4829,8 +4829,11 @@ void TabFilament::toggle_options()
} }
if (m_active_page->title() == L("Filament")) if (m_active_page->title() == L("Filament"))
{ {
bool pa = m_config->opt_bool("enable_pressure_advance", 0); const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
toggle_option("pressure_advance", pa); 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. //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; DynamicConfig& proj_cfg = m_preset_bundle->project_config;
@@ -4858,12 +4861,12 @@ void TabFilament::toggle_options()
// Orca: adaptive pressure advance and calibration model // Orca: adaptive pressure advance and calibration model
// If PA is not enabled, disable adaptive pressure advance and hide the model section // 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 // If adaptive PA is not enabled, hide the adaptive PA model section
toggle_option("adaptive_pressure_advance", pa); toggle_option("adaptive_pressure_advance", pa, 0);
toggle_option("adaptive_pressure_advance_overhangs", pa); toggle_option("adaptive_pressure_advance_overhangs", pa, 0);
bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", 0); bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", variant_idx);
toggle_line("adaptive_pressure_advance_overhangs", has_adaptive_pa && pa); toggle_line("adaptive_pressure_advance_overhangs", has_adaptive_pa && pa, 0);
toggle_line("adaptive_pressure_advance_model", has_adaptive_pa && pa); toggle_line("adaptive_pressure_advance_model", has_adaptive_pa && pa, 0);
toggle_line("adaptive_pressure_advance_bridges", has_adaptive_pa && pa); toggle_line("adaptive_pressure_advance_bridges", has_adaptive_pa && pa, 0);
bool is_pellet_printer = printer_cfg.opt_bool("pellet_modded_printer"); bool is_pellet_printer = printer_cfg.opt_bool("pellet_modded_printer");
toggle_line("pellet_flow_coefficient", is_pellet_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")); 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); 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"; bool enable_fit = volumetric_speed_cos != "0 0 0 0 0 0";
toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_idx); 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->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 = 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("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("auxiliary_fan", "printer_basic_information_accessory#auxiliary-part-cooling-fan");
optgroup->append_single_option_line("fan_direction"); 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); 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_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_type", "", extruder_idx);
optgroup->append_single_option_line("nozzle_volume", "printer_extruder_basic_information#nozzle-volume", 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) 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"); 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; SuppressBackgroundProcessingUpdate sbpu;
const double new_nd = boost::any_cast<double>(value); const double new_nd = boost::any_cast<double>(value);
+5 -4
View File
@@ -31,14 +31,15 @@ int UserManager::parse_json(std::string payload)
{ {
bool restored_json = false; bool restored_json = false;
json j; json j;
json j_pre = json::parse(payload);
if (j_pre.empty()) {
return -1;
}
//bind/unbind //bind/unbind
try { try {
json j_pre = json::parse(payload);
if (j_pre.empty()) {
return -1;
}
if (j_pre.contains("bind")) { if (j_pre.contains("bind")) {
if (j_pre["bind"].contains("command")) { if (j_pre["bind"].contains("command")) {
+1 -3
View File
@@ -38,9 +38,7 @@ DownPluginFrame::DownPluginFrame(GUI_App *pGUI) : wxDialog((wxWindow *) (pGUI->m
// set the frame icon // set the frame icon
wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL);
wxString TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/6/index.html").make_preferred().string()); wxString TargetUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/6/index.html");
TargetUrl = "file://" + TargetUrl;
// Create the webview // Create the webview
m_browser = WebView::CreateWebView(this, TargetUrl); m_browser = WebView::CreateWebView(this, TargetUrl);
+34 -42
View File
@@ -218,37 +218,38 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load)
m_page = startpage; m_page = startpage;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, load=%1%, start_page=%2%")%load%int(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() ); //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()); //wxLogMessage("GUIDE: webpage_2 %s", TargetUrl.mb_str());
if (startpage == BBL_WELCOME){ if (startpage == BBL_WELCOME){
SetTitle(_L("Setup Wizard")); 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) { } else if (startpage == BBL_REGION) {
SetTitle(_L("Setup Wizard")); 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) { } else if (startpage == BBL_MODELS) {
SetTitle(_L("Setup Wizard")); 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) { } else if (startpage == BBL_FILAMENTS) {
SetTitle(_L("Setup Wizard")); SetTitle(_L("Setup Wizard"));
int nSize = m_ProfileJson["model"].size(); int nSize = m_ProfileJson["model"].size();
if (nSize>0) 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 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) { } else if (startpage == BBL_FILAMENT_ONLY) {
SetTitle(""); 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) { } else if (startpage == BBL_MODELS_ONLY) {
SetTitle(""); 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 { else {
SetTitle(_L("Setup Wizard")); 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(); wxString strlang = wxGetApp().current_language_code_safe();
@@ -256,7 +257,6 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load)
if (strlang != "") if (strlang != "")
TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang); TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang);
TargetUrl = "file://" + TargetUrl;
if (load) if (load)
load_url(TargetUrl); load_url(TargetUrl);
@@ -1284,7 +1284,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
entry["vendor"] = vp.id; entry["vendor"] = vp.id;
entry["nozzle_diameter"] = nozzle_str; entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str; entry["materials"] = materials_str;
entry["cover"] = cover_path.string(); entry["cover"] = into_u8(file_url_from_path(cover_path));
entry["nozzle_selected"] = ""; entry["nozzle_selected"] = "";
entry["sub_path"] = ""; entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry); 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 // is served from the shipped profiles. Each is stamped by name and
// version alone: a profile change requires a version bump, so those two // version alone: a profile change requires a version bump, so those two
// determine content wherever the vendor's copy sits. // determine content wherever the vendor's copy sits.
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; }; std::vector<PresetBundle::VendorSource> ordered;
std::vector<VendorSource> ordered; json stamps = json::array();
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) { 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 // 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 // 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. // it), the cache's own stamp where the cache is the whole vendor.
// A profile without a version (blacklist.json) carries no presets // A profile without a version (blacklist.json) carries no presets
// and is passed over. // and is passed over.
const boost::filesystem::path profile = dir / (name + ".json"); const boost::filesystem::path profile = dir / (name + ".json");
std::string version;
if (boost::filesystem::exists(profile)) { if (boost::filesystem::exists(profile)) {
const Semver v = get_version_from_json(profile.string()); const Semver v = get_version_from_json(profile.string());
if (v.valid()) if (! v.valid())
ordered.push_back({name, dir, v.to_string()}); return;
version = v.to_string();
} else { } else {
ordered.push_back({name, dir, version = VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name);
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); const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end()) if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
@@ -1431,9 +1434,6 @@ bool GuideFrame::BuildProfileDataFromVendors()
add_vendor(name, dir); add_vendor(name, dir);
if (ordered.empty()) if (ordered.empty())
return false; 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 // 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 // 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 // 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 // what makes this worth doing instead of the scan below.
// bundle per vendor keeps the install order the startup path has. PresetBundle bundle;
PresetBundle bundle; std::vector<std::string> failed;
auto load_vendor = [](PresetBundle& into, const std::string& vendor, const std::string errors = bundle.load_vendors(ordered, ForwardCompatibilitySubstitutionRule::EnableSilent,
const boost::filesystem::path& dir, const PresetBundle* base) { /*allow_cache=*/true, m_cancel_token.get(), &failed).second;
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem, if (*m_cancel_token || bundle.vendors.empty())
ForwardCompatibilitySubstitutionRule::EnableSilent, base); return false;
}; if (! errors.empty())
for (const VendorSource& v : ordered) { BOOST_LOG_TRIVIAL(warning) << "GuideFrame: loading the vendors reported: " << errors;
if (*m_cancel_token) // A vendor that failed to load sends this open to the scan below, which lists
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time // what it can read of every vendor.
if (v.name == filament_library) { if (! failed.empty())
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())
return false; return false;
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false)) if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
return 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) cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
.make_preferred(); .make_preferred();
} }
OneModel["cover"] = cover_path.string(); OneModel["cover"] = into_u8(file_url_from_path(cover_path));
OneModel["nozzle_selected"] = ""; OneModel["nozzle_selected"] = "";
+1 -1
View File
@@ -599,7 +599,7 @@ void ZUserLogin::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt))
bool ZUserLogin::ShowErrorPage() 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); load_url(ErrortUrl);
return true; return true;
+2 -2
View File
@@ -36,10 +36,10 @@ namespace GUI {
WebViewPanel::WebViewPanel(wxWindow *parent) WebViewPanel::WebViewPanel(wxWindow *parent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) : 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(); wxString strlang = wxGetApp().current_language_code_safe();
if (strlang != "") 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); wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
+2 -4
View File
@@ -1,5 +1,6 @@
#include "MultiNozzleSync.hpp" #include "MultiNozzleSync.hpp"
#include "../GUI.hpp"
#include "../GUI_App.hpp" #include "../GUI_App.hpp"
#include "../I18N.hpp" #include "../I18N.hpp"
#include "../Plater.hpp" #include "../Plater.hpp"
@@ -21,8 +22,6 @@
#include <set> #include <set>
#include <wx/choice.h> #include <wx/choice.h>
#include <wx/filename.h>
#include <wx/filesys.h>
#include <wx/sizer.h> #include <wx/sizer.h>
#include <wx/stattext.h> #include <wx/stattext.h>
@@ -641,8 +640,7 @@ NozzleListTable::NozzleListTable(wxWindow* parent) : wxPanel(parent,wxID_ANY,wxD
m_web_view->AddScriptMessageHandler("nozzleListTable"); m_web_view->AddScriptMessageHandler("nozzleListTable");
m_web_view->EnableContextMenu(false); m_web_view->EnableContextMenu(false);
fs::path filepath = fs::path(resources_dir()) / "web/flush/NozzleListTable.html"; fs::path filepath = fs::path(resources_dir()) / "web/flush/NozzleListTable.html";
wxFileName fn(wxString::FromUTF8(filepath.string())); wxString url = file_url_from_path(filepath);
wxString url = wxFileSystem::FileNameToURL(fn);
m_web_view->LoadURL(url); m_web_view->LoadURL(url);
auto sizer = new wxBoxSizer(wxVERTICAL); auto sizer = new wxBoxSizer(wxVERTICAL);
+5 -6
View File
@@ -48,20 +48,19 @@ constexpr char ORCA_BRIDGE_JS[] = R"JS(
wxString bootstrap_url() 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() wxString content_base_url()
{ {
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string(); return file_url_from_path(boost::filesystem::path(resources_dir()) / "web") + "/";
return wxString("file://") + from_u8(dir) + "/";
} }
bool is_content_url(const wxString& url) bool is_content_url(const wxString& url)
{ {
// The web view reports the URL it parsed, which escapes anything the resources path holds // The web view reports the URL it parsed, which may escape the resources path differently
// (a space, a non-ASCII character), while content_base_url() is the raw path. // from content_base_url().
return wxURI::Unescape(url.BeforeFirst('#')) == content_base_url(); return wxURI::Unescape(url.BeforeFirst('#')) == wxURI::Unescape(content_base_url());
} }
const char* orca_bridge_script() { return ORCA_BRIDGE_JS; } const char* orca_bridge_script() { return ORCA_BRIDGE_JS; }
+2 -2
View File
@@ -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 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()) { if (append_language_to_url()) {
const wxString lang = wxGetApp().current_language_code_safe(); 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) void WebViewHostDialog::load_url(const wxString& url)
+1 -1
View File
@@ -464,7 +464,7 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) :
wxString filepath_str = from_path(filepath); wxString filepath_str = from_path(filepath);
wxFileName fn(filepath_str); wxFileName fn(filepath_str);
if(fn.FileExists()) { 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(); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "File exists and load url " << url.ToStdString();
m_webview->LoadURL(url); m_webview->LoadURL(url);
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "Successfully loaded url: " << url.ToStdString(); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__<< "Successfully loaded url: " << url.ToStdString();
-9
View File
@@ -274,13 +274,4 @@ bool Duet::start_print(wxString &msg, const std::string &filename, ConnectionTyp
return res; 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<int>("err", 0);
}
} }
-1
View File
@@ -40,7 +40,6 @@ private:
ConnectionType connect(wxString &msg) const; ConnectionType connect(wxString &msg) const;
void disconnect(ConnectionType connectionType) const; void disconnect(ConnectionType connectionType) const;
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) 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;
}; };
} }
-9
View File
@@ -146,15 +146,6 @@ bool ESP3D::start_print(wxString& msg, const std::string& filename) const
return ret; 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<int>("err", 0);
}
// ESP3D only accepts 8.3 filenames else it crashes marlin and other undefined behaviour // 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 std::string ESP3D::get_short_name(const std::string& filename) const
{ {
-1
View File
@@ -33,7 +33,6 @@ private:
std::string m_console_port; std::string m_console_port;
bool start_print(wxString& msg, 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;
std::string get_short_name(const std::string& filename) 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; std::string format_command(const std::string& path, const std::string& arg, const std::string& val) const;
}; };
+1 -3
View File
@@ -340,9 +340,7 @@ namespace Slic3r {
if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2) if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2)
return fallback_webui; return fallback_webui;
std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html"; std::string web_path = GUI::into_u8(GUI::file_url_from_path(boost::filesystem::path(resources_dir()) / "web/elegoolink/lan_service_web/index.html"));
std::replace(web_path.begin(), web_path.end(), '\\', '/');
web_path = "file://" + web_path;
const std::string token = get_cc2_token(config->opt_string("printhost_apikey")); const std::string token = get_cc2_token(config->opt_string("printhost_apikey"));
const std::string host_ip = Http::get_host_header_value(host); const std::string host_ip = Http::get_host_header_value(host);
+25 -15
View File
@@ -510,12 +510,22 @@ bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots
if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg)) if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg))
return false; return false;
const auto parsed = json::parse(body, nullptr, false, true); if (!parse_material_slots(body, slots, supports_material_station)) {
if (parsed.is_discarded()) {
msg = _(L("Flashforge returned an invalid JSON response.")); msg = _(L("Flashforge returned an invalid JSON response."));
return false; return false;
} }
return true;
}
bool Flashforge::parse_material_slots(const std::string& body, std::vector<FlashforgeMaterialSlot>& 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& detail = parsed.contains("detail") ? parsed["detail"] : parsed;
const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] : const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] :
detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json(); detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json();
@@ -542,12 +552,21 @@ bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots
if (supports_material_station != nullptr) if (supports_material_station != nullptr)
*supports_material_station = reports_material_station; *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) { for (const auto& slot : slot_infos) {
if (!slot.is_object())
continue;
FlashforgeMaterialSlot info; FlashforgeMaterialSlot info;
info.slot_id = slot.value("slotId", static_cast<int>(slots.size()) + 1); info.slot_id = static_cast<int>(slots.size()) + 1;
info.has_filament = slot.value("hasFilament", false); if (const auto it = slot.find("slotId"); it != slot.end())
info.material_name = slot.value("materialName", std::string()); try_parse_json_int(*it, info.slot_id);
info.material_color = slot.value("materialColor", std::string()); 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<std::string>();
if (const auto it = slot.find("materialColor"); it != slot.end() && it->is_string())
info.material_color = it->get<std::string>();
slots.emplace_back(std::move(info)); slots.emplace_back(std::move(info));
} }
@@ -670,13 +689,4 @@ std::string Flashforge::extract_host_name() const
return out; 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<int>("err", 0);
}
} // namespace Slic3r } // namespace Slic3r
+2 -1
View File
@@ -45,6 +45,8 @@ public:
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
std::string get_host() const override { return m_host; } std::string get_host() const override { return m_host; }
bool fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const; bool fetch_material_slots(std::vector<FlashforgeMaterialSlot>& 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<FlashforgeMaterialSlot>& slots, bool* supports_material_station);
static bool discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3); static bool discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3);
private: 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; 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 make_http_url(const std::string& path) const;
std::string extract_host_name() const; std::string extract_host_name() const;
int get_err_code_from_body(const std::string &body) const;
bool connect(wxString& msg) const; bool connect(wxString& msg) const;
bool start_print(wxString& msg, const std::string& filename) const; bool start_print(wxString& msg, const std::string& filename) const;
}; };
-9
View File
@@ -141,13 +141,4 @@ bool MKS::start_print(wxString& msg, const std::string& filename) const
return ret; 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<int>("err", 0);
}
} // Slic3r } // Slic3r
-1
View File
@@ -34,7 +34,6 @@ private:
std::string get_upload_url(const std::string& filename) const; std::string get_upload_url(const std::string& filename) const;
bool start_print(wxString& msg, 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;
}; };
} }
+2 -56
View File
@@ -339,62 +339,8 @@ bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &targe
//BBS: refine preset update logic //BBS: refine preset update logic
bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path) bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path)
{ {
bool res = true; const std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string();
std::string file_path = source_path.string(); return extract_archive_confined(source_path.string(), parent_path);
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 "<<file_path;
return false;
}
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
mz_zip_archive_file_stat stat;
// we first loop the entries to read from the archive the .amf file only, in order to extract the version from it
for (mz_uint i = 0; i < num_entries; ++i)
{
if (mz_zip_reader_file_stat(&archive, i, &stat))
{
std::string dest_file = parent_path+"/"+stat.m_filename;
if (stat.m_is_directory) {
fs::path dest_path(dest_file);
if (!fs::exists(dest_path))
fs::create_directories(dest_path);
continue;
}
else if (stat.m_uncomp_size == 0) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: invalid size for file "<<stat.m_filename;
continue;
}
try
{
res = mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0);
if (!res) {
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]extract file "<<stat.m_filename<<" to dest "<<dest_file<<" failed";
close_zip_reader(&archive);
return res;
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]successfully extract file " << stat.m_file_index << " to "<<dest_file;
}
catch (const std::exception& e)
{
// ensure the zip archive is closed and rethrow the exception
close_zip_reader(&archive);
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]Archive read exception:"<<e.what();
return false;
}
}
else {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: read file stat failed";
}
}
close_zip_reader(&archive);
return true;
} }
// Remove a leftover partial archive for the vendor about to be synchronized. // Remove a leftover partial archive for the vendor about to be synchronized.
+42 -9
View File
@@ -3,10 +3,13 @@
#include <vector> #include <vector>
#include <thread> #include <thread>
#include <exception> #include <exception>
#include <sstream>
#include <boost/optional.hpp> #include <boost/optional.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <wx/string.h> #include <wx/string.h>
#include <wx/app.h> #include <wx/app.h>
@@ -172,6 +175,20 @@ std::string moonraker_error_reason(const std::string &body)
} // namespace } // 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<int>("err", 0);
}
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
{ {
if (status != 0) { if (status != 0) {
@@ -415,12 +432,10 @@ void PrintHostJobQueue::priv::remove_source()
source_to_remove.clear(); 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. // 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; LifecycleEventContext ctx;
@@ -429,19 +444,37 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx); fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx);
} }
bool success = the_job.printhost->upload(std::move(the_job.upload_data), bool success = false;
[this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); }, std::string error;
[this](wxString error) { this->error_fn(std::move(error)); }, // A throwing upload must not stop the worker, or later jobs would stay queued forever.
[this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); } 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; LifecycleEventContext ctx;
ctx.name = upload_filename; ctx.name = upload_filename;
ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error; ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error;
ctx.msg = error;
fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx); 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) { if (success) {
emit_progress(100); emit_progress(100);
if (the_job.switch_to_device_tab) { if (the_job.switch_to_device_tab) {
+6
View File
@@ -87,6 +87,8 @@ public:
static PrintHost* get_print_host(DynamicPrintConfig *config); static PrintHost* get_print_host(DynamicPrintConfig *config);
static std::string get_print_host_webui(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 //Support for cloud webui login
virtual bool is_cloud() const { return false; } virtual bool is_cloud() const { return false; }
@@ -150,6 +152,10 @@ public:
void enqueue(PrintHostJob job); void enqueue(PrintHostJob job);
void cancel(size_t id); 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: private:
struct priv; struct priv;
std::shared_ptr<priv> p; std::shared_ptr<priv> p;
-9
View File
@@ -654,13 +654,4 @@ bool UltiMaker::start_print(wxString &msg, const std::string &filename, Connecti
return res; 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<int>("err", 0);
}
} }
-1
View File
@@ -64,7 +64,6 @@ private:
void set_auth(Http& http) const; void set_auth(Http& http) const;
void disconnect(ConnectionType connectionType) const; void disconnect(ConnectionType connectionType) const;
bool start_print(wxString &msg, const std::string &filename, 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;
}; };
} }
+125
View File
@@ -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); 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<std::string> pressure_advance_values(const std::string &gcode)
{
const std::string token = "SET_PRESSURE_ADVANCE ADVANCE=";
std::set<std::string> 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<NozzleVolumeType, int, std::string, std::string>({
{ 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<ConfigOptionStrings>("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<ConfigOptionEnumsGeneric>("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<std::string> 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<int, int, std::string, std::string>({
{ 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<ConfigOptionStrings>("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<ConfigOptionEnumsGeneric>("extruder_type", true)->values = { etDirectDrive, etDirectDrive };
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = { nvtStandard, nvtHighFlow };
// keep the mapping above rather than grouping the filaments automatically
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
const std::string gcode = slice({ cube(20) }, config);
std::set<std::string> 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);
}
}
+19
View File
@@ -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); 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. // Custom G-code templates substitute placeholders during export.
TEST_CASE("Custom G-code placeholders are substituted", "[Print]") TEST_CASE("Custom G-code placeholders are substituted", "[Print]")
{ {
+3
View File
@@ -16,8 +16,10 @@ add_executable(${_TEST_NAME}_tests
test_clipper_utils.cpp test_clipper_utils.cpp
test_config.cpp test_config.cpp
test_config_variant_expansion.cpp test_config_variant_expansion.cpp
test_locales_utils.cpp
test_drc.cpp test_drc.cpp
test_toolordering_nozzle_group.cpp test_toolordering_nozzle_group.cpp
test_parallel_resolve.cpp
test_preset_bundle_loading.cpp test_preset_bundle_loading.cpp
test_preset_setting_id.cpp test_preset_setting_id.cpp
test_preset_diff.cpp test_preset_diff.cpp
@@ -44,6 +46,7 @@ add_executable(${_TEST_NAME}_tests
test_lay_on_face.cpp test_lay_on_face.cpp
test_model.cpp test_model.cpp
test_utils.cpp test_utils.cpp
test_miniz_extension.cpp
test_timeutils.cpp test_timeutils.cpp
test_voronoi.cpp test_voronoi.cpp
test_wipe_tower_estimate.cpp test_wipe_tower_estimate.cpp
@@ -717,3 +717,15 @@ TEST_CASE("get_index_for_extruder scales the variant column by the requested str
REQUIRE(col0_stride2 == 0); REQUIRE(col0_stride2 == 0);
REQUIRE(col1_stride2 == 2); 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<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2};
config.load_from_ini_string("pressure_advance = 0.021", ForwardCompatibilitySubstitutionRule::Disable);
REQUIRE(config.option<ConfigOptionFloats>("pressure_advance")->values == std::vector<double>({0.021, 0.021, 0.021}));
}
+69
View File
@@ -0,0 +1,69 @@
#include <catch2/catch_all.hpp>
#include <clocale>
#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());
}
}
+194
View File
@@ -0,0 +1,194 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/miniz_extension.hpp"
#include "test_utils.hpp"
#include <boost/filesystem.hpp>
#include <algorithm>
#include <fstream>
#include <iterator>
#include <string>
#include <utility>
#include <vector>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace {
void write_zip(const fs::path &zip_file, const std::vector<std::pair<std::string, std::string>> &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<char>(in), std::istreambuf_iterator<char>());
}
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<std::string> list_dir(const fs::path &dir)
{
std::vector<std::string> 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<char>(in), std::istreambuf_iterator<char>());
}
} // 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<std::string>{"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
+54 -9
View File
@@ -19,9 +19,8 @@ struct LoadedObj
ObjInfo info; ObjInfo info;
}; };
// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1). // Loads an OBJ made of the given lines, with a material library that defines material "a".
// Only the first face and the vt lines are varied; the other three faces reference vt 1. LoadedObj load_textured_obj(const std::string &body)
LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::string &vts = "vt 0.25 0.5\nvt 0.75 1\n")
{ {
ScopedTemporaryFile obj(".obj"); ScopedTemporaryFile obj(".obj");
ScopedTemporaryFile mtl(".mtl"); 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()); boost::nowide::ofstream out(obj.string());
out << "mtllib " << mtl.path().filename().string() << "\n" out << "mtllib " << mtl.path().filename().string() << "\n" << body;
<< "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";
} }
LoadedObj loaded; LoadedObj loaded;
std::string message; std::string message;
@@ -44,6 +38,21 @@ LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::st
return loaded; 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 } // namespace
TEST_CASE("An out-of-range texture index falls back to a zero UV and keeps the geometry", "[OBJ][Regression]") 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].x(), WithinAbs(0.75, 1e-6));
CHECK_THAT(uv[2].y(), WithinAbs(1., 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);
}

Some files were not shown because too many files have changed in this diff Show More