Compare commits

..
Author SHA1 Message Date
Hanif Koh 5ecb596b1c Read BambuStudio Nil Slots as Not Set Instead of Aborting the Load
BambuStudio and its forks save a nozzle variant that matches the parent
preset as "nil", including in keys OrcaSlicer can't leave empty, such as
retraction, z-hop and nozzle temperature. Reading one threw, which ended
the rest of the settings file: a project kept only the keys before it
alphabetically, and an embedded or user preset was dropped.

load_from_json now reads such a slot as not set when the caller opts in
(project settings, embedded presets, user presets). If every slot is nil,
the key is left out. Otherwise the slot holds the option default and is
recorded, and update_diff_values_to_child_config gives it the parent
preset's value. Other loaders still reject nil.
2026-10-02 18:00:54 +08:00
Hanif Koh 0bc8912667 Map BambuStudio Keys and Values to Their OrcaSlicer Equivalents
BambuStudio projects lost or misread several settings on import:

- Rename role_base_wipe_speed, no_slow_down_for_cooling_on_outwalls,
  process_notes, prime_tower_max_speed, enable_support_ironing and
  sparse_infill_lattice_angle_1/2 to their OrcaSlicer names.
- Map the values 2dlattice, 2dhoneycomb and tree_organic.
- Map BambuStudio's ensure_vertical_shell_thickness levels, and read
  top_one_wall_type = not apply as off.
- Keep prime_tower_rib_wall = 0 as a rectangle wall instead of dropping
  the key, which fell back to a rib wall.
