Compare commits

..
Author SHA1 Message Date
ExPikaPaka 19b8d2138e Stop the slicer before freeing what it is still reading
Loading an undo snapshot deletes every PartPlate, and the slicing thread keeps
dereferencing its current plate and the status callback that captured it on every
progress tick. Nothing stopped the process first, and Ctrl+Z does not go through
can_undo() at all. undo_redo_to() now stops it, which is what the cancel button
and the project reset already do. The cost is that undo during a slice waits for
the next cancellation checkpoint, exactly like pressing Cancel.

priv::reset() deleted the plates, and with them the Print and the GCodeResult,
five lines before stopping the process, so the stop then ran on a freed Print.

can_delete_plate() was missing the slicing guard its neighbour can_add_plate()
has, and delete_plate() destroys the Print the worker is slicing. The grabber in
the 3D scene does not go through the menu check, so the stop is in delete_plate()
as well.

OnEditingDone runs from ~wxDataViewCtrl while ~Plater is already running, where
the Plater pointer is not null but the object behind it is gone, so a null check
cannot catch it. It uses the shutdown flag the file already checks elsewhere.

PrintBase::m_status_callback was a bare std::function reassigned by the UI thread
while the worker invoked it, on the common path rather than a rare one: the
callback is reinstalled on the running Print on every background process update.
It is now assigned under a mutex and invoked as a copy with the lock released, so
a callback that blocks cannot deadlock against the assignment. The shared
timestamp counter is incremented under a different mutex per Print, and Orca
keeps one Print per plate, which is what the FIXME above it anticipated.

