Compare commits

..
Author SHA1 Message Date
Hanif Koh c6d89d6978 Hide Infill in the Shell Drag Mode Only Where the Profile Covers It
A profile with no top or bottom shell layers, or no walls, leaves the infill on the surface,
so hiding it changed the picture. The viewer now reads the print's default region settings
when it loads a print and leaves the infill in place for such a print; per-object overrides
are not consulted.
2026-09-28 17:10:54 +08:00
Hanif Koh abecef53b0 Keep Only the Solid Model and Shell Modes While Dragging
The skip-layers and outer-walls modes drew one layer in every N, and every visual
complaint on the review came from the gaps that leaves at close range. The shell mode
draws every layer and looks the same from outside, and the solid model is the one mode
whose cost does not depend on the toolpaths, so the two of them cover what the stride
modes were for. The layer stride goes with them; a saved preference naming a removed
mode falls back to off.
2026-09-28 17:10:54 +08:00
Hanif Koh 44d5a22a32 Add a Shell-Only Drag Mode That Keeps What Can Be Seen From Outside
A third mode of the drag preference: the reduced set keeps every role but the sparse
infill, internal solid infill, internal bridge infill and gap fill, which lie under the
skins or between the walls, on every layer. Walls, top and bottom surfaces, bridges,
supports and the prime tower are drawn whole, so from outside the print looks as it does
at rest, and the two end layers of the visible range stay whole as in every mode.

The roles come from the slicer, so the classification is exact wherever the slicer's is.
What a role cannot tell apart is the inside of the prime tower or of a support from its
outside; both are kept whole. A profile with no top or bottom shell layers exposes infill,
which this mode hides.
2026-09-28 17:10:54 +08:00
Hanif Koh dec146f1e6 Let the User Choose What the Preview Draws While Dragging
The solid model is one answer to a preview that cannot keep up with a drag; a
reviewer asked for the two that keep the drag view made of toolpaths. The
checkbox is now a combobox, "Simplify preview while dragging", with Off, Solid
model, Skip layers and Outer walls, and a spin, "Draw one layer in every N",
for the toolpath modes. Off is the default, and with it nothing is built.

Skip layers keeps one layer in every N. Outer walls keeps the outer and overhang
perimeters of those layers, so the prime tower and supports, whose segments have
other roles, drop out of it. Both keep the bottom and top layers of the visible
range whole, as the solid model does, so the faces the range cuts open stay what
was printed there.

libvgcode takes the choice as EReducedDetailMode and a stride; the sets are
rebuilt when either changes, and drawing from the reduced one is still a buffer
binding. A mode change reaches the loaded preview at once, as the checkbox did.
2026-09-28 16:20:11 +08:00
Hanif Koh a9fec0ffc4 Release the Reduced Set When Unused and Share the Drag Check With the Scene Cache
The reduced index buffers were only re-uploaded while the preference was on, so
the last set stayed allocated until the next load once it was switched off, and
their size was missing from get_used_gpu_memory(). They are now uploaded on every
rebuild, empty when nothing was built, and counted.

The scene cache and the solid model both asked whether the user was dragging,
with different lists: the cache knew about gizmos and the rectangle selection,
the solid model about the navigator and the sliders. One is_user_interacting()
now answers both.

The tooltip says that negative volumes are not cut out of the solid model, since
load_shells() drops every non-model-part volume, and the preferences handler
keeps the dimming comment with the branch it documents.
2026-09-28 16:20:11 +08:00
Hanif Koh 9ab6bbebf0 Offer the Sliced Objects as a Solid Model While Dragging
The G-code preview draws every toolpath segment as an instanced box, and its frame cost is
linear in the number of segments drawn. On a plate of large objects that is enough that
dragging the camera or a preview slider cannot keep up, and no amount of per-segment work
changes that; only drawing fewer segments does.

The objects themselves are cheaper: a mesh costs its triangles once, however many layers it
has, and the preview already loads the objects as shells for its translucent ghost. A new
preference, "Only render solid model when dragging" (off by default), draws those shells
opaque in their filament colours instead of the toolpaths while the user drags.

The visible layer range still holds. The shells are cut at the range's top and bottom through
the gouraud shader's z range, and libvgcode keeps a second index buffer holding just the
range's bottom and top layers, filled in the same walk as the full one, which is drawn
afterwards so that it caps the cut with what was really printed there. Switching between the
two sets is a buffer binding, never a rebuild. The prime tower is added to the shells from its
sliced mesh while the preference is on, positioned as the print placed it; it keeps its opaque
colour and so stays out of the translucent ghost. Supports have no mesh and are not shown.

Dragging is the camera, the navigator or either slider being held; a slider reports it from
ImGui's active id, since its dirty flag is raised and consumed inside one frame. A wheel step
holds the solid model for a 150 ms settle time, with the frame that restores the toolpaths
scheduled for when it runs out. The switch is decided at the top of the frame, before the
cached scene is consulted, and a frame that switches redraws the scene. A drag cut short by
focus or capture loss is ended explicitly, and a button release wakes the idle loop, since on
some platforms no idle event follows it until the next input.