- Read a negative raft_first_layer_expansion (BambuStudio's auto) as
  2 mm, which also stops it failing the range check.
- Rename whole entries of different_settings_to_system, so a rename no
  longer rewrites longer keys that contain the old name.
2026-10-02 17:59:51 +08:00
SoftFever 3384daa6bc Fix bundled Python crashing on macOS 26 and older when built with Xcode 27 (#16035)
# Description

With Xcode 27, building deps on macOS 26 fails at the Python install
step with a segfault, and a libpython built with Xcode 27 crashes on
macOS 12–26 the first time anything calls `os.pipe()`, which every
plugin `subprocess` call does. The macOS 27 SDK declares `pipe2()` and
`dup3()` as macOS 27-only, and CPython 3.12 calls them without a runtime
check once configure finds them, so on older systems they resolve to
NULL. This keeps CPython on the `pipe()`/`dup2()` fallbacks it already
uses with older SDKs; upstream fixed it in 3.13+
([python/cpython#153711](https://github.com/python/cpython/issues/153711))
but not in 3.12.

No change for builds with Xcode 26 or older, or on Linux and Windows.

# Screenshots/Recordings/Graphs

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

## Tests

Rebuilt deps with Xcode 27 on macOS 26.6: the Python install step now
completes, the installed libpython no longer imports `pipe2`/`dup3`, and
CPython's `test_os`, `test_subprocess` and `test_posix` pass, apart from
one test that needs `_testcapi`, which `--disable-test-modules` leaves
out. Running CPython's configure against the macOS 26.5 and 27.0 SDKs
gives a byte-identical `pyconfig.h` for 26.5 with and without this
change, and for 27.0 with it. The x86_64 cross-build path was configured
on arm64 to confirm both of its configure runs pick up the change.

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

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-01 14:57:30 +08:00
HanifKoh 6842d9c778 Keep the Plugin Tests' Python Packages Out of the Working Directory (#15981)
The plugin test fixtures start the interpreter before the test points
data_dir at its temporary directory, so PythonInterpreter creates
{data_dir}/python/packages and {data_dir}/log with an empty data_dir: a
python/ and log/ folder in whatever directory the tests run from. When that
is the test binary's folder, the next run's embedded-interpreter tests took
the stray python/ as their home and failed to start Python.

Give each fixture that initializes the plugin manager its own temporary
data directory, set up before initialize(), and only use the python/ folder
next to the test binary as the interpreter's home when it holds a standard
library.
2026-10-01 14:42:20 +08:00
HanifKoh 1a5bc8982d Stop Logged-Out Login Polling from Hitting the System Keychain (#15979)
With stealth mode off the home page asks for the login status every 2 s,
and while nobody is logged in that ends in clear_user_secret(), which
opened the system keychain and deleted the OrcaSlicer/Auth entry on the
UI thread every tick. A working keychain cost a D-Bus round trip per tick;
a keychain that never answers blocked the UI for 25 s per tick. It also
deleted a login another running instance had just saved, and ignored
use_encrypted_token_file, so opting out of the keychain did not help.

Remember whether this process read a secret from the store or wrote one,
and only then touch the store when a logged-out poll asks for a logout.
A logged-out instance never touches the keychain, and in encrypted-file
mode the poll no longer opens the keychain at all. A secret this process
cannot read, such as a token file encrypted for another OS user sharing
the data directory, is left alone by the poll. An explicit logout still
wipes both backends, so a token stranded by switching the token storage
option cannot sign the account back in later.
2026-10-01 14:41:50 +08:00
SoftFever 2a9cb32c1f Fix bundled Python crashing on macOS 26 and older when built with Xcode 27
Building deps with Xcode 27 on macOS 26 failed at the Python install step,
and a libpython built with Xcode 27 segfaulted on macOS 12-26 whenever a
plugin started a subprocess.
2026-10-01 12:29:57 +08:00
29 changed files with 484 additions and 162 deletions
+11 -1
View File
@@ -151,6 +151,12 @@ elseif(APPLE)
# the post-install -add_rpath below. # the post-install -add_rpath below.
set(_python_ldflags "${_python_arch_flags} -Wl,-headerpad_max_install_names") 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) if(IS_CROSS_COMPILE)
set(_python_build_tgt --build=${_python_build_arch}-apple-darwin --host=${_python_host_arch}-apple-darwin) 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}") set(_python_build_arch_flags "-arch ${_python_build_arch_flag} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}")
@@ -174,7 +180,8 @@ elseif(APPLE)
--enable-shared \ --enable-shared \
--without-static-libpython \ --without-static-libpython \
--disable-test-modules \ --disable-test-modules \
--build=${_python_build_arch}-apple-darwin && \ --build=${_python_build_arch}-apple-darwin \
ac_cv_func_pipe2=no ac_cv_func_dup3=no && \
make -j${NPROC} python && \ make -j${NPROC} python && \
cd '<SOURCE_DIR>' && \ cd '<SOURCE_DIR>' && \
env \ env \
@@ -191,6 +198,7 @@ elseif(APPLE)
--without-static-libpython \ --without-static-libpython \
--with-openssl='${DESTDIR}' \ --with-openssl='${DESTDIR}' \
--disable-test-modules \ --disable-test-modules \
ac_cv_func_pipe2=no ac_cv_func_dup3=no \
${_python_build_tgt} \ ${_python_build_tgt} \
--with-build-python='${_python_build_python}' \ --with-build-python='${_python_build_python}' \
py_cv_module__tkinter=n/a" py_cv_module__tkinter=n/a"
@@ -213,6 +221,8 @@ elseif(APPLE)
--with-openssl=${DESTDIR} --with-openssl=${DESTDIR}
--disable-test-modules --disable-test-modules
${_python_build_tgt} ${_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 # 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. # _tkinter; OrcaSlicer's embedded Python does not need tkinter anyway.
py_cv_module__tkinter=n/a py_cv_module__tkinter=n/a
+36 -5
View File
@@ -873,10 +873,10 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
CNumericLocalesSetter locales_setter; CNumericLocalesSetter locales_setter;
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 { 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 {
// must have consistent type name // must have consistent type name
std::string consistent_type; std::string consistent_type;
for (auto iter = it.value().begin(); iter != it.value().end(); ++iter) { for (auto iter = arr.begin(); iter != arr.end(); ++iter) {
if (consistent_type.empty()) if (consistent_type.empty())
consistent_type = iter.value().type_name(); consistent_type = iter.value().type_name();
else { else {
@@ -886,13 +886,13 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
} }
bool first = true; bool first = true;
for (auto iter = it.value().begin(); iter != it.value().end(); iter++) { for (auto iter = arr.begin(); iter != arr.end(); iter++) {
if (iter.value().is_array()) { if (iter.value().is_array()) {
if (!first) if (!first)
value_str += array_sep; value_str += array_sep;
else else
first = false; first = false;
bool success = parse_str_arr(iter, single_sep, array_sep,escape_string_style, value_str); bool success = parse_str_arr(iter.value(), single_sep, array_sep,escape_string_style, value_str);
if (!success) if (!success)
return false; return false;
} }
@@ -1038,8 +1038,39 @@ 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 // BBS: we only support 2 depth array
valid = parse_str_arr(it, single_sep, array_sep,escape_string_type, value_str); valid = parse_str_arr(*values, single_sep, array_sep,escape_string_type, value_str);
if (!valid) { if (!valid) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << file << " error, invalid json array for " << it.key(); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << file << " error, invalid json array for " << it.key();
break; break;
+5
View File
@@ -267,6 +267,11 @@ struct ConfigSubstitutionContext
ForwardCompatibilitySubstitutionRule rule; ForwardCompatibilitySubstitutionRule rule;
ConfigSubstitutions substitutions; ConfigSubstitutions substitutions;
std::vector<std::string> unrecogized_keys; 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. // A generic value of a configuration option.
+10 -1
View File
@@ -2696,6 +2696,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
} }
std::map<std::string, std::string> key_values; std::map<std::string, std::string> key_values;
std::string reason; 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); int ret = config.load_from_json(dest_file, config_substitutions, true, key_values, reason);
if (ret) { if (ret) {
add_error("Error load config from json:"+reason); add_error("Error load config from json:"+reason);
@@ -2736,7 +2738,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//ConfigSubstitutions config_substitutions = config.load_from_ini(dest_file, Enable); //ConfigSubstitutions config_substitutions = config.load_from_ini(dest_file, Enable);
std::map<std::string, std::string> key_values; std::map<std::string, std::string> key_values;
std::string reason; std::string reason;
ConfigSubstitutions config_substitutions = use_json? config.load_from_json(dest_file, Enable, key_values, reason) : config.load_from_ini(dest_file, Enable); 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);
if (!reason.empty()) { if (!reason.empty()) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", load project embedded config from %1% failed\n") % dest_file; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", load project embedded config from %1% failed\n") % dest_file;
//skip this file //skip this file
@@ -2785,6 +2793,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
preset->is_project_embedded = true; preset->is_project_embedded = true;
preset->is_external = true; preset->is_external = true;
preset->is_dirty = false; preset->is_dirty = false;
preset->nil_slots = std::move(load_context.nil_slots);
std::string version_str = key_values[BBL_JSON_KEY_VERSION]; std::string version_str = key_values[BBL_JSON_KEY_VERSION];
boost::optional<Semver> version = Semver::parse(version_str); boost::optional<Semver> version = Semver::parse(version_str);
+8 -5
View File
@@ -1726,9 +1726,11 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); //ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
std::map<std::string, std::string> key_values; std::map<std::string, std::string> key_values;
std::string reason; std::string reason;
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason); ConfigSubstitutionContext load_context(substitution_rule);
if (! config_substitutions.empty()) load_context.accept_nil = true;
out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) }); 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) });
if (!reason.empty()) { if (!reason.empty()) {
out.discard_file = true; out.discard_file = true;
out.errors.push_back((boost::format("parse config %1% failed") % preset.file).str()); out.errors.push_back((boost::format("parse config %1% failed") % preset.file).str());
@@ -1764,7 +1766,7 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
preset.config = inherit_preset->config; preset.config = inherit_preset->config;
preset.filament_id = inherit_preset->filament_id; preset.filament_id = inherit_preset->filament_id;
extend_default_config_length(config, false, {}); 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); preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2, &load_context.nil_slots);
} }
else { else {
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config); auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
@@ -2137,7 +2139,8 @@ 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; BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue; continue;
} }
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); 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.apply(std::move(config)); //preset->config.apply(std::move(config));
Preset::normalize(preset->config); Preset::normalize(preset->config);
// Report configuration fields, which are misplaced into a wrong group. // Report configuration fields, which are misplaced into a wrong group.
+2
View File
@@ -269,6 +269,8 @@ public:
//BBS: add type for project-embedded //BBS: add type for project-embedded
bool is_project_embedded = false; bool is_project_embedded = false;
ConfigSubstitutions *loading_substitutions{nullptr}; 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 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 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; } //bool is_user() const { return ! this->is_default && ! this->is_system; }
+83 -20
View File
@@ -10,6 +10,8 @@
#include "GCode/Thumbnails.hpp" #include "GCode/Thumbnails.hpp"
#include <set> #include <set>
#include <boost/algorithm/string/case_conv.hpp> #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/replace.hpp>
#include <boost/algorithm/string/split.hpp> #include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/trim.hpp> #include <boost/algorithm/string/trim.hpp>
@@ -9185,16 +9187,18 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
} else if (opt_key == "tree_support_wall_count" && value == "-1") { } else if (opt_key == "tree_support_wall_count" && value == "-1") {
value = "0"; value = "0";
} else if (opt_key == "different_settings_to_system") { } else if (opt_key == "different_settings_to_system") {
std::string copy_value = value; // Rename whole entries: a substring rename would also hit longer keys that contain the old name.
copy_value.erase(std::remove(copy_value.begin(), copy_value.end(), '\"'), copy_value.end()); // remove '"' in string std::vector<std::string> entries;
std::set<std::string> split_keys = SplitStringAndRemoveDuplicateElement(copy_value, ";"); boost::split(entries, value, boost::is_any_of(";"));
for (std::string split_key : split_keys) { for (std::string &entry : entries) {
std::string copy_key = split_key, copy_value = ""; std::string key = entry, unused_value;
handle_legacy(copy_key, copy_value); key.erase(std::remove(key.begin(), key.end(), '\"'), key.end());
if (copy_key != split_key) { std::string new_key = key;
ReplaceString(value, split_key, copy_key); handle_legacy(new_key, unused_value);
} if (new_key != key)
ReplaceString(entry, key, new_key);
} }
value = boost::algorithm::join(entries, ";");
} else if (opt_key == "overhang_fan_threshold" && value == "5%") { } else if (opt_key == "overhang_fan_threshold" && value == "5%") {
value = "10%"; value = "10%";
} else if( opt_key == "wall_infill_order" ) { } else if( opt_key == "wall_infill_order" ) {
@@ -9236,6 +9240,13 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
else if (value == "0"){ else if (value == "0"){
value = "ensure_moderate"; 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") { } else if (opt_key == "rotate_solid_infill_direction") {
opt_key = "solid_infill_rotate_template"; opt_key = "solid_infill_rotate_template";
if (value == "1") { if (value == "1") {
@@ -9251,9 +9262,11 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key = "chamber_temperature"; opt_key = "chamber_temperature";
} else if (opt_key == "thumbnail_size") { } else if (opt_key == "thumbnail_size") {
opt_key = "thumbnails"; opt_key = "thumbnails";
} else if (opt_key == "top_one_wall_type" && value != "none") { } else if (opt_key == "top_one_wall_type") {
// BambuStudio's "topmost" (one wall on the topmost layer only) widens to every top surface.
opt_key = "only_one_wall_top"; opt_key = "only_one_wall_top";
value = "1"; if (!value.empty())
value = (value == "not apply" || value == "none") ? "0" : "1";
} else if (opt_key == "initial_layer_flow_ratio") { } else if (opt_key == "initial_layer_flow_ratio") {
opt_key = "bottom_solid_infill_flow_ratio"; opt_key = "bottom_solid_infill_flow_ratio";
} else if (opt_key == "ironing_direction") { } else if (opt_key == "ironing_direction") {
@@ -9271,6 +9284,20 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key == "ironing_pattern" || opt_key == "ironing_pattern" ||
opt_key == "support_ironing_pattern") && value == "zig-zag") { opt_key == "support_ironing_pattern") && value == "zig-zag") {
value = "rectilinear"; 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") { } else if (opt_key == "filament_map_mode") {
if (value == "Auto") value = "Auto For Flush"; if (value == "Auto") value = "Auto For Flush";
} }
@@ -9299,12 +9326,11 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
} }
// Orca: Rename wipe tower ribs related options // Orca: Rename wipe tower ribs related options
else if (opt_key == "prime_tower_rib_wall") { else if (opt_key == "prime_tower_rib_wall") {
if (value == "1") { opt_key = "wipe_tower_wall_type";
opt_key = "wipe_tower_wall_type"; if (value == "1" || value == "true")
value = "rib"; value = "rib";
} else { else if (!value.empty())
opt_key = ""; value = "rectangle";
}
} else if (opt_key == "prime_tower_extra_rib_length") { } else if (opt_key == "prime_tower_extra_rib_length") {
opt_key = "wipe_tower_extra_rib_length"; opt_key = "wipe_tower_extra_rib_length";
} else if (opt_key == "prime_tower_rib_width") { } else if (opt_key == "prime_tower_rib_width") {
@@ -9316,6 +9342,18 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
} else if (opt_key == "machine_switch_extruder_time") { } else if (opt_key == "machine_switch_extruder_time") {
opt_key = "machine_tool_change_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") { else if (opt_key == "wall_direction" && value == "auto") {
value = "ccw"; value = "ccw";
} }
@@ -11558,7 +11596,8 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
return; 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) 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)
{ {
std::vector<int> cur_extruder_ids, target_extruder_ids, variant_index; std::vector<int> cur_extruder_ids, target_extruder_ids, variant_index;
std::vector<std::string> cur_extruder_variants, target_extruder_variants; std::vector<std::string> cur_extruder_variants, target_extruder_variants;
@@ -11619,6 +11658,13 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
if (opt_src && opt_target && (*opt_src != *opt_target)) { 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%") 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()); %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() if (opt_target->is_scalar()
|| ((key_set1.find(opt) == key_set1.end()) && (key_set2.empty() || (key_set2.find(opt) == key_set2.end())))) { || ((key_set1.find(opt) == key_set1.end()) && (key_set2.empty() || (key_set2.find(opt) == key_set2.end())))) {
//nothing to do, keep the original one //nothing to do, keep the original one
@@ -11627,7 +11673,6 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
else { else {
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src); ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src);
const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(opt_target); const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(opt_target);
int stride = 1;
if (key_set2.find(opt) != key_set2.end()) if (key_set2.find(opt) != key_set2.end())
stride = 2; stride = 2;
// set_only_diff() requires the base vector length to equal variant_index.size()*stride, where // set_only_diff() requires the base vector length to equal variant_index.size()*stride, where
@@ -11641,8 +11686,26 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
if (opt_vec_src->size() != variant_index.size() * size_t(stride)) { if (opt_vec_src->size() != variant_index.size() * size_t(stride)) {
opt_src->set(opt_target); opt_src->set(opt_target);
} }
else else {
opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride); 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);
}
} }
} }
} }
+4 -1
View File
@@ -859,7 +859,10 @@ 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, 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); std::set<std::string>& key_set1, std::set<std::string>& key_set2);
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); // 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);
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_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); 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);
-2
View File
@@ -55,8 +55,6 @@ public:
static bool parse_color(const std::string& scolor, unsigned char* rgb_out); static bool parse_color(const std::string& scolor, unsigned char* rgb_out);
static bool parse_color4(const std::string& scolor, unsigned char* rgba_out); static bool parse_color4(const std::string& scolor, unsigned char* rgba_out);
// Rasterizes the SVG into a freshly generated GL texture; the caller owns it and has to delete
// it (ImGuiWrapper::svg_texture() caches the result for the whole session).
static bool load_from_svg_file_change_color(const std::string &filename, unsigned width, unsigned height, ImTextureID &texture_id, const char *hexColor); static bool load_from_svg_file_change_color(const std::string &filename, unsigned width, unsigned height, ImTextureID &texture_id, const char *hexColor);
+11 -28
View File
@@ -3245,8 +3245,7 @@ void GLCanvas3D::load_sla_preview()
void GLCanvas3D::bind_event_handlers() void GLCanvas3D::bind_event_handlers()
{ {
// Every view switch binds, so binding twice would run each handler twice per event. if (m_canvas != nullptr) {
if (m_canvas != nullptr && !m_event_handlers_bound) {
m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this); m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this); m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this); m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
@@ -3256,9 +3255,9 @@ void GLCanvas3D::bind_event_handlers()
m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this); m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
m_canvas->Bind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this); m_canvas->Bind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this);
m_toolbar_highlighter.set_timer_owner(m_canvas, 0); m_toolbar_highlighter.set_timer_owner(m_canvas, 0);
m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, &GLCanvas3D::on_toolbar_highlighter_timer, this); m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_toolbar_highlighter.blink(); });
m_gizmo_highlighter.set_timer_owner(m_canvas, 0); m_gizmo_highlighter.set_timer_owner(m_canvas, 0);
m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, &GLCanvas3D::on_gizmo_highlighter_timer, this); m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_gizmo_highlighter.blink(); });
m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
@@ -3273,7 +3272,14 @@ void GLCanvas3D::bind_event_handlers()
m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this); m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this); m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
m_canvas->Bind(wxEVT_KILL_FOCUS, &GLCanvas3D::on_kill_focus, this); m_canvas->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& evt) {
// The key-up that would commit a keyboard edit goes to whatever took the focus.
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
render();
evt.Skip();
});
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);
@@ -3311,9 +3317,6 @@ void GLCanvas3D::unbind_event_handlers()
m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this); m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
m_canvas->Unbind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this); m_canvas->Unbind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
m_canvas->Unbind(wxEVT_KILL_FOCUS, &GLCanvas3D::on_kill_focus, this);
m_canvas->Unbind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, &GLCanvas3D::on_toolbar_highlighter_timer, this);
m_canvas->Unbind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, &GLCanvas3D::on_gizmo_highlighter_timer, this);
m_event_handlers_bound = false; m_event_handlers_bound = false;
m_canvas->Unbind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this); m_canvas->Unbind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
@@ -4890,26 +4893,6 @@ void GLCanvas3D::on_set_focus(wxFocusEvent& evt)
m_is_touchpad_navigation = wxGetApp().app_config->get_bool("camera_navigation_style"); m_is_touchpad_navigation = wxGetApp().app_config->get_bool("camera_navigation_style");
} }
void GLCanvas3D::on_kill_focus(wxFocusEvent& evt)
{
// The key-up that would commit a keyboard edit goes to whatever took the focus.
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
render();
evt.Skip();
}
void GLCanvas3D::on_toolbar_highlighter_timer(wxTimerEvent& evt)
{
m_toolbar_highlighter.blink();
}
void GLCanvas3D::on_gizmo_highlighter_timer(wxTimerEvent& evt)
{
m_gizmo_highlighter.blink();
}
bool GLCanvas3D::clicked_button_matches_action(const wxMouseEvent& evt, const MouseAction action, const std::map<MouseButton, MouseAction>& mappings) const bool GLCanvas3D::clicked_button_matches_action(const wxMouseEvent& evt, const MouseAction action, const std::map<MouseButton, MouseAction>& mappings) const
{ {
MouseButton clicked = MouseButton::None; MouseButton clicked = MouseButton::None;
-3
View File
@@ -1133,9 +1133,6 @@ public:
void on_gesture(wxGestureEvent& evt); void on_gesture(wxGestureEvent& evt);
void on_paint(wxPaintEvent& evt); void on_paint(wxPaintEvent& evt);
void on_set_focus(wxFocusEvent& evt); void on_set_focus(wxFocusEvent& evt);
void on_kill_focus(wxFocusEvent& evt);
void on_toolbar_highlighter_timer(wxTimerEvent& evt);
void on_gizmo_highlighter_timer(wxTimerEvent& evt);
void force_set_focus(); void force_set_focus();
enum class MouseButton { None, Left, Middle, Right }; enum class MouseButton { None, Left, Middle, Right };
+6 -12
View File
@@ -2057,15 +2057,9 @@ wxMenu* MenuFactory::instance_menu()
return &m_instance_menu; return &m_instance_menu;
} }
MenuWithSeparators* MenuFactory::new_transient_menu()
{
m_transient_menu = std::make_unique<MenuWithSeparators>();
return m_transient_menu.get();
}
wxMenu* MenuFactory::layer_menu() wxMenu* MenuFactory::layer_menu()
{ {
MenuWithSeparators* menu = new_transient_menu(); MenuWithSeparators* menu = new MenuWithSeparators();
append_menu_item_settings(menu); append_menu_item_settings(menu);
return menu; return menu;
@@ -2091,13 +2085,13 @@ wxMenu* MenuFactory::multi_selection_menu()
} }
if (all_plates) { if (all_plates) {
wxMenu* menu = new_transient_menu(); wxMenu* menu = new MenuWithSeparators();
append_menu_item_replace_all_with_stl(menu); append_menu_item_replace_all_with_stl(menu);
return menu; return menu;
} }
if (undefined_type) if (undefined_type)
return nullptr; return nullptr;
wxMenu* menu = new_transient_menu(); wxMenu* menu = new MenuWithSeparators();
if (!multi_volume) { if (!multi_volume) {
int index = 0; int index = 0;
if (obj_list()->can_merge_to_multipart_object()) { if (obj_list()->can_merge_to_multipart_object()) {
@@ -2171,7 +2165,7 @@ wxMenu* MenuFactory::assemble_multi_selection_menu()
// show this menu only for Objects(Instances mixed with Objects)/Volumes selection // show this menu only for Objects(Instances mixed with Objects)/Volumes selection
return nullptr; return nullptr;
wxMenu* menu = new_transient_menu(); wxMenu* menu = new MenuWithSeparators();
append_menu_item_set_visible(menu); append_menu_item_set_visible(menu);
//append_menu_item_fix_through_cgal(menu); //append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu); //append_menu_item_simplify(menu);
@@ -2217,7 +2211,7 @@ wxMenu* MenuFactory::plate_menu()
wxMenu* MenuFactory::assemble_object_menu() wxMenu* MenuFactory::assemble_object_menu()
{ {
wxMenu* menu = new_transient_menu(); wxMenu* menu = new MenuWithSeparators();
// Set Visible // Set Visible
append_menu_item_set_visible(menu); append_menu_item_set_visible(menu);
// Delete // Delete
@@ -2237,7 +2231,7 @@ wxMenu* MenuFactory::assemble_object_menu()
wxMenu* MenuFactory::assemble_part_menu() wxMenu* MenuFactory::assemble_part_menu()
{ {
wxMenu* menu = new_transient_menu(); wxMenu* menu = new MenuWithSeparators();
append_menu_item_set_visible(menu); append_menu_item_set_visible(menu);
append_menu_item_delete(menu); append_menu_item_delete(menu);
-10
View File
@@ -2,7 +2,6 @@
#define slic3r_GUI_Factories_hpp_ #define slic3r_GUI_Factories_hpp_
#include <map> #include <map>
#include <memory>
#include <vector> #include <vector>
#include <array> #include <array>
#include <cstddef> #include <cstddef>
@@ -121,12 +120,6 @@ private:
MenuWithSeparators m_assemble_part_menu; MenuWithSeparators m_assemble_part_menu;
wxMenu m_filament_action_menu; wxMenu m_filament_action_menu;
// The selection dependent menus are rebuilt for every popup, so they cannot be members that
// outlive a build like the ones above; this owns the current one and destroys the previous.
// One slot is enough because PopupMenu() is synchronous: the menu a caller was handed is gone
// from the screen before anything can ask for the next one.
std::unique_ptr<MenuWithSeparators> m_transient_menu;
// Removed/Prepended Items according to the view mode // Removed/Prepended Items according to the view mode
@@ -134,9 +127,6 @@ private:
std::array<wxMenuItem*, mtCount> items_decrease; std::array<wxMenuItem*, mtCount> items_decrease;
std::array<wxMenuItem*, mtCount> items_set_number_of_copies; std::array<wxMenuItem*, mtCount> items_set_number_of_copies;
// Replaces m_transient_menu with an empty menu and returns it.
MenuWithSeparators* new_transient_menu();
void create_default_menu(); void create_default_menu();
void create_common_object_menu(wxMenu *menu); void create_common_object_menu(wxMenu *menu);
void create_object_menu(); void create_object_menu();
-4
View File
@@ -2272,10 +2272,6 @@ void GLGizmoMeasure::update_measurement_result()
void GLGizmoMeasure::reset_all_pick() void GLGizmoMeasure::reset_all_pick()
{ {
std::map<GLVolume*, std::shared_ptr<PickRaycaster>>().swap(m_mesh_raycaster_map); std::map<GLVolume*, std::shared_ptr<PickRaycaster>>().swap(m_mesh_raycaster_map);
// register_single_mesh_pick() fills both maps in lockstep, so the measurings have to go with
// the raycasters; otherwise the entries keyed on the GLVolumes of the previous selection stay
// behind for the rest of the session.
std::map<GLVolume*, std::shared_ptr<Measure::Measuring>>().swap(m_mesh_measure_map);
reset_gripper_pick(GripperType::UNDEFINE,true); reset_gripper_pick(GripperType::UNDEFINE,true);
} }
+3 -4
View File
@@ -27,7 +27,8 @@ enum class PainterGizmoType {
FDM_SUPPORTS, FDM_SUPPORTS,
SEAM, SEAM,
MM_SEGMENTATION, MM_SEGMENTATION,
FUZZY_SKIN FUZZY_SKIN,
TEXTURE_DISPLACEMENT
}; };
class TriangleSelectorGUI : public TriangleSelector { class TriangleSelectorGUI : public TriangleSelector {
@@ -101,9 +102,7 @@ class TriangleSelectorPatch : public TriangleSelectorGUI {
public: public:
explicit TriangleSelectorPatch(const TriangleMesh& mesh, const std::vector<ColorRGBA> ebt_colors, float edge_limit = 0.6f) explicit TriangleSelectorPatch(const TriangleMesh& mesh, const std::vector<ColorRGBA> ebt_colors, float edge_limit = 0.6f)
: TriangleSelectorGUI(mesh, edge_limit), m_ebt_colors(ebt_colors) {} : TriangleSelectorGUI(mesh, edge_limit), m_ebt_colors(ebt_colors) {}
// Releases the VAO and the per-patch VBOs built by finalize_triangle_indices(). The base class virtual ~TriangleSelectorPatch() = default;
// already deletes GL buffers from its GLModel members here, so this needs no context of its own.
virtual ~TriangleSelectorPatch() { release_geometry(); }
// Render current selection. Transformation matrices are supposed // Render current selection. Transformation matrices are supposed
// to be already set. // to be already set.
+9 -45
View File
@@ -361,7 +361,6 @@ ImGuiWrapper::~ImGuiWrapper()
{ {
//destroy_fonts_texture(); //destroy_fonts_texture();
destroy_font(); destroy_font();
destroy_svg_textures();
ImGui::DestroyContext(); ImGui::DestroyContext();
} }
@@ -543,12 +542,6 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt)
return ret; return ret;
} }
// SVG icons rasterized into GL textures, keyed on file name, size and recolor. Cleared as a whole
// from new_frame() once it grows past MAX_SVG_TEXTURES, which is safe there: the previous frame has
// been rendered and the frame about to be recorded asks for every icon it draws again.
static std::map<std::string, ImTextureID> s_svg_textures;
static const size_t MAX_SVG_TEXTURES = 256;
void ImGuiWrapper::new_frame() void ImGuiWrapper::new_frame()
{ {
if (m_new_frame_open) { if (m_new_frame_open) {
@@ -559,11 +552,6 @@ void ImGuiWrapper::new_frame()
init_font(true); init_font(true);
} }
// Recolored icons accumulate one texture per color the session has shown; drop them before
// anything references them again. This frame recreates the handful it actually draws.
if (s_svg_textures.size() > MAX_SVG_TEXTURES)
destroy_svg_textures();
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
ImGui::NewFrame(); ImGui::NewFrame();
@@ -1816,7 +1804,8 @@ bool menu_item_with_icon(const char *label, const char *shortcut, ImVec2 icon_si
if (icon_color != 0) if (icon_color != 0)
ImGui::RenderFrame(icon_pos, icon_pos + icon_size, icon_color); ImGui::RenderFrame(icon_pos, icon_pos + icon_size, icon_color);
else { else {
ImTextureID transparent = ImGuiWrapper::svg_texture(Slic3r::resources_dir() + "/images/transparent.svg", icon_size.x, icon_size.y); static ImTextureID transparent;
IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/transparent.svg", icon_size.x, icon_size.y, transparent);
window->DrawList->AddImage(transparent, icon_pos, icon_pos + icon_size, { 0,0 }, { 1,1 }, ImGui::GetColorU32(ImVec4(1.f, 1.f, 1.f, 1.f))); window->DrawList->AddImage(transparent, icon_pos, icon_pos + icon_size, { 0,0 }, { 1,1 }, ImGui::GetColorU32(ImVec4(1.f, 1.f, 1.f, 1.f)));
} }
} }
@@ -2642,7 +2631,11 @@ void ImGuiWrapper::push_toolbar_style(const float scale)
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 1.00f)); // 10 ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 1.00f)); // 10
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 0.00f)); // 11 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 0.00f)); // 11
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, COL_GREEN_LIGHT); // 12 ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, COL_GREEN_LIGHT); // 12
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));//13 // The checkbox/radio frame behind this is drawn fully transparent (see FrameBg above,
// alpha 0), showing the light window background through it - a white check mark there is
// invisible. Dark mode doesn't have this problem (its window background is dark), so only
// this branch needs a check mark color with real contrast against a light background.
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.f, 156 / 255.f, 136 / 255.f, 1.00f));//13
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.42f, 0.42f, 0.42f, 1.00f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.42f, 0.42f, 0.42f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.93f, 0.93f, 0.93f, 1.00f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.93f, 0.93f, 0.93f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.93f, 0.93f, 0.93f, 1.00f)); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.93f, 0.93f, 0.93f, 1.00f));
@@ -3396,36 +3389,6 @@ bool ImGuiWrapper::display_initialized() const
return io.DisplaySize.x >= 0.0f && io.DisplaySize.y >= 0.0f; return io.DisplaySize.x >= 0.0f && io.DisplaySize.y >= 0.0f;
} }
ImTextureID ImGuiWrapper::svg_texture(const std::string& filename, unsigned width, unsigned height, const char* hex_color)
{
std::string key = filename + "|" + std::to_string(width) + "x" + std::to_string(height);
if (hex_color != nullptr)
key += std::string("|") + hex_color;
const auto it = s_svg_textures.find(key);
if (it != s_svg_textures.end())
return it->second;
ImTextureID texture_id = nullptr;
const bool loaded = (hex_color != nullptr) ?
BitmapCache::load_from_svg_file_change_color(filename, width, height, texture_id, hex_color) :
IMTexture::load_from_svg_file(filename, width, height, texture_id);
if (!loaded)
return nullptr;
s_svg_textures.emplace(std::move(key), texture_id);
return texture_id;
}
void ImGuiWrapper::destroy_svg_textures()
{
for (const auto& texture : s_svg_textures) {
GLuint texture_id = (GLuint)(intptr_t)texture.second;
glsafe(::glDeleteTextures(1, &texture_id));
}
s_svg_textures.clear();
}
void ImGuiWrapper::destroy_font() void ImGuiWrapper::destroy_font()
{ {
if (m_font_texture != 0) { if (m_font_texture != 0) {
@@ -3505,6 +3468,7 @@ void ImGuiWrapper::filament_group(const std::string& filament_type, const char*
//ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); //ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
std::string id = std::to_string(static_cast<unsigned int> (filament_id + 1)); std::string id = std::to_string(static_cast<unsigned int> (filament_id + 1));
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList* draw_list = ImGui::GetWindowDrawList();
static ImTextureID transparent;
ImVec2 text_size = ImGui::CalcTextSize(filament_type.c_str()); ImVec2 text_size = ImGui::CalcTextSize(filament_type.c_str());
// BBS image sizing based on text width (DPI scaling) // BBS image sizing based on text width (DPI scaling)
float img_width = ImGui::CalcTextSize("ABC").x; float img_width = ImGui::CalcTextSize("ABC").x;
@@ -3517,7 +3481,7 @@ void ImGuiWrapper::filament_group(const std::string& filament_type, const char*
if (rgba[3] == 0x00) { if (rgba[3] == 0x00) {
svg_path = "/images/outlined_rect_transparent.svg"; svg_path = "/images/outlined_rect_transparent.svg";
} }
ImTextureID transparent = svg_texture(Slic3r::resources_dir() + svg_path, img_size.x, img_size.y, hex_color); BitmapCache::load_from_svg_file_change_color(Slic3r::resources_dir() + svg_path, img_size.x, img_size.y, transparent, hex_color);
ImGui::BeginGroup(); ImGui::BeginGroup();
{ {
ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
-8
View File
@@ -104,14 +104,6 @@ public:
// Hash of every draw list's vertices, indices and commands. // Hash of every draw list's vertices, indices and commands.
static ImGuiID draw_data_signature(const ImDrawData* draw_data); static ImGuiID draw_data_signature(const ImDrawData* draw_data);
// A GL texture holding an SVG icon rasterized at width x height, optionally recolored.
// Rasterizing an SVG is far too expensive to redo for every frame that draws the icon, and the
// texture the previous frame generated would leak, so the result is kept until the frame that
// finds the cache overgrown drops it (and rebuilds only what it still draws).
static ImTextureID svg_texture(const std::string& filename, unsigned width, unsigned height, const char* hex_color = nullptr);
// Deletes every texture svg_texture() handed out. Requires a current GL context.
static void destroy_svg_textures();
float scaled(float x) const { return x * m_font_size; } float scaled(float x) const { return x * m_font_size; }
ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); } ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); }
/// <summary> /// <summary>
+1 -2
View File
@@ -2225,12 +2225,11 @@ void NotificationManager::close_and_delete_self(PopNotification * self)
} }
void NotificationManager::remove_notification_of_type(const NotificationType type) { void NotificationManager::remove_notification_of_type(const NotificationType type) {
// Seven notification types may have several instances alive at once, so erase every match:
// stopping at the first one leaves the rest (and the ObjectIDs they hold) behind.
for (auto it = m_pop_notifications.begin(); it != m_pop_notifications.end();) { for (auto it = m_pop_notifications.begin(); it != m_pop_notifications.end();) {
std::unique_ptr<PopNotification> &notification = *it; std::unique_ptr<PopNotification> &notification = *it;
if (notification->get_type() == type) { if (notification->get_type() == type) {
it = m_pop_notifications.erase(it); it = m_pop_notifications.erase(it);
break;
} else } else
++it; ++it;
} }
+23 -8
View File
@@ -819,7 +819,9 @@ int OrcaCloudServiceAgent::user_logout(bool request)
} }
} }
clear_session(); // 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);
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
@@ -1604,7 +1606,9 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
} }
} }
(void) stored; if (stored) {
secret_stored = true;
}
} }
bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret) bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
@@ -1644,6 +1648,7 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
} }
if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) { if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) {
secret_stored = true;
out_secret = plain; out_secret = plain;
// Upgrade legacy payloads to signed format // Upgrade legacy payloads to signed format
if (payload.rfind("v2:", 0) != 0) { if (payload.rfind("v2:", 0) != 0) {
@@ -1661,6 +1666,7 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) { if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) {
out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize()); out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize());
if (!out_secret.empty()) { if (!out_secret.empty()) {
secret_stored = true;
return true; return true;
} }
} }
@@ -1670,11 +1676,20 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
return false; return false;
} }
void OrcaCloudServiceAgent::clear_user_secret() void OrcaCloudServiceAgent::clear_user_secret(bool all_backends)
{ {
wxSecretStore store = wxSecretStore::GetDefault(); // Nothing this process loaded or saved: leave the store alone. Deleting would only cost a
if (store.IsOk()) { // keychain round trip (or a hang while the keychain is unresponsive) and could remove a
store.Delete(SECRET_STORE_SERVICE); // 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);
}
} }
compute_fallback_path(); compute_fallback_path();
@@ -2023,13 +2038,13 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
return success; return success;
} }
void OrcaCloudServiceAgent::clear_session() void OrcaCloudServiceAgent::clear_session(bool all_backends)
{ {
{ {
std::lock_guard<std::mutex> lock(session_mutex); std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{}; session = SessionInfo{};
} }
clear_user_secret(); clear_user_secret(all_backends);
} }
// ============================================================================ // ============================================================================
+7 -2
View File
@@ -326,7 +326,7 @@ public:
void persist_user_secret(const std::string& secret); void persist_user_secret(const std::string& secret);
bool load_user_secret(std::string& out_secret); bool load_user_secret(std::string& out_secret);
void clear_user_secret(); void clear_user_secret(bool all_backends = false);
// Token refresh helpers // Token refresh helpers
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason); bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
@@ -344,7 +344,7 @@ public:
bool persist = true); bool persist = true);
// Accepts either nested Orca cloud / GoTrue session JSON or flat WebView token JSON. // 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); bool set_user_session(const nlohmann::json& session_json, bool notify_login = true);
void clear_session(); void clear_session(bool all_backends = false);
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = ""); static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
@@ -413,6 +413,11 @@ private:
// Member variables - auth state // Member variables - auth state
PkceBundle pkce_bundle; PkceBundle pkce_bundle;
std::string secret_fallback_path; 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; SessionHandler session_handler;
OnLoginCompleteHandler on_login_complete_handler; OnLoginCompleteHandler on_login_complete_handler;
SessionInfo session; SessionInfo session;
+152
View File
@@ -508,6 +508,158 @@ TEST_CASE("load_from_json hands a preset's include list to the caller instead of
CHECK(config.opt_string("machine_end_gcode") == "M84"); 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]") { TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
DynamicPrintConfig config; DynamicPrintConfig config;
config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
+1
View File
@@ -13,6 +13,7 @@ add_executable(${_TEST_NAME}_tests
test_prebuild_queue.cpp test_prebuild_queue.cpp
test_staged_build.cpp test_staged_build.cpp
test_network_versions.cpp test_network_versions.cpp
test_orca_cloud_agent.cpp
test_action_source.cpp test_action_source.cpp
test_plugin_host_api.cpp test_plugin_host_api.cpp
# Exercise seam enums and predicates through the embedded Python host API. # Exercise seam enums and predicates through the embedded Python host API.
+9 -1
View File
@@ -6,6 +6,7 @@
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <memory.h> #include <memory.h>
#include <stdexcept> #include <stdexcept>
#include <string>
#include <pybind11/embed.h> #include <pybind11/embed.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
@@ -25,8 +26,15 @@ void ensure_python_initialized()
config.parse_argv = 0; config.parse_argv = 0;
const auto python_home = boost::dll::program_location().parent_path() / "python"; 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
if (boost::filesystem::exists(python_home)) { // Only a real runtime: a stray python/ folder (packages a test left behind) is not a home.
if (boost::filesystem::exists(stdlib / "encodings")) {
const std::string home = python_home.string(); const std::string home = python_home.string();
const PyStatus status = PyConfig_SetBytesString(&config, &config.home, home.c_str()); const PyStatus status = PyConfig_SetBytesString(&config, &config.home, home.c_str());
@@ -0,0 +1,86 @@
#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,6 +38,9 @@ namespace {
// before this destructor's shutdown() runs. // before this destructor's shutdown() runs.
struct ScopedPluginManager 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(); bool initialized = PluginManager::instance().initialize();
~ScopedPluginManager() ~ScopedPluginManager()
@@ -31,6 +31,9 @@ namespace {
// same as any other plugin. // same as any other plugin.
struct ScopedManagerShutdown 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(); bool initialized = PluginManager::instance().initialize();
~ScopedManagerShutdown() ~ScopedManagerShutdown()
@@ -42,6 +42,9 @@ namespace {
// Declare this FIRST in a test so it is destroyed last. // Declare this FIRST in a test so it is destroyed last.
struct ScopedPluginManager 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; bool initialized = false;
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); } ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }
@@ -12,6 +12,8 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include "plugin_test_utils.hpp"
namespace py = pybind11; namespace py = pybind11;
using namespace Slic3r; using namespace Slic3r;
@@ -21,6 +23,9 @@ namespace {
// into Python unless PythonInterpreter::instance() reports initialized. // into Python unless PythonInterpreter::instance() reports initialized.
struct ScopedPluginManager 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(); bool initialized = PluginManager::instance().initialize();
~ScopedPluginManager() ~ScopedPluginManager()
@@ -35,6 +35,9 @@ namespace {
struct ScopedPluginManager 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; bool initialized = false;
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); } ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }