mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 19:31:22 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35dc1ca6ef | ||
|
|
7f30eb8a49 | ||
|
|
b5d1866445 | ||
|
|
3b1a5c0630 | ||
|
|
18867094fa | ||
|
|
5ae88ae5c7 | ||
|
|
ea3a40d073 | ||
|
|
806568287c | ||
|
|
0342ddbe26 | ||
|
|
33af0651b7 | ||
|
|
781a5d8516 | ||
|
|
38446ab77a | ||
|
|
9080269f6f |
@@ -1,80 +0,0 @@
|
|||||||
# 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.
|
|
||||||
+45
-19
@@ -28,8 +28,8 @@ Per-vendor granularity is what makes the system practical:
|
|||||||
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
|
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
|
||||||
vendors keep theirs — even when the bumped vendor is the shared Orca filament
|
vendors keep theirs — even when the bumped vendor is the shared Orca filament
|
||||||
library everyone else inherits from.
|
library everyone else inherits from.
|
||||||
- The setup wizard, which loads vendors one at a time, gets the same speedup as
|
- The setup wizard loads its vendors through the same routine as startup, so it
|
||||||
startup without a second code path.
|
gets the same speedup without a second code path.
|
||||||
- A vendor with no cache, or a broken one, costs only that vendor a parse.
|
- A vendor with no cache, or a broken one, costs only that vendor a parse.
|
||||||
|
|
||||||
A cache holds *system* presets only. User presets, project settings and modified
|
A cache holds *system* presets only. User presets, project settings and modified
|
||||||
@@ -162,15 +162,20 @@ cache nothing can invalidate is worse than no cache.
|
|||||||
|
|
||||||
Vendors load in a fixed order, because filament inheritance crosses exactly one
|
Vendors load in a fixed order, because filament inheritance crosses exactly one
|
||||||
boundary: any vendor's filament may inherit from the shared Orca filament library,
|
boundary: any vendor's filament may inherit from the shared Orca filament library,
|
||||||
and nothing else reaches across vendors — an `include` is always vendor-local. The
|
and nothing else reaches across vendors — an `include` is always vendor-local. Only
|
||||||
library therefore goes first, alone; every other vendor follows in parallel, resolving
|
installing a vendor's presets crosses it; reading the vendor, from its cache or its
|
||||||
against it; and the results are merged in a stable order:
|
JSONs, needs nothing from the library. So every other vendor is read while the library
|
||||||
|
loads, each is installed against it as soon as both are done, and the results are
|
||||||
|
merged in a stable order:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
lib["1 · OrcaFilamentLibrary<br/>loaded first, synchronously"] --> par["2 · every other vendor in parallel,<br/>each into its own bundle, filaments<br/>resolving against the loaded library"] --> merge["3 · bundles merged into one,<br/>sequentially, in stable vendor order"]
|
lib["1 · OrcaFilamentLibrary loaded;<br/>meanwhile every other vendor read<br/>from its cache or its JSONs"] --> par["2 · every other vendor installed<br/>in parallel, each into its own bundle,<br/>filaments resolving against the library"] --> merge["3 · bundles merged into one,<br/>in one pass per collection,<br/>in stable vendor order"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`PresetBundle::load_vendors` runs these steps for startup and for the setup wizard,
|
||||||
|
which hand it the vendors to load and the directory each is installed in.
|
||||||
|
|
||||||
Whether a vendor comes from its cache or from a parse changes nothing in that
|
Whether a vendor comes from its cache or from a parse changes nothing in that
|
||||||
order — both produce the same bundle, so cached and parsed vendors mix freely in
|
order — both produce the same bundle, so cached and parsed vendors mix freely in
|
||||||
one startup.
|
one startup.
|
||||||
@@ -211,11 +216,12 @@ shipped cache answered first, so the profile in `<data_dir>/system/` was never p
|
|||||||
and its cache was never written back.
|
and its cache was never written back.
|
||||||
|
|
||||||
Serving from a cache is not a memory-image restore. The entries are deserialized and
|
Serving from a cache is not a memory-image restore. The entries are deserialized and
|
||||||
then installed one by one — inheritance resolved against the presets installed before
|
then installed by `install_vendor`, the routine the JSON path hands the vendor's entries
|
||||||
them and the currently loaded filament library, includes layered in, configs flattened
|
to once it has parsed the sub-files: inheritance resolved against the presets installed
|
||||||
onto the collection defaults, validated and registered — by the same function the JSON
|
before them and the currently loaded filament library, includes layered in, configs
|
||||||
path calls straight after parsing a sub-file. An `include` layers what the included
|
flattened onto the collection defaults, validated and registered. An `include` layers
|
||||||
base states, between the parent and the preset's own keys: the base's diff against the
|
what the included base states, between the parent and the preset's own keys: the base's
|
||||||
|
diff against the
|
||||||
default, taken when the base itself was installed and before the per-variant padding
|
default, taken when the base itself was installed and before the per-variant padding
|
||||||
`inherits` sees, so only what a template sets reaches the presets including it. The two
|
`inherits` sees, so only what a template sets reaches the presets including it. The two
|
||||||
paths share everything below the parse, which is what makes a cache-loaded bundle
|
paths share everything below the parse, which is what makes a cache-loaded bundle
|
||||||
@@ -223,6 +229,16 @@ indistinguishable from a JSON-loaded one by construction rather than by test cov
|
|||||||
Installation also rebuilds each preset's file path from the local data directory, so a
|
Installation also rebuilds each preset's file path from the local data directory, so a
|
||||||
shipped cache never carries the generating machine's paths.
|
shipped cache never carries the generating machine's paths.
|
||||||
|
|
||||||
|
Installing an entry is split in two. `resolve_vendor_preset` flattens it, reading only
|
||||||
|
what is registered under the names it inherits and includes, and `commit_vendor_preset`
|
||||||
|
registers it, the only step that writes anything shared. Entries resolve across threads
|
||||||
|
in runs and commit in the order the vendor lists them. A run ends before an entry that
|
||||||
|
inherits or includes one already in it, since that one's commit registers what the
|
||||||
|
entry resolves against, so no entry in a run reads what another in it registers. An
|
||||||
|
entry's parse messages are held until it commits. The bundle, the log's parse and
|
||||||
|
install messages and the error count therefore come out as parsing and installing one
|
||||||
|
entry at a time would leave them, whatever the listing order.
|
||||||
|
|
||||||
App upgrades work because a cache normally survives one. Only a deliberate
|
App upgrades work because a cache normally survives one. Only a deliberate
|
||||||
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
|
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
|
||||||
install time rather than at load: a vendor whose cache this build cannot read counts
|
install time rather than at load: a vendor whose cache this build cannot read counts
|
||||||
@@ -254,17 +270,20 @@ the wizard caches the *derived JSON*, not another form of the inputs:
|
|||||||
open, the wizard computes the current stamps (one version peek per vendor) and, when
|
open, the wizard computes the current stamps (one version peek per vendor) and, when
|
||||||
they match, serves the catalog from the file — no bundle built, no preset installed.
|
they match, serves the catalog from the file — no bundle built, no preset installed.
|
||||||
Caching bundle inputs instead was tried and measured: rebuilding the bundle from
|
Caching bundle inputs instead was tried and measured: rebuilding the bundle from
|
||||||
per-vendor caches costs ~2 s of preset installation whatever feeds it, so only
|
per-vendor caches costs over a second of preset installation whatever feeds it, so
|
||||||
skipping the rebuild entirely wins.
|
only skipping the rebuild entirely wins.
|
||||||
|
|
||||||
Any change to the set — a vendor added, removed or updated, or its cache-only
|
Any change to the set — a vendor added, removed or updated, or its cache-only
|
||||||
`.opc` replaced by a newer one — changes the stamps and retires the whole file;
|
`.opc` replaced by a newer one — changes the stamps and retires the whole file;
|
||||||
the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where
|
the wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load
|
||||||
they cover) and writes the catalog back. Selections, region and per-open decorations
|
startup uses (per-vendor caches serving where they cover), and writes the catalog
|
||||||
are applied downstream of the cache either way, so a served catalog is
|
back. When a vendor fails to load, the filament library included, that open falls
|
||||||
indistinguishable from a rebuilt one. Nothing ships this file and the updater never
|
back to the wizard's own scan of the vendor JSONs, as when no bundle can be built,
|
||||||
touches it; it is a locally written artifact, re-derived whenever stale, written
|
and writes nothing. Selections, region and per-open decorations are applied
|
||||||
through a temp file and rename so half a cache is never readable.
|
downstream of the cache either way, so a served catalog is indistinguishable from a
|
||||||
|
rebuilt one. Nothing ships this file and the updater never touches it; it is a
|
||||||
|
locally written artifact, re-derived whenever stale, written through a temp file and
|
||||||
|
rename so half a cache is never readable.
|
||||||
|
|
||||||
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
|
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
|
||||||
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor
|
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor
|
||||||
@@ -378,6 +397,13 @@ enumerates only `*.json` will find no vendors at all in a packaged build.
|
|||||||
the `CachedPreset` field list — written and read by `visit_entry` in
|
the `CachedPreset` field list — written and read by `visit_entry` in
|
||||||
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
|
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
|
||||||
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
|
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
|
||||||
|
- **Adding a kind of reference between presets**, as `inherits` and `include` are:
|
||||||
|
parse the names into `CachedPreset` (a field change, so `CACHE_VERSION` is bumped),
|
||||||
|
have `install_vendor_entries` end a run before an entry that names one already in it
|
||||||
|
and retain what the names point at, look them up only in `resolve_vendor_preset`, and
|
||||||
|
register what they point at only in `commit_vendor_preset`. The listing-order test in
|
||||||
|
`test_vendor_cache.cpp` fails for a kind the runs do not check once its fixture uses
|
||||||
|
it.
|
||||||
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
|
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
|
||||||
65535 options and one cache at most 65535 distinct enum value names.
|
65535 options and one cache at most 65535 distinct enum value names.
|
||||||
`CacheDictionary::save` throws past that, which surfaces when CI generates the
|
`CacheDictionary::save` throws past that, which surfaces when CI generates the
|
||||||
|
|||||||
+46
-71
@@ -5,6 +5,7 @@
|
|||||||
//BBS
|
//BBS
|
||||||
#include "Preset.hpp"
|
#include "Preset.hpp"
|
||||||
#include "Exception.hpp"
|
#include "Exception.hpp"
|
||||||
|
#include "InstanceLock.hpp"
|
||||||
#include "LocalesUtils.hpp"
|
#include "LocalesUtils.hpp"
|
||||||
#include "Thread.hpp"
|
#include "Thread.hpp"
|
||||||
#include "format.hpp"
|
#include "format.hpp"
|
||||||
@@ -205,13 +206,6 @@ void AppConfig::set_defaults()
|
|||||||
if (get("seq_top_layer_only").empty())
|
if (get("seq_top_layer_only").empty())
|
||||||
set("seq_top_layer_only", "1");
|
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
|
// ORCA: darken the layers the preview layer slider is not scrubbed to
|
||||||
if (get("preview_dim_previous_layers").empty())
|
if (get("preview_dim_previous_layers").empty())
|
||||||
set_bool("preview_dim_previous_layers", false);
|
set_bool("preview_dim_previous_layers", false);
|
||||||
@@ -746,10 +740,13 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs)
|
|||||||
|
|
||||||
|
|
||||||
#ifdef USE_JSON_CONFIG
|
#ifdef USE_JSON_CONFIG
|
||||||
std::string AppConfig::load()
|
std::string AppConfig::load(bool read_only)
|
||||||
{
|
{
|
||||||
json j;
|
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.
|
// 1) Read the complete config file into a boost::property_tree.
|
||||||
namespace pt = boost::property_tree;
|
namespace pt = boost::property_tree;
|
||||||
pt::ptree tree;
|
pt::ptree tree;
|
||||||
@@ -999,7 +996,6 @@ void AppConfig::save()
|
|||||||
// The config is first written to a file with a PID suffix and then moved
|
// The config is first written to a file with a PID suffix and then moved
|
||||||
// to avoid race conditions with multiple instances of Slic3r
|
// to avoid race conditions with multiple instances of Slic3r
|
||||||
const auto path = config_path();
|
const auto path = config_path();
|
||||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
|
||||||
|
|
||||||
json j;
|
json j;
|
||||||
|
|
||||||
@@ -1129,43 +1125,18 @@ void AppConfig::save()
|
|||||||
|
|
||||||
j["local_machines"][local_machine.first] = m_json;
|
j["local_machines"][local_machine.first] = m_json;
|
||||||
}
|
}
|
||||||
boost::nowide::ofstream c;
|
const std::string config_str = j.dump(1, '\t');
|
||||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
if (write_config_file(path, config_str + "\n", config_str))
|
||||||
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;
|
m_dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
std::string AppConfig::load()
|
std::string AppConfig::load(bool read_only)
|
||||||
{
|
{
|
||||||
|
// 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.
|
// 1) Read the complete config file into a boost::property_tree.
|
||||||
namespace pt = boost::property_tree;
|
namespace pt = boost::property_tree;
|
||||||
pt::ptree tree;
|
pt::ptree tree;
|
||||||
@@ -1303,7 +1274,6 @@ void AppConfig::save()
|
|||||||
// The config is first written to a file with a PID suffix and then moved
|
// The config is first written to a file with a PID suffix and then moved
|
||||||
// to avoid race conditions with multiple instances of Slic3r
|
// to avoid race conditions with multiple instances of Slic3r
|
||||||
const auto path = config_path();
|
const auto path = config_path();
|
||||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
|
||||||
|
|
||||||
std::stringstream config_ss;
|
std::stringstream config_ss;
|
||||||
if (m_mode == EAppMode::Editor)
|
if (m_mode == EAppMode::Editor)
|
||||||
@@ -1339,39 +1309,39 @@ void AppConfig::save()
|
|||||||
// One empty line before the MD5 sum.
|
// One empty line before the MD5 sum.
|
||||||
config_ss << std::endl;
|
config_ss << std::endl;
|
||||||
|
|
||||||
std::string config_str = config_ss.str();
|
const std::string config_str = config_ss.str();
|
||||||
boost::nowide::ofstream c;
|
if (write_config_file(path, config_str, config_str))
|
||||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
|
||||||
c << config_str;
|
|
||||||
#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(config_str);
|
|
||||||
#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;
|
m_dirty = false;
|
||||||
}
|
}
|
||||||
#endif
|
#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());
|
||||||
|
#ifdef WIN32
|
||||||
|
// WIN32 specific: the final replace 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);
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
||||||
{
|
{
|
||||||
const auto it_v = m_vendors.find(vendor);
|
const auto it_v = m_vendors.find(vendor);
|
||||||
@@ -1860,6 +1830,11 @@ void AppConfig::reset_selections()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string AppConfig::lock_path()
|
||||||
|
{
|
||||||
|
return Slic3r::data_dir().empty() ? std::string() : config_path() + ".lock";
|
||||||
|
}
|
||||||
|
|
||||||
std::string AppConfig::config_path()
|
std::string AppConfig::config_path()
|
||||||
{
|
{
|
||||||
#ifdef USE_JSON_CONFIG
|
#ifdef USE_JSON_CONFIG
|
||||||
@@ -1896,7 +1871,7 @@ bool AppConfig::exists()
|
|||||||
|
|
||||||
std::string AppConfig::load_if_exists()
|
std::string AppConfig::load_if_exists()
|
||||||
{
|
{
|
||||||
return boost::filesystem::exists(loading_path()) ? load() : std::string();
|
return boost::filesystem::exists(loading_path()) ? load(/*read_only=*/true) : std::string();
|
||||||
}
|
}
|
||||||
|
|
||||||
}; // namespace Slic3r
|
}; // namespace Slic3r
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define slic3r_AppConfig_hpp_
|
#define slic3r_AppConfig_hpp_
|
||||||
|
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <chrono>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "nlohmann/json.hpp"
|
#include "nlohmann/json.hpp"
|
||||||
@@ -122,14 +123,18 @@ public:
|
|||||||
|
|
||||||
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
||||||
// Return an error string, or an empty string on success.
|
// Return an error string, or an empty string on success.
|
||||||
std::string load();
|
std::string load(bool read_only = false);
|
||||||
// Treat a missing config as default state; otherwise load it normally.
|
// 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();
|
std::string load_if_exists();
|
||||||
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
||||||
void save();
|
void save();
|
||||||
|
|
||||||
// Does this config need to be saved?
|
// Does this config need to be saved?
|
||||||
bool dirty() const { return m_dirty; }
|
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; }
|
void set_dirty() { m_dirty = true; }
|
||||||
@@ -339,6 +344,8 @@ public:
|
|||||||
|
|
||||||
// Get the default config path from Slic3r::data_dir().
|
// Get the default config path from Slic3r::data_dir().
|
||||||
std::string config_path();
|
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)
|
// 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; }
|
bool legacy_datadir() const { return m_legacy_datadir; }
|
||||||
@@ -449,8 +456,16 @@ private:
|
|||||||
|
|
||||||
// Preset for each machine
|
// Preset for each machine
|
||||||
MachineSettingMap m_printer_settings;
|
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?
|
// Has any value been modified since the config.ini has been last saved or loaded?
|
||||||
bool m_dirty;
|
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
|
// Original version found in the ini file before it was overwritten
|
||||||
Semver m_orig_version;
|
Semver m_orig_version;
|
||||||
// Whether the existing version is before system profiles & configuration updating
|
// Whether the existing version is before system profiles & configuration updating
|
||||||
|
|||||||
@@ -304,6 +304,8 @@ set(lisbslic3r_sources
|
|||||||
Geometry/VoronoiUtils.cpp
|
Geometry/VoronoiUtils.cpp
|
||||||
Geometry/VoronoiUtils.hpp
|
Geometry/VoronoiUtils.hpp
|
||||||
Geometry/VoronoiVisualUtils.hpp
|
Geometry/VoronoiVisualUtils.hpp
|
||||||
|
InstanceLock.cpp
|
||||||
|
InstanceLock.hpp
|
||||||
Int128.hpp
|
Int128.hpp
|
||||||
KDTreeIndirect.hpp
|
KDTreeIndirect.hpp
|
||||||
Layer.cpp
|
Layer.cpp
|
||||||
@@ -353,6 +355,7 @@ set(lisbslic3r_sources
|
|||||||
Optimize/Optimizer.hpp
|
Optimize/Optimizer.hpp
|
||||||
Orient.cpp
|
Orient.cpp
|
||||||
Orient.hpp
|
Orient.hpp
|
||||||
|
ParallelResolve.hpp
|
||||||
ParameterUtils.cpp
|
ParameterUtils.cpp
|
||||||
ParameterUtils.hpp
|
ParameterUtils.hpp
|
||||||
pchheader.cpp
|
pchheader.cpp
|
||||||
|
|||||||
+29
-18
@@ -849,6 +849,19 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC
|
|||||||
return std::move(substitutions_ctxt.substitutions);
|
return std::move(substitutions_ctxt.substitutions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Case-insensitive compare of a JSON key against a fixed ASCII one, without
|
||||||
|
// boost::iequals, whose std::locale() takes a lock the whole process shares in the
|
||||||
|
// MSVC runtime.
|
||||||
|
static bool ascii_iequals(const std::string &key, const char *literal)
|
||||||
|
{
|
||||||
|
auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; };
|
||||||
|
size_t i = 0;
|
||||||
|
for (; i < key.size() && literal[i] != '\0'; ++ i)
|
||||||
|
if (lower(key[i]) != lower(literal[i]))
|
||||||
|
return false;
|
||||||
|
return i == key.size() && literal[i] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
|
int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
|
||||||
{
|
{
|
||||||
json j;
|
json j;
|
||||||
@@ -916,44 +929,44 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
|
|||||||
}
|
}
|
||||||
//parse the json elements
|
//parse the json elements
|
||||||
for (auto it = j.begin(); it != j.end(); it++) {
|
for (auto it = j.begin(); it != j.end(); it++) {
|
||||||
if (boost::iequals(it.key(),BBL_JSON_KEY_VERSION)) {
|
if (ascii_iequals(it.key(), BBL_JSON_KEY_VERSION)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_VERSION, it.value());
|
key_values.emplace(BBL_JSON_KEY_VERSION, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
|
||||||
//skip it
|
//skip it
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_NAME)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_NAME, it.value());
|
key_values.emplace(BBL_JSON_KEY_NAME, it.value());
|
||||||
if (it.value() == "project_settings")
|
if (it.value() == "project_settings")
|
||||||
is_project_settings = true;
|
is_project_settings = true;
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_URL)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_URL, it.value());
|
key_values.emplace(BBL_JSON_KEY_URL, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_TYPE)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_TYPE)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_TYPE, it.value());
|
key_values.emplace(BBL_JSON_KEY_TYPE, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_SETTING_ID, it.value());
|
key_values.emplace(BBL_JSON_KEY_SETTING_ID, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_FILAMENT_ID, it.value());
|
key_values.emplace(BBL_JSON_KEY_FILAMENT_ID, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_FROM)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FROM)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_FROM, it.value());
|
key_values.emplace(BBL_JSON_KEY_FROM, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_DESCRIPTION, it.value());
|
key_values.emplace(BBL_JSON_KEY_DESCRIPTION, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_INSTANTIATION, it.value());
|
key_values.emplace(BBL_JSON_KEY_INSTANTIATION, it.value());
|
||||||
}
|
}
|
||||||
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
|
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
|
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
|
||||||
}
|
}
|
||||||
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
|
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
|
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
|
||||||
} else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
|
} else if (ascii_iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
|
||||||
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
|
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
|
||||||
} else {
|
} else {
|
||||||
t_config_option_key opt_key = it.key();
|
t_config_option_key opt_key = it.key();
|
||||||
@@ -1525,11 +1538,9 @@ 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.
|
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
this->save_to_json(ss, name, from, version);
|
this->save_to_json(ss, name, from, version);
|
||||||
boost::nowide::ofstream c;
|
if (const std::error_code ec = write_file_atomically(file, ss.str()))
|
||||||
c.open(file, std::ios::out | std::ios::trunc);
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": failed to save config to %1%: %2%") % file % ec.message();
|
||||||
c << ss.str();
|
else
|
||||||
c.close();
|
|
||||||
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-10
@@ -2504,8 +2504,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(print->model().id().id);
|
ctx.name = std::to_string(print->model().id().id);
|
||||||
ctx.name = print->get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = path;
|
ctx.msg = path;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2557,8 +2556,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(print->model().id().id);
|
ctx.name = std::to_string(print->model().id().id);
|
||||||
ctx.name = print->get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\n" + err_msg;
|
ctx.msg = std::string(path) + "\n" + err_msg;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2582,8 +2580,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
boost::nowide::remove(path_tmp.c_str());
|
boost::nowide::remove(path_tmp.c_str());
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(print->model().id().id);
|
ctx.name = std::to_string(print->model().id().id);
|
||||||
ctx.name = print->get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\n" + ex.what();
|
ctx.msg = std::string(path) + "\n" + ex.what();
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2696,8 +2693,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
if (ret) {
|
if (ret) {
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(print->model().id().id);
|
ctx.name = std::to_string(print->model().id().id);
|
||||||
ctx.name = print->get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2716,8 +2712,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(print->model().id().id);
|
ctx.name = std::to_string(print->model().id().id);
|
||||||
ctx.name = print->get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = path;
|
ctx.msg = path;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#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
|
||||||
@@ -13,8 +13,19 @@
|
|||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
|
|
||||||
|
// How many setters this thread holds, so the ones nested in another can skip
|
||||||
|
// setlocale, which takes a lock the whole process shares on Windows.
|
||||||
|
static thread_local int s_numeric_locale_depth = 0;
|
||||||
|
|
||||||
CNumericLocalesSetter::CNumericLocalesSetter()
|
CNumericLocalesSetter::CNumericLocalesSetter()
|
||||||
{
|
{
|
||||||
|
// Nested in another setter on this thread, whose "C" the separator check
|
||||||
|
// confirms is still set.
|
||||||
|
if (s_numeric_locale_depth > 0 && is_decimal_separator_point()) {
|
||||||
|
m_nested = true;
|
||||||
|
++ s_numeric_locale_depth;
|
||||||
|
return;
|
||||||
|
}
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
||||||
m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr);
|
m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr);
|
||||||
@@ -29,12 +40,17 @@ CNumericLocalesSetter::CNumericLocalesSetter()
|
|||||||
m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale);
|
m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale);
|
||||||
uselocale(m_new_locale);
|
uselocale(m_new_locale);
|
||||||
#endif
|
#endif
|
||||||
|
// Counted last, since the destructor does not run for a constructor that throws.
|
||||||
|
++ s_numeric_locale_depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CNumericLocalesSetter::~CNumericLocalesSetter()
|
CNumericLocalesSetter::~CNumericLocalesSetter()
|
||||||
{
|
{
|
||||||
|
-- s_numeric_locale_depth;
|
||||||
|
if (m_nested)
|
||||||
|
return;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
|
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -19,8 +19,13 @@ class CNumericLocalesSetter {
|
|||||||
public:
|
public:
|
||||||
CNumericLocalesSetter();
|
CNumericLocalesSetter();
|
||||||
~CNumericLocalesSetter();
|
~CNumericLocalesSetter();
|
||||||
|
// A copy would restore the locale twice, and count down once more than up.
|
||||||
|
CNumericLocalesSetter(const CNumericLocalesSetter&) = delete;
|
||||||
|
CNumericLocalesSetter& operator=(const CNumericLocalesSetter&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Inside another setter on this thread, which does the setting and restoring.
|
||||||
|
bool m_nested { false };
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
std::string m_orig_numeric_locale;
|
std::string m_orig_numeric_locale;
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <tbb/blocked_range.h>
|
||||||
|
#include <tbb/parallel_for.h>
|
||||||
|
#include <tbb/task_arena.h>
|
||||||
|
#include <tbb/task_group.h>
|
||||||
|
|
||||||
|
#include "Exception.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
// A batch of resolved presets, each a whole config, is what resolve_then_commit adds
|
||||||
|
// to peak memory, so it stays far below a vendor's preset count and above any core count.
|
||||||
|
inline constexpr size_t resolve_batch_size = 64;
|
||||||
|
|
||||||
|
// Resolve `count` items that do not depend on each other and install them one at
|
||||||
|
// a time.
|
||||||
|
//
|
||||||
|
// `resolve(i)` runs on any thread and must read only, since the items are
|
||||||
|
// resolved side by side. `commit(i, resolved)` is called for every item in index
|
||||||
|
// order, on the calling thread, and is where shared state is written.
|
||||||
|
//
|
||||||
|
// The items are worked through in batches, so what is resolved and held at once
|
||||||
|
// does not grow with `count`. An exception from either callable propagates after
|
||||||
|
// the batches before it have been committed. A cancellation of the caller's task
|
||||||
|
// group, which stops a batch partway without an exception, throws RuntimeError
|
||||||
|
// before that batch commits.
|
||||||
|
//
|
||||||
|
// `ChunkSetup`, when given, is constructed once for each piece of a batch TBB hands
|
||||||
|
// out, for per-thread state a resolve would otherwise set up per item, such as the
|
||||||
|
// C numeric locale, whose setting takes a lock the whole process shares.
|
||||||
|
template<class ChunkSetup = std::monostate, class Resolve, class Commit>
|
||||||
|
void resolve_then_commit(size_t count, Resolve resolve, Commit commit)
|
||||||
|
{
|
||||||
|
using Resolved = std::invoke_result_t<Resolve&, size_t>;
|
||||||
|
|
||||||
|
std::vector<Resolved> resolved(std::min(count, resolve_batch_size));
|
||||||
|
for (size_t first = 0; first < count; first += resolve_batch_size) {
|
||||||
|
const size_t last = std::min(first + resolve_batch_size, count);
|
||||||
|
// Isolated, so a thread waiting on the batch runs none of the caller's other
|
||||||
|
// tasks before finishing it.
|
||||||
|
tbb::this_task_arena::isolate([&] {
|
||||||
|
tbb::parallel_for(tbb::blocked_range<size_t>(first, last),
|
||||||
|
[&](const tbb::blocked_range<size_t>& range) {
|
||||||
|
ChunkSetup setup;
|
||||||
|
(void) setup;
|
||||||
|
for (size_t i = range.begin(); i < range.end(); ++ i)
|
||||||
|
resolved[i - first] = resolve(i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (tbb::is_current_task_group_canceling())
|
||||||
|
throw RuntimeError("resolve_then_commit: canceled before the batch was resolved");
|
||||||
|
for (size_t i = first; i < last; ++ i)
|
||||||
|
commit(i, std::move(resolved[i - first]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
+351
-168
@@ -5,6 +5,8 @@
|
|||||||
#include "Preset.hpp"
|
#include "Preset.hpp"
|
||||||
#include "PresetBundle.hpp"
|
#include "PresetBundle.hpp"
|
||||||
#include "AppConfig.hpp"
|
#include "AppConfig.hpp"
|
||||||
|
#include "LocalesUtils.hpp"
|
||||||
|
#include "ParallelResolve.hpp"
|
||||||
|
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
#define WIN32_LEAN_AND_MEAN
|
#define WIN32_LEAN_AND_MEAN
|
||||||
@@ -49,6 +51,9 @@
|
|||||||
#include "libslic3r.h"
|
#include "libslic3r.h"
|
||||||
#include "LifecycleEvents.hpp"
|
#include "LifecycleEvents.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "InstanceLock.hpp"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
#include "Time.hpp"
|
#include "Time.hpp"
|
||||||
#include "PlaceholderParser.hpp"
|
#include "PlaceholderParser.hpp"
|
||||||
#include "libslic3r/GCode/Thumbnails.hpp"
|
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||||
@@ -105,6 +110,32 @@ 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)
|
std::string get_preset_bare_name(const std::string &canonical_name)
|
||||||
{
|
{
|
||||||
const auto pos = canonical_name.find_last_of('/');
|
const auto pos = canonical_name.find_last_of('/');
|
||||||
@@ -506,10 +537,11 @@ void Preset::normalize(DynamicPrintConfig &config)
|
|||||||
handle_legacy_sla(config);
|
handle_legacy_sla(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config)
|
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
|
||||||
|
const DynamicPrintConfig *added)
|
||||||
{
|
{
|
||||||
std::string incorrect_keys;
|
std::string incorrect_keys;
|
||||||
for (const std::string &key : config.keys())
|
for (const std::string &key : (added != nullptr ? *added : config).keys())
|
||||||
if (! default_config.has(key)) {
|
if (! default_config.has(key)) {
|
||||||
if (incorrect_keys.empty())
|
if (incorrect_keys.empty())
|
||||||
incorrect_keys = key;
|
incorrect_keys = key;
|
||||||
@@ -621,7 +653,6 @@ void Preset::load_info(const std::string& file)
|
|||||||
}
|
}
|
||||||
else if (v.first.compare("base_id") == 0) {
|
else if (v.first.compare("base_id") == 0) {
|
||||||
this->base_id = v.second.get_value<std::string>();
|
this->base_id = v.second.get_value<std::string>();
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << file << " and base_id: " << this->base_id;
|
|
||||||
if (this->base_id.compare("null") == 0)
|
if (this->base_id.compare("null") == 0)
|
||||||
this->base_id.clear();
|
this->base_id.clear();
|
||||||
}
|
}
|
||||||
@@ -647,18 +678,20 @@ void Preset::save_info(std::string file)
|
|||||||
file = idx_file.string();
|
file = idx_file.string();
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::nowide::ofstream c;
|
|
||||||
c.open(file, std::ios::out | std::ios::trunc);
|
|
||||||
std::string sync_info_to_save;
|
std::string sync_info_to_save;
|
||||||
//BBS: hold is used for stop requesting to server this time
|
//BBS: hold is used for stop requesting to server this time
|
||||||
if (this->sync_info.compare("hold") != 0)
|
if (this->sync_info.compare("hold") != 0)
|
||||||
sync_info_to_save = this->sync_info;
|
sync_info_to_save = this->sync_info;
|
||||||
|
std::ostringstream c;
|
||||||
c << "sync_info" << " = " << sync_info_to_save << std::endl;
|
c << "sync_info" << " = " << sync_info_to_save << std::endl;
|
||||||
c << "user_id" << " = " << this->user_id << std::endl;
|
c << "user_id" << " = " << this->user_id << std::endl;
|
||||||
c << "setting_id" << " = " << this->setting_id << std::endl;
|
c << "setting_id" << " = " << this->setting_id << std::endl;
|
||||||
c << "base_id" << " = " << this->base_id << std::endl;
|
c << "base_id" << " = " << this->base_id << std::endl;
|
||||||
c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl;
|
c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl;
|
||||||
c.close();
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preset::remove_files(bool cloud_already_deleted)
|
void Preset::remove_files(bool cloud_already_deleted)
|
||||||
@@ -667,6 +700,7 @@ void Preset::remove_files(bool cloud_already_deleted)
|
|||||||
if (this->is_project_embedded) {
|
if (this->is_project_embedded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
// Erase the preset file.
|
// Erase the preset file.
|
||||||
boost::nowide::remove(this->file.c_str());
|
boost::nowide::remove(this->file.c_str());
|
||||||
fs::path idx_path(this->file);
|
fs::path idx_path(this->file);
|
||||||
@@ -684,11 +718,11 @@ void Preset::remove_files(bool cloud_already_deleted)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//BBS: add logic for only difference save
|
//BBS: add logic for only difference save
|
||||||
void Preset::save(DynamicPrintConfig* parent_config)
|
bool Preset::save(DynamicPrintConfig* parent_config)
|
||||||
{
|
{
|
||||||
//BBS: add project embedded preset logic
|
//BBS: add project embedded preset logic
|
||||||
if (this->is_project_embedded)
|
if (this->is_project_embedded)
|
||||||
return;
|
return true;
|
||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
//this->config.save(this->file);
|
//this->config.save(this->file);
|
||||||
std::string from_str;
|
std::string from_str;
|
||||||
@@ -703,12 +737,16 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
|||||||
else
|
else
|
||||||
from_str = std::string("Default");
|
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);
|
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
|
//BBS: only save difference if it has parent
|
||||||
if (parent_config) {
|
if (parent_config) {
|
||||||
DynamicPrintConfig temp_config;
|
|
||||||
std::vector<std::string> dirty_options = config.diff(*parent_config);
|
std::vector<std::string> dirty_options = config.diff(*parent_config);
|
||||||
|
|
||||||
std::string extruder_id_name, extruder_variant_name;
|
std::string extruder_id_name, extruder_variant_name;
|
||||||
@@ -744,13 +782,22 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
|||||||
opt_dst->set(opt_src);
|
opt_dst->set(opt_src);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
to_save = &temp_config;
|
||||||
} else if (!filament_id.empty() && inherits().empty()) {
|
} else if (!filament_id.empty() && inherits().empty()) {
|
||||||
DynamicPrintConfig temp_config = config;
|
temp_config = config;
|
||||||
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
|
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
|
||||||
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
to_save = &temp_config;
|
||||||
} else {
|
}
|
||||||
this->config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id;
|
||||||
|
|
||||||
@@ -760,6 +807,7 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
|||||||
idx_file.replace_extension(".info");
|
idx_file.replace_extension(".info");
|
||||||
this->save_info(idx_file.string());
|
this->save_info(idx_file.string());
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preset::reload(Preset const &parent)
|
void Preset::reload(Preset const &parent)
|
||||||
@@ -771,6 +819,7 @@ void Preset::reload(Preset const &parent)
|
|||||||
std::string reason;
|
std::string reason;
|
||||||
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
|
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
|
||||||
try {
|
try {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
|
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
|
||||||
this->config = parent.config;
|
this->config = parent.config;
|
||||||
this->config.apply(std::move(config));
|
this->config.apply(std::move(config));
|
||||||
@@ -1669,6 +1718,186 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
|
|||||||
return get_preset_canonical_name(parsed.bare, origin);
|
return get_preset_canonical_name(parsed.bare, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PresetCollection::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.
|
// Load all presets found in dir_path.
|
||||||
// Throws an exception on error.
|
// Throws an exception on error.
|
||||||
void PresetCollection::load_presets(
|
void PresetCollection::load_presets(
|
||||||
@@ -1706,6 +1935,8 @@ void PresetCollection::load_presets(
|
|||||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||||
|
|
||||||
|
struct UserPresetFile { fs::path path; std::string canonical_name; };
|
||||||
|
std::vector<UserPresetFile> files;
|
||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
|
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
|
||||||
{
|
{
|
||||||
@@ -1721,149 +1952,47 @@ void PresetCollection::load_presets(
|
|||||||
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
|
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
files.push_back({ dir_entry.path(), std::move(canonical_name) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_then_commit<CNumericLocalesSetter>(files.size(),
|
||||||
|
[&](size_t i) {
|
||||||
|
return this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
|
||||||
|
extruder_id_name, extruder_variant_name, key_set1, key_set2);
|
||||||
|
},
|
||||||
|
[&](size_t 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 {
|
try {
|
||||||
Preset preset(m_type, canonical_name, false);
|
this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, leave_files);
|
||||||
preset.bundle_id = resolved_origin.bundle_id;
|
|
||||||
preset.file = dir_entry.path().string();
|
|
||||||
// Load the preset file, apply preset values on top of defaults.
|
|
||||||
try {
|
|
||||||
fs::path idx_path(preset.file);
|
|
||||||
idx_path.replace_extension(".info");
|
|
||||||
if (fs::exists(idx_path)) {
|
|
||||||
preset.load_info(idx_path.string());
|
|
||||||
}
|
|
||||||
DynamicPrintConfig config;
|
|
||||||
//BBS: change to json format
|
|
||||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
|
||||||
std::map<std::string, std::string> key_values;
|
|
||||||
std::string reason;
|
|
||||||
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
|
||||||
if (! config_substitutions.empty())
|
|
||||||
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
|
||||||
if (!reason.empty()) {
|
|
||||||
fs::path file_path(preset.file);
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
|
|
||||||
++m_errors;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
|
||||||
boost::optional<Semver> version = Semver::parse(version_str);
|
|
||||||
if (!version) continue;
|
|
||||||
preset.version = *version;
|
|
||||||
|
|
||||||
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
|
|
||||||
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
|
|
||||||
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
|
|
||||||
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
|
||||||
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
|
|
||||||
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
|
|
||||||
|
|
||||||
//Orca: find and use the inherit config as the base
|
|
||||||
Preset* inherit_preset = nullptr;
|
|
||||||
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
|
|
||||||
|
|
||||||
// check inherits_config
|
|
||||||
if (inherits_config) {
|
|
||||||
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
|
|
||||||
std::string inherits_value = option_str->value;
|
|
||||||
// Orca: try to find if the parent preset has been renamed
|
|
||||||
inherit_preset = this->find_preset2(inherits_value);
|
|
||||||
Preset::normalize_inherits(config, inherit_preset);
|
|
||||||
} else {
|
|
||||||
;
|
|
||||||
}
|
|
||||||
const Preset& default_preset = this->default_preset_for(config);
|
|
||||||
if (inherit_preset) {
|
|
||||||
preset.config = inherit_preset->config;
|
|
||||||
preset.filament_id = inherit_preset->filament_id;
|
|
||||||
extend_default_config_length(config, false, {});
|
|
||||||
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
|
|
||||||
if ((inherits_config2 && !inherits_config2->value.empty())) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%inherits_config2->value %preset.file;
|
|
||||||
++m_errors;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// We support custom root preset now
|
|
||||||
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
|
|
||||||
preset.config = default_preset.config;
|
|
||||||
preset.config.apply(std::move(config));
|
|
||||||
extend_default_config_length(preset.config, true, default_preset.config);
|
|
||||||
}
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
|
|
||||||
|
|
||||||
Preset::normalize(preset.config);
|
|
||||||
// Report configuration fields, which are misplaced into a wrong group.
|
|
||||||
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
|
|
||||||
if (!incorrect_keys.empty()) {
|
|
||||||
++m_errors;
|
|
||||||
BOOST_LOG_TRIVIAL(error)
|
|
||||||
<< "Error in a preset file: The preset \"" << preset.file
|
|
||||||
<< "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
|
|
||||||
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
|
|
||||||
if (compatible_printers && compatible_printers->values.empty()) {
|
|
||||||
size_t at_pos = name.find('@');
|
|
||||||
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
|
|
||||||
compatible_printers->values.push_back(name.substr(at_pos + 1));
|
|
||||||
if (!read_only)
|
|
||||||
preset.save(nullptr);
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preset.loaded = true;
|
|
||||||
//BBS: add some workaround for previous incorrect settings
|
|
||||||
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
|
|
||||||
preset.setting_id.clear();
|
|
||||||
//BBS: add config related logs
|
|
||||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
|
|
||||||
// add alias for custom filament preset
|
|
||||||
set_custom_preset_alias(preset);
|
|
||||||
} catch (const std::ifstream::failure &err) {
|
|
||||||
++m_errors;
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
|
|
||||||
fs::path file_path(preset.file);
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
|
||||||
} catch (const std::runtime_error &err) {
|
|
||||||
++m_errors;
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
|
|
||||||
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
|
||||||
fs::path file_path(preset.file);
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preset_loaded_fn != nullptr)
|
|
||||||
preset_loaded_fn(preset);
|
|
||||||
|
|
||||||
presets_loaded.emplace_back(preset);
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
|
|
||||||
} catch (const std::runtime_error &err) {
|
} catch (const std::runtime_error &err) {
|
||||||
errors_cummulative += err.what();
|
errors_cummulative += err.what();
|
||||||
errors_cummulative += "\n";
|
errors_cummulative += "\n";
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
|
||||||
if (presets_loaded.size() > 0)
|
if (presets_loaded.size() > 0)
|
||||||
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
|
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
|
||||||
sort_presets();
|
sort_presets();
|
||||||
@@ -2167,7 +2296,10 @@ void PresetCollection::set_sync_info_and_save(std::string name, std::string sett
|
|||||||
preset->setting_id = setting_id;
|
preset->setting_id = setting_id;
|
||||||
if (update_time > 0)
|
if (update_time > 0)
|
||||||
preset->updated_time = update_time;
|
preset->updated_time = update_time;
|
||||||
preset->sync_info == "update" ? preset->save(nullptr) : preset->save_info();
|
if (preset->sync_info == "update")
|
||||||
|
preset->save(nullptr);
|
||||||
|
else
|
||||||
|
preset->save_info();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2827,6 +2959,18 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
|||||||
return std::make_pair(&preset, false);
|
return std::make_pair(&preset, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Preset& PresetCollection::append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config)
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
Preset &preset = m_presets.emplace_back(m_type, name, false);
|
||||||
|
preset.file = std::move(path);
|
||||||
|
preset.config = std::move(config);
|
||||||
|
preset.loaded = true;
|
||||||
|
preset.is_dirty = false;
|
||||||
|
unlock();
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
|
||||||
Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version)
|
Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version)
|
||||||
{
|
{
|
||||||
lock();
|
lock();
|
||||||
@@ -3846,28 +3990,53 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
std::vector<std::vector<std::string>> PresetCollection::merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors)
|
||||||
std::vector<std::string> PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors)
|
|
||||||
{
|
{
|
||||||
std::vector<std::string> duplicates;
|
auto less = [this](const Preset &a, const Preset &b) {
|
||||||
for (Preset &preset : other.m_presets) {
|
return m_type == Preset::TYPE_FILAMENT ? filament_preset_less(a, b) : a < b;
|
||||||
if (preset.is_default || preset.is_external)
|
};
|
||||||
continue;
|
struct Incoming { Preset *preset; size_t source; };
|
||||||
Preset key(m_type, preset.name);
|
auto incoming_less = [&less](const Incoming &a, const Incoming &b) { return less(*a.preset, *b.preset); };
|
||||||
auto it = (m_type == Preset::TYPE_FILAMENT)
|
// Each of `others` is sorted, so its presets form one sorted run.
|
||||||
? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less)
|
std::vector<Incoming> incoming;
|
||||||
: std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key);
|
std::vector<size_t> run_ends { 0 };
|
||||||
if (it == m_presets.end() || it->name != preset.name) {
|
for (size_t source = 0; source < others.size(); ++ source) {
|
||||||
|
for (Preset &preset : others[source]->m_presets)
|
||||||
|
if (! preset.is_default && ! preset.is_external)
|
||||||
|
incoming.push_back({ &preset, source });
|
||||||
|
assert(std::is_sorted(incoming.begin() + run_ends.back(), incoming.end(), incoming_less));
|
||||||
|
run_ends.push_back(incoming.size());
|
||||||
|
}
|
||||||
|
// Merged pairwise and stably, so equal names stay in the order of `others`.
|
||||||
|
const size_t runs = others.size();
|
||||||
|
for (size_t width = 1; width < runs; width *= 2)
|
||||||
|
for (size_t i = 0; i + width < runs; i += 2 * width)
|
||||||
|
std::inplace_merge(incoming.begin() + run_ends[i], incoming.begin() + run_ends[i + width],
|
||||||
|
incoming.begin() + run_ends[std::min(i + 2 * width, runs)], incoming_less);
|
||||||
|
|
||||||
|
std::vector<std::vector<std::string>> duplicates(others.size());
|
||||||
|
std::deque<Preset> merged;
|
||||||
|
auto own = m_presets.begin() + m_num_default_presets;
|
||||||
|
std::move(m_presets.begin(), own, std::back_inserter(merged));
|
||||||
|
// On equal names this collection's preset is kept, else the earliest of `others`,
|
||||||
|
// and each repeat is listed under the collection it came from.
|
||||||
|
for (auto next = incoming.begin(); own != m_presets.end() || next != incoming.end();) {
|
||||||
|
if (next == incoming.end() || (own != m_presets.end() && ! less(*next->preset, *own)))
|
||||||
|
merged.emplace_back(std::move(*own ++));
|
||||||
|
else {
|
||||||
|
Preset &preset = *(next ++)->preset;
|
||||||
if (preset.vendor != nullptr) {
|
if (preset.vendor != nullptr) {
|
||||||
// Re-assign a pointer to the vendor structure in the new PresetBundle.
|
// Re-assign a pointer to the vendor structure in the new PresetBundle.
|
||||||
auto it = new_vendors.find(preset.vendor->id);
|
auto it = new_vendors.find(preset.vendor->id);
|
||||||
assert(it != new_vendors.end());
|
assert(it != new_vendors.end());
|
||||||
preset.vendor = &it->second;
|
preset.vendor = &it->second;
|
||||||
}
|
}
|
||||||
m_presets.emplace(it, std::move(preset));
|
merged.emplace_back(std::move(preset));
|
||||||
} else
|
|
||||||
duplicates.emplace_back(std::move(preset.name));
|
|
||||||
}
|
}
|
||||||
|
for (; next != incoming.end() && next->preset->name == merged.back().name; ++ next)
|
||||||
|
duplicates[next->source].emplace_back(next->preset->name);
|
||||||
|
}
|
||||||
|
m_presets = std::move(merged);
|
||||||
return duplicates;
|
return duplicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4212,8 +4381,15 @@ 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)
|
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
|
// rename the file
|
||||||
boost::nowide::rename(file_name_from.data(), file_name_to.data());
|
boost::nowide::rename(file_name_from.data(), file_name_to.data());
|
||||||
this->file = file_name_to;
|
this->file = file_name_to;
|
||||||
@@ -4345,6 +4521,7 @@ void PhysicalPrinterCollection::load_printers(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
PhysicalPrinter printer(name, this->default_config());
|
PhysicalPrinter printer(name, this->default_config());
|
||||||
printer.file = dir_entry.path().string();
|
printer.file = dir_entry.path().string();
|
||||||
// Load the preset file, apply preset values on top of defaults.
|
// Load the preset file, apply preset values on top of defaults.
|
||||||
@@ -4537,7 +4714,10 @@ bool PhysicalPrinterCollection::delete_printer(const std::string& name)
|
|||||||
|
|
||||||
const PhysicalPrinter& printer = *it;
|
const PhysicalPrinter& printer = *it;
|
||||||
// Erase the preset file.
|
// 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);
|
m_printers.erase(it);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4549,7 +4729,10 @@ bool PhysicalPrinterCollection::delete_selected_printer()
|
|||||||
const PhysicalPrinter& printer = this->get_selected_printer();
|
const PhysicalPrinter& printer = this->get_selected_printer();
|
||||||
|
|
||||||
// Erase the preset file.
|
// 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.
|
// Remove the preset from the list.
|
||||||
m_printers.erase(m_printers.begin() + m_idx_selected);
|
m_printers.erase(m_printers.begin() + m_idx_selected);
|
||||||
// unselect all printers
|
// unselect all printers
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
#include <boost/filesystem/path.hpp>
|
#include <boost/filesystem/path.hpp>
|
||||||
#include <boost/property_tree/ptree_fwd.hpp>
|
#include <boost/property_tree/ptree_fwd.hpp>
|
||||||
@@ -330,7 +331,8 @@ public:
|
|||||||
|
|
||||||
//BBS: add logic for only difference save
|
//BBS: add logic for only difference save
|
||||||
//if parent_config is null, save all keys, otherwise, only save difference
|
//if parent_config is null, save all keys, otherwise, only save difference
|
||||||
void save(DynamicPrintConfig* parent_config);
|
// Returns false when the preset file could not be written.
|
||||||
|
bool save(DynamicPrintConfig* parent_config);
|
||||||
void reload(Preset const & parent);
|
void reload(Preset const & parent);
|
||||||
|
|
||||||
// Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty.
|
// Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty.
|
||||||
@@ -445,7 +447,10 @@ public:
|
|||||||
static std::string remove_suffix_modified(const std::string& name);
|
static std::string remove_suffix_modified(const std::string& name);
|
||||||
static void normalize(DynamicPrintConfig &config);
|
static void normalize(DynamicPrintConfig &config);
|
||||||
// Report configuration fields, which are misplaced into a wrong group, remove them from the config.
|
// Report configuration fields, which are misplaced into a wrong group, remove them from the config.
|
||||||
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config);
|
// `added`, when given, is the diff applied over a copy of default_config, and only
|
||||||
|
// its keys are checked, since no other key can be missing from default_config.
|
||||||
|
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
|
||||||
|
const DynamicPrintConfig *added = nullptr);
|
||||||
|
|
||||||
// BBS: move constructor to public
|
// BBS: move constructor to public
|
||||||
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
|
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
|
||||||
@@ -485,6 +490,12 @@ 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.
|
// 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);
|
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.
|
// 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());
|
PresetOrigin detect_origin_from_path(const boost::filesystem::path &path, const PresetOrigin &explicit_origin = PresetOrigin());
|
||||||
|
|
||||||
@@ -801,6 +812,8 @@ public:
|
|||||||
// Return number of presets including the "- default -" preset.
|
// Return number of presets including the "- default -" preset.
|
||||||
size_t size() const { return m_presets.size(); }
|
size_t size() const { return m_presets.size(); }
|
||||||
bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; }
|
bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; }
|
||||||
|
// How many presets this collection refused or repaired while loading.
|
||||||
|
int error_count() const { return m_errors; }
|
||||||
|
|
||||||
// For Print / Filament presets, disable those, which are not compatible with the printer.
|
// For Print / Filament presets, disable those, which are not compatible with the printer.
|
||||||
template<typename PreferedCondition>
|
template<typename PreferedCondition>
|
||||||
@@ -874,8 +887,10 @@ protected:
|
|||||||
// This is a temporary state, which shall be fixed immediately by the following step.
|
// This is a temporary state, which shall be fixed immediately by the following step.
|
||||||
bool select_preset_by_name_strict(const std::string &name);
|
bool select_preset_by_name_strict(const std::string &name);
|
||||||
|
|
||||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
// Move the presets of `others` into this collection in one pass. A name this
|
||||||
std::vector<std::string> merge_presets(PresetCollection &&other, const VendorMap &new_vendors);
|
// collection or an earlier one of `others` already has is left out, and reported
|
||||||
|
// in the list of the collection that repeats it.
|
||||||
|
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors);
|
||||||
|
|
||||||
// Update m_map_alias_to_profile_name from loaded system profiles.
|
// Update m_map_alias_to_profile_name from loaded system profiles.
|
||||||
void update_map_alias_to_profile_name();
|
void update_map_alias_to_profile_name();
|
||||||
@@ -890,6 +905,56 @@ protected:
|
|||||||
void set_custom_preset_alias(Preset &preset);
|
void set_custom_preset_alias(Preset &preset);
|
||||||
|
|
||||||
private:
|
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;
|
std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const;
|
||||||
|
|
||||||
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
|
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
|
||||||
@@ -901,6 +966,10 @@ private:
|
|||||||
return a.name < b.name;
|
return a.name < b.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append a preset without keeping the collection sorted, for a caller installing
|
||||||
|
// many at once; find_preset() is unusable until sort_presets() runs.
|
||||||
|
Preset& append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config);
|
||||||
|
|
||||||
// Sort presets: filament presets use generic-first ordering, others sort alphabetically.
|
// Sort presets: filament presets use generic-first ordering, others sort alphabetically.
|
||||||
void sort_presets() {
|
void sort_presets() {
|
||||||
if (m_type == Preset::TYPE_FILAMENT)
|
if (m_type == Preset::TYPE_FILAMENT)
|
||||||
@@ -1054,7 +1123,7 @@ public:
|
|||||||
|
|
||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
//void save() { this->config.save(this->file); }
|
//void save() { this->config.save(this->file); }
|
||||||
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(DynamicPrintConfig* parent_config);
|
||||||
void save(const std::string& file_name_from, const std::string& file_name_to);
|
void save(const std::string& file_name_from, const std::string& file_name_to);
|
||||||
|
|
||||||
void update_from_preset(const Preset& preset);
|
void update_from_preset(const Preset& preset);
|
||||||
|
|||||||
+584
-398
File diff suppressed because it is too large
Load Diff
+172
-27
@@ -15,6 +15,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
#include <boost/filesystem/path.hpp>
|
#include <boost/filesystem/path.hpp>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
@@ -622,36 +623,180 @@ public:
|
|||||||
// default_filament_profile must resolve to a system filament.
|
// default_filament_profile must resolve to a system filament.
|
||||||
bool check_printer_default_materials() const;
|
bool check_printer_default_materials() const;
|
||||||
|
|
||||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
// One vendor to load, and the directory it is installed in.
|
||||||
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
|
struct VendorSource
|
||||||
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
|
{
|
||||||
std::vector<std::string> merge_presets(PresetBundle &&other);
|
std::string name;
|
||||||
|
boost::filesystem::path dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load `vendors` into this bundle, the Orca filament library directly and every
|
||||||
|
// other vendor in parallel into a bundle of its own that inherits from it, merged
|
||||||
|
// in the order given. A vendor that cannot be loaded has its error added to the
|
||||||
|
// returned text, or thrown in validation mode, and its name to `failed`; it is
|
||||||
|
// left out, except for the library, which keeps what it installed before the
|
||||||
|
// failure. Once `cancel` is set, no further vendor starts loading.
|
||||||
|
std::pair<PresetsConfigSubstitutions, std::string> load_vendors(const std::vector<VendorSource>& vendors,
|
||||||
|
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache,
|
||||||
|
const std::atomic<bool>* cancel = nullptr, std::vector<std::string>* failed = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Load one vendor from the preset cache installed in `dir`, judged against
|
// Move the presets and vendor profiles of `others` into this bundle, in one pass
|
||||||
// the vendor profile there. False, with this bundle left clean, when there
|
// over each collection. A preset whose name this bundle or an earlier one of
|
||||||
// is no usable cache and the vendor has to be parsed. This is how
|
// `others` already has is left out and listed under the bundle that repeats it.
|
||||||
// load_vendor_configs_from_json reads a cache.
|
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetBundle*> &others);
|
||||||
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
|
|
||||||
|
|
||||||
// Load one source-form preset entry into this bundle: resolve `inherits`
|
// What parsing one entry's JSON sub-file reported. Its errors and warnings are
|
||||||
// and `include`, flatten, validate and register the preset. Returns the
|
// logged when the entry installs, so they come out in listing order with the
|
||||||
// reason loading failed, empty on success. See the definition for the
|
// entry's install errors, as parsing and installing one entry at a time leaves them.
|
||||||
// sharing contract between the JSON parse and the cache load.
|
struct EntryParse
|
||||||
// retain_configs / retain_includes, when non-null, name the only presets
|
{
|
||||||
// registered into config_maps / include_maps (a config copy each). The
|
ConfigSubstitutions substitutions;
|
||||||
// cache load passes the names its entries inherit / include — the only
|
// Counted in the bundle's error count.
|
||||||
// ones ever looked up again; the JSON parse retains all, not knowing what
|
std::vector<std::string> errors;
|
||||||
// later subfiles name.
|
std::vector<std::string> warnings;
|
||||||
std::string load_vendor_preset(const CachedPreset& entry,
|
};
|
||||||
const std::string& path, const std::string& vendor_name,
|
|
||||||
const PresetBundle* base_bundle,
|
// An EntryParse for each entry of the VendorCacheData list of the same name.
|
||||||
LoadConfigBundleAttributes flags,
|
struct VendorParse
|
||||||
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
|
{
|
||||||
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
|
std::vector<EntryParse> process_entries;
|
||||||
std::map<std::string, std::string>& filament_id_maps,
|
std::vector<EntryParse> filament_entries;
|
||||||
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
|
std::vector<EntryParse> machine_entries;
|
||||||
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
|
};
|
||||||
|
|
||||||
|
// One vendor read from its cache or its JSONs by read_vendor, for
|
||||||
|
// install_vendor_read to install.
|
||||||
|
struct VendorRead
|
||||||
|
{
|
||||||
|
std::string dir;
|
||||||
|
std::string vendor_name;
|
||||||
|
LoadConfigBundleAttributes flags;
|
||||||
|
ForwardCompatibilitySubstitutionRule compatibility_rule;
|
||||||
|
// The errors this bundle had counted before the read, which the cache stamp leaves out.
|
||||||
|
int errors_at_entry { 0 };
|
||||||
|
// A whole-vendor load, which can read a cache and write one.
|
||||||
|
bool cacheable { false };
|
||||||
|
// Read from the cache at cache_path, which install can still reject.
|
||||||
|
bool from_cache { false };
|
||||||
|
std::string cache_path;
|
||||||
|
// Only the vendor profile was asked for.
|
||||||
|
bool vendor_only { false };
|
||||||
|
VendorCacheData data;
|
||||||
|
// What each entry's JSON parse reported, and whether and with which version
|
||||||
|
// the cache is written once the entries install.
|
||||||
|
VendorParse parsed;
|
||||||
|
bool will_cache { false };
|
||||||
|
std::string version;
|
||||||
|
// The sub-file the parse stopped at, its kind, why, and the errors it reported.
|
||||||
|
std::string reason;
|
||||||
|
std::string failed_subfile;
|
||||||
|
const char* failed_kind { nullptr };
|
||||||
|
std::vector<std::string> failed_errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read a vendor into this bundle's vendor profiles and `data`, from its cache
|
||||||
|
// when one covers it, else from its JSONs; nothing is installed. Throws
|
||||||
|
// ConfigurationError when the vendor's own JSON cannot be parsed.
|
||||||
|
VendorRead read_vendor(const std::string& dir, const std::string& vendor_name, LoadConfigBundleAttributes flags,
|
||||||
|
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache);
|
||||||
|
|
||||||
|
// Parse the vendor's JSONs into `read`, up to the first sub-file that fails.
|
||||||
|
void parse_vendor_json(VendorRead& read);
|
||||||
|
|
||||||
|
// Install what read_vendor read, against base_bundle's filament library. A cache
|
||||||
|
// that cannot be installed is replaced by a parse of the JSONs. Throws
|
||||||
|
// ConfigurationError at the first entry that cannot be installed, or after
|
||||||
|
// installing the entries before a sub-file that could not be parsed.
|
||||||
|
std::pair<PresetsConfigSubstitutions, size_t> install_vendor_read(VendorRead&& read, const PresetBundle* base_bundle);
|
||||||
|
|
||||||
|
// Install a cache's entries. False, with this bundle left clean, when one of them
|
||||||
|
// cannot be installed.
|
||||||
|
bool install_vendor_cache(const std::string& cache_path, const std::string& vendor_name, VendorCacheData&& data,
|
||||||
|
const PresetBundle* base_bundle);
|
||||||
|
|
||||||
|
// Log and count errors reported by a resolve or a parse.
|
||||||
|
void log_errors(const std::vector<std::string>& errors);
|
||||||
|
|
||||||
|
// The state of installing one collection of one vendor, which
|
||||||
|
// resolve_vendor_preset reads through a const reference and only
|
||||||
|
// commit_vendor_preset writes.
|
||||||
|
struct VendorInstall
|
||||||
|
{
|
||||||
|
// The directory holding <vendor_name>/, whose sub-paths the entries name.
|
||||||
|
std::string path;
|
||||||
|
std::string vendor_name;
|
||||||
|
const VendorProfile* vendor_profile;
|
||||||
|
const PresetBundle* base_bundle;
|
||||||
|
LoadConfigBundleAttributes flags;
|
||||||
|
PresetCollection* presets;
|
||||||
|
// The Orca filament library, which keeps every config for other vendors to
|
||||||
|
// resolve against.
|
||||||
|
bool is_from_lib;
|
||||||
|
PresetsConfigSubstitutions* substitutions;
|
||||||
|
// The names some entry inherits / includes, the only ones whose configs /
|
||||||
|
// include diffs are looked up again.
|
||||||
|
std::set<std::string> inherited;
|
||||||
|
std::set<std::string> included;
|
||||||
|
std::map<std::string, DynamicPrintConfig> config_maps;
|
||||||
|
std::map<std::string, DynamicPrintConfig> include_maps;
|
||||||
|
std::map<std::string, std::string> filament_id_maps;
|
||||||
|
std::unordered_set<std::string> installed_names;
|
||||||
|
size_t count { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Install a vendor's source-form entries, parsed from its JSON or read from its
|
||||||
|
// cache: processes, then filaments, then printers. Both loads go through here,
|
||||||
|
// so a cache-loaded bundle cannot come out different from a JSON-loaded one.
|
||||||
|
// `parsed` is given for entries parsed just now. `complete` says the entries
|
||||||
|
// are the vendor's whole lists; only then are the filament library's configs
|
||||||
|
// and filament ids left in m_config_maps and m_filament_id_maps. Returns the
|
||||||
|
// number of presets installed, and throws ConfigurationError at the first
|
||||||
|
// entry that cannot be installed.
|
||||||
|
size_t install_vendor(const std::string& path, const std::string& vendor_name, const PresetBundle* base_bundle,
|
||||||
|
LoadConfigBundleAttributes flags, const VendorCacheData& entries,
|
||||||
|
VendorParse* parsed, bool complete, PresetsConfigSubstitutions& substitutions);
|
||||||
|
|
||||||
|
// Install one collection's entries in the order they are listed.
|
||||||
|
void install_vendor_entries(VendorInstall& install, const std::vector<CachedPreset>& entries,
|
||||||
|
std::vector<EntryParse>* parsed);
|
||||||
|
|
||||||
|
// One entry flattened against the preset it inherits, before anything this
|
||||||
|
// bundle shares has been touched.
|
||||||
|
struct PresetInstall
|
||||||
|
{
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
std::string file_path;
|
||||||
|
// Empty when the preset is its own alias.
|
||||||
|
std::string alias;
|
||||||
|
std::string filament_id;
|
||||||
|
std::vector<std::string> renamed_from;
|
||||||
|
// Reported by commit, so resolving entries together leaves the log and
|
||||||
|
// the error count as one entry at a time produces them.
|
||||||
|
std::vector<std::string> errors;
|
||||||
|
// What a base states for the presets that include it, when it is retained.
|
||||||
|
std::optional<DynamicPrintConfig> included;
|
||||||
|
// The config kept for the entries that inherit this one, or for other
|
||||||
|
// vendors when this is the filament library.
|
||||||
|
std::optional<DynamicPrintConfig> retained;
|
||||||
|
// Not instantiated, so it contributes a config and no preset.
|
||||||
|
bool config_only { false };
|
||||||
|
// Non-empty when the entry is rejected, and says why.
|
||||||
|
std::string reason;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flatten one entry against the config it inherits, from this collection's
|
||||||
|
// config_maps or base_bundle's filament library, with the include diffs it
|
||||||
|
// names layered in. It looks up nothing but the names the entry inherits and
|
||||||
|
// includes, and writes nothing.
|
||||||
|
PresetInstall resolve_vendor_preset(const CachedPreset& entry, const VendorInstall& install) const;
|
||||||
|
|
||||||
|
// Install a resolved entry. The collections, the maps in `install` and the
|
||||||
|
// error count are touched here and only here, one entry at a time.
|
||||||
|
// Presets are appended, so a repeated name is caught with `installed_names`,
|
||||||
|
// and the collection is sorted once every entry is in.
|
||||||
|
std::string commit_vendor_preset(const CachedPreset& entry, PresetInstall&& resolved,
|
||||||
|
ConfigSubstitutions&& substitutions, VendorInstall& install);
|
||||||
|
|
||||||
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
|
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
|
||||||
void clear_printer_hold_aliases();
|
void clear_printer_hold_aliases();
|
||||||
|
|||||||
@@ -400,46 +400,24 @@ bool write_cache_blob(const std::string& path, const std::string& blob)
|
|||||||
{
|
{
|
||||||
boost::crc_32_type crc;
|
boost::crc_32_type crc;
|
||||||
crc.process_bytes(blob.data(), blob.size());
|
crc.process_bytes(blob.data(), blob.size());
|
||||||
// Written beside the target and moved into place, as AppConfig::save does:
|
// Written beside the target and moved into place: a cache is truncated and
|
||||||
// a cache is truncated and rewritten in full, so a write that dies partway
|
// rewritten in full, so a write that dies partway would otherwise leave a
|
||||||
// would otherwise leave a header claiming more body than the file holds.
|
// 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 {
|
try {
|
||||||
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
||||||
{
|
|
||||||
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;
|
CacheFileHeader fhdr;
|
||||||
fhdr.magic = CACHE_MAGIC;
|
fhdr.magic = CACHE_MAGIC;
|
||||||
fhdr.version = CACHE_VERSION;
|
fhdr.version = CACHE_VERSION;
|
||||||
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
||||||
fhdr.crc32 = crc.checksum();
|
fhdr.crc32 = crc.checksum();
|
||||||
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
const std::string_view header(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
||||||
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
|
if (const std::error_code ec = write_file_atomically(path, { header, std::string_view(blob) }, /*binary=*/true)) {
|
||||||
ofs.close(); // flush; close() raises failbit on error
|
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << ec.message();
|
||||||
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
|
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
|
||||||
boost::system::error_code ec;
|
|
||||||
boost::filesystem::remove(tmp_path, ec);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
|
|||||||
|
|
||||||
// One preset as its JSON subfile states it: the config diff, the names of the
|
// One preset as its JSON subfile states it: the config diff, the names of the
|
||||||
// preset it inherits and the presets it includes, and the parse metadata —
|
// preset it inherits and the presets it includes, and the parse metadata —
|
||||||
// everything the parse phase of load_vendor_configs_from_json extracts and
|
// everything PresetBundle::parse_vendor_json extracts and
|
||||||
// nothing it derives. Inheritance and includes are resolved when the entry is
|
// nothing it derives. Inheritance and includes are resolved when the entry is
|
||||||
// installed, against whatever filament library is loaded then, so a cache
|
// installed, against whatever filament library is loaded then, so a cache
|
||||||
// carries no other vendor's values and no other vendor's update can make it
|
// carries no other vendor's values and no other vendor's update can make it
|
||||||
|
|||||||
+5
-10
@@ -2717,8 +2717,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(m_model.id().id);
|
ctx.name = std::to_string(m_model.id().id);
|
||||||
ctx.name = get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
||||||
@@ -3345,8 +3344,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(m_model.id().id);
|
ctx.name = std::to_string(m_model.id().id);
|
||||||
ctx.name = get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
||||||
@@ -4952,8 +4950,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(m_model.id().id);
|
ctx.name = std::to_string(m_model.id().id);
|
||||||
ctx.name = get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = file;
|
ctx.msg = file;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
@@ -4983,8 +4980,7 @@ 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();
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(m_model.id().id);
|
ctx.name = std::to_string(m_model.id().id);
|
||||||
ctx.name = get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = file + "\n" + ex.what();
|
ctx.msg = file + "\n" + ex.what();
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
@@ -4998,8 +4994,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.id = std::to_string(m_model.id().id);
|
ctx.name = std::to_string(m_model.id().id);
|
||||||
ctx.name = get_model_name();
|
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = file;
|
ctx.msg = file;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <string_view>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include <boost/system/error_code.hpp>
|
#include <boost/system/error_code.hpp>
|
||||||
@@ -224,6 +226,21 @@ 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
|
// 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.
|
// 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);
|
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 {
|
enum CopyFileResult {
|
||||||
SUCCESS = 0,
|
SUCCESS = 0,
|
||||||
|
|||||||
+90
-2
@@ -9,6 +9,8 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <mutex>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -702,13 +704,99 @@ namespace WindowsSupport
|
|||||||
std::error_code rename_file(const std::string &from, const std::string &to)
|
std::error_code rename_file(const std::string &from, const std::string &to)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
// Retries and moves an open destination aside itself.
|
||||||
return WindowsSupport::rename(from, to);
|
return WindowsSupport::rename(from, to);
|
||||||
#else
|
#else
|
||||||
boost::nowide::remove(to.c_str());
|
// rename(2) replaces an existing target atomically; removing it first would
|
||||||
return std::make_error_code(static_cast<std::errc>(boost::nowide::rename(from.c_str(), to.c_str())));
|
// 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));
|
||||||
#endif
|
#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__
|
#ifdef __linux__
|
||||||
// Copied from boost::filesystem.
|
// Copied from boost::filesystem.
|
||||||
// Called by copy_file_linux() in case linux sendfile() API is not supported.
|
// Called by copy_file_linux() in case linux sendfile() API is not supported.
|
||||||
|
|||||||
@@ -158,21 +158,6 @@ enum class EGCodeExtrusionRole : uint8_t
|
|||||||
|
|
||||||
static constexpr std::size_t GCODE_EXTRUSION_ROLES_COUNT = static_cast<std::size_t>(EGCodeExtrusionRole::COUNT);
|
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
|
// Option types
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -114,19 +114,6 @@ public:
|
|||||||
//
|
//
|
||||||
bool is_dim_previous_layers() const;
|
bool is_dim_previous_layers() const;
|
||||||
void set_dim_previous_layers(bool value);
|
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;
|
float get_dim_previous_layers_brightness() const;
|
||||||
void set_dim_previous_layers_brightness(float value);
|
void set_dim_previous_layers_brightness(float value);
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ struct Settings
|
|||||||
// ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black
|
// ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black
|
||||||
float dim_previous_layers_brightness{ 0.4f };
|
float dim_previous_layers_brightness{ 0.4f };
|
||||||
bool spiral_vase_mode{ false };
|
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
|
// Required update flags
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ static const char* Segments_Vertex_Shader =
|
|||||||
// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not
|
// 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.
|
// world_position, so the caster would write a depth the receiver never looks up.
|
||||||
"uniform float bias_scale;\n"
|
"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"
|
"in int vertex_id;\n"
|
||||||
"out vec3 color;\n"
|
"out vec3 color;\n"
|
||||||
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
|
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
|
||||||
@@ -63,8 +59,7 @@ static const char* Segments_Vertex_Shader =
|
|||||||
" return top_diffuse + front_diffuse + top_specular;\n"
|
" return top_diffuse + front_diffuse + top_specular;\n"
|
||||||
"}\n"
|
"}\n"
|
||||||
"void main() {\n"
|
"void main() {\n"
|
||||||
" int instance = (reverse_order != 0) ? instance_count - 1 - gl_InstanceID : gl_InstanceID;\n"
|
" int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n"
|
||||||
" int id_a = int(texelFetch(segment_index_tex, instance).r);\n"
|
|
||||||
" int id_b = id_a + 1;\n"
|
" int id_b = id_a + 1;\n"
|
||||||
" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n"
|
" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n"
|
||||||
" vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n"
|
" vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n"
|
||||||
|
|||||||
@@ -92,36 +92,6 @@ bool Viewer::is_dim_previous_layers() const
|
|||||||
return m_impl->is_dim_previous_layers();
|
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)
|
void Viewer::set_dim_previous_layers(bool value)
|
||||||
{
|
{
|
||||||
m_impl->set_dim_previous_layers(value);
|
m_impl->set_dim_previous_layers(value);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <cfloat>
|
|
||||||
|
|
||||||
namespace libvgcode {
|
namespace libvgcode {
|
||||||
|
|
||||||
@@ -764,8 +763,6 @@ 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_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_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_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
|
// ORCA: realistic view
|
||||||
m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map");
|
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");
|
m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp");
|
||||||
@@ -902,17 +899,9 @@ void ViewerImpl::reset()
|
|||||||
#else
|
#else
|
||||||
m_enabled_segments_count = 0;
|
m_enabled_segments_count = 0;
|
||||||
m_enabled_options_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;
|
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_textures(m_enabled_options_tex_id);
|
||||||
delete_buffers(m_enabled_options_buf_id);
|
delete_buffers(m_enabled_options_buf_id);
|
||||||
delete_textures(m_enabled_segments_tex_id);
|
delete_textures(m_enabled_segments_tex_id);
|
||||||
@@ -1172,17 +1161,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
|
|||||||
glsafe(glGenTextures(1, &m_enabled_options_tex_id));
|
glsafe(glGenTextures(1, &m_enabled_options_tex_id));
|
||||||
glsafe(glBindTexture(GL_TEXTURE_BUFFER, 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(glBindBuffer(GL_TEXTURE_BUFFER, 0));
|
||||||
glsafe(glBindTexture(GL_TEXTURE_BUFFER, old_bound_texture));
|
glsafe(glBindTexture(GL_TEXTURE_BUFFER, old_bound_texture));
|
||||||
#endif // ENABLE_OPENGL_ES
|
#endif // ENABLE_OPENGL_ES
|
||||||
@@ -1194,18 +1172,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
|
|||||||
update_colors();
|
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()
|
void ViewerImpl::update_enabled_entities()
|
||||||
{
|
{
|
||||||
if (m_vertices.empty())
|
if (m_vertices.empty())
|
||||||
@@ -1213,17 +1179,6 @@ void ViewerImpl::update_enabled_entities()
|
|||||||
|
|
||||||
std::vector<uint32_t> enabled_segments;
|
std::vector<uint32_t> enabled_segments;
|
||||||
std::vector<uint32_t> enabled_options;
|
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();
|
Interval range = m_view_range.get_visible();
|
||||||
|
|
||||||
// when top layer only visualization is enabled, we need to render
|
// when top layer only visualization is enabled, we need to render
|
||||||
@@ -1271,20 +1226,6 @@ void ViewerImpl::update_enabled_entities()
|
|||||||
enabled_options.push_back(static_cast<uint32_t>(i));
|
enabled_options.push_back(static_cast<uint32_t>(i));
|
||||||
else
|
else
|
||||||
enabled_segments.push_back(static_cast<uint32_t>(i));
|
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
|
#ifdef ENABLE_OPENGL_ES
|
||||||
@@ -1313,23 +1254,6 @@ void ViewerImpl::update_enabled_entities()
|
|||||||
else
|
else
|
||||||
glsafe(glBufferData(GL_TEXTURE_BUFFER, 0, nullptr, GL_STATIC_DRAW));
|
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));
|
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0));
|
||||||
#endif // ENABLE_OPENGL_ES
|
#endif // ENABLE_OPENGL_ES
|
||||||
|
|
||||||
@@ -1537,23 +1461,6 @@ void ViewerImpl::toggle_top_layer_only_view_range()
|
|||||||
update_colors_texture();
|
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
|
// ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to
|
||||||
void ViewerImpl::set_dim_previous_layers(bool value)
|
void ViewerImpl::set_dim_previous_layers(bool value)
|
||||||
{
|
{
|
||||||
@@ -1947,8 +1854,6 @@ size_t ViewerImpl::get_used_gpu_memory() const
|
|||||||
ret += m_colors_tex_size;
|
ret += m_colors_tex_size;
|
||||||
ret += m_enabled_segments_tex_size;
|
ret += m_enabled_segments_tex_size;
|
||||||
ret += m_enabled_options_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
|
#endif // ENABLE_OPENGL_ES
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -2165,8 +2070,7 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
|
|||||||
#ifdef ENABLE_OPENGL_ES
|
#ifdef ENABLE_OPENGL_ES
|
||||||
if (m_texture_data.get_enabled_segments_count() == 0)
|
if (m_texture_data.get_enabled_segments_count() == 0)
|
||||||
#else
|
#else
|
||||||
const ActiveSet segments = active_segments();
|
if (m_enabled_segments_count == 0)
|
||||||
if (segments.count == 0)
|
|
||||||
#endif // ENABLE_OPENGL_ES
|
#endif // ENABLE_OPENGL_ES
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -2186,15 +2090,6 @@ 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_view_matrix_id, 1, GL_FALSE, view_matrix.data()));
|
||||||
glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_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()));
|
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
|
// ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades
|
||||||
// with the lookup off.
|
// with the lookup off.
|
||||||
glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit));
|
glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit));
|
||||||
@@ -2243,10 +2138,10 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
|
|||||||
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
|
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
|
||||||
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
|
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
|
||||||
glsafe(glActiveTexture(GL_TEXTURE3));
|
glsafe(glActiveTexture(GL_TEXTURE3));
|
||||||
glsafe(glBindTexture(GL_TEXTURE_BUFFER, segments.tex_id));
|
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_tex_id));
|
||||||
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, segments.buf_id));
|
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_segments_buf_id));
|
||||||
|
|
||||||
m_segment_template.render(segments.count);
|
m_segment_template.render(m_enabled_segments_count);
|
||||||
#endif // ENABLE_OPENGL_ES
|
#endif // ENABLE_OPENGL_ES
|
||||||
|
|
||||||
if (curr_cull_face)
|
if (curr_cull_face)
|
||||||
@@ -2272,8 +2167,7 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
|
|||||||
#ifdef ENABLE_OPENGL_ES
|
#ifdef ENABLE_OPENGL_ES
|
||||||
if (m_texture_data.get_enabled_options_count() == 0)
|
if (m_texture_data.get_enabled_options_count() == 0)
|
||||||
#else
|
#else
|
||||||
const ActiveSet options = active_options();
|
if (m_enabled_options_count == 0)
|
||||||
if (options.count == 0)
|
|
||||||
#endif // ENABLE_OPENGL_ES
|
#endif // ENABLE_OPENGL_ES
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -2331,10 +2225,10 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
|
|||||||
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
|
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
|
||||||
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
|
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
|
||||||
glsafe(glActiveTexture(GL_TEXTURE3));
|
glsafe(glActiveTexture(GL_TEXTURE3));
|
||||||
glsafe(glBindTexture(GL_TEXTURE_BUFFER, options.tex_id));
|
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id));
|
||||||
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, options.buf_id));
|
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_options_buf_id));
|
||||||
|
|
||||||
m_option_template.render(options.count);
|
m_option_template.render(m_enabled_options_count);
|
||||||
#endif // ENABLE_OPENGL_ES
|
#endif // ENABLE_OPENGL_ES
|
||||||
|
|
||||||
if (!curr_cull_face)
|
if (!curr_cull_face)
|
||||||
|
|||||||
@@ -109,21 +109,6 @@ public:
|
|||||||
// 0.0 = black
|
// 0.0 = black
|
||||||
bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; }
|
bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; }
|
||||||
void set_dim_previous_layers(bool value);
|
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; }
|
float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; }
|
||||||
void set_dim_previous_layers_brightness(float value);
|
void set_dim_previous_layers_brightness(float value);
|
||||||
|
|
||||||
@@ -377,8 +362,6 @@ private:
|
|||||||
int m_uni_segments_height_width_angle_tex_id{ -1 };
|
int m_uni_segments_height_width_angle_tex_id{ -1 };
|
||||||
int m_uni_segments_colors_tex_id{ -1 };
|
int m_uni_segments_colors_tex_id{ -1 };
|
||||||
int m_uni_segments_segment_index_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_map_id{ -1 };
|
||||||
int m_uni_segments_shadow_light_vp_id{ -1 };
|
int m_uni_segments_shadow_light_vp_id{ -1 };
|
||||||
int m_uni_segments_shadow_intensity_id{ -1 };
|
int m_uni_segments_shadow_intensity_id{ -1 };
|
||||||
@@ -516,15 +499,6 @@ private:
|
|||||||
unsigned int m_enabled_options_tex_id{ 0 };
|
unsigned int m_enabled_options_tex_id{ 0 };
|
||||||
size_t m_enabled_options_count{ 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
|
// Caches for size of data sent to gpu, in bytes
|
||||||
//
|
//
|
||||||
size_t m_positions_tex_size{ 0 };
|
size_t m_positions_tex_size{ 0 };
|
||||||
@@ -532,27 +506,6 @@ private:
|
|||||||
size_t m_colors_tex_size{ 0 };
|
size_t m_colors_tex_size{ 0 };
|
||||||
size_t m_enabled_segments_tex_size{ 0 };
|
size_t m_enabled_segments_tex_size{ 0 };
|
||||||
size_t m_enabled_options_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
|
#endif // ENABLE_OPENGL_ES
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxHSCROLL);
|
||||||
m_scrolled_window->SetBackgroundColour(*wxWHITE);
|
m_scrolled_window->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrolled_window->SetScrollRate(0, FromDIP(20));
|
m_scrolled_window->SetScrollRate(0, FromDIP(10));
|
||||||
|
|
||||||
wxBoxSizer *title_sizer_h= new wxBoxSizer(wxHORIZONTAL);
|
wxBoxSizer *title_sizer_h= new wxBoxSizer(wxHORIZONTAL);
|
||||||
wxBoxSizer *title_sizer_v = new wxBoxSizer(wxVERTICAL);
|
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();
|
show_pos.x = screen_size.GetLeft();
|
||||||
m_scrolled_window->SetMaxSize(wxSize(screen_size.GetWidth(), popup_height));
|
m_scrolled_window->SetMaxSize(wxSize(screen_size.GetWidth(), popup_height));
|
||||||
m_scrolled_window->SetMinSize(wxSize(screen_size.GetWidth(), popup_height));
|
m_scrolled_window->SetMinSize(wxSize(screen_size.GetWidth(), popup_height));
|
||||||
m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(20));
|
m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(10));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_scrolled_window->SetMaxSize(wxSize(popup_width, popup_height));
|
m_scrolled_window->SetMaxSize(wxSize(popup_width, popup_height));
|
||||||
m_scrolled_window->SetMinSize(wxSize(popup_width, popup_height));
|
m_scrolled_window->SetMinSize(wxSize(popup_width, popup_height));
|
||||||
m_scrolled_window->SetScrollRate(0, FromDIP(20));
|
m_scrolled_window->SetScrollRate(0, FromDIP(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Vertical Direction Processing
|
//Vertical Direction Processing
|
||||||
@@ -2117,7 +2117,7 @@ void AmsReplaceMaterialDialog::create()
|
|||||||
identical_filament->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#009688"))); // Orca: accent teal (not brand green)
|
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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
|
||||||
m_scrollview_groups->SetScrollRate(5, FromDIP(20));
|
m_scrollview_groups->SetScrollRate(5, 5);
|
||||||
//m_scrollview_groups->SetMinSize(wxSize(400, 400));
|
//m_scrollview_groups->SetMinSize(wxSize(400, 400));
|
||||||
//m_scrollview_groups->SetMaxSize(wxSize(400, 400));
|
//m_scrollview_groups->SetMaxSize(wxSize(400, 400));
|
||||||
m_scrollview_sizer = new wxBoxSizer(wxVERTICAL);
|
m_scrollview_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|||||||
@@ -606,7 +606,7 @@ AuFolderPanel::AuFolderPanel(wxWindow *parent, AuxiliaryFolderType type, wxWindo
|
|||||||
wxBoxSizer *sizer_main = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer *sizer_main = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
|
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
|
||||||
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
|
m_scrolledWindow->SetScrollRate(5, 5);
|
||||||
wxBoxSizer *sizer_body = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer *sizer_body = new wxBoxSizer(wxVERTICAL);
|
||||||
wxBoxSizer *sizer_top = new wxBoxSizer(wxHORIZONTAL);
|
wxBoxSizer *sizer_top = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ PingCodeBindDialog::~PingCodeBindDialog() {
|
|||||||
//show bind failed info
|
//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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(450), FromDIP(300)), wxVSCROLL);
|
||||||
m_sw_bind_failed_info->SetBackgroundColour(*wxWHITE);
|
m_sw_bind_failed_info->SetBackgroundColour(*wxWHITE);
|
||||||
m_sw_bind_failed_info->SetScrollRate(5, FromDIP(20));
|
m_sw_bind_failed_info->SetScrollRate(5, 5);
|
||||||
m_sw_bind_failed_info->SetMinSize(wxSize(FromDIP(450), FromDIP(90)));
|
m_sw_bind_failed_info->SetMinSize(wxSize(FromDIP(450), FromDIP(90)));
|
||||||
m_sw_bind_failed_info->SetMaxSize(wxSize(FromDIP(450), FromDIP(90)));
|
m_sw_bind_failed_info->SetMaxSize(wxSize(FromDIP(450), FromDIP(90)));
|
||||||
|
|
||||||
|
|||||||
@@ -3521,7 +3521,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
|||||||
cards->Show(m_box_mate, false, true);
|
cards->Show(m_box_mate, false, true);
|
||||||
|
|
||||||
m_form->FitInside();
|
m_form->FitInside();
|
||||||
m_form->SetScrollRate(0, FromDIP(20)); // vertical only, like Prepare's sidebar: never scroll labels out
|
m_form->SetScrollRate(0, 10); // vertical only, like Prepare's sidebar: never scroll labels out
|
||||||
m_form->SetMinSize(wxSize(264, -1));
|
m_form->SetMinSize(wxSize(264, -1));
|
||||||
|
|
||||||
// Right column: a small view toolbar over the live 3D viewport that mirrors
|
// Right column: a small view toolbar over the live 3D viewport that mirrors
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ HistoryWindow::HistoryWindow(wxWindow* parent, const std::vector<PACalibResult>&
|
|||||||
auto main_sizer = new wxBoxSizer(wxVERTICAL);
|
auto main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
auto scroll_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
|
auto scroll_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
|
||||||
scroll_window->SetScrollRate(5, FromDIP(20));
|
scroll_window->SetScrollRate(5, 5);
|
||||||
scroll_window->SetBackgroundColour(*wxWHITE);
|
scroll_window->SetBackgroundColour(*wxWHITE);
|
||||||
scroll_window->SetMinSize(HISTORY_WINDOW_SIZE);
|
scroll_window->SetMinSize(HISTORY_WINDOW_SIZE);
|
||||||
scroll_window->SetSize(HISTORY_WINDOW_SIZE);
|
scroll_window->SetSize(HISTORY_WINDOW_SIZE);
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ SelectMObjectPopup::SelectMObjectPopup(wxWindow* parent)
|
|||||||
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
|
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
|
||||||
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
|
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
|
||||||
m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y);
|
m_scrolledWindow->SetScrollRate(0, 5);
|
||||||
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
|
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
|
||||||
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
|
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
|
||||||
m_scrolledWindow->Layout();
|
m_scrolledWindow->Layout();
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ CalibrationWizard::CalibrationWizard(wxWindow* parent, CalibMode mode, wxWindowI
|
|||||||
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
|
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
|
||||||
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
|
m_scrolledWindow->SetScrollRate(5, 5);
|
||||||
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
||||||
|
|
||||||
wxBoxSizer* padding_sizer = new wxBoxSizer(wxHORIZONTAL);
|
wxBoxSizer* padding_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
|||||||
@@ -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 = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL);
|
||||||
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
|
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
|
||||||
m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
|
m_sw_print_failed_info->SetScrollRate(0, 5);
|
||||||
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
|
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
|
||||||
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
|
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
|
||||||
|
|
||||||
|
|||||||
@@ -2710,7 +2710,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent)
|
|||||||
SetSizerAndFit(vsizer);
|
SetSizerAndFit(vsizer);
|
||||||
|
|
||||||
// We can now enable scrolling on hscroll
|
// We can now enable scrolling on hscroll
|
||||||
p->hscroll->SetScrollRate(30, FromDIP(20));
|
p->hscroll->SetScrollRate(30, 30);
|
||||||
|
|
||||||
on_window_geometry(this, [this]() {
|
on_window_geometry(this, [this]() {
|
||||||
p->init_dialog_size();
|
p->init_dialog_size();
|
||||||
|
|||||||
@@ -699,7 +699,7 @@ CreateFilamentPresetDialog::CreateFilamentPresetDialog(wxWindow *parent)
|
|||||||
m_scrolled_preset_panel = new wxScrolledWindow(this, wxID_ANY);
|
m_scrolled_preset_panel = new wxScrolledWindow(this, wxID_ANY);
|
||||||
m_scrolled_preset_panel->SetMaxSize(wxSize(-1, FromDIP(350)));
|
m_scrolled_preset_panel->SetMaxSize(wxSize(-1, FromDIP(350)));
|
||||||
m_scrolled_preset_panel->SetBackgroundColour(*wxWHITE);
|
m_scrolled_preset_panel->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrolled_preset_panel->SetScrollRate(5, FromDIP(20));
|
m_scrolled_preset_panel->SetScrollRate(5, 5);
|
||||||
m_scrolled_sizer = new wxBoxSizer(wxVERTICAL);
|
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(create_item(FilamentOptionType::PRESET_FOR_PRINTER), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
|
||||||
m_scrolled_sizer->Add(0, 0, 0, wxTOP, 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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||||
m_page1->SetBackgroundColour(*wxWHITE);
|
m_page1->SetBackgroundColour(*wxWHITE);
|
||||||
m_page1->SetScrollRate(5, FromDIP(20));
|
m_page1->SetScrollRate(5, 5);
|
||||||
m_page2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page2->SetBackgroundColour(*wxWHITE);
|
m_page2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page2->SetBackgroundColour(*wxWHITE);
|
||||||
|
|
||||||
create_printer_page1(m_page1);
|
create_printer_page1(m_page1);
|
||||||
@@ -2652,7 +2652,7 @@ wxBoxSizer *CreatePrinterPresetDialog::create_presets_template_item(wxWindow *pa
|
|||||||
wxBoxSizer *vertical_sizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer *vertical_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
m_scrolled_preset_window = new wxScrolledWindow(parent);
|
m_scrolled_preset_window = new wxScrolledWindow(parent);
|
||||||
m_scrolled_preset_window->SetScrollRate(5, FromDIP(20));
|
m_scrolled_preset_window->SetScrollRate(5, 5);
|
||||||
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
|
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
|
||||||
//m_scrolled_preset_window->SetMinSize(wxSize(FromDIP(1500), FromDIP(-1)));
|
//m_scrolled_preset_window->SetMinSize(wxSize(FromDIP(1500), FromDIP(-1)));
|
||||||
m_scrolled_preset_window->SetMaxSize(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);
|
optionSizer->SetMinSize(OPTION_SIZE);
|
||||||
horizontal_sizer->Add(optionSizer, 0, wxEXPAND | wxALL, FromDIP(10));
|
horizontal_sizer->Add(optionSizer, 0, wxEXPAND | wxALL, FromDIP(10));
|
||||||
m_scrolled_preset_window = new wxScrolledWindow(parent);
|
m_scrolled_preset_window = new wxScrolledWindow(parent);
|
||||||
m_scrolled_preset_window->SetScrollRate(5, FromDIP(20));
|
m_scrolled_preset_window->SetScrollRate(5, 5);
|
||||||
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
|
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(660), FromDIP(400)));
|
m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(660), FromDIP(400)));
|
||||||
m_scrolled_preset_window->SetSize(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);
|
wxBoxSizer *filament_preset_tree_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
m_preset_tree_window = new wxScrolledWindow(this);
|
m_preset_tree_window = new wxScrolledWindow(this);
|
||||||
m_preset_tree_window->SetScrollRate(5, FromDIP(20));
|
m_preset_tree_window->SetScrollRate(5, 5);
|
||||||
m_preset_tree_window->SetBackgroundColour(PRINTER_LIST_COLOUR);
|
m_preset_tree_window->SetBackgroundColour(PRINTER_LIST_COLOUR);
|
||||||
m_preset_tree_window->SetMinSize(wxSize(-1, FromDIP(400)));
|
m_preset_tree_window->SetMinSize(wxSize(-1, FromDIP(400)));
|
||||||
m_preset_tree_window->SetMaxSize(wxSize(-1, FromDIP(300)));
|
m_preset_tree_window->SetMaxSize(wxSize(-1, FromDIP(300)));
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ DeviceErrorDialog::DeviceErrorDialog(MachineObject* obj, wxWindow* parent, wxWin
|
|||||||
m_line_top->SetBackgroundColour(wxColour(166, 169, 170));
|
m_line_top->SetBackgroundColour(wxColour(166, 169, 170));
|
||||||
|
|
||||||
m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_scroll_area->SetScrollRate(0, FromDIP(20));
|
m_scroll_area->SetScrollRate(0, 5);
|
||||||
m_scroll_area->SetBackgroundColour(*wxWHITE);
|
m_scroll_area->SetBackgroundColour(*wxWHITE);
|
||||||
m_scroll_area->SetMinSize(wxSize(FromDIP(320), FromDIP(250)));
|
m_scroll_area->SetMinSize(wxSize(FromDIP(320), FromDIP(250)));
|
||||||
|
|
||||||
|
|||||||
@@ -1267,8 +1267,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
|||||||
if (current_top_layer_only != required_top_layer_only)
|
if (current_top_layer_only != required_top_layer_only)
|
||||||
m_viewer.toggle_top_layer_only_view_range();
|
m_viewer.toggle_top_layer_only_view_range();
|
||||||
|
|
||||||
read_reduced_detail_preferences();
|
|
||||||
|
|
||||||
// ORCA: darken the layers the preview layer slider is not scrubbed to
|
// 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(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")));
|
m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness")));
|
||||||
@@ -1492,10 +1490,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
|||||||
|
|
||||||
// BBS: data for rendering color arrangement recommendation
|
// BBS: data for rendering color arrangement recommendation
|
||||||
m_nozzle_nums = print.config().option<ConfigOptionFloats>("nozzle_diameter")->values.size();
|
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
|
// Orca hack: Hide filament group for non-bbl printers
|
||||||
if (!print.is_BBL_printer()) m_nozzle_nums = 1;
|
if (!print.is_BBL_printer()) m_nozzle_nums = 1;
|
||||||
std::vector<int> filament_maps = print.get_filament_maps();
|
std::vector<int> filament_maps = print.get_filament_maps();
|
||||||
@@ -1666,7 +1660,6 @@ void GCodeViewer::reset_shell()
|
|||||||
{
|
{
|
||||||
m_shells.volumes.clear();
|
m_shells.volumes.clear();
|
||||||
m_shells.print_id = -1;
|
m_shells.print_id = -1;
|
||||||
m_shells.with_wipe_tower = false;
|
|
||||||
m_shell_bounding_box = BoundingBoxf3();
|
m_shell_bounding_box = BoundingBoxf3();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1703,11 +1696,6 @@ void GCodeViewer::reset()
|
|||||||
void GCodeViewer::render_scene(int canvas_width, int canvas_height)
|
void GCodeViewer::render_scene(int canvas_width, int canvas_height)
|
||||||
{
|
{
|
||||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||||
// 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);
|
render_shells(canvas_width, canvas_height);
|
||||||
|
|
||||||
if (m_viewer.get_extrusion_roles_count() == 0)
|
if (m_viewer.get_extrusion_roles_count() == 0)
|
||||||
@@ -2031,56 +2019,6 @@ void GCodeViewer::update_layers_slider_mode()
|
|||||||
// TODO m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder);
|
// 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)
|
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]));
|
m_viewer.set_layers_view_range(static_cast<uint32_t>(layers_z_range[0]), static_cast<uint32_t>(layers_z_range[1]));
|
||||||
@@ -2405,11 +2343,7 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const
|
|||||||
void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_previewing)
|
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;
|
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)) {
|
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
|
//BBS: update force previewing logic
|
||||||
if (force_previewing)
|
if (force_previewing)
|
||||||
m_shells.previewing = force_previewing;
|
m_shells.previewing = force_previewing;
|
||||||
@@ -2518,45 +2452,10 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p
|
|||||||
m_shells.print_id = print.id().id;
|
m_shells.print_id = print.id().id;
|
||||||
m_shells.print_modify_count = print.get_modified_count();
|
m_shells.print_modify_count = print.get_modified_count();
|
||||||
m_shells.previewing = true;
|
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%")
|
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();
|
% 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()
|
void GCodeViewer::render_toolpaths()
|
||||||
{
|
{
|
||||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||||
@@ -2757,50 +2656,6 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height)
|
|||||||
glsafe(::glDepthMask(GL_TRUE));
|
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
|
//BBS
|
||||||
void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show /*= true*/) const {
|
void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show /*= true*/) const {
|
||||||
if (!show)
|
if (!show)
|
||||||
|
|||||||
@@ -174,8 +174,6 @@ public:
|
|||||||
int print_id{-1};
|
int print_id{-1};
|
||||||
int print_modify_count{-1};
|
int print_modify_count{-1};
|
||||||
bool previewing{false};
|
bool previewing{false};
|
||||||
// the prime tower was loaded with the objects, for the solid model
|
|
||||||
bool with_wipe_tower{false};
|
|
||||||
};
|
};
|
||||||
//BBS
|
//BBS
|
||||||
ConflictResultOpt m_conflict_result;
|
ConflictResultOpt m_conflict_result;
|
||||||
@@ -236,17 +234,6 @@ private:
|
|||||||
|
|
||||||
bool m_legend_visible{ true };
|
bool m_legend_visible{ true };
|
||||||
bool m_legend_enabled{ 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;
|
float m_legend_height;
|
||||||
PrintEstimatedStatistics m_print_statistics;
|
PrintEstimatedStatistics m_print_statistics;
|
||||||
@@ -304,7 +291,7 @@ public:
|
|||||||
// void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
|
// 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_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);
|
// 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_count() != 0; }
|
bool has_data() const { return !m_viewer.get_extrusion_roles().empty(); }
|
||||||
|
|
||||||
bool can_export_toolpaths() const;
|
bool can_export_toolpaths() const;
|
||||||
std::vector<int> get_plater_extruder();
|
std::vector<int> get_plater_extruder();
|
||||||
@@ -376,14 +363,6 @@ public:
|
|||||||
void set_dim_previous_layers_brightness(float value) { m_viewer.set_dim_previous_layers_brightness(value); }
|
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(); }
|
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);
|
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; }
|
bool is_legend_shown() const { return m_legend_visible && m_legend_enabled; }
|
||||||
|
|||||||
@@ -2084,11 +2084,6 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
|
|||||||
const bool overlay_tick = m_fps_overlay_tick;
|
const bool overlay_tick = m_fps_overlay_tick;
|
||||||
m_fps_overlay_tick = false;
|
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
|
// 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.
|
// 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);
|
const bool reuse_scene = !scene_dirty && _can_reuse_cached_scene(camera);
|
||||||
@@ -3282,16 +3277,9 @@ void GLCanvas3D::bind_event_handlers()
|
|||||||
if (m_selection_edit.kind != SelectionEdit::None)
|
if (m_selection_edit.kind != SelectionEdit::None)
|
||||||
finish_selection_edit();
|
finish_selection_edit();
|
||||||
ImGui::SetWindowFocus(nullptr);
|
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();
|
render();
|
||||||
evt.Skip();
|
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_event_handlers_bound = true;
|
||||||
|
|
||||||
m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
|
m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
|
||||||
@@ -3367,17 +3355,6 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
|
|||||||
m_overlay_dirty |= imgui_requires_extra_frame;
|
m_overlay_dirty |= imgui_requires_extra_frame;
|
||||||
#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
|
#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
|
||||||
m_dirty |= GLTexture::Compressor::has_compressed_texture_to_refresh();
|
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)
|
if (!m_dirty && !m_overlay_dirty)
|
||||||
return;
|
return;
|
||||||
@@ -3908,10 +3885,6 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only a wheel the panels did not take moves the camera
|
|
||||||
if (m_canvas_type == CanvasPreview)
|
|
||||||
note_preview_interaction();
|
|
||||||
|
|
||||||
#ifdef __WXMSW__
|
#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,
|
// 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.
|
// if the event is not allowed to be passed further.
|
||||||
@@ -4012,11 +3985,6 @@ void GLCanvas3D::on_fps_overlay_timer(wxTimerEvent& evt)
|
|||||||
wxWakeUpIdle();
|
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)
|
void GLCanvas3D::schedule_extra_frame(int milliseconds)
|
||||||
{
|
{
|
||||||
// Schedule idle event right now
|
// Schedule idle event right now
|
||||||
@@ -5663,9 +5631,6 @@ void GLCanvas3D::mouse_up_cleanup()
|
|||||||
m_mouse.ignore_left_up = false;
|
m_mouse.ignore_left_up = false;
|
||||||
m_mouse.ignore_right_up = false;
|
m_mouse.ignore_right_up = false;
|
||||||
m_dirty = true;
|
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())
|
if (m_canvas->HasCapture())
|
||||||
m_canvas->ReleaseMouse();
|
m_canvas->ReleaseMouse();
|
||||||
@@ -7814,20 +7779,13 @@ bool GLCanvas3D::_is_scene_cacheable() const
|
|||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// The scene follows the cursor while the user drags, under a gizmo that draws at the cursor, and
|
// The scene follows the cursor during a drag, under a gizmo that draws at the cursor, and while
|
||||||
// while the cursor is on the layer height bar, where the object shader draws a band at its height.
|
// 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 GLGizmoBase* gizmo = m_gizmos.get_current();
|
||||||
const bool cursor_on_layers_bar = is_layers_editing_enabled() &&
|
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());
|
m_layers_editing.bar_rect_contains(*this, (float)m_mouse.position.x(), (float)m_mouse.position.y());
|
||||||
return !is_user_interacting() && (gizmo == nullptr || !gizmo->render_follows_cursor()) && !cursor_on_layers_bar;
|
return !m_mouse.dragging && !m_gizmos.is_dragging() && !m_rectangle_selection.is_dragging() &&
|
||||||
}
|
(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
|
bool GLCanvas3D::_is_frame_skipping_enabled() const
|
||||||
@@ -8818,24 +8776,6 @@ void GLCanvas3D::_render_wireframe_overlay()
|
|||||||
shader->stop_using();
|
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
|
//BBS: GUI refactor: add canvas size as parameters
|
||||||
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
|
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -656,10 +656,6 @@ private:
|
|||||||
ECursorType m_cursor_type;
|
ECursorType m_cursor_type;
|
||||||
GLSelectionRectangle m_rectangle_selection;
|
GLSelectionRectangle m_rectangle_selection;
|
||||||
bool m_navigator_dragging{ false };
|
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
|
//BBS:add plate related logic
|
||||||
mutable std::vector<int> m_hover_volume_idxs;
|
mutable std::vector<int> m_hover_volume_idxs;
|
||||||
@@ -1226,10 +1222,6 @@ public:
|
|||||||
void msw_rescale() { m_gcode_viewer.invalidate_legend(); }
|
void msw_rescale() { m_gcode_viewer.invalidate_legend(); }
|
||||||
|
|
||||||
void request_extra_frame() { m_extra_frame_requested = true; }
|
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);
|
void schedule_extra_frame(int milliseconds);
|
||||||
|
|
||||||
@@ -1380,9 +1372,6 @@ private:
|
|||||||
//BBS: GUI refactor: add canvas size as parameters
|
//BBS: GUI refactor: add canvas size as parameters
|
||||||
void _render_gcode(int canvas_width, int canvas_height);
|
void _render_gcode(int canvas_width, int canvas_height);
|
||||||
void _render_gcode_overlay(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
|
//BBS: render a plane for assemble
|
||||||
void _render_plane() const;
|
void _render_plane() const;
|
||||||
void _render_selection();
|
void _render_selection();
|
||||||
|
|||||||
@@ -456,9 +456,8 @@ void GLModel::init_from(const indexed_triangle_set& its)
|
|||||||
data.reserve_vertices(3 * its.indices.size());
|
data.reserve_vertices(3 * its.indices.size());
|
||||||
data.reserve_indices(3 * its.indices.size());
|
data.reserve_indices(3 * its.indices.size());
|
||||||
|
|
||||||
// CLI thumbnail generation can initialize models without a wx application.
|
// Read user preference: smooth normals enabled
|
||||||
const bool smooth_normals_enabled = wxApp::GetInstance() != nullptr && wxGetApp().app_config != nullptr &&
|
const bool smooth_normals_enabled = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS);
|
||||||
wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS);
|
|
||||||
|
|
||||||
if (smooth_normals_enabled) {
|
if (smooth_normals_enabled) {
|
||||||
// Use per-corner smooth normals (via IGL)
|
// Use per-corner smooth normals (via IGL)
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/I18N.hpp"
|
#include "libslic3r/I18N.hpp"
|
||||||
#include "libslic3r/PresetBundle.hpp"
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
|
#include "libslic3r/InstanceLock.hpp"
|
||||||
#include "libslic3r/Thread.hpp"
|
#include "libslic3r/Thread.hpp"
|
||||||
#include "libslic3r/miniz_extension.hpp"
|
#include "libslic3r/miniz_extension.hpp"
|
||||||
#include "libslic3r/Utils.hpp"
|
#include "libslic3r/Utils.hpp"
|
||||||
@@ -3549,7 +3550,7 @@ bool GUI_App::on_init_inner()
|
|||||||
update_publish_status();
|
update_publish_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_post_initialized && app_config->dirty())
|
if (m_post_initialized && app_config->dirty() && app_config->save_due())
|
||||||
app_config->save();
|
app_config->save();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -7643,8 +7644,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
|
|||||||
|
|
||||||
// Delete the bundle folder and bundle
|
// Delete the bundle folder and bundle
|
||||||
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
|
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
|
||||||
|
{
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::filesystem::remove_all(bundle_folder, ec);
|
boost::filesystem::remove_all(bundle_folder, ec);
|
||||||
|
}
|
||||||
|
|
||||||
preset_bundle->bundles.WriteLock();
|
preset_bundle->bundles.WriteLock();
|
||||||
preset_bundle->bundles.m_bundles.erase(bundle.id);
|
preset_bundle->bundles.m_bundles.erase(bundle.id);
|
||||||
@@ -8931,6 +8935,7 @@ void GUI_App::preset_deleted_from_cloud(std::string setting_id)
|
|||||||
|
|
||||||
// Delete the .info file after cloud deletion is confirmed
|
// Delete the .info file after cloud deletion is confirmed
|
||||||
if (!preset_file_path.empty() && fs::exists(fs::path(preset_file_path))) {
|
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::nowide::remove(preset_file_path.c_str());
|
||||||
BOOST_LOG_TRIVIAL(info) << "Deleted .info file after cloud confirmation: " << preset_file_path;
|
BOOST_LOG_TRIVIAL(info) << "Deleted .info file after cloud confirmation: " << preset_file_path;
|
||||||
}
|
}
|
||||||
@@ -8993,17 +8998,21 @@ void GUI_App::scan_orphaned_info_files()
|
|||||||
fs::path preset_file = info_file;
|
fs::path preset_file = info_file;
|
||||||
preset_file.replace_extension(".json");
|
preset_file.replace_extension(".json");
|
||||||
|
|
||||||
// If .json doesn't exist, .info is orphaned
|
// If .json doesn't exist, .info is orphaned. Read under the lock, so a
|
||||||
if (!fs::exists(preset_file)) {
|
// remove_files() in another instance is seen whole or not at all; the
|
||||||
// Extract setting_id from .info file
|
// delete queue's own mutex is taken after the lock is released.
|
||||||
std::string setting_id = extract_setting_id_from_info(info_file.string());
|
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()) {
|
if (!setting_id.empty()) {
|
||||||
// Add to need_delete_presets
|
// Add to need_delete_presets
|
||||||
delete_preset_from_cloud(setting_id, info_file.string());
|
delete_preset_from_cloud(setting_id, info_file.string());
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
|
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (ec)
|
if (ec)
|
||||||
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
|
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2764,12 +2764,12 @@ ObjectTablePanel::ObjectTablePanel( wxWindow* parent, wxWindowID id, const wxPoi
|
|||||||
//m_object_grid->AssignTable(m_object_grid_table);
|
//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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(440),FromDIP(480)), wxVSCROLL);
|
||||||
m_side_window->SetScrollRate(0, FromDIP(20));
|
m_side_window->SetScrollRate( 0, 5 );
|
||||||
m_page_sizer = new wxBoxSizer(wxVERTICAL);
|
m_page_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
//m_page_top_sizer = new wxBoxSizer(wxHORIZONTAL);
|
//m_page_top_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
m_side_window->SetBackgroundColour(wxColour(0xff, 0xff, 0xff));
|
m_side_window->SetBackgroundColour(wxColour(0xff, 0xff, 0xff));
|
||||||
m_side_window->SetSizer(m_page_sizer);
|
m_side_window->SetSizer(m_page_sizer);
|
||||||
m_side_window->SetScrollbars(1, FromDIP(20), 1, 2);
|
m_side_window->SetScrollbars(1, 20, 1, 2);
|
||||||
//m_side_window->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
|
//m_side_window->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
|
||||||
|
|
||||||
//m_side_window->EnableScrolling(false, true);
|
//m_side_window->EnableScrolling(false, true);
|
||||||
|
|||||||
@@ -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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
|
m_scrolledWindow->SetScrollRate(5, 5);
|
||||||
|
|
||||||
m_top_sizer = new wxBoxSizer(wxVERTICAL);
|
m_top_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
|
|||||||
@@ -483,11 +483,6 @@ 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());
|
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)
|
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();
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
||||||
@@ -496,7 +491,6 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
|
|||||||
|
|
||||||
ImGuiContext& context = *GImGui;
|
ImGuiContext& context = *GImGui;
|
||||||
const ImGuiID id = window->GetID(str_id);
|
const ImGuiID id = window->GetID(str_id);
|
||||||
m_imgui_id = id;
|
|
||||||
|
|
||||||
const ImVec2 pos = window->DC.CursorPos;
|
const ImVec2 pos = window->DC.CursorPos;
|
||||||
const ImRect draw_region(pos, pos + size);
|
const ImRect draw_region(pos, pos + size);
|
||||||
@@ -889,7 +883,6 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
|
|||||||
|
|
||||||
ImGuiContext& context = *GImGui;
|
ImGuiContext& context = *GImGui;
|
||||||
const ImGuiID id = window->GetID(str_id);
|
const ImGuiID id = window->GetID(str_id);
|
||||||
m_imgui_id = id;
|
|
||||||
|
|
||||||
const ImVec2 pos = window->DC.CursorPos;
|
const ImVec2 pos = window->DC.CursorPos;
|
||||||
const ImRect draw_region(pos, pos + size);
|
const ImRect draw_region(pos, pos + size);
|
||||||
|
|||||||
@@ -118,9 +118,6 @@ public:
|
|||||||
|
|
||||||
//BBS update scroll value changed
|
//BBS update scroll value changed
|
||||||
bool is_dirty() { return m_dirty; }
|
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; }
|
void set_as_dirty(bool dirty = true) { m_dirty = dirty; }
|
||||||
bool is_need_post_tick_event() { return m_is_need_post_tick_changed_event; }
|
bool is_need_post_tick_event() { return m_is_need_post_tick_changed_event; }
|
||||||
void reset_post_tick_event(bool val = false) {
|
void reset_post_tick_event(bool val = false) {
|
||||||
@@ -185,8 +182,6 @@ private:
|
|||||||
int m_higher_value;
|
int m_higher_value;
|
||||||
int m_one_layer_value; // ORCA
|
int m_one_layer_value; // ORCA
|
||||||
bool m_dirty = false;
|
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 };
|
bool m_render_as_disabled{ false };
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const Page& page)
|
|||||||
wxGetApp().UpdateDarkUI(scrollable_panel);
|
wxGetApp().UpdateDarkUI(scrollable_panel);
|
||||||
const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE);
|
const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE);
|
||||||
scrollable_panel->SetBackgroundColour(page_colour);
|
scrollable_panel->SetBackgroundColour(page_colour);
|
||||||
scrollable_panel->SetScrollRate(0, FromDIP(20));
|
scrollable_panel->SetScrollRate(0, 20);
|
||||||
const int page_width = FromDIP(PAGE_WIDTH);
|
const int page_width = FromDIP(PAGE_WIDTH);
|
||||||
scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450)));
|
scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450)));
|
||||||
|
|
||||||
|
|||||||
@@ -1125,7 +1125,7 @@ wxBoxSizer* MixedFilamentDialog::create_recommendation_grid()
|
|||||||
outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4));
|
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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116)));
|
||||||
m_recommendation_scroll->SetScrollRate(0, FromDIP(20));
|
m_recommendation_scroll->SetScrollRate(0, 5);
|
||||||
m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8")));
|
m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8")));
|
||||||
|
|
||||||
m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES);
|
m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES);
|
||||||
|
|||||||
@@ -194,10 +194,9 @@ Button* MsgDialog::add_button(wxWindowID btn_id, bool set_focus /*= false*/, con
|
|||||||
bd->button = btn;
|
bd->button = btn;
|
||||||
//bd->type = type;
|
//bd->type = type;
|
||||||
|
|
||||||
// m_buttons.size() is a size_t, which does not match the %d conversion
|
mb->id = wxString::Format("%d", m_buttons.size());
|
||||||
mb->id = wxString(std::to_string(m_buttons.size()));
|
|
||||||
mb->buttondata = bd;
|
mb->buttondata = bd;
|
||||||
m_buttons[ wxString(std::to_string(m_buttons.size()))] = mb;
|
m_buttons[ wxString::Format("%d", m_buttons.size())] = mb;
|
||||||
return btn;
|
return btn;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -399,7 +398,7 @@ static void add_msg_content(wxWindow *parent,
|
|||||||
}
|
}
|
||||||
wxScrolledWindow *scrolledWindow = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
wxScrolledWindow *scrolledWindow = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
scrolledWindow->SetBackgroundColour(*wxWHITE);
|
scrolledWindow->SetBackgroundColour(*wxWHITE);
|
||||||
scrolledWindow->SetScrollRate(0, scrolledWindow->FromDIP(20));
|
scrolledWindow->SetScrollRate(0, 20);
|
||||||
scrolledWindow->EnableScrolling(false, true);
|
scrolledWindow->EnableScrolling(false, true);
|
||||||
wxBoxSizer *sizer_scrolled = new wxBoxSizer(wxHORIZONTAL);
|
wxBoxSizer *sizer_scrolled = new wxBoxSizer(wxHORIZONTAL);
|
||||||
Label *wrapped_text = new Label(scrolledWindow, font, msg, LB_AUTO_WRAP, wxSize(info_width, -1));
|
Label *wrapped_text = new Label(scrolledWindow, font, msg, LB_AUTO_WRAP, wxSize(info_width, -1));
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ MultiMachineManagerPage::MultiMachineManagerPage(wxWindow* parent)
|
|||||||
|
|
||||||
m_machine_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
m_machine_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||||
m_machine_list->SetBackgroundColour(*wxWHITE);
|
m_machine_list->SetBackgroundColour(*wxWHITE);
|
||||||
m_machine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
|
m_machine_list->SetScrollRate(0, 5);
|
||||||
m_machine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * 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)));
|
m_machine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
|
||||||
|
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ MultiMachinePickPage::MultiMachinePickPage(Plater* plater /*= nullptr*/)
|
|||||||
scroll_macine_list->SetMinSize(wxSize(FromDIP(400), FromDIP(10 * 30)));
|
scroll_macine_list->SetMinSize(wxSize(FromDIP(400), FromDIP(10 * 30)));
|
||||||
scroll_macine_list->SetMaxSize(wxSize(FromDIP(400), FromDIP(10 * 30)));
|
scroll_macine_list->SetMaxSize(wxSize(FromDIP(400), FromDIP(10 * 30)));
|
||||||
scroll_macine_list->SetBackgroundColour(*wxWHITE);
|
scroll_macine_list->SetBackgroundColour(*wxWHITE);
|
||||||
scroll_macine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
|
scroll_macine_list->SetScrollRate(0, 5);
|
||||||
|
|
||||||
sizer_machine_list = new wxBoxSizer(wxVERTICAL);
|
sizer_machine_list = new wxBoxSizer(wxVERTICAL);
|
||||||
scroll_macine_list->SetSizer(sizer_machine_list);
|
scroll_macine_list->SetSizer(sizer_machine_list);
|
||||||
|
|||||||
@@ -675,7 +675,7 @@ LocalTaskManagerPage::LocalTaskManagerPage(wxWindow* parent)
|
|||||||
|
|
||||||
m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||||
m_task_list->SetBackgroundColour(*wxWHITE);
|
m_task_list->SetBackgroundColour(*wxWHITE);
|
||||||
m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
|
m_task_list->SetScrollRate(0, 5);
|
||||||
m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 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)));
|
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 = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||||
m_task_list->SetBackgroundColour(*wxWHITE);
|
m_task_list->SetBackgroundColour(*wxWHITE);
|
||||||
m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
|
m_task_list->SetScrollRate(0, 5);
|
||||||
m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 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)));
|
m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
|
||||||
|
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c
|
|||||||
//new color table
|
//new color table
|
||||||
m_scrolledWindow = new wxScrolledWindow(m_page_simple, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
m_scrolledWindow = new wxScrolledWindow(m_page_simple, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrolledWindow->SetScrollRate(0, FromDIP(20));
|
m_scrolledWindow->SetScrollRate(0, 20);
|
||||||
m_scrolledWindow->EnableScrolling(false, true);
|
m_scrolledWindow->EnableScrolling(false, true);
|
||||||
m_scrolledWindow->ShowScrollbars(wxScrollbarVisibility::wxSHOW_SB_NEVER, wxScrollbarVisibility::wxSHOW_SB_DEFAULT);
|
m_scrolledWindow->ShowScrollbars(wxScrollbarVisibility::wxSHOW_SB_NEVER, wxScrollbarVisibility::wxSHOW_SB_DEFAULT);
|
||||||
draw_new_table();
|
draw_new_table();
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
|
|||||||
m_page_sizer = new wxBoxSizer(wxVERTICAL);
|
m_page_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
m_page_view->SetSizer(m_page_sizer);
|
m_page_view->SetSizer(m_page_sizer);
|
||||||
m_page_view->SetScrollbars(1, FromDIP(20), 1, 2);
|
m_page_view->SetScrollbars(1, 20, 1, 2);
|
||||||
//m_page_view->SetScrollRate( 5, 5 );
|
//m_page_view->SetScrollRate( 5, 5 );
|
||||||
|
|
||||||
if (m_mode_region)
|
if (m_mode_region)
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _
|
|||||||
m_line->SetBackgroundColour(wxColor(238, 238, 238));
|
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 = new wxScrolledWindow(m_book_third_panel, wxID_ANY, wxDefaultPosition, wxSize(267, -1), wxHSCROLL | wxVSCROLL);
|
||||||
m_list_view->SetScrollRate(5, FromDIP(30));
|
m_list_view->SetScrollRate(5, 5);
|
||||||
m_list_view->SetMinSize(wxSize(FromDIP(267), FromDIP(378)));
|
m_list_view->SetMinSize(wxSize(FromDIP(267), FromDIP(378)));
|
||||||
m_list_view->SetMaxSize(wxSize(FromDIP(267), FromDIP(378)));
|
m_list_view->SetMaxSize(wxSize(FromDIP(267), FromDIP(378)));
|
||||||
m_list_view->SetBackgroundColour(*wxWHITE);
|
m_list_view->SetBackgroundColour(*wxWHITE);
|
||||||
|
|||||||
@@ -3049,7 +3049,8 @@ Sidebar::Sidebar(Plater *parent)
|
|||||||
|
|
||||||
// add filament content
|
// 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 = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
|
||||||
p->m_panel_filament_content->SetScrollRate(0, FromDIP(20));
|
p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2);
|
||||||
|
p->m_panel_filament_content->SetScrollRate(0, 5);
|
||||||
//p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)});
|
//p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)});
|
||||||
p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255));
|
p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255));
|
||||||
|
|
||||||
@@ -3141,7 +3142,8 @@ Sidebar::Sidebar(Plater *parent)
|
|||||||
// 3) Mixed filament rows, in their own scroll area so a long mixed list does not
|
// 3) Mixed filament rows, in their own scroll area so a long mixed list does not
|
||||||
// push the physical filament list off screen.
|
// 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 = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
|
||||||
p->m_mixed_scroll_area->SetScrollRate(0, FromDIP(20));
|
p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2);
|
||||||
|
p->m_mixed_scroll_area->SetScrollRate(0, 5);
|
||||||
p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||||
{
|
{
|
||||||
auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
@@ -12800,6 +12802,8 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|||||||
notification_manager->set_slicing_progress_export_possible();
|
notification_manager->set_slicing_progress_export_possible();
|
||||||
|
|
||||||
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
// 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->background_process.reset_export();
|
||||||
// This bool stops showing export finished notification even when process_completed_with_error is false
|
// This bool stops showing export finished notification even when process_completed_with_error is false
|
||||||
bool has_error = false;
|
bool has_error = false;
|
||||||
@@ -12846,18 +12850,8 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|||||||
|
|
||||||
{
|
{
|
||||||
Slic3r::LifecycleEventContext ctx;
|
Slic3r::LifecycleEventContext ctx;
|
||||||
if (const PrintBase* print = this->background_process.current_print()) {
|
ctx.name = lifecycle_job_name;
|
||||||
const Model& model = print->model();
|
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
||||||
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());
|
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
|
||||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString too
|
|||||||
return sizer;
|
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, std::function<void(std::string)> onchange, 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, const wxString wiki_url)
|
||||||
{
|
{
|
||||||
assert(vlist.size() == config_name_index.size());
|
assert(vlist.size() == config_name_index.size());
|
||||||
unsigned int current_index = 0;
|
unsigned int current_index = 0;
|
||||||
@@ -338,9 +338,8 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString too
|
|||||||
auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index);
|
auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index);
|
||||||
|
|
||||||
//// save config
|
//// save config
|
||||||
combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index, onchange](wxCommandEvent& e) {
|
combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index](wxCommandEvent& e) {
|
||||||
app_config->set(param, config_name_index[e.GetSelection()]);
|
app_config->set(param, config_name_index[e.GetSelection()]);
|
||||||
if (onchange != nullptr) onchange(config_name_index[e.GetSelection()]);
|
|
||||||
e.Skip();
|
e.Skip();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1476,7 +1475,7 @@ void PreferencesDialog::create()
|
|||||||
app_config = get_app_config();
|
app_config = get_app_config();
|
||||||
|
|
||||||
m_parent = new MyscrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
m_parent = new MyscrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_parent->SetScrollRate(0, FromDIP(20));
|
m_parent->SetScrollRate(5, 5);
|
||||||
m_parent->SetBackgroundColour(*wxWHITE);
|
m_parent->SetBackgroundColour(*wxWHITE);
|
||||||
|
|
||||||
m_sizer_body = new wxBoxSizer(wxVERTICAL);
|
m_sizer_body = new wxBoxSizer(wxVERTICAL);
|
||||||
@@ -2045,32 +2044,6 @@ void PreferencesDialog::create_items()
|
|||||||
"preview_default_view_type", PreviewViewTypeLabels, PreviewViewTypeValues);
|
"preview_default_view_type", PreviewViewTypeLabels, PreviewViewTypeValues);
|
||||||
g_sizer->Add(item_preview_view_type);
|
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(
|
auto item_dim_previous_layers = create_item_checkbox(
|
||||||
_L("Dim lower layers"),
|
_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."),
|
_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."),
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public:
|
|||||||
wxBoxSizer *create_item_title(wxString title);
|
wxBoxSizer *create_item_title(wxString title);
|
||||||
wxBoxSizer *create_item_label(wxString label, const wxString tooltip = "", const wxString wiki_url = "");
|
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::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, std::function<void(std::string)> 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_region_combobox(wxString title, wxString tooltip);
|
wxBoxSizer *create_item_region_combobox(wxString title, wxString tooltip);
|
||||||
wxBoxSizer *create_item_language_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);
|
wxBoxSizer *create_item_loglevel_combobox(wxString title, wxString tooltip, std::vector<wxString> vlist);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ PrintOptionsDialog::PrintOptionsDialog(wxWindow* parent)
|
|||||||
|
|
||||||
|
|
||||||
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
|
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
|
||||||
m_scrollwindow->SetScrollRate(0, FromDIP(20));
|
m_scrollwindow->SetScrollRate(0, FromDIP(10));
|
||||||
m_scrollwindow->SetBackgroundColour(*wxWHITE);
|
m_scrollwindow->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord));
|
m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord));
|
||||||
m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord));
|
m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord));
|
||||||
|
|||||||
@@ -1225,7 +1225,7 @@ size_t PublishSettingsDialog::category_index_for(
|
|||||||
}
|
}
|
||||||
|
|
||||||
category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
category.scroll->SetScrollRate(0, FromDIP(20));
|
category.scroll->SetScrollRate(0, 10);
|
||||||
category.scroll->SetBackgroundColour(GetBackgroundColour());
|
category.scroll->SetBackgroundColour(GetBackgroundColour());
|
||||||
category.list_sizer = new wxBoxSizer(wxVERTICAL);
|
category.list_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
category.scroll->SetSizer(category.list_sizer);
|
category.scroll->SetSizer(category.list_sizer);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ ReleaseNoteDialog::ReleaseNoteDialog(Plater *plater /*= nullptr*/)
|
|||||||
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
|
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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL);
|
||||||
m_vebview_release_note->SetScrollRate(5, FromDIP(20));
|
m_vebview_release_note->SetScrollRate(5, 5);
|
||||||
m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
|
m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
|
||||||
m_vebview_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430)));
|
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));
|
operation_tips->SetMaxSize(wxSize(FromDIP(260), -1));
|
||||||
|
|
||||||
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_vebview_release_note->SetScrollRate(5, FromDIP(20));
|
m_vebview_release_note->SetScrollRate(5, 5);
|
||||||
m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
|
m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
|
||||||
m_vebview_release_note->SetMinSize(wxSize(FromDIP(260), FromDIP(150)));
|
m_vebview_release_note->SetMinSize(wxSize(FromDIP(260), FromDIP(150)));
|
||||||
m_vebview_release_note->SetMaxSize(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_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 = new wxScrolledWindow(m_simplebook_release_note, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL);
|
||||||
m_scrollwindows_release_note->SetScrollRate(5, FromDIP(20));
|
m_scrollwindows_release_note->SetScrollRate(5, 5);
|
||||||
m_scrollwindows_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
|
m_scrollwindows_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
|
||||||
|
|
||||||
//webview
|
//webview
|
||||||
@@ -538,7 +538,7 @@ SecondaryCheckDialog::SecondaryCheckDialog(wxWindow* parent, wxWindowID id, cons
|
|||||||
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
|
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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_vebview_release_note->SetScrollRate(0, FromDIP(20));
|
m_vebview_release_note->SetScrollRate(0, 5);
|
||||||
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
|
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
|
||||||
m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380)));
|
m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380)));
|
||||||
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
||||||
@@ -823,7 +823,7 @@ PrintErrorDialog::PrintErrorDialog(wxWindow* parent, wxWindowID id, const wxStri
|
|||||||
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(5));
|
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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_vebview_release_note->SetScrollRate(0, FromDIP(20));
|
m_vebview_release_note->SetScrollRate(0, 5);
|
||||||
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
|
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
|
||||||
m_vebview_release_note->SetMinSize(wxSize(FromDIP(320), FromDIP(250)));
|
m_vebview_release_note->SetMinSize(wxSize(FromDIP(320), FromDIP(250)));
|
||||||
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
||||||
@@ -1122,7 +1122,7 @@ ConfirmBeforeSendDialog::ConfirmBeforeSendDialog(wxWindow* parent, wxWindowID id
|
|||||||
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
|
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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_vebview_release_note->SetScrollRate(0, FromDIP(20));
|
m_vebview_release_note->SetScrollRate(0, 5);
|
||||||
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
|
m_vebview_release_note->SetBackgroundColour(*wxWHITE);
|
||||||
m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380)));
|
m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380)));
|
||||||
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ SafetyOptionsDialog::SafetyOptionsDialog(wxWindow* parent)
|
|||||||
SetSize(FromDIP(480),FromDIP(320));
|
SetSize(FromDIP(480),FromDIP(320));
|
||||||
|
|
||||||
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
|
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
|
||||||
m_scrollwindow->SetScrollRate(0, FromDIP(20));
|
m_scrollwindow->SetScrollRate(0, FromDIP(10));
|
||||||
m_scrollwindow->SetBackgroundColour(*wxWHITE);
|
m_scrollwindow->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord));
|
m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord));
|
||||||
m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord));
|
m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord));
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater)
|
|||||||
|
|
||||||
wxBoxSizer* m_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer* m_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
m_scroll_area = new wxScrolledWindow(this);
|
m_scroll_area = new wxScrolledWindow(this);
|
||||||
m_scroll_area->SetScrollRate(0, FromDIP(20));
|
m_scroll_area->SetScrollRate(0, 20);
|
||||||
m_scroll_area->SetBackgroundColour(m_colour_def_color);
|
m_scroll_area->SetBackgroundColour(m_colour_def_color);
|
||||||
m_scroll_area->SetMinSize(wxSize(FromDIP(700), FromDIP(600)));
|
m_scroll_area->SetMinSize(wxSize(FromDIP(700), FromDIP(600)));
|
||||||
m_scroll_area->SetMaxSize(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
|
//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 = 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->SetBackgroundColour(*wxWHITE);
|
||||||
m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
|
m_sw_print_failed_info->SetScrollRate(0, 5);
|
||||||
m_sw_print_failed_info->SetMinSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)));
|
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)));
|
m_sw_print_failed_info->SetMaxSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)));
|
||||||
|
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ SelectMachinePopup::SelectMachinePopup(wxWindow *parent)
|
|||||||
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
|
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
|
||||||
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
|
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
|
||||||
m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y);
|
m_scrolledWindow->SetScrollRate(0, 5);
|
||||||
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
|
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
|
||||||
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
|
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
|
||||||
m_scrolledWindow->Layout();
|
m_scrolledWindow->Layout();
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ SendMultiMachinePage::SendMultiMachinePage(Plater* plater)
|
|||||||
|
|
||||||
m_main_scroll = new ScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
m_main_scroll = new ScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_main_scroll->SetBackgroundColour(*wxWHITE);
|
m_main_scroll->SetBackgroundColour(*wxWHITE);
|
||||||
m_main_scroll->SetScrollRate(5, FromDIP(20));
|
m_main_scroll->SetScrollRate(5, 5);
|
||||||
|
|
||||||
m_sizer_body = new wxBoxSizer(wxVERTICAL);
|
m_sizer_body = new wxBoxSizer(wxVERTICAL);
|
||||||
m_main_page = create_page();
|
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 = new wxScrolledWindow(main_page, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(800), FromDIP(300)), wxHSCROLL | wxVSCROLL);
|
||||||
scroll_macine_list->SetBackgroundColour(*wxWHITE);
|
scroll_macine_list->SetBackgroundColour(*wxWHITE);
|
||||||
scroll_macine_list->SetScrollRate(5, FromDIP(SEND_ITEM_MAX_HEIGHT));
|
scroll_macine_list->SetScrollRate(5, 5);
|
||||||
scroll_macine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * 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)));
|
scroll_macine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT)));
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater)
|
|||||||
//show bind failed info
|
//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 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL);
|
||||||
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
|
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
|
||||||
m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
|
m_sw_print_failed_info->SetScrollRate(0, 5);
|
||||||
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
|
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
|
||||||
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
|
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name)
|
||||||
: wxScrolledWindow(parent, id, pos, size, wxHSCROLL | wxVSCROLL)
|
: wxScrolledWindow(parent, id, pos, size, wxHSCROLL | wxVSCROLL)
|
||||||
{
|
{
|
||||||
SetScrollRate(25, FromDIP(20));
|
this->SetScrollRate(25, 25);
|
||||||
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
|
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
|
||||||
if (!dev) return;
|
if (!dev) return;
|
||||||
obj = dev->get_selected_machine();
|
obj = dev->get_selected_machine();
|
||||||
|
|||||||
@@ -700,7 +700,7 @@ SyncAmsInfoDialog::SyncAmsInfoDialog(wxWindow *parent, SyncInfo &info) :
|
|||||||
//wxBoxSizer *m_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
//wxBoxSizer *m_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
m_scrolledWindow = new wxScrolledWindow(m_show_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
m_scrolledWindow = new wxScrolledWindow(m_show_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
|
||||||
m_scrolledWindow->SetScrollRate(0, FromDIP(20));
|
m_scrolledWindow->SetScrollRate(0, 20);
|
||||||
m_scrolledWindow->SetMinSize(wxSize(-1, SyncAmsInfoDialogHeightMAX));
|
m_scrolledWindow->SetMinSize(wxSize(-1, SyncAmsInfoDialogHeightMAX));
|
||||||
m_scrolledWindow->SetMaxSize(wxSize(-1, SyncAmsInfoDialogHeightMAX));
|
m_scrolledWindow->SetMaxSize(wxSize(-1, SyncAmsInfoDialogHeightMAX));
|
||||||
m_scrolledWindow->EnableScrolling(false,true);
|
m_scrolledWindow->EnableScrolling(false,true);
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ public:
|
|||||||
|
|
||||||
m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_content->SetBackgroundColour(pop_bg);
|
m_content->SetBackgroundColour(pop_bg);
|
||||||
m_content->SetScrollRate(0, FromDIP(20));
|
m_content->SetScrollRate(0, FromDIP(5));
|
||||||
auto* outer = new wxBoxSizer(wxVERTICAL);
|
auto* outer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
const int pop_w = std::max(FromDIP(213), popup_width);
|
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,
|
m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition,
|
||||||
wxSize(-1, FromDIP(300)));
|
wxSize(-1, FromDIP(300)));
|
||||||
m_mapping_scroll->SetScrollRate(0, FromDIP(20));
|
m_mapping_scroll->SetScrollRate(0, FromDIP(10));
|
||||||
m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||||
m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
|
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) {
|
if (m_mapping_scroll) {
|
||||||
m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300)));
|
m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300)));
|
||||||
m_mapping_scroll->SetScrollRate(0, FromDIP(20));
|
m_mapping_scroll->SetScrollRate(0, FromDIP(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_btn_skip) {
|
if (m_btn_skip) {
|
||||||
|
|||||||
@@ -942,7 +942,7 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_
|
|||||||
m_sizer_tab->Add(m_table_top, 1, 0, 0);
|
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 = new wxScrolledWindow(m_panel_tab, wxID_ANY, wxDefaultPosition, UNSAVE_CHANGE_DIALOG_SCROLL_WINDOW_SIZE, wxNO_BORDER|wxVSCROLL);
|
||||||
m_scrolledWindow->SetScrollRate(0, FromDIP(20));
|
m_scrolledWindow->SetScrollRate(0, 5);
|
||||||
m_scrolledWindow->SetBackgroundColour(GREY200);
|
m_scrolledWindow->SetBackgroundColour(GREY200);
|
||||||
m_sizer_bottom = new wxBoxSizer(wxVERTICAL);
|
m_sizer_bottom = new wxBoxSizer(wxVERTICAL);
|
||||||
m_sizer_bottom->Add(m_scrolledWindow, 1, wxEXPAND, 0);
|
m_sizer_bottom->Add(m_scrolledWindow, 1, wxEXPAND, 0);
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector<Update> &updates, bool force_
|
|||||||
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
|
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);
|
auto m_scrollwindw_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)),wxVSCROLL);
|
||||||
m_scrollwindw_release_note->SetScrollRate(0, FromDIP(20));
|
m_scrollwindw_release_note->SetScrollRate(0, 5);
|
||||||
m_scrollwindw_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
|
m_scrollwindw_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
|
||||||
m_scrollwindw_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430)));
|
m_scrollwindw_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430)));
|
||||||
m_scrollwindw_release_note->SetWindowStyle(wxVSCROLL);
|
m_scrollwindw_release_note->SetWindowStyle(wxVSCROLL);
|
||||||
|
|||||||
@@ -1629,7 +1629,7 @@ UpgradePanel::UpgradePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos,
|
|||||||
auto m_main_sizer = new wxBoxSizer(wxVERTICAL);
|
auto m_main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
||||||
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
|
m_scrolledWindow->SetScrollRate(5, 25);
|
||||||
|
|
||||||
m_machine_list_sizer = new wxBoxSizer(wxVERTICAL);
|
m_machine_list_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
|
|||||||
@@ -1405,23 +1405,26 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
// is served from the shipped profiles. Each is stamped by name and
|
// is served from the shipped profiles. Each is stamped by name and
|
||||||
// version alone: a profile change requires a version bump, so those two
|
// version alone: a profile change requires a version bump, so those two
|
||||||
// determine content wherever the vendor's copy sits.
|
// determine content wherever the vendor's copy sits.
|
||||||
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; };
|
std::vector<PresetBundle::VendorSource> ordered;
|
||||||
std::vector<VendorSource> ordered;
|
json stamps = json::array();
|
||||||
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) {
|
auto add_vendor = [&ordered, &stamps](const std::string& name, const boost::filesystem::path& dir) {
|
||||||
// The version a load from `dir` would serve: the profile's where one
|
// The version a load from `dir` would serve: the profile's where one
|
||||||
// exists (a cache is only served while it covers the profile beside
|
// exists (a cache is only served while it covers the profile beside
|
||||||
// it), the cache's own stamp where the cache is the whole vendor.
|
// it), the cache's own stamp where the cache is the whole vendor.
|
||||||
// A profile without a version (blacklist.json) carries no presets
|
// A profile without a version (blacklist.json) carries no presets
|
||||||
// and is passed over.
|
// and is passed over.
|
||||||
const boost::filesystem::path profile = dir / (name + ".json");
|
const boost::filesystem::path profile = dir / (name + ".json");
|
||||||
|
std::string version;
|
||||||
if (boost::filesystem::exists(profile)) {
|
if (boost::filesystem::exists(profile)) {
|
||||||
const Semver v = get_version_from_json(profile.string());
|
const Semver v = get_version_from_json(profile.string());
|
||||||
if (v.valid())
|
if (! v.valid())
|
||||||
ordered.push_back({name, dir, v.to_string()});
|
return;
|
||||||
|
version = v.to_string();
|
||||||
} else {
|
} else {
|
||||||
ordered.push_back({name, dir,
|
version = VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name);
|
||||||
VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)});
|
|
||||||
}
|
}
|
||||||
|
ordered.push_back({name, dir});
|
||||||
|
stamps.push_back({name, version});
|
||||||
};
|
};
|
||||||
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||||
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
|
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
|
||||||
@@ -1431,9 +1434,6 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
add_vendor(name, dir);
|
add_vendor(name, dir);
|
||||||
if (ordered.empty())
|
if (ordered.empty())
|
||||||
return false;
|
return false;
|
||||||
json stamps = json::array();
|
|
||||||
for (const VendorSource& v : ordered)
|
|
||||||
stamps.push_back({v.name, v.version});
|
|
||||||
|
|
||||||
// What this function derives is a pure function of that stamped set, so
|
// What this function derives is a pure function of that stamped set, so
|
||||||
// the derived JSON is cached whole: a fresh cache makes an open one
|
// the derived JSON is cached whole: a fresh cache makes an open one
|
||||||
@@ -1461,34 +1461,24 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Each vendor comes from its preset cache where one covers it, which is
|
// Each vendor comes from its preset cache where one covers it, which is
|
||||||
// what makes this worth doing instead of the scan below; loading into a
|
// what makes this worth doing instead of the scan below.
|
||||||
// bundle per vendor keeps the install order the startup path has.
|
|
||||||
PresetBundle bundle;
|
PresetBundle bundle;
|
||||||
auto load_vendor = [](PresetBundle& into, const std::string& vendor,
|
std::vector<std::string> failed;
|
||||||
const boost::filesystem::path& dir, const PresetBundle* base) {
|
const std::string errors = bundle.load_vendors(ordered, ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||||
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem,
|
/*allow_cache=*/true, m_cancel_token.get(), &failed).second;
|
||||||
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
|
if (*m_cancel_token || bundle.vendors.empty())
|
||||||
};
|
return false;
|
||||||
for (const VendorSource& v : ordered) {
|
if (! errors.empty())
|
||||||
if (*m_cancel_token)
|
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: loading the vendors reported: " << errors;
|
||||||
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
|
// A vendor that failed to load sends this open to the scan below, which lists
|
||||||
if (v.name == filament_library) {
|
// what it can read of every vendor.
|
||||||
load_vendor(bundle, v.name, v.dir, nullptr);
|
if (! failed.empty())
|
||||||
} else {
|
|
||||||
PresetBundle tmp;
|
|
||||||
load_vendor(tmp, v.name, v.dir, &bundle);
|
|
||||||
bundle.merge_presets(std::move(tmp));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (bundle.vendors.empty())
|
|
||||||
return false;
|
return false;
|
||||||
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
|
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Written through a temp file and moved into place, as the preset caches
|
// Written through a temp file and moved into place, as the preset caches
|
||||||
// are: half a cache must never be readable, and the PID suffix keeps two
|
// are: half a cache must never be readable.
|
||||||
// instances from interleaving on one temp file.
|
|
||||||
const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp";
|
|
||||||
try {
|
try {
|
||||||
json out;
|
json out;
|
||||||
out["format"] = 1;
|
out["format"] = 1;
|
||||||
@@ -1497,18 +1487,9 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
for (const char* key : { "model", "machine", "filament", "process" })
|
for (const char* key : { "model", "machine", "filament", "process" })
|
||||||
profile[key] = m_ProfileJson[key];
|
profile[key] = m_ProfileJson[key];
|
||||||
boost::filesystem::create_directories(cache_file.parent_path());
|
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());
|
throw std::runtime_error(ec.message());
|
||||||
} catch (const std::exception& e) {
|
} 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();
|
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ CheckList::CheckList(
|
|||||||
|
|
||||||
s_sizer = new wxBoxSizer(wxVERTICAL);
|
s_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, scroll_style);
|
m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, scroll_style);
|
||||||
m_scroll_area->SetScrollRate(0, FromDIP(20));
|
m_scroll_area->SetScrollRate(0, 10);
|
||||||
m_scroll_area->SetSizer(s_sizer);
|
m_scroll_area->SetSizer(s_sizer);
|
||||||
m_scroll_area->SetBackgroundColour(parent->GetBackgroundColour());
|
m_scroll_area->SetBackgroundColour(parent->GetBackgroundColour());
|
||||||
m_scroll_area->Bind(wxEVT_RIGHT_DOWN, &CheckList::ShowMenu, this);
|
m_scroll_area->Bind(wxEVT_RIGHT_DOWN, &CheckList::ShowMenu, this);
|
||||||
|
|||||||
@@ -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));
|
m_sizer_main->Add(m_simplebook, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28));
|
||||||
} else {
|
} else {
|
||||||
m_msg_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL );
|
m_msg_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL );
|
||||||
m_msg_scrolledWindow->SetScrollRate(0, FromDIP(20));
|
m_msg_scrolledWindow->SetScrollRate(0,5);
|
||||||
wxBoxSizer* m_msg_sizer= new wxBoxSizer(wxVERTICAL);
|
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);
|
m_msg = new wxStaticText(m_msg_scrolledWindow, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x, -1), 0);
|
||||||
|
|||||||
@@ -1222,7 +1222,7 @@ ImageTransientPopup::ImageTransientPopup( wxWindow *parent, bool scrolled, wxBit
|
|||||||
m_panel->SetSize(300, 300);
|
m_panel->SetSize(300, 300);
|
||||||
|
|
||||||
// And also actually enable them.
|
// And also actually enable them.
|
||||||
m_panel->SetScrollRate(10, FromDIP(20));
|
m_panel->SetScrollRate(10, 10);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <system_error>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <boost/format.hpp>
|
#include <boost/format.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
@@ -580,9 +581,10 @@ bool C3DPrinterOS::save_api_session(const std::string &session, const std::strin
|
|||||||
j.put("session", session);
|
j.put("session", session);
|
||||||
j.put("email", email);
|
j.put("email", email);
|
||||||
try {
|
try {
|
||||||
auto temp_path = m_api_session_file_path + ".tmp";
|
std::ostringstream json;
|
||||||
pt::write_json(temp_path, j);
|
pt::write_json(json, j);
|
||||||
boost::filesystem::rename(temp_path, m_api_session_file_path);
|
if (const std::error_code ec = write_file_atomically(m_api_session_file_path, json.str()))
|
||||||
|
throw std::system_error(ec);
|
||||||
} catch (const std::exception &err) {
|
} catch (const std::exception &err) {
|
||||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
|
||||||
<< m_api_session_file_path
|
<< m_api_session_file_path
|
||||||
|
|||||||
@@ -1475,15 +1475,8 @@ void OrcaCloudServiceAgent::save_sync_state()
|
|||||||
if (sync_state_path.empty())
|
if (sync_state_path.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
if (const std::error_code ec = write_file_atomically(sync_state_path, std::to_string(sync_state.last_sync_timestamp)))
|
||||||
std::string tmp_path = sync_state_path + ".tmp";
|
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to save the sync state: " << ec.message();
|
||||||
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()
|
void OrcaCloudServiceAgent::clear_sync_state()
|
||||||
@@ -1572,22 +1565,10 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
|
|||||||
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
|
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string tmp_path = secret_fallback_path + ".tmp";
|
if (const std::error_code ec = write_file_atomically(secret_fallback_path, signed_payload, /*binary=*/true))
|
||||||
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary);
|
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot write user secret file " << secret_fallback_path << ": " << ec.message();
|
||||||
if (ofs.good()) {
|
else
|
||||||
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;
|
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 {
|
} else {
|
||||||
// Use wxSecretStore only
|
// Use wxSecretStore only
|
||||||
wxSecretStore store = wxSecretStore::GetDefault();
|
wxSecretStore store = wxSecretStore::GetDefault();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
#include <boost/optional.hpp>
|
#include <boost/optional.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <wx/app.h>
|
#include <wx/app.h>
|
||||||
@@ -116,67 +115,10 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
|
|||||||
return webui_url;
|
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
|
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
|
||||||
{
|
{
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
const std::string reason = moonraker_error_reason(body);
|
auto wxbody = wxString::FromUTF8(body.data());
|
||||||
auto wxbody = wxString::FromUTF8(reason.empty() ? body : reason);
|
|
||||||
return wxString::Format("HTTP %u: %s", status, wxbody);
|
return wxString::Format("HTTP %u: %s", status, wxbody);
|
||||||
} else {
|
} else {
|
||||||
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
||||||
|
|||||||
@@ -249,21 +249,10 @@ bool PluginConfig::save()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to a PID-suffixed file and rename it into place, so a crash mid-write cannot truncate an
|
// Written beside the target and moved into place, so a crash mid-write cannot truncate an
|
||||||
// existing config. Same approach as AppConfig::save().
|
// existing config.
|
||||||
const std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
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";
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,6 @@
|
|||||||
#include "libslic3r/Layer.hpp"
|
#include "libslic3r/Layer.hpp"
|
||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/GCodeReader.hpp"
|
#include "libslic3r/GCodeReader.hpp"
|
||||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
|
||||||
#include "libslic3r/Exception.hpp"
|
|
||||||
#include "libslic3r/LifecycleEvents.hpp"
|
|
||||||
|
|
||||||
#include "test_helpers.hpp"
|
#include "test_helpers.hpp"
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
@@ -25,10 +22,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <memory>
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
using namespace Slic3r::Test;
|
using namespace Slic3r::Test;
|
||||||
@@ -230,88 +224,8 @@ std::string resolved_output_name(Model& model, const std::string& format, const
|
|||||||
return print.output_filename(filename_base);
|
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
|
} // 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]")
|
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
|
||||||
{
|
{
|
||||||
Model model;
|
Model model;
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_clipper_utils.cpp
|
test_clipper_utils.cpp
|
||||||
test_config.cpp
|
test_config.cpp
|
||||||
test_config_variant_expansion.cpp
|
test_config_variant_expansion.cpp
|
||||||
|
test_locales_utils.cpp
|
||||||
test_toolordering_nozzle_group.cpp
|
test_toolordering_nozzle_group.cpp
|
||||||
|
test_parallel_resolve.cpp
|
||||||
test_preset_bundle_loading.cpp
|
test_preset_bundle_loading.cpp
|
||||||
test_preset_setting_id.cpp
|
test_preset_setting_id.cpp
|
||||||
test_preset_diff.cpp
|
test_preset_diff.cpp
|
||||||
@@ -49,6 +51,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_ordering_strategies.cpp
|
test_ordering_strategies.cpp
|
||||||
# test_png_io.cpp
|
# test_png_io.cpp
|
||||||
test_indexed_triangle_set.cpp
|
test_indexed_triangle_set.cpp
|
||||||
|
test_instance_lock.cpp
|
||||||
../libnest2d/printer_parts.cpp
|
../libnest2d/printer_parts.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <clocale>
|
||||||
|
|
||||||
|
#include "libslic3r/LocalesUtils.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Switches this thread's numeric locale to one whose decimal separator is a comma,
|
||||||
|
// when the system has one installed.
|
||||||
|
struct CommaNumericLocale
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
bool apply()
|
||||||
|
{
|
||||||
|
for (const char* name : { "de-DE", "German_Germany.1252" })
|
||||||
|
if (std::setlocale(LC_NUMERIC, name) != nullptr)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
locale_t locale { (locale_t) 0 };
|
||||||
|
bool apply()
|
||||||
|
{
|
||||||
|
for (const char* name : { "de_DE.UTF-8", "de_DE.utf8", "de_DE" })
|
||||||
|
if ((locale = newlocale(LC_NUMERIC_MASK, name, (locale_t) 0)) != (locale_t) 0) {
|
||||||
|
uselocale(locale);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Freed once the setters around apply() have put the thread's own locale back.
|
||||||
|
~CommaNumericLocale()
|
||||||
|
{
|
||||||
|
if (locale != (locale_t) 0)
|
||||||
|
freelocale(locale);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("a setter nested in another leaves the C locale in place for the outer one", "[LocalesUtils]")
|
||||||
|
{
|
||||||
|
CNumericLocalesSetter outer;
|
||||||
|
{
|
||||||
|
CNumericLocalesSetter inner;
|
||||||
|
CHECK(is_decimal_separator_point());
|
||||||
|
}
|
||||||
|
CHECK(is_decimal_separator_point());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a setter nested in another sets C again when the locale changed between them", "[LocalesUtils]")
|
||||||
|
{
|
||||||
|
CommaNumericLocale comma;
|
||||||
|
{
|
||||||
|
CNumericLocalesSetter outer;
|
||||||
|
if (! comma.apply())
|
||||||
|
SKIP("no locale with a comma decimal separator is installed");
|
||||||
|
REQUIRE_FALSE(is_decimal_separator_point());
|
||||||
|
{
|
||||||
|
CNumericLocalesSetter inner;
|
||||||
|
CHECK(is_decimal_separator_point());
|
||||||
|
}
|
||||||
|
CHECK_FALSE(is_decimal_separator_point());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#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,6 +5,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
#include "libslic3r/PresetBundle.hpp"
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
|
#include "libslic3r/ParallelResolve.hpp"
|
||||||
#include "libslic3r/AppConfig.hpp"
|
#include "libslic3r/AppConfig.hpp"
|
||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/TriangleMesh.hpp"
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
@@ -17,6 +18,10 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <unistd.h> // geteuid
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -59,6 +64,16 @@ void write_preset_with_inherits(const DynamicPrintConfig &default_config, const
|
|||||||
config.save_to_json(file.string(), name, "User", "1.0.0");
|
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).
|
// 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 = {})
|
Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {})
|
||||||
{
|
{
|
||||||
@@ -261,6 +276,162 @@ TEST_CASE("Selected printer uses its default or saved bed type", "[Preset][Bundl
|
|||||||
CHECK(app_config.get_printer_setting("Test Printer", "curr_bed_type") == std::to_string(static_cast<int>(expected_bed_type)));
|
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]")
|
TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]")
|
||||||
{
|
{
|
||||||
RenameTestCollection coll;
|
RenameTestCollection coll;
|
||||||
@@ -5482,3 +5653,214 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
|
|||||||
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
|
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
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
#include <unistd.h> // getuid
|
#include <unistd.h> // getuid
|
||||||
@@ -62,6 +64,113 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
|
|||||||
#endif
|
#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]") {
|
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
|
||||||
ScopedTemporaryFile source(".txt");
|
ScopedTemporaryFile source(".txt");
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,12 +3,21 @@
|
|||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <boost/crc.hpp>
|
#include <boost/crc.hpp>
|
||||||
#include <cereal/archives/binary.hpp>
|
#include <cereal/archives/binary.hpp>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <numeric>
|
||||||
|
#include <random>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <tbb/blocked_range.h>
|
||||||
|
#include <tbb/parallel_for.h>
|
||||||
|
#include <tbb/task_group.h>
|
||||||
|
|
||||||
#include "libslic3r/PresetBundle.hpp"
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
#include "libslic3r/PresetCacheFormat.hpp"
|
#include "libslic3r/PresetCacheFormat.hpp"
|
||||||
#include "libslic3r/Preset.hpp"
|
#include "libslic3r/Preset.hpp"
|
||||||
@@ -52,6 +61,27 @@ void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std
|
|||||||
<< R"(","from":"system","instantiation":"true","layer_height":"0.2"})";
|
<< 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
|
// 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
|
// base filament with an instantiated child that inherits it and includes a
|
||||||
// dual-extruder template, a second standalone filament carrying explicit
|
// dual-extruder template, a second standalone filament carrying explicit
|
||||||
@@ -128,6 +158,80 @@ void write_lib_tree(const fs::path& dir, const std::string& version, const std::
|
|||||||
<< R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})";
|
<< 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
|
// 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.
|
// 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)
|
void write_vendor_with_lib_filament(const fs::path& dir, const std::string& vendor, const std::string& version)
|
||||||
@@ -624,6 +728,298 @@ 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
|
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]")
|
TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]")
|
||||||
{
|
{
|
||||||
TempDir tmp;
|
TempDir tmp;
|
||||||
@@ -866,6 +1262,27 @@ TEST_CASE("a vendor installed as its cache alone still loads after a library upd
|
|||||||
CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9));
|
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]")
|
TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSONs", "[VendorCache]")
|
||||||
{
|
{
|
||||||
TempDir tmp;
|
TempDir tmp;
|
||||||
@@ -893,6 +1310,12 @@ TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSON
|
|||||||
user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
CHECK(presets_loaded == 1);
|
CHECK(presets_loaded == 1);
|
||||||
CHECK(out.vendors.at("Acme").name == "Acme"); // the profile's name, not the cache's
|
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]")
|
TEST_CASE("a profile with no usable version is never served from cache", "[VendorCache]")
|
||||||
@@ -1380,24 +1803,6 @@ 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)));
|
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]")
|
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
|
// The regression the fingerprint used to prevent by refusing the file
|
||||||
@@ -1691,6 +2096,31 @@ 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.});
|
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]")
|
TEST_CASE("a G-code template that states no instantiation is included, not loaded as a preset", "[VendorCache]")
|
||||||
{
|
{
|
||||||
InstallDirs dirs;
|
InstallDirs dirs;
|
||||||
@@ -1712,3 +2142,35 @@ TEST_CASE("a G-code template that states no instantiation is included, not loade
|
|||||||
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template");
|
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template");
|
||||||
CHECK(presets_for(bundle.printers, "Acme").size() == 1);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_plugin_install.cpp
|
test_plugin_install.cpp
|
||||||
test_plugin_lifecycle.cpp
|
test_plugin_lifecycle.cpp
|
||||||
test_plugin_printer_agent.cpp
|
test_plugin_printer_agent.cpp
|
||||||
test_printhost.cpp
|
|
||||||
test_slicing_pipeline_bindings.cpp
|
test_slicing_pipeline_bindings.cpp
|
||||||
test_slicing_pipeline_config.cpp
|
test_slicing_pipeline_config.cpp
|
||||||
test_plugin_sort.cpp
|
test_plugin_sort.cpp
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
#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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user