Merge branch 'main' into weilun/speed_dial

This commit is contained in:
Lam Wei Lun
2026-09-14 16:35:48 +08:00
7 changed files with 320 additions and 187 deletions
+152 -1
View File
@@ -53,6 +53,7 @@ using namespace nlohmann;
#include "libslic3r/libslic3r.h"
#include "libslic3r/Config.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/GCode.hpp"
@@ -162,6 +163,7 @@ std::map<int, std::string> cli_errors = {
{CLI_FILAMENT_CAN_NOT_MAP, "Some filaments cannot be mapped to correct extruders for multi-extruder Printer."},
{CLI_ONLY_ONE_TPU_SUPPORTED, "Not support printing 2 or more TPU filaments."},
{CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER, "Some filaments cannot be printed on the extruder mapped to."},
{CLI_MIXED_FILAMENT_INVALID, "A mixed filament is invalid: its components are different filament types, or it has no filament of its own."},
{CLI_SLICING_ERROR, "Failed slicing the model. Please verify the slicing of all plates on Orca Slicer before uploading."},
{CLI_GCODE_PATH_CONFLICTS, " G-code conflicts detected after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer. If the file slices normally in Orca Slicer, try moving the wipe tower further from other models, as we use more conservative parameters for it during upload."},
{CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, "Found G-code in unprintable area of multi-extruder printers after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer."}
@@ -3700,6 +3702,15 @@ int CLI::run(int argc, char **argv)
}
}
// A mixed slot never reaches a nozzle, so its row and column stay empty, as in the GUI.
// Command line options are not merged into m_print_config yet, so they win here.
const ConfigOptionBools *is_mixed_opt = m_extra_config.option<ConfigOptionBools>("filament_is_mixed");
if (!is_mixed_opt)
is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
auto is_mixed_slot = [is_mixed_opt](int idx) {
return is_mixed_opt && idx < static_cast<int>(is_mixed_opt->values.size()) && is_mixed_opt->values[idx];
};
for (size_t nozzle_id = 0; nozzle_id < new_extruder_count; ++nozzle_id) {
std::vector<double> flush_vol_mtx = get_flush_volumes_matrix(flush_vol_matrix, nozzle_id, new_extruder_count);
for (int from_idx = 0; from_idx < project_filament_count; from_idx++) {
@@ -3709,7 +3720,7 @@ int CLI::run(int argc, char **argv)
bool is_from_support = filament_is_support->get_at(from_idx);
for (int to_idx = 0; to_idx < project_filament_count; to_idx++) {
bool is_to_support = filament_is_support->get_at(to_idx);
if (from_idx == to_idx) {
if (from_idx == to_idx || is_mixed_slot(from_idx) || is_mixed_slot(to_idx)) {
flush_vol_mtx[project_filament_count * from_idx + to_idx] = 0.f;
} else {
int flushing_volume = 0;
@@ -3846,12 +3857,113 @@ int CLI::run(int argc, char **argv)
}
}
//ORCA: settings passed on the command line (--sparse-infill-density 25% ...) override the loaded
// presets right here, so they belong in different_settings_to_system just as a preset
// override does. Without them re-opening the exported project in the GUI shows nothing
// modified and reverts those values to the system presets'.
//
// The keys come from m_config, not m_extra_config: read_cli() puts only what the user typed
// into m_config (setup() adds nothing but CLI-own defaults), whereas the CLI writes its own
// values into m_extra_config. Only keys whose value the override actually changed are
// recorded -- a typed value equal to the loaded one modifies nothing -- and each lands in
// the column(s) whose preset type owns it: [0] process, [1..n-2] filaments, [n-1] printer.
//
// "Changed" is judged the way the value is read: a list is compared entry by entry with a
// missing entry read as the first, as get_at() does -- so --nozzle-temperature 245 against
// 245,245,245 is no change, although the two serialize differently.
//
// A key the loaded config does not carry at all is always recorded, even if the typed value
// equals the built-in default. On reopen the GUI restores an unlisted key from the SYSTEM
// preset, which need not match that default: a 3MF written before an option existed leaves
// it absent here, and --sparse-infill-density 20% (the default) against a Prusa system 15%
// would otherwise go unrecorded and be reverted. Over-recording is cosmetic; under-recording
// loses the value.
std::map<std::string, std::unique_ptr<ConfigOption>> cli_override_before;
for (const std::string &key : m_config.keys()) {
if (!m_extra_config.has(key))
continue;
const ConfigOption *loaded = m_print_config.option(key);
cli_override_before[key].reset(loaded != nullptr ? loaded->clone() : nullptr); // null: always recorded
}
// Apply command line options to a more specific DynamicPrintConfig which provides normalize()
// (command line options override --load files)
m_print_config.apply(m_extra_config, true);
if (!cli_override_before.empty()) {
std::vector<std::string> &columns = m_print_config.option<ConfigOptionStrings>("different_settings_to_system", true)->values;
auto owned_by = [](const std::vector<std::string> &options, const std::string &key) {
return std::find(options.begin(), options.end(), key) != options.end();
};
auto add_to_column = [&columns](size_t index, const std::string &key) {
std::vector<std::string> keys;
Slic3r::unescape_strings_cstyle(columns[index], keys);
if (std::find(keys.begin(), keys.end(), key) == keys.end()) {
keys.push_back(key);
columns[index] = Slic3r::escape_strings_cstyle(keys);
}
};
auto same_value = [](const ConfigOption *a, const ConfigOption *b) {
if (a == nullptr || b == nullptr)
return false;
const auto *va = dynamic_cast<const ConfigOptionVectorBase *>(a);
const auto *vb = dynamic_cast<const ConfigOptionVectorBase *>(b);
if (va == nullptr || vb == nullptr)
return va == vb && a->serialize() == b->serialize();
const std::vector<std::string> ea = va->vserialize(), eb = vb->vserialize();
if (ea.empty() || eb.empty())
return ea.empty() && eb.empty();
for (size_t i = 0; i < std::max(ea.size(), eb.size()); ++i)
if (ea[i < ea.size() ? i : 0] != eb[i < eb.size() ? i : 0])
return false;
return true;
};
//ORCA: always true after the resize to filament_count + 2 above, and nothing in between can
// shrink the column vector -- different_settings_to_system is not a CLI option. Kept as
// a check rather than an assert: release builds compile asserts out, so an assert would
// protect nothing, while a build with _GLIBCXX_ASSERTIONS would abort on columns[0].
if (columns.size() >= 2) {
for (const auto &[key, before] : cli_override_before) {
if (same_value(before.get(), m_print_config.option(key)))
continue;
bool recorded = false;
if (owned_by(Preset::print_options(), key)) {
add_to_column(0, key);
recorded = true;
}
if (owned_by(Preset::filament_options(), key)) {
for (size_t i = 1; i + 1 < columns.size(); ++i)
add_to_column(i, key);
recorded = true;
}
if (owned_by(Preset::printer_options(), key)) {
add_to_column(columns.size() - 1, key);
recorded = true;
}
if (recorded)
BOOST_LOG_TRIVIAL(info) << boost::format("CLI: override %1% recorded in different_settings_to_system") % key;
}
}
}
// Normalizing after importing the 3MFs / AMFs
m_print_config.normalize_fdm();
// A mixed slot is virtual but still needs a filament entry of its own. Without one, feature
// filament ids aimed at it fall outside the filament count, are reset to the first filament
// and the model silently prints in a single colour.
if (const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed")) {
const auto &is_mixed = is_mixed_opt->values;
for (size_t slot = static_cast<size_t>(std::max(filament_count, 0)); slot < is_mixed.size(); ++slot) {
if (!is_mixed[slot])
continue;
BOOST_LOG_TRIVIAL(error) << boost::format("mixed filament slot %1% has no filament of its own, only %2% filaments are loaded; "
"load one filament per slot, including each mixed one")
% (slot + 1) % filament_count;
record_exit_reson(outfile_dir, CLI_MIXED_FILAMENT_INVALID, 0, cli_errors[CLI_MIXED_FILAMENT_INVALID], sliced_info);
flush_and_exit(CLI_MIXED_FILAMENT_INVALID);
}
}
m_print_config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value = printer_technology;
bool has_wipe_tower_position = m_print_config.option<ConfigOptionFloats>("wipe_tower_x") && m_print_config.option<ConfigOptionFloats>("wipe_tower_y");
@@ -3906,6 +4018,15 @@ int CLI::run(int argc, char **argv)
bool is_smooth_timelapse = false;
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
is_smooth_timelapse = true;
// A mixed filament swaps between its components every layer, so it needs the tower even when
// every loaded preset is the same.
if (disable_wipe_tower_after_mapping) {
if (const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
is_mixed_opt && has_any_mixed_filament(is_mixed_opt->values)) {
disable_wipe_tower_after_mapping = false;
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to a mixed filament")%__LINE__;
}
}
if (disable_wipe_tower_after_mapping) {
if (is_smooth_timelapse)
{
@@ -6112,6 +6233,36 @@ int CLI::run(int argc, char **argv)
flush_and_exit(CLI_ONLY_ONE_TPU_SUPPORTED);
}
// Same type gate as the GUI's Sidebar::has_broken_mixed_filament: refuse a plate that uses a
// mixed slot whose components are different filament types. Missing or out-of-range
// components never get here, validate() already rejects them for the whole project.
const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *components_opt = m_print_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && components_opt && has_any_mixed_filament(is_mixed_opt->values)) {
const auto &is_mixed = is_mixed_opt->values;
const auto &components = components_opt->values;
const size_t num_physical = static_cast<size_t>(filament_count) - static_cast<size_t>(std::count(is_mixed.begin(), is_mixed.end(), true));
std::vector<std::string> physical_types(num_physical);
for (size_t f_index = 0; f_index < num_physical; ++f_index) {
std::string displayed_type;
physical_types[f_index] = m_print_config.get_filament_type(displayed_type, static_cast<int>(f_index));
if (physical_types[f_index].empty())
physical_types[f_index] = "PLA";
}
const std::vector<size_t> mismatched_slots = check_mixed_filament_type_consistency(is_mixed, components, physical_types);
// plate_filaments has mixed slots expanded to their components; the gate needs the slots.
const std::vector<int> plate_slots = mismatched_slots.empty() ? std::vector<int>() :
part_plate->get_extruders_under_cli(true, m_print_config, false);
for (size_t slot : mismatched_slots) {
if (std::find(plate_slots.begin(), plate_slots.end(), static_cast<int>(slot) + 1) == plate_slots.end())
continue;
BOOST_LOG_TRIVIAL(error) << boost::format("plate %1%: mixed filament %2% mixes components of different filament types")
% (index + 1) % (slot + 1);
record_exit_reson(outfile_dir, CLI_MIXED_FILAMENT_INVALID, index + 1, cli_errors[CLI_MIXED_FILAMENT_INVALID], sliced_info);
flush_and_exit(CLI_MIXED_FILAMENT_INVALID);
}
}
if (new_extruder_count > 1) {
std::vector<std::vector<int>> unprintable_filament_vec;
for (const std::set<int>& filamnt_ids : unprintable_filament_ids) {
+39 -176
View File
@@ -10936,6 +10936,28 @@ std::vector<int> DynamicPrintConfig::update_values_to_printer_extruders(DynamicP
return variant_index;
}
// Regathers a vector option's values through per-slot source indices (one input index per
// output slot). Out-of-range indices keep the first value, matching get_at's fallback.
template<typename OptType, typename ValueType>
static void gather_option_values(const char *caller, const std::string &key, OptType *opt, const std::vector<int> &slot_param_indices)
{
if (!opt || opt->values.empty()) {
BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key;
return;
}
std::vector<ValueType> new_values;
new_values.reserve(slot_param_indices.size());
for (int idx : slot_param_indices) {
if (idx < 0 || static_cast<size_t>(idx) >= opt->values.size()) {
BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx;
new_values.emplace_back(opt->values.front());
}
else
new_values.emplace_back(opt->values[idx]);
}
opt->values = std::move(new_values);
}
void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::set<std::string>& key_set, std::string id_name, std::string variant_name)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: extruder_count %2%, extruder_nozzle_volume_count %3%")%__LINE__ %extruder_count %extruder_nozzle_volume_count;
@@ -11013,155 +11035,18 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key;
continue;
}
// An empty option has no first value to fall back on; give it one registered default per filament.
if (auto *vec = dynamic_cast<ConfigOptionVectorBase*>(this->option(key)); vec && vec->empty() && optdef->default_value)
vec->resize(filament_count, optdef->default_value.get());
switch (optdef->type) {
case coStrings:
{
ConfigOptionStrings * opt = this->option<ConfigOptionStrings>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<std::string> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coInts:
{
ConfigOptionInts * opt = this->option<ConfigOptionInts>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<int> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coFloats:
{
ConfigOptionFloats * opt = this->option<ConfigOptionFloats>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<double> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coPercents:
{
ConfigOptionPercents * opt = this->option<ConfigOptionPercents>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<double> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coFloatsOrPercents:
{
ConfigOptionFloatsOrPercents * opt = this->option<ConfigOptionFloatsOrPercents>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<FloatOrPercent> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coBools:
{
ConfigOptionBools * opt = this->option<ConfigOptionBools>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<unsigned char> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coEnums:
{
ConfigOptionEnumsGeneric * opt = this->option<ConfigOptionEnumsGeneric>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<int> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(__FUNCTION__, key, this->option<ConfigOptionStrings>(key), variant_index); break;
case coInts: gather_option_values<ConfigOptionInts, int>(__FUNCTION__, key, this->option<ConfigOptionInts>(key), variant_index); break;
case coFloats: gather_option_values<ConfigOptionFloats, double>(__FUNCTION__, key, this->option<ConfigOptionFloats>(key), variant_index); break;
case coPercents: gather_option_values<ConfigOptionPercents, double>(__FUNCTION__, key, this->option<ConfigOptionPercents>(key), variant_index); break;
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(__FUNCTION__, key, this->option<ConfigOptionFloatsOrPercents>(key), variant_index); break;
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(__FUNCTION__, key, this->option<ConfigOptionBools>(key), variant_index); break;
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(__FUNCTION__, key, this->option<ConfigOptionEnumsGeneric>(key), variant_index); break;
default:
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key;
break;
@@ -11180,28 +11065,6 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
}
}
// Regathers a vector option's values through per-slot source indices (one input index per
// output slot). Out-of-range indices keep the first value, matching get_at's fallback.
template<typename OptType, typename ValueType>
static void gather_option_values(const std::string &key, OptType *opt, const std::vector<int> &slot_param_indices)
{
if (!opt || opt->values.empty()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key;
return;
}
std::vector<ValueType> new_values;
new_values.reserve(slot_param_indices.size());
for (int idx : slot_param_indices) {
if (idx < 0 || static_cast<size_t>(idx) >= opt->values.size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx;
new_values.emplace_back(opt->values.front());
}
else
new_values.emplace_back(opt->values[idx]);
}
opt->values = std::move(new_values);
}
void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config,
const std::unordered_map<int, std::vector<FilamentVariantUse>>& filament_variant_uses,
int extruder_count, int extruder_nozzle_volume_count,
@@ -11296,13 +11159,13 @@ void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(Dy
continue;
}
switch (optdef->type) {
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(key, this->option<ConfigOptionStrings>(key), slot_param_indices); break;
case coInts: gather_option_values<ConfigOptionInts, int>(key, this->option<ConfigOptionInts>(key), slot_param_indices); break;
case coFloats: gather_option_values<ConfigOptionFloats, double>(key, this->option<ConfigOptionFloats>(key), slot_param_indices); break;
case coPercents: gather_option_values<ConfigOptionPercents, double>(key, this->option<ConfigOptionPercents>(key), slot_param_indices); break;
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(key, this->option<ConfigOptionFloatsOrPercents>(key), slot_param_indices); break;
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(key, this->option<ConfigOptionBools>(key), slot_param_indices); break;
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(key, this->option<ConfigOptionEnumsGeneric>(key), slot_param_indices); break;
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(__FUNCTION__, key, this->option<ConfigOptionStrings>(key), slot_param_indices); break;
case coInts: gather_option_values<ConfigOptionInts, int>(__FUNCTION__, key, this->option<ConfigOptionInts>(key), slot_param_indices); break;
case coFloats: gather_option_values<ConfigOptionFloats, double>(__FUNCTION__, key, this->option<ConfigOptionFloats>(key), slot_param_indices); break;
case coPercents: gather_option_values<ConfigOptionPercents, double>(__FUNCTION__, key, this->option<ConfigOptionPercents>(key), slot_param_indices); break;
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(__FUNCTION__, key, this->option<ConfigOptionFloatsOrPercents>(key), slot_param_indices); break;
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(__FUNCTION__, key, this->option<ConfigOptionBools>(key), slot_param_indices); break;
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(__FUNCTION__, key, this->option<ConfigOptionEnumsGeneric>(key), slot_param_indices); break;
default:
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key;
break;
+1
View File
@@ -70,6 +70,7 @@
#define CLI_FILAMENT_CAN_NOT_MAP -66
#define CLI_ONLY_ONE_TPU_SUPPORTED -67
#define CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER -68
#define CLI_MIXED_FILAMENT_INVALID -69
#define CLI_SLICING_ERROR -100
#define CLI_GCODE_PATH_CONFLICTS -101
+2 -2
View File
@@ -1717,7 +1717,7 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode, const Dynam
return plate_extruders;
}
std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const
std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config, bool expand_mixed_slots) const
{
std::vector<int> plate_extruders;
@@ -1878,7 +1878,7 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead.
{
if (expand_mixed_slots) {
auto* is_mixed_opt = full_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = full_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
+2 -1
View File
@@ -350,7 +350,8 @@ public:
// get used filaments from config, 1 based idx
std::vector<int> get_extruders(bool conside_custom_gcode = false) const;
std::vector<int> get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const;
std::vector<int> get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const;
// expand_mixed_slots = false keeps mixed filament slots as slots instead of their components.
std::vector<int> get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config, bool expand_mixed_slots = true) const;
std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const;
// get used filaments from gcode result, 1 based idx
std::vector<int> get_used_filaments();
+96 -7
View File
@@ -9,6 +9,7 @@
#include <slic3r/GUI/MsgDialog.hpp>
#include <slic3r/GUI/PluginProgressDialog.hpp>
#include <slic3r/GUI/PluginWebDialog.hpp>
#include <slic3r/GUI/NotificationManager.hpp>
#include <nlohmann/json.hpp>
#include <pybind11/pybind11.h>
@@ -19,6 +20,7 @@
#include <wx/defs.h>
#include <wx/window.h>
#include <atomic>
#include <cstdint>
#include <future>
#include <memory>
@@ -44,16 +46,20 @@ namespace {
struct GilSafeCallable
{
py::object fn;
std::atomic_bool active{true};
explicit GilSafeCallable(py::object f) : fn(std::move(f)) {}
void disable()
{
active.store(false, std::memory_order_release);
PythonGILState gil;
if (gil)
fn = py::object();
else
(void) fn.release();
}
~GilSafeCallable()
{
if (fn) {
PythonGILState gil;
if (gil)
fn = py::object();
else
(void) fn.release();
}
disable();
}
};
using CallablePtr = std::shared_ptr<GilSafeCallable>;
@@ -166,11 +172,34 @@ public:
}
return out;
}
void bind_callback(const CallablePtr& callback, const std::string& plugin_key)
{
if (!callback)
return;
std::lock_guard<std::mutex> lk(m_mtx);
m_callbacks[plugin_key].push_back(callback);
}
std::vector<CallablePtr> take_callbacks_for_plugin(const std::string& plugin_key)
{
std::lock_guard<std::mutex> lk(m_mtx);
auto it = m_callbacks.find(plugin_key);
if (it == m_callbacks.end())
return {};
std::vector<CallablePtr> callbacks;
callbacks.reserve(it->second.size());
for (const std::weak_ptr<GilSafeCallable>& weak_callback : it->second) {
if (auto callback = weak_callback.lock())
callbacks.push_back(std::move(callback));
}
m_callbacks.erase(it);
return callbacks;
}
private:
std::mutex m_mtx;
std::unordered_map<int, wxWindow*> m_resources;
std::unordered_map<int, std::string> m_owners;
std::unordered_map<std::string, std::vector<std::weak_ptr<GilSafeCallable>>> m_callbacks;
int m_next_id{1};
};
@@ -448,6 +477,46 @@ void progress_close(int id)
});
}
void plater_notification(NotificationManager::NotificationLevel notification_level, const std::string& text,
const std::string& hypertext, py::object on_click)
{
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
CallablePtr holder = make_holder(std::move(on_click));
if (holder)
UiRegistry::instance().bind_callback(holder, plugin_key);
std::function<bool(wxEvtHandler*)> callback;
if (holder) {
callback = [holder](wxEvtHandler*) -> bool {
if (!holder->active.load(std::memory_order_acquire))
return false;
PythonGILState gil;
if (!gil)
return false;
try {
py::object result = holder->fn();
return result.is_none() || result.cast<bool>();
} catch (py::error_already_set& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what();
PyErr_Clear();
return false;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what();
return false;
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised an unknown exception";
return false;
}
};
}
run_on_ui_blocking([notification_level, text, hypertext, callback = std::move(callback)]() mutable {
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::CustomNotification, notification_level, text,
hypertext, std::move(callback));
});
}
} // namespace
void PluginHostUi::RegisterBindings(pybind11::module_& host)
@@ -530,6 +599,23 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
ui.def("create_progress_dialog", &ui_create_progress_dialog, py::arg("title"), py::arg("message"),
py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE,
"Create a native progress dialog and return a ProgressDialog handle.");
py::enum_<NotificationManager::NotificationLevel>(ui, "NotificationLevel")
.value("ProgressBarNotificationLevel", NotificationManager::NotificationLevel::ProgressBarNotificationLevel)
.value("HintNotificationLevel", NotificationManager::NotificationLevel::HintNotificationLevel)
.value("RegularNotificationLevel", NotificationManager::NotificationLevel::RegularNotificationLevel)
.value("PrintInfoNotificationLevel", NotificationManager::NotificationLevel::PrintInfoNotificationLevel)
.value("PrintInfoShortNotificationLevel", NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel)
.value("ImportantNotificationLevel", NotificationManager::NotificationLevel::ImportantNotificationLevel)
.value("WarningNotificationLevel", NotificationManager::NotificationLevel::WarningNotificationLevel)
.value("SeriousWarningNotificationLevel", NotificationManager::NotificationLevel::SeriousWarningNotificationLevel)
.value("ErrorNotificationLevel", NotificationManager::NotificationLevel::ErrorNotificationLevel)
.export_values();
ui.def("push_notification", &plater_notification, py::arg("notification_level"), py::arg("text"),
py::arg("hyper_text") = "", py::arg("on_click") = py::none(),
"Push a plater notification. hyper_text is an underlined label; on_click() is called when it is clicked "
"and may return True to close the notification.");
}
void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
@@ -538,6 +624,9 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
return;
auto teardown = [plugin_key]() {
for (auto& callback : UiRegistry::instance().take_callbacks_for_plugin(plugin_key))
callback->disable();
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
// forced teardown (intended); the resource destructor still cleans the registry.
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {