mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-02 13:20:59 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbe342c978 |
Vendored
+1
-11
@@ -151,12 +151,6 @@ elseif(APPLE)
|
||||
# the post-install -add_rpath below.
|
||||
set(_python_ldflags "${_python_arch_flags} -Wl,-headerpad_max_install_names")
|
||||
|
||||
# The macOS 27 SDK declares pipe2() and dup3() as available from macOS 27, so
|
||||
# configure finds them and CPython 3.12 calls them without a runtime check.
|
||||
# Below a macOS 27 deployment target they are weak-linked and resolve to NULL
|
||||
# on older systems, where os.pipe() then segfaults -- in `make install`
|
||||
# (compileall, ensurepip) and in the shipped app alike. Every configure below
|
||||
# keeps the pipe()/dup2() fallbacks (python/cpython#153711).
|
||||
if(IS_CROSS_COMPILE)
|
||||
set(_python_build_tgt --build=${_python_build_arch}-apple-darwin --host=${_python_host_arch}-apple-darwin)
|
||||
set(_python_build_arch_flags "-arch ${_python_build_arch_flag} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
@@ -180,8 +174,7 @@ elseif(APPLE)
|
||||
--enable-shared \
|
||||
--without-static-libpython \
|
||||
--disable-test-modules \
|
||||
--build=${_python_build_arch}-apple-darwin \
|
||||
ac_cv_func_pipe2=no ac_cv_func_dup3=no && \
|
||||
--build=${_python_build_arch}-apple-darwin && \
|
||||
make -j${NPROC} python && \
|
||||
cd '<SOURCE_DIR>' && \
|
||||
env \
|
||||
@@ -198,7 +191,6 @@ elseif(APPLE)
|
||||
--without-static-libpython \
|
||||
--with-openssl='${DESTDIR}' \
|
||||
--disable-test-modules \
|
||||
ac_cv_func_pipe2=no ac_cv_func_dup3=no \
|
||||
${_python_build_tgt} \
|
||||
--with-build-python='${_python_build_python}' \
|
||||
py_cv_module__tkinter=n/a"
|
||||
@@ -221,8 +213,6 @@ elseif(APPLE)
|
||||
--with-openssl=${DESTDIR}
|
||||
--disable-test-modules
|
||||
${_python_build_tgt}
|
||||
ac_cv_func_pipe2=no
|
||||
ac_cv_func_dup3=no
|
||||
# Tcl/Tk 9.0 (e.g. from Homebrew) is incompatible with CPython 3.12's
|
||||
# _tkinter; OrcaSlicer's embedded Python does not need tkinter anyway.
|
||||
py_cv_module__tkinter=n/a
|
||||
|
||||
@@ -873,10 +873,10 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
|
||||
|
||||
CNumericLocalesSetter locales_setter;
|
||||
|
||||
std::function<bool(const json&, const char,const char,const bool,std::string&)> parse_str_arr = [&parse_str_arr](const json& arr, const char single_sep,const char array_sep,const bool escape_string_style,std::string& value_str)->bool {
|
||||
std::function<bool(const json::const_iterator&, const char,const char,const bool,std::string&)> parse_str_arr = [&parse_str_arr](const json::const_iterator& it, const char single_sep,const char array_sep,const bool escape_string_style,std::string& value_str)->bool {
|
||||
// must have consistent type name
|
||||
std::string consistent_type;
|
||||
for (auto iter = arr.begin(); iter != arr.end(); ++iter) {
|
||||
for (auto iter = it.value().begin(); iter != it.value().end(); ++iter) {
|
||||
if (consistent_type.empty())
|
||||
consistent_type = iter.value().type_name();
|
||||
else {
|
||||
@@ -886,13 +886,13 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
|
||||
}
|
||||
|
||||
bool first = true;
|
||||
for (auto iter = arr.begin(); iter != arr.end(); iter++) {
|
||||
for (auto iter = it.value().begin(); iter != it.value().end(); iter++) {
|
||||
if (iter.value().is_array()) {
|
||||
if (!first)
|
||||
value_str += array_sep;
|
||||
else
|
||||
first = false;
|
||||
bool success = parse_str_arr(iter.value(), single_sep, array_sep,escape_string_style, value_str);
|
||||
bool success = parse_str_arr(iter, single_sep, array_sep,escape_string_style, value_str);
|
||||
if (!success)
|
||||
return false;
|
||||
}
|
||||
@@ -1038,39 +1038,8 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
|
||||
}
|
||||
}
|
||||
|
||||
// BambuStudio and its forks save a nozzle variant that matches the parent preset as "nil".
|
||||
// An option that can't hold nil gets its default in that slot, and the slot is reported so
|
||||
// the merge onto the parent keeps the parent's value. All slots nil means the key is not set.
|
||||
const json *values = &it.value();
|
||||
json values_with_defaults;
|
||||
if (substitution_context.accept_nil && optdef && !optdef->nullable && optdef->default_value &&
|
||||
(optdef->type == coFloats || optdef->type == coPercents || optdef->type == coFloatsOrPercents ||
|
||||
optdef->type == coInts || optdef->type == coEnums || optdef->type == coBools)) {
|
||||
auto is_nil = [](const json &v) { return v.is_string() && v.get<std::string>() == "nil"; };
|
||||
std::vector<size_t> nil_slots;
|
||||
for (size_t i = 0; i < values->size(); ++i)
|
||||
if (is_nil((*values)[i]))
|
||||
nil_slots.push_back(i);
|
||||
if (!nil_slots.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": " << file << ": " << it.key() << " is nil in "
|
||||
<< nil_slots.size() << " of " << values->size()
|
||||
<< " slots, read as not set (the parent preset's value, or the default)";
|
||||
if (nil_slots.size() == values->size())
|
||||
continue;
|
||||
// create_default_option() gives enums their names, which vserialize() needs.
|
||||
std::unique_ptr<ConfigOption> default_option(optdef->create_default_option());
|
||||
const std::vector<std::string> defaults = static_cast<const ConfigOptionVectorBase*>(default_option.get())->vserialize();
|
||||
values_with_defaults = *values;
|
||||
const json first_value = *std::find_if_not(values->begin(), values->end(), is_nil);
|
||||
for (size_t i : nil_slots)
|
||||
values_with_defaults[i] = defaults.empty() ? first_value : json(defaults[i % defaults.size()]);
|
||||
values = &values_with_defaults;
|
||||
substitution_context.nil_slots[opt_key] = std::move(nil_slots);
|
||||
}
|
||||
}
|
||||
|
||||
// BBS: we only support 2 depth array
|
||||
valid = parse_str_arr(*values, single_sep, array_sep,escape_string_type, value_str);
|
||||
valid = parse_str_arr(it, single_sep, array_sep,escape_string_type, value_str);
|
||||
if (!valid) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << file << " error, invalid json array for " << it.key();
|
||||
break;
|
||||
|
||||
@@ -267,11 +267,6 @@ struct ConfigSubstitutionContext
|
||||
ForwardCompatibilitySubstitutionRule rule;
|
||||
ConfigSubstitutions substitutions;
|
||||
std::vector<std::string> unrecogized_keys;
|
||||
// Read "nil" in an option that can't hold it as not set instead of failing. Set by callers that hand
|
||||
// nil_slots to the merge onto the parent preset, or for which the option default is the right fallback.
|
||||
bool accept_nil = false;
|
||||
// Slots of options that can't hold nil but were "nil" in the file; they hold the option default.
|
||||
std::map<std::string, std::vector<size_t>> nil_slots;
|
||||
};
|
||||
|
||||
// A generic value of a configuration option.
|
||||
|
||||
@@ -809,13 +809,20 @@ void priv::set_skip_for_out_of_aoi(std::vector<bool> &skip_indicies,
|
||||
}); // END parallel for
|
||||
|
||||
// inspect all triangles, when it is out of bounding box
|
||||
// NOTE: std::vector<bool> is bit packed, thus setting its items from multiple threads is a
|
||||
// read-modify-write race on the shared words and silently loses flags. Collect the flags into
|
||||
// a byte per triangle, where the chunks do not share memory, and merge them afterwards.
|
||||
std::vector<unsigned char> skip_triangle(its.indices.size(), 0);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, its.indices.size()),
|
||||
[&its, &is_on_sides, &skip_indicies](const tbb::blocked_range<size_t> &range) {
|
||||
[&its, &is_on_sides, &skip_triangle](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t i = range.begin(); i < range.end(); ++i) {
|
||||
if (is_all_on_one_side(its.indices[i], is_on_sides))
|
||||
skip_indicies[i] = true;
|
||||
skip_triangle[i] = 1;
|
||||
}
|
||||
}); // END parallel for
|
||||
for (size_t i = 0; i < skip_triangle.size(); ++i)
|
||||
if (skip_triangle[i])
|
||||
skip_indicies[i] = true;
|
||||
}
|
||||
|
||||
indexed_triangle_set Slic3r::its_mask(const indexed_triangle_set &its,
|
||||
|
||||
@@ -2696,8 +2696,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
// No parent preset here: a nil slot keeps the option default.
|
||||
config_substitutions.accept_nil = true;
|
||||
int ret = config.load_from_json(dest_file, config_substitutions, true, key_values, reason);
|
||||
if (ret) {
|
||||
add_error("Error load config from json:"+reason);
|
||||
@@ -2738,13 +2736,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(dest_file, Enable);
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
ConfigSubstitutionContext load_context(Enable);
|
||||
load_context.accept_nil = true;
|
||||
if (use_json)
|
||||
config.load_from_json(dest_file, load_context, true, key_values, reason);
|
||||
else
|
||||
load_context.substitutions = config.load_from_ini(dest_file, Enable);
|
||||
ConfigSubstitutions config_substitutions = std::move(load_context.substitutions);
|
||||
ConfigSubstitutions config_substitutions = use_json? config.load_from_json(dest_file, Enable, key_values, reason) : config.load_from_ini(dest_file, Enable);
|
||||
if (!reason.empty()) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", load project embedded config from %1% failed\n") % dest_file;
|
||||
//skip this file
|
||||
@@ -2793,7 +2785,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
preset->is_project_embedded = true;
|
||||
preset->is_external = true;
|
||||
preset->is_dirty = false;
|
||||
preset->nil_slots = std::move(load_context.nil_slots);
|
||||
|
||||
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
||||
boost::optional<Semver> version = Semver::parse(version_str);
|
||||
|
||||
@@ -1378,55 +1378,66 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
return out;
|
||||
};
|
||||
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers, granularity), [&granularity, &num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
|
||||
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
|
||||
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
|
||||
size_t group_idx = range.begin() / granularity;
|
||||
size_t layer_idx_offset = (group_idx & 1) * num_layers;
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
|
||||
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
|
||||
throw_on_cancel_callback();
|
||||
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
|
||||
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
|
||||
if (ExPolygons top_ex = union_ex(top[layer_idx]); ! top_ex.empty()) {
|
||||
// Clean up thin projections. They are not printable anyways.
|
||||
top_ex = opening_ex(top_ex, stat.small_region_threshold);
|
||||
if (! top_ex.empty()) {
|
||||
append(triangles_by_color_top[color_idx][layer_idx + layer_idx_offset], top_ex);
|
||||
float offset = 0.f;
|
||||
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
|
||||
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
//offset -= stat.extrusion_width ;
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
|
||||
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
|
||||
if (last.empty())
|
||||
break;
|
||||
append(shell_triangles_by_color_top[color_idx][last_idx + layer_idx_offset], std::move(last));
|
||||
// The layers are processed in groups of "granularity" layers. A layer projects its shells up to "granularity"
|
||||
// layers away, thus a group may write into the slots of its neighbor groups. The even and the odd groups
|
||||
// therefore write into two disjoint halves of the output vectors (the 2nd half is offset by num_layers) and
|
||||
// both halves are merged below. The group index has to be derived from the layer index and not from the extent
|
||||
// of the TBB sub-range: tbb::blocked_range bisects at midpoints, thus a sub-range neither starts at a multiple
|
||||
// of the grain size nor covers a whole group, and two sub-ranges of one group would append into a single
|
||||
// ExPolygons concurrently. Iterating over the groups keeps every group on a single thread, in ascending order.
|
||||
const size_t num_groups = (num_layers + size_t(granularity) - 1) / size_t(granularity);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_groups, 1), [&granularity, &num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
|
||||
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
|
||||
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t group_idx = range.begin(); group_idx < range.end(); ++ group_idx) {
|
||||
const size_t layer_idx_offset = (group_idx & 1) * num_layers;
|
||||
const size_t layer_idx_begin = group_idx * size_t(granularity);
|
||||
const size_t layer_idx_end = std::min(num_layers, layer_idx_begin + size_t(granularity));
|
||||
for (size_t layer_idx = layer_idx_begin; layer_idx < layer_idx_end; ++ layer_idx) {
|
||||
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
|
||||
throw_on_cancel_callback();
|
||||
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
|
||||
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
|
||||
if (ExPolygons top_ex = union_ex(top[layer_idx]); ! top_ex.empty()) {
|
||||
// Clean up thin projections. They are not printable anyways.
|
||||
top_ex = opening_ex(top_ex, stat.small_region_threshold);
|
||||
if (! top_ex.empty()) {
|
||||
append(triangles_by_color_top[color_idx][layer_idx + layer_idx_offset], top_ex);
|
||||
float offset = 0.f;
|
||||
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
|
||||
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
//offset -= stat.extrusion_width ;
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
|
||||
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
|
||||
if (last.empty())
|
||||
break;
|
||||
append(shell_triangles_by_color_top[color_idx][last_idx + layer_idx_offset], std::move(last));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
|
||||
if (ExPolygons bottom_ex = union_ex(bottom[layer_idx]); ! bottom_ex.empty()) {
|
||||
// Clean up thin projections. They are not printable anyways.
|
||||
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
|
||||
if (! bottom_ex.empty()) {
|
||||
append(triangles_by_color_bottom[color_idx][layer_idx + layer_idx_offset], bottom_ex);
|
||||
float offset = 0.f;
|
||||
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
|
||||
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
//offset -= stat.extrusion_width;
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
|
||||
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
|
||||
if (last.empty())
|
||||
break;
|
||||
append(shell_triangles_by_color_bottom[color_idx][last_idx + layer_idx_offset], std::move(last));
|
||||
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
|
||||
if (ExPolygons bottom_ex = union_ex(bottom[layer_idx]); ! bottom_ex.empty()) {
|
||||
// Clean up thin projections. They are not printable anyways.
|
||||
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
|
||||
if (! bottom_ex.empty()) {
|
||||
append(triangles_by_color_bottom[color_idx][layer_idx + layer_idx_offset], bottom_ex);
|
||||
float offset = 0.f;
|
||||
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
|
||||
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
//offset -= stat.extrusion_width;
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
|
||||
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
|
||||
if (last.empty())
|
||||
break;
|
||||
append(shell_triangles_by_color_bottom[color_idx][last_idx + layer_idx_offset], std::move(last));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1726,11 +1726,9 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
|
||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
ConfigSubstitutionContext load_context(substitution_rule);
|
||||
load_context.accept_nil = true;
|
||||
config.load_from_json(preset.file, load_context, true, key_values, reason);
|
||||
if (! load_context.substitutions.empty())
|
||||
out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(load_context.substitutions) });
|
||||
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());
|
||||
@@ -1766,7 +1764,7 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_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, &load_context.nil_slots);
|
||||
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);
|
||||
@@ -2139,8 +2137,7 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
|
||||
continue;
|
||||
}
|
||||
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2, &preset->nil_slots);
|
||||
preset->nil_slots.clear();
|
||||
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
//preset->config.apply(std::move(config));
|
||||
Preset::normalize(preset->config);
|
||||
// Report configuration fields, which are misplaced into a wrong group.
|
||||
|
||||
@@ -269,8 +269,6 @@ public:
|
||||
//BBS: add type for project-embedded
|
||||
bool is_project_embedded = false;
|
||||
ConfigSubstitutions *loading_substitutions{nullptr};
|
||||
// Slots that were nil in the embedded preset's file, see ConfigSubstitutionContext::nil_slots.
|
||||
std::map<std::string, std::vector<size_t>> nil_slots;
|
||||
bool is_user() const { return ! this->is_default && ! this->is_system && ! this->is_project_embedded && ! this->is_from_bundle(); }
|
||||
bool can_overwrite() const { return ! this->is_default && ! this->is_system && ! this->is_from_bundle(); }
|
||||
//bool is_user() const { return ! this->is_default && ! this->is_system; }
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#include "GCode/Thumbnails.hpp"
|
||||
#include <set>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include <boost/algorithm/string/join.hpp>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
@@ -9187,18 +9185,16 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
} else if (opt_key == "tree_support_wall_count" && value == "-1") {
|
||||
value = "0";
|
||||
} else if (opt_key == "different_settings_to_system") {
|
||||
// Rename whole entries: a substring rename would also hit longer keys that contain the old name.
|
||||
std::vector<std::string> entries;
|
||||
boost::split(entries, value, boost::is_any_of(";"));
|
||||
for (std::string &entry : entries) {
|
||||
std::string key = entry, unused_value;
|
||||
key.erase(std::remove(key.begin(), key.end(), '\"'), key.end());
|
||||
std::string new_key = key;
|
||||
handle_legacy(new_key, unused_value);
|
||||
if (new_key != key)
|
||||
ReplaceString(entry, key, new_key);
|
||||
std::string copy_value = value;
|
||||
copy_value.erase(std::remove(copy_value.begin(), copy_value.end(), '\"'), copy_value.end()); // remove '"' in string
|
||||
std::set<std::string> split_keys = SplitStringAndRemoveDuplicateElement(copy_value, ";");
|
||||
for (std::string split_key : split_keys) {
|
||||
std::string copy_key = split_key, copy_value = "";
|
||||
handle_legacy(copy_key, copy_value);
|
||||
if (copy_key != split_key) {
|
||||
ReplaceString(value, split_key, copy_key);
|
||||
}
|
||||
}
|
||||
value = boost::algorithm::join(entries, ";");
|
||||
} else if (opt_key == "overhang_fan_threshold" && value == "5%") {
|
||||
value = "10%";
|
||||
} else if( opt_key == "wall_infill_order" ) {
|
||||
@@ -9240,13 +9236,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
else if (value == "0"){
|
||||
value = "ensure_moderate";
|
||||
}
|
||||
// BambuStudio's levels
|
||||
else if (value == "enabled")
|
||||
value = "ensure_all";
|
||||
else if (value == "partial")
|
||||
value = "ensure_moderate";
|
||||
else if (value == "disabled")
|
||||
value = "none";
|
||||
} else if (opt_key == "rotate_solid_infill_direction") {
|
||||
opt_key = "solid_infill_rotate_template";
|
||||
if (value == "1") {
|
||||
@@ -9262,11 +9251,9 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
opt_key = "chamber_temperature";
|
||||
} else if (opt_key == "thumbnail_size") {
|
||||
opt_key = "thumbnails";
|
||||
} else if (opt_key == "top_one_wall_type") {
|
||||
// BambuStudio's "topmost" (one wall on the topmost layer only) widens to every top surface.
|
||||
} else if (opt_key == "top_one_wall_type" && value != "none") {
|
||||
opt_key = "only_one_wall_top";
|
||||
if (!value.empty())
|
||||
value = (value == "not apply" || value == "none") ? "0" : "1";
|
||||
value = "1";
|
||||
} else if (opt_key == "initial_layer_flow_ratio") {
|
||||
opt_key = "bottom_solid_infill_flow_ratio";
|
||||
} else if (opt_key == "ironing_direction") {
|
||||
@@ -9284,20 +9271,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
opt_key == "ironing_pattern" ||
|
||||
opt_key == "support_ironing_pattern") && value == "zig-zag") {
|
||||
value = "rectilinear";
|
||||
} else if (opt_key == "sparse_infill_pattern" && value == "2dlattice") {
|
||||
// BambuStudio's name, and OrcaSlicer's before the lateral patterns were renamed
|
||||
value = "lateral-lattice";
|
||||
} else if (opt_key == "sparse_infill_pattern" && value == "2dhoneycomb") {
|
||||
value = "lateral-honeycomb";
|
||||
} else if (opt_key == "sparse_infill_lattice_angle_1" || opt_key == "lattice_angle_1") {
|
||||
opt_key = "lateral_lattice_angle_1";
|
||||
} else if (opt_key == "sparse_infill_lattice_angle_2" || opt_key == "lattice_angle_2") {
|
||||
opt_key = "lateral_lattice_angle_2";
|
||||
} else if (opt_key == "support_style" && value == "tree_organic") {
|
||||
value = "organic";
|
||||
} else if (opt_key == "raft_first_layer_expansion" && !value.empty() && value.front() == '-') {
|
||||
// BambuStudio's auto: 2 mm for normal supports and rafts. Tree branches use tree_support_auto_brim here.
|
||||
value = "2";
|
||||
} else if (opt_key == "filament_map_mode") {
|
||||
if (value == "Auto") value = "Auto For Flush";
|
||||
}
|
||||
@@ -9326,11 +9299,12 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
}
|
||||
// Orca: Rename wipe tower ribs related options
|
||||
else if (opt_key == "prime_tower_rib_wall") {
|
||||
opt_key = "wipe_tower_wall_type";
|
||||
if (value == "1" || value == "true")
|
||||
value = "rib";
|
||||
else if (!value.empty())
|
||||
value = "rectangle";
|
||||
if (value == "1") {
|
||||
opt_key = "wipe_tower_wall_type";
|
||||
value = "rib";
|
||||
} else {
|
||||
opt_key = "";
|
||||
}
|
||||
} else if (opt_key == "prime_tower_extra_rib_length") {
|
||||
opt_key = "wipe_tower_extra_rib_length";
|
||||
} else if (opt_key == "prime_tower_rib_width") {
|
||||
@@ -9342,18 +9316,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
} else if (opt_key == "machine_switch_extruder_time") {
|
||||
opt_key = "machine_tool_change_time";
|
||||
}
|
||||
// BambuStudio names of OrcaSlicer options
|
||||
else if (opt_key == "role_base_wipe_speed") {
|
||||
opt_key = "role_based_wipe_speed";
|
||||
} else if (opt_key == "no_slow_down_for_cooling_on_outwalls") {
|
||||
opt_key = "dont_slow_down_outer_wall";
|
||||
} else if (opt_key == "process_notes") {
|
||||
opt_key = "notes";
|
||||
} else if (opt_key == "prime_tower_max_speed") {
|
||||
opt_key = "wipe_tower_max_purge_speed";
|
||||
} else if (opt_key == "enable_support_ironing") {
|
||||
opt_key = "support_ironing";
|
||||
}
|
||||
else if (opt_key == "wall_direction" && value == "auto") {
|
||||
value = "ccw";
|
||||
}
|
||||
@@ -11596,8 +11558,7 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
|
||||
return;
|
||||
}
|
||||
|
||||
void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig& new_config, std::string extruder_id_name, std::string extruder_variant_name, std::set<std::string>& key_set1, std::set<std::string>& key_set2,
|
||||
const std::map<std::string, std::vector<size_t>>* nil_slots)
|
||||
void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig& new_config, std::string extruder_id_name, std::string extruder_variant_name, std::set<std::string>& key_set1, std::set<std::string>& key_set2)
|
||||
{
|
||||
std::vector<int> cur_extruder_ids, target_extruder_ids, variant_index;
|
||||
std::vector<std::string> cur_extruder_variants, target_extruder_variants;
|
||||
@@ -11658,13 +11619,6 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
|
||||
if (opt_src && opt_target && (*opt_src != *opt_target)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" change key %1% from base_value %2% to child's value %3%")
|
||||
%opt %(opt_src->serialize()) %(opt_target->serialize());
|
||||
const std::vector<size_t> *unset_slots = nullptr;
|
||||
if (nil_slots && opt_src->is_vector())
|
||||
if (auto it = nil_slots->find(opt); it != nil_slots->end())
|
||||
unset_slots = &it->second;
|
||||
std::unique_ptr<ConfigOption> base_value(unset_slots ? opt_src->clone() : nullptr);
|
||||
int stride = 1;
|
||||
bool merged_by_variant = false;
|
||||
if (opt_target->is_scalar()
|
||||
|| ((key_set1.find(opt) == key_set1.end()) && (key_set2.empty() || (key_set2.find(opt) == key_set2.end())))) {
|
||||
//nothing to do, keep the original one
|
||||
@@ -11673,6 +11627,7 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
|
||||
else {
|
||||
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src);
|
||||
const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(opt_target);
|
||||
int stride = 1;
|
||||
if (key_set2.find(opt) != key_set2.end())
|
||||
stride = 2;
|
||||
// set_only_diff() requires the base vector length to equal variant_index.size()*stride, where
|
||||
@@ -11686,26 +11641,8 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
|
||||
if (opt_vec_src->size() != variant_index.size() * size_t(stride)) {
|
||||
opt_src->set(opt_target);
|
||||
}
|
||||
else {
|
||||
else
|
||||
opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride);
|
||||
merged_by_variant = true;
|
||||
}
|
||||
}
|
||||
// A slot that was nil in the child's file is not set there, so it keeps this config's value.
|
||||
if (unset_slots) {
|
||||
auto *merged = static_cast<ConfigOptionVectorBase*>(opt_src);
|
||||
const size_t base_size = static_cast<const ConfigOptionVectorBase*>(base_value.get())->size();
|
||||
for (size_t slot = 0; slot < merged->size() && slot < base_size; ++slot) {
|
||||
size_t child_slot = slot;
|
||||
if (merged_by_variant) {
|
||||
const int child_variant = variant_index[slot / stride];
|
||||
if (child_variant == -1)
|
||||
continue;
|
||||
child_slot = size_t(child_variant) * stride + slot % stride;
|
||||
}
|
||||
if (std::find(unset_slots->begin(), unset_slots->end(), child_slot) != unset_slots->end())
|
||||
merged->set_at(base_value.get(), slot, slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,10 +859,7 @@ public:
|
||||
|
||||
void update_non_diff_values_to_base_config(DynamicPrintConfig& new_config, const t_config_option_keys& keys, const std::set<std::string>& different_keys, std::string extruder_id_name, std::string extruder_variant_name,
|
||||
std::set<std::string>& key_set1, std::set<std::string>& key_set2);
|
||||
// nil_slots: per option, the slots of new_config that were nil in its file (see ConfigSubstitutionContext::nil_slots);
|
||||
// those variants keep this config's value.
|
||||
void update_diff_values_to_child_config(DynamicPrintConfig& new_config, std::string extruder_id_name, std::string extruder_variant_name, std::set<std::string>& key_set1, std::set<std::string>& key_set2,
|
||||
const std::map<std::string, std::vector<size_t>>* nil_slots = nullptr);
|
||||
void update_diff_values_to_child_config(DynamicPrintConfig& new_config, std::string extruder_id_name, std::string extruder_variant_name, std::set<std::string>& key_set1, std::set<std::string>& key_set2);
|
||||
|
||||
int update_values_from_single_to_multi(DynamicPrintConfig& multi_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name);
|
||||
int update_values_from_multi_to_multi(DynamicPrintConfig& new_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name, std::vector<std::string>& extruder_variants);
|
||||
|
||||
@@ -424,8 +424,16 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
|
||||
[this](size_t i, size_t j) { return m_layer_outlines[i].second.size() < m_layer_outlines[j].second.size(); });
|
||||
|
||||
// Layer range for which the collisions will be calculated.
|
||||
// Another thread may have advanced getMaxCalculatedLayer() past max_layer_idx after this calculation
|
||||
// was requested. Bail out in that case, otherwise the layer range would be negative and allocating
|
||||
// it would throw std::length_error out of a parallel task.
|
||||
const LayerIndex start_layer = 1 + m_collision_cache.getMaxCalculatedLayer(radius);
|
||||
if (start_layer > max_layer_idx) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Requested calculation for value already calculated ?";
|
||||
return;
|
||||
}
|
||||
LayerPolygonCache data;
|
||||
data.allocate(m_collision_cache.getMaxCalculatedLayer(radius) + 1, max_layer_idx + 1);
|
||||
data.allocate(start_layer, max_layer_idx + 1);
|
||||
|
||||
const bool calculate_placable = m_support_rests_on_model && radius == 0;
|
||||
LayerPolygonCache data_placeable;
|
||||
@@ -804,6 +812,12 @@ void TreeModelVolumes::calculateWallRestrictions(const std::vector<RadiusLayerPa
|
||||
const coord_t radius = keys[key_idx].first;
|
||||
const LayerIndex max_required_layer = keys[key_idx].second;
|
||||
const coord_t min_layer_bottom = std::max(1, m_wall_restrictions_cache.getMaxCalculatedLayer(radius));
|
||||
if (min_layer_bottom > max_required_layer) {
|
||||
// Another thread has calculated this range in the meantime. Continuing would make
|
||||
// buffer_size negative and allocating it would throw std::length_error.
|
||||
BOOST_LOG_TRIVIAL(debug) << "Requested calculation for value already calculated ?";
|
||||
continue;
|
||||
}
|
||||
const size_t buffer_size = max_required_layer + 1 - min_layer_bottom;
|
||||
std::vector<Polygons> data(buffer_size, Polygons{});
|
||||
std::vector<Polygons> data_min;
|
||||
|
||||
+59
-57
@@ -12,6 +12,7 @@
|
||||
#include <thread>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/task_arena.h>
|
||||
#include <tbb/task_scheduler_observer.h>
|
||||
|
||||
#include "Thread.hpp"
|
||||
#include "Utils.hpp"
|
||||
@@ -212,70 +213,71 @@ bool is_main_thread_active()
|
||||
return get_main_thread_id() == boost::this_thread::get_id();
|
||||
}
|
||||
|
||||
// Spawn (n - 1) worker threads on Intel TBB thread pool and name them by an index and a system thread ID.
|
||||
// Also it sets locale of the worker threads to "C" for the G-code generator to produce "." as a decimal separator.
|
||||
// Name the current TBB worker thread and set its locale to "C", so that the G-code generator
|
||||
// produces "." as a decimal separator. Called once per worker thread, before it runs its first task.
|
||||
static void setup_tbb_worker_thread()
|
||||
{
|
||||
static std::atomic<size_t> s_worker_idx{ 0 };
|
||||
std::ostringstream name;
|
||||
name << "slic3r_tbb_" << (1 + s_worker_idx.fetch_add(1, std::memory_order_relaxed));
|
||||
set_current_thread_name(name.str().c_str());
|
||||
#ifdef _WIN32
|
||||
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
||||
std::setlocale(LC_ALL, "C");
|
||||
#else
|
||||
// We are leaking some memory here, because the newlocale() produced memory will never be released.
|
||||
// This is not a problem though, as there will be a maximum one worker thread created per physical thread.
|
||||
uselocale(newlocale(
|
||||
#ifdef __APPLE__
|
||||
LC_ALL_MASK
|
||||
#else // some Unix / Linux / BSD
|
||||
LC_ALL
|
||||
#endif
|
||||
, "C", nullptr));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Sets up the TBB worker threads of the arena of the thread, which activated the observation.
|
||||
// A worker sets itself up on entry to the arena, before it executes its first task, thus unlike a barrier
|
||||
// inside a parallel_for, this does not depend on TBB running any number of tasks simultaneously.
|
||||
class TBBWorkerThreadSetupObserver : public tbb::task_scheduler_observer
|
||||
{
|
||||
public:
|
||||
TBBWorkerThreadSetupObserver() { this->observe(true); }
|
||||
|
||||
void on_scheduler_entry(bool is_worker) override
|
||||
{
|
||||
// Leave the external threads (the calling / UI thread) alone, their name and locale must not be modified here.
|
||||
if (! is_worker)
|
||||
return;
|
||||
// A worker thread enters an arena many times, while its name and locale have to be set just once.
|
||||
static thread_local bool initialized = false;
|
||||
if (initialized)
|
||||
return;
|
||||
initialized = true;
|
||||
setup_tbb_worker_thread();
|
||||
}
|
||||
};
|
||||
|
||||
// Name the threads of the Intel TBB thread pool by an index and set their locale to "C"
|
||||
// for the G-code generator to produce "." as a decimal separator.
|
||||
// Formerly all the worker threads were caught inside a single parallel_for, which was held on a condition
|
||||
// variable barrier until max_concurrency() of its chunks were running. TBB guarantees no such simultaneity,
|
||||
// thus the barrier was able to block the slicing threads indefinitely. The TBB scheduler observer below
|
||||
// sets each worker up on its own, thus no two chunks have to run at the same time.
|
||||
void name_tbb_thread_pool_threads_set_locale()
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (initialized)
|
||||
return;
|
||||
initialized = true;
|
||||
|
||||
// see GH issue #5661 PrusaSlicer hangs on Linux when run with non standard task affinity
|
||||
// TBB will respect the task affinity mask on Linux and spawn less threads than std::thread::hardware_concurrency().
|
||||
// const size_t nthreads_hw = std::thread::hardware_concurrency();
|
||||
const size_t nthreads_hw = tbb::this_task_arena::max_concurrency();
|
||||
size_t nthreads = nthreads_hw;
|
||||
|
||||
#ifdef SLIC3R_PROFILE
|
||||
// Shiny profiler is not thread safe, thus disable parallelization.
|
||||
disable_multi_threading();
|
||||
nthreads = 1;
|
||||
#endif
|
||||
|
||||
size_t nthreads_running(0);
|
||||
std::condition_variable cv;
|
||||
std::mutex cv_m;
|
||||
auto master_thread_id = std::this_thread::get_id();
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, nthreads, 1),
|
||||
[&nthreads_running, nthreads, &master_thread_id, &cv, &cv_m](const tbb::blocked_range<size_t> &range) {
|
||||
assert(range.begin() + 1 == range.end());
|
||||
if (std::unique_lock<std::mutex> lk(cv_m); ++nthreads_running == nthreads) {
|
||||
lk.unlock();
|
||||
// All threads are spinning.
|
||||
// Wake them up.
|
||||
cv.notify_all();
|
||||
} else {
|
||||
// Wait for the last thread to wake the others.
|
||||
cv.wait(lk, [&nthreads_running, nthreads]{return nthreads_running == nthreads;});
|
||||
}
|
||||
auto thread_id = std::this_thread::get_id();
|
||||
if (thread_id == master_thread_id) {
|
||||
// The calling thread runs the 0'th task.
|
||||
assert(range.begin() == 0);
|
||||
} else {
|
||||
assert(range.begin() > 0);
|
||||
std::ostringstream name;
|
||||
name << "slic3r_tbb_" << range.begin();
|
||||
set_current_thread_name(name.str().c_str());
|
||||
// Set locales of the worker thread to "C".
|
||||
#ifdef _WIN32
|
||||
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
||||
std::setlocale(LC_ALL, "C");
|
||||
#else
|
||||
// We are leaking some memory here, because the newlocale() produced memory will never be released.
|
||||
// This is not a problem though, as there will be a maximum one worker thread created per physical thread.
|
||||
uselocale(newlocale(
|
||||
#ifdef __APPLE__
|
||||
LC_ALL_MASK
|
||||
#else // some Unix / Linux / BSD
|
||||
LC_ALL
|
||||
#endif
|
||||
, "C", nullptr));
|
||||
#endif
|
||||
}
|
||||
});
|
||||
// An observer is local to the arena of the thread which activates it, thus one observer is registered
|
||||
// per calling thread. Being function local and thread local, it is also initialized exactly once per
|
||||
// thread without a race. It is intentionally never destroyed, as it has to stay alive as long as the
|
||||
// TBB scheduler may notify it, which includes the shutdown of the process.
|
||||
static thread_local tbb::task_scheduler_observer *observer = new TBBWorkerThreadSetupObserver();
|
||||
(void)observer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const index
|
||||
area_sum_to_triangle_idx[area_sum] = t_idx;
|
||||
}
|
||||
|
||||
if (area_sum_to_triangle_idx.empty())
|
||||
// No triangle to sample from.
|
||||
return {};
|
||||
|
||||
std::mt19937_64 mersenne_engine { 27644437 };
|
||||
// random numbers on interval [0, 1)
|
||||
std::uniform_real_distribution<double> fdistribution;
|
||||
@@ -50,7 +54,10 @@ TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const index
|
||||
tbb::blocked_range<size_t> r) {
|
||||
for (size_t s_idx = r.begin(); s_idx < r.end(); ++s_idx) {
|
||||
double t_sample = random_samples[s_idx].x() * area_sum;
|
||||
size_t t_idx = area_sum_to_triangle_idx.upper_bound(t_sample)->second;
|
||||
// The keys of area_sum_to_triangle_idx are accumulated areas in double precision, while area_sum
|
||||
// is a float, thus t_sample may reach or exceed the largest key and upper_bound() may return end().
|
||||
auto t_it = area_sum_to_triangle_idx.upper_bound(t_sample);
|
||||
size_t t_idx = (t_it == area_sum_to_triangle_idx.end() ? std::prev(t_it) : t_it)->second;
|
||||
|
||||
double sq_u = std::sqrt(random_samples[s_idx].y());
|
||||
double v = random_samples[s_idx].z();
|
||||
|
||||
@@ -819,9 +819,7 @@ int OrcaCloudServiceAgent::user_logout(bool request)
|
||||
}
|
||||
}
|
||||
|
||||
// An explicit logout also wipes the backend the token storage option is not using, so a token
|
||||
// stranded by switching that option cannot sign the account back in later.
|
||||
clear_session(/*all_backends=*/request);
|
||||
clear_session();
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -1606,9 +1604,7 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
|
||||
}
|
||||
}
|
||||
|
||||
if (stored) {
|
||||
secret_stored = true;
|
||||
}
|
||||
(void) stored;
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
|
||||
@@ -1648,7 +1644,6 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
|
||||
}
|
||||
|
||||
if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) {
|
||||
secret_stored = true;
|
||||
out_secret = plain;
|
||||
// Upgrade legacy payloads to signed format
|
||||
if (payload.rfind("v2:", 0) != 0) {
|
||||
@@ -1666,7 +1661,6 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
|
||||
if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) {
|
||||
out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize());
|
||||
if (!out_secret.empty()) {
|
||||
secret_stored = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1676,20 +1670,11 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
|
||||
return false;
|
||||
}
|
||||
|
||||
void OrcaCloudServiceAgent::clear_user_secret(bool all_backends)
|
||||
void OrcaCloudServiceAgent::clear_user_secret()
|
||||
{
|
||||
// Nothing this process loaded or saved: leave the store alone. Deleting would only cost a
|
||||
// keychain round trip (or a hang while the keychain is unresponsive) and could remove a
|
||||
// login another instance just saved.
|
||||
if (!secret_stored.exchange(false) && !all_backends) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (all_backends || !m_use_encrypted_token_file) {
|
||||
wxSecretStore store = wxSecretStore::GetDefault();
|
||||
if (store.IsOk()) {
|
||||
store.Delete(SECRET_STORE_SERVICE);
|
||||
}
|
||||
wxSecretStore store = wxSecretStore::GetDefault();
|
||||
if (store.IsOk()) {
|
||||
store.Delete(SECRET_STORE_SERVICE);
|
||||
}
|
||||
|
||||
compute_fallback_path();
|
||||
@@ -2038,13 +2023,13 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
|
||||
return success;
|
||||
}
|
||||
|
||||
void OrcaCloudServiceAgent::clear_session(bool all_backends)
|
||||
void OrcaCloudServiceAgent::clear_session()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
session = SessionInfo{};
|
||||
}
|
||||
clear_user_secret(all_backends);
|
||||
clear_user_secret();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -326,7 +326,7 @@ public:
|
||||
|
||||
void persist_user_secret(const std::string& secret);
|
||||
bool load_user_secret(std::string& out_secret);
|
||||
void clear_user_secret(bool all_backends = false);
|
||||
void clear_user_secret();
|
||||
|
||||
// Token refresh helpers
|
||||
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
|
||||
@@ -344,7 +344,7 @@ public:
|
||||
bool persist = true);
|
||||
// Accepts either nested Orca cloud / GoTrue session JSON or flat WebView token JSON.
|
||||
bool set_user_session(const nlohmann::json& session_json, bool notify_login = true);
|
||||
void clear_session(bool all_backends = false);
|
||||
void clear_session();
|
||||
|
||||
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
|
||||
|
||||
@@ -413,11 +413,6 @@ private:
|
||||
// Member variables - auth state
|
||||
PkceBundle pkce_bundle;
|
||||
std::string secret_fallback_path;
|
||||
// Set once this process has read a secret from the store or written one. Unless the user logs
|
||||
// out explicitly, clear_user_secret() only touches the store while it is set, so a logged-out
|
||||
// instance (the GUI polls the login status every 2 s) makes no keychain calls and cannot wipe
|
||||
// a login another instance saved.
|
||||
std::atomic_bool secret_stored{false};
|
||||
SessionHandler session_handler;
|
||||
OnLoginCompleteHandler on_login_complete_handler;
|
||||
SessionInfo session;
|
||||
|
||||
@@ -508,158 +508,6 @@ TEST_CASE("load_from_json hands a preset's include list to the caller instead of
|
||||
CHECK(config.opt_string("machine_end_gcode") == "M84");
|
||||
}
|
||||
|
||||
TEST_CASE("handle_legacy maps BambuStudio keys and values to their OrcaSlicer equivalents", "[Config]") {
|
||||
struct Row { std::string key, value, expected_key, expected_value; };
|
||||
const Row row = GENERATE(values<Row>({
|
||||
{"role_base_wipe_speed", "0", "role_based_wipe_speed", "0"},
|
||||
{"no_slow_down_for_cooling_on_outwalls", "1", "dont_slow_down_outer_wall", "1"},
|
||||
{"process_notes", "note", "notes", "note"},
|
||||
{"prime_tower_max_speed", "70", "wipe_tower_max_purge_speed", "70"},
|
||||
{"enable_support_ironing", "1", "support_ironing", "1"},
|
||||
{"sparse_infill_lattice_angle_1", "-30", "lateral_lattice_angle_1", "-30"},
|
||||
{"sparse_infill_lattice_angle_2", "20", "lateral_lattice_angle_2", "20"},
|
||||
{"lattice_angle_1", "-30", "lateral_lattice_angle_1", "-30"},
|
||||
{"lattice_angle_2", "20", "lateral_lattice_angle_2", "20"},
|
||||
{"sparse_infill_pattern", "2dlattice", "sparse_infill_pattern", "lateral-lattice"},
|
||||
{"sparse_infill_pattern", "2dhoneycomb", "sparse_infill_pattern", "lateral-honeycomb"},
|
||||
{"support_style", "tree_organic", "support_style", "organic"},
|
||||
{"raft_first_layer_expansion", "-1", "raft_first_layer_expansion", "2"},
|
||||
{"raft_first_layer_expansion", "3.5", "raft_first_layer_expansion", "3.5"},
|
||||
{"ensure_vertical_shell_thickness", "enabled", "ensure_vertical_shell_thickness", "ensure_all"},
|
||||
{"ensure_vertical_shell_thickness", "partial", "ensure_vertical_shell_thickness", "ensure_moderate"},
|
||||
{"ensure_vertical_shell_thickness", "disabled", "ensure_vertical_shell_thickness", "none"},
|
||||
{"top_one_wall_type", "not apply", "only_one_wall_top", "0"},
|
||||
{"top_one_wall_type", "all top", "only_one_wall_top", "1"},
|
||||
{"top_one_wall_type", "topmost", "only_one_wall_top", "1"},
|
||||
{"prime_tower_rib_wall", "1", "wipe_tower_wall_type", "rib"},
|
||||
{"prime_tower_rib_wall", "0", "wipe_tower_wall_type", "rectangle"},
|
||||
{"prime_tower_rib_wall", "true", "wipe_tower_wall_type", "rib"},
|
||||
// A rename touches whole entries, not keys that contain the old name.
|
||||
{"different_settings_to_system", "lattice_angle_1;sparse_infill_lattice_angle_1;wall_loops", "different_settings_to_system",
|
||||
"lateral_lattice_angle_1;lateral_lattice_angle_1;wall_loops"},
|
||||
// different_settings_to_system and JSON arrays pass the key alone
|
||||
{"prime_tower_rib_wall", "", "wipe_tower_wall_type", ""},
|
||||
{"top_one_wall_type", "", "only_one_wall_top", ""},
|
||||
{"enable_height_slowdown", "1", "", "1"},
|
||||
{"counter_coef_2", "0.025", "", "0.025"},
|
||||
}));
|
||||
t_config_option_key key = row.key;
|
||||
std::string value = row.value;
|
||||
PrintConfigDef::handle_legacy(key, value);
|
||||
INFO(row.key << " = " << row.value);
|
||||
CHECK(key == row.expected_key);
|
||||
CHECK(value == row.expected_value);
|
||||
}
|
||||
|
||||
TEST_CASE("A BambuStudio project config loads its renamed settings without substitutions", "[Config]") {
|
||||
ScopedTemporaryFile tmp(".json");
|
||||
{
|
||||
boost::nowide::ofstream ofs(tmp.string());
|
||||
ofs << R"({"role_base_wipe_speed":"0","no_slow_down_for_cooling_on_outwalls":["1","0"],)"
|
||||
R"("process_notes":"note","prime_tower_max_speed":"70","enable_support_ironing":"1",)"
|
||||
R"("sparse_infill_pattern":"2dlattice","sparse_infill_lattice_angle_1":"-30",)"
|
||||
R"("support_style":"tree_organic","ensure_vertical_shell_thickness":"partial",)"
|
||||
R"("top_one_wall_type":"not apply","prime_tower_rib_wall":"0","raft_first_layer_expansion":"-1",)"
|
||||
R"("enable_height_slowdown":["1"]})";
|
||||
}
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
ConfigSubstitutionContext substitutions(ForwardCompatibilitySubstitutionRule::Disable);
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
REQUIRE(config.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0);
|
||||
CHECK(substitutions.substitutions.empty());
|
||||
CHECK(substitutions.unrecogized_keys == std::vector<std::string>{"enable_height_slowdown"});
|
||||
|
||||
CHECK_FALSE(config.opt_bool("role_based_wipe_speed"));
|
||||
CHECK(config.option<ConfigOptionBools>("dont_slow_down_outer_wall")->values == std::vector<unsigned char>{1, 0});
|
||||
CHECK(config.opt_string("notes") == "note");
|
||||
CHECK_THAT(config.opt_float("wipe_tower_max_purge_speed"), Catch::Matchers::WithinAbs(70., 1e-9));
|
||||
CHECK(config.opt_bool("support_ironing"));
|
||||
CHECK(config.opt_enum<InfillPattern>("sparse_infill_pattern") == ipLateralLattice);
|
||||
CHECK_THAT(config.opt_float("lateral_lattice_angle_1"), Catch::Matchers::WithinAbs(-30., 1e-9));
|
||||
CHECK(config.opt_enum<SupportMaterialStyle>("support_style") == smsTreeOrganic);
|
||||
CHECK(config.opt_enum<EnsureVerticalShellThickness>("ensure_vertical_shell_thickness") == evstModerate);
|
||||
CHECK_FALSE(config.opt_bool("only_one_wall_top"));
|
||||
CHECK(config.opt_enum<WipeTowerWallType>("wipe_tower_wall_type") == wtwRectangle);
|
||||
CHECK_THAT(config.opt_float("raft_first_layer_expansion"), Catch::Matchers::WithinAbs(2., 1e-9));
|
||||
CHECK(config.validate().count("raft_first_layer_expansion") == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("load_from_json reads a BambuStudio nil slot as not set", "[Config]") {
|
||||
ScopedTemporaryFile tmp(".json");
|
||||
{
|
||||
boost::nowide::ofstream ofs(tmp.string());
|
||||
// Keys after retraction_length in file order must still load.
|
||||
ofs << R"({"layer_height":"0.2","retraction_length":["0.8","nil"],"wall_loops":"3","z_hop":["nil","nil"],)"
|
||||
R"("z_hop_types":["Spiral Lift","nil"]})";
|
||||
}
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
|
||||
SECTION("a loader that doesn't merge onto a parent still rejects nil") {
|
||||
DynamicPrintConfig config;
|
||||
ConfigSubstitutionContext context(ForwardCompatibilitySubstitutionRule::Enable);
|
||||
config.load_from_json(tmp.string(), context, true, key_values, reason);
|
||||
CHECK_FALSE(reason.empty());
|
||||
}
|
||||
|
||||
SECTION("a loader that opts in reads it as not set") {
|
||||
DynamicPrintConfig config;
|
||||
ConfigSubstitutionContext context(ForwardCompatibilitySubstitutionRule::Enable);
|
||||
context.accept_nil = true;
|
||||
REQUIRE(config.load_from_json(tmp.string(), context, true, key_values, reason) == 0);
|
||||
CHECK(reason.empty());
|
||||
|
||||
const auto *default_length = static_cast<const ConfigOptionFloats*>(print_config_def.get("retraction_length")->default_value.get());
|
||||
const auto &retraction_length = config.option<ConfigOptionFloats>("retraction_length")->values;
|
||||
REQUIRE(retraction_length.size() == 2);
|
||||
CHECK_THAT(retraction_length[0], Catch::Matchers::WithinAbs(0.8, 1e-9));
|
||||
CHECK_THAT(retraction_length[1], Catch::Matchers::WithinAbs(default_length->get_at(1), 1e-9));
|
||||
|
||||
std::unique_ptr<ConfigOption> default_types(print_config_def.get("z_hop_types")->create_default_option());
|
||||
const std::vector<std::string> type_defaults = static_cast<const ConfigOptionVectorBase*>(default_types.get())->vserialize();
|
||||
CHECK(static_cast<const ConfigOptionVectorBase*>(config.option("z_hop_types"))->vserialize() ==
|
||||
std::vector<std::string>{"Spiral Lift", type_defaults[1 % type_defaults.size()]});
|
||||
|
||||
CHECK(context.nil_slots == std::map<std::string, std::vector<size_t>>{{"retraction_length", {1}}, {"z_hop_types", {1}}});
|
||||
CHECK_FALSE(config.has("z_hop"));
|
||||
CHECK(config.opt_int("wall_loops") == 3);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A nozzle variant that was nil keeps the parent preset's value", "[Config]") {
|
||||
auto printer = [](std::vector<double> nozzle_diameter, std::vector<double> retraction_length, std::vector<double> max_speed_x) {
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
|
||||
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 1}));
|
||||
// Not a per-variant key, so the merge copies it whole.
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats(nozzle_diameter));
|
||||
config.set_key_value("retraction_length", new ConfigOptionFloats(retraction_length));
|
||||
// Two values per variant: normal and silent mode.
|
||||
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats(max_speed_x));
|
||||
return config;
|
||||
};
|
||||
auto check_values = [](const DynamicPrintConfig &config, const char *key, const std::vector<double> &expected) {
|
||||
const std::vector<double> &values = config.option<ConfigOptionFloats>(key)->values;
|
||||
INFO(key);
|
||||
REQUIRE(values.size() == expected.size());
|
||||
for (size_t i = 0; i < expected.size(); ++i)
|
||||
CHECK_THAT(values[i], Catch::Matchers::WithinAbs(expected[i], 1e-9));
|
||||
};
|
||||
DynamicPrintConfig parent = printer({0.4, 0.6}, {0.6, 0.5}, {500, 200, 400, 100});
|
||||
// The nil slots of the child hold the option default after loading.
|
||||
DynamicPrintConfig child = printer({0.2, 0.4}, {0.8, 0.4}, {500, 200, 300, 90});
|
||||
const std::map<std::string, std::vector<size_t>> nil_slots{{"nozzle_diameter", {1}}, {"retraction_length", {1}}, {"machine_max_speed_x", {3}}};
|
||||
|
||||
parent.update_diff_values_to_child_config(child, "printer_extruder_id", "printer_extruder_variant",
|
||||
printer_options_with_variant_1, printer_options_with_variant_2, &nil_slots);
|
||||
|
||||
check_values(parent, "nozzle_diameter", {0.2, 0.6});
|
||||
check_values(parent, "retraction_length", {0.8, 0.5});
|
||||
// Only the silent-mode value of the second variant was nil.
|
||||
check_values(parent, "machine_max_speed_x", {500, 200, 300, 100});
|
||||
}
|
||||
|
||||
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
|
||||
|
||||
@@ -13,7 +13,6 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_prebuild_queue.cpp
|
||||
test_staged_build.cpp
|
||||
test_network_versions.cpp
|
||||
test_orca_cloud_agent.cpp
|
||||
test_action_source.cpp
|
||||
test_plugin_host_api.cpp
|
||||
# Exercise seam enums and predicates through the embedded Python host API.
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <memory.h>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <pybind11/embed.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
@@ -26,15 +25,8 @@ void ensure_python_initialized()
|
||||
config.parse_argv = 0;
|
||||
|
||||
const auto python_home = boost::dll::program_location().parent_path() / "python";
|
||||
#ifdef _WIN32
|
||||
const auto stdlib = python_home / "Lib";
|
||||
#else
|
||||
const auto stdlib = python_home / "lib" /
|
||||
("python" + std::to_string(PY_MAJOR_VERSION) + "." + std::to_string(PY_MINOR_VERSION));
|
||||
#endif
|
||||
|
||||
// Only a real runtime: a stray python/ folder (packages a test left behind) is not a home.
|
||||
if (boost::filesystem::exists(stdlib / "encodings")) {
|
||||
if (boost::filesystem::exists(python_home)) {
|
||||
const std::string home = python_home.string();
|
||||
const PyStatus status = PyConfig_SetBytesString(&config, &config.home, home.c_str());
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "slic3r/Utils/OrcaCloudServiceAgent.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
// The encrypted token file is the one secret backend a test can observe without a system
|
||||
// keychain. Every agent pointed at the same directory shares it, like separate app instances
|
||||
// share the keychain entry.
|
||||
std::unique_ptr<OrcaCloudServiceAgent> make_file_backed_agent(const fs::path& dir)
|
||||
{
|
||||
auto agent = std::make_unique<OrcaCloudServiceAgent>(dir.string());
|
||||
agent->set_use_encrypted_token_file(true);
|
||||
agent->set_config_dir(dir.string());
|
||||
return agent;
|
||||
}
|
||||
|
||||
fs::path secret_file(const fs::path& dir) { return dir / secret_constants::USER_SECRET_FILENAME; }
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Logging out removes the secret this instance saved", "[OrcaCloudServiceAgent]")
|
||||
{
|
||||
ScopedTemporaryDir dir("orca-secret");
|
||||
auto agent = make_file_backed_agent(dir.path());
|
||||
|
||||
agent->persist_user_secret("refresh-token");
|
||||
REQUIRE(fs::exists(secret_file(dir.path())));
|
||||
|
||||
agent->user_logout(false);
|
||||
CHECK_FALSE(fs::exists(secret_file(dir.path())));
|
||||
}
|
||||
|
||||
TEST_CASE("Logging out removes a secret this instance loaded from the store", "[OrcaCloudServiceAgent]")
|
||||
{
|
||||
ScopedTemporaryDir dir("orca-secret");
|
||||
make_file_backed_agent(dir.path())->persist_user_secret("refresh-token");
|
||||
|
||||
auto agent = make_file_backed_agent(dir.path());
|
||||
std::string secret;
|
||||
REQUIRE(agent->load_user_secret(secret));
|
||||
CHECK(secret == "refresh-token");
|
||||
|
||||
agent->user_logout(false);
|
||||
CHECK_FALSE(fs::exists(secret_file(dir.path())));
|
||||
}
|
||||
|
||||
TEST_CASE("Logging out leaves a secret this instance never loaded or saved alone", "[OrcaCloudServiceAgent]")
|
||||
{
|
||||
ScopedTemporaryDir dir("orca-secret");
|
||||
make_file_backed_agent(dir.path())->persist_user_secret("refresh-token");
|
||||
|
||||
// A logged-out instance is asked to log out on every login-status poll.
|
||||
auto other = make_file_backed_agent(dir.path());
|
||||
other->user_logout(false);
|
||||
other->user_logout(false);
|
||||
CHECK(fs::exists(secret_file(dir.path())));
|
||||
|
||||
std::string secret;
|
||||
REQUIRE(make_file_backed_agent(dir.path())->load_user_secret(secret));
|
||||
CHECK(secret == "refresh-token");
|
||||
}
|
||||
|
||||
TEST_CASE("Logging out leaves a secret this instance could not read alone", "[OrcaCloudServiceAgent]")
|
||||
{
|
||||
ScopedTemporaryDir dir("orca-secret");
|
||||
// Written under another encryption key, e.g. by another OS user sharing the data directory.
|
||||
fs::ofstream(secret_file(dir.path())) << "v2:0000:not-a-payload-this-user-can-decrypt";
|
||||
|
||||
auto agent = make_file_backed_agent(dir.path());
|
||||
std::string secret;
|
||||
REQUIRE_FALSE(agent->load_user_secret(secret));
|
||||
|
||||
agent->user_logout(false);
|
||||
CHECK(fs::exists(secret_file(dir.path())));
|
||||
}
|
||||
@@ -38,9 +38,6 @@ namespace {
|
||||
// before this destructor's shutdown() runs.
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
|
||||
// which would otherwise land in the working directory.
|
||||
ScopedDataDir python_data_dir{"plugin-python"};
|
||||
bool initialized = PluginManager::instance().initialize();
|
||||
|
||||
~ScopedPluginManager()
|
||||
|
||||
@@ -31,9 +31,6 @@ namespace {
|
||||
// same as any other plugin.
|
||||
struct ScopedManagerShutdown
|
||||
{
|
||||
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
|
||||
// which would otherwise land in the working directory.
|
||||
ScopedDataDir python_data_dir{"plugin-python"};
|
||||
bool initialized = PluginManager::instance().initialize();
|
||||
|
||||
~ScopedManagerShutdown()
|
||||
|
||||
@@ -42,9 +42,6 @@ namespace {
|
||||
// Declare this FIRST in a test so it is destroyed last.
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
|
||||
// which would otherwise land in the working directory.
|
||||
ScopedDataDir python_data_dir{"plugin-python"};
|
||||
bool initialized = false;
|
||||
|
||||
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "plugin_test_utils.hpp"
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace Slic3r;
|
||||
|
||||
@@ -23,9 +21,6 @@ namespace {
|
||||
// into Python unless PythonInterpreter::instance() reports initialized.
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
|
||||
// which would otherwise land in the working directory.
|
||||
ScopedDataDir python_data_dir{"plugin-python"};
|
||||
bool initialized = PluginManager::instance().initialize();
|
||||
|
||||
~ScopedPluginManager()
|
||||
|
||||
@@ -35,9 +35,6 @@ namespace {
|
||||
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log,
|
||||
// which would otherwise land in the working directory.
|
||||
ScopedDataDir python_data_dir{"plugin-python"};
|
||||
bool initialized = false;
|
||||
|
||||
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }
|
||||
|
||||
Reference in New Issue
Block a user