The plate drops its status callback where both it and the Print are still alive.
The destructor cannot do it: delete_plate() destroys the Print first.
2026-10-01 09:15:48 +02:00
25 changed files with 146 additions and 661 deletions
+1 -11
View File
@@ -151,12 +151,6 @@ 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}")
@@ -180,8 +174,7 @@ 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 \
@@ -198,7 +191,6 @@ 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"
@@ -221,8 +213,6 @@ 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
+5 -36
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 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 // must have consistent type name
std::string consistent_type; 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()) 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 = arr.begin(); iter != arr.end(); iter++) { for (auto iter = it.value().begin(); iter != it.value().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.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) if (!success)
return false; 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 // 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) { 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,11 +267,6 @@ 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.
+1 -10
View File
@@ -2696,8 +2696,6 @@ 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);
@@ -2738,13 +2736,7 @@ 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;
ConfigSubstitutionContext load_context(Enable); ConfigSubstitutions config_substitutions = use_json? config.load_from_json(dest_file, Enable, key_values, reason) : config.load_from_ini(dest_file, 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
@@ -2793,7 +2785,6 @@ 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);
+5 -8
View File
@@ -1726,11 +1726,9 @@ 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;
ConfigSubstitutionContext load_context(substitution_rule); ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
load_context.accept_nil = true; if (! config_substitutions.empty())
config.load_from_json(preset.file, load_context, true, key_values, reason); out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
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());
@@ -1766,7 +1764,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, &load_context.nil_slots); preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
} }
else { else {
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config); 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; 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->nil_slots); preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
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,8 +269,6 @@ 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; }
+24 -7
View File
@@ -19,7 +19,7 @@ void PrintTryCancel::operator()()
m_print->throw_if_canceled(); m_print->throw_if_canceled();
} }
size_t PrintStateBase::g_last_timestamp = 0; std::atomic<size_t> PrintStateBase::g_last_timestamp{0};
// Update "scale", "input_filename", "input_filename_base", "first_object_name" placeholders from the current m_objects. // Update "scale", "input_filename", "input_filename_base", "first_object_name" placeholders from the current m_objects.
void PrintBase::update_object_placeholders(DynamicConfig &config, const std::string &default_ext) const void PrintBase::update_object_placeholders(DynamicConfig &config, const std::string &default_ext) const
@@ -107,11 +107,26 @@ std::string PrintBase::output_filepath(const std::string &path, const std::strin
return path; return path;
} }
void PrintBase::set_status_callback(status_callback_type cb)
{
std::scoped_lock<std::mutex> lock(m_status_callback_mutex);
m_status_callback = std::move(cb);
}
// Returns a copy, so that the callback is invoked with m_status_callback_mutex released: the callback
// may block on the UI thread, which in turn may be assigning a new callback.
PrintBase::status_callback_type PrintBase::status_callback() const
{
std::scoped_lock<std::mutex> lock(m_status_callback_mutex);
return m_status_callback;
}
//BBS: move set_status from hpp to cpp //BBS: move set_status from hpp to cpp
void PrintBase::set_status(int percent, const std::string &message, unsigned int flags, int warning_step) const void PrintBase::set_status(int percent, const std::string &message, unsigned int flags, int warning_step) const
{ {
if (m_status_callback) status_callback_type status_callback = this->status_callback();
m_status_callback(SlicingStatus(percent, message, flags, warning_step)); if (status_callback)
status_callback(SlicingStatus(percent, message, flags, warning_step));
else else
BOOST_LOG_TRIVIAL(debug) <<boost::format("Percent %1%: %2%\n")%percent %message.c_str(); BOOST_LOG_TRIVIAL(debug) <<boost::format("Percent %1%: %2%\n")%percent %message.c_str();
} }
@@ -119,9 +134,10 @@ void PrintBase::set_status(int percent, const std::string &message, unsigned in
void PrintBase::status_update_warnings(int step, PrintStateBase::WarningLevel warning_level, void PrintBase::status_update_warnings(int step, PrintStateBase::WarningLevel warning_level,
const std::string &message, const PrintObjectBase* print_object, PrintStateBase::SlicingNotificationType message_id) const std::string &message, const PrintObjectBase* print_object, PrintStateBase::SlicingNotificationType message_id)
{ {
if (this->m_status_callback) { status_callback_type status_callback = this->status_callback();
if (status_callback) {
auto status = print_object ? SlicingStatus(*print_object, step, message, message_id, warning_level) : SlicingStatus(*this, step, message, message_id, warning_level); auto status = print_object ? SlicingStatus(*print_object, step, message, message_id, warning_level) : SlicingStatus(*this, step, message, message_id, warning_level);
m_status_callback(status); status_callback(status);
} }
else if (! message.empty()) else if (! message.empty())
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Print warning: %1%\n")% message.c_str(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Print warning: %1%\n")% message.c_str();
@@ -132,8 +148,9 @@ void PrintBase::status_update_warnings(int step, PrintStateBase::WarningLevel wa
const std::string& message, PrintObjectBase &object, PrintStateBase::SlicingNotificationType message_id) const std::string& message, PrintObjectBase &object, PrintStateBase::SlicingNotificationType message_id)
{ {
//BBS: add object it into slicing status //BBS: add object it into slicing status
if (this->m_status_callback) { status_callback_type status_callback = this->status_callback();
m_status_callback(SlicingStatus(object, step, message, message_id, warning_level)); if (status_callback) {
status_callback(SlicingStatus(object, step, message, message_id, warning_level));
} }
else if (!message.empty()) else if (!message.empty())
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", PrintObject warning: %1%\n")% message.c_str(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", PrintObject warning: %1%\n")% message.c_str();
+11 -8
View File
@@ -99,10 +99,9 @@ public:
}; };
protected: protected:
//FIXME last timestamp is shared between Print & SLAPrint, // The last timestamp is shared between all the Print & SLAPrint instances, and Orca keeps one Print
// and if multiple Print or SLAPrint instances are executed in parallel, modification of g_last_timestamp // per PartPlate, so it is incremented under different state mutexes: it has to be atomic.
// is not synchronized! static std::atomic<size_t> g_last_timestamp;
static size_t g_last_timestamp;
}; };
// To be instantiated over PrintStep or PrintObjectStep enums. // To be instantiated over PrintStep or PrintObjectStep enums.
@@ -473,11 +472,12 @@ public:
}; };
typedef std::function<void(const SlicingStatus&)> status_callback_type; typedef std::function<void(const SlicingStatus&)> status_callback_type;
// Default status console print out in the form of percent => message. // Default status console print out in the form of percent => message.
void set_status_default() { m_status_callback = nullptr; } void set_status_default() { this->set_status_callback(nullptr); }
// No status output or callback whatsoever, useful mostly for automatic tests. // No status output or callback whatsoever, useful mostly for automatic tests.
void set_status_silent() { m_status_callback = [](const SlicingStatus&){}; } void set_status_silent() { this->set_status_callback([](const SlicingStatus&){}); }
// Register a custom status callback. // Register a custom status callback. Called from the UI thread while the worker thread may be
void set_status_callback(status_callback_type cb) { m_status_callback = cb; } // invoking the previous callback, therefore guarded by m_status_callback_mutex.
void set_status_callback(status_callback_type cb);
// Calls a registered callback to update the status, or print out the default message. // Calls a registered callback to update the status, or print out the default message.
void set_status(int percent, const std::string &message, unsigned int flags = SlicingStatus::DEFAULT, int warning_step = -1) const; void set_status(int percent, const std::string &message, unsigned int flags = SlicingStatus::DEFAULT, int warning_step = -1) const;
@@ -563,7 +563,10 @@ protected:
std::string m_plate_name; std::string m_plate_name;
// Callback to be evoked regularly to update state of the UI thread. // Callback to be evoked regularly to update state of the UI thread.
// Guarded by m_status_callback_mutex, always invoke the copy returned by status_callback().
status_callback_type m_status_callback; status_callback_type m_status_callback;
mutable std::mutex m_status_callback_mutex;
status_callback_type status_callback() const;
private: private:
std::atomic<CancelStatus> m_cancel_status; std::atomic<CancelStatus> m_cancel_status;
+20 -83
View File
@@ -10,8 +10,6 @@
#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>
@@ -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") { } 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") {
// Rename whole entries: a substring rename would also hit longer keys that contain the old name. std::string copy_value = value;
std::vector<std::string> entries; copy_value.erase(std::remove(copy_value.begin(), copy_value.end(), '\"'), copy_value.end()); // remove '"' in string
boost::split(entries, value, boost::is_any_of(";")); std::set<std::string> split_keys = SplitStringAndRemoveDuplicateElement(copy_value, ";");
for (std::string &entry : entries) { for (std::string split_key : split_keys) {
std::string key = entry, unused_value; std::string copy_key = split_key, copy_value = "";
key.erase(std::remove(key.begin(), key.end(), '\"'), key.end()); handle_legacy(copy_key, copy_value);
std::string new_key = key; if (copy_key != split_key) {
handle_legacy(new_key, unused_value); ReplaceString(value, split_key, copy_key);
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" ) {
@@ -9240,13 +9236,6 @@ 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") {
@@ -9262,11 +9251,9 @@ 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") { } else if (opt_key == "top_one_wall_type" && value != "none") {
// 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";
if (!value.empty()) value = "1";
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") {
@@ -9284,20 +9271,6 @@ 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";
} }
@@ -9326,11 +9299,12 @@ 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") {
opt_key = "wipe_tower_wall_type"; if (value == "1") {
if (value == "1" || value == "true") opt_key = "wipe_tower_wall_type";
value = "rib"; value = "rib";
else if (!value.empty()) } else {
value = "rectangle"; opt_key = "";
}
} 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") {
@@ -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") { } 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";
} }
@@ -11596,8 +11558,7 @@ 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;
@@ -11658,13 +11619,6 @@ 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
@@ -11673,6 +11627,7 @@ 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
@@ -11686,26 +11641,8 @@ 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);
}
} }
} }
} }
+1 -4
View File
@@ -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, 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);
// nil_slots: per option, the slots of new_config that were nil in its file (see ConfigSubstitutionContext::nil_slots); 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);
// 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);
+5
View File
@@ -6693,6 +6693,11 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
void ObjectList::OnEditingDone(wxDataViewEvent &event) void ObjectList::OnEditingDone(wxDataViewEvent &event)
{ {
// ~wxDataViewCtrl ends the in-place editing, so this handler runs while ~Plater is already tearing
// the Plater down. Nothing below may touch the Plater or the plates any more.
if (wxGetApp().is_closing())
return;
if (event.GetColumn() != colName) if (event.GetColumn() != colName)
return; return;
+13
View File
@@ -3572,6 +3572,8 @@ void PartPlate::update_slice_result_valid_state(bool valid)
//update current slice context into backgroud slicing process //update current slice context into backgroud slicing process
void PartPlate::update_slice_context(BackgroundSlicingProcess & process) void PartPlate::update_slice_context(BackgroundSlicingProcess & process)
{ {
//this callback outlives the call, so it is dropped again in PartPlateList::clear() and
//PartPlateList::delete_plate() before the plate is destroyed
auto statuscb = [this](const Slic3r::PrintBase::SlicingStatus& status) { auto statuscb = [this](const Slic3r::PrintBase::SlicingStatus& status) {
Slic3r::SlicingStatusEvent *event = new Slic3r::SlicingStatusEvent(EVT_SLICING_UPDATE, 0, status); Slic3r::SlicingStatusEvent *event = new Slic3r::SlicingStatusEvent(EVT_SLICING_UPDATE, 0, status);
//BBS: GUI refactor: add plate info befor message //BBS: GUI refactor: add plate info befor message
@@ -4543,7 +4545,14 @@ void PartPlateList::clear(bool delete_plates, bool release_print_list, bool exce
else else
plate->clear(); plate->clear();
if (delete_plates) if (delete_plates)
{
//the slicing status callback installed by update_slice_context() captures the plate, so drop it
//while the Print is still alive: the prints are only released below, after this loop, and are
//not released at all when release_print_list is false.
if (Print* print = plate->fff_print())
print->set_status_default();
delete plate; delete plate;
}
} }
if (delete_plates) if (delete_plates)
@@ -4886,6 +4895,10 @@ int PartPlateList::delete_plate(int index)
//destroy the print object //destroy the print object
int print_index; int print_index;
plate->get_print(nullptr, nullptr, &print_index); plate->get_print(nullptr, nullptr, &print_index);
//the slicing status callback installed by update_slice_context() captures the plate, and destroy_print()
//frees the Print, so drop the callback here, the last point where both are still alive.
if (Print* print = plate->fff_print())
print->set_status_default();
destroy_print(print_index); destroy_print(print_index);
delete plate; delete plate;
+47 -192
View File
@@ -103,7 +103,6 @@
#include "Selection.hpp" #include "Selection.hpp"
#include "GLToolbar.hpp" #include "GLToolbar.hpp"
#include "GUI_Preview.hpp" #include "GUI_Preview.hpp"
#include "UVEditorCanvas.hpp"
#include "3DBed.hpp" #include "3DBed.hpp"
#include "PartPlate.hpp" #include "PartPlate.hpp"
#include "Camera.hpp" #include "Camera.hpp"
@@ -1798,14 +1797,11 @@ bool Sidebar::priv::switch_diameter_to(const wxString &diameter)
Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
// The combo lists printer variants, and the variant of a mixed-nozzle machine ("0.4+0.6") is no // The combo lists printer variants, and the variant of a mixed-nozzle machine ("0.4+0.6") is no
// single extruder's diameter, so the preset's own variant answers first. // single extruder's diameter, so the preset's own variant answers first.
const std::string &printer_variant = printer_preset.config.opt_string("printer_variant"); if (printer_preset.config.opt_string("printer_variant") == diameter.ToStdString()) {
if (printer_variant == diameter.ToStdString()) {
return true; return true;
} }
// A named variant ("0.4 High Flow") shares its diameter with the standard profile, which selecting
// the plain diameter switches back to, so only a preset naming no variant is kept by its diameter.
auto* nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(printer_preset.config.option("nozzle_diameter")); auto* nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(printer_preset.config.option("nozzle_diameter"));
if (printer_variant.empty() && nozzle_diameter && nozzle_diameter->size() > 0) { if (nozzle_diameter && nozzle_diameter->size() > 0) {
auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]); auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]);
// If the selected diameter is the same as current nozzle, don't switch profiles // If the selected diameter is the same as current nozzle, don't switch profiles
if (current_nozzle_dia == diameter.ToStdString()) { if (current_nozzle_dia == diameter.ToStdString()) {
@@ -2240,14 +2236,12 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
std::string machine_print_name = obj->get_show_printer_type(); std::string machine_print_name = obj->get_show_printer_type();
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle);
const bool optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type); Preset* machine_preset = get_printer_preset(obj);
const bool optional_target_model = DevPrinterConfigUtil::is_optional_printer_model_id(target_model_id); if (!machine_preset) {
Preset* machine_preset = optional_printer_model ? nullptr : get_printer_preset(obj);
if (!optional_printer_model && !optional_target_model && !machine_preset) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty";
return false; return false;
} }
if (!optional_printer_model && !optional_target_model && machine_print_name != target_model_id) { if (machine_print_name != target_model_id) {
MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n" MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n"
"Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); "Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO);
if (dlg.ShowModal() == wxID_NO) { if (dlg.ShowModal() == wxID_NO) {
@@ -2439,11 +2433,6 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
return; return;
} }
if (DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type)) {
clear_all_sync_status();
return;
}
bool printer_synced = false; bool printer_synced = false;
// 1. update printer status // 1. update printer status
const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
@@ -3937,18 +3926,13 @@ void Sidebar::update_presets(Preset::Type preset_type)
combo_flow->Show(combo_flow->GetCount() > 0); combo_flow->Show(combo_flow->GetCount() > 0);
}; };
auto update_extruder_diameter = [&diameters, &nozzle_diameter, &diameter](int extruder_index,ExtruderGroup & extruder) { auto update_extruder_diameter = [&diameters, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) {
extruder.combo_diameter->Clear(); extruder.combo_diameter->Clear();
if (extruder_index >= int(nozzle_diameter->values.size())) if (extruder_index >= int(nozzle_diameter->values.size()))
return; return;
int select = -1; int select = -1;
// ORCA get the actual nozzle diameter from printer config // ORCA get the actual nozzle diameter from printer config
auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]); auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]);
// Named variants such as "0.4HS" and "0.4 High Flow" share a physical diameter.
// Retain the variant selection unless the diameter was customized.
const bool keep_variant = diameter.substr(0, diameter.find_first_not_of("0123456789.")) == nozzle_dia &&
std::find(diameters.begin(), diameters.end(), diameter) != diameters.end();
const std::string &selected_variant = keep_variant ? diameter : nozzle_dia;
// ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias // ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias
if(!diameters.empty() && diameters[0].empty() && !nozzle_dia.empty()){ if(!diameters.empty() && diameters[0].empty() && !nozzle_dia.empty()){
diameters[0] = nozzle_dia; diameters[0] = nozzle_dia;
@@ -3958,7 +3942,7 @@ void Sidebar::update_presets(Preset::Type preset_type)
diameters.push_back(nozzle_dia); diameters.push_back(nozzle_dia);
} }
for (size_t i = 0; i < diameters.size(); ++i) { for (size_t i = 0; i < diameters.size(); ++i) {
if (diameters[i] == selected_variant) if (diameters[i] == nozzle_dia)
select = extruder.combo_diameter->GetCount(); select = extruder.combo_diameter->GetCount();
extruder.combo_diameter->Append(diameters[i], {}); extruder.combo_diameter->Append(diameters[i], {});
} }
@@ -6082,30 +6066,11 @@ void Sidebar::load_ams_list(MachineObject* obj)
filament_ams_list = build_filament_ams_list(obj); filament_ams_list = build_filament_ams_list(obj);
} }
bool device_change = false;
const std::string& device = obj ? obj->get_dev_id() : ""; const std::string& device = obj ? obj->get_dev_id() : "";
const bool same_device = p->ams_list_device == device; if (p->ams_list_device != device) {
// Keep sync metadata out of the device payload, but preserve it across a
// subscription refresh when the physical filament in a slot is unchanged.
// Otherwise the refreshed configs differ only by the missing
// filament_changed key, causing combo boxes to rebuild and lose their
// transient post-sync badges.
auto &previous_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list;
for (auto &entry : filament_ams_list) {
auto previous = previous_filament_ams_list.find(entry.first);
const auto *previous_changed = previous == previous_filament_ams_list.end() ? nullptr :
dynamic_cast<const ConfigOptionBool *>(previous->second.option("filament_changed"));
if (!same_device || previous_changed == nullptr ||
previous->second.opt_string("filament_id", 0u) != entry.second.opt_string("filament_id", 0u)) {
continue;
}
entry.second.set_key_value("filament_changed",
new ConfigOptionBool{previous_changed->value});
}
bool device_change = !same_device;
if (device_change) {
p->ams_list_device = device; p->ams_list_device = device;
device_change = true;
} }
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size();
if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change)
@@ -6115,27 +6080,9 @@ void Sidebar::load_ams_list(MachineObject* obj)
wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; wxGetApp().preset_bundle->filament_ams_list = filament_ams_list;
for (auto c : p->combos_filament){ for (auto c : p->combos_filament){
c->set_sync_badge(false);
c->update(); c->update();
} if (device_change) {
c->ShowBadge(false);//change printer,then clear badge
if (!device_change) {
size_t combo_index = 0;
for (const auto &entry : filament_ams_list) {
const auto &tray = entry.second;
const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
tray.opt_bool("filament_slot_placeholder", 0u);
if (!has_filament && !is_placeholder) {
continue;
}
if (combo_index >= p->combos_filament.size()) {
break;
}
const auto *filament_changed = dynamic_cast<const ConfigOptionBool *>(tray.option("filament_changed"));
p->combos_filament[combo_index]->set_sync_badge(
has_filament && !is_placeholder && filament_changed != nullptr && filament_changed->value);
++combo_index;
} }
} }
@@ -6309,32 +6256,18 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") : auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") :
_L("Filament type and color information have been synchronized, but slot information is not included."); _L("Filament type and color information have been synchronized, but slot information is not included.");
c->SetToolTip(tip); c->SetToolTip(tip);
c->set_sync_badge(true); c->ShowBadge(true);
}; };
{ // badge ams filament { // badge ams filament
clear_combos_filament_badge(); clear_combos_filament_badge();
if (sync_result.direct_sync) { if (sync_result.direct_sync) {
// A placeholder contributes a preserved project filament to the // Orca: PresetBundle::sync_ams_list rebuilds combos_filament
// overwrite result, but it is not AMS-sourced and must not get a // 1:1 from the AMS trays that produce a combo (loaded trays + placeholders; non-placeholder
// sync badge. Non-placeholder empty trays are omitted entirely. // empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The
size_t combo_index = 0; // previous per-tray index walked the full filament_ams_list (including the skipped empties),
for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) { // so an empty slot before a loaded one dropped the badge for the trailing filaments.
const auto &tray = entry.second; for (auto &c : p->combos_filament) {
const bool has_filament = !tray.opt_string("filament_id", 0u).empty(); badge_combox_filament(c);
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
tray.opt_bool("filament_slot_placeholder", 0u);
if (!has_filament && !is_placeholder) {
continue;
}
if (combo_index >= p->combos_filament.size()) {
break;
}
if (is_placeholder) {
p->combos_filament[combo_index]->set_sync_badge(false);
} else {
badge_combox_filament(p->combos_filament[combo_index]);
}
++combo_index;
} }
} }
} }
@@ -6602,11 +6535,6 @@ template<typename T> void setup_dialog_position(T& info)
void Sidebar::pop_sync_nozzle_and_ams_dialog() { void Sidebar::pop_sync_nozzle_and_ams_dialog() {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog";
auto agent = wxGetApp().getAgent();
if (!agent || agent->get_filament_sync_mode() == FilamentSyncMode::none) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " filament synchronization is not supported; skipping dialog";
return;
}
wxTheApp->CallAfter([this]() { wxTheApp->CallAfter([this]() {
SyncNozzleAndAmsDialog::InputInfo temp_na_info; SyncNozzleAndAmsDialog::InputInfo temp_na_info;
wxPoint big_btn_pt; wxPoint big_btn_pt;
@@ -6738,14 +6666,17 @@ void Sidebar::clear_combos_filament_badge()
{ {
auto &combos_filament = p->combos_filament; auto &combos_filament = p->combos_filament;
for (auto &c : combos_filament) { // clear flag for (auto &c : combos_filament) { // clear flag
c->set_sync_badge(false); c->ShowBadge(false);
} }
} }
void Sidebar::udpate_combos_filament_badge() { void Sidebar::udpate_combos_filament_badge() {
auto &combos_filament = p->combos_filament; auto &combos_filament = p->combos_filament;
for (auto &c : combos_filament) { for (auto &c : combos_filament) {
c->update_badge_according_flag(); auto selection = c->GetSelection();
auto select_flag = c->GetFlag(selection);
auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS;
c->ShowBadge(ok);
} }
} }
@@ -7117,13 +7048,6 @@ struct Plater::priv
GLToolbar collapse_toolbar; GLToolbar collapse_toolbar;
Preview *preview; Preview *preview;
AssembleView* assemble_view { nullptr }; AssembleView* assemble_view { nullptr };
// Docked/resizable 2D pane showing GLGizmoTextureDisplacement's LSCM unwrap of a painted
// patch; a sibling AUI pane alongside "sidebar"/"main", not part of the view3D/preview/
// assemble_view sizer - see its registration below and Plater::get_uv_editor_canvas(). The
// pane hosts the panel (toolbar + canvas + status line); uv_editor_canvas is its inner canvas,
// cached so the gizmo can reach it directly.
UVEditorPanel* uv_editor_panel { nullptr };
UVEditorCanvas* uv_editor_canvas { nullptr };
bool first_enter_assemble{ true }; bool first_enter_assemble{ true };
std::unique_ptr<NotificationManager> notification_manager; std::unique_ptr<NotificationManager> notification_manager;
@@ -7373,8 +7297,6 @@ struct Plater::priv
void undo(); void undo();
void redo(); void redo();
// True, and tells the user, while a background job is working on the model - see the definition.
bool undo_redo_blocked_by_job();
void undo_redo_to(size_t time_to_load); void undo_redo_to(size_t time_to_load);
// BBS: backup // BBS: backup
@@ -7828,26 +7750,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
.BottomDockable(false) .BottomDockable(false)
.BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit()))); .BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit())));
// UV editor pane for GLGizmoTextureDisplacement's LSCM unwrap preview - a resizable/dockable
// sibling of "sidebar"/"main" like everything else registered on this same AUI manager, not a
// change to the view3D/preview/assemble_view sizer above. Hidden by default: only relevant
// while that gizmo is active with a layer using the "Unwrap (LSCM)" projection method (see
// Plater::show_uv_editor()), so it stays out of the way of everyone else's window layout.
uv_editor_panel = new UVEditorPanel(q);
uv_editor_canvas = uv_editor_panel->canvas();
m_aui_mgr.AddPane(uv_editor_panel, wxAuiPaneInfo()
.Name("uv_editor")
.Caption(_L("UV Editor"))
.Right()
.Hide()
.BestSize(wxSize(40 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit())));
// Closing the pane with its own X has to reach the gizmo, or its next update would simply show the pane again.
q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent &evt) {
evt.Skip();
if (evt.GetPane() != nullptr && evt.GetPane()->window == uv_editor_panel && uv_editor_canvas != nullptr)
uv_editor_canvas->run_command(UVEditorCanvas::Command::PaneClosed);
});
auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL); auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL);
panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0); panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0);
panel_sizer->Add(preview, 1, wxEXPAND | wxALL, 0); panel_sizer->Add(preview, 1, wxEXPAND | wxALL, 0);
@@ -7889,13 +7791,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
BOOST_LOG_TRIVIAL(info) << "Removed floating AUI state from saved window layout for Wayland"; BOOST_LOG_TRIVIAL(info) << "Removed floating AUI state from saved window layout for Wayland";
} }
// The UV editor is a transient, gizmo-driven pane (see show_uv_editor()); a saved layout
// from a session that happened to close with it open would otherwise restore it visible on
// startup, with nothing painted in it. Force it hidden here so it only ever appears when the
// texture-displacement gizmo asks for it.
if (wxAuiPaneInfo &uv_pane = m_aui_mgr.GetPane("uv_editor"); uv_pane.IsOk())
uv_pane.Hide();
sidebar_layout.is_collapsed = !sidebar.IsShown(); sidebar_layout.is_collapsed = !sidebar.IsShown();
} }
@@ -10744,13 +10639,15 @@ void Plater::priv::reset(bool apply_presets_change, bool reload_presets)
m_worker.cancel_all(); m_worker.cancel_all();
// Stop and reset the Print content. m_worker.cancel_all() only stops the UI jobs, so this has to
// happen before reinit() deletes the plates together with the Print the slicing thread may still
// be working on.
this->background_process.reset();
//BBS: clear the partplate list's object before object cleared //BBS: clear the partplate list's object before object cleared
partplate_list.reinit(); partplate_list.reinit();
partplate_list.update_slice_context_to_current_plate(background_process); partplate_list.update_slice_context_to_current_plate(background_process);
preview->update_gcode_result(partplate_list.get_current_slice_result()); preview->update_gcode_result(partplate_list.get_current_slice_result());
// Stop and reset the Print content.
this->background_process.reset();
model.clear_objects(); model.clear_objects();
// clear_objects() only drops the ModelObjects; the CAD recipe is Model-level state and would // clear_objects() only drops the ModelObjects; the CAD recipe is Model-level state and would
// otherwise be written into every project saved for the rest of the session. // otherwise be written into every project saved for the rest of the session.
@@ -12766,7 +12663,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
sidebar->auto_calc_flushing_volumes(idx); sidebar->auto_calc_flushing_volumes(idx);
} }
auto select_flag = combo->GetFlag(selection); auto select_flag = combo->GetFlag(selection);
combo->set_sync_badge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS);
q->on_filament_change(idx); q->on_filament_change(idx);
} }
bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); bool select_preset = !combo->selection_is_changed_according_to_physical_printers();
@@ -15038,25 +14935,8 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed
BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info();
} }
// A background job holds the model it is working on: the texture displacement bake, for one, hands its
// result to the volume when it finishes, and it was queued against the geometry as it was at the time.
// Undoing while it runs restores an older state under it - a different transform, a different mesh -
// and the result then lands on geometry it was never computed for. Undo and redo therefore wait for
// the job, and say so rather than doing nothing.
bool Plater::priv::undo_redo_blocked_by_job()
{
if (m_worker.is_idle())
return false;
notification_manager->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
_u8L("Cannot undo or redo while an operation is running. Stop it first."));
return true;
}
void Plater::priv::undo() void Plater::priv::undo()
{ {
if (this->undo_redo_blocked_by_job())
return;
const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots(); const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots();
auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time()));
// BBS: undo-redo until modify record // BBS: undo-redo until modify record
@@ -15074,8 +14954,6 @@ void Plater::priv::undo()
void Plater::priv::redo() void Plater::priv::redo()
{ {
if (this->undo_redo_blocked_by_job())
return;
const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots(); const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots();
auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time()));
// BBS: undo-redo until modify record // BBS: undo-redo until modify record
@@ -15114,6 +14992,11 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator
// Make sure that no updating function calls take_snapshot until we are done. // Make sure that no updating function calls take_snapshot until we are done.
SuppressSnapshots snapshot_supressor(q); SuppressSnapshots snapshot_supressor(q);
// Loading a snapshot deletes every PartPlate, which the slicing thread keeps dereferencing (its
// current plate and the status callback). Cancel it and wait for it to finish before the jump,
// update_after_undo_redo() re-applies the background process to the rebuilt plates afterwards.
this->background_process.stop();
bool temp_snapshot_was_taken = this->undo_redo_stack().temp_snapshot_active(); bool temp_snapshot_was_taken = this->undo_redo_stack().temp_snapshot_active();
PrinterTechnology new_printer_technology = it_snapshot->snapshot_data.printer_technology; PrinterTechnology new_printer_technology = it_snapshot->snapshot_data.printer_technology;
bool printer_technology_changed = this->printer_technology != new_printer_technology; bool printer_technology_changed = this->printer_technology != new_printer_technology;
@@ -16721,7 +16604,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
/// -- scale -- /// --- scale ---
// model is created for a 0.4 nozzle, scale z with nozzle size. // model is created for a 0.4 nozzle, scale z with nozzle size.
const ConfigOptionFloats* nozzle_diameter_config = printer_config->option<ConfigOptionFloats>("nozzle_diameter"); const ConfigOptionFloats* nozzle_diameter_config = printer_config->option<ConfigOptionFloats>("nozzle_diameter");
std::vector<int> extruder_types = printer_config->option<ConfigOptionEnumsGeneric>("extruder_type")->values; std::vector<int> extruder_types = printer_config->option<ConfigOptionEnumsGeneric>("extruder_type")->values;
@@ -21198,33 +21081,6 @@ GLCanvas3D* Plater::get_assmeble_canvas3D()
return nullptr; return nullptr;
} }
UVEditorCanvas* Plater::get_uv_editor_canvas()
{
return p->uv_editor_canvas;
}
void Plater::show_uv_editor(bool show)
{
if (p->uv_editor_panel == nullptr)
return;
const wxAuiPaneInfo &pane = p->m_aui_mgr.GetPane(p->uv_editor_panel);
if (!pane.IsOk() || pane.IsShown() == show)
return;
// Deferred, because GLGizmoTextureDisplacement calls this from its ImGui panel - that is, from
// the middle of the 3D canvas's GL frame. Showing an AUI pane re-lays out the window and
// delivers the resulting size/paint events synchronously, and the UV canvas painting itself
// makes its own surface current in the app's *shared* GL context, which mid-frame is the one
// the 3D canvas is drawing into. Doing the layout once the frame is over avoids that entirely.
CallAfter([this, show]() {
wxAuiPaneInfo &deferred_pane = p->m_aui_mgr.GetPane(p->uv_editor_panel);
if (!deferred_pane.IsOk() || deferred_pane.IsShown() == show)
return;
deferred_pane.Show(show);
p->m_aui_mgr.Update();
});
}
GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview) GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview)
{ {
return p->get_current_canvas3D(exclude_preview); return p->get_current_canvas3D(exclude_preview);
@@ -21486,14 +21342,9 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning)
} }
if (!check_printer_initialized(obj, true, popup_warning)) if (!check_printer_initialized(obj, true, popup_warning))
return false; return false;
const std::string machine_model = obj->printer_type; Preset * machine_preset = get_printer_preset(obj);
PresetBundle *preset_bundle = wxGetApp().preset_bundle; if (!machine_preset)
const std::string selected_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string();
if (!DevPrinterConfigUtil::is_optional_printer_model_id(machine_model) &&
!DevPrinterConfigUtil::is_optional_printer_model_id(selected_model) &&
!get_printer_preset(obj)) {
return false; return false;
}
if (wxGetApp().is_blocking_printing()) { if (wxGetApp().is_blocking_printing()) {
if (popup_warning) { if (popup_warning) {
@@ -22578,6 +22429,11 @@ int Plater::delete_plate(int plate_index)
if (plate_index == -1) if (plate_index == -1)
index = p->partplate_list.get_curr_plate_index(); index = p->partplate_list.get_curr_plate_index();
// Orca: delete_plate() destroys the plate's Print and GCodeResult, which the slicing thread is
// still working on, so it has to be stopped first. can_delete_plate() also refuses while slicing,
// but the plate grabber in the 3D scene does not go through it.
p->background_process.stop();
take_snapshot("delete partplate"); take_snapshot("delete partplate");
ret = p->partplate_list.delete_plate(index); ret = p->partplate_list.delete_plate(index);
@@ -22907,7 +22763,7 @@ bool Plater::can_delete() const { return p->can_delete(); }
bool Plater::can_delete_all() const { return p->can_delete_all(); } bool Plater::can_delete_all() const { return p->can_delete_all(); }
bool Plater::can_add_model() const { return !is_background_process_slicing(); } bool Plater::can_add_model() const { return !is_background_process_slicing(); }
bool Plater::can_add_plate() const { return !is_background_process_slicing() && p->can_add_plate(); } bool Plater::can_add_plate() const { return !is_background_process_slicing() && p->can_add_plate(); }
bool Plater::can_delete_plate() const { return p->can_delete_plate(); } bool Plater::can_delete_plate() const { return !is_background_process_slicing() && p->can_delete_plate(); }
bool Plater::can_increase_instances() const { return p->can_increase_instances(); } bool Plater::can_increase_instances() const { return p->can_increase_instances(); }
bool Plater::can_decrease_instances() const { return p->can_decrease_instances(); } bool Plater::can_decrease_instances() const { return p->can_decrease_instances(); }
bool Plater::can_set_instance_to_object() const { return p->can_set_instance_to_object(); } bool Plater::can_set_instance_to_object() const { return p->can_set_instance_to_object(); }
@@ -22964,9 +22820,8 @@ bool Plater::can_copy_to_clipboard() const
return true; return true;
} }
// The job check keeps the buttons in step with priv::undo()/redo(), which refuse while one runs. bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_undo_snapshot(); }
bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && p->m_worker.is_idle() && p->undo_redo_stack().has_undo_snapshot(); } bool Plater::can_redo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_redo_snapshot(); }
bool Plater::can_redo() const { return IsShown() && p->is_view3D_shown() && p->m_worker.is_idle() && p->undo_redo_stack().has_redo_snapshot(); }
bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); } bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); }
//BBS //BBS
bool Plater::can_fillcolor() const { return p->can_fillcolor(); } bool Plater::can_fillcolor() const { return p->can_fillcolor(); }
+8 -23
View File
@@ -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 clear_session();
// 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;
} }
@@ -1606,9 +1604,7 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
} }
} }
if (stored) { (void) stored;
secret_stored = true;
}
} }
bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret) 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()) { 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) {
@@ -1666,7 +1661,6 @@ 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;
} }
} }
@@ -1676,20 +1670,11 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
return false; 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 wxSecretStore store = wxSecretStore::GetDefault();
// keychain round trip (or a hang while the keychain is unresponsive) and could remove a if (store.IsOk()) {
// login another instance just saved. store.Delete(SECRET_STORE_SERVICE);
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();
@@ -2038,13 +2023,13 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
return success; return success;
} }
void OrcaCloudServiceAgent::clear_session(bool all_backends) void OrcaCloudServiceAgent::clear_session()
{ {
{ {
std::lock_guard<std::mutex> lock(session_mutex); std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{}; session = SessionInfo{};
} }
clear_user_secret(all_backends); clear_user_secret();
} }
// ============================================================================ // ============================================================================
+2 -7
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(bool all_backends = false); void clear_user_secret();
// 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(bool all_backends = false); void clear_session();
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,11 +413,6 @@ 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;
+2
View File
@@ -1025,6 +1025,8 @@ void StackImpl::load_snapshot(size_t timestamp, Slic3r::Model& model, Slic3r::GU
std::vector<std::string> previous_gcode_paths; std::vector<std::string> previous_gcode_paths;
plate_list.get_sliced_result(previous_slice_result, previous_gcode_paths); plate_list.get_sliced_result(previous_slice_result, previous_gcode_paths);
// The plates are dereferenced by the slicing thread, which the caller
// (Plater::priv::undo_redo_to) has stopped before loading the snapshot.
plate_list.reset(false); plate_list.reset(false);
this->load_mutable_object<Slic3r::GUI::PartPlateList>(plate_list.id(), plate_list); this->load_mutable_object<Slic3r::GUI::PartPlateList>(plate_list.id(), plate_list);
plate_list.rebuild_plates_after_deserialize(previous_slice_result, previous_gcode_paths); plate_list.rebuild_plates_after_deserialize(previous_slice_result, previous_gcode_paths);
-152
View File
@@ -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"); 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,7 +13,6 @@ 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.
+1 -9
View File
@@ -6,7 +6,6 @@
#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>
@@ -26,15 +25,8 @@ 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
// Only a real runtime: a stray python/ folder (packages a test left behind) is not a home. if (boost::filesystem::exists(python_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());
@@ -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. // 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,9 +31,6 @@ 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,9 +42,6 @@ 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,8 +12,6 @@
#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;
@@ -23,9 +21,6 @@ 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,9 +35,6 @@ 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(); }