With the preference off, nothing is built and the preview is unchanged.
2026-09-28 16:20:11 +08:00
HanifKoh cda1588578 Draw the Toolpaths Top-Down When the Camera Looks Down on the Print (#15883)
The segments come in print order, bottom layer first, which seen from above is back to
front: every hidden fragment is shaded before the one that covers it, and on an integrated
GPU that overdraw is most of the frame. Drawing the instances last to first whenever the
camera looks down lets the depth test reject the hidden fragments instead. Side views and
views from below keep the print order, and the shadow-caster pass is unchanged.
2026-09-28 15:36:08 +08:00
Ian Chua ec0d8c225f fix: slicing lifecycle event naming on cancellation (#15889)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Avoid calling Print::output_filename() when a slice is canceled or
fails, since unresolved filename placeholders can throw before G-code
export completes.
Instead, we should use the model name in ctx.name and the stable model
ID in ctx.id, consistently across slice and G-code export events.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
Fix #15885
2026-09-28 12:20:08 +08:00
Ian Chua 3040ebaac1 Merge branch 'main' into fix/slicing-evt-name 2026-09-28 12:19:59 +08:00
Kris Austin 5298e49dd2 fix: Klipper/Moonraker upload errors show a raw Python traceback (#14841) 2026-09-27 19:08:27 -03:00
Kenneth Rapleeandyw4z 4964f49765 Fix size_t/%d format mismatch in MsgDialog::add_button (#15852)
Fix size_t/%d mismatch in MsgDialog button keys

`m_buttons.size()` is a `size_t`, which does not match the `%d` conversion in
printf-style variadics. Build the key with `std::to_string` instead; exact
for any size_t, no behavior change for realistic counts.

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-09-27 19:21:03 +03:00
Chris Bennight ea471dc82d Fix model initialization without a wx application (#15866)
Guard the smooth-normals preference read when CLI thumbnail generation initializes geometry without a wx application. Use the existing flat-normal path in that case and preserve GUI preferences.
2026-09-27 18:53:11 +03:00
Kiss Lorand bd65cce3f4 Unify GUI scroll rates (#15902)
Use DPI-aware 20 DIP vertical scrolling for general-purpose GUI
scroll areas.

Keep list-based views aligned to their item height so one wheel increment follows the visible row rhythm.

This makes Preferences and other dialogs scroll consistently across
Windows DPI settings.
2026-09-27 16:58:10 +03:00
Ian Chua d347a80ef8 Merge branch 'main' into fix/slicing-evt-name 2026-09-27 17:57:40 +08:00
peachismomo e340a13c18 fix: use current print for slicing lifecycle identity 2026-09-27 17:53:41 +08:00
peachismomo 9e16cdb23b fix: comment above reset_export 2026-09-27 17:29:25 +08:00
peachismomo 58842bab05 test: test for SliceStarted ensuring stable model ID is presetn when model name is absent 2026-09-27 17:29:01 +08:00
peachismomo 04204ec0d3 test: cover slicing lifecycle event context 2026-09-27 17:25:56 +08:00
Ian Chua 9ed459e132 fix: slicing event name and ID 2026-09-25 13:25:45 +08:00
93 changed files with 1915 additions and 3282 deletions
+80
View File
@@ -0,0 +1,80 @@
# G-code preview while dragging
The sliced preview draws every toolpath segment of the plate as an instanced box. On a large
plate that is tens of millions of segments, and the frame is GPU-bound: the cost is the number of
instances drawn, not anything the CPU does per frame. Dragging the camera over such a plate cannot
keep up. The `preview_reduced_detail_mode` preference (*Graphics > G-code Preview*, off by
default) lets the preview draw less while the user drags and put the full toolpaths back when they
let go.
| Preference | Values | Effect |
|---|---|---|
| `preview_reduced_detail_mode` | `off`, `solid`, `shell` | what is drawn while dragging |
libvgcode (`src/libvgcode`) builds and binds the reduced toolpath set, `GCodeViewer` maps the
preferences onto it and draws the solid model, and `GLCanvas3D` decides when the user is dragging.
The OpenGL ES path keeps a single set and ignores the preference.
## Two sets, one walk
`ViewerImpl::update_enabled_entities()` walks the visible vertex range once and fills two segment
index buffers side by side: the **full** set and the **reduced** set (segments and options).
Building them together is what makes switching free: starting or ending a drag is a buffer
binding, never a rebuild. A change of mode does rebuild. Nothing is built while the mode
is off, and the reduced buffers are then uploaded empty so that the last set does not stay
allocated.
Whatever the mode leaves out, the bottom and top layers of the visible range are kept whole: they
are the faces the range cuts open, and the top is what the user is looking at.
### Modes
- `EndLayersOnly` (`solid` in the preference) keeps only the two end layers. `GCodeViewer` then
draws the sliced objects and the prime tower as opaque solids, see below.
- `ShellOnly` (`shell`) keeps every role but the sparse infill, internal solid infill, internal
bridge infill and gap fill, which lie under the skins or between the walls. Walls, top and
bottom surfaces, bridges, supports and the prime tower are drawn whole, so from outside the
print looks as it does at rest. The roles come from
the slicer, so the classification is exact wherever the slicer's is; what it cannot express is
the inside of the prime tower and of a support, which share one role with their outside and are
kept whole. A profile with no top or bottom shell layers, or no walls, leaves the infill on the
surface, so `GCodeViewer` turns the hiding off for such a print when it loads it, from the
print's default region settings; per-object overrides are not consulted.
## The solid model
The preview already loads the sliced objects as shells for its translucent ghost.
`GCodeViewer::render_solid_model()` draws those shells opaque, in their filament colors, with the
`gouraud` shader, whose z range cuts them to the visible layer range. The two toolpath layers of
the reduced set are drawn afterwards and cap the cut with what was really printed there. The
shells hold only the objects, so while this mode is on the prime tower is added from its sliced
mesh, positioned as the print placed it. It is added or removed on its own when the mode changes,
without reloading the objects, keeps its opaque color so that it never appears among the
translucent shells, and stays out of their bounding box. Supports have no mesh and are not shown,
and `load_shells()` drops every non-model-part volume, so a negative volume is not cut out.
A plate whose shells are not loaded keeps drawing toolpaths, since the solid model would leave
only the end layers.
## Deciding that the user is dragging
`GLCanvas3D::_update_preview_interaction()` runs at the top of every preview frame, before the
canvas decides whether to reuse its cached scene, so that the switch lands in that frame. Dragging
is `GLCanvas3D::is_user_interacting()`, the same answer the scene cache reads: the camera, the
navigator, a gizmo, the rectangle selection or either slider being held. A slider reports this from
ImGui's active id rather than its dirty flag, which is raised and consumed inside one frame. A
wheel step has no duration, so it holds the reduced set for a 150 ms settle time instead, and the
frame that restores the toolpaths is scheduled for when that time runs out, since the render timer
only wakes the idle loop. A drag cut short by focus or capture loss is ended explicitly, and a
button release wakes the idle loop, because on some platforms nothing else would until the next
input.
## Reused scene frames
`GLCanvas3D` keeps its last scene pass for frames that only rebuild the overlay (`SceneCache`). Its
key covers the canvas size, the camera and hover state, not what the toolpath sets draw, so a frame
that reuses the scene must never be one on which the set is switched.
`_update_preview_interaction()` therefore reports whether the bound set changed, and a frame on
which it did redraws the scene. The canvas neither captures nor reuses the scene while the user
drags, so no reduced frame outlives a drag, and the frame that ends a wheel's settle time is
requested as a full frame.
+19 -45
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
vendors keep theirs — even when the bumped vendor is the shared Orca filament
library everyone else inherits from.
- The setup wizard loads its vendors through the same routine as startup, so it
gets the same speedup without a second code path.
- The setup wizard, which loads vendors one at a time, gets the same speedup as
startup without a second code path.
- A vendor with no cache, or a broken one, costs only that vendor a parse.
A cache holds *system* presets only. User presets, project settings and modified
@@ -162,20 +162,15 @@ cache nothing can invalidate is worse than no cache.
Vendors load in a fixed order, because filament inheritance crosses exactly one
boundary: any vendor's filament may inherit from the shared Orca filament library,
and nothing else reaches across vendors — an `include` is always vendor-local. Only
installing a vendor's presets crosses it; reading the vendor, from its cache or its
JSONs, needs nothing from the library. So every other vendor is read while the library
loads, each is installed against it as soon as both are done, and the results are
merged in a stable order:
and nothing else reaches across vendors — an `include` is always vendor-local. The
library therefore goes first, alone; every other vendor follows in parallel, resolving
against it; and the results are merged in a stable order:
```mermaid
flowchart LR
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"]
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"]
```
`PresetBundle::load_vendors` runs these steps for startup and for the setup wizard,
which hand it the vendors to load and the directory each is installed in.
Whether a vendor comes from its cache or from a parse changes nothing in that
order — both produce the same bundle, so cached and parsed vendors mix freely in
one startup.
@@ -216,12 +211,11 @@ shipped cache answered first, so the profile in `<data_dir>/system/` was never p
and its cache was never written back.
Serving from a cache is not a memory-image restore. The entries are deserialized and
then installed by `install_vendor`, the routine the JSON path hands the vendor's entries
to once it has parsed the sub-files: inheritance resolved against the presets installed
before them and the currently loaded filament library, includes layered in, configs
flattened onto the collection defaults, validated and registered. An `include` layers
what the included base states, between the parent and the preset's own keys: the base's
diff against the
then installed one by one — inheritance resolved against the presets installed before
them and the currently loaded filament library, includes layered in, configs flattened
onto the collection defaults, validated and registered — by the same function the JSON
path calls straight after parsing a sub-file. An `include` layers what the included
base states, between the parent and the preset's own keys: the base's diff against the
default, taken when the base itself was installed and before the per-variant padding
`inherits` sees, so only what a template sets reaches the presets including it. The two
paths share everything below the parse, which is what makes a cache-loaded bundle
@@ -229,16 +223,6 @@ indistinguishable from a JSON-loaded one by construction rather than by test cov
Installation also rebuilds each preset's file path from the local data directory, so a
shipped cache never carries the generating machine's paths.
Installing an entry is split in two. `resolve_vendor_preset` flattens it, reading only
what is registered under the names it inherits and includes, and `commit_vendor_preset`
registers it, the only step that writes anything shared. Entries resolve across threads
in runs and commit in the order the vendor lists them. A run ends before an entry that
inherits or includes one already in it, since that one's commit registers what the
entry resolves against, so no entry in a run reads what another in it registers. An
entry's parse messages are held until it commits. The bundle, the log's parse and
install messages and the error count therefore come out as parsing and installing one
entry at a time would leave them, whatever the listing order.
App upgrades work because a cache normally survives one. Only a deliberate
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
install time rather than at load: a vendor whose cache this build cannot read counts
@@ -270,20 +254,17 @@ the wizard caches the *derived JSON*, not another form of the inputs:
open, the wizard computes the current stamps (one version peek per vendor) and, when
they match, serves the catalog from the file — no bundle built, no preset installed.
Caching bundle inputs instead was tried and measured: rebuilding the bundle from
per-vendor caches costs over a second of preset installation whatever feeds it, so
only skipping the rebuild entirely wins.
per-vendor caches costs ~2 s of preset installation whatever feeds it, so only
skipping the rebuild entirely wins.
Any change to the set — a vendor added, removed or updated, or its cache-only
`.opc` replaced by a newer one — changes the stamps and retires the whole file;
the wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load
startup uses (per-vendor caches serving where they cover), and writes the catalog
back. When a vendor fails to load, the filament library included, that open falls
back to the wizard's own scan of the vendor JSONs, as when no bundle can be built,
and writes nothing. Selections, region and per-open decorations are applied
downstream of the cache either way, so a served catalog is indistinguishable from a
rebuilt one. Nothing ships this file and the updater never touches it; it is a
locally written artifact, re-derived whenever stale, written through a temp file and
rename so half a cache is never readable.
the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where
they cover) and writes the catalog back. Selections, region and per-open decorations
are applied downstream of the cache either way, so a served catalog is
indistinguishable from a rebuilt one. Nothing ships this file and the updater never
touches it; it is a locally written artifact, re-derived whenever stale, written
through a temp file and rename so half a cache is never readable.
The 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
@@ -397,13 +378,6 @@ enumerates only `*.json` will find no vendors at all in a packaged build.
the `CachedPreset` field list — written and read by `visit_entry` in
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
- **Adding a kind of reference between presets**, as `inherits` and `include` are:
parse the names into `CachedPreset` (a field change, so `CACHE_VERSION` is bumped),
have `install_vendor_entries` end a run before an entry that names one already in it
and retain what the names point at, look them up only in `resolve_vendor_preset`, and
register what they point at only in `commit_vendor_preset`. The listing-order test in
`test_vendor_cache.cpp` fails for a kind the runs do not check once its fixture uses
it.
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
65535 options and one cache at most 65535 distinct enum value names.
`CacheDictionary::save` throws past that, which surfaces when CI generates the
+67 -42
View File
@@ -5,7 +5,6 @@
//BBS
#include "Preset.hpp"
#include "Exception.hpp"
#include "InstanceLock.hpp"
#include "LocalesUtils.hpp"
#include "Thread.hpp"
#include "format.hpp"
@@ -206,6 +205,13 @@ void AppConfig::set_defaults()
if (get("seq_top_layer_only").empty())
set("seq_top_layer_only", "1");
// what the preview draws while the user drags it
{
const std::string mode = get("preview_reduced_detail_mode");
if (mode != "off" && mode != "solid" && mode != "shell")
set("preview_reduced_detail_mode", "off");
}
// ORCA: darken the layers the preview layer slider is not scrubbed to
if (get("preview_dim_previous_layers").empty())
set_bool("preview_dim_previous_layers", false);
@@ -740,13 +746,10 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs)
#ifdef USE_JSON_CONFIG
std::string AppConfig::load(bool read_only)
std::string AppConfig::load()
{
json j;
// Keep another instance from replacing or restoring the file mid-read.
InstanceLock instance_lock(read_only ? std::string() : lock_path());
// 1) Read the complete config file into a boost::property_tree.
namespace pt = boost::property_tree;
pt::ptree tree;
@@ -996,6 +999,7 @@ void AppConfig::save()
// The config is first written to a file with a PID suffix and then moved
// to avoid race conditions with multiple instances of Slic3r
const auto path = config_path();
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
json j;
@@ -1125,18 +1129,43 @@ void AppConfig::save()
j["local_machines"][local_machine.first] = m_json;
}
const std::string config_str = j.dump(1, '\t');
if (write_config_file(path, config_str + "\n", config_str))
m_dirty = false;
boost::nowide::ofstream c;
c.open(path_pid, std::ios::out | std::ios::trunc);
c << j.dump(1, '\t') << std::endl;
#ifdef WIN32
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
// we save the config file into a backup first before moving it to the final destination.
c << appconfig_md5_hash_line(j.dump(1, '\t'));
#endif
c.close();
if (c.fail()) {
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
return;
}
#ifdef WIN32
// Make a backup of the configuration file before copying it to the final destination.
std::string error_message;
std::string backup_path = (boost::format("%1%.bak") % path).str();
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
#endif
// Rename the config atomically.
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
// To cope with that, we already made a backup of the config on Windows.
rename_file(path_pid, path);
m_dirty = false;
}
#else
std::string AppConfig::load(bool read_only)
std::string AppConfig::load()
{
// Keep another instance from replacing or restoring the file mid-read.
InstanceLock instance_lock(read_only ? std::string() : lock_path());
// 1) Read the complete config file into a boost::property_tree.
namespace pt = boost::property_tree;
pt::ptree tree;
@@ -1274,6 +1303,7 @@ void AppConfig::save()
// The config is first written to a file with a PID suffix and then moved
// to avoid race conditions with multiple instances of Slic3r
const auto path = config_path();
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
std::stringstream config_ss;
if (m_mode == EAppMode::Editor)
@@ -1309,38 +1339,38 @@ void AppConfig::save()
// One empty line before the MD5 sum.
config_ss << std::endl;
const std::string config_str = config_ss.str();
if (write_config_file(path, config_str, config_str))
m_dirty = false;
}
#endif
bool AppConfig::write_config_file(const std::string &path, std::string body, const std::string &checksum_source)
{
// Everything before this is assembly; only the writes need the other instances kept out.
InstanceLock instance_lock(lock_path());
std::string config_str = config_ss.str();
boost::nowide::ofstream c;
c.open(path_pid, std::ios::out | std::ios::trunc);
c << config_str;
#ifdef WIN32
// WIN32 specific: the final replace is not safe in case of an application crash, there is no atomic "rename file" API
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
// we save the config file into a backup first before moving it to the final destination.
body += appconfig_md5_hash_line(checksum_source);
c << appconfig_md5_hash_line(config_str);
#endif
// Not flushed to the device: the idle handler saves on the GUI thread after
// any change, and the rename already gives a complete old or new file.
if (const std::error_code ec = write_file_atomically(path, body)) {
BOOST_LOG_TRIVIAL(error) << "Failed to write the configuration " << path << ": " << ec.message() << "; trying again in 10 s";
m_retry_save_at = std::chrono::steady_clock::now() + std::chrono::seconds(10);
return false;
c.close();
if (c.fail()) {
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
return;
}
m_retry_save_at = {};
#ifdef WIN32
// Written after the config, so the backup never holds a state that was not confirmed written.
const std::string backup_path = (boost::format("%1%.bak") % path).str();
if (const std::error_code ec = write_file_atomically(backup_path, body))
BOOST_LOG_TRIVIAL(error) << "Failed to write the backup configuration " << backup_path << ": " << ec.message();
// Make a backup of the configuration file before copying it to the final destination.
std::string error_message;
std::string backup_path = (boost::format("%1%.bak") % path).str();
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
#endif
return true;
// Rename the config atomically.
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
// To cope with that, we already made a backup of the config on Windows.
rename_file(path_pid, path);
m_dirty = false;
}
#endif
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
{
@@ -1830,11 +1860,6 @@ void AppConfig::reset_selections()
}
}
std::string AppConfig::lock_path()
{
return Slic3r::data_dir().empty() ? std::string() : config_path() + ".lock";
}
std::string AppConfig::config_path()
{
#ifdef USE_JSON_CONFIG
@@ -1871,7 +1896,7 @@ bool AppConfig::exists()
std::string AppConfig::load_if_exists()
{
return boost::filesystem::exists(loading_path()) ? load(/*read_only=*/true) : std::string();
return boost::filesystem::exists(loading_path()) ? load() : std::string();
}
}; // namespace Slic3r
+1 -16
View File
@@ -2,7 +2,6 @@
#define slic3r_AppConfig_hpp_
#include <set>
#include <chrono>
#include <map>
#include <string>
#include "nlohmann/json.hpp"
@@ -123,18 +122,14 @@ public:
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
// Return an error string, or an empty string on success.
std::string load(bool read_only = false);
std::string load();
// Treat a missing config as default state; otherwise load it normally.
// The CLI's load: it never saves, so it takes no lock and creates no lock file.
std::string load_if_exists();
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
void save();
// Does this config need to be saved?
bool dirty() const { return m_dirty; }
// False for ten seconds after a failed write, so the idle handler does not
// repeat a hopeless attempt on every event; an explicit save() always tries.
bool save_due() const { return std::chrono::steady_clock::now() >= m_retry_save_at; }
void set_dirty() { m_dirty = true; }
@@ -344,8 +339,6 @@ public:
// Get the default config path from Slic3r::data_dir().
std::string config_path();
// Lock file guarding config_path() against other running instances; empty without a data dir.
std::string lock_path();
// Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
bool legacy_datadir() const { return m_legacy_datadir; }
@@ -456,16 +449,8 @@ private:
// Preset for each machine
MachineSettingMap m_printer_settings;
// Writes the assembled config text, and on Windows its checksum and a backup copy; false when the
// config itself could not be written, in which case the caller stays dirty and retries. `checksum_source`
// is the text load() will verify, which for the JSON config ends before the trailing newline.
bool write_config_file(const std::string &path, std::string body, const std::string &checksum_source);
// Has any value been modified since the config.ini has been last saved or loaded?
bool m_dirty;
// After a failed write, save_due() is false for the next ten seconds, so the
// idle handler does not repeat a hopeless write on every event.
std::chrono::steady_clock::time_point m_retry_save_at{};
// Original version found in the ini file before it was overwritten
Semver m_orig_version;
// Whether the existing version is before system profiles & configuration updating
-3
View File
@@ -304,8 +304,6 @@ set(lisbslic3r_sources
Geometry/VoronoiUtils.cpp
Geometry/VoronoiUtils.hpp
Geometry/VoronoiVisualUtils.hpp
InstanceLock.cpp
InstanceLock.hpp
Int128.hpp
KDTreeIndirect.hpp
Layer.cpp
@@ -355,7 +353,6 @@ set(lisbslic3r_sources
Optimize/Optimizer.hpp
Orient.cpp
Orient.hpp
ParallelResolve.hpp
ParameterUtils.cpp
ParameterUtils.hpp
pchheader.cpp
+19 -30
View File
@@ -849,19 +849,6 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC
return std::move(substitutions_ctxt.substitutions);
}
// Case-insensitive compare of a JSON key against a fixed ASCII one, without
// boost::iequals, whose std::locale() takes a lock the whole process shares in the
// MSVC runtime.
static bool ascii_iequals(const std::string &key, const char *literal)
{
auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; };
size_t i = 0;
for (; i < key.size() && literal[i] != '\0'; ++ i)
if (lower(key[i]) != lower(literal[i]))
return false;
return i == key.size() && literal[i] == '\0';
}
int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
{
json j;
@@ -929,44 +916,44 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
}
//parse the json elements
for (auto it = j.begin(); it != j.end(); it++) {
if (ascii_iequals(it.key(), BBL_JSON_KEY_VERSION)) {
if (boost::iequals(it.key(),BBL_JSON_KEY_VERSION)) {
key_values.emplace(BBL_JSON_KEY_VERSION, it.value());
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
//skip it
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_NAME)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) {
key_values.emplace(BBL_JSON_KEY_NAME, it.value());
if (it.value() == "project_settings")
is_project_settings = true;
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_URL)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) {
key_values.emplace(BBL_JSON_KEY_URL, it.value());
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_TYPE)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_TYPE)) {
key_values.emplace(BBL_JSON_KEY_TYPE, it.value());
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
key_values.emplace(BBL_JSON_KEY_SETTING_ID, it.value());
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
key_values.emplace(BBL_JSON_KEY_FILAMENT_ID, it.value());
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FROM)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_FROM)) {
key_values.emplace(BBL_JSON_KEY_FROM, it.value());
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
key_values.emplace(BBL_JSON_KEY_DESCRIPTION, it.value());
}
else if (ascii_iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
else if (boost::iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
key_values.emplace(BBL_JSON_KEY_INSTANTIATION, it.value());
}
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
}
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
} else if (ascii_iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
} else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
} else {
t_config_option_key opt_key = it.key();
@@ -1538,10 +1525,12 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
std::ostringstream ss;
this->save_to_json(ss, name, from, version);
if (const std::error_code ec = write_file_atomically(file, ss.str()))
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": failed to save config to %1%: %2%") % file % ec.message();
else
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << ss.str();
c.close();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
}
void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const
+10 -5
View File
@@ -2504,7 +2504,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2556,7 +2557,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
}
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + err_msg;
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2580,7 +2582,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
boost::nowide::remove(path_tmp.c_str());
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + ex.what();
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2693,7 +2696,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
if (ret) {
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2712,7 +2716,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
-165
View File
@@ -1,165 +0,0 @@
#include "InstanceLock.hpp"
#include <map>
#include <memory>
#include <system_error>
#include <thread>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#ifdef _WIN32
#include <boost/interprocess/sync/file_lock.hpp>
#include <boost/nowide/convert.hpp>
#else
#include <cerrno>
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
#endif
namespace Slic3r {
#ifdef _WIN32
// LockFileEx, held by this handle alone.
using NativeFileLock = boost::interprocess::file_lock;
#else
// flock(2) rather than an fcntl lock: it belongs to this open file description,
// so any other code in the process that opens and closes the lock file, as a
// backup or an export walking the data dir might, cannot drop it. An fcntl
// lock would go with the first such close.
class NativeFileLock
{
public:
explicit NativeFileLock(const char *path) : m_fd(::open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0644))
{
if (m_fd < 0)
throw std::system_error(errno, std::generic_category(), path);
}
~NativeFileLock() { ::close(m_fd); }
bool try_lock()
{
if (::flock(m_fd, LOCK_EX | LOCK_NB) == 0)
return true;
// A signal (a child exiting, for one) interrupts the call like any other; the caller polls again.
if (errno == EWOULDBLOCK || errno == EINTR)
return false;
throw std::system_error(errno, std::generic_category(), "flock");
}
void unlock() { ::flock(m_fd, LOCK_UN); }
private:
int m_fd;
};
#endif
// One slot per lock file, shared by every guard in the process: one lock
// object per path behind a mutex is what makes the guard re-entrant and safe
// to use from the preset sync thread and the GUI thread at once. The lock
// file is opened by the outermost guard and closed when it goes, so the file
// is never held open between guards: whatever is at the path is what gets
// locked, and a data dir can be removed once nothing is saving into it. The
// file is kept rather than deleted on release because the lock state lives in
// the kernel on the open file, and deleting it would let a third instance
// lock a fresh file while the second still holds the old one.
struct InstanceLock::Slot
{
std::recursive_mutex mutex;
// Non-null exactly while this process holds the file lock.
std::unique_ptr<NativeFileLock> file_lock;
int depth{0};
// Until this point, after a guard could not open, lock or wait out the
// file, guards do not touch it.
std::chrono::steady_clock::time_point cooldown_until{};
};
InstanceLock::Slot &InstanceLock::slot_for(const std::string &lock_file_path)
{
// Never freed: a save during static destruction still needs its slot.
static auto *registry_mutex = new std::mutex();
static auto *registry = new std::map<std::string, std::unique_ptr<Slot>>();
std::lock_guard<std::mutex> guard(*registry_mutex);
std::unique_ptr<Slot> &slot = (*registry)[lock_file_path];
if (! slot)
slot = std::make_unique<Slot>();
return *slot;
}
// Starts the cool-down. Called with the slot mutex held.
void InstanceLock::defer(Slot &slot, const std::string &reason)
{
slot.cooldown_until = std::chrono::steady_clock::now() + cooldown;
BOOST_LOG_TRIVIAL(warning) << reason << "; proceeding without the lock for the next " << cooldown.count() << " ms";
}
// Creates the lock file if needed and opens it, or starts the cool-down.
// Called with the slot mutex held.
bool InstanceLock::open_lock_file(Slot &slot, const std::string &lock_file_path)
{
try {
#ifdef _WIN32
// The lock opens an existing file; created once, on the first miss.
const std::wstring wide_path = boost::nowide::widen(lock_file_path);
try {
slot.file_lock = std::make_unique<NativeFileLock>(wide_path.c_str());
} catch (const std::exception &) {
boost::nowide::ofstream(lock_file_path, std::ios::app).close();
slot.file_lock = std::make_unique<NativeFileLock>(wide_path.c_str());
}
#else
slot.file_lock = std::make_unique<NativeFileLock>(lock_file_path.c_str());
#endif
return true;
} catch (const std::exception &e) {
defer(slot, "Cannot open lock file " + lock_file_path + ": " + e.what() + " (check its owner and permissions)");
return false;
}
}
InstanceLock::InstanceLock(const std::string &lock_file_path, std::chrono::milliseconds timeout)
{
if (lock_file_path.empty())
return;
m_slot = &slot_for(lock_file_path);
m_slot_guard = std::unique_lock<std::recursive_mutex>(m_slot->mutex);
const auto now = std::chrono::steady_clock::now();
if (m_slot->depth == 0 && now >= m_slot->cooldown_until && open_lock_file(*m_slot, lock_file_path)) {
const auto deadline = now + timeout;
bool taken = false;
for (;;) {
try {
if ((taken = m_slot->file_lock->try_lock()))
break;
} catch (const std::exception &e) {
defer(*m_slot, "Cannot lock " + lock_file_path + ": " + e.what());
break;
}
if (std::chrono::steady_clock::now() >= deadline) {
defer(*m_slot, "Another instance has held " + lock_file_path + " for over " + std::to_string(timeout.count()) + " ms");
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
if (! taken)
m_slot->file_lock.reset();
}
// Counted last, so a throw above leaves the slot exactly as it was found.
++ m_slot->depth;
m_locked = m_slot->file_lock != nullptr;
}
InstanceLock::~InstanceLock()
{
if (m_slot == nullptr)
return;
if (-- m_slot->depth == 0 && m_slot->file_lock) {
try {
m_slot->file_lock->unlock();
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(warning) << "Cannot unlock instance lock: " << e.what();
}
m_slot->file_lock.reset();
}
}
} // namespace Slic3r
-60
View File
@@ -1,60 +0,0 @@
#pragma once
#include <chrono>
#include <mutex>
#include <string>
namespace Slic3r {
// Scoped write lock on a file shared by every running instance of the
// application, such as the app config or the user preset directory: threads
// of this process are serialised through a recursive mutex, other processes
// through an advisory OS file lock on `lock_file_path`. The lock file is
// created on first use and kept; the OS releases the lock when its holder
// exits, so a crashed instance never leaves a stale lock behind.
//
// Best effort: when the lock file cannot be opened or locked, or another
// instance still holds it after `timeout`, the guard keeps only the in-process
// mutex, locked() reports false and the write proceeds, since a hung instance
// must never block another one from saving. For `cooldown` afterwards guards
// leave the file alone. The wait for the in-process mutex is bounded only by
// the longest critical section, so a guard covers a few file operations and
// nothing slower.
//
// Lock order: the preset collection mutex may be held when a guard is taken
// (set_sync_info_and_save() calls save_info() under it), never the reverse;
// that is why the guards sit at the leaf readers and writers and why a guard
// must not be added around save_user_presets(), which takes the collection
// mutex through delete_preset().
class InstanceLock
{
public:
// Long against a critical section of milliseconds, short against the GUI
// thread, which is where most guards are taken.
static constexpr std::chrono::milliseconds default_timeout{1000};
// Long enough that a holder stuck in a debugger does not cost a stall per
// save; mutable so tests can shorten it.
static inline std::chrono::milliseconds cooldown{10000};
// An empty path makes the guard a no-op.
explicit InstanceLock(const std::string &lock_file_path, std::chrono::milliseconds timeout = default_timeout);
~InstanceLock();
InstanceLock(const InstanceLock &) = delete;
InstanceLock &operator=(const InstanceLock &) = delete;
// True while this process holds the cross-process file lock.
bool locked() const { return m_locked; }
private:
struct Slot;
static Slot &slot_for(const std::string &lock_file_path);
static bool open_lock_file(Slot &slot, const std::string &lock_file_path);
static void defer(Slot &slot, const std::string &reason);
Slot *m_slot{nullptr};
std::unique_lock<std::recursive_mutex> m_slot_guard;
bool m_locked{false};
};
} // namespace Slic3r
-16
View File
@@ -13,19 +13,8 @@
namespace Slic3r {
// How many setters this thread holds, so the ones nested in another can skip
// setlocale, which takes a lock the whole process shares on Windows.
static thread_local int s_numeric_locale_depth = 0;
CNumericLocalesSetter::CNumericLocalesSetter()
{
// Nested in another setter on this thread, whose "C" the separator check
// confirms is still set.
if (s_numeric_locale_depth > 0 && is_decimal_separator_point()) {
m_nested = true;
++ s_numeric_locale_depth;
return;
}
#ifdef _WIN32
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr);
@@ -40,17 +29,12 @@ CNumericLocalesSetter::CNumericLocalesSetter()
m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale);
uselocale(m_new_locale);
#endif
// Counted last, since the destructor does not run for a constructor that throws.
++ s_numeric_locale_depth;
}
CNumericLocalesSetter::~CNumericLocalesSetter()
{
-- s_numeric_locale_depth;
if (m_nested)
return;
#ifdef _WIN32
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
#else
-5
View File
@@ -19,13 +19,8 @@ class CNumericLocalesSetter {
public:
CNumericLocalesSetter();
~CNumericLocalesSetter();
// A copy would restore the locale twice, and count down once more than up.
CNumericLocalesSetter(const CNumericLocalesSetter&) = delete;
CNumericLocalesSetter& operator=(const CNumericLocalesSetter&) = delete;
private:
// Inside another setter on this thread, which does the setting and restoring.
bool m_nested { false };
#ifdef _WIN32
std::string m_orig_numeric_locale;
#else
-65
View File
@@ -1,65 +0,0 @@
#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
+170 -353
View File
@@ -5,8 +5,6 @@
#include "Preset.hpp"
#include "PresetBundle.hpp"
#include "AppConfig.hpp"
#include "LocalesUtils.hpp"
#include "ParallelResolve.hpp"
#ifdef _MSC_VER
#define WIN32_LEAN_AND_MEAN
@@ -51,9 +49,6 @@
#include "libslic3r.h"
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "InstanceLock.hpp"
#include <sstream>
#include "Time.hpp"
#include "PlaceholderParser.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
@@ -110,32 +105,6 @@ std::string get_preset_canonical_name(const std::string &preset_bare_name, const
}
}
std::string user_presets_lock_path(bool read_only)
{
return read_only || data_dir().empty() ? std::string() : (fs::path(data_dir()) / (PRESET_USER_DIR ".lock")).string();
}
// Removes a preset file the scan could not load, and its .info, under the lock.
// The scan passes read_only when it could not take the lock, since the file may
// then be another instance's fresh write that it merely raced.
static void remove_preset_files(const std::string &preset_file, bool read_only)
{
if (read_only)
return;
InstanceLock instance_lock(user_presets_lock_path());
// A symlink whose target is missing stays, since the target may come back; one
// that cannot even be followed is removed like any other unreadable file.
auto remove = [](const fs::path &file) {
boost::system::error_code ec;
if (fs::status(file, ec).type() != fs::file_not_found)
fs::remove(file, ec);
};
fs::path file_path(preset_file);
remove(file_path);
file_path.replace_extension(".info");
remove(file_path);
}
std::string get_preset_bare_name(const std::string &canonical_name)
{
const auto pos = canonical_name.find_last_of('/');
@@ -537,11 +506,10 @@ void Preset::normalize(DynamicPrintConfig &config)
handle_legacy_sla(config);
}
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
const DynamicPrintConfig *added)
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config)
{
std::string incorrect_keys;
for (const std::string &key : (added != nullptr ? *added : config).keys())
for (const std::string &key : config.keys())
if (! default_config.has(key)) {
if (incorrect_keys.empty())
incorrect_keys = key;
@@ -653,6 +621,7 @@ void Preset::load_info(const std::string& file)
}
else if (v.first.compare("base_id") == 0) {
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)
this->base_id.clear();
}
@@ -678,20 +647,18 @@ void Preset::save_info(std::string file)
file = idx_file.string();
}
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
std::string sync_info_to_save;
//BBS: hold is used for stop requesting to server this time
if (this->sync_info.compare("hold") != 0)
sync_info_to_save = this->sync_info;
std::ostringstream c;
c << "sync_info" << " = " << sync_info_to_save << std::endl;
c << "user_id" << " = " << this->user_id << std::endl;
c << "setting_id" << " = " << this->setting_id << std::endl;
c << "base_id" << " = " << this->base_id << std::endl;
c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl;
InstanceLock instance_lock(user_presets_lock_path());
if (const std::error_code ec = write_file_atomically(file, c.str()))
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << file << ": " << ec.message();
c.close();
}
void Preset::remove_files(bool cloud_already_deleted)
@@ -700,7 +667,6 @@ void Preset::remove_files(bool cloud_already_deleted)
if (this->is_project_embedded) {
return;
}
InstanceLock instance_lock(user_presets_lock_path());
// Erase the preset file.
boost::nowide::remove(this->file.c_str());
fs::path idx_path(this->file);
@@ -718,11 +684,11 @@ void Preset::remove_files(bool cloud_already_deleted)
}
//BBS: add logic for only difference save
bool Preset::save(DynamicPrintConfig* parent_config)
void Preset::save(DynamicPrintConfig* parent_config)
{
//BBS: add project embedded preset logic
if (this->is_project_embedded)
return true;
return;
//BBS: change to json format
//this->config.save(this->file);
std::string from_str;
@@ -737,16 +703,12 @@ bool Preset::save(DynamicPrintConfig* parent_config)
else
from_str = std::string("Default");
boost::filesystem::create_directories(fs::path(this->file).parent_path());
const std::string bare_name = get_preset_bare_name(this->name);
// What gets written: the diff against the parent, the config plus its
// filament id, or the config as is. Built before the lock is taken so the
// exclusive window covers only the file writes.
DynamicPrintConfig temp_config;
const DynamicPrintConfig *to_save = &this->config;
//BBS: only save difference if it has parent
if (parent_config) {
DynamicPrintConfig temp_config;
std::vector<std::string> dirty_options = config.diff(*parent_config);
std::string extruder_id_name, extruder_variant_name;
@@ -782,22 +744,13 @@ bool Preset::save(DynamicPrintConfig* parent_config)
opt_dst->set(opt_src);
}
}
to_save = &temp_config;
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
} else if (!filament_id.empty() && inherits().empty()) {
temp_config = config;
DynamicPrintConfig temp_config = config;
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
to_save = &temp_config;
}
std::ostringstream json;
to_save->save_to_json(json, bare_name, from_str, this->version.to_string());
InstanceLock instance_lock(user_presets_lock_path());
boost::filesystem::create_directories(fs::path(this->file).parent_path());
if (const std::error_code ec = write_file_atomically(this->file, json.str())) {
// No .info either: one without its preset reads as a cloud deletion request.
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << this->file << ": " << ec.message();
return false;
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
} else {
this->config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id;
@@ -807,7 +760,6 @@ bool Preset::save(DynamicPrintConfig* parent_config)
idx_file.replace_extension(".info");
this->save_info(idx_file.string());
}
return true;
}
void Preset::reload(Preset const &parent)
@@ -819,7 +771,6 @@ void Preset::reload(Preset const &parent)
std::string reason;
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
try {
InstanceLock instance_lock(user_presets_lock_path());
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
this->config = parent.config;
this->config.apply(std::move(config));
@@ -1718,186 +1669,6 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
return get_preset_canonical_name(parsed.bare, origin);
}
PresetCollection::PresetFilesOnDisk PresetCollection::PresetFilesOnDisk::read(const boost::filesystem::path &file)
{
auto read_if_present = [](const fs::path &path) -> std::optional<std::string> {
std::string bytes;
try {
load_string_file(path, bytes);
} catch (const std::exception &) {
return std::nullopt;
}
return bytes;
};
fs::path info_path(file);
info_path.replace_extension(".info");
return { read_if_present(file), read_if_present(info_path) };
}
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();
// Before either file is parsed, so a save that lands during the parse still shows
// up as a difference when commit compares.
out.on_disk = PresetFilesOnDisk::read(file);
// 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)
remove_preset_files(loaded.preset.file, read_only);
if (!loaded.install)
return;
if (loaded.complete) {
if (loaded.save_compatible_printers) {
// The preset loads with the derived printer whether or not its file
// can be rewritten.
try {
if (read_only)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name << " (not written back)";
else if (preset.save(nullptr))
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name;
else
++m_errors; // save() logged why
} catch (const std::runtime_error &err) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " can not write compatible_printers back to " << preset.file << ": " << err.what();
}
}
//BBS: add some workaround for previous incorrect settings
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
preset.setting_id.clear();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << preset.name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
// add alias for custom filament preset
set_custom_preset_alias(preset);
}
if (preset_loaded_fn != nullptr)
preset_loaded_fn(preset);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
presets_loaded.emplace_back(std::move(preset));
}
// Load all presets found in dir_path.
// Throws an exception on error.
void PresetCollection::load_presets(
@@ -1935,8 +1706,6 @@ void PresetCollection::load_presets(
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);
struct UserPresetFile { fs::path path; std::string canonical_name; };
std::vector<UserPresetFile> files;
//BBS: change to json format
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
{
@@ -1952,47 +1721,149 @@ void PresetCollection::load_presets(
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
continue;
}
files.push_back({ dir_entry.path(), std::move(canonical_name) });
}
}
resolve_then_commit<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 i, UserPresetLoad &&loaded) {
// Resolve read the files without the lock, so another instance may have saved
// over them since. A file that changed is resolved again under the lock before
// commit removes or rewrites it, which also keeps its .json and .info from two
// different saves apart. Without the lock nothing is checked, so commit leaves
// the files alone.
const std::string lock_path = user_presets_lock_path(read_only);
InstanceLock instance_lock(lock_path);
if (instance_lock.locked()) {
const PresetFilesOnDisk on_disk = PresetFilesOnDisk::read(files[i].path);
boost::system::error_code ec;
// The link itself, and a stat error counts as present, so a file that is
// there but cannot be read is still counted and removed by commit.
if (! on_disk.json && fs::symlink_status(files[i].path, ec).type() == fs::file_not_found)
return; // removed by another instance since it was read
if (! (on_disk == loaded.on_disk)) {
CNumericLocalesSetter locales_setter;
loaded = 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);
}
}
const bool leave_files = read_only || (! lock_path.empty() && ! instance_lock.locked());
// Committing can remove an unreadable preset's file, and a filesystem error
// there is reported without stopping the rest of the directory.
try {
this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, leave_files);
Preset preset(m_type, canonical_name, false);
preset.bundle_id = resolved_origin.bundle_id;
preset.file = dir_entry.path().string();
// Load the preset file, apply preset values on top of defaults.
try {
fs::path idx_path(preset.file);
idx_path.replace_extension(".info");
if (fs::exists(idx_path)) {
preset.load_info(idx_path.string());
}
DynamicPrintConfig config;
//BBS: change to json format
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
std::map<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) {
errors_cummulative += err.what();
errors_cummulative += "\n";
}
});
}
}
if (presets_loaded.size() > 0)
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
sort_presets();
@@ -2296,10 +2167,7 @@ void PresetCollection::set_sync_info_and_save(std::string name, std::string sett
preset->setting_id = setting_id;
if (update_time > 0)
preset->updated_time = update_time;
if (preset->sync_info == "update")
preset->save(nullptr);
else
preset->save_info();
preset->sync_info == "update" ? preset->save(nullptr) : preset->save_info();
break;
}
}
@@ -2959,18 +2827,6 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
return std::make_pair(&preset, false);
}
Preset& PresetCollection::append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config)
{
lock();
Preset &preset = m_presets.emplace_back(m_type, name, false);
preset.file = std::move(path);
preset.config = std::move(config);
preset.loaded = true;
preset.is_dirty = false;
unlock();
return preset;
}
Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version)
{
lock();
@@ -3990,53 +3846,28 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name)
return false;
}
std::vector<std::vector<std::string>> PresetCollection::merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors)
// Merge one vendor's presets with the other vendor's presets, report duplicates.
std::vector<std::string> PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors)
{
auto less = [this](const Preset &a, const Preset &b) {
return m_type == Preset::TYPE_FILAMENT ? filament_preset_less(a, b) : a < b;
};
struct Incoming { Preset *preset; size_t source; };
auto incoming_less = [&less](const Incoming &a, const Incoming &b) { return less(*a.preset, *b.preset); };
// Each of `others` is sorted, so its presets form one sorted run.
std::vector<Incoming> incoming;
std::vector<size_t> run_ends { 0 };
for (size_t source = 0; source < others.size(); ++ source) {
for (Preset &preset : others[source]->m_presets)
if (! preset.is_default && ! preset.is_external)
incoming.push_back({ &preset, source });
assert(std::is_sorted(incoming.begin() + run_ends.back(), incoming.end(), incoming_less));
run_ends.push_back(incoming.size());
}
// Merged pairwise and stably, so equal names stay in the order of `others`.
const size_t runs = others.size();
for (size_t width = 1; width < runs; width *= 2)
for (size_t i = 0; i + width < runs; i += 2 * width)
std::inplace_merge(incoming.begin() + run_ends[i], incoming.begin() + run_ends[i + width],
incoming.begin() + run_ends[std::min(i + 2 * width, runs)], incoming_less);
std::vector<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;
std::vector<std::string> duplicates;
for (Preset &preset : other.m_presets) {
if (preset.is_default || preset.is_external)
continue;
Preset key(m_type, preset.name);
auto it = (m_type == Preset::TYPE_FILAMENT)
? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less)
: std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key);
if (it == m_presets.end() || it->name != preset.name) {
if (preset.vendor != nullptr) {
// Re-assign a pointer to the vendor structure in the new PresetBundle.
auto it = new_vendors.find(preset.vendor->id);
assert(it != new_vendors.end());
preset.vendor = &it->second;
}
merged.emplace_back(std::move(preset));
}
for (; next != incoming.end() && next->preset->name == merged.back().name; ++ next)
duplicates[next->source].emplace_back(next->preset->name);
m_presets.emplace(it, std::move(preset));
} else
duplicates.emplace_back(std::move(preset.name));
}
m_presets = std::move(merged);
return duplicates;
}
@@ -4381,15 +4212,8 @@ void PhysicalPrinter::update_preset_names_in_config()
}
}
void PhysicalPrinter::save(DynamicPrintConfig* /* parent_config */)
{
InstanceLock instance_lock(user_presets_lock_path());
this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION));
}
void PhysicalPrinter::save(const std::string& file_name_from, const std::string& file_name_to)
{
InstanceLock instance_lock(user_presets_lock_path());
// rename the file
boost::nowide::rename(file_name_from.data(), file_name_to.data());
this->file = file_name_to;
@@ -4521,7 +4345,6 @@ void PhysicalPrinterCollection::load_printers(
continue;
}
try {
InstanceLock instance_lock(user_presets_lock_path());
PhysicalPrinter printer(name, this->default_config());
printer.file = dir_entry.path().string();
// Load the preset file, apply preset values on top of defaults.
@@ -4714,10 +4537,7 @@ bool PhysicalPrinterCollection::delete_printer(const std::string& name)
const PhysicalPrinter& printer = *it;
// Erase the preset file.
{
InstanceLock instance_lock(user_presets_lock_path());
boost::nowide::remove(printer.file.c_str());
}
boost::nowide::remove(printer.file.c_str());
m_printers.erase(it);
return true;
}
@@ -4729,10 +4549,7 @@ bool PhysicalPrinterCollection::delete_selected_printer()
const PhysicalPrinter& printer = this->get_selected_printer();
// Erase the preset file.
{
InstanceLock instance_lock(user_presets_lock_path());
boost::nowide::remove(printer.file.c_str());
}
boost::nowide::remove(printer.file.c_str());
// Remove the preset from the list.
m_printers.erase(m_printers.begin() + m_idx_selected);
// unselect all printers
+5 -74
View File
@@ -8,7 +8,6 @@
#include <unordered_set>
#include <functional>
#include <mutex>
#include <optional>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/property_tree/ptree_fwd.hpp>
@@ -331,8 +330,7 @@ public:
//BBS: add logic for only difference save
//if parent_config is null, save all keys, otherwise, only save difference
// Returns false when the preset file could not be written.
bool save(DynamicPrintConfig* parent_config);
void save(DynamicPrintConfig* parent_config);
void reload(Preset const & parent);
// Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty.
@@ -447,10 +445,7 @@ public:
static std::string remove_suffix_modified(const std::string& name);
static void normalize(DynamicPrintConfig &config);
// Report configuration fields, which are misplaced into a wrong group, remove them from the config.
// `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);
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config);
// BBS: move constructor to public
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
@@ -490,12 +485,6 @@ std::string get_preset_canonical_name(const std::string &preset_bare_name, const
// Tail segment of a canonical name — what's written to the bundle's .json filename and JSON "name" field.
std::string get_preset_bare_name(const std::string &canonical_name);
// Lock file guarding every user preset file under data_dir() against other
// running instances and the preset sync thread. Empty without a data dir, and
// for a read-only load (the CLI), which never rewrites or deletes and may run
// many jobs on one data dir.
std::string user_presets_lock_path(bool read_only = false);
// Resolve an origin from a directory path when the caller passes Kind::Auto.
PresetOrigin detect_origin_from_path(const boost::filesystem::path &path, const PresetOrigin &explicit_origin = PresetOrigin());
@@ -812,8 +801,6 @@ public:
// Return number of presets including the "- default -" preset.
size_t size() const { return m_presets.size(); }
bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; }
// How many presets this collection refused or repaired while loading.
int error_count() const { return m_errors; }
// For Print / Filament presets, disable those, which are not compatible with the printer.
template<typename PreferedCondition>
@@ -887,10 +874,8 @@ protected:
// This is a temporary state, which shall be fixed immediately by the following step.
bool select_preset_by_name_strict(const std::string &name);
// Move the presets of `others` into this collection in one pass. A name this
// collection or an earlier one of `others` already has is left out, and reported
// in the list of the collection that repeats it.
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors);
// Merge one vendor's presets with the other vendor's presets, report duplicates.
std::vector<std::string> merge_presets(PresetCollection &&other, const VendorMap &new_vendors);
// Update m_map_alias_to_profile_name from loaded system profiles.
void update_map_alias_to_profile_name();
@@ -905,56 +890,6 @@ protected:
void set_custom_preset_alias(Preset &preset);
private:
// A preset file and its .info as read from disk, std::nullopt for one that is missing.
struct PresetFilesOnDisk
{
std::optional<std::string> json;
std::optional<std::string> info;
static PresetFilesOnDisk read(const boost::filesystem::path &file);
bool operator==(const PresetFilesOnDisk &rhs) const { return json == rhs.json && info == rhs.info; }
};
// 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;
// Both files as they were before the preset was read from them. Resolve runs
// without the instance lock, so commit compares this with the disk under it.
PresetFilesOnDisk on_disk;
// 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;
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
@@ -966,10 +901,6 @@ private:
return a.name < b.name;
}
// Append a preset without keeping the collection sorted, for a caller installing
// many at once; find_preset() is unusable until sort_presets() runs.
Preset& append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config);
// Sort presets: filament presets use generic-first ordering, others sort alphabetically.
void sort_presets() {
if (m_type == Preset::TYPE_FILAMENT)
@@ -1123,7 +1054,7 @@ public:
//BBS: change to json format
//void save() { this->config.save(this->file); }
void save(DynamicPrintConfig* parent_config);
void save(DynamicPrintConfig* parent_config) { this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION)); }
void save(const std::string& file_name_from, const std::string& file_name_to);
void update_from_preset(const Preset& preset);
File diff suppressed because it is too large Load Diff
+27 -172
View File
@@ -15,7 +15,6 @@
#include <unordered_map>
#include <optional>
#include <array>
#include <atomic>
#include <boost/filesystem/path.hpp>
#include <unordered_set>
@@ -623,180 +622,36 @@ public:
// default_filament_profile must resolve to a system filament.
bool check_printer_default_materials() const;
// One vendor to load, and the directory it is installed in.
struct VendorSource
{
std::string name;
boost::filesystem::path dir;
};
// Load `vendors` into this bundle, the Orca filament library directly and every
// other vendor in parallel into a bundle of its own that inherits from it, merged
// in the order given. A vendor that cannot be loaded has its error added to the
// returned text, or thrown in validation mode, and its name to `failed`; it is
// left out, except for the library, which keeps what it installed before the
// failure. Once `cancel` is set, no further vendor starts loading.
std::pair<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);
// Merge one vendor's presets with the other vendor's presets, report duplicates.
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
std::vector<std::string> merge_presets(PresetBundle &&other);
private:
// Move the presets and vendor profiles of `others` into this bundle, in one pass
// over each collection. A preset whose name this bundle or an earlier one of
// `others` already has is left out and listed under the bundle that repeats it.
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetBundle*> &others);
// Load one vendor from the preset cache installed in `dir`, judged against
// the vendor profile there. False, with this bundle left clean, when there
// is no usable cache and the vendor has to be parsed. This is how
// load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// What parsing one entry's JSON sub-file reported. Its errors and warnings are
// logged when the entry installs, so they come out in listing order with the
// entry's install errors, as parsing and installing one entry at a time leaves them.
struct EntryParse
{
ConfigSubstitutions substitutions;
// Counted in the bundle's error count.
std::vector<std::string> errors;
std::vector<std::string> warnings;
};
// An EntryParse for each entry of the VendorCacheData list of the same name.
struct VendorParse
{
std::vector<EntryParse> process_entries;
std::vector<EntryParse> filament_entries;
std::vector<EntryParse> machine_entries;
};
// One vendor read from its cache or its JSONs by read_vendor, for
// install_vendor_read to install.
struct VendorRead
{
std::string dir;
std::string vendor_name;
LoadConfigBundleAttributes flags;
ForwardCompatibilitySubstitutionRule compatibility_rule;
// The errors this bundle had counted before the read, which the cache stamp leaves out.
int errors_at_entry { 0 };
// A whole-vendor load, which can read a cache and write one.
bool cacheable { false };
// Read from the cache at cache_path, which install can still reject.
bool from_cache { false };
std::string cache_path;
// Only the vendor profile was asked for.
bool vendor_only { false };
VendorCacheData data;
// What each entry's JSON parse reported, and whether and with which version
// the cache is written once the entries install.
VendorParse parsed;
bool will_cache { false };
std::string version;
// The sub-file the parse stopped at, its kind, why, and the errors it reported.
std::string reason;
std::string failed_subfile;
const char* failed_kind { nullptr };
std::vector<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);
// Load one source-form preset entry into this bundle: resolve `inherits`
// and `include`, flatten, validate and register the preset. Returns the
// reason loading failed, empty on success. See the definition for the
// sharing contract between the JSON parse and the cache load.
// retain_configs / retain_includes, when non-null, name the only presets
// registered into config_maps / include_maps (a config copy each). The
// cache load passes the names its entries inherit / include — the only
// ones ever looked up again; the JSON parse retains all, not knowing what
// later subfiles name.
std::string load_vendor_preset(const CachedPreset& entry,
const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases();
+33 -11
View File
@@ -400,24 +400,46 @@ bool write_cache_blob(const std::string& path, const std::string& blob)
{
boost::crc_32_type crc;
crc.process_bytes(blob.data(), blob.size());
// Written beside the target and moved into place: a cache is truncated and
// rewritten in full, so a write that dies partway would otherwise leave a
// header claiming more body than the file holds.
// Written beside the target and moved into place, as AppConfig::save does:
// a cache is truncated and rewritten in full, so a write that dies partway
// would otherwise leave a header claiming more body than the file holds.
// The PID suffix also keeps two instances writing the same vendor from
// interleaving.
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
try {
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
CacheFileHeader fhdr;
fhdr.magic = CACHE_MAGIC;
fhdr.version = CACHE_VERSION;
fhdr.data_size = static_cast<uint64_t>(blob.size());
fhdr.crc32 = crc.checksum();
const std::string_view header(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
if (const std::error_code ec = write_file_atomically(path, { header, std::string_view(blob) }, /*binary=*/true)) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << ec.message();
{
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path;
return false;
}
CacheFileHeader fhdr;
fhdr.magic = CACHE_MAGIC;
fhdr.version = CACHE_VERSION;
fhdr.data_size = static_cast<uint64_t>(blob.size());
fhdr.crc32 = crc.checksum();
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
ofs.close(); // flush; close() raises failbit on error
if (! ofs.good()) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")";
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
return false;
}
}
if (const std::error_code ec = rename_file(tmp_path, path)) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message();
boost::system::error_code rm;
boost::filesystem::remove(tmp_path, rm);
return false;
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
return false;
}
}
+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
// preset it inherits and the presets it includes, and the parse metadata —
// everything PresetBundle::parse_vendor_json extracts and
// everything the parse phase of load_vendor_configs_from_json extracts and
// nothing it derives. Inheritance and includes are resolved when the entry is
// installed, against whatever filament library is loaded then, so a cache
// carries no other vendor's values and no other vendor's update can make it
+10 -5
View File
@@ -2717,7 +2717,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
@@ -3344,7 +3345,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
@@ -4950,7 +4952,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
{
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
@@ -4980,7 +4983,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Error;
ctx.msg = file + "\n" + ex.what();
ctx.cancellation_check = [this]() { return canceled(); };
@@ -4994,7 +4998,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
-17
View File
@@ -8,8 +8,6 @@
#include <functional>
#include <type_traits>
#include <system_error>
#include <initializer_list>
#include <string_view>
#include <regex>
#include <boost/system/error_code.hpp>
@@ -226,21 +224,6 @@ extern std::vector<std::string> split_string(const std::string &str, char delimi
// On Windows, the file explorer (or anti-virus or whatever else) often locks the file
// for a short while, so the file may not be movable. Retry while we see recoverable errors.
extern std::error_code rename_file(const std::string &from, const std::string &to);
// Write `chunks`, in order, to `path` through a temporary file beside it that is
// then renamed over the target, so a concurrent reader sees the old or the new
// file, never a partial one. The temporary is removed on failure and an existing
// target keeps its permissions. Text mode unless `binary`, so Windows writes CRLF
// as the streams this replaces did. A target that is not a regular file (a
// device or pipe) is written in place, since replacing it would change what it
// is, and so is an existing target beside which no temporary can be created or
// whose replace the filesystem refuses; a symlink is followed and the file it
// names is replaced. On Windows a reader holding the
// target open without sharing its deletion, which the C runtime does not, makes
// the replace fall back to the in-place write too, so an unlocked reader there
// can still see a partial file.
extern std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary = false);
inline std::error_code write_file_atomically(const std::string &path, const std::string &content, bool binary = false)
{ return write_file_atomically(path, { std::string_view(content) }, binary); }
enum CopyFileResult {
SUCCESS = 0,
+2 -90
View File
@@ -9,8 +9,6 @@
#include <stdio.h>
#include <filesystem>
#include <sstream>
#include <cerrno>
#include <mutex>
#include <iomanip>
#include <algorithm>
#include <cmath>
@@ -704,99 +702,13 @@ namespace WindowsSupport
std::error_code rename_file(const std::string &from, const std::string &to)
{
#ifdef _WIN32
// Retries and moves an open destination aside itself.
return WindowsSupport::rename(from, to);
#else
// rename(2) replaces an existing target atomically; removing it first would
// leave a window in which the file does not exist at all.
if (boost::nowide::rename(from.c_str(), to.c_str()) == 0)
return {};
const int err = errno;
// Some mounts (sshfs, gvfs, MTP and a few SMB setups) refuse to replace an
// existing target in one step, each with the error it sees fit; every error
// is worth the remove-then-rename this always did, except the ones no retry
// can help: nothing at the source, a different device, or a directory where
// a file was expected and the reverse.
const bool worth_retrying = err != ENOENT && err != EXDEV && err != ENOTDIR && err != EISDIR;
if (worth_retrying && boost::nowide::remove(to.c_str()) == 0 && boost::nowide::rename(from.c_str(), to.c_str()) == 0)
return {};
return std::make_error_code(static_cast<std::errc>(err));
boost::nowide::remove(to.c_str());
return std::make_error_code(static_cast<std::errc>(boost::nowide::rename(from.c_str(), to.c_str())));
#endif
}
static std::error_code write_whole_file(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
{
errno = 0;
FILE *file = boost::nowide::fopen(path.c_str(), binary ? "wb" : "w");
if (file == nullptr)
return std::make_error_code(errno != 0 ? static_cast<std::errc>(errno) : std::errc::io_error);
bool ok = true;
for (const std::string_view chunk : chunks)
ok = ok && std::fwrite(chunk.data(), 1, chunk.size(), file) == chunk.size();
ok = ok && std::fflush(file) == 0;
const int err = ok ? 0 : errno;
ok = std::fclose(file) == 0 && ok;
if (ok)
return {};
return std::make_error_code(err != 0 ? static_cast<std::errc>(err) : std::errc::io_error);
}
// The in-place fallback truncates the target, so two threads of this process
// on the same file must not both be in it. One mutex for all such writes: they
// are the rare case. Never freed, like the InstanceLock registry, so a save
// during static destruction still finds it.
static std::error_code write_in_place(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
{
static auto *mutex = new std::mutex();
std::lock_guard<std::mutex> guard(*mutex);
return write_whole_file(path, chunks, binary);
}
std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
{
boost::system::error_code bec;
const boost::filesystem::file_status target = boost::filesystem::symlink_status(path, bec);
const bool target_exists = ! bec && boost::filesystem::exists(target);
if (target_exists && boost::filesystem::is_symlink(target)) {
// A config or preset kept in a dotfiles repository: the link stays,
// the file it points to is replaced like any other.
const boost::filesystem::path resolved = boost::filesystem::canonical(path, bec);
if (! bec && boost::filesystem::is_regular_file(resolved, bec))
return write_file_atomically(resolved.string(), chunks, binary);
}
if (target_exists && ! boost::filesystem::is_regular_file(target))
return write_in_place(path, chunks, binary);
// Unique per process and per call, so two threads writing one target
// without a lock never share a temporary.
static std::atomic<unsigned> counter{0};
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + "." + std::to_string(counter++) + ".tmp";
if (const std::error_code ec = write_whole_file(tmp_path, chunks, binary)) {
boost::nowide::remove(tmp_path.c_str());
if (! target_exists)
return ec;
// A directory that lets this process write its files but not create
// one: losing the save is worse than a reader seeing a partial file.
BOOST_LOG_TRIVIAL(warning) << "Cannot create a temporary beside " << path << " (" << ec.message() << "); writing in place";
return write_in_place(path, chunks, binary);
}
#ifndef _WIN32
// Not on Windows, where a read-only bit on the temporary would stop the rename itself.
if (target_exists)
boost::filesystem::permissions(tmp_path, target.permissions(), bec);
#endif
if (const std::error_code ec = rename_file(tmp_path, path)) {
boost::nowide::remove(tmp_path.c_str());
// A reader on Windows holding the target open without FILE_SHARE_DELETE,
// or a mount that cannot replace a file at all. Losing the save is worse
// than a reader seeing a partial file, so write in place the way this
// used to work before the atomic path existed.
BOOST_LOG_TRIVIAL(warning) << "Cannot replace " << path << " (" << ec.message() << "); writing in place";
return write_in_place(path, chunks, binary);
}
return {};
}
#ifdef __linux__
// Copied from boost::filesystem.
// Called by copy_file_linux() in case linux sendfile() API is not supported.
+15
View File
@@ -158,6 +158,21 @@ enum class EGCodeExtrusionRole : uint8_t
static constexpr std::size_t GCODE_EXTRUSION_ROLES_COUNT = static_cast<std::size_t>(EGCodeExtrusionRole::COUNT);
//
// What the reduced set, drawn while the user is dragging, holds in place of the full toolpaths
//
enum class EReducedDetailMode : uint8_t
{
// nothing: no reduced set is built
Off,
// only the bottom and top layers of the visible range, for a caller that draws the print
// itself some other way
EndLayersOnly,
// every role but the infill under the skins and the gap fill: what can be seen from outside
ShellOnly,
COUNT
};
//
// Option types
//
+13
View File
@@ -114,6 +114,19 @@ public:
//
bool is_dim_previous_layers() const;
void set_dim_previous_layers(bool value);
//
// The reduced set drawn while the user drags: what the mode keeps, and always the bottom and
// top layers of the visible range. While a mode is set it is built alongside the full set, so
// set_reduced_detail() rebuilds nothing. Ignored on the OpenGL ES path.
//
EReducedDetailMode get_reduced_detail_mode() const;
void set_reduced_detail_mode(EReducedDetailMode mode);
// Whether the shell mode hides the infill roles; off for a profile that leaves them on the
// surface, with no top or bottom shell or no walls.
bool get_reduced_detail_hide_infill() const;
void set_reduced_detail_hide_infill(bool value);
void set_reduced_detail(bool value);
bool is_reduced_detail() const;
float get_dim_previous_layers_brightness() const;
void set_dim_previous_layers_brightness(float value);
//
+5
View File
@@ -25,6 +25,11 @@ struct Settings
// ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black
float dim_previous_layers_brightness{ 0.4f };
bool spiral_vase_mode{ false };
// what the reduced set holds and whether it is drawn. Ignored on the OpenGL ES path.
EReducedDetailMode reduced_detail_mode{ EReducedDetailMode::Off };
// whether the shell mode may hide the infill roles: false when the profile leaves them on the surface
bool reduced_detail_hide_infill{ true };
bool reduced_detail{ false };
//
// Required update flags
//
+6 -1
View File
@@ -34,6 +34,10 @@ static const char* Segments_Vertex_Shader =
// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not
// world_position, so the caster would write a depth the receiver never looks up.
"uniform float bias_scale;\n"
// draw the instances last to first, top layers before the ones they hide, so that early depth
// rejection discards most of the hidden fragments; set when the camera looks down on the print
"uniform int reverse_order;\n"
"uniform int instance_count;\n"
"in int vertex_id;\n"
"out vec3 color;\n"
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
@@ -59,7 +63,8 @@ static const char* Segments_Vertex_Shader =
" return top_diffuse + front_diffuse + top_specular;\n"
"}\n"
"void main() {\n"
" int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n"
" int instance = (reverse_order != 0) ? instance_count - 1 - gl_InstanceID : gl_InstanceID;\n"
" int id_a = int(texelFetch(segment_index_tex, instance).r);\n"
" int id_b = id_a + 1;\n"
" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n"
" vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n"
+30
View File
@@ -92,6 +92,36 @@ bool Viewer::is_dim_previous_layers() const
return m_impl->is_dim_previous_layers();
}
void Viewer::set_reduced_detail(bool value)
{
m_impl->set_reduced_detail(value);
}
bool Viewer::is_reduced_detail() const
{
return m_impl->is_reduced_detail();
}
EReducedDetailMode Viewer::get_reduced_detail_mode() const
{
return m_impl->get_reduced_detail_mode();
}
void Viewer::set_reduced_detail_mode(EReducedDetailMode mode)
{
m_impl->set_reduced_detail_mode(mode);
}
bool Viewer::get_reduced_detail_hide_infill() const
{
return m_impl->get_reduced_detail_hide_infill();
}
void Viewer::set_reduced_detail_hide_infill(bool value)
{
m_impl->set_reduced_detail_hide_infill(value);
}
void Viewer::set_dim_previous_layers(bool value)
{
m_impl->set_dim_previous_layers(value);
+114 -8
View File
@@ -17,6 +17,7 @@
#include <algorithm>
#include <cmath>
#include <numeric>
#include <cfloat>
namespace libvgcode {
@@ -763,6 +764,8 @@ void ViewerImpl::init(const std::string& opengl_context_version)
m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex");
m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex");
m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex");
m_uni_segments_reverse_order_id = glGetUniformLocation(m_segments_shader_id, "reverse_order");
m_uni_segments_instance_count_id = glGetUniformLocation(m_segments_shader_id, "instance_count");
// ORCA: realistic view
m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map");
m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp");
@@ -899,9 +902,17 @@ void ViewerImpl::reset()
#else
m_enabled_segments_count = 0;
m_enabled_options_count = 0;
m_enabled_segments_reduced_count = 0;
m_enabled_options_reduced_count = 0;
m_enabled_segments_reduced_tex_size = 0;
m_enabled_options_reduced_tex_size = 0;
m_settings_used_for_ranges = std::nullopt;
delete_textures(m_enabled_options_reduced_tex_id);
delete_buffers(m_enabled_options_reduced_buf_id);
delete_textures(m_enabled_segments_reduced_tex_id);
delete_buffers(m_enabled_segments_reduced_buf_id);
delete_textures(m_enabled_options_tex_id);
delete_buffers(m_enabled_options_buf_id);
delete_textures(m_enabled_segments_tex_id);
@@ -1161,6 +1172,17 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
glsafe(glGenTextures(1, &m_enabled_options_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id));
// create (but do not fill) the reduced counterparts of the two buffers above
glsafe(glGenBuffers(1, &m_enabled_segments_reduced_buf_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_buf_id));
glsafe(glGenTextures(1, &m_enabled_segments_reduced_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_tex_id));
glsafe(glGenBuffers(1, &m_enabled_options_reduced_buf_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_options_reduced_buf_id));
glsafe(glGenTextures(1, &m_enabled_options_reduced_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_reduced_tex_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, old_bound_texture));
#endif // ENABLE_OPENGL_ES
@@ -1172,6 +1194,18 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
update_colors();
}
#ifndef ENABLE_OPENGL_ES
// the roles that lie under a skin or between walls, never seen from outside the print
static bool is_hidden_in_shell(EGCodeExtrusionRole role)
{
return role == EGCodeExtrusionRole::InternalInfill ||
role == EGCodeExtrusionRole::SolidInfill ||
role == EGCodeExtrusionRole::InternalBridgeInfill ||
role == EGCodeExtrusionRole::GapFill;
}
#endif // ENABLE_OPENGL_ES
void ViewerImpl::update_enabled_entities()
{
if (m_vertices.empty())
@@ -1179,6 +1213,17 @@ void ViewerImpl::update_enabled_entities()
std::vector<uint32_t> enabled_segments;
std::vector<uint32_t> enabled_options;
#ifndef ENABLE_OPENGL_ES
// the reduced set is filled by the same walk, so switching to it costs no rebuild. Whatever the
// mode leaves out, the bottom and top layers of the visible range are kept whole: they are the
// surfaces the range cuts open
const EReducedDetailMode reduced_mode = m_settings.reduced_detail_mode;
const bool build_reduced = reduced_mode != EReducedDetailMode::Off;
const bool hide_infill = m_settings.reduced_detail_hide_infill;
std::vector<uint32_t> enabled_segments_reduced;
std::vector<uint32_t> enabled_options_reduced;
const Interval& layers_range = m_layers.get_view_range();
#endif // ENABLE_OPENGL_ES
Interval range = m_view_range.get_visible();
// when top layer only visualization is enabled, we need to render
@@ -1226,6 +1271,20 @@ void ViewerImpl::update_enabled_entities()
enabled_options.push_back(static_cast<uint32_t>(i));
else
enabled_segments.push_back(static_cast<uint32_t>(i));
#ifndef ENABLE_OPENGL_ES
if (build_reduced) {
const bool end_layer = v.layer_id == layers_range[0] || v.layer_id == layers_range[1];
if (end_layer)
(v.is_option() ? enabled_options_reduced : enabled_segments_reduced).push_back(static_cast<uint32_t>(i));
else if (reduced_mode == EReducedDetailMode::ShellOnly) {
if (v.is_option())
enabled_options_reduced.push_back(static_cast<uint32_t>(i));
else if (!v.is_extrusion() || !hide_infill || !is_hidden_in_shell(v.role))
enabled_segments_reduced.push_back(static_cast<uint32_t>(i));
}
}
#endif // ENABLE_OPENGL_ES
}
#ifdef ENABLE_OPENGL_ES
@@ -1254,6 +1313,23 @@ void ViewerImpl::update_enabled_entities()
else
glsafe(glBufferData(GL_TEXTURE_BUFFER, 0, nullptr, GL_STATIC_DRAW));
m_enabled_segments_reduced_count = enabled_segments_reduced.size();
m_enabled_options_reduced_count = enabled_options_reduced.size();
m_enabled_segments_reduced_tex_size = enabled_segments_reduced.size() * sizeof(uint32_t);
m_enabled_options_reduced_tex_size = enabled_options_reduced.size() * sizeof(uint32_t);
// uploaded even when nothing was built, so that the last reduced set is released as soon as
// the preference is switched off
assert(m_enabled_segments_reduced_buf_id > 0);
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_buf_id));
glsafe(glBufferData(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_tex_size,
enabled_segments_reduced.empty() ? nullptr : enabled_segments_reduced.data(), GL_STATIC_DRAW));
assert(m_enabled_options_reduced_buf_id > 0);
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_options_reduced_buf_id));
glsafe(glBufferData(GL_TEXTURE_BUFFER, m_enabled_options_reduced_tex_size,
enabled_options_reduced.empty() ? nullptr : enabled_options_reduced.data(), GL_STATIC_DRAW));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0));
#endif // ENABLE_OPENGL_ES
@@ -1461,6 +1537,23 @@ void ViewerImpl::toggle_top_layer_only_view_range()
update_colors_texture();
}
// Both decide which vertices land in the reduced set, so the sets are rebuilt.
void ViewerImpl::set_reduced_detail_mode(EReducedDetailMode mode)
{
if (m_settings.reduced_detail_mode == mode)
return;
m_settings.reduced_detail_mode = mode;
m_settings.update_enabled_entities = true;
}
void ViewerImpl::set_reduced_detail_hide_infill(bool value)
{
if (m_settings.reduced_detail_hide_infill == value)
return;
m_settings.reduced_detail_hide_infill = value;
m_settings.update_enabled_entities = true;
}
// ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to
void ViewerImpl::set_dim_previous_layers(bool value)
{
@@ -1854,6 +1947,8 @@ size_t ViewerImpl::get_used_gpu_memory() const
ret += m_colors_tex_size;
ret += m_enabled_segments_tex_size;
ret += m_enabled_options_tex_size;
ret += m_enabled_segments_reduced_tex_size;
ret += m_enabled_options_reduced_tex_size;
#endif // ENABLE_OPENGL_ES
return ret;
}
@@ -2070,7 +2165,8 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
#ifdef ENABLE_OPENGL_ES
if (m_texture_data.get_enabled_segments_count() == 0)
#else
if (m_enabled_segments_count == 0)
const ActiveSet segments = active_segments();
if (segments.count == 0)
#endif // ENABLE_OPENGL_ES
return;
@@ -2090,6 +2186,15 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data()));
glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data()));
glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data()));
// The segments come in print order, bottom layer first. Seen from above, that is back to front,
// and every hidden fragment is shaded before the one that covers it. Drawing them last to first
// lets the depth test reject the hidden ones instead. The camera looks down when the world's
// up axis points towards it, which is the view matrix's (2, 2) entry being positive.
const bool top_down = !m_rendering_shadow_casters && view_matrix[10] > 0.0f;
glsafe(glUniform1i(m_uni_segments_reverse_order_id, top_down ? 1 : 0));
#ifndef ENABLE_OPENGL_ES
glsafe(glUniform1i(m_uni_segments_instance_count_id, static_cast<int>(segments.count)));
#endif // ENABLE_OPENGL_ES
// ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades
// with the lookup off.
glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit));
@@ -2138,10 +2243,10 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
glsafe(glActiveTexture(GL_TEXTURE3));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_segments_buf_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, segments.tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, segments.buf_id));
m_segment_template.render(m_enabled_segments_count);
m_segment_template.render(segments.count);
#endif // ENABLE_OPENGL_ES
if (curr_cull_face)
@@ -2167,7 +2272,8 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
#ifdef ENABLE_OPENGL_ES
if (m_texture_data.get_enabled_options_count() == 0)
#else
if (m_enabled_options_count == 0)
const ActiveSet options = active_options();
if (options.count == 0)
#endif // ENABLE_OPENGL_ES
return;
@@ -2225,10 +2331,10 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
glsafe(glActiveTexture(GL_TEXTURE3));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_options_buf_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, options.tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, options.buf_id));
m_option_template.render(m_enabled_options_count);
m_option_template.render(options.count);
#endif // ENABLE_OPENGL_ES
if (!curr_cull_face)
+47
View File
@@ -109,6 +109,21 @@ public:
// 0.0 = black
bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; }
void set_dim_previous_layers(bool value);
//
// Draw from the reduced set; it is already built, so this is just a buffer binding.
//
void set_reduced_detail(bool value) {
#ifdef ENABLE_OPENGL_ES
// no reduced set is built on OpenGL ES
value = false;
#endif // ENABLE_OPENGL_ES
m_settings.reduced_detail = value;
}
bool is_reduced_detail() const { return m_settings.reduced_detail; }
EReducedDetailMode get_reduced_detail_mode() const { return m_settings.reduced_detail_mode; }
void set_reduced_detail_mode(EReducedDetailMode mode);
bool get_reduced_detail_hide_infill() const { return m_settings.reduced_detail_hide_infill; }
void set_reduced_detail_hide_infill(bool value);
float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; }
void set_dim_previous_layers_brightness(float value);
@@ -362,6 +377,8 @@ private:
int m_uni_segments_height_width_angle_tex_id{ -1 };
int m_uni_segments_colors_tex_id{ -1 };
int m_uni_segments_segment_index_tex_id{ -1 };
int m_uni_segments_reverse_order_id{ -1 };
int m_uni_segments_instance_count_id{ -1 };
int m_uni_segments_shadow_map_id{ -1 };
int m_uni_segments_shadow_light_vp_id{ -1 };
int m_uni_segments_shadow_intensity_id{ -1 };
@@ -499,6 +516,15 @@ private:
unsigned int m_enabled_options_tex_id{ 0 };
size_t m_enabled_options_count{ 0 };
//
// OpenGL buffers to store the reduced set drawn while Settings::reduced_detail is set
//
unsigned int m_enabled_segments_reduced_buf_id{ 0 };
unsigned int m_enabled_segments_reduced_tex_id{ 0 };
size_t m_enabled_segments_reduced_count{ 0 };
unsigned int m_enabled_options_reduced_buf_id{ 0 };
unsigned int m_enabled_options_reduced_tex_id{ 0 };
size_t m_enabled_options_reduced_count{ 0 };
//
// Caches for size of data sent to gpu, in bytes
//
size_t m_positions_tex_size{ 0 };
@@ -506,6 +532,27 @@ private:
size_t m_colors_tex_size{ 0 };
size_t m_enabled_segments_tex_size{ 0 };
size_t m_enabled_options_tex_size{ 0 };
size_t m_enabled_segments_reduced_tex_size{ 0 };
size_t m_enabled_options_reduced_tex_size{ 0 };
// The set the next draw reads from: the reduced one while dragging, if one is built.
bool use_reduced_set() const { return m_settings.reduced_detail && m_settings.reduced_detail_mode != EReducedDetailMode::Off; }
struct ActiveSet
{
size_t count{ 0 };
unsigned int buf_id{ 0 };
unsigned int tex_id{ 0 };
};
ActiveSet active_segments() const {
if (use_reduced_set())
return { m_enabled_segments_reduced_count, m_enabled_segments_reduced_buf_id, m_enabled_segments_reduced_tex_id };
return { m_enabled_segments_count, m_enabled_segments_buf_id, m_enabled_segments_tex_id };
}
ActiveSet active_options() const {
if (use_reduced_set())
return { m_enabled_options_reduced_count, m_enabled_options_reduced_buf_id, m_enabled_options_reduced_tex_id };
return { m_enabled_options_count, m_enabled_options_buf_id, m_enabled_options_tex_id };
}
#endif // ENABLE_OPENGL_ES
//
+4 -4
View File
@@ -819,7 +819,7 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) :
m_scrolled_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxHSCROLL);
m_scrolled_window->SetBackgroundColour(*wxWHITE);
m_scrolled_window->SetScrollRate(0, FromDIP(10));
m_scrolled_window->SetScrollRate(0, FromDIP(20));
wxBoxSizer *title_sizer_h= new wxBoxSizer(wxHORIZONTAL);
wxBoxSizer *title_sizer_v = new wxBoxSizer(wxVERTICAL);
@@ -1000,13 +1000,13 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) :
show_pos.x = screen_size.GetLeft();
m_scrolled_window->SetMaxSize(wxSize(screen_size.GetWidth(), popup_height));
m_scrolled_window->SetMinSize(wxSize(screen_size.GetWidth(), popup_height));
m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(10));
m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(20));
}
else
{
m_scrolled_window->SetMaxSize(wxSize(popup_width, popup_height));
m_scrolled_window->SetMinSize(wxSize(popup_width, popup_height));
m_scrolled_window->SetScrollRate(0, FromDIP(10));
m_scrolled_window->SetScrollRate(0, FromDIP(20));
}
//Vertical Direction Processing
@@ -2117,7 +2117,7 @@ void AmsReplaceMaterialDialog::create()
identical_filament->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#009688"))); // Orca: accent teal (not brand green)
m_scrollview_groups = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrollview_groups->SetScrollRate(5, 5);
m_scrollview_groups->SetScrollRate(5, FromDIP(20));
//m_scrollview_groups->SetMinSize(wxSize(400, 400));
//m_scrollview_groups->SetMaxSize(wxSize(400, 400));
m_scrollview_sizer = new wxBoxSizer(wxVERTICAL);
+1 -1
View File
@@ -606,7 +606,7 @@ AuFolderPanel::AuFolderPanel(wxWindow *parent, AuxiliaryFolderType type, wxWindo
wxBoxSizer *sizer_main = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetScrollRate(5, 5);
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
wxBoxSizer *sizer_body = new wxBoxSizer(wxVERTICAL);
wxBoxSizer *sizer_top = new wxBoxSizer(wxHORIZONTAL);
+1 -1
View File
@@ -558,7 +558,7 @@ PingCodeBindDialog::~PingCodeBindDialog() {
//show bind failed info
m_sw_bind_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(450), FromDIP(300)), wxVSCROLL);
m_sw_bind_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_bind_failed_info->SetScrollRate(5, 5);
m_sw_bind_failed_info->SetScrollRate(5, FromDIP(20));
m_sw_bind_failed_info->SetMinSize(wxSize(FromDIP(450), FromDIP(90)));
m_sw_bind_failed_info->SetMaxSize(wxSize(FromDIP(450), FromDIP(90)));
+1 -1
View File
@@ -3521,7 +3521,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
cards->Show(m_box_mate, false, true);
m_form->FitInside();
m_form->SetScrollRate(0, 10); // vertical only, like Prepare's sidebar: never scroll labels out
m_form->SetScrollRate(0, FromDIP(20)); // vertical only, like Prepare's sidebar: never scroll labels out
m_form->SetMinSize(wxSize(264, -1));
// Right column: a small view toolbar over the live 3D viewport that mirrors
+1 -1
View File
@@ -115,7 +115,7 @@ HistoryWindow::HistoryWindow(wxWindow* parent, const std::vector<PACalibResult>&
auto main_sizer = new wxBoxSizer(wxVERTICAL);
auto scroll_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
scroll_window->SetScrollRate(5, 5);
scroll_window->SetScrollRate(5, FromDIP(20));
scroll_window->SetBackgroundColour(*wxWHITE);
scroll_window->SetMinSize(HISTORY_WINDOW_SIZE);
scroll_window->SetSize(HISTORY_WINDOW_SIZE);
+1 -1
View File
@@ -207,7 +207,7 @@ SelectMObjectPopup::SelectMObjectPopup(wxWindow* parent)
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
m_scrolledWindow->SetScrollRate(0, 5);
m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y);
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
m_scrolledWindow->Layout();
+1 -1
View File
@@ -81,7 +81,7 @@ CalibrationWizard::CalibrationWizard(wxWindow* parent, CalibMode mode, wxWindowI
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetScrollRate(5, 5);
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
wxBoxSizer* padding_sizer = new wxBoxSizer(wxHORIZONTAL);
+1 -1
View File
@@ -834,7 +834,7 @@ void CaliPageSendingPanel::create(wxWindow* parent)
m_sw_print_failed_info = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL);
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_print_failed_info->SetScrollRate(0, 5);
m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
+1 -1
View File
@@ -2710,7 +2710,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent)
SetSizerAndFit(vsizer);
// We can now enable scrolling on hscroll
p->hscroll->SetScrollRate(30, 30);
p->hscroll->SetScrollRate(30, FromDIP(20));
on_window_geometry(this, [this]() {
p->init_dialog_size();
+5 -5
View File
@@ -699,7 +699,7 @@ CreateFilamentPresetDialog::CreateFilamentPresetDialog(wxWindow *parent)
m_scrolled_preset_panel = new wxScrolledWindow(this, wxID_ANY);
m_scrolled_preset_panel->SetMaxSize(wxSize(-1, FromDIP(350)));
m_scrolled_preset_panel->SetBackgroundColour(*wxWHITE);
m_scrolled_preset_panel->SetScrollRate(5, 5);
m_scrolled_preset_panel->SetScrollRate(5, FromDIP(20));
m_scrolled_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolled_sizer->Add(create_item(FilamentOptionType::PRESET_FOR_PRINTER), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
m_scrolled_sizer->Add(0, 0, 0, wxTOP, FromDIP(5));
@@ -1593,7 +1593,7 @@ CreatePrinterPresetDialog::CreatePrinterPresetDialog(wxWindow *parent)
m_page1 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_page1->SetBackgroundColour(*wxWHITE);
m_page1->SetScrollRate(5, 5);
m_page1->SetScrollRate(5, FromDIP(20));
m_page2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page2->SetBackgroundColour(*wxWHITE);
create_printer_page1(m_page1);
@@ -2652,7 +2652,7 @@ wxBoxSizer *CreatePrinterPresetDialog::create_presets_template_item(wxWindow *pa
wxBoxSizer *vertical_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolled_preset_window = new wxScrolledWindow(parent);
m_scrolled_preset_window->SetScrollRate(5, 5);
m_scrolled_preset_window->SetScrollRate(5, FromDIP(20));
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
//m_scrolled_preset_window->SetMinSize(wxSize(FromDIP(1500), FromDIP(-1)));
m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(1500), FromDIP(-1)));
@@ -4297,7 +4297,7 @@ wxBoxSizer *ExportConfigsDialog::create_select_printer(wxWindow *parent)
optionSizer->SetMinSize(OPTION_SIZE);
horizontal_sizer->Add(optionSizer, 0, wxEXPAND | wxALL, FromDIP(10));
m_scrolled_preset_window = new wxScrolledWindow(parent);
m_scrolled_preset_window->SetScrollRate(5, 5);
m_scrolled_preset_window->SetScrollRate(5, FromDIP(20));
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(660), FromDIP(400)));
m_scrolled_preset_window->SetSize(wxSize(FromDIP(660), FromDIP(400)));
@@ -4736,7 +4736,7 @@ wxBoxSizer *EditFilamentPresetDialog::create_preset_tree_sizer()
{
wxBoxSizer *filament_preset_tree_sizer = new wxBoxSizer(wxHORIZONTAL);
m_preset_tree_window = new wxScrolledWindow(this);
m_preset_tree_window->SetScrollRate(5, 5);
m_preset_tree_window->SetScrollRate(5, FromDIP(20));
m_preset_tree_window->SetBackgroundColour(PRINTER_LIST_COLOUR);
m_preset_tree_window->SetMinSize(wxSize(-1, FromDIP(400)));
m_preset_tree_window->SetMaxSize(wxSize(-1, FromDIP(300)));
+1 -1
View File
@@ -39,7 +39,7 @@ DeviceErrorDialog::DeviceErrorDialog(MachineObject* obj, wxWindow* parent, wxWin
m_line_top->SetBackgroundColour(wxColour(166, 169, 170));
m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scroll_area->SetScrollRate(0, 5);
m_scroll_area->SetScrollRate(0, FromDIP(20));
m_scroll_area->SetBackgroundColour(*wxWHITE);
m_scroll_area->SetMinSize(wxSize(FromDIP(320), FromDIP(250)));
+146 -1
View File
@@ -1267,6 +1267,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
if (current_top_layer_only != required_top_layer_only)
m_viewer.toggle_top_layer_only_view_range();
read_reduced_detail_preferences();
// ORCA: darken the layers the preview layer slider is not scrubbed to
m_viewer.set_dim_previous_layers(get_app_config()->get_bool("preview_dim_previous_layers"));
m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness")));
@@ -1490,6 +1492,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
// BBS: data for rendering color arrangement recommendation
m_nozzle_nums = print.config().option<ConfigOptionFloats>("nozzle_diameter")->values.size();
// the shell drag mode hides the infill only where the profile covers it: with no top or bottom
// shell, or no walls, the infill is the surface. Per-object settings are not consulted.
const PrintRegionConfig& region = print.default_region_config();
m_viewer.set_reduced_detail_hide_infill(region.top_shell_layers.value > 0 && region.bottom_shell_layers.value > 0 && region.wall_loops.value > 0);
// Orca hack: Hide filament group for non-bbl printers
if (!print.is_BBL_printer()) m_nozzle_nums = 1;
std::vector<int> filament_maps = print.get_filament_maps();
@@ -1660,6 +1666,7 @@ void GCodeViewer::reset_shell()
{
m_shells.volumes.clear();
m_shells.print_id = -1;
m_shells.with_wipe_tower = false;
m_shell_bounding_box = BoundingBoxf3();
}
@@ -1696,7 +1703,12 @@ void GCodeViewer::reset()
void GCodeViewer::render_scene(int canvas_width, int canvas_height)
{
glsafe(::glEnable(GL_DEPTH_TEST));
render_shells(canvas_width, canvas_height);
// while dragging in the solid model mode, the objects stand in for their toolpaths, cut to the
// visible layer range; the toolpath set then holds only the range's bottom and top layers
if (m_viewer.is_reduced_detail() && solid_model_enabled())
render_solid_model(canvas_width, canvas_height);
else
render_shells(canvas_width, canvas_height);
if (m_viewer.get_extrusion_roles_count() == 0)
return;
@@ -2019,6 +2031,56 @@ void GCodeViewer::update_layers_slider_mode()
// TODO m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder);
}
void GCodeViewer::set_interacting(bool interacting)
{
// with no shells to stand in for the toolpaths, the solid model would leave only the end layers
const bool usable = !solid_model_enabled() || !m_shells.volumes.empty();
m_viewer.set_reduced_detail(interacting && usable);
}
void GCodeViewer::read_reduced_detail_preferences()
{
m_reduced_detail_mode = reduced_detail_mode_from_string(get_app_config()->get("preview_reduced_detail_mode"));
apply_reduced_detail_settings();
}
void GCodeViewer::apply_reduced_detail_settings()
{
m_viewer.set_reduced_detail_mode(m_reduced_detail_mode);
}
void GCodeViewer::set_reduced_detail_mode(const std::string& mode)
{
const bool was_solid = solid_model_enabled();
m_reduced_detail_mode = reduced_detail_mode_from_string(mode);
apply_reduced_detail_settings();
reload_shells_if_solid_model_changed(was_solid);
}
libvgcode::EReducedDetailMode GCodeViewer::reduced_detail_mode_from_string(const std::string& mode)
{
if (mode == "solid")
return libvgcode::EReducedDetailMode::EndLayersOnly;
if (mode == "shell")
return libvgcode::EReducedDetailMode::ShellOnly;
return libvgcode::EReducedDetailMode::Off;
}
void GCodeViewer::reload_shells_if_solid_model_changed(bool was_enabled)
{
if (was_enabled == solid_model_enabled() || m_shells.print_id == -1)
return;
// only the prime tower comes and goes with the mode: a full reload would drop the shells
// whenever the print has moved on since they were loaded, leaving the solid model nothing to draw
if (wxGetApp().plater() == nullptr)
return;
// the shells are loaded from the current plate's print, which is not the plater's own
const Print& print = wxGetApp().plater()->get_partplate_list().get_current_fff_print();
if (static_cast<int>(print.id().id) != m_shells.print_id)
return;
update_shell_wipe_tower(print, m_gl_data_initialized);
}
void GCodeViewer::set_layers_z_range(const std::array<unsigned int, 2>& layers_z_range)
{
m_viewer.set_layers_view_range(static_cast<uint32_t>(layers_z_range[0]), static_cast<uint32_t>(layers_z_range[1]));
@@ -2343,7 +2405,11 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const
void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_previewing)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": initialized=%1%, force_previewing=%2%")%initialized %force_previewing;
// the shells can load before the first G-code does, so the preferences are read here as well
read_reduced_detail_preferences();
if ((print.id().id == m_shells.print_id)&&(print.get_modified_count() == m_shells.print_modify_count)) {
// the prime tower comes and goes on its own, without reloading the objects
update_shell_wipe_tower(print, initialized);
//BBS: update force previewing logic
if (force_previewing)
m_shells.previewing = force_previewing;
@@ -2452,10 +2518,45 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p
m_shells.print_id = print.id().id;
m_shells.print_modify_count = print.get_modified_count();
m_shells.previewing = true;
update_shell_wipe_tower(print, initialized);
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": shell loaded, id change to %1%, modify_count %2%, object count %3%, glvolume count %4%")
% m_shells.print_id % m_shells.print_modify_count % object_count %m_shells.volumes.volumes.size();
}
// The prime tower as it was sliced, so that the solid model shows what the print shows. It keeps its
// opaque colour, so it never appears among the translucent shells, and stays out of their bounding box.
void GCodeViewer::update_shell_wipe_tower(const Print& print, bool initialized)
{
const bool with_wipe_tower = solid_model_enabled() && print.is_step_done(psWipeTower) && print.wipe_tower_data().wipe_tower_mesh_data;
if (with_wipe_tower == m_shells.with_wipe_tower)
return;
m_shells.with_wipe_tower = with_wipe_tower;
GLVolumePtrs& volumes = m_shells.volumes.volumes;
if (!with_wipe_tower) {
volumes.erase(std::remove_if(volumes.begin(), volumes.end(), [](GLVolume* volume) {
if (!volume->is_wipe_tower)
return false;
delete volume;
return true;
}), volumes.end());
return;
}
const PrintConfig& config = print.config();
const int plate_idx = print.get_plate_index();
const Vec3d plate_origin = print.get_plate_origin();
const float x = static_cast<float>(config.wipe_tower_x.get_at(plate_idx) + plate_origin.x());
const float y = static_cast<float>(config.wipe_tower_y.get_at(plate_idx) + plate_origin.y());
const size_t first_new = volumes.size();
m_shells.volumes.load_real_wipe_tower_preview(1000 + plate_idx, x, y, print.wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh,
print.wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh, true,
static_cast<float>(config.wipe_tower_rotation_angle), false, initialized);
for (size_t i = first_new; i < volumes.size(); ++i) {
volumes[i]->zoom_to_volumes = false;
volumes[i]->force_native_color = true;
volumes[i]->set_render_color();
}
}
void GCodeViewer::render_toolpaths()
{
const Camera& camera = wxGetApp().plater()->get_camera();
@@ -2656,6 +2757,50 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height)
glsafe(::glDepthMask(GL_TRUE));
}
// The sliced objects and the prime tower drawn opaque, in their filament colours, cut to the
// visible layer range by the shader's z range. The toolpaths of the range's bottom and top layers
// are drawn afterwards and cap the cut.
void GCodeViewer::render_solid_model(int canvas_width, int canvas_height)
{
if (m_shells.volumes.empty())
return;
// gouraud_light has no z range, so it could not cut the model
GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
if (shader == nullptr)
return;
const libvgcode::Interval& layers = m_viewer.get_layers_view_range();
const float z_top = m_viewer.get_layer_z(layers[1]) - m_z_offset + 0.001f;
const float z_bottom = (layers[0] > 0) ? m_viewer.get_layer_z(layers[0] - 1) - m_z_offset - 0.001f : -FLT_MAX;
std::vector<float> alphas;
alphas.reserve(m_shells.volumes.volumes.size());
for (GLVolume* volume : m_shells.volumes.volumes) {
alphas.push_back(volume->color.a());
volume->color.a(1.0f);
volume->set_render_color();
}
m_shells.volumes.set_z_range(z_bottom, z_top);
// gouraud also clips by this plane, which nothing else sets on the shells
m_shells.volumes.set_clipping_plane(ClippingPlane::ClipsNothing().get_data());
shader->start_using();
// the 3D view leaves its shadow settings on the shared program
shader->set_uniform("shadow_intensity", 0.0f);
const Camera& camera = wxGetApp().plater()->get_camera();
shader->set_uniform("z_far", camera.get_far_z());
shader->set_uniform("z_near", camera.get_near_z());
m_shells.volumes.render(GLVolumeCollection::ERenderType::Opaque, false, camera.get_view_matrix(), camera.get_projection_matrix(), {canvas_width, canvas_height});
shader->stop_using();
m_shells.volumes.set_z_range(-FLT_MAX, FLT_MAX);
size_t k = 0;
for (GLVolume* volume : m_shells.volumes.volumes) {
volume->color.a(alphas[k++]);
volume->set_render_color();
}
}
//BBS
void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show /*= true*/) const {
if (!show)
+22 -1
View File
@@ -174,6 +174,8 @@ public:
int print_id{-1};
int print_modify_count{-1};
bool previewing{false};
// the prime tower was loaded with the objects, for the solid model
bool with_wipe_tower{false};
};
//BBS
ConflictResultOpt m_conflict_result;
@@ -234,6 +236,17 @@ private:
bool m_legend_visible{ true };
bool m_legend_enabled{ true };
// the reduced-detail preferences, pushed to libvgcode by apply_reduced_detail_settings()
libvgcode::EReducedDetailMode m_reduced_detail_mode{ libvgcode::EReducedDetailMode::Off };
void read_reduced_detail_preferences();
void apply_reduced_detail_settings();
static libvgcode::EReducedDetailMode reduced_detail_mode_from_string(const std::string& mode);
// in the solid model mode, the sliced objects are drawn as solid shapes instead of toolpaths
bool solid_model_enabled() const { return m_reduced_detail_mode == libvgcode::EReducedDetailMode::EndLayersOnly; }
void render_solid_model(int canvas_width, int canvas_height);
// the prime tower is only among the shells for the solid model, so it is added or removed when that changes
void reload_shells_if_solid_model_changed(bool was_enabled);
void update_shell_wipe_tower(const Print& print, bool initialized);
float m_legend_height;
PrintEstimatedStatistics m_print_statistics;
@@ -291,7 +304,7 @@ public:
// void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
// void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
// void render_calibration_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
bool has_data() const { return !m_viewer.get_extrusion_roles().empty(); }
bool has_data() const { return m_viewer.get_extrusion_roles_count() != 0; }
bool can_export_toolpaths() const;
std::vector<int> get_plater_extruder();
@@ -363,6 +376,14 @@ public:
void set_dim_previous_layers_brightness(float value) { m_viewer.set_dim_previous_layers_brightness(value); }
float get_dim_previous_layers_brightness() const { return m_viewer.get_dim_previous_layers_brightness(); }
// whether the mouse is holding either slider's handle
bool is_slider_dragging() const { return m_layers_slider->is_dragging() || m_moves_slider->is_dragging(); }
// while the user drags the camera or a slider, draw the reduced set, if the preference asks for one
void set_interacting(bool interacting);
bool is_reduced_detail() const { return m_viewer.is_reduced_detail(); }
// the preference's string value: "off", "solid" or "shell"
void set_reduced_detail_mode(const std::string& mode);
void set_layers_z_range(const std::array<unsigned int, 2>& layers_z_range);
bool is_legend_shown() const { return m_legend_visible && m_legend_enabled; }
+64 -4
View File
@@ -2084,6 +2084,11 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
const bool overlay_tick = m_fps_overlay_tick;
m_fps_overlay_tick = false;
// Whether the preview draws its reduced set is decided before the cached scene is consulted,
// since switching changes what the scene pass draws.
if (m_canvas_type == ECanvasType::CanvasPreview && m_render_preview && m_gcode_viewer.has_data() && _update_preview_interaction())
scene_dirty = true;
// An overlay-only frame reuses the last scene pass. The overlay is rebuilt either way, and drawn
// below once it is known whether the frame differs from the one on screen.
const bool reuse_scene = !scene_dirty && _can_reuse_cached_scene(camera);
@@ -3277,9 +3282,16 @@ void GLCanvas3D::bind_event_handlers()
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
// a drag cut short never sees its button release, which would leave the reduced set drawn
if (m_canvas_type == CanvasPreview && m_mouse.dragging && m_gcode_viewer.is_reduced_detail())
mouse_up_cleanup();
render();
evt.Skip();
});
m_canvas->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) {
if (m_canvas_type == CanvasPreview && m_mouse.dragging && m_gcode_viewer.is_reduced_detail())
mouse_up_cleanup();
});
m_event_handlers_bound = true;
m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
@@ -3355,6 +3367,17 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
m_overlay_dirty |= imgui_requires_extra_frame;
#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
m_dirty |= GLTexture::Compressor::has_compressed_texture_to_refresh();
// the render timer only wakes the idle loop; the frame that puts the preview's toolpaths back
// after a wheel burst has to be asked for here, once the settle time is really up
if (m_preview_settle_pending) {
const auto now = std::chrono::steady_clock::now();
if (now >= m_preview_interaction_until) {
m_preview_settle_pending = false;
m_dirty = true;
}
else // the timer fired early
schedule_extra_frame(static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(m_preview_interaction_until - now).count()) + 1);
}
if (!m_dirty && !m_overlay_dirty)
return;
@@ -3885,6 +3908,10 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt)
return;
}
// only a wheel the panels did not take moves the camera
if (m_canvas_type == CanvasPreview)
note_preview_interaction();
#ifdef __WXMSW__
// For some reason the Idle event is not being generated after the mouse scroll event in case of scrolling with the two fingers on the touch pad,
// if the event is not allowed to be passed further.
@@ -3985,6 +4012,11 @@ void GLCanvas3D::on_fps_overlay_timer(wxTimerEvent& evt)
wxWakeUpIdle();
}
void GLCanvas3D::note_preview_interaction()
{
m_preview_interaction_until = std::chrono::steady_clock::now() + std::chrono::milliseconds(150);
}
void GLCanvas3D::schedule_extra_frame(int milliseconds)
{
// Schedule idle event right now
@@ -5631,6 +5663,9 @@ void GLCanvas3D::mouse_up_cleanup()
m_mouse.ignore_left_up = false;
m_mouse.ignore_right_up = false;
m_dirty = true;
// the frame that follows a release puts the preview's toolpaths back, and on some platforms
// no idle event follows a button release until the next input
wxWakeUpIdle();
if (m_canvas->HasCapture())
m_canvas->ReleaseMouse();
@@ -7779,13 +7814,20 @@ bool GLCanvas3D::_is_scene_cacheable() const
return false;
#endif
// The scene follows the cursor during a drag, under a gizmo that draws at the cursor, and while
// the cursor is on the layer height bar, where the object shader draws a band at its height.
// The scene follows the cursor while the user drags, under a gizmo that draws at the cursor, and
// while the cursor is on the layer height bar, where the object shader draws a band at its height.
const GLGizmoBase* gizmo = m_gizmos.get_current();
const bool cursor_on_layers_bar = is_layers_editing_enabled() &&
m_layers_editing.bar_rect_contains(*this, (float)m_mouse.position.x(), (float)m_mouse.position.y());
return !m_mouse.dragging && !m_gizmos.is_dragging() && !m_rectangle_selection.is_dragging() &&
(gizmo == nullptr || !gizmo->render_follows_cursor()) && !cursor_on_layers_bar;
return !is_user_interacting() && (gizmo == nullptr || !gizmo->render_follows_cursor()) && !cursor_on_layers_bar;
}
// Whether the user is holding something that moves the scene: the camera, the navigator, a gizmo,
// the rectangle selection or a preview slider.
bool GLCanvas3D::is_user_interacting() const
{
return m_mouse.dragging || m_navigator_dragging || m_gizmos.is_dragging() || m_rectangle_selection.is_dragging() ||
m_gcode_viewer.is_slider_dragging();
}
bool GLCanvas3D::_is_frame_skipping_enabled() const
@@ -8776,6 +8818,24 @@ void GLCanvas3D::_render_wireframe_overlay()
shader->stop_using();
}
// The reduced set is drawn while the camera, the navigator or either slider is dragged. A wheel
// step has no duration, so it holds the reduced set for a settle time instead, and the frame that
// restores the full toolpaths is scheduled for when that time runs out. Returns whether what the scene
// pass draws changed, since a frame that reuses the cached scene would hide the change.
bool GLCanvas3D::_update_preview_interaction()
{
const auto now = std::chrono::steady_clock::now();
const bool settling = now < m_preview_interaction_until;
const bool dragging = is_user_interacting();
const bool was_reduced = m_gcode_viewer.is_reduced_detail();
m_gcode_viewer.set_interacting(dragging || settling);
if (settling && !dragging && m_gcode_viewer.is_reduced_detail()) {
m_preview_settle_pending = true;
schedule_extra_frame(static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(m_preview_interaction_until - now).count()) + 1);
}
return m_gcode_viewer.is_reduced_detail() != was_reduced;
}
//BBS: GUI refactor: add canvas size as parameters
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
{
+11
View File
@@ -656,6 +656,10 @@ private:
ECursorType m_cursor_type;
GLSelectionRectangle m_rectangle_selection;
bool m_navigator_dragging{ false };
// until when a wheel step keeps the preview's reduced set drawn
std::chrono::time_point<std::chrono::steady_clock> m_preview_interaction_until{};
// whether the frame that restores the toolpaths once that time is up is still owed
bool m_preview_settle_pending{ false };
//BBS:add plate related logic
mutable std::vector<int> m_hover_volume_idxs;
@@ -1222,6 +1226,10 @@ public:
void msw_rescale() { m_gcode_viewer.invalidate_legend(); }
void request_extra_frame() { m_extra_frame_requested = true; }
// whether the user is holding the camera, the navigator, a gizmo, the rectangle selection or a preview slider
bool is_user_interacting() const;
// a wheel step is over before the next frame, so it holds the preview's reduced set for a settle time
void note_preview_interaction();
void schedule_extra_frame(int milliseconds);
@@ -1372,6 +1380,9 @@ private:
//BBS: GUI refactor: add canvas size as parameters
void _render_gcode(int canvas_width, int canvas_height);
void _render_gcode_overlay(int canvas_width, int canvas_height);
// decides whether the preview draws its reduced set this frame and returns whether what the scene
// pass draws changed; runs before the cached scene is consulted
bool _update_preview_interaction();
//BBS: render a plane for assemble
void _render_plane() const;
void _render_selection();
+3 -2
View File
@@ -456,8 +456,9 @@ void GLModel::init_from(const indexed_triangle_set& its)
data.reserve_vertices(3 * its.indices.size());
data.reserve_indices(3 * its.indices.size());
// Read user preference: smooth normals enabled
const bool smooth_normals_enabled = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS);
// CLI thumbnail generation can initialize models without a wx application.
const bool smooth_normals_enabled = wxApp::GetInstance() != nullptr && wxGetApp().app_config != nullptr &&
wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS);
if (smooth_normals_enabled) {
// Use per-corner smooth normals (via IGL)
+12 -21
View File
@@ -85,7 +85,6 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/InstanceLock.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp"
@@ -3550,7 +3549,7 @@ bool GUI_App::on_init_inner()
update_publish_status();
}
if (m_post_initialized && app_config->dirty() && app_config->save_due())
if (m_post_initialized && app_config->dirty())
app_config->save();
});
@@ -7644,11 +7643,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Delete the bundle folder and bundle
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
{
boost::system::error_code ec;
InstanceLock instance_lock(user_presets_lock_path());
boost::filesystem::remove_all(bundle_folder, ec);
}
boost::system::error_code ec;
boost::filesystem::remove_all(bundle_folder, ec);
preset_bundle->bundles.WriteLock();
preset_bundle->bundles.m_bundles.erase(bundle.id);
@@ -8935,7 +8931,6 @@ void GUI_App::preset_deleted_from_cloud(std::string setting_id)
// Delete the .info file after cloud deletion is confirmed
if (!preset_file_path.empty() && fs::exists(fs::path(preset_file_path))) {
InstanceLock instance_lock(user_presets_lock_path());
boost::nowide::remove(preset_file_path.c_str());
BOOST_LOG_TRIVIAL(info) << "Deleted .info file after cloud confirmation: " << preset_file_path;
}
@@ -8998,19 +8993,15 @@ void GUI_App::scan_orphaned_info_files()
fs::path preset_file = info_file;
preset_file.replace_extension(".json");
// If .json doesn't exist, .info is orphaned. Read under the lock, so a
// remove_files() in another instance is seen whole or not at all; the
// delete queue's own mutex is taken after the lock is released.
std::string setting_id;
{
InstanceLock instance_lock(user_presets_lock_path());
if (!fs::exists(preset_file))
setting_id = extract_setting_id_from_info(info_file.string());
}
if (!setting_id.empty()) {
// Add to need_delete_presets
delete_preset_from_cloud(setting_id, info_file.string());
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
// If .json doesn't exist, .info is orphaned
if (!fs::exists(preset_file)) {
// Extract setting_id from .info file
std::string setting_id = extract_setting_id_from_info(info_file.string());
if (!setting_id.empty()) {
// Add to need_delete_presets
delete_preset_from_cloud(setting_id, info_file.string());
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
}
}
}
if (ec)
+2 -2
View File
@@ -2764,12 +2764,12 @@ ObjectTablePanel::ObjectTablePanel( wxWindow* parent, wxWindowID id, const wxPoi
//m_object_grid->AssignTable(m_object_grid_table);
m_side_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(440),FromDIP(480)), wxVSCROLL);
m_side_window->SetScrollRate( 0, 5 );
m_side_window->SetScrollRate(0, FromDIP(20));
m_page_sizer = new wxBoxSizer(wxVERTICAL);
//m_page_top_sizer = new wxBoxSizer(wxHORIZONTAL);
m_side_window->SetBackgroundColour(wxColour(0xff, 0xff, 0xff));
m_side_window->SetSizer(m_page_sizer);
m_side_window->SetScrollbars(1, 20, 1, 2);
m_side_window->SetScrollbars(1, FromDIP(20), 1, 2);
//m_side_window->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
//m_side_window->EnableScrolling(false, true);
+1 -1
View File
@@ -171,7 +171,7 @@ HMSPanel::HMSPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wx
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetScrollRate(5, 5);
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
m_top_sizer = new wxBoxSizer(wxVERTICAL);
+7
View File
@@ -483,6 +483,11 @@ void IMSlider::draw_background_and_groove(const ImRect& bg_rect, const ImRect& g
ImGui::RenderFrame(groove.Min, groove.Max, groove_col, false, 0.5 * groove.GetWidth());
}
bool IMSlider::is_dragging() const
{
return GImGui != nullptr && m_imgui_id != 0 && GImGui->ActiveId == m_imgui_id && GImGui->IO.MouseDown[0];
}
bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int v_max, const ImVec2& size, float scale)
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
@@ -491,6 +496,7 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
ImGuiContext& context = *GImGui;
const ImGuiID id = window->GetID(str_id);
m_imgui_id = id;
const ImVec2 pos = window->DC.CursorPos;
const ImRect draw_region(pos, pos + size);
@@ -883,6 +889,7 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
ImGuiContext& context = *GImGui;
const ImGuiID id = window->GetID(str_id);
m_imgui_id = id;
const ImVec2 pos = window->DC.CursorPos;
const ImRect draw_region(pos, pos + size);
+5
View File
@@ -118,6 +118,9 @@ public:
//BBS update scroll value changed
bool is_dirty() { return m_dirty; }
// whether the mouse is holding this slider's handle, read from ImGui's active id rather than
// from the dirty flag, which is raised and consumed inside a single frame
bool is_dragging() const;
void set_as_dirty(bool dirty = true) { m_dirty = dirty; }
bool is_need_post_tick_event() { return m_is_need_post_tick_changed_event; }
void reset_post_tick_event(bool val = false) {
@@ -182,6 +185,8 @@ private:
int m_higher_value;
int m_one_layer_value; // ORCA
bool m_dirty = false;
// the ImGui id of the slider widget, as of its last render
unsigned int m_imgui_id = 0;
bool m_render_as_disabled{ false };
+1 -1
View File
@@ -211,7 +211,7 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const Page& page)
wxGetApp().UpdateDarkUI(scrollable_panel);
const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE);
scrollable_panel->SetBackgroundColour(page_colour);
scrollable_panel->SetScrollRate(0, 20);
scrollable_panel->SetScrollRate(0, FromDIP(20));
const int page_width = FromDIP(PAGE_WIDTH);
scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450)));
+1 -1
View File
@@ -1125,7 +1125,7 @@ wxBoxSizer* MixedFilamentDialog::create_recommendation_grid()
outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4));
m_recommendation_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116)));
m_recommendation_scroll->SetScrollRate(0, 5);
m_recommendation_scroll->SetScrollRate(0, FromDIP(20));
m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8")));
m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES);
+4 -3
View File
@@ -194,9 +194,10 @@ Button* MsgDialog::add_button(wxWindowID btn_id, bool set_focus /*= false*/, con
bd->button = btn;
//bd->type = type;
mb->id = wxString::Format("%d", m_buttons.size());
// m_buttons.size() is a size_t, which does not match the %d conversion
mb->id = wxString(std::to_string(m_buttons.size()));
mb->buttondata = bd;
m_buttons[ wxString::Format("%d", m_buttons.size())] = mb;
m_buttons[ wxString(std::to_string(m_buttons.size()))] = mb;
return btn;
};
@@ -398,7 +399,7 @@ static void add_msg_content(wxWindow *parent,
}
wxScrolledWindow *scrolledWindow = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
scrolledWindow->SetBackgroundColour(*wxWHITE);
scrolledWindow->SetScrollRate(0, 20);
scrolledWindow->SetScrollRate(0, scrolledWindow->FromDIP(20));
scrolledWindow->EnableScrolling(false, true);
wxBoxSizer *sizer_scrolled = new wxBoxSizer(wxHORIZONTAL);
Label *wrapped_text = new Label(scrolledWindow, font, msg, LB_AUTO_WRAP, wxSize(info_width, -1));
+1 -1
View File
@@ -398,7 +398,7 @@ MultiMachineManagerPage::MultiMachineManagerPage(wxWindow* parent)
m_machine_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_machine_list->SetBackgroundColour(*wxWHITE);
m_machine_list->SetScrollRate(0, 5);
m_machine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
m_machine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
m_machine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
+1 -1
View File
@@ -323,7 +323,7 @@ MultiMachinePickPage::MultiMachinePickPage(Plater* plater /*= nullptr*/)
scroll_macine_list->SetMinSize(wxSize(FromDIP(400), FromDIP(10 * 30)));
scroll_macine_list->SetMaxSize(wxSize(FromDIP(400), FromDIP(10 * 30)));
scroll_macine_list->SetBackgroundColour(*wxWHITE);
scroll_macine_list->SetScrollRate(0, 5);
scroll_macine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
sizer_machine_list = new wxBoxSizer(wxVERTICAL);
scroll_macine_list->SetSizer(sizer_machine_list);
+2 -2
View File
@@ -675,7 +675,7 @@ LocalTaskManagerPage::LocalTaskManagerPage(wxWindow* parent)
m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_task_list->SetBackgroundColour(*wxWHITE);
m_task_list->SetScrollRate(0, 5);
m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
@@ -1056,7 +1056,7 @@ CloudTaskManagerPage::CloudTaskManagerPage(wxWindow* parent)
m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_task_list->SetBackgroundColour(*wxWHITE);
m_task_list->SetScrollRate(0, 5);
m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
+1 -1
View File
@@ -355,7 +355,7 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c
//new color table
m_scrolledWindow = new wxScrolledWindow(m_page_simple, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetScrollRate(0, 20);
m_scrolledWindow->SetScrollRate(0, FromDIP(20));
m_scrolledWindow->EnableScrolling(false, true);
m_scrolledWindow->ShowScrollbars(wxScrollbarVisibility::wxSHOW_SB_NEVER, wxScrollbarVisibility::wxSHOW_SB_DEFAULT);
draw_new_table();
+1 -1
View File
@@ -391,7 +391,7 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
m_page_sizer = new wxBoxSizer(wxVERTICAL);
m_page_view->SetSizer(m_page_sizer);
m_page_view->SetScrollbars(1, 20, 1, 2);
m_page_view->SetScrollbars(1, FromDIP(20), 1, 2);
//m_page_view->SetScrollRate( 5, 5 );
if (m_mode_region)
+1 -1
View File
@@ -148,7 +148,7 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _
m_line->SetBackgroundColour(wxColor(238, 238, 238));
m_list_view = new wxScrolledWindow(m_book_third_panel, wxID_ANY, wxDefaultPosition, wxSize(267, -1), wxHSCROLL | wxVSCROLL);
m_list_view->SetScrollRate(5, 5);
m_list_view->SetScrollRate(5, FromDIP(30));
m_list_view->SetMinSize(wxSize(FromDIP(267), FromDIP(378)));
m_list_view->SetMaxSize(wxSize(FromDIP(267), FromDIP(378)));
m_list_view->SetBackgroundColour(*wxWHITE);
+14 -8
View File
@@ -3049,8 +3049,7 @@ Sidebar::Sidebar(Plater *parent)
// add filament content
p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2);
p->m_panel_filament_content->SetScrollRate(0, 5);
p->m_panel_filament_content->SetScrollRate(0, FromDIP(20));
//p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)});
p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255));
@@ -3142,8 +3141,7 @@ Sidebar::Sidebar(Plater *parent)
// 3) Mixed filament rows, in their own scroll area so a long mixed list does not
// push the physical filament list off screen.
p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2);
p->m_mixed_scroll_area->SetScrollRate(0, 5);
p->m_mixed_scroll_area->SetScrollRate(0, FromDIP(20));
p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
{
auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL);
@@ -12802,8 +12800,6 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
notification_manager->set_slicing_progress_export_possible();
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
const std::string lifecycle_job_name = this->background_process.fff_print() ?
this->background_process.fff_print()->output_filename() : std::string();
this->background_process.reset_export();
// This bool stops showing export finished notification even when process_completed_with_error is false
bool has_error = false;
@@ -12850,8 +12846,18 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
{
Slic3r::LifecycleEventContext ctx;
ctx.name = lifecycle_job_name;
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
if (const PrintBase* print = this->background_process.current_print()) {
const Model& model = print->model();
ctx.id = std::to_string(model.id().id);
if (model.model_info)
ctx.name = model.model_info->model_name;
} else {
// Realistically Printbase* print will never be null because select_technology already asserts an active print
// and the worker thread asserts it before processing.
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": slicing completed without an active print; lifecycle event has no model ID";
}
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn :
(has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
}
+30 -3
View File
@@ -322,7 +322,7 @@ wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString too
return sizer;
}
wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, const wxString wiki_url)
wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, std::function<void(std::string)> onchange, const wxString wiki_url)
{
assert(vlist.size() == config_name_index.size());
unsigned int current_index = 0;
@@ -338,8 +338,9 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString too
auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index);
//// save config
combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index](wxCommandEvent& e) {
combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index, onchange](wxCommandEvent& e) {
app_config->set(param, config_name_index[e.GetSelection()]);
if (onchange != nullptr) onchange(config_name_index[e.GetSelection()]);
e.Skip();
});
@@ -1475,7 +1476,7 @@ void PreferencesDialog::create()
app_config = get_app_config();
m_parent = new MyscrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_parent->SetScrollRate(5, 5);
m_parent->SetScrollRate(0, FromDIP(20));
m_parent->SetBackgroundColour(*wxWHITE);
m_sizer_body = new wxBoxSizer(wxVERTICAL);
@@ -2044,6 +2045,32 @@ void PreferencesDialog::create_items()
"preview_default_view_type", PreviewViewTypeLabels, PreviewViewTypeValues);
g_sizer->Add(item_preview_view_type);
auto item_reduced_detail_mode = create_item_combobox(
_L("Simplify preview while dragging"),
_L("What the sliced preview draws while you drag the camera or a preview slider, or zoom with the mouse wheel, so that large prints stay responsive. "
"The full toolpaths are restored as soon as you let go.\n"
"Off: the full toolpaths.\n"
"Solid model: the sliced objects and the prime tower as solid shapes in their filament colors, cut to the visible layer range, "
"with its bottom and top layers drawn as toolpaths. Supports are not shown, and negative volumes are not cut out.\n"
"Shell only: every layer without its sparse infill, internal solid infill and gap fill, which lie under the walls and skins. "
"Walls, top and bottom surfaces, bridges, supports and the prime tower are drawn whole, so the print looks the same from outside.\n"
"The bottom and top of the visible layer range are always drawn whole."),
"preview_reduced_detail_mode",
{_L("Off"), _L("Solid model"), _L("Shell only")},
{"off", "solid", "shell"},
// apply the new mode immediately to the currently loaded preview
[](std::string value) {
if (Plater* plater = wxGetApp().plater()) {
if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) {
canvas->get_gcode_viewer().set_reduced_detail_mode(value);
canvas->set_as_dirty();
canvas->request_extra_frame();
}
}
}
);
g_sizer->Add(item_reduced_detail_mode);
auto item_dim_previous_layers = create_item_checkbox(
_L("Dim lower layers"),
_L("When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness."),
+1 -1
View File
@@ -93,7 +93,7 @@ public:
wxBoxSizer *create_item_title(wxString title);
wxBoxSizer *create_item_label(wxString label, const wxString tooltip = "", const wxString wiki_url = "");
wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::function<void(wxString)> onchange = {}, const wxString wiki_url = "");
wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, const wxString wiki_url = "");
wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, std::function<void(std::string)> onchange = {}, const wxString wiki_url = "");
wxBoxSizer *create_item_region_combobox(wxString title, wxString tooltip);
wxBoxSizer *create_item_language_combobox(wxString title, wxString tooltip);
wxBoxSizer *create_item_loglevel_combobox(wxString title, wxString tooltip, std::vector<wxString> vlist);
+1 -1
View File
@@ -32,7 +32,7 @@ PrintOptionsDialog::PrintOptionsDialog(wxWindow* parent)
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
m_scrollwindow->SetScrollRate(0, FromDIP(10));
m_scrollwindow->SetScrollRate(0, FromDIP(20));
m_scrollwindow->SetBackgroundColour(*wxWHITE);
m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord));
m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord));
+1 -1
View File
@@ -1225,7 +1225,7 @@ size_t PublishSettingsDialog::category_index_for(
}
category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
category.scroll->SetScrollRate(0, 10);
category.scroll->SetScrollRate(0, FromDIP(20));
category.scroll->SetBackgroundColour(GetBackgroundColour());
category.list_sizer = new wxBoxSizer(wxVERTICAL);
category.scroll->SetSizer(category.list_sizer);
+6 -6
View File
@@ -76,7 +76,7 @@ ReleaseNoteDialog::ReleaseNoteDialog(Plater *plater /*= nullptr*/)
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL);
m_vebview_release_note->SetScrollRate(5, 5);
m_vebview_release_note->SetScrollRate(5, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
m_vebview_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430)));
@@ -142,7 +142,7 @@ UpdatePluginDialog::UpdatePluginDialog(wxWindow* parent /*= nullptr*/)
operation_tips->SetMaxSize(wxSize(FromDIP(260), -1));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_vebview_release_note->SetScrollRate(5, 5);
m_vebview_release_note->SetScrollRate(5, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
m_vebview_release_note->SetMinSize(wxSize(FromDIP(260), FromDIP(150)));
m_vebview_release_note->SetMaxSize(wxSize(FromDIP(260), FromDIP(150)));
@@ -277,7 +277,7 @@ UpdateVersionDialog::UpdateVersionDialog(wxWindow *parent)
m_simplebook_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
m_scrollwindows_release_note = new wxScrolledWindow(m_simplebook_release_note, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL);
m_scrollwindows_release_note->SetScrollRate(5, 5);
m_scrollwindows_release_note->SetScrollRate(5, FromDIP(20));
m_scrollwindows_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
//webview
@@ -538,7 +538,7 @@ SecondaryCheckDialog::SecondaryCheckDialog(wxWindow* parent, wxWindowID id, cons
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_vebview_release_note->SetScrollRate(0, 5);
m_vebview_release_note->SetScrollRate(0, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380)));
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
@@ -823,7 +823,7 @@ PrintErrorDialog::PrintErrorDialog(wxWindow* parent, wxWindowID id, const wxStri
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(5));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_vebview_release_note->SetScrollRate(0, 5);
m_vebview_release_note->SetScrollRate(0, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
m_vebview_release_note->SetMinSize(wxSize(FromDIP(320), FromDIP(250)));
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
@@ -1122,7 +1122,7 @@ ConfirmBeforeSendDialog::ConfirmBeforeSendDialog(wxWindow* parent, wxWindowID id
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_vebview_release_note->SetScrollRate(0, 5);
m_vebview_release_note->SetScrollRate(0, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380)));
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
+1 -1
View File
@@ -31,7 +31,7 @@ SafetyOptionsDialog::SafetyOptionsDialog(wxWindow* parent)
SetSize(FromDIP(480),FromDIP(320));
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
m_scrollwindow->SetScrollRate(0, FromDIP(10));
m_scrollwindow->SetScrollRate(0, FromDIP(20));
m_scrollwindow->SetBackgroundColour(*wxWHITE);
m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord));
m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord));
+2 -2
View File
@@ -148,7 +148,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater)
wxBoxSizer* m_scroll_sizer = new wxBoxSizer(wxVERTICAL);
m_scroll_area = new wxScrolledWindow(this);
m_scroll_area->SetScrollRate(0, 20);
m_scroll_area->SetScrollRate(0, FromDIP(20));
m_scroll_area->SetBackgroundColour(m_colour_def_color);
m_scroll_area->SetMinSize(wxSize(FromDIP(700), FromDIP(600)));
m_scroll_area->SetMaxSize(wxSize(FromDIP(700), FromDIP(600)));
@@ -707,7 +707,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater)
//show bind failed info
m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)), wxVSCROLL);
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_print_failed_info->SetScrollRate(0, 5);
m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
m_sw_print_failed_info->SetMinSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)));
m_sw_print_failed_info->SetMaxSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)));
+1 -1
View File
@@ -327,7 +327,7 @@ SelectMachinePopup::SelectMachinePopup(wxWindow *parent)
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
m_scrolledWindow->SetScrollRate(0, 5);
m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y);
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
m_scrolledWindow->Layout();
+2 -2
View File
@@ -278,7 +278,7 @@ SendMultiMachinePage::SendMultiMachinePage(Plater* plater)
m_main_scroll = new ScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_main_scroll->SetBackgroundColour(*wxWHITE);
m_main_scroll->SetScrollRate(5, 5);
m_main_scroll->SetScrollRate(5, FromDIP(20));
m_sizer_body = new wxBoxSizer(wxVERTICAL);
m_main_page = create_page();
@@ -1353,7 +1353,7 @@ wxPanel* SendMultiMachinePage::create_page()
scroll_macine_list = new wxScrolledWindow(main_page, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(800), FromDIP(300)), wxHSCROLL | wxVSCROLL);
scroll_macine_list->SetBackgroundColour(*wxWHITE);
scroll_macine_list->SetScrollRate(5, 5);
scroll_macine_list->SetScrollRate(5, FromDIP(SEND_ITEM_MAX_HEIGHT));
scroll_macine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT)));
scroll_macine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT)));
+1 -1
View File
@@ -384,7 +384,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater)
//show bind failed info
m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL);
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_print_failed_info->SetScrollRate(0, 5);
m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
+1 -1
View File
@@ -1302,7 +1302,7 @@ void PrintingTaskPanel::set_star_count(int star_count)
StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name)
: wxScrolledWindow(parent, id, pos, size, wxHSCROLL | wxVSCROLL)
{
this->SetScrollRate(25, 25);
SetScrollRate(25, FromDIP(20));
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return;
obj = dev->get_selected_machine();
+1 -1
View File
@@ -700,7 +700,7 @@ SyncAmsInfoDialog::SyncAmsInfoDialog(wxWindow *parent, SyncInfo &info) :
//wxBoxSizer *m_scroll_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(m_show_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetScrollRate(0, 20);
m_scrolledWindow->SetScrollRate(0, FromDIP(20));
m_scrolledWindow->SetMinSize(wxSize(-1, SyncAmsInfoDialogHeightMAX));
m_scrolledWindow->SetMaxSize(wxSize(-1, SyncAmsInfoDialogHeightMAX));
m_scrolledWindow->EnableScrolling(false,true);
+3 -3
View File
@@ -534,7 +534,7 @@ public:
m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_content->SetBackgroundColour(pop_bg);
m_content->SetScrollRate(0, FromDIP(5));
m_content->SetScrollRate(0, FromDIP(20));
auto* outer = new wxBoxSizer(wxVERTICAL);
const int pop_w = std::max(FromDIP(213), popup_width);
@@ -2274,7 +2274,7 @@ void TextureImportDialog::build_mapping_panel(wxWindow* parent, wxSizer* sizer)
m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition,
wxSize(-1, FromDIP(300)));
m_mapping_scroll->SetScrollRate(0, FromDIP(10));
m_mapping_scroll->SetScrollRate(0, FromDIP(20));
m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
@@ -4335,7 +4335,7 @@ void TextureImportDialog::on_dpi_changed(const wxRect&)
if (m_mapping_scroll) {
m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300)));
m_mapping_scroll->SetScrollRate(0, FromDIP(10));
m_mapping_scroll->SetScrollRate(0, FromDIP(20));
}
if (m_btn_skip) {
+1 -1
View File
@@ -942,7 +942,7 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_
m_sizer_tab->Add(m_table_top, 1, 0, 0);
m_scrolledWindow = new wxScrolledWindow(m_panel_tab, wxID_ANY, wxDefaultPosition, UNSAVE_CHANGE_DIALOG_SCROLL_WINDOW_SIZE, wxNO_BORDER|wxVSCROLL);
m_scrolledWindow->SetScrollRate(0, 5);
m_scrolledWindow->SetScrollRate(0, FromDIP(20));
m_scrolledWindow->SetBackgroundColour(GREY200);
m_sizer_bottom = new wxBoxSizer(wxVERTICAL);
m_sizer_bottom->Add(m_scrolledWindow, 1, wxEXPAND, 0);
+1 -1
View File
@@ -130,7 +130,7 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector<Update> &updates, bool force_
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
auto m_scrollwindw_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)),wxVSCROLL);
m_scrollwindw_release_note->SetScrollRate(0, 5);
m_scrollwindw_release_note->SetScrollRate(0, FromDIP(20));
m_scrollwindw_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
m_scrollwindw_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430)));
m_scrollwindw_release_note->SetWindowStyle(wxVSCROLL);
+1 -1
View File
@@ -1629,7 +1629,7 @@ UpgradePanel::UpgradePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos,
auto m_main_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scrolledWindow->SetScrollRate(5, 25);
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
m_machine_list_sizer = new wxBoxSizer(wxVERTICAL);
+43 -24
View File
@@ -1405,26 +1405,23 @@ bool GuideFrame::BuildProfileDataFromVendors()
// is served from the shipped profiles. Each is stamped by name and
// version alone: a profile change requires a version bump, so those two
// determine content wherever the vendor's copy sits.
std::vector<PresetBundle::VendorSource> ordered;
json stamps = json::array();
auto add_vendor = [&ordered, &stamps](const std::string& name, const boost::filesystem::path& dir) {
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; };
std::vector<VendorSource> ordered;
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) {
// The version a load from `dir` would serve: the profile's where one
// exists (a cache is only served while it covers the profile beside
// it), the cache's own stamp where the cache is the whole vendor.
// A profile without a version (blacklist.json) carries no presets
// and is passed over.
const boost::filesystem::path profile = dir / (name + ".json");
std::string version;
if (boost::filesystem::exists(profile)) {
const Semver v = get_version_from_json(profile.string());
if (! v.valid())
return;
version = v.to_string();
if (v.valid())
ordered.push_back({name, dir, v.to_string()});
} else {
version = VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name);
ordered.push_back({name, dir,
VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)});
}
ordered.push_back({name, dir});
stamps.push_back({name, version});
};
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
@@ -1434,6 +1431,9 @@ bool GuideFrame::BuildProfileDataFromVendors()
add_vendor(name, dir);
if (ordered.empty())
return false;
json stamps = json::array();
for (const VendorSource& v : ordered)
stamps.push_back({v.name, v.version});
// What this function derives is a pure function of that stamped set, so
// the derived JSON is cached whole: a fresh cache makes an open one
@@ -1461,24 +1461,34 @@ bool GuideFrame::BuildProfileDataFromVendors()
}
// Each vendor comes from its preset cache where one covers it, which is
// what makes this worth doing instead of the scan below.
PresetBundle bundle;
std::vector<std::string> failed;
const std::string errors = bundle.load_vendors(ordered, ForwardCompatibilitySubstitutionRule::EnableSilent,
/*allow_cache=*/true, m_cancel_token.get(), &failed).second;
if (*m_cancel_token || bundle.vendors.empty())
return false;
if (! errors.empty())
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: loading the vendors reported: " << errors;
// A vendor that failed to load sends this open to the scan below, which lists
// what it can read of every vendor.
if (! failed.empty())
// what makes this worth doing instead of the scan below; loading into a
// bundle per vendor keeps the install order the startup path has.
PresetBundle bundle;
auto load_vendor = [](PresetBundle& into, const std::string& vendor,
const boost::filesystem::path& dir, const PresetBundle* base) {
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
};
for (const VendorSource& v : ordered) {
if (*m_cancel_token)
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
if (v.name == filament_library) {
load_vendor(bundle, v.name, v.dir, nullptr);
} else {
PresetBundle tmp;
load_vendor(tmp, v.name, v.dir, &bundle);
bundle.merge_presets(std::move(tmp));
}
}
if (bundle.vendors.empty())
return false;
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
return false;
// Written through a temp file and moved into place, as the preset caches
// are: half a cache must never be readable.
// are: half a cache must never be readable, and the PID suffix keeps two
// instances from interleaving on one temp file.
const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp";
try {
json out;
out["format"] = 1;
@@ -1487,9 +1497,18 @@ bool GuideFrame::BuildProfileDataFromVendors()
for (const char* key : { "model", "machine", "filament", "process" })
profile[key] = m_ProfileJson[key];
boost::filesystem::create_directories(cache_file.parent_path());
if (const std::error_code ec = write_file_atomically(cache_file.string(), out.dump(-1, ' ', false, json::error_handler_t::ignore), /*binary=*/true))
{
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
ofs << out.dump(-1, ' ', false, json::error_handler_t::ignore);
ofs.close();
if (! ofs.good())
throw std::runtime_error("write failed");
}
if (const std::error_code ec = rename_file(tmp_path, cache_file.string()))
throw std::runtime_error(ec.message());
} catch (const std::exception& e) {
boost::system::error_code rm;
boost::filesystem::remove(tmp_path, rm);
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
}
return true;
+1 -1
View File
@@ -62,7 +62,7 @@ CheckList::CheckList(
s_sizer = new wxBoxSizer(wxVERTICAL);
m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, scroll_style);
m_scroll_area->SetScrollRate(0, 10);
m_scroll_area->SetScrollRate(0, FromDIP(20));
m_scroll_area->SetSizer(s_sizer);
m_scroll_area->SetBackgroundColour(parent->GetBackgroundColour());
m_scroll_area->Bind(wxEVT_RIGHT_DOWN, &CheckList::ShowMenu, this);
+1 -1
View File
@@ -199,7 +199,7 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int
m_sizer_main->Add(m_simplebook, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28));
} else {
m_msg_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL );
m_msg_scrolledWindow->SetScrollRate(0,5);
m_msg_scrolledWindow->SetScrollRate(0, FromDIP(20));
wxBoxSizer* m_msg_sizer= new wxBoxSizer(wxVERTICAL);
m_msg = new wxStaticText(m_msg_scrolledWindow, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x, -1), 0);
+1 -1
View File
@@ -1222,7 +1222,7 @@ ImageTransientPopup::ImageTransientPopup( wxWindow *parent, bool scrolled, wxBit
m_panel->SetSize(300, 300);
// And also actually enable them.
m_panel->SetScrollRate(10, 10);
m_panel->SetScrollRate(10, FromDIP(20));
}
else
{
+3 -5
View File
@@ -2,7 +2,6 @@
#include <algorithm>
#include <sstream>
#include <system_error>
#include <exception>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
@@ -581,10 +580,9 @@ bool C3DPrinterOS::save_api_session(const std::string &session, const std::strin
j.put("session", session);
j.put("email", email);
try {
std::ostringstream json;
pt::write_json(json, j);
if (const std::error_code ec = write_file_atomically(m_api_session_file_path, json.str()))
throw std::system_error(ec);
auto temp_path = m_api_session_file_path + ".tmp";
pt::write_json(temp_path, j);
boost::filesystem::rename(temp_path, m_api_session_file_path);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
<< m_api_session_file_path
+25 -6
View File
@@ -1475,8 +1475,15 @@ void OrcaCloudServiceAgent::save_sync_state()
if (sync_state_path.empty())
return;
if (const std::error_code ec = write_file_atomically(sync_state_path, std::to_string(sync_state.last_sync_timestamp)))
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to save the sync state: " << ec.message();
try {
std::string tmp_path = sync_state_path + ".tmp";
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc);
if (ofs.good()) {
ofs << std::to_string(sync_state.last_sync_timestamp);
ofs.close();
boost::filesystem::rename(tmp_path, sync_state_path);
}
} catch (...) {}
}
void OrcaCloudServiceAgent::clear_sync_state()
@@ -1565,10 +1572,22 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
}
if (const std::error_code ec = write_file_atomically(secret_fallback_path, signed_payload, /*binary=*/true))
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot write user secret file " << secret_fallback_path << ": " << ec.message();
else
stored = true;
const std::string tmp_path = secret_fallback_path + ".tmp";
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary);
if (ofs.good()) {
ofs << signed_payload;
ofs.flush();
ofs.close();
if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(secret_fallback_path.c_str()), true)) {
stored = true;
} else {
wxRemoveFile(wxString::FromUTF8(tmp_path.c_str()));
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to atomically replace user secret file";
}
} else {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot open user secret file for write - " << secret_fallback_path;
}
} else {
// Use wxSecretStore only
wxSecretStore store = wxSecretStore::GetDefault();
+59 -1
View File
@@ -6,6 +6,7 @@
#include <boost/optional.hpp>
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <wx/string.h>
#include <wx/app.h>
@@ -115,10 +116,67 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
return webui_url;
}
namespace {
// Moonraker (Klipper's API server) reports a raised exception as { "error": { "code", "message", "traceback" } }
// under every host type that connects to it, often with the cause only in the traceback. Returns the reason to show,
// or empty for any other body.
std::string moonraker_error_reason(const std::string &body)
{
const auto root = nlohmann::json::parse(body, nullptr, false);
const auto err = root.find("error");
if (err == root.end())
return {};
const auto message = err->find("message");
const auto traceback = err->find("traceback");
if (message == err->end() || traceback == err->end() || !message->is_string() || !traceback->is_string())
return {};
const auto &msg = message->get_ref<const std::string &>();
const auto &tb = traceback->get_ref<const std::string &>();
if (msg.empty())
return {};
const auto end = tb.find_last_not_of(" \t\r\n");
if (end == std::string::npos)
return msg;
// Chained exceptions each start a new traceback; the one that failed the request is the last.
const auto header = tb.rfind("Traceback (most recent call last):", end);
// Tornado renders a raised HTTPError as "HTTP <code>: <reason>[ (<detail>)]", and the detail may span lines.
const auto code = err->find("code");
if (code != err->end() && code->is_number_integer()) {
const std::string marker = "HTTP " + std::to_string(code->get<int>()) + ": ";
const auto pos = tb.rfind(marker, end);
if (pos != std::string::npos && (header == std::string::npos || pos > header) && pos + marker.size() <= end) {
const std::string reason = tb.substr(pos + marker.size(), end + 1 - pos - marker.size());
// An HTTPError whose detail equals its reason, like HTTPError(401, "Unauthorized"), renders the phrase twice.
return reason == msg + " (" + msg + ")" ? msg : reason;
}
}
// Any other exception's type and message are everything from the first unindented line after its frames.
auto begin = (header == std::string::npos) ? std::string::npos : tb.find('\n', header);
while (begin != std::string::npos && begin < end) {
++begin;
if (tb[begin] != ' ' && tb[begin] != '\r' && tb[begin] != '\n')
break;
begin = tb.find('\n', begin);
}
if (begin == std::string::npos || begin > end) {
const auto nl = tb.rfind('\n', end);
begin = (nl == std::string::npos) ? 0 : nl + 1;
}
return msg + " (" + tb.substr(begin, end + 1 - begin) + ")";
}
} // namespace
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
{
if (status != 0) {
auto wxbody = wxString::FromUTF8(body.data());
const std::string reason = moonraker_error_reason(body);
auto wxbody = wxString::FromUTF8(reason.empty() ? body : reason);
return wxString::Format("HTTP %u: %s", status, wxbody);
} else {
if (error.find("curl:Timeout was reached") != std::string::npos) {
+15 -4
View File
@@ -249,10 +249,21 @@ bool PluginConfig::save()
return false;
}
// Written beside the target and moved into place, so a crash mid-write cannot truncate an
// existing config.
if (const std::error_code ec = write_file_atomically(path, root.dump(1, '\t') + "\n")) {
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path << ": " << ec.message() << "; keeping the existing config";
// Write to a PID-suffixed file and rename it into place, so a crash mid-write cannot truncate an
// existing config. Same approach as AppConfig::save().
const std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
boost::nowide::ofstream file;
file.open(path_pid, std::ios::out | std::ios::trunc);
file << root.dump(1, '\t') << std::endl;
file.close();
if (file.fail()) {
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path_pid << "; keeping the existing config";
return false;
}
if (const std::error_code rename_ec = rename_file(path_pid, path)) {
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to move " << path_pid << " onto " << path << ": " << rename_ec.message();
return false;
}
+86
View File
@@ -15,6 +15,9 @@
#include "libslic3r/Layer.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Exception.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "test_helpers.hpp"
#include "test_utils.hpp"
@@ -22,7 +25,10 @@
#include <algorithm>
#include <fstream>
#include <iterator>
#include <memory>
#include <string_view>
#include <utility>
#include <vector>
using namespace Slic3r;
using namespace Slic3r::Test;
@@ -224,8 +230,88 @@ std::string resolved_output_name(Model& model, const std::string& format, const
return print.output_filename(filename_base);
}
struct ScopedLifecycleHook
{
explicit ScopedLifecycleHook(LifecycleHookFn hook) { set_lifecycle_hook_fn(std::move(hook)); }
~ScopedLifecycleHook() { set_lifecycle_hook_fn(nullptr); }
};
} // namespace
TEST_CASE("Slicing lifecycle events identify the model", "[Print][LifecycleEvents]")
{
struct ObservedEvent {
LifecycleEvent event;
std::string id;
std::string name;
};
std::vector<ObservedEvent> events;
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
events.push_back({ event, ctx.id, ctx.name });
});
Print print;
Model model;
ModelInfo info;
info.model_name = "Lifecycle test model";
model.model_info = std::make_shared<ModelInfo>(std::move(info));
init_print({cube(20)}, print, model);
print.process();
ScopedTemporaryFile temp(".gcode");
print.export_gcode(temp.string(), nullptr, nullptr);
GCodeProcessorResult result;
print.export_gcode_from_previous_file(temp.string(), &result);
const std::string expected_id = std::to_string(print.model().id().id);
const std::vector<LifecycleEvent> expected_events = {
LifecycleEvent::SliceStarted,
LifecycleEvent::SliceGeometryFinished,
LifecycleEvent::GCodeExportStarted,
LifecycleEvent::GCodeExportFinished,
LifecycleEvent::GCodeExportStarted,
LifecycleEvent::GCodeExportFinished,
};
REQUIRE(events.size() == expected_events.size());
for (size_t i = 0; i < expected_events.size(); ++i) {
CHECK(events[i].event == expected_events[i]);
CHECK(events[i].id == expected_id);
CHECK(events[i].name == "Lifecycle test model");
}
}
TEST_CASE("Slicing lifecycle event name is empty without model metadata", "[Print][LifecycleEvents]")
{
std::string event_id;
std::string event_name = "unset";
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
if (event == LifecycleEvent::SliceStarted) {
event_id = ctx.id;
event_name = ctx.name;
}
});
Print print;
Model model;
init_print({cube(20)}, print, model);
print.process();
CHECK(event_id == std::to_string(print.model().id().id));
CHECK(event_name.empty());
}
TEST_CASE("Output filenames with numeric statistics fail before slicing finishes", "[Print][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("filename_format", new ConfigOptionString("{int(total_weight*10) / 10.0}"));
Print print;
Model model;
init_print({cube(20)}, print, model, config);
CHECK_THROWS_AS(print.output_filename(), PlaceholderParserError);
}
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
{
Model model;
-3
View File
@@ -14,9 +14,7 @@ add_executable(${_TEST_NAME}_tests
test_clipper_utils.cpp
test_config.cpp
test_config_variant_expansion.cpp
test_locales_utils.cpp
test_toolordering_nozzle_group.cpp
test_parallel_resolve.cpp
test_preset_bundle_loading.cpp
test_preset_setting_id.cpp
test_preset_diff.cpp
@@ -51,7 +49,6 @@ add_executable(${_TEST_NAME}_tests
test_ordering_strategies.cpp
# test_png_io.cpp
test_indexed_triangle_set.cpp
test_instance_lock.cpp
../libnest2d/printer_parts.cpp
)
-204
View File
@@ -1,204 +0,0 @@
#include <catch2/catch_all.hpp>
#include <atomic>
#include <chrono>
#include <thread>
#include <boost/filesystem.hpp>
#include "libslic3r/InstanceLock.hpp"
#include "test_utils.hpp"
#ifndef _WIN32
#include <fcntl.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
using namespace Slic3r;
using namespace std::chrono_literals;
// Sets a process-wide knob for one test and restores it however the test ends.
template<typename T> struct ScopedStaticValue
{
T &ref;
T saved;
ScopedStaticValue(T &ref, T value) : ref(ref), saved(ref) { ref = value; }
~ScopedStaticValue() { ref = saved; }
};
TEST_CASE("InstanceLock creates its lock file and holds it for the guard's scope", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
{
InstanceLock lock(path);
REQUIRE(lock.locked());
REQUIRE(boost::filesystem::exists(path));
}
// Released: a fresh guard gets the lock at once instead of waiting out a timeout.
const auto started = std::chrono::steady_clock::now();
InstanceLock again(path, 5000ms);
REQUIRE(again.locked());
// Well inside the timeout it would otherwise have waited out; loose enough for a loaded runner.
REQUIRE(std::chrono::steady_clock::now() - started < 4000ms);
}
TEST_CASE("InstanceLock nests within one thread", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
InstanceLock outer(path);
{
InstanceLock inner(path, 100ms);
REQUIRE(inner.locked());
}
// The inner guard leaving does not release the outer one.
REQUIRE(outer.locked());
}
TEST_CASE("InstanceLock is a no-op for an empty path and survives an unwritable one", "[InstanceLock]")
{
ScopedTemporaryDir dir;
InstanceLock none("");
REQUIRE_FALSE(none.locked());
// The directory does not exist, so the lock file cannot be created; the
// guard still constructs and the write it guards can go ahead.
InstanceLock unwritable((dir.path() / "missing" / "shared.lock").string(), 100ms);
REQUIRE_FALSE(unwritable.locked());
}
TEST_CASE("InstanceLock retries a lock file it could not open once the cool-down passes", "[InstanceLock]")
{
ScopedTemporaryDir dir;
const std::string path = (dir.path() / "later" / "shared.lock").string();
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
bool before_dir, during_cooldown, after_cooldown;
const auto started = std::chrono::steady_clock::now();
{
InstanceLock lock(path, 100ms);
before_dir = lock.locked();
}
boost::filesystem::create_directories(dir.path() / "later");
{
InstanceLock lock(path, 100ms);
during_cooldown = lock.locked();
}
const bool second_guard_inside_cooldown = std::chrono::steady_clock::now() - started < InstanceLock::cooldown;
std::this_thread::sleep_for(400ms);
{
InstanceLock lock(path, 100ms);
after_cooldown = lock.locked();
}
REQUIRE_FALSE(before_dir);
// A loaded runner may take longer than the cool-down to get here; then the
// second guard legitimately retried, so only assert when the timing held.
if (second_guard_inside_cooldown)
REQUIRE_FALSE(during_cooldown);
REQUIRE(after_cooldown);
}
TEST_CASE("InstanceLock reopens a lock file that was replaced on disk", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
{
InstanceLock lock(path);
REQUIRE(lock.locked());
}
boost::filesystem::remove(path);
InstanceLock lock(path);
REQUIRE(lock.locked());
// Each outermost guard opens the file afresh, so the deleted path is back.
REQUIRE(boost::filesystem::exists(path));
}
TEST_CASE("InstanceLock serialises the threads of one process", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
std::atomic<bool> holder_ready{false};
std::atomic<bool> holder_released{false};
std::thread holder([&] {
InstanceLock lock(path);
holder_ready = true;
std::this_thread::sleep_for(150ms);
holder_released = true;
});
while (! holder_ready)
std::this_thread::yield();
bool released_before_acquire = false;
{
InstanceLock lock(path);
released_before_acquire = holder_released;
}
holder.join();
REQUIRE(released_before_acquire);
}
#ifndef _WIN32
// The cross-process side of the lock is a POSIX flock, which a child process
// takes here directly; LockFileEx backs the guard on Windows, but spawning a
// child there is not worth a test.
TEST_CASE("InstanceLock yields to another process and reports it", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
int child_holds[2], child_may_exit[2];
REQUIRE(::pipe(child_holds) == 0);
REQUIRE(::pipe(child_may_exit) == 0);
const pid_t child = ::fork();
REQUIRE(child >= 0);
if (child == 0) {
int fd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644);
char byte = ::flock(fd, LOCK_EX | LOCK_NB) == 0 ? '1' : '0';
if (::write(child_holds[1], &byte, 1) != 1 || ::read(child_may_exit[0], &byte, 1) != 1)
::_exit(1);
::_exit(0);
}
char byte = '0';
REQUIRE(::read(child_holds[0], &byte, 1) == 1);
REQUIRE(byte == '1');
bool locked_while_child_holds;
{
InstanceLock lock(path, 100ms);
locked_while_child_holds = lock.locked();
}
// The timed-out wait starts a cool-down: the next guard does not touch the file.
const auto started = std::chrono::steady_clock::now();
bool locked_during_cooldown;
{
InstanceLock lock(path, 5000ms);
locked_during_cooldown = lock.locked();
}
const auto cooldown_wait = std::chrono::steady_clock::now() - started;
REQUIRE(::write(child_may_exit[1], "x", 1) == 1);
int status = 0;
REQUIRE(::waitpid(child, &status, 0) == child);
for (int fd : {child_holds[0], child_holds[1], child_may_exit[0], child_may_exit[1]})
::close(fd);
REQUIRE_FALSE(locked_while_child_holds);
REQUIRE_FALSE(locked_during_cooldown);
REQUIRE(cooldown_wait < 4000ms);
// Once the cool-down passes, the lock the child released is taken again.
std::this_thread::sleep_for(400ms);
InstanceLock lock(path);
REQUIRE(lock.locked());
}
#endif
-69
View File
@@ -1,69 +0,0 @@
#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());
}
}
-122
View File
@@ -1,122 +0,0 @@
#include <catch2/catch_all.hpp>
#include <numeric>
#include <stdexcept>
#include <thread>
#include <vector>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/task_group.h>
#include "libslic3r/ParallelResolve.hpp"
using namespace Slic3r;
namespace {
thread_local int t_live_setups = 0;
// Counts how many are alive on the constructing thread.
struct CountingSetup
{
CountingSetup() { ++ t_live_setups; }
~CountingSetup() { -- t_live_setups; }
};
std::vector<size_t> first_indices(size_t count)
{
std::vector<size_t> indices(count);
std::iota(indices.begin(), indices.end(), size_t(0));
return indices;
}
} // namespace
TEST_CASE("every item resolves once and commits in index order on the calling thread", "[ParallelResolve]")
{
const size_t count = 200;
std::vector<int> resolves(count, 0);
std::vector<size_t> committed, values;
std::vector<bool> on_caller;
const std::thread::id caller = std::this_thread::get_id();
resolve_then_commit(count,
[&](size_t i) { ++ resolves[i]; return i * 3; },
[&](size_t i, size_t resolved) {
committed.push_back(i);
values.push_back(resolved);
on_caller.push_back(std::this_thread::get_id() == caller);
});
CHECK(committed == first_indices(count));
for (size_t i = 0; i < count; ++ i) {
CHECK(resolves[i] == 1);
CHECK(values[i] == i * 3);
CHECK(on_caller[i]);
}
}
TEST_CASE("every item resolves inside one chunk setup", "[ParallelResolve]")
{
const size_t count = 200;
std::vector<int> live(count, 0);
resolve_then_commit<CountingSetup>(count,
[&](size_t i) { live[i] = t_live_setups; return 0; },
[](size_t, int) {});
for (size_t i = 0; i < count; ++ i)
CHECK(live[i] == 1);
}
TEST_CASE("an exception from resolve leaves the batches before it committed", "[ParallelResolve]")
{
const size_t count = 200, fails_at = 150;
REQUIRE(fails_at >= resolve_batch_size);
std::vector<size_t> committed;
CHECK_THROWS_AS(resolve_then_commit(count,
[&](size_t i) {
if (i == fails_at)
throw std::runtime_error("resolve failed");
return i;
},
[&](size_t i, size_t) { committed.push_back(i); }),
std::runtime_error);
CHECK(committed == first_indices(fails_at / resolve_batch_size * resolve_batch_size));
}
TEST_CASE("an exception from commit stops at the item that threw", "[ParallelResolve]")
{
const size_t count = 200, fails_at = 90;
std::vector<size_t> committed;
CHECK_THROWS_AS(resolve_then_commit(count,
[](size_t i) { return i; },
[&](size_t i, size_t) {
if (i == fails_at)
throw std::runtime_error("commit failed");
committed.push_back(i);
}),
std::runtime_error);
CHECK(committed == first_indices(fails_at));
}
TEST_CASE("a canceled task group stops before committing an item it did not resolve", "[ParallelResolve]")
{
std::vector<size_t> committed;
bool threw = false;
tbb::task_group_context context;
tbb::parallel_for(tbb::blocked_range<size_t>(0, 1), [&](const tbb::blocked_range<size_t>&) {
context.cancel_group_execution();
try {
resolve_then_commit(200,
[](size_t i) { return i + 1; },
[&](size_t, size_t resolved) { committed.push_back(resolved); });
} catch (const std::runtime_error&) {
threw = true;
}
}, context);
CHECK(threw);
CHECK(committed.empty());
}
@@ -5,7 +5,6 @@
#include <fstream>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ParallelResolve.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -18,10 +17,6 @@
#include <iostream>
#include <initializer_list>
#ifndef _WIN32
#include <unistd.h> // geteuid
#endif
using namespace Slic3r;
namespace {
@@ -64,16 +59,6 @@ void write_preset_with_inherits(const DynamicPrintConfig &default_config, const
config.save_to_json(file.string(), name, "User", "1.0.0");
}
// A user preset file stating nothing but the preset it inherits, so every value it
// ends up with came from resolving that parent.
void write_minimal_child(const fs::path &file, const std::string &name, const std::string &inherits)
{
fs::create_directories(file.parent_path());
std::ofstream(file.string())
<< R"({"type":"process","name":")" << name << R"(","from":"User","version":"1.0.0","inherits":")"
<< inherits << R"("})";
}
// Add an in-memory preset (no file) with the given inherits value (empty => root preset).
Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {})
{
@@ -276,162 +261,6 @@ TEST_CASE("Selected printer uses its default or saved bed type", "[Preset][Bundl
CHECK(app_config.get_printer_setting("Test Printer", "curr_bed_type") == std::to_string(static_cast<int>(expected_bed_type)));
}
TEST_CASE("A directory of user presets loads with each one resolved against its parent", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.config.option<ConfigOptionFloat>("layer_height", true)->value = 0.24;
parent.is_system = true;
constexpr int children = 400;
for (int i = 0; i < children; ++ i)
write_minimal_child(temp_dir.path() / PRESET_PRINT_NAME / ("Child " + std::to_string(i) + ".json"),
"Child " + std::to_string(i), "Parent Process");
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(coll.size() == size_t(children) + 2); // the children, the default preset and the parent
CHECK(coll.error_count() == 0);
for (int i = 0; i < children; ++ i) {
const Preset *child = coll.find_preset("Child " + std::to_string(i));
REQUIRE(child != nullptr);
CHECK(child->inherits() == "Parent Process");
CHECK(child->alias == "Child " + std::to_string(i));
CHECK(child->loaded);
REQUIRE(child->config.option<ConfigOptionFloat>("layer_height") != nullptr);
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-9));
}
}
TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
auto seed_directory = [&]() {
for (int i = 0; i < 200; ++ i)
write_minimal_child(temp_dir.path() / PRESET_PRINT_NAME / ("Child " + std::to_string(i) + ".json"),
"Child " + std::to_string(i), "Parent Process");
};
std::vector<std::vector<std::string>> names_per_run;
for (int run = 0; run < 3; ++ run) {
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
if (run == 0)
seed_directory();
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
std::vector<std::string> names;
for (auto it = coll.begin(); it != coll.end(); ++ it)
names.push_back(it->name + "|" + it->alias + "|" + it->inherits());
names_per_run.push_back(std::move(names));
}
REQUIRE(names_per_run[0].size() > 200);
CHECK(names_per_run[1] == names_per_run[0]);
CHECK(names_per_run[2] == names_per_run[0]);
}
TEST_CASE("An unreadable user preset is counted and removed while the rest still load", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
RenameTestCollection coll;
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
for (int i = 0; i < 20; ++ i)
write_preset_with_inherits(coll.default_preset().config, dir / ("Good " + std::to_string(i) + ".json"),
"Good " + std::to_string(i), std::string());
fs::create_directories(dir);
std::ofstream((dir / "Broken.json").string()) << "{not-json";
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Broken") == nullptr);
CHECK_FALSE(fs::exists(dir / "Broken.json"));
for (int i = 0; i < 20; ++ i)
CHECK(coll.find_preset("Good " + std::to_string(i)) != nullptr);
}
TEST_CASE("A user filament naming no compatible printer gets the one after its @, in memory and on disk", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
REQUIRE(bundle.filaments.default_preset().config.option<ConfigOptionStrings>("compatible_printers")->values.empty());
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
const std::vector<std::string> expected { "Test Printer" };
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
REQUIRE(preset != nullptr);
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == expected);
DynamicPrintConfig saved;
std::map<std::string, std::string> key_values;
std::string reason;
saved.load_from_json(file.string(), ForwardCompatibilitySubstitutionRule::EnableSilent, key_values, reason);
REQUIRE(reason.empty());
REQUIRE(saved.option<ConfigOptionStrings>("compatible_printers") != nullptr);
CHECK(saved.option<ConfigOptionStrings>("compatible_printers")->values == expected);
}
TEST_CASE("A user preset's setting id equal to its base id is dropped in memory, not in the .info written back", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
fs::path info = file;
info.replace_extension(".info");
std::ofstream(info.string()) << "sync_info = \nuser_id = \nsetting_id = PFUS1\nbase_id = PFUS1\nupdated_time = 0\n";
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
REQUIRE(preset != nullptr);
CHECK(preset->setting_id.empty());
CHECK(preset->base_id == "PFUS1");
Preset reloaded(Preset::TYPE_FILAMENT, "My PLA @Test Printer");
reloaded.load_info(info.string());
CHECK(reloaded.setting_id == "PFUS1");
}
TEST_CASE("A user preset that is not loaded still reports its substituted values", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
RenameTestCollection coll;
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
fs::create_directories(dir);
std::ofstream((dir / "Orphan.json").string())
<< R"({"type":"process","name":"Orphan","from":"User","version":"1.0.0","inherits":"No Such Parent",)"
<< R"("wall_generator":"no_such_generator"})";
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Enable);
CHECK(coll.find_preset("Orphan") == nullptr);
CHECK(coll.error_count() == 1);
REQUIRE(substitutions.size() == 1);
CHECK(substitutions.front().preset_name == "Orphan");
}
TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]")
{
RenameTestCollection coll;
@@ -5653,214 +5482,3 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
}
}
TEST_CASE("A user preset saved over by another instance while its directory loads is read again under the lock", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.config.option<ConfigOptionFloat>("layer_height", true)->value = 0.24;
parent.is_system = true;
// Fewer than one batch, so every file is read before the first one is committed.
constexpr int children = 8;
static_assert(children <= int(resolve_batch_size));
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
for (int i = 0; i < children; ++ i)
write_minimal_child(dir / ("Child " + std::to_string(i) + ".json"), "Child " + std::to_string(i), "Parent Process");
// The first commit stands in for another instance saving every other preset after
// they were read and before they are committed.
std::string first;
auto save_the_others = [&](Preset &preset) {
if (! first.empty())
return;
first = preset.name;
for (int i = 0; i < children; ++ i) {
const std::string name = "Child " + std::to_string(i);
if (name != first)
std::ofstream((dir / (name + ".json")).string())
<< R"({"type":"process","name":")" << name
<< R"(","from":"User","version":"1.0.0","inherits":"Parent Process","layer_height":"0.3"})";
}
};
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable, save_the_others);
REQUIRE_FALSE(first.empty());
CHECK(coll.error_count() == 0);
for (int i = 0; i < children; ++ i) {
const std::string name = "Child " + std::to_string(i);
const Preset *child = coll.find_preset(name);
REQUIRE(child != nullptr);
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(name == first ? 0.24 : 0.3, 1e-9));
}
}
TEST_CASE("A user preset removed by another instance while its directory loads is not installed", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
// Fewer than one batch, so every file is read before the first one is committed.
constexpr int children = 8;
static_assert(children <= int(resolve_batch_size));
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
for (int i = 0; i < children; ++ i)
write_minimal_child(dir / ("Child " + std::to_string(i) + ".json"), "Child " + std::to_string(i), "Parent Process");
std::string first;
auto remove_the_others = [&](Preset &preset) {
if (! first.empty())
return;
first = preset.name;
for (int i = 0; i < children; ++ i)
if (const std::string name = "Child " + std::to_string(i); name != first)
fs::remove(dir / (name + ".json"));
};
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable, remove_the_others);
REQUIRE_FALSE(first.empty());
CHECK(coll.error_count() == 0);
CHECK(coll.size() == 3); // the default preset, the parent and the first child
CHECK(coll.find_preset(first) != nullptr);
}
TEST_CASE("Without the instance lock a user preset loads but its file is not written back", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
// A directory where the lock file belongs, so the lock cannot be taken.
fs::create_directories(fs::path(user_presets_lock_path()));
PresetBundle bundle;
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
const std::string before = read_file(file);
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
REQUIRE(preset != nullptr);
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == std::vector<std::string>{ "Test Printer" });
CHECK(read_file(file) == before);
}
#ifndef _WIN32
// File permissions stop reading only on POSIX.
TEST_CASE("A user preset that cannot be read under the instance lock is counted and removed", "[Preset][Bundle][InstanceLock]")
{
if (::geteuid() == 0)
SKIP("file permissions do not stop root");
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
const fs::path file = temp_dir.path() / PRESET_PRINT_NAME / "Unreadable.json";
write_minimal_child(file, "Unreadable", "Parent Process");
fs::permissions(file, fs::no_perms);
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Unreadable") == nullptr);
CHECK_FALSE(fs::exists(file));
}
#endif
#ifndef _WIN32
// Creating a symlink needs no privilege only on POSIX.
TEST_CASE("A user preset that is a symlink to itself is counted and removed while the rest still load", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
write_minimal_child(dir / "Good A.json", "Good A", "Parent Process");
write_minimal_child(dir / "Good B.json", "Good B", "Parent Process");
const fs::path loop = dir / "Loop.json";
fs::create_symlink(loop.filename(), loop);
PresetsConfigSubstitutions substitutions;
REQUIRE_NOTHROW(coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable));
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Good A") != nullptr);
CHECK(coll.find_preset("Good B") != nullptr);
CHECK(coll.find_preset("Loop") == nullptr);
boost::system::error_code ec;
CHECK(fs::symlink_status(loop, ec).type() == fs::file_not_found);
}
#endif
#ifndef _WIN32
// Creating a symlink needs no privilege only on POSIX.
TEST_CASE("A user preset symlinked to a missing target is counted but the link is kept", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
const fs::path link = dir / "Linked.json";
fs::create_directories(dir);
fs::create_symlink(temp_dir.path() / "unmounted" / "Linked.json", link);
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Linked") == nullptr);
boost::system::error_code ec;
CHECK(fs::symlink_status(link, ec).type() == fs::symlink_file);
}
#endif
#ifndef _WIN32
// The read-only bit on a directory stops file creation only on POSIX.
TEST_CASE("A user filament whose derived compatible printer cannot be written back still loads with the failure counted", "[Preset][Bundle]")
{
if (::geteuid() == 0)
SKIP("a read-only directory does not stop root");
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path dir = temp_dir.path() / PRESET_FILAMENT_NAME;
const fs::path file = dir / "My PLA @Test Printer.json";
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
const std::string before = read_file(file);
fs::permissions(file, fs::owner_read);
fs::permissions(dir, fs::owner_read | fs::owner_exe);
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
fs::permissions(dir, fs::owner_all);
fs::permissions(file, fs::owner_read | fs::owner_write);
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
REQUIRE(preset != nullptr);
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == std::vector<std::string>{ "Test Printer" });
CHECK(bundle.filaments.error_count() == 1);
CHECK(read_file(file) == before);
}
#endif
-109
View File
@@ -10,8 +10,6 @@
#include <cctype>
#include <fstream>
#include <string>
#include <thread>
#include <system_error>
#ifndef _WIN32
#include <unistd.h> // getuid
@@ -64,113 +62,6 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
#endif
}
TEST_CASE("write_file_atomically replaces the target and leaves no temporary file", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "preset.json";
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
REQUIRE_FALSE(write_file_atomically(target.string(), "second"));
std::string content;
load_string_file(target, content);
REQUIRE(content == "second");
size_t entries = 0;
for (auto &entry : boost::filesystem::directory_iterator(dir.path())) {
(void) entry;
++entries;
}
REQUIRE(entries == 1);
}
TEST_CASE("write_file_atomically reports a missing directory and writes nothing", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "missing" / "preset.json";
const std::error_code ec = write_file_atomically(target.string(), "x");
REQUIRE(ec == std::errc::no_such_file_or_directory);
REQUIRE_FALSE(boost::filesystem::exists(target));
}
#ifndef _WIN32
// The read-only bit on a directory stops file creation only on POSIX.
TEST_CASE("write_file_atomically writes in place when no temporary can be created beside an existing target", "[utils]") {
if (::geteuid() == 0)
SKIP("a read-only directory does not stop root");
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "preset.json";
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_read | boost::filesystem::owner_exe);
const std::error_code replaced = write_file_atomically(target.string(), "second");
const std::error_code created = write_file_atomically((dir.path() / "new.json").string(), "x");
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_all);
REQUIRE_FALSE(replaced);
REQUIRE(created == std::errc::permission_denied);
std::string content;
load_string_file(target, content);
REQUIRE(content == "second");
}
#endif
TEST_CASE("write_file_atomically keeps bytes intact in binary mode", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "blob.bin";
const std::string bytes("a\r\nb\0c", 6);
REQUIRE_FALSE(write_file_atomically(target.string(), bytes, /*binary=*/true));
REQUIRE(boost::filesystem::file_size(target) == bytes.size());
}
#ifndef _WIN32
TEST_CASE("write_file_atomically writes through a symlink and keeps the target's permissions", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path real = dir.path() / "real.json";
const boost::filesystem::path link = dir.path() / "link.json";
REQUIRE_FALSE(write_file_atomically(real.string(), "first"));
boost::filesystem::permissions(real, boost::filesystem::owner_read | boost::filesystem::owner_write);
boost::filesystem::create_symlink(real, link);
REQUIRE_FALSE(write_file_atomically(link.string(), "second"));
REQUIRE(boost::filesystem::is_symlink(boost::filesystem::symlink_status(link)));
std::string content;
load_string_file(real, content);
REQUIRE(content == "second");
REQUIRE_FALSE(write_file_atomically(real.string(), "third"));
const auto perms = boost::filesystem::status(real).permissions() & boost::filesystem::all_all;
REQUIRE(perms == (boost::filesystem::owner_read | boost::filesystem::owner_write));
}
#endif
TEST_CASE("write_file_atomically survives two threads writing one target", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "shared.json";
const std::string a(20000, 'a'), b(20000, 'b');
std::thread other([&] {
for (int i = 0; i < 50; ++i)
write_file_atomically(target.string(), a);
});
for (int i = 0; i < 50; ++i)
write_file_atomically(target.string(), b);
other.join();
std::string content;
load_string_file(target, content);
const bool whole = content == a || content == b;
REQUIRE(whole);
// No temporary may be left; a scanner on Windows may briefly hold the old
// file under another name, so only the temporaries are counted.
size_t temporaries = 0;
for (auto &entry : boost::filesystem::directory_iterator(dir.path()))
if (entry.path().extension() == ".tmp")
++temporaries;
REQUIRE(temporaries == 0);
}
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
ScopedTemporaryFile source(".txt");
{
+18 -480
View File
@@ -3,21 +3,12 @@
#include <boost/filesystem.hpp>
#include <boost/crc.hpp>
#include <cereal/archives/binary.hpp>
#include <algorithm>
#include <atomic>
#include <cstring>
#include <fstream>
#include <functional>
#include <memory>
#include <numeric>
#include <random>
#include <set>
#include <sstream>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/task_group.h>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PresetCacheFormat.hpp"
#include "libslic3r/Preset.hpp"
@@ -61,27 +52,6 @@ void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std
<< R"(","from":"system","instantiation":"true","layer_height":"0.2"})";
}
// A vendor whose process list is `processes`, each a preset name and the text of
// its sub-file, listed in that order.
void write_process_vendor(const fs::path& dir, const std::string& vendor,
const std::vector<std::pair<std::string, std::string>>& processes)
{
fs::create_directories(dir / vendor / "process");
std::ofstream index((dir / (vendor + ".json")).string());
index << R"({"version":"1.0.0","name":")" << vendor << R"(","process_list":[)";
for (size_t i = 0; i < processes.size(); ++ i) {
const std::string sub_path = "process/p" + std::to_string(i) + ".json";
index << (i ? "," : "") << R"({"name":")" << processes[i].first << R"(","sub_path":")" << sub_path << R"("})";
std::ofstream((dir / vendor / sub_path).string()) << processes[i].second;
}
index << "]}";
}
std::string process_json(const std::string& name, const std::string& extra = std::string())
{
return R"({"type":"process","name":")" + name + R"(","from":"system",)" + extra + R"("layer_height":"0.2"})";
}
// A small but complete vendor: one machine model, one process, a non-instantiated
// base filament with an instantiated child that inherits it and includes a
// dual-extruder template, a second standalone filament carrying explicit
@@ -158,80 +128,6 @@ void write_lib_tree(const fs::path& dir, const std::string& version, const std::
<< R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})";
}
struct FixtureEntry {
std::string name, sub_path;
std::vector<size_t> deps; // indices of the entries this one inherits or includes
};
// Filaments with two levels of inheritance, a template to include, and `leaves`
// presets that inherit either level, some including the template. Only the
// subfiles are written; write_filament_list lists them.
std::vector<FixtureEntry> write_layered_filaments(const fs::path& dir, const std::string& vendor, int leaves)
{
fs::create_directories(dir / vendor / "filament");
std::vector<FixtureEntry> entries {
{ vendor + " Base PLA", "filament/base.json", {} },
{ vendor + " Mid PLA", "filament/mid.json", {0} },
{ vendor + " dual template", "filament/template.json", {} },
};
std::ofstream((dir / vendor / "filament" / "base.json").string())
<< R"({"type":"filament","name":")" << entries[0].name << R"(","from":"system","instantiation":"false",)"
<< R"("filament_id":"GFA_base","filament_cost":"42","filament_max_volumetric_speed":["12"]})";
std::ofstream((dir / vendor / "filament" / "mid.json").string())
<< R"({"type":"filament","name":")" << entries[1].name << R"(","from":"system","instantiation":"false",)"
<< R"("inherits":")" << entries[0].name << R"(","filament_flow_ratio":"0.95"})";
std::ofstream((dir / vendor / "filament" / "template.json").string())
<< R"({"type":"filament","name":")" << entries[2].name << R"(","from":"system","instantiation":"false",)"
<< R"("filament_extruder_variant":["Direct Drive Standard","Direct Drive High Flow"],)"
<< R"("filament_max_volumetric_speed":["20","22"]})";
for (int i = 0; i < leaves; ++ i) {
const size_t parent = i % 2;
const bool include = i % 3 == 0;
entries.push_back({ vendor + " PLA " + std::to_string(i) + " @0.4", "filament/leaf" + std::to_string(i) + ".json",
include ? std::vector<size_t>{parent, 2} : std::vector<size_t>{parent} });
std::ofstream((dir / vendor / entries.back().sub_path).string())
<< R"({"type":"filament","name":")" << entries.back().name << R"(","from":"system","instantiation":"true",)"
<< R"("inherits":")" << entries[parent].name << R"(",)"
<< (include ? R"("include":[")" + entries[2].name + R"("],)" : std::string())
<< R"("nozzle_temperature":[")" << (200 + i % 40) << R"("]})";
}
return entries;
}
// The vendor profile for write_layered_filaments, listing its entries in `order`,
// or as they were written when `order` is empty.
void write_filament_list(const fs::path& dir, const std::string& vendor, const std::vector<FixtureEntry>& entries,
std::vector<size_t> order = {})
{
if (order.empty()) {
order.resize(entries.size());
std::iota(order.begin(), order.end(), 0);
}
std::ofstream f((dir / (vendor + ".json")).string());
f << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)";
for (size_t k = 0; k < order.size(); ++ k)
f << (k ? "," : "") << R"({"name":")" << entries[order[k]].name << R"(","sub_path":")"
<< entries[order[k]].sub_path << R"("})";
f << "]}";
}
// A random order of `entries` that lists each one after everything it depends on.
std::vector<size_t> shuffled_after_dependencies(const std::vector<FixtureEntry>& entries, std::mt19937& rng)
{
std::vector<size_t> order;
std::vector<bool> listed(entries.size(), false);
while (order.size() < entries.size()) {
std::vector<size_t> ready;
for (size_t i = 0; i < entries.size(); ++ i)
if (! listed[i] && std::all_of(entries[i].deps.begin(), entries[i].deps.end(), [&](size_t d) { return listed[d]; }))
ready.push_back(i);
const size_t next = ready[std::uniform_int_distribution<size_t>(0, ready.size() - 1)(rng)];
listed[next] = true;
order.push_back(next);
}
return order;
}
// A vendor whose one filament inherits the library's base and states nothing of
// its own — everything it shows comes from the library it is resolved against.
void write_vendor_with_lib_filament(const fs::path& dir, const std::string& vendor, const std::string& version)
@@ -728,298 +624,6 @@ TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", "
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // through the include
}
TEST_CASE("a wide vendor loads from its cache exactly as it loads from JSON", "[VendorCache]")
{
InstallDirs dirs;
constexpr int leaves = 400;
write_filament_list(dirs.system, "Acme", write_layered_filaments(dirs.system, "Acme", leaves));
PresetBundle from_json;
from_json.set_generate_vendor_caches(true);
from_json.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
REQUIRE(fs::exists(dirs.system / "Acme.opc"));
fs::remove_all(dirs.system / "Acme");
PresetBundle from_cache;
from_cache.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
auto a = presets_for(from_json.filaments, "Acme");
auto b = presets_for(from_cache.filaments, "Acme");
REQUIRE(a.size() == size_t(leaves));
REQUIRE(b.size() == a.size());
for (size_t i = 0; i < a.size(); ++ i) {
CHECK(a[i]->name == b[i]->name);
CHECK(preset_deep_equal(*a[i], *b[i]));
}
CHECK(from_cache.error_count() == from_json.error_count());
// The two-level chain resolved, so the leaf has the middle profile's flow ratio
// and the base profile's filament id, neither of which it states.
const Preset* leaf = from_cache.filaments.find_preset("Acme PLA 7 @0.4", false);
REQUIRE(leaf != nullptr);
CHECK(leaf->filament_id == "GFA_base");
const auto* flow = leaf->config.option<ConfigOptionFloats>("filament_flow_ratio");
REQUIRE(flow != nullptr);
CHECK_THAT(flow->values.front(), WithinAbs(0.95, 1e-9));
}
TEST_CASE("presets install the same in any order that lists each after what it depends on", "[VendorCache]")
{
InstallDirs dirs;
const std::vector<FixtureEntry> entries = write_layered_filaments(dirs.system, "Acme", 150);
auto load = [&](const std::vector<size_t>& order) {
write_filament_list(dirs.system, "Acme", entries, order);
auto bundle = std::make_unique<PresetBundle>();
bundle->load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
return bundle;
};
const auto reference = load({});
CHECK(reference->error_count() == 0);
const auto expected = presets_for(reference->filaments, "Acme");
REQUIRE(expected.size() == 150);
// Every seed checks against one reference load, which GENERATE would repeat per seed.
for (unsigned seed = 0; seed < 8; ++ seed) {
CAPTURE(seed);
std::mt19937 rng(seed);
const auto shuffled = load(shuffled_after_dependencies(entries, rng));
CHECK(shuffled->error_count() == 0);
const auto actual = presets_for(shuffled->filaments, "Acme");
REQUIRE(actual.size() == expected.size());
for (size_t i = 0; i < actual.size(); ++ i)
CHECK(preset_deep_equal(*actual[i], *expected[i]));
}
}
TEST_CASE("a key misplaced into a vendor preset is reported and removed", "[VendorCache]")
{
InstallDirs dirs;
write_process_vendor(dirs.system, "Acme", {
{ "Acme template", R"({"type":"process","name":"Acme template","from":"system","instantiation":"false","wall_loops":"5"})" },
{ "Acme base", R"({"type":"process","name":"Acme base","from":"system","instantiation":"false","filament_cost":"5"})" },
{ "0.20mm Standard @Acme", process_json("0.20mm Standard @Acme",
R"("instantiation":"true","inherits":"Acme base","include":["Acme template"],"nozzle_temperature":["210"],)") } });
PresetBundle bundle;
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(bundle.error_count() == 2);
const Preset* preset = bundle.prints.find_preset("0.20mm Standard @Acme", false);
REQUIRE(preset != nullptr);
CHECK_FALSE(preset->config.has("filament_cost"));
CHECK_FALSE(preset->config.has("nozzle_temperature"));
CHECK(preset->config.opt_int("wall_loops") == 5);
}
TEST_CASE("each vendor loads from the directory it is listed with", "[VendorCache]")
{
InstallDirs dirs;
write_vendor_tree(dirs.system, "Acme", "1.0.0");
write_vendor_tree(dirs.profiles, "Zeta", "2.0.0");
PresetBundle bundle;
bundle.load_vendors({ { "Acme", dirs.system }, { "Zeta", dirs.profiles } },
ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false);
CHECK(bundle.vendors.count("Acme") == 1);
CHECK(bundle.vendors.count("Zeta") == 1);
CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr);
CHECK(bundle.prints.find_preset("0.20mm Standard @Zeta", false) != nullptr);
}
TEST_CASE("a vendor that fails to load is left out, reported, and the others still load", "[VendorCache]")
{
InstallDirs dirs;
write_vendor_tree(dirs.system, "Acme", "1.0.0");
write_process_vendor(dirs.system, "Broken", { { "Broken A", "{not-json" } });
PresetBundle bundle;
std::vector<std::string> failed;
const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { "Broken", dirs.system } },
ForwardCompatibilitySubstitutionRule::EnableSilent,
/*allow_cache=*/false, nullptr, &failed).second;
CHECK(bundle.vendors.count("Acme") == 1);
CHECK(bundle.vendors.count("Broken") == 0);
CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr);
CHECK(errors.find("Broken") != std::string::npos);
CHECK(failed == std::vector<std::string>{ "Broken" });
}
TEST_CASE("a filament library that fails partway is reported, and vendors inheriting from it are left out", "[VendorCache]")
{
InstallDirs dirs;
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
write_lib_tree(dirs.system, "1.0.0", "20");
std::ofstream((dirs.system / (lib + ".json")).string())
<< R"({"version":"1.0.0","name":")" << lib << R"(","filament_list":[)"
<< R"({"name":"Generic PLA","sub_path":"filament/generic_pla.json"},)"
<< R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json"}]})";
std::ofstream((dirs.system / lib / "filament" / "generic_petg.json").string()) << "{not-json";
write_vendor_with_lib_filament(dirs.system, "Acme", "1.0.0");
write_vendor_tree(dirs.system, "Zeta", "1.0.0");
PresetBundle bundle;
std::vector<std::string> failed;
const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { lib, dirs.system }, { "Zeta", dirs.system } },
ForwardCompatibilitySubstitutionRule::EnableSilent,
/*allow_cache=*/false, nullptr, &failed).second;
CHECK(failed == std::vector<std::string>{ lib, "Acme" });
CHECK(errors.find("generic_petg.json") != std::string::npos);
CHECK(bundle.vendors.count(lib) == 1);
CHECK(bundle.vendors.count("Acme") == 0);
CHECK(bundle.filaments.find_preset("Acme PLA @0.4", false) == nullptr);
CHECK(bundle.vendors.count("Zeta") == 1);
CHECK(bundle.prints.find_preset("0.20mm Standard @Zeta", false) != nullptr);
}
TEST_CASE("a vendor whose profile cannot be read is left out, reported, and the others still load", "[VendorCache]")
{
InstallDirs dirs;
write_vendor_tree(dirs.system, "Acme", "1.0.0");
std::ofstream((dirs.system / "Broken.json").string()) << "{not-json";
PresetBundle bundle;
const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { "Broken", dirs.system } },
ForwardCompatibilitySubstitutionRule::EnableSilent,
/*allow_cache=*/false).second;
CHECK(bundle.vendors.count("Acme") == 1);
CHECK(bundle.vendors.count("Broken") == 0);
CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr);
CHECK(errors.find("Broken.json") != std::string::npos);
}
TEST_CASE("a canceled vendor load starts no vendor", "[VendorCache]")
{
InstallDirs dirs;
write_vendor_tree(dirs.system, "Acme", "1.0.0");
write_vendor_tree(dirs.system, "Zeta", "1.0.0");
const std::atomic<bool> cancel { true };
PresetBundle bundle;
bundle.load_vendors({ { "Acme", dirs.system }, { "Zeta", dirs.system } },
ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false, &cancel);
CHECK(bundle.vendors.empty());
CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) == nullptr);
}
TEST_CASE("a preset two vendors both define is kept from the first listed and reported under the others", "[VendorCache]")
{
InstallDirs dirs;
for (const std::string vendor : { "Acme", "Mira", "Zeta" })
write_process_vendor(dirs.system, vendor, {
{ "Shared", process_json("Shared", R"("instantiation":"true",)") },
{ "Own @" + vendor, process_json("Own @" + vendor, R"("instantiation":"true",)") } });
PresetBundle bundle;
const std::string errors = bundle.load_vendors({ { "Mira", dirs.system }, { "Acme", dirs.system }, { "Zeta", dirs.system } },
ForwardCompatibilitySubstitutionRule::EnableSilent,
/*allow_cache=*/false).second;
const Preset* shared = bundle.prints.find_preset("Shared", false);
REQUIRE(shared != nullptr);
REQUIRE(shared->vendor != nullptr);
CHECK(shared->vendor->id == "Mira");
CHECK(errors.find("vendor Mira") == std::string::npos);
CHECK(errors.find("Found duplicated settings in vendor Acme's json file lists: Shared") != std::string::npos);
CHECK(errors.find("Found duplicated settings in vendor Zeta's json file lists: Shared") != std::string::npos);
CHECK(bundle.error_count() == 2);
for (const std::string vendor : { "Acme", "Mira", "Zeta" }) {
const Preset* own = bundle.prints.find_preset("Own @" + vendor, false);
REQUIRE(own != nullptr);
CHECK(own->vendor == &bundle.vendors.at(vendor));
}
}
TEST_CASE("filaments merged from several vendors come out generic first, then by name", "[VendorCache]")
{
InstallDirs dirs;
auto write_filaments = [&](const std::string& vendor, const std::vector<std::string>& names) {
fs::create_directories(dirs.system / vendor / "filament");
std::ofstream index((dirs.system / (vendor + ".json")).string());
index << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)";
for (size_t i = 0; i < names.size(); ++ i) {
const std::string sub_path = "filament/f" + std::to_string(i) + ".json";
index << (i ? "," : "") << R"({"name":")" << names[i] << R"(","sub_path":")" << sub_path << R"("})";
std::ofstream((dirs.system / vendor / sub_path).string())
<< R"({"type":"filament","name":")" << names[i] << R"(","from":"system","instantiation":"true",)"
<< R"("filament_id":"GF)" << vendor << i << R"("})";
}
index << "]}";
};
write_filaments("Zeta", { "Zeta PLA @0.4", "Generic PETG @Zeta" });
write_filaments("Acme", { "Acme PLA @0.4", "Generic PLA @Acme" });
PresetBundle bundle;
bundle.load_vendors({ { "Zeta", dirs.system }, { "Acme", dirs.system } },
ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false);
std::vector<std::string> names;
for (const Preset& preset : bundle.filaments.get_presets())
if (! preset.is_default)
names.push_back(preset.name);
CHECK(names == std::vector<std::string>{ "Generic PETG @Zeta", "Generic PLA @Acme", "Acme PLA @0.4", "Zeta PLA @0.4" });
for (const std::string& name : names)
CHECK(bundle.filaments.find_preset(name, false) != nullptr);
}
TEST_CASE("a vendor read while the filament library loads resolves against it, from JSON and from its cache", "[VendorCache]")
{
InstallDirs dirs;
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
write_lib_tree(dirs.system, "1.0.0", "20");
write_vendor_with_lib_filament(dirs.system, "Acme", "1.0.0");
auto load = [&] {
auto bundle = std::make_unique<PresetBundle>();
bundle->set_generate_vendor_caches(true);
bundle->load_vendors({ { "Acme", dirs.system }, { lib, dirs.system } },
ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/true);
return bundle;
};
const auto from_json = load();
REQUIRE(fs::exists(dirs.system / "Acme.opc"));
fs::remove_all(dirs.system / "Acme");
const auto from_cache = load();
for (const PresetBundle* bundle : { from_json.get(), from_cache.get() }) {
const Preset* pla = bundle->filaments.find_preset("Acme PLA @0.4", false);
REQUIRE(pla != nullptr);
CHECK(pla->filament_id == "GFL99");
CHECK_THAT(pla->config.option<ConfigOptionFloats>("filament_cost")->values.front(), WithinAbs(20., 1e-9));
CHECK(bundle->error_count() == 0);
}
}
TEST_CASE("repeated cache loads of one vendor produce the same presets", "[VendorCache]")
{
InstallDirs dirs;
write_filament_list(dirs.system, "Acme", write_layered_filaments(dirs.system, "Acme", 400));
PresetBundle seed;
seed.set_generate_vendor_caches(true);
seed.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
REQUIRE(fs::exists(dirs.system / "Acme.opc"));
fs::remove_all(dirs.system / "Acme");
std::vector<PresetBundle> loads(3);
for (PresetBundle& bundle : loads)
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
auto first = presets_for(loads[0].filaments, "Acme");
REQUIRE(!first.empty());
for (size_t run = 1; run < loads.size(); ++ run) {
auto again = presets_for(loads[run].filaments, "Acme");
REQUIRE(again.size() == first.size());
for (size_t i = 0; i < first.size(); ++ i) {
CHECK(first[i]->name == again[i]->name);
CHECK(preset_deep_equal(*first[i], *again[i]));
}
CHECK(loads[run].error_count() == loads[0].error_count());
}
}
TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]")
{
TempDir tmp;
@@ -1262,27 +866,6 @@ TEST_CASE("a vendor installed as its cache alone still loads after a library upd
CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9));
}
TEST_CASE("a cache install stopped by a canceled task group throws instead of rejecting the cache", "[VendorCache]")
{
TempDir tmp;
const std::string cache = (tmp.path / "Acme.opc").string();
REQUIRE(save_one_vendor(cache, one_vendor("Acme"), "Acme", "1.0.0", {filament_entry("Acme PLA @0.4")}));
PresetBundle bundle;
bool threw = false;
tbb::task_group_context context;
tbb::parallel_for(tbb::blocked_range<size_t>(0, 1), [&](const tbb::blocked_range<size_t>&) {
context.cancel_group_execution();
try {
bundle.load_vendor_cache(cache, "Acme", Semver(1, 0, 0));
} catch (const std::runtime_error&) {
threw = true;
}
}, context);
CHECK(threw);
}
TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSONs", "[VendorCache]")
{
TempDir tmp;
@@ -1310,12 +893,6 @@ TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSON
user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(presets_loaded == 1);
CHECK(out.vendors.at("Acme").name == "Acme"); // the profile's name, not the cache's
// Through load_vendors, which reads the cache before installing it.
PresetBundle several;
several.load_vendors({ { "Acme", user } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/true);
CHECK(several.vendors.at("Acme").name == "Acme");
CHECK(several.prints.find_preset("0.20mm Standard @Acme", false) != nullptr);
}
TEST_CASE("a profile with no usable version is never served from cache", "[VendorCache]")
@@ -1803,6 +1380,24 @@ TEST_CASE("a header claiming more body than the file holds is rejected", "[Vendo
REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0)));
}
TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]")
{
TempDir tmp;
const std::string cache = (tmp.path / "Durable.opc").string();
REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0"));
const std::string before = slurp(cache);
// A directory where the temp file wants to go: the write cannot complete,
// and must not have destroyed what was already there to find that out.
const fs::path blocker = fs::path(cache + "." + std::to_string(get_current_pid()) + ".tmp");
fs::create_directories(blocker);
REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0"));
CHECK(slurp(cache) == before);
fs::remove_all(blocker);
}
TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]")
{
// The regression the fingerprint used to prevent by refusing the file
@@ -2096,31 +1691,6 @@ TEST_CASE("an include listed after the preset that names it is an error, and the
CHECK(silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>{12.});
}
TEST_CASE("an include listed after the preset that names it is missing from the cache load too", "[VendorCache]")
{
InstallDirs dirs;
write_full_vendor_tree(dirs.system, "Acme", "1.0.0", /*templates_last=*/true);
PresetBundle from_json;
from_json.set_generate_vendor_caches(true);
from_json.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
REQUIRE(fs::exists(dirs.system / "Acme.opc"));
fs::remove_all(dirs.system / "Acme");
PresetBundle from_cache;
from_cache.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(from_cache.error_count() == 2);
const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr);
CHECK(pr->config.opt_string("machine_start_gcode") != "G28 ; template");
const Preset* silk = from_cache.filaments.find_preset("Acme Silk PLA @0.4", false);
REQUIRE(silk != nullptr);
const auto& speed = silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values;
REQUIRE(speed.size() == 1);
CHECK_THAT(speed.front(), WithinAbs(12., 1e-9));
}
TEST_CASE("a G-code template that states no instantiation is included, not loaded as a preset", "[VendorCache]")
{
InstallDirs dirs;
@@ -2142,35 +1712,3 @@ TEST_CASE("a G-code template that states no instantiation is included, not loade
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template");
CHECK(presets_for(bundle.printers, "Acme").size() == 1);
}
TEST_CASE("a sub-file that fails to parse leaves the ones listed before it installed", "[VendorCache]")
{
InstallDirs dirs;
write_process_vendor(dirs.system, "Acme", {
{ "Acme A", process_json("Acme A", R"("instantiation":"true",)") },
{ "Acme B", "{not-json" },
{ "Acme C", process_json("Acme C", R"("instantiation":"true",)") } });
PresetBundle bundle;
CHECK_THROWS_AS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent),
ConfigurationError);
CHECK(bundle.prints.find_preset("Acme A", false) != nullptr);
CHECK(bundle.prints.find_preset("Acme C", false) == nullptr);
}
TEST_CASE("a load that fails to install partway counts no errors of the sub-files after it", "[VendorCache]")
{
InstallDirs dirs;
// B names a parent nobody defines. C states no instantiation, an error of its own.
write_process_vendor(dirs.system, "Acme", {
{ "Acme A", process_json("Acme A", R"("instantiation":"true",)") },
{ "Acme B", process_json("Acme B", R"("instantiation":"true","inherits":"Nobody",)") },
{ "Acme C", process_json("Acme C") } });
PresetBundle bundle;
CHECK_THROWS_AS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent),
ConfigurationError);
// B's missing parent and B's failed install.
CHECK(bundle.error_count() == 2);
CHECK(bundle.prints.find_preset("Acme A", false) != nullptr);
}
+1
View File
@@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_install.cpp
test_plugin_lifecycle.cpp
test_plugin_printer_agent.cpp
test_printhost.cpp
test_slicing_pipeline_bindings.cpp
test_slicing_pipeline_config.cpp
test_plugin_sort.cpp
+213
View File
@@ -0,0 +1,213 @@
#include <catch2/catch_all.hpp>
#include <nlohmann/json.hpp>
#include "slic3r/Utils/PrintHost.hpp"
using namespace Slic3r;
namespace {
class TestPrintHost : public PrintHost
{
public:
using PrintHost::format_error;
const char* get_name() const override { return "Test"; }
bool test(wxString&) const override { return true; }
wxString get_test_ok_msg() const override { return {}; }
wxString get_test_failed_msg(wxString&) const override { return {}; }
bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { return true; }
bool has_auto_discovery() const override { return false; }
bool can_test() const override { return false; }
PrintHostPostUploadActions get_post_upload_actions() const override { return {}; }
std::string get_host() const override { return {}; }
};
std::string format_error(const std::string& body, const std::string& error, unsigned status)
{
return TestPrintHost().format_error(body, error, status).ToStdString();
}
std::string envelope(int code, const std::string& message, const std::string& traceback)
{
return nlohmann::json{{"error", {{"code", code}, {"message", message}, {"traceback", traceback}}}}.dump();
}
std::string moonraker_error(int code, const std::string& message, const std::string& detail = {})
{
std::string line = "tornado.web.HTTPError: HTTP " + std::to_string(code) + ": " + message;
if (!detail.empty())
line += " (" + detail + ")";
return envelope(code, message, "Traceback (most recent call last):\n ...\n" + line + "\n");
}
// A real Moonraker body for uploading a file that is being printed.
constexpr const char* k_busy_file_403 =
R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON";
} // namespace
TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]")
{
const std::string msg = format_error(k_busy_file_403, "", 403);
INFO("actual: " << msg);
CHECK(msg == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("Traceback"));
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("file_manager.py"));
}
TEST_CASE("The specific cause is recovered from a file endpoint's traceback", "[PrintHost]")
{
SECTION("a plain detail")
{
const std::string body = moonraker_error(403, "Forbidden", "File is loaded, upload not permitted");
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
}
SECTION("a detail whose own parentheses nest (a filename)")
{
const std::string detail = "Directory does not exist (/home/pi/gcodes/plate (1).gcode)";
const std::string body = moonraker_error(400, "Bad Request", detail);
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
}
SECTION("a detail that contains the reason phrase")
{
const std::string body = moonraker_error(403, "Forbidden", "Forbidden zone: access denied");
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (Forbidden zone: access denied)");
}
SECTION("a detail that spans lines")
{
const std::string detail = "Move out of range\nX=250.000 Y=10.000";
const std::string body = moonraker_error(400, "Bad Request", detail);
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
}
SECTION("a detail that only repeats the reason phrase is dropped")
{
const std::string body = moonraker_error(401, "Unauthorized", "Unauthorized");
CHECK(format_error(body, "", 401) == "HTTP 401: Unauthorized");
}
}
TEST_CASE("An unhandled exception shows its type and message", "[PrintHost]")
{
const std::string frame = "Traceback (most recent call last):\n"
" File \"/home/pi/moonraker/moonraker/components/file_manager/file_manager.py\", line 1, in write\n"
" self._write(data)\n";
SECTION("a one-line message")
{
const std::string body = envelope(500, "Internal Server Error", frame + "OSError: [Errno 28] No space left on device\n");
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 28] No space left on device)");
}
SECTION("a message that spans lines")
{
const std::string body = envelope(500, "Internal Server Error", frame + "ServerError: Klippy request failed\n see klippy.log\n");
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (ServerError: Klippy request failed\n see klippy.log)");
}
SECTION("raised while handling an HTTPError with the same code")
{
const std::string traceback = frame + "tornado.web.HTTPError: HTTP 500: Internal Server Error (Database locked)\n\n"
"During handling of the above exception, another exception occurred:\n\n" +
frame + "OSError: [Errno 5] Input/output error\n";
const std::string body = envelope(500, "Internal Server Error", traceback);
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
}
SECTION("a traceback with no header")
{
const std::string body = envelope(500, "Internal Server Error", "OSError: [Errno 5] Input/output error");
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
}
}
TEST_CASE("A reason already complete in message is shown unchanged", "[PrintHost]")
{
SECTION("message is the whole reason, no trailing detail")
{
const std::string body = moonraker_error(503, "Klippy is not ready");
CHECK(format_error(body, "", 503) == "HTTP 503: Klippy is not ready");
}
SECTION("a message that itself contains parentheses is not duplicated")
{
const std::string reason = "Requested blocks (0-5) are unavailable";
const std::string body = moonraker_error(400, reason);
CHECK(format_error(body, "", 400) == "HTTP 400: " + reason);
}
}
TEST_CASE("A Moonraker error with no usable detail shows just the reason phrase", "[PrintHost]")
{
struct Case
{
const char* name;
const char* body;
unsigned status;
const char* expected;
};
const auto c = GENERATE(
Case{"an empty traceback", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": ""}})JSON", 500,
"HTTP 500: Internal Server Error"},
Case{"a traceback of only whitespace", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": "\n \n"}})JSON",
500, "HTTP 500: Internal Server Error"});
DYNAMIC_SECTION(c.name) { CHECK(format_error(c.body, "", c.status) == c.expected); }
}
TEST_CASE("A percent sign in the reason is not a format specifier", "[PrintHost]")
{
const std::string body = moonraker_error(507, "Insufficient Storage", "disk 100% full");
CHECK(format_error(body, "", 507) == "HTTP 507: Insufficient Storage (disk 100% full)");
}
TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "[PrintHost]")
{
SECTION("OctoPrint's string-valued error member")
{
const std::string body = R"JSON({"error": "File not found"})JSON";
CHECK(format_error(body, "", 404) == "HTTP 404: " + body);
}
SECTION("PrusaLink's top-level message, not under error")
{
const std::string body = R"JSON({"title": "Conflict", "message": "Printer is printing"})JSON";
CHECK(format_error(body, "", 409) == "HTTP 409: " + body);
}
SECTION("a body that is not JSON")
{
const std::string html = "<html><head><title>502 Bad Gateway</title></head></html>";
CHECK(format_error(html, "", 502) == "HTTP 502: " + html);
}
SECTION("an error object with no traceback")
{
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error"}})JSON";
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
}
SECTION("an error object whose traceback is null")
{
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": null}})JSON";
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
}
SECTION("an envelope whose reason phrase is empty")
{
const std::string body = envelope(403, "", "Traceback (most recent call last):\nOSError: denied\n");
CHECK(format_error(body, "", 403) == "HTTP 403: " + body);
}
SECTION("a transport error with no HTTP status")
{
CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect");
}
}