From 2ddc7d703afca6157eae17fcdac5da1476a86bcc Mon Sep 17 00:00:00 2001 From: viewit Date: Wed, 3 Jun 2026 21:35:08 +0200 Subject: [PATCH] OrcaSlicer-KX v2.4.0-alpha-kx2 patch files --- README.md | 15 + src/libslic3r/Preset.cpp | 4444 +++++ src/libslic3r/PresetBundle.cpp | 5534 ++++++ src/slic3r/GUI/Plater.cpp | 18565 +++++++++++++++++++ src/slic3r/GUI/PresetComboBoxes.cpp | 2102 +++ src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 2404 +++ src/slic3r/Utils/MoonrakerPrinterAgent.hpp | 216 + 7 files changed, 33280 insertions(+) create mode 100644 README.md create mode 100644 src/libslic3r/Preset.cpp create mode 100644 src/libslic3r/PresetBundle.cpp create mode 100644 src/slic3r/GUI/Plater.cpp create mode 100644 src/slic3r/GUI/PresetComboBoxes.cpp create mode 100644 src/slic3r/Utils/MoonrakerPrinterAgent.cpp create mode 100644 src/slic3r/Utils/MoonrakerPrinterAgent.hpp diff --git a/README.md b/README.md new file mode 100644 index 0000000000..92c7831e93 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# OrcaSlicer-KX Source Patches + +Patch files for OrcaSlicer-KX build (v2.4.0-alpha-kx2). + +Based on OrcaSlicer v2.4.0-alpha (https://github.com/SoftFever/OrcaSlicer) +with additional patches for KX-Bridge (Anycubic Kobra X Moonraker bridge). + +## Applied patches +- PR #13372: Fix Moonraker Happy Hare AMS filament sync +- PR #13719: Vendor-aware filament matching for Moonraker +- KX-Bridge filament hint support (tray_info_idx + vendor) +- User preset filament_id generation and sync fixes + +## License +GNU AGPL-3.0 — same as upstream OrcaSlicer diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp new file mode 100644 index 0000000000..af3bfc05e1 --- /dev/null +++ b/src/libslic3r/Preset.cpp @@ -0,0 +1,4444 @@ +#include + +#include "Config.hpp" +#include "Exception.hpp" +#include "Preset.hpp" +#include "PresetBundle.hpp" +#include "AppConfig.hpp" + +#ifdef _MSC_VER + #define WIN32_LEAN_AND_MEAN + #define NOMINMAX + #include +#endif /* _MSC_VER */ + +// instead of #include "slic3r/GUI/I18N.hpp" : +#ifndef L +// !!! If you needed to translate some string, +// !!! please use _L(string) +// !!! _() - is a standard wxWidgets macro to translate +// !!! L() is used only for marking localizable string +// !!! It will be used in "xgettext" to create a Locating Message Catalog. +#define L(s) s +#endif /* L */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +//BBS: add regex +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r.h" +#include "Utils.hpp" +#include "Time.hpp" +#include "PlaceholderParser.hpp" +#include "libslic3r/GCode/Thumbnails.hpp" + +using boost::property_tree::ptree; + +namespace Slic3r { + +namespace { + +struct ParsedName { + PresetOrigin::Kind kind { PresetOrigin::Kind::User }; + std::string bundle_id; + std::string bare; +}; + +// Canonical names are built in-memory with '/' separators, so a straight prefix+split match is enough. +static ParsedName parse_preset_name(const std::string &raw_name) +{ + ParsedName out; + + auto try_prefix = [&](const char *dir, PresetOrigin::Kind kind) { + const std::string prefix = std::string(dir) + "/"; + if (! boost::starts_with(raw_name, prefix)) + return false; + const size_t id_start = prefix.size(); + const size_t id_end = raw_name.find('/', id_start); + if (id_end == std::string::npos || id_end == id_start) + return false; + out.kind = kind; + out.bundle_id = raw_name.substr(id_start, id_end - id_start); + out.bare = raw_name.substr(id_end + 1); + return true; + }; + + if (! try_prefix(PRESET_LOCAL_DIR, PresetOrigin::Kind::LocalBundle) && + ! try_prefix(PRESET_SUBSCRIBED_DIR, PresetOrigin::Kind::SubscribedBundle)) + out.bare = raw_name; + + return out; +} + +} // namespace + +std::string get_preset_canonical_name(const std::string &preset_bare_name, const PresetOrigin &origin) +{ + switch (origin.kind) { + case PresetOrigin::Kind::LocalBundle: + return origin.bundle_id.empty() ? preset_bare_name : std::string(PRESET_LOCAL_DIR) + "/" + origin.bundle_id + "/" + preset_bare_name; + case PresetOrigin::Kind::SubscribedBundle: + return origin.bundle_id.empty() ? preset_bare_name : std::string(PRESET_SUBSCRIBED_DIR) + "/" + origin.bundle_id + "/" + preset_bare_name; + default: + return preset_bare_name; + } +} + +std::string get_preset_bare_name(const std::string &canonical_name) +{ + const auto pos = canonical_name.find_last_of('/'); + return pos == std::string::npos ? canonical_name : canonical_name.substr(pos + 1); +} + +PresetOrigin detect_origin_from_path(const boost::filesystem::path &path, const PresetOrigin &explicit_origin) +{ + if (explicit_origin.kind != PresetOrigin::Kind::Auto) + return explicit_origin; + + for (auto it = path.begin(); it != path.end(); ++ it) { + const auto next = std::next(it); + if (next == path.end()) + break; + const std::string segment = it->string(); + if (segment == PRESET_LOCAL_DIR) + return PresetOrigin(PresetOrigin::Kind::LocalBundle, next->string()); + if (segment == PRESET_SUBSCRIBED_DIR) + return PresetOrigin(PresetOrigin::Kind::SubscribedBundle, next->string()); + } + return PresetOrigin(PresetOrigin::Kind::User); +} + +//BBS: add a function to load the version from xxx.json +Semver get_version_from_json(std::string file_path) +{ + try { + boost::nowide::ifstream ifs(file_path); + json j; + ifs >> j; + std::string version_str = j.at(BBL_JSON_KEY_VERSION); + + auto config_version = Semver::parse(version_str); + if (! config_version) { + return Semver(); + } else { + return *config_version; + } + } + catch(nlohmann::detail::parse_error &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<& keys, std::map& key_values) +{ + try { + boost::nowide::ifstream ifs(file_path); + json j; + ifs >> j; + + for (int i=0; i < keys.size(); i++) + { + if (j.contains(keys[i])) { + std::string value = j.at(keys[i]); + key_values.emplace(keys[i], value); + } + } + } + catch(nlohmann::detail::parse_error &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "< bundle && app_config > config) ? CONFIG_FILE_TYPE_APP_CONFIG : + (bundle > config) ? CONFIG_FILE_TYPE_CONFIG_BUNDLE : CONFIG_FILE_TYPE_CONFIG; +} + + +VendorProfile VendorProfile::from_ini(const boost::filesystem::path &path, bool load_all) +{ + ptree tree; + boost::filesystem::ifstream ifs(path); + boost::property_tree::read_ini(ifs, tree); + return VendorProfile::from_ini(tree, path, load_all); +} + +static const std::unordered_map pre_family_model_map {{ + { "MK3", "MK3" }, + { "MK3MMU2", "MK3" }, + { "MK2.5", "MK2.5" }, + { "MK2.5MMU2", "MK2.5" }, + { "MK2S", "MK2" }, + { "MK2SMM", "MK2" }, + { "SL1", "SL1" }, +}}; + + +// 中间版本兼容性处理,如果是nil值,先改成default值,再进行扩展 +void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil_to_default, const DynamicPrintConfig& defaults) +{ + constexpr int default_param_length = 1; + int filament_variant_length = default_param_length; + int process_variant_length = default_param_length; + int machine_variant_length = default_param_length; + + // Orca: use nozzle/extruder count as the default printer variant length + // because non-BBL multi-extruder printers currently do not support extruder variant. + if (config.has("nozzle_diameter")) { + auto* nozzle_diameter = dynamic_cast(config.option("nozzle_diameter")); + machine_variant_length = nozzle_diameter->values.size(); + } + + if(config.has("filament_extruder_variant")) + filament_variant_length = config.option("filament_extruder_variant")->size(); + if(config.has("print_extruder_variant")) + process_variant_length = config.option("print_extruder_variant")->size(); + if(config.has("printer_extruder_variant")) // Use existing variant list if specified, so BBL's multi-variant profiles still works + machine_variant_length = config.option("printer_extruder_variant")->size(); + + auto replace_nil_and_resize = [&](const std::string & key, int length){ + ConfigOption* raw_ptr = config.option(key); + ConfigOptionVectorBase* opt_vec = static_cast(raw_ptr); + if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), key) == filament_extruder_override_keys.end()){ + opt_vec->clear(); + opt_vec->resize(length, defaults.option(key)); + } + else{ + opt_vec->resize(length, raw_ptr); + } + }; + + for(auto& key :config.keys()){ + if(auto iter = print_options_with_variant.find(key); iter != print_options_with_variant.end()){ + replace_nil_and_resize(key, process_variant_length); + } + else if(auto iter = filament_options_with_variant.find(key); iter != filament_options_with_variant.end()){ + replace_nil_and_resize(key, filament_variant_length); + } + else if(auto iter = printer_options_with_variant_1.find(key); iter != printer_options_with_variant_1.end()){ + replace_nil_and_resize(key, machine_variant_length); + } + else if(auto iter = printer_options_with_variant_2.find(key); iter != printer_options_with_variant_2.end()){ + replace_nil_and_resize(key, machine_variant_length * 2); + } + } +} + + +VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem::path &path, bool load_all) +{ + static const std::string printer_model_key = "printer_model:"; + static const std::string filaments_section = "default_filaments"; + static const std::string materials_section = "default_sla_materials"; + + const std::string id = path.stem().string(); + + if (! boost::filesystem::exists(path)) { + throw Slic3r::RuntimeError((boost::format("Cannot load Vendor Config Bundle `%1%`: File not found: `%2%`.") % id % path).str()); + } + + VendorProfile res(id); + + // Helper to get compulsory fields + auto get_or_throw = [&](const ptree &tree, const std::string &key) -> ptree::const_assoc_iterator + { + auto res = tree.find(key); + if (res == tree.not_found()) { + throw Slic3r::RuntimeError((boost::format("Vendor Config Bundle `%1%` is not valid: Missing secion or key: `%2%`.") % id % key).str()); + } + return res; + }; + + // Load the header + const auto &vendor_section = get_or_throw(tree, "vendor")->second; + res.name = get_or_throw(vendor_section, "name")->second.data(); + + auto config_version_str = get_or_throw(vendor_section, "config_version")->second.data(); + auto config_version = Semver::parse(config_version_str); + if (! config_version) { + throw Slic3r::RuntimeError((boost::format("Vendor Config Bundle `%1%` is not valid: Cannot parse config_version: `%2%`.") % id % config_version_str).str()); + } else { + res.config_version = std::move(*config_version); + } + + // Load URLs + const auto config_update_url = vendor_section.find("config_update_url"); + if (config_update_url != vendor_section.not_found()) { + res.config_update_url = config_update_url->second.data(); + } + + const auto changelog_url = vendor_section.find("changelog_url"); + if (changelog_url != vendor_section.not_found()) { + res.changelog_url = changelog_url->second.data(); + } + + if (! load_all) { + return res; + } + + // Load printer models + for (auto §ion : tree) { + if (boost::starts_with(section.first, printer_model_key)) { + VendorProfile::PrinterModel model; + model.id = section.first.substr(printer_model_key.size()); + model.name = section.second.get("name", model.id); + + const char *technology_fallback = boost::algorithm::starts_with(model.id, "SL") ? "SLA" : "FFF"; + + auto technology_field = section.second.get("technology", technology_fallback); + if (! ConfigOptionEnum::from_string(technology_field, model.technology)) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Invalid printer technology field: `%2%`") % id % technology_field; + model.technology = ptFFF; + } + + model.family = section.second.get("family", std::string()); + if (model.family.empty() && res.name == "BBL") { + // If no family is specified, it can be inferred for known printers + const auto from_pre_map = pre_family_model_map.find(model.id); + if (from_pre_map != pre_family_model_map.end()) { model.family = from_pre_map->second; } + } +#if 0 + // Remove SLA printers from the initial alpha. + if (model.technology == ptSLA) + continue; +#endif + section.second.get("variants", ""); + const auto variants_field = section.second.get("variants", ""); + std::vector variants; + if (Slic3r::unescape_strings_cstyle(variants_field, variants)) { + for (const std::string &variant_name : variants) { + if (model.variant(variant_name) == nullptr) + model.variants.emplace_back(VendorProfile::PrinterVariant(variant_name)); + } + } else { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Malformed variants field: `%2%`") % id % variants_field; + } + auto default_materials_field = section.second.get("default_materials", ""); + if (default_materials_field.empty()) + default_materials_field = section.second.get("default_filaments", ""); + if (Slic3r::unescape_strings_cstyle(default_materials_field, model.default_materials)) { + Slic3r::sort_remove_duplicates(model.default_materials); + if (! model.default_materials.empty() && model.default_materials.front().empty()) + // An empty material was inserted into the list of default materials. Remove it. + model.default_materials.erase(model.default_materials.begin()); + } else { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Malformed default_materials field: `%2%`") % id % default_materials_field; + } + model.bed_model = section.second.get("bed_model", ""); + model.bed_texture = section.second.get("bed_texture", ""); + if (! model.id.empty() && ! model.variants.empty()) + res.models.push_back(std::move(model)); + } + } + + // Load filaments and sla materials to be installed by default + const auto filaments = tree.find(filaments_section); + if (filaments != tree.not_found()) { + for (auto &pair : filaments->second) { + if (pair.second.data() == "1") { + res.default_filaments.insert(pair.first); + } + } + } + const auto materials = tree.find(materials_section); + if (materials != tree.not_found()) { + for (auto &pair : materials->second) { + if (pair.second.data() == "1") { + res.default_sla_materials.insert(pair.first); + } + } + } + + return res; +} + +std::vector VendorProfile::families() const +{ + std::vector res; + unsigned num_familiies = 0; + + for (auto &model : models) { + if (std::find(res.begin(), res.end(), model.family) == res.end()) { + res.push_back(model.family); + num_familiies++; + } + } + + return res; +} + +// Suffix to be added to a modified preset name in the combo box. +static std::string g_suffix_modified = " (modified)"; +const std::string& Preset::suffix_modified() +{ + return g_suffix_modified; +} + +void Preset::update_suffix_modified(const std::string& new_suffix_modified) +{ + g_suffix_modified = new_suffix_modified; +} +// Remove an optional "(modified)" suffix from a name. +// This converts a UI name to a unique preset identifier. +std::string Preset::remove_suffix_modified(const std::string &name) +{ + return boost::algorithm::starts_with(name, g_suffix_modified) ? + name.substr(g_suffix_modified.size()) : + name; +} + +// Update new extruder fields at the printer profile. +void Preset::normalize(DynamicPrintConfig &config) +{ + size_t n = 1; + if (config.option("single_extruder_multi_material") == nullptr || config.opt_bool("single_extruder_multi_material")) { + // BBS + auto* filament_diameter = dynamic_cast(config.option("filament_diameter")); + if (filament_diameter != nullptr) { + n = filament_diameter->values.size(); + // Loaded the FFF Printer settings. Verify, that all extruder dependent values have enough values. + config.set_num_filaments((unsigned int) n); + } + } else { + auto* nozzle_diameter = dynamic_cast(config.option("nozzle_diameter")); + if (nozzle_diameter != nullptr) { + n = nozzle_diameter->values.size(); + // Loaded the FFF Printer settings. Verify, that all extruder dependent values have enough values. + config.set_num_extruders((unsigned int) n); + } + } + + if (config.option("filament_diameter") != nullptr) { + // This config contains single or multiple filament presets. + // Ensure that the filament preset vector options contain the correct number of values. + const auto &defaults = FullPrintConfig::defaults(); + for (const std::string &key : Preset::filament_options()) { + if (key == "compatible_prints" || key == "compatible_printers") + continue; + if (filament_options_with_variant.find(key) != filament_options_with_variant.end()) + continue; + auto *opt = config.option(key, false); + /*assert(opt != nullptr); + assert(opt->is_vector());*/ + if (opt != nullptr && opt->is_vector()) + static_cast(opt)->resize(n, defaults.option(key)); + } + // The following keys are mandatory for the UI, but they are not part of FullPrintConfig, therefore they are handled separately. + for (const std::string &key : { "filament_settings_id" }) { + auto *opt = config.option(key, false); + assert(opt == nullptr || opt->type() == coStrings); + if (opt != nullptr && opt->type() == coStrings) + static_cast(opt)->values.resize(n, std::string()); + } + } + + handle_legacy_sla(config); +} + +std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config) +{ + std::string incorrect_keys; + for (const std::string &key : config.keys()) + if (! default_config.has(key)) { + if (incorrect_keys.empty()) + incorrect_keys = key; + else { + incorrect_keys += ", "; + incorrect_keys += key; + } + config.erase(key); + } + return incorrect_keys; +} + +std::string Preset::get_type_string(Preset::Type type) +{ + switch (type) { + case Preset::Type::TYPE_FILAMENT: + return PRESET_FILAMENT_NAME; + case Preset::Type::TYPE_PRINT: + return PRESET_PRINT_NAME; + case Preset::Type::TYPE_PRINTER: + return PRESET_PRINTER_NAME; + case Preset::Type::TYPE_PHYSICAL_PRINTER: + return "physical_printer"; + case Preset::Type::TYPE_INVALID: + return "invalid"; + default: + return "invalid"; + } +} + +std::string Preset::get_iot_type_string(Preset::Type type) +{ + switch (type) { + case Preset::Type::TYPE_FILAMENT: + return PRESET_IOT_FILAMENT_TYPE; + case Preset::Type::TYPE_PRINT: + return PRESET_IOT_PRINT_TYPE; + case Preset::Type::TYPE_PRINTER: + return PRESET_IOT_PRINTER_TYPE; + + default: + return "invalid"; + } +} + +//make the type string compatibility with local and iot type string +Preset::Type Preset::get_type_from_string(std::string type_str) +{ + if (type_str.compare(PRESET_PRINT_NAME) == 0 || type_str.compare(PRESET_IOT_PRINT_TYPE) == 0) + return Preset::Type::TYPE_PRINT; + else if (type_str.compare(PRESET_FILAMENT_NAME) == 0 || type_str.compare(PRESET_IOT_FILAMENT_TYPE) == 0) + return Preset::Type::TYPE_FILAMENT; + else if (type_str.compare(PRESET_PRINTER_NAME) == 0 || type_str.compare(PRESET_IOT_PRINTER_TYPE) == 0) + return Preset::Type::TYPE_PRINTER; + else + return Preset::Type::TYPE_INVALID; +} + + +void Preset::load_info(const std::string& file) +{ + try { + boost::property_tree::ptree tree; + boost::nowide::ifstream ifs(file); + boost::property_tree::read_ini(ifs, tree); + if (tree.empty()) return; + for (const boost::property_tree::ptree::value_type &v : tree) { + if (v.first.compare("sync_info") == 0) + this->sync_info = v.second.get_value(); + else if (v.first.compare("user_id") == 0) + this->user_id = v.second.get_value(); + else if (v.first.compare("setting_id") == 0) { + this->setting_id = v.second.get_value(); + if (this->setting_id.compare("null") == 0) + this->setting_id.clear(); + } + else if (v.first.compare("base_id") == 0) { + this->base_id = v.second.get_value(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << file << " and base_id: " << this->base_id; + if (this->base_id.compare("null") == 0) + this->base_id.clear(); + } + else if (v.first.compare("updated_time") == 0) { + std::string time = v.second.get_value(); + this->updated_time = std::atoll(time.c_str()); + } + } + } + catch (...) { + return; + } +} + +void Preset::save_info(std::string file) +{ + //BBS: add project embedded preset logic + if (this->is_project_embedded || this->is_from_bundle()) + return; + if (file.empty()) { + fs::path idx_file(this->file); + idx_file.replace_extension(".info"); + file = idx_file.string(); + } + + boost::nowide::ofstream c; + c.open(file, std::ios::out | std::ios::trunc); + std::string sync_info_to_save; + //BBS: hold is used for stop requesting to server this time + if (this->sync_info.compare("hold") != 0) + sync_info_to_save = this->sync_info; + c << "sync_info" << " = " << sync_info_to_save << std::endl; + c << "user_id" << " = " << this->user_id << std::endl; + c << "setting_id" << " = " << this->setting_id << std::endl; + c << "base_id" << " = " << this->base_id << std::endl; + c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl; + c.close(); +} + +void Preset::remove_files(bool cloud_already_deleted) +{ + //BBS: add project embedded preset logic + if (this->is_project_embedded) { + return; + } + // Erase the preset file. + boost::nowide::remove(this->file.c_str()); + fs::path idx_path(this->file); + idx_path.replace_extension(".info"); + if (fs::exists(idx_path)) { + if (!this->setting_id.empty() && !cloud_already_deleted) { + // Cloud-synced preset - mark for deletion and keep .info file until sync confirms + this->sync_info = "delete"; + this->save_info(idx_path.string()); + } else { + // Local-only preset or cloud already confirmed deletion - safe to delete .info immediately + boost::nowide::remove(idx_path.string().c_str()); + } + } +} + +//BBS: add logic for only difference save +void Preset::save(DynamicPrintConfig* parent_config) +{ + //BBS: add project embedded preset logic + if (this->is_project_embedded) + return; + + // Generate a unique filament_id for user filament presets that don't have one yet. + // Inherited presets (e.g. "My PLA" inheriting "Generic PLA @System") previously had + // no filament_id which caused AMS sync to fall back to the parent's Generic ID. + // Also generate a new ID if filament_id is inherited from the parent (== base_id). + // This happens when a user preset is saved for the first time without having its own ID. + // A user preset needs its own filament_id if: + // - it has no filament_id at all, OR + // - its filament_id does not start with "P" (user preset IDs start with "P", + // system IDs start with GFL/OGFL/GFA/etc.) + bool needs_unique_filament_id = this->is_user() && !this->name.empty() && + this->type == Preset::TYPE_FILAMENT && + (this->filament_id.empty() || this->filament_id.front() != 'P'); + if (needs_unique_filament_id) { + boost::uuids::detail::md5 hash; + boost::uuids::detail::md5::digest_type digest; + hash.process_bytes(this->name.data(), this->name.size()); + hash.get_digest(digest); + const auto char_digest = reinterpret_cast(&digest); + std::string result; + boost::algorithm::hex(char_digest, char_digest + sizeof(boost::uuids::detail::md5::digest_type), std::back_inserter(result)); + this->filament_id = "P" + result.substr(0, 7); + BOOST_LOG_TRIVIAL(info) << "Preset::save: generated filament_id='" << this->filament_id << "' for user preset '" << this->name << "'"; + } + + //BBS: change to json format + //this->config.save(this->file); + std::string from_str; + if (this->is_user()) + from_str = std::string("User"); + else if (this->is_project_embedded) + from_str = std::string("Project"); + else if (this->is_from_bundle()) + from_str = std::string("Bundle"); + else if (this->is_system) + from_str = std::string("System"); + else + from_str = std::string("Default"); + + boost::filesystem::create_directories(fs::path(this->file).parent_path()); + const std::string bare_name = get_preset_bare_name(this->name); + + //BBS: only save difference if it has parent + if (parent_config) { + DynamicPrintConfig temp_config; + std::vector dirty_options = config.diff(*parent_config); + + std::string extruder_id_name, extruder_variant_name; + std::set *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + + if (!extruder_id_name.empty()) { + dirty_options.emplace_back(extruder_id_name); + } + if (!extruder_variant_name.empty()) { + dirty_options.emplace_back(extruder_variant_name); + } + + for (auto option: dirty_options) + { + ConfigOption *opt_src = config.option(option); + ConfigOption *opt_dst = temp_config.option(option, true); + if (opt_dst->is_scalar() || !(opt_dst->nullable())) + opt_dst->set(opt_src); + else { + ConfigOptionVectorBase* opt_vec_src = static_cast(opt_src); + ConfigOptionVectorBase* opt_vec_dst = static_cast(opt_dst); + ConfigOptionVectorBase* opt_vec_inherit = static_cast(parent_config->option(option)); + if (opt_vec_src->size() == 1) + opt_dst->set(opt_src); + else if (key_set1->find(option) != key_set1->end()) { + opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 1); + } + else if (key_set2->find(option) != key_set2->end()) { + opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 2); + } + else + opt_dst->set(opt_src); + } + } + if (!filament_id.empty()) + temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id)); + temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string()); + } else if (!filament_id.empty() && inherits().empty()) { + DynamicPrintConfig temp_config = config; + temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id)); + temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string()); + } else { + this->config.save_to_json(this->file, bare_name, from_str, this->version.to_string()); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id; + + // Bundle presets are synced via bundle_id and don't need individual .info files. + if (! this->is_from_bundle()) { + fs::path idx_file(this->file); + idx_file.replace_extension(".info"); + this->save_info(idx_file.string()); + } +} + +void Preset::reload(Preset const &parent) +{ + DynamicPrintConfig config; + // BBS: change to json format + // ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); + std::map key_values; + std::string reason; + ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable; + try { + ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason); + this->config = parent.config; + this->config.apply(std::move(config)); + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%") % file % err.what(); + } +} + +// Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty. +std::string Preset::label(bool no_alias) const +{ + return (this->is_dirty ? g_suffix_modified : "") + + ((no_alias || this->alias.empty()) ? this->name : this->alias); +} + +bool is_compatible_with_print(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer) +{ + // Orca: we allow cross vendor compatibility + // if (preset.vendor != nullptr && preset.vendor != active_printer.vendor) + // // The current profile has a vendor assigned and it is different from the active print's vendor. + // return false; + auto &condition = preset.preset.compatible_prints_condition(); + auto *compatible_prints = dynamic_cast(preset.preset.config.option("compatible_prints")); + bool has_compatible_prints = compatible_prints != nullptr && ! compatible_prints->values.empty(); + if (! has_compatible_prints && ! condition.empty()) { + try { + return PlaceholderParser::evaluate_boolean_expression(condition, active_print.preset.config); + } catch (const std::runtime_error &err) { + //FIXME in case of an error, return "compatible with everything". + printf("Preset::is_compatible_with_print - parsing error of compatible_prints_condition %s:\n%s\n", active_print.preset.name.c_str(), err.what()); + return true; + } + } + return preset.preset.is_default || active_print.preset.name.empty() || ! has_compatible_prints || + std::find(compatible_prints->values.begin(), compatible_prints->values.end(), active_print.preset.name) != + compatible_prints->values.end(); +} + +//BBS: If one filament or process preset is compatible with one system printer preset, +// then we think this filament or process preset should be compatible with all +// user printer preset which is inherited from this system printer preset. +// Because printer_model and nozzle_diameter in BBL system machine preset +// can't be changed by user. +bool is_compatible_with_parent_printer(const PresetWithVendorProfile& preset, const PresetWithVendorProfile& active_printer) +{ + auto *compatible_printers = dynamic_cast(preset.preset.config.option("compatible_printers")); + bool has_compatible_printers = compatible_printers != nullptr && ! compatible_printers->values.empty(); + //BBS: FIXME only check the parent now, but should check grand-parent as well. + return has_compatible_printers && + std::find(compatible_printers->values.begin(), compatible_printers->values.end(), active_printer.preset.inherits()) != + compatible_printers->values.end(); +} + +bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config) +{ + // Orca: we allow cross vendor compatibility + // if (preset.vendor != nullptr && preset.vendor != active_printer.vendor) + // // The current profile has a vendor assigned and it is different from the active print's vendor. + // return false; + + // Orca: check excluded printers + if (preset.vendor != nullptr && preset.preset.type == Preset::TYPE_FILAMENT) { + const auto& excluded_printers = preset.preset.m_excluded_from; + const auto excluded = preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY && + (excluded_printers.find(active_printer.preset.name) != excluded_printers.end() || + excluded_printers.find(active_printer.preset.inherits()) != excluded_printers.end()); + if (excluded) + return false; + } + auto &condition = preset.preset.compatible_printers_condition(); + auto *compatible_printers = dynamic_cast(preset.preset.config.option("compatible_printers")); + bool has_compatible_printers = compatible_printers != nullptr && ! compatible_printers->values.empty(); + if (! has_compatible_printers && ! condition.empty()) { + try { + return PlaceholderParser::evaluate_boolean_expression(condition, active_printer.preset.config, extra_config); + } catch (const std::runtime_error &err) { + //FIXME in case of an error, return "compatible with everything". + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": parsing error of compatible_printers_condition %1%: %2%")%active_printer.preset.name %err.what(); + return true; + } + } + return preset.preset.is_default || active_printer.preset.name.empty() || !has_compatible_printers || + std::find(compatible_printers->values.begin(), compatible_printers->values.end(), active_printer.preset.name) != + compatible_printers->values.end() || + (!active_printer.preset.is_system && is_compatible_with_parent_printer(preset, active_printer)); +} + +bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer) +{ + DynamicPrintConfig config; + config.set_key_value("printer_preset", new ConfigOptionString(active_printer.preset.name)); + const ConfigOption *opt = active_printer.preset.config.option("nozzle_diameter"); + if (opt) + config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast(opt)->values.size())); + return is_compatible_with_printer(preset, active_printer, &config); +} + +void Preset::set_visible_from_appconfig(const AppConfig &app_config) +{ + //BBS: add config related log + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": name %1%, is_visible %2%")%name % is_visible; + if (vendor == nullptr) { return; } + + if (type == TYPE_PRINTER) { + const std::string &model = config.opt_string("printer_model"); + const std::string &variant = config.opt_string("printer_variant"); + if (model.empty() || variant.empty()) + return; + is_visible = app_config.get_variant(vendor->id, model, variant); + } else if (type == TYPE_FILAMENT || type == TYPE_SLA_MATERIAL) { + const std::string §ion_name = (type == TYPE_FILAMENT) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; + if (app_config.has_section(section_name)) { + // Check whether this profile is marked as "installed" in PrusaSlicer.ini, + // or whether a profile is marked as "installed", which this profile may have been renamed from. + const std::map &installed = app_config.get_section(section_name); + auto has = [&installed](const std::string &name) { + auto it = installed.find(name); + return it != installed.end() && ! it->second.empty(); + }; + is_visible = has(this->name); + for (auto it = this->renamed_from.begin(); ! is_visible && it != this->renamed_from.end(); ++ it) + is_visible = has(*it); + } + } + //BBS: add config related log + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": name %1%, is_visible set to %2%")%name % is_visible; +} + +std::string Preset::get_filament_type(std::string &display_filament_type) +{ + return config.get_filament_type(display_filament_type); +} + +std::string Preset::get_printer_type(PresetBundle *preset_bundle) +{ + if (preset_bundle) { + auto config = &preset_bundle->printers.get_edited_preset().config; + std::string vendor_name; + for (auto vendor_profile : preset_bundle->vendors) { + for (auto vendor_model : vendor_profile.second.models) + if (vendor_model.name == config->opt_string("printer_model")) + { + vendor_name = vendor_profile.first; + return vendor_model.model_id; + } + } + } + return ""; +} + +std::string Preset::get_current_printer_type(PresetBundle *preset_bundle) +{ + if (preset_bundle) { + auto config = &(this->config); + std::string vendor_name; + for (auto vendor_profile : preset_bundle->vendors) { + for (auto vendor_model : vendor_profile.second.models) + if (vendor_model.name == config->opt_string("printer_model")) { + vendor_name = vendor_profile.first; + return vendor_model.model_id; + } + } + } + return ""; +} + +void Preset::get_extruder_names_and_keysets(Type type, std::string& extruder_id_name, std::string& extruder_variant_name, std::set** p_key_set1, std::set** p_key_set2) +{ + if (type == Preset::TYPE_PRINT) { + extruder_id_name = "print_extruder_id"; + extruder_variant_name = "print_extruder_variant"; + *p_key_set1 = &print_options_with_variant; + *p_key_set2 = &empty_options; + } + else if (type == Preset::TYPE_PRINTER) { + extruder_id_name = "printer_extruder_id"; + extruder_variant_name = "printer_extruder_variant"; + *p_key_set1 = &printer_options_with_variant_1; + *p_key_set2 = &printer_options_with_variant_2; + } + else if (type == Preset::TYPE_FILAMENT) { + extruder_variant_name = "filament_extruder_variant"; + *p_key_set1 = &filament_options_with_variant; + *p_key_set2 = &empty_options; + } + else { + *p_key_set1 = &empty_options; + *p_key_set2 = &empty_options; + } +} + +bool Preset::has_lidar(PresetBundle *preset_bundle) +{ + bool has_lidar = false; + if (preset_bundle) { + auto config = &preset_bundle->printers.get_edited_preset().config; + std::string vendor_name; + for (auto vendor_profile : preset_bundle->vendors) { + for (auto vendor_model : vendor_profile.second.models) + if (vendor_model.name == config->opt_string("printer_model")) { + vendor_name = vendor_profile.first; + break; + } + } + if (!vendor_name.empty()) + has_lidar = vendor_name.compare("BBL") == 0 ? true : false; + } + return has_lidar; +} + +BedType Preset::get_default_bed_type(PresetBundle* preset_bundle) +{ + if (config.has("default_bed_type") && !config.opt_string("default_bed_type").empty()) { + try { + std::string str_bed_type = config.opt_string("default_bed_type"); + + // Try parsing as integer first (legacy format) + int bed_type_value = atoi(str_bed_type.c_str()); + if (bed_type_value > 0) { + return BedType(bed_type_value); + } + else { + BOOST_LOG_TRIVIAL(error) << "default_bed_type: invalid bed type: " << str_bed_type; + } + return BedType::btPEI; + + } catch(...) { + ; + } + } + + std::string model_id = this->get_printer_type(preset_bundle); + if (model_id == "BL-P001" || model_id == "BL-P002" || model_id == "C13") { + return BedType::btPC; + } else if (model_id == "C11") { + return BedType::btPEI; + } + return BedType::btPEI; +} + +bool Preset::has_cali_lines(PresetBundle* preset_bundle) +{ + std::string model_id = this->get_printer_type(preset_bundle); + if (model_id == "BL-P001" || model_id == "BL-P002" || model_id == "C13") { + return true; + } + return false; +} + +static std::vector s_Preset_print_options{ + "layer_height", + "initial_layer_print_height", + "wall_loops", + "alternate_extra_wall", + "slice_closing_radius", + "spiral_mode", + "spiral_mode_smooth", + "spiral_mode_max_xy_smoothing", + "spiral_starting_flow_ratio", + "spiral_finishing_flow_ratio", + "slicing_mode", + "top_shell_layers", + "top_shell_thickness", + "top_surface_density", + "bottom_surface_density", + "bottom_shell_layers", + "bottom_shell_thickness", + "extra_perimeters_on_overhangs", + "ensure_vertical_shell_thickness", + "reduce_crossing_wall", + "detect_thin_wall", + "detect_overhang_wall", + "overhang_reverse", + "overhang_reverse_threshold", + "overhang_reverse_internal_only", + "wall_direction", + "seam_position", + "staggered_inner_seams", + "wall_sequence", + "is_infill_first", + "sparse_infill_density", + "fill_multiline", + "gyroid_optimized", + "sparse_infill_pattern", + "lateral_lattice_angle_1", + "lateral_lattice_angle_2", + "infill_overhang_angle", + "top_surface_pattern", + "bottom_surface_pattern", + "infill_direction", + "solid_infill_direction", + "counterbore_hole_bridging", + "infill_shift_step", + "sparse_infill_rotate_template", + "solid_infill_rotate_template", + "symmetric_infill_y_axis", + "skeleton_infill_density", + "infill_lock_depth", + "skin_infill_depth", + "skin_infill_density", + "align_infill_direction_to_model", + "extra_solid_infills", + "minimum_sparse_infill_area", + "reduce_infill_retraction", + "internal_solid_infill_pattern", + "gap_fill_target", + "ironing_type", + "ironing_pattern", + "ironing_flow", + "ironing_speed", + "ironing_spacing", + "ironing_angle", + "ironing_angle_fixed", + "ironing_inset", + "support_ironing", + "support_ironing_pattern", + "support_ironing_flow", + "support_ironing_spacing", + "max_travel_detour_distance", + "fuzzy_skin", "fuzzy_skin_thickness", "fuzzy_skin_point_distance", "fuzzy_skin_first_layer", "fuzzy_skin_noise_type", "fuzzy_skin_mode", "fuzzy_skin_scale", "fuzzy_skin_octaves", "fuzzy_skin_persistence", "fuzzy_skin_ripples_per_layer", "fuzzy_skin_ripple_offset", "fuzzy_skin_layers_between_ripple_offset", + "max_volumetric_extrusion_rate_slope", "max_volumetric_extrusion_rate_slope_segment_length","extrusion_rate_smoothing_external_perimeter_only", + "inner_wall_speed", "outer_wall_speed", "sparse_infill_speed", "internal_solid_infill_speed", + "top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed", + "bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed", + "outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield", + "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", + "raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion", + "support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style", + // BBS + "print_extruder_id", + "print_extruder_variant", + "independent_support_layer_height", + "support_angle", + "support_interface_top_layers", + "support_interface_bottom_layers", + "support_interface_pattern", + "support_interface_spacing", + "support_interface_loop_pattern", + "support_top_z_distance", + "support_on_build_plate_only", + "support_critical_regions_only", + "bridge_no_support", + "thick_bridges", + "thick_internal_bridges", + "dont_filter_internal_bridges", + "enable_extra_bridge_layer", + "max_bridge_length", + "print_sequence", + "print_order", + "support_remove_small_overhang", + "filename_format", + "wall_filament", + "support_bottom_z_distance", + "sparse_infill_filament", + "solid_infill_filament", + "support_filament", + "support_interface_filament", + "support_interface_not_for_body", + "ooze_prevention", + "standby_temperature_delta", + "preheat_time", + "preheat_steps", + "interface_shells", + "line_width", + "initial_layer_line_width", + "inner_wall_line_width", + "outer_wall_line_width", + "sparse_infill_line_width", + "internal_solid_infill_line_width", + "skin_infill_line_width", + "skeleton_infill_line_width", + "top_surface_line_width", + "support_line_width", + "infill_wall_overlap", + "top_bottom_infill_wall_overlap", + "bridge_flow", + "internal_bridge_flow", + "elefant_foot_compensation", + "elefant_foot_compensation_layers", + "elefant_foot_layers_density", + "xy_contour_compensation", + "xy_hole_compensation", + "resolution", + "enable_prime_tower", + "prime_tower_enable_framework", + "prime_tower_width", + "prime_tower_brim_width", + "prime_tower_skip_points", + "prime_volume", + "prime_tower_infill_gap", + "prime_tower_flat_ironing", + "enable_tower_interface_features", + "enable_tower_interface_cooldown_during_tower", + "wipe_tower_no_sparse_layers", + "compatible_printers", + "compatible_printers_condition", + "inherits", + "flush_into_infill", + "flush_into_objects", + "flush_into_support", + "tree_support_branch_angle", + "tree_support_angle_slow", + "tree_support_wall_count", + "tree_support_top_rate", + "tree_support_branch_distance", + "tree_support_tip_diameter", + "tree_support_branch_diameter", + "tree_support_branch_diameter_angle", + "detect_narrow_internal_solid_infill", + "gcode_add_line_number", + "enable_arc_fitting", + "precise_z_height", + "infill_combination", + "infill_combination_max_layer_height", /*"adaptive_layer_height",*/ + "support_bottom_interface_spacing", + "enable_overhang_speed", + "slowdown_for_curled_perimeters", + "overhang_1_4_speed", + "overhang_2_4_speed", + "overhang_3_4_speed", + "overhang_4_4_speed", + "initial_layer_infill_speed", + "only_one_wall_top", + "timelapse_type", + "wall_generator", + "wall_transition_length", + "wall_transition_filter_deviation", + "wall_transition_angle", + "wall_distribution_count", + "min_feature_size", + "min_bead_width", + "post_process", + "process_change_extrusion_role_gcode", + "min_length_factor", + "wall_maximum_resolution", + "wall_maximum_deviation", + "small_perimeter_speed", + "small_perimeter_threshold", + "bridge_angle", + "internal_bridge_angle", + "filter_out_gap_fill", + "travel_acceleration", + "inner_wall_acceleration", + "min_width_top_surface", + "default_jerk", + "outer_wall_jerk", + "inner_wall_jerk", + "infill_jerk", + "top_surface_jerk", + "initial_layer_jerk", + "travel_jerk", + "default_junction_deviation", + "top_solid_infill_flow_ratio", + "bottom_solid_infill_flow_ratio", + "only_one_wall_first_layer", + "print_flow_ratio", + "seam_gap", + "set_other_flow_ratios", + "first_layer_flow_ratio", + "outer_wall_flow_ratio", + "inner_wall_flow_ratio", + "overhang_flow_ratio", + "sparse_infill_flow_ratio", + "internal_solid_infill_flow_ratio", + "gap_fill_flow_ratio", + "support_flow_ratio", + "support_interface_flow_ratio", + "role_based_wipe_speed", + "wipe_speed", + "accel_to_decel_enable", + "accel_to_decel_factor", + "wipe_on_loops", + "wipe_before_external_loop", + "bridge_density", + "internal_bridge_density", + "precise_outer_wall", + "bridge_acceleration", + "sparse_infill_acceleration", + "internal_solid_infill_acceleration", + "tree_support_auto_brim", + "tree_support_brim_width", + "gcode_comments", + "gcode_label_objects", + "initial_layer_travel_speed", + "initial_layer_travel_acceleration", + "initial_layer_travel_jerk", + "exclude_object", + "slow_down_layers", + "infill_anchor", + "infill_anchor_max", + "initial_layer_min_bead_width", + "make_overhang_printable", + "make_overhang_printable_angle", + "make_overhang_printable_hole_size", + "notes", + "wipe_tower_cone_angle", + "wipe_tower_extra_spacing", + "wipe_tower_max_purge_speed", + "wipe_tower_wall_type", + "wipe_tower_extra_rib_length", + "wipe_tower_rib_width", + "wipe_tower_fillet_wall", + "wipe_tower_filament", + "wiping_volumes_extruders", + "wipe_tower_bridging", + "wipe_tower_extra_flow", + "single_extruder_multi_material_priming", + "wipe_tower_rotation_angle", + "tree_support_branch_distance_organic", + "tree_support_branch_diameter_organic", + "tree_support_branch_angle_organic", + "hole_to_polyhole", + "hole_to_polyhole_threshold", + "hole_to_polyhole_twisted", + "mmu_segmented_region_max_width", + "mmu_segmented_region_interlocking_depth", + "small_area_infill_flow_compensation", + "small_area_infill_flow_compensation_model", + "enable_wrapping_detection", + "seam_slope_type", + "seam_slope_conditional", + "scarf_angle_threshold", + "scarf_joint_speed", + "scarf_joint_flow_ratio", + "seam_slope_start_height", + "seam_slope_entire_loop", + "seam_slope_min_length", + "seam_slope_steps", + "seam_slope_inner_walls", + "scarf_overhang_threshold", + "interlocking_beam", + "interlocking_orientation", + "interlocking_beam_layer_count", + "interlocking_depth", + "interlocking_boundary_avoidance", + "interlocking_beam_width", + "calib_flowrate_topinfill_special_order", + // Z Anti-Aliasing (ZAA) + "zaa_enabled", + "zaa_minimize_perimeter_height", + "zaa_dont_alternate_fill_direction", + "zaa_min_z", + "ironing_expansion", +}; + +static std::vector s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type", + "filament_soluble", "filament_is_support", "filament_printable", + "filament_max_volumetric_speed", "filament_adaptive_volumetric_speed", + "filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower", + "filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume", + "filament_tower_interface_print_temp", + "nozzle_temperature", "nozzle_temperature_initial_layer", + // BBS + "cool_plate_temp", "textured_cool_plate_temp", "eng_plate_temp", "hot_plate_temp", "textured_plate_temp", "cool_plate_temp_initial_layer", "textured_cool_plate_temp_initial_layer", "eng_plate_temp_initial_layer", "hot_plate_temp_initial_layer", "textured_plate_temp_initial_layer", "supertack_plate_temp_initial_layer", "supertack_plate_temp", + // "bed_type", + //BBS:temperature_vitrification + "temperature_vitrification", "reduce_fan_stop_start_freq","dont_slow_down_outer_wall", "slow_down_for_layer_cooling", "fan_min_speed", + "fan_max_speed", "enable_overhang_bridge_fan", "overhang_fan_speed", "overhang_fan_threshold", "close_fan_the_first_x_layers", "close_additional_fan_first_x_layers", "first_x_layer_fan_speed", "full_fan_speed_layer", "additional_fan_full_speed_layer", "fan_cooling_layer_time", "slow_down_layer_time", "slow_down_min_speed", + "filament_start_gcode", "filament_end_gcode", "filament_change_extrusion_role_gcode", + //exhaust fan control + "activate_air_filtration","activate_air_filtration_during_print","activate_air_filtration_on_completion","during_print_exhaust_fan_speed","complete_print_exhaust_fan_speed", + // Retract overrides + "filament_retraction_length", "filament_z_hop", "filament_z_hop_types", "filament_retract_lift_above", "filament_retract_lift_below", "filament_retract_lift_enforce", "filament_retraction_speed", "filament_deretraction_speed", "filament_retract_restart_extra", "filament_retraction_minimum_travel", + "filament_retract_when_changing_layer", "filament_wipe", "filament_retract_before_wipe", + // Profile compatibility + "filament_vendor", "compatible_prints", "compatible_prints_condition", "compatible_printers", "compatible_printers_condition", "inherits", + //BBS + "filament_wipe_distance", "additional_cooling_fan_speed", + "nozzle_temperature_range_low", "nozzle_temperature_range_high", + "filament_extruder_variant", + //SoftFever + "enable_pressure_advance", "pressure_advance","adaptive_pressure_advance","adaptive_pressure_advance_model","adaptive_pressure_advance_overhangs", "adaptive_pressure_advance_bridges","chamber_temperature", "filament_shrink","filament_shrinkage_compensation_z", "support_material_interface_fan_speed","internal_bridge_fan_speed", "filament_notes" /*,"filament_seam_gap"*/, + "ironing_fan_speed", + // Filament ironing overrides + "filament_ironing_flow", "filament_ironing_spacing", "filament_ironing_inset", "filament_ironing_speed", + "filament_loading_speed", "filament_loading_speed_start", + "filament_unloading_speed", "filament_unloading_speed_start", "filament_toolchange_delay", "filament_cooling_moves", "filament_stamping_loading_speed", "filament_stamping_distance", + "filament_cooling_initial_speed", "filament_cooling_final_speed", "filament_ramming_parameters", + "filament_multitool_ramming", "filament_multitool_ramming_volume", "filament_multitool_ramming_flow", "activate_chamber_temp_control", + "filament_long_retractions_when_cut","filament_retraction_distances_when_cut", "idle_temperature", + //BBS filament change length while the extruder color + "filament_change_length","filament_flush_volumetric_speed","filament_flush_temp", "filament_cooling_before_tower", + "long_retractions_when_ec", "retraction_distances_when_ec" + }; + +static std::vector s_Preset_machine_limits_options { + "machine_max_acceleration_extruding", "machine_max_acceleration_retracting", "machine_max_acceleration_travel", + "machine_max_acceleration_x", "machine_max_acceleration_y", "machine_max_acceleration_z", "machine_max_acceleration_e", + "machine_max_speed_x", "machine_max_speed_y", "machine_max_speed_z", "machine_max_speed_e", + "machine_min_extruding_rate", "machine_min_travel_rate", + "machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e", + "machine_max_junction_deviation", + //resonance avoidance ported from qidi slicer + "resonance_avoidance", "min_resonance_avoidance_speed", "max_resonance_avoidance_speed", + // Orca: input shaping + "input_shaping_emit", "input_shaping_type", "input_shaping_freq_x", "input_shaping_freq_y", "input_shaping_damp_x", "input_shaping_damp_y", +}; + +static std::vector s_Preset_printer_options { + "printer_technology", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", + "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", + "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", + "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", + "printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", + "nozzle_height", "master_extruder_id", + "default_print_profile", "inherits", + "silent_mode", + "scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode", + "nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure", + "best_object_pos", "head_wrap_detect_zone", + "host_type", "print_host", "printhost_apikey", "bbl_use_printhost", "printer_agent", + "print_host_webui", + "printhost_cafile","printhost_port","printhost_authorization_type", + "printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format", + "use_relative_e_distances", "extruder_type", "use_firmware_retraction", "printer_notes", + "grab_length", "support_object_skip_flush", "physical_extruder_map", + "cooling_tube_retraction", + "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower", + "z_offset", + "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", + "bed_temperature_formula", "nozzle_flush_dataset" + }; + +static std::vector s_Preset_sla_print_options { + "layer_height", + "faded_layers", + "supports_enable", + "support_head_front_diameter", + "support_head_penetration", + "support_head_width", + "support_pillar_diameter", + "support_small_pillar_diameter_percent", + "support_max_bridges_on_pillar", + "support_pillar_connection_mode", + "support_buildplate_only", + "support_pillar_widening_factor", + "support_base_diameter", + "support_base_height", + "support_base_safety_distance", + "support_critical_angle", + "support_max_bridge_length", + "support_max_pillar_link_distance", + "support_object_elevation", + "support_points_density_relative", + "support_points_minimal_distance", + "slice_closing_radius", + "pad_enable", + "pad_wall_thickness", + "pad_wall_height", + "pad_brim_size", + "pad_max_merge_distance", + // "pad_edge_radius", + "pad_wall_slope", + "pad_object_gap", + "pad_around_object", + "pad_around_object_everywhere", + "pad_object_connector_stride", + "pad_object_connector_width", + "pad_object_connector_penetration", + "hollowing_enable", + "hollowing_min_thickness", + "hollowing_quality", + "hollowing_closing_distance", + "filename_format", + "default_sla_print_profile", + "compatible_printers", + "compatible_printers_condition", + "inherits" +}; + +static std::vector s_Preset_sla_material_options { + "material_colour", + "material_type", + "initial_layer_height", + "bottle_cost", + "bottle_volume", + "bottle_weight", + "material_density", + "exposure_time", + "initial_exposure_time", + "material_correction", + "material_correction_x", + "material_correction_y", + "material_correction_z", + "material_vendor", + "material_print_speed", + "default_sla_material_profile", + "compatible_prints", "compatible_prints_condition", + "compatible_printers", "compatible_printers_condition", "inherits" +}; + +static std::vector s_Preset_sla_printer_options { + "printer_technology", + "printable_area","bed_custom_texture", "bed_custom_model", "printable_height", + "display_width", "display_height", "display_pixels_x", "display_pixels_y", + "display_mirror_x", "display_mirror_y", + "display_orientation", + "fast_tilt_time", "slow_tilt_time", "area_fill", + "relative_correction", + "relative_correction_x", + "relative_correction_y", + "relative_correction_z", + "absolute_correction", + "elefant_foot_compensation", + "elefant_foot_min_width", + "gamma_correction", + "min_exposure_time", "max_exposure_time", + "min_initial_exposure_time", "max_initial_exposure_time", + "inherits" +}; + +const std::vector& Preset::print_options() { return s_Preset_print_options; } +const std::vector& Preset::filament_options() { return s_Preset_filament_options; } +const std::vector& Preset::machine_limits_options() { return s_Preset_machine_limits_options; } +// The following nozzle options of a printer profile will be adjusted to match the size +// of the nozzle_diameter vector. +const std::vector& Preset::nozzle_options() { return print_config_def.extruder_option_keys(); } +const std::vector& Preset::sla_print_options() { return s_Preset_sla_print_options; } +const std::vector& Preset::sla_material_options() { return s_Preset_sla_material_options; } +const std::vector& Preset::sla_printer_options() { return s_Preset_sla_printer_options; } + +const std::vector& Preset::printer_options() +{ + static std::vector s_opts = [](){ + std::vector opts = s_Preset_printer_options; + append(opts, s_Preset_machine_limits_options); + append(opts, Preset::nozzle_options()); + return opts; + }(); + return s_opts; +} + +PresetCollection::PresetCollection(Preset::Type type, const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name) : + m_type(type), + m_edited_preset(type, "", false), + m_saved_preset(type, "", false), + m_idx_selected(0) +{ + // Insert just the default preset. + this->add_default_preset(keys, defaults, default_name); + m_edited_preset.config.apply(m_presets.front().config); + update_saved_preset_from_current_preset(); +} + + //BBS: add operator= implemention +PresetCollection& PresetCollection::operator=(const PresetCollection &rhs) +{ + m_type = rhs.m_type; + m_presets = rhs.m_presets; + m_map_alias_to_profile_name = rhs.m_map_alias_to_profile_name; + m_map_system_profile_renamed = rhs.m_map_system_profile_renamed; + m_edited_preset = rhs.m_edited_preset; + m_saved_preset = rhs.m_saved_preset; + m_idx_selected = rhs.m_idx_selected; + m_default_suppressed = rhs.m_default_suppressed; + m_num_default_presets = rhs.m_num_default_presets; + m_dir_path = rhs.m_dir_path; + + return *this; +} + +void PresetCollection::reset(bool delete_files) +{ + //BBS: add lock logic for sync preset in background + lock(); + if (m_presets.size() > m_num_default_presets) { + if (delete_files) { + // Erase the preset files. + for (Preset &preset : m_presets) + if (! preset.is_default && ! preset.is_external && ! preset.is_system) { + //BBS remove idx and ini files + preset.remove_files(); + } + } + // Don't use m_presets.resize() here as it requires a default constructor for Preset. + m_presets.erase(m_presets.begin() + m_num_default_presets, m_presets.end()); + this->select_preset(0); + } + //BBS: add lock logic for sync preset in background + unlock(); + m_map_alias_to_profile_name.clear(); + m_map_system_profile_renamed.clear(); +} + +void PresetCollection::add_default_preset(const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name) +{ + // Insert just the default preset. + m_presets.emplace_back(Preset(this->type(), preset_name, true)); + m_presets.back().config.apply_only(defaults, keys.empty() ? defaults.keys() : keys); + m_presets.back().loaded = true; + ++ m_num_default_presets; +} + +std::string PresetCollection::canonical_preset_name(const std::string &name, const PresetOrigin &load_origin) const +{ + const ParsedName parsed = parse_preset_name(name); + PresetOrigin origin = load_origin; + if (origin.kind == PresetOrigin::Kind::Auto) { + origin.kind = parsed.kind; + origin.bundle_id = parsed.bundle_id; + } else if (origin.is_bundle() && origin.bundle_id.empty()) { + origin.bundle_id = parsed.bundle_id; + } + return get_preset_canonical_name(parsed.bare, origin); +} + +// Load all presets found in dir_path. +// Throws an exception on error. +void PresetCollection::load_presets( + const std::string &dir_path, const std::string &subdir, + PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule substitution_rule, + std::function preset_loaded_fn, const PresetOrigin &load_origin) +{ + // Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points, + // see https://github.com/prusa3d/PrusaSlicer/issues/732 + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(dir_path) / subdir).make_preferred(); + const PresetOrigin resolved_origin = detect_origin_from_path(dir, load_origin); + + // Load custom roots first + if (fs::exists(dir / "base")) { + load_presets(dir.string(), "base", substitutions, substitution_rule, nullptr, resolved_origin); + } + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, load presets from %1%, current type %2%")%dir %Preset::get_type_string(m_type); + //BBS do not parse folder if not exists + m_dir_path = dir.string(); + if (!fs::exists(dir)) { + fs::create_directory(dir); + return; + } + + std::string errors_cummulative; + // Store the loaded presets into a new vector, otherwise the binary search for already existing presets would be broken. + // (see the "Preset already present, not loading" message). + std::deque presets_loaded; + + //BBS: get the extruder related info for this preset collection + std::string extruder_id_name, extruder_variant_name; + std::set *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + + //BBS: change to json format + for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) + { + std::string file_name = dir_entry.path().filename().string(); + //if (Slic3r::is_ini_file(dir_entry)) { + if (Slic3r::is_json_file(file_name)) { + // Remove the .ini suffix. + std::string name = file_name.erase(file_name.size() - 5); + std::string canonical_name = this->canonical_preset_name(name, resolved_origin); + if (this->find_preset(canonical_name, false)) { + // This happens when there's is a preset (most likely legacy one) with the same name as a system preset + // that's already been loaded from a bundle. + BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name; + continue; + } + try { + Preset preset(m_type, canonical_name, false); + preset.bundle_id = resolved_origin.bundle_id; + preset.file = dir_entry.path().string(); + // Load the preset file, apply preset values on top of defaults. + try { + fs::path idx_path(preset.file); + idx_path.replace_extension(".info"); + if (fs::exists(idx_path)) { + preset.load_info(idx_path.string()); + } + DynamicPrintConfig config; + //BBS: change to json format + //ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); + std::map key_values; + std::string reason; + ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason); + if (! config_substitutions.empty()) + substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) }); + if (!reason.empty()) { + fs::path file_path(preset.file); + if (fs::exists(file_path)) + fs::remove(file_path); + file_path.replace_extension(".info"); + if (fs::exists(file_path)) + fs::remove(file_path); + BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file; + ++m_errors; + continue; + } + + std::string version_str = key_values[BBL_JSON_KEY_VERSION]; + boost::optional version = Semver::parse(version_str); + if (!version) continue; + preset.version = *version; + + if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end()) + preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID]; + if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end()) + preset.description = key_values[BBL_JSON_KEY_DESCRIPTION]; + if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end()) + preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false"; + + //Orca: find and use the inherit config as the base + Preset* inherit_preset = nullptr; + ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS); + + // check inherits_config + if (inherits_config) { + ConfigOptionString * option_str = dynamic_cast (inherits_config); + std::string inherits_value = option_str->value; + // Orca: try to find if the parent preset has been renamed + inherit_preset = this->find_preset2(inherits_value); + } else { + ; + } + const Preset& default_preset = this->default_preset_for(config); + if (inherit_preset) { + preset.config = inherit_preset->config; + // Only inherit filament_id from parent if this preset has no own ID in JSON. + // User presets with a P-prefix ID (generated by Preset::save) must keep their own ID. + if (preset.filament_id.empty()) + preset.filament_id = inherit_preset->filament_id; + extend_default_config_length(config, false, {}); + preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); + } + else { + auto inherits_config2 = dynamic_cast(inherits_config); + if ((inherits_config2 && !inherits_config2->value.empty())) { + BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%inherits_config2->value %preset.file; + ++m_errors; + continue; + } + // We support custom root preset now + // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. + preset.config = default_preset.config; + preset.config.apply(std::move(config)); + extend_default_config_length(preset.config, true, default_preset.config); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id; + + Preset::normalize(preset.config); + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) + << "Error in a preset file: The preset \"" << preset.file + << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; + } + + if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) { + auto compatible_printers = dynamic_cast(preset.config.option("compatible_printers", true)); + if (compatible_printers && compatible_printers->values.empty()) { + size_t at_pos = name.find('@'); + if (at_pos != std::string::npos && at_pos + 1 < name.length()) { + compatible_printers->values.push_back(name.substr(at_pos + 1)); + preset.save(nullptr); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name; + } + } + } + + preset.loaded = true; + //BBS: add some workaround for previous incorrect settings + if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id)) + preset.setting_id.clear(); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible; + // add alias for custom filament preset + set_custom_preset_alias(preset); + } catch (const std::ifstream::failure &err) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what(); + fs::path file_path(preset.file); + if (fs::exists(file_path)) + fs::remove(file_path); + file_path.replace_extension(".info"); + if (fs::exists(file_path)) + fs::remove(file_path); + //throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what()); + } catch (const std::runtime_error &err) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what(); + //throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what()); + fs::path file_path(preset.file); + if (fs::exists(file_path)) + fs::remove(file_path); + file_path.replace_extension(".info"); + if (fs::exists(file_path)) + fs::remove(file_path); + } + + if (preset_loaded_fn != nullptr) + preset_loaded_fn(preset); + + presets_loaded.emplace_back(preset); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name; + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + } + } + } + if (presets_loaded.size() > 0) + m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end())); + sort_presets(); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": loaded %1% presets from %2%, type %3%")%presets_loaded.size() %dir %Preset::get_type_string(m_type); + //this->select_preset(first_visible_idx()); + if (! errors_cummulative.empty()) + throw Slic3r::RuntimeError(errors_cummulative); +} + +//BBS: add function to generate differed preset for save +//the pointer should be freed by the caller +Preset* PresetCollection::get_preset_differed_for_save(Preset& preset) +{ + if (preset.is_system || preset.is_default) + return nullptr; + + Preset* new_preset = nullptr; + //BBS: only save difference for user preset + std::string& inherits = preset.inherits(); + Preset* parent_preset = nullptr; + if (!inherits.empty()) { + parent_preset = this->find_preset(inherits, false, true); + } + if (parent_preset) { + new_preset = new Preset(); + *new_preset = preset; + + DynamicPrintConfig temp_config; + std::vector dirty_options = preset.config.diff(parent_preset->config); + + std::string extruder_id_name, extruder_variant_name; + std::set *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + + if (!extruder_id_name.empty()) { + dirty_options.emplace_back(extruder_id_name); + } + if (!extruder_variant_name.empty()) { + dirty_options.emplace_back(extruder_variant_name); + } + + for (auto option: dirty_options) + { + ConfigOption *opt_src = preset.config.option(option); + ConfigOption *opt_dst = temp_config.option(option, true); + if (opt_dst->is_scalar() || !(opt_dst->nullable())) + opt_dst->set(opt_src); + else { + ConfigOptionVectorBase* opt_vec_src = static_cast(opt_src); + ConfigOptionVectorBase* opt_vec_dst = static_cast(opt_dst); + ConfigOptionVectorBase* opt_vec_inherit = static_cast(parent_preset->config.option(option)); + if (opt_vec_src->size() == 1) + opt_dst->set(opt_src); + else if (key_set1->find(option) != key_set1->end()) { + opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 1); + } + else if (key_set2->find(option) != key_set2->end()) { + opt_vec_dst->set_with_nil(opt_vec_src, opt_vec_inherit, 2); + } + else + opt_dst->set(opt_src); + } + } + + new_preset->config = temp_config; + } + + return new_preset; +} + +//BBS:get the differencen values to update +int PresetCollection::get_differed_values_to_update(Preset& preset, std::map& key_values) +{ + if (preset.is_system || preset.is_default || preset.is_project_embedded) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" Error: not a user preset! Should not happen, name %1%") %preset.name; + ++m_errors; + return -1; + } + + //BBS: only save difference for user preset + std::string& inherit_preset = preset.inherits(); + Preset* parent_preset = nullptr; + if (!inherit_preset.empty()) { + parent_preset = this->find_preset(inherit_preset, false, true); + } + if (parent_preset) { + DynamicPrintConfig temp_config; + std::vector dirty_options = preset.config.diff(parent_preset->config); + + for (auto option: dirty_options) + { + ConfigOption *opt_src = preset.config.option(option); + if (opt_src) + key_values[option] = opt_src->serialize(); + } + } + else { + for (auto iter = preset.config.cbegin(); iter != preset.config.cend(); ++iter) + { + key_values[iter->first] = iter->second->serialize(); + } + } + + //add other values + key_values[BBL_JSON_KEY_VERSION] = preset.version.to_string(); + if (!preset.base_id.empty()) { + key_values[BBL_JSON_KEY_BASE_ID] = preset.base_id; + } else { + key_values.erase(BBL_JSON_KEY_BASE_ID); + if (get_preset_base(preset) == &preset && !preset.filament_id.empty()) { + key_values[BBL_JSON_KEY_FILAMENT_ID] = preset.filament_id; + } + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " uploading user preset name is: " << preset.name << "and create filament_id is: " << preset.filament_id + << " and base_id is: " << preset.base_id; + key_values[ORCA_JSON_KEY_UPDATE_TIME] = std::to_string(preset.updated_time); + key_values[BBL_JSON_KEY_TYPE] = Preset::get_iot_type_string(preset.type); + return 0; +} + +//BBS: save user presets to local +void PresetCollection::load_project_embedded_presets(std::vector& project_presets, const std::string& type, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule) +{ + std::string errors_cummulative; + // Store the loaded presets into a new vector, otherwise the binary search for already existing presets would be broken. + // (see the "Preset already present, not loading" message). + std::deque presets_loaded; + std::vector::iterator it; + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , total preset counts %2%")%Preset::get_type_string(m_type) %project_presets.size(); + std::string extruder_id_name, extruder_variant_name; + std::set *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + + lock(); + for (it = project_presets.begin(); it != project_presets.end(); it++) { + Preset* preset = *it; + if (preset->type != Preset::get_type_from_string(type)) continue; + if (!preset->is_project_embedded) continue; + std::string name = preset->name; + if (this->find_preset(name, false)) { + BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << name; + continue; + } + try { + DynamicPrintConfig config = preset->config; + if (preset->loading_substitutions && ! preset->loading_substitutions->empty()) { + substitutions.push_back({ preset->name, m_type, PresetConfigSubstitutions::Source::ProjectFile, preset->name, std::move(*(preset->loading_substitutions))}); + free(preset->loading_substitutions); + preset->loading_substitutions = NULL; + } + //BBS: use inherit config as the base + Preset* inherit_preset = nullptr; + ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS); + if (inherits_config) { + ConfigOptionString * option_str = dynamic_cast (inherits_config); + std::string inherits_value = option_str->value; + /*size_t pos = inherits_value.find_first_of('*'); + if (pos != std::string::npos) { + inherits_value.replace(pos, 1, 1, '~'); + option_str->value = inherits_value; + }*/ + inherit_preset = this->find_preset2(inherits_value, true); + } + const Preset& default_preset = this->default_preset_for(config); + if (inherit_preset) { + preset->config = inherit_preset->config; + preset->filament_id = inherit_preset->filament_id; + } + else { + // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. + //BBS 202407: don't load project embedded preset when can not find inherit + //preset->config = default_preset.config; + BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file; + continue; + } + preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); + //preset->config.apply(std::move(config)); + Preset::normalize(preset->config); + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(preset->config, default_preset.config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a preset file: The preset \"" << preset->name + << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; + } + preset->loaded = true; + presets_loaded.emplace_back(*preset); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", %1% got preset, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset->name %preset->file %preset->is_system %preset->is_default %preset->is_visible; + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + } + } + + m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end())); + sort_presets(); + //don't select it here + //this->select_preset(first_visible_idx()); + unlock(); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, %1% got %2% presets, errors_cummulative %3%")%Preset::get_type_string(m_type) %presets_loaded.size() %errors_cummulative; + if (! errors_cummulative.empty()) + throw Slic3r::RuntimeError(errors_cummulative); +} + +//BBS: get project embedded presets from +std::vector PresetCollection::get_project_embedded_presets() +{ + std::vector project_presets; + + lock(); + for (Preset &preset : m_presets) { + //if (preset.type != Preset::get_type_from_string(type)) continue; + if (!preset.is_project_embedded) continue; + + Preset* new_preset = get_preset_differed_for_save(preset); + + if (new_preset) + project_presets.push_back(new_preset); + } + unlock(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , total preset counts %2%")%Preset::get_type_string(m_type) %project_presets.size(); + return project_presets; +} + +//BBS: reset project embedded presets +bool PresetCollection::reset_project_embedded_presets() +{ + std::deque::iterator it = m_presets.begin(); + bool re_select = false; + int count = -1; + + lock(); + while ( it!=m_presets.end() ) + { + count++; + //if (preset.type != Preset::get_type_from_string(type)) continue; + if (it->is_project_embedded) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" type %1% , delete preset %2%")%Preset::get_type_string(m_type) % it->name; + if ((!re_select) && (m_idx_selected == count)) + re_select = true; + if (m_idx_selected > count) { + m_idx_selected--; + count--; + } + it = m_presets.erase(it); + } + else + it++; + } + + if (re_select) + m_idx_selected = -1; + + unlock(); + + return re_select; +} + +void PresetCollection::set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time) +{ + lock(); + const std::string canonical_name = this->canonical_preset_name(name); + for (auto it = m_presets.begin(); it != m_presets.end(); it++) { + Preset* preset = &m_presets[it - m_presets.begin()]; + if (preset->name == canonical_name) { + if (syncinfo.empty()) + preset->sync_info.clear(); + else + preset->sync_info = syncinfo; + if (get_preset_base(*preset) == preset) { + for (auto & preset2 : m_presets) + if (preset2.inherits() == preset->name) { + preset2.base_id = setting_id; + preset2.save_info(); + } + } + preset->setting_id = setting_id; + if (update_time > 0) + preset->updated_time = update_time; + preset->sync_info == "update" ? preset->save(nullptr) : preset->save_info(); + break; + } + } + unlock(); +} + +bool PresetCollection::need_sync(std::string name, std::string setting_id, long long update_time) +{ + lock(); + auto preset = find_preset(name, false, true); + bool need = preset == nullptr || preset->setting_id != setting_id || preset->updated_time < update_time; + unlock(); + return need; +} + +//BBS: get user presets +int PresetCollection::get_user_presets(PresetBundle *preset_bundle, std::vector &result_presets) +{ + int count = 0; + result_presets.clear(); + + lock(); + for (Preset &preset : m_presets) { + if (!preset.is_user()) continue; + if (preset.base_id.empty() && preset.inherits() != "") continue; + if (!preset.setting_id.empty() && preset.sync_info.empty()) continue; + //if (!preset.is_bbl_vendor_preset(preset_bundle)) continue; + if (preset.sync_info == "hold") continue; + + result_presets.push_back(preset); + count++; + } + unlock(); + + return count; +} + +//BBS: update user presets directory +void PresetCollection::update_user_presets_directory(const std::string& dir_path, const std::string& type) +{ + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(dir_path) / type).make_preferred(); + + if (!fs::exists(dir)) + fs::create_directory(dir); + + m_dir_path = dir.string(); +} + +//BBS: save user presets to local +void PresetCollection::save_user_presets(const std::string& dir_path, const std::string& type, std::map& need_to_delete_list) +{ + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(dir_path) / type).make_preferred(); + + if (!fs::exists(dir)) + fs::create_directory(dir); + + m_dir_path = dir.string(); + + std::vector delete_name_list; + //std::map::iterator it; + //for (it = my_presets.begin(); it != my_presets.end(); it++) { + for (auto it = m_presets.begin(); it != m_presets.end(); it++) { + Preset* preset = &m_presets[it - m_presets.begin()]; + if (!preset->is_user()) continue; + if (preset->sync_info != "save") continue; + preset->sync_info.clear(); + preset->file = path_for_preset(*preset); + + //BBS: only save difference for user preset + std::string inherits = Preset::inherits(preset->config); + if (inherits.empty()) { + // We support custom root preset now + //BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" can not find inherits for %1% , should not happen")%preset->name; + //// BBS add sync info + //preset->sync_info = "delete"; + //need_to_delete_list.push_back(preset->setting_id); + //delete_name_list.push_back(preset->name); + preset->save(nullptr); + continue; + } + Preset* parent_preset = this->find_preset(inherits, false, true); + if (!parent_preset) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" can not find parent preset for %1% , inherits %2%")%preset->name %inherits; + continue; + } + + if (preset->base_id.empty()) + preset->base_id = parent_preset->setting_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << preset->name << " filament_id: " << preset->filament_id << " base_id: " << preset->base_id; + preset->save(&(parent_preset->config)); + } + + for (auto delete_name: delete_name_list) + { + this->delete_preset(delete_name); + } + delete_name_list.clear(); + + return; +} + +//BBS: load one user preset from key-values +bool PresetCollection::load_user_preset(std::string name, std::map preset_values, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, const PresetOrigin &load_origin) +{ + std::string errors_cummulative; + // Store the loaded presets into a new vector, otherwise the binary search for already existing presets would be broken. + // (see the "Preset already present, not loading" message). + //std::deque presets_loaded; + int count = 0; + const std::string canonical_name = this->canonical_preset_name(name, load_origin); + auto update_alias = [this](Preset &preset) { + if (! preset.alias.empty()) + return; + set_custom_preset_alias(preset); + }; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, name %1% , total value counts %2%")%canonical_name %preset_values.size(); + + //if the version is not matching, skip it + if (preset_values.find(BBL_JSON_KEY_VERSION) == preset_values.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("can not find version, not loading for user preset %1%")%canonical_name; + return false; + } + std::string version_str = preset_values[BBL_JSON_KEY_VERSION]; + boost::optional cloud_version = Semver::parse(version_str); + if (!cloud_version) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid version %1%, not loading for user preset %2%")%version_str %canonical_name; + return false; + } + + //setting_id + if (preset_values.find(BBL_JSON_KEY_SETTING_ID) == preset_values.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("can not find setting_id, not loading for user preset %1%")%canonical_name; + return false; + } + std::string cloud_setting_id = preset_values[BBL_JSON_KEY_SETTING_ID]; + + //update_time + long long cloud_update_time = 0; + if (preset_values.find(ORCA_JSON_KEY_UPDATE_TIME) != preset_values.end()) { + cloud_update_time = std::atoll(preset_values[ORCA_JSON_KEY_UPDATE_TIME].c_str()); + } + + //user_id + if (preset_values.find(BBL_JSON_KEY_USER_ID) == preset_values.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("can not find user_id, not loading for user preset %1%")%canonical_name; + return false; + } + std::string cloud_user_id = preset_values[BBL_JSON_KEY_USER_ID]; + + lock(); + //std::string name = preset->name; + auto iter = this->find_preset_internal(canonical_name); + bool need_update = false; + if ((iter != m_presets.end()) && (iter->name == canonical_name)) { + BOOST_LOG_TRIVIAL(info) << "Found the Preset locally: " << canonical_name; + //BBS: we should compare the time between cloud and local + if ((cloud_update_time == 0) || (cloud_update_time <= iter->updated_time)) { + if (cloud_update_time < iter->updated_time) + iter->sync_info = "update"; + else + iter->sync_info.clear(); + // Fixup possible data lost + iter->setting_id = cloud_setting_id; + fs::path idx_file(iter->file); + idx_file.replace_extension(".info"); + iter->save_info(idx_file.string()); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("preset %1%'s update_time is eqaul or newer, cloud update_time %2%, local update_time %3%")%canonical_name %cloud_update_time %iter->updated_time; + unlock(); + return false; + } + else { + //update the one from cloud which is newer + need_update = true; + } + } + + // base_id is only required for presets inheriting from a parent. Root presets + // with an empty "inherits" field intentionally have no base_id. + std::string based_id; + const auto base_id = preset_values.find(BBL_JSON_KEY_BASE_ID); + if (base_id != preset_values.end()) { + based_id = base_id->second; + } else { + const auto inherits_iter = preset_values.find(BBL_JSON_KEY_INHERITS); + const bool preset_inherits_from_parent = inherits_iter != preset_values.end() && !inherits_iter->second.empty(); + if (preset_inherits_from_parent) { + // This indicates that there is inherits exists but there is no base_id + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ + << boost::format("can not find base_id, not loading for user preset %1%") % canonical_name; + unlock(); + return false; + } + } + + //filament_id + std::string cloud_filament_id; + if ((m_type == Preset::TYPE_FILAMENT) && preset_values.find(BBL_JSON_KEY_FILAMENT_ID) != preset_values.end()) { + cloud_filament_id = preset_values[BBL_JSON_KEY_FILAMENT_ID]; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << canonical_name << " filament_id: " << cloud_filament_id << " base_id: " << based_id; + } + + DynamicPrintConfig new_config, cloud_config; + try { + ConfigSubstitutions config_substitutions = cloud_config.load_string_map(preset_values, rule); + if (! config_substitutions.empty()) + substitutions.push_back({ canonical_name, m_type, PresetConfigSubstitutions::Source::UserCloud, canonical_name, std::move(config_substitutions) }); + + //BBS: use inherit config as the base + Preset* inherit_preset = nullptr; + ConfigOption* inherits_config = cloud_config.option(BBL_JSON_KEY_INHERITS); + if (inherits_config) { + ConfigOptionString * option_str = dynamic_cast (inherits_config); + std::string inherits_value = option_str->value; + inherit_preset = this->find_preset2(inherits_value, true); + } + const Preset& default_preset = this->default_preset_for(cloud_config); + if (inherit_preset) { + new_config = inherit_preset->config; + if (cloud_filament_id == "null") { + cloud_filament_id = inherit_preset->filament_id; + } + } + else { + // We support custom root preset now + auto inherits_config2 = dynamic_cast(inherits_config); + if (inherits_config2 && !inherits_config2->value.empty()) { + //we should skip this preset here + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", can not find inherit preset for user preset %1%, just skip")%canonical_name; + unlock(); + return false; + } + // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. + new_config = default_preset.config; + } + + extend_default_config_length(cloud_config, false, {}); + + if (inherit_preset) { + std::string extruder_id_name, extruder_variant_name; + std::set *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + + new_config.update_diff_values_to_child_config(cloud_config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); + } + else{ + new_config.apply(std::move(cloud_config)); + extend_default_config_length(new_config, true, default_preset.config); + } + Preset::normalize(new_config); + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(new_config, default_preset.config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a preset file: The preset \"" << canonical_name + << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; + } + if (need_update) { + if (iter->name == m_edited_preset.name && iter->is_dirty) { + // Keep modifies when update from remote + new_config.apply_only(m_edited_preset.config, m_edited_preset.config.diff(iter->config)); + } else if (iter->name == m_edited_preset.name) { + // Preset is not dirty (no local unsaved changes) — also update the edited preset + // to prevent a false "dirty" indication (orange highlight) after a silent cloud sync + m_edited_preset.config = new_config; + } + iter->config = new_config; + iter->updated_time = cloud_update_time; + iter->sync_info = "save"; + iter->version = cloud_version.value(); + iter->user_id = cloud_user_id; + iter->setting_id = cloud_setting_id; + iter->base_id = based_id; + iter->filament_id = cloud_filament_id; + update_alias(*iter); + //presets_loaded.emplace_back(*it->second); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", update the user preset %1% from cloud, type %2%, setting_id %3%, base_id %4%, sync_info %5% inherits %6%, filament_id %7%") + % iter->name %Preset::get_type_string(m_type) %iter->setting_id %iter->base_id %iter->sync_info %iter->inherits() % iter->filament_id; + } + else { + //create a new one + Preset preset(m_type, canonical_name, false); + preset.is_system = false; + preset.loaded = true; + preset.bundle_id = load_origin.bundle_id; + preset.config = new_config; + preset.updated_time = cloud_update_time; + preset.sync_info = "save"; + preset.version = cloud_version.value(); + preset.user_id = cloud_user_id; + preset.setting_id = cloud_setting_id; + preset.base_id = based_id; + preset.filament_id = cloud_filament_id; + update_alias(preset); + + size_t cur_index = iter - m_presets.begin(); + m_presets.insert(iter, preset); + //m_presets.emplace_back (preset); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", insert a new user preset %1%, type %2%, setting_id %3%, base_id %4%, sync_info %5% inherits %6%, filament_id %7%") + %preset.name %Preset::get_type_string(m_type) %preset.setting_id %preset.base_id %preset.sync_info %preset.inherits() %preset.filament_id; + if (cur_index <= m_idx_selected) { + m_idx_selected ++; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", increase m_idx_selected to %1%, due to user preset inserted")%m_idx_selected; + } + } + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + } + + unlock(); + + if (! errors_cummulative.empty()) + throw Slic3r::RuntimeError(errors_cummulative); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, load user preset %1% , type %2%, errors_cummulative %3%")%canonical_name %Preset::get_type_string(m_type) %errors_cummulative; + return (need_update)?false:true; +} + +//re-sort and re-select +void PresetCollection::update_after_user_presets_loaded() +{ + lock(); + std::string selected_name = get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", before sort, type %1%, selected_idx %2%, selected_name %3%") %m_type %m_idx_selected %selected_name; + sort_presets(); + this->select_preset_by_name(selected_name, false); + unlock(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", after sort, type %1%, selected_idx %2%") %m_type %m_idx_selected; + + return; +} + +//BBS: validate_preset +bool PresetCollection::validate_preset(const std::string &preset_name, std::string &inherit_name) +{ + // Presets that came from system vendors, the built-in defaults, or any loaded bundle (local or + // subscribed) are trusted — their g-code isn't user-authored, so the 3MF importer should not + // warn about them. + auto is_trusted = [](const Preset &p) { return p.is_system || p.is_default || p.is_from_bundle(); }; + + const std::string canonical_name = this->canonical_preset_name(preset_name); + std::deque::iterator it = this->find_preset_internal(canonical_name); + bool found = (it != m_presets.end()) && (it->name == canonical_name) && is_trusted(*it); + if (!found) { + it = this->find_preset_renamed(canonical_name); + found = it != m_presets.end() && is_trusted(*it); + } + if (!found) { + if (!inherit_name.empty()) { + const std::string canonical_inherit_name = this->canonical_preset_name(inherit_name); + it = this->find_preset_internal(canonical_inherit_name); + found = it != m_presets.end() && it->name == canonical_inherit_name && is_trusted(*it); + if (found) + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": preset_name %1%, inherit_name %2%, found inherit in list")%preset_name %inherit_name; + else + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": preset_name %1%, inherit_name %2%, can not found preset and inherit in list")%preset_name %inherit_name; + } + else { + //inherit is null , should not happen , just consider it as valid + found = false; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": preset_name %1%, no inherit, set to not found")%preset_name; + } + } + else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": preset_name %1%, found in list")%preset_name; + } + + return found; +} + + +// Load a preset from an already parsed config file, insert it into the sorted sequence of presets +// and select it, losing previous modifications. +Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, const DynamicPrintConfig &config, bool select, Semver file_version) +{ + DynamicPrintConfig cfg(this->default_preset().config); + cfg.apply_only(config, cfg.keys(), true); + return this->load_preset(path, name, std::move(cfg), select, file_version); +} + +static bool profile_print_params_same(const DynamicPrintConfig &cfg_old, const DynamicPrintConfig &cfg_new) +{ + t_config_option_keys diff = cfg_old.diff(cfg_new); + // Following keys are used by the UI, not by the slicing core, therefore they are not important + // when comparing profiles for equality. Ignore them. + for (const char *key : { "compatible_prints", "compatible_prints_condition", + "compatible_printers", "compatible_printers_condition", "inherits", + "print_settings_id", "filament_settings_id", "sla_print_settings_id", "sla_material_settings_id", "printer_settings_id", + "printer_model", "printer_variant", "default_print_profile", "default_filament_profile", "default_sla_print_profile", "default_sla_material_profile" + }) + diff.erase(std::remove(diff.begin(), diff.end(), key), diff.end()); + // Preset with the same name as stored inside the config exists. + return diff.empty(); +} + +// Load a preset from an already parsed config file, insert it into the sorted sequence of presets +// and select it, losing previous modifications. +// Only a single profile could be edited at at the same time, which introduces complexity when loading +// filament profiles for multi-extruder printers. +std::pair PresetCollection::load_external_preset( + // Path to the profile source file (a G-code, an AMF or 3MF file, a config file) + const std::string &path, + // Name of the profile, derived from the source file name. + const std::string &name, + // Original name of the profile, extracted from the loaded config. Empty, if the name has not been stored. + const std::string &original_name, + // Config to initialize the preset from. It may contain configs of all presets merged in a single dictionary! + const DynamicPrintConfig &combined_config, + //different settings list + const std::set &different_settings_list, + // Select the preset after loading? + LoadAndSelect select, + const Semver file_version, + const std::string filament_id) +{ + // Load the preset over a default preset, so that the missing fields are filled in from the default preset. + DynamicPrintConfig cfg(this->default_preset_for(combined_config).config); + // SoftFever: ignore print connection info from project + auto keys = cfg.keys(); + keys.erase(std::remove_if(keys.begin(), keys.end(), + [](std::string &val) { + return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" || + val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port"; + }), + keys.end()); + cfg.apply_only(combined_config, keys, true); + std::string &inherits = Preset::inherits(cfg); + + //add different settings check logic, replace the old system preset's default value with new system preset's default values + std::deque::iterator it = this->find_preset_internal(original_name); + bool found = it != m_presets.end() && it->name == original_name; + if (! found) { + // Try to match the original_name against the "renamed_from" profile names of loaded system profiles. + it = this->find_preset_renamed(original_name); + found = it != m_presets.end(); + } + + std::string extruder_id_name, extruder_variant_name; + std::set *key_set1 = nullptr, *key_set2 = nullptr; + Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); + + if (!inherits.empty() && (different_settings_list.size() > 0)) { + auto iter = this->find_preset_internal(inherits); + if (iter != m_presets.end() && iter->name == inherits) { + //std::vector dirty_options = cfg.diff(iter->config); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path; + cfg.update_non_diff_values_to_base_config(iter->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); + } + } + else if (found && it->is_system && (different_settings_list.size() > 0)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path; + cfg.update_non_diff_values_to_base_config(it->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); + } + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , path %2%, name %3%, original_name %4%, inherits %5%")%Preset::get_type_string(m_type) %path %name %original_name %inherits; + if (select == LoadAndSelect::Never) { + // Some filament profile has been selected and modified already. + // Check whether this profile is equal to the modified edited profile. + const Preset &edited = this->get_edited_preset(); + if ((edited.name == original_name || edited.name == inherits) && profile_print_params_same(edited.config, cfg)) { + // Just point to that already selected and edited profile. + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" Just point to that already selected and edited profile %1%")%edited.name; + return std::make_pair(&(*this->find_preset_internal(edited.name)), false); + } + } + // Is there a preset already loaded with the name stored inside the config? + /*std::deque::iterator it = this->find_preset_internal(original_name); + bool found = it != m_presets.end() && it->name == original_name; + if (! found) { + // Try to match the original_name against the "renamed_from" profile names of loaded system profiles. + it = this->find_preset_renamed(original_name); + found = it != m_presets.end(); + }*/ + if (found && profile_print_params_same(it->config, cfg)) { + // The preset exists and it matches the values stored inside config. + if (select == LoadAndSelect::Always) + this->select_preset(it - m_presets.begin()); + //BBS: set the preset to visible + if ( !it->is_visible ) { + it->is_visible = true; + //AppConfig* app_config = get_app_config(); + //if (app_config) + // app_config->set(AppConfig::SECTION_FILAMENTS, it->name, "1"); + } + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" The preset exists and it matches the values stored inside config. using original_name %1%")%original_name; + return std::make_pair(&(*it), false); + } + if (! found && select != LoadAndSelect::Never && ! inherits.empty()) { + // Try to use a system profile as a base to select the system profile + // and override its settings with the loaded ones. + assert(it == m_presets.end()); + it = this->find_preset_internal(inherits); + found = it != m_presets.end() && it->name == inherits; + if (found && profile_print_params_same(it->config, cfg)) { + // The system preset exists and it matches the values stored inside config. + if (select == LoadAndSelect::Always) + this->select_preset(it - m_presets.begin()); + //BBS: set the preset to visible + if ( !it->is_visible ) { + it->is_visible = true; + //AppConfig* app_config = get_app_config(); + //if (app_config) + // app_config->set(AppConfig::SECTION_FILAMENTS, it->name, "1"); + } + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" The preset exists and it matches the values stored inside config. using inherits %1%")%inherits; + return std::make_pair(&(*it), false); + } + } + if (found) { + //BBS: only select preset for always + //if (select != LoadAndSelect::Never) { + if (select == LoadAndSelect::Always) { + // Select the existing preset and override it with new values, so that + // the differences will be shown in the preset editor against the referenced profile. + this->select_preset(it - m_presets.begin()); + // The source config may contain keys from many possible preset types. Just copy those that relate to this preset. + //this->get_edited_preset().config.apply_only(combined_config, keys, true); + this->get_edited_preset().config.apply_only(cfg, keys, true); + this->update_dirty(); + update_saved_preset_from_current_preset(); + assert(this->get_edited_preset().is_dirty); + //BBS: set the preset to visible + if ( !it->is_visible ) { + it->is_visible = true; + //AppConfig* app_config = get_app_config(); + //if (app_config) + // app_config->set(AppConfig::SECTION_FILAMENTS, it->name, "1"); + } + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" Select the existing preset %1% and override it with new values")%it->name; + return std::make_pair(&(*it), this->get_edited_preset().is_dirty); + } + + //BBS: for other filaments under AMS + if (it->is_project_embedded) { + //update the properties back to the preset + it->config.apply_only(cfg, keys, true); + it->is_dirty = false; + + return std::make_pair(&(*it), false); + } + if (inherits.empty()) { + // Update the "inherits" field. + // There is a profile with the same name already loaded. Should we update the "inherits" field? + inherits = it->vendor ? it->name : it->inherits(); + } + } + + // The external preset does not match an internal preset, load the external preset. + std::string new_name; + //BBS: add project embedded preset logic + //BBS: refine the name logic + for (size_t idx = 0;; ++ idx) { + std::string prefix; + if (original_name.empty()) { + if (!inherits.empty()) { + if (idx == 0) + prefix = inherits; + else + prefix = inherits + "-" + std::to_string(idx); + } + else { + if (idx > 0) + prefix = std::to_string(idx); + } + } else { + std::string reduced_name = original_name; + //TODO + //boost::regex rx("3mf\(*\)"); + //boost::iterator_range result = boost::algorithm::find_regex(reduced_name, rx); + //if (!result.empty()) { + // reduced_name = std::string(result.begin(), result.end()); + //} + + if (idx == 0) + prefix = reduced_name; + else + prefix = reduced_name + "-" + std::to_string(idx) ; + } + //new_name = name + suffix; + new_name = prefix + "(" + name + ")"; + it = this->find_preset_internal(new_name); + if (it == m_presets.end() || it->name != new_name) + // Unique profile name. Insert a new profile. + break; + if (profile_print_params_same(it->config, cfg)) { + // The preset exists and it matches the values stored inside config. + if (select == LoadAndSelect::Always) + this->select_preset(it - m_presets.begin()); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" The preset %1% exists and it matches the values stored inside config.")%new_name; + return std::make_pair(&(*it), false); + } + // Form another profile name. + } + // Insert a new profile. + //BBS: add project embedded preset logic + bool from_project = boost::algorithm::iends_with(name, ".3mf"); + if (m_type == Preset::TYPE_PRINT) + cfg.option("print_settings_id", true)->value = new_name; + else if (m_type == Preset::TYPE_FILAMENT) + cfg.option("filament_settings_id", true)->values[0] = new_name; + else if (m_type == Preset::TYPE_PRINTER) + cfg.option("printer_settings_id", true)->value = new_name; + Preset &preset = this->load_preset(path, new_name, std::move(cfg), select == LoadAndSelect::Always); + preset.is_external = true; + preset.version = file_version; + if (!filament_id.empty()) + preset.filament_id = filament_id; + else { + if (!inherits.empty()) { + Preset *parent = this->find_preset(inherits, false, true); + if (parent) + preset.filament_id = parent->filament_id; + } + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << preset.name << " filament_id: " << preset.filament_id << " base_id: " << preset.base_id; + if (from_project) { + preset.is_project_embedded = true; + } + else { + //external config + preset.file = path_for_preset(preset); + //BBS: save full config here for external + //we can not reach here + preset.save(nullptr); + } + if (&this->get_selected_preset() == &preset) + this->get_edited_preset().is_external = true; + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", type %1% added a preset, name %2%, path %3%, is_system %4%, is_default %5% is_external %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_external; + return std::make_pair(&preset, false); +} + +Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version) +{ + lock(); + auto it = this->find_preset_internal(name); + if (it == m_presets.end() || it->name != name) { + // The preset was not found. Create a new preset. + if (m_presets.begin() + m_idx_selected >= it) + ++m_idx_selected; + it = m_presets.emplace(it, Preset(m_type, name, false)); + } + Preset &preset = *it; + preset.file = path; + preset.config = std::move(config); + preset.loaded = true; + preset.is_dirty = false; + + //BBS + if (file_version.valid()) + preset.version = file_version; + if (select) + this->select_preset_by_name(name, true); + unlock(); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible; + return preset; +} + +bool PresetCollection::clone_presets(std::vector const &presets, std::vector &failures, std::function modifier, bool force_rewritten) +{ + std::vector new_presets; + for (auto curr_preset : presets) { + new_presets.push_back(*curr_preset); + auto &preset = new_presets.back(); + preset.vendor = nullptr; + preset.renamed_from.clear(); + preset.m_excluded_from.clear(); + preset.setting_id.clear(); + preset.inherits().clear(); + preset.is_default = false; + preset.is_system = false; + preset.is_external = false; + preset.is_visible = true; + preset.is_project_embedded = false; + modifier(preset, m_type); + if (find_preset(preset.name) && !force_rewritten) { + failures.push_back(preset.name); + } + preset.file = this->path_for_preset(preset); + if (m_type == Preset::TYPE_PRINT) + preset.config.option("print_settings_id", true)->value = preset.name; + else if (m_type == Preset::TYPE_FILAMENT) + preset.config.option("filament_settings_id", true)->values[0] = preset.name; + else if (m_type == Preset::TYPE_PRINTER) + preset.config.option("printer_settings_id", true)->value = preset.name; + } + if (!failures.empty() && !force_rewritten) + return false; + lock(); + auto old_name = this->get_edited_preset().name; + for (auto preset : new_presets) { + preset.alias.clear(); + set_custom_preset_alias(preset); + preset.base_id.clear(); + auto it = this->find_preset_internal(preset.name); + assert((it == m_presets.end() || it->name != preset.name) || force_rewritten); + if (it == m_presets.end() || it->name != preset.name) { + Preset &new_preset = *m_presets.insert(it, preset); + new_preset.save(nullptr); + } else if (force_rewritten) { + *it = preset; + (*it).save(nullptr); + } + } + this->select_preset_by_name(old_name, true); + unlock(); + return true; +} + +bool PresetCollection::clone_presets_for_printer(std::vector const & templates, + std::vector & failures, + std::string const & printer, + std::function create_filament_id, + bool force_rewritten) +{ + return clone_presets(templates, failures, [printer, create_filament_id](Preset &preset, Preset::Type &type) { + std::string prefix = preset.name.substr(0, preset.name.find(" @")); + std::replace(prefix.begin(), prefix.end(), '/', '-'); + preset.name = prefix + " @" + printer; + auto *compatible_printers = dynamic_cast(preset.config.option("compatible_printers")); + compatible_printers->values = std::vector{printer}; + preset.is_visible = true; + if (type == Preset::TYPE_FILAMENT) { + preset.filament_id = create_filament_id(prefix); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << preset.name << " create filament_id: " << preset.filament_id; + } + }, force_rewritten); +} + +bool PresetCollection::clone_presets_for_filament(Preset const *const & preset, + std::vector &failures, + std::string const & filament_name, + std::string const & filament_id, + const DynamicConfig & dynamic_config, + const std::string & compatible_printers, + bool force_rewritten) +{ + std::vector const presets = {preset}; + return clone_presets(presets, failures, [&filament_name, &filament_id, &dynamic_config, &compatible_printers](Preset &preset, Preset::Type &type) { + preset.name = filament_name + " @" + compatible_printers; + if (type == Preset::TYPE_FILAMENT) { + preset.config.apply_only(dynamic_config, {"filament_vendor", "compatible_printers", "filament_type"},true); + + preset.filament_id = filament_id; + auto compatible = dynamic_cast(preset.config.option("compatible_printers")); + if (compatible->values.empty()) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " " << __LINE__ << preset.name << " apply compatible_printer failed"; + compatible->values.push_back(compatible_printers); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << preset.name << " is cloned and filament_id: " << filament_id; + } + }, + force_rewritten); +} + +std::map> PresetCollection::get_filament_presets() const +{ + std::map> filament_presets; + for (auto &preset : m_presets) { + if (preset.is_user()) { + if (preset.inherits() == "") { filament_presets[preset.filament_id].push_back(&preset); } + continue; + } + if (get_preset_base(preset) == &preset) { filament_presets[preset.filament_id].push_back(&preset); } + } + return filament_presets; +} + +//BBS: add project embedded preset logic +void PresetCollection::save_current_preset(const std::string &new_name, bool detach, bool save_to_project, Preset* _curr_preset) +{ + Preset curr_preset = _curr_preset ? *_curr_preset : m_edited_preset; + //BBS: add lock logic for sync preset in background + std::string final_inherits; + lock(); + // 1) Find the preset with a new_name or create a new one, + // initialize it with the edited config. + auto it = this->find_preset_internal(new_name); + if (it != m_presets.end() && it->name == new_name) { + // Preset with the same name found. + Preset &preset = *it; + //BBS: add project embedded preset logic + if (!preset.can_overwrite()) { + //if (preset.is_default || preset.is_external || preset.is_system) + // Cannot overwrite the default preset. + //BBS: add lock logic for sync preset in background + unlock(); + return; + } + // Overwriting an existing preset. + preset.config = std::move(curr_preset.config); + // The newly saved preset will be activated -> make it visible. + preset.is_visible = true; + //TODO: remove the detach logic + if (detach) { + // Clear the link to the parent profile. + preset.vendor = nullptr; + preset.inherits().clear(); + preset.alias.clear(); + preset.renamed_from.clear(); + preset.m_excluded_from.clear(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": save preset %1% , with detach")%new_name; + } + //BBS: add lock logic for sync preset in background + + if (m_type == Preset::TYPE_PRINT) + preset.config.option("print_settings_id", true)->value = new_name; + else if (m_type == Preset::TYPE_FILAMENT) { + preset.config.option("filament_settings_id", true)->values[0] = new_name; + // Generate a unique filament_id for user presets that don't have one yet (PR #13315). + if (preset.filament_id.empty()) { + boost::uuids::detail::md5 hash; + boost::uuids::detail::md5::digest_type digest; + hash.process_bytes(new_name.data(), new_name.size()); + hash.get_digest(digest); + const auto char_digest = reinterpret_cast(&digest); + std::string result; + boost::algorithm::hex(char_digest, char_digest + sizeof(boost::uuids::detail::md5::digest_type), std::back_inserter(result)); + preset.filament_id = "P" + result.substr(0, 7); + } + } + else if (m_type == Preset::TYPE_PRINTER) + preset.config.option("printer_settings_id", true)->value = new_name; + final_inherits = preset.inherits(); + unlock(); + // TODO: apply change from custom root to devided presets. + if (preset.inherits().empty()) { + for (auto &preset2 : m_presets) + if (preset2.inherits() == preset.name) + preset2.reload(preset); + } + } else { + // Creating a new preset. + Preset &preset = *m_presets.insert(it, curr_preset); + std::string &inherits = preset.inherits(); + std::string old_name = preset.name; + preset.name = new_name; + preset.vendor = nullptr; + preset.alias.clear(); + preset.renamed_from.clear(); + preset.m_excluded_from.clear(); + preset.setting_id.clear(); + if (detach) { + // Clear the link to the parent profile. + inherits.clear(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": save preset %1% , with detach")%new_name; + } else if (is_base_preset(preset)) { + inherits = old_name; + } + + preset.is_default = false; + preset.is_system = false; + preset.is_external = false; + + preset.bundle_id.clear(); + + preset.file = this->path_for_preset(preset); + // The newly saved preset will be activated -> make it visible. + preset.is_visible = true; + // Just system presets have aliases + preset.alias.clear(); + //BBS: add project embedded preset logic + if (save_to_project) { + preset.is_project_embedded = true; + } + else + preset.is_project_embedded = false; + if (m_type == Preset::TYPE_PRINT) + preset.config.option("print_settings_id", true)->value = new_name; + else if (m_type == Preset::TYPE_FILAMENT) + preset.config.option("filament_settings_id", true)->values[0] = new_name; + else if (m_type == Preset::TYPE_PRINTER) + preset.config.option("printer_settings_id", true)->value = new_name; + //BBS: add lock logic for sync preset in background + final_inherits = inherits; + unlock(); + } + // 2) Activate the saved preset. + this->select_preset_by_name(new_name, true); + // 2) Store the active preset to disk. + //BBS: only save difference for user preset + Preset* parent_preset = nullptr; + if (!final_inherits.empty()) { + parent_preset = this->find_preset(final_inherits, false, true); + if (parent_preset && this->get_selected_preset().base_id.empty()) { + this->get_selected_preset().base_id = parent_preset->setting_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " base_id: " << parent_preset->setting_id; + } + } + if (parent_preset) + this->get_selected_preset().save(&(parent_preset->config)); + else + this->get_selected_preset().save(nullptr); +} + +bool PresetCollection::delete_current_preset() +{ + Preset &selected = this->get_selected_preset(); + if (!selected.can_overwrite()) + return false; + + if (get_preset_base(selected) == &selected) { + for (auto &preset2 : m_presets) + if (preset2.inherits() == selected.name) + return false; + } + + //BBS: add project embedded preset logic and refine is_external + //if (! selected.is_external && ! selected.is_system) { + if (! selected.is_system) { + //BBS Erase the preset file. + selected.remove_files(); + } + //BBS: add lock logic for sync preset in background + lock(); + // Remove the preset from the list. + m_presets.erase(m_presets.begin() + m_idx_selected); + unlock(); + + // Find the next visible preset. + size_t new_selected_idx = m_idx_selected; + if (new_selected_idx < m_presets.size()) + for (; new_selected_idx < m_presets.size() && ! m_presets[new_selected_idx].is_visible; ++ new_selected_idx) ; + if (new_selected_idx == m_presets.size()) + for (--new_selected_idx; new_selected_idx > 0 && !m_presets[new_selected_idx].is_visible; --new_selected_idx); + this->select_preset(new_selected_idx); + return true; +} + +bool PresetCollection::delete_preset(const std::string& name, bool force) +{ + Preset *preset_ptr = this->find_preset(name, false, true); + if (preset_ptr == nullptr) + return false; + + auto it = this->find_preset_internal(preset_ptr->name); + if (it == m_presets.end() || it->name != preset_ptr->name) + return false; + + Preset& preset = *it; + // ORCA: if the preset can't be overridden then don't allow deletion + // force=true bypasses this for bundle preset cleanup from cloud sync + if (!force && !preset.can_overwrite()) + return false; + + preset.remove_files(); + + //BBS: add lock logic for sync preset in background + lock(); + set_printer_hold_alias(it->alias, *it, true); + m_presets.erase(it); + unlock(); + + return true; +} + +void PresetCollection::check_and_fix_syncinfo(Preset& preset, const std::string& user_id) +{ + // user id can't be empty + if (user_id.empty()) + return; + // correct the sync info if preset.user_id is empty(the profile json file is copied to the user folder with missing .info file) or preset.user_id + // is not equal to the current user id or preset.setting_id is not in expected format(the .info is copied from the older format) + if (preset.user_id.empty() || preset.user_id != user_id || preset.setting_id.find('-') == std::string::npos) { + preset.user_id = user_id; + preset.setting_id = ""; + + if (preset.base_id.empty()) { + const std::string inherits = Preset::inherits(preset.config); + Preset* parent_preset = find_preset2(inherits, true); + if (parent_preset) + preset.base_id = parent_preset->setting_id; + } + // tell the sync logic to sync it as a new preset + preset.updated_time = 0; + preset.sync_info = "create"; + preset.save_info(); + } +} + +const Preset* PresetCollection::get_selected_preset_parent() const +{ + if (this->get_selected_idx() == size_t(-1)) + // This preset collection has no preset activated yet. Only the get_edited_preset() is valid. + return nullptr; + + const Preset &selected_preset = this->get_selected_preset(); + if (get_preset_base(selected_preset) == &selected_preset) + return &selected_preset; + + const Preset &edited_preset = this->get_edited_preset(); + const std::string &inherits = edited_preset.inherits(); + const Preset *preset = nullptr; + if (inherits.empty()) { + if (selected_preset.is_external) + return nullptr; + preset = &this->default_preset(m_type == Preset::Type::TYPE_PRINTER && edited_preset.printer_technology() == ptSLA ? 1 : 0); + } else + preset = this->find_preset(inherits, false); + if (preset == nullptr) { + // Resolve the "renamed_from" field. + assert(! inherits.empty()); + auto it = this->find_preset_renamed(inherits); + if (it != m_presets.end()) + preset = &(*it); + } + //BBS: add project embedded preset logic and refine is_external + return (preset == nullptr/* || preset->is_default || preset->is_external*/) ? nullptr : preset; + //return (preset == nullptr/* || preset->is_default*/ || preset->is_external) ? nullptr : preset; +} + +const Preset* PresetCollection::get_preset_parent(const Preset& child) const +{ + const std::string &inherits = child.inherits(); + if (inherits.empty()) +// return this->get_selected_preset().is_system ? &this->get_selected_preset() : nullptr; + return nullptr; + const Preset* preset = this->find_preset(inherits, false); + if (preset == nullptr) { + auto it = this->find_preset_renamed(inherits); + if (it != m_presets.end()) + preset = &(*it); + } + return + // not found + (preset == nullptr/* || preset->is_default */|| + // this should not happen, user profile should not derive from an external profile + //BBS: add project embedded preset logic and refine is_external + /*preset->is_external ||*/ + // this should not happen, however people are creative, see GH #4996 + preset == &child) ? + nullptr : + preset; +} + +const Preset *PresetCollection::get_preset_base(const Preset &child) const +{ + if (child.is_system || child.is_default) + return &child; + // Handle user preset + if (child.inherits().empty()) + return &child; // this is user root + auto inherits = find_preset2(child.inherits(), true); + return inherits ? get_preset_base(*inherits) : nullptr; +} + +// Return vendor of the first parent profile, for which the vendor is defined, or null if such profile does not exist. +PresetWithVendorProfile PresetCollection::get_preset_with_vendor_profile(const Preset &preset) const +{ + const Preset *p = &preset; + const VendorProfile *v = nullptr; + do { + if (p->vendor != nullptr) { + v = p->vendor; + break; + } + p = this->get_preset_parent(*p); + } while (p != nullptr); + return PresetWithVendorProfile(preset, v); +} + +const std::string& PresetCollection::get_preset_name_by_alias(const std::string& alias) const +{ + for ( + // Find the 1st profile name with the alias. + auto it = Slic3r::lower_bound_by_predicate(m_map_alias_to_profile_name.begin(), m_map_alias_to_profile_name.end(), [&alias](auto &l){ return l.first < alias; }); + // Continue over all profile names with the same alias. + it != m_map_alias_to_profile_name.end() && it->first == alias; ++ it) + for (const std::string &preset_name : it->second) { + if (auto it_preset = this->find_preset_internal(preset_name); + it_preset != m_presets.end() && it_preset->name == preset_name && + it_preset->is_visible && (it_preset->is_compatible || size_t(it_preset - m_presets.begin()) == m_idx_selected)) + return it_preset->name; + } + + return alias; +} + +const std::string* PresetCollection::get_preset_name_renamed(const std::string &old_name) const +{ + auto it_renamed = m_map_system_profile_renamed.find(old_name); + if (it_renamed != m_map_system_profile_renamed.end()) + return &it_renamed->second; + return nullptr; +} + +bool PresetCollection::is_alias_exist(const std::string &alias, Preset* preset) +{ + auto it = m_map_alias_to_profile_name.find(alias); + if (m_map_alias_to_profile_name.end() == it) return false; + if (!preset) return true; + + auto compatible_printers = dynamic_cast(preset->config.option("compatible_printers")); + if (compatible_printers == nullptr) return true; + + for (const std::string &printer_name : compatible_printers->values) { + auto printer_iter = m_printer_hold_alias.find(printer_name); + if (m_printer_hold_alias.end() != printer_iter) { + auto alias_iter = m_printer_hold_alias[printer_name].find(alias); + if (m_printer_hold_alias[printer_name].end() != alias_iter) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << " The alias already exists: " << alias << " and the preset name: " << preset->name; + return true; + } + } + } + return false; +} + +const std::string& PresetCollection::get_suffix_modified() { + return g_suffix_modified; +} + +// Return a preset by its name. If the preset is active, a temporary copy is returned. +// If a preset was not found by its name, null is returned. +Preset* PresetCollection::find_preset(const std::string &name, bool first_visible_if_not_found, bool real, bool only_from_library) +{ + const ParsedName parsed = parse_preset_name(name); + const std::string canonical = get_preset_canonical_name(parsed.bare, PresetOrigin(parsed.kind, parsed.bundle_id)); + auto it = this->find_preset_internal(canonical, only_from_library); + if (it != m_presets.end() && it->name == canonical) + return &this->preset(it - m_presets.begin(), real); + return first_visible_if_not_found ? &this->first_visible() : nullptr; +} + +Preset* PresetCollection::find_preset2(const std::string& name, bool auto_match/* = true */) +{ + auto preset = find_preset(name, false, true); + if (preset == nullptr) { + auto _name = get_preset_name_renamed(name); + if (_name != nullptr) + preset = find_preset(*_name, false, true); + if (auto_match && preset == nullptr) { + //Orca: one more try, find the most likely preset in OrcaFilamentLibrary + if (name.find("Generic") != std::string::npos) { + // The regex pattern matches an optional prefix ending in '_' then "Generic" followed by the material name. + std::regex re(R"(^(?:.*?\b(?:\w+_)?)(Generic)\b\s+([^@]+?)\s*(?:@.*)?$)"); + auto alter_name = std::regex_replace(name, re, "Generic $2 @System"); + preset = find_preset2(alter_name, false); + // print preset file name + if (preset != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << "Failed to find: " << name + << ". fallback to library preset: " << preset->file; + } + } + } + } + return preset; +} + +// Return index of the first visible preset. Certainly at least the '- default -' preset shall be visible. +size_t PresetCollection::first_visible_idx() const +{ + //BBS: set first visible filament to fla + size_t first_visible = -1; + size_t idx = m_default_suppressed ? m_num_default_presets : 0; + for (; idx < m_presets.size(); ++ idx) + if (m_presets[idx].is_visible && m_presets[idx].get_printer_id() == PresetBundle::ORCA_FILAMENT_LIBRARY) { + if (first_visible == -1) + first_visible = idx; + if (m_type != Preset::TYPE_FILAMENT) + break; + else { + if (m_presets[idx].name.find("PLA") != std::string::npos) { + first_visible = idx; + break; + } + } + } + if (first_visible == -1) { + if (m_presets.size() > 1 && m_default_suppressed) + first_visible = m_presets.size() == m_num_default_presets ? 0 : m_num_default_presets; + else + first_visible = 0; + } + return first_visible; +} + +size_t PresetCollection::first_visible_idx_by_type(const std::string& filament_type) const +{ + size_t start = m_default_suppressed ? m_num_default_presets : 0; + + // Find the first visible, compatible, system base preset whose filament_type matches target. + auto find_by_type = [&](const std::string& target) -> size_t { + for (size_t i = start; i < m_presets.size(); ++i) { + const auto& p = m_presets[i]; + if (p.is_visible && p.is_compatible && p.is_system + && get_preset_base(p) == &p + && p.config.opt_string("filament_type", 0u) == target) + return i; + } + return size_t(-1); + }; + + // 1. Exact filament_type match + size_t idx = find_by_type(filament_type); + if (idx != size_t(-1)) + return idx; + + // 2. Base type fallback: strip modifier after first space + // e.g. "PLA High Speed" -> "PLA" + // Dash-separated types like "PA-CF", "PET-CF" are distinct materials, not modifiers. + auto sep = filament_type.find(' '); + if (sep != std::string::npos) { + idx = find_by_type(filament_type.substr(0, sep)); + if (idx != size_t(-1)) + return idx; + } + + // 3. Any visible preset + return first_visible_idx(); +} + +std::string PresetCollection::filament_id_by_type(const std::string& filament_type) const +{ + return preset(first_visible_idx_by_type(filament_type)).filament_id; +} + +std::vector PresetCollection::diameters_of_selected_printer() +{ + std::set diameters; + auto printer_model = m_edited_preset.config.opt_string("printer_model"); + for (auto &preset : m_presets) { + if (preset.config.opt_string("printer_model") == printer_model) + diameters.insert(preset.config.opt_string("printer_variant")); + } + return std::vector{diameters.begin(), diameters.end()}; +} + +void PresetCollection::set_default_suppressed(bool default_suppressed) +{ + if (m_default_suppressed != default_suppressed) { + m_default_suppressed = default_suppressed; + bool default_visible = ! default_suppressed || m_idx_selected < m_num_default_presets; + for (size_t i = 0; i < m_num_default_presets; ++ i) + m_presets[i].is_visible = default_visible; + } +} + +size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfile &active_printer, const PresetWithVendorProfile *active_print, PresetSelectCompatibleType unselect_if_incompatible) +{ + DynamicPrintConfig config; + config.set_key_value("printer_preset", new ConfigOptionString(active_printer.preset.name)); + const ConfigOption *opt = active_printer.preset.config.option("nozzle_diameter"); + if (opt) + config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast(opt)->values.size())); + int some_compatible = 0; + + if (active_print) + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": active printer %1%, print %2%, unselect_if_incompatible %3%")%active_printer.preset.name %active_print->preset.name % (int)unselect_if_incompatible; + else + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": active printer %1%, unselect_if_incompatible %2%")%active_printer.preset.name % (int)unselect_if_incompatible; + for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++ idx_preset) { + bool selected = idx_preset == m_idx_selected; + Preset &preset_selected = m_presets[idx_preset]; + Preset &preset_edited = selected ? m_edited_preset : preset_selected; + + const PresetWithVendorProfile this_preset_with_vendor_profile = this->get_preset_with_vendor_profile(preset_edited); + bool was_compatible = preset_edited.is_compatible; + preset_edited.is_compatible = is_compatible_with_printer(this_preset_with_vendor_profile, active_printer, &config); + if (preset_edited.is_compatible) + some_compatible++; + if (active_print != nullptr) + preset_edited.is_compatible &= is_compatible_with_print(this_preset_with_vendor_profile, *active_print, active_printer); + if (! preset_edited.is_compatible && selected && + (unselect_if_incompatible == PresetSelectCompatibleType::Always || (unselect_if_incompatible == PresetSelectCompatibleType::OnlyIfWasCompatible && was_compatible))) + { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": type %1% , previous selected %2% becomes uncompatible, will select later")%Preset::get_type_string(m_type) %m_idx_selected; + m_idx_selected = size_t(-1); + } + if (selected) + preset_selected.is_compatible = preset_edited.is_compatible; + } + // Update visibility of the default profiles here if the defaults are suppressed, the current profile is not compatible and we don't want to select another compatible profile. + if (m_idx_selected >= m_num_default_presets && m_default_suppressed) + { + for (size_t i = 0; i < m_num_default_presets; ++ i) + { + m_presets[i].is_visible = (some_compatible == 0); + } + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": type %1% returned m_idx_selected %2%, some_compatible %3%")%Preset::get_type_string(m_type) %m_idx_selected %some_compatible; + return m_idx_selected; +} + +// Update a dirty flag of the current preset +// Return true if the dirty flag changed. +bool PresetCollection::update_dirty() +{ + bool was_dirty = this->get_selected_preset().is_dirty; + bool is_dirty = current_is_dirty(); + this->get_selected_preset().is_dirty = is_dirty; + this->get_edited_preset().is_dirty = is_dirty; + + return was_dirty != is_dirty; +} + +template +void add_correct_opts_to_diff(const std::string &opt_key, t_config_option_keys& vec, const ConfigBase &other, const ConfigBase &this_c, bool strict) +{ + const T* opt_init = static_cast(other.option(opt_key)); + const T* opt_cur = static_cast(this_c.option(opt_key)); + int opt_init_max_id = opt_init->values.size() - 1; + if (opt_init_max_id < 0) { + for (int i = 0; i < int(opt_cur->values.size()); i++) + vec.emplace_back(opt_key + "#" + std::to_string(i)); + return; + } + + for (int i = 0; i < int(opt_cur->values.size()); i++) + { + int init_id = i <= opt_init_max_id ? i : 0; + if (opt_cur->values[i] != opt_init->values[init_id]) { + if (opt_cur->nullable()) { + if (opt_cur->is_nil(i)) { + if (strict && !opt_init->is_nil(init_id)) + vec.emplace_back(opt_key + "#" + std::to_string(i)); + } else { + if (strict || !opt_init->is_nil(init_id)) + vec.emplace_back(opt_key + "#" + std::to_string(i)); + } + } else { + vec.emplace_back(opt_key + "#" + std::to_string(i)); + } + } + } +} + +// Use deep_diff to correct return of changed options, considering individual options for each extruder. +inline t_config_option_keys deep_diff(const ConfigBase &config_this, const ConfigBase &config_other, bool strict = true) +{ + t_config_option_keys diff; + t_config_option_keys keys; + if (strict) { + t_config_option_keys keys_this = config_this.keys(); + t_config_option_keys keys_other = config_other.keys(); + std::set_union(keys_this.begin(), keys_this.end(), keys_other.begin(), keys_other.end(), std::back_inserter(keys)); + } else { + keys = config_this.keys(); + } + for (const t_config_option_key &opt_key : keys) { + const ConfigOption *this_opt = config_this.option(opt_key); + const ConfigOption *other_opt = config_other.option(opt_key); + if (this_opt != nullptr && other_opt != nullptr && *this_opt != *other_opt) + { + //BBS: add bed_exclude_area + if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area") { + // Scalar variable, or a vector variable, which is independent from number of extruders, + // thus the vector is presented to the user as a single input. + diff.emplace_back(opt_key); + } else if (opt_key == "default_filament_profile") { + // Ignore this field, it is not presented to the user, therefore showing a "modified" flag for this parameter does not help. + // Also the length of this field may differ, which may lead to a crash if the block below is used. + } + else if (opt_key == "thumbnails") { + // "thumbnails" can not contain extensions in old config but they are valid and use PNG extension by default + // So, check if "thumbnails" is really changed + // We will compare full thumbnails instead of exactly config values + auto [thumbnails, er] = GCodeThumbnails::make_and_check_thumbnail_list(config_this); + auto [thumbnails_new, er_new] = GCodeThumbnails::make_and_check_thumbnail_list(config_other); + if (thumbnails != thumbnails_new || er != er_new) + // if those strings are actually the same, erase them from the list of dirty oprions + diff.emplace_back(opt_key); + } else { + switch (other_opt->type()) { + case coInts: add_correct_opts_to_diff(opt_key, diff, config_other, config_this, strict); break; + case coBools: add_correct_opts_to_diff(opt_key, diff, config_other, config_this, strict); break; + case coFloats: add_correct_opts_to_diff(opt_key, diff, config_other, config_this, strict); break; + case coStrings: add_correct_opts_to_diff(opt_key, diff, config_other, config_this, strict); break; + case coPercents:add_correct_opts_to_diff(opt_key, diff, config_other, config_this, strict); break; + case coFloatsOrPercents: add_correct_opts_to_diff(opt_key, diff, config_other, config_this, strict); break; + case coPoints: add_correct_opts_to_diff(opt_key, diff, config_other, config_this, strict); break; + // BBS + case coEnums: add_correct_opts_to_diff(opt_key, diff, config_other, config_this, strict); break; + default: diff.emplace_back(opt_key); break; + } + } + } + else if (strict) { + const ConfigOption *opt = nullptr; + if (this_opt != nullptr && other_opt == nullptr) + opt = this_opt; + else if (this_opt == nullptr && other_opt != nullptr) + opt = other_opt; + if (opt) { + if (opt->type() & coVectorType) { + auto vec = dynamic_cast(opt); + for (size_t i = 0; i < vec->size(); i++) + diff.push_back(opt_key + "#" + std::to_string(i)); + } else { + diff.push_back(opt_key); + } + } + } + } + return diff; +} + +static constexpr const std::initializer_list optional_keys { "compatible_prints", "compatible_printers" }; +//BBS: skip these keys for dirty check +static std::set skipped_in_dirty = {"printer_settings_id", "print_settings_id", "filament_settings_id"}; + +bool PresetCollection::is_dirty(const Preset *edited, const Preset *reference) +{ + if (edited != nullptr && reference != nullptr) { + // Only compares options existing in both configs. + if (! reference->config.equals(edited->config, &skipped_in_dirty)) + return true; + // The "compatible_printers" option key is handled differently from the others: + // It is not mandatory. If the key is missing, it means it is compatible with any printer. + // If the key exists and it is empty, it means it is compatible with no printer. + for (auto &opt_key : optional_keys) + if (reference->config.has(opt_key) != edited->config.has(opt_key)) + return true; + } + return false; +} + +std::vector PresetCollection::dirty_options(const Preset *edited, const Preset *reference, const bool deep_compare /*= false*/) +{ + std::vector changed; + if (edited != nullptr && reference != nullptr) { + // Only compares options existing in both configs. + changed = deep_compare ? + deep_diff(edited->config, reference->config) : + reference->config.diff(edited->config); + // The "compatible_printers" option key is handled differently from the others: + // It is not mandatory. If the key is missing, it means it is compatible with any printer. + // If the key exists and it is empty, it means it is compatible with no printer. + for (auto &opt_key : optional_keys) + if (reference->config.has(opt_key) != edited->config.has(opt_key)) + changed.emplace_back(opt_key); + } + return changed; +} + +//BBS: add function for dirty_options_without_option_list +std::vector PresetCollection::dirty_options_without_option_list(const Preset *edited, const Preset *reference, const std::set& option_ignore_list, const bool deep_compare) +{ + std::vector changed; + if (edited != nullptr && reference != nullptr) { + // Only compares options existing in both configs. + changed = deep_compare ? + deep_diff(edited->config, reference->config) : + reference->config.diff(edited->config); + // The "compatible_printers" option key is handled differently from the others: + // It is not mandatory. If the key is missing, it means it is compatible with any printer. + // If the key exists and it is empty, it means it is compatible with no printer. + for (auto &opt_key : optional_keys) { + if (reference->config.has(opt_key) != edited->config.has(opt_key)) + changed.emplace_back(opt_key); + } + auto iter = changed.begin(); + while (iter != changed.end()) { + if (option_ignore_list.find(*iter) != option_ignore_list.end()) { + iter = changed.erase(iter); + } + else { + ++iter; + } + } + } + return changed; +} + +// Select a new preset. This resets all the edits done to the currently selected preset. +// If the preset with index idx does not exist, a first visible preset is selected. +Preset& PresetCollection::select_preset(size_t idx) +{ + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% try to select preset %2%")%Preset::get_type_string(m_type) %idx; + for (Preset &preset : m_presets) + preset.is_dirty = false; + if (idx >= m_presets.size()) + idx = first_visible_idx(); + m_idx_selected = idx; + m_edited_preset = m_presets[idx]; + update_saved_preset_from_current_preset(); + bool default_visible = ! m_default_suppressed || m_idx_selected < m_num_default_presets; + for (size_t i = 0; i < m_num_default_presets; ++i) + m_presets[i].is_visible = default_visible; + + //set this preset to true + if (!m_presets[idx].is_visible) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% set %2%, idx %3% to visible") % Preset::get_type_string(m_type) % m_presets[idx].name % idx; + m_presets[idx].is_visible = true; + } + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% select success, m_idx_selected %2%, name %3%, is_system %4%, is_default %5%")%Preset::get_type_string(m_type) % m_idx_selected % m_edited_preset.name % m_edited_preset.is_system % m_edited_preset.is_default; + return m_presets[idx]; +} + +bool PresetCollection::select_preset_by_name(const std::string &name_w_suffix, bool force) +{ + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1%, try to select by name %2%, force %3%")%Preset::get_type_string(m_type) %name_w_suffix %force; + std::string name = Preset::remove_suffix_modified(name_w_suffix); + const std::string normalized_name = this->canonical_preset_name(name); + // 1) Try to find the preset by its name. + auto it = this->find_preset_internal(normalized_name); + size_t idx = 0; + if (it != m_presets.end() && it->name == normalized_name && it->is_visible) + // Preset found by its name and it is visible. + idx = it - m_presets.begin(); + else { + // Find the first visible preset. + for (size_t i = m_default_suppressed ? m_num_default_presets : 0; i < m_presets.size(); ++ i) + if (m_presets[i].is_visible) { + idx = i; + break; + } + // If the first visible preset was not found, return the 0th element, which is the default preset. + } + + // 2) Select the new preset. + if (m_idx_selected != idx || force) { + this->select_preset(idx); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1%, select %2%, success")%Preset::get_type_string(m_type) %name_w_suffix; + return true; + } + + //BBS: add config related logs + if (m_idx_selected == idx) + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1%, already selected before") % Preset::get_type_string(m_type); + else + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1%, select %2%, failed")%Preset::get_type_string(m_type) %name_w_suffix; + return false; +} + +bool PresetCollection::select_preset_by_name_strict(const std::string &name) +{ + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1%, try to select by name %2%")%Preset::get_type_string(m_type) %name; + const std::string canonical_name = this->canonical_preset_name(name); + // 1) Try to find the preset by its name. + auto it = this->find_preset_internal(canonical_name); + + size_t idx = (size_t)-1; + if (it != m_presets.end() && it->name == canonical_name && it->is_visible) + // Preset found by its name. + idx = it - m_presets.begin(); + // 2) Select the new preset. + if (idx != (size_t)-1) { + this->select_preset(idx); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1%, select %2%, success")%Preset::get_type_string(m_type) %name; + return true; + } + m_idx_selected = idx; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1%, select %2%, failed")%Preset::get_type_string(m_type) %name; + return false; +} + +// Merge one vendor's presets with the other vendor's presets, report duplicates. +std::vector PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors) +{ + std::vector duplicates; + for (Preset &preset : other.m_presets) { + if (preset.is_default || preset.is_external) + continue; + Preset key(m_type, preset.name); + auto it = (m_type == Preset::TYPE_FILAMENT) + ? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less) + : std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key); + if (it == m_presets.end() || it->name != preset.name) { + if (preset.vendor != nullptr) { + // Re-assign a pointer to the vendor structure in the new PresetBundle. + auto it = new_vendors.find(preset.vendor->id); + assert(it != new_vendors.end()); + preset.vendor = &it->second; + } + m_presets.emplace(it, std::move(preset)); + } else + duplicates.emplace_back(std::move(preset.name)); + } + return duplicates; +} + +void PresetCollection::update_vendor_ptrs_after_copy(const VendorMap &new_vendors) +{ + for (Preset &preset : m_presets) + if (preset.vendor != nullptr) { + assert(! preset.is_default && ! preset.is_external); + // Re-assign a pointer to the vendor structure in the new PresetBundle. + auto it = new_vendors.find(preset.vendor->id); + assert(it != new_vendors.end()); + preset.vendor = &it->second; + } +} + +void PresetCollection::update_map_alias_to_profile_name() +{ + m_map_alias_to_profile_name.clear(); + for (const Preset &preset : m_presets) { + m_map_alias_to_profile_name[preset.alias].push_back(preset.name); + } + // now m_map_alias_to_profile_name is map, not need sort + //std::sort(m_map_alias_to_profile_name.begin(), m_map_alias_to_profile_name.end(), [](auto &l, auto &r) { return l.first < r.first; }); +} + +void PresetCollection::update_library_profile_excluded_from() +{ + // Orca: Collect all filament presets that has empty compatible_printers and belongs to the Orca Filament Library. + std::map*> excluded_froms; + for (Preset& preset : m_presets) { + if (preset.vendor != nullptr && preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY) { + // check if the preset has empty compatible_printers + const auto* compatible_printers = dynamic_cast(preset.config.option("compatible_printers")); + if (compatible_printers == nullptr || compatible_printers->values.empty()) + excluded_froms[preset.alias] = &preset.m_excluded_from; + } + } + + // Check all presets that has the same alias as the filament presets with empty compatible_printers in Orca Filament Library. + for (const Preset& preset : m_presets) { + if (preset.vendor == nullptr || preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY) + continue; + + const auto* compatible_printers = dynamic_cast(preset.config.option("compatible_printers")); + // All profiles in concrete vendor profile shouldn't have empty compatible_printers, but here we check it for safety. + if (compatible_printers == nullptr || compatible_printers->values.empty()) + continue; + auto itr = excluded_froms.find(preset.alias); + if (itr != excluded_froms.end()) { + // Add the printer models to the excluded_from list. + for (const std::string& printer_name : compatible_printers->values) { + itr->second->insert(printer_name); + } + } + } +} + +void PresetCollection::update_map_system_profile_renamed() +{ + m_map_system_profile_renamed.clear(); + for (Preset &preset : m_presets) + for (const std::string &renamed_from : preset.renamed_from) { + const auto [it, success] = m_map_system_profile_renamed.insert(std::pair(renamed_from, preset.name)); + if (!success) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << boost::format("Preset name \"%1%\" was marked as renamed from \"%2%\", though preset name " + "\"%3%\" was marked as renamed from \"%2%\" as well.") % + preset.name % renamed_from % it->second; + } + } +} + +void PresetCollection::set_custom_preset_alias(Preset &preset) +{ + // For filaments, remove the postfix + // For printers, there is nothing to remove + // For prints AKA processes, the postfix should be kept + // Alias should be set here, as the preset name may be augmented further later (i.e., prefixing relative path for bundles) + std::string bare_preset_name = get_preset_bare_name(preset.name); + std::string alias_name = bare_preset_name; + + const bool is_root_filament_preset = + m_type == Preset::Type::TYPE_FILAMENT && + preset.config.has(BBL_JSON_KEY_INHERITS) && + preset.config.option(BBL_JSON_KEY_INHERITS)->value.empty(); + if (is_root_filament_preset) { + const size_t suffix_separator_pos = bare_preset_name.find_first_of("@"); + if (suffix_separator_pos != std::string::npos) { + alias_name = bare_preset_name.substr(0, suffix_separator_pos); + boost::trim_right(alias_name); + if (alias_name.empty()) + alias_name = bare_preset_name; + } + } + + preset.alias = std::move(alias_name); + m_map_alias_to_profile_name[preset.alias].push_back(preset.name); + set_printer_hold_alias(preset.alias, preset); +} + +void PresetCollection::set_printer_hold_alias(const std::string &alias, Preset &preset, bool remove) +{ + auto compatible_printers = dynamic_cast(preset.config.option("compatible_printers")); + if (compatible_printers == nullptr) return; + for (const std::string &printer_name : compatible_printers->values) { + auto printer_iter = m_printer_hold_alias.find(printer_name); + bool insert_success = false, remove_success = false; + if (m_printer_hold_alias.end() == printer_iter) { + if (!remove) { + insert_success = true; + m_printer_hold_alias[printer_name].insert(alias); + } + } else { + auto &printer_filament_alias = m_printer_hold_alias[printer_name]; + auto alias_iter = printer_filament_alias.find(alias); + if (printer_filament_alias.end() == alias_iter) { + if (!remove) { + insert_success = true; + printer_filament_alias.insert(alias); + } + } else { + if (remove) { + if (preset.inherits() == "") { + remove_success = true; + printer_filament_alias.erase(alias); + } + if (auto alias_iter = m_map_alias_to_profile_name.find(alias); alias_iter != m_map_alias_to_profile_name.end()) { + auto& presets = alias_iter->second; + auto new_end = std::remove(presets.begin(), presets.end(), preset.name); + presets.erase(new_end, presets.end()); + if (presets.empty()) { m_map_alias_to_profile_name.erase(alias); } + } + } + } + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << " preset name : " << preset.name << " remove action: " << remove << " insert success: " + << insert_success << " remove success: " << remove_success << " alias: " << alias; + } +} + +std::string PresetCollection::name() const +{ + switch (this->type()) { + case Preset::TYPE_PRINT: return L(PRESET_PRINT_NAME); + case Preset::TYPE_FILAMENT: return L(PRESET_FILAMENT_NAME); + //case Preset::TYPE_SLA_PRINT: return L("SLA print"); + //case Preset::TYPE_SLA_MATERIAL: return L("SLA material"); + case Preset::TYPE_PRINTER: return L(PRESET_PRINTER_NAME); + default: return "invalid"; + } +} + +//BBS: change directoties by design +std::string PresetCollection::section_name() const +{ + switch (this->type()) { + case Preset::TYPE_PRINT: return PRESET_PRINT_NAME; + case Preset::TYPE_FILAMENT: return PRESET_FILAMENT_NAME; + //case Preset::TYPE_SLA_PRINT: return PRESET_SLA_PRINT_NAME; + //case Preset::TYPE_SLA_MATERIAL: return PRESET_SLA_MATERIALS_NAME; + case Preset::TYPE_PRINTER: return PRESET_PRINTER_NAME; + default: return "invalid"; + } +} + +// Used for validating the "inherits" flag when importing user's config bundles. +// Returns names of all system presets including the former names of these presets. +std::vector PresetCollection::system_preset_names() const +{ + size_t num = 0; + for (const Preset &preset : m_presets) + if (preset.is_system) + ++ num; + std::vector out; + out.reserve(num); + for (const Preset &preset : m_presets) + if (preset.is_system) { + out.emplace_back(preset.name); + out.insert(out.end(), preset.renamed_from.begin(), preset.renamed_from.end()); + } + std::sort(out.begin(), out.end()); + return out; +} + +// Generate a file path from a profile name. Add the ".ini" suffix if it is missing. +std::string PresetCollection::path_from_name(const std::string &new_name, bool detach) const +{ + //BBS: change to json format + //std::string file_name = boost::iends_with(new_name, ".ini") ? new_name : (new_name + ".ini"); + std::string file_name = boost::iends_with(new_name, ".json") ? new_name : (new_name + ".json"); + if (detach) + return (boost::filesystem::path(m_dir_path) / "base" / file_name).make_preferred().string(); + else + return (boost::filesystem::path(m_dir_path) / file_name).make_preferred().string(); +} + +std::string PresetCollection::path_for_preset(const Preset &preset) const +{ + return path_from_name(get_preset_bare_name(preset.name), is_base_preset(preset)); +} + +const Preset& PrinterPresetCollection::default_preset_for(const DynamicPrintConfig &config) const +{ + const ConfigOptionEnumGeneric *opt_printer_technology = config.opt("printer_technology"); + return this->default_preset((opt_printer_technology == nullptr || opt_printer_technology->value == ptFFF) ? 0 : 1); +} + +const Preset* PrinterPresetCollection::find_system_preset_by_model_and_variant(const std::string &model_id, const std::string& variant) const +{ + if (model_id.empty()) { return nullptr; } + + const auto it = std::find_if(cbegin(), cend(), [&](const Preset &preset) { + if (!preset.is_system || preset.config.opt_string("printer_model") != model_id) + return false; + if (variant.empty()) + return true; + return preset.config.opt_string("printer_variant") == variant; + }); + + return it != cend() ? &*it : nullptr; +} + +const Preset *PrinterPresetCollection::find_custom_preset_by_model_and_variant(const std::string &model_id, const std::string &variant) const +{ + if (model_id.empty()) { return nullptr; } + + const auto it = std::find_if(cbegin(), cend(), [&](const Preset &preset) { + if (preset.config.opt_string("printer_model") != model_id) + return false; + if (variant.empty()) + return true; + return preset.config.opt_string("printer_variant") == variant; + }); + + return it != cend() ? &*it : nullptr; +} + +bool PrinterPresetCollection::only_default_printers() const +{ + for (const auto& printer : get_presets()) { + if (!boost::starts_with(printer.name,"Default") && printer.is_visible) + return false; + } + return true; +} +// ------------------------- +// *** PhysicalPrinter *** +// ------------------------- + +std::string PhysicalPrinter::separator() +{ + return " * "; +} + +static std::vector s_PhysicalPrinter_opts { + "preset_name", // temporary option to compatibility with older Slicer + "preset_names", + "printer_technology", + "bbl_use_printhost", + "host_type", + "printer_agent", + "print_host", + "print_host_webui", + "printhost_apikey", + "printhost_cafile", + "printhost_port", + "printhost_authorization_type", + // HTTP digest authentization (RFC 2617) + "printhost_user", + "printhost_password", + "printhost_ssl_ignore_revoke" +}; + +const std::vector& PhysicalPrinter::printer_options() +{ + return s_PhysicalPrinter_opts; +} + +std::vector PhysicalPrinter::presets_with_print_host_information(const PrinterPresetCollection& printer_presets) +{ + std::vector presets; + for (const Preset& preset : printer_presets) + if (has_print_host_information(preset.config)) + presets.emplace_back(preset.name); + + return presets; +} + +bool PhysicalPrinter::has_print_host_information(const DynamicPrintConfig& config) +{ + return false; +} + +const std::set& PhysicalPrinter::get_preset_names() const +{ + return preset_names; +} + +// temporary workaround for compatibility with older Slicer +static void update_preset_name_option(const std::set& preset_names, DynamicPrintConfig& config) +{ + std::string name; + for (auto el : preset_names) + name += el + ";"; + name.pop_back(); + config.set_key_value("preset_name", new ConfigOptionString(name)); +} + +void PhysicalPrinter::update_preset_names_in_config() +{ + if (!preset_names.empty()) { + std::vector& values = config.option("preset_names")->values; + values.clear(); + for (auto preset : preset_names) + values.push_back(preset); + + // temporary workaround for compatibility with older Slicer + update_preset_name_option(preset_names, config); + } +} + +void PhysicalPrinter::save(const std::string& file_name_from, const std::string& file_name_to) +{ + // rename the file + boost::nowide::rename(file_name_from.data(), file_name_to.data()); + this->file = file_name_to; + // save configuration + //BBS: change to save + //this->config.save(this->file); + this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION)); +} + +void PhysicalPrinter::update_from_preset(const Preset& preset) +{ + config.apply_only(preset.config, printer_options(), true); + // add preset names to the options list + preset_names.emplace(preset.name); + update_preset_names_in_config(); +} + +void PhysicalPrinter::update_from_config(const DynamicPrintConfig& new_config) +{ + config.apply_only(new_config, printer_options(), false); + + const std::vector& values = config.option("preset_names")->values; + + if (values.empty()) + preset_names.clear(); + else { + for (const std::string& val : values) + preset_names.emplace(val); + // temporary workaround for compatibility with older Slicer + update_preset_name_option(preset_names, config); + } +} + +void PhysicalPrinter::reset_presets() +{ + return preset_names.clear(); +} + +bool PhysicalPrinter::add_preset(const std::string& preset_name) +{ + return preset_names.emplace(preset_name).second; +} + +bool PhysicalPrinter::delete_preset(const std::string& preset_name) +{ + return preset_names.erase(preset_name) > 0; +} + +PhysicalPrinter::PhysicalPrinter(const std::string& name, const DynamicPrintConfig& default_config) : + name(name), config(default_config) +{ + update_from_config(config); +} + +PhysicalPrinter::PhysicalPrinter(const std::string& name, const DynamicPrintConfig &default_config, const Preset& preset) : + name(name), config(default_config) +{ + update_from_preset(preset); +} + +void PhysicalPrinter::set_name(const std::string& name) +{ + this->name = name; +} + +std::string PhysicalPrinter::get_full_name(std::string preset_name) const +{ + return name + separator() + preset_name; +} + +std::string PhysicalPrinter::get_short_name(std::string full_name) +{ + int pos = full_name.find(separator()); + if (pos > 0) + boost::erase_tail(full_name, full_name.length() - pos); + return full_name; +} + +std::string PhysicalPrinter::get_preset_name(std::string name) +{ + int pos = name.find(separator()); + boost::erase_head(name, pos + 3); + return Preset::remove_suffix_modified(name); +} + + +// ----------------------------------- +// *** PhysicalPrinterCollection *** +// ----------------------------------- + +PhysicalPrinterCollection::PhysicalPrinterCollection( const std::vector& keys) +{ + // Default config for a physical printer containing all key/value pairs of PhysicalPrinter::printer_options(). + for (const std::string &key : keys) { + const ConfigOptionDef *opt = print_config_def.get(key); + assert(opt); + assert(opt->default_value); + m_default_config.set_key_value(key, opt->default_value->clone()); + } +} + +// Load all printers found in dir_path. +// Throws an exception on error. +void PhysicalPrinterCollection::load_printers( + const std::string& dir_path, const std::string& subdir, + PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule substitution_rule) +{ + // Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points, + // see https://github.com/prusa3d/PrusaSlicer/issues/732 + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(dir_path) / subdir).make_preferred(); + m_dir_path = dir.string(); + if(!boost::filesystem::exists(dir)) + return; + std::string errors_cummulative; + // Store the loaded printers into a new vector, otherwise the binary search for already existing presets would be broken. + std::deque printers_loaded; + //BBS: change to json format + for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) + { + std::string file_name = dir_entry.path().filename().string(); + //if (Slic3r::is_ini_file(dir_entry)) { + if (Slic3r::is_json_file(file_name)) { + // Remove the .json suffix. + std::string name = file_name.erase(file_name.size() - 5); + if (this->find_printer(name, false)) { + // This happens when there's is a preset (most likely legacy one) with the same name as a system preset + // that's already been loaded from a bundle. + BOOST_LOG_TRIVIAL(warning) << "Printer already present, not loading: " << name; + continue; + } + try { + PhysicalPrinter printer(name, this->default_config()); + printer.file = dir_entry.path().string(); + // Load the preset file, apply preset values on top of defaults. + try { + DynamicPrintConfig config; + //ConfigSubstitutions config_substitutions = config.load_from_ini(printer.file, substitution_rule); + std::map key_values; + std::string reason; + ConfigSubstitutions config_substitutions = config.load_from_json(printer.file, substitution_rule, key_values, reason); + if (! config_substitutions.empty()) + substitutions.push_back({ name, Preset::TYPE_PHYSICAL_PRINTER, PresetConfigSubstitutions::Source::UserFile, printer.file, std::move(config_substitutions) }); + printer.update_from_config(config); + printer.loaded = true; + } + catch (const std::ifstream::failure& err) { + throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + printer.file + "\n\tReason: " + err.what()); + } + catch (const std::runtime_error& err) { + throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + printer.file + "\n\tReason: " + err.what()); + } + printers_loaded.emplace_back(printer); + } + catch (const std::runtime_error& err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + } + } + } + m_printers.insert(m_printers.end(), std::make_move_iterator(printers_loaded.begin()), std::make_move_iterator(printers_loaded.end())); + std::sort(m_printers.begin(), m_printers.end()); + if (!errors_cummulative.empty()) + throw Slic3r::RuntimeError(errors_cummulative); +} + +void PhysicalPrinterCollection::load_printer(const std::string& path, const std::string& name, DynamicPrintConfig&& config, bool select, bool save/* = false*/) +{ + auto it = this->find_printer_internal(name); + if (it == m_printers.end() || it->name != name) { + // The preset was not found. Create a new preset. + it = m_printers.emplace(it, PhysicalPrinter(name, config)); + } + + it->file = path; + it->config = std::move(config); + it->loaded = true; + if (select) + this->select_printer(*it); + + if (save) + it->save(nullptr); +} + +// if there is saved user presets, contains information about "Print Host upload", +// Create default printers with this presets +// Note! "Print Host upload" options will be cleared after physical printer creations +void PhysicalPrinterCollection::load_printers_from_presets(PrinterPresetCollection& printer_presets) +{ +//BBS +#if 0 + int cnt=0; + for (Preset& preset: printer_presets) { + DynamicPrintConfig& config = preset.config; + for(const char* option : legacy_print_host_options) { + if (!config.opt_string(option).empty()) { + // check if printer with those "Print Host upload" options already exist + PhysicalPrinter* existed_printer = find_printer_with_same_config(config); + if (existed_printer) + // just add preset for this printer + existed_printer->add_preset(preset.name); + else { + std::string new_printer_name = (boost::format("Printer %1%") % ++cnt ).str(); + while (find_printer(new_printer_name)) + new_printer_name = (boost::format("Printer %1%") % ++cnt).str(); + + // create new printer from this preset + PhysicalPrinter printer(new_printer_name, this->default_config(), preset); + printer.loaded = true; + save_printer(printer); + } + + // erase "Print Host upload" information from the preset + for (const char *opt : legacy_print_host_options) + config.opt_string(opt).clear(); + // save changes for preset + preset.save(nullptr); + + // update those changes for edited preset if it's equal to the preset + Preset& edited = printer_presets.get_edited_preset(); + if (preset.name == edited.name) { + for (const char *opt : legacy_print_host_options) + edited.config.opt_string(opt).clear(); + } + + break; + } + } + } +#endif +} + +PhysicalPrinter* PhysicalPrinterCollection::find_printer( const std::string& name, bool case_sensitive_search) +{ + auto it = this->find_printer_internal(name, case_sensitive_search); + + // Ensure that a temporary copy is returned if the preset found is currently selected. + auto is_equal_name = [name, case_sensitive_search](const std::string& in_name) { + if (case_sensitive_search) + return in_name == name; + return boost::to_lower_copy(in_name) == boost::to_lower_copy(name); + }; + + if (it == m_printers.end() || !is_equal_name(it->name)) + return nullptr; + return &this->printer(it - m_printers.begin()); +} + +std::deque::iterator PhysicalPrinterCollection::find_printer_internal(const std::string& name, bool case_sensitive_search/* = true*/) +{ + if (case_sensitive_search) + return Slic3r::lower_bound_by_predicate(m_printers.begin(), m_printers.end(), [&name](const auto& l) { return l.name < name; }); + + std::string low_name = boost::to_lower_copy(name); + + size_t i = 0; + for (const PhysicalPrinter& printer : m_printers) { + if (boost::to_lower_copy(printer.name) == low_name) + break; + i++; + } + if (i == m_printers.size()) + return m_printers.end(); + + return m_printers.begin() + i; +} + +// Generate a file path from a profile name. Add the ".ini" suffix if it is missing. +std::string PhysicalPrinterCollection::path_from_name(const std::string& new_name) const +{ + //BBS: change to json format + //std::string file_name = boost::iends_with(new_name, ".ini") ? new_name : (new_name + ".ini"); + std::string file_name = boost::iends_with(new_name, ".json") ? new_name : (new_name + ".json"); + return (boost::filesystem::path(m_dir_path) / file_name).make_preferred().string(); +} + +void PhysicalPrinterCollection::save_printer(PhysicalPrinter& edited_printer, const std::string& renamed_from/* = ""*/) +{ + // controll and update preset_names in edited_printer config + edited_printer.update_preset_names_in_config(); + + std::string name = renamed_from.empty() ? edited_printer.name : renamed_from; + // 1) Find the printer with a new_name or create a new one, + // initialize it with the edited config. + auto it = this->find_printer_internal(name); + if (it != m_printers.end() && it->name == name) { + // Printer with the same name found. + // Overwriting an existing preset. + it->config = std::move(edited_printer.config); + it->name = edited_printer.name; + it->preset_names = edited_printer.preset_names; + // sort printers and get new it + std::sort(m_printers.begin(), m_printers.end()); + it = this->find_printer_internal(edited_printer.name); + } + else { + // Creating a new printer. + it = m_printers.emplace(it, edited_printer); + } + assert(it != m_printers.end()); + + // 2) Save printer + PhysicalPrinter& printer = *it; + if (printer.file.empty()) + printer.file = this->path_from_name(printer.name); + + if (printer.file == this->path_from_name(printer.name)) + printer.save(nullptr); + else + // if printer was renamed, we should rename a file and than save the config + printer.save(printer.file, this->path_from_name(printer.name)); + + // update idx_selected + m_idx_selected = it - m_printers.begin(); +} + +bool PhysicalPrinterCollection::delete_printer(const std::string& name) +{ + auto it = this->find_printer_internal(name); + if (it == m_printers.end()) + return false; + + const PhysicalPrinter& printer = *it; + // Erase the preset file. + boost::nowide::remove(printer.file.c_str()); + m_printers.erase(it); + return true; +} + +bool PhysicalPrinterCollection::delete_selected_printer() +{ + if (!has_selection()) + return false; + const PhysicalPrinter& printer = this->get_selected_printer(); + + // Erase the preset file. + boost::nowide::remove(printer.file.c_str()); + // Remove the preset from the list. + m_printers.erase(m_printers.begin() + m_idx_selected); + // unselect all printers + unselect_printer(); + + return true; +} + +bool PhysicalPrinterCollection::delete_preset_from_printers( const std::string& preset_name) +{ + std::vector printers_for_delete; + for (PhysicalPrinter& printer : m_printers) { + if (printer.preset_names.size() == 1 && *printer.preset_names.begin() == preset_name) + printers_for_delete.emplace_back(printer.name); + else if (printer.delete_preset(preset_name)) + save_printer(printer); + } + + if (!printers_for_delete.empty()) + for (const std::string& printer_name : printers_for_delete) + delete_printer(printer_name); + + unselect_printer(); + return true; +} + +// Get list of printers which have more than one preset and "preset_names" preset is one of them +std::vector PhysicalPrinterCollection::get_printers_with_preset(const std::string& preset_name) +{ + std::vector printers; + + for (auto printer : m_printers) { + if (printer.preset_names.size() == 1) + continue; + if (printer.preset_names.find(preset_name) != printer.preset_names.end()) + printers.emplace_back(printer.name); + } + + return printers; +} + +// Get list of printers which has only "preset_names" preset +std::vector PhysicalPrinterCollection::get_printers_with_only_preset(const std::string& preset_name) +{ + std::vector printers; + + for (auto printer : m_printers) + if (printer.preset_names.size() == 1 && *printer.preset_names.begin() == preset_name) + printers.emplace_back(printer.name); + + return printers; +} + +std::string PhysicalPrinterCollection::get_selected_full_printer_name() const +{ + return (m_idx_selected == size_t(-1)) ? std::string() : this->get_selected_printer().get_full_name(m_selected_preset); +} + +void PhysicalPrinterCollection::select_printer(const std::string& full_name) +{ + std::string printer_name = PhysicalPrinter::get_short_name(full_name); + auto it = this->find_printer_internal(printer_name); + if (it == m_printers.end()) { + unselect_printer(); + return; + } + + // update idx_selected + m_idx_selected = it - m_printers.begin(); + + // update name of the currently selected preset + if (printer_name == full_name) + // use first preset in the list + m_selected_preset = *it->preset_names.begin(); + else + m_selected_preset = it->get_preset_name(full_name); +} + +void PhysicalPrinterCollection::select_printer(const std::string& printer_name, const std::string& preset_name) +{ + if (preset_name.empty()) + return select_printer(printer_name); + return select_printer(printer_name + PhysicalPrinter::separator() + preset_name); +} + +void PhysicalPrinterCollection::select_printer(const PhysicalPrinter& printer) +{ + return select_printer(printer.name); +} + +bool PhysicalPrinterCollection::has_selection() const +{ + return m_idx_selected != size_t(-1); +} + +void PhysicalPrinterCollection::unselect_printer() +{ + m_idx_selected = size_t(-1); + m_selected_preset.clear(); +} + +bool PhysicalPrinterCollection::is_selected(PhysicalPrinterCollection::ConstIterator it, const std::string& preset_name) const +{ + return m_idx_selected == size_t(it - m_printers.begin()) && + m_selected_preset == preset_name; +} + + +namespace PresetUtils { + const VendorProfile::PrinterModel* system_printer_model(const Preset &preset) + { + const VendorProfile::PrinterModel *out = nullptr; + if (preset.vendor != nullptr) { + auto *printer_model = preset.config.opt("printer_model"); + if (printer_model != nullptr && ! printer_model->value.empty()) { + auto it = std::find_if(preset.vendor->models.begin(), preset.vendor->models.end(), [printer_model](const VendorProfile::PrinterModel &pm) { return pm.id == printer_model->value; }); + if (it != preset.vendor->models.end()) + out = &(*it); + } + } + return out; + } + + std::string system_printer_bed_model(const Preset& preset) + { + std::string out; + const VendorProfile::PrinterModel* pm = PresetUtils::system_printer_model(preset); + if (pm != nullptr && !pm->bed_model.empty()) { + out = Slic3r::data_dir() + "/vendor/" + preset.vendor->id + "/" + pm->bed_model; + if (!boost::filesystem::exists(boost::filesystem::path(out))) + out = Slic3r::resources_dir() + "/profiles/" + preset.vendor->id + "/" + pm->bed_model; + } + return out; + } + + std::string system_printer_bed_texture(const Preset& preset) + { + std::string out; + const VendorProfile::PrinterModel* pm = PresetUtils::system_printer_model(preset); + if (pm != nullptr && !pm->bed_texture.empty()) { + out = Slic3r::data_dir() + "/vendor/" + preset.vendor->id + "/" + pm->bed_texture; + if (!boost::filesystem::exists(boost::filesystem::path(out))) + out = Slic3r::resources_dir() + "/profiles/" + preset.vendor->id + "/" + pm->bed_texture; + } + return out; + } + + std::string system_printer_hotend_model(const Preset& preset) + { + std::string out; + const VendorProfile::PrinterModel* pm = PresetUtils::system_printer_model(preset); + if (pm != nullptr && !pm->hotend_model.empty()) { + out = Slic3r::data_dir() + "/vendor/" + preset.vendor->id + "/" + pm->hotend_model; + if (!boost::filesystem::exists(boost::filesystem::path(out))) + out = Slic3r::resources_dir() + "/profiles/" + preset.vendor->id + "/" + pm->hotend_model; + } + + if (out.empty() ||!boost::filesystem::exists(boost::filesystem::path(out))) + out = Slic3r::resources_dir() + "/profiles/hotend.stl"; + return out; + } +} // namespace PresetUtils + +} // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp new file mode 100644 index 0000000000..684f9256bb --- /dev/null +++ b/src/libslic3r/PresetBundle.cpp @@ -0,0 +1,5534 @@ +#include +#include + +#include "PresetBundle.hpp" +#include "PrintConfig.hpp" +#include "libslic3r.h" +#include "I18N.hpp" +#include "Utils.hpp" +#include "Model.hpp" +#include "libslic3r_version.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Mark string for localization and translate. +#define L(s) Slic3r::I18N::translate(s) + +// Store the print/filament/printer presets into a "presets" subdirectory of the Slic3rPE config dir. +// This breaks compatibility with the upstream Slic3r if the --datadir is used to switch between the two versions. +//#define SLIC3R_PROFILE_USE_PRESETS_SUBDIR + +namespace Slic3r { + +static std::vector s_project_options { + "flush_volumes_vector", + "flush_volumes_matrix", + // BBS + "filament_colour", + "filament_colour_type", + "filament_multi_colour", + "wipe_tower_x", + "wipe_tower_y", + "wipe_tower_rotation_angle", + "curr_bed_type", + "flush_multiplier", + "nozzle_volume_type", + "filament_map_mode", + "filament_map" +}; + +//Orca: add custom as default +const char *PresetBundle::ORCA_DEFAULT_BUNDLE = "Custom"; +const char *PresetBundle::ORCA_DEFAULT_PRINTER_MODEL = "MyKlipper 0.4 nozzle"; +const char *PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT = "0.4"; +const char *PresetBundle::ORCA_DEFAULT_FILAMENT = "Generic PLA @System"; +const char *PresetBundle::ORCA_FILAMENT_LIBRARY = "OrcaFilamentLibrary"; +const char *PresetBundle::ORCA_DEFAULT_FILAMENT_PLACEHOLDER = "Default Filament"; + +DynamicPrintConfig PresetBundle::construct_full_config( + Preset& in_printer_preset, + Preset& in_print_preset, + const DynamicPrintConfig& project_config, + std::vector& in_filament_presets, + bool apply_extruder, + std::optional> filament_maps_new) +{ + DynamicPrintConfig &printer_config = in_printer_preset.config; + DynamicPrintConfig &print_config = in_print_preset.config; + + DynamicPrintConfig out; + out.apply(FullPrintConfig::defaults()); + out.apply(printer_config); + out.apply(print_config); + out.apply(project_config); + out.apply(in_filament_presets[0].config); + + size_t num_filaments = in_filament_presets.size(); + + std::vector filament_maps = out.option("filament_map")->values; + if (filament_maps_new.has_value()) + filament_maps = *filament_maps_new; + // in some middle state, they may be different + if (filament_maps.size() != num_filaments) { + filament_maps.resize(num_filaments, 1); + } + + auto *extruder_diameter = dynamic_cast(out.option("nozzle_diameter")); + // Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector. + std::vector compatible_printers_condition; + std::vector compatible_prints_condition; + std::vector inherits; + std::vector filament_ids; + std::vector print_compatible_printers; + // BBS: add logic for settings check between different system presets + std::vector different_settings; + std::string different_print_settings, different_printer_settings; + compatible_printers_condition.emplace_back(in_print_preset.compatible_printers_condition()); + + const ConfigOptionStrings *compatible_printers = print_config.option("compatible_printers", false); + if (compatible_printers) print_compatible_printers = compatible_printers->values; + // BBS: add logic for settings check between different system presets + std::string print_inherits = in_print_preset.inherits(); + inherits.emplace_back(print_inherits); + + // BBS: update printer config related with variants + if (apply_extruder) { + out.update_values_to_printer_extruders(out, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); + out.update_values_to_printer_extruders(out, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); + // update print config related with variants + out.update_values_to_printer_extruders(out, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + } + + if (num_filaments <= 1) { + // BBS: update filament config related with variants + DynamicPrintConfig filament_config = in_filament_presets[0].config; + if (apply_extruder) filament_config.update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0]); + out.apply(filament_config); + compatible_printers_condition.emplace_back(in_filament_presets[0].compatible_printers_condition()); + compatible_prints_condition.emplace_back(in_filament_presets[0].compatible_prints_condition()); + std::string filament_inherits = in_filament_presets[0].inherits(); + inherits.emplace_back(filament_inherits); + filament_ids.emplace_back(in_filament_presets[0].filament_id); + + std::vector &filament_self_indice = out.option("filament_self_index", true)->values; + int index_size = out.option("filament_extruder_variant")->size(); + filament_self_indice.resize(index_size, 1); + } else { + std::vector filament_configs; + std::vector filament_presets; + for (const Preset & preset : in_filament_presets) { + filament_presets.emplace_back(&preset); + filament_configs.emplace_back(&(preset.config)); + } + + std::vector filament_temp_configs; + filament_temp_configs.resize(num_filaments); + for (size_t i = 0; i < num_filaments; ++i) { + filament_temp_configs[i] = *(filament_configs[i]); + if (apply_extruder) + filament_temp_configs[i].update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i]); + } + + // loop through options and apply them to the resulting config. + std::vector filament_variant_count(num_filaments, 1); + for (const t_config_option_key &key : in_filament_presets[0].config.keys()) { + if (key == "compatible_prints" || key == "compatible_printers") continue; + // Get a destination option. + ConfigOption *opt_dst = out.option(key, false); + if (opt_dst->is_scalar()) { + // Get an option, do not create if it does not exist. + const ConfigOption *opt_src = filament_temp_configs.front().option(key); + if (opt_src != nullptr) opt_dst->set(opt_src); + } else { + // BBS + ConfigOptionVectorBase *opt_vec_dst = static_cast(opt_dst); + { + if (apply_extruder) { + std::vector filament_opts(num_filaments, nullptr); + // Setting a vector value from all filament_configs. + for (size_t i = 0; i < filament_opts.size(); ++i) filament_opts[i] = filament_temp_configs[i].option(key); + opt_vec_dst->set(filament_opts); + } else { + for (size_t i = 0; i < num_filaments; ++i) { + const ConfigOptionVectorBase *filament_option = static_cast(filament_temp_configs[i].option(key)); + if (i == 0) + opt_vec_dst->set(filament_option); + else + opt_vec_dst->append(filament_option); + + if (key == "filament_extruder_variant") filament_variant_count[i] = filament_option->size(); + } + } + } + } + } + + if (!apply_extruder) { + // append filament_self_index + std::vector &filament_self_indice = out.option("filament_self_index", true)->values; + int index_size = out.option("filament_extruder_variant")->size(); + filament_self_indice.resize(index_size, 1); + int k = 0; + for (size_t i = 0; i < num_filaments; i++) { + for (size_t j = 0; j < filament_variant_count[i]; j++) { filament_self_indice[k++] = i + 1; } + } + } + } + + // These value types clash between the print and filament profiles. They should be renamed. + out.erase("compatible_prints"); + out.erase("compatible_prints_condition"); + out.erase("compatible_printers"); + out.erase("compatible_printers_condition"); + out.erase("inherits"); + // BBS: add logic for settings check between different system presets + out.erase("different_settings_to_system"); + + static const char *keys[] = {"support_filament", "support_interface_filament"}; + for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { + std::string key = std::string(keys[i]); + auto *opt = dynamic_cast(out.option(key, false)); + assert(opt != nullptr); + opt->value = boost::algorithm::clamp(opt->value, 0, int(num_filaments)); + } + + std::vector filamnet_preset_names; + for (auto preset : in_filament_presets) { + filamnet_preset_names.emplace_back(preset.name); + } + out.option("print_settings_id", true)->value = in_print_preset.name; + out.option("filament_settings_id", true)->values = filamnet_preset_names; + out.option("printer_settings_id", true)->value = in_printer_preset.name; + out.option("filament_ids", true)->values = filament_ids; + out.option("filament_map", true)->values = filament_maps; + + auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { + bool nonempty = false; + for (const std::string &v : values) + if (!v.empty()) { + nonempty = true; + break; + } + if (nonempty) out.set_key_value(key, new ConfigOptionStrings(std::move(values))); + }; + add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); + add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); + add_if_some_non_empty(std::move(inherits), "inherits_group"); + // BBS: add logic for settings check between different system presets + //add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); + add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers"); + + out.option("printer_technology", true)->value = ptFFF; + return out; +} + +std::string PresetBundle::find_preset_vendor(const std::string &preset_name, Preset::Type type) +{ + // Get the resources preset directory (contains all bundled vendor profiles) + fs::path system_dir = fs::path(Slic3r::resources_dir()) / PRESET_PROFILES_DIR; + if (!fs::exists(system_dir) || !fs::is_directory(system_dir)) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " Resources profiles directory does not exist: " << system_dir.string(); + return ""; + } + + // Determine which preset list key to search for based on type + const char* preset_list_key = nullptr; + if (type == Preset::Type::TYPE_PRINT) + preset_list_key = BBL_JSON_KEY_PROCESS_LIST; + else if (type == Preset::Type::TYPE_FILAMENT) + preset_list_key = BBL_JSON_KEY_FILAMENT_LIST; + else if (type == Preset::Type::TYPE_PRINTER) + preset_list_key = BBL_JSON_KEY_MACHINE_LIST; + else { + // Not supported for other types + return ""; + } + + // Iterate through vendor JSON files in the system directory + for (auto& dir_entry : fs::directory_iterator(system_dir)) { + std::string vendor_file = dir_entry.path().string(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Checking vendor: " << vendor_file; + if (!Slic3r::is_json_file(vendor_file)) + continue; + + // Get vendor name (filename without .json extension) + std::string vendor_name = dir_entry.path().filename().string(); + vendor_name.erase(vendor_name.size() - 5); // Remove ".json" + + try { + // Load and parse the vendor JSON file + boost::nowide::ifstream ifs(vendor_file); + json j; + ifs >> j; + + // Check if the preset list key exists + if (!j.contains(preset_list_key)) + continue; + + auto& preset_list = j[preset_list_key]; + if (!preset_list.is_array()) + continue; + + // Search for the preset in the list + for (auto& preset_entry : preset_list) { + if (!preset_entry.is_object()) + continue; + + // Get the preset name + std::string p_name; + if (preset_entry.contains(BBL_JSON_KEY_NAME) && preset_entry[BBL_JSON_KEY_NAME].is_string()) + p_name = preset_entry[BBL_JSON_KEY_NAME].get(); + + if (p_name != preset_name) + continue; + + // Found the preset! Get the vendor name and install the entire bundle + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << p_name + << " in vendor bundle " << vendor_name; + + return vendor_name; + } + } + catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to find vendor name for " << preset_name << ": " << e.what(); + return ""; + } + } + + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Could not find vendor for preset " << preset_name; + return ""; +} + +PresetBundle::PresetBundle() + : prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast(FullPrintConfig::defaults())) + , filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), static_cast(FullPrintConfig::defaults()), ORCA_DEFAULT_FILAMENT_PLACEHOLDER) + , sla_materials(Preset::TYPE_SLA_MATERIAL, Preset::sla_material_options(), static_cast(SLAFullPrintConfig::defaults())) + , sla_prints(Preset::TYPE_SLA_PRINT, Preset::sla_print_options(), static_cast(SLAFullPrintConfig::defaults())) + , printers(Preset::TYPE_PRINTER, Preset::printer_options(), static_cast(FullPrintConfig::defaults()), "Default Printer") + , physical_printers(PhysicalPrinter::printer_options()) +{ + // The following keys are handled by the UI, they do not have a counterpart in any StaticPrintConfig derived classes, + // therefore they need to be handled differently. As they have no counterpart in StaticPrintConfig, they are not being + // initialized based on PrintConfigDef(), but to empty values (zeros, empty vectors, empty strings). + // + // "compatible_printers", "compatible_printers_condition", "inherits", + // "print_settings_id", "filament_settings_id", "printer_settings_id", "printer_settings_id" + // "printer_model", "printer_variant", "default_print_profile", "default_filament_profile" + + // Create the ID config keys, as they are not part of the Static print config classes. + this->prints.default_preset().config.optptr("print_settings_id", true); + this->prints.default_preset().compatible_printers_condition(); + this->prints.default_preset().inherits(); + + this->filaments.default_preset().config.option("filament_settings_id", true)->values = {""}; + this->filaments.default_preset().compatible_printers_condition(); + this->filaments.default_preset().inherits(); + // Set all the nullable values to nils. + { + auto& default_config = this->filaments.default_preset().config; + for(const std::string& opt_key : default_config.keys()){ + ConfigOption* opt = default_config.optptr(opt_key, false); + bool is_override_key = std::find(filament_extruder_override_keys.begin(),filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end(); + if(!is_override_key || !opt->nullable()) + continue; + opt->deserialize("nil",ForwardCompatibilitySubstitutionRule::Disable); + } + } + + this->sla_materials.default_preset().config.optptr("sla_material_settings_id", true); + this->sla_materials.default_preset().compatible_printers_condition(); + this->sla_materials.default_preset().inherits(); + + this->sla_prints.default_preset().config.optptr("sla_print_settings_id", true); + this->sla_prints.default_preset().config.opt_string("filename_format", true) = "[input_filename_base].sl1"; + this->sla_prints.default_preset().compatible_printers_condition(); + this->sla_prints.default_preset().inherits(); + + //this->printers.add_default_preset(Preset::sla_printer_options(), static_cast(SLAFullPrintConfig::defaults()), "- default SLA -"); + //this->printers.preset(1).printer_technology_ref() = ptSLA; + for (size_t i = 0; i < 1; ++i) { + // The following ugly switch is to avoid printers.preset(0) to return the edited instance, as the 0th default is the current one. + Preset &preset = this->printers.default_preset(i); + for (const char *key : {"printer_settings_id", "printer_model", "printer_variant", "thumbnails"}) preset.config.optptr(key, true); + //if (i == 0) { + preset.config.optptr("default_print_profile", true); + preset.config.option("default_filament_profile", true); + //} else { + // preset.config.optptr("default_sla_print_profile", true); + // preset.config.optptr("default_sla_material_profile", true); + //} + // default_sla_material_profile + preset.inherits(); + } + + // Re-activate the default presets, so their "edited" preset copies will be updated with the additional configuration values above. + this->prints.select_preset(0); + this->sla_prints.select_preset(0); + this->filaments.select_preset(0); + this->sla_materials.select_preset(0); + this->printers.select_preset(0); + + this->project_config.apply_only(FullPrintConfig::defaults(), s_project_options); +} + +PresetBundle::PresetBundle(const PresetBundle &rhs) +{ + *this = rhs; +} + +PresetBundle& PresetBundle::operator=(const PresetBundle &rhs) +{ + prints = rhs.prints; + sla_prints = rhs.sla_prints; + filaments = rhs.filaments; + sla_materials = rhs.sla_materials; + printers = rhs.printers; + physical_printers = rhs.physical_printers; + + filament_presets = rhs.filament_presets; + project_config = rhs.project_config; + vendors = rhs.vendors; + obsolete_presets = rhs.obsolete_presets; + m_errors = rhs.m_errors; + + // Adjust Preset::vendor pointers to point to the copied vendors map. + prints .update_vendor_ptrs_after_copy(this->vendors); + sla_prints .update_vendor_ptrs_after_copy(this->vendors); + filaments .update_vendor_ptrs_after_copy(this->vendors); + sla_materials.update_vendor_ptrs_after_copy(this->vendors); + printers .update_vendor_ptrs_after_copy(this->vendors); + + return *this; +} + +void PresetBundle::reset(bool delete_files) +{ + // Clear the existing presets, delete their respective files. + this->vendors.clear(); + this->prints .reset(delete_files); + this->sla_prints .reset(delete_files); + this->filaments .reset(delete_files); + this->sla_materials.reset(delete_files); + this->printers .reset(delete_files); + // BBS: filament_presets is load from project config, not handled here + //this->filament_presets.clear(); + if (this->filament_presets.empty()) + this->filament_presets.emplace_back(this->filaments.get_selected_preset_name()); + this->obsolete_presets.prints.clear(); + this->obsolete_presets.sla_prints.clear(); + this->obsolete_presets.filaments.clear(); + this->obsolete_presets.sla_materials.clear(); + this->obsolete_presets.printers.clear(); +} + +void PresetBundle::setup_directories() +{ + boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); + //BBS: change directoties by design + std::initializer_list paths = { + data_dir, + data_dir / "ota", + data_dir / PRESET_SYSTEM_DIR, + data_dir / PRESET_USER_DIR, + // Store the print/filament/printer presets at the same location as the upstream Slic3r. + //data_dir / PRESET_SYSTEM_DIR / PRESET_PRINT_NAME, + //data_dir / PRESET_SYSTEM_DIR / PRESET_FILAMENT_NAME, + //data_dir / PRESET_SYSTEM_DIR / PRESET_PRINTER_NAME + }; + for (const boost::filesystem::path &path : paths) { + boost::filesystem::path subdir = path; + subdir.make_preferred(); + if (! boost::filesystem::is_directory(subdir) && + ! boost::filesystem::create_directory(subdir)) { + if (boost::filesystem::is_directory(subdir)) { + BOOST_LOG_TRIVIAL(warning) << boost::format("creating directory %1% failed, maybe created by other instance, go on!")%subdir.string(); + } + else + throw Slic3r::RuntimeError(std::string("Unable to create directory ") + subdir.string()); + } + } +} + +// recursively copy all files and dirs in from_dir to to_dir +static void copy_dir(const boost::filesystem::path& from_dir, const boost::filesystem::path& to_dir) +{ + if(!boost::filesystem::is_directory(from_dir)) + return; + // i assume to_dir.parent surely exists + if (!boost::filesystem::is_directory(to_dir)) + boost::filesystem::create_directory(to_dir); + for (auto& dir_entry : boost::filesystem::directory_iterator(from_dir)) { + if (!boost::filesystem::is_directory(dir_entry.path())) { + std::string em; + CopyFileResult cfr = copy_file(dir_entry.path().string(), (to_dir / dir_entry.path().filename()).string(), em, false); + if (cfr != SUCCESS) { + BOOST_LOG_TRIVIAL(error) << "Error when copying files from " << from_dir << " to " << to_dir << ": " << em; + } + } else { + copy_dir(dir_entry.path(), to_dir / dir_entry.path().filename()); + } + } +} + +void PresetBundle::copy_files(const std::string& from) +{ + boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); + // list of searched paths based on current directory system in setup_directories() + // do not copy cache and snapshots + boost::filesystem::path from_data_dir = boost::filesystem::path(from); + //BBS: change directoties by design + std::initializer_list from_dirs= { + //from_data_dir / "vendor", + // Store the print/filament/printer presets at the same location as the upstream Slic3r. + from_data_dir / PRESET_PRINT_NAME, + from_data_dir / PRESET_FILAMENT_NAME, + from_data_dir / PRESET_PRINTER_NAME + }; + // copy recursively all files + //BBS: change directoties by design + for (const boost::filesystem::path& from_dir : from_dirs) { + copy_dir(from_dir, data_dir /"old"/from_dir.filename()); + } +} + +PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule substitution_rule, + const PresetPreferences& preferred_selection/* = PresetPreferences()*/) +{ + // First load the vendor specific system presets. + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%")%substitution_rule%preferred_selection.printer_model_id; + //BBS: change system config to json + std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule); + + // BBS load preset from user's folder, load system default if + // BBS: change directories by design + std::string dir_user_presets = config.get("preset_folder"); + if (dir_user_presets.empty()) { + load_user_presets(DEFAULT_USER_FOLDER_NAME, substitution_rule); + } else { + load_user_presets(dir_user_presets, substitution_rule); + } + + this->update_multi_material_filament_presets(); + this->update_compatible(PresetSelectCompatibleType::Never); + + this->load_selections(config, preferred_selection); + + set_calibrate_printer(""); + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); + return substitutions; +} + +//BBS: add function to generate differed preset for save +//the pointer should be freed by the caller +Preset* PresetBundle::get_preset_differed_for_save(Preset& preset) +{ + PresetCollection* preset_collection; + + switch(preset.type) { + case Preset::TYPE_PRINT: + preset_collection = &(this->prints); + break; + case Preset::TYPE_PRINTER: + preset_collection = &(this->printers); + break; + case Preset::TYPE_FILAMENT: + preset_collection = &(this->filaments); + break; + default: + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly")%preset.type; + return nullptr; + } + + return preset_collection->get_preset_differed_for_save(preset); +} + +int PresetBundle::get_differed_values_to_update(Preset& preset, std::map& key_values) +{ + PresetCollection* preset_collection; + + switch(preset.type) { + case Preset::TYPE_PRINT: + preset_collection = &(this->prints); + break; + case Preset::TYPE_PRINTER: + preset_collection = &(this->printers); + break; + case Preset::TYPE_FILAMENT: + preset_collection = &(this->filaments); + break; + default: + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly")%preset.type; + return -1; + } + + return preset_collection->get_differed_values_to_update(preset, key_values); +} + +//BBS: get vendor's current version +Semver PresetBundle::get_vendor_profile_version(std::string vendor_name) +{ + Semver result_ver; + + auto vendor_profile = vendors.find(vendor_name); + if (vendor_profile != vendors.end()) { + result_ver = vendor_profile->second.config_version; + } + + return result_ver; +} + +VendorType PresetBundle::get_current_vendor_type() +{ + auto t = VendorType::Unknown; + auto config = &printers.get_edited_preset().config; + const auto* printer_model = config->opt("printer_model"); + if (printer_model == nullptr) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": printer_model is " + << (config->has("printer_model") ? "not a string" : "missing") + << ", vendor type is Unknown"; + return t; + } + + std::string vendor_name; + for (const auto& vendor_profile : vendors) { + for (const auto& vendor_model : vendor_profile.second.models) { + if (vendor_model.name == printer_model->value) { + vendor_name = vendor_profile.first; + break; + } + } + if (!vendor_name.empty()) + break; + } + if (!vendor_name.empty()) + { + if(vendor_name.compare("BBL") == 0) + t = VendorType::Marlin_BBL; + + if(vendor_name.compare("Qidi") == 0) + t = VendorType::Klipper_Qidi; + } + return t; +} + +bool PresetBundle::use_bbl_network() +{ + const auto cfg = printers.get_edited_preset().config; + const bool use_bbl_network = is_bbl_vendor() && !cfg.opt_bool("bbl_use_printhost"); + return use_bbl_network; +} + +bool PresetBundle::use_bbl_device_tab() { + if (!is_bbl_vendor()) { + return false; + } + + if (use_bbl_network()) { + return true; + } + + const auto cfg = printers.get_edited_preset().config; + // Use bbl device tab if printhost webui url is not set + return cfg.opt_string("print_host_webui").empty(); +} + +bool PresetBundle::backup_user_folder() const +{ + const std::string backup_folderpath = data_dir() + "/" + (boost::format("user_backup-v%1%") % SoftFever_VERSION).str(); + + // Check if backup file already exists + if (boost::filesystem::exists(boost::filesystem::path(backup_folderpath))) + return false; + + BOOST_LOG_TRIVIAL(info) << "Backing up user folder to: " << backup_folderpath; + try { + // Copy the user folder to the backup folder + boost::filesystem::copy(data_dir() + "/" + PRESET_USER_DIR, backup_folderpath, boost::filesystem::copy_options::recursive); + BOOST_LOG_TRIVIAL(info) << "User folder backup completed successfully"; + return true; + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << "Exception during user folder backup: " << ex.what(); + // Try to clean up partially copied backup folder + if (boost::filesystem::exists(boost::filesystem::path(backup_folderpath))) + boost::filesystem::remove_all(boost::filesystem::path(backup_folderpath)); + return false; + } +} + +std::optional PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const +{ + if (filament_id.empty()) + return std::nullopt; + + // basic filament info should be same in the parent preset and child preset + // so just match the filament id is enough + + for (auto iter = filaments.begin(); iter != filaments.end(); ++iter) { + const Preset& filament_preset = *iter; + const auto& config = filament_preset.config; + if (filament_preset.filament_id == filament_id) { + FilamentBaseInfo info; + info.filament_id = filament_id; + info.is_system = filament_preset.is_system; + info.filament_name = filament_preset.alias; + if (config.has("filament_is_support")) + info.is_support = config.option("filament_is_support")->values[0]; + if (config.has("filament_type")) + info.filament_type = config.option("filament_type")->values[0]; + if (config.has("filament_vendor")) + info.vendor = config.option("filament_vendor")->values[0]; + if (config.has("nozzle_temperature_range_high")) + info.nozzle_temp_range_high = config.option("nozzle_temperature_range_high")->values[0]; + if (config.has("nozzle_temperature_range_low")) + info.nozzle_temp_range_low = config.option("nozzle_temperature_range_low")->values[0]; + if(config.has("temperature_vitrification")) + info.temperature_vitrification = config.option("temperature_vitrification")->values[0]; + + if (!printer_name.empty()) { + std::vector compatible_printers = config.option("compatible_printers")->values; + auto iter = std::find(compatible_printers.begin(), compatible_printers.end(), printer_name); + if (iter != compatible_printers.end() && config.has("filament_printable")) { + info.filament_printable = config.option("filament_printable")->values[0]; + return info; + } + } + else { + return info; + } + } + } + return std::nullopt; +} + +//BBS: load project embedded presets +PresetsConfigSubstitutions PresetBundle::load_project_embedded_presets(std::vector project_presets, ForwardCompatibilitySubstitutionRule substitution_rule) +{ + // First load the vendor specific system presets. + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preset toltal count %2%")%substitution_rule% project_presets.size(); + try { + this->prints.load_project_embedded_presets(project_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + } + try { + this->filaments.load_project_embedded_presets(project_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + } + try { + this->printers.load_project_embedded_presets(project_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + } + + //this->update_multi_material_filament_presets(); + //this->update_compatible(PresetSelectCompatibleType::Never); + if (! errors_cummulative.empty()) + throw Slic3r::RuntimeError(errors_cummulative); + + //this->load_selections(config, ""); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); + return substitutions; +} + +//BBS: get current project embedded presets +std::vector PresetBundle::get_current_project_embedded_presets() +{ + std::vector project_presets; + + project_presets = this->prints.get_project_embedded_presets(); + + auto filament_presets = this->filaments.get_project_embedded_presets(); + if (!filament_presets.empty()) + std::copy(filament_presets.begin(), filament_presets.end(), std::back_inserter(project_presets)); + auto printer_presets = this->printers.get_project_embedded_presets(); + if (!printer_presets.empty()) + std::copy(printer_presets.begin(), printer_presets.end(), std::back_inserter(project_presets)); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned project_presets count %1%")%project_presets.size(); + return project_presets; +} + +//BBS: reset project embedded presets +void PresetBundle::reset_project_embedded_presets() +{ + std::string prefer_printer; + Preset& current_printer = this->printers.get_selected_preset(); + ConfigOption* inherits = current_printer.config.option("inherits"); + if (inherits) { + prefer_printer = dynamic_cast(inherits)->value; + } + //first printer, then filament, then print + bool printer_reselect = this->printers.reset_project_embedded_presets(); + bool filament_reselect = this->filaments.reset_project_embedded_presets(); + bool print_reselect = this->prints.reset_project_embedded_presets(); + + if (printer_reselect) { + if (!prefer_printer.empty()) + this->printers.select_preset_by_name(prefer_printer, true); + else + this->printers.select_preset(this->printers.first_visible_idx()); + + //this->update_multi_material_filament_presets(); + this->update_compatible(PresetSelectCompatibleType::Never); + } + else if (filament_reselect || print_reselect) { + //Preset& current_printer = this->printers.get_selected_preset(); + /*if (filament_reselect) { + const std::vector &prefered_filament_profiles = current_printer.config.option("default_filament_profile")->values; + const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); + if (!prefered_filament_profile.empty()) + this->filaments.select_preset_by_name(prefered_filament_profile, true); + else + this->filaments.select_preset(this->filaments.first_visible_idx()); + } + + if (print_reselect) { + }*/ + this->update_compatible(PresetSelectCompatibleType::Never); + } + + //this->update_multi_material_filament_presets(); + + //update filament_presets + for (size_t i = 0; i < filament_presets.size(); ++ i) + { + Preset* selected_filament = this->filaments.find_preset(filament_presets[i], false); + if (!selected_filament) { + //it should be the project embedded presets + Preset& current_printer = this->printers.get_selected_preset(); + const std::vector &prefered_filament_profiles = current_printer.config.option("default_filament_profile")->values; + const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); + if (!prefered_filament_profile.empty()) { + // Check if preferred filament exists and is visible + const Preset* preferred_preset = this->filaments.find_preset(prefered_filament_profile, false); + if (preferred_preset && preferred_preset->is_visible) { + filament_presets[i] = prefered_filament_profile; + } else { + // Fall back to first visible filament + filament_presets[i] = this->filaments.first_visible().name; + } + } else + filament_presets[i] = this->filaments.first_visible().name; + } + } +} + +//BBS: get bed texture for printer model +std::string PresetBundle::get_texture_for_printer_model(std::string model_name) +{ + std::string texture_name, vendor_name, out; + + for (auto vendor_profile: this->vendors) + { + for (auto vendor_model: vendor_profile.second.models) + { + if (vendor_model.name == model_name || vendor_model.id == model_name) + { + texture_name = vendor_model.bed_texture; + vendor_name = vendor_profile.first; + break; + } + } + } + + if (!texture_name.empty()) + { + out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + texture_name; + if (!boost::filesystem::exists(boost::filesystem::path(out))) + out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + texture_name; + } + + return out; +} + +//BBS: get stl model for printer model +std::string PresetBundle::get_stl_model_for_printer_model(std::string model_name) +{ + std::string stl_name, vendor_name, out; + + for (auto vendor_profile: this->vendors) + { + for (auto vendor_model: vendor_profile.second.models) + { + if (vendor_model.name == model_name) + { + stl_name = vendor_model.bed_model; + vendor_name = vendor_profile.first; + break; + } + } + } + + if (!stl_name.empty()) + { + out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + stl_name; + if (!boost::filesystem::exists(boost::filesystem::path(out))) + out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + stl_name; + } + + return out; +} + +std::string PresetBundle::get_hotend_model_for_printer_model(std::string model_name) +{ + std::string hotend_stl, vendor_name, out; + + for (auto vendor_profile: this->vendors) + { + for (auto vendor_model: vendor_profile.second.models) + { + if (vendor_model.name == model_name) + { + hotend_stl = vendor_model.hotend_model; + vendor_name = vendor_profile.first; + break; + } + } + } + + if (!hotend_stl.empty()) + { + out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + hotend_stl; + if (!boost::filesystem::exists(boost::filesystem::path(out))) + out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + hotend_stl; + } + + if (out.empty() ||!boost::filesystem::exists(boost::filesystem::path(out))) + out = Slic3r::resources_dir() + "/profiles/hotend.stl"; + + return out; +} + +PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule substitution_rule) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " entry and user is: " << user; + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + + fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); + if (!fs::exists(user_folder)) fs::create_directory(user_folder); + + std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + user; + fs::path folder(user_folder / user); + if (!fs::exists(folder)) fs::create_directory(folder); + + bundles.WriteLock(); + bundles.m_bundles.clear(); + bundles.WriteUnlock(); + + // Load bundle metadata from _local directory first + fs::path local_dir(folder / PRESET_LOCAL_DIR); + if (fs::exists(local_dir)) { + dir_user_presets_local = local_dir; + for (auto& entry : fs::directory_iterator(local_dir)) { + if (!fs::is_directory(entry.path())) continue; + + std::string bundle_dir = entry.path().string(); + + fs::path metadata_file = entry.path() / PRESET_BUNDLE_METADATA; + if (!fs::exists(metadata_file)) continue; + + BundleMetadata metadata; + if (!metadata.load_from_json(metadata_file.string())) continue; + metadata.print_presets.clear(); + metadata.filament_presets.clear(); + metadata.printer_presets.clear(); + + // Add the profiles + this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.print_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.filament_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.printer_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + metadata.bundle_type = BundleType::Local; + metadata.path = metadata_file.string(); + + bundles.WriteLock(); + bundles.m_bundles[metadata.id] = metadata; + bundles.WriteUnlock(); + } + } + + // Load bundle metadata from _subscribed directory + fs::path subscribed_dir(folder / PRESET_SUBSCRIBED_DIR); + if (fs::exists(subscribed_dir)) { + for (auto& entry : fs::directory_iterator(subscribed_dir)) { + if (!fs::is_directory(entry.path())) continue; + + std::string bundle_dir = entry.path().string(); + + fs::path metadata_file = entry.path() / PRESET_BUNDLE_METADATA; + if (!fs::exists(metadata_file)) continue; + + BundleMetadata metadata; + if (!metadata.load_from_json(metadata_file.string())) continue; + metadata.print_presets.clear(); + metadata.filament_presets.clear(); + metadata.printer_presets.clear(); + metadata.is_subscribed = true; + + // Load presets from bundle (same logic as __local__) + this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.print_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.filament_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.printer_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + + metadata.bundle_type = BundleType::Subscribed; + metadata.path = metadata_file.string(); + metadata.update_available = false; + + bundles.WriteLock(); + bundles.m_bundles[metadata.id] = metadata; + bundles.WriteUnlock(); + } + } + + + // BBS do not load sla_print + // BBS: change directoties by design + try { + std::string print_selected_preset_name = prints.get_selected_preset().name; + this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); + prints.select_preset_by_name(print_selected_preset_name, false); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + } + try { + std::string filament_selected_preset_name = filaments.get_selected_preset().name; + this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); + filaments.select_preset_by_name(filament_selected_preset_name, false); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + } + try { + std::string printer_selected_preset_name = printers.get_selected_preset().name; + this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); + printers.select_preset_by_name(printer_selected_preset_name, false); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + } + if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); + this->update_multi_material_filament_presets(); + this->update_compatible(PresetSelectCompatibleType::Never); + + set_calibrate_printer(""); + + return PresetsConfigSubstitutions(); +} + +PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig & config, + std::map> &my_presets, + ForwardCompatibilitySubstitutionRule substitution_rule) +{ + // First load the vendor specific system presets. + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + bool process_added = false, filament_added = false, machine_added = false; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preset toltal count %2%")%substitution_rule%my_presets.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" print's selected_idx %1%, selected_name %2%") %prints.get_selected_idx() %prints.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" filament's selected_idx %1%, selected_name %2%") %filaments.get_selected_idx() %filaments.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" printers's selected_idx %1%, selected_name %2%") %printers.get_selected_idx() %printers.get_selected_preset_name(); + + // Sync removing + remove_users_preset(config, &my_presets); + + std::map>::iterator it; + for (int pass = 0; pass < 2; ++pass) + for (it = my_presets.begin(); it != my_presets.end(); it++) { + std::string name = it->first; + std::map& value_map = it->second; + // Load user root presets at first pass + std::map::iterator inherits_iter = value_map.find(BBL_JSON_KEY_INHERITS); + if ((pass == 1) == (inherits_iter == value_map.end() || inherits_iter->second.empty())) + continue; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " start load from cloud: " << name; + //get the type first + std::map::iterator type_iter = value_map.find(BBL_JSON_KEY_TYPE); + if (type_iter == value_map.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%")%name; + continue; + } + try { + PresetCollection *preset_collection = nullptr; + if (type_iter->second == PRESET_IOT_PRINT_TYPE) { + preset_collection = &(this->prints); + process_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); + } + else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { + preset_collection = &(this->filaments); + filament_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); + } + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + preset_collection = &(this->printers); + machine_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid type %1% for setting %2%") %type_iter->second %name; + continue; + } + } + catch (const std::runtime_error& err) { + errors_cummulative += err.what(); + } + } + /*if (process_added) { + this->prints.update_after_user_presets_loaded(); + } + if (filament_added) { + this->filaments.update_after_user_presets_loaded(); + } + if (machine_added) { + this->printers.update_after_user_presets_loaded(); + }*/ + + this->update_multi_material_filament_presets(); + this->update_compatible(PresetSelectCompatibleType::Never); + //this->load_selections(config, PresetPreferences()); + + set_calibrate_printer(""); + + if (! errors_cummulative.empty()) + throw Slic3r::RuntimeError(errors_cummulative); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, process_added %1%, filament_added %2%, machine_added %3%")%process_added %filament_added %machine_added; + return substitutions; +} + +bool PresetBundle::apply_vendor_config( + const std::map>>& new_vendors, + const std::map& new_filaments, + AppConfig* app_config, + bool overwrite, + const std::string& preferred_printer_model, + const std::string& preferred_printer_variant, + const std::string& preferred_filament) +{ + namespace fs = boost::filesystem; + + // Get current configuration from AppConfig + const auto old_vendors = app_config->vendors(); + const auto old_filaments = app_config->has_section(AppConfig::SECTION_FILAMENTS) + ? app_config->get_section(AppConfig::SECTION_FILAMENTS) + : std::map(); + + // Find vendors that need installation + const auto vendor_dir = (fs::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); + + std::vector install_bundles; + for (const auto &it : new_vendors) { + if (it.second.size() > 0) { + auto vendor_file = vendor_dir / (it.first + ".json"); + if (!fs::exists(vendor_file)) { + install_bundles.emplace_back(it.first); + } + } + } + + // Install bundles from resources + if (!install_bundles.empty()) { + BOOST_LOG_TRIVIAL(info) << "Installing " << install_bundles.size() << " vendor bundles from resources"; + if (!Slic3r::install_vendor_bundles_from_resources(install_bundles)) { + BOOST_LOG_TRIVIAL(error) << "Failed to install vendor bundles"; + return false; + } + } else { + BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resource directory"; + } + + // For each @System filament, check if a vendor-specific override exists + // in the loaded profiles. If so, replace the @System variant with the + // override (e.g. replace "Generic ABS @System" with BBL "Generic ABS"). + // When printers from the default bundle are also selected, keep @System + // too since those printers need it. + static const std::string system_suffix = " @System"; + auto it_default = new_vendors.find(PresetBundle::ORCA_DEFAULT_BUNDLE); + bool has_default_bundle_printer = it_default != new_vendors.end() && !it_default->second.empty(); + + // Check if any non-default vendor has selected printers + bool has_vendor_printer = false; + for (const auto& [vendor, models] : new_vendors) { + if (vendor != PresetBundle::ORCA_DEFAULT_BUNDLE && !models.empty()) { + has_vendor_printer = true; + break; + } + } + + std::map supplemented_filaments; + for (const auto& [name, value] : new_filaments) { + if (name.size() > system_suffix.size() && + name.compare(name.size() - system_suffix.size(), system_suffix.size(), system_suffix) == 0) { + std::string short_name = name.substr(0, name.size() - system_suffix.size()); + + if (has_vendor_printer) { + // Check if this filament exists in the loaded vendor profiles + // For @System filaments, we check if the short_name exists as a vendor-specific filament + bool has_vendor_filament = false; + for (const auto& [vendor, models] : new_vendors) { + if (vendor != PresetBundle::ORCA_DEFAULT_BUNDLE) { + auto vendor_it = this->vendors.find(vendor); + // Check if this vendor is loaded in the preset bundle + if (vendor_it != this->vendors.end()) { + // Vendor is loaded, check if the filament exists + for (auto f : vendor_it->second.default_filaments) { + BOOST_LOG_TRIVIAL(info) << " checking if vendor filament " << f << " matches " << short_name << "(" << name << ")"; + if (f.find(short_name) != std::string::npos) { + BOOST_LOG_TRIVIAL(info) << name << " has filament from vendor: " << vendor; + has_vendor_filament = true; + break; + } + } + break; + } + } + } + + if (has_vendor_filament) { + supplemented_filaments[short_name] = value; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Replacing @System filament: '" << name << "' -> '" << short_name << "'"; + if (has_default_bundle_printer) { + supplemented_filaments[name] = value; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Also keeping '" << name << "' for default bundle printers"; + } + continue; + } + } + } + supplemented_filaments[name] = value; + } + + // Update AppConfig - merge with existing values instead of overwriting depending on bool + if (overwrite) { + app_config->set_section(AppConfig::SECTION_FILAMENTS, supplemented_filaments); + app_config->set_vendors(new_vendors); + } + else { + // Merge filaments + std::map merged_filaments = old_filaments; + for (const auto& [name, value] : supplemented_filaments) { + merged_filaments[name] = value; + } + app_config->set_section(AppConfig::SECTION_FILAMENTS, merged_filaments); + + // Merge vendors + std::map>> merged_vendors = old_vendors; + for (const auto& [vendor, models] : new_vendors) { + auto& vendor_entry = merged_vendors[vendor]; + for (const auto& [model, variants] : models) { + auto& model_entry = vendor_entry[model]; + model_entry.insert(variants.begin(), variants.end()); + } + } + app_config->set_vendors(merged_vendors); + } + + // Load presets with new configuration + this->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable, + {preferred_printer_model, preferred_printer_variant, preferred_filament, std::string()}); + + // Ensure active filament compatibility + // If the active filament is not in the wizard-selected filaments, switch to the first + // compatible wizard-selected filament. This handles the first-run case where load_presets + // falls back to "Generic PLA" even though the user selected a different filament. + if (!supplemented_filaments.empty()) { + bool active_filament_selected = supplemented_filaments.count(this->filament_presets.front()) > 0; + if (!active_filament_selected) { + for (const auto& [filament_name, _] : supplemented_filaments) { + const Preset* preset = this->filaments.find_preset(filament_name); + if (preset && preset->is_visible && preset->is_compatible) { + this->filaments.select_preset_by_name(filament_name, true); + this->filament_presets.front() = this->filaments.get_selected_preset_name(); + break; + } + } + } + } + + // Export selections + this->export_selections(*app_config); + return true; +} + +// Import presets from UI control +PresetsConfigSubstitutions PresetBundle::import_presets(std::vector & files, + std::function override_confirm, + ForwardCompatibilitySubstitutionRule rule, + AppConfig& config) +{ + bundles.PauseRead(); // Pause threads from reading + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " entry"; + PresetsConfigSubstitutions substitutions; + int overwrite = 0; + std::vector result; + std::string user_id = config.get("preset_folder"); + if (user_id.empty()) + user_id = DEFAULT_USER_FOLDER_NAME; + this->update_user_presets_directory(user_id); + for (auto &file : files) { + if (Slic3r::is_json_file(file)) { + import_json_presets(substitutions, file, override_confirm, rule, overwrite, result); + } + // Determine if it is a preset bundle + if (boost::iends_with(file, ".orca_printer") || boost::iends_with(file, ".orca_bundle") || boost::iends_with(file, ".orca_filament") || boost::iends_with(file, ".zip")) { + boost::system::error_code ec; + // create user folder + fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); + if (!fs::exists(user_folder)) fs::create_directory(user_folder, ec); + if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + // create default folder + fs::path configs_folder(user_folder / user_id); + if (!fs::exists(configs_folder)) fs::create_directory(configs_folder, ec); + if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + //create temp folder + //std::string user_default_temp_dir = data_dir() + "/" + PRESET_USER_DIR + "/" + DEFAULT_USER_FOLDER_NAME + "/" + "temp"; + fs::path temp_folder(configs_folder / "temp"); + std::string user_default_temp_dir = temp_folder.make_preferred().string(); + if (fs::exists(temp_folder)) fs::remove_all(temp_folder); + fs::create_directory(temp_folder, ec); + if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + + file = boost::filesystem::path(file).make_preferred().string(); + mz_zip_archive zip_archive; + mz_zip_zero_struct(&zip_archive); + mz_bool status; + + FILE *zipFile = boost::nowide::fopen(file.c_str(), "rb"); + status = mz_zip_reader_init_cfile(&zip_archive, zipFile, 0, MZ_ZIP_FLAG_CASE_SENSITIVE | MZ_ZIP_FLAG_IGNORE_PATH); + if (MZ_FALSE == status) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to initialize reader ZIP archive"; + return substitutions; + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Success to initialize reader ZIP archive"; + } + + // First, extract bundle_structure.json to get the bundle_id + // Track whether bundle_structure.json exists to determine routing + bool has_bundle_structure = false; + BundleMetadata metadata; + fs::path metadata_path = temp_folder / BUNDLE_STRUCTURE_JSON_NAME; + status = mz_zip_reader_extract_file_to_file(&zip_archive, BUNDLE_STRUCTURE_JSON_NAME, encode_path(metadata_path.string().c_str()).c_str(), MZ_ZIP_FLAG_CASE_SENSITIVE); + if (status) { + if (metadata.load_from_json(metadata_path.string())) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found bundle_id: " << metadata.id << " from " << BUNDLE_STRUCTURE_JSON_NAME; + has_bundle_structure = true; + } + } + + if (has_bundle_structure && metadata.id.empty()) { + boost::uuids::uuid uuid = boost::uuids::random_generator()(); + metadata.id = to_string(uuid); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " bundle_id was empty, so generating a UUID: " << metadata.id; + } + + // Build bundle directory path based on whether bundle_structure.json was present + fs::path bundle_base_dir; + if (has_bundle_structure) { + // Use the bundle ID from metadata when bundle_structure.json exists + bundle_base_dir = user_folder / user_id / PRESET_LOCAL_DIR / metadata.id; + if (!fs::exists(bundle_base_dir)) + fs::create_directories(bundle_base_dir, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to create bundle directory: " << bundle_base_dir.string() << " error: " << ec.message(); + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No bundle_structure.json found, importing presets into the user preset directory"; + } + + // Extract Files + int num_files = mz_zip_reader_get_num_files(&zip_archive); + for (int i = 0; i < num_files; i++) { + mz_zip_archive_file_stat file_stat; + status = mz_zip_reader_file_stat(&zip_archive, i, &file_stat); + if (status) { + std::string file_name = file_stat.m_filename; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " From zip file: " << file << ". Read file name: " << file_stat.m_filename; + size_t index = file_name.find_last_of('/'); + if (std::string::npos != index) { + file_name = file_name.substr(index + 1); + } + if (BUNDLE_STRUCTURE_JSON_NAME == file_name) continue; + // create target file path + std::string target_file_path = boost::filesystem::path(temp_folder / file_name).make_preferred().string(); + + status = mz_zip_reader_extract_to_file(&zip_archive, i, encode_path(target_file_path.c_str()).c_str(), MZ_ZIP_FLAG_CASE_SENSITIVE); + // target file is opened + if (MZ_FALSE == status) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to open target file: " << target_file_path; + } else { + bool is_success = import_json_presets(substitutions, target_file_path, override_confirm, rule, overwrite, result, + has_bundle_structure ? bundle_base_dir.string() : std::string()); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " import target file: " << target_file_path << " import result" << is_success; + } + } + } + + // Set imported_time to current time if not already set + if (metadata.imported_time == 0) { + metadata.imported_time = std::time(nullptr); + } + + // Only save bundle_metadata.json for bundles (when bundle_structure.json was present) + if (has_bundle_structure) { + // Save metadata to bundle_metadata.json + fs::path metadata_save_path = bundle_base_dir / PRESET_BUNDLE_METADATA; + if (metadata.save_to_json(metadata_save_path.string())) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Saved bundle metadata to: " << metadata_save_path.string(); + + metadata.bundle_type = BundleType::Local; + metadata.path = metadata_save_path.string(); + // Store the bundle metadata in m_bundles for tracking + + + bundles.WriteLock(); + bundles.m_bundles[metadata.id] = metadata; + bundles.WriteUnlock(); + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to save bundle metadata to: " << metadata_save_path.string(); + } + } + + fclose(zipFile); + if (fs::exists(temp_folder)) fs::remove_all(temp_folder, ec); + if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " remove directory failed: " << ec.message(); + } + } + bundles.UnpauseRead(); + files = result; + return substitutions; +} + +bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & substitutions, + std::string & file, + std::function override_confirm, + ForwardCompatibilitySubstitutionRule rule, + int & overwrite, + std::vector & result, + const std::string & bundle_dir) +{ + try { + DynamicPrintConfig config; + // BBS: change to json format + // ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); + std::map key_values; + std::string reason; + ConfigSubstitutions config_substitutions = config.load_from_json(file, rule, key_values, reason); + std::string name = key_values[BBL_JSON_KEY_NAME]; + std::string version_str = key_values[BBL_JSON_KEY_VERSION]; + boost::optional version = Semver::parse(version_str); + if (!version) return false; + + std::string type_subdir; // also note the type subdir for bundles + PresetCollection *collection = nullptr; + if (config.has("printer_settings_id")) { + collection = &printers; + type_subdir = PRESET_PRINTER_NAME; + } + else if (config.has("print_settings_id")) { + collection = &prints; + type_subdir = PRESET_PRINT_NAME; + } + else if (config.has("filament_settings_id")) { + collection = &filaments; + type_subdir = PRESET_FILAMENT_NAME; + } + if (collection == nullptr) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset type is unknown, not loading: " << name; + return false; + } + const PresetOrigin load_origin = detect_origin_from_path(boost::filesystem::path(bundle_dir)); + const std::string preset_name = get_preset_canonical_name(name, load_origin); + + if (overwrite == 0) overwrite = 1; + if (auto p = collection->find_preset(preset_name, false)) { + if (p->is_default || p->is_system) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset already present and is system preset, not loading: " << preset_name; + return false; + } + if (overwrite != 2 && overwrite != 3) overwrite = override_confirm(preset_name); //3: yes to all 2: no to all + } + if (overwrite == 0 || overwrite == 2) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset already present, not loading: " << preset_name; + return false; + } + + DynamicPrintConfig new_config; + Preset * inherit_preset = nullptr; + ConfigOption * inherits_config = config.option(BBL_JSON_KEY_INHERITS); + std::string inherits_value; + if (inherits_config) { + ConfigOptionString *option_str = dynamic_cast(inherits_config); + inherits_value = option_str->value; + inherit_preset = collection->find_preset2(inherits_value, true); + } + if (inherit_preset) { + new_config = inherit_preset->config; + new_config.apply(std::move(config)); + } else { + // We support custom root preset now + auto inherits_config2 = dynamic_cast(inherits_config); + if (inherits_config2 && !inherits_config2->value.empty()) { + // we should skip this preset here + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", can not find inherit preset for user preset %1%, just skip") % name; + return false; + } + // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. + const Preset &default_preset = collection->default_preset_for(config); + new_config = default_preset.config; + new_config.apply(std::move(config)); + extend_default_config_length(new_config, true, default_preset.config); + } + + Preset &preset = collection->load_preset(collection->path_from_name(name, inherit_preset == nullptr), preset_name, std::move(new_config), false); + preset.bundle_id = load_origin.bundle_id; + if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end()) + preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID]; + preset.is_external = true; + preset.version = *version; + inherit_preset = collection->find_preset(inherits_value, false, true); // pointer maybe wrong after insert, redo find + if (inherit_preset) preset.base_id = inherit_preset->setting_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << preset.name << " have filament_id: " << preset.filament_id << " and base_id: " << preset.base_id; + Preset::normalize(preset.config); + // Report configuration fields, which are misplaced into a wrong group. + const Preset &default_preset = collection->default_preset_for(new_config); + std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a preset file: The preset \"" << preset.file + << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; + } + if (!config_substitutions.empty()) + substitutions.push_back({name, collection->type(), PresetConfigSubstitutions::Source::UserFile, file, std::move(config_substitutions)}); + collection->set_custom_preset_alias(preset); + + // If bundle_dir is provided, use it for the save operation + if (!bundle_dir.empty()) { + if (!save_preset_to_bundle_dir(preset, collection, load_origin.bundle_id, type_subdir, bundle_dir)) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to save preset " << preset_name << " to bundle directory"; + return false; + } + } else { + preset.save(inherit_preset ? &inherit_preset->config : nullptr); + } + + result.push_back(file); + } catch (const std::ifstream::failure &err) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << boost::format("The config cannot be loaded: %1%. Reason: %2%") % file % err.what(); + } catch (const std::runtime_error &err) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << boost::format("Failed importing config file: %1%. Reason: %2%") % file % err.what(); + } + return true; +} + +//BBS save user preset to user_id preset folder +void PresetBundle::save_user_presets(AppConfig& config, std::map& need_to_delete_list) +{ + std::string user_sub_folder = DEFAULT_USER_FOLDER_NAME; + if (!config.get("preset_folder").empty()) + user_sub_folder = config.get("preset_folder"); + //BBS: change directory by design + const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/"+ user_sub_folder; + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, save to %1%")%dir_user_presets; + + fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); + if (!fs::exists(user_folder)) + fs::create_directory(user_folder); + + fs::path folder(dir_user_presets); + if (!fs::exists(folder)) + fs::create_directory(folder); + + this->prints.save_user_presets(dir_user_presets, PRESET_PRINT_NAME, need_to_delete_list); + this->filaments.save_user_presets(dir_user_presets, PRESET_FILAMENT_NAME, need_to_delete_list); + this->printers.save_user_presets(dir_user_presets, PRESET_PRINTER_NAME, need_to_delete_list); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished"); +} + +void PresetBundle::check_and_fix_user_presets_syncinfo(const std::string& user_id) +{ + auto process_collection = [&user_id](PresetCollection& collection) { + collection.lock(); + for (auto& preset : collection) { + if (preset.is_user()) { + collection.check_and_fix_syncinfo(preset, user_id); + } + } + collection.unlock(); + }; + process_collection(this->prints); + process_collection(this->filaments); + process_collection(this->printers); +} + +//Orca: Import subscribed bundle presets (load and save to disk in one operation) +PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( + AppConfig& config, + const std::map>& bundle_presets, + const BundleMetadata& remote_metadata, + ForwardCompatibilitySubstitutionRule substitution_rule) +{ + PresetsConfigSubstitutions substitutions; + std::string errors_cumulative; + bool process_added = false, filament_added = false, machine_added = false; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " enter, substitution_rule " << substitution_rule << ", bundle_id: " << remote_metadata.id << ", preset count: " << bundle_presets.size(); + + BundleMetadata merged_metadata; + auto existing_it = bundles.m_bundles.find(remote_metadata.id); + if (existing_it != bundles.m_bundles.end()) { + merged_metadata = existing_it->second; + } else { + merged_metadata.imported_time = std::time(nullptr); + } + + merged_metadata.id = remote_metadata.id; + merged_metadata.name = remote_metadata.name; + merged_metadata.version = remote_metadata.version; + merged_metadata.description = remote_metadata.description; + merged_metadata.author = remote_metadata.author; + merged_metadata.updated_time = remote_metadata.updated_time; + merged_metadata.bundle_type = BundleType::Subscribed; + merged_metadata.is_subscribed = true; + merged_metadata.update_available = false; + merged_metadata.unauthorized = false; + + const PresetOrigin subscribed_origin(PresetOrigin::Kind::SubscribedBundle, remote_metadata.id); + + std::unordered_set remote_prints; + std::unordered_set remote_filaments; + std::unordered_set remote_printers; + + for (const auto& [preset_name, value_map] : bundle_presets) { + auto type_iter = value_map.find(BBL_JSON_KEY_TYPE); + if (type_iter == value_map.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " cannot find type for preset " << preset_name; + continue; + } + + const std::string subscribed_name = get_preset_canonical_name(preset_name, subscribed_origin); + if (type_iter->second == PRESET_IOT_PRINT_TYPE) + remote_prints.insert(subscribed_name); + else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) + remote_filaments.insert(subscribed_name); + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) + remote_printers.insert(subscribed_name); + } + + auto remove_obsolete_bundle_presets = + [&](PresetCollection& collection, const std::unordered_set& remote_names, const char* type_name) -> int { + int removed_count = 0; + std::vector to_delete; + + for (const Preset& preset : collection.get_presets()) { + if (!preset.is_from_bundle() || preset.bundle_id != remote_metadata.id) + continue; + + if (remote_names.find(preset.name) == remote_names.end()) + to_delete.push_back(preset.name); + } + + for (const std::string& preset_name : to_delete) { + if (collection.delete_preset(preset_name, true)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << type_name << " preset '" << preset_name << "' no longer in remote bundle, deleted"; + ++removed_count; + } else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to delete obsolete " << type_name << " preset '" << preset_name << "'"; + } + } + + return removed_count; + }; + + int total_removed = 0; + total_removed += remove_obsolete_bundle_presets(this->prints, remote_prints, "print"); + total_removed += remove_obsolete_bundle_presets(this->filaments, remote_filaments, "filament"); + total_removed += remove_obsolete_bundle_presets(this->printers, remote_printers, "printer"); + + if (total_removed > 0) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": deleted " << total_removed << " obsolete presets from bundle " << remote_metadata.id; + } + + // Get current user ID for path construction + std::string user_id = config.get("preset_folder"); + if (user_id.empty()) user_id = DEFAULT_USER_FOLDER_NAME; + + // Create the subscribed directory base path + boost::filesystem::path user_folder(Slic3r::data_dir() + "/" + PRESET_USER_DIR); + boost::filesystem::path subscribed_base(user_folder / user_id / PRESET_SUBSCRIBED_DIR); + + // Ensure subscribed directory exists + boost::system::error_code ec; + if (!boost::filesystem::exists(subscribed_base)) + boost::filesystem::create_directories(subscribed_base, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create subscribed directory: " << subscribed_base.string() << " error: " << ec.message(); + return substitutions; + } + + dir_user_presets_subscribed = subscribed_base; + + // Create bundle directory + boost::filesystem::path bundle_dir(subscribed_base / remote_metadata.id); + if (!boost::filesystem::exists(bundle_dir)) + boost::filesystem::create_directories(bundle_dir, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() << " error: " << ec.message(); + return substitutions; + } + + merged_metadata.print_presets.clear(); + merged_metadata.filament_presets.clear(); + merged_metadata.printer_presets.clear(); + + // Load each preset from the bundle and save to disk + for (const auto& preset_entry : bundle_presets) { + const std::string& preset_name = preset_entry.first; + const std::string subscribed_name = get_preset_canonical_name(preset_name, subscribed_origin); + std::map value_map = preset_entry.second; // Make a copy since we might modify it + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " importing preset: " << preset_name << " from bundle: " << remote_metadata.id; + + // Get the type first + auto type_iter = value_map.find(BBL_JSON_KEY_TYPE); + if (type_iter == value_map.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " cannot find type for preset " << preset_name; + continue; + } + + // If this preset inherits from another preset inside the same bundle, rewrite the + // reference to the canonical (bundle-prefixed) name so the lookup matches the stored identity. + auto inherits_iter = value_map.find(BBL_JSON_KEY_INHERITS); + if (inherits_iter != value_map.end() && !inherits_iter->second.empty() && bundle_presets.find(inherits_iter->second) != bundle_presets.end()) + inherits_iter->second = get_preset_canonical_name(inherits_iter->second, subscribed_origin); + + try { + PresetCollection* preset_collection = nullptr; + std::string type_subdir; + bool preset_added = false; + + if (type_iter->second == PRESET_IOT_PRINT_TYPE) { + preset_collection = &(this->prints); + type_subdir = PRESET_PRINT_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); + process_added |= preset_added; + } + else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { + preset_collection = &(this->filaments); + type_subdir = PRESET_FILAMENT_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); + filament_added |= preset_added; + } + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + preset_collection = &(this->printers); + type_subdir = PRESET_PRINTER_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); + machine_added |= preset_added; + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid type " << type_iter->second << " for preset " << preset_name; + continue; + } + + // If preset was loaded/added, save it to the bundle directory. + // Find the preset that was just loaded using its canonical (bundle-prefixed) name. + Preset* preset = preset_collection->find_preset(subscribed_name, false, true); + if (preset) { + // Use helper function to save preset to bundle directory + if (!save_preset_to_bundle_dir(*preset, preset_collection, remote_metadata.id, type_subdir, bundle_dir.string())) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to save preset " << preset_name << " to bundle directory"; + continue; + } + + if (type_iter->second == PRESET_IOT_PRINT_TYPE) + merged_metadata.print_presets.push_back(preset->name); + else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) + merged_metadata.filament_presets.push_back(preset->name); + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) + merged_metadata.printer_presets.push_back(preset->name); + } + } + catch (const std::runtime_error& err) { + errors_cumulative += err.what(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " error importing preset " << preset_name << ": " << err.what(); + } + } + + boost::filesystem::path metadata_save_path = bundle_dir / PRESET_BUNDLE_METADATA; + merged_metadata.path = metadata_save_path.string(); + bundles.m_bundles[remote_metadata.id] = merged_metadata; + + if (bundles.m_bundles[remote_metadata.id].save_to_json(metadata_save_path.string())) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " saved bundle metadata to: " << metadata_save_path.string(); + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to save bundle metadata to: " << metadata_save_path.string(); + } + + this->update_multi_material_filament_presets(); + this->update_compatible(PresetSelectCompatibleType::Never); + + set_calibrate_printer(""); + + if (!errors_cumulative.empty()) + throw Slic3r::RuntimeError(errors_cumulative); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " finished, process_added " << process_added << ", filament_added " << filament_added << ", machine_added " << machine_added; + return substitutions; +} + +// Helper function: save preset to bundle directory with common logic +// This function extracts the common code used by both import_json_presets and import_subscribed_presets +bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection, + const std::string& bundle_id, const std::string& type_subdir, + const std::string& bundle_base_dir) +{ + if (bundle_base_dir.empty()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " bundle_base_dir is empty, cannot save preset " << preset.name; + return false; + } + + // Store original directory path + std::string original_dir_path = collection->m_dir_path; + + try { + // Create bundle directory if it doesn't exist + boost::filesystem::path bundle_dir(bundle_base_dir); + boost::system::error_code ec; + if (!boost::filesystem::exists(bundle_dir)) + boost::filesystem::create_directories(bundle_dir, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() << " error: " << ec.message(); + return false; + } + + // Create bundle type directory + boost::filesystem::path type_dir = bundle_dir / type_subdir; + if (!boost::filesystem::exists(type_dir)) { + boost::filesystem::create_directories(type_dir, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create type directory: " << type_dir.string() << " error: " << ec.message(); + return false; + } + } + + preset.bundle_id = bundle_id; + + // Bundle preset names may include the subscribed/local prefix path. + // Persist the file under the type directory using only the base preset name. + const std::string preset_filename = boost::filesystem::path(preset.name).filename().string(); + const std::string file_name = boost::iends_with(preset_filename, ".json") ? preset_filename : (preset_filename + ".json"); + preset.file = (type_dir / file_name).make_preferred().string(); + + // Save with parent config if inherits from another preset + std::string inherits = Preset::inherits(preset.config); + if (inherits.empty()) { + // Root preset - save full config + preset.save(nullptr); + } else { + Preset* parent_preset = collection->find_preset2(inherits, true); + if (!parent_preset) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " cannot find parent preset for " << preset.name << ", inherits " << inherits; + } else { + if (preset.base_id.empty()) + preset.base_id = parent_preset->setting_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " saved preset " << preset.name + << " filament_id: " << preset.filament_id + << " base_id: " << preset.base_id + << " bundle: " << bundle_id; + preset.save(&(parent_preset->config)); + } + } + + // Restore original directory path + collection->m_dir_path = original_dir_path; + return true; + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " exception saving preset " << preset.name << ": " << e.what(); + collection->m_dir_path = original_dir_path; + return false; + } +} + +//BBS: save user preset to user_id preset folder +void PresetBundle::update_user_presets_directory(const std::string preset_folder) +{ + //BBS: change directory by design + const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/"+ preset_folder; + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, update directory to %1%")%dir_user_presets; + + fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); + if (!fs::exists(user_folder)) + fs::create_directory(user_folder); + + fs::path folder(dir_user_presets); + if (!fs::exists(folder)) + fs::create_directory(folder); + + this->prints.update_user_presets_directory(dir_user_presets, PRESET_PRINT_NAME); + this->filaments.update_user_presets_directory(dir_user_presets, PRESET_FILAMENT_NAME); + this->printers.update_user_presets_directory(dir_user_presets, PRESET_PRINTER_NAME); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished"); +} + +void PresetBundle::remove_user_presets_directory(const std::string preset_folder) +{ + const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + preset_folder; + + if (preset_folder.empty()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": preset_folder is empty, no need to remove directory : %1%") % dir_user_presets; + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, delete directory : %1%") % dir_user_presets; + fs::path folder(dir_user_presets); + if (fs::exists(folder)) { + fs::remove_all(folder); + } +} + +void PresetBundle::update_system_preset_setting_ids(std::map>& system_presets) +{ + for (auto iterator: system_presets) + { + std::string name = iterator.first; + std::map& value_map = iterator.second; + //get the type first + std::map::iterator type_iter = value_map.find(BBL_JSON_KEY_TYPE); + if (type_iter == value_map.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%")%name; + continue; + } + PresetCollection *preset_collection = nullptr; + if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + preset_collection = &(this->printers); + } + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + preset_collection = &(this->printers); + } + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + preset_collection = &(this->printers); + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid type %1% for setting %2%") %type_iter->second %name; + continue; + } + std::string setting_id; + if (value_map.count(BBL_JSON_KEY_SETTING_ID) > 0) + setting_id = value_map[BBL_JSON_KEY_SETTING_ID]; + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find setting_id for setting %1%")%name; + continue; + } + Preset* preset = preset_collection->find_preset(name, false, true); + if (preset) { + if (!preset->setting_id.empty() && (preset->setting_id.compare(setting_id) != 0)) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << boost::format("name %1%, local setting_id %2% is different with remote id %3%") + %preset->name %preset->setting_id %setting_id; + } + else if (preset->setting_id.empty()) + preset->setting_id = setting_id; + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("can not find setting %1% in system presets, type %2%") %name %type_iter->second; + continue; + } + } + return; +} + +//BBS: validate printers from previous project +static std::set gcodes_key_set = {"filament_end_gcode", "filament_start_gcode", "change_filament_gcode", "layer_change_gcode", "machine_end_gcode", "machine_pause_gcode", "machine_start_gcode", + "template_custom_gcode", "printing_by_object_gcode", "before_layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode"}; +int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintConfig& config, std::set& different_gcodes) +{ + bool validated = false; + std::vector inherits_values = config.option("inherits_group", true)->values; + std::vector filament_preset_name = config.option("filament_settings_id", true)->values; + std::string printer_preset = config.option("printer_settings_id", true)->value; + bool has_different_settings_to_system = config.option("different_settings_to_system")?true:false; + std::vector different_values; + int ret = VALIDATE_PRESETS_SUCCESS; + + if (has_different_settings_to_system) + different_values = config.option("different_settings_to_system", true)->values; + + //PrinterTechnology printer_technology = Preset::printer_technology(config); + size_t filament_count = config.option("filament_diameter")->values.size(); + inherits_values.resize(filament_count + 2, std::string()); + different_values.resize(filament_count + 2, std::string()); + filament_preset_name.resize(filament_count, std::string()); + + std::string printer_inherits = inherits_values[filament_count + 1]; + + validated = this->printers.validate_preset(printer_preset, printer_inherits); + if (!validated) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found the printer preset not inherit from system") % file_name; + different_gcodes.emplace(printer_preset); + ret = VALIDATE_PRESETS_PRINTER_NOT_FOUND; + } + for(unsigned int index = 0; index < filament_count; index ++) + { + std::string filament_preset = filament_preset_name[index]; + std::string filament_inherits = inherits_values[index+1]; + + // filament_preset_name is padded up to filament_count from filament_diameter. Unfilled + // slots have no assigned preset, so there's nothing to validate or warn about. + if (filament_preset.empty()) + continue; + + validated = this->filaments.validate_preset(filament_preset, filament_inherits); + if (!validated) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found the filament %2% preset not inherit from system") % file_name %(index+1); + different_gcodes.emplace(filament_preset); + ret = VALIDATE_PRESETS_FILAMENTS_NOT_FOUND; + } + } + + //self defined presets, return directly + if (ret) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found self defined presets, count %2%") %file_name %different_gcodes.size(); + return ret; + } + + for(unsigned int index = 1; index < filament_count; index ++) + { + std::string different_settingss = different_values[index]; + + std::vector different_keys; + + Slic3r::unescape_strings_cstyle(different_settingss, different_keys); + + for (unsigned int j = 0; j < different_keys.size(); j++) { + if (gcodes_key_set.find(different_keys[j]) != gcodes_key_set.end()) { + different_gcodes.emplace(different_keys[j]); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":preset index %1%, different key %2%") %index %different_keys[j]; + } + } + } + + if (!different_gcodes.empty()) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found different gcodes count %2%") %file_name %different_gcodes.size(); + return VALIDATE_PRESETS_MODIFIED_GCODES; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":file_name %1%, validate presets success!") % file_name; + + return VALIDATE_PRESETS_SUCCESS; +} + +void PresetBundle::remove_users_preset(AppConfig &config, std::map> *my_presets) +{ + auto check_removed = [my_presets, this](Preset &preset) -> bool { + if (my_presets == nullptr) return true; + if (my_presets->find(preset.name) != my_presets->end()) return false; + if (!preset.sync_info.empty()) return false; // syncing, not remove + if (preset.setting_id.empty()) return false; // no id, not remove + // Saved preset is removed by another session + if (preset.is_dirty) { + preset.setting_id.clear(); + return false; + } + preset.remove_files(true /* cloud_already_deleted */); + return true; + }; + std::string preset_folder_user_id = config.get("preset_folder"); + std::string printer_selected_preset_name = printers.get_selected_preset().name; + bool need_reset_printer_preset = false; + for (auto it = printers.begin(); it != printers.end();) { + if (it->is_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":printers erase %1%, type %2%, user_id %3%") % it->name % Preset::get_type_string(it->type) % it->user_id; + if (it->name == printer_selected_preset_name) + need_reset_printer_preset = true; + it = printers.erase(it); + } + else { + it++; + } + } + + if (need_reset_printer_preset) { + std::string default_printer_model = ORCA_DEFAULT_PRINTER_MODEL; + std::string default_printer_name; + for (auto it = printers.begin(); it != printers.end(); it++) { + if (it->config.has("printer_model")) { + if (it->config.opt_string("printer_model") == default_printer_model) { + default_printer_name = it->name; + break; + } + } + } + printers.select_preset_by_name(default_printer_name, true); + } else { + printers.select_preset_by_name(printer_selected_preset_name, false); + } + + std::string selected_print_name = prints.get_selected_preset().name; + bool need_reset_print_preset = false; + // remove preset if user_id is not current user + for (auto it = prints.begin(); it != prints.end();) { + if (it->is_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":prints erase %1%, type %2%, user_id %3%")%it->name %Preset::get_type_string(it->type) %it->user_id; + if (it->name == selected_print_name) + need_reset_print_preset = true; + it = prints.erase(it); + } + else { + it++; + } + } + if (need_reset_print_preset && printers.get_selected_preset().config.has("default_print_profile")) { + std::string default_print_profile_name = printers.get_selected_preset().config.opt_string("default_print_profile"); + prints.select_preset_by_name(default_print_profile_name, true); + } else { + prints.select_preset_by_name(selected_print_name, false); + } + + std::string selected_filament_name = filaments.get_selected_preset().name; + bool need_reset_filament_preset = false; + for (auto it = filaments.begin(); it != filaments.end();) { + if (it->is_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":filaments erase %1%, type %2%, user_id %3%")%it->name %Preset::get_type_string(it->type) %it->user_id; + if (it->name == selected_filament_name) + need_reset_filament_preset = true; + it = filaments.erase(it); + } + else { + it++; + } + } + if (need_reset_filament_preset && printers.get_selected_preset().config.has("default_filament_profile")) { + const std::vector& prefered_filament_profiles = printers.get_selected_preset().config.option("default_filament_profile")->values; + if (prefered_filament_profiles.size() > 0) + filaments.select_preset_by_name(prefered_filament_profiles[0], true); + } else { + filaments.select_preset_by_name(selected_filament_name, false); + } + + update_compatible(PresetSelectCompatibleType::Always); + + /* set selected preset */ + for (size_t i = 0; i < filament_presets.size(); ++i) + { + auto preset = this->filaments.find_preset(filament_presets[i]); + if (preset == nullptr) + filament_presets[i] = filaments.get_selected_preset_name(); + } +} + + +//BBS: add json related logic, load system presets from json +std::pair PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) +{ + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule; + if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) + // Loading system presets, don't log substitutions. + compatibility_rule = ForwardCompatibilitySubstitutionRule::EnableSilent; + else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) + // Loading system presets, throw on unknown option value. + compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; + + // Here the vendor specific read only Config Bundles are stored. + //BBS: change directory by design + boost::filesystem::path dir = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); + if (validation_mode) + dir = (boost::filesystem::path(data_dir())).make_preferred(); + + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + bool first = true; + std::vector vendor_names; + // store all vendor names in vendor_names + for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { + std::string vendor_file = dir_entry.path().string(); + if (!Slic3r::is_json_file(vendor_file)) + continue; + + std::string vendor_name = dir_entry.path().filename().string(); + + // Remove the .json suffix. + vendor_name.erase(vendor_name.size() - 5); + vendor_names.push_back(vendor_name); + } + // Separate ORCA_FILAMENT_LIBRARY from other vendors. It must be loaded + // first because other vendors' filaments may inherit from it via the + // `base_bundle` lookup in parse_subfile. The remaining vendors are + // independent (no cross-vendor inheritance) and can be loaded in parallel. + std::string orca_lib_vendor; + std::vector other_vendors; + other_vendors.reserve(vendor_names.size()); + for (auto& vn : vendor_names) { + if (vn == ORCA_FILAMENT_LIBRARY) + orca_lib_vendor = vn; + else if (!(validation_mode && !vendor_to_validate.empty() && vn != vendor_to_validate)) + other_vendors.push_back(vn); + } + + // Step 1: Load ORCA_FILAMENT_LIBRARY into `this` synchronously. + if (!orca_lib_vendor.empty()) { + try { + append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); + first = false; + } catch (const std::runtime_error &err) { + if (validation_mode) + throw err; + errors_cummulative += err.what(); + errors_cummulative += "\n"; + } + } + + // Step 2: Load remaining vendors in parallel. Each gets its own + // PresetBundle and uses `this` (which contains ORCA_FILAMENT_LIBRARY) + // as the base_bundle for cross-bundle inheritance lookups. + std::vector> parallel_bundles(other_vendors.size()); + std::vector parallel_substitutions(other_vendors.size()); + std::vector parallel_errors(other_vendors.size()); + + tbb::parallel_for(tbb::blocked_range(0, other_vendors.size()), + [&](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + auto bundle = std::make_unique(); + try { + auto result = bundle->load_vendor_configs_from_json( + dir.string(), other_vendors[i], PresetBundle::LoadSystem, + compatibility_rule, this); + parallel_substitutions[i] = std::move(result.first); + parallel_bundles[i] = std::move(bundle); + } catch (const std::runtime_error &err) { + parallel_errors[i] = err.what(); + } + } + }); + + // Step 3: Sequentially merge the parallel-loaded bundles into `this`. + // The merge order is the original vendor order so any duplicate-warning + // output stays stable across runs. + for (size_t i = 0; i < other_vendors.size(); ++i) { + if (!parallel_errors[i].empty()) { + if (validation_mode) + throw std::runtime_error(parallel_errors[i]); + errors_cummulative += parallel_errors[i]; + errors_cummulative += "\n"; + continue; + } + if (!parallel_bundles[i]) + continue; + + const std::string& vendor_name = other_vendors[i]; + append(substitutions, std::move(parallel_substitutions[i])); + std::vector duplicates = this->merge_presets(std::move(*parallel_bundles[i])); + first = false; + if (!duplicates.empty()) { + errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; + for (size_t j = 0; j < duplicates.size(); ++j) { + if (j > 0) + errors_cummulative += ", "; + errors_cummulative += duplicates[j]; + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Found duplicated preset: " + duplicates[j] + " in vendor: " + vendor_name + ": "; + } + } + } + + if (first) { + // No config bundle loaded, reset. + this->reset(false); + } + + this->update_system_maps(); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative; + return std::make_pair(std::move(substitutions), errors_cummulative); +} + +std::pair PresetBundle::load_system_models_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%") % compatibility_rule; + if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) + // Loading system presets, don't log substitutions. + compatibility_rule = ForwardCompatibilitySubstitutionRule::EnableSilent; + else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) + // Loading system presets, throw on unknown option value. + compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; + + // Here the vendor specific read only Config Bundles are stored. + boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { + std::string vendor_file = dir_entry.path().string(); + if (Slic3r::is_json_file(vendor_file)) { + std::string vendor_name = dir_entry.path().filename().string(); + // Remove the .json suffix. + vendor_name.erase(vendor_name.size() - 5); + try { + // Load the config bundle, flatten it. + append(substitutions, load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + } + } + } + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%") % errors_cummulative; + return std::make_pair(std::move(substitutions), errors_cummulative); +} + +std::pair PresetBundle::load_system_filaments_json(ForwardCompatibilitySubstitutionRule compatibility_rule) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%") % compatibility_rule; + if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) + // Loading system presets, don't log substitutions. + compatibility_rule = ForwardCompatibilitySubstitutionRule::EnableSilent; + else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) + // Loading system presets, throw on unknown option value. + compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; + + // Here the vendor specific read only Config Bundles are stored. + boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + bool first = true; + for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { + std::string vendor_file = dir_entry.path().string(); + if (Slic3r::is_json_file(vendor_file)) { + std::string vendor_name = dir_entry.path().filename().string(); + // Remove the .json suffix. + vendor_name.erase(vendor_name.size() - 5); + try { + if (first) { + // Reset this PresetBundle and load the first vendor config. + append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); + first = false; + } else { + // Load the other vendor configs, merge them with this PresetBundle. + // Report duplicate profiles. + PresetBundle other; + append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); + std::vector duplicates = this->merge_presets(std::move(other)); + if (!duplicates.empty()) { + errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; + for (size_t i = 0; i < duplicates.size(); ++i) { + if (i > 0) errors_cummulative += ", "; + errors_cummulative += duplicates[i]; + } + } + } + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + } + } + } + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%") % errors_cummulative; + return std::make_pair(std::move(substitutions), errors_cummulative); +} + +VendorProfile PresetBundle::get_custom_vendor_models() const +{ + VendorProfile vendor; + vendor.name = PRESET_CUSTOM_VENDOR; + vendor.id = PRESET_CUSTOM_VENDOR; + for (auto &preset : printers.get_presets()) { + if (preset.is_system) continue; + if (printers.get_preset_base(preset) != &preset) continue; + if (preset.is_default) continue; + auto model = preset.config.opt_string("printer_model"); + auto variant = preset.config.opt_string("printer_variant"); + auto iter_model = std::find_if(vendor.models.begin(), vendor.models.end(), [model](VendorProfile::PrinterModel &m) { + return m.name == model; + }); + if (iter_model == vendor.models.end()) { + iter_model = vendor.models.emplace(vendor.models.end(), VendorProfile::PrinterModel{}); + iter_model->id = model; + iter_model->name = model; + iter_model->variants = {VendorProfile::PrinterVariant(variant)}; + } else { + iter_model->variants.push_back(VendorProfile::PrinterVariant(variant)); + } + } + return vendor; +} + +// Merge one vendor's presets with the other vendor's presets, report duplicates. +std::vector PresetBundle::merge_presets(PresetBundle &&other) +{ + this->vendors.insert(other.vendors.begin(), other.vendors.end()); + std::vector duplicate_prints = this->prints .merge_presets(std::move(other.prints), this->vendors); + std::vector duplicate_sla_prints = this->sla_prints .merge_presets(std::move(other.sla_prints), this->vendors); + std::vector duplicate_filaments = this->filaments .merge_presets(std::move(other.filaments), this->vendors); + std::vector duplicate_sla_materials = this->sla_materials.merge_presets(std::move(other.sla_materials), this->vendors); + std::vector duplicate_printers = this->printers .merge_presets(std::move(other.printers), this->vendors); + append(this->obsolete_presets.prints, std::move(other.obsolete_presets.prints)); + append(this->obsolete_presets.sla_prints, std::move(other.obsolete_presets.sla_prints)); + append(this->obsolete_presets.filaments, std::move(other.obsolete_presets.filaments)); + append(this->obsolete_presets.sla_materials, std::move(other.obsolete_presets.sla_materials)); + append(this->obsolete_presets.printers, std::move(other.obsolete_presets.printers)); + append(duplicate_prints, std::move(duplicate_sla_prints)); + append(duplicate_prints, std::move(duplicate_filaments)); + append(duplicate_prints, std::move(duplicate_sla_materials)); + append(duplicate_prints, std::move(duplicate_printers)); + m_errors += other.m_errors; + return duplicate_prints; +} + +void PresetBundle::update_system_maps() +{ + this->prints .update_map_system_profile_renamed(); + this->sla_prints .update_map_system_profile_renamed(); + this->filaments .update_map_system_profile_renamed(); + this->sla_materials.update_map_system_profile_renamed(); + this->printers .update_map_system_profile_renamed(); + + this->prints .update_map_alias_to_profile_name(); + this->sla_prints .update_map_alias_to_profile_name(); + this->filaments .update_map_alias_to_profile_name(); + this->sla_materials.update_map_alias_to_profile_name(); + this->printers .update_map_alias_to_profile_name(); + + this->filaments.update_library_profile_excluded_from(); +} + +static inline std::string remove_ini_suffix(const std::string &name) +{ + std::string out = name; + if (boost::iends_with(out, ".ini")) + out.erase(out.end() - 4, out.end()); + return out; +} + +// Set the "enabled" flag for printer vendors, printer models and printer variants +// based on the user configuration. +// If the "vendor" section is missing, enable all models and variants of the particular vendor. +void PresetBundle::load_installed_printers(const AppConfig &config) +{ + this->update_system_maps(); + for (auto &preset : printers) + preset.set_visible_from_appconfig(config); +} + +const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& preset_type, const std::string& alias) const +{ + if (preset_type == Preset::TYPE_INVALID) + return alias; + + const PresetCollection& presets = preset_type == Preset::TYPE_PRINT ? prints : + preset_type == Preset::TYPE_SLA_PRINT ? sla_prints : + preset_type == Preset::TYPE_FILAMENT ? filaments : + preset_type == Preset::TYPE_PRINTER ? printers : + sla_materials; + + return presets.get_preset_name_by_alias(alias); +} + +//BBS: get filament required hrc by filament type +const int PresetBundle::get_required_hrc_by_filament_type(const std::string& filament_type) const +{ + static std::unordered_mapfilament_type_to_hrc; + if (filament_type_to_hrc.empty()) { + for (auto iter = filaments.m_presets.begin(); iter != filaments.m_presets.end(); iter++) { + if (iter->vendor && iter->vendor->id == "BBL") { + if (iter->config.has("filament_type") && iter->config.has("required_nozzle_HRC")) { + auto type = iter->config.opt_string("filament_type", 0); + auto hrc = iter->config.opt_int("required_nozzle_HRC", 0); + filament_type_to_hrc[type] = hrc; + } + } + } + } + auto iter = filament_type_to_hrc.find(filament_type); + if (iter != filament_type_to_hrc.end()) + return iter->second; + else + return 0; +} + +//BBS: add project embedded preset logic +void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset::Type type, + const std::vector& unselected_options, bool save_to_project) +{ + PresetCollection& presets = type == Preset::TYPE_PRINT ? prints : + type == Preset::TYPE_SLA_PRINT ? sla_prints : + type == Preset::TYPE_FILAMENT ? filaments : + type == Preset::TYPE_SLA_MATERIAL ? sla_materials : printers; + + // if we want to save just some from selected options + if (!unselected_options.empty()) { + // revert unselected options to the old values + presets.get_edited_preset().config.apply_only(presets.get_selected_preset().config, unselected_options); + } + + // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini + //BBS: add project embedded preset logic + //presets.save_current_preset(new_name); + presets.save_current_preset(new_name, false, save_to_project); + // Mark the print & filament enabled if they are compatible with the currently selected preset. + // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible. + update_compatible(PresetSelectCompatibleType::Never); + + if (type == Preset::TYPE_FILAMENT) { + // synchronize the first filament presets. + set_filament_preset(0, filaments.get_selected_preset_name()); + } +} + +void PresetBundle::load_installed_filaments(AppConfig &config) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, printer size %1%")%printers.size(); + //if (! config.has_section(AppConfig::SECTION_FILAMENTS) + // || config.get_section(AppConfig::SECTION_FILAMENTS).empty()) { + // Compatibility with the PrusaSlicer 2.1.1 and older, where the filament profiles were not installable yet. + // Find all filament profiles, which are compatible with installed printers, and act as if these filament profiles + // were installed. + std::unordered_set compatible_filaments; + for (const Preset &printer : printers) + if (printer.is_visible && printer.printer_technology() == ptFFF && printer.vendor && (!printer.vendor->models.empty())) { + bool add_default_materials = true; + if (config.has_section(AppConfig::SECTION_FILAMENTS)) + { + const std::map& installed_filament = config.get_section(AppConfig::SECTION_FILAMENTS); + for (auto filament_iter : installed_filament) + { + Preset* filament = filaments.find_preset(filament_iter.first, false, true); + if (filament && is_compatible_with_printer(PresetWithVendorProfile(*filament, filament->vendor), PresetWithVendorProfile(printer, printer.vendor))) + { + + //already has compatible filament + add_default_materials = false; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1% vendor %2% already has default filament %3%")%printer.name %printer.vendor %filament_iter.first; + break; + } + } + } + + if (!add_default_materials) + continue; + + const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); + if (!printer_model) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find printer_model for printer %1%")%printer.name; + continue; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1% vendor %2% don't have filament visible, will add %3% default filaments")%printer.name %printer.vendor %printer_model->default_materials.size(); + for (auto default_filament: printer_model->default_materials) + { + Preset* filament = filaments.find_preset(default_filament, false, true); + if (filament && filament->is_system) + compatible_filaments.insert(filament); + } + //const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); + //for (const Preset &filament : filaments) + // if (filament.is_system && is_compatible_with_printer(filaments.get_preset_with_vendor_profile(filament), printer_with_vendor_profile)) + // compatible_filaments.insert(&filament); + } + // and mark these filaments as installed, therefore this code will not be executed at the next start of the application. + for (const auto &filament: compatible_filaments) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set filament %1% to visible by default")%filament->name; + config.set(AppConfig::SECTION_FILAMENTS, filament->name, "true"); + } + //} + + for (auto &preset : filaments) + preset.set_visible_from_appconfig(config); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": exit."); +} + +void PresetBundle::load_installed_sla_materials(AppConfig &config) +{ + if (! config.has_section(AppConfig::SECTION_MATERIALS)) { + std::unordered_set comp_sla_materials; + // Compatibility with the PrusaSlicer 2.1.1 and older, where the SLA material profiles were not installable yet. + // Find all SLA material profiles, which are compatible with installed printers, and act as if these SLA material profiles + // were installed. + for (const Preset &printer : printers) + if (printer.is_visible && printer.printer_technology() == ptSLA) { + const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); + for (const Preset &material : sla_materials) + if (material.is_system && is_compatible_with_printer(sla_materials.get_preset_with_vendor_profile(material), printer_with_vendor_profile)) + comp_sla_materials.insert(&material); + } + // and mark these SLA materials as installed, therefore this code will not be executed at the next start of the application. + for (const auto &material: comp_sla_materials) + config.set(AppConfig::SECTION_MATERIALS, material->name, "true"); + } + + for (auto &preset : sla_materials) + preset.set_visible_from_appconfig(config); +} + +void PresetBundle::update_selections(AppConfig &config) +{ + std::string initial_printer_profile_name = printers.get_selected_preset_name(); + // Orca: load from orca_presets + std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); + std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); + + // Selects the profiles, which were selected at the last application close. + prints.select_preset_by_name_strict(initial_print_profile_name); + filaments.select_preset_by_name_strict(initial_filament_profile_name); + + // Load the names of the other filament profiles selected for a multi-material printer. + // Load it even if the current printer technology is SLA. + // The possibly excessive filament names will be later removed with this->update_multi_material_filament_presets() + // once the FFF technology gets selected. + this->filament_presets = { filaments.get_selected_preset_name() }; + for (unsigned int i = 1; i < 1000; ++ i) { + char name[64]; + sprintf(name, "filament_%02u", i); + auto f_name = config.get_printer_setting(initial_printer_profile_name, name); + if (f_name.empty()) + break; + this->filament_presets.emplace_back(remove_ini_suffix(f_name)); + } + + update_filament_count(); + + std::vector filament_colors; + auto f_colors = config.get_printer_setting(initial_printer_profile_name, "filament_colors"); + if (!f_colors.empty()) { + boost::algorithm::split(filament_colors, f_colors, boost::algorithm::is_any_of(",")); + } + filament_colors.resize(filament_presets.size(), "#26A69A"); + project_config.option("filament_colour")->values = filament_colors; + + std::vector multi_filament_colors; + if (config.has_printer_setting(initial_printer_profile_name, "filament_multi_colors")) { + boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), boost::algorithm::is_any_of(",")); + } + if (multi_filament_colors.size() == 0) project_config.option("filament_multi_colour")->values = filament_colors; + else project_config.option("filament_multi_colour")->values = multi_filament_colors; + + std::vector filament_color_types; + if (config.has_printer_setting(initial_printer_profile_name, "filament_color_types")) { + boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), boost::algorithm::is_any_of(",")); + } + filament_color_types.resize(filament_presets.size(), "1"); + project_config.option("filament_colour_type")->values = filament_color_types; + + std::vector filament_maps(filament_colors.size(), 1); + project_config.option("filament_map")->values = filament_maps; + + std::vector extruder_ams_count_str; + if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) { + boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(",")); + } + this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); + + std::vector matrix; + if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_matrix")) { + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), boost::algorithm::is_any_of("|")); + auto flush_volumes_matrix = matrix | boost::adaptors::transformed(boost::lexical_cast); + project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), flush_volumes_matrix.end()); + } + if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_vector")) { + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), boost::algorithm::is_any_of("|")); + auto flush_volumes_vector = matrix | boost::adaptors::transformed(boost::lexical_cast); + project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), flush_volumes_vector.end()); + } + if (config.has_printer_setting(initial_printer_profile_name, "flush_multiplier")) { + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), boost::algorithm::is_any_of("|")); + auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast); + project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), flush_multipliers.end()); + } + + // Update visibility of presets based on their compatibility with the active printer. + // Always try to select a compatible print and filament preset to the current printer preset, + // as the application may have been closed with an active "external" preset, which does not + // exist. + this->update_compatible(PresetSelectCompatibleType::Always); + this->update_multi_material_filament_presets(); + + std::string first_visible_filament_name; + for (auto & fp : filament_presets) { + // Orca: also match the ORCA_DEFAULT_FILAMENT_PLACEHOLDER placeholder. update_compatible_internal + // iterates from m_num_default_presets, so the placeholder's is_compatible flag + // stays true and the not-found/visible/compatible predicate alone would miss it. + if (auto it = filaments.find_preset_internal(fp); fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { + if (first_visible_filament_name.empty()) + first_visible_filament_name = filaments.first_compatible().name; + fp = first_visible_filament_name; + } + } + +} + +// Load selections (current print, current filaments, current printer) from config.ini +// This is done on application start up or after updates are applied. +void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& preferred_selection/* = PresetPreferences()*/) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, preferred printer_model_id %1%")%preferred_selection.printer_model_id; + // Update visibility of presets based on application vendor / model / variant configuration. + this->load_installed_printers(config); + + // Update visibility of filament and sla material presets + this->load_installed_filaments(config); + this->load_installed_sla_materials(config); + + // Parse the initial print / filament / printer profile names. + // std::string initial_sla_print_profile_name = remove_ini_suffix(config.get("presets", PRESET_SLA_PRINT_NAME)); + // std::string initial_sla_material_profile_name = remove_ini_suffix(config.get("presets", PRESET_SLA_MATERIALS_NAME)); + std::string initial_printer_profile_name = remove_ini_suffix(config.get("presets", PRESET_PRINTER_NAME)); + + // Activate print / filament / printer profiles from either the config, + // or from the preferred_model_id suggestion passed in by ConfigWizard. + // If the printer profile enumerated by the config are not visible, select an alternate preset. + // Do not select alternate profiles for the print / filament profiles as those presets + // will be selected by the following call of this->update_compatible(PresetSelectCompatibleType::Always). + + const Preset *initial_printer = printers.find_preset(initial_printer_profile_name); + // If executed due to a Config Wizard update, preferred_printer contains the first newly installed printer, otherwise nullptr. + const Preset *preferred_printer = printers.find_system_preset_by_model_and_variant(preferred_selection.printer_model_id, preferred_selection.printer_variant); + printers.select_preset_by_name(preferred_printer ? preferred_printer->name : initial_printer_profile_name, true); + CNumericLocalesSetter locales_setter; + + // Orca: load from orca_presets + // const auto os_presets = config.get_machine_settings(initial_printer_profile_name); + std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); + std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); + + //BBS: set default print/filament profiles to BBL's default setting + if (preferred_printer) + { + const std::string& prefered_print_profile = preferred_printer->config.opt_string("default_print_profile"); + if ((!initial_print_profile_name.compare("Default Setting")) && (prefered_print_profile.size() > 0)) + initial_print_profile_name = prefered_print_profile; + + const std::vector& prefered_filament_profiles = preferred_printer->config.option("default_filament_profile")->values; + if ((!initial_filament_profile_name.compare(ORCA_DEFAULT_FILAMENT_PLACEHOLDER)) && (prefered_filament_profiles.size() > 0)) { + // Check if preferred filament is visible + const Preset* preferred_preset = this->filaments.find_preset(prefered_filament_profiles[0], false); + if (preferred_preset && preferred_preset->is_visible) { + initial_filament_profile_name = prefered_filament_profiles[0]; + } + // If not visible, keep the default ORCA_DEFAULT_FILAMENT_PLACEHOLDER which will be resolved later + } + } + + // Selects the profile, leaves it to -1 if the initial profile name is empty or if it was not found. + prints.select_preset_by_name_strict(initial_print_profile_name); + filaments.select_preset_by_name_strict(initial_filament_profile_name); + // sla_prints.select_preset_by_name_strict(initial_sla_print_profile_name); + // sla_materials.select_preset_by_name_strict(initial_sla_material_profile_name); + + // Load the names of the other filament profiles selected for a multi-material printer. + // Load it even if the current printer technology is SLA. + // The possibly excessive filament names will be later removed with this->update_multi_material_filament_presets() + // once the FFF technology gets selected. + this->filament_presets = { filaments.get_selected_preset_name() }; + for (unsigned int i = 1; i < 1000; ++ i) { + char name[64]; + sprintf(name, "filament_%02u", i); + auto f_name = config.get_printer_setting(initial_printer_profile_name, name); + if (f_name.empty()) + break; + this->filament_presets.emplace_back(remove_ini_suffix(f_name)); + } + + update_filament_count(); + + // Load data from AppConfig to ProjectConfig when Studio is initialized. + std::vector filament_colors; + auto f_colors = config.get_printer_setting(initial_printer_profile_name, "filament_colors"); + if (!f_colors.empty()) { + boost::algorithm::split(filament_colors, f_colors, boost::algorithm::is_any_of(",")); + } + filament_colors.resize(filament_presets.size(), "#26A69A"); + project_config.option("filament_colour")->values = filament_colors; + + std::vector multi_filament_colors; + if (config.has_printer_setting(initial_printer_profile_name, "filament_multi_colors")) { + boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), boost::algorithm::is_any_of(",")); + } + if (multi_filament_colors.size() == 0) project_config.option("filament_multi_colour")->values = filament_colors; + else project_config.option("filament_multi_colour")->values = multi_filament_colors; + + std::vector filament_color_types; + if (config.has_printer_setting(initial_printer_profile_name, "filament_color_types")) { + boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), boost::algorithm::is_any_of(",")); + } + filament_color_types.resize(filament_presets.size(), "1"); + project_config.option("filament_colour_type")->values = filament_color_types; + + std::vector filament_maps(filament_colors.size(), 1); + project_config.option("filament_map")->values = filament_maps; + + std::vector extruder_ams_count_str; + if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) { + boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(",")); + } + this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); + + std::vector matrix; + if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_matrix")) { + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), boost::algorithm::is_any_of("|")); + auto flush_volumes_matrix = matrix | boost::adaptors::transformed(boost::lexical_cast); + project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), flush_volumes_matrix.end()); + } + if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_vector")) { + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), boost::algorithm::is_any_of("|")); + auto flush_volumes_vector = matrix | boost::adaptors::transformed(boost::lexical_cast); + project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), flush_volumes_vector.end()); + } + if (config.has_printer_setting(initial_printer_profile_name, "flush_multiplier")) { + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), boost::algorithm::is_any_of("|")); + auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast); + project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), flush_multipliers.end()); + } + + // Update visibility of presets based on their compatibility with the active printer. + // Always try to select a compatible print and filament preset to the current printer preset, + // as the application may have been closed with an active "external" preset, which does not + // exist. + this->update_compatible(PresetSelectCompatibleType::Always); + this->update_multi_material_filament_presets(); + + if (initial_printer != nullptr && (preferred_printer == nullptr || initial_printer == preferred_printer)) { + // Don't run the following code, as we want to activate default filament / SLA material profiles when installing and selecting a new printer. + // Only run this code if just a filament / SLA material was installed by Config Wizard for an active Printer. + auto printer_technology = printers.get_selected_preset().printer_technology(); + if (printer_technology == ptFFF && ! preferred_selection.filament.empty()) { + std::string preferred_preset_name = get_preset_name_by_alias(Preset::Type::TYPE_FILAMENT, preferred_selection.filament); + if (auto it = filaments.find_preset_internal(preferred_preset_name); + it != filaments.end() && (it->name == preferred_preset_name ) && it->is_visible && it->is_compatible) { + filaments.select_preset_by_name_strict(preferred_preset_name); + this->filament_presets.front() = filaments.get_selected_preset_name(); + } + } else if (printer_technology == ptSLA && ! preferred_selection.sla_material.empty()) { + std::string preferred_preset_name = get_preset_name_by_alias(Preset::Type::TYPE_SLA_MATERIAL, preferred_selection.sla_material); + if (auto it = sla_materials.find_preset_internal(preferred_preset_name); + it != sla_materials.end() && it->is_visible && it->is_compatible) + sla_materials.select_preset_by_name_strict(preferred_preset_name); + } + } + + std::string first_visible_filament_name; + for (auto & fp : filament_presets) { + // Orca: also match the ORCA_DEFAULT_FILAMENT_PLACEHOLDER placeholder — see update_selections. + if (auto it = filaments.find_preset_internal(fp); fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { + if (first_visible_filament_name.empty()) + first_visible_filament_name = filaments.first_compatible().name; + fp = first_visible_filament_name; + } + } + + const Preset& current_printer = printers.get_selected_preset(); + const Preset* base_printer = printers.get_preset_base(current_printer); + bool use_default_nozzle_volume_type = true; + if (base_printer) { + std::string prev_nozzle_volume_type = config.get_nozzle_volume_types_from_config(base_printer->name); + if (!prev_nozzle_volume_type.empty()) { + ConfigOptionEnumsGeneric* nozzle_volume_type_option = project_config.option("nozzle_volume_type"); + if (nozzle_volume_type_option->deserialize(prev_nozzle_volume_type)) { + use_default_nozzle_volume_type = false; + } + } + } + + if (use_default_nozzle_volume_type) { + project_config.option("nozzle_volume_type")->values = current_printer.config.option("default_nozzle_volume_type")->values; + } + + // Parse the initial physical printer name. + std::string initial_physical_printer_name = remove_ini_suffix(config.get("presets", "physical_printer")); + + // Activate physical printer from the config + if (!initial_physical_printer_name.empty()) + physical_printers.select_printer(initial_physical_printer_name); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, preferred printer_model_id %1%")%preferred_selection.printer_model_id; +} + +// Export selections (current print, current filaments, current printer) into config.ini +// BBS: change directories by design +void PresetBundle::export_selections(AppConfig &config) +{ + assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() >= 1); + //assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() > 1 || filaments.get_selected_preset_name() == filament_presets.front()); + config.clear_section("presets"); + auto printer_name = printers.get_selected_preset_name(); + config.set("presets", PRESET_PRINTER_NAME, printer_name); + + // Don't persist settings for the built-in "Default Printer" placeholder — + // it's only the initial state before a real printer is loaded/selected. + // Also clean up any stale entry that other code paths (e.g. bed type change) + // may have created for "Default Printer". + if (printer_name == "Default Printer") { + config.clear_printer_settings("Default Printer"); + return; + } + + config.clear_printer_settings(printer_name); + config.set_printer_setting(printer_name, PRESET_PRINTER_NAME, printer_name); + config.set_printer_setting(printer_name, PRESET_PRINT_NAME, prints.get_selected_preset_name()); + config.set_printer_setting(printer_name, PRESET_FILAMENT_NAME, filament_presets.front()); + config.set_printer_setting(printer_name, "curr_bed_type", config.get("curr_bed_type")); + for (unsigned i = 1; i < filament_presets.size(); ++i) { + char name[64]; + assert(!filament_presets[i].empty()); + sprintf(name, "filament_%02u", i); + config.set_printer_setting(printer_name, name, filament_presets[i]); + } + // Load project config data into app config + CNumericLocalesSetter locales_setter; + std::string filament_colors = boost::algorithm::join(project_config.option("filament_colour")->values, ","); + config.set_printer_setting(printer_name, "filament_colors", filament_colors); + + // Load filament multi color data into app config + std::string filament_multi_colors = boost::algorithm::join(project_config.option("filament_multi_colour")->values, ","); + config.set_printer_setting(printer_name, "filament_multi_colors", filament_multi_colors); + + // Load filament color type data into app config + std::string filament_color_types = boost::algorithm::join(project_config.option("filament_colour_type")->values, ","); + config.set_printer_setting(printer_name, "filament_color_types", filament_color_types); + + // Load ams counts data into app config + std::string extruder_ams_count_str = boost::algorithm::join(save_extruder_ams_count_to_string(this->extruder_ams_counts), ","); + config.set_printer_setting(printer_name, "extruder_ams_count", extruder_ams_count_str); + + std::string flush_volumes_matrix = boost::algorithm::join(project_config.option("flush_volumes_matrix")->values | + boost::adaptors::transformed(static_cast(std::to_string)), + "|"); + config.set_printer_setting(printer_name, "flush_volumes_matrix", flush_volumes_matrix); + std::string flush_volumes_vector = boost::algorithm::join(project_config.option("flush_volumes_vector")->values | + boost::adaptors::transformed(static_cast(std::to_string)), + "|"); + config.set_printer_setting(printer_name, "flush_volumes_vector", flush_volumes_vector); + + + std::string flush_multiplier_str = boost::algorithm::join(project_config.option("flush_multiplier")->values | + boost::adaptors::transformed(static_cast(std::to_string)), + "|"); + config.set_printer_setting(printer_name, "flush_multiplier", flush_multiplier_str); + + // BBS + //config.set("presets", "sla_print", sla_prints.get_selected_preset_name()); + //config.set("presets", "sla_material", sla_materials.get_selected_preset_name()); + //config.set("presets", "physical_printer", physical_printers.get_selected_full_printer_name()); + //BBS: add config related log + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1%, print %2%, filaments[0] %3% ")%printers.get_selected_preset_name() % prints.get_selected_preset_name() %filament_presets[0]; +} + +// BBS +void PresetBundle::set_num_filaments(unsigned int n, std::vector new_colors) { + int old_filament_count = this->filament_presets.size(); + if (n > old_filament_count && old_filament_count != 0) + filament_presets.resize(n, filament_presets.back()); + else { + filament_presets.resize(n); + } + ConfigOptionStrings* filament_color = project_config.option("filament_colour"); + ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); + ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + + + filament_color->resize(n); + // Sync filament multi colour + filament_multi_color->values.resize(n); + for (size_t i = 0; i < n; i++) { + filament_multi_color->values[i] = filament_color->values[i]; + } + filament_color_type->resize(n); + filament_map->values.resize(n, 1); + ams_multi_color_filment.resize(n); + + // BBS set new filament color to new_color + if (old_filament_count < n) { + if (!new_colors.empty()) { + for (int i = old_filament_count; i < n; i++) { + filament_color->values[i] = new_colors[i - old_filament_count]; + filament_multi_color->values[i] = new_colors[i - old_filament_count]; + filament_color_type->values[i] = "1"; // default color type + } + } + } + + update_multi_material_filament_presets(); +} +void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) +{ + unsigned old_filament_count = this->filament_presets.size(); + if (n > old_filament_count && old_filament_count != 0) + filament_presets.resize(n, filament_presets.back()); + else { + filament_presets.resize(n); + } + ConfigOptionStrings* filament_color = project_config.option("filament_colour"); + ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); + ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + + + filament_color->resize(n); + // Sync filament multi colour + filament_multi_color->values.resize(n); + for (size_t i = 0; i < n; i++) { + filament_multi_color->values[i] = filament_color->values[i]; + } + filament_color_type->resize(n); + filament_map->values.resize(n, 1); + ams_multi_color_filment.resize(n); + + //BBS set new filament color to new_color + if (old_filament_count < n) { + if (!new_color.empty()) { + for (unsigned i = old_filament_count; i < n; i++) { + filament_color->values[i] = new_color; + filament_multi_color->values[i] = new_color; + filament_color_type->values[i] = "1"; // default color type + } + } + } + + update_multi_material_filament_presets(); +} + +void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) +{ + unsigned old_filament_count = this->filament_presets.size(); + assert(to_del_flament_id < old_filament_count); + filament_presets.erase(filament_presets.begin() + to_del_flament_id); + + // update edited_preset + { + Preset& edited_preset = filaments.get_edited_preset(); + bool edited_preset_deleted = true; + for (std::string filament_preset_name : filament_presets) { + if (filament_preset_name == edited_preset.name) { + edited_preset_deleted = false; + } + } + if (edited_preset_deleted) { + filaments.select_preset_by_name(filament_presets.front(), false); + } + } + + ConfigOptionStrings *filament_color = project_config.option("filament_colour"); + ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); + ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + if (filament_color->values.size() > to_del_flament_id) { + filament_color->values.erase(filament_color->values.begin() + to_del_flament_id); + if (filament_map->values.size() > to_del_flament_id) { + filament_map->values.erase(filament_map->values.begin() + to_del_flament_id); + } + } + else { + filament_color->values.resize(to_del_flament_id); + filament_map->values.resize(to_del_flament_id, 1); + } + + // lambda function to erase or resize the container + auto erase_or_resize = [to_del_flament_id](auto& container) { + if (container.size() > to_del_flament_id) { + container.erase(container.begin() + to_del_flament_id); + } else { + container.resize(to_del_flament_id); + } + }; + + erase_or_resize(filament_multi_color->values); + erase_or_resize(filament_color_type->values); + erase_or_resize(ams_multi_color_filment); + + update_multi_material_filament_presets(to_del_flament_id); +} + + +void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) +{ + combox_info.clear(); + for (auto &entry : filament_ams_list) { + auto &ams = entry.second; + auto filament_id = ams.opt_string("filament_id", 0u); + auto filament_color = ams.opt_string("filament_colour", 0u); + auto ams_name = ams.opt_string("tray_name", 0u); + auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); + auto filament_multi_color = ams.opt("filament_multi_colour")->values; + if (filament_id.empty()) { + continue; + } + if (!filament_changed && this->filament_presets.size() > combox_info.ams_filament_presets.size()) { + combox_info.ams_filament_presets.push_back(this->filament_presets[combox_info.ams_filament_presets.size()]); + combox_info.ams_filament_colors.push_back(filament_color); + combox_info.ams_multi_color_filment.push_back(filament_multi_color); + combox_info.ams_names.push_back(ams_name); + continue; + } + auto iter = std::find_if(filaments.begin(), filaments.end(), + [this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); + if (iter == filaments.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; + auto filament_type = ams.opt_string("filament_type", 0u); + if (!filament_type.empty()) { + filament_type = "Generic " + filament_type; + iter = std::find_if(filaments.begin(), filaments.end(), + [&filament_type](auto &f) { return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); }); + } + if (iter == filaments.end()) { + // Prefer old selection + if (combox_info.ams_filament_presets.size() < this->filament_presets.size()) { + combox_info.ams_filament_presets.push_back(this->filament_presets[combox_info.ams_filament_presets.size()]); + combox_info.ams_filament_colors.push_back(filament_color); + combox_info.ams_multi_color_filment.push_back(filament_multi_color); + combox_info.ams_names.push_back(ams_name); + continue; + } + iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) { return f.is_compatible && f.is_system; }); + if (iter == filaments.end()) + continue; + } + filament_id = iter->filament_id; + } + combox_info.ams_filament_presets.push_back(iter->name); + combox_info.ams_filament_colors.push_back(filament_color); + combox_info.ams_multi_color_filment.push_back(filament_multi_color); + combox_info.ams_names.push_back(ams_name); + } +} + +unsigned int PresetBundle::sync_ams_list(std::vector> &unknowns, bool use_map, std::map &maps, bool enable_append, MergeFilamentInfo &merge_info, bool color_only) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "use_map:" << use_map << " enable_append:" << enable_append; + std::vector ams_filament_presets; + std::vector ams_filament_colors; + std::vector ams_filament_color_types; + std::vector ams_array_maps; + ams_multi_color_filment.clear(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_ams_list size: %1%") % filament_ams_list.size(); + struct AmsInfo + { + bool valid{false}; + bool is_map{false}; + bool is_placeholder{false}; + std::string filament_color = ""; + std::string filament_color_type = ""; + std::string filament_preset = ""; + std::vector mutli_filament_color; + }; + auto is_double_extruder = get_printer_extruder_count() == 2; + std::vector ams_infos; + int index = 0; + for (auto &entry : filament_ams_list) { + auto & ams = entry.second; + auto filament_id = ams.opt_string("filament_id", 0u); + auto filament_color = ams.opt_string("filament_colour", 0u); + auto filament_color_type = ams.opt_string("filament_colour_type", 0u); + auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); + auto filament_multi_color = ams.opt("filament_multi_colour")->values; + auto ams_id = ams.opt_string("ams_id", 0u); + auto slot_id = ams.opt_string("slot_id", 0u); + auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u); + ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color}); + AMSMapInfo temp = {ams_id, slot_id}; + ams_array_maps.push_back(temp); + index++; + if (filament_id.empty()) { + if (use_map) { + for (int j = maps.size() - 1; j >= 0; j--) { + if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) { + maps.erase(j); + } + } + ams_filament_presets.push_back("Generic PLA");//for unknow matieral + auto default_unknown_color = "#CECECE"; + ams_filament_colors.push_back(default_unknown_color); + ams_filament_color_types.push_back("1"); + if (filament_multi_color.size() == 0) { + filament_multi_color.push_back(default_unknown_color); + } + ams_multi_color_filment.push_back(filament_multi_color); + } else if (is_placeholder) { + // Orca: push placeholders to keep index alignment with ams_infos + ams_filament_presets.push_back(""); + ams_filament_colors.push_back(""); + ams_filament_color_types.push_back(""); + ams_multi_color_filment.push_back({}); + } + continue; + } + if (!filament_changed && this->filament_presets.size() > ams_filament_presets.size()) { + ams_filament_presets.push_back(this->filament_presets[ams_filament_presets.size()]); + ams_filament_colors.push_back(filament_color); + ams_filament_color_types.push_back(filament_color_type); + ams_multi_color_filment.push_back(filament_multi_color); + continue; + } + bool has_type = false; + auto filament_type = ams.opt_string("filament_type", 0u); + auto sub_brands = ams.opt_string("filament_sub_brands", 0u); + // If sub_brands (filament brand name) is known, prefer a preset whose name starts with it + // to resolve ID collisions where two different vendors share the same filament_id. + auto iter = filaments.end(); + if (!sub_brands.empty() && !filament_id.empty()) { + iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type, &sub_brands](auto &f) { + has_type |= f.config.opt_string("filament_type", 0u) == filament_type; + return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id + && boost::algorithm::istarts_with(f.name, sub_brands); }); + } + if (iter == filaments.end()) { + iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) { + has_type |= f.config.opt_string("filament_type", 0u) == filament_type; + return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); + } + // Also search user presets (not base presets) with matching filament_id. + // User presets have P-prefix IDs and are not returned by get_preset_base(f) == &f. + if (iter == filaments.end() && !filament_id.empty()) { + iter = std::find_if(filaments.begin(), filaments.end(), [&filament_id, &has_type, filament_type](auto &f) { + has_type |= f.config.opt_string("filament_type", 0u) == filament_type; + return f.is_compatible && !f.is_system && f.filament_id == filament_id; }); + } + if (iter == filaments.end()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; + if (!filament_type.empty()) { + auto original_type = filament_type; + filament_type = "Generic " + filament_type; + iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) { + return f.is_compatible && f.is_system + && boost::algorithm::starts_with(f.name, filament_type); + }); + if (iter == filaments.end()) { + // Similarity fallback: find a generic preset whose filament_type + // appears as a whole word in the AMS type (e.g. "ASA" in "ASA Sparkle"). + auto upper_type = boost::to_upper_copy(original_type); + auto contains_word = [](const std::string& haystack, const std::string& needle) { + auto pos = haystack.find(needle); + while (pos != std::string::npos) { + bool start_ok = (pos == 0 || !std::isalnum(static_cast(haystack[pos - 1]))); + bool end_ok = (pos + needle.size() >= haystack.size() || + !std::isalnum(static_cast(haystack[pos + needle.size()]))); + if (start_ok && end_ok) + return true; + pos = haystack.find(needle, pos + 1); + } + return false; + }; + // Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA". + size_t best_len = 0; + for (auto it = filaments.begin(); it != filaments.end(); ++it) { + if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic ")) + continue; + auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u)); + if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) { + iter = it; + best_len = preset_type.size(); + filament_type = "Generic " + it->config.opt_string("filament_type", 0u); + } + } + } + } + if (iter == filaments.end()) { + // Prefer old selection + if (ams_filament_presets.size() < this->filament_presets.size()) { + ams_filament_presets.push_back(this->filament_presets[ams_filament_presets.size()]); + ams_filament_colors.push_back(filament_color); + ams_filament_color_types.push_back(filament_color_type); + ams_multi_color_filment.push_back(filament_multi_color); + unknowns.emplace_back(&ams, has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") : + L("The filament model is unknown. Still using the previous filament preset.")); + continue; + } + iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) { + return f.is_compatible && f.is_system + && boost::algorithm::starts_with(f.name, "Generic "); + }); + if (iter == filaments.end()) + iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) { + return f.is_compatible && f.is_system; + }); + if (iter == filaments.end()) + continue; + } + unknowns.emplace_back(&ams, boost::algorithm::starts_with(iter->name, filament_type) ? + (has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") : + L("The filament model is unknown. Generic filament presets will be used.")) : + (has_type ? L("The filament may not be compatible with the current machine settings. A random filament preset will be used.") : + L("The filament model is unknown. A random filament preset will be used."))); + filament_id = iter->filament_id; + } + ams_filament_presets.push_back(iter->name); + ams_filament_colors.push_back(filament_color); + ams_filament_color_types.push_back(filament_color_type); + ams_multi_color_filment.push_back(filament_multi_color); + } + if (ams_filament_presets.empty()) + return 0; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "get filament_colour and from config"; + ConfigOptionStrings *filament_color = project_config.option("filament_colour"); + ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts * filament_map = project_config.option("filament_map"); + if (color_only) { + auto get_map_index = [&ams_infos](const std::vector &infos, const AMSMapInfo &temp) { + for (int i = 0; i < infos.size(); i++) { + if (infos[i].slot_id == temp.slot_id && infos[i].ams_id == temp.ams_id) { + ams_infos[i].is_map = true; + return i; + } + } + return -1; + }; + + auto exist_colors = filament_color->values; + std::vector> exist_multi_color_filment(exist_colors.size()); + for (size_t i = 0; i < exist_colors.size(); i++) { + exist_multi_color_filment[i] = {exist_colors[i]}; + } + + ConfigOptionStrings *project_multi_color = project_config.option("filament_multi_colour"); + if (project_multi_color) { + for (size_t i = 0; i < std::min(exist_multi_color_filment.size(), project_multi_color->values.size()); i++) { + std::vector colors = split_string(project_multi_color->values[i], ' '); + if (!colors.empty()) { + exist_multi_color_filment[i] = colors; + } + } + } + + bool mapped_any = false; + if (use_map && !maps.empty()) { + for (size_t i = 0; i < exist_colors.size(); i++) { + if (maps.find(i) == maps.end()) { + continue; + } + int valid_index = get_map_index(ams_array_maps, maps[i]); + if (valid_index >= 0 && valid_index < int(ams_filament_colors.size()) && !ams_filament_colors[valid_index].empty()) { + exist_colors[i] = ams_filament_colors[valid_index]; + mapped_any = true; + if (valid_index < int(ams_multi_color_filment.size()) && !ams_multi_color_filment[valid_index].empty()) { + exist_multi_color_filment[i] = ams_multi_color_filment[valid_index]; + } else { + exist_multi_color_filment[i] = {ams_filament_colors[valid_index]}; + } + } + } + } + // Fallback to index-based color sync if no mapping was applied. + if (!use_map || maps.empty() || !mapped_any) { + size_t sync_count = std::min(exist_colors.size(), ams_filament_colors.size()); + for (size_t i = 0; i < sync_count; i++) { + if (ams_filament_colors[i].empty()) { + continue; + } + exist_colors[i] = ams_filament_colors[i]; + if (i < ams_multi_color_filment.size() && !ams_multi_color_filment[i].empty()) { + exist_multi_color_filment[i] = ams_multi_color_filment[i]; + } else { + exist_multi_color_filment[i] = {ams_filament_colors[i]}; + } + } + } + + filament_color->values = exist_colors; + ams_multi_color_filment = exist_multi_color_filment; + merge_info.merges.clear(); + } else if (use_map) { + auto check_has_merge_info = [](std::map &maps, MergeFilamentInfo &merge_info, int exist_colors_size) { + std::set done; + for (auto it_i = maps.begin(); it_i != maps.end(); ++it_i) { + std::vector same_ams; + same_ams.emplace_back(it_i->first); + for (auto it_j = std::next(it_i); it_j != maps.end(); ++it_j) { + if (done.find(it_j->first) != done.end()) { + continue; + } + if (it_i->second.slot_id == "" || it_i->second.ams_id == ""){ + continue; + } + if (it_i->second.slot_id == it_j->second.slot_id && it_i->second.ams_id == it_j->second.ams_id) { + same_ams.emplace_back(it_j->first); + done.insert(it_j->first); + } + } + if (same_ams.size() > 1) { + merge_info.merges.emplace_back(same_ams); + } + } + }; + check_has_merge_info(maps, merge_info,filament_color->values.size()); + auto get_map_index = [&ams_infos](const std::vector &infos, const AMSMapInfo &temp) { + for (int i = 0; i < infos.size(); i++) { + if (infos[i].slot_id == temp.slot_id && infos[i].ams_id == temp.ams_id) { + ams_infos[i].is_map = true; + return i; + } + } + return -1; + }; + std::vector need_append_colors; + auto exist_colors = filament_color->values; + auto exist_color_types = filament_color_type->values; + auto exist_filament_presets = this->filament_presets; + std::vector> exist_multi_color_filment; + exist_multi_color_filment.resize(exist_colors.size()); + for (int i = 0; i < exist_colors.size(); i++) { + exist_multi_color_filment[i] = {exist_colors[i]}; + } + for (size_t i = 0; i < exist_colors.size(); i++) { + if (maps.find(i) != maps.end()) {//mapping exist + auto valid_index = get_map_index(ams_array_maps, maps[i]); + if (valid_index >= 0 && valid_index < ams_filament_presets.size()) { + exist_colors[i] = ams_filament_colors[valid_index]; + exist_color_types[i] = ams_filament_color_types[valid_index]; + exist_filament_presets[i] = ams_filament_presets[valid_index]; + exist_multi_color_filment[i] = ams_multi_color_filment[valid_index]; + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (mapping exist)"; + } + } + } + for (size_t i = 0; i < ams_infos.size(); i++) {// check append + if (ams_infos[i].valid) { + if (i >= ams_filament_presets.size()) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)"; + continue; + } + ams_infos[i].filament_preset = ams_filament_presets[i]; + ams_infos[i].mutli_filament_color = ams_multi_color_filment[i]; + if (!ams_infos[i].is_map) { + need_append_colors.emplace_back(ams_infos[i]); + ams_filament_colors[i] = ""; + ams_filament_color_types[i] = ""; + ams_filament_presets[i] = ""; + ams_multi_color_filment[i] = std::vector(); + } + } + else { + ams_filament_colors[i] = ""; + ams_filament_color_types[i] = ""; + ams_filament_presets[i] = ""; + ams_multi_color_filment[i] = std::vector(); + } + } + //delete redundant color + ams_filament_colors.erase(std::remove_if(ams_filament_colors.begin(), ams_filament_colors.end(), [](std::string &value) { return value.empty(); }), + ams_filament_colors.end()); + ams_filament_color_types.erase(std::remove_if(ams_filament_color_types.begin(), ams_filament_color_types.end(), [](std::string &value) { return value.empty(); }), + ams_filament_color_types.end()); + ams_filament_presets.erase(std::remove_if(ams_filament_presets.begin(), ams_filament_presets.end(), [](std::string &value) { return value.empty(); }), + ams_filament_presets.end()); + ams_multi_color_filment.erase(std::remove_if(ams_multi_color_filment.begin(), ams_multi_color_filment.end(), + [](std::vector &value) { return value.empty(); }), + ams_multi_color_filment.end()); + if (need_append_colors.size() > 0 && enable_append) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "need_append_colors.size() > 0 && enable_append"; + auto get_idx_in_array = [](std::vector &presets, std::vector &colors, const std::string &preset, const std::string &color) -> int { + for (size_t i = 0; i < presets.size(); i++) { + if (presets[i] == preset && colors[i] == color) { + return i; + } + } + return -1; + }; + for (size_t i = 0; i < need_append_colors.size(); i++){ + if (exist_filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)){ + break; + } + auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color); + if (idx >= 0) { + continue; + } + exist_filament_presets.push_back(need_append_colors[i].filament_preset); + exist_colors.push_back(need_append_colors[i].filament_color); + exist_color_types.push_back(need_append_colors[i].filament_color_type); + exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color); + } + } + filament_color->values = exist_colors; + filament_color_type->values = exist_color_types; + ams_multi_color_filment = exist_multi_color_filment; + this->filament_presets = exist_filament_presets; + filament_map->values.resize(exist_filament_presets.size(), 1); + } + else {//overwrite; + bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(), + [](const AmsInfo& a) { return a.is_placeholder; }); + if (has_placeholders) { + // Orca: merge — keep existing filaments for empty slots + auto exist_colors = filament_color->values; + auto exist_color_types = filament_color_type->values; + auto exist_presets = this->filament_presets; + + size_t tray_count = ams_filament_presets.size(); + size_t total = std::max(tray_count, exist_presets.size()); + + std::vector result_colors; + std::vector result_color_types; + std::vector result_presets; + std::vector> result_multi_colors; + + for (size_t i = 0; i < total; i++) { + bool is_loaded = (i < ams_infos.size() && ams_infos[i].valid); + + if (is_loaded) { + // Loaded tray: use tray's filament data + result_colors.push_back(ams_filament_colors[i]); + result_color_types.push_back(ams_filament_color_types[i]); + result_presets.push_back(ams_filament_presets[i]); + result_multi_colors.push_back( + i < ams_multi_color_filment.size() ? ams_multi_color_filment[i] + : std::vector{ams_filament_colors[i]}); + } else if (i < exist_presets.size()) { + // Empty tray or beyond tray count: keep existing filament + result_colors.push_back(exist_colors[i]); + result_color_types.push_back(exist_color_types[i]); + result_presets.push_back(exist_presets[i]); + result_multi_colors.push_back({exist_colors[i]}); + } else { + // New slot beyond existing count: prefer a generic filament preset + auto it = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) { + return f.is_compatible && f.is_system + && boost::algorithm::starts_with(f.name, "Generic "); + }); + std::string fallback_name = (it != filaments.end()) ? it->name : filaments.first_visible().name; + result_colors.push_back("#CECECE"); + result_color_types.push_back("1"); + result_presets.push_back(fallback_name); + result_multi_colors.push_back({"#CECECE"}); + } + } + + filament_color->values = result_colors; + filament_color_type->values = result_color_types; + this->filament_presets = result_presets; + ams_multi_color_filment = result_multi_colors; + filament_map->values.resize(total, 1); + } else { + // BBL: existing wholesale replace + filament_color->values = ams_filament_colors; + filament_color_type->values = ams_filament_color_types; + this->filament_presets = ams_filament_presets; + filament_map->values.resize(ams_filament_colors.size(), 1); + } + + auto& print_config = this->prints.get_edited_preset().config; + auto support_filament_opt = print_config.option("support_filament"); + auto support_interface_filament_opt = print_config.option("support_interface_filament"); + if (support_filament_opt->value > filament_color_type->values.size()) + support_filament_opt->value = 0; + + if (support_interface_filament_opt->value > filament_color_type->values.size()) + support_interface_filament_opt->value = 0; + } + // Update ams_multi_color_filment + update_filament_multi_color(); + update_multi_material_filament_presets(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish sync ams list"; + return this->filament_presets.size(); +} + +void PresetBundle::update_filament_multi_color() +{ + std::vector exsit_multi_colors; + for (auto &fil_item : ams_multi_color_filment){ + if (fil_item.empty()) break; + if (fil_item.size() == 1) + exsit_multi_colors.push_back(fil_item[0]); + else { + std::string colors = ""; + for (auto &color : fil_item){ + colors += color + " "; + } + colors.erase(colors.size() - 1); // remove last space + exsit_multi_colors.push_back(colors); + } + } + ConfigOptionStrings *filament_multi_colour = project_config.option("filament_multi_colour"); + filament_multi_colour->resize(exsit_multi_colors.size()); + filament_multi_colour->values = exsit_multi_colors; +} + +std::vector PresetBundle::get_used_tpu_filaments(const std::vector &used_filaments) +{ + std::vector tpu_filaments; + for (size_t i = 0; i < this->filament_presets.size(); ++i) { + auto iter = std::find(used_filaments.begin(), used_filaments.end(), i + 1); + if (iter == used_filaments.end()) continue; + + std::string filament_name = this->filament_presets[i]; + for (int f_index = 0; f_index < this->filaments.size(); f_index++) { + PresetCollection *filament_presets = &this->filaments; + Preset *preset = &filament_presets->preset(f_index); + int size = this->filaments.size(); + if (preset && filament_name.compare(preset->name) == 0) { + std::string display_filament_type; + std::string filament_type = preset->config.get_filament_type(display_filament_type); + if (display_filament_type == "TPU") { + tpu_filaments.push_back(i); + } + } + } + } + return tpu_filaments; +} + +void PresetBundle::set_calibrate_printer(std::string name) +{ + if (name.empty()) { + calibrate_filaments.clear(); + return; + } + if (!name.empty()) + calibrate_printer = printers.find_preset(name); + const Preset & printer_preset = calibrate_printer ? *calibrate_printer : printers.get_edited_preset(); + const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer_preset); + DynamicPrintConfig config; + config.set_key_value("printer_preset", new ConfigOptionString(active_printer.preset.name)); + const ConfigOption *opt = active_printer.preset.config.option("nozzle_diameter"); + if (opt) config.set_key_value("num_extruders", new ConfigOptionInt((int) static_cast(opt)->values.size())); + calibrate_filaments.clear(); + for (size_t i = filaments.num_default_presets(); i < filaments.size(); ++i) { + const Preset & preset = filaments.m_presets[i]; + const PresetWithVendorProfile this_preset_with_vendor_profile = filaments.get_preset_with_vendor_profile(preset); + bool is_compatible = is_compatible_with_printer(this_preset_with_vendor_profile, active_printer, &config); + if (is_compatible) calibrate_filaments.insert(&preset); + } +} + +std::vector> PresetBundle::get_extruder_filament_info() const +{ + std::vector> filament_infos; + int extruder_nums = get_printer_extruder_count(); + if (extruder_nums > 1) { + filament_infos.resize(extruder_nums, std::vector()); + for (auto ams_item : filament_ams_list) { + if (ams_item.first & 0x10000) { // right + filament_infos[1].push_back(ams_item.second); + } else { // left + filament_infos[0].push_back(ams_item.second); + } + } + } + return filament_infos; +} + +std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only) +{ + std::set printer_names; + if ("0.0" == nozzle_diameter_str || nozzle_diameter_str.empty()) { + nozzle_diameter_str = "0.4"; + } + std::ostringstream stream; + + for (auto printer_it = this->printers.begin(); printer_it != this->printers.end(); printer_it++) { + if (system_only && !printer_it->is_system) continue; + + ConfigOption * printer_model_opt = printer_it->config.option("printer_model"); + ConfigOptionString *printer_model_str = dynamic_cast(printer_model_opt); + if (!printer_model_str) continue; + + // use printer_model as printer type + if (printer_model_str->value != printer_type) continue; + + if (printer_it->name.find(nozzle_diameter_str) != std::string::npos) printer_names.insert(printer_it->name); + } + + assert(printer_names.size() == 1); + + for (auto& printer_name : printer_names) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " printer name: " << printer_name; + } + + return printer_names; +} + +bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray( + const std::string &printer_type, std::string& nozzle_diameter_str, std::string &setting_id, std::string &tag_uid, std::string &nozzle_temp_min, std::string &nozzle_temp_max, std::string& preset_setting_id) +{ + bool is_equation = true; + + std::map> filament_list = filaments.get_filament_presets(); + std::set printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str); + + for (const Preset *preset : filament_list.find(setting_id)->second) { + if (tag_uid == "0" || (tag_uid.size() == 16 && tag_uid.substr(12, 2) == "01")) continue; + if (preset && !preset->is_user()) continue; + ConfigOption * printer_opt = const_cast(preset)->config.option("compatible_printers"); + ConfigOptionStrings *printer_strs = dynamic_cast(printer_opt); + bool compared = false; + for (const std::string &printer_str : printer_strs->values) { + if (printer_names.find(printer_str) != printer_names.end()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "nozzle temp matching: preset name: " << preset->name << " printer name: " << printer_str; + // Compare only once + if (!compared) { + compared = true; + bool min_temp_equation = false, max_temp_equation = false; + int min_nozzle_temp = std::stoi(nozzle_temp_min); + int max_nozzle_temp = std::stoi(nozzle_temp_max); + ConfigOption *opt_min = const_cast(preset)->config.option("nozzle_temperature_range_low"); + if (opt_min) { + ConfigOptionInts *opt_min_ints = dynamic_cast(opt_min); + min_nozzle_temp = opt_min_ints->get_at(0); + if (std::to_string(min_nozzle_temp) == nozzle_temp_min) + min_temp_equation = true; + else { + BOOST_LOG_TRIVIAL(info) << "tray min temp: " << nozzle_temp_min << " preset min temp: " << min_nozzle_temp; + nozzle_temp_min = std::to_string(min_nozzle_temp); + } + } + ConfigOption *opt_max = const_cast(preset)->config.option("nozzle_temperature_range_high"); + if (opt_max) { + ConfigOptionInts *opt_max_ints = dynamic_cast(opt_max); + max_nozzle_temp = opt_max_ints->get_at(0); + if (std::to_string(max_nozzle_temp) == nozzle_temp_max) + max_temp_equation = true; + else { + BOOST_LOG_TRIVIAL(info) << "tray max temp: " << nozzle_temp_max << " preset min temp: " << max_nozzle_temp; + nozzle_temp_max = std::to_string(max_nozzle_temp); + } + } + if (min_temp_equation && max_temp_equation) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: no changed"; + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: has changed"; + preset_setting_id = preset->setting_id; + is_equation = false; + } + } else { + assert(false); + } + } + } + } + return is_equation; +} + +Preset *PresetBundle::get_similar_printer_preset(std::string printer_model, std::string printer_variant) +{ + if (printer_model.empty()) + printer_model = printers.get_selected_preset().config.opt_string("printer_model"); + if (printer_model.empty()) // ORCA ensure a compatible model exist. fixes switches to blank preset if preset has no inherited value + return nullptr; + auto printer_variant_old = printers.get_selected_preset().config.opt_string("printer_variant"); + std::map printer_presets; + for (auto &preset : printers.m_presets) { + if (printer_variant.empty() && !preset.is_system) + continue; + if (preset.config.opt_string("printer_model") == printer_model) + printer_presets.insert({preset.name, &preset}); + } + if (printer_presets.empty()) + return nullptr; + auto prefer_printer = printers.get_selected_preset().alias; //.name ORCA use alias instead "name" for calling system presets. otherwise nozzle combo will not change printer presets if they custom named + + if (!printer_variant.empty()) + boost::replace_all(prefer_printer, printer_variant_old, printer_variant); + else if (auto n = prefer_printer.find(printer_variant_old); n != std::string::npos) + prefer_printer = printer_model + " " + printer_variant_old + prefer_printer.substr(n + printer_variant_old.length()); + if (auto iter = printer_presets.find(prefer_printer); iter != printer_presets.end()) { + return iter->second; + } + if (printer_variant.empty()) + printer_variant = printer_variant_old; + for (auto& preset : printer_presets) { + if (preset.second->config.opt_string("printer_variant") == printer_variant) + return preset.second; + } + return printer_presets.begin()->second; +} + +//BBS: check whether this is the only edited filament +bool PresetBundle::is_the_only_edited_filament(unsigned int filament_index) +{ + unsigned n = this->filament_presets.size(); + if (filament_index >= n) + return false; + + std::string name = this->filament_presets[filament_index]; + Preset& edited_preset = this->filaments.get_edited_preset(); + if (edited_preset.name != name) + return false; + + unsigned index = 0; + while (index < n) + { + if (index == filament_index) { + index ++; + continue; + } + std::string filament_preset = this->filament_presets[index]; + if (edited_preset.name == filament_preset) + return false; + else + index ++; + } + return true; +} + +void PresetBundle::reset_default_nozzle_volume_type() +{ + Preset& current_printer = this->printers.get_edited_preset(); + this->project_config.option("nozzle_volume_type")->values = current_printer.config.option("default_nozzle_volume_type")->values; +} + +int PresetBundle::get_printer_extruder_count() const +{ + const Preset& printer_preset = this->printers.get_edited_preset(); + + const auto* nozzle_diameter = printer_preset.config.option("nozzle_diameter"); + if (nozzle_diameter == nullptr) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": nozzle_diameter is missing, using 1 extruder"; + return 1; + } + if (nozzle_diameter->values.empty()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": nozzle_diameter is empty, using 1 extruder"; + return 1; + } + + int count = int(nozzle_diameter->values.size()); + + return count; +} + +void PresetBundle::update_filament_count() +{ + if (printers.get_edited_preset().printer_technology() != ptFFF) + return; + const size_t num_extruders = static_cast(get_printer_extruder_count()); + if (filament_presets.size() >= num_extruders) + return; + filament_presets.resize(num_extruders, filament_presets.empty() + ? filaments.first_visible().name + : filament_presets.back()); +} + +bool PresetBundle::support_different_extruders() +{ + Preset& printer_preset = this->printers.get_edited_preset(); + int extruder_count; + bool supported = printer_preset.config.support_different_extruders(extruder_count); + + return supported; +} + +DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional>filament_maps) const +{ + return (this->printers.get_edited_preset().printer_technology() == ptFFF) ? + this->full_fff_config(apply_extruder, filament_maps) : + this->full_sla_config(); +} + +DynamicPrintConfig PresetBundle::full_config_secure(std::optional>filament_maps) const +{ + DynamicPrintConfig config = this->full_fff_config(false, filament_maps); + //FIXME legacy, the keys should not be there after conversion to a Physical Printer profile. + config.erase("print_host"); + config.erase("print_host_webui"); + config.erase("printhost_apikey"); + config.erase("printhost_cafile"); + config.erase("printhost_user"); + config.erase("printhost_password"); + config.erase("printhost_port"); + return config; +} + +const std::set ignore_settings_list ={ + "inherits", + "print_settings_id", "filament_settings_id", "printer_settings_id" +}; + +DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional> filament_maps_new) const +{ + DynamicPrintConfig out; + out.apply(FullPrintConfig::defaults()); + out.apply(this->prints.get_edited_preset().config); + // Add the default filament preset to have the "filament_preset_id" defined. + out.apply(this->filaments.default_preset().config); + out.apply(this->printers.get_edited_preset().config); + out.apply(this->project_config); + + // BBS + size_t num_filaments = this->filament_presets.size(); + + std::vector filament_maps = out.option("filament_map")->values; + if (filament_maps_new.has_value()) + filament_maps = *filament_maps_new; + //in some middle state, they may be different + if (filament_maps.size() != num_filaments) { + filament_maps.resize(num_filaments, 1); + } + else { + assert(filament_maps.size() == num_filaments); + } + + auto* extruder_diameter = dynamic_cast(out.option("nozzle_diameter")); + // Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector. + std::vector compatible_printers_condition; + std::vector compatible_prints_condition; + std::vector inherits; + std::vector filament_ids; + std::vector print_compatible_printers; + //BBS: add logic for settings check between different system presets + std::vector different_settings; + std::string different_print_settings, different_printer_settings; + compatible_printers_condition.emplace_back(this->prints.get_edited_preset().compatible_printers_condition()); + + const ConfigOptionStrings* compatible_printers = (const_cast(this))->prints.get_edited_preset().config.option("compatible_printers", false); + if (compatible_printers) + print_compatible_printers = compatible_printers->values; + //BBS: add logic for settings check between different system presets + std::string print_inherits = this->prints.get_edited_preset().inherits(); + inherits .emplace_back(print_inherits); + const Preset* print_parent_preset = this->prints.get_selected_preset_parent(); + if (print_parent_preset) { + std::vector dirty_options = this->prints.dirty_options_without_option_list(&(this->prints.get_edited_preset()), print_parent_preset, ignore_settings_list, false); + if (!dirty_options.empty()) { + different_print_settings = Slic3r::escape_strings_cstyle(dirty_options); + } + } + different_settings.emplace_back(different_print_settings); + + //BBS: update printer config related with variants + if (apply_extruder) { + out.update_values_to_printer_extruders(out, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); + out.update_values_to_printer_extruders(out, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); + //update print config related with variants + out.update_values_to_printer_extruders(out, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + } + + if (num_filaments <= 1) { + //BBS: update filament config related with variants + DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config; + if (apply_extruder) + filament_config.update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0]); + out.apply(filament_config); + compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition()); + compatible_prints_condition .emplace_back(this->filaments.get_edited_preset().compatible_prints_condition()); + //BBS: add logic for settings check between different system presets + //std::string filament_inherits = this->filaments.get_edited_preset().inherits(); + std::string current_preset_name = this->filament_presets[0]; + const Preset* preset = this->filaments.find_preset(current_preset_name, true); + std::string filament_inherits = preset->inherits(); + inherits .emplace_back(filament_inherits); + filament_ids.emplace_back(this->filaments.get_edited_preset().filament_id); + + std::string different_filament_settings; + const Preset* filament_parent_preset = this->filaments.get_selected_preset_parent(); + if (filament_parent_preset) { + std::vector dirty_options = this->filaments.dirty_options_without_option_list(&(this->filaments.get_edited_preset()), filament_parent_preset, ignore_settings_list, false); + if (!dirty_options.empty()) { + different_filament_settings = Slic3r::escape_strings_cstyle(dirty_options); + } + } + + different_settings.emplace_back(different_filament_settings); + + std::vector& filament_self_indice = out.option("filament_self_index", true)->values; + int index_size = out.option("filament_extruder_variant")->size(); + filament_self_indice.resize(index_size, 1); + } else { + // Retrieve filament presets and build a single config object for them. + // First collect the filament configurations based on the user selection of this->filament_presets. + // Here this->filaments.find_preset() and this->filaments.first_visible() return the edited copy of the preset if active. + std::vector filament_configs; + std::vector filament_presets; + for (const std::string& filament_preset_name : this->filament_presets) { + const Preset* preset = this->filaments.find_preset(filament_preset_name, true); + filament_presets.emplace_back(preset); + filament_configs.emplace_back(&(preset->config)); + } + while (filament_configs.size() < num_filaments) { + const Preset* preset = &this->filaments.first_visible(); + filament_presets.emplace_back(preset); + filament_configs.emplace_back(&(preset->config)); + } + for (int index = 0; index < num_filaments; index++) { + const DynamicPrintConfig *cfg = filament_configs[index]; + const Preset *preset = filament_presets[index]; + // The compatible_prints/printers_condition() returns a reference to configuration key, which may not yet exist. + DynamicPrintConfig &cfg_rw = *const_cast(cfg); + compatible_printers_condition.emplace_back(Preset::compatible_printers_condition(cfg_rw)); + compatible_prints_condition .emplace_back(Preset::compatible_prints_condition(cfg_rw)); + + //BBS: add logic for settings check between different system presets + std::string filament_inherits = Preset::inherits(cfg_rw); + inherits .emplace_back(filament_inherits); + filament_ids.emplace_back(preset->filament_id); + std::string different_filament_settings; + + const Preset* filament_parent_preset = nullptr; + if (preset->is_system || preset->is_default) { + bool is_selected = this->filaments.get_selected_preset_name() == preset->name; + if (is_selected) { + //use the real preset + filament_parent_preset = const_cast(this)->filaments.find_preset(preset->name, false, true); + } + else { + filament_parent_preset = preset; + } + } + else if (!filament_inherits.empty()) + filament_parent_preset = const_cast(this)->filaments.find_preset(filament_inherits, false, true); + + if (filament_parent_preset) { + std::vector dirty_options = cfg_rw.diff(filament_parent_preset->config); + if (!dirty_options.empty()) { + auto iter = dirty_options.begin(); + while (iter != dirty_options.end()) { + if (ignore_settings_list.find(*iter) != ignore_settings_list.end()) { + iter = dirty_options.erase(iter); + } + else { + ++iter; + } + } + different_filament_settings = Slic3r::escape_strings_cstyle(dirty_options); + } + } + + different_settings.emplace_back(different_filament_settings); + } + + std::vector filament_temp_configs; + filament_temp_configs.resize(num_filaments); + for (size_t i = 0; i < num_filaments; ++i) { + filament_temp_configs[i] = *(filament_configs[i]); + if (apply_extruder) + filament_temp_configs[i].update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i]); + } + + // loop through options and apply them to the resulting config. + std::vector filament_variant_count(num_filaments, 1); + for (const t_config_option_key &key : this->filaments.default_preset().config.keys()) { + if (key == "compatible_prints" || key == "compatible_printers") + continue; + // Get a destination option. + ConfigOption *opt_dst = out.option(key, false); + if (opt_dst->is_scalar()) { + // Get an option, do not create if it does not exist. + const ConfigOption *opt_src = filament_temp_configs.front().option(key); + if (opt_src != nullptr) + opt_dst->set(opt_src); + } else { + // BBS + ConfigOptionVectorBase* opt_vec_dst = static_cast(opt_dst); + { + if (apply_extruder) { + std::vector filament_opts(num_filaments, nullptr); + // Setting a vector value from all filament_configs. + for (size_t i = 0; i < filament_opts.size(); ++i) + filament_opts[i] = filament_temp_configs[i].option(key); + opt_vec_dst->set(filament_opts); + } + else { + for (size_t i = 0; i < num_filaments; ++i) { + const ConfigOptionVectorBase* filament_option = static_cast(filament_temp_configs[i].option(key)); + if (i == 0) + opt_vec_dst->set(filament_option); + else + opt_vec_dst->append(filament_option); + + if (key == "filament_extruder_variant") + filament_variant_count[i] = filament_option->size(); + } + } + } + } + } + + if (!apply_extruder) { + //append filament_self_index + std::vector& filament_self_indice = out.option("filament_self_index", true)->values; + int index_size = out.option("filament_extruder_variant")->size(); + filament_self_indice.resize(index_size, 1); + int k = 0; + for (size_t i = 0; i < num_filaments; i++) { + for (size_t j = 0; j < filament_variant_count[i]; j++) { + filament_self_indice[k++] = i + 1; + } + } + } + } + + //BBS: add logic for settings check between different system presets + std::string printer_inherits = this->printers.get_edited_preset().inherits(); + // Don't store the "compatible_printers_condition" for the printer profile, there is none. + inherits .emplace_back(printer_inherits); + const Preset* printer_parent_preset = this->printers.get_selected_preset_parent(); + if (printer_parent_preset) { + std::vector dirty_options = this->printers.dirty_options_without_option_list(&(this->printers.get_edited_preset()), printer_parent_preset, ignore_settings_list, false); + if (!dirty_options.empty()) { + different_printer_settings = Slic3r::escape_strings_cstyle(dirty_options); + } + } + different_settings.emplace_back(different_printer_settings); + + // These value types clash between the print and filament profiles. They should be renamed. + out.erase("compatible_prints"); + out.erase("compatible_prints_condition"); + out.erase("compatible_printers"); + out.erase("compatible_printers_condition"); + out.erase("inherits"); + //BBS: add logic for settings check between different system presets + out.erase("different_settings_to_system"); + + static const char* keys[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; + for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++ i) { + std::string key = std::string(keys[i]); + auto *opt = dynamic_cast(out.option(key, false)); + assert(opt != nullptr); + opt->value = boost::algorithm::clamp(opt->value, 0, int(num_filaments)); + } + + static const char* keys_1based[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"}; + for (size_t i = 0; i < sizeof(keys_1based) / sizeof(keys_1based[0]); ++ i) { + std::string key = std::string(keys_1based[i]); + auto *opt = dynamic_cast(out.option(key, false)); + assert(opt != nullptr); + if(opt->value < 1 || opt->value > int(num_filaments)) + opt->value = 1; + } + out.option("print_settings_id", true)->value = this->prints.get_selected_preset_name(); + out.option("filament_settings_id", true)->values = this->filament_presets; + out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); + out.option("filament_ids", true)->values = filament_ids; + out.option("filament_map", true)->values = filament_maps; + // Serialize the collected "compatible_printers_condition" and "inherits" fields. + // There will be 1 + num_exturders fields for "inherits" and 2 + num_extruders for "compatible_printers_condition" stored. + // The vector will not be stored if all fields are empty strings. + auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { + bool nonempty = false; + for (const std::string &v : values) + if (! v.empty()) { + nonempty = true; + break; + } + if (nonempty) + out.set_key_value(key, new ConfigOptionStrings(std::move(values))); + }; + add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); + add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); + add_if_some_non_empty(std::move(inherits), "inherits_group"); + //BBS: add logic for settings check between different system presets + add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); + add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers"); + out.option("extruder_ams_count", true)->values = save_extruder_ams_count_to_string(this->extruder_ams_counts); + + out.option("printer_technology", true)->value = ptFFF; + return out; +} + +DynamicPrintConfig PresetBundle::full_sla_config() const +{ + DynamicPrintConfig out; + out.apply(SLAFullPrintConfig::defaults()); + out.apply(this->sla_prints.get_edited_preset().config); + out.apply(this->sla_materials.get_edited_preset().config); + out.apply(this->printers.get_edited_preset().config); + // There are no project configuration values as of now, the project_config is reserved for FFF printers. +// out.apply(this->project_config); + + // Collect the "compatible_printers_condition" and "inherits" values over all presets (sla_prints, sla_materials, printers) into a single vector. + std::vector compatible_printers_condition; + std::vector compatible_prints_condition; + std::vector inherits; + compatible_printers_condition.emplace_back(this->sla_prints.get_edited_preset().compatible_printers_condition()); + inherits .emplace_back(this->sla_prints.get_edited_preset().inherits()); + compatible_printers_condition.emplace_back(this->sla_materials.get_edited_preset().compatible_printers_condition()); + compatible_prints_condition .emplace_back(this->sla_materials.get_edited_preset().compatible_prints_condition()); + inherits .emplace_back(this->sla_materials.get_edited_preset().inherits()); + inherits .emplace_back(this->printers.get_edited_preset().inherits()); + + // These two value types clash between the print and filament profiles. They should be renamed. + out.erase("compatible_printers"); + out.erase("compatible_printers_condition"); + out.erase("inherits"); + + out.option("sla_print_settings_id", true)->value = this->sla_prints.get_selected_preset_name(); + out.option("sla_material_settings_id", true)->value = this->sla_materials.get_selected_preset_name(); + out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); + + // Serialize the collected "compatible_printers_condition" and "inherits" fields. + // There will be 1 + num_exturders fields for "inherits" and 2 + num_extruders for "compatible_printers_condition" stored. + // The vector will not be stored if all fields are empty strings. + auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { + bool nonempty = false; + for (const std::string &v : values) + if (! v.empty()) { + nonempty = true; + break; + } + if (nonempty) + out.set_key_value(key, new ConfigOptionStrings(std::move(values))); + }; + add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); + add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); + add_if_some_non_empty(std::move(inherits), "inherits_group"); + + out.option("printer_technology", true)->value = ptSLA; + return out; +} + +// Load an external config file containing the print, filament and printer presets. +// Instead of a config file, a G-code may be loaded containing the full set of parameters. +// In the future the configuration will likely be read from an AMF file as well. +// If the file is loaded successfully, its print / filament / printer profiles will be activated. +ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule) +{ + if (is_gcode_file(path)) { + DynamicPrintConfig config; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, gcodefile %1%, compatibility_rule %2%")%path %compatibility_rule; + config.apply(FullPrintConfig::defaults()); + ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, compatibility_rule); + Preset::normalize(config); + load_config_file_config(path, true, std::move(config)); + return config_substitutions; + } + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode")%path ; + throw Slic3r::RuntimeError(std::string("Unknown configuration file: ") + path); + + return ConfigSubstitutions{}; +} + + +//some filament presets split from one to sperate ones +//following map recording these filament presets +//for example: previously ''Bambu PLA Basic @BBL H2D 0.6 nozzle' was saved in ''Bambu PLA Basic @BBL H2D' with 0.4 +static std::map> filament_preset_convert = { +{"Bambu Lab H2D 0.6 nozzle", {{"Bambu PLA Basic @BBL H2D", "Bambu PLA Basic @BBL H2D 0.6 nozzle"}, + {"Bambu PLA Matte @BBL H2D", "Bambu PLA Matte @BBL H2D 0.6 nozzle"}, + {"Bambu ABS @BBL H2D", "Bambu ABS @BBL H2D 0.6 nozzle"}}}, +{"Bambu Lab H2D 0.8 nozzle", {{"Bambu PETG HF @BBL H2D 0.6 nozzle", "Bambu PETG HF @BBL H2D 0.8 nozzle"}, + {"Bambu ASA @BBL H2D 0.6 nozzle", "Bambu ASA @BBL H2D 0.8 nozzle"}}} +}; + +//convert the old filament preset to new one after split +static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name) +{ + auto machine_iter = filament_preset_convert.find(machine_name); + if (machine_iter != filament_preset_convert.end()) + { + std::map& filament_maps = machine_iter->second; + auto filament_iter = filament_maps.find(filament_name); + if (filament_iter != filament_maps.end()) + { + filament_name = filament_iter->second; + } + } +} +// Load a config file from a boost property_tree. This is a private method called from load_config_file. +// is_external == false on if called from ConfigWizard +void PresetBundle::load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version, bool selected) +{ + PrinterTechnology printer_technology = Preset::printer_technology(config); + + auto clear_compatible_printers = [](DynamicPrintConfig& config){ + ConfigOption *opt_compatible = config.optptr("compatible_printers"); + if (opt_compatible != nullptr) { + assert(opt_compatible->type() == coStrings); + if (opt_compatible->type() == coStrings) + static_cast(opt_compatible)->values.clear(); + } + }; + clear_compatible_printers(config); + +#if 0 + size_t num_extruders = (printer_technology == ptFFF) ? + std::min(config.option("nozzle_diameter" )->values.size(), + config.option("filament_diameter")->values.size()) : + // 1 SLA material + 1; +#else + // BBS: use filament_colour insteadof filament_settings_id, filament_settings_id sometimes is not generated + ConfigOptionStrings* filament_colour_option = config.option("filament_colour"); + size_t num_filaments = filament_colour_option?filament_colour_option->size():0; + if (num_filaments == 0) + throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); +#endif + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": , name_or_path %1%, is_external %2%, num_filaments %3%") % name_or_path % is_external % num_filaments; + // Make a copy of the "compatible_machine_expression_group" and "inherits_group" vectors, which + // accumulate values over all presets (print, filaments, printers). + // These values will be distributed into their particular presets when loading. + std::vector compatible_printers_condition_values = std::move(config.option("compatible_machine_expression_group", true)->values); + std::vector compatible_prints_condition_values = std::move(config.option("compatible_process_expression_group", true)->values); + std::vector inherits_values = std::move(config.option("inherits_group", true)->values); + std::vector filament_ids = std::move(config.option("filament_ids", true)->values); + std::vector print_compatible_printers = std::move(config.option("print_compatible_printers", true)->values); + //BBS: add different settings check logic + bool has_different_settings_to_system = config.option("different_settings_to_system")?true:false; + std::vector different_values = std::move(config.option("different_settings_to_system", true)->values); + std::string &compatible_printers_condition = Preset::compatible_printers_condition(config); + std::string &compatible_prints_condition = Preset::compatible_prints_condition(config); + std::string &inherits = Preset::inherits(config); + compatible_printers_condition_values.resize(num_filaments + 2, std::string()); + compatible_prints_condition_values.resize(num_filaments, std::string()); + inherits_values.resize(num_filaments + 2, std::string()); + different_values.resize(num_filaments + 2, std::string()); + filament_ids.resize(num_filaments, std::string()); + // The "default_filament_profile" will be later extracted into the printer profile. + switch (printer_technology) { + case ptFFF: + config.option("default_print_profile", true); + config.option("default_filament_profile", true); + break; + case ptSLA: + config.option("default_sla_print_profile", true); + config.option("default_sla_material_profile", true); + break; + default: break; + } + + bool process_multi_extruder = false; + std::vector filament_variant_index; + size_t extruder_variant_count; + if (!config.option("filament_self_index")) { + std::vector& filament_self_indice = config.option("filament_self_index", true)->values; + filament_self_indice.resize(num_filaments); + for (int index = 0; index < num_filaments; index++) + filament_self_indice[index] = index + 1; + } + std::vector filament_self_indice = std::move(config.option("filament_self_index")->values); + // ORCA: Initialize filament_extruder_variant for backward compatibility with old 3mf files + // that don't have this option saved or have it with default single-element value + ConfigOptionStrings* filament_extruder_variant_opt = config.option("filament_extruder_variant"); + if (!filament_extruder_variant_opt || filament_extruder_variant_opt->size() < num_filaments) { + std::vector& filament_extruder_variant = config.option("filament_extruder_variant", true)->values; + filament_extruder_variant.resize(num_filaments, "Direct Drive Standard"); + } + if (config.option("extruder_variant_list")) { + //3mf support multiple extruder logic + size_t extruder_count = config.option("nozzle_diameter")->values.size(); + extruder_variant_count = config.option("filament_extruder_variant", true)->size(); + if ((extruder_variant_count != filament_self_indice.size()) + || (extruder_variant_count < num_filaments)) { + assert(false); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % name_or_path; + throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); + } + if (num_filaments != extruder_variant_count) { + process_multi_extruder = true; + filament_variant_index.resize(num_filaments, 0); + + size_t cur_filament_id = 1; + for (size_t index = 0; index < filament_self_indice.size(); index++) { + if (filament_self_indice[index] == cur_filament_id) { + filament_variant_index[cur_filament_id - 1] = index; + cur_filament_id++; + if (cur_filament_id > num_filaments) + break; + } + } + } + } + //no need to parse extruder_ams_count + std::vector extruder_ams_count = std::move(config.option("extruder_ams_count", true)->values); + config.erase("extruder_ams_count"); + if (this->extruder_ams_counts.empty()) + this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count); + + + // 1) Create a name from the file name. + // Keep the suffix (.ini, .gcode, .amf, .3mf etc) to differentiate it from the normal profiles. + std::string name = is_external ? boost::filesystem::path(name_or_path).filename().string() : name_or_path; + + // 2) If the loading succeeded, split and load the config into print / filament / printer settings. + // First load the print and printer presets. + + auto load_preset = + [&config, &inherits, &inherits_values, + &compatible_printers_condition, &compatible_printers_condition_values, + &compatible_prints_condition, &compatible_prints_condition_values, + is_external, &name, &name_or_path, file_version, selected] + (PresetCollection &presets, size_t idx, const std::string &key, const std::set &different_keys, std::string filament_id) { + // Split the "compatible_printers_condition" and "inherits" values one by one from a single vector to the print & printer profiles. + inherits = inherits_values[idx]; + compatible_printers_condition = compatible_printers_condition_values[idx]; + if (idx > 0 && idx - 1 < compatible_prints_condition_values.size()) + compatible_prints_condition = compatible_prints_condition_values[idx - 1]; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": , name %1%, is_external %2%, inherits %3%")%name %is_external %inherits; + if (is_external) + presets.load_external_preset(name_or_path, name, config.opt_string(key, true), config, different_keys, PresetCollection::LoadAndSelect::Always, file_version, filament_id); + else + presets.load_preset(presets.path_from_name(name, inherits.empty()), name, config, selected, file_version).save(nullptr); + }; + + switch (Preset::printer_technology(config)) { + case ptFFF: + { + //BBS: add different settings logic + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id"); + std::vector print_different_keys_vector; + std::string print_different_settings = different_values[0]; + Slic3r::unescape_strings_cstyle(print_different_settings, print_different_keys_vector); + std::set print_different_keys_set(print_different_keys_vector.begin(), print_different_keys_vector.end()); + //if (!has_different_settings_to_system) { + // print_different_keys_set.clear(); + //} + //else + print_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + if (!print_compatible_printers.empty()) { + ConfigOptionStrings* compatible_printers = config.option("compatible_printers", true); + compatible_printers->values = print_compatible_printers; + } + + load_preset(this->prints, 0, "print_settings_id", print_different_keys_set, std::string()); + + //clear compatible printers + clear_compatible_printers(config); + + std::vector printer_different_keys_vector; + std::string printer_different_settings = different_values[num_filaments + 1]; + Slic3r::unescape_strings_cstyle(printer_different_settings, printer_different_keys_vector); + std::set printer_different_keys_set(printer_different_keys_vector.begin(), printer_different_keys_vector.end()); + //if (!has_different_settings_to_system) { + // printer_different_keys_set.clear(); + //} + //else + printer_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id"); + load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string()); + + // 3) Now load the filaments. If there are multiple filament presets, split them and load them. + auto old_filament_profile_names = config.option("filament_settings_id", true); + old_filament_profile_names->values.resize(num_filaments, std::string()); + + auto old_machine_profile_name = config.option("printer_settings_id", true); + + if (num_filaments <= 1) { + // Split the "compatible_printers_condition" and "inherits" values from the cummulative vectors to separate filament presets. + inherits = inherits_values[1]; + compatible_printers_condition = compatible_printers_condition_values[1]; + compatible_prints_condition = compatible_prints_condition_values.front(); + Preset *loaded = nullptr; + + //BBS: add different settings logic + std::vector filament_different_keys_vector; + std::string filament_different_settings = different_values[1]; + Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); + std::set filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end()); + //if (!has_different_settings_to_system) { + // filament_different_keys_set.clear(); + //} + //else + filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + + std::string filament_id = filament_ids[0]; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load single filament preset from filament_settings_id"); + if (is_external) { + if (inherits.empty()) + convert_filament_preset_name(old_machine_profile_name->value, old_filament_profile_names->values.front()); + else + convert_filament_preset_name(old_machine_profile_name->value, inherits); + loaded = this->filaments.load_external_preset(name_or_path, name, old_filament_profile_names->values.front(), config, filament_different_keys_set, PresetCollection::LoadAndSelect::Always, file_version, filament_id).first; + } + else { + // called from Config Wizard. + loaded= &this->filaments.load_preset(this->filaments.path_from_name(name, inherits.empty()), name, config, true, file_version); + loaded->save(nullptr); + } + this->filament_presets.clear(); + this->filament_presets.emplace_back(loaded->name); + } else { + assert(is_external); + // Split the filament presets, load each of them separately. + std::vector configs(num_filaments, this->filaments.default_preset().config); + // loop through options and scatter them into configs. + for (const t_config_option_key &key : this->filaments.default_preset().config.keys()) { + ConfigOption *other_opt = config.option(key); + if (other_opt == nullptr) + continue; + if (other_opt->is_scalar()) { + for (size_t i = 0; i < configs.size(); ++ i) + configs[i].option(key, false)->set(other_opt); + } + else if (key != "compatible_printers" && key != "compatible_prints") { + for (size_t i = 0; i < configs.size(); ++i) { + if (process_multi_extruder && (filament_options_with_variant.find(key) != filament_options_with_variant.end())) { + ConfigOptionVectorBase* other_opt_vec = static_cast(other_opt); + if (other_opt_vec->size() != extruder_variant_count) { + other_opt_vec->resize(extruder_variant_count); + } + size_t next_index = (i < (configs.size() - 1)) ? filament_variant_index[i + 1] : extruder_variant_count; + static_cast(configs[i].option(key, false))->set(other_opt, filament_variant_index[i], next_index - filament_variant_index[i]); + } + else + static_cast(configs[i].option(key, false))->set_at(other_opt, 0, i); + } + } + } + // Load the configs into this->filaments and make them active. + this->filament_presets = std::vector(configs.size()); + // To avoid incorrect selection of the first filament preset (means a value of Preset->m_idx_selected) + // in a case when next added preset take a place of previosly selected preset, + // we should add presets from last to first + bool any_modified = false; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load multiple filament preset from filament_settings_id"); + for (int i = (int)configs.size()-1; i >= 0; i--) { + DynamicPrintConfig &cfg = configs[i]; + // Split the "compatible_printers_condition" and "inherits" from the cummulative vectors to separate filament presets. + cfg.opt_string("compatible_printers_condition", true) = compatible_printers_condition_values[i + 1]; + cfg.opt_string("compatible_prints_condition", true) = compatible_prints_condition_values[i]; + cfg.opt_string("inherits", true) = inherits_values[i + 1]; + + //BBS: add different settings logic + std::vector filament_different_keys_vector; + std::string filament_different_settings = different_values[i+1]; + Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); + std::set filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end()); + //if (!has_different_settings_to_system) { + // filament_different_keys_set.clear(); + //} + //else + filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + + std::string filament_id = filament_ids[i]; + + // Load all filament presets, but only select the first one in the preset dialog. + std::string& filament_inherit = cfg.opt_string("inherits", true); + if (filament_inherit.empty() && (i < int(old_filament_profile_names->values.size()))) + convert_filament_preset_name(old_machine_profile_name->value, old_filament_profile_names->values[i]); + else + convert_filament_preset_name(old_machine_profile_name->value, filament_inherit); + auto [loaded, modified] = this->filaments.load_external_preset(name_or_path, name, + (i < int(old_filament_profile_names->values.size())) ? old_filament_profile_names->values[i] : "", + std::move(cfg), + filament_different_keys_set, + i == 0 ? + PresetCollection::LoadAndSelect::Always : + any_modified ? + PresetCollection::LoadAndSelect::Never : + PresetCollection::LoadAndSelect::OnlyIfModified, + file_version, + filament_id); + any_modified |= modified; + this->filament_presets[i] = loaded->name; + } + } + + // 4) Load the project config values (the per extruder wipe matrix etc). + this->project_config.apply_only(config, s_project_options); + + break; + } + case ptSLA: + { + /*std::set different_keys_set; + load_preset(this->sla_prints, 0, "sla_print_settings_id", different_keys_set); + load_preset(this->sla_materials, 1, "sla_material_settings_id", different_keys_set); + load_preset(this->printers, 2, "printer_settings_id", different_keys_set);*/ + break; + } + default: + break; + } + + this->update_compatible(PresetSelectCompatibleType::Never); + this->update_multi_material_filament_presets(); + + //BBS + //const std::string &physical_printer = config.option("physical_printer_settings_id", true)->value; + const std::string physical_printer; + if (this->printers.get_edited_preset().is_external || physical_printer.empty()) { + this->physical_printers.unselect_printer(); + } else { + // Activate the physical printer profile if possible. + PhysicalPrinter *pp = this->physical_printers.find_printer(physical_printer, true); + if (pp != nullptr && std::find(pp->preset_names.begin(), pp->preset_names.end(), this->printers.get_edited_preset().name) != pp->preset_names.end()) + this->physical_printers.select_printer(pp->name, this->printers.get_edited_preset().name); + else + this->physical_printers.unselect_printer(); + } + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": finished"); +} + +//BBS: Load a config bundle file from json +std::pair PresetBundle::load_vendor_configs_from_json( + const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) +{ + // Enable substitutions for user config bundle, throw an exception when loading a system profile. + ConfigSubstitutionContext substitution_context { compatibility_rule }; + PresetsConfigSubstitutions substitutions; + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%")%path.c_str()%compatibility_rule; + if (flags.has(LoadConfigBundleAttribute::ResetUserProfile) || flags.has(LoadConfigBundleAttribute::LoadSystem)) + // Reset this bundle, delete user profile files if SaveImported. + this->reset(flags.has(LoadConfigBundleAttribute::SaveImported)); + + // 1) load the vroot json and construct the vendor profile + VendorProfile vendor_profile(vendor_name); + std::string root_file = path + "/" + vendor_name + ".json"; + std::vector> machine_model_subfiles; + std::vector> process_subfiles; + std::vector> filament_subfiles; + std::vector> machine_subfiles; + auto get_name_and_subpath = [this](json::iterator& it, std::vector>& subfile_map) { + if (it.value().is_array()) { + for (auto iter1 = it.value().begin(); iter1 != it.value().end(); iter1++) { + if (iter1.value().is_object()) { + std::string name, subpath; + for (auto iter2 = iter1.value().begin(); iter2 != iter1.value().end(); iter2++) { + if (iter2.value().is_string()) { + if (boost::iequals(iter2.key(), BBL_JSON_KEY_NAME)) { + name = iter2.value(); + } else if (boost::iequals(iter2.key(), BBL_JSON_KEY_SUB_PATH)) { + subpath = iter2.value(); + } + } + else { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": invalid value type for " << iter2.key(); + } + } + if (!name.empty() && !subpath.empty()) + subfile_map.push_back(std::make_pair(name, subpath)); + } else { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid type for " << iter1.key(); + } + } + } else { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid type for " << it.key(); + } + }; + try { + boost::nowide::ifstream ifs(root_file); + json j; + ifs >> j; + //parse the json elements + for (auto it = j.begin(); it != j.end(); it++) { + if (boost::iequals(it.key(), BBL_JSON_KEY_VERSION)) { + //get version + std::string version_str = it.value(); + auto config_version = Semver::parse(version_str); + if (! config_version) { + ++m_errors; + throw ConfigurationError((boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly") + % vendor_name % version_str % path).str()); + } else { + vendor_profile.config_version = std::move(*config_version); + } + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { + //get url + vendor_profile.config_update_url = it.value(); + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) { + //get description + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< ": parse "<> j; + //parse the json elements + for (auto it = j.begin(); it != j.end(); it++) { + if (boost::iequals(it.key(), BBL_JSON_KEY_VERSION)) { + //get version + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { + //get url + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) { + //get name + model.name = it.value(); + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_MODEL_ID)) { + //get model_id + model.model_id = it.value(); + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_NOZZLE_DIAMETER)) { + //get nozzle diameter + std::string nozzle_diameters = it.value(); + std::vector variants; + if (Slic3r::unescape_strings_cstyle(nozzle_diameters, variants)) { + for (const std::string &variant_name : variants) { + if (model.variant(variant_name) == nullptr) + model.variants.emplace_back(VendorProfile::PrinterVariant(variant_name)); + } + } else { + ++m_errors; + BOOST_LOG_TRIVIAL(error)<< __FUNCTION__ << boost::format(": invalid nozzle_diameters %1% for Vendor %1%") % nozzle_diameters % vendor_name; + } + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_PRINTER_TECH)) { + //get printer tech + if (boost::algorithm::starts_with(it.value(), "SL")) + model.technology = ptSLA; + else + model.technology = ptFFF; + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_FAMILY)) { + //get family + model.family = it.value(); + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_MODEL)) { + //get bed model + model.bed_model = it.value(); + } else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_END_NAME)) { + model.bottom_texture_end_name = it.value(); + } else if (boost::iequals(it.key(), BBL_JSON_KEY_USE_DOUBLE_EXTRUDER_DEFAULT_TEXTURE)) { + model.use_double_extruder_default_texture = it.value(); + } else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT)) { + model.bottom_texture_rect = it.value(); + } else if (boost::iequals(it.key(), BBL_JSON_KEY_MIDDLE_TEXTURE_RECT)) { + model.middle_texture_rect = it.value(); + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_IMAGE_BED_TYPE)) { + model.image_bed_type = it.value(); + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_TEXTURE)) { + //get bed texture + model.bed_texture = it.value(); + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_HOTEND_MODEL)) { + model.hotend_model = it.value(); + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_DEFAULT_MATERIALS)) { + //get machine list + std::string default_materials_field = it.value(); + if (Slic3r::unescape_strings_cstyle(default_materials_field, model.default_materials)) { + Slic3r::sort_remove_duplicates(model.default_materials); + if (! model.default_materials.empty() && model.default_materials.front().empty()) + // An empty material was inserted into the list of default materials. Remove it. + model.default_materials.erase(model.default_materials.begin()); + } else { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid default_materials %1% for Vendor %1%") % default_materials_field % vendor_name; + } + } else if (boost::iequals(it.key(), BBL_JSON_KEY_NOT_SUPPORT_BED_TYPE)) { + // get machine list + std::string not_support_bed_type_field = it.value(); + if (Slic3r::unescape_strings_cstyle(not_support_bed_type_field, model.not_support_bed_types)) { + Slic3r::sort_remove_duplicates(model.not_support_bed_types); + if (!model.not_support_bed_types.empty() && model.not_support_bed_types.front().empty()) + // An empty material was inserted into the list of default materials. Remove it. + model.not_support_bed_types.erase(model.not_support_bed_types.begin()); + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ + << boost::format(": invalid not_support_bed_types %1% for Vendor %1%") % not_support_bed_type_field % vendor_name; + } + } + } + } + catch(nlohmann::detail::parse_error &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); + throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly") + %subfile %err.what() % path).str()); + } + + if (! model.id.empty() && ! model.variants.empty()) + vendor_profile.models.push_back(std::move(model)); + } + + //insert the vendor profile + this->vendors.emplace(vendor_name, vendor_profile); + const VendorProfile* current_vendor_profile = &this->vendors[vendor_name]; + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", loaded vendor profile, name %1%, id %2%, version %3%")%vendor_profile.name%vendor_profile.id%vendor_profile.config_version.to_string(); + + if (flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) + return std::make_pair(PresetsConfigSubstitutions{}, 0); + + // 3) paste the process/filament/print configs + PresetCollection *presets = nullptr; + size_t presets_loaded = 0; + + auto parse_subfile = [this, path, vendor_name, presets_loaded, current_vendor_profile, base_bundle]( + ConfigSubstitutionContext& substitution_context, + PresetsConfigSubstitutions& substitutions, + LoadConfigBundleAttributes& flags, + std::pair& subfile_iter, + std::map& config_maps, + std::map& filament_id_maps, + PresetCollection* presets_collection, + size_t& count, bool is_from_lib = false) -> std::string { + + std::string subfile = path + "/" + vendor_name + "/" + subfile_iter.second; + // Load the print, filament or printer preset. + std::string preset_name; + DynamicPrintConfig config; + std::string alias_name, inherits, description, instantiation, setting_id, filament_id; + std::vector renamed_from; + const DynamicPrintConfig* default_config = nullptr; + std::string reason; + try { + std::map key_values; + substitution_context.substitutions.clear(); + + //parse the json elements + DynamicPrintConfig config_src; + std::string _renamed_from_str; + config_src.load_from_json(subfile, substitution_context, false, key_values, reason); + if (!reason.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": load config file "<second; + auto filament_it = key_values.find(BBL_JSON_KEY_FILAMENT_ID); + if (filament_it != key_values.end()) + filament_id = filament_it->second; + //check whether it inherits other preset or not + auto it1 = key_values.find(BBL_JSON_KEY_INHERITS); + if (it1 != key_values.end()) { + inherits = it1->second; + auto it2 = config_maps.find(inherits); + default_config = nullptr; + if (it2 != config_maps.end()) + default_config = &(it2->second); + if(default_config == nullptr && base_bundle != nullptr) { + auto base_it2 = base_bundle->m_config_maps.find(inherits); + if (base_it2 != base_bundle->m_config_maps.end()) + default_config = &(base_it2->second); + } + if (default_config != nullptr) { + if (filament_id.empty() && (presets_collection->type() == Preset::TYPE_FILAMENT)) { + auto filament_id_map_iter = filament_id_maps.find(inherits); + if (filament_id_map_iter != filament_id_maps.end()) { + filament_id = filament_id_map_iter->second; + } + if (filament_id.empty() && base_bundle != nullptr) { + auto filament_id_map_iter = base_bundle->m_filament_id_maps.find(inherits); + if (filament_id_map_iter != base_bundle->m_filament_id_maps.end()) { + filament_id = filament_id_map_iter->second; + } + } + } + } + else { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << inherits << " for " << preset_name; + // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); + reason = "Can not find inherits: " + inherits; + return reason; + } + } + else { + if (presets_collection->type() == Preset::TYPE_PRINTER) + default_config = &presets_collection->default_preset_for(config_src).config; + else + default_config = &presets_collection->default_preset().config; + } + config = *default_config; + config.apply(config_src); + extend_default_config_length(config, true, *default_config); + if (instantiation == "false" && "Template" != vendor_name) { + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys + << ", which were removed"; + } + + config_maps.emplace(preset_name, std::move(config)); + if ((presets_collection->type() == Preset::TYPE_FILAMENT) && (!filament_id.empty())) + filament_id_maps.emplace(preset_name, filament_id); + return reason; + } + if (config.has("alias")) + alias_name = (dynamic_cast(config.option("alias")))->value; + + if (key_values.find(ORCA_JSON_KEY_RENAMED_FROM) != key_values.end()) { + if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], renamed_from)) { + BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << path << "\": The preset \"" << preset_name + << "\" contains invalid \"renamed_from\" key, which is being ignored."; + } + } + Preset::normalize(config); + } + catch(nlohmann::detail::parse_error &err) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); + reason = std::string("json parse error") + err.what(); + return reason; + } + + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys + << ", which were removed"; + } + + if (presets_collection->type() == Preset::TYPE_PRINTER) { + // Filter out printer presets, which are not mentioned in the vendor profile. + // These presets are considered not installed. + auto printer_model = config.opt_string("printer_model"); + if (printer_model.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines no printer model, it will be ignored."; + reason = std::string("can not find printer_model"); + return reason; + } + auto printer_variant = config.opt_string("printer_variant"); + if (printer_variant.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines no printer variant, it will be ignored."; + reason = std::string("can not find printer_variant"); + return reason; + } + auto it_model = std::find_if(current_vendor_profile->models.cbegin(), current_vendor_profile->models.cend(), + [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } + ); + if (it_model == current_vendor_profile->models.end()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; + reason = std::string("can not find printer model in vendor profile"); + return reason; + } + auto it_variant = it_model->variant(printer_variant); + if (it_variant == nullptr) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; + reason = std::string("can not find printer_variant in vendor profile"); + return reason; + } + } + const Preset *preset_existing = presets_collection->find_preset(preset_name, false); + if (preset_existing != nullptr) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" has already been loaded from another Config Bundle."; + reason = std::string("duplicated defines"); + return reason; + } + + auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / subfile_iter.second).make_preferred(); + if(validation_mode) + file_path = (boost::filesystem::path(data_dir()) / vendor_name / subfile_iter.second).make_preferred(); + + // Load the preset into the list of presets, save it to disk. + Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); + if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { + loaded.is_system = true; + loaded.vendor = current_vendor_profile; + loaded.version = current_vendor_profile->config_version; + loaded.description = description; + loaded.setting_id = setting_id; + loaded.filament_id = filament_id; + loaded.m_from_orca_filament_lib = is_from_lib; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; + if (presets_collection->type() == Preset::TYPE_FILAMENT) { + if (filament_id.empty() && "Template" != vendor_name) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find filament_id for " << preset_name; + //throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); + reason = "Can not find filament_id for " + preset_name; + return reason; + } + else { + filament_id_maps.emplace(preset_name, filament_id); + } + } + } + + // Derive the profile logical name aka alias from the preset name if the alias was not stated explicitely. + if (alias_name.empty()) { + size_t end_pos = preset_name.find_first_of("@"); + if (end_pos != std::string::npos) { + alias_name = preset_name.substr(0, end_pos); + if (renamed_from.empty()) + // Add the preset name with the '@' character removed into the "renamed_from" list. + renamed_from.emplace_back(alias_name + preset_name.substr(end_pos + 1)); + boost::trim_right(alias_name); + } + } + if (alias_name.empty()) + loaded.alias = preset_name; + else { + loaded.alias = std::move(alias_name); + filaments.set_printer_hold_alias(loaded.alias, loaded); + } + loaded.renamed_from = std::move(renamed_from); + if (! substitution_context.empty()) + substitutions.push_back({ + preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); + config_maps.emplace(preset_name, loaded.config); + ++count; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %subfile; + return reason; + }; + + std::map configs; + std::map filament_id_maps; + //3.1) paste the process + presets = &this->prints; + configs.clear(); + filament_id_maps.clear(); + for (auto& subfile : process_subfiles) + { + std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); + if (!reason.empty()) { + ++m_errors; + //parse error + std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse process setting from %1%") % subfile_path; + throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); + } + } + + //3.2) paste the filaments + presets = &this->filaments; + configs.clear(); + filament_id_maps.clear(); + const auto is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; + for (auto& subfile : filament_subfiles) + { + std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, + presets_loaded, is_orca_lib); + if (!reason.empty()) { + ++m_errors; + //parse error + std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse filament setting from %1%") % subfile_path; + throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); + } + } + if (is_orca_lib) { + m_config_maps = configs; + m_filament_id_maps = filament_id_maps; + } + + //3.3) paste the printers + presets = &this->printers; + configs.clear(); + filament_id_maps.clear(); + for (auto& subfile : machine_subfiles) + { + std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); + if (!reason.empty()) { + ++m_errors; + //parse error + std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse printer setting from %1%") % subfile_path; + throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); + } + } + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", finished, presets_loaded %1%")%presets_loaded; + return std::make_pair(std::move(substitutions), presets_loaded); +} + +void PresetBundle::on_extruders_count_changed(int extruders_count) +{ + printers.get_edited_preset().set_num_extruders(extruders_count); + update_multi_material_filament_presets(); + reset_default_nozzle_volume_type(); + extruder_ams_counts.resize(extruders_count); +} + +void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filament_id) +{ + if (printers.get_edited_preset().printer_technology() != ptFFF) + return; + + // Orca: when the number of existing filament presets is less than the number of extruders, we will append new filament presets with the + // same value as the last existing one. + // + // Verify and select the filament presets. + size_t num_filaments = this->filament_presets.size(); + + auto* nozzle_diameter = static_cast(printers.get_edited_preset().config.option("nozzle_diameter")); + size_t num_extruders = nozzle_diameter->values.size(); + if (num_extruders > num_filaments) { // Verify validity of the current filament presets. + for (size_t i = 0; i < std::min(this->filament_presets.size(), num_extruders); ++i) + this->filament_presets[i] = this->filaments.find_preset(this->filament_presets[i], true)->name; + // Append the rest of filament presets. + this->filament_presets.resize(num_extruders, this->filament_presets.empty() ? this->filaments.first_visible().name : + this->filament_presets.back()); + num_filaments = this->filament_presets.size(); + } + if (to_delete_filament_id == -1) + to_delete_filament_id = num_filaments; + + // Now verify if flush_volumes_matrix has proper size (it is used to deduce number of extruders in wipe tower generator): + std::vector old_matrix = this->project_config.option("flush_volumes_matrix")->values; + size_t old_nozzle_nums = this->project_config.option("flush_multiplier")->values.size(); + size_t old_number_of_filaments = size_t(sqrt(old_matrix.size() / old_nozzle_nums) + EPSILON); + size_t nozzle_nums = get_printer_extruder_count(); + if (old_nozzle_nums != nozzle_nums) { + std::vector& f_multiplier = this->project_config.option("flush_multiplier")->values; + f_multiplier.resize(nozzle_nums, 1.f); + } + + if ( (num_filaments * num_filaments) != size_t(old_matrix.size() / old_nozzle_nums) ) { + // First verify if purging volumes presets for each extruder matches number of extruders + std::vector& filaments = this->project_config.option("flush_volumes_vector")->values; + while (filaments.size() < 2* num_filaments) { + filaments.push_back(filaments.size()>1 ? filaments[0] : 140.); // copy the values from the first extruder + filaments.push_back(filaments.size()>1 ? filaments[1] : 140.); + } + while (filaments.size() > 2* num_filaments) { + filaments.pop_back(); + filaments.pop_back(); + } + + size_t old_matrix_size = old_number_of_filaments * old_number_of_filaments; + size_t new_matrix_size = num_filaments * num_filaments; + std::vector new_matrix(new_matrix_size * nozzle_nums, 0); + for (unsigned int i = 0; i < num_filaments; ++i) + for (unsigned int j = 0; j < num_filaments; ++j) { + if (i < old_number_of_filaments && j < old_number_of_filaments) { + unsigned int old_i = i >= to_delete_filament_id ? i + 1 : i; + unsigned int old_j = j >= to_delete_filament_id ? j + 1 : j; + for (size_t nozzle_id = 0; nozzle_id < nozzle_nums; ++nozzle_id) { + // Orca: only copy from old_matrix when the old layout actually has data + // for this nozzle slot; otherwise initialize from the per-filament + // flush volumes the same way the (i,j) out-of-range branch does. + if (nozzle_id < old_nozzle_nums) { + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = old_matrix[old_i * old_number_of_filaments + old_j + old_matrix_size * nozzle_id]; + } else { + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? 0. : filaments[2 * i] + filaments[2 * j + 1]); + } + } + } else { + for (size_t nozzle_id = 0; nozzle_id < nozzle_nums; ++nozzle_id) { + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? 0. : filaments[2 * i] + filaments[2 * j + 1]); + } + } + } + this->project_config.option("flush_volumes_matrix")->values = new_matrix; + } +} + +void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible) +{ + const Preset &printer_preset = this->printers.get_edited_preset(); + const PresetWithVendorProfile printer_preset_with_vendor_profile = this->printers.get_preset_with_vendor_profile(printer_preset); + + class PreferedProfileMatch + { + public: + PreferedProfileMatch(const std::string &prefered_alias, const std::string &prefered_name) : + m_prefered_alias(prefered_alias), m_prefered_name(prefered_name) {} + + int operator()(const Preset &preset) const + { + return + preset.is_default || preset.is_external ? + // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. + 0 : + ! m_prefered_alias.empty() && m_prefered_alias == preset.alias ? + // Matching an alias, always take this preset with priority. + std::numeric_limits::max() : + // Otherwise take the prefered profile, or the first compatible. + preset.name == m_prefered_name; + } + + private: + const std::string m_prefered_alias; + const std::string &m_prefered_name; + }; + + // Matching by the layer height in addition. + class PreferedPrintProfileMatch : public PreferedProfileMatch + { + public: + PreferedPrintProfileMatch(const Preset *preset, const std::string &prefered_name) : + PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name), m_prefered_layer_height(preset ? preset->config.opt_float("layer_height") : 0) {} + + int operator()(const Preset &preset) const + { + // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. + if (preset.is_default || preset.is_external) + return 0; + int match_quality = PreferedProfileMatch::operator()(preset); + if (match_quality < std::numeric_limits::max()) { + match_quality += 1; + if (preset.is_visible) + match_quality += 1; + if (m_prefered_layer_height > 0. && std::abs(preset.config.opt_float("layer_height") - m_prefered_layer_height) < 0.0005) + match_quality *= 10; + } + return match_quality; + } + + private: + const double m_prefered_layer_height; + }; + + // Matching by the layer height in addition. + class PreferedFilamentProfileMatch : public PreferedProfileMatch + { + public: + PreferedFilamentProfileMatch(const Preset *preset, const std::string &prefered_name) : + PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name), + m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string()) {} + + int operator()(const Preset &preset) const + { + // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. + if (preset.is_default || preset.is_external) + return 0; + int match_quality = PreferedProfileMatch::operator()(preset); + if (match_quality < std::numeric_limits::max()) { + match_quality += 1; + if(preset.is_visible) + match_quality += 1; + if (! m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) + match_quality *= 10; + } + return match_quality; + } + + private: + const std::string m_prefered_filament_type; + }; + + // Matching by the layer height in addition. + class PreferedFilamentsProfileMatch + { + public: + PreferedFilamentsProfileMatch(const Preset *preset, const std::vector &prefered_names) : + m_prefered_alias(preset ? preset->alias : std::string()), + m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string("PLA")), // BBS: default choose PLA + m_prefered_names(prefered_names) + {} + + int operator()(const Preset &preset) const + { + // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. + if (preset.is_default || preset.is_external || !preset.is_visible) + return 0; + if (! m_prefered_alias.empty() && m_prefered_alias == preset.alias) + // Matching an alias, always take this preset with priority. + return std::numeric_limits::max(); + int match_quality = (std::find(m_prefered_names.begin(), m_prefered_names.end(), preset.name) != m_prefered_names.end()) + 1; + if (! m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) + match_quality *= 10; + return match_quality; + } + + private: + const std::string m_prefered_alias; + const std::string m_prefered_filament_type; + const std::vector &m_prefered_names; + }; + + BOOST_LOG_TRIVIAL(info) << boost::format("update_compatibility for all presets enter, select_other_print_if_incompatible %1%, select_other_filament_if_incompatible %2%")%(int)select_other_print_if_incompatible %(int)select_other_filament_if_incompatible; + switch (printer_preset.printer_technology()) { + case ptFFF: + { + assert(printer_preset.config.has("default_print_profile")); + assert(printer_preset.config.has("default_filament_profile")); + const std::vector &prefered_filament_profiles = printer_preset.config.option("default_filament_profile")->values; + this->prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_print_if_incompatible, + PreferedPrintProfileMatch(this->prints.get_selected_idx() == size_t(-1) ? nullptr : &this->prints.get_edited_preset(), printer_preset.config.opt_string("default_print_profile"))); + const PresetWithVendorProfile print_preset_with_vendor_profile = this->prints.get_edited_preset_with_vendor_profile(); + // Remember whether the filament profiles were compatible before updating the filament compatibility. + std::vector filament_preset_was_compatible(this->filament_presets.size(), false); + for (size_t idx = 0; idx < this->filament_presets.size(); ++ idx) { + Preset *preset = this->filaments.find_preset(this->filament_presets[idx], false); + filament_preset_was_compatible[idx] = preset != nullptr && preset->is_compatible; + } + // First select a first compatible profile for the preset editor. + BOOST_LOG_TRIVIAL(info) << boost::format("prefered filaments: size %1%, previous selected %2%") %prefered_filament_profiles.size() % this->filaments.get_selected_idx(); + if (this->filaments.get_selected_idx() != size_t(-1)) + { + BOOST_LOG_TRIVIAL(info) << boost::format("previous selected filament: %1%") % this->filaments.get_edited_preset().name; + } + for (size_t idx = 0; idx < prefered_filament_profiles.size(); ++idx) { + BOOST_LOG_TRIVIAL(info) << boost::format("prefered filament: %1%") % prefered_filament_profiles[idx]; + } + this->filaments.update_compatible(printer_preset_with_vendor_profile, &print_preset_with_vendor_profile, select_other_filament_if_incompatible, + PreferedFilamentsProfileMatch(this->filaments.get_selected_idx() == size_t(-1) ? nullptr : &this->filaments.get_edited_preset(), prefered_filament_profiles)); + if (select_other_filament_if_incompatible != PresetSelectCompatibleType::Never) { + // Verify validity of the current filament presets. + const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); + if (this->filament_presets.size() == 1) { + // The compatible profile should have been already selected for the preset editor. Just use it. + if (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || filament_preset_was_compatible.front()) + this->filament_presets.front() = this->filaments.get_edited_preset().name; + } else { + for (size_t idx = 0; idx < this->filament_presets.size(); ++ idx) { + std::string &filament_name = this->filament_presets[idx]; + Preset *preset = this->filaments.find_preset(filament_name, false); + if (preset == nullptr || (! preset->is_compatible && (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || filament_preset_was_compatible[idx]))) + // Pick a compatible profile. If there are prefered_filament_profiles, use them. + filament_name = this->filaments.first_compatible( + PreferedFilamentProfileMatch(preset, + (idx < prefered_filament_profiles.size()) ? prefered_filament_profiles[idx] : prefered_filament_profile)).name; + } + } + } + break; + } + case ptSLA: + { + assert(printer_preset.config.has("default_sla_print_profile")); + assert(printer_preset.config.has("default_sla_material_profile")); + this->sla_prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_print_if_incompatible, + PreferedPrintProfileMatch(this->sla_prints.get_selected_idx() == size_t(-1) ? nullptr : &this->sla_prints.get_edited_preset(), printer_preset.config.opt_string("default_sla_print_profile"))); + const PresetWithVendorProfile sla_print_preset_with_vendor_profile = this->sla_prints.get_edited_preset_with_vendor_profile(); + this->sla_materials.update_compatible(printer_preset_with_vendor_profile, &sla_print_preset_with_vendor_profile, select_other_filament_if_incompatible, + PreferedProfileMatch(this->sla_materials.get_selected_idx() == size_t(-1) ? std::string() : this->sla_materials.get_edited_preset().alias, printer_preset.config.opt_string("default_sla_material_profile"))); + break; + } + default: break; + } + + BOOST_LOG_TRIVIAL(info) << boost::format("update_compatibility for all presets exit"); +} + + +std::vector PresetBundle::export_current_configs(const std::string & path, + std::function override_confirm, + bool include_modify, + bool export_system_settings) +{ + const Preset &print_preset = include_modify ? prints.get_edited_preset() : prints.get_selected_preset(); + const Preset &printer_preset = include_modify ? printers.get_edited_preset() : printers.get_selected_preset(); + std::set presets { &print_preset, &printer_preset }; + for (auto &f : filament_presets) { + auto filament_preset = filaments.find_preset(f, include_modify); + if (filament_preset) presets.insert(filament_preset); + } + + int overwrite = 0; + std::vector result; + for (auto preset : presets) { + if ((preset->is_system && !export_system_settings) || preset->is_default) + continue; + std::string file = path + "/" + preset->name + ".json"; + if (overwrite == 0) overwrite = 1; + if (boost::filesystem::exists(file) && overwrite < 2) { + overwrite = override_confirm(preset->name); + if (overwrite == 0 || overwrite == 2) + continue; + } + preset->config.save_to_json(file, preset->name, "", preset->version.to_string()); + result.push_back(file); + } + return result; +} + +// Set the filament preset name. As the name could come from the UI selection box, +// an optional "(modified)" suffix will be removed from the filament name. +void PresetBundle::set_filament_preset(size_t idx, const std::string &name) +{ + if (idx >= filament_presets.size()) { + BOOST_LOG_TRIVIAL(warning) << boost::format("Warning: set_filament_preset out of range %1% - %2%") % idx % filament_presets.size(); + return; + } + filament_presets[idx] = Preset::remove_suffix_modified(name); +} + +void PresetBundle::set_default_suppressed(bool default_suppressed) +{ + prints.set_default_suppressed(default_suppressed); + filaments.set_default_suppressed(default_suppressed); + sla_prints.set_default_suppressed(default_suppressed); + sla_materials.set_default_suppressed(default_suppressed); + printers.set_default_suppressed(default_suppressed); +} + +bool PresetBundle::has_errors() const +{ + if (m_errors != 0 || printers.m_errors != 0 || filaments.m_errors != 0 || prints.m_errors != 0) + return true; + + bool has_errors = false; + // Orca: check if all filament presets have compatible_printers setting + for (auto& preset : filaments) { + if (!preset.is_system) + continue; + // It's per design that the Orca Filament Library can have the empty compatible_printers. + if(preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY) + continue; + auto* compatible_printers = dynamic_cast(preset.config.option("compatible_printers")); + if (compatible_printers == nullptr || compatible_printers->values.empty()) { + has_errors = true; + BOOST_LOG_TRIVIAL(error) << "Filament preset \"" << preset.file << "\" is missing compatible_printers setting"; + } + } + + return has_errors; +} + +// Orca: BundleMetadata method implementations +bool BundleMetadata::load_from_json(const std::string& path) +{ + try { + boost::nowide::ifstream ifs(path); + if (!ifs.good()) + return false; + + json j; + ifs >> j; + + if (j.contains("id")) this->id = j["id"].get(); + + if (j.contains("name")) this->name = j["name"].get(); + else if (j.contains("bundle_id")) this->name = j["bundle_id"].get(); // backwards compat w bundle_structure.json + + if (j.contains("version")) this->version = j["version"].get(); + + if (j.contains("description")) this->description = j["description"].get(); + else if (j.contains("bundle_type")) this->description = j["bundle_type"].get(); // backwards compat w bundle_structure.json + + if (j.contains("author")) this->author = j["author"].get(); + + if (j.contains("imported_time")) this->imported_time = j["imported_time"].get(); + + if (j.contains("updated_time")) this->updated_time = j["updated_time"].get(); + + if (j.contains("print_presets")) + this->print_presets = j["print_presets"].get>(); + + if (j.contains("filament_presets")) + this->filament_presets = j["filament_presets"].get>(); + + if (j.contains("printer_presets")) + this->printer_presets = j["printer_presets"].get>(); + + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Failed to load bundle metadata from " << path << ": " << e.what(); + return false; + } +} + +bool BundleMetadata::save_to_json(const std::string& path) const +{ + auto strip_prefix = [](const std::vector& names) { + json arr = json::array(); + for (const auto& name : names) + { + arr.push_back(boost::filesystem::path(name).filename().string()); + std::string test = boost::filesystem::path(name).filename().string(); + } + return arr; + }; + try { + json j; + j["id"] = this->id; + j["name"] = this->name; + j["version"] = this->version; + j["description"] = this->description; + j["author"] = this->author; + j["imported_time"] = this->imported_time; + j["updated_time"] = this->updated_time; + + j["print_presets"] = strip_prefix(this->print_presets); + j["filament_presets"] = strip_prefix(this->filament_presets); + j["printer_presets"] = strip_prefix(this->printer_presets); + + boost::nowide::ofstream ofs(path); + ofs << j.dump(4); + return ofs.good(); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Failed to save bundle metadata to " << path << ": " << e.what(); + return false; + } +} +} // namespace Slic3r diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp new file mode 100644 index 0000000000..bb61e0a660 --- /dev/null +++ b/src/slic3r/GUI/Plater.cpp @@ -0,0 +1,18565 @@ +#include "Plater.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r_version.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#endif +#include +#include +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/Format/STL.hpp" +#include "libslic3r/Format/DRC.hpp" +#include "libslic3r/Format/STEP.hpp" +#include "libslic3r/Format/AMF.hpp" +//#include "libslic3r/Format/3mf.hpp" +#include "libslic3r/Format/bbs_3mf.hpp" +#include "libslic3r/GCode/ThumbnailData.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/SLA/Hollowing.hpp" +#include "libslic3r/SLA/SupportPoint.hpp" +#include "libslic3r/SLA/ReprojectPointsOnMesh.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/SLAPrint.hpp" +#include "libslic3r/Utils.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/ObjColorUtils.hpp" +// For stl export +#include "libslic3r/CSGMesh/ModelToCSGMesh.hpp" +#include "libslic3r/CSGMesh/PerformCSGMeshBooleans.hpp" + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GuiColor.hpp" +#include "GUI_ObjectList.hpp" +#ifdef __WXGTK__ +#include "LinuxDisplayBackend.hpp" +#endif +#include "GUI_Utils.hpp" +#include "GUI_Factories.hpp" +#include "wxExtensions.hpp" +#include "../Utils/PrintHost.hpp" +#include "MainFrame.hpp" +#include "format.hpp" +#include "3DScene.hpp" +#include "GLCanvas3D.hpp" +#include "Selection.hpp" +#include "GLToolbar.hpp" +#include "GUI_Preview.hpp" +#include "3DBed.hpp" +#include "PartPlate.hpp" +#include "Camera.hpp" +#include "Mouse3DController.hpp" +#include "Tab.hpp" +#include "Jobs/OrientJob.hpp" +#include "Jobs/ArrangeJob.hpp" +#include "Jobs/FillBedJob.hpp" +#include "Jobs/RotoptimizeJob.hpp" +#include "Jobs/SLAImportJob.hpp" +#include "Jobs/SLAImportDialog.hpp" +#include "Jobs/PrintJob.hpp" +#include "Jobs/NotificationProgressIndicator.hpp" +#include "Jobs/PlaterWorker.hpp" +#include "Jobs/BoostThreadWorker.hpp" +#include "BackgroundSlicingProcess.hpp" +#include "SelectMachine.hpp" +#include "SendMultiMachinePage.hpp" +#include "SendToPrinter.hpp" +#include "PublishDialog.hpp" +#include "ModelMall.hpp" +#include "ConfigWizard.hpp" +#include "SyncAmsInfoDialog.hpp" +#include "../Utils/ASCIIFolding.hpp" +#include "../Utils/UndoRedo.hpp" +#include "../Utils/PresetUpdater.hpp" +#include "../Utils/Process.hpp" +#include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "PresetComboBoxes.hpp" +#include "MsgDialog.hpp" +#include "ProjectDirtyStateManager.hpp" +#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification +#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file +#include "Gizmos/GizmoObjectManipulation.hpp" + +// BBS +#include "Widgets/ProgressDialog.hpp" +#include "BBLStatusBar.hpp" +#include "BitmapCache.hpp" +#include "ParamsDialog.hpp" +#include "ImageDPIFrame.hpp" +#include "Widgets/Label.hpp" +#include "Widgets/RoundedRectangle.hpp" +#include "Widgets/RadioGroup.hpp" +#include "Widgets/CheckBox.hpp" +#include "Widgets/Button.hpp" +#include "Widgets/StaticGroup.hpp" + +#include "GUI_ObjectTable.hpp" +#include "libslic3r/Thread.hpp" + +#ifdef __APPLE__ +#include "Gizmos/GLGizmosManager.hpp" +#endif // __APPLE__ + +#include +#include // Needs to be last because reasons :-/ +#include +#include "WipeTowerDialog.hpp" +#include "ObjColorDialog.hpp" + +#include "libslic3r/CustomGCode.hpp" +#include "libslic3r/Platform.hpp" +#include "nlohmann/json.hpp" + +#include "PhysicalPrinterDialog.hpp" +#include "PrintHostDialogs.hpp" +#include "PlateSettingsDialog.hpp" +#include "DailyTips.hpp" +#include "CreatePresetsDialog.hpp" +#include "FileArchiveDialog.hpp" +#include "../Utils/Http.hpp" +#include "../Utils/OrcaCloudServiceAgent.hpp" +#include "StepMeshDialog.hpp" +#include "FilamentMapDialog.hpp" +#include "CloneDialog.hpp" + +#include "DeviceCore/DevFilaSystem.h" +#include "DeviceCore/DevManager.h" +#include "DeviceCore/DevConfigUtil.h" +#include "DeviceCore/DevDefs.h" + +using boost::optional; +namespace fs = boost::filesystem; +using Slic3r::_3DScene; +using Slic3r::Preset; +using Slic3r::GUI::format_wxstr; +using namespace nlohmann; + +static const std::pair THUMBNAIL_SIZE_3MF = { 512, 512 }; + +namespace Slic3r { +namespace GUI { + +wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); +wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); +wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); +wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, SlicingProcessCompletedEvent); +wxDEFINE_EVENT(EVT_EXPORT_BEGAN, wxCommandEvent); +wxDEFINE_EVENT(EVT_EXPORT_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_IMPORT_MODEL_ID, wxCommandEvent); +wxDEFINE_EVENT(EVT_DOWNLOAD_PROJECT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PUBLISH, wxCommandEvent); +wxDEFINE_EVENT(EVT_OPEN_PLATESETTINGSDIALOG, wxCommandEvent); +wxDEFINE_EVENT(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG, wxCommandEvent); +// BBS: backup & restore +wxDEFINE_EVENT(EVT_RESTORE_PROJECT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PRINT_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_SEND_CALIBRATION_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_SEND_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_PUBLISH_FINISHED, wxCommandEvent); +//BBS: repair model +wxDEFINE_EVENT(EVT_REPAIR_MODEL, wxCommandEvent); +wxDEFINE_EVENT(EVT_FILAMENT_COLOR_CHANGED, wxCommandEvent); +wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_NETWORKING, wxCommandEvent); +wxDEFINE_EVENT(EVT_UPDATE_PLUGINS_WHEN_LAUNCH, wxCommandEvent); +wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_HINT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PREVIEW_ONLY_MODE_HINT, wxCommandEvent); +//BBS: change light/dark mode +wxDEFINE_EVENT(EVT_GLCANVAS_COLOR_MODE_CHANGED, SimpleEvent); +//BBS: print +wxDEFINE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); + +wxDEFINE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); +wxDEFINE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); +wxDEFINE_EVENT(EVT_ADD_FILAMENT, SimpleEvent); +wxDEFINE_EVENT(EVT_DEL_FILAMENT, SimpleEvent); +wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); +wxDEFINE_EVENT(EVT_NOTICE_CHILDE_SIZE_CHANGED, SimpleEvent); +wxDEFINE_EVENT(EVT_NOTICE_FULL_SCREEN_CHANGED, IntEvent); +#define PRINTER_THUMBNAIL_SIZE (wxSize(40, 40)) // ORCA +#define PRINTER_PANEL_SIZE ( wxSize(70, 60)) // ORCA +#define PRINTER_PANEL_RADIUS (6) // ORCA +#define BTN_SYNC_SIZE (wxSize(FromDIP(96), FromDIP(98))) + +static string get_diameter_string(float diameter) +{ + std::ostringstream stream; // ORCA ensure 0.25 returned as 0.25. previous code returned as 0.2 because of std::setprecision(1) + stream << std::fixed << std::setprecision(2) << diameter; // Use 2 decimals to capture 0.25 / 0.15 reliably + std::string s = stream.str(); + if (s.find('.') != std::string::npos) { // Remove trailing zeros, but keep at least one decimal if needed + s.erase(s.find_last_not_of('0') + 1); + if (s.back() == '.') s += '0'; // Ensure "1." → "1.0" + } + return s; +} + +bool Plater::has_illegal_filename_characters(const wxString& wxs_name) +{ + std::string name = into_u8(wxs_name); + return has_illegal_filename_characters(name); +} + +bool Plater::has_illegal_filename_characters(const std::string& name) +{ + const char* illegal_characters = "<>:/\\|?*\""; + for (size_t i = 0; i < std::strlen(illegal_characters); i++) + if (name.find_first_of(illegal_characters[i]) != std::string::npos) + return true; + + return false; +} + +void Plater::show_illegal_characters_warning(wxWindow* parent) +{ + show_error(parent, _L("Invalid name, the following characters are not allowed:") + " <>:/\\|?*\""); +} + +static std::map bed_type_thumbnails = { + {BedType::btPC, "bed_cool" }, + {BedType::btEP, "bed_engineering" }, + {BedType::btPEI, "bed_high_templ" }, + {BedType::btPTE, "bed_pei" }, + {BedType::btPCT, "bed_pei_cool" }, + {BedType::btSuperTack, "bed_cool_supertack" } +}; + +enum SlicedInfoIdx +{ + siFilament_m, + siFilament_mm3, + siFilament_g, + siMateril_unit, + siCost, + siEstimatedTime, + siWTNumbetOfToolchanges, + siCount +}; + +enum class LoadFilesType { + NoFile, + Single3MF, + SingleOther, + Multiple3MF, + MultipleOther, + Multiple3MFOther, +}; + +enum class LoadType : unsigned char +{ + Unknown, + OpenProject, + LoadGeometry, + LoadConfig +}; + +class SlicedInfo : public wxStaticBoxSizer +{ +public: + SlicedInfo(wxWindow *parent); + void SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label=""); + +private: + std::vector> info_vec; +}; + +SlicedInfo::SlicedInfo(wxWindow *parent) : + wxStaticBoxSizer(new wxStaticBox(parent, wxID_ANY, _L("Sliced Info")), wxVERTICAL) +{ + GetStaticBox()->SetFont(wxGetApp().bold_font()); + wxGetApp().UpdateDarkUI(GetStaticBox()); + + auto *grid_sizer = new wxFlexGridSizer(2, 5, 15); + grid_sizer->SetFlexibleDirection(wxVERTICAL); + + info_vec.reserve(siCount); + + auto init_info_label = [this, parent, grid_sizer](wxString text_label) { + auto *text = new wxStaticText(parent, wxID_ANY, text_label); + text->SetForegroundColour(*wxBLACK); + text->SetFont(wxGetApp().small_font()); + auto info_label = new wxStaticText(parent, wxID_ANY, "N/A"); + info_label->SetForegroundColour(*wxBLACK); + info_label->SetFont(wxGetApp().small_font()); + grid_sizer->Add(text, 0); + grid_sizer->Add(info_label, 0); + info_vec.push_back(std::pair(text, info_label)); + }; + + init_info_label(_L("Used Filament (m)")); + init_info_label(_L("Used Filament (mm³)")); + init_info_label(_L("Used Filament (g)")); + init_info_label(_L("Used Materials")); + init_info_label(_L("Cost")); + init_info_label(_L("Estimated time")); + init_info_label(_L("Filament changes")); + + Add(grid_sizer, 0, wxEXPAND); + this->Show(false); +} + +void SlicedInfo::SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label/*=""*/) +{ + const bool show = text != "N/A"; + if (show) + info_vec[idx].second->SetLabelText(text); + if (!new_label.IsEmpty()) + info_vec[idx].first->SetLabelText(new_label); + info_vec[idx].first->Show(show); + info_vec[idx].second->Show(show); +} + +static wxString temp_dir; + +namespace { + +#ifdef __WXGTK__ +wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* removed_floating_state = nullptr) +{ + if (!Slic3r::GUI::is_running_on_wayland() || layout.empty()) { + if (removed_floating_state != nullptr) + *removed_floating_state = false; + return layout; + } + + static const std::regex state_pattern(R"(state=(\d+);)"); + constexpr unsigned int disabled_wayland_flags = + static_cast(wxAuiPaneInfo::optionFloating) | + static_cast(wxAuiPaneInfo::optionFloatable); + + const std::string input = layout.utf8_string(); + std::string output; + output.reserve(input.size()); + + bool modified = false; + std::smatch match; + auto search_start = input.cbegin(); + + while (std::regex_search(search_start, input.cend(), match, state_pattern)) { + output.append(search_start, match[0].first); + + try { + const unsigned long state = std::stoul(match[1].str()); + const unsigned long sanitized_state = state & ~static_cast(disabled_wayland_flags); + modified = modified || sanitized_state != state; + + output += "state=" + std::to_string(sanitized_state) + ";"; + } catch (const std::exception&) { + output += match[0].str(); + } + + search_start = match[0].second; + } + + output.append(search_start, input.cend()); + + if (removed_floating_state != nullptr) + *removed_floating_state = modified; + + return modified ? wxString::FromUTF8(output) : layout; +} +#endif + +} // namespace + +// Sidebar / private + +enum class ActionButtonType : int { + abReslice, + abExport, + abSendGCode +}; + +struct ExtruderGroup : StaticGroup +{ + ExtruderGroup(wxWindow * parent, int index, wxString const &title); + wxStaticBoxSizer *sizer = nullptr; + ScalableButton * btn_edit = nullptr; + ComboBox * combo_diameter = nullptr; + ComboBox * combo_flow = nullptr; + AMSPreview * ams[4] = {nullptr}; + wxStaticText *ams_not_installed_msg{nullptr}; + ScalableButton * btn_up{nullptr}; + ScalableButton * btn_down{nullptr}; + wxBoxSizer *hsizer_ams { nullptr }; + size_t page_cur{0}; + size_t page_num{3}; + size_t ams_n4 = 0; + size_t ams_n1 = 0; + std::vector ams_4; + std::vector ams_1; + wxString diameter; + + void set_ams_count(int n4, int n1) + { + if (n4 == ams_n4 && n1 == ams_n1) + return; + ams_n4 = n4; + ams_n1 = n1; + if (btn_edit) { + update_ams(); + } + } + + void update_ams(); + void SetTitle(const wxString& title); + + void sync_ams(MachineObject const *obj, std::vector const &ams4, std::vector const &ams1); + + void Rescale() + { + if (btn_edit) + btn_edit->msw_rescale(); + btn_up->msw_rescale(); + btn_down->msw_rescale(); + combo_diameter->Rescale(); + combo_flow->Rescale(); + for (int i = 0; i < 4; ++i) + ams[i]->msw_rescale(); + } +}; + +struct Sidebar::priv +{ + Plater *plater; + + wxPanel *scrolled = nullptr; + PlaterPresetComboBox *combo_sla_print = nullptr; + PlaterPresetComboBox *combo_sla_material = nullptr; + + // Printer + wxSizer * vsizer_printer = nullptr; + wxBoxSizer * extruder_dual_sizer = nullptr; + wxBoxSizer * extruder_single_sizer = nullptr; + // Printer - preset + StaticBox * panel_printer_preset = nullptr; + wxStaticBitmap * image_printer = nullptr; + PlaterPresetComboBox *combo_printer = nullptr; + ScalableButton * btn_edit_printer = nullptr; + ScalableButton * btn_connect_printer = nullptr; + + // Nozzle diameter + StaticBox * panel_nozzle_dia = nullptr; + Label * label_nozzle_title= nullptr; + ComboBox * combo_nozzle_dia = nullptr; + Label * label_nozzle_type = nullptr; + + // Printer - bed + StaticBox * panel_printer_bed = nullptr; + wxStaticBitmap *image_printer_bed = nullptr; + ComboBox * combo_printer_bed = nullptr; + + ImageDPIFrame *big_bed_image_popup = nullptr; + // Printer - sync + //Button *btn_sync_printer; + std::shared_ptr counter_sync_printer = std::make_shared(); + wxTimer * timer_sync_printer = new wxTimer(); + // Printer - ams + ExtruderGroup *left_extruder = nullptr; + ExtruderGroup *right_extruder = nullptr; + ExtruderGroup *single_extruder = nullptr; + + int FromDIP(int n) { return plater->FromDIP(n); } + void layout_printer(bool isBBL, bool isDual); + + void flush_printer_sync(bool restart = false); + + PlaterPresetComboBox *combo_print = nullptr; + std::vector combos_filament; + int editing_filament = -1; + wxBoxSizer *sizer_filaments = nullptr; + + //BBS Sidebar widgets + wxPanel* m_panel_print_title; + wxStaticText* m_staticText_print_title; + wxPanel* m_panel_print_content; + wxBoxSizer *sizer_params; + + //wxComboBox * m_comboBox_print_preset; + wxStaticLine * m_staticline1; + StaticBox* m_panel_filament_title; + wxStaticText* m_staticText_filament_settings; + ScalableButton * m_bpButton_add_filament; + ScalableButton * m_bpButton_del_filament; + ScalableButton * m_bpButton_ams_filament; + ScalableButton * m_bpButton_set_filament; + int m_menu_filament_id = -1; + wxScrolledWindow* m_panel_filament_content; + wxScrolledWindow* m_scrolledWindow_filament_content; + wxStaticLine* m_staticline2; + wxPanel* m_panel_project_title; + ScalableButton* m_filament_icon = nullptr; + Button * m_flushing_volume_btn = nullptr; + TextInput* m_search_item = nullptr; + StaticBox* m_search_bar = nullptr; + Search::SearchObjectDialog* dia = nullptr; + + // BBS printer config + StaticBox* m_panel_printer_title = nullptr; + ScalableButton* m_printer_icon = nullptr; + ScalableButton* m_printer_connect = nullptr; + ScalableButton* m_printer_bbl_sync = nullptr; + ScalableButton* m_printer_setting = nullptr; + wxStaticText * m_text_printer_settings = nullptr; + wxPanel* m_panel_printer_content = nullptr; + + ObjectList *m_object_list{ nullptr }; + ObjectSettings *object_settings{ nullptr }; + ObjectLayers *object_layers{ nullptr }; + + wxButton *btn_export_gcode; + wxButton *btn_reslice; + ScalableButton *btn_send_gcode; + //ScalableButton *btn_eject_device; + ScalableButton* btn_export_gcode_removable; //exports to removable drives (appears only if removable drive is connected) + + bool is_switching_diameter{false}; + Search::OptionsSearcher searcher; + std::string ams_list_device; + + priv(Plater *plater) : plater(plater) {} + ~priv(); + + void show_preset_comboboxes(); + void jump_to_object(ObjectDataViewModelNode* item); + void can_search(); + + bool sync_extruder_list(bool &only_external_material); + bool switch_diameter(bool single); + void update_sync_status(const MachineObject* obj); + +#ifdef _WIN32 + wxString btn_reslice_tip; + void show_rich_tip(const wxString& tooltip, wxButton* btn); + void hide_rich_tip(wxButton* btn); +#endif +}; + +void Sidebar::priv::layout_printer(bool isBBL, bool isDual) +{ + // Printer - preset + if (auto sizer = static_cast(panel_printer_preset->GetSizer()); + sizer == nullptr /*|| isBBL != (sizer->GetOrientation() == wxVERTICAL)*/) { + + //if (isBBL) { + wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); + hsizer->Add(image_printer, 0, wxLEFT | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + hsizer->Add(combo_printer, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(2)); // 1 already triggers wxEXPAND + hsizer->AddSpacer(FromDIP(2)); + hsizer->Add(btn_edit_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); + //hsizer->Add(btn_connect_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); + panel_printer_preset->SetSizer(hsizer); + //} else { + // wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); + // hsizer->Add(image_printer, 0, wxLEFT | wxALIGN_CENTER, FromDIP(4)); + // hsizer->Add(combo_printer, 1, wxALIGN_CENTRE | wxLEFT | wxRIGHT, FromDIP(6)); + // hsizer->Add(hsizer_printer_btn, 0, wxALIGN_TOP | wxTOP | wxRIGHT, FromDIP(4)); + // hsizer->AddSpacer(FromDIP(10)); + // panel_printer_preset->SetSizer(hsizer); + //} + } + + if (vsizer_printer->GetItemCount() == 0) { + wxBoxSizer *hsizer_printer = new wxBoxSizer(wxHORIZONTAL); + hsizer_printer->Add(panel_printer_preset, 1, wxEXPAND, 0); + hsizer_printer->Add(panel_nozzle_dia , 0, wxLEFT, FromDIP(4)); + hsizer_printer->Add(panel_printer_bed, 0, wxLEFT, FromDIP(4)); + //hsizer_printer->Add(btn_sync_printer , 0, wxLEFT, FromDIP(4)); + vsizer_printer->AddSpacer(FromDIP(SidebarProps::ContentMarginV())); + vsizer_printer->Add(hsizer_printer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); + + // Printer - extruder + + // double + extruder_dual_sizer = new wxBoxSizer(wxHORIZONTAL); + extruder_dual_sizer->Add(left_extruder->sizer, 1, wxEXPAND, 0); + extruder_dual_sizer->AddSpacer(FromDIP(4)); + extruder_dual_sizer->Add(right_extruder->sizer, 1, wxEXPAND, 0); + + // single + extruder_single_sizer = single_extruder->sizer; + wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL); + extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); + extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); + + vsizer_printer->Add(extruder_sizer, 1, wxEXPAND | wxTOP, FromDIP(2)); + + vsizer_printer->AddSpacer(FromDIP(SidebarProps::ContentMarginV())); + } + + //btn_connect_printer->Show(!isBBL); + m_printer_connect->Show(!isBBL); + //btn_sync_printer->Show(isBBL); + m_printer_bbl_sync->Show(isBBL); + + // ORCA show plate type combo box only when its supported + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + auto cfg = preset_bundle.printers.get_edited_preset().config; + // Orca: we use preset_bundle.is_bbl_vendor() instead of isBBL to determine if the plate type combo box should be shown + // ref: https://github.com/OrcaSlicer/OrcaSlicer/pull/11610#discussion_r2607411847 + panel_printer_bed->Show(preset_bundle.is_bbl_vendor() || cfg.opt_bool("support_multi_bed_types")); + + extruder_dual_sizer->Show(isDual); + + // NEEDFIX requires AMS check or any type of ??? + // Single nozzle & non ams + panel_nozzle_dia->Show(!isDual && preset_bundle.get_printer_extruder_count() < 2); + extruder_single_sizer->Show(false); +} + +void Sidebar::priv::flush_printer_sync(bool restart) +{ + if (restart) { + *counter_sync_printer = 6; + timer_sync_printer->Start(500); + } + //btn_sync_printer->SetBackgroundColorNormal((*counter_sync_printer & 1) ? "#F8F8F8" :"#009688"); + m_printer_bbl_sync->SetBitmap_((*counter_sync_printer & 1) ? "printer_sync_not" : "printer_sync_ok"); + if (--*counter_sync_printer <= 0) + timer_sync_printer->Stop(); +} + +Sidebar::priv::~priv() +{ + // BBS + //delete object_manipulation; + delete object_settings; + // BBS +#if 0 + delete frequently_changed_parameters; +#endif +} + +void Sidebar::priv::show_preset_comboboxes() +{ + const bool showSLA = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA; + +//BBS +#if 0 + for (size_t i = 0; i < 4; ++i) + sizer_presets->Show(i, !showSLA); + + for (size_t i = 4; i < 8; ++i) { + if (sizer_presets->IsShown(i) != showSLA) + sizer_presets->Show(i, showSLA); + } + + frequently_changed_parameters->Show(!showSLA); +#endif + + scrolled->GetParent()->Layout(); + scrolled->Refresh(); +} + +void Sidebar::priv::jump_to_object(ObjectDataViewModelNode* item) +{ + m_object_list->selected_object(item); +} + +void Sidebar::priv::can_search() +{ + if (m_search_bar->IsShown()) { + m_search_item->SetFocus(); + } +} + +#ifdef _WIN32 +using wxRichToolTipPopup = wxCustomBackgroundWindow; +static wxRichToolTipPopup* get_rtt_popup(wxButton* btn) +{ + auto children = btn->GetChildren(); + for (auto child : children) + if (child->IsShown()) + return dynamic_cast(child); + return nullptr; +} + +void Sidebar::priv::show_rich_tip(const wxString& tooltip, wxButton* btn) +{ + if (tooltip.IsEmpty()) + return; + wxRichToolTip tip(tooltip, ""); + tip.SetIcon(wxICON_NONE); + tip.SetTipKind(wxTipKind_BottomRight); + tip.SetTitleFont(wxGetApp().normal_font()); + tip.SetBackgroundColour(wxGetApp().get_window_default_clr()); + + tip.ShowFor(btn); + // Every call of the ShowFor() creates new RichToolTip and show it. + // Every one else are hidden. + // So, set a text color just for the shown rich tooltip + if (wxRichToolTipPopup* popup = get_rtt_popup(btn)) { + auto children = popup->GetChildren(); + for (auto child : children) { + child->SetForegroundColour(wxGetApp().get_label_clr_default()); + // we neen just first text line for out rich tooltip + return; + } + } +} + +void Sidebar::priv::hide_rich_tip(wxButton* btn) +{ + if (wxRichToolTipPopup* popup = get_rtt_popup(btn)) + popup->Dismiss(); +} +#endif + +std::vector get_min_flush_volumes(const DynamicPrintConfig &full_config, size_t nozzle_id) +{ + std::vectorextra_flush_volumes; + //const auto& full_config = wxGetApp().preset_bundle->full_config(); + //auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + + const ConfigOptionFloatsNullable* nozzle_volume_opt = full_config.option("nozzle_volume"); + int nozzle_volume_val = nozzle_volume_opt ? (int)nozzle_volume_opt->get_at(nozzle_id) : 0; + + const ConfigOptionInt* enable_long_retraction_when_cut_opt = full_config.option("enable_long_retraction_when_cut"); + int machine_enabled_level = 0; + if (enable_long_retraction_when_cut_opt) { + machine_enabled_level = enable_long_retraction_when_cut_opt->value; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get enable_long_retraction_when_cut from config, value=%1%")%machine_enabled_level; + } + const ConfigOptionBools* long_retractions_when_cut_opt = full_config.option("long_retractions_when_cut"); + bool machine_activated = false; + if (long_retractions_when_cut_opt) { + machine_activated = long_retractions_when_cut_opt->values[nozzle_id] == 1; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get long_retractions_when_cut from config, value=%1%, activated=%2%")%long_retractions_when_cut_opt->values[0] %machine_activated; + } + + size_t filament_size = full_config.option("filament_diameter")->values.size(); + std::vector filament_retraction_distance_when_cut(filament_size, 18.0f), printer_retraction_distance_when_cut(filament_size, 18.0f); + std::vector filament_long_retractions_when_cut(filament_size, 0); + const ConfigOptionFloats* filament_retraction_distances_when_cut_opt = full_config.option("filament_retraction_distances_when_cut"); + if (filament_retraction_distances_when_cut_opt) { + filament_retraction_distance_when_cut = filament_retraction_distances_when_cut_opt->values; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get filament_retraction_distance_when_cut from config, size=%1%, values=%2%")%filament_retraction_distance_when_cut.size() %filament_retraction_distances_when_cut_opt->serialize(); + } + + const ConfigOptionFloats* printer_retraction_distance_when_cut_opt = full_config.option("retraction_distances_when_cut"); + if (printer_retraction_distance_when_cut_opt) { + printer_retraction_distance_when_cut = printer_retraction_distance_when_cut_opt->values; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get retraction_distances_when_cut from config, size=%1%, values=%2%")%printer_retraction_distance_when_cut.size() %printer_retraction_distance_when_cut_opt->serialize(); + } + + const ConfigOptionBools* filament_long_retractions_when_cut_opt = full_config.option("filament_long_retractions_when_cut"); + if (filament_long_retractions_when_cut_opt) { + filament_long_retractions_when_cut = filament_long_retractions_when_cut_opt->values; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get filament_long_retractions_when_cut from config, size=%1%, values=%2%")%filament_long_retractions_when_cut.size() %filament_long_retractions_when_cut_opt->serialize(); + } + + for (size_t idx = 0; idx < filament_size; ++idx) { + int extra_flush_volume = nozzle_volume_val; + int retract_length = machine_enabled_level && machine_activated ? printer_retraction_distance_when_cut[nozzle_id] : 0; + + unsigned char filament_activated = filament_long_retractions_when_cut[idx]; + double filament_retract_length = filament_retraction_distance_when_cut[idx]; + + if (filament_activated == 0) + retract_length = 0; + else if (filament_activated == 1 && machine_enabled_level == LongRectrationLevel::EnableFilament) { + if (!std::isnan(filament_retract_length)) + retract_length = (int)filament_retraction_distance_when_cut[idx]; + else + retract_length = printer_retraction_distance_when_cut[nozzle_id]; + } + + extra_flush_volume -= PI * 1.75 * 1.75 / 4 * retract_length; + extra_flush_volumes.emplace_back(extra_flush_volume); + } + return extra_flush_volumes; +} + +// Sidebar / public + +struct DynamicFilamentList : DynamicList +{ + std::vector> items; + + void apply_on(Choice *c) override + { + if (items.empty()) + update(true); + auto cb = dynamic_cast(c->window); + wxString old_selection = cb->GetStringSelection(); + int old_index = cb->GetSelection(); + cb->Clear(); + cb->Append(_L("Default")); + for (auto i : items) { + cb->Append(i.first, i.second ? *i.second : wxNullBitmap); + } + + if (old_index >= 0 && (unsigned int) old_index < cb->GetCount()) { + cb->SetSelection(old_index); + return; + } + + int new_index = cb->FindString(old_selection); + if (old_index == cb->GetCount()) { + cb->SetSelection(old_index - 1); + } else if (new_index != wxNOT_FOUND) { + cb->SetSelection(new_index); + } else { + cb->SetSelection(0); + } + } + wxString get_value(int index) override + { + wxString str; + str << index; + return str; + } + int index_of(wxString value) override + { + long n = 0; + return (value.ToLong(&n) && n <= items.size()) ? int(n) : -1; + } + void update(bool force = false) + { + items.clear(); + if (!force && m_choices.empty()) + return; + auto icons = get_extruder_color_icons(true); + auto presets = wxGetApp().preset_bundle->filament_presets; + for (int i = 0; i < presets.size(); ++i) { + wxString str; + std::string type; + wxGetApp().preset_bundle->filaments.find_preset(presets[i])->get_filament_type(type); + str << type; + items.push_back({str, i < icons.size() ? icons[i] : nullptr}); + } + DynamicList::update(); + } +}; + +struct DynamicFilamentList1Based : DynamicFilamentList +{ + void apply_on(Choice *c) override + { + if (items.empty()) + update(true); + auto cb = dynamic_cast(c->window); + auto n = cb->GetSelection(); + cb->Clear(); + for (auto i : items) { + cb->Append(i.first, *i.second); + } + if (n < cb->GetCount()) + cb->SetSelection(n); + } + wxString get_value(int index) override + { + wxString str; + str << index+1; + return str; + } + int index_of(wxString value) override + { + long n = 0; + if(!value.ToLong(&n)) + return -1; + --n; + return (n >= 0 && n <= items.size()) ? int(n) : -1; + } + void update(bool force = false) + { + items.clear(); + if (!force && m_choices.empty()) + return; + auto icons = get_extruder_color_icons(true); + auto presets = wxGetApp().preset_bundle->filament_presets; + for (int i = 0; i < presets.size(); ++i) { + wxString str; + std::string type; + wxGetApp().preset_bundle->filaments.find_preset(presets[i])->get_filament_type(type); + str << type; + items.push_back({str, i < icons.size() ? icons[i] : nullptr}); + } + DynamicList::update(); + } + +}; + +// Check if the machine supports Junction Deviation (Marlin firmware with machine_max_junction_deviation > 0) +static bool has_junction_deviation(const DynamicPrintConfig* printer_config) +{ + if (!printer_config) { + return false; + } + const auto gcode_flavor = printer_config->option>("gcode_flavor"); + const auto junction_dev = printer_config->option("machine_max_junction_deviation"); + return gcode_flavor && + gcode_flavor->value == GCodeFlavor::gcfMarlinFirmware && + junction_dev && + !junction_dev->values.empty() && + junction_dev->values.front() > 0.0; +} + +static DynamicFilamentList dynamic_filament_list; +static DynamicFilamentList1Based dynamic_filament_list_1_based; + +class AMSCountPopupWindow : public PopupWindow +{ +public: + AMSCountPopupWindow(ExtruderGroup *extruder, int index) + : PopupWindow(extruder, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) + { + SetBackgroundColour(*wxWHITE); + auto msg = new wxStaticText(this, wxID_ANY, _L("Set the number of AMS installed on the nozzle.")); + msg->SetFont(Label::Body_14); + msg->SetForegroundColour("#262E30"); + msg->Wrap(FromDIP(280)); + auto box = new StaticBox(this, wxID_ANY); + box->SetBackgroundColor(0xF8F8F8); + box->SetBorderWidth(0); + auto img4 = new ScalableButton(box, wxID_ANY, "ams_4_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 44); + //img4->SetBackgroundColour(*wxWHITE); + auto img1 = new ScalableButton(box, wxID_ANY, "ams_1_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 44); + //img1->SetBackgroundColour(*wxWHITE); + auto txt4 = new wxStaticText(box, wxID_ANY, _L("AMS(4 slots)")); + txt4->SetFont(Label::Body_14); + txt4->SetBackgroundColour(0xF8F8F8); + txt4->SetForegroundColour("#262E30"); + auto txt1 = new wxStaticText(box, wxID_ANY, _L("AMS(1 slot)")); + txt1->SetFont(Label::Body_14); + txt1->SetBackgroundColour(0xF8F8F8); + txt1->SetForegroundColour("#262E30"); + int ams4 = 0, ams1 = 0; + int oth4 = 0, oth1 = 0; + GetAMSCount(index, ams4, ams1); + GetAMSCount(1 - index, oth4, oth1); + auto val4 = new SpinInput(box, {}, {}, wxDefaultPosition, {FromDIP(60), -1}, 0, 0, 4 - oth4, ams4); + auto val1 = new SpinInput(box, {}, {}, wxDefaultPosition, {FromDIP(60), -1}, 0, 0, 8 - oth1, ams1); + auto event_handler = [index, val4, val1, extruder](auto &evt) { + SetAMSCount(index, val4->GetValue(), val1->GetValue()); + UpdateAMSCount(index, extruder); + }; + val4->Bind(wxEVT_SPINCTRL, event_handler); + val1->Bind(wxEVT_SPINCTRL, event_handler); + + wxSizer * sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(msg, 0, wxTOP | wxLEFT | wxRIGHT, FromDIP(10)); + wxSizer *sizer2 = new wxBoxSizer(wxVERTICAL); + wxSizer *sizer21 = new wxBoxSizer(wxHORIZONTAL); + sizer21->Add(img4, 0, wxALIGN_CENTRE); + sizer21->Add(txt4, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer21->Add(val4, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer2->Add(sizer21, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, FromDIP(14)); + sizer2->AddSpacer(FromDIP(6)); + wxSizer *sizer22 = new wxBoxSizer(wxHORIZONTAL); + sizer22->Add(img1, 0, wxALIGN_CENTRE); + sizer22->Add(txt1, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer22->Add(val1, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer2->Add(sizer22, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, FromDIP(14)); + box->SetSizer(sizer2); + sizer->Add(box, 0, wxTOP | wxBOTTOM | wxLEFT | wxRIGHT | wxEXPAND, FromDIP(14)); + SetSizer(sizer); + + Layout(); + Fit(); + + Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) { + wxPaintDC dc(this); + dc.SetPen(StateColor::darkModeColorFor(wxColour("#DBDBDB"))); // ORCA match popup border color + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0); + }); + + SetBackgroundColour(*wxWHITE); + wxGetApp().UpdateDarkUIWin(this); + } + + static void SetAMSCount(int index, int ams4, int ams1) + { + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + preset_bundle.extruder_ams_counts.resize(2); + auto &ams_map = preset_bundle.extruder_ams_counts[index]; + ams_map[4] = ams4; + ams_map[1] = ams1; + + std::vector extruder_ams_count = save_extruder_ams_count_to_string(preset_bundle.extruder_ams_counts); + std::string extruder_ams_count_str = boost::algorithm::join(extruder_ams_count, ","); + wxGetApp().app_config->set("presets", "extruder_ams_count", extruder_ams_count_str); + wxGetApp().plater()->update(); // update slice status + } + + static void GetAMSCount(int index, int & ams4, int & ams1) + { + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + if (preset_bundle.extruder_ams_counts.empty()) { + ams4 = 0; + ams1 = 0; + } + else { + assert(preset_bundle.extruder_ams_counts.size() == 2); + ams4 = preset_bundle.extruder_ams_counts[index][4]; + ams1 = preset_bundle.extruder_ams_counts[index][1]; + } + } + + static void UpdateAMSCount(int index, ExtruderGroup *extruder) + { + std::vector> &ams_counts = wxGetApp().preset_bundle->extruder_ams_counts; + ams_counts.resize(2); + std::map& ams_map = ams_counts[index]; + if (ams_map.find(4) == ams_map.end()) { + ams_map[4] = 0; + } + if (ams_map.find(1) == ams_map.end()) { + ams_map[1] = 0; + } + + extruder->set_ams_count(ams_map[4], ams_map[1]); + } +}; + +ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title) + : StaticGroup(parent, wxID_ANY, title) +{ + SetFont(Label::Body_10); + SetForegroundColour(wxColour("#CECECE")); + SetBorderColor(wxColour("#EEEEEE")); + SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); // ORCA match radius with other boxes + ShowBadge(true); + // Nozzle + wxStaticText *label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter")); + label_diameter->SetFont(Label::Body_14); + label_diameter->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + if (index >= 0) label_diameter->SetMinSize({FromDIP(80), -1}); + auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + this->combo_diameter = combo_diameter; + wxStaticText *label_flow = new wxStaticText(this, wxID_ANY, _L("Flow")); + label_flow->SetFont(Label::Body_14); + label_flow->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + if (index >= 0) label_flow->SetMinSize({FromDIP(80), -1}); + auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + combo_flow->GetDropDown().SetUseContentWidth(true); + combo_flow->Bind(wxEVT_COMBOBOX, [this, index, combo_flow](wxCommandEvent &evt) { + auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + printer_tab->set_extruder_volume_type(index, NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt())))); + if (GUI::wxGetApp().plater()) + GUI::wxGetApp().plater()->update_machine_sync_status(); + }); + this->combo_flow = combo_flow; + + // AMS + wxStaticText *label_ams = new wxStaticText(this, wxID_ANY, _L("AMS")); + label_ams->SetFont(Label::Body_14); + label_ams->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + //label_ams->SetMinSize({FromDIP(70), -1}); + if (index >= 0) { + btn_edit = new ScalableButton(this, wxID_ANY, "dot"); +#ifdef __WXOSX__ + btn_edit->SetBackgroundColour("#F7F7F7"); +#else + btn_edit->SetBackgroundColour(*wxWHITE); +#endif + btn_edit->Hide(); + btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { + PopupWindow *window = new AMSCountPopupWindow(this, index); + auto size = GetSize(); + auto pos = ClientToScreen({0, size.y + 12}); + size.SetWidth(size.GetWidth() + FromDIP(10)); + window->Position(pos, {0, 0}); + window->Popup(); + }); + + auto hovered = std::make_shared(); + for (wxWindow *w : std::initializer_list{this, label_diameter, combo_diameter, label_flow, combo_flow, btn_edit, label_ams}) { + w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) { *hovered = w; btn_edit->SetBitmap_("edit"); }); + w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) { if (*hovered == w) { btn_edit->SetBitmap_("dot"); *hovered = nullptr; } }); + } + } + + // AMS not installed message + ams_not_installed_msg = new wxStaticText(this, wxID_ANY, _L("Not installed")); + ams_not_installed_msg->SetFont(Label::Body_14); + ams_not_installed_msg->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + + // AMS group + for (size_t i = 0; i < 4; ++i) { + ams[i] = new AMSPreview(this, wxID_ANY, AMSinfo(), AMSModel::GENERIC_AMS); + ams[i]->Close(); + } + + hsizer_ams = new wxBoxSizer(wxHORIZONTAL); + hsizer_ams->SetMinSize(0, ams[0]->GetMinHeight()); + hsizer_ams->Add(label_ams, 0, wxALIGN_CENTER); + if (btn_edit) + hsizer_ams->Add(btn_edit, 0, wxLEFT | wxALIGN_CENTER, FromDIP(2)); + hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER); + + btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); + btn_up->SetBackgroundColour(*wxWHITE); + btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { + if (page_cur > 0) + --page_cur; + update_ams(); + }); + btn_up->Hide(); + btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); + btn_down->SetBackgroundColour(*wxWHITE); + btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { + if (page_cur + 1 < page_num) + ++page_cur; + update_ams(); + }); + btn_down->Hide(); + + wxBoxSizer *hsizer_diameter = new wxBoxSizer(wxHORIZONTAL); + hsizer_diameter->Add(label_diameter, 0, wxALIGN_CENTER); + hsizer_diameter->Add(combo_diameter, 1, wxEXPAND); + wxBoxSizer * hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL); + hsizer_nozzle->Add(label_flow, 0, wxALIGN_CENTER); + hsizer_nozzle->Add(combo_flow, 1, wxEXPAND); + label_flow->Hide(); // TODO: Orca hack, hide flow selection + combo_flow->Hide(); + if (index < 0) { + label_ams->Hide(); + ams_not_installed_msg->Hide(); + wxStaticBoxSizer *hsizer = new wxStaticBoxSizer(this, wxHORIZONTAL); + hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP| wxBOTTOM, FromDIP(8)); + //hsizer->Add(hsizer_nozzle, 1, wxEXPAND | wxALL, FromDIP(8)); + hsizer->AddSpacer(FromDIP(2)); // Avoid badge + this->sizer = hsizer; + } else { + wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL); + vsizer->Add(hsizer_ams, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(2)); + vsizer->Add(hsizer_diameter, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT | wxBOTTOM, FromDIP(2)); + //vsizer->Add(hsizer_nozzle, 0, wxEXPAND | wxALL, FromDIP(2)); + this->sizer = vsizer; + } + AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this); +} + +void ExtruderGroup::update_ams() +{ + static AMSinfo info4; + static AMSinfo info1; + if (info4.cans.empty()) { + for (size_t i = 0; i < 4; ++i) info4.cans.push_back({}); + info1.ams_type = AMSModel::N3S_AMS; + info1.cans.push_back({}); + } + + if (btn_edit == nullptr) + return; + + page_num = (ams_n4 * 2 + ams_n1 + 3) / 4; + size_t i4 = page_cur * 2; + size_t i1 = 0; + if (i4 > ams_n4) { + i1 = (i4 - ams_n4) * 2; + i4 = ams_n4; + } + + size_t left = 4; + size_t index = 0; + for (size_t i = i4; i < ams_n4 && left > 0; ++i, ++index, left -= 2) { + ams[index]->Update(i < ams_4.size() ? ams_4[i] : info4); + ams[index]->Refresh(); + ams[index]->Open(); + } + for (size_t i = i1; i < ams_n1 && left > 0; ++i, ++index, --left) { + ams[index]->Update(i < ams_1.size() ? ams_1[i] : info1); + ams[index]->Refresh(); + ams[index]->Open(); + } + for (; index < 4; ++index) + ams[index]->Close(); + + ams_not_installed_msg->Show(ams_n4 == 0 && ams_n1 == 0); + btn_up->Show(page_cur > 0); + btn_down->Show(page_cur + 1 < page_num); + + while (hsizer_ams->GetItemCount() > 2) + hsizer_ams->Remove(2); + if (ams_not_installed_msg->IsShown()) { + hsizer_ams->AddStretchSpacer(1); + hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER); + hsizer_ams->AddStretchSpacer(1); + } + for (size_t i = 0; i < 4; ++i) { + if (ams[i]->IsShown()) + hsizer_ams->Add(this->ams[i], 0, wxLEFT, FromDIP(1)); + } + if (btn_up->IsShown() || btn_down->IsShown()) { + if (btn_edit) + hsizer_ams->AddStretchSpacer(1); + if (btn_up->IsShown() && btn_down->IsShown()) { + auto vsizer_btn = new wxBoxSizer(wxVERTICAL); + auto size = btn_up->GetSize(); + vsizer_btn->Add(btn_up, 0); + vsizer_btn->Add(btn_down, 0); + hsizer_ams->Add(vsizer_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(2)); + } else if (btn_up->IsShown()) { + hsizer_ams->Add(btn_up, 0, wxALIGN_CENTER | wxLEFT, FromDIP(2)); + } else { + hsizer_ams->Add(btn_down, 0, wxALIGN_CENTER | wxLEFT, FromDIP(2)); + } + } + + sizer->Layout(); +} + +void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector const &ams4, std::vector const &ams1) +{ + if (ams_4.empty() && ams4.empty() + && ams_1.empty() && ams1.empty()) + return; + auto sync = [obj](std::vector &infos, std::vector const &ams) -> bool { + std::vector infos2; + for (auto a : ams) { + AMSinfo ams_info; + ams_info.parse_ams_info(const_cast(obj), a, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity); + infos2.push_back(ams_info); + } + if (infos == infos2) + return false; + infos.swap(infos2); + return true; + }; + if (sync(ams_4, ams4) || sync(ams_1, ams1)) + update_ams(); +} + +void ExtruderGroup::SetTitle(const wxString& title) +{ + m_label = title; + int tW, tH, descent, externalLeading; + GetTextExtent(m_label.IsEmpty() ? "Orca" : m_label, &tW, &tH, &descent, &externalLeading, &m_font); + m_label_height = tH - externalLeading; + m_label_width = tW; + Refresh(); +} + +bool Sidebar::priv::switch_diameter(bool single) +{ + wxString diameter; + if (single) { + diameter = single_extruder->combo_diameter->GetValue(); + } else { + auto diameter_left = left_extruder->combo_diameter->GetValue(); + auto diameter_right = right_extruder->combo_diameter->GetValue(); + if (diameter_left != diameter_right) { + std::string printer_type = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle); + auto left_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase)); + auto right_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase)); + MessageDialog dlg(this->plater, + _L("The software does not support using different diameter of nozzles for one print. " + "If the left and right nozzles are inconsistent, we can only proceed with single-head printing. " + "Please confirm which nozzle you would like to use for this project."), + _L("Switch diameter"), wxYES_NO | wxNO_DEFAULT); + dlg.SetButtonLabel(wxID_YES, wxString::Format("%s: %smm", left_name, diameter_left)); + dlg.SetButtonLabel(wxID_NO, wxString::Format("%s: %smm", right_name, diameter_right)); + int result = dlg.ShowModal(); + if (result == wxID_YES) + diameter = diameter_left; + else if (result == wxID_NO) + diameter = diameter_right; + else + return false; + } + else { + diameter = diameter_left; + } + } + + // ORCA: Check if the selected diameter matches the current nozzle diameter in the config + Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); + if (nozzle_diameter && nozzle_diameter->size() > 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 (current_nozzle_dia == diameter.ToStdString()) { + return true; + } + } + + auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter.ToStdString()); + if (preset == nullptr) { + // ORCA add a text. this appears when user tries to change nozzle value but config doesnt have a inherited or compatible preset + MessageDialog dlg(this->plater, _L("Configuration incompatible"), _L("Warning"), wxICON_WARNING | wxOK); + dlg.ShowModal(); + return false; + } + preset->is_visible = true; // force visible + return wxGetApp().get_tab(Preset::TYPE_PRINTER)->select_preset(preset->name); +} + +bool Sidebar::priv::sync_extruder_list(bool &only_external_material) +{ + MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); + auto printer_name = plater->get_selected_printer_name_in_combox(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " begin sync_extruder_list"; + if (obj == nullptr) { + plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, _L("Sync printer information")); + return false; + } + //if (obj->get_extder_system()->extders.size() != 2) {//wxString(obj->get_preset_printer_model_name(machine_print_name)) + // plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::INCONSISTENT, _L("Sync printer information")); + // return false; + //} + + if (!plater->check_printer_initialized(obj)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " check_printer_initialized fail"; + return false; + } + + std::string machine_print_name = obj->get_show_printer_type(); + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); + Preset* machine_preset = get_printer_preset(obj); + if (!machine_preset) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty"; + return false; + } + 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" + "Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() == wxID_NO) { + return false; + } + + if (!this->plater) + return false; + + this->plater->update_objects_position_when_select_preset([&obj, machine_preset]() { + Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); + printer_tab->select_preset(machine_preset->name); + }); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " go on sync_extruder_list"; + const Preset &cur_preset = preset_bundle->printers.get_selected_preset(); + int extruder_nums = preset_bundle->get_printer_extruder_count(); + std::vector extruder_map(extruder_nums); + std::iota(extruder_map.begin(), extruder_map.end(), 0); + const ConfigOptionInts *physical_extruder_map = cur_preset.config.option("physical_extruder_map"); + if (physical_extruder_map != nullptr) { + assert(physical_extruder_map->values.size() == extruder_nums); + extruder_map = physical_extruder_map->values; + } + assert(obj->GetExtderSystem()->GetTotalExtderCount() == extruder_nums); + + std::vector nozzle_diameters; + nozzle_diameters.resize(extruder_nums); + for (size_t index = 0; index < extruder_nums; ++index) { + int extruder_id = extruder_map[index]; + nozzle_diameters[extruder_id] = obj->GetExtderSystem()->GetNozzleDiameter(index); + NozzleVolumeType target_type = NozzleVolumeType::nvtStandard; + auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + if (obj->is_nozzle_flow_type_supported()) { + if (obj->GetExtderSystem()->GetNozzleFlowType(index) == NozzleFlowType::NONE_FLOWTYPE) { + MessageDialog dlg(this->plater, _L("There are unset nozzle types. Please set the nozzle types of all extruders before synchronizing."), + _L("Sync extruder infomation"), wxICON_WARNING | wxOK); + dlg.ShowModal(); + continue; + } + // hack code, only use standard flow for 0.2 + if (std::fabs(nozzle_diameters[extruder_id] - 0.2) > EPSILON) + target_type = NozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(extruder_id) - 1); + } + printer_tab->set_extruder_volume_type(index, target_type); + } + + int deputy_4 = 0, main_4 = 0, deputy_1 = 0, main_1 = 0; + for (auto ams : obj->GetFilaSystem()->GetAmsList()) { + // Main (first) extruder at right + if (ams.second->GetExtruderId() == 0) { + if (ams.second->GetAmsType() == DevAms::N3S) // N3S + ++main_1; + else + ++main_4; + } else if (ams.second->GetExtruderId() == 1) { + if (ams.second->GetAmsType() == DevAms::N3S) // N3S + ++deputy_1; + else + ++deputy_4; + } + } + only_external_material = !obj->GetFilaSystem()->HasAms(); + int main_index = obj->is_main_extruder_on_left() ? 0 : 1; + int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0; + + if (extruder_nums > 1) { + int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0])); + int right_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[1])); + assert(left_index != -1 && right_index != -1); + left_extruder->combo_diameter->SetSelection(left_index); + right_extruder->combo_diameter->SetSelection(right_index); + is_switching_diameter = true; + switch_diameter(false); + is_switching_diameter = false; + AMSCountPopupWindow::SetAMSCount(deputy_index, deputy_4, deputy_1); + AMSCountPopupWindow::SetAMSCount(main_index, main_4, main_1); + AMSCountPopupWindow::UpdateAMSCount(0, left_extruder); + AMSCountPopupWindow::UpdateAMSCount(1, right_extruder); + } else { + int index = single_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0])); + assert(index != -1); + single_extruder->combo_diameter->SetSelection(index); + is_switching_diameter = true; + switch_diameter(true); + is_switching_diameter = false; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " finish sync_extruder_list"; + return true; +} + +void Sidebar::priv::update_sync_status(const MachineObject *obj) +{ + StateColor not_synced_colour(std::pair(wxColour("#009688"), StateColor::Normal)); + auto clear_all_sync_status = [this, ¬_synced_colour]() { + panel_printer_preset->ShowBadge(false); + panel_printer_bed->ShowBadge(false); + panel_nozzle_dia->ShowBadge(false); // ORCA add support for nozzle sync + left_extruder->ShowBadge(false); + left_extruder->sync_ams(nullptr, {}, {}); + right_extruder->ShowBadge(false); + right_extruder->sync_ams(nullptr, {}, {}); + single_extruder->ShowBadge(false); + single_extruder->sync_ams(nullptr, {}, {}); + //btn_sync_printer->SetBorderColor(not_synced_colour); + //btn_sync_printer->SetIcon("printer_sync"); + m_printer_bbl_sync->SetBitmap_("printer_sync_not"); + }; + + if (!obj || !obj->is_info_ready()) { + clear_all_sync_status(); + return; + } + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) { + clear_all_sync_status(); + return; + } + + bool printer_synced = false; + // 1. update printer status + const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type()) { + panel_printer_preset->ShowBadge(true); + printer_synced = true; + + wxGetApp().plater()->sidebar().udpate_combos_filament_badge(); + } else { + clear_all_sync_status(); + + wxGetApp().plater()->sidebar().clear_combos_filament_badge(); + return; + } + + struct ExtruderInfo + { + float diameter{0.4}; + //int nozzle_volue_type{0}; + int ams_4{0}; + int ams_1{0}; + std::vector ams_v4; + std::vector ams_v1; + + bool operator==(const ExtruderInfo &other) const + { + return abs(diameter - other.diameter) < EPSILON + && /*nozzle_volue_type == other.nozzle_volue_type + &&*/ ams_4 == other.ams_4 + && ams_1 == other.ams_1; + } + }; + + auto is_same_nozzle_info = [obj](const ExtruderInfo &left, const ExtruderInfo &right) { + bool is_same_nozzle_type = true; + if (obj->is_nozzle_flow_type_supported()) + is_same_nozzle_type = true;//left.nozzle_volue_type == right.nozzle_volue_type; // TODO: Orca hack + return abs(left.diameter - right.diameter) < EPSILON && is_same_nozzle_type; + }; + + // 2. update extruder status + int extruder_nums = preset_bundle->get_printer_extruder_count(); + if (extruder_nums != obj->GetExtderSystem()->GetTotalExtderCount()) + return; + + std::vector extruder_infos(extruder_nums); + std::vector nozzle_volume_types = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; + //for (size_t i = 0; i < nozzle_volume_types.size(); ++i) { + // extruder_infos[i].nozzle_volue_type = nozzle_volume_types[i]; + //} + + std::vector> extruder_ams_counts = wxGetApp().preset_bundle->extruder_ams_counts; + if (extruder_ams_counts.size() >= extruder_nums) { + for (size_t i = 0; i < extruder_nums; ++i) { + for (auto iter = extruder_ams_counts[i].begin(); iter != extruder_ams_counts[i].end(); ++iter) { + if (iter->first == 4) + extruder_infos[i].ams_4 = iter->second; + if (iter->first == 1) + extruder_infos[i].ams_1 = iter->second; + } + } + } + + if (extruder_nums == 1) { + double value = 0.0; + single_extruder->diameter.ToDouble(&value); + extruder_infos[0].diameter = float(value); + } + else if(extruder_nums == 2){ + double value = 0.0; + left_extruder->diameter.ToDouble(&value); + extruder_infos[0].diameter = float(value); + + value = 0.0; + right_extruder->diameter.ToDouble(&value); + extruder_infos[1].diameter = float(value); + } + + std::vector machine_extruder_infos(obj->GetExtderSystem()->GetTotalExtderCount()); + + const auto& extruders = obj->GetExtderSystem()->GetExtruders(); + for (const DevExtder &extruder : extruders) { + //machine_extruder_infos[extruder.GetExtId()].nozzle_volue_type = int(extruder.GetNozzleFlowType()) - 1; + machine_extruder_infos[extruder.GetExtId()].diameter = extruder.GetNozzleDiameter(); + } + for (auto &item : obj->GetFilaSystem()->GetAmsList()) { + if (item.second->GetExtruderId() >= machine_extruder_infos.size()) + continue; + + if (item.second->GetAmsType() == DevAms::N3S) + { // N3S + machine_extruder_infos[item.second->GetExtruderId()].ams_1++; + machine_extruder_infos[item.second->GetExtruderId()].ams_v1.push_back(item.second); + } else { + machine_extruder_infos[item.second->GetExtruderId()].ams_4++; + machine_extruder_infos[item.second->GetExtruderId()].ams_v4.push_back(item.second); + } + } + + std::reverse(machine_extruder_infos.begin(), machine_extruder_infos.end()); + + std::vector extruder_synced(extruder_nums, false); + if (extruder_nums == 1) { + if (is_same_nozzle_info(extruder_infos[0], machine_extruder_infos[0])) { + single_extruder->ShowBadge(true); + panel_nozzle_dia->ShowBadge(true); // ORCA add support for nozzle sync + single_extruder->sync_ams(obj, machine_extruder_infos[0].ams_v4, machine_extruder_infos[0].ams_v1); + extruder_synced[0] = true; + } + else { + single_extruder->ShowBadge(false); + panel_nozzle_dia->ShowBadge(false); // ORCA add support for nozzle sync + single_extruder->sync_ams(obj, {}, {}); + } + } + else if (extruder_nums == 2) { + if (extruder_infos[0] == machine_extruder_infos[0]) { + left_extruder->ShowBadge(true); + left_extruder->sync_ams(obj, machine_extruder_infos[0].ams_v4, machine_extruder_infos[0].ams_v1); + extruder_synced[0] = true; + } + else { + left_extruder->ShowBadge(false); + left_extruder->sync_ams(obj, {}, {}); + } + + if (extruder_infos[1] == machine_extruder_infos[1]) { + right_extruder->ShowBadge(true); + right_extruder->sync_ams(obj, machine_extruder_infos[1].ams_v4, machine_extruder_infos[1].ams_v1); + extruder_synced[1] = true; + } + else { + right_extruder->ShowBadge(false); + right_extruder->sync_ams(obj, {}, {}); + } + } + + StateColor synced_colour(std::pair(wxColour("#CECECE"), StateColor::Normal)); + bool all_extruder_synced = std::all_of(extruder_synced.begin(), extruder_synced.end(), [](bool value) { return value; }); + if (printer_synced && all_extruder_synced) { + // btn_sync_printer->SetBorderColor(synced_colour); + // btn_sync_printer->SetIcon("ams_nozzle_sync"); + m_printer_bbl_sync->SetBitmap_("printer_sync_ok"); + } + else { + // btn_sync_printer->SetBorderColor(not_synced_colour); + // btn_sync_printer->SetIcon("printer_sync"); + m_printer_bbl_sync->SetBitmap_("printer_sync_not"); + } + } + +void Sidebar::update_sync_ams_btn_enable(wxUpdateUIEvent &e) + { + if (m_last_slice_state != p->plater->is_background_process_slicing()) { + m_last_slice_state = p->plater->is_background_process_slicing(); + //btn_sync->Enable(!m_last_slice_state); + p->m_printer_bbl_sync->Enable(!m_last_slice_state); + ams_btn->Enable(!m_last_slice_state); + Refresh(); + } + } + +Sidebar::Sidebar(Plater *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(39 * wxGetApp().em_unit(), -1)), p(new priv(parent)) +{ + Choice::register_dynamic_list("support_filament", &dynamic_filament_list); + Choice::register_dynamic_list("support_interface_filament", &dynamic_filament_list); + Choice::register_dynamic_list("wall_filament", &dynamic_filament_list_1_based); + Choice::register_dynamic_list("sparse_infill_filament", &dynamic_filament_list_1_based); + Choice::register_dynamic_list("solid_infill_filament", &dynamic_filament_list_1_based); + Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); + + p->scrolled = new wxPanel(this); + // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 + // but this cause the bad layout of the sidebar, when all infoboxes appear. + // As a result we can see the empty block at the bottom of the sidebar + // But if we set this value to 5, layout will be better + //p->scrolled->SetScrollRate(0, 5); + p->scrolled->SetBackgroundColour(*wxWHITE); + + + SetFont(wxGetApp().normal_font()); +#ifndef __APPLE__ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this); + wxGetApp().UpdateDarkUI(p->scrolled); +#else + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif +#endif + + int em = wxGetApp().em_unit(); + //BBS refine layout and styles + // Sizer in the scrolled area + auto* scrolled_sizer = m_scrolled_sizer = new wxBoxSizer(wxVERTICAL); + p->scrolled->SetSizer(scrolled_sizer); + + wxColour title_bg = wxColour(248, 248, 248); + wxColour inactive_text = wxColour(86, 86, 86); + wxColour active_text = wxColour(0, 0, 0); + wxColour static_line_col = wxColour(166, 169, 170); + +#ifdef __WINDOWS__ + p->scrolled->SetDoubleBuffered(true); +#endif //__WINDOWS__ + + // add printer + { + /***************** 1. create printer title bar **************/ + // 1.1 create title bar resources + p->m_panel_printer_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_printer_title->SetBackgroundColor(title_bg); + p->m_panel_printer_title->SetBackgroundColor2(0xF1F1F1); + + p->m_printer_icon = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer"); + p->m_text_printer_settings = new Label(p->m_panel_printer_title, _L("Printer"), LB_PROPAGATE_MOUSE_EVENT); + + p->m_printer_icon->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + //auto wizard_t = new ConfigWizard(wxGetApp().mainframe); + //wizard_t->run(ConfigWizard::RR_USER, ConfigWizard::SP_CUSTOM); + }); + + // ORCA use connect button on titlebar + p->m_printer_connect = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "monitor_signal_strong"); + p->m_printer_connect->SetToolTip(_L("Connection")); + p->m_printer_connect->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + PhysicalPrinterDialog dlg(this->GetParent()); + dlg.ShowModal(); + }); + + // ORCA use sync button on titlebar + p->m_printer_bbl_sync = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer_sync_not"); + p->m_printer_bbl_sync->SetToolTip(_L("Synchronize nozzle information and the number of AMS")); + p->m_printer_bbl_sync->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + deal_btn_sync(); + }); + + p->m_printer_setting = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "settings"); + p->m_printer_setting->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + // p->editing_filament = -1; + // wxGetApp().params_dialog()->Popup(); + // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); + wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_PRINTERS); + }); + + wxBoxSizer* h_sizer_title = new wxBoxSizer(wxHORIZONTAL); + h_sizer_title->Add(p->m_printer_icon, 0, wxALIGN_CENTRE | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + h_sizer_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + h_sizer_title->Add(p->m_text_printer_settings, 0, wxALIGN_CENTER); + h_sizer_title->AddStretchSpacer(); + h_sizer_title->Add(p->m_printer_connect , 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks + h_sizer_title->Add(p->m_printer_bbl_sync, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks + h_sizer_title->Add(p->m_printer_setting, 0, wxALIGN_CENTER); + h_sizer_title->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + h_sizer_title->SetMinSize(-1, 3 * em); + + p->m_panel_printer_title->SetSizer(h_sizer_title); + p->m_panel_printer_title->Layout(); + + // 1.2 Add spliters around title bar + // add spliter 1 + //auto spliter_1 = new ::StaticLine(p->scrolled); + //spliter_1->SetBackgroundColour("#A6A9AA"); + //scrolled_sizer->Add(spliter_1, 0, wxEXPAND); + + // add printer title + scrolled_sizer->Add(p->m_panel_printer_title, 0, wxEXPAND | wxALL, 0); + p->m_panel_printer_title->Bind(wxEVT_LEFT_UP, [this] (auto & e) { + p->m_panel_printer_content->Show(!p->m_panel_printer_content->IsShown()); + m_scrolled_sizer->Layout(); + }); + + // add spliter 2 + auto spliter_2 = new ::StaticLine(p->scrolled); + spliter_2->SetLineColour("#CECECE"); + scrolled_sizer->Add(spliter_2, 0, wxEXPAND); + + + /*************************** 2. add printer content ************************/ + + p->m_panel_printer_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + p->m_panel_printer_content->SetBackgroundColour(wxColour(255, 255, 255)); + + struct PanelColors { + wxColour bg_normal = "#FFFFFF"; + wxColour bg_focus = "#E5F0EE"; + wxColour bd_normal = "#DBDBDB"; + wxColour bd_hover = "#009688"; + wxColour bd_focus = "#009688"; + }; + PanelColors panel_color; + + p->panel_printer_preset = new StaticBox(p->m_panel_printer_content); + p->panel_printer_preset->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); + p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); + p->panel_printer_preset->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); + p->panel_printer_preset->Bind(wxEVT_LEFT_DOWN, [this](auto & evt) { + p->combo_printer->wxEvtHandler::ProcessEvent(evt); + }); + // ORCA Hide Cover automatically if there is not enough space + p->panel_printer_preset->Bind(wxEVT_SIZE, [this](auto & e) { + auto current_width = e.GetSize().GetWidth(); + auto narrow_width = FromDIP(235); + auto label_width = p->combo_printer->GetTextExtent(p->combo_printer->GetStringSelection()).GetWidth(); + auto min_width = label_width + FromDIP(25 + PRINTER_PANEL_SIZE.GetWidth()); + if(((min_width < narrow_width && min_width > current_width) || (current_width < narrow_width && min_width > narrow_width)) && p->image_printer->IsShown()) + p->image_printer->Hide(); + else if((current_width > min_width || !(current_width < narrow_width)) && !p->image_printer->IsShown()) + p->image_printer->Show(); + e.Skip(); + }); + + p->btn_edit_printer = new ScalableButton(p->panel_printer_preset, wxID_ANY, "edit"); + p->btn_edit_printer->SetToolTip(_L("Click to edit preset")); + p->btn_edit_printer->Hide(); // hide for first launch + p->btn_edit_printer->Bind(wxEVT_BUTTON, [this, panel_color](wxCommandEvent){ + p->editing_filament = -1; + if (p->combo_printer->switch_to_tab()) + p->editing_filament = 0; + // ORCA: FIX crash on wxGTK, directly modifying UI (self->Hide() / parent->Layout()) inside a button event can crash because callbacks are not re-entrant, leaving widgets in an inconsistent state + wxGetApp().CallAfter([this, panel_color]() { + // ORCA clicking edit button not triggers wxEVT_KILL_FOCUS wxEVT_LEAVE_WINDOW make changes manually to prevent stucked colors when opening printer settings + if (!p || !p->panel_printer_preset || !p->btn_edit_printer) + return; + p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); + p->btn_edit_printer->Hide(); + p->panel_printer_preset->Layout(); + }); + }); + + ScalableBitmap bitmap_printer(p->panel_printer_preset, "printer_placeholder", PRINTER_THUMBNAIL_SIZE.GetHeight()); + p->image_printer = new wxStaticBitmap(p->panel_printer_preset, wxID_ANY, bitmap_printer.bmp(), wxDefaultPosition, FromDIP(PRINTER_THUMBNAIL_SIZE), 0); + p->image_printer->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { + p->combo_printer->wxEvtHandler::ProcessEvent(evt); + }); + + p->combo_printer = new PlaterPresetComboBox(p->panel_printer_preset, Preset::TYPE_PRINTER); + p->combo_printer->SetBorderWidth(0); + p->combo_printer->SetMaxSize(wxSize(-1, FromDIP(30))); // limiting height makes badge visible + // ORCA paint whole combobox on focus + auto printer_focus_bg = [this, panel_color](bool focused){ + auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); + p->panel_printer_preset->SetBackgroundColor(bg_color); + p->panel_printer_preset->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); + p->btn_edit_printer->SetBackgroundColour(bg_color); + p->image_printer->SetBackgroundColour(bg_color); + p->combo_printer->SetBackgroundColour(bg_color); // paints margins instead combo background + }; + p->combo_printer->Bind(wxEVT_SET_FOCUS, [this, printer_focus_bg](auto& e) {printer_focus_bg(true ); e.Skip();}); + p->combo_printer->Bind(wxEVT_KILL_FOCUS, [this, printer_focus_bg](auto& e) {printer_focus_bg(false); e.Skip();}); + + /* ORCA This part moved to titlebar + p->btn_connect_printer = new ScalableButton(p->panel_printer_preset, wxID_ANY, "monitor_signal_strong"); + p->btn_connect_printer->SetBackgroundColour(wxColour(255, 255, 255)); + p->btn_connect_printer->SetToolTip(_L("Connection")); + p->btn_connect_printer->Bind(wxEVT_BUTTON, [this, combo_printer](wxCommandEvent) + { + PhysicalPrinterDialog dlg(this->GetParent()); + dlg.ShowModal(); + }); + */ + // ORCA use Show/Hide to gain text area instead using blank icon. also manages hover effect for border + for (wxWindow *w : std::initializer_list{p->panel_printer_preset, p->btn_edit_printer, p->image_printer, p->combo_printer}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, panel_color](wxMouseEvent &e) { + if(!p->combo_printer->HasFocus()) + p->panel_printer_preset->SetBorderColor(panel_color.bd_hover); + if(!p->btn_edit_printer->IsShown()){ + p->btn_edit_printer->Show(); + p->panel_printer_preset->Layout(); + } + e.Skip(); + }); + w->Bind(wxEVT_LEAVE_WINDOW, [this, panel_color](wxMouseEvent &e) { + // Use event-relative coords instead of wxGetMousePosition() which + // returns (0,0) on Wayland for global screen coordinates. + wxWindow* evtObj = dynamic_cast(e.GetEventObject()); + wxPoint screenPos = evtObj ? evtObj->ClientToScreen(e.GetPosition()) : wxGetMousePosition(); + wxWindow* next_w = wxFindWindowAtPoint(screenPos); + if (!next_w || !p->panel_printer_preset->IsDescendant(next_w)){ + if(!p->combo_printer->HasFocus()) + p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); + p->btn_edit_printer->Hide(); + p->panel_printer_preset->Layout(); + } + e.Skip(); + }); + } + + // ORCA unified Nozzle diameter selection + p->panel_nozzle_dia = new StaticBox(p->m_panel_printer_content); + p->panel_nozzle_dia->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); + p->panel_nozzle_dia->SetBorderColor(panel_color.bd_normal); + p->panel_nozzle_dia->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); + p->panel_nozzle_dia->Bind(wxEVT_LEFT_DOWN, [this](auto & evt) { + p->combo_nozzle_dia->wxEvtHandler::ProcessEvent(evt); + }); + + p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Nozzle"), LB_PROPAGATE_MOUSE_EVENT); + p->label_nozzle_title->SetFont(Label::Body_10); + + p->combo_nozzle_dia = new ComboBox(p->panel_nozzle_dia, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + p->combo_nozzle_dia->SetBorderWidth(0); + p->combo_nozzle_dia->GetDropDown().SetUseContentWidth(true); + p->combo_nozzle_dia->SetMinSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // requires a static value in here + p->combo_nozzle_dia->SetMaxSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // using -1 with wxEXPAND has issues + p->combo_nozzle_dia->Bind(wxEVT_COMBOBOX, [this](auto &e) { + auto evt_combo = (*p->single_extruder).combo_diameter; + evt_combo->SetSelection(e.GetSelection()); + wxCommandEvent evt(wxEVT_COMBOBOX, evt_combo->GetId()); + evt.SetEventObject(evt_combo); + evt.SetInt(e.GetSelection()); + wxPostEvent(evt_combo, evt); + e.Skip(); + }); + // ORCA paint whole combobox on focus + auto nozzle_focus_bg = [this, panel_color](bool focused){ + auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); + p->panel_nozzle_dia->SetBackgroundColor(bg_color); + p->panel_nozzle_dia->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); + p->label_nozzle_title->SetBackgroundColour(bg_color); + p->label_nozzle_type->SetBackgroundColour(bg_color); + p->combo_nozzle_dia->SetBackgroundColour(bg_color); // paints margins instead combo background + }; + p->combo_nozzle_dia->Bind(wxEVT_SET_FOCUS, [this, nozzle_focus_bg](auto& e) {nozzle_focus_bg(true ); e.Skip();}); + p->combo_nozzle_dia->Bind(wxEVT_KILL_FOCUS, [this, nozzle_focus_bg](auto& e) {nozzle_focus_bg(false); e.Skip();}); + + p->label_nozzle_type = new Label(p->panel_nozzle_dia, "Brass", LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END | wxALIGN_CENTRE_HORIZONTAL); + p->label_nozzle_type->SetFont(Label::Body_10); + p->label_nozzle_type->SetMinSize(FromDIP(wxSize(56, -1))); + p->label_nozzle_type->SetMaxSize(FromDIP(wxSize(56, -1))); + + // highlight border on hover + for (wxWindow *w : std::initializer_list{p->panel_nozzle_dia, p->label_nozzle_title, p->label_nozzle_type, p->combo_nozzle_dia}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, panel_color](wxMouseEvent &e) { + if(!p->combo_nozzle_dia->HasFocus()) + p->panel_nozzle_dia->SetBorderColor(panel_color.bd_hover); + e.Skip(); + }); + w->Bind(wxEVT_LEAVE_WINDOW, [this, panel_color](wxMouseEvent &e) { + // Use event-relative coords instead of wxGetMousePosition() which + // returns (0,0) on Wayland for global screen coordinates. + wxWindow* evtObj = dynamic_cast(e.GetEventObject()); + wxPoint screenPos = evtObj ? evtObj->ClientToScreen(e.GetPosition()) : wxGetMousePosition(); + wxWindow* next_w = wxFindWindowAtPoint(screenPos); + if (!p->combo_nozzle_dia->HasFocus() && (!next_w || !p->panel_nozzle_dia->IsDescendant(next_w))) + p->panel_nozzle_dia->SetBorderColor(panel_color.bd_normal); + e.Skip(); + }); + } + + wxGridSizer *nozzle_dia_sizer = new wxGridSizer(3, 1, FromDIP(2), 0); + nozzle_dia_sizer->Add(p->label_nozzle_title, 0, wxALIGN_CENTER | wxTOP, FromDIP(4)); + nozzle_dia_sizer->Add(p->combo_nozzle_dia , 0, wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(2)); + nozzle_dia_sizer->Add(p->label_nozzle_type , 0, wxALIGN_CENTER); + + p->panel_nozzle_dia->SetSizer(nozzle_dia_sizer); + + // Bed type selection + p->panel_printer_bed = new StaticBox(p->m_panel_printer_content); + p->panel_printer_bed->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); + p->panel_printer_bed->SetBorderColor(panel_color.bd_normal); + p->panel_printer_bed->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); + p->panel_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { + on_leave_image_printer_bed(evt); + p->combo_printer_bed->wxEvtHandler::ProcessEvent(evt); + }); + + //ScalableButton *wiki_bed = new ScalableButton(p->panel_printer_bed, wxID_ANY, "help"); + //wiki_bed->Bind(wxEVT_BUTTON, [](wxCommandEvent) { + // wxLaunchDefaultBrowser("https://wiki.bambulab.com/en/x1/manual/compatibility-and-parameter-settings-of-filaments"); + //}); + + ScalableBitmap bitmap_bed(p->panel_printer_bed, "printer_placeholder", PRINTER_THUMBNAIL_SIZE.GetHeight()); + p->image_printer_bed = new wxStaticBitmap(p->panel_printer_bed, wxID_ANY, bitmap_bed.bmp(), wxDefaultPosition, wxDefaultSize, 0); + p->image_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { + on_leave_image_printer_bed(evt); + p->combo_printer_bed->wxEvtHandler::ProcessEvent(evt); + }); + + p->combo_printer_bed = new ComboBox(p->panel_printer_bed, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + p->combo_printer_bed->SetBorderWidth(0); + p->combo_printer_bed->GetDropDown().SetUseContentWidth(true); + p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow + p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow + reset_bed_type_combox_choices(true); + + p->combo_printer_bed->Bind(wxEVT_COMBOBOX, [this](auto &e) { + auto image_path = get_cur_select_bed_image(); + p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); + e.Skip(); + }); + + // ORCA paint whole combobox on focus + auto bed_focus_bg = [this, panel_color](bool focused){ + auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); + p->panel_printer_bed->SetBackgroundColor(bg_color); + p->panel_printer_bed->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); + p->image_printer_bed->SetBackgroundColour(bg_color); + p->combo_printer_bed->SetBackgroundColour(bg_color); // paints margins instead combo background + }; + p->combo_printer_bed->Bind(wxEVT_SET_FOCUS, [this, bed_focus_bg](auto& e) {bed_focus_bg(true ); e.Skip();}); + p->combo_printer_bed->Bind(wxEVT_KILL_FOCUS, [this, bed_focus_bg](auto& e) {bed_focus_bg(false); e.Skip();}); + + // highlight border on hover + for (wxWindow *w : std::initializer_list{p->panel_printer_bed, p->image_printer_bed, p->combo_printer_bed}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color](wxMouseEvent &e) { + if(!p->combo_printer_bed->HasFocus()) + p->panel_printer_bed->SetBorderColor(panel_color.bd_hover); + if(w == p->image_printer_bed && !p->combo_printer_bed->is_drop_down()) // dont trigger while combo open + on_enter_image_printer_bed(e); + e.Skip(); + }); + w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color](wxMouseEvent &e) { + // Use event-relative coords instead of wxGetMousePosition() which + // returns (0,0) on Wayland for global screen coordinates. + wxWindow* evtObj = dynamic_cast(e.GetEventObject()); + wxPoint screenPos = evtObj ? evtObj->ClientToScreen(e.GetPosition()) : wxGetMousePosition(); + wxWindow* next_w = wxFindWindowAtPoint(screenPos); + if (!p->combo_printer_bed->HasFocus() && (!next_w || !p->panel_printer_bed->IsDescendant(next_w))) + p->panel_printer_bed->SetBorderColor(panel_color.bd_normal); + if(w == p->image_printer_bed) + on_leave_image_printer_bed(e); + e.Skip(); + }); + } + + wxBoxSizer *bed_type_sizer = new wxBoxSizer(wxHORIZONTAL); + bed_type_sizer->Add(p->combo_printer_bed, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(2)); + bed_type_sizer->Add(p->image_printer_bed, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(2)); + + p->panel_printer_bed->SetSizer(bed_type_sizer); + + AppConfig *app_config = wxGetApp().app_config; + std::string str_bed_type = app_config->get("curr_bed_type"); + int bed_type_value = atoi(str_bed_type.c_str()); + // hotfix: btDefault is added as the first one in BedType, and app_config should not be btDefault + if (bed_type_value == 0) { + app_config->set("curr_bed_type", "1"); + bed_type_value = 1; + } + + int bed_type_idx = bed_type_value - 1; + p->combo_printer_bed->Select(bed_type_idx); + + auto& project_config = wxGetApp().preset_bundle->project_config; + /*const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map; + BedType bed_type = btCount; + for (auto item : *keys_map) { + if (item.first == str_bed_type) + bed_type = (BedType)item.second; + }*/ + BedType bed_type = (BedType)bed_type_value; + project_config.set_key_value("curr_bed_type", new ConfigOptionEnum(bed_type)); + + /* ORCA THIS PART MOVED TO TITLEBAR + // Sync printer information + btn_sync = new Button(p->m_panel_printer_content, _L("Sync info"), "printer_sync", 0, 32); + //btn_sync->SetFont(Label::Body_8); + btn_sync->SetToolTip(_L("Synchronize nozzle information and the number of AMS")); + btn_sync->SetCornerRadius(8); + StateColor btn_sync_bg_col( + std::pair(wxColour("#CECECE"), StateColor::Pressed), + std::pair(wxColour("#F8F8F8"), StateColor::Hovered), + std::pair(wxColour("#F8F8F8"), StateColor::Normal)); + StateColor btn_sync_bd_col( + std::pair(wxColour("#009688"), StateColor::Pressed), + std::pair(wxColour("#009688"), StateColor::Hovered), + std::pair(wxColour("#EEEEEE"), StateColor::Normal)); + btn_sync->SetBackgroundColor(btn_sync_bg_col); + btn_sync->SetBorderColor(btn_sync_bd_col); + btn_sync->SetCanFocus(false); + btn_sync->SetPaddingSize({FromDIP(6), FromDIP(12)}); + btn_sync->SetMinSize(BTN_SYNC_SIZE); + btn_sync->SetMaxSize(BTN_SYNC_SIZE); + btn_sync->SetVertical(); + btn_sync->Bind(wxEVT_UPDATE_UI, &Sidebar::update_sync_ams_btn_enable, this); + btn_sync->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + deal_btn_sync(); + }); + p->btn_sync_printer = btn_sync; + */ + p->timer_sync_printer->Bind(wxEVT_TIMER, [this] (wxTimerEvent & e) { + p->flush_printer_sync(); + }); + + + p->left_extruder = new ExtruderGroup(p->m_panel_printer_content, 0, _L("Left Nozzle")); + p->right_extruder = new ExtruderGroup(p->m_panel_printer_content, 1, _L("Right Nozzle")); + p->single_extruder = new ExtruderGroup(p->m_panel_printer_content, -1, _L("Nozzle")); + auto switch_diameter = [this](wxCommandEvent & evt) { + auto extruder = dynamic_cast(dynamic_cast(evt.GetEventObject())->GetParent()); + p->is_switching_diameter = true; + p->switch_diameter(extruder == p->single_extruder); + p->is_switching_diameter = false; + }; + p->left_extruder->combo_diameter->Bind(wxEVT_COMBOBOX, switch_diameter); + p->right_extruder->combo_diameter->Bind(wxEVT_COMBOBOX, switch_diameter); + p->single_extruder->combo_diameter->Bind(wxEVT_COMBOBOX, switch_diameter); + + p->vsizer_printer = new wxBoxSizer(wxVERTICAL); + p->layout_printer(true, true); + p->m_panel_printer_content->SetSizer(p->vsizer_printer); + p->m_panel_printer_content->Layout(); + scrolled_sizer->Add(p->m_panel_printer_content, 0, wxEXPAND, 0); + } + + { + + // Orca: Sidebar - Filament titlebar UI + // add filament title + p->m_panel_filament_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_filament_title->SetBackgroundColor(title_bg); + p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); + p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { + if (e.GetPosition().x > (p->m_flushing_volume_btn->IsShown() + ? p->m_flushing_volume_btn->GetPosition().x : (p->m_bpButton_add_filament->GetPosition().x - FromDIP(30)))) // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button + return; + p->m_panel_filament_content->Show(!p->m_panel_filament_content->IsShown()); + m_scrolled_sizer->Layout(); + }); + + wxBoxSizer* bSizer39; + bSizer39 = new wxBoxSizer( wxHORIZONTAL ); + p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); + p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Project Filaments"), LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + bSizer39->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); + bSizer39->Add( p->m_staticText_filament_settings, 0, wxALIGN_CENTER ); + bSizer39->Add(FromDIP(10), 0, 0, 0, 0); + bSizer39->SetMinSize(-1, FromDIP(30)); + + p->m_panel_filament_title->SetSizer( bSizer39 ); + p->m_panel_filament_title->Layout(); + auto spliter_1 = new ::StaticLine(p->scrolled); + spliter_1->SetLineColour("#A6A9AA"); + scrolled_sizer->Add(spliter_1, 0, wxEXPAND); + scrolled_sizer->Add(p->m_panel_filament_title, 0, wxEXPAND | wxALL, 0); + auto spliter_2 = new ::StaticLine(p->scrolled); + spliter_2->SetLineColour("#CECECE"); + scrolled_sizer->Add(spliter_2, 0, wxEXPAND); + + bSizer39->AddStretchSpacer(1); + + // BBS + // add wiping dialog + //wiping_dialog_button->SetFont(wxGetApp().normal_font()); + p->m_flushing_volume_btn = new Button(p->m_panel_filament_title, _L("Flushing volumes")); + p->m_flushing_volume_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_flushing_volume_btn->SetId(wxID_RESET); + auto has_modify = is_flush_config_modified(); + set_flushing_volume_warning(has_modify); + + p->m_flushing_volume_btn->Bind(wxEVT_BUTTON, ([parent, this](wxCommandEvent &e) { + open_flushing_dialog(parent, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, parent)); + p->plater->get_view3D_canvas3D()->reload_scene(true); + p->plater->update(); + })); + + bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + bSizer39->Hide(p->m_flushing_volume_btn); // ORCA Ensure button is hidden on launch while 1 filament exist + + ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); + add_btn->SetToolTip(_L("Add one filament")); + add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e){ + add_filament(); + }); + p->m_bpButton_add_filament = add_btn; + + // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + + ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); + del_btn->SetToolTip(_L("Remove last filament")); + del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { + delete_filament(); + }); + p->m_bpButton_del_filament = del_btn; + + bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + + bSizer39->Hide(p->m_bpButton_del_filament); // ORCA Ensure button is hidden on launch while 1 filament exist + + ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 + ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); + ams_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { + sync_ams_list(); + }); + + ams_btn->Bind(wxEVT_UPDATE_UI, &Sidebar::update_sync_ams_btn_enable, this); + p->m_bpButton_ams_filament = ams_btn; + + bSizer39->Add(ams_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); + //bSizer39->Add(FromDIP(10), 0, 0, 0, 0 ); + + ScalableButton* set_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "settings"); + set_btn->SetToolTip(_L("Set filaments to use")); + set_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + p->editing_filament = -1; + // wxGetApp().params_dialog()->Popup(); + // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); + wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_FILAMENTS); + }); + p->m_bpButton_set_filament = set_btn; + + bSizer39->Add(set_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); + bSizer39->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + + // add filament content + p->m_panel_filament_content = new wxScrolledWindow( p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); + p->m_panel_filament_content->SetScrollRate(0, 5); + //p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); + p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255)); + + //wxBoxSizer* bSizer_filament_content; + //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); + + // Orca: Sidebar - Filament content UI: setup filament selection combos panel layout + // Creates a two-column grid layout for filament selection dropdowns within the scrollable panel + p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); + p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + + p->combos_filament.push_back(nullptr); + + /* first filament item */ + init_filament_combo(&p->combos_filament[0], 0); + + //bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); + wxSizer *sizer_filaments2 = new wxBoxSizer(wxVERTICAL); + sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); + p->m_panel_filament_content->SetSizer(sizer_filaments2); + p->m_panel_filament_content->Layout(); + + update_filaments_area_height(); // ORCA + + scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(SidebarProps::ContentMarginV())); // ORCA use vertical margin on parent otherwise it shows scrollbar even on 1 filament + } + + { + //add project title + auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel; + if (params_panel) { + params_panel->get_top_panel()->Reparent(p->scrolled); + auto spliter_1 = new ::StaticLine(p->scrolled); + spliter_1->SetLineColour("#A6A9AA"); + scrolled_sizer->Add(spliter_1, 0, wxEXPAND); + scrolled_sizer->Add(params_panel->get_top_panel(), 0, wxEXPAND); + auto spliter_2 = new ::StaticLine(p->scrolled); + spliter_2->SetLineColour("#CECECE"); + scrolled_sizer->Add(spliter_2, 0, wxEXPAND); + } + + //add project content + p->sizer_params = new wxBoxSizer(wxVERTICAL); + + // ORCA: Update search box to modern style + p->m_search_bar = new StaticBox(p->scrolled); + p->m_search_bar->SetCornerRadius(0); + p->m_search_bar->SetBorderColor(wxColour("#CECECE")); + + p->m_search_item = new TextInput(p->m_search_bar, wxEmptyString, wxEmptyString, "", wxDefaultPosition, wxDefaultSize, 0 | wxBORDER_NONE); + p->m_search_item->SetIcon(*BitmapCache().load_svg("search", FromDIP(16), FromDIP(16))); // ORCA: Add search icon to search box + + wxTextCtrl* text_ctrl = p->m_search_item->GetTextCtrl(); + text_ctrl->SetHint(_L("Search plate, object and part.")); + text_ctrl->SetForegroundColour(wxColour("#262E30")); + text_ctrl->SetFont(Label::Body_13); + text_ctrl->SetSize(wxSize(-1, FromDIP(16))); // Centers text vertically + + text_ctrl->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& e) { + if (p->dia->IsShown()) { + e.Skip(); + return; + } + p->m_search_bar->SetBorderColor(wxColour("#009688")); + wxPoint pos = this->p->m_search_bar->ClientToScreen(wxPoint(0, 0)); +#ifndef __WXGTK__ + pos.y += this->p->m_search_bar->GetRect().height; +#else + this->p->m_search_item->Enable(false); +#endif + p->dia->SetPosition(pos); + p->dia->Popup(); + e.Skip(); // required to show caret + }); + + auto search_sizer = new wxBoxSizer(wxHORIZONTAL); + search_sizer->Add(new wxWindow(p->m_search_bar, wxID_ANY, wxDefaultPosition, wxSize(0, 0)), 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(1)); + search_sizer->Add(p->m_search_item, 1, wxEXPAND | wxALL, FromDIP(2)); + p->m_search_bar->SetSizer(search_sizer); + p->m_search_bar->Layout(); + search_sizer->Fit(p->m_search_bar); + + p->m_object_list = new ObjectList(p->scrolled); + p->m_object_list->Bind(wxCUSTOMEVT_EXIT_SEARCH, [this](wxCommandEvent&) { +#ifdef __WXGTK__ + this->p->m_search_item->Enable(true); +#endif + this->p->m_search_bar->SetBorderColor(wxColour("#CECECE")); + this->p->m_search_item->GetTextCtrl()->SetValue(""); // reset value when close + }); + + p->sizer_params->Add(p->m_search_bar, 0, wxALL | wxEXPAND, 0); + p->sizer_params->Add(p->m_object_list, 1, wxEXPAND | wxTOP, 0); + scrolled_sizer->Add(p->sizer_params, 2, wxEXPAND | wxLEFT, 0); + p->m_object_list->Hide(); + p->m_search_bar->Hide(); + // Frequently Object Settings + p->object_settings = new ObjectSettings(p->scrolled); + + p->dia = new Search::SearchObjectDialog(p->m_object_list, p->scrolled->GetParent(), p->m_search_item); +#if !NEW_OBJECT_SETTING + p->object_settings->Hide(); + p->sizer_params->Add(p->object_settings->get_sizer(), 0, wxEXPAND | wxTOP, 5 * em / 10); +#else + if (params_panel) { + params_panel->Reparent(p->scrolled); + scrolled_sizer->Add(params_panel, 3, wxEXPAND); + } +#endif + } + + p->object_layers = new ObjectLayers(p->scrolled); + p->object_layers->Hide(); + p->sizer_params->Add(p->object_layers->get_sizer(), 0, wxEXPAND | wxTOP, 0); + + auto *sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(p->scrolled, 1, wxEXPAND); + SetSizer(sizer); +} + +Sidebar::~Sidebar() {} + +void Sidebar::on_enter_image_printer_bed(wxMouseEvent &evt) { + //p->image_printer_bed->Bind(wxEVT_LEAVE_WINDOW, &Sidebar::on_leave_image_printer_bed, this); + auto pos = p->panel_printer_bed->GetScreenPosition(); + auto rect = p->panel_printer_bed->GetRect(); + wxPoint temp_pos(pos.x + rect.GetWidth() + FromDIP(3), pos.y); + if (p->big_bed_image_popup == nullptr) + p->big_bed_image_popup = new ImageDPIFrame(); + auto image_path = get_cur_select_bed_image(); + p->big_bed_image_popup->set_bitmap(create_scaled_bitmap("big_" + image_path, p->big_bed_image_popup, p->big_bed_image_popup->get_image_px())); + p->big_bed_image_popup->set_title(p->combo_printer_bed->GetString(p->combo_printer_bed->GetSelection())); + p->big_bed_image_popup->SetCanFocus(false); + p->big_bed_image_popup->SetPosition(temp_pos); + p->big_bed_image_popup->on_show(); +} + +void Sidebar::on_leave_image_printer_bed(wxMouseEvent &evt) { + //auto pos_x = evt.GetX(); + //auto pos_y = evt.GetY(); + //auto rect = p->image_printer_bed->GetRect(); + //if ((pos_x <= 0 || pos_y <= 0 || pos_x >= rect.GetWidth()) && p->big_bed_image_popup) { + if (p->big_bed_image_popup) { + bool was_visible = p->big_bed_image_popup->IsShown(); + p->big_bed_image_popup->on_hide(); + if(!p->combo_printer_bed->is_drop_down() && was_visible) + p->combo_printer_bed->SetFocus(); // set focus back to bed type combo. this prevents weird look if focus on other item + } +} +void Sidebar::on_change_color_mode(bool is_dark) { + const ModelObjectPtrs &mos = wxGetApp().model().objects; + for (int i = 0; i < mos.size(); i++) { + wxGetApp().obj_list()->update_info_items(i,nullptr,false,true); + } + +} + +void Sidebar::create_printer_preset() +{ + CreatePrinterPresetDialog dlg(wxGetApp().mainframe); + int res = dlg.ShowModal(); + if (wxID_OK == res) { + wxGetApp().load_current_presets(); + wxGetApp().mainframe->update_side_preset_ui(); + update_ui_from_settings(); + update_all_preset_comboboxes(); + CreatePresetSuccessfulDialog success_dlg(wxGetApp().mainframe, SuccessType::PRINTER); + int res = success_dlg.ShowModal(); + if (res == wxID_OK) { + p->editing_filament = -1; + if (p->combo_printer->switch_to_tab()) p->editing_filament = 0; + } + } +} + +void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx) +{ + *combo = new PlaterPresetComboBox(p->m_panel_filament_content, Slic3r::Preset::TYPE_FILAMENT); + (*combo)->set_filament_idx(filament_idx); + + auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); + + // BBS: filament double columns + + // int em = wxGetApp().em_unit(); + if ((filament_idx % 2) == 0) // Dont add right column item. this one create equal spacing on left, right & middle + combo_and_btn_sizer->AddSpacer(FromDIP((filament_idx % 2) == 0 ? 12 : 3)); // Content Margin + + (*combo)->clr_picker->SetLabel(wxString::Format("%d", filament_idx + 1)); + combo_and_btn_sizer->Add((*combo)->clr_picker, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) + combo_and_btn_sizer->Add(*combo, 1, wxALL | wxEXPAND, FromDIP(2))->SetMinSize({-1, 30 * wxGetApp().em_unit() / 10}); // ORCA ensure height matches with PlaterPresetComboBox + + /* BBS hide del_btn + ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "delete_filament"); + del_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e){ + int extruder_count = std::max(1, (int)p->combos_filament.size() - 1); + + update_objects_list_filament_column(std::max(1, extruder_count - 1)); + on_filament_count_change(extruder_count); + wxGetApp().preset_bundle->printers.get_edited_preset().set_num_extruders(extruder_count); + wxGetApp().preset_bundle->update_multi_material_filament_presets(); + }); + + combo_and_btn_sizer->Add(32 * em / 10, 0, 0, 0, 0); + combo_and_btn_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL, 5 * em / 10); + */ + ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "menu_filament"); + edit_btn->SetToolTip(_L("Click to edit preset")); + + PlaterPresetComboBox* combobox = (*combo); + edit_btn->Bind(wxEVT_BUTTON, [this, edit_btn, combobox, filament_idx](wxCommandEvent) { + bool single_or_bbl = should_show_SEMM_buttons(); + bool is_multi_material = p->combos_filament.size() > 1; + if(single_or_bbl && is_multi_material) { + // MULTI MATERIAL Show menu + auto menu = p->plater->filament_action_menu(filament_idx); + wxPoint pt { 0, edit_btn->GetSize().GetHeight() + FromDIP(2) }; + pt = edit_btn->ClientToScreen(pt); + pt = wxGetApp().mainframe->ScreenToClient(pt); + p->m_menu_filament_id = filament_idx; + p->plater->PopupMenu(menu, (int) pt.x, pt.y); + } + else { + // SINGLE MATERIAL / MULTI EXTRUDER / TOOLCHANGER / IDEX Opens Dialog directly + p->editing_filament = filament_idx; + combobox->switch_to_tab(); + } + }); + combobox->edit_btn = edit_btn; + + combo_and_btn_sizer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) + + combo_and_btn_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); + + // BBS: filament double columns + auto side = filament_idx % 2; + auto /***/sizer_filaments = this->p->sizer_filaments->GetItem(side)->GetSizer(); + if (side == 1 && filament_idx > 1) sizer_filaments->Remove(filament_idx / 2); + sizer_filaments->Add(combo_and_btn_sizer, 1, wxEXPAND); + if (side == 0 && filament_idx > 0) { + sizer_filaments = this->p->sizer_filaments->GetItem(1)->GetSizer(); + sizer_filaments->AddStretchSpacer(1); + } +} + +void Sidebar::remove_unused_filament_combos(const size_t current_extruder_count) +{ + if (current_extruder_count >= p->combos_filament.size()) + return; + while (p->combos_filament.size() > current_extruder_count) { + const int last = p->combos_filament.size() - 1; + auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); + sizer_filaments->Remove(last / 2); + (*p->combos_filament[last]).Destroy(); + p->combos_filament.pop_back(); + } + // BBS: filament double columns + auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t)0)->GetSizer(); + auto sizer_filaments1 = this->p->sizer_filaments->GetItem(1)->GetSizer(); + if (current_extruder_count < 2) { + sizer_filaments1->Clear(); + } else { + size_t c0 = sizer_filaments0->GetChildren().GetCount(); + size_t c1 = sizer_filaments1->GetChildren().GetCount(); + if (c0 < c1) + sizer_filaments1->Remove(c1 - 1); + else if (c0 > c1) + sizer_filaments1->AddStretchSpacer(1); + } +} + +void Sidebar::update_all_preset_comboboxes() +{ + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + + bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); + + auto p_mainframe = wxGetApp().mainframe; + auto cfg = preset_bundle.printers.get_edited_preset().config; + + if (preset_bundle.use_bbl_network()) { + //only show connection button for not-BBL printer + //p->btn_connect_printer->Hide(); + p->m_printer_connect->Hide(); + //only show sync-ams button for BBL printer + p->m_bpButton_ams_filament->Show(); + //update print button default value for bbl or third-party printer + p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); + } else { + //p->btn_connect_printer->Show(); + p->m_printer_connect->Show(); + + // ORCA: show/hide sync-ams button based on filament sync mode + auto agent = wxGetApp().getAgent(); + if (agent && agent->get_filament_sync_mode() != FilamentSyncMode::none) + p->m_bpButton_ams_filament->Show(); + else + p->m_bpButton_ams_filament->Hide(); + + auto print_btn_type = MainFrame::PrintSelectType::eExportGcode; + wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); + wxString apikey; + if(url.empty()) + url = wxString::Format("file://%s/web/orca/missing_connection.html", from_u8(resources_dir())); + else { + const auto host_type = cfg.option>("host_type")->value; + if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) + apikey = cfg.opt_string("printhost_apikey"); + print_btn_type = preset_bundle.is_bbl_vendor() ? MainFrame::PrintSelectType::ePrintPlate : MainFrame::PrintSelectType::eSendGcode; + } + + p_mainframe->load_printer_url(url, apikey); + + + p_mainframe->set_print_button_to_default(print_btn_type); + + } + + if (cfg.opt_bool("pellet_modded_printer")) { + p->m_staticText_filament_settings->SetLabel(_L("Pellets")); + p->m_filament_icon->SetBitmap_("pellets"); + } else { + p->m_staticText_filament_settings->SetLabel(_L("Filament")); + p->m_filament_icon->SetBitmap_("filament"); + } + + show_SEMM_buttons(); + + //p->m_staticText_filament_settings->Update(); + + if (is_bbl_vendor || cfg.opt_bool("support_multi_bed_types")) { + p->combo_printer_bed->Enable(); + // Orca: don't update bed type if loading project + if (!p->plater->is_loading_project()) { + bool has_changed = reset_bed_type_combox_choices(); + bool flag = m_begin_sync_printer_status && !has_changed; + if (!(flag)) { + auto str_bed_type = wxGetApp().app_config->get_printer_setting(wxGetApp().preset_bundle->printers.get_selected_preset_name(), + "curr_bed_type"); + if (!str_bed_type.empty()) { + int bed_type_value = atoi(str_bed_type.c_str()); + if (bed_type_value <= 0 || bed_type_value >= btCount) { + bed_type_value = preset_bundle.printers.get_edited_preset().get_default_bed_type(&preset_bundle); + } + + set_bed_type_accord_combox((BedType) bed_type_value); + } else { + BedType bed_type = preset_bundle.printers.get_edited_preset().get_default_bed_type(&preset_bundle); + set_bed_type_accord_combox(bed_type); + } + } else { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":no need reset_bed_type_combox_choices"; + } + } + } else { + // m_bed_type_list->SelectAndNotify(btPEI - 1); + BedType bed_type = preset_bundle.printers.get_edited_preset().get_default_bed_type(&preset_bundle); + set_bed_type_accord_combox(bed_type); + p->combo_printer_bed->Disable(); + } + + // ORCA Hide plate selector if not supported by printer + p->panel_printer_bed->Show(is_bbl_vendor || cfg.opt_bool("support_multi_bed_types")); + + // Update the print choosers to only contain the compatible presets, update the dirty flags. + //BBS + + // Update the printer choosers, update the dirty flags. + //p->combo_printer->update(); + // Update the filament choosers to only contain the compatible presets, update the color preview, + // update the dirty flags. + if (print_tech == ptFFF) { + for (PlaterPresetComboBox* cb : p->combos_filament) + cb->update(); + } + + if (p->combo_printer) { + p->combo_printer->update(); + update_printer_thumbnail(); + } + + // Orca:: show device tab based on vendor type + p_mainframe->show_device(preset_bundle.use_bbl_device_tab()); + p_mainframe->m_tabpanel->SetSelection(p_mainframe->m_tabpanel->GetSelection()); +} + +void Sidebar::update_presets(Preset::Type preset_type) +{ + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%")%preset_type; + switch (preset_type) { + case Preset::TYPE_FILAMENT: + { + // BBS +#if 0 + const size_t extruder_cnt = print_tech != ptFFF ? 1 : + dynamic_cast(preset_bundle.printers.get_edited_preset().config.option("nozzle_diameter"))->values.size(); + const size_t filament_cnt = p->combos_filament.size() > extruder_cnt ? extruder_cnt : p->combos_filament.size(); +#else + const size_t filament_cnt = p->combos_filament.size(); +#endif + const std::string &name = preset_bundle.filaments.get_selected_preset_name(); + if (p->editing_filament >= 0) { + preset_bundle.set_filament_preset(p->editing_filament, name); + } else if (filament_cnt == 1) { + // Single filament printer, synchronize the filament presets. + Preset *preset = preset_bundle.filaments.find_preset(name, false); + if (preset) { + if (preset->is_compatible) preset_bundle.set_filament_preset(0, name); + } + + } + + for (size_t i = 0; i < filament_cnt; i++) + p->combos_filament[i]->update(); + + update_dynamic_filament_list(); + break; + } + + case Preset::TYPE_PRINT: + //wxGetApp().mainframe->m_param_panel; + //p->combo_print->update(); + { + Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT); + if (print_tab) { + print_tab->get_combo_box()->update(); + } + break; + } + case Preset::TYPE_SLA_PRINT: + ;// p->combo_sla_print->update(); + break; + + case Preset::TYPE_SLA_MATERIAL: + ;// p->combo_sla_material->update(); + break; + + case Preset::TYPE_PRINTER: + { + update_all_preset_comboboxes(); + p->show_preset_comboboxes(); + + /* update bed shape */ + Tab* printer_tab = wxGetApp().get_tab(Preset::TYPE_PRINTER); + if (printer_tab) { + printer_tab->update(); + printer_tab->on_preset_loaded(); + } + + Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + + bool isBBL = preset_bundle.is_bbl_vendor(); + + if (auto printer_structure_opt = printer_preset.config.option>("printer_structure")) { + wxGetApp().plater()->get_current_canvas3D()->get_arrange_settings().align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3); + } + else + wxGetApp().plater()->get_current_canvas3D()->get_arrange_settings().align_to_y_axis = false; + + // Update dual extrudes + auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); + + bool is_dual_extruder = nozzle_diameter->size() == 2; + p->layout_printer(preset_bundle.use_bbl_network(), isBBL && is_dual_extruder); + auto diameters = wxGetApp().preset_bundle->printers.diameters_of_selected_printer(); + auto diameter = printer_preset.config.opt_string("printer_variant"); + auto update_extruder_diameter = [&diameters, &diameter, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) { + extruder.combo_diameter->Clear(); + int select = -1; + // ORCA get the actual nozzle diameter from printer config + auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]); + // ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias + if(diameters[0].empty() && !nozzle_dia.empty()){ + diameters[0] = nozzle_dia; + } + // Orca: Check if the actual nozzle diameter exists in the list, if not add it as a custom option + if (std::find(diameters.begin(), diameters.end(), nozzle_dia) == diameters.end() && !nozzle_dia.empty()) { + diameters.push_back(nozzle_dia); + } + for (size_t i = 0; i < diameters.size(); ++i) { + if (diameters[i] == nozzle_dia) + select = extruder.combo_diameter->GetCount(); + extruder.combo_diameter->Append(diameters[i], {}); + } + extruder.combo_diameter->SetSelection(select); + extruder.diameter = nozzle_dia; + }; + auto image_path = get_cur_select_bed_image(); + if (is_dual_extruder) { + std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle); + p->left_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); + p->right_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); + AMSCountPopupWindow::UpdateAMSCount(0, p->left_extruder); + AMSCountPopupWindow::UpdateAMSCount(1, p->right_extruder); + //if (!p->is_switching_diameter) { + update_extruder_diameter(0, *p->left_extruder); + update_extruder_diameter(1, *p->right_extruder); + //} + p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); + } else { + AMSCountPopupWindow::UpdateAMSCount(0, p->single_extruder); + //if (!p->is_switching_diameter) + update_extruder_diameter(0, *p->single_extruder); + + // ORCA sync unified nozzle combo box + p->combo_nozzle_dia->Clear(); + for (size_t i = 0; i < diameters.size(); ++i) + p->combo_nozzle_dia->Append(diameters[i], {}); + p->combo_nozzle_dia->SetSelection((*p->single_extruder).combo_diameter->GetSelection()); + + // ORCA update nozzle type + const auto& full_config = wxGetApp().preset_bundle->full_config(); + wxString nozzle_type = "-"; + const ConfigOptionEnumsGenericNullable* cfg_nozzle_type = full_config.option("nozzle_type"); + if(cfg_nozzle_type != nullptr){ + std::vector nozzle_types(cfg_nozzle_type->size()); + for (size_t idx = 0; idx < cfg_nozzle_type->size(); ++idx) + nozzle_types[idx] = NozzleType(cfg_nozzle_type->values[idx]); + nozzle_type = _L( // NEEDFIX this part can be replaced with shorter names + nozzle_types[0] == ntHardenedSteel ? "Hardened Steel" : + nozzle_types[0] == ntStainlessSteel ? "Stainless Steel" : + nozzle_types[0] == ntTungstenCarbide ? "Tungsten Carbide" : + nozzle_types[0] == ntBrass ? "Brass" + : "-" // Undefined + ); + } + p->label_nozzle_type->SetLabel(nozzle_type); + p->label_nozzle_type->SetToolTip(nozzle_type == "-" ? "" : nozzle_type); + + p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); + } + + if (GUI::wxGetApp().plater()) + GUI::wxGetApp().plater()->update_machine_sync_status(); + + Layout(); + + break; + } + + default: break; + } + + // Synchronize config.ini with the current selections. + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": exit."); +} + +//BBS +void Sidebar::update_presets_from_to(Slic3r::Preset::Type preset_type, std::string from, std::string to) +{ + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%, from %2% to %3%")%preset_type %from %to; + + switch (preset_type) { + case Preset::TYPE_FILAMENT: + { + const size_t filament_cnt = p->combos_filament.size(); + for (auto it = preset_bundle.filament_presets.begin(); it != preset_bundle.filament_presets.end(); it++) + { + if ((*it).compare(from) == 0) { + (*it) = to; + } + } + for (size_t i = 0; i < filament_cnt; i++) + p->combos_filament[i]->update(); + break; + } + + default: break; + } + + // Synchronize config.ini with the current selections. + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": exit!"); +} + +BedType Sidebar::get_cur_select_bed_type() { + int selection = p->combo_printer_bed->GetSelection(); + if (selection < 0 && selection >= m_cur_combox_bed_types.size()) { + p->combo_printer_bed->SetSelection(0); + selection = 0; + } + auto select_bed_type = m_cur_combox_bed_types[selection]; + return select_bed_type; +} + +std::string Sidebar::get_cur_select_bed_image() +{ + auto select_bed_type = get_cur_select_bed_type(); + //auto series_suffix_str = m_cur_image_bed_type.empty() ? "" : ("_" + m_cur_image_bed_type); + auto image_path = bed_type_thumbnails[select_bed_type];// + series_suffix_str; + return image_path; +} + +void Sidebar::set_bed_type_accord_combox(BedType bed_type) { + for (size_t i = 0; i < m_cur_combox_bed_types.size(); i++) { + if (m_cur_combox_bed_types[i] == bed_type) { + p->combo_printer_bed->SelectAndNotify(i); + return; + } + } + p->combo_printer_bed->SelectAndNotify(0); +} + +bool Sidebar::reset_bed_type_combox_choices(bool is_sidebar_init) +{ + if (!p->combo_printer_bed) { + return false; + } + + auto pm = p->plater->get_curr_printer_model(); + if (pm) { + if (m_cur_image_bed_type != pm->image_bed_type) { + m_cur_image_bed_type = pm->image_bed_type; + } + } + if (m_last_combo_bedtype_count != 0 && pm) { + auto cur_count = (int) BedType::btCount - 1 - pm->not_support_bed_types.size(); + if (cur_count == m_last_combo_bedtype_count) {//no change + return false; + } + } + const ConfigOptionDef *bed_type_def = print_config_def.get("curr_bed_type"); + p->combo_printer_bed->Clear(); + m_cur_combox_bed_types.clear(); + if (pm &&bed_type_def && bed_type_def->enum_keys_map) { + int index = 0; + for (auto item : bed_type_def->enum_labels) { + index++; + bool find = std::find(pm->not_support_bed_types.begin(), pm->not_support_bed_types.end(), item) != pm->not_support_bed_types.end(); + if (find) { + continue; + } + m_cur_combox_bed_types.emplace_back(BedType(index));//BedType //btPC =1 + p->combo_printer_bed->AppendString(_L(item)); + } + } + else { + m_cur_image_bed_type = ""; + int index = 0; + for (auto item : bed_type_def->enum_labels) { + index++; + m_cur_combox_bed_types.emplace_back(BedType(index)); // BedType //btPC =1 + p->combo_printer_bed->AppendString(_L(item)); + } + } + m_last_combo_bedtype_count = p->combo_printer_bed->GetCount(); + if (!is_sidebar_init && &p->plater->get_partplate_list()) { + p->plater->get_partplate_list().check_all_plate_local_bed_type(m_cur_combox_bed_types); + } + return true; +} + +void Sidebar::change_top_border_for_mode_sizer(bool increase_border) +{ + // BBS +#if 0 + if (p->mode_sizer) { + p->mode_sizer->set_items_flag(increase_border ? wxTOP : 0); + p->mode_sizer->set_items_border(increase_border ? int(0.5 * wxGetApp().em_unit()) : 0); + } +#endif +} + +void Sidebar::update_filaments_area_height() +// ORCA +{ + // ORCA use a height with user preference + auto left_sizer = p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); + auto combo_sizer = left_sizer->GetItem((size_t) 0)->GetSizer(); + int preferred_rows = std::ceil(0.5 * std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count"))); + auto height_with_borders = combo_sizer->GetSize().GetHeight(); // gets height from sizer instead static numbers + p->m_panel_filament_content->SetMaxSize(wxSize{-1, preferred_rows * height_with_borders}); + + // fixes wxScrolledWindow not shrinks its height to content size + auto min_size = p->m_panel_filament_content->GetSizer()->GetMinSize(); + if (min_size.y > p->m_panel_filament_content->GetMaxHeight()) + min_size.y = p->m_panel_filament_content->GetMaxHeight(); + p->m_panel_filament_content->SetMinSize({-1, min_size.y}); +} + +void Sidebar::msw_rescale() +{ + SetMinSize(wxSize(39 * wxGetApp().em_unit(), -1)); + p->m_panel_printer_title->GetSizer()->SetMinSize(-1, 3 * wxGetApp().em_unit()); + p->m_panel_filament_title->GetSizer() + ->SetMinSize(-1, 3 * wxGetApp().em_unit()); + p->m_printer_icon->msw_rescale(); + p->m_printer_connect->msw_rescale(); + p->m_printer_bbl_sync->msw_rescale(); + p->m_printer_icon->msw_rescale(); + p->m_printer_setting->msw_rescale(); + + p->panel_printer_preset->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); + p->panel_printer_preset->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); + p->image_printer->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE)); + update_printer_thumbnail(); + p->combo_printer->Rescale(); + p->combo_printer->SetMaxSize(wxSize(-1, FromDIP(30))); // limiting height makes badge visible + p->btn_edit_printer->msw_rescale(); + + p->panel_nozzle_dia->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); + p->panel_nozzle_dia->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); + p->combo_nozzle_dia->Rescale(); + + p->panel_printer_bed->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); + p->panel_printer_bed->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); + p->combo_printer_bed->Rescale(); + p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow + p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow + bool isDual = static_cast(p->panel_printer_preset->GetSizer())->GetOrientation() == wxVERTICAL; + auto image_path = get_cur_select_bed_image(); + p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); + if (p->big_bed_image_popup){ // ORCA force rebuild frame. current wxwidget version not supports wxBITMAP_SCALE_FILL flag on wxStaticBitmap + // also wxImage scaledImage = bit_map.ConvertToImage(); scaledImage.Rescale(FromDIP(m_image_px), FromDIP(m_image_px), wxIMAGE_QUALITY_HIGH); + // didnt worked as expected and it requires use on set_bitmap. so that will try to scale everytime + p->big_bed_image_popup->Destroy(); + p->big_bed_image_popup = nullptr; + } + + p->m_filament_icon->msw_rescale(); + p->m_bpButton_add_filament->msw_rescale(); + p->m_bpButton_del_filament->msw_rescale(); + p->m_bpButton_ams_filament->msw_rescale(); + p->m_bpButton_set_filament->msw_rescale(); + p->m_flushing_volume_btn->Rescale(); + set_flushing_volume_warning(is_flush_config_modified()); // ORCA reapply appearance + + //BBS + p->left_extruder->Rescale(); + p->right_extruder->Rescale(); + p->single_extruder->Rescale(); + + //p->btn_sync_printer->SetPaddingSize({FromDIP(6), FromDIP(12)}); + //p->btn_sync_printer->SetMinSize(BTN_SYNC_SIZE); + //p->btn_sync_printer->Rescale(); +#if 0 + if (p->mode_sizer) + p->mode_sizer->msw_rescale(); +#endif + + //for (PlaterPresetComboBox* combo : std::vector { p->combo_print, + // //p->combo_sla_print, + // //p->combo_sla_material, + // //p->combo_printer + // } ) + // combo->msw_rescale(); + + for (PlaterPresetComboBox* combo : p->combos_filament) + combo->msw_rescale(); + + p->m_panel_filament_content->Layout(); + update_filaments_area_height(); // ORCA resize after combos scaled + + // BBS + //p->frequently_changed_parameters->msw_rescale(); + //obj_list()->msw_rescale(); + // BBS TODO: add msw_rescale for newly added windows + // BBS + //p->object_manipulation->msw_rescale(); + p->object_settings->msw_rescale(); + p->m_search_item->Rescale(); + p->m_search_item->GetTextCtrl()->SetSize(wxSize(-1, FromDIP(16))); + p->m_search_bar->Layout(); + + // BBS +#if 0 + p->object_info->msw_rescale(); + + p->btn_send_gcode->msw_rescale(); +// p->btn_eject_device->msw_rescale(); + p->btn_export_gcode_removable->msw_rescale(); +#ifdef _WIN32 + const int scaled_height = p->btn_export_gcode_removable->GetBitmapHeight(); +#else + const int scaled_height = p->btn_export_gcode_removable->GetBitmapHeight() + 4; +#endif + p->btn_export_gcode->SetMinSize(wxSize(-1, scaled_height)); + p->btn_reslice ->SetMinSize(wxSize(-1, scaled_height)); +#endif + p->scrolled->Layout(); + + p->searcher.dlg_msw_rescale(); +} + +void Sidebar::sys_color_changed() +{ + wxWindowUpdateLocker noUpdates(this); + +#if 0 + for (wxWindow* win : std::vector{ this, p->sliced_info->GetStaticBox(), p->object_info->GetStaticBox(), p->btn_reslice, p->btn_export_gcode }) + wxGetApp().UpdateDarkUI(win); + p->object_info->msw_rescale(); + + for (wxWindow* win : std::vector{ p->scrolled, p->presets_panel }) + wxGetApp().UpdateAllStaticTextDarkUI(win); +#endif + //p->btn_sync_printer->SetIcon("printer_sync"); + p->m_printer_bbl_sync->msw_rescale(); + p->m_printer_connect->msw_rescale(); + // for (wxWindow* btn : std::vector{ p->btn_reslice, p->btn_export_gcode }) + // wxGetApp().UpdateDarkUI(btn, true); + p->m_printer_icon->msw_rescale(); + p->m_printer_setting->msw_rescale(); + p->m_printer_setting->msw_rescale(); + p->m_filament_icon->msw_rescale(); + p->m_bpButton_add_filament->msw_rescale(); + p->m_bpButton_del_filament->msw_rescale(); + p->m_bpButton_ams_filament->msw_rescale(); + p->m_bpButton_set_filament->msw_rescale(); + p->m_flushing_volume_btn->Rescale(); + set_flushing_volume_warning(is_flush_config_modified()); // ORCA reapply appearance + + // BBS +#if 0 + if (p->mode_sizer) + p->mode_sizer->msw_rescale(); + p->frequently_changed_parameters->sys_color_changed(); +#endif + p->object_settings->sys_color_changed(); + + //BBS: remove print related combos +#if 0 + for (PlaterPresetComboBox* combo : std::vector{ p->combo_print, + p->combo_sla_print, + p->combo_sla_material, + p->combo_printer }) + combo->sys_color_changed(); +#endif + p->combo_printer->sys_color_changed(); + for (PlaterPresetComboBox* combo : p->combos_filament) + combo->sys_color_changed(); + + if (p->big_bed_image_popup) // ORCA + p->big_bed_image_popup->sys_color_changed(); + + p->btn_edit_printer->msw_rescale(); + p->image_printer->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE)); + p->image_printer_bed->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE)); + + // call a kill focus event to ensure new colors applied + for (ComboBox* combo : std::vector{p->combo_printer, p->combo_nozzle_dia, p->combo_printer_bed}){ + wxFocusEvent fakeEvent(wxEVT_KILL_FOCUS); + fakeEvent.SetEventObject(combo); + combo->HandleWindowEvent(fakeEvent); + } + + // BBS + obj_list()->sys_color_changed(); + obj_layers()->sys_color_changed(); + // BBS + //p->object_manipulation->sys_color_changed(); + + // btn...->msw_rescale() updates icon on button, so use it + //p->btn_send_gcode->msw_rescale(); +// p->btn_eject_device->msw_rescale(); + //p->btn_export_gcode_removable->msw_rescale(); + + p->scrolled->Layout(); + + p->searcher.dlg_sys_color_changed(); +} + +void Sidebar::search() +{ + p->searcher.search(); +} + +void Sidebar::jump_to_option(const std::string& opt_key, Preset::Type type, const std::wstring& category) +{ + //const Search::Option& opt = p->searcher.get_option(opt_key, type); + if (type == Preset::TYPE_PRINT) { + auto tab = dynamic_cast(wxGetApp().params_panel()->get_current_tab()); + if (tab && tab->has_key(opt_key)) { + tab->activate_option(opt_key, category); + return; + } + wxGetApp().params_panel()->switch_to_global(); + } + wxGetApp().get_tab(type)->activate_option(opt_key, category); +} + +void Sidebar::jump_to_option(size_t selected) +{ + const Search::Option& opt = p->searcher.get_option(selected); + jump_to_option(opt.opt_key(), opt.type, opt.category); + + // Switch to the Settings NotePad +// wxGetApp().mainframe->select_tab(); +} + +// BBS. Move logic from Plater::on_extruders_change() to Sidebar::on_filament_count_change(). +void Sidebar::on_filament_count_change(size_t num_filaments) +{ + auto& choices = combos_filament(); + + if (num_filaments == choices.size()) + return; + + if (choices.size() == 1 || num_filaments == 1) + choices[0]->GetDropDown().Invalidate(); + + wxWindowUpdateLocker noUpdates_scrolled_panel(this); + + size_t i = choices.size(); + while (i < num_filaments) + { + PlaterPresetComboBox* choice/*{ nullptr }*/; + init_filament_combo(&choice, i); + int last_selection = choices.back()->GetSelection(); + choices.push_back(choice); + + // initialize selection + choice->update(); + choice->SetSelection(last_selection); + ++i; + } + + // remove unused choices if any + remove_unused_filament_combos(num_filaments); + + show_SEMM_buttons(); // ORCA + + update_filaments_area_height(); // ORCA + + Layout(); + p->m_panel_filament_title->Refresh(); + update_ui_from_settings(); + update_dynamic_filament_list(); +} + +void Sidebar::on_filaments_delete(size_t filament_id) +{ + auto &choices = combos_filament(); + + if (filament_id >= choices.size()) + return; + + if (choices.size() == 1) + choices[0]->GetDropDown().Invalidate(); + + wxWindowUpdateLocker noUpdates_scrolled_panel(this); + + // delete UI item + if (filament_id < p->combos_filament.size()) { + const int last = p->combos_filament.size() - 1; + auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); + sizer_filaments->Remove(last / 2); + + PlaterPresetComboBox* to_delete_combox = p->combos_filament[filament_id]; + (*p->combos_filament[last]).Destroy(); + p->combos_filament.pop_back(); + + // BBS: filament double columns + auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); + auto sizer_filaments1 = this->p->sizer_filaments->GetItem(1)->GetSizer(); + if (p->combos_filament.size() < 2) { + sizer_filaments1->Clear(); + } else { + size_t c0 = sizer_filaments0->GetChildren().GetCount(); + size_t c1 = sizer_filaments1->GetChildren().GetCount(); + if (c0 < c1) + sizer_filaments1->Remove(c1 - 1); + else if (c0 > c1) + sizer_filaments1->AddStretchSpacer(1); + } + } + + show_SEMM_buttons(); // ORCA + + for (size_t idx = filament_id ; idx < p->combos_filament.size(); ++idx) { + p->combos_filament[idx]->update(); + } + + update_filaments_area_height(); // ORCA + + Layout(); + p->m_panel_filament_title->Refresh(); + update_ui_from_settings(); + dynamic_filament_list.update(); +} + +void Sidebar::add_filament() { + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + wxColour new_col = Plater::get_next_color_for_filament(); + add_custom_filament(new_col); +} + +void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) { + if (is_new_project_in_gcode3mf()) { return; } + if (p->combos_filament.size() <= 1) return; + + size_t filament_count = p->combos_filament.size() - 1; + if (filament_id == size_t(-2)) { + filament_id = p->m_menu_filament_id; + } + if (filament_id == size_t(-1)) { + filament_id = filament_count; + } + + if (filament_id > filament_count) + return; + + if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + } + + if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { + p->editing_filament = -1; + } + + wxGetApp().preset_bundle->update_num_filaments(filament_id); + wxGetApp().plater()->get_partplate_list().on_filament_deleted(filament_count, filament_id); + wxGetApp().plater()->on_filaments_delete(filament_count, filament_id, replace_filament_id > (int)filament_id ? (replace_filament_id - 1) : replace_filament_id); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + + wxGetApp().plater()->update(); +} + +void Sidebar::change_filament(size_t from_id, size_t to_id) +{ + delete_filament(from_id, int(to_id)); +} + +void Sidebar::edit_filament() +{ + p->editing_filament = -1; + if (p->m_menu_filament_id >= 0 && p->m_menu_filament_id < p->combos_filament.size() + && p->combos_filament[p->m_menu_filament_id]->switch_to_tab()) + p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx +} + +void Sidebar::add_custom_filament(wxColour new_col) { + if (is_new_project_in_gcode3mf()) { return; } + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + + int filament_count = p->combos_filament.size() + 1; + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); + wxGetApp().plater()->get_partplate_list().on_filament_added(filament_count); + wxGetApp().plater()->on_filament_count_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + auto_calc_flushing_volumes(filament_count - 1); +} + +bool Sidebar::is_new_project_in_gcode3mf() +{ + if (p->plater->using_exported_file()) { + auto filename = p->plater->get_preview_only_filename(); + auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename); + MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"), + wxOK | wxICON_WARNING); + dlg.ShowModal(); + p->plater->new_project(); + return true; + } + return false; +} + +void Sidebar::on_bed_type_change(BedType bed_type) +{ + // Orca: Map BedType to the current combo list (some printers filter types). + + if (p->combo_printer_bed == nullptr) + return; + + for (size_t i = 0; i < m_cur_combox_bed_types.size(); ++i) { + if (m_cur_combox_bed_types[i] == bed_type) { + p->combo_printer_bed->SetSelection(int(i)); + return; + } + } + + if (!m_cur_combox_bed_types.empty()) + p->combo_printer_bed->SetSelection(0); +} + +/** + * Build a map of filament configurations from the connected printer's AMS (Automatic Material System). + * + * Data Flow Architecture: + * ======================= + * This function reads pre-populated state from MachineObject - it does NOT directly call + * NetworkAgent APIs. The data pipeline is: + * + * Printer Device (MQTT/LAN messages) + * ↓ + * NetworkAgent (receives JSON, triggers OnMessageFn callbacks) + * ↓ + * MachineObject::parse_json() (updates device state) + * ├── vt_slot (std::vector) - virtual tray data for external filament + * └── DevFilaSystem → DevAms → DevAmsTray - AMS unit hierarchy + * ↓ + * build_filament_ams_list() [THIS FUNCTION] - aggregates into DynamicPrintConfig maps + * + * Data Sources: + * - obj->vt_slot: Virtual trays for external/manual filament loading (when ams_support_virtual_tray is true) + * - obj->GetFilaSystem()->GetAmsList(): Map of AMS units, each containing multiple DevAmsTray slots + * + * Return Value: + * - Map key encoding: + * - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary) + * - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary) + * - The 0x10000 flag indicates the main/right extruder + * - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.) + * + * @param obj The MachineObject representing the connected printer (nullable) + * @return Map of tray indices to filament configurations + */ +std::map Sidebar::build_filament_ams_list(MachineObject* obj) +{ + std::map filament_ams_list; + if (!obj) return filament_ams_list; + + // For pull-mode agents (e.g., HTTP REST API), refresh DevFilaSystem first + auto* agent = wxGetApp().getDeviceManager()->get_agent(); + if (agent && agent->get_filament_sync_mode() == FilamentSyncMode::pull) { + if (!agent->fetch_filament_info(obj->get_dev_id())) { + return filament_ams_list; + } + } + + auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) { + BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%") + % name % tray.setting_id % tray.m_fila_type % tray.color; + DynamicPrintConfig tray_config; + tray_config.set_key_value("filament_id", new ConfigOptionStrings{tray.setting_id}); + tray_config.set_key_value("tag_uid", new ConfigOptionStrings{tray.tag_uid}); + tray_config.set_key_value("ams_id", new ConfigOptionStrings{ams_id}); + tray_config.set_key_value("slot_id", new ConfigOptionStrings{slot_id}); + tray_config.set_key_value("filament_type", new ConfigOptionStrings{tray.m_fila_type}); + tray_config.set_key_value("tray_name", new ConfigOptionStrings{ name }); + const std::string filament_color = into_u8(wxColour("#" + tray.color).GetAsString(wxC2S_HTML_SYNTAX)); + tray_config.set_key_value("filament_colour", new ConfigOptionStrings{filament_color}); + tray_config.set_key_value("filament_multi_colour", new ConfigOptionStrings{}); + tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::to_string(tray.ctype)}); + tray_config.set_key_value("filament_exist", new ConfigOptionBools{tray.is_exists}); + tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder}); + tray_config.set_key_value("filament_sub_brands", new ConfigOptionStrings{tray.sub_brands}); + std::optional info; + if (wxGetApp().preset_bundle) { + info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id); + } + tray_config.set_key_value("filament_is_support", new ConfigOptionBools{ info.has_value() ? info->is_support : false}); + for (int i = 0; i < tray.cols.size(); ++i) { + tray_config.opt("filament_multi_colour")->values.push_back(into_u8(wxColour("#" + tray.cols[i]).GetAsString(wxC2S_HTML_SYNTAX))); + } + if (tray_config.opt("filament_multi_colour")->values.empty() && !filament_color.empty()) { + tray_config.opt("filament_multi_colour")->values.push_back(filament_color); + } + return tray_config; + }; + + if (obj->ams_support_virtual_tray) { + int extruder = 0x10000; // Main (first) extruder at right + for (auto & vt_tray : obj->vt_slot) { + if (!vt_tray.is_exists && vt_tray.setting_id.empty() && vt_tray.m_fila_type.empty() && vt_tray.color.empty()) { + extruder = 0; + continue; + } + filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext",vt_tray.id, "0"));//254 or 255 + extruder = 0; + } + } + + auto get_ams_name = [](int ams_id, int slot_id)->std::string { + if (ams_id >= 0 && ams_id < 26) { + char slot_name = slot_id + '1'; + return std::string(1, 'A' + ams_id) + std::string(1, slot_name); + } else if (ams_id >= 128 && ams_id < 153) { + return "HT-" + std::string(1, 'A' + (ams_id - 128)); + } else { + assert(false); + } + return std::string(); + }; + + auto list = obj->GetFilaSystem()->GetAmsList(); + for (auto ams : list) { + int ams_id = std::stoi(ams.first); + int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right + for (auto tray : ams.second->GetTrays()) { + int slot_id = std::stoi(tray.first); + filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id), + build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id))); + } + } + return filament_ams_list; +} + +bool Sidebar::sync_extruder_list() +{ + bool only_external_material; + return p->sync_extruder_list(only_external_material); +} + +bool Sidebar::need_auto_sync_extruder_list_after_connect_priner(const MachineObject *obj) +{ + if(!obj) + return false; + + std::string machine_print_name = obj->get_show_printer_type(); + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); + if (machine_print_name != target_model_id) { + return false; + } + + if (preset_bundle->get_printer_extruder_count() <= 1 || !obj->is_multi_extruders()) + return false; + + return true; +} + +void Sidebar::update_sync_status(const MachineObject *obj) +{ + p->update_sync_status(obj); +} + +int Sidebar::get_sidebar_pos_right_x() +{ + return this->GetScreenPosition().x + this->GetSize().x; +} + +void Sidebar::on_size(SimpleEvent &e) { + if (m_sna_dialog && m_sna_dialog->IsShown()) { + pop_sync_nozzle_and_ams_dialog(); + } + if (m_fna_dialog && m_fna_dialog->IsShown()) { + pop_finsish_sync_ams_dialog(); + } +} + +void Sidebar::on_full_screen(IntEvent &e) { + if (m_sna_dialog) { m_sna_dialog->on_full_screen(e); } + if (m_fna_dialog) { m_fna_dialog->on_full_screen(e); } +} + +void Sidebar::get_big_btn_sync_pos_size(wxPoint &pt, wxSize &size) +{ + size = p->m_printer_bbl_sync->GetSize(); + pt = p->m_printer_bbl_sync->GetScreenPosition(); +} + +void Sidebar::get_small_btn_sync_pos_size(wxPoint &pt, wxSize &size) { + size = ams_btn->GetSize(); + pt = ams_btn->GetScreenPosition(); +} + +void Sidebar::load_ams_list(MachineObject* obj) +{ + std::map filament_ams_list; + + // build_filament_ams_list handles both subscription-based and non-subscription-based agents: + // - For non-subscription agents, it calls fetch_filament_info() first to populate DevFilaSystem + // - Then it always reads from DevFilaSystem to build the filament list + if (obj) { + filament_ams_list = build_filament_ams_list(obj); + } + + bool device_change = false; + const std::string& device = obj ? obj->get_dev_id() : ""; + if (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(); + if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) + { + return; + } + wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; + + for (auto c : p->combos_filament){ + c->update(); + if (device_change) { + c->ShowBadge(false);//change printer,then clear badge + } + } + + p->combo_printer->update(); +} + +void Sidebar::sync_ams_list(bool is_from_big_sync_btn) +{ + wxBusyCursor cursor; + // Force load ams list + auto obj = wxGetApp().getDeviceManager()->get_selected_machine(); + if (!obj) + return; + GUI::wxGetApp().sidebar().load_ams_list(obj); + + auto & list = wxGetApp().preset_bundle->filament_ams_list; + if (list.empty()) { + auto printer_name = p->plater->get_selected_printer_name_in_combox(); + p->plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, _L("Sync printer information")); + return; + } + bool exist_at_list_one_filament =false; + for (auto &cur : list) { + auto temp_config = cur.second; + auto filament_type = temp_config.opt_string("filament_type", 0u); + auto filament_color = temp_config.opt_string("filament_colour", 0u); + if (!filament_type.empty() || temp_config.opt_bool("filament_exist", 0u)) { + exist_at_list_one_filament = true; + break; + } + } + if (!exist_at_list_one_filament) { + if (!obj->is_filament_installed()) { + p->plater->pop_warning_and_go_to_device_page("", Plater::PrinterWarningType::UNINSTALL_FILAMENT, _L("Sync printer information")); + return; + } + p->plater->pop_warning_and_go_to_device_page("", Plater::PrinterWarningType::EMPTY_FILAMENT, _L("Sync printer information")); + return; + } + auto* agent = wxGetApp().getDeviceManager()->get_agent(); + const bool direct_pull_sync = agent && agent->get_filament_sync_mode() == FilamentSyncMode::pull; + if (!direct_pull_sync && !wxGetApp().plater()->is_same_printer_for_connected_and_selected()) { + return; + } + std::string ams_filament_ids = wxGetApp().app_config->get("ams_filament_ids", p->ams_list_device); + std::vector list2; + if (!ams_filament_ids.empty()) { + boost::algorithm::split(list2, ams_filament_ids, boost::algorithm::is_any_of(",")); + } + SyncAmsInfoDialog::SyncResult sync_result; + int dlg_res{(int) wxID_YES}; + if (direct_pull_sync) { + sync_result.direct_sync = true; + sync_result.is_same_printer = true; + } else { + wxGetApp().plater()->update_all_plate_thumbnails(true);//preview thumbnail for sync_dlg + SyncAmsInfoDialog::SyncInfo temp_info; + temp_info.use_dialog_pos = false; + temp_info.cancel_text_to_later = is_from_big_sync_btn; + if (m_sync_dlg == nullptr) { + m_sync_dlg = new SyncAmsInfoDialog(this, temp_info); + } else { + m_sync_dlg->set_info(temp_info); + } + dlg_res = (int) wxID_CANCEL; + if (m_sync_dlg->is_need_show()) { + m_sync_dlg->deal_only_exist_ext_spool(obj); + if (m_sync_dlg->is_dirty_filament()) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", false, true); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + dynamic_filament_list.update(); + } + m_sync_dlg->set_check_dirty_fialment(false); + dlg_res = m_sync_dlg->ShowModal(); + } else { + dlg_res =(int) wxID_YES; + } + } + if (dlg_res == wxID_CANCEL) + return; + if (!direct_pull_sync) + sync_result = m_sync_dlg->get_result(); + if (!sync_result.is_same_printer) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "check error: sync_result.is_same_printer value is false"; + return; + } + list2.resize(list.size()); + auto iter = list.begin(); + for (int i = 0; i < list.size(); ++i, ++iter) { + auto & ams = iter->second; + auto filament_id = ams.opt_string("filament_id", 0u); + ams.set_key_value("filament_changed", new ConfigOptionBool{dlg_res == wxID_YES || list2[i] != filament_id}); + list2[i] = filament_id; + } + + // BBS:Record consumables information before synchronization + std::vector color_before_sync; + std::vector is_support_before; + DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; + ConfigOptionStrings* color_opt = project_config.option("filament_colour"); + for (int i = 0; i < p->combos_filament.size(); ++i) { + is_support_before.push_back(is_support_filament(i)); + color_before_sync.push_back(color_opt->values[i]); + } + MergeFilamentInfo merge_info; + std::vector> unknowns; + auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams"); + auto sync_color_only = wxGetApp().app_config->get("sync_ams_filament_mode") == "1"; + auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns, !sync_result.direct_sync, sync_result.sync_maps, enable_append, merge_info, sync_color_only); + wxString detail; + for (auto & uk : unknowns) { + auto tray_name = uk.first->opt_string("tray_name", 0u); + auto filament_type = uk.first->opt_string("filament_type", 0u); + detail += from_u8("\n- " + tray_name + "(" + filament_type + ") ") + _L(uk.second); + } + if (n == 0) { + MessageDialog dlg(this, + _L("There are no compatible filaments, and sync is not performed.") + detail, + _L("Sync filaments with AMS"), wxOK); + dlg.ShowModal(); + return; + } + // Replace unknown filament IDs with the resolved preset's filament_id + auto &filaments = wxGetApp().preset_bundle->filaments; + auto &filament_presets = wxGetApp().preset_bundle->filament_presets; + for (size_t i = 0; i < list2.size() && i < filament_presets.size(); ++i) { + if (list2[i] == UNKNOWN_FILAMENT_ID) { + const Preset *resolved = filaments.find_preset(filament_presets[i]); + if (resolved) + list2[i] = resolved->filament_id; + } + } + ams_filament_ids = boost::algorithm::join(list2, ","); + wxGetApp().app_config ->set("ams_filament_ids", p->ams_list_device, ams_filament_ids); + if (!unknowns.empty()) { + MessageDialog dlg(this, + _L("There are some unknown or incompatible filaments mapped to generic preset.\nPlease update Orca Slicer or restart Orca Slicer to check if there is an update to system presets.") + detail, + _L("Sync filaments with AMS"), wxOK); + dlg.ShowModal(); + } + if (!sync_color_only) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "on_filament_count_change"; + wxGetApp().plater()->on_filament_count_change(n); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish on_filament_count_change"; + } + for (auto& c : p->combos_filament) + c->update(); + // Expand filament list + update_filaments_area_height(); // ORCA + + // BBS:Synchronized consumables information + // auto calculation of flushing volumes + for (int i = 0; i < p->combos_filament.size(); ++i) { + if (i >= color_before_sync.size()) { + auto_calc_flushing_volumes(i); + } + else if(color_before_sync[i] != color_opt->values[i] && wxGetApp().app_config->get("auto_calculate_flush") != "disabled"){ + auto_calc_flushing_volumes(i); + } + else if(is_support_filament(i) !=is_support_before[i] && wxGetApp().app_config->get("auto_calculate_flush") == "all"){ + auto_calc_flushing_volumes(i); + } + } + Layout(); + + // For full sync, preset selection/list update may rebuild combo widgets. + // For color-only, keep current presets untouched and refresh colors only. + if (!sync_color_only) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0]); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + update_dynamic_filament_list(); + } else { + wxGetApp().plater()->update_filament_colors_in_full_config(); + for (auto &c : p->combos_filament) + c->update(); + obj_list()->update_filament_colors(); + update_dynamic_filament_list(); + } + + auto badge_combox_filament = [sync_color_only](PlaterPresetComboBox *c) { + 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."); + c->SetToolTip(tip); + c->ShowBadge(true); + }; + { // badge ams filament + clear_combos_filament_badge(); + if (sync_result.direct_sync) { + auto& ams_list = wxGetApp().preset_bundle->filament_ams_list; + size_t tray_idx = 0; + for (auto& entry : ams_list) { + if (tray_idx >= p->combos_filament.size()) break; + auto filament_id = entry.second.opt_string("filament_id", 0u); + if (!filament_id.empty()) { + badge_combox_filament(p->combos_filament[tray_idx]); + } + tray_idx++; + } + } + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "prepare enable_merge_color_by_sync_ams"; + if (!merge_info.is_empty() && wxGetApp().app_config->get_bool("enable_merge_color_by_sync_ams")) { // merge same color and preset filament//use same ams + auto reduce_index = [](MergeFilamentInfo &merge_info,int value) { + for (size_t i = 0; i < merge_info.merges.size(); i++) { + auto &cur = merge_info.merges[i]; + for (size_t j = 0; j < cur.size(); j++) { + if (value < cur[j]) { + cur[j] = cur[j] - 1; + } + } + } + }; + std::vector sync_ams_badges; + for (auto iter : sync_result.sync_maps) { + sync_ams_badges.push_back(false); + if (iter.second.ams_id == "" || iter.second.slot_id == "") { + continue; + } + sync_ams_badges.back() = true; + } + + for (size_t i = 0; i < merge_info.merges.size(); i++) { + auto& cur = merge_info.merges[i]; + for (int j = cur.size() -1; j >= 1 ; j--) { + auto last_index = cur[j]; + change_filament(last_index, cur[0]); + cur.erase(cur.begin() + j); + sync_ams_badges.erase(sync_ams_badges.begin() + last_index); + reduce_index(merge_info, last_index); + } + } + for (size_t i = 0; i < sync_ams_badges.size(); i++) { + if (sync_ams_badges[i] == true) { + if (i < p->combos_filament.size()) { + auto &c = p->combos_filament[i]; + badge_combox_filament(c); + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: p->combos_filament array out of bound"; + } + } + } + } else { + for (auto iter : sync_result.sync_maps) { + if (iter.second.ams_id == "" || iter.second.slot_id == "") { + continue; + } + auto temp_index = iter.first; + if (temp_index < p->combos_filament.size() && temp_index >= 0) { + auto &c = p->combos_filament[temp_index]; + badge_combox_filament(c); + } + } + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "begin pop_finsish_sync_ams_dialog"; + pop_finsish_sync_ams_dialog(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish pop_finsish_sync_ams_dialog"; +} + +bool Sidebar::should_show_SEMM_buttons() +{ + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); + auto cfg = preset_bundle.printers.get_edited_preset().config; + + return cfg.opt_bool("single_extruder_multi_material") || is_bbl_vendor; +} + +void Sidebar::show_SEMM_buttons() +{ + // ORCA + if (!p || p->combos_filament.empty() || !p->m_bpButton_add_filament || !p->m_bpButton_del_filament || !p->m_flushing_volume_btn) + return; + + bool is_multi_material = p->combos_filament.size() > 1; + bool single_or_bbl = should_show_SEMM_buttons(); + bool is_single = single_or_bbl && !is_multi_material; // SINGLE EXTRUDER / BBL WITH 1 MATERIAL + bool is_multi = single_or_bbl && is_multi_material; // MULTI MATERIAL WITH SINGLE EXTRUDER + bool is_fixed = !is_single && !is_multi; // MULTI EXTRUDER / TOOLCHANGER / IDEX WITH FIXED MATERIAL + + p->m_bpButton_add_filament->Show(single_or_bbl); + p->m_bpButton_del_filament->Show(is_multi); + p->m_flushing_volume_btn->Show( is_multi); + + if (is_multi) { + for (auto &c : p->combos_filament) + c->edit_btn->SetBitmap_("menu_filament"); + } + else if (is_single || is_fixed) { + for (auto &c : p->combos_filament) + c->edit_btn->SetBitmap_("edit"); + } + + Layout(); +} + +void Sidebar::update_dynamic_filament_list() +{ + dynamic_filament_list.update(); + dynamic_filament_list_1_based.update(); +} + +PlaterPresetComboBox* Sidebar::printer_combox() +{ + return p->combo_printer; +} + +ObjectList* Sidebar::obj_list() +{ + // BBS + //return obj_list(); + return p->m_object_list; +} + +ObjectSettings* Sidebar::obj_settings() +{ + return p->object_settings; +} + +ObjectLayers* Sidebar::obj_layers() +{ + return p->object_layers; +} + +wxPanel* Sidebar::scrolled_panel() +{ + return p->scrolled; +} + +wxPanel* Sidebar::print_panel() +{ + return p->m_panel_print_content; +} + +wxPanel* Sidebar::filament_panel() +{ + return p->m_panel_filament_content; +} + +ConfigOptionsGroup* Sidebar::og_freq_chng_params(const bool is_fff) +{ + // BBS +#if 0 + return p->frequently_changed_parameters->get_og(is_fff); +#endif + return NULL; +} + +wxButton* Sidebar::get_wiping_dialog_button() +{ +#if 0 + return p->frequently_changed_parameters->get_wiping_dialog_button(); +#endif + return NULL; +} + +void Sidebar::set_flushing_volume_warning(const bool flushing_volume_modify) +{ + if(flushing_volume_modify){ + p->m_flushing_volume_btn->SetStyle(ButtonStyle::Regular, ButtonType::Compact); + p->m_flushing_volume_btn->SetBorderColor(wxColour("#FF6F00")); + } + else + p->m_flushing_volume_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); +} + +void Sidebar::enable_buttons(bool enable) +{ +#if 0 + p->btn_reslice->Enable(enable); + p->btn_export_gcode->Enable(enable); + p->btn_send_gcode->Enable(enable); +// p->btn_eject_device->Enable(enable); + p->btn_export_gcode_removable->Enable(enable); +#endif +} + +bool Sidebar::show_reslice(bool show) const { return p->btn_reslice->Show(show); } +bool Sidebar::show_export(bool show) const { return p->btn_export_gcode->Show(show); } +bool Sidebar::show_send(bool show) const { return p->btn_send_gcode->Show(show); } +bool Sidebar::show_export_removable(bool show) const { return p->btn_export_gcode_removable->Show(show); } +//bool Sidebar::show_eject(bool show) const { return p->btn_eject_device->Show(show); } +//bool Sidebar::get_eject_shown() const { return p->btn_eject_device->IsShown(); } + +bool Sidebar::is_multifilament() +{ + return p->combos_filament.size() > 1; +} + +void Sidebar::deal_btn_sync() { + m_begin_sync_printer_status = true; + bool only_external_material; + auto ok = p->sync_extruder_list(only_external_material); + if (ok) { + pop_sync_nozzle_and_ams_dialog(); + } + m_begin_sync_printer_status = false; + wxGetApp().plater()->update_machine_sync_status(); +} + +template void setup_dialog_position(T& info) +{ + auto plater = wxGetApp().plater(); + auto& sidebar = plater->sidebar(); + auto docking = plater->get_sidebar_docking_state(); + bool on_right = true; + + if (docking == Sidebar::Left) { + on_right = true; + } else if (docking == Sidebar::Right) { + on_right = false; + } else { + // If sidebar is too close to screen right edge, then move the dialog to the left side instead + + auto screen_width = wxDisplay(&sidebar).GetClientArea().GetSize().x; + auto right_space = screen_width - sidebar.get_sidebar_pos_right_x(); + if (right_space < sidebar.FromDIP(400)) { + on_right = false; + } + } + + if (on_right) { + info.dialog_pos.x = sidebar.get_sidebar_pos_right_x() + sidebar.FromDIP(5); + info.dialog_pos_align_right = true; + } else { + info.dialog_pos.x = sidebar.GetScreenPosition().x - sidebar.FromDIP(5); + info.dialog_pos_align_right = false; + } +} + +void Sidebar::pop_sync_nozzle_and_ams_dialog() { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog"; + wxTheApp->CallAfter([this]() { + SyncNozzleAndAmsDialog::InputInfo temp_na_info; + wxPoint big_btn_pt; + wxSize big_btn_size; + wxGetApp().plater()->sidebar().get_big_btn_sync_pos_size(big_btn_pt, big_btn_size); + temp_na_info.dialog_pos = big_btn_pt + wxPoint(big_btn_size.x, big_btn_size.y) + wxPoint(FromDIP(big_btn_size.x / 10.f - 5), FromDIP(big_btn_size.y / 10.f)); + + temp_na_info.dialog_pos.y += FromDIP(2); + setup_dialog_position(temp_na_info); + + wxPoint small_btn_pt; + wxSize small_btn_size; + get_small_btn_sync_pos_size(small_btn_pt, small_btn_size); + temp_na_info.ams_btn_pos = small_btn_pt + wxPoint(small_btn_size.x / 2, small_btn_size.y / 2); + if (m_fna_dialog) { m_fna_dialog->on_hide(); } + if (m_sna_dialog) { + m_sna_dialog->Destroy(); + m_sna_dialog = nullptr; + } + m_sna_dialog = new SyncNozzleAndAmsDialog(temp_na_info); + m_sna_dialog->on_show(); + }); +} + +void Sidebar::pop_finsish_sync_ams_dialog() +{ + wxTheApp->CallAfter([this]() { + wxPoint small_btn_pt; + wxSize small_btn_size; + get_small_btn_sync_pos_size(small_btn_pt, small_btn_size); + + FinishSyncAmsDialog::InputInfo temp_fsa_info; + temp_fsa_info.dialog_pos.y = small_btn_pt.y; + setup_dialog_position(temp_fsa_info); + temp_fsa_info.ams_btn_pos = small_btn_pt + wxPoint(small_btn_size.x / 2, small_btn_size.y / 2); + if (m_sna_dialog) { m_sna_dialog->on_hide(); } + if (m_fna_dialog) { + m_fna_dialog->Destroy(); + m_fna_dialog = nullptr; + } + m_fna_dialog = new FinishSyncAmsDialog(temp_fsa_info); + m_fna_dialog->on_show(); + }); + +} + +static std::vector get_search_inputs(ConfigOptionMode mode) +{ + std::vector ret {}; + + auto& tabs_list = wxGetApp().tabs_list; + auto print_tech = wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology(); + for (auto tab : tabs_list) + if (tab->supports_printer_technology(print_tech)) + ret.emplace_back(Search::InputInfo {tab->get_config(), tab->type(), mode}); + + return ret; +} + +void Sidebar::update_searcher() +{ + p->searcher.init(get_search_inputs(m_mode)); +} + +void Sidebar::update_mode() +{ + m_mode = wxGetApp().get_mode(); + + //BBS: remove print related combos + update_searcher(); + + wxWindowUpdateLocker noUpdates(this); + + // BBS + //obj_list()->get_sizer()->Show(m_mode > comSimple); + + obj_list()->unselect_objects(); + obj_list()->update_selections(); +// obj_list()->update_object_menu(); + + Layout(); +} + +bool Sidebar::is_collapsed() { return p->plater->is_sidebar_collapsed(); } + +void Sidebar::collapse(bool collapse) { p->plater->collapse_sidebar(collapse); } + +#ifdef _MSW_DARK_MODE +void Sidebar::show_mode_sizer(bool show) +{ + //p->mode_sizer->Show(show); +} +#endif + +void Sidebar::update_ui_from_settings() +{ + // BBS + //p->object_manipulation->update_ui_from_settings(); + // update Cut gizmo, if it's open + p->plater->canvas3D()->update_gizmos_on_off_state(); + p->plater->set_current_canvas_as_dirty(); + p->plater->get_current_canvas3D()->request_extra_frame(); +#if 0 + p->object_list->apply_volumes_order(); +#endif +} + +bool Sidebar::show_object_list(bool show) const +{ + p->m_search_bar->Show(show); + if (!p->m_object_list->Show(show)) + return false; + if (!show) + p->object_layers->Show(false); + else + p->m_object_list->part_selection_changed(); + p->scrolled->Layout(); + return true; +} + +void Sidebar::finish_param_edit() { p->editing_filament = -1; } + +std::vector& Sidebar::combos_filament() +{ + return p->combos_filament; +} + +void Sidebar::clear_combos_filament_badge() +{ + auto &combos_filament = p->combos_filament; + for (auto &c : combos_filament) { // clear flag + c->ShowBadge(false); + } +} + +void Sidebar::udpate_combos_filament_badge() { + auto &combos_filament = p->combos_filament; + for (auto &c : combos_filament) { + auto selection = c->GetSelection(); + auto select_flag = c->GetFlag(selection); + auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; + c->ShowBadge(ok); + } + +} + +Search::OptionsSearcher& Sidebar::get_searcher() +{ + return p->searcher; +} + +std::string& Sidebar::get_search_line() +{ + return p->searcher.search_string(); +} + +static std::map printer_thumbnails = {}; + +void Sidebar::update_printer_thumbnail() +{ + auto& preset_bundle = wxGetApp().preset_bundle; + Preset & selected_preset = preset_bundle->printers.get_edited_preset(); + std::string printer_type = selected_preset.get_current_printer_type(preset_bundle); + if (printer_thumbnails.find(printer_type) != printer_thumbnails.end()) // Use known cache first + p->image_printer->SetBitmap(create_scaled_bitmap(printer_thumbnails[printer_type], this, PRINTER_THUMBNAIL_SIZE.GetHeight())); + else { + /* ORCA this part check images folder for BBL covers but not checks file existence and causes crash on Linux + * BBL covers already exist on profile folder so no need to use this section + try { + // No cache, try dedicated printer preview + p->image_printer->SetBitmap(create_scaled_bitmap("printer_preview_" + printer_type, this, 48)); + // Success, cache it + printer_thumbnails[printer_type] = "printer_preview_" + printer_type; + return; + } catch (...) {} + */ + + // Orca: try to use the printer model cover as the thumbnail + const auto model_name = selected_preset.config.opt_string("printer_model"); + std::string cover_file = model_name + "_cover.png"; + for (auto vendor_profile : preset_bundle->vendors) { + for (auto vendor_model : vendor_profile.second.models) { + if (vendor_model.name == model_name) { + // Try to find the printer cover + boost::filesystem::path cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / + "/profiles/" / vendor_profile.second.id / cover_file) + .make_preferred(); + if (boost::filesystem::exists(cover_path)) { + try { + p->image_printer->SetBitmap(create_scaled_bitmap(cover_path.string(), this, PRINTER_THUMBNAIL_SIZE.GetHeight())); + printer_thumbnails[printer_type] = cover_path.string(); // Cache the path so we don't look up again + return; + } catch (...) {} + } + } + } + } + p->image_printer->SetBitmap(create_scaled_bitmap("printer_placeholder", this, PRINTER_THUMBNAIL_SIZE.GetHeight())); + printer_thumbnails[printer_type] = "printer_placeholder"; // Avoid unnecessary try + } +} + +void Sidebar::auto_calc_flushing_volumes(const int filament_idx, const int extruder_id) { + + std::vector filament_indices; + std::vector extruder_indices; + + auto& preset_bundle = wxGetApp().preset_bundle; + auto filament_ptr = preset_bundle->project_config.option("filament_colour"); + int filament_count = filament_ptr ? filament_ptr->size() : 0; + int extruder_count = preset_bundle->get_printer_extruder_count(); + + if (filament_idx < 0) { + filament_indices.resize(filament_count); + std::iota(filament_indices.begin(), filament_indices.end(), 0); + } + else { + filament_indices.emplace_back(filament_idx); + } + + if (extruder_id < 0) { + extruder_indices.resize(extruder_count); + std::iota(extruder_indices.begin(), extruder_indices.end(), 0); + } + else { + extruder_indices.emplace_back(extruder_id); + } + + for (auto eidx : extruder_indices) { + for (auto fidx : filament_indices) { + auto_calc_flushing_volumes_internal(fidx, eidx); + } + } + + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + wxGetApp().plater()->update_project_dirty_from_presets(); + wxPostEvent(this, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, this)); + auto has_modify = is_flush_config_modified(); + set_flushing_volume_warning(has_modify); + p->plater->update(); +} + + +void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int extruder_id) +{ + auto& preset_bundle = wxGetApp().preset_bundle; + auto& project_config = preset_bundle->project_config; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; + size_t extruder_nums = preset_bundle->get_printer_extruder_count(); + int nozzle_flush_dataset = full_config.option("nozzle_flush_dataset")->values[extruder_id]; + std::vector init_matrix = get_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, extruder_id, extruder_nums); + + const std::vector& min_flush_volumes = get_min_flush_volumes(full_config, extruder_id); + + const auto* flush_multi_opt = project_config.option("flush_multiplier"); + float flush_multiplier = flush_multi_opt ? (float)flush_multi_opt->get_at(extruder_id) : 1.f; + std::vector matrix = init_matrix; + int m_max_flush_volume = Slic3r::g_max_flush_volume; + unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001); + + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + std::vector> multi_colours; + + // Support for multi-color filament + for (int i = 0; i < extruder_colours.size(); ++i) { + std::vector single_filament; + if (i < ams_multi_color_filament.size()) { + if (!ams_multi_color_filament[i].empty()) { + std::vector colors = ams_multi_color_filament[i]; + for (int j = 0; j < colors.size(); ++j) { + single_filament.push_back(wxColour(colors[j])); + } + multi_colours.push_back(single_filament); + continue; + } + } + + single_filament.push_back(wxColour(extruder_colours[i])); + multi_colours.push_back(single_filament); + } + + if (modify_id >= 0 && modify_id < multi_colours.size()) { + for (int i = 0; i < multi_colours.size(); ++i) { + // from to modify + int from_idx = i; + if (from_idx != modify_id) { + Slic3r::FlushVolCalculator calculator(min_flush_volumes[from_idx], m_max_flush_volume, nozzle_flush_dataset); + int flushing_volume = 0; + bool is_from_support = is_support_filament(from_idx); + bool is_to_support = is_support_filament(modify_id); + if (is_to_support) { + flushing_volume = Slic3r::g_flush_volume_to_support; + } + else { + for (int j = 0; j < multi_colours[from_idx].size(); ++j) { + const wxColour& from = multi_colours[from_idx][j]; + for (int k = 0; k < multi_colours[modify_id].size(); ++k) { + const wxColour& to = multi_colours[modify_id][k]; + int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), to.Red(), to.Green(), to.Blue()); + flushing_volume = std::max(flushing_volume, volume); + } + } + if (is_from_support) + flushing_volume = std::max(flushing_volume, Slic3r::g_min_flush_volume_from_support); + } + matrix[m_number_of_extruders * from_idx + modify_id] = flushing_volume; + } + + // modify to to + int to_idx = i; + if (to_idx != modify_id) { + Slic3r::FlushVolCalculator calculator(min_flush_volumes[modify_id], m_max_flush_volume, nozzle_flush_dataset); + bool is_from_support = is_support_filament(modify_id); + bool is_to_support = is_support_filament(to_idx); + int flushing_volume = 0; + if (is_to_support) { + flushing_volume = Slic3r::g_flush_volume_to_support; + } + else { + for (int j = 0; j < multi_colours[modify_id].size(); ++j) { + const wxColour& from = multi_colours[modify_id][j]; + for (int k = 0; k < multi_colours[to_idx].size(); ++k) { + const wxColour& to = multi_colours[to_idx][k]; + int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), to.Red(), to.Green(), to.Blue()); + flushing_volume = std::max(flushing_volume, volume); + } + } + if (is_from_support) + flushing_volume = std::max(flushing_volume, Slic3r::g_min_flush_volume_from_support); + + matrix[m_number_of_extruders * modify_id + to_idx] = flushing_volume; + } + } + } + } + set_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, matrix, extruder_id, extruder_nums); +} + +void Sidebar::jump_to_object(ObjectDataViewModelNode* item) +{ + p->jump_to_object(item); +} + +void Sidebar::can_search() +{ + p->can_search(); +} + +class PlaterDropTarget : public wxFileDropTarget +{ +public: + PlaterDropTarget(MainFrame& mainframe, Plater& plater) : m_mainframe(mainframe), m_plater(plater) { + this->SetDefaultAction(wxDragCopy); + } + + virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames); + +private: + MainFrame& m_mainframe; + Plater& m_plater; +}; + +namespace { +bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position) +{ + std::string svg_file_str = into_u8(svg_file); + GLCanvas3D* canvas = plater.canvas3D(); + if (canvas == nullptr) + return false; + auto base_svg = canvas->get_gizmos_manager().get_gizmo(GLGizmosManager::Svg); + if (base_svg == nullptr) + return false; + GLGizmoSVG* svg = dynamic_cast(base_svg); + if (svg == nullptr) + return false; + + // Refresh hover state to find surface point under mouse + wxMouseEvent evt(wxEVT_MOTION); + evt.SetPosition(wxPoint(mouse_drop_position.x(), mouse_drop_position.y())); + canvas->on_mouse(evt); // call render where is call GLCanvas3D::_picking_pass() + + return svg->create_volume(svg_file_str, mouse_drop_position, ModelVolumeType::MODEL_PART); +} +} + +// State to manage showing after export notifications and device ejecting +enum ExportingStatus{ + NOT_EXPORTING, + EXPORTING_TO_REMOVABLE, + EXPORTING_TO_LOCAL +}; + + +// TODO: listen on dark ui change +class FloatFrame : public wxAuiFloatingFrame +{ +public: + FloatFrame(wxWindow* parent, wxAuiManager* ownerMgr, const wxAuiPaneInfo& pane) : wxAuiFloatingFrame(parent, ownerMgr, pane) + { + wxGetApp().UpdateFrameDarkUI(this); + } +}; + +class AuiMgr : public wxAuiManager +{ +public: + AuiMgr() : wxAuiManager(){} + + virtual wxAuiFloatingFrame* CreateFloatingFrame(wxWindow* parent, const wxAuiPaneInfo& p) override + { + return new FloatFrame(parent, this, p); + } +}; + +// Plater / private +struct Plater::priv +{ + // PIMPL back pointer ("Q-Pointer") + Plater *q; + Sidebar * sidebar; + MainFrame *main_frame; + + MenuFactory menus; + + SelectMachineDialog* m_select_machine_dlg = nullptr; + SendMultiMachinePage* m_send_multi_dlg = nullptr; + SendToPrinterDialog* m_send_to_sdcard_dlg = nullptr; + PublishDialog *m_publish_dlg = nullptr; + + // Data + Slic3r::DynamicPrintConfig *config; // FIXME: leak? + Slic3r::Print fff_print; + Slic3r::SLAPrint sla_print; + Slic3r::Model model; + PrinterTechnology printer_technology = ptFFF; + Slic3r::GCodeProcessorResult gcode_result; + + // GUI elements + AuiMgr m_aui_mgr; + wxString m_default_window_layout; + wxPanel* current_panel{ nullptr }; + std::vector panels; + + struct SidebarLayout + { + bool is_enabled{false}; + bool is_collapsed{false}; + bool show{false}; + } sidebar_layout; + Bed3D bed; + Camera camera; + //BBS: partplate related structure + PartPlateList partplate_list; + //BBS: add a flag to ignore cancel event + bool m_ignore_event{false}; + bool m_slice_all{false}; + bool m_is_slicing {false}; + bool auto_reslice_pending {false}; + bool auto_reslice_after_cancel {false}; + bool m_is_publishing {false}; + int m_is_RightClickInLeftUI{-1}; + int m_cur_slice_plate; + //BBS: m_slice_all in .gcode.3mf file case, set true when slice all + bool m_slice_all_only_has_gcode{ false }; + + bool m_need_update{false}; + //BBS: add popup object table logic + //ObjectTableDialog* m_popup_table{ nullptr }; + +#if ENABLE_ENVIRONMENT_MAP + GLTexture environment_texture; +#endif // ENABLE_ENVIRONMENT_MAP + Mouse3DController mouse3d_controller; + View3D* view3D; + // BBS + //GLToolbar view_toolbar; + GLToolbar collapse_toolbar; + Preview *preview; + AssembleView* assemble_view { nullptr }; + bool first_enter_assemble{ true }; + std::unique_ptr notification_manager; + + ProjectDirtyStateManager dirty_state; + + BackgroundSlicingProcess background_process; + bool suppressed_backround_processing_update { false }; + + // TODO: A mechanism would be useful for blocking the plater interactions: + // objects would be frozen for the user. In case of arrange, an animation + // could be shown, or with the optimize orientations, partial results + // could be displayed. + // + // UIThreadWorker can be used as a replacement for BoostThreadWorker if + // no additional worker threads are desired (useful for debugging or profiling) + PlaterWorker m_worker; + SLAImportDialog * m_sla_import_dlg; + + int m_job_prepare_state; + + bool delayed_scene_refresh; + std::string delayed_error_message; + + wxTimer background_process_timer; + wxTimer auto_reslice_timer; + + std::string label_btn_export; + std::string label_btn_send; + + bool show_render_statistic_dialog{ false }; + bool show_wireframe{ false }; + bool wireframe_enabled{ true }; + + static const std::regex pattern_bundle; + static const std::regex pattern_3mf; + static const std::regex pattern_zip_amf; + static const std::regex pattern_any_amf; + static const std::regex pattern_prusa; + + bool m_is_dark = false; + + priv(Plater *q, MainFrame *main_frame); + ~priv(); + + + bool need_update() const { return m_need_update; } + void set_need_update(bool need_update) { m_need_update = need_update; } + + void set_plater_dirty(bool is_dirty) { dirty_state.set_plater_dirty(is_dirty); } + bool is_project_dirty() const { return dirty_state.is_dirty(); } + bool is_presets_dirty() const { return dirty_state.is_presets_dirty(); } + void update_project_dirty_from_presets() + { + // BBS: backup + Slic3r::put_other_changes(); + dirty_state.update_from_presets(); + } + int save_project_if_dirty(const wxString& reason) { + int res = wxID_NO; + if (dirty_state.is_dirty()) { + MainFrame* mainframe = wxGetApp().mainframe; + if (mainframe->can_save_as()) { + wxString suggested_project_name; + wxString project_name = suggested_project_name = get_project_filename(".3mf"); + if (suggested_project_name.IsEmpty()) { + fs::path output_file = get_export_file_path(FT_3MF); + suggested_project_name = output_file.empty() ? _L("Untitled") : from_u8(output_file.stem().string()); + } + res = MessageDialog(mainframe, reason + "\n" + format_wxstr(_L("Do you want to save changes to \"%1%\"?"), suggested_project_name), + wxString(SLIC3R_APP_FULL_NAME), wxYES_NO | wxCANCEL).ShowModal(); + if (res == wxID_YES) + if (!mainframe->save_project_as(project_name)) + res = wxID_CANCEL; + } + } + return res; + } + void reset_project_dirty_after_save() { m_undo_redo_stack_main.mark_current_as_saved(); dirty_state.reset_after_save(); } + void reset_project_dirty_initial_presets() { dirty_state.reset_initial_presets(); } + +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + void render_project_state_debug_window() const { dirty_state.render_debug_window(); } +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + + enum class UpdateParams { + FORCE_FULL_SCREEN_REFRESH = 1, + FORCE_BACKGROUND_PROCESSING_UPDATE = 2, + POSTPONE_VALIDATION_ERROR_MESSAGE = 4, + }; + void update(unsigned int flags = 0); + void select_view(const std::string& direction); + //BBS: add no_slice option + void select_view_3D(const std::string& name, bool no_slice = true); + void select_next_view_3D(); + + bool is_preview_shown() const { return current_panel == preview; } + bool is_preview_loaded() const { return preview->is_loaded(); } + bool is_view3D_shown() const { return current_panel == view3D; } + bool is_assemble_view_show() const { return current_panel == assemble_view; } + + bool are_view3D_labels_shown() const { return (current_panel == view3D) && view3D->get_canvas3d()->are_labels_shown(); } + void show_view3D_labels(bool show) + { + if (current_panel == view3D) { + view3D->get_canvas3d()->show_labels(show); + wxGetApp().app_config->set_bool("show_labels", show); + } + } + + bool is_view3D_overhang_shown() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_overhang_shown(); } + void show_view3D_overhang(bool show) + { + if (current_panel == view3D) { + view3D->get_canvas3d()->show_overhang(show); + wxGetApp().app_config->set_bool("show_overhang", show); + } + } + + void enable_sidebar(bool enabled); + void collapse_sidebar(bool collapse); + void update_sidebar(bool force_update = false); + void reset_window_layout(); + Sidebar::DockingState get_sidebar_docking_state(); + + bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); } + + void set_current_canvas_as_dirty(); + GLCanvas3D* get_current_canvas3D(bool exclude_preview = false); + void unbind_canvas_event_handlers(); + void reset_canvas_volumes(); + bool check_ams_status_impl(bool is_slice_all); // Check whether the printer and ams status are consistent, for grouping algorithm + bool get_machine_sync_status(); // check whether the printer is linked and the printer type is same as selected profile + + // BBS + bool init_collapse_toolbar(); + + // BBS + void hide_select_machine_dlg() + { + if (m_select_machine_dlg) + m_select_machine_dlg->EndModal(wxID_OK); + } + + void enter_prepare_mode() + { + if (m_select_machine_dlg) + m_select_machine_dlg->prepare_mode(); + } + + void hide_send_to_printer_dlg() { m_send_to_sdcard_dlg->EndModal(wxID_OK); } + + void update_preview_bottom_toolbar(); + + void reset_gcode_toolpaths(); + + void reset_all_gizmos(); + void apply_free_camera_correction(bool apply = true); + void update_ui_from_settings(); + // BBS + std::shared_ptr statusbar(); + std::string get_config(const std::string &key) const; + BoundingBoxf bed_shape_bb() const; + BoundingBox scaled_bed_shape_bb() const; + + // BBS: backup & restore + std::vector load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi = false); + std::vector load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool split_object = false, bool auto_drop = true); + + fs::path get_export_file_path(GUI::FileType file_type); + wxString get_export_file(GUI::FileType file_type); + + // BBS + void load_auxiliary_files(); + + const Selection& get_selection() const; + Selection& get_selection(); + Selection& get_curr_selection(); + + int get_selected_object_idx() const; + int get_selected_volume_idx() const; + void selection_changed(); + void object_list_changed(); + + // BBS + void select_curr_plate_all(); + void remove_curr_plate_all(); + + void select_all(); + void deselect_all(); + void exit_gizmo(); + void remove(size_t obj_idx); + bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS + void delete_all_objects_from_model(); + void reset(bool apply_presets_change = false); + void center_selection(); + void drop_selection(); + void mirror(Axis axis); + void split_object(bool auto_drop = true); + void split_object(int obj_idx, bool auto_drop = true); + void split_volume(); + void scale_selection_to_fit_print_volume(); + + // Return the active Undo/Redo stack. It may be either the main stack or the Gimzo stack. + Slic3r::UndoRedo::Stack& undo_redo_stack() { assert(m_undo_redo_stack_active != nullptr); return *m_undo_redo_stack_active; } + Slic3r::UndoRedo::Stack& undo_redo_stack_main() { return m_undo_redo_stack_main; } + void enter_gizmos_stack(); + bool leave_gizmos_stack(); + + void take_snapshot(const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type = UndoRedo::SnapshotType::Action); + /*void take_snapshot(const wxString& snapshot_name, UndoRedo::SnapshotType snapshot_type = UndoRedo::SnapshotType::Action) + { this->take_snapshot(std::string(snapshot_name.ToUTF8().data()), snapshot_type); }*/ + int get_active_snapshot_index(); + + void undo(); + void redo(); + void undo_redo_to(size_t time_to_load); + + // BBS: backup + bool up_to_date(bool saved, bool backup); + + void suppress_snapshots() { m_prevent_snapshots++; } + void allow_snapshots() { m_prevent_snapshots--; } + // BBS: single snapshot + void single_snapshots_enter(SingleSnapshot *single) + { + if (m_single == nullptr) m_single = single; + } + void single_snapshots_leave(SingleSnapshot *single) + { + if (m_single == single) m_single = nullptr; + } + + void process_validation_warning(StringObjectException const &warning) const; + + bool background_processing_enabled() const { +#ifdef SUPPORT_BACKGROUND_PROCESSING + return this->get_config("background_processing") == "1"; +#else + return false; +#endif + } + std::vector> get_extruder_filament_info(); + void update_print_volume_state(); + void schedule_background_process(); + void schedule_auto_reslice_if_needed(); + void trigger_auto_reslice_now(); + int auto_slice_delay_seconds() const; + // Update background processing thread from the current config and Model. + enum UpdateBackgroundProcessReturnState { + // update_background_process() reports, that the Print / SLAPrint was updated in a way, + // that the background process was invalidated and it needs to be re-run. + UPDATE_BACKGROUND_PROCESS_RESTART = 1, + // update_background_process() reports, that the Print / SLAPrint was updated in a way, + // that a scene needs to be refreshed (you should call _3DScene::reload_scene(canvas3Dwidget, false)) + UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE = 2, + // update_background_process() reports, that the Print / SLAPrint is invalid, and the error message + // was sent to the status line. + UPDATE_BACKGROUND_PROCESS_INVALID = 4, + // Restart even if the background processing is disabled. + UPDATE_BACKGROUND_PROCESS_FORCE_RESTART = 8, + // Restart for G-code (or SLA zip) export or upload. + UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT = 16, + }; + // returns bit mask of UpdateBackgroundProcessReturnState + unsigned int update_background_process(bool force_validation = false, bool postpone_error_messages = false, bool switch_print = true); + // Restart background processing thread based on a bitmask of UpdateBackgroundProcessReturnState. + bool restart_background_process(unsigned int state); + // returns bit mask of UpdateBackgroundProcessReturnState + unsigned int update_restart_background_process(bool force_scene_update, bool force_preview_update); + void show_delayed_error_message() { + if (!this->delayed_error_message.empty()) { + std::string msg = std::move(this->delayed_error_message); + this->delayed_error_message.clear(); + GUI::show_error(this->q, msg); + } + } + void export_gcode(fs::path output_path, bool output_path_on_removable_media); + void export_gcode(fs::path output_path, bool output_path_on_removable_media, PrintHostJob upload_job); + + void reload_from_disk(); + bool replace_volume_with_stl(int object_idx, int volume_idx, const fs::path& new_path, const std::string& snapshot = ""); + void replace_with_stl(); + void replace_all_with_stl(); + void reload_all_from_disk(); + + //BBS: add no_slice option + void set_current_panel(wxPanel* panel, bool no_slice = true); + + void on_combobox_select(wxCommandEvent&); + void on_select_bed_type(wxCommandEvent&); + void on_select_preset(wxCommandEvent&); + void on_slicing_update(SlicingStatusEvent&); + void on_slicing_completed(wxCommandEvent&); + void on_process_completed(SlicingProcessCompletedEvent&); + void on_export_began(wxCommandEvent&); + void on_export_finished(wxCommandEvent&); + void on_slicing_began(); + + void clear_warnings(); + void add_warning(const Slic3r::PrintStateBase::Warning &warning, size_t oid); + // Update notification manager with the current state of warnings produced by the background process (slicing). + void actualize_slicing_warnings(const PrintBase &print); + void actualize_object_warnings(const PrintBase& print); + // Displays dialog window with list of warnings. + // Returns true if user clicks OK. + // Returns true if current_warnings vector is empty without showning the dialog + bool warnings_dialog(); + + void on_action_add(SimpleEvent&); + void on_action_add_plate(SimpleEvent&); + void on_action_del_plate(SimpleEvent&); + void on_action_split_objects(SimpleEvent&); + void on_action_split_volumes(SimpleEvent&); + void on_action_layersediting(SimpleEvent&); + void on_create_filament(SimpleEvent &); + void on_modify_filament(SimpleEvent &); + void on_add_filament(SimpleEvent &); + void on_delete_filament(SimpleEvent &); + void on_add_custom_filament(ColorEvent &); + + void on_object_select(SimpleEvent&); + void show_right_click_menu(Vec2d mouse_position, wxMenu *menu); + void on_right_click(RBtnEvent&); + //BBS: add model repair + void on_repair_model(wxCommandEvent &event); + void on_filament_color_changed(wxCommandEvent &event); + void show_install_plugin_hint(wxCommandEvent &event); + void install_network_plugin(wxCommandEvent &event); + void show_preview_only_hint(wxCommandEvent &event); + //BBS: add part plate related logic + void on_plate_right_click(RBtnPlateEvent&); + void on_plate_selected(SimpleEvent&); + void on_action_request_model_id(wxCommandEvent& evt); + void on_action_download_project(wxCommandEvent& evt); + void on_slice_button_status(bool enable); + //BBS: GUI refactor: GLToolbar + void on_action_open_project(SimpleEvent&); + void on_action_slice_plate(SimpleEvent&); + void on_action_slice_all(SimpleEvent&); + void on_action_publish(wxCommandEvent &evt); + void on_action_print_plate(SimpleEvent&); + void on_action_print_all(SimpleEvent&); + void on_action_export_gcode(SimpleEvent&); + void on_action_send_gcode(SimpleEvent&); + void on_action_export_sliced_file(SimpleEvent&); + void on_action_export_all_sliced_file(SimpleEvent&); + void on_action_select_sliced_plate(wxCommandEvent& evt); + //BBS: change dark/light mode + void on_change_color_mode(SimpleEvent& evt); + void on_apple_change_color_mode(wxSysColourChangedEvent& evt); + void apply_color_mode(); + void on_update_geometry(Vec3dsEvent<2>&); + void on_3dcanvas_mouse_dragging_started(SimpleEvent&); + void on_3dcanvas_mouse_dragging_finished(SimpleEvent&); + + //void show_action_buttons(const bool is_ready_to_slice) const; + bool show_publish_dlg(bool show = true); + void update_publish_dialog_status(wxString &msg, int percent = -1); + void on_action_print_plate_from_sdcard(SimpleEvent&); + + void on_tab_selection_changing(wxBookCtrlEvent&); + + // Set the bed shape to a single closed 2D polygon(array of two element arrays), + // triangulate the bed and store the triangles into m_bed.m_triangles, + // fills the m_bed.m_grid_lines and sets m_bed.m_origin. + // Sets m_bed.m_polygon to limit the object placement. + //BBS: add bed exclude area + void set_bed_shape(const Pointfs &shape, + const Pointfs &exclude_areas, + const Pointfs &wrapping_exclude_areas, + const double printable_height, + std::vector extruder_areas, + std::vector extruder_heights, + const std::string &custom_texture, + const std::string &custom_model, + bool force_as_custom = false); + + bool can_delete() const; + bool can_delete_all() const; + bool can_add_plate() const; + bool can_delete_plate() const; + bool can_increase_instances() const; + bool can_decrease_instances() const; + bool can_split_to_objects() const; + bool can_split_to_volumes() const; + bool can_arrange() const; + bool can_layers_editing() const; + bool can_fix_through_cgal() const; + bool can_simplify() const; + bool can_smooth_mesh() const; + bool can_set_instance_to_object() const; + bool can_mirror() const; + bool can_reload_from_disk() const; + //BBS: + bool can_fillcolor() const; + bool has_assemble_view() const; + bool can_replace_with_stl() const; + bool can_replace_all_with_stl() const; + bool can_split(bool to_objects) const; +#if ENABLE_ENHANCED_PRINT_VOLUME_FIT + bool can_scale_to_print_volume() const; +#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT + + //BBS: add plate_id for thumbnail + void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, + Camera::EType camera_type, + Camera::ViewAngleType camera_view_angle_type = Camera::ViewAngleType::Iso, + bool for_picking = false, + bool ban_light = false); + ThumbnailsList generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type); + PlateBBoxData generate_first_layer_bbox(); + + void bring_instance_forward() const; + + // returns the path to project file with the given extension (none if extension == wxEmptyString) + // extension should contain the leading dot, i.e.: ".3mf" + wxString get_project_filename(const wxString& extension = wxEmptyString) const; + wxString get_export_gcode_filename(const wxString& extension = wxEmptyString, bool only_filename = false, bool export_all = false) const; + void set_project_filename(const wxString& filename); + + //BBS store bbs project name + wxString get_project_name(); + void set_project_name(const wxString& project_name); + void update_title_dirty_status(); + + // Call after plater and Canvas#D is initialized + void init_notification_manager(); + + void update_objects_position_when_select_preset(const std::function& select_prest); + + // Caching last value of show_action_buttons parameter for show_action_buttons(), so that a callback which does not know this state will not override it. + //mutable bool ready_to_slice = { false }; + // Flag indicating that the G-code export targets a removable device, therefore the show_action_buttons() needs to be called at any case when the background processing finishes. + ExportingStatus exporting_status { NOT_EXPORTING }; + std::string last_output_path; + std::string last_output_dir_path; + //BBS store machine_sn and 3mf_path for PrintJob + PrintPrepareData m_print_job_data; + bool inside_snapshot_capture() { return m_prevent_snapshots != 0; } + int process_completed_with_error { -1 }; //-1 means no error + + //BBS: project + BBLProject project; + + //BBS: add print project related logic + void update_fff_scene_only_shells(bool only_shells = true); + //BBS: add popup object table logic + bool PopupObjectTable(int object_id, int volume_id, const wxPoint& position); + void on_action_send_to_printer(bool isall = false); + void on_action_send_to_multi_machine(SimpleEvent&); + int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path); +private: + bool layers_height_allowed() const; + + void update_fff_scene(); + void update_sla_scene(); + + void undo_redo_to(std::vector::const_iterator it_snapshot); + void update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool temp_snapshot_was_taken = false); + void on_action_export_to_sdcard(SimpleEvent&); + void on_action_export_to_sdcard_all(SimpleEvent&); + void update_plugin_when_launch(wxCommandEvent& event); + // path to project folder stored with no extension + boost::filesystem::path m_project_folder; + + /* display project name */ + wxString m_project_name; + + Slic3r::UndoRedo::Stack m_undo_redo_stack_main; + Slic3r::UndoRedo::Stack m_undo_redo_stack_gizmos; + Slic3r::UndoRedo::Stack *m_undo_redo_stack_active = &m_undo_redo_stack_main; + int m_prevent_snapshots = 0; /* Used for avoid of excess "snapshoting". + * Like for "delete selected" or "set numbers of copies" + * we should call tack_snapshot just ones + * instead of calls for each action separately + * */ + // BBS: single snapshot + Plater::SingleSnapshot *m_single = nullptr; + // BBS: backup + size_t m_saved_timestamp = 0; + size_t m_backup_timestamp = 0; + std::string m_last_fff_printer_profile_name; + std::string m_last_sla_printer_profile_name; + + // vector of all warnings generated by last slicing + std::vector> current_warnings; + bool show_warning_dialog { false }; +}; + +const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf)", std::regex::icase); +const std::regex Plater::priv::pattern_3mf(".*3mf", std::regex::icase); +const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase); +const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase); +const std::regex Plater::priv::pattern_prusa(".*bbl", std::regex::icase); + +bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) +{ +#ifdef WIN32 + // hides the system icon + this->MSWUpdateDragImageOnLeave(); +#endif // WIN32 + + m_mainframe.Raise(); + m_mainframe.select_tab(size_t(MainFrame::tp3DEditor)); + if (wxGetApp().is_editor()) + m_plater.select_view_3D("3D"); + + // When only one .svg file is dropped on scene + if (filenames.size() == 1) { + const wxString &filename = filenames.Last(); + const wxString file_extension = filename.substr(filename.length() - 4); + if (file_extension.CmpNoCase(".svg") == 0) { + // BBS: GUI refactor: move sidebar to the left + const wxPoint offset = m_plater.GetPosition() + m_plater.p->current_panel->GetPosition(); + Vec2d mouse_position(x - offset.x, y - offset.y); + // Scale for retina displays + const GLCanvas3D *canvas = m_plater.canvas3D(); + canvas->apply_retina_scale(mouse_position); + return emboss_svg(m_plater, filename, mouse_position); + } + } + bool res = m_plater.load_files(filenames); + m_mainframe.update_title(); + return res; +} + +Plater::priv::priv(Plater *q, MainFrame *main_frame) + : q(q) + , main_frame(main_frame) + //BBS: add bed_exclude_area + , config(Slic3r::DynamicPrintConfig::new_from_defaults_keys({ + "printable_area", "bed_exclude_area", "wrapping_exclude_area", "extruder_printable_area", "bed_custom_texture", "bed_custom_model", "print_sequence", + "extruder_clearance_radius", + "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", + "nozzle_height", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", + "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "nozzle_diameter", "single_extruder_multi_material", "preferred_orientation", + "enable_prime_tower", "wipe_tower_x", "wipe_tower_y", "prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "prime_tower_enable_framework", + "prime_tower_infill_gap", "prime_volume", + "extruder_colour", "filament_colour", "filament_type", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology", + // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. + "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", + "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers", + "enable_support", "support_filament", "support_interface_filament", + "support_top_z_distance", "support_bottom_z_distance", "raft_layers", + "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed", + "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", + "wipe_tower_filament", + "best_object_pos", "master_extruder_id" + })) + , sidebar(new Sidebar(q)) + , notification_manager(std::make_unique(q)) + , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} + , m_sla_import_dlg{new SLAImportDialog{q}} + , m_job_prepare_state(Job::JobPrepareState::PREPARE_STATE_DEFAULT) + , delayed_scene_refresh(false) + , collapse_toolbar(GLToolbar::Normal, "Collapse") + //BBS :partplatelist construction + , partplate_list(this->q, &model) +{ + m_is_dark = wxGetApp().app_config->get("dark_color_mode") == "1"; + +#ifdef __WXGTK__ + const bool disable_wayland_floating = Slic3r::GUI::is_running_on_wayland(); +#endif + + m_aui_mgr.SetManagedWindow(q); + m_aui_mgr.SetDockSizeConstraint(1, 1); +#ifdef __WXGTK__ + if (disable_wayland_floating) + m_aui_mgr.SetFlags(m_aui_mgr.GetFlags() & ~wxAUI_MGR_ALLOW_FLOATING); +#endif + //m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_PANE_BORDER_SIZE, 0); + //m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_SASH_SIZE, 2); + m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_CAPTION_SIZE, 18); + m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_GRADIENT_TYPE, wxAUI_GRADIENT_NONE); + + this->q->SetFont(Slic3r::GUI::wxGetApp().normal_font()); + + //BBS: use the first partplate's print for background process + partplate_list.update_slice_context_to_current_plate(background_process); + /* + background_process.set_fff_print(&fff_print); + background_process.set_sla_print(&sla_print); + background_process.set_gcode_result(&gcode_result); + background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); + background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); + background_process.set_finished_event(EVT_PROCESS_COMPLETED); + background_process.set_export_began_event(EVT_EXPORT_BEGAN); + // Default printer technology for default config. + background_process.select_technology(this->printer_technology); + // Register progress callback from the Print class to the Plater. + + auto statuscb = [this](const Slic3r::PrintBase::SlicingStatus &status) { + wxQueueEvent(this->q, new Slic3r::SlicingStatusEvent(EVT_SLICING_UPDATE, 0, status)); + }; + fff_print.set_status_callback(statuscb); + sla_print.set_status_callback(statuscb); */ + + // BBS: to be checked. Not follow patch. + background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); + background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); + background_process.set_finished_event(EVT_PROCESS_COMPLETED); + background_process.set_export_began_event(EVT_EXPORT_BEGAN); + background_process.set_export_finished_event(EVT_EXPORT_FINISHED); + this->q->Bind(EVT_SLICING_UPDATE, &priv::on_slicing_update, this); + this->q->Bind(EVT_PUBLISH, &priv::on_action_publish, this); + this->q->Bind(EVT_REPAIR_MODEL, &priv::on_repair_model, this); + this->q->Bind(EVT_FILAMENT_COLOR_CHANGED, &priv::on_filament_color_changed, this); + this->q->Bind(EVT_INSTALL_PLUGIN_NETWORKING, &priv::install_network_plugin, this); + this->q->Bind(EVT_INSTALL_PLUGIN_HINT, &priv::show_install_plugin_hint, this); + this->q->Bind(EVT_UPDATE_PLUGINS_WHEN_LAUNCH, &priv::update_plugin_when_launch, this); + this->q->Bind(EVT_PREVIEW_ONLY_MODE_HINT, &priv::show_preview_only_hint, this); + this->q->Bind(EVT_GLCANVAS_COLOR_MODE_CHANGED, &priv::on_change_color_mode, this); + this->q->Bind(wxEVT_SYS_COLOUR_CHANGED, &priv::on_apple_change_color_mode, this); + this->q->Bind(EVT_CREATE_FILAMENT, &priv::on_create_filament, this); + this->q->Bind(EVT_MODIFY_FILAMENT, &priv::on_modify_filament, this); + this->q->Bind(EVT_NOTICE_CHILDE_SIZE_CHANGED, &Sidebar::on_size, sidebar); + this->q->Bind(EVT_NOTICE_FULL_SCREEN_CHANGED, &Sidebar::on_full_screen, sidebar); + this->q->Bind(EVT_ADD_CUSTOM_FILAMENT, &priv::on_add_custom_filament, this); + main_frame->m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, &priv::on_tab_selection_changing, this); + + auto* panel_3d = new wxPanel(q); + view3D = new View3D(panel_3d, bed, &model, config, &background_process); + //BBS: use partplater's gcode + preview = new Preview(panel_3d, bed, &model, config, &background_process, partplate_list.get_current_slice_result(), [this]() { schedule_background_process(); }); + + assemble_view = new AssembleView(panel_3d, bed, &model, config, &background_process); + +#ifdef __APPLE__ + // BBS + // set default view_toolbar icons size equal to GLGizmosManager::Default_Icons_Size + //view_toolbar.set_icons_size(GLGizmosManager::Default_Icons_Size); +#endif // __APPLE__ + + panels.push_back(view3D); + panels.push_back(preview); + panels.push_back(assemble_view); + + this->background_process_timer.SetOwner(this->q, 0); + this->auto_reslice_timer.SetOwner(this->q, 0); + this->q->Bind(wxEVT_TIMER, [this](wxTimerEvent &evt) + { + if (&evt.GetTimer() == &this->background_process_timer) { + if (!this->suppressed_backround_processing_update) + this->update_restart_background_process(false, false); + } else if (&evt.GetTimer() == &this->auto_reslice_timer) { + this->auto_reslice_timer.Stop(); + this->trigger_auto_reslice_now(); + } else { + evt.Skip(); + } + }); + + update(); + + // Orca: Make sidebar dockable + m_aui_mgr.AddPane(sidebar, wxAuiPaneInfo() + .Name("sidebar") + .Left() + .CloseButton(false) + .TopDockable(false) + .BottomDockable(false) + .BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit()))); + + auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL); + panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0); + panel_sizer->Add(preview, 1, wxEXPAND | wxALL, 0); + panel_sizer->Add(assemble_view, 1, wxEXPAND | wxALL, 0); + panel_3d->SetSizer(panel_sizer); + m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false)); + + m_default_window_layout = m_aui_mgr.SavePerspective(); + + { + auto& sidebar = m_aui_mgr.GetPane(this->sidebar); + + // Load previous window layout + const auto cfg = wxGetApp().app_config; + wxString layout = wxString::FromUTF8(cfg->get("window_layout")); + if (!layout.empty()) { + bool removed_floating_state = false; +#ifdef __WXGTK__ + if (disable_wayland_floating) + layout = sanitize_window_layout_for_wayland(layout, &removed_floating_state); +#endif + + if (!m_aui_mgr.LoadPerspective(layout, false)) { + BOOST_LOG_TRIVIAL(warning) << "Failed to restore saved window layout"; + m_aui_mgr.LoadPerspective(m_default_window_layout, false); + } else if (removed_floating_state) { + BOOST_LOG_TRIVIAL(info) << "Removed floating AUI state from saved window layout for Wayland"; + } + + sidebar_layout.is_collapsed = !sidebar.IsShown(); + } + + // Keep tracking the current sidebar size, by storing it using `best_size`, which will be stored + // in the config and re-applied when the app is opened again. + this->sidebar->Bind(wxEVT_IDLE, [&sidebar, this](wxIdleEvent& e) { + if (sidebar.IsShown() && sidebar.IsDocked() && sidebar.rect.GetWidth() > 0) { + sidebar.BestSize(sidebar.rect.GetWidth(), sidebar.best_size.GetHeight()); + } + e.Skip(); + }); + + // Hide sidebar initially, will re-show it after initialization when we got proper window size + sidebar.Hide(); + m_aui_mgr.Update(); + } + + menus.init(main_frame); + + + // Events: + + if (wxGetApp().is_editor()) { + // Preset change event + sidebar->Bind(wxEVT_COMBOBOX, &priv::on_combobox_select, this); + sidebar->Bind(EVT_OBJ_LIST_OBJECT_SELECT, [this](wxEvent&) { priv::selection_changed(); }); + // BBS: should bind BACKGROUND_PROCESS event to plater + q->Bind(EVT_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { this->schedule_background_process(); }); + // jump to found option from SearchDialog + q->Bind(wxCUSTOMEVT_JUMP_TO_OPTION, [this](wxCommandEvent& evt) { sidebar->jump_to_option(evt.GetInt()); }); + q->Bind(wxCUSTOMEVT_JUMP_TO_OBJECT, [this](wxCommandEvent& evt) { + auto client_data = evt.GetClientData(); + ObjectDataViewModelNode* data = static_cast(client_data); + sidebar->jump_to_object(data); + } + ); + } + + wxGLCanvas* view3D_canvas = view3D->get_wxglcanvas(); + //BBS: GUI refactor + wxGLCanvas* preview_canvas = preview->get_wxglcanvas(); + + if (wxGetApp().is_editor()) { + // 3DScene events: + view3D_canvas->Bind(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { + delayed_error_message.clear(); + this->background_process_timer.Start(500, wxTIMER_ONE_SHOT); + }); + view3D_canvas->Bind(EVT_GLCANVAS_OBJECT_SELECT, &priv::on_object_select, this); + view3D_canvas->Bind(EVT_GLCANVAS_RIGHT_CLICK, &priv::on_right_click, this); + //BBS: add part plate related logic + view3D_canvas->Bind(EVT_GLCANVAS_PLATE_RIGHT_CLICK, &priv::on_plate_right_click, this); + view3D_canvas->Bind(EVT_GLCANVAS_REMOVE_OBJECT, [q](SimpleEvent&) { q->remove_selected(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent& evt) { + //BBS arrange from EVT set default state. + this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); + this->q->arrange(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE_PARTPLATE, [this](SimpleEvent& evt) { + //BBS arrange from EVT set default state. + this->q->set_prepare_state(Job::PREPARE_STATE_MENU); + this->q->arrange(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ORIENT, [this](SimpleEvent& evt) { + //BBS orient from EVT set default state. + this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); + this->q->orient(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ORIENT_PARTPLATE, [this](SimpleEvent& evt) { + //BBS orient from EVT set default state. + this->q->set_prepare_state(Job::PREPARE_STATE_MENU); + this->q->orient(); }); + //BBS + view3D_canvas->Bind(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, [this](SimpleEvent&) {this->q->select_curr_plate_all(); }); + view3D_canvas->Bind(EVT_GLCANVAS_PRINTABLE, [this](SimpleEvent& evt) { this->sidebar->obj_list()->toggle_printable_state(); }); + + view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); }); + view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); + view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event& evt) + { if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); }); + view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); }); + view3D_canvas->Bind(EVT_GLCANVAS_FORCE_UPDATE, [this](SimpleEvent&) { update(); }); + view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); }); + view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_SCALED, [this](SimpleEvent&) { update(); }); + // BBS + //view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { this->sidebar->enable_buttons(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { on_slice_button_status(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_GEOMETRY, &priv::on_update_geometry, this); + view3D_canvas->Bind(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED, &priv::on_3dcanvas_mouse_dragging_started, this); + view3D_canvas->Bind(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, &priv::on_3dcanvas_mouse_dragging_finished, this); + view3D_canvas->Bind(EVT_GLCANVAS_TAB, [this](SimpleEvent&) { select_next_view_3D(); }); + view3D_canvas->Bind(EVT_GLCANVAS_RESETGIZMOS, [this](SimpleEvent&) { reset_all_gizmos(); }); + view3D_canvas->Bind(EVT_GLCANVAS_UNDO, [this](SimpleEvent&) { this->undo(); }); + view3D_canvas->Bind(EVT_GLCANVAS_REDO, [this](SimpleEvent&) { this->redo(); }); + view3D_canvas->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); + view3D_canvas->Bind(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, [this](SimpleEvent&) { this->view3D->get_canvas3d()->reset_layer_height_profile(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, [this](Event& evt) { this->view3D->get_canvas3d()->adaptive_layer_height_profile(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, [this](HeightProfileSmoothEvent& evt) { this->view3D->get_canvas3d()->smooth_layer_height_profile(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_RELOAD_FROM_DISK, [this](SimpleEvent&) { this->reload_all_from_disk(); }); + + // 3DScene/Toolbar: + view3D_canvas->Bind(EVT_GLTOOLBAR_ADD, &priv::on_action_add, this); + view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE, [q](SimpleEvent&) { q->remove_selected(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [this](SimpleEvent&) { delete_all_objects_from_model(); }); +// view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [q](SimpleEvent&) { q->reset_with_confirm(); }); + + view3D_canvas->Bind(EVT_GLTOOLBAR_ADD_PLATE, &priv::on_action_add_plate, this); + view3D_canvas->Bind(EVT_GLTOOLBAR_DEL_PLATE, &priv::on_action_del_plate, this); + view3D_canvas->Bind(EVT_GLTOOLBAR_ORIENT, [this](SimpleEvent&) { + //BBS arrange from EVT set default state. + this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); + this->q->orient(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE, [this](SimpleEvent&) { + //BBS arrange from EVT set default state. + this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); + this->q->arrange(); + }); + view3D_canvas->Bind(EVT_GLTOOLBAR_CUT, [q](SimpleEvent&) { q->cut_selection_to_clipboard(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_COPY, [q](SimpleEvent&) { q->copy_selection_to_clipboard(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_PASTE, [q](SimpleEvent&) { q->paste_from_clipboard(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_LAYERSEDITING, &priv::on_action_layersediting, this); + //BBS: add clone + view3D_canvas->Bind(EVT_GLTOOLBAR_CLONE, [q](SimpleEvent&) { q->clone_selection(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_MORE, [q](SimpleEvent&) { q->increase_instances(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_FEWER, [q](SimpleEvent&) { q->decrease_instances(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_OBJECTS, &priv::on_action_split_objects, this); + view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_VOLUMES, &priv::on_action_split_volumes, this); + //BBS: GUI refactor: GLToolbar + view3D_canvas->Bind(EVT_GLTOOLBAR_OPEN_PROJECT, &priv::on_action_open_project, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); + view3D_canvas->Bind(EVT_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); + //preview also send these events + //preview_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); + //preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); + //preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); + //review_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); + view3D_canvas->Bind(EVT_GLCANVAS_SWITCH_TO_OBJECT, [main_frame](SimpleEvent&) { + if (main_frame->m_param_panel) { + main_frame->m_param_panel->switch_to_object(false); + } + }); + view3D_canvas->Bind(EVT_GLCANVAS_SWITCH_TO_GLOBAL, [main_frame](SimpleEvent&) { + if (main_frame->m_param_panel) { + main_frame->m_param_panel->switch_to_global(); + } + }); + } + view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); + + // Preview events: + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE, [this](SimpleEvent &) { + preview->get_canvas3d()->set_as_dirty(); + }); + if (wxGetApp().is_editor()) { + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_TAB, [this](SimpleEvent&) { select_next_view_3D(); }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); + preview->get_wxglcanvas()->Bind(EVT_CUSTOMEVT_TICKSCHANGED, [this](wxCommandEvent& event) { + Type tick_event_type = (Type)event.GetInt(); + Model& model = wxGetApp().plater()->model(); + //BBS: replace model custom gcode with current plate custom gcode + model.plates_custom_gcodes[model.curr_plate_index] = preview->get_canvas3d()->get_gcode_viewer().get_layers_slider()->GetTicksValues(); + + // BBS set to invalid state only + if (tick_event_type == Type::ToolChange || tick_event_type == Type::Custom || tick_event_type == Type::Template || tick_event_type == Type::PausePrint) { + PartPlate *plate = this->q->get_partplate_list().get_curr_plate(); + if (plate) { + plate->update_slice_result_valid_state(false); + } + } + set_plater_dirty(true); + + preview->on_tick_changed(tick_event_type); + + // update slice and print button + wxGetApp().mainframe->update_slice_print_status(MainFrame::SlicePrintEventType::eEventSliceUpdate, true, false); + set_need_update(true); + }); + } + if (wxGetApp().is_gcode_viewer()) + preview->Bind(EVT_GLCANVAS_RELOAD_FROM_DISK, [this](SimpleEvent&) { this->q->reload_gcode_from_disk(); }); + + //BBS + wxGLCanvas* assemble_canvas = assemble_view->get_wxglcanvas(); + if (wxGetApp().is_editor()) { + assemble_canvas->Bind(EVT_GLTOOLBAR_FILLCOLOR, [q](IntEvent& evt) { q->fill_color(evt.get_data()); }); + assemble_canvas->Bind(EVT_GLCANVAS_OBJECT_SELECT, &priv::on_object_select, this); + assemble_canvas->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); + assemble_canvas->Bind(EVT_GLCANVAS_RIGHT_CLICK, &priv::on_right_click, this); + assemble_canvas->Bind(EVT_GLCANVAS_FORCE_UPDATE, [this](SimpleEvent&) { update(); }); + assemble_canvas->Bind(EVT_GLCANVAS_UNDO, [this](SimpleEvent&) { this->undo(); }); + assemble_canvas->Bind(EVT_GLCANVAS_REDO, [this](SimpleEvent&) { this->redo(); }); + } + + if (wxGetApp().is_editor()) { + q->Bind(EVT_SLICING_COMPLETED, &priv::on_slicing_completed, this); + q->Bind(EVT_PROCESS_COMPLETED, &priv::on_process_completed, this); + q->Bind(EVT_EXPORT_BEGAN, &priv::on_export_began, this); + q->Bind(EVT_EXPORT_FINISHED, &priv::on_export_finished, this); + q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); + //BBS: set on_slice to false + q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview", false); }); + q->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); + q->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); + q->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); + q->Bind(EVT_PRINT_FROM_SDCARD_VIEW, &priv::on_action_print_plate_from_sdcard, this); + q->Bind(EVT_GLTOOLBAR_SELECT_SLICED_PLATE, &priv::on_action_select_sliced_plate, this); + q->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); + q->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); + q->Bind(EVT_GLTOOLBAR_SEND_GCODE, &priv::on_action_send_gcode, this); + q->Bind(EVT_GLTOOLBAR_EXPORT_SLICED_FILE, &priv::on_action_export_sliced_file, this); + q->Bind(EVT_GLTOOLBAR_EXPORT_ALL_SLICED_FILE, &priv::on_action_export_all_sliced_file, this); + q->Bind(EVT_GLTOOLBAR_SEND_TO_PRINTER, &priv::on_action_export_to_sdcard, this); + q->Bind(EVT_GLTOOLBAR_SEND_TO_PRINTER_ALL, &priv::on_action_export_to_sdcard_all, this); + q->Bind(EVT_GLTOOLBAR_PRINT_MULTI_MACHINE, &priv::on_action_send_to_multi_machine, this); + q->Bind(EVT_GLCANVAS_PLATE_SELECT, &priv::on_plate_selected, this); + q->Bind(EVT_DOWNLOAD_PROJECT, &priv::on_action_download_project, this); + q->Bind(EVT_IMPORT_MODEL_ID, &priv::on_action_request_model_id, this); + q->Bind(EVT_PRINT_FINISHED, [q](wxCommandEvent& evt) { q->print_job_finished(evt); }); + q->Bind(EVT_SEND_CALIBRATION_FINISHED, [q](wxCommandEvent& evt) { q->send_calibration_job_finished(evt); }); + q->Bind(EVT_SEND_FINISHED, [q](wxCommandEvent& evt) { q->send_job_finished(evt); }); + q->Bind(EVT_PUBLISH_FINISHED, [q](wxCommandEvent& evt) { q->publish_job_finished(evt);}); + q->Bind(EVT_OPEN_PLATESETTINGSDIALOG, [q](wxCommandEvent& evt) { q->open_platesettings_dialog(evt);}); + q->Bind(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG, [q](wxCommandEvent &evt) { q->open_filament_map_setting_dialog(evt); }); + //q->Bind(EVT_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); + } + + // Drop target: + q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership + q->Layout(); + + apply_color_mode(); + + set_current_panel(wxGetApp().is_editor() ? static_cast(view3D) : static_cast(preview)); + + // updates camera type from .ini file + camera.enable_update_config_on_type_change(true); + // BBS set config + bool use_perspective_camera = get_config("use_perspective_camera").compare("true") == 0; + if (use_perspective_camera) { + camera.set_type(Camera::EType::Perspective); + } else { + camera.set_type(Camera::EType::Ortho); + } + + // Load the 3DConnexion device database. + mouse3d_controller.load_config(*wxGetApp().app_config); + // Start the background thread to detect and connect to a HID device (Windows and Linux). + // Connect to a 3DConnextion driver (OSX). + mouse3d_controller.init(); +#ifdef _WIN32 + // Register an USB HID (Human Interface Device) attach event. evt contains Win32 path to the USB device containing VID, PID and other info. + // This event wakes up the Mouse3DController's background thread to enumerate HID devices, if the VID of the callback event + // is one of the 3D Mouse vendors (3DConnexion or Logitech). + this->q->Bind(EVT_HID_DEVICE_ATTACHED, [this](HIDDeviceAttachedEvent &evt) { + mouse3d_controller.device_attached(evt.data); + }); + this->q->Bind(EVT_HID_DEVICE_DETACHED, [this](HIDDeviceAttachedEvent& evt) { + mouse3d_controller.device_detached(evt.data); + }); +#endif /* _WIN32 */ + //notification_manager = new NotificationManager(this->q); + + if (wxGetApp().is_editor()) { + this->q->Bind(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, [this](EjectDriveNotificationClickedEvent&) { this->q->eject_drive(); }); + this->q->Bind(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, [this](ExportGcodeNotificationClickedEvent&) { this->q->export_gcode(true); }); + this->q->Bind(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, [](PresetUpdateAvailableClickedEvent&) { wxGetApp().get_preset_updater()->on_update_notification_confirm(); }); + this->q->Bind(EVT_PRINTER_CONFIG_UPDATE_AVAILABLE_CLICKED, [](PrinterConfigUpdateAvailableClickedEvent&) { + wxGetApp().get_preset_updater()->do_printer_config_update(); + wxGetApp().getDeviceManager()->reload_printer_settings(); }); + + /* BBS do not handle removeable driver event */ + this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this](RemovableDriveEjectEvent &evt) { + if (evt.data.second) { + // BBS + //this->show_action_buttons(this->ready_to_slice); + notification_manager->close_notification_of_type(NotificationType::ExportFinished); + notification_manager->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + format(_L("Successfully unmounted. The device %s (%s) can now be safely removed from the computer."), evt.data.first.name, evt.data.first.path) + ); + } else { + notification_manager->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::ErrorNotificationLevel, + format(_L("Ejecting of device %s (%s) has failed."), evt.data.first.name, evt.data.first.path) + ); + } + }); + this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this](RemovableDrivesChangedEvent &) { + // BBS + //this->show_action_buttons(this->ready_to_slice); + // Close notification ExportingFinished but only if last export was to removable + notification_manager->device_ejected(); + }); + // Start the background thread and register this window as a target for update events. + wxGetApp().removable_drive_manager()->init(this->q); +#ifdef _WIN32 + //Trigger enumeration of removable media on Win32 notification. + this->q->Bind(EVT_VOLUME_ATTACHED, [this](VolumeAttachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); + this->q->Bind(EVT_VOLUME_DETACHED, [this](VolumeDetachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); +#endif /* _WIN32 */ + } + + // Initialize the Undo / Redo stack with a first snapshot. + //this->take_snapshot("New Project", UndoRedo::SnapshotType::ProjectSeparator); + // Reset the "dirty project" flag. + m_undo_redo_stack_main.mark_current_as_saved(); + dirty_state.update_from_undo_redo_stack(false); + //this->take_snapshot("New Project"); + // BBS: save project confirm + up_to_date(true, false); + up_to_date(true, true); + model.set_need_backup(); + + // BBS: restore project + if (wxGetApp().is_editor()) { + auto last_backup = wxGetApp().app_config->get_last_backup_dir(); + this->q->Bind(EVT_RESTORE_PROJECT, [this, last = last_backup](wxCommandEvent& e) { + std::string last_backup = last; + std::string originfile; + if (Slic3r::has_restore_data(last_backup, originfile)) { + auto result = MessageDialog(this->q, _L("Previous unsaved project detected, do you want to restore it?"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Restore"), wxYES_NO | wxYES_DEFAULT | wxCENTRE).ShowModal(); + if (result == wxID_YES) { + this->q->load_project(from_path(last_backup), from_path(originfile)); + Slic3r::backup_soon(); + return; + } + } + + try { + if (originfile != "") // see bbs_3mf.cpp for lock detail + boost::filesystem::remove_all(last); + } + + catch (...) {} + + if (this->q->get_project_filename().IsEmpty() && this->q->is_empty_project()) { + int skip_confirm = e.GetInt(); + this->q->new_project(skip_confirm, true); + } + }); + //wxPostEvent(this->q, wxCommandEvent{EVT_RESTORE_PROJECT}); + } + + this->q->Bind(EVT_LOAD_MODEL_OTHER_INSTANCE, [this](LoadFromOtherInstanceEvent& evt) { + BOOST_LOG_TRIVIAL(trace) << "Received load from other instance event."; + wxArrayString input_files; + for (size_t i = 0; i < evt.data.size(); ++i) { + input_files.push_back(from_u8(evt.data[i].string())); + } + wxGetApp().mainframe->Show(); + wxGetApp().mainframe->Raise(); + this->q->load_files(input_files); + }); + + this->q->Bind(EVT_START_DOWNLOAD_OTHER_INSTANCE, [](StartDownloadOtherInstanceEvent& evt) { + BOOST_LOG_TRIVIAL(trace) << "Received url from other instance event."; + wxGetApp().mainframe->Show(); + wxGetApp().mainframe->Raise(); + for (size_t i = 0; i < evt.data.size(); ++i) { + wxGetApp().start_download(evt.data[i]); + } + + }); + this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) { + bring_instance_forward(); + }); + wxGetApp().other_instance_message_handler()->init(this->q); + + // collapse sidebar according to saved value + //if (wxGetApp().is_editor()) { + // bool is_collapsed = wxGetApp().app_config->get("collapsed_sidebar") == "1"; + // sidebar->collapse(is_collapsed); + //} + update_sidebar(true); +} + +Plater::priv::~priv() +{ + if (config != nullptr) + delete config; + // Saves the database of visited (already shown) hints into hints.ini. + notification_manager->deactivate_loaded_hints(); + main_frame->m_tabpanel->Unbind(wxEVT_NOTEBOOK_PAGE_CHANGING, &priv::on_tab_selection_changing, this); +} + +void Plater::priv::update(unsigned int flags) +{ + // the following line, when enabled, causes flickering on NVIDIA graphics cards +// wxWindowUpdateLocker freeze_guard(q); +#ifdef SUPPORT_AUTOCENTER + if (get_config("autocenter") == "true") + model.center_instances_around_point(this->bed.build_volume().bed_center()); +#endif + + unsigned int update_status = 0; + const bool force_background_processing_restart = this->printer_technology == ptSLA || (flags & (unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE); + if (force_background_processing_restart) + // Update the SLAPrint from the current Model, so that the reload_scene() + // pulls the correct data. + update_status = this->update_background_process(false, flags & (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); + //BBS TODO reload_scene + this->view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH); + if (is_preview_shown()) this->preview->reload_print(); + //BBS assemble view + this->assemble_view->reload_scene(false, flags); + + if (current_panel && is_preview_shown()) { + q->force_update_all_plate_thumbnails(); + //update_fff_scene_only_shells(true); + } + + if (force_background_processing_restart) + this->restart_background_process(update_status); + else + this->schedule_background_process(); + + // BBS +#if 0 + if (get_config("autocenter") == "true" && this->sidebar->obj_manipul()->IsShown()) + this->sidebar->obj_manipul()->UpdateAndShow(true); +#endif + + update_sidebar(); +} + +void Plater::priv::select_view(const std::string& direction) +{ + if (current_panel == view3D) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; + view3D->select_view(direction); + wxGetApp().update_ui_from_settings(); + } + else if (current_panel == preview) { + BOOST_LOG_TRIVIAL(info) << "select preview"; + preview->select_view(direction); + wxGetApp().update_ui_from_settings(); + } + else if (current_panel == assemble_view) { + BOOST_LOG_TRIVIAL(info) << "select assemble view"; + assemble_view->select_view(direction); + } +} + +const VendorProfile::PrinterModel *Plater::get_curr_printer_model() +{ + auto bundle = wxGetApp().preset_bundle; + if (bundle) { + const Preset *curr = &bundle->printers.get_selected_preset(); + if (curr) { + const VendorProfile::PrinterModel *pm = PresetUtils::system_printer_model(*curr); + if (!pm) { + auto curr_parent = bundle->printers.get_selected_preset_parent(); + if (curr_parent) { + pm = PresetUtils::system_printer_model(*curr_parent); + } + } + return pm; + } + } + return nullptr; +} + +std::map Plater::get_bed_texture_maps() +{ + auto pm = get_curr_printer_model(); + if (pm) { + std::map maps; + if (pm->use_double_extruder_default_texture.size() > 0) { + maps["use_double_extruder_default_texture"] = pm->use_double_extruder_default_texture; + } + if (pm->bottom_texture_end_name.size() > 0) { + maps["bottom_texture_end_name"] = pm->bottom_texture_end_name; + } + if (pm->bottom_texture_rect.size() > 0) { + maps["bottom_texture_rect"] = pm->bottom_texture_rect; + } + if (pm->middle_texture_rect.size() > 0) { + maps["middle_texture_rect"] = pm->middle_texture_rect; + } + return maps; + } + return {}; +} + +bool Plater::get_enable_wrapping_detection() +{ + const DynamicPrintConfig & print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const ConfigOptionBool * wrapping_detection = print_config.option("enable_wrapping_detection"); + return (wrapping_detection != nullptr) && wrapping_detection->value; +} + +wxColour Plater::get_next_color_for_filament() +{ + static int curr_color_filamenet = 0; + // refs to https://www.ebaomonthly.com/window/photo/lesson/colorList.htm + wxColour colors[FILAMENT_SYSTEM_COLORS_NUM] = { + // ORCA updated all color palette + wxColour("#00C1AE"), + wxColour("#F4E2C1"), + wxColour("#ED1C24"), + wxColour("#00FF7F"), + wxColour("#F26722"), + wxColour("#FFEB31"), + wxColour("#7841CE"), + wxColour("#115877"), + wxColour("#ED1E79"), + wxColour("#2EBDEF"), + wxColour("#345B2F"), + wxColour("#800080"), + wxColour("#FA8173"), + wxColour("#800000"), + wxColour("#F7B763"), + wxColour("#A4C41E"), + }; + return colors[curr_color_filamenet++ % FILAMENT_SYSTEM_COLORS_NUM]; +} + +wxString Plater::get_slice_warning_string(GCodeProcessorResult::SliceWarning& warning) +{ + if (warning.msg == BED_TEMP_TOO_HIGH_THAN_FILAMENT) { + return _L("The current hot bed temperature is relatively high. The nozzle may be clogged when printing this filament in a closed enclosure. Please open the front door and/or remove the upper glass."); + } else if (warning.msg == NOZZLE_HRC_CHECKER) { + return _L("The nozzle hardness required by the filament is higher than the default nozzle hardness of the printer. Please replace the hardened nozzle or filament, otherwise, the nozzle will be attrited or damaged."); + } else if (warning.msg == NOT_SUPPORT_TRADITIONAL_TIMELAPSE) { + return _L("Enabling traditional timelapse photography may cause surface imperfections. It is recommended to change to smooth mode."); + } else if (warning.msg == NOT_GENERATE_TIMELAPSE) { + return wxString(); + } else if (warning.msg == SMOOTH_TIMELAPSE_WITHOUT_PRIME_TOWER) { + return _L("Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the prime tower, re-slice and print again."); + } + else { + return wxString(warning.msg); + } +} + +void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) +{ + bool use_perspective_camera = get_config("use_perspective_camera").compare("true") == 0; + if (use_perspective_camera) + camera.set_type(Camera::EType::Perspective); + else + camera.set_type(Camera::EType::Ortho); + if (apply && wxGetApp().app_config->get_bool("use_free_camera")) + camera.recover_from_free_camera(); +} + +//BBS: add no slice option +void Plater::priv::select_view_3D(const std::string& name, bool no_slice) +{ + if (name == "3D") { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; + if (q->only_gcode_mode() || q->using_exported_file()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("goto preview page when loading gcode/exported_3mf"); + } + set_current_panel(view3D, no_slice); + } + else if (name == "Preview") { + BOOST_LOG_TRIVIAL(info) << "select preview"; + //BBS update extruder params and speed table before slicing + const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + + Model::setExtruderParams(config, numExtruders); + Model::setPrintSpeedTable(config, print_config); + set_current_panel(preview, no_slice); + } + else if (name == "Assemble") { + BOOST_LOG_TRIVIAL(info) << "select assemble view"; + set_current_panel(assemble_view, no_slice); + } + + //BBS update selection + wxGetApp().obj_list()->update_selections(); + selection_changed(); + + apply_free_camera_correction(false); +} + +void Plater::priv::select_next_view_3D() +{ + + if (current_panel == view3D) + wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview)); + else if (current_panel == preview) + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); +// else if (current_panel == assemble_view) +// set_current_panel(view3D); +} + +void Plater::priv::enable_sidebar(bool enabled) +{ + if (q->m_only_gcode) + enabled = false; + + sidebar_layout.is_enabled = enabled; + update_sidebar(); +} + +void Plater::priv::collapse_sidebar(bool collapse) +{ + if (q->m_only_gcode) + return; + + sidebar_layout.is_collapsed = collapse; + + // Now update the tooltip in the toolbar. + std::string new_tooltip = collapse + ? _u8L("Expand sidebar") + : _u8L("Collapse sidebar"); + new_tooltip += " [" + _u8L("Shift+") + _u8L("Tab") + "]"; + int id = collapse_toolbar.get_item_id("collapse_sidebar"); + collapse_toolbar.set_tooltip(id, new_tooltip); + + update_sidebar(); +} + +void Plater::priv::update_sidebar(bool force_update) { + auto& sidebar = m_aui_mgr.GetPane(this->sidebar); + if (!sidebar.IsOk() || this->current_panel == nullptr) { + return; + } + bool needs_update = force_update; + + if (!sidebar_layout.is_enabled) { + if (sidebar.IsShown()) { + sidebar.Hide(); + needs_update = true; + } + } else { + // Only hide if collapsed or is floating and is not 3d view + const bool should_hide = sidebar_layout.is_collapsed || (sidebar.IsFloating() && !sidebar_layout.show); + const bool should_show = !should_hide; + if (should_show != sidebar.IsShown()) { + sidebar.Show(should_show); + needs_update = true; + } + } + + if (needs_update) { + notification_manager->set_sidebar_collapsed(sidebar.IsShown()); + m_aui_mgr.Update(); + } +} + +void Plater::priv::reset_window_layout() +{ + m_aui_mgr.LoadPerspective(m_default_window_layout, false); + sidebar_layout.is_collapsed = false; + update_sidebar(true); +} + +Sidebar::DockingState Plater::priv::get_sidebar_docking_state() { + if (!sidebar_layout.is_enabled) { + return Sidebar::None; + } + + const auto& sidebar = m_aui_mgr.GetPane(this->sidebar); + if(sidebar.IsFloating()) { + return Sidebar::None; + } + + return sidebar.dock_direction == wxAUI_DOCK_RIGHT ? Sidebar::Right : Sidebar::Left; +} + +void Plater::priv::reset_all_gizmos() +{ + view3D->get_canvas3d()->reset_all_gizmos(); +} + +// Called after the Preferences dialog is closed and the program settings are saved. +// Update the UI based on the current preferences. +void Plater::priv::update_ui_from_settings() +{ + apply_free_camera_correction(); + + view3D->get_canvas3d()->update_ui_from_settings(); + preview->get_canvas3d()->update_ui_from_settings(); + + sidebar->update_ui_from_settings(); +} + +// BBS +std::shared_ptr Plater::priv::statusbar() +{ + return nullptr; +} + +std::string Plater::priv::get_config(const std::string &key) const +{ + return wxGetApp().app_config->get(key); +} + +BoundingBoxf Plater::priv::bed_shape_bb() const +{ + BoundingBox bb = scaled_bed_shape_bb(); + return BoundingBoxf(unscale(bb.min), unscale(bb.max)); +} + +BoundingBox Plater::priv::scaled_bed_shape_bb() const +{ + const auto *bed_shape_opt = config->opt("printable_area"); + const auto printable_area = Slic3r::Polygon::new_scale(bed_shape_opt->values); + return printable_area.bounding_box(); +} + + +void read_binary_stl(const std::string& filename, std::string& model_id, std::string& code) { + std::ifstream file( encode_path(filename.c_str()), std::ios::binary); + if (!file) { + return; + } + + try { + // Read the first 80 bytes + char data[80]; + file.read(data, 80); + if (!file) { + file.close(); + return; + } + + if (data[0] == '\0' || data[0] == ' ') { + file.close(); + return; + } + + char magic[2] = { data[0], data[1] }; + if (magic[0] != 'M' || magic[1] != 'W') { + file.close(); + return; + } + + if (data[2] != ' ') { + file.close(); + return; + } + + char protocol_version[3] = { data[3], data[4], data[5] }; + + //version + if (protocol_version[0] != '1' || protocol_version[1] != '.' || protocol_version[2] != '0') { + file.close(); + return; + } + + std::vector tokens; + std::istringstream iss(data); + std::string token; + while (std::getline(iss, token, ' ')) { + char* tokenPtr = new char[token.length() + 1]; + std::strcpy(tokenPtr, token.c_str()); + tokens.push_back(tokenPtr); + } + + //model id + if (tokens.size() < 4) { + file.close(); + return; + } + + model_id = tokens[2]; + code = tokens[3]; + file.close(); + } + catch (...) { + } + return; +} + +// BBS: backup & restore +std::vector Plater::priv::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi) +{ + std::vector empty_result; + bool dlg_cont = true; + bool is_user_cancel = false; + bool translate_old = false; + int current_width = 0, current_depth = 0, current_height = 0, project_filament_count = 1; + + if (input_files.empty()) + return std::vector(); + + if (!input_files.empty()) + q->m_3mf_path = input_files[0].string(); + + // SoftFever: ugly fix so we can exist pa calib mode + background_process.fff_print()->calib_mode() = CalibMode::Calib_None; + + + // BBS + int filaments_cnt = config->opt("filament_colour")->values.size(); + bool one_by_one = input_files.size() == 1 || printer_technology == ptSLA/* || filaments_cnt <= 1*/; + if (! one_by_one) { + for (const auto &path : input_files) { + if (std::regex_match(path.string(), pattern_bundle)) { + one_by_one = true; + break; + } + } + } + + bool load_model = strategy & LoadStrategy::LoadModel; + bool load_config = strategy & LoadStrategy::LoadConfig; + bool imperial_units = strategy & LoadStrategy::ImperialUnits; + bool silence = strategy & LoadStrategy::Silence; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": load_model %1%, load_config %2%, input_files size %3%")%load_model %load_config %input_files.size(); + + const auto loading = _L("Loading") + dots; + ProgressDialog dlg(loading, "", 100, find_toplevel_parent(q), wxPD_AUTO_HIDE | wxPD_CAN_ABORT | wxPD_APP_MODAL); + wxBusyCursor busy; + + auto *new_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); + std::vector obj_idxs; + + std::string designer_model_id; + std::string designer_country_code; + + int answer_convert_from_meters = wxOK_DEFAULT; + int answer_convert_from_imperial_units = wxOK_DEFAULT; + int tolal_model_count = 0; + + int progress_percent = 0; + int total_files = input_files.size(); + const int stage_percent[IMPORT_STAGE_MAX+1] = { + 5, // IMPORT_STAGE_RESTORE + 10, // IMPORT_STAGE_OPEN + 30, // IMPORT_STAGE_READ_FILES + 50, // IMPORT_STAGE_EXTRACT + 60, // IMPORT_STAGE_LOADING_OBJECTS + 70, // IMPORT_STAGE_LOADING_PLATES + 80, // IMPORT_STAGE_FINISH + 85, // IMPORT_STAGE_ADD_INSTANCE + 90, // IMPORT_STAGE_UPDATE_GCODE + 92, // IMPORT_STAGE_CHECK_MODE_GCODE + 95, // UPDATE_GCODE_RESULT + 98, // IMPORT_LOAD_CONFIG + 99, // IMPORT_LOAD_MODEL_OBJECTS + 100 + }; + const int step_percent[LOAD_STEP_STAGE_NUM+1] = { + 5, // LOAD_STEP_STAGE_READ_FILE + 30, // LOAD_STEP_STAGE_GET_SOLID + 60, // LOAD_STEP_STAGE_GET_MESH + 100 + }; + + const float INPUT_FILES_RATIO = 0.7; + const float INIT_MODEL_RATIO = 0.75; + const float CENTER_AROUND_ORIGIN_RATIO = 0.8; + const float LOAD_MODEL_RATIO = 0.9; + + for (size_t i = 0; i < input_files.size(); ++i) { + int file_percent = 0; + +#ifdef _WIN32 + auto path = input_files[i]; + // On Windows, we swap slashes to back slashes, see GH #6803 as read_from_file() does not understand slashes on Windows thus it assignes full path to names of loaded objects. + path.make_preferred(); +#else // _WIN32 + // Don't make a copy on Posix. Slash is a path separator, back slashes are not accepted as a substitute. + const auto &path = input_files[i]; +#endif // _WIN32 + const auto filename = path.filename(); + int progress_percent = static_cast(100.0f * static_cast(i) / static_cast(input_files.size())); + const auto real_filename = (strategy & LoadStrategy::Restore) ? input_files[++i].filename() : filename; + const auto dlg_info = _L("Loading file") + ": " + from_path(real_filename); + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << boost::format(": load file %1%") % filename; + dlg_cont = dlg.Update(progress_percent, dlg_info); + if (!dlg_cont) return empty_result; + + const bool type_3mf = std::regex_match(path.string(), pattern_3mf); + // const bool type_zip_amf = !type_3mf && std::regex_match(path.string(), pattern_zip_amf); + const bool type_any_amf = !type_3mf && std::regex_match(path.string(), pattern_any_amf); + // const bool type_prusa = std::regex_match(path.string(), pattern_prusa); + + Slic3r::Model model; + // BBS: add auxiliary files related logic + bool load_aux = strategy & LoadStrategy::LoadAuxiliary, load_old_project = false; + if (load_model && load_config && type_3mf) { + load_aux = true; + strategy = strategy | LoadStrategy::LoadAuxiliary; + } + if (load_config) strategy = strategy | LoadStrategy::CheckVersion; + bool is_project_file = false; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": is_project_file %1%, type_3mf %2%") % is_project_file % type_3mf; + try { + if (type_3mf) { + DynamicPrintConfig config; + Semver file_version; + En3mfType en_3mf_file_type = En3mfType::From_BBS; + { + DynamicPrintConfig config_loaded; + + // BBS: add part plate related logic + PlateDataPtrs plate_data; + ConfigSubstitutionContext config_substitutions{ForwardCompatibilitySubstitutionRule::Enable}; + std::vector project_presets; + // BBS: backup & restore + q->skip_thumbnail_invalid = true; + model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, en_3mf_file_type, strategy, &plate_data, &project_presets, + &file_version, + [this, &dlg, real_filename, &progress_percent, &file_percent, stage_percent, INPUT_FILES_RATIO, total_files, i, + &is_user_cancel](int import_stage, int current, int total, bool &cancel) { + bool cont = true; + float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * ((float)stage_percent[import_stage] + (float)current * (float)(stage_percent[import_stage + 1] - stage_percent[import_stage]) /(float) total) / (float)total_files; + BOOST_LOG_TRIVIAL(trace) << "load_3mf_file: percent(float)=" << percent_float << ", stage = " << import_stage << ", curr = " << current << ", total = " << total; + progress_percent = (int)percent_float; + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + cont = dlg.Update(progress_percent, msg); + cancel = !cont; + if (cancel) + is_user_cancel = cancel; + }); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ + << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % plate_data.size() % + project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % file_version.to_string(); + + // 1. add extruder for prusa model if the number of existing extruders is not enough + // 2. add extruder for BBS or Other model if only import geometry + if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) { + std::set extruderIds; + for (ModelObject *o : model.objects) { + if (o->config.option("extruder")) extruderIds.insert(o->config.extruder()); + for (auto volume : o->volumes) { + if (volume->config.option("extruder")) extruderIds.insert(volume->config.extruder()); + for (int extruder : volume->get_extruders()) { extruderIds.insert(extruder); } + } + } + int size = extruderIds.size() == 0 ? 0 : *(extruderIds.rbegin()); + + int filament_size = sidebar->combos_filament().size(); + while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) { + int filament_count = filament_size + 1; + wxColour new_col = Plater::get_next_color_for_filament(); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); + wxGetApp().plater()->on_filament_count_change(filament_count); + ++filament_size; + } + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + } + + std::string import_project_action = wxGetApp().app_config->get("import_project_action"); + LoadType load_type; + if (import_project_action.empty()) + load_type = LoadType::Unknown; + else + load_type = static_cast(std::stoi(import_project_action)); + + // BBS: version check + Semver app_version = *(Semver::parse(SoftFever_VERSION)); + const wxString load_3mf_title = _L("Load 3MF"); + const wxString newer_3mf_title = _L("Newer 3MF version"); + const wxString bambu_project_title = _L("BambuStudio Project"); + const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only."); + const wxString msg_old_orca_geometry = _L("The 3MF file was generated by an old OrcaSlicer version, loading geometry data only."); + const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only."); + const wxString msg_bambu_geometry = _L("The 3MF file was generated by BambuStudio, loading geometry data only."); + auto log_and_show_3mf_info = [&](const wxString& text, const wxString& title) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ + << " " + << boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string(); + show_info(q, text, title); + }; + if (en_3mf_file_type == En3mfType::From_Prusa) { + // do not reset the model config + load_config = false; + if (load_type != LoadType::LoadGeometry) + log_and_show_3mf_info(msg_unsupported_geometry, load_3mf_title); + } + else if (en_3mf_file_type == En3mfType::From_Orca) { + // OrcaSlicer file (has OrcaSlicer tag) - compare file_version with SoftFever_VERSION + // Migration fix for OrcaSlicer 2.3.1-alpha sparse infill rotation template + if (load_config && (file_version < app_version) && file_version == Semver("2.3.1-alpha")) { + if (!config_loaded.opt_string("sparse_infill_rotate_template").empty()) { + const auto _sparse_infill_pattern = + config_loaded.option>("sparse_infill_pattern")->value; + bool is_safe_to_rotate = _sparse_infill_pattern == ipRectilinear || _sparse_infill_pattern == ipLine || + _sparse_infill_pattern == ipZigZag || _sparse_infill_pattern == ipCrossZag || + _sparse_infill_pattern == ipLockedZag; + if (!is_safe_to_rotate) { + wxString msg_text = _( + L("This project was created with an OrcaSlicer 2.3.1-alpha and uses " + "infill rotation template settings that may not work properly with your current infill pattern. " + "This could result in weak support or print quality issues.")); + msg_text += "\n\n" + + _(L("Would you like OrcaSlicer to automatically fix this by clearing the rotation template settings?")); + MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxYES | wxNO); + dialog.SetButtonLabel(wxID_YES, _L("Yes")); + dialog.SetButtonLabel(wxID_NO, _L("No")); + if (dialog.ShowModal() == wxID_YES) { + config_loaded.opt_string("sparse_infill_rotate_template") = ""; + } + } + } + } else if (load_config && (file_version > app_version)) { + if (config_substitutions.unrecogized_keys.size() > 0) { + wxString text = wxString::Format(_L("The 3MF file version %s is newer than %s's version %s, found the following unrecognized keys:"), + file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); + text += "\n"; + wxString context = text; + wxString append = _L("You'd better upgrade your software.\n"); + context += "\n\n"; + context += append; + log_and_show_3mf_info(context, newer_3mf_title); + } + else { + //if the minor version is not matched + if (file_version.min() != app_version.min()) { + wxString text = wxString::Format(_L("The 3MF file version %s is newer than %s's version %s, we suggest to upgrade your software."), + file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); + text += "\n"; + log_and_show_3mf_info(text, newer_3mf_title); + } + } + } + else if (load_config && config_loaded.empty()) { + load_config = false; + log_and_show_3mf_info(msg_old_orca_geometry, load_3mf_title); + } + } + else if (en_3mf_file_type == En3mfType::From_BBS) { + // No OrcaSlicer tag - check Bambu/Application version + Semver orca_tag_start_version(2, 3, 2); + if (file_version <= orca_tag_start_version) { + // Compatible old version (before OrcaSlicer tagging was introduced after 2.3.2). + // Any version prior or equal to 2.3.2 is older than the current one, no version warnings needed. + // Still apply migration fixes for known old versions. + if (load_config && (file_version == Semver("2.3.1-alpha"))) { + if (!config_loaded.opt_string("sparse_infill_rotate_template").empty()) { + const auto _sparse_infill_pattern = + config_loaded.option>("sparse_infill_pattern")->value; + bool is_safe_to_rotate = _sparse_infill_pattern == ipRectilinear || _sparse_infill_pattern == ipLine || + _sparse_infill_pattern == ipZigZag || _sparse_infill_pattern == ipCrossZag || + _sparse_infill_pattern == ipLockedZag; + if (!is_safe_to_rotate) { + wxString msg_text = _( + L("This project was created with an OrcaSlicer 2.3.1-alpha and uses " + "infill rotation template settings that may not work properly with your current infill pattern. " + "This could result in weak support or print quality issues.")); + msg_text += "\n\n" + + _(L("Would you like OrcaSlicer to automatically fix this by clearing the rotation template settings?")); + MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxYES | wxNO); + dialog.SetButtonLabel(wxID_YES, _L("Yes")); + dialog.SetButtonLabel(wxID_NO, _L("No")); + if (dialog.ShowModal() == wxID_YES) { + config_loaded.opt_string("sparse_infill_rotate_template") = ""; + } + } + } + } + else if (load_config && config_loaded.empty()) { + load_config = false; + log_and_show_3mf_info(msg_older_geometry, load_3mf_title); + } + } else { + // BambuStudio project (version > 2.3.2 without OrcaSlicer tag) + // Report that a BambuStudio project is being imported and compare with SLIC3R_VERSION + Semver slic3r_version = *(Semver::parse(SLIC3R_VERSION)); + if (load_config && config_loaded.empty()) { + load_config = false; + log_and_show_3mf_info(msg_bambu_geometry, load_3mf_title); + } + else if (load_config && (file_version > slic3r_version)) { + // BambuStudio file version is newer than our compatible SLIC3R_VERSION + if (config_substitutions.unrecogized_keys.size() > 0) { + wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Found unrecognized settings:"), + file_version.to_string(), slic3r_version.to_string()); + text += "\n"; + wxString context = text; + wxString append = _L("You'd better upgrade your software.\n"); + context += "\n\n"; + context += append; + log_and_show_3mf_info(context, bambu_project_title); + } else { + wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Some settings may not be fully compatible."), + file_version.to_string(), slic3r_version.to_string()); + text += "\n"; + log_and_show_3mf_info(text, bambu_project_title); + } + } else if (load_config) { + // BambuStudio version is older or same as our SLIC3R_VERSION + wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer."); + log_and_show_3mf_info(text, bambu_project_title); + } + } + } + else if (en_3mf_file_type == En3mfType::From_Other) { + // Generic CAD/other 3MF without slicer metadata: import geometry silently. + if (load_config && config_loaded.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ + << " " + << boost::format("3MF has no slicer metadata/project config, importing geometry only: %1%") % path.string(); + load_config = false; + } + } + else if (load_config && config_loaded.empty()) { + load_config = false; + log_and_show_3mf_info(msg_old_orca_geometry, load_3mf_title); + } + else if (!load_config) { + // reset config except color + for (ModelObject *model_object : model.objects) { + bool has_extruder = model_object->config.has("extruder"); + int extruder_id = -1; + // save the extruder information before reset + if (has_extruder) { extruder_id = model_object->config.extruder(); } + + model_object->config.reset(); + + // restore the extruder after reset + if (has_extruder) { model_object->config.set("extruder", extruder_id); } + + // Is there any modifier or advanced config data? + for (ModelVolume *model_volume : model_object->volumes) { + has_extruder = model_volume->config.has("extruder"); + if (has_extruder) { extruder_id = model_volume->config.extruder(); } + + model_volume->config.reset(); + + if (has_extruder) { model_volume->config.set("extruder", extruder_id); } + } + } + } + + // plate data + if (plate_data.size() > 0) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", import 3mf UPDATE_GCODE_RESULT \n"); + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + dlg_cont = dlg.Update(progress_percent, msg); + if (!dlg_cont) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + + Semver old_version(1, 5, 9); + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && load_model && load_config && !config_loaded.empty()) { + translate_old = true; + partplate_list.get_plate_size(current_width, current_depth, current_height); + } + + if (load_config) { + if (translate_old) { + //set the size back + partplate_list.reset_size(current_width + Bed3D::Axes::DefaultTipRadius, current_depth + Bed3D::Axes::DefaultTipRadius, current_height, false); + } + project_filament_count = config_loaded.option("filament_colour")->size(); + partplate_list.load_from_3mf_structure(plate_data, project_filament_count); + partplate_list.update_slice_context_to_current_plate(background_process); + this->preview->update_gcode_result(partplate_list.get_current_slice_result()); + release_PlateData_list(plate_data); + sidebar->obj_list()->reload_all_plates(); + q->suppress_background_process(true); + } else { + partplate_list.reload_all_objects(); + } + } + + // BBS:: project embedded presets + if ((project_presets.size() > 0) && load_config) { + // load project embedded presets + PresetsConfigSubstitutions preset_substitutions; + PresetBundle & preset_bundle = *wxGetApp().preset_bundle; + preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable); + if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions); + } + if (project_presets.size() > 0) { + for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; } + project_presets.clear(); + } + + if (load_config && !config_loaded.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", import 3mf IMPORT_LOAD_CONFIG \n"); + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + dlg_cont = dlg.Update(progress_percent, msg); + if (!dlg_cont) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + + // Based on the printer technology field found in the loaded config, select the base for the config, + PrinterTechnology printer_technology = Preset::printer_technology(config_loaded); + + config.apply(static_cast(FullPrintConfig::defaults())); + // and place the loaded config over the base. + config += std::move(config_loaded); + std::map validity = config.validate(); + if (!validity.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("Param values in 3mf error: "); + for (std::map::iterator it=validity.begin(); it!=validity.end(); ++it) + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("%1%: %2%")%it->first %it->second; + // + NotificationManager *notify_manager = q->get_notification_manager(); + std::string error_message = L("Invalid values found in the 3MF:"); + error_message += "\n"; + for (std::map::iterator it=validity.begin(); it!=validity.end(); ++it) + error_message += "-" + it->first + ": " + it->second + "\n"; + error_message += "\n"; + error_message += L("Please correct them in the param tabs"); + notify_manager->bbl_show_3mf_warn_notification(error_message); + } + } + if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string()); + + // BBS + if (load_model && !load_config) { + ; + } + else { + this->model.plates_custom_gcodes = model.plates_custom_gcodes; + this->model.design_info = model.design_info; + this->model.model_info = model.model_info; + } + } + + if (load_config) { + if (!config.empty()) { + Preset::normalize(config); + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + + { + // BBS: modify the prime tower params for old version file + Semver old_version3(2, 0, 0); + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && file_version < old_version3) { + double old_filament_prime_volume = 0.; + int filament_count = 0; + { + ConfigOptionFloats *filament_prime_volume_option = config.option("filament_prime_volume"); + ConfigOptionStrings *filament_colors_option = config.option("filament_colour", true); + filament_count = filament_colors_option->values.size(); + if (filament_prime_volume_option) { + std::vector &filament_prime_volume_values = filament_prime_volume_option->values; + if (!filament_prime_volume_values.empty()) { + old_filament_prime_volume = filament_prime_volume_values[0]; + if (filament_count > 1) filament_prime_volume_values.resize(filament_count, old_filament_prime_volume); + } + } + } + ConfigOptionEnum *prime_tower_rib_wall_option = config.option>("wipe_tower_wall_type", true); + prime_tower_rib_wall_option->value = WipeTowerWallType::wtwRectangle; + + ConfigOptionPercent *prime_tower_infill_gap_option = config.option("prime_tower_infill_gap", true); + prime_tower_infill_gap_option->value = 100; + + ConfigOptionInts *filament_adhesiveness_category_option = config.option("filament_adhesiveness_category", true); + std::vector &filament_adhesiveness_category_values = filament_adhesiveness_category_option->values; + filament_adhesiveness_category_values.resize(filament_count); + for (int index = 0; index < filament_count; index++) + filament_adhesiveness_category_values[index] = 100; + + std::vector &diff_settings = config.option("different_settings_to_system", true)->values; + diff_settings.resize(filament_count + 2); + + std::vector diff_process_keys; + std::string diff_process_settings = diff_settings[0]; + Slic3r::unescape_strings_cstyle(diff_process_settings, diff_process_keys); + diff_process_keys.emplace_back("wipe_tower_wall_type"); + diff_process_keys.emplace_back("prime_tower_infill_gap"); + diff_process_settings = Slic3r::escape_strings_cstyle(diff_process_keys); + diff_settings[0] = diff_process_settings; + + for (int index = 0; index < filament_count; index++) { + std::vector diff_filament_keys; + std::string diff_filament_settings = diff_settings[index + 1]; + Slic3r::unescape_strings_cstyle(diff_filament_settings, diff_filament_keys); + diff_filament_keys.emplace_back("filament_adhesiveness_category"); + diff_filament_settings = Slic3r::escape_strings_cstyle(diff_filament_keys); + diff_settings[index + 1] = diff_filament_settings; + } + } + } + + auto choise = wxGetApp().app_config->get("no_warn_when_modified_gcodes"); + if (choise.empty() || choise != "true") { + // BBS: first validate the printer + // validate the system profiles + std::set modified_gcodes; + int validated = preset_bundle->validate_presets(filename.string(), config, modified_gcodes); + if (validated == VALIDATE_PRESETS_MODIFIED_GCODES) { + std::string warning_message; + warning_message += "\n"; + for (std::set::iterator it=modified_gcodes.begin(); it!=modified_gcodes.end(); ++it) + warning_message += "-" + *it + "\n"; + warning_message += "\n"; + //show_info(q, _L("The 3MF has the following modified G-code in filament or printer presets:") + warning_message + _L("Please confirm that all modified G-code is safe to prevent any damage to the machine!"), _L("Modified G-code")); + MessageDialog dlg(q, _L("The 3MF has the following modified G-code in filament or printer presets:") + warning_message + _L("Please confirm that all modified G-code is safe to prevent any damage to the machine!"), _L("Modified G-code")); + dlg.show_dsa_button(); + auto res = dlg.ShowModal(); + if (dlg.get_checkbox_state()) + wxGetApp().app_config->set("no_warn_when_modified_gcodes", "true"); + } + else if ((validated == VALIDATE_PRESETS_PRINTER_NOT_FOUND) || (validated == VALIDATE_PRESETS_FILAMENTS_NOT_FOUND)) { + std::string warning_message; + warning_message += "\n"; + for (std::set::iterator it=modified_gcodes.begin(); it!=modified_gcodes.end(); ++it) + warning_message += "-" + *it + "\n"; + warning_message += "\n"; + //show_info(q, _L("The 3MF has the following customized filament or printer presets:") + warning_message + _L("Please confirm that the G-code within these presets is safe to prevent any damage to the machine!"), _L("Customized Preset")); + MessageDialog dlg(q, _L("The 3MF has the following customized filament or printer presets:") + from_u8(warning_message)+ _L("Please confirm that the G-code within these presets is safe to prevent any damage to the machine!"), _L("Customized Preset")); + dlg.show_dsa_button(); + auto res = dlg.ShowModal(); + if (dlg.get_checkbox_state()) + wxGetApp().app_config->set("no_warn_when_modified_gcodes", "true"); + } + } + + //always load config + { + // BBS: save the wipe tower pos in file here, will be used later + ConfigOptionFloats* wipe_tower_x_opt = config.opt("wipe_tower_x"); + ConfigOptionFloats* wipe_tower_y_opt = config.opt("wipe_tower_y"); + std::optionalfile_wipe_tower_x; + std::optionalfile_wipe_tower_y; + if (wipe_tower_x_opt) + file_wipe_tower_x = *wipe_tower_x_opt; + if (wipe_tower_y_opt) + file_wipe_tower_y = *wipe_tower_y_opt; + + preset_bundle->load_config_model(filename.string(), std::move(config), file_version); + + ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type"); + if (bed_type_opt != nullptr) { + BedType bed_type = (BedType)bed_type_opt->getInt(); + // update app config for bed type + bool is_bbl_preset = preset_bundle->is_bbl_vendor(); + if (is_bbl_preset) { + AppConfig* app_config = wxGetApp().app_config; + if (app_config) + app_config->set("curr_bed_type", std::to_string(int(bed_type))); + } + q->on_bed_type_change(bed_type); + } + + // BBS: moved this logic to presetcollection + //{ + // // After loading of the presets from project, check if they are visible. + // // Set them to visible if they are not. + + // auto update_selected_preset_visibility = [](PresetCollection& presets, std::vector& names) { + // if (!presets.get_selected_preset().is_visible) { + // assert(presets.get_selected_preset().name == presets.get_edited_preset().name); + // presets.get_selected_preset().is_visible = true; + // presets.get_edited_preset().is_visible = true; + // names.emplace_back(presets.get_selected_preset().name); + // } + // }; + + // std::vector names; + // if (printer_technology == ptFFF) { + // update_selected_preset_visibility(preset_bundle->prints, names); + // for (const std::string& filament : preset_bundle->filament_presets) { + // Preset* preset = preset_bundle->filaments.find_preset(filament); + // if (preset && !preset->is_visible) { + // preset->is_visible = true; + // names.emplace_back(preset->name); + // if (preset->name == preset_bundle->filaments.get_edited_preset().name) + // preset_bundle->filaments.get_selected_preset().is_visible = true; + // } + // } + // } + // else { + // update_selected_preset_visibility(preset_bundle->sla_prints, names); + // update_selected_preset_visibility(preset_bundle->sla_materials, names); + // } + // update_selected_preset_visibility(preset_bundle->printers, names); + + // preset_bundle->update_compatible(PresetSelectCompatibleType::Never); + + // // show notification about temporarily installed presets + // if (!names.empty()) { + // std::string notif_text = into_u8(_L_PLURAL("The preset below was temporarily installed on the active instance of PrusaSlicer", + // "The presets below were temporarily installed on the active instance of PrusaSlicer", + // names.size())) + ":"; + // for (std::string& name : names) + // notif_text += "\n - " + name; + // notification_manager->push_notification(NotificationType::CustomNotification, + // NotificationManager::NotificationLevel::PrintInfoNotificationLevel, notif_text); + // } + //} + + // BBS + // if (printer_technology == ptFFF) + // CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, &preset_bundle->project_config); + + // For exporting from the amf/3mf we shouldn't check printer_presets for the containing information about "Print Host upload" + // BBS: add preset combo box re-active logic + // currently found only needs re-active here + wxGetApp().load_current_presets(false, false); + // Update filament colors for the MM-printer profile in the full config + // to avoid black (default) colors for Extruders in the ObjectList, + // when for extruder colors are used filament colors + q->on_filament_count_change(preset_bundle->filament_presets.size()); + is_project_file = true; + + DynamicConfig& proj_cfg = preset_bundle->project_config; + // do some post process after loading config + { + //BBS: rewrite wipe tower pos stored in 3mf file , the code above should be seriously reconsidered + ConfigOptionFloats* wipe_tower_x = proj_cfg.opt("wipe_tower_x"); + ConfigOptionFloats* wipe_tower_y = proj_cfg.opt("wipe_tower_y"); + if (file_wipe_tower_x) + *wipe_tower_x = *file_wipe_tower_x; + if (file_wipe_tower_y) + *wipe_tower_y = *file_wipe_tower_y; + + ConfigOptionStrings* filament_color = proj_cfg.opt("filament_colour"); + if (filament_color) { + size_t filament_count = filament_color->size(); + + // Sync filament map + ConfigOptionInts* filament_map = proj_cfg.opt("filament_map", true); + if (filament_map->size() != filament_count) { + filament_map->values.resize(filament_count, 1); + } + + // Sync filament multi colour + ConfigOptionStrings* filament_multi_color = proj_cfg.opt("filament_multi_colour", true); + if (filament_multi_color->size() != filament_count) { + filament_multi_color->values.resize(filament_count); + } + // If there is no multi-color data or color is not match, use single color as default value + for (size_t i = 0; i < filament_count; i++) { + std::vector colors = Slic3r::split_string(filament_multi_color->values[i], ' '); + if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) { + filament_multi_color->values[i] = filament_color->values[i]; + } + } + // Sync filament colour type + ConfigOptionStrings* filament_color_type = proj_cfg.opt("filament_colour_type", true); + if (filament_color_type && filament_color_type->size() != filament_count) { + filament_color_type->values.resize(filament_count); + + for (size_t i = 0; i < filament_count; i++) { + if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) { + filament_color_type->values[i] = "1"; + } + } + } + } + } + // Update filament combobox after loading config + wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT); + } + } + if (!silence) wxGetApp().app_config->update_config_dir(path.parent_path().string()); + } + } else { + // BBS: add plate data related logic + PlateDataPtrs plate_data; + // BBS: project embedded settings + std::vector project_presets; + bool is_xxx; + Semver file_version; + + //ObjImportColorFn obj_color_fun=nullptr; + auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { + + if (!boost::iends_with(path.string(), ".obj")) { return; } + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); + if (color_dlg.ShowModal() != wxID_OK) { + in_out.filament_ids.clear(); + } + }; + if (boost::iends_with(path.string(), ".stp") || + boost::iends_with(path.string(), ".step")) { + double linear = string_to_double_decimal_point(wxGetApp().app_config->get("linear_defletion")); + if (linear <= 0) linear = 0.003; + double angle = string_to_double_decimal_point(wxGetApp().app_config->get("angle_defletion")); + if (angle <= 0) angle = 0.5; + bool split_compound = wxGetApp().app_config->get_bool("is_split_compound"); + model = Slic3r::Model:: read_from_step(path.string(), strategy, + [this, &dlg, real_filename, &progress_percent, &file_percent, step_percent, INPUT_FILES_RATIO, total_files, i](int load_stage, int current, int total, bool &cancel) + { + bool cont = true; + float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * ((float)step_percent[load_stage] + (float)current * (float)(step_percent[load_stage + 1] - step_percent[load_stage]) / (float)total) / (float)total_files; + BOOST_LOG_TRIVIAL(trace) << "load_step_file: percent(float)=" << percent_float << ", stage = " << load_stage << ", curr = " << current << ", total = " << total; + progress_percent = (int)percent_float; + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + cont = dlg.Update(progress_percent, msg); + cancel = !cont; + }, + [](int isUtf8StepFile) { + if (!isUtf8StepFile) { + const auto no_warn = wxGetApp().app_config->get_bool("step_not_utf8_no_warn"); + if (!no_warn) { + MessageDialog dlg(nullptr, _L("Name of components inside STEP file is not UTF8 format!") + "\n\n" + _L("The name may show garbage characters!"), + wxString(SLIC3R_APP_FULL_NAME " - ") + _L("Attention!"), wxOK | wxICON_INFORMATION); + dlg.show_dsa_button(_L("Remember my choice.")); + dlg.ShowModal(); + if (dlg.get_checkbox_state()) { + wxGetApp().app_config->set_bool("step_not_utf8_no_warn", true); + } + } + } + }, + [this, &path, &is_user_cancel, &linear, &angle, &split_compound](Slic3r::Step& file, double& linear_value, double& angle_value, bool& is_split)-> int { + if (wxGetApp().app_config->get_bool("enable_step_mesh_setting")) { + StepMeshDialog mesh_dlg(nullptr, file, linear, angle); + if (mesh_dlg.ShowModal() == wxID_OK) { + linear_value = mesh_dlg.get_linear_defletion(); + angle_value = mesh_dlg.get_angle_defletion(); + is_split = mesh_dlg.get_split_compound_value(); + return 1; + } + }else { + linear_value = linear; + angle_value = angle; + is_split = split_compound; + return 1; + } + is_user_cancel = true; + return -1; + }, linear, angle, split_compound); + }else { + model = Slic3r::Model:: read_from_file( + path.string(), nullptr, nullptr, strategy, &plate_data, &project_presets, &is_xxx, &file_version, nullptr, + [this, &dlg, real_filename, &progress_percent, &file_percent, INPUT_FILES_RATIO, total_files, i, &designer_model_id, &designer_country_code](int current, int total, bool &cancel, std::string &mode_id, std::string &code) + { + designer_model_id = mode_id; + designer_country_code = code; + + bool cont = true; + float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * 100.0f * ((float)current / (float)total) / (float)total_files; + BOOST_LOG_TRIVIAL(trace) << "load_stl_file: percent(float)=" << percent_float << ", curr = " << current << ", total = " << total; + progress_percent = (int)percent_float; + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + cont = dlg.Update(progress_percent, msg); + cancel = !cont; + }, + nullptr, 0, obj_color_fun); + } + + if (designer_model_id.empty() && boost::algorithm::iends_with(path.string(), ".stl")) { + read_binary_stl(path.string(), designer_model_id, designer_country_code); + } + + if (type_any_amf && is_xxx) imperial_units = true; + + for (auto obj : model.objects) { + if (obj->name.empty()) { + obj->name = fs::path(obj->input_file).filename().string(); + } + obj->rotate(Geometry::deg2rad(config->opt_float("preferred_orientation")), Axis::Z); + } + + if (plate_data.size() > 0) { + partplate_list.load_from_3mf_structure(plate_data, project_filament_count); + partplate_list.update_slice_context_to_current_plate(background_process); + this->preview->update_gcode_result(partplate_list.get_current_slice_result()); + release_PlateData_list(plate_data); + sidebar->obj_list()->reload_all_plates(); + } + + // BBS:: project embedded presets + if (project_presets.size() > 0) { + // load project embedded presets + PresetsConfigSubstitutions preset_substitutions; + PresetBundle & preset_bundle = *wxGetApp().preset_bundle; + preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable); + if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions); + + for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; } + project_presets.clear(); + } + } + } catch (const ConfigurationError &e) { + std::string message = GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), filename.string()) + "\n\n" + e.what(); + GUI::show_error(q, message); + continue; + } catch (const std::exception &e) { + if (!is_user_cancel) + GUI::show_error(q, e.what()); + continue; + } + + progress_percent = 100.0f * (float)i / (float)total_files + INIT_MODEL_RATIO * 100.0f / (float)total_files; + dlg_cont = dlg.Update(progress_percent); + if (!dlg_cont) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + + if (load_model) { + // The model should now be initialized + auto convert_from_imperial_units = [](Model &model, bool only_small_volumes) { model.convert_from_imperial_units(only_small_volumes); }; + + // BBS: add load_old_project logic + if ((!is_project_file) && (!load_old_project)) { + // if (!is_project_file) { + if (int deleted_objects = model.removed_objects_with_zero_volume(); deleted_objects > 0) { + MessageDialog(q, _L("Objects with zero volume removed"), _L("The volume of the object is zero"), wxICON_INFORMATION | wxOK).ShowModal(); + } + if (imperial_units) + // Convert even if the object is big. + convert_from_imperial_units(model, false); + else if (model.looks_like_saved_in_meters()) { + // BBS do not handle look like in meters + MessageDialog dlg(q, + format_wxstr(_L("The object from file %s is too small, and maybe in meters or inches.\n Do you want to scale to millimeters?"), + from_path(filename)), + _L("Object too small"), wxICON_QUESTION | wxYES_NO); + int answer = dlg.ShowModal(); + if (answer == wxID_YES) model.convert_from_meters(true); + } else if (model.looks_like_imperial_units()) { + // BBS do not handle look like in meters + MessageDialog dlg(q, + format_wxstr(_L("The object from file %s is too small, and maybe in meters or inches.\n Do you want to scale to millimeters?"), + from_path(filename)), + _L("Object too small"), wxICON_QUESTION | wxYES_NO); + int answer = dlg.ShowModal(); + if (answer == wxID_YES) convert_from_imperial_units(model, true); + } + // else if (model.looks_like_imperial_units()) { + // BBS do not handle look like in imperial + // auto convert_model_if = [convert_from_imperial_units](Model& model, bool condition) { + // if (condition) + // //FIXME up-scale only the small parts? + // convert_from_imperial_units(model, true); + //}; + // if (answer_convert_from_imperial_units == wxOK_DEFAULT) { + // RichMessageDialog dlg(q, format_wxstr(_L_PLURAL( + // "The dimensions of the object from file %s seem to be defined in inches.\n" + // "The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of the object?", + // "The dimensions of some objects from file %s seem to be defined in inches.\n" + // "The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of these objects?", model.objects.size()), from_path(filename)) + // + "\n", _L("The object is too small"), wxICON_QUESTION | wxYES_NO); + // dlg.ShowCheckBox(_L("Apply to all the remaining small objects being loaded.")); + // int answer = dlg.ShowModal(); + // if (dlg.IsCheckBoxChecked()) + // answer_convert_from_imperial_units = answer; + // else + // convert_model_if(model, answer == wxID_YES); + //} + // convert_model_if(model, answer_convert_from_imperial_units == wxID_YES); + } + + if (!is_project_file && model.looks_like_multipart_object()) { + MessageDialog msg_dlg(q, _L( + "This file contains several objects positioned at multiple heights.\n" + "Instead of considering them as multiple objects, should \n" + "the file be loaded as a single object having multiple parts?") + "\n", + _L("Multi-part object detected"), wxICON_WARNING | wxYES | wxNO); + if (msg_dlg.ShowModal() == wxID_YES) { + model.convert_multipart_object(filaments_cnt); + } + } + } + // else if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) { + // MessageDialog msg_dlg(q, _L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?")+"\n", + // _L("Detected advanced data"), wxICON_WARNING | wxYES | wxNO); + // if (msg_dlg.ShowModal() == wxID_YES) { + // Slic3r::GUI::wxGetApp().save_mode(comAdvanced); + // view3D->set_as_dirty(); + // } + // else + // return obj_idxs; + //} + + progress_percent = 100.0f * (float)i / (float)total_files + CENTER_AROUND_ORIGIN_RATIO * 100.0f / (float)total_files; + dlg_cont = dlg.Update(progress_percent); + if (!dlg_cont) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + + int model_idx = 0; + for (ModelObject *model_object : model.objects) { + if (!type_3mf && !type_any_amf) + model_object->center_around_origin(false); + + // BBS + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("import 3mf IMPORT_LOAD_MODEL_OBJECTS \n"); + wxString msg = wxString::Format("Loading file: %s", from_path(real_filename)); + model_idx++; + dlg_cont = dlg.Update(progress_percent, msg); + if (!dlg_cont) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + + if (!model_object->instances.empty()) + model_object->ensure_on_bed(is_project_file); + } + + tolal_model_count += model_idx; + + progress_percent = 100.0f * (float)i / (float)total_files + LOAD_MODEL_RATIO * 100.0f / (float)total_files; + dlg_cont = dlg.Update(progress_percent); + if (!dlg_cont) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + + if (one_by_one) { + // BBS: add load_old_project logic + if (type_3mf && !is_project_file && !load_old_project) + // if (type_3mf && !is_project_file) + model.center_instances_around_point(this->bed.build_volume().bed_center()); + // BBS: add auxiliary files logic + // BBS: backup & restore + if (load_aux) { + q->model().load_from(model); + load_auxiliary_files(); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", before load_model_objects, count %1%")%model.objects.size(); + auto loaded_idxs = load_model_objects(model.objects, is_project_file); + obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished load_model_objects"); + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + dlg_cont = dlg.Update(progress_percent, msg); + if (!dlg_cont) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + } else { + // This must be an .stl or .obj file, which may contain a maximum of one volume. + for (const ModelObject *model_object : model.objects) { + new_model->add_object(*model_object); + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":" << __LINE__ << boost::format(", added object %1%")%model_object->name; + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + dlg_cont = dlg.Update(progress_percent, msg); + if (!dlg_cont) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + } + } + } + + if (new_model != nullptr && new_model->objects.size() > 1) { + //BBS do not popup this dialog + + bool new_model_auto_drop = true; + int single_object_answer = false; + if (ask_multi) { + RichMessageDialog dlg(q, _L("Load these files as a single object with multiple parts?\n"), + _L("Object with multiple parts was detected"), wxICON_QUESTION | wxYES_NO); + + dlg.ShowCheckBox(_L("Auto-Drop"), true); + single_object_answer = dlg.ShowModal(); + + if (dlg.IsCheckBoxChecked() == false) + new_model_auto_drop = false; + + // convert to multipart and split after load_model_objects + // to keep relative positioning if auto_drop == false + if (single_object_answer == wxID_YES || new_model_auto_drop == false) + new_model->convert_multipart_object(filaments_cnt); + } + + // TODO + // DONE always convert to multipart, split afterwards to retain relative position + // DONE if !auto_drop move all objects over the z-position 0, so that none are clipped by the bed. + // DONE retain auto_drop (and printable) state when assembling or splitting objects. + // DONE when manually split to object ask users if looks_like_multipart and none have auto_drob disabled if they want to disable auto_drop for all resulting objects. + // - add icon in object list, similar to fuzzy painting, etc. + + auto loaded_idxs = load_model_objects(new_model->objects, false, false, new_model_auto_drop); + obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); + + if (single_object_answer == wxID_NO && new_model_auto_drop == false) { + split_object(loaded_idxs[0], new_model_auto_drop); + } + } + + if (load_config) { + DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (dev) { + MachineObject *obj = dev->get_selected_machine(); + if (obj && obj->is_info_ready()) { + if (obj->GetExtderSystem()->GetTotalExtderCount() > 0) { + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + Preset &printer_preset = preset_bundle->printers.get_selected_preset(); + + double preset_nozzle_diameter = 0.4; + const ConfigOption *opt = printer_preset.config.option("nozzle_diameter"); + if (opt) preset_nozzle_diameter = static_cast(opt)->values[0]; + float machine_nozzle_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0); + + std::string machine_type = obj->printer_type; + if (obj->is_support_upgrade_kit && obj->installed_upgrade_kit) machine_type = "C12"; + + if (printer_preset.get_current_printer_type(preset_bundle) != machine_type || !is_approx((float) preset_nozzle_diameter, machine_nozzle_diameter)) { + Preset *machine_preset = get_printer_preset(obj); + if (machine_preset != nullptr) { + std::string printer_model = machine_preset->config.option("printer_model")->value; + bool sync_printer_info = false; + if (!wxGetApp().app_config->has("sync_after_load_file_show_flag")) { + wxString tips = from_u8((boost::format(_u8L("Connected printer is %s. It must match the project preset for printing.\n")) % printer_model).str()); + + tips += _L("Do you want to sync the printer information and automatically switch the preset?"); + TipsDialog dlg(wxGetApp().mainframe, _L("Tips"), tips, "sync_after_load_file_show_flag", wxYES_NO); + if (dlg.ShowModal() == wxID_YES) { sync_printer_info = true; } + } + else { + sync_printer_info = wxGetApp().app_config->get("sync_after_load_file_show_flag") == "true"; + } + if (sync_printer_info) { + Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); + printer_tab->select_preset(machine_preset->name); + if (obj->is_multi_extruders()) GUI::wxGetApp().sidebar().sync_extruder_list(); + } + } + } + } + } + } + } + + if (new_model) delete new_model; + + //BBS: translate old 3mf to correct positions + if (translate_old) { + //translate the objects + int plate_count = partplate_list.get_plate_count(); + for (int index = 1; index < plate_count; index ++) { + PartPlate* cur_plate = (PartPlate *)partplate_list.get_plate(index); + + Vec3d cur_origin = cur_plate->get_origin(); + Vec3d new_origin = partplate_list.compute_origin_using_new_size(index, current_width, current_depth); + + cur_plate->translate_all_instance(new_origin - cur_origin); + } + view3D->get_canvas3d()->remove_raycasters_for_picking(SceneRaycaster::EType::Bed); + partplate_list.reset_size(current_width, current_depth, current_height, true, true); + partplate_list.register_raycasters_for_picking(*view3D->get_canvas3d()); + } + + //BBS: add gcode loading logic in the end + q->m_exported_file = false; + q->skip_thumbnail_invalid = false; + if (load_model && load_config) { + if (model.objects.empty()) { + partplate_list.load_gcode_files(); + PartPlate * first_plate = nullptr, *cur_plate = nullptr; + int plate_cnt = partplate_list.get_plate_count(); + int index = 0, first_plate_index = 0; + q->m_valid_plates_count = 0; + for (index = 0; index < plate_cnt; index ++) + { + cur_plate = partplate_list.get_plate(index); + if (!first_plate && cur_plate->is_slice_result_valid()) { + first_plate = cur_plate; + first_plate_index = index; + } + if (cur_plate->is_slice_result_valid()) + q->m_valid_plates_count ++; + } + if (first_plate&&first_plate->is_slice_result_valid()) { + q->m_exported_file = true; + //select plate 0 as default + q->select_plate(first_plate_index); + //set to 3d tab + q->select_view_3D("Preview"); + wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + } + else { + //set to 3d tab + q->select_view_3D("3D"); + //select plate 0 as default + q->select_plate(0); + } + } + else { + //set to 3d tab + q->select_view_3D("3D"); + //select plate 0 as default + q->select_plate(0); + } + } + else { + //always set to 3D after loading files + q->select_view_3D("3D"); + wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + } + + if (load_model) { + if (!q->m_exported_file && view3D != nullptr) { + // Force a 3D scene refresh after view/plate selection to avoid losing the first load + // on platforms where the GL canvas mapping lags behind model loading. + view3D->reload_scene(true); + view3D->set_as_dirty(); + } + if (!silence) wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string()); + // XXX: Plater.pm had @loaded_files, but didn't seem to fill them with the filenames... + } + + // automatic selection of added objects + if (!obj_idxs.empty() && view3D != nullptr) { + // update printable state for new volumes on canvas3D + wxGetApp().plater()->canvas3D()->update_instance_printable_state_for_objects(obj_idxs); + + if (!load_config) { + Selection& selection = view3D->get_canvas3d()->get_selection(); + selection.clear(); + for (size_t idx : obj_idxs) { + selection.add_object((unsigned int)idx, false); + } + } + // BBS: update object list selection + this->sidebar->obj_list()->update_selections(); + + if (view3D->get_canvas3d()->get_gizmos_manager().is_enabled()) + // this is required because the selected object changed and the flatten on face an sla support gizmos need to be updated accordingly + view3D->get_canvas3d()->update_gizmos_on_off_state(); + } + + GLGizmoSimplify::add_simplify_suggestion_notification( + obj_idxs, model.objects, *notification_manager); + + //set designer_model_id + q->model().stl_design_id = designer_model_id; + q->model().stl_design_country = designer_country_code; + //if (!designer_model_id.empty() && q->model().stl_design_id.empty() && !designer_country_code.empty()) { + // q->model().stl_design_id = designer_model_id; + // q->model().stl_design_country = designer_country_code; + //} + //else { + // q->model().stl_design_id = ""; + // q->model().stl_design_country = ""; + //} + + if (tolal_model_count <= 0 && !q->m_exported_file) { + dlg.Hide(); + if (!is_user_cancel) { + MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING); + if (msg.ShowModal() == wxID_YES) {} + } + } + q->schedule_background_process(true); + return obj_idxs; +} + + #define AUTOPLACEMENT_ON_LOAD + +std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z, bool split_object, bool auto_drop) +{ + const Vec3d bed_size = Slic3r::to_3d(this->bed.build_volume().bounding_volume2d().size(), 1.0) - 2.0 * Vec3d::Ones(); + +#ifndef AUTOPLACEMENT_ON_LOAD + // bool need_arrange = false; +#endif /* AUTOPLACEMENT_ON_LOAD */ + bool scaled_down = false; + std::vector obj_idxs; + unsigned int obj_count = model.objects.size(); + +#ifdef AUTOPLACEMENT_ON_LOAD + ModelInstancePtrs new_instances; +#endif /* AUTOPLACEMENT_ON_LOAD */ + for (ModelObject *model_object : model_objects) { + auto *object = model.add_object(*model_object); + object->sort_volumes(true); + std::string object_name = object->name.empty() ? fs::path(object->input_file).filename().string() : object->name; + obj_idxs.push_back(obj_count++); + + if (model_object->instances.empty()) { +#ifdef AUTOPLACEMENT_ON_LOAD + object->center_around_origin(); + new_instances.emplace_back(object->add_instance()); +#else /* AUTOPLACEMENT_ON_LOAD */ + // if object has no defined position(s) we need to rearrange everything after loading + // need_arrange = true; + // add a default instance and center object around origin + object->center_around_origin(); // also aligns object to Z = 0 + ModelInstance* instance = object->add_instance(); + + //BBS calc transformation + Geometry::Transformation t = instance->get_transformation(); + instance->set_offset(Slic3r::to_3d(this->bed.build_volume().bed_center(), -object->origin_translation(2))); +#endif /* AUTOPLACEMENT_ON_LOAD */ + } + + //BBS: when the object is too large, let the user choose whether to scale it down + for (size_t i = 0; i < object->instances.size(); ++i) { + ModelInstance* instance = object->instances[i]; + const Vec3d size = object->instance_bounding_box(i).size(); + const Vec3d ratio = size.cwiseQuotient(bed_size); + const double max_ratio = std::max(ratio(0), ratio(1)); + if (max_ratio > 10000) { + MessageDialog dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), _L("Object too large"), + wxICON_QUESTION | wxYES); + int answer = dlg.ShowModal(); + // the size of the object is too big -> this could lead to overflow when moving to clipper coordinates, + // so scale down the mesh + object->scale_mesh_after_creation(1. / max_ratio); + object->origin_translation = Vec3d::Zero(); + object->center_around_origin(); + scaled_down = true; + break; + } + else if (max_ratio > 10) { + MessageDialog dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), _L("Object too large"), + wxICON_QUESTION | wxYES_NO); + int answer = dlg.ShowModal(); + if (answer == wxID_YES) { + instance->set_scaling_factor(instance->get_scaling_factor() / max_ratio); + scaled_down = true; + } + } + } + + if (!auto_drop) { + for (size_t i = 0; i < object->instances.size(); ++i) { + ModelInstance* instance = object->instances[i]; + instance->auto_drop = auto_drop; + } + + // if under the bed, move over the bed + double dist_to_bed = std::min(object->min_z(), double(0)); + object->translate_instances(Vec3d(0, 0, -dist_to_bed)); + } + else { + object->ensure_on_bed(allow_negative_z); + } + + if (!split_object) { + //BBS initial assemble transformation + for (ModelObject* model_object : model.objects) { + //BBS initialize assemble transformation + for (int i = 0; i < model_object->instances.size(); i++) { + if (!model_object->instances[i]->is_assemble_initialized()) { + model_object->instances[i]->set_assemble_transformation(model_object->instances[i]->get_transformation()); + } + } + } + } + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", loaded objects, begin to auto placement"); +#ifdef AUTOPLACEMENT_ON_LOAD +#if 0 + // FIXME distance should be a config value ///////////////////////////////// + auto min_obj_distance = static_cast(6/SCALING_FACTOR); + const auto *bed_shape_opt = config->opt("printable_area"); + assert(bed_shape_opt); + auto& bedpoints = bed_shape_opt->values; + Polyline bed; bed.points.reserve(bedpoints.size()); + for(auto& v : bedpoints) bed.append(Point::new_scale(v(0), v(1))); + + // BBS: get wipe tower of current plate + int cur_plate_idx = partplate_list.get_curr_plate_index(); + std::pair wti = view3D->get_canvas3d()->get_wipe_tower_info(cur_plate_idx); + + arr::find_new_position(model, new_instances, min_obj_distance, bed, wti); + + // it remains to move the wipe tower: + view3D->get_canvas3d()->arrange_wipe_tower(wti); +#else + // BBS: find an empty cell to put the copied object + for (auto& instance : new_instances) { + auto offset = instance->get_offset(); + auto start_point = this->bed.build_volume().bounding_volume2d().center(); + bool plate_empty = partplate_list.get_curr_plate()->empty(); + Vec3d displacement; + if (plate_empty) + displacement = {start_point(0), start_point(1), offset(2)}; + else { + auto empty_cell = wxGetApp().plater()->canvas3D()->get_nearest_empty_cell({start_point(0), start_point(1)}); + displacement = {empty_cell.x(), empty_cell.y(), offset(2)}; + } + instance->set_offset(displacement); + } +#endif + +#endif /* AUTOPLACEMENT_ON_LOAD */ + + //BBS: remove the auto scaled_down logic when load models + //if (scaled_down) { + // GUI::show_info(q, + // _L("Your object appears to be too large, so it was automatically scaled down to fit your print bed."), + // _L("Object too large?")); + //} + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished auto placement, before add_objects_to_list"); + notification_manager->close_notification_of_type(NotificationType::UpdatedItemsInfo); + + if (obj_idxs.size() > 1) { + std::vector obj_idxs_1 (obj_idxs.begin(), obj_idxs.end() - 1); + + wxGetApp().obj_list()->add_objects_to_list(obj_idxs_1, false); + wxGetApp().obj_list()->add_object_to_list(obj_idxs[obj_idxs.size() - 1]); + } + else + wxGetApp().obj_list()->add_objects_to_list(obj_idxs); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", after add_objects_to_list"); + update(); + // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), + // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call + for (const size_t idx : obj_idxs) + wxGetApp().obj_list()->update_info_items(idx); + + object_list_changed(); + + this->schedule_background_process(); + + return obj_idxs; +} + +// BBS +void Plater::priv::load_auxiliary_files() +{ + std::string auxiliary_path = encode_path(q->model().get_auxiliary_file_temp_path().c_str()); + //wxGetApp().mainframe->m_project->Reload(auxiliary_path); +} + +fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) +{ + // Update printbility state of each of the ModelInstances. + this->update_print_volume_state(); + + const Selection& selection = get_selection(); + int obj_idx = selection.get_object_idx(); + + fs::path output_file; + if (file_type == FT_3MF) + // for 3mf take the path from the project filename, if any + output_file = into_path(get_project_filename(".3mf")); + else if (file_type == FT_STL) { + if (obj_idx > 0 && obj_idx < this->model.objects.size() && selection.is_single_full_object()) { + output_file = this->model.objects[obj_idx]->get_export_filename(); + } + else { + output_file = into_path(get_project_name()); + } + } + //bbs name the project using the part name + if (output_file.empty()) { + if (get_project_name() != _L("Untitled")) { + output_file = into_path(get_project_name() + ".3mf"); + } + } + + if (output_file.empty()) + { + // first try to get the file name from the current selection + if ((0 <= obj_idx) && (obj_idx < (int)this->model.objects.size())) + output_file = this->model.objects[obj_idx]->get_export_filename(); + + if (output_file.empty()) + // Find the file name of the first printable object. + output_file = this->model.propose_export_file_name_and_path(); + + if (output_file.empty() && !model.objects.empty()) + // Find the file name of the first object. + output_file = this->model.objects[0]->get_export_filename(); + + if (output_file.empty()) + // Use _L("Untitled") name + output_file = into_path(_L("Untitled")); + } + return output_file; +} + +wxString Plater::priv::get_export_file(GUI::FileType file_type) +{ + wxString wildcard; + switch (file_type) { + case FT_STL: + case FT_DRC: + case FT_AMF: + case FT_3MF: + case FT_GCODE: + case FT_OBJ: + wildcard = file_wildcards(file_type); + break; + default: + wildcard = file_wildcards(FT_MODEL); + break; + } + + fs::path output_file = get_export_file_path(file_type); + + wxString dlg_title; + switch (file_type) { + case FT_STL: + { + output_file.replace_extension("stl"); + dlg_title = _L("Export STL file:"); + break; + } + case FT_DRC: + { + output_file.replace_extension("drc"); + dlg_title = _L("Export Draco file:"); + break; + } + case FT_AMF: + { + // XXX: Problem on OS X with double extension? + output_file.replace_extension("zip.amf"); + dlg_title = _L("Export AMF file:"); + break; + } + case FT_3MF: + { + output_file.replace_extension("3mf"); + dlg_title = _L("Save file as:"); + break; + } + case FT_OBJ: + { + output_file.replace_extension("obj"); + dlg_title = _L("Export OBJ file:"); + break; + } + default: break; + } + + std::string out_dir = (boost::filesystem::path(output_file).parent_path()).string(); + + wxFileDialog dlg(q, dlg_title, + is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path()), from_path(output_file.filename()), + wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxPD_APP_MODAL); + + int result = dlg.ShowModal(); + if (result == wxID_CANCEL) + return ""; + if (result != wxID_OK) + return wxEmptyString; + + wxString out_path = dlg.GetPath(); + fs::path path(into_path(out_path)); +#ifdef __WXMSW__ + if (boost::iequals(path.extension().string(), output_file.extension().string()) == false) { + out_path += output_file.extension().string(); + boost::system::error_code ec; + if (boost::filesystem::exists(into_u8(out_path), ec)) { + auto result = MessageBox(q->GetHandle(), + wxString::Format(_L("The file %s already exists\nDo you want to replace it?"), out_path), + _L("Confirm Save As"), + MB_YESNO | MB_ICONWARNING); + if (result != IDYES) + return wxEmptyString; + } + } +#endif + wxGetApp().app_config->update_last_output_dir(path.parent_path().string()); + + return out_path; +} + +const Selection& Plater::priv::get_selection() const +{ + return view3D->get_canvas3d()->get_selection(); +} + +Selection& Plater::priv::get_selection() +{ + return view3D->get_canvas3d()->get_selection(); +} + +Selection& Plater::priv::get_curr_selection() +{ + return get_current_canvas3D()->get_selection(); +} + +int Plater::priv::get_selected_object_idx() const +{ + int idx = get_selection().get_object_idx(); + return ((0 <= idx) && (idx < 1000)) ? idx : -1; +} + +int Plater::priv::get_selected_volume_idx() const +{ + auto& selection = get_selection(); + int idx = selection.get_object_idx(); + if ((0 > idx) || (idx > 1000)) + return-1; + const GLVolume* v = selection.get_first_volume(); + if (model.objects[idx]->volumes.size() > 1) + return v->volume_idx(); + return -1; +} + +void Plater::priv::selection_changed() +{ + // if the selection is not valid to allow for layer editing, we need to turn off the tool if it is running + if (!layers_height_allowed() && view3D->is_layers_editing_enabled()) { + SimpleEvent evt(EVT_GLTOOLBAR_LAYERSEDITING); + on_action_layersediting(evt); + } + + // forces a frame render to update the view (to avoid a missed update if, for example, the context menu appears) + if (get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView) { + assemble_view->render(); + } else { + view3D->render(); + } +} + +void Plater::priv::object_list_changed() +{ + const bool export_in_progress = this->background_process.is_export_scheduled(); // || ! send_gcode_file.empty()); + // XXX: is this right? + //const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside; + ObjectFilamentResults object_results; + bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state(&object_results) != ModelInstancePVS_Partly_Outside; + model_fits = model_fits && object_results.filaments.empty(); + + PartPlate* part_plate = partplate_list.get_curr_plate(); + + // BBS + //sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances()); + bool can_slice = !model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": can_slice %1%, model_fits= %2%, export_in_progress %3%, has_printable_instances %4% ")%can_slice %model_fits %export_in_progress %part_plate->has_printable_instances(); + main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice); + + wxGetApp().params_panel()->notify_object_config_changed(); +} + +void Plater::priv::select_curr_plate_all() +{ + view3D->select_curr_plate_all(); + this->sidebar->obj_list()->update_selections(); +} + +void Plater::priv::remove_curr_plate_all() +{ + SingleSnapshot ss(q); + view3D->remove_curr_plate_all(); + this->sidebar->obj_list()->update_selections(); +} + +void Plater::priv::select_all() +{ + view3D->select_all(); + this->sidebar->obj_list()->update_selections(); +} + +void Plater::priv::deselect_all() +{ + view3D->deselect_all(); +} + +void Plater::priv::exit_gizmo() +{ + view3D->exit_gizmo(); +} + +void Plater::priv::remove(size_t obj_idx) +{ + if (view3D->is_layers_editing_enabled()) + view3D->enable_layers_editing(false); + + m_worker.cancel_all(); + model.delete_object(obj_idx); + //BBS: notify partplate the instance removed + partplate_list.notify_instance_removed(obj_idx, -1); + update(); + // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. + sidebar->obj_list()->delete_object_from_list(obj_idx); + object_list_changed(); +} + + +bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immediately) +{ + // check if object isn't cut + // show warning message that "cut consistancy" will not be supported any more + ModelObject *obj = model.objects[obj_idx]; + if (obj->is_cut()) { + InfoDialog dialog(q, _L("Delete object which is a part of cut object"), + _L("You try to delete an object which is a part of a cut object.\n" + "This action will break a cut correspondence.\n" + "After that model consistency can't be guaranteed."), + false, wxYES | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); + dialog.SetButtonLabel(wxID_YES, _L("Delete")); + if (dialog.ShowModal() == wxID_CANCEL) + return false; + } + + std::string snapshot_label = "Delete Object"; + if (!obj->name.empty()) + snapshot_label += ": " + obj->name; + Plater::TakeSnapshot snapshot(q, snapshot_label); + m_worker.cancel_all(); + + if (obj->is_cut()) + sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); + + model.delete_object(obj_idx); + //BBS: notify partplate the instance removed + partplate_list.notify_instance_removed(obj_idx, -1); + + //BBS + if (refresh_immediately) { + update(); + object_list_changed(); + } + + return true; +} + +void Plater::priv::delete_all_objects_from_model() +{ + Plater::TakeSnapshot snapshot(q, "Delete All Objects"); + + if (view3D->is_layers_editing_enabled()) + view3D->enable_layers_editing(false); + + reset_gcode_toolpaths(); + gcode_result.reset(); + + view3D->get_canvas3d()->reset_sequential_print_clearance(); + + m_worker.cancel_all(); + + // Stop and reset the Print content. + background_process.reset(); + + //BBS: update partplate + partplate_list.clear(); + + model.clear_objects(); + update(); + // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. + sidebar->obj_list()->delete_all_objects_from_list(); + object_list_changed(); + + //BBS + model.calib_pa_pattern.reset(); + model.plates_custom_gcodes.clear(); +} + +void Plater::priv::reset(bool apply_presets_change) +{ + Plater::TakeSnapshot snapshot(q, "Reset Project", UndoRedo::SnapshotType::ProjectSeparator); + + clear_warnings(); + + set_project_filename(""); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: empty"; + + if (view3D->is_layers_editing_enabled()) + view3D->get_canvas3d()->force_main_toolbar_left_action(view3D->get_canvas3d()->get_main_toolbar_item_id("layersediting")); + view3D->get_canvas3d()->reset_all_gizmos(); + + reset_gcode_toolpaths(); + //BBS: update gcode to current partplate's + //GCodeProcessorResult* current_result = this->background_process.get_current_plate()->get_slice_result(); + //current_result->reset(); + //gcode_result.reset(); + + view3D->get_canvas3d()->reset_sequential_print_clearance(); + + m_worker.cancel_all(); + + //BBS: clear the partplate list's object before object cleared + partplate_list.reinit(); + partplate_list.update_slice_context_to_current_plate(background_process); + preview->update_gcode_result(partplate_list.get_current_slice_result()); + + // Stop and reset the Print content. + this->background_process.reset(); + model.clear_objects(); + assemble_view->get_canvas3d()->reset_explosion_ratio(); + update(); + + //BBS + if (wxGetApp().is_editor()) { + // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. + sidebar->obj_list()->delete_all_objects_from_list(); + object_list_changed(); + } + + project.reset(); + + wxGetApp().sidebar().printer_combox()->clear_selected_dev_id(); + //BBS: reset all project embedded presets + wxGetApp().preset_bundle->reset_project_embedded_presets(); + if (apply_presets_change) + wxGetApp().apply_keeped_preset_modifications(); + else + wxGetApp().load_current_presets(false, false); + + //BBS + model.calib_pa_pattern.reset(); + model.plates_custom_gcodes.clear(); + + // BBS + m_saved_timestamp = m_backup_timestamp = size_t(-1); + + // Save window layout + if (sidebar_layout.is_enabled) { + // Reset show state + auto& sidebar = m_aui_mgr.GetPane(this->sidebar); + if (!sidebar_layout.is_collapsed && !sidebar.IsShown()) { + sidebar.Show(); + } + auto layout = m_aui_mgr.SavePerspective(); + wxGetApp().app_config->set("window_layout", layout.utf8_string()); + } +} + +void Plater::priv::center_selection() +{ + view3D->center_selected(); +} + +void Plater::priv::drop_selection() +{ + view3D->drop_selected(); +} + +void Plater::priv::mirror(Axis axis) +{ + view3D->mirror_selection(axis); +} + +void Plater::find_new_position(const ModelInstancePtrs &instances) +{ + arrangement::ArrangePolygons movable, fixed; + arrangement::ArrangeParams arr_params = init_arrange_params(this); + + for (const ModelObject *mo : p->model.objects) + for (ModelInstance *inst : mo->instances) { + auto it = std::find(instances.begin(), instances.end(), inst); + arrangement::ArrangePolygon arrpoly; + inst->get_arrange_polygon(&arrpoly); + + if (it == instances.end()) + fixed.emplace_back(std::move(arrpoly)); + else { + arrpoly.setter = [it](const arrangement::ArrangePolygon &p) { + if (p.is_arranged() && p.bed_idx == 0) { + Vec2d t = p.translation.cast(); + (*it)->apply_arrange_result(t, p.rotation); + } + }; + movable.emplace_back(std::move(arrpoly)); + } + } + + if (auto wt = get_wipe_tower_arrangepoly(*this)) + fixed.emplace_back(*wt); + + arrangement::arrange(movable, fixed, this->build_volume().polygon(), arr_params); + + for (auto & m : movable) + m.apply(); +} + +// split selected object into multiple objects by its volumes +void Plater::priv::split_object(bool auto_drop /* = true */) +{ + int obj_idx = get_selected_object_idx(); + priv::split_object(obj_idx, auto_drop); +} + +// split provided object into multiple objects by its volumes +void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) +{ + if (obj_idx == -1) + return; + + // we clone model object because split_object() adds the split volumes + // into the same model object, thus causing duplicates when we call load_model_objects() + Model new_model = model; + ModelObject* current_model_object = new_model.objects[obj_idx]; + + wxBusyCursor wait; + ModelObjectPtrs new_objects; + current_model_object->split(&new_objects, wxGetApp().app_config->get_bool("keep_painting")); + if (new_objects.size() == 1) + // #ysFIXME use notification + Slic3r::GUI::warning_catcher(q, _L("The selected object couldn't be split.")); + else + { + // BBS no solid parts removed + // If we splited object which is contain some parts/modifiers then all non-solid parts (modifiers) were deleted + //if (current_model_object->volumes.size() > 1 && current_model_object->volumes.size() != new_objects.size()) + // notification_manager->push_notification(NotificationType::CustomNotification, + // NotificationManager::NotificationLevel::PrintInfoNotificationLevel, + // _u8L("All non-solid parts (modifiers) were deleted")); + + Plater::TakeSnapshot snapshot(q, "Split to Objects"); + + auto is_atleast_one_floating = [new_objects]() { + for (ModelObject* new_object : new_objects) { + if (new_object->get_instance_min_z(0) >= SINKING_MIN_Z_THRESHOLD) + return true; + } + return false; + }; + bool split_auto_drop = auto_drop; + if (current_model_object->instances[0]->auto_drop && is_atleast_one_floating()) { + MessageDialog dlg(q, _L("Disable Auto-Drop to preserve z positioning?\n"), + _L("Object with floating parts was detected"), wxICON_QUESTION | wxYES_NO); + + if (dlg.ShowModal() == wxID_YES) + split_auto_drop = false; + } + + remove(obj_idx); + + // load all model objects at once, otherwise the plate would be rearranged after each one + // causing original positions not to be kept + //BBS: set split_object to true to avoid re-compute assemble matrix + std::vector idxs = load_model_objects(new_objects, false, true, split_auto_drop); + + wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_objects(idxs); + + // select newly added objects + for (size_t idx : idxs) + { + get_selection().add_object((unsigned int)idx, false); + } + } +} + +void Plater::priv::split_volume() +{ + wxGetApp().obj_list()->split(); +} + +void Plater::priv::scale_selection_to_fit_print_volume() +{ +#if ENABLE_ENHANCED_PRINT_VOLUME_FIT + this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(this->bed.build_volume()); +#else + this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(*config); +#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT +} + +void Plater::priv::schedule_background_process() +{ + delayed_error_message.clear(); + // Trigger the timer event after 0.5s + this->background_process_timer.Start(500, wxTIMER_ONE_SHOT); + // Notify the Canvas3D that something has changed, so it may invalidate some of the layer editing stuff. + this->view3D->get_canvas3d()->set_config(this->config); +} + +void Plater::priv::schedule_auto_reslice_if_needed() +{ + AppConfig* cfg = wxGetApp().app_config; + if (cfg == nullptr || !cfg->get_bool("auto_slice_after_change")) + return; + + if (!is_preview_shown()) + return; + + if (model.objects.empty()) + return; + + PartPlate* plate = partplate_list.get_curr_plate(); + if (plate == nullptr || !plate->has_printable_instances()) + return; + + if (background_process.running() || m_is_slicing) { + // Remember to restart once the current slice stops and cancel it now. + auto_reslice_after_cancel = true; + background_process.stop(); + return; + } + + const int delay_seconds = auto_slice_delay_seconds(); + if (delay_seconds > 0) { + auto_reslice_pending = true; + auto_reslice_timer.Stop(); + auto_reslice_timer.Start(delay_seconds * 1000, wxTIMER_ONE_SHOT); + return; + } + + if (auto_reslice_pending) + return; + + auto_reslice_pending = true; + auto_reslice_timer.Stop(); + wxGetApp().CallAfter([this]() { this->trigger_auto_reslice_now(); }); +} + +void Plater::priv::trigger_auto_reslice_now() +{ + this->auto_reslice_pending = false; + + AppConfig* cfg = wxGetApp().app_config; + if (cfg == nullptr || !cfg->get_bool("auto_slice_after_change")) + return; + + if (!is_preview_shown()) + return; + + if (this->model.objects.empty()) + return; + + if (this->background_process.running() || this->m_is_slicing) + return; + + PartPlate* plate = this->partplate_list.get_curr_plate(); + if (plate == nullptr || !plate->has_printable_instances()) + return; + + this->q->reslice(); +} + +int Plater::priv::auto_slice_delay_seconds() const +{ + AppConfig* cfg = wxGetApp().app_config; + if (cfg == nullptr) + return 0; + + std::string delay_str = cfg->get("auto_slice_change_delay_seconds"); + if (delay_str.empty()) + return 0; + + long delay_seconds = 0; + try { + delay_seconds = std::stol(delay_str); + } catch (...) { + delay_seconds = 0; + } + + if (delay_seconds < 0) + delay_seconds = 0; + + const long max_seconds = std::numeric_limits::max() / 1000; + if (delay_seconds > max_seconds) + delay_seconds = max_seconds; + + return static_cast(delay_seconds); +} + +std::vector> Plater::priv::get_extruder_filament_info() +{ + std::vector> filament_infos; + DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) + return filament_infos; + + MachineObject *obj_ = dev->get_selected_machine(); + if (obj_ == nullptr) + return filament_infos; + + if (!obj_->is_multi_extruders()) + return filament_infos; + + filament_infos = wxGetApp().preset_bundle->get_extruder_filament_info(); + return filament_infos; +} + +void Plater::priv::update_print_volume_state() +{ + //BBS: use the plate's bounding box instead of the bed's + PartPlate* pp = partplate_list.get_curr_plate(); + BuildVolume build_volume(pp->get_shape(), this->bed.build_volume().printable_height(), this->bed.build_volume().extruder_areas(), this->bed.build_volume().extruder_heights()); + this->model.update_print_volume_state(build_volume); +} + +void Plater::priv::process_validation_warning(StringObjectException const &warning) const +{ + if (warning.string.empty()) + notification_manager->close_notification_of_type(NotificationType::ValidateWarning); + else { + std::string text = warning.string; + auto po = dynamic_cast(warning.object); + auto mo = po ? po->model_object() : dynamic_cast(warning.object); + //ORCA: Update process_validation_warning to handle ModelInstance selection and include fallback + auto mi = dynamic_cast(warning.object); + + auto action_fn = (mo || mi || !warning.opt_key.empty()) ? [id = mo ? mo->id() : (mi ? mi->id() : 0), + parent_id = mi ? mi->get_object()->id() : 0, + is_inst = (mi != nullptr), + opt = warning.opt_key](wxEvtHandler *) { + auto & objects = wxGetApp().model().objects; + + if (is_inst) { + bool selected = false; + auto iter = std::find_if(objects.begin(), objects.end(), [parent_id](auto o) { return o->id() == parent_id; }); + if (iter != objects.end()) { + ModelObject* obj = *iter; + int inst_idx = -1; + for(size_t i=0; iinstances.size(); ++i) { + if (obj->instances[i]->id() == id) { + inst_idx = i; + break; + } + } + + wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + + if (inst_idx != -1) { + auto* model = wxGetApp().obj_list()->GetModel(); + wxDataViewItem item; + wxDataViewItem objItem = model->GetObjectItem(obj); + if (objItem.IsOk()) { + int vm_obj_idx = model->GetIdByItem(objItem); + if (vm_obj_idx != -1) { + item = model->GetItemByInstanceId(vm_obj_idx, inst_idx); + } + } + if (item.IsOk()) { + wxDataViewItemArray sel_items; + sel_items.Add(item); + wxGetApp().obj_list()->select_items(sel_items); + wxGetApp().obj_list()->update_selections_on_canvas(); + selected = true; + } + } + + if (!selected) { + wxGetApp().obj_list()->select_items({ {obj, nullptr} }); + wxGetApp().obj_list()->update_selections_on_canvas(); + } + } + } else { + auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end(); + if (iter != objects.end()) { + wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().obj_list()->select_items({{*iter, nullptr}}); + wxGetApp().obj_list()->update_selections_on_canvas(); + } + } + if (!opt.empty()) { + if ((!is_inst && id.id) || (is_inst && parent_id.id)) + wxGetApp().params_panel()->switch_to_object(); + wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); + } + return false; + } : std::function(); + auto hypertext = (mo || mi || !warning.opt_key.empty()) ? _u8L("Jump to") : ""; + if (mo) hypertext += std::string(" [") + mo->name + "]"; + if (mi) hypertext += std::string(" [") + mi->get_object()->name + "]"; + if (!warning.opt_key.empty()) hypertext += std::string(" (") + warning.opt_key + ")"; + + // BBS disable support enforcer + //if (text == "_SUPPORTS_OFF") { + // text = _u8L("An object has custom support enforcers which will not be used " + // "because supports are disabled.")+"\n"; + // hypertext = _u8L("Enable supports for enforcers only"); + + // action_fn = [](wxEvtHandler*) { + // Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT); + // assert(print_tab); + // DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + // config.set_key_value("enable_support", new ConfigOptionBool(true)); + // config.set_key_value("auto_support_type", new ConfigOptionEnum(stNormalAuto)); + // print_tab->on_value_change("enable_support", config.opt_bool("enable_support")); + // print_tab->on_value_change("support_material_auto", config.opt_bool("support_material_auto")); + // return true; + // }; + //} + + notification_manager->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + text, hypertext, action_fn + ); + } +} + + +// Update background processing thread from the current config and Model. +// Returns a bitmask of UpdateBackgroundProcessReturnState. +unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) +{ + // bitmap of enum UpdateBackgroundProcessReturnState + unsigned int return_state = 0; + + // If the update_background_process() was not called by the timer, kill the timer, + // so the update_restart_background_process() will not be called again in vain. + background_process_timer.Stop(); + // Update the "out of print bed" state of ModelInstances. + update_print_volume_state(); + // Apply new config to the possibly running background task. + bool was_running = background_process.running(); + //BBS: add the switch print logic before Print::Apply + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, force_validation=%1% postpone_error_messages=%2%, switch_print=%3%, was_running=%4%")%force_validation %postpone_error_messages %switch_print %was_running; + if (switch_print) + { + //BBS: update the current print to the current plate + this->partplate_list.update_slice_context_to_current_plate(background_process); + this->preview->update_gcode_result(partplate_list.get_current_slice_result()); + } + + background_process.fff_print()->set_check_multi_filaments_compatibility(wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); + + Print::ApplyStatus invalidated; + const auto& preset_bundle = wxGetApp().preset_bundle; + if (preset_bundle->get_printer_extruder_count() > 1) { + PartPlate* cur_plate = background_process.get_current_plate(); + std::vector f_maps = cur_plate->get_real_filament_maps(preset_bundle->project_config); + invalidated = background_process.apply(this->model, preset_bundle->full_config(false, f_maps)); + background_process.fff_print()->set_extruder_filament_info(get_extruder_filament_info()); + } + else + invalidated = background_process.apply(this->model, preset_bundle->full_config(false)); + + if ((invalidated == Print::APPLY_STATUS_CHANGED) || (invalidated == Print::APPLY_STATUS_INVALIDATED)) + // BBS: add only gcode mode + q->set_only_gcode(false); + + //BBS: add slicing related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process apply result=%1%")%invalidated; + if (background_process.empty()) + view3D->get_canvas3d()->reset_sequential_print_clearance(); + + if (invalidated == Print::APPLY_STATUS_INVALIDATED) { + //BBS: update current plater's slicer result to invalid + this->background_process.get_current_plate()->update_slice_result_valid_state(false); + + //no need, should be done in background_process.apply + //this->background_process.get_current_gcode_result()->reset(); + // Reset preview canvases. If the print has been invalidated, the preview canvases will be cleared. + // Otherwise they will be just refreshed. + if (preview != nullptr) { + // If the preview is not visible, the following line just invalidates the preview, + // but the G-code paths or SLA preview are calculated first once the preview is made visible. + reset_gcode_toolpaths(); + preview->reload_print(); + } + // In FDM mode, we need to reload the 3D scene because of the wipe tower preview box. + // In SLA mode, we need to reload the 3D scene every time to show the support structures. + if (printer_technology == ptSLA || (printer_technology == ptFFF && config->opt_bool("enable_prime_tower"))) + return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE; + + notification_manager->set_slicing_progress_hidden(); + } + else { + if (preview && preview->get_reload_paint_after_background_process_apply()) { + preview->set_reload_paint_after_background_process_apply(false); + preview->reload_print(); + } + } + + if ((invalidated != Print::APPLY_STATUS_UNCHANGED || force_validation) && ! background_process.empty()) { + // The delayed error message is no more valid. + delayed_error_message.clear(); + // The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors. + + //BBS: add is_warning logic + StringObjectException warning; + //BBS: refine seq-print logic + Polygons polygons; + std::vector> height_polygons; + StringObjectException err = background_process.validate(&warning, &polygons, &height_polygons); + // update string by type + q->post_process_string_object_exception(err); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%")%err.string%warning.string; + + if (err.string.empty()) { + this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); + notification_manager->set_all_slicing_errors_gray(true); + notification_manager->close_notification_of_type(NotificationType::ValidateError); + notification_manager->bbl_close_3mf_warn_notification(); + + if (invalidated != Print::APPLY_STATUS_UNCHANGED && background_processing_enabled()) + return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; + + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); + if (printer_technology == ptFFF) { + view3D->get_canvas3d()->reset_sequential_print_clearance(); + view3D->get_canvas3d()->set_as_dirty(); + view3D->get_canvas3d()->request_extra_frame(); + } + } + else { + this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + // The print is not valid. + // Show error as notification. + notification_manager->push_validate_error_notification(err); + //also update the warnings + process_validation_warning(warning); + return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; + if (printer_technology == ptFFF) { + const Print* print = background_process.fff_print(); + //Polygons polygons; + //if (print->config().print_sequence == PrintSequence::ByObject) + // Print::sequential_print_clearance_valid(*print, &polygons); + view3D->get_canvas3d()->set_sequential_print_clearance_visible(true); + view3D->get_canvas3d()->set_sequential_print_clearance_render_fill(true); + view3D->get_canvas3d()->set_sequential_print_clearance_polygons(polygons, height_polygons); + } + } + } + else if (! this->delayed_error_message.empty()) { + // Reusing the old state. + return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; + } + + //actualizate warnings + if (invalidated != Print::APPLY_STATUS_UNCHANGED || background_process.empty()) { + if (background_process.empty()) + process_validation_warning({}); + actualize_slicing_warnings(*this->background_process.current_print()); + actualize_object_warnings(*this->background_process.current_print()); + show_warning_dialog = false; + process_completed_with_error = -1; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: was_running = %2%, running %3%, invalidated=%4%, return_state=%5%, internal_cancel=%6%") + % __LINE__ % was_running % this->background_process.running() % invalidated % return_state % this->background_process.is_internal_cancelled(); + if (was_running && ! this->background_process.running() && (return_state & UPDATE_BACKGROUND_PROCESS_RESTART) == 0) { + if (invalidated != Print::APPLY_STATUS_UNCHANGED || this->background_process.is_internal_cancelled()) + { + // The background processing was killed and it will not be restarted. + // Post the "canceled" callback message, so that it will be processed after any possible pending status bar update messages. + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Cancelled, nullptr); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%, post an EVT_PROCESS_COMPLETED to main, status %2%")%__LINE__ %evt.status(); + wxQueueEvent(q, evt.Clone()); + } + } + + if ((return_state & UPDATE_BACKGROUND_PROCESS_INVALID) != 0) + { + // Validation of the background data failed. + //BBS: add slice&&print status update logic + this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); + + process_completed_with_error = partplate_list.get_curr_plate_index(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: set to process_completed_with_error, return_state=%2%")%__LINE__%return_state; + } + else + { + // Background data is valid. + if ((return_state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 || + (return_state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0 ) + notification_manager->set_slicing_progress_hidden(); + + //BBS: add slice&&print status update logic + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: background data valid, return_state=%2%")%__LINE__%return_state; + PartPlate* cur_plate = background_process.get_current_plate(); + if (background_process.finished() && cur_plate && cur_plate->is_slice_result_valid()) + { + //ready_to_slice = false; + this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); + } + else if (!background_process.empty() && + !background_process.running()) /* Do not update buttons if background process is running + * This condition is important for SLA mode especially, + * when this function is called several times during calculations + * */ + { + if (cur_plate->can_slice()) { + //ready_to_slice = true; + this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true); + process_completed_with_error = -1; + } + else { + //ready_to_slice = false; + this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); + process_completed_with_error = partplate_list.get_curr_plate_index(); + } + } +#if 0 + //sidebar->set_btn_label(ActionButtonType::abExport, _(label_btn_export)); + //sidebar->set_btn_label(ActionButtonType::abSendGCode, _(label_btn_send)); + + //const wxString slice_string = background_process.running() && wxGetApp().get_mode() == comSimple ? + // _L("Slicing") + dots : _L("Slice now"); + //sidebar->set_btn_label(ActionButtonType::abReslice, slice_string); + + //if (background_process.finished()) + // show_action_buttons(false); + //else if (!background_process.empty() && + // !background_process.running()) /* Do not update buttons if background process is running + // * This condition is important for SLA mode especially, + // * when this function is called several times during calculations + // * */ + // show_action_buttons(true); +#endif + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: exit, return_state=%2%")%__LINE__%return_state; + return return_state; +} + +// Restart background processing thread based on a bitmask of UpdateBackgroundProcessReturnState. +bool Plater::priv::restart_background_process(unsigned int state) +{ + if (!m_worker.is_idle()) { + // Avoid a race condition + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: ui jobs running, return false")%__LINE__; + return false; + } + + if ( ! this->background_process.empty() && + (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) == 0 && + ( ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && ! this->background_process.finished()) || + (state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 || + (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 ) ) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: print is valid, try to start it now")%__LINE__; + // The print is valid and it can be started. + if (this->background_process.start()) { + if (!show_warning_dialog) + on_slicing_began(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully")%__LINE__; + return true; + } + } + else if (this->background_process.empty()) { + PartPlate* cur_plate = background_process.get_current_plate(); + if (cur_plate->is_slice_result_valid() && ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0)) { + if (this->background_process.start()) { + if (!show_warning_dialog) + on_slicing_began(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully")%__LINE__; + return true; + } + } + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: not started")%__LINE__; + return false; +} + +void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_removable_media) +{ + wxCHECK_RET(!(output_path.empty()), "export_gcode: output_path and upload_job empty"); + + BOOST_LOG_TRIVIAL(trace) << boost::format("export_gcode: output_path %1%")%output_path.string(); + if (model.objects.empty()) + return; + + if (background_process.is_export_scheduled()) { + GUI::show_error(q, _L("Another export job is running.")); + return; + } + + // bitmask of UpdateBackgroundProcessReturnState + unsigned int state = update_background_process(true); + if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) + view3D->reload_scene(false); + + if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) + return; + + show_warning_dialog = true; + if (! output_path.empty()) { + background_process.schedule_export(output_path.string(), output_path_on_removable_media); + notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() {return true; }, 1000, 0); + } else { + BOOST_LOG_TRIVIAL(info) << "output_path is empty"; + } + + // If the SLA processing of just a single object's supports is running, restart slicing for the whole object. + this->background_process.set_task(PrintBase::TaskParams()); + this->restart_background_process(priv::UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT); +} +void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_removable_media, PrintHostJob upload_job) +{ + wxCHECK_RET(!(output_path.empty() && upload_job.empty()), "export_gcode: output_path and upload_job empty"); + + if (model.objects.empty()) + return; + + if (background_process.is_export_scheduled()) { + GUI::show_error(q, _L("Another export job is running.")); + return; + } + + // bitmask of UpdateBackgroundProcessReturnState + unsigned int state = update_background_process(true); + if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) + view3D->reload_scene(false); + + if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) + return; + + show_warning_dialog = true; + if (! output_path.empty()) { + background_process.schedule_export(output_path.string(), output_path_on_removable_media); + notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() {return true; }, 1000, 0); + } else { + background_process.schedule_upload(std::move(upload_job)); + } + + // If the SLA processing of just a single object's supports is running, restart slicing for the whole object. + this->background_process.set_task(PrintBase::TaskParams()); + this->restart_background_process(priv::UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT); +} +unsigned int Plater::priv::update_restart_background_process(bool force_update_scene, bool force_update_preview) +{ + bool switch_print = true; + //BBS: judge whether can switch print or not + if ((partplate_list.get_plate_count() > 1) && !this->background_process.can_switch_print()) + { + //can not switch print currently + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate count %1%, can not switch") % partplate_list.get_plate_count(); + switch_print = false; + } + // bitmask of UpdateBackgroundProcessReturnState + unsigned int state = this->update_background_process(false, false, switch_print); + if (force_update_scene || (state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0) + view3D->reload_scene(false); + + if (force_update_preview) + this->preview->reload_print(); + this->restart_background_process(state); + return state; +} + +void Plater::priv::update_fff_scene() +{ + if (this->preview != nullptr) + this->preview->reload_print(); + // In case this was MM print, wipe tower bounding box on 3D tab might need redrawing with exact depth: + view3D->reload_scene(true); + //BBS: add assemble view related logic + assemble_view->reload_scene(true); +} + +//BBS: add print project related logic +void Plater::priv::update_fff_scene_only_shells(bool only_shells) +{ + if (this->preview != nullptr) + { + const Print* current_print = this->background_process.fff_print(); + if (current_print) + { + //this->preview->reset_shells(); + this->preview->load_shells(*current_print); + } + } + + if (!only_shells) { + view3D->reload_scene(true); + assemble_view->reload_scene(true); + } +} + +void Plater::priv::update_sla_scene() +{ + // Update the SLAPrint from the current Model, so that the reload_scene() + // pulls the correct data. + delayed_scene_refresh = false; + this->update_restart_background_process(true, true); +} + +bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const fs::path& new_path, const std::string& snapshot) +{ + const std::string path = new_path.string(); + wxBusyCursor wait; + + Model new_model; + try { + const bool is_step = boost::algorithm::iends_with(path, ".stp") || boost::algorithm::iends_with(path, ".step"); + if (is_step) { + auto config = wxGetApp().app_config; + double linear = std::max(0.003, string_to_double_decimal_point(config->get("linear_defletion"))); + double angle = std::max(0.5, string_to_double_decimal_point(config->get("angle_defletion"))); + bool split_compound = config->get_bool("is_split_compound"); + bool is_user_cancel = false; + + auto callback = [&is_user_cancel, linear, angle, split_compound](Slic3r::Step &file, double &linear_value, double &angle_value, bool &is_split) -> int { + if (wxGetApp().app_config->get_bool("enable_step_mesh_setting")) { + StepMeshDialog mesh_dlg(nullptr, file, linear, angle); + if (mesh_dlg.ShowModal() == wxID_OK) { + linear_value = mesh_dlg.get_linear_defletion(); + angle_value = mesh_dlg.get_angle_defletion(); + is_split = mesh_dlg.get_split_compound_value(); + return 1; + } + } else { + linear_value = linear; + angle_value = angle; + is_split = split_compound; + return 1; + } + is_user_cancel = true; + return -1; + }; + + new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, callback, linear, angle, split_compound); + if (is_user_cancel) return false; + } else { + new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel); + } + for (ModelObject* model_object : new_model.objects) { + model_object->center_around_origin(); + model_object->ensure_on_bed(); + } + } + catch (std::exception&) { + // error while loading + return false; + } + + if (new_model.objects.size() > 1 || new_model.objects.front()->volumes.size() > 1) { + MessageDialog dlg(q, _L("Unable to replace with more than one volume"), _L("Error during replace"), wxOK | wxOK_DEFAULT | wxICON_WARNING); + dlg.ShowModal(); + return false; + } + + wxBusyInfo info(_L("Replace from:") + " " + from_u8(path), q->get_current_canvas3D()->get_wxglcanvas()); + + if (!snapshot.empty()) + q->take_snapshot(snapshot); + + ModelObject* old_model_object = model.objects[object_idx]; + ModelVolume* old_volume = old_model_object->volumes[volume_idx]; + + bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD; + + ModelObject* new_model_object = new_model.objects.front(); + old_model_object->add_volume(*new_model_object->volumes.front()); + ModelVolume* new_volume = old_model_object->volumes.back(); + new_volume->set_new_unique_id(); + new_volume->config.apply(old_volume->config); + new_volume->set_type(old_volume->type()); + new_volume->set_material_id(old_volume->material_id()); + new_volume->set_transformation(old_volume->get_transformation()); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); + if (old_volume->source.is_converted_from_inches) + new_volume->convert_from_imperial_units(); + else if (old_volume->source.is_converted_from_meters) + new_volume->convert_from_meters(); + if (wxGetApp().app_config->get_bool("keep_painting")) { + // Proper paint remapping + auto saved_painting = old_volume->save_painting(); + if (saved_painting) { + saved_painting->mesh.transform(Geometry::translation_transform(new_volume->mesh().get_init_shift())); + new_volume->restore_painting(saved_painting); + } + } else { + // Won't work well if mesh changed, but kept for old behavior + new_volume->supported_facets.assign(old_volume->supported_facets); + new_volume->seam_facets.assign(old_volume->seam_facets); + new_volume->mmu_segmentation_facets.assign(old_volume->mmu_segmentation_facets); + new_volume->fuzzy_skin_facets.assign(old_volume->fuzzy_skin_facets); + } + std::swap(old_model_object->volumes[volume_idx], old_model_object->volumes.back()); + old_model_object->delete_volume(old_model_object->volumes.size() - 1); + if (!sinking) + old_model_object->ensure_on_bed(); + old_model_object->sort_volumes(true); + + // if object has just one volume, rename object too + if (old_model_object->volumes.size() == 1) + old_model_object->name = old_model_object->volumes.front()->name; + + // update new name in ObjectList + sidebar->obj_list()->update_name_in_list(object_idx, volume_idx); + + sla::reproject_points_and_holes(old_model_object); + + return true; +} + +void Plater::priv::replace_with_stl() +{ + if (! q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) + return; + + const Selection& selection = get_selection(); + + if (selection.is_wipe_tower() || get_selection().get_volume_idxs().size() != 1) + return; + + const GLVolume* v = selection.get_first_volume(); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); + + // collects paths of files to load + + const ModelObject* object = model.objects[object_idx]; + const ModelVolume* volume = object->volumes[volume_idx]; + + fs::path input_path; + if (!volume->source.input_file.empty() && fs::exists(volume->source.input_file)) + input_path = volume->source.input_file; + + wxString title = _L("Select a new file"); + title += ":"; + wxFileDialog dialog(q, title, "", from_u8(input_path.filename().string()), file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + fs::path out_path = dialog.GetPath().ToUTF8().data(); + if (out_path.empty()) { + MessageDialog dlg(q, _L("File for the replace wasn't selected"), _L("Error during replace"), wxOK | wxOK_DEFAULT | wxICON_WARNING); + dlg.ShowModal(); + return; + } + + if (!replace_volume_with_stl(object_idx, volume_idx, out_path, "Replace with 3D file")) + return; + + // update 3D scene + update(); + + // new GLVolumes have been created at this point, so update their printable state + for (size_t i = 0; i < model.objects.size(); ++i) { + view3D->get_canvas3d()->update_instance_printable_state_for_object(i); + } +} + +void Plater::priv::replace_all_with_stl() +{ + if (! q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) + return; + + const Selection& selection = get_selection(); + + if (selection.is_wipe_tower()) + return; + + fs::path input_path; + Selection::IndicesList volume_idxs = selection.get_volume_idxs(); + + // when plates are selected instead of volumes + // then selection is inaccurate, we need to + // find volumes contained in selected plates + + if (selection.is_empty() || volume_idxs.empty()) { + std::vector selected_plate_idxs; + + wxDataViewItemArray sels; + wxGetApp().obj_list()->GetSelections(sels); + for (const wxDataViewItem& item : sels) { + Slic3r::GUI::ItemType item_type = wxGetApp().obj_list()->GetModel()->GetItemType(item); + if (item_type & itPlate) { + if (item.IsOk()) { + ObjectDataViewModelNode *node = static_cast(item.GetID()); + selected_plate_idxs.push_back(node->GetPlateIdx()); + } + } + } + PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list(); + for (int obj_idx = 0; obj_idx < selection.get_model()->objects.size(); obj_idx++) { + for (int plate_idx : selected_plate_idxs) { + PartPlate* plate = plate_list.get_plate(plate_idx); + if (plate && plate->contain_instance_totally(obj_idx, 0)) { + std::vector indices = selection.get_volume_idxs_from_object(obj_idx); + volume_idxs.insert(indices.begin(), indices.end()); + } + } + } + } + + // find path for initializing the file selection dialog + + for (unsigned int idx : volume_idxs) { + const GLVolume* v = selection.get_volume(idx); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); + + const ModelObject* object = model.objects[object_idx]; + const ModelVolume* volume = object->volumes[volume_idx]; + + if (!volume->source.input_file.empty() && fs::exists(volume->source.input_file)) { + input_path = volume->source.input_file; + break; + } + } + + wxString title = _L("Select folder to replace from"); + title += ":"; + wxDirDialog dialog(q, title, from_u8(input_path.parent_path().string()), wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + fs::path out_path = dialog.GetPath().ToUTF8().data(); + if (out_path.empty()) { + MessageDialog dlg(q, _L("Directory for the replace wasn't selected"), _L("Error during replace"), wxOK | wxOK_DEFAULT | wxICON_WARNING); + dlg.ShowModal(); + return; + } + + std::string status = _L("Replaced with 3D files from directory:\n").ToStdString() + out_path.string() + "\n\n"; + + for (unsigned int idx : volume_idxs) { + const GLVolume* v = selection.get_volume(idx); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); + + const ModelObject* object = model.objects[object_idx]; + const ModelVolume* volume = object->volumes[volume_idx]; + + if (volume->source.input_file.empty()) + continue; + + input_path = volume->source.input_file; + + fs::path new_path = out_path / input_path.filename(); + + std::string volume_name = volume->name; + + if (new_path == input_path) { + status += boost::str(boost::format(_L("✖ Skipped %1%: same file.\n").ToStdString()) % volume_name); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " skipping replace volume : same filename " << new_path; + continue; + } + + if (!fs::exists(new_path)) { + status += boost::str(boost::format(_L("✖ Skipped %1%: file does not exist.\n").ToStdString()) % volume_name); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cannot replace volume : filen does not exist " << new_path; + continue; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " replacing volume : " << input_path << " with " << new_path; + + if (!replace_volume_with_stl(object_idx, volume_idx, new_path, "Replace with 3D file")) { + status += boost::str(boost::format(_L("✖ Skipped %1%: failed to replace.\n").ToStdString()) % volume_name); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cannot replace volume : failed to replace with " << new_path; + continue; + } + + status += boost::str(boost::format(_L("✔ Replaced %1%.\n").ToStdString()) % volume_name); + } + + // update 3D scene + update(); + + // new GLVolumes have been created at this point, so update their printable state + for (size_t i = 0; i < model.objects.size(); ++i) { + view3D->get_canvas3d()->update_instance_printable_state_for_object(i); + } + + MessageDialog dlg(q, status, _L("Replaced volumes"), wxOK | wxOK_DEFAULT | wxICON_INFORMATION); + dlg.ShowModal(); +} + +#if ENABLE_RELOAD_FROM_DISK_REWORK +static std::vector> reloadable_volumes(const Model &model, const Selection &selection) +{ + std::vector> ret; + const std::set & selected_volumes_idxs = selection.get_volume_idxs(); + for (unsigned int idx : selected_volumes_idxs) { + const GLVolume &v = *selection.get_volume(idx); + const int o_idx = v.object_idx(); + if (0 <= o_idx && o_idx < int(model.objects.size())) { + const ModelObject *obj = model.objects[o_idx]; + const int v_idx = v.volume_idx(); + if (0 <= v_idx && v_idx < int(obj->volumes.size())) { + const ModelVolume *vol = obj->volumes[v_idx]; + if (!vol->source.is_from_builtin_objects && !vol->source.input_file.empty() && !fs::path(vol->source.input_file).extension().string().empty()) + ret.push_back({o_idx, v_idx}); + } + } + } + return ret; +} +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + +void Plater::priv::reload_from_disk() +{ +#if ENABLE_RELOAD_FROM_DISK_REWORK + // collect selected reloadable ModelVolumes + std::vector> selected_volumes = reloadable_volumes(model, get_selection()); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " entry, and reloadable volumes number is: " << selected_volumes.size(); + // nothing to reload, return + if (selected_volumes.empty()) + return; + + std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { + return (v1.first < v2.first) || (v1.first == v2.first && v1.second < v2.second); + }); + selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { + return (v1.first == v2.first) && (v1.second == v2.second); + }), selected_volumes.end()); +#else + Plater::TakeSnapshot snapshot(q, "Reload from disk"); + + const Selection& selection = get_selection(); + + if (selection.is_wipe_tower()) + return; + + // struct to hold selected ModelVolumes by their indices + struct SelectedVolume + { + int object_idx; + int volume_idx; + + // operators needed by std::algorithms + bool operator < (const SelectedVolume& other) const { return object_idx < other.object_idx || (object_idx == other.object_idx && volume_idx < other.volume_idx); } + bool operator == (const SelectedVolume& other) const { return object_idx == other.object_idx && volume_idx == other.volume_idx; } + }; + std::vector selected_volumes; + + // collects selected ModelVolumes + const std::set& selected_volumes_idxs = selection.get_volume_idxs(); + for (unsigned int idx : selected_volumes_idxs) { + const GLVolume* v = selection.get_volume(idx); + int v_idx = v->volume_idx(); + if (v_idx >= 0) { + int o_idx = v->object_idx(); + if (0 <= o_idx && o_idx < (int)model.objects.size()) + selected_volumes.push_back({ o_idx, v_idx }); + } + } + std::sort(selected_volumes.begin(), selected_volumes.end()); + selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end()), selected_volumes.end()); +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + + // collects paths of files to load + std::vector input_paths; + std::vector missing_input_paths; +#if ENABLE_RELOAD_FROM_DISK_REWORK + std::vector> replace_paths; + for (auto [obj_idx, vol_idx] : selected_volumes) { + const ModelObject *object = model.objects[obj_idx]; + const ModelVolume *volume = object->volumes[vol_idx]; + if (fs::exists(volume->source.input_file)) + input_paths.push_back(volume->source.input_file); + else { + // searches the source in the same folder containing the object + bool found = false; + if (!object->input_file.empty()) { + fs::path object_path = fs::path(object->input_file).remove_filename(); + if (!object_path.empty()) { + object_path /= fs::path(volume->source.input_file).filename(); + if (fs::exists(object_path)) { + input_paths.push_back(object_path); + found = true; + } + } + } + if (!found) + missing_input_paths.push_back(volume->source.input_file); + } + } +#else + std::vector replace_paths; + for (const SelectedVolume& v : selected_volumes) { + const ModelObject* object = model.objects[v.object_idx]; + const ModelVolume* volume = object->volumes[v.volume_idx]; + + if (!volume->source.input_file.empty()) { + if (fs::exists(volume->source.input_file)) + input_paths.push_back(volume->source.input_file); + else { + // searches the source in the same folder containing the object + bool found = false; + if (!object->input_file.empty()) { + fs::path object_path = fs::path(object->input_file).remove_filename(); + if (!object_path.empty()) { + object_path /= fs::path(volume->source.input_file).filename(); + const std::string source_input_file = object_path.string(); + if (fs::exists(source_input_file)) { + input_paths.push_back(source_input_file); + found = true; + } + } + } + if (!found) + missing_input_paths.push_back(volume->source.input_file); + } + } + else if (!object->input_file.empty() && volume->is_model_part() && !volume->name.empty() && !volume->source.is_from_builtin_objects) + missing_input_paths.push_back(volume->name); + } +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + + std::sort(missing_input_paths.begin(), missing_input_paths.end()); + missing_input_paths.erase(std::unique(missing_input_paths.begin(), missing_input_paths.end()), missing_input_paths.end()); + + while (!missing_input_paths.empty()) { + // ask user to select the missing file + fs::path search = missing_input_paths.back(); + wxString title = _L("Please select a file"); +#if defined(__APPLE__) + title += " (" + from_u8(search.filename().string()) + ")"; +#endif // __APPLE__ + title += ":"; + wxFileDialog dialog(q, title, "", from_u8(search.filename().string()), file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + std::string sel_filename_path = dialog.GetPath().ToUTF8().data(); + std::string sel_filename = fs::path(sel_filename_path).filename().string(); + if (boost::algorithm::iequals(search.filename().string(), sel_filename)) { + input_paths.push_back(sel_filename_path); + missing_input_paths.pop_back(); + + fs::path sel_path = fs::path(sel_filename_path).remove_filename().string(); + + std::vector::iterator it = missing_input_paths.begin(); + while (it != missing_input_paths.end()) { + // try to use the path of the selected file with all remaining missing files + fs::path repathed_filename = sel_path; + repathed_filename /= it->filename(); + if (fs::exists(repathed_filename)) { + input_paths.push_back(repathed_filename.string()); + it = missing_input_paths.erase(it); + } + else + ++it; + } + } + else { + wxString message = _L("Do you want to replace it") + " ?"; + MessageDialog dlg(q, message, _L("Message"), wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION); + if (dlg.ShowModal() == wxID_YES) +#if ENABLE_RELOAD_FROM_DISK_REWORK + replace_paths.emplace_back(search, sel_filename_path); +#else + replace_paths.emplace_back(sel_filename_path); +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + missing_input_paths.pop_back(); + } + } + + std::sort(input_paths.begin(), input_paths.end()); + input_paths.erase(std::unique(input_paths.begin(), input_paths.end()), input_paths.end()); + + std::sort(replace_paths.begin(), replace_paths.end()); + replace_paths.erase(std::unique(replace_paths.begin(), replace_paths.end()), replace_paths.end()); + +#if ENABLE_RELOAD_FROM_DISK_REWORK + Plater::TakeSnapshot snapshot(q, "Reload from disk"); +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + + std::vector fail_list; + + // load one file at a time + for (size_t i = 0; i < input_paths.size(); ++i) { + const auto& path = input_paths[i].string(); + auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { + if (!boost::iends_with(path, ".obj")) { return; } + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); + if (color_dlg.ShowModal() != wxID_OK) { + in_out.filament_ids.clear(); + } + }; + wxBusyCursor wait; + wxBusyInfo info(_L("Reload from:") + " " + from_u8(path), q->get_current_canvas3D()->get_wxglcanvas()); + + Model new_model; + try + { + //BBS: add plate data related logic + PlateDataPtrs plate_data; + //BBS: project embedded settings + std::vector project_presets; + + // BBS: backup + if (boost::iends_with(path, ".stp") || + boost::iends_with(path, ".step")) { + double linear = string_to_double_decimal_point(wxGetApp().app_config->get("linear_defletion")); + double angle = string_to_double_decimal_point(wxGetApp().app_config->get("angle_defletion")); + bool is_split = wxGetApp().app_config->get_bool("is_split_compound"); + new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, nullptr, linear, angle, is_split); + }else { + new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, &plate_data, &project_presets, nullptr, nullptr, nullptr, nullptr, nullptr, 0, obj_color_fun); + } + + + for (ModelObject* model_object : new_model.objects) + { + model_object->center_around_origin(); + model_object->ensure_on_bed(); + } + + if (plate_data.size() > 0) + { + //partplate_list.load_from_3mf_structure(plate_data); + partplate_list.update_slice_context_to_current_plate(background_process); + this->preview->update_gcode_result(partplate_list.get_current_slice_result()); + release_PlateData_list(plate_data); + sidebar->obj_list()->reload_all_plates(); + } + } + catch (std::exception&) + { + // error while loading + return; + } + +#if ENABLE_RELOAD_FROM_DISK_REWORK + for (auto [obj_idx, vol_idx] : selected_volumes) { + ModelObject *old_model_object = model.objects[obj_idx]; + ModelVolume *old_volume = old_model_object->volumes[vol_idx]; + + bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD; + + bool has_source = !old_volume->source.input_file.empty() && + boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); + bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); + if (has_source || has_name) { + int new_volume_idx = -1; + int new_object_idx = -1; + bool match_found = false; + // take idxs from the matching volume + if (has_source && old_volume->source.object_idx < int(new_model.objects.size())) { + const ModelObject *obj = new_model.objects[old_volume->source.object_idx]; + if (old_volume->source.volume_idx < int(obj->volumes.size())) { + if (obj->volumes[old_volume->source.volume_idx]->source.input_file == old_volume->source.input_file) { + new_volume_idx = old_volume->source.volume_idx; + new_object_idx = old_volume->source.object_idx; + match_found = true; + } + } + } + + if (!match_found && has_name) { + // take idxs from the 1st matching volume + for (size_t o = 0; o < new_model.objects.size(); ++o) { + ModelObject *obj = new_model.objects[o]; + bool found = false; + for (size_t v = 0; v < obj->volumes.size(); ++v) { + if (obj->volumes[v]->name == old_volume->name) { + new_volume_idx = (int) v; + new_object_idx = (int) o; + found = true; + break; + } + } + if (found) break; + // BBS: step model,object loaded as a volume. GUI_ObfectList.cpp load_modifier() + if (obj->name == old_volume->name) { + new_object_idx = (int) o; + break; + } + } + } + + if (new_object_idx < 0 || int(new_model.objects.size()) <= new_object_idx) { + fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); + continue; + } + ModelObject *new_model_object = new_model.objects[new_object_idx]; + if (int(new_model_object->volumes.size()) <= new_volume_idx) { + fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); + continue; + } + + ModelVolume *new_volume = nullptr; + // BBS: step model + if (new_volume_idx < 0 && new_object_idx >= 0) { + TriangleMesh mesh = new_model_object->mesh(); + new_volume = old_model_object->add_volume(std::move(mesh)); + new_volume->name = new_model_object->name; + new_volume->source.input_file = new_model_object->input_file; + }else { + new_volume = old_model_object->add_volume(*new_model_object->volumes[new_volume_idx]); + // new_volume = old_model_object->volumes.back(); + } + + new_volume->set_new_unique_id(); + new_volume->config.apply(old_volume->config); + new_volume->set_type(old_volume->type()); + new_volume->set_material_id(old_volume->material_id()); + + new_volume->source.mesh_offset = old_volume->source.mesh_offset; + new_volume->set_transformation(old_volume->get_transformation()); + + new_volume->source.object_idx = old_volume->source.object_idx; + new_volume->source.volume_idx = old_volume->source.volume_idx; + assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); + if (old_volume->source.is_converted_from_inches) + new_volume->convert_from_imperial_units(); + else if (old_volume->source.is_converted_from_meters) + new_volume->convert_from_meters(); + + // Remap paint + if (wxGetApp().app_config->get_bool("keep_painting")) { + auto saved_painting = old_volume->save_painting(); + if (saved_painting) { + saved_painting->mesh.transform(Geometry::translation_transform(new_volume->mesh().get_init_shift())); + new_volume->restore_painting(saved_painting); + } + } + + std::swap(old_model_object->volumes[vol_idx], old_model_object->volumes.back()); + old_model_object->delete_volume(old_model_object->volumes.size() - 1); + if (!sinking) old_model_object->ensure_on_bed(); + old_model_object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); + + sla::reproject_points_and_holes(old_model_object); + + // Fix warning icon in object list + wxGetApp().obj_list()->update_item_error_icon(obj_idx, vol_idx); + } + } +#else + // update the selected volumes whose source is the current file + for (const SelectedVolume& sel_v : selected_volumes) { + ModelObject* old_model_object = model.objects[sel_v.object_idx]; + ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; + + bool sinking = old_model_object->bounding_box().min.z() < SINKING_Z_THRESHOLD; + + bool has_source = !old_volume->source.input_file.empty() && boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); + bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); + if (has_source || has_name) { + int new_volume_idx = -1; + int new_object_idx = -1; +// if (has_source) { +// // take idxs from source +// new_volume_idx = old_volume->source.volume_idx; +// new_object_idx = old_volume->source.object_idx; +// } +// else { + // take idxs from the 1st matching volume + for (size_t o = 0; o < new_model.objects.size(); ++o) { + ModelObject* obj = new_model.objects[o]; + bool found = false; + for (size_t v = 0; v < obj->volumes.size(); ++v) { + if (obj->volumes[v]->name == old_volume->name) { + new_volume_idx = (int)v; + new_object_idx = (int)o; + found = true; + break; + } + } + if (found) + break; + } +// } + + if (new_object_idx < 0 || int(new_model.objects.size()) <= new_object_idx) { + fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); + continue; + } + ModelObject* new_model_object = new_model.objects[new_object_idx]; + if (new_volume_idx < 0 || int(new_model_object->volumes.size()) <= new_volume_idx) { + fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); + continue; + } + + old_model_object->add_volume(*new_model_object->volumes[new_volume_idx]); + ModelVolume* new_volume = old_model_object->volumes.back(); + new_volume->set_new_unique_id(); + new_volume->config.apply(old_volume->config); + new_volume->set_type(old_volume->type()); + new_volume->set_material_id(old_volume->material_id()); + new_volume->set_transformation(old_volume->get_transformation()); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + new_volume->source.object_idx = old_volume->source.object_idx; + new_volume->source.volume_idx = old_volume->source.volume_idx; + assert(! old_volume->source.is_converted_from_inches || ! old_volume->source.is_converted_from_meters); + if (old_volume->source.is_converted_from_inches) + new_volume->convert_from_imperial_units(); + else if (old_volume->source.is_converted_from_meters) + new_volume->convert_from_meters(); + std::swap(old_model_object->volumes[sel_v.volume_idx], old_model_object->volumes.back()); + old_model_object->delete_volume(old_model_object->volumes.size() - 1); + if (!sinking) + old_model_object->ensure_on_bed(); + old_model_object->sort_volumes(true); + + sla::reproject_points_and_holes(old_model_object); + } + } +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + } + +#if ENABLE_RELOAD_FROM_DISK_REWORK + for (auto [src, dest] : replace_paths) { + for (auto [obj_idx, vol_idx] : selected_volumes) { + if (boost::algorithm::iequals(model.objects[obj_idx]->volumes[vol_idx]->source.input_file, src.string())) + // When an error occurs, either the dest parsing error occurs, or the number of objects in the dest is greater than 1 and cannot be replaced, and cannot be replaced in this loop. + if (!replace_volume_with_stl(obj_idx, vol_idx, dest, "")) break; + } + } +#else + for (size_t i = 0; i < replace_paths.size(); ++i) { + const auto& path = replace_paths[i].string(); + for (const SelectedVolume& sel_v : selected_volumes) { + ModelObject* old_model_object = model.objects[sel_v.object_idx]; + ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; + bool has_source = !old_volume->source.input_file.empty() && boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); + if (!replace_volume_with_stl(sel_v.object_idx, sel_v.volume_idx, path, "")) { + fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); + } + } + } +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + + if (!fail_list.empty()) { + wxString message = _L("Unable to reload:") + "\n"; + for (const wxString& s : fail_list) { + message += s + "\n"; + } + MessageDialog dlg(q, message, _L("Error during reload"), wxOK | wxOK_DEFAULT | wxICON_WARNING); + dlg.ShowModal(); + } + + // update 3D scene + update(); + + // new GLVolumes have been created at this point, so update their printable state + for (size_t i = 0; i < model.objects.size(); ++i) { + view3D->get_canvas3d()->update_instance_printable_state_for_object(i); + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " finish."; +} + +void Plater::priv::reload_all_from_disk() +{ + if (model.objects.empty()) + return; + + Plater::TakeSnapshot snapshot(q, "Reload all"); + Plater::SuppressSnapshots suppress(q); + + Selection& selection = get_selection(); + Selection::IndicesList curr_idxs = selection.get_volume_idxs(); + // reload from disk uses selection + select_all(); + reload_from_disk(); + // restore previous selection + selection.clear(); + for (unsigned int idx : curr_idxs) { + selection.add(idx, false); + } +} + +//BBS: add no_slice logic +void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) +{ + if (std::find(panels.begin(), panels.end(), panel) == panels.end()) + return; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current_panel %1%, new_panel %2%")%current_panel%panel; +#ifdef __WXMAC__ + bool force_render = (current_panel != nullptr); +#endif // __WXMAC__ + + //BBS: add slice logic when switch to preview page + auto do_reslice = [this, no_slice]() { + // see: Plater::priv::object_list_changed() + // FIXME: it may be better to have a single function making this check and let it be called wherever needed + bool export_in_progress = this->background_process.is_export_scheduled(); + ObjectFilamentResults object_results; + bool model_fits = this->view3D->get_canvas3d()->check_volumes_outside_state(&object_results) != ModelInstancePVS_Partly_Outside; + model_fits = model_fits&&object_results.filaments.empty(); + //BBS: add partplate logic + PartPlate * current_plate = this->partplate_list.get_curr_plate(); + bool only_has_gcode_need_preview = false; + bool current_has_print_instances = current_plate->has_printable_instances(); + if (current_plate->is_slice_result_valid() && this->model.objects.empty() && !current_has_print_instances) + only_has_gcode_need_preview = true; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%")%no_slice%export_in_progress%model_fits%m_is_slicing; + + if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances) + { + //if already running in background, not relice here + //BBS: add more judge for slicing + if (!this->background_process.running() && !this->m_is_slicing) + { + this->m_slice_all = false; + this->q->reslice(); + } + else { + //reset current plate to the slicing plate + int plate_index = this->background_process.get_current_plate()->get_index(); + this->partplate_list.select_plate(plate_index); + } + } + else if (only_has_gcode_need_preview) + { + this->m_slice_all = false; + this->q->reslice(); + } + //BBS: process empty plate, reset previous toolpath + else + { + //if (!this->m_slice_all) + if (!current_has_print_instances) + reset_gcode_toolpaths(); + //this->q->refresh_print(); + if (!preview->get_canvas3d()->is_initialized()) + { + preview->get_canvas3d()->render(true); + } + } + //TODO: turn off this switch currently + /*auto canvas_w = float(preview->get_canvas3d()->get_canvas_size().get_width()); + auto canvas_h = float(preview->get_canvas3d()->get_canvas_size().get_height()); + Point screen_center(canvas_w/2, canvas_h/2); + auto center_point = preview->get_canvas3d()->_mouse_to_3d(screen_center); + center_point(2) = 0.f; + if (!current_plate->contains(center_point)) + this->partplate_list.select_plate_view();*/ + + // keeps current gcode preview, if any + if (this->m_slice_all) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": slicing all, just reload shells"); + this->update_fff_scene_only_shells(); + } + else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": single slice, reload print"); + if (model_fits) + this->preview->reload_print(); // TODO + else + this->update_fff_scene_only_shells(); + } + + preview->set_as_dirty(); + }; + + // Add sidebar and toolbar collapse logic + if (panel == view3D || panel == preview) { + this->enable_sidebar(!q->only_gcode_mode()); + } + if (panel == preview) { + if (q->only_gcode_mode()) { + preview->get_canvas3d()->enable_select_plate_toolbar(false); + } else if (q->using_exported_file() && (q->m_valid_plates_count <= 1)) { + preview->get_canvas3d()->enable_select_plate_toolbar(false); + } else { + preview->get_canvas3d()->enable_select_plate_toolbar(true); + } + } + else { + preview->get_canvas3d()->enable_select_plate_toolbar(false); + } + + if (current_panel == panel) + { + if (panel == view3D) { + if (view3D->is_reload_delayed()) { + // Delayed loading of the 3D scene when caller requests the already active tab. + if (printer_technology == ptSLA) + update_restart_background_process(true, false); + else + view3D->reload_scene(true); + } + + view3D->set_as_dirty(); + view3D->get_canvas3d()->reset_old_size(); + if (notification_manager != nullptr) + notification_manager->set_in_preview(false); + } + //BBS: add slice logic when switch to preview page + //BBS: add only gcode mode + if (!q->only_gcode_mode() && (current_panel == preview) && (wxGetApp().is_editor())) { + do_reslice(); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same panel, exit"); + return; + } + + //BBS: wish to reset all plates stats item selected state when back to View3D Tab + preview->get_canvas3d()->reset_select_plate_toolbar_selection(); + + wxPanel* old_panel = current_panel; +//#if BBL_HAS_FIRST_PAGE + if (!old_panel) { + // Wayland may report the first canvas as not yet shown while the frame is still mapping. + // Keep the panel switch anyway so handlers are bound and the first paint can initialize GL later. + panel->Show(); + } +//#endif + current_panel = panel; + + // to reduce flickering when changing view, first set as visible the new current panel + for (wxPanel* p : panels) { + if (p == current_panel) { +#ifdef __WXMAC__ + // On Mac we need also to force a render to avoid flickering when changing view + if (force_render) { + if (p == view3D) + dynamic_cast(p)->get_canvas3d()->render(); + else if (p == preview) + dynamic_cast(p)->get_canvas3d()->render(); + } +#endif // __WXMAC__ + p->Show(); + } + } + // then set to invisible the other + for (wxPanel* p : panels) { + if (p != current_panel) + p->Hide(); + } + + update_sidebar(true); + + if (wxGetApp().plater()) { + Camera& cam = wxGetApp().plater()->get_camera(); + if (old_panel == preview || old_panel == view3D) { + view3D->get_canvas3d()->get_camera().load_camera_view(cam); + } else if (old_panel == assemble_view) { + assemble_view->get_canvas3d()->get_camera().load_camera_view(cam); + } + if (current_panel == view3D || current_panel == preview) { + cam.load_camera_view(view3D->get_canvas3d()->get_camera()); + } + else if (current_panel == assemble_view) { + cam.load_camera_view(assemble_view->get_canvas3d()->get_camera()); + } + } + + if (current_panel == view3D) { + if (old_panel == preview) + preview->get_canvas3d()->unbind_event_handlers(); + else if (old_panel == assemble_view) { + assemble_view->get_canvas3d()->unbind_event_handlers(); + + GLCanvas3D* assemble_canvas = assemble_view->get_canvas3d(); + Selection::IndicesList select_idxs = assemble_canvas->get_selection().get_volume_idxs(); + Selection& view3d_selection = view3D->get_canvas3d()->get_selection(); + view3d_selection.clear(); + for (unsigned int idx : select_idxs) { + auto v = assemble_canvas->get_selection().get_volume(idx); + auto real_idx = view3d_selection.query_real_volume_idx_from_other_view(v->object_idx(), v->instance_idx(), v->volume_idx()); + if (real_idx >= 0) { + view3d_selection.add(real_idx, false); + } + } + } + + view3D->get_canvas3d()->bind_event_handlers(); + + if (view3D->is_reload_delayed()) { + // Delayed loading of the 3D scene. + if (printer_technology == ptSLA) { + // Update the SLAPrint from the current Model, so that the reload_scene() + // pulls the correct data. + update_restart_background_process(true, false); + } else + view3D->reload_scene(true); + } + + // sets the canvas as dirty to force a render at the 1st idle event (wxWidgets IsShownOnScreen() is buggy and cannot be used reliably) + view3D->set_as_dirty(); + // reset cached size to force a resize on next call to render() to keep imgui in synch with canvas size + view3D->get_canvas3d()->reset_old_size(); + // BBS + //view_toolbar.select_item("3D"); + if (notification_manager != nullptr) + notification_manager->set_in_preview(false); + } + else if (current_panel == preview) { + q->invalid_all_plate_thumbnails(); + if (old_panel == view3D) + view3D->get_canvas3d()->unbind_event_handlers(); + else if (old_panel == assemble_view) + assemble_view->get_canvas3d()->unbind_event_handlers(); + + preview->get_canvas3d()->bind_event_handlers(); + + GLGizmosManager& gizmos = view3D->get_canvas3d()->get_gizmos_manager(); + if (gizmos.is_running()) { + gizmos.reset_all_states(); + gizmos.update_data(); + } + + if (wxGetApp().is_editor()) { + // see: Plater::priv::object_list_changed() + // FIXME: it may be better to have a single function making this check and let it be called wherever needed + /*bool export_in_progress = this->background_process.is_export_scheduled(); + bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; + //BBS: add partplate logic + PartPlate* current_plate = partplate_list.get_curr_plate(); + if (!no_slice && !model.objects.empty() && !export_in_progress && model_fits && current_plate->has_printable_instances()) { + preview->get_canvas3d()->init_gcode_viewer(); + // BBS + //if already running in background, not relice here + if (!this->background_process.running()) + { + m_slice_all = false; + this->q->reslice(); + } + } + // keeps current gcode preview, if any + preview->reload_print(true); + + preview->set_as_dirty();*/ + if (wxGetApp().is_editor() && !q->only_gcode_mode()) + do_reslice(); + } + + // reset cached size to force a resize on next call to render() to keep imgui in synch with canvas size + preview->get_canvas3d()->reset_old_size(); + // BBS + //view_toolbar.select_item("Preview"); + if (notification_manager != nullptr) + notification_manager->set_in_preview(true); + } + else if (current_panel == assemble_view) { + if (old_panel == view3D) { + view3D->get_canvas3d()->unbind_event_handlers(); + } + else if (old_panel == preview) + preview->get_canvas3d()->unbind_event_handlers(); + + assemble_view->get_canvas3d()->bind_event_handlers(); + assemble_view->reload_scene(true); + + if (old_panel == view3D) { + GLCanvas3D* view3D_canvas = view3D->get_canvas3d(); + Selection::IndicesList select_idxs = view3D_canvas->get_selection().get_volume_idxs(); + Selection& assemble_selection = assemble_view->get_canvas3d()->get_selection(); + assemble_selection.clear(); + for (unsigned int idx : select_idxs) { + auto v = view3D_canvas->get_selection().get_volume(idx); + auto real_idx = assemble_selection.query_real_volume_idx_from_other_view(v->object_idx(), v->instance_idx(), v->volume_idx()); + if (real_idx >= 0) { + assemble_selection.add(real_idx, false); + } + } + } + + // BBS set default view and zoom + if (first_enter_assemble) { + wxGetApp().plater()->get_camera().requires_zoom_to_volumes = true; + first_enter_assemble = false; + } + + assemble_view->set_as_dirty(); + // BBS + //view_toolbar.select_item("Assemble"); + } + + current_panel->SetFocusFromKbd(); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": successfully, exit"); +} + +// BBS +void Plater::priv::on_combobox_select(wxCommandEvent &evt) +{ + PlaterPresetComboBox* preset_combo_box = dynamic_cast(evt.GetEventObject()); + if (preset_combo_box) { + this->on_select_preset(evt); + sidebar->update_printer_thumbnail(); + } + else { + this->on_select_bed_type(evt); + } +} + +void Plater::priv::on_select_bed_type(wxCommandEvent &evt) +{ + ComboBox* combo = static_cast(evt.GetEventObject()); + auto select_bed_type = sidebar->get_cur_select_bed_type(); + std::string bed_type_name = print_config_def.get("curr_bed_type")->enum_values[(int)select_bed_type - 1]; + + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + DynamicPrintConfig& proj_config = wxGetApp().preset_bundle->project_config; + const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map; + + if (keys_map) { + BedType new_bed_type = btCount; + for (auto item : *keys_map) { + if (item.first == bed_type_name) { + new_bed_type = (BedType)item.second; + break; + } + } + + if (new_bed_type != btCount) { + BedType old_bed_type = proj_config.opt_enum("curr_bed_type"); + if (old_bed_type != new_bed_type) { + proj_config.set_key_value("curr_bed_type", new ConfigOptionEnum(new_bed_type)); + + wxGetApp().plater()->update_project_dirty_from_presets(); + + // update plater with new config + q->on_config_change(wxGetApp().preset_bundle->full_config()); + + // update app_config + AppConfig* app_config = wxGetApp().app_config; + app_config->set("curr_bed_type", std::to_string(int(new_bed_type))); + app_config->set_printer_setting(wxGetApp().preset_bundle->printers.get_selected_preset_name(), + "curr_bed_type", std::to_string(int(new_bed_type))); + + //update slice status + auto plate_list = partplate_list.get_plate_list(); + for (auto plate : plate_list) { + if (plate->get_bed_type() == btDefault) { + plate->update_slice_result_valid_state(false); + } + } + + // update render + view3D->get_canvas3d()->render(); + preview->msw_rescale(); + } + } + } +} + +void Plater::priv::on_select_preset(wxCommandEvent &evt) +{ + PlaterPresetComboBox* combo = static_cast(evt.GetEventObject()); + Preset::Type preset_type = combo->get_type(); + + // Under OSX: in case of use of a same names written in different case (like "ENDER" and "Ender"), + // m_presets_choice->GetSelection() will return first item, because search in PopupListCtrl is case-insensitive. + // So, use GetSelection() from event parameter + int selection = evt.GetSelection(); + + auto marker = reinterpret_cast(combo->GetClientData(selection)); + if (PresetComboBox::LabelItemType::LABEL_ITEM_WIZARD_ADD_PRINTERS == marker) { + sidebar->create_printer_preset(); + return; + } + + auto idx = combo->get_filament_idx(); + + // BBS:Save the plate parameters before switching + PartPlateList& old_plate_list = this->partplate_list; + PartPlate* old_plate = old_plate_list.get_selected_plate(); + Vec3d old_plate_pos = old_plate->get_center_origin(); + + // BBS: Save the model in the current platelist + std::vector > plate_object; + for (size_t i = 0; i < old_plate_list.get_plate_count(); ++i) { + PartPlate* plate = old_plate_list.get_plate(i); + std::vector obj_idxs; + for (int obj_idx = 0; obj_idx < model.objects.size(); obj_idx++) { + if (plate && plate->contain_instance(obj_idx, 0)) { + obj_idxs.emplace_back(obj_idx); + } + } + plate_object.emplace_back(obj_idxs); + } + + bool flag = is_support_filament(idx); + //! Because of The MSW and GTK version of wxBitmapComboBox derived from wxComboBox, + //! but the OSX version derived from wxOwnerDrawnCombo. + //! So, to get selected string we do + //! combo->GetString(combo->GetSelection()) + //! instead of + //! combo->GetStringSelection().ToUTF8().data()); + + // Prefer the internal preset name stored per combo item to avoid alias collisions + // (e.g. user preset and bundle preset sharing the same display alias). + wxString wx_stored_name = combo->GetItemAlias(selection); + std::string preset_name; + if (!wx_stored_name.empty()) { + preset_name = Preset::remove_suffix_modified(wx_stored_name.ToUTF8().data()); + } else { + wxString wx_name = combo->GetString(selection); + preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, + Preset::remove_suffix_modified(wx_name.ToUTF8().data())); + } + + if (preset_type == Preset::TYPE_FILAMENT) { + wxGetApp().preset_bundle->set_filament_preset(idx, preset_name); + wxGetApp().plater()->update_project_dirty_from_presets(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + sidebar->update_dynamic_filament_list(); + bool flag_is_change = is_support_filament(idx); + if (flag != flag_is_change && wxGetApp().app_config->get("auto_calculate_flush") == "all") { + sidebar->auto_calc_flushing_volumes(idx); + } + auto select_flag = combo->GetFlag(selection); + combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); + q->on_filament_change(idx); + } + bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); + // TODO: ? + if (preset_type == Preset::TYPE_FILAMENT && sidebar->is_multifilament()) { + // Only update the plater UI for the 2nd and other filaments. + combo->update(); + } + else if (select_preset) { + if (preset_type == Preset::TYPE_PRINTER) { + PhysicalPrinterCollection& physical_printers = wxGetApp().preset_bundle->physical_printers; + if(combo->is_selected_physical_printer()) + preset_name = physical_printers.get_selected_printer_preset_name(); + else + physical_printers.unselect_printer(); + + if (combo->is_selected_printer_model()) { + auto preset = wxGetApp().preset_bundle->get_similar_printer_preset(preset_name, {}); + if (preset == nullptr) { + MessageDialog dlg(this->sidebar, "", ""); + dlg.ShowModal(); + } + preset->is_visible = true; // force visible + preset_name = preset->name; + } + std::string old_preset_name = wxGetApp().preset_bundle->printers.get_edited_preset().name; + + update_objects_position_when_select_preset([this, &preset_type, &preset_name]() { + wxWindowUpdateLocker noUpdates2(sidebar->filament_panel()); + wxGetApp().get_tab(preset_type)->select_preset(preset_name); + // update plater with new config + q->on_config_change(wxGetApp().preset_bundle->full_config()); + }); + + + if (old_preset_name != preset_name && wxGetApp().app_config->get("auto_calculate_flush") == "all") { + wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(-1); + } + + // sync extruder info when select multi_extruder preset + if (Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager()) { + MachineObject *obj = dev->get_selected_machine(); + if (obj && obj->is_multi_extruders()) { + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + Preset& cur_preset = preset_bundle->printers.get_edited_preset(); + if (cur_preset.get_printer_type(preset_bundle) == obj->get_show_printer_type()) { + double preset_nozzle_diameter = cur_preset.config.option("nozzle_diameter")->values[0]; + bool same_nozzle_diameter = true; + + const auto& extruders = obj->GetExtderSystem()->GetExtruders(); + for (const DevExtder &extruder : extruders) { + if (!is_approx(extruder.GetNozzleDiameter(), float(preset_nozzle_diameter))) { + same_nozzle_diameter = false; + } + } + + if (cur_preset.is_system || (!cur_preset.is_system && same_nozzle_diameter)) { + GUI::wxGetApp().sidebar().sync_extruder_list(); + } + } + } + } + } + //BBS + //wxWindowUpdateLocker noUpdates1(sidebar->print_panel()); + wxWindowUpdateLocker noUpdates2(sidebar->filament_panel()); + wxGetApp().get_tab(preset_type)->select_preset(preset_name); + } + + // update plater with new config + q->on_config_change(wxGetApp().preset_bundle->full_config()); + if (preset_type == Preset::TYPE_PRINTER) { + /* Settings list can be changed after printer preset changing, so + * update all settings items for all item had it. + * Furthermore, Layers editing is implemented only for FFF printers + * and for SLA presets they should be deleted + */ + wxGetApp().obj_list()->update_object_list_by_printer_technology(); + + // BBS:Model reset by plate center + PartPlateList& cur_plate_list = this->partplate_list; + PartPlate* cur_plate = cur_plate_list.get_curr_plate(); + Vec3d cur_plate_pos = cur_plate->get_center_origin(); + + if (old_plate_pos.x() != cur_plate_pos.x() || old_plate_pos.y() != cur_plate_pos.y()) { + for (int i = 0; i < plate_object.size(); ++i) { + view3D->select_object_from_idx(plate_object[i]); + this->sidebar->obj_list()->update_selections(); + view3D->center_selected_plate(i); + } + + view3D->deselect_all(); + } +#if 0 // do not toggle auto calc when change printer + // update flush matrix + size_t filament_size = wxGetApp().plater()->get_extruder_colors_from_plater_config().size(); + for (size_t idx = 0; idx < filament_size; ++idx) + wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(idx); +#endif + + // Show shared profiles notification for the newly selected printer + if (wxGetApp().app_config->get_bool("show_shared_profiles_notification")) + { + std::string printer_name = wxGetApp().preset_bundle->printers.get_selected_preset_base().name; + std::string encoded_name = Http::url_encode(printer_name); + + std::string cloud_url; + if (auto agent = wxGetApp().getAgent()) { + if (auto orca_agent = std::dynamic_pointer_cast(agent->get_cloud_agent())) { + cloud_url = orca_agent->get_cloud_base_url(); + } + } + if (cloud_url.empty()) + cloud_url = "https://cloud.orcaslicer.com"; + + std::string explore_url = cloud_url + "/app/bundles/explore?printers=" + encoded_name; + + wxGetApp().plater()->get_notification_manager()->push_shared_profiles_notification(explore_url); + } + } + +#ifdef __WXMSW__ + // From the Win 2004 preset combobox lose a focus after change the preset selection + // and that is why the up/down arrow doesn't work properly + // So, set the focus to the combobox explicitly + combo->SetFocus(); +#endif + if (preset_type == Preset::TYPE_FILAMENT && wxGetApp().app_config->get("auto_calculate_flush") == "all") { + wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(idx); + } + + // BBS: log modify of filament selection + Slic3r::put_other_changes(); + + // update slice state and set bedtype default for 3rd-party printer + auto plate_list = partplate_list.get_plate_list(); + for (auto plate : plate_list) { + plate->update_slice_result_valid_state(false); + } +} + +void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": event_type %1%, percent %2%, text %3%") % evt.GetEventType() % evt.status.percent % evt.status.text; + //BBS: add slice project logic + std::string title_text = _u8L("Slicing"); + evt.status.text = title_text + evt.status.text; + if (evt.status.percent >= 0) { + if (!m_worker.is_idle()) { + // Avoid a race condition + return; + } + + notification_manager->set_slicing_progress_percentage(evt.status.text, (float)evt.status.percent / 100.0f); + + // update slicing percent + PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list(); + //slicing parallel, only update if percent is greater than before + if (evt.status.percent > plate_list.get_curr_plate()->get_slicing_percent()) + plate_list.get_curr_plate()->update_slicing_percent(evt.status.percent); + } + + if (evt.status.flags & (PrintBase::SlicingStatus::RELOAD_SCENE | PrintBase::SlicingStatus::RELOAD_SLA_SUPPORT_POINTS)) { + switch (this->printer_technology) { + case ptFFF: + //BBS: add slice project logic, only display shells at the beginning + if (!m_slice_all || (m_cur_slice_plate == (partplate_list.get_plate_count() - 1))) + //this->update_fff_scene(); + this->update_fff_scene_only_shells(); + break; + case ptSLA: + // If RELOAD_SLA_SUPPORT_POINTS, then the SLA gizmo is updated (reload_scene calls update_gizmos_data) + if (view3D->is_dragging()) + delayed_scene_refresh = true; + else + this->update_sla_scene(); + break; + default: break; + } + } else if (evt.status.flags & PrintBase::SlicingStatus::RELOAD_SLA_PREVIEW) { + // Update the SLA preview. Only called if not RELOAD_SLA_SUPPORT_POINTS, as the block above will refresh the preview anyways. + this->preview->reload_print(); + } + + if (evt.status.flags & (PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS | PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS)) { + // Update notification center with warnings of object_id and its warning_step. + ObjectID object_id = evt.status.warning_object_id; + int warning_step = evt.status.warning_step; + PrintStateBase::StateWithWarnings state; + ModelObject const * model_object = nullptr; + + //BBS: add partplate related logic, use the print in background process + if (evt.status.flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) { + state = this->printer_technology == ptFFF ? + this->background_process.m_fff_print->step_state_with_warnings(static_cast(warning_step)) : + this->background_process.m_sla_print->step_state_with_warnings(static_cast(warning_step)); + } else if (this->printer_technology == ptFFF) { + const PrintObject *print_object = this->background_process.m_fff_print->get_object(object_id); + if (print_object) { + state = print_object->step_state_with_warnings(static_cast(warning_step)); + model_object = print_object->model_object(); + } + } else { + const SLAPrintObject *print_object = this->background_process.m_sla_print->get_object(object_id); + if (print_object) { + state = print_object->step_state_with_warnings(static_cast(warning_step)); + model_object = print_object->model_object(); + } + } + // Now process state.warnings. + for (auto const& warning : state.warnings) { + if (warning.current) { + NotificationManager::NotificationLevel notif_level = NotificationManager::NotificationLevel::WarningNotificationLevel; + if (evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingReplaceInitEmptyLayers || evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingEmptyGcodeLayers) { + notif_level = NotificationManager::NotificationLevel::SeriousWarningNotificationLevel; + } + notification_manager->push_slicing_warning_notification(warning.message, false, model_object, object_id, warning_step, warning.message_id, notif_level); + add_warning(warning, object_id.id); + } + } + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("exit."); +} + +void Plater::priv::on_slicing_completed(wxCommandEvent & evt) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": event_type %1%, string %2%") % evt.GetEventType() % evt.GetString(); + //BBS: add slice project logic + if (m_slice_all && (m_cur_slice_plate < (partplate_list.get_plate_count() - 1))) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("slicing all, finished plate %1%, will continue next.")%m_cur_slice_plate; + return; + } + + if (view3D->is_dragging()) // updating scene now would interfere with the gizmo dragging + delayed_scene_refresh = true; + else { + if (this->printer_technology == ptFFF) { + //BBS: only reload shells + this->update_fff_scene_only_shells(false); + //this->update_fff_scene(); + } + else + this->update_sla_scene(); + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("exit."); +} + +void Plater::priv::on_export_began(wxCommandEvent& evt) +{ + if (show_warning_dialog) + warnings_dialog(); +} + +void Plater::priv::on_export_finished(wxCommandEvent& evt) +{ +#if 0 + //BBS: also export 3mf to the same directory for debugging + std::string gcode_path_str(evt.GetString().ToUTF8().data()); + fs::path gcode_path(gcode_path_str); + + if (q) { + q->export_3mf(gcode_path.replace_extension(".3mf"), SaveStrategy::Silence); // BBS: silence + } +#endif +} + +void Plater::priv::on_slicing_began() +{ + clear_warnings(); + notification_manager->close_notification_of_type(NotificationType::SignDetected); + notification_manager->close_notification_of_type(NotificationType::ExportFinished); + bool is_first_plate = m_cur_slice_plate == 0; + bool slice_all = q->m_only_gcode ? m_slice_all_only_has_gcode : m_slice_all; + bool need_change_dailytips = !(slice_all && !is_first_plate); + notification_manager->set_slicing_progress_began(); + notification_manager->update_slicing_notif_dailytips(need_change_dailytips); +} +void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, size_t oid) +{ + for (auto& it : current_warnings) { + if (warning.message_id == it.first.message_id) { + if (warning.message_id != 0 || (warning.message_id == 0 && warning.message == it.first.message)) + { + if (warning.message_id != 0) + it.first.message = warning.message; + return; + } + } + } + current_warnings.emplace_back(std::pair(warning, oid)); +} +void Plater::priv::actualize_slicing_warnings(const PrintBase &print) +{ + std::vector ids = print.print_object_ids(); + if (ids.empty()) { + clear_warnings(); + return; + } + ids.emplace_back(print.id()); + std::sort(ids.begin(), ids.end()); + notification_manager->remove_slicing_warnings_of_released_objects(ids); + notification_manager->set_all_slicing_warnings_gray(true); +} +void Plater::priv::actualize_object_warnings(const PrintBase& print) +{ + std::vector ids; + for (const ModelObject* object : print.model().objects ) + { + ids.push_back(object->id()); + } + std::sort(ids.begin(), ids.end()); + notification_manager->remove_simplify_suggestion_of_released_objects(ids); +} +void Plater::priv::clear_warnings() +{ + notification_manager->close_slicing_errors_and_warnings(); + this->current_warnings.clear(); +} +bool Plater::priv::warnings_dialog() +{ + if (current_warnings.empty()) + return true; + std::string text = _u8L("There are warnings after slicing models:") + "\n"; + for (auto const& it : current_warnings) { + size_t next_n = it.first.message.find_first_of('\n', 0); + text += "\n"; + if (next_n != std::string::npos) + text += it.first.message.substr(0, next_n); + else + text += it.first.message; + } + //text += "\n\nDo you still wish to export?"; + MessageDialog msg_window(this->q, from_u8(text), _L("warnings"), wxOK); + const auto res = msg_window.ShowModal(); + return res == wxID_OK; + +} + +//BBS: add project slice logic +void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, m_ignore_event %1%, status %2%")%m_ignore_event %evt.status(); + //BBS:ignore cancel event for some special case + if (m_ignore_event) + { + m_ignore_event = false; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": ignore this event %1%") % evt.status(); + return; + } + //BBS: add project slice logic + bool is_finished = !m_slice_all || (m_cur_slice_plate == (partplate_list.get_plate_count() - 1)); + + //BBS: slice .gcode.3mf file related logic, assign is_finished again + bool only_has_gcode_need_preview = false; + auto plate_list = this->partplate_list.get_plate_list(); + bool has_print_instances = false; + for (auto plate : plate_list) + has_print_instances = has_print_instances || plate->has_printable_instances(); + if (this->model.objects.empty() && !has_print_instances) + only_has_gcode_need_preview = true; + if (only_has_gcode_need_preview && m_slice_all_only_has_gcode) { + is_finished = (m_cur_slice_plate == (partplate_list.get_plate_count() - 1)); + if (is_finished) + m_slice_all_only_has_gcode = false; + } + + // Stop the background task, wait until the thread goes into the "Idle" state. + // At this point of time the thread should be either finished or canceled, + // so the following call just confirms, that the produced data were consumed. + this->background_process.stop(); + notification_manager->set_slicing_progress_export_possible(); + + // Reset the "export G-code path" name, so that the automatic background processing will be enabled again. + this->background_process.reset_export(); + // This bool stops showing export finished notification even when process_completed_with_error is false + bool has_error = false; + if (evt.error()) { + auto message = evt.format_error_message(); + if (evt.critical_error()) { + if (q->m_tracking_popup_menu) { + // We don't want to pop-up a message box when tracking a pop-up menu. + // We postpone the error message instead. + q->m_tracking_popup_menu_error_message = message.first; + } else { + show_error(q, message.first, message.second.size() != 0 && message.second[0] != 0); + notification_manager->set_slicing_progress_hidden(); + } + } else { + std::vector ptrs; + for (auto oid : message.second) + { + const PrintObject *print_object = this->background_process.m_fff_print->get_object(ObjectID(oid)); + if (print_object) { ptrs.push_back(print_object->model_object()); } + } + notification_manager->push_slicing_error_notification(message.first, ptrs); + } + if (evt.invalidate_plater()) + { + // BBS +#if 0 + const wxString invalid_str = _L("Invalid data"); + for (auto btn : { ActionButtonType::abReslice, ActionButtonType::abSendGCode, ActionButtonType::abExport }) + sidebar->set_btn_label(btn, invalid_str); +#endif + process_completed_with_error = partplate_list.get_curr_plate_index();; + } + has_error = true; + is_finished = true; + } + if (evt.cancelled()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", cancel event, status: %1%") % evt.status(); + this->notification_manager->set_slicing_progress_canceled(_u8L("Slicing Canceled")); + is_finished = true; + } + + //BBS: set the current plater's slice result to valid + if (!this->background_process.empty()) + this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success()); + + //BBS: update the action button according to the current plate's status + bool ready_to_slice = !this->partplate_list.get_curr_plate()->is_slice_result_valid(); + + // BBS +#if 0 + this->sidebar->show_sliced_info_sizer(evt.success()); +#endif + + // This updates the "Slice now", "Export G-code", "Arrange" buttons status. + // Namely, it refreshes the "Out of print bed" property of all the ModelObjects, and it enables + // the "Slice now" and "Export G-code" buttons based on their "out of bed" status. + //BBS: remove this update here, will be updated in update_fff_scene later + //this->object_list_changed(); + + // refresh preview + if (view3D->is_dragging()) // updating scene now would interfere with the gizmo dragging + delayed_scene_refresh = true; + else { + if (this->printer_technology == ptFFF) { + if (is_finished) + this->update_fff_scene(); + } + else + this->update_sla_scene(); + } + + //BBS: add slice&&print status update logic + if (evt.cancelled()) { + /*if (wxGetApp().get_mode() == comSimple) + sidebar->set_btn_label(ActionButtonType::abReslice, "Slice now"); + show_action_buttons(true);*/ + ready_to_slice = true; + //this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true, true); + + //BBS + if (m_is_publishing) { + m_publish_dlg->cancel(); + } + } else { + if((ready_to_slice) || (wxGetApp().get_mode() == comSimple)) { + //this means the current plate is not the slicing plate + //show_action_buttons(ready_to_slice); + //this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, ready_to_slice, true); + } + if (exporting_status != ExportingStatus::NOT_EXPORTING && !has_error) { + notification_manager->stop_delayed_notifications_of_type(NotificationType::ExportOngoing); + notification_manager->close_notification_of_type(NotificationType::ExportOngoing); + } + // If writing to removable drive was scheduled, show notification with eject button + if (exporting_status == ExportingStatus::EXPORTING_TO_REMOVABLE && !has_error) { + //show_action_buttons(ready_to_slice); + this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, ready_to_slice, true); + notification_manager->push_exporting_finished_notification(last_output_path, last_output_dir_path, + // Don't offer the "Eject" button on ChromeOS, the Linux side has no control over it. + platform_flavor() != PlatformFlavor::LinuxOnChromium); + wxGetApp().removable_drive_manager()->set_exporting_finished(true); + }else + if (exporting_status == ExportingStatus::EXPORTING_TO_LOCAL && !has_error) + notification_manager->push_exporting_finished_notification(last_output_path, last_output_dir_path, false); + + // BBS, Generate calibration thumbnail for current plate + if (!has_error && preview) { + // generate calibration data + /* BBS generate calibration data by printer + preview->reload_print(); + ThumbnailData* calibration_data = &partplate_list.get_curr_plate()->cali_thumbnail_data; + const ThumbnailsParams calibration_params = { {}, false, true, true, true, partplate_list.get_curr_plate_index() }; + generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, calibration_params); + preview->get_canvas3d()->reset_gcode_toolpaths();*/ + + // generate bbox data + PlateBBoxData* plate_bbox_data = &partplate_list.get_curr_plate()->cali_bboxes_data; + *plate_bbox_data = generate_first_layer_bbox(); + } + } + + exporting_status = ExportingStatus::NOT_EXPORTING; + + + // BBS stop publishing if error occur + //if (m_is_publishing) { + // GCodeProcessorResult *gcode_result = background_process.get_current_gcode_result(); + // m_publish_dlg->UpdateStatus(_L("Error occurred during slicing"), -1, false); + // // if toolpath is outside + // if (!gcode_result || gcode_result->toolpath_outside) { + // m_is_publishing = false; + // } + //} + + + if (is_finished) + { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":finished, reload print soon"); + m_is_slicing = false; + this->preview->reload_print(false); + /* BBS if in publishing progress */ + if (m_is_publishing) { + if (m_publish_dlg && !m_publish_dlg->was_cancelled()) { + if (m_publish_dlg->IsShown()) { + q->publish_project(); + } else { + m_is_publishing = false; + } + } + } + q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); + } + else + { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% finished, start next slice...")%m_cur_slice_plate; + m_cur_slice_plate++; + + q->Freeze(); + q->select_plate(m_cur_slice_plate); + partplate_list.select_plate_view(); + int ret = q->start_next_slice(); + if (ret) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% can not be sliced, will stop")%m_cur_slice_plate; + m_is_slicing = false; + } + //not the last plate + update_fff_scene_only_shells(); + q->Thaw(); + if (m_is_publishing) { + if (m_publish_dlg && !m_publish_dlg->was_cancelled()) { + wxString msg = wxString::Format(_L("Slicing Plate %d"), m_cur_slice_plate + 1); + int percent = 70 * m_cur_slice_plate / partplate_list.get_plate_count(); + m_publish_dlg->UpdateStatus(msg, percent, false); + } + } + } + if (auto_reslice_after_cancel) { + auto_reslice_after_cancel = false; + schedule_auto_reslice_if_needed(); + } + + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", exit."); +} + +void Plater::priv::on_action_add(SimpleEvent&) +{ + if (q != nullptr) { + //q->add_model(); + //BBS open file in toolbar add + q->add_file(); + } +} + +//BBS: add plate from toolbar +void Plater::priv::on_action_add_plate(SimpleEvent&) +{ + if (q != nullptr) { + take_snapshot("add partplate"); + this->partplate_list.create_plate(); + int new_plate = this->partplate_list.get_plate_count() - 1; + this->partplate_list.select_plate(new_plate); + update(); + + // BBS set default view + //q->get_camera().select_view("topfront"); + q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + } +} + +//BBS: remove plate from toolbar +void Plater::priv::on_action_del_plate(SimpleEvent&) +{ + if (q != nullptr) { + q->delete_plate(); + //q->get_camera().select_view("topfront"); + //q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + } +} + +//BBS: GUI refactor: GLToolbar +void Plater::priv::on_action_open_project(SimpleEvent&) +{ + if (q != nullptr) { + q->load_project(); + } +} + +//BBS: GUI refactor: slice plate +void Plater::priv::on_action_slice_plate(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; + //BBS update extruder params and speed table before slicing + const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + + Model::setExtruderParams(config, numExtruders); + Model::setPrintSpeedTable(config, print_config); + m_slice_all = false; + q->reslice(); + q->select_view_3D("Preview"); + } +} + +//BBS: GUI refactor: slice all +void Plater::priv::on_action_slice_all(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; + //BBS update extruder params and speed table before slicing + const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + + Model::setExtruderParams(config, numExtruders); + Model::setPrintSpeedTable(config, print_config); + m_slice_all = true; + m_slice_all_only_has_gcode = true; + m_cur_slice_plate = 0; + //select plate + q->select_plate(m_cur_slice_plate); + q->reslice(); + if (!m_is_publishing) + q->select_view_3D("Preview"); + //BBS: wish to select all plates stats item + preview->get_canvas3d()->_update_select_plate_toolbar_stats_item(true); + } +} + +void Plater::priv::on_action_publish(wxCommandEvent &event) +{ + if (q != nullptr) { + if (event.GetInt() == EVT_PUBLISHING_START) { + // update by background slicing process + if (process_completed_with_error >= 0) { + wxString msg = _L("Please resolve the slicing errors and publish again."); + this->m_publish_dlg->UpdateStatus(msg, false); + return; + } + + m_is_publishing = true; + // if slicing is ready publish project, else slicing first + if (partplate_list.is_all_slice_results_valid()) { + q->publish_project(); + } else { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project in background event\n"; + SimpleEvent evt = SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL); + this->on_action_slice_all(evt); + } + } else { + m_is_publishing = false; + show_publish_dlg(false); + } + } +} + +void Plater::priv::on_action_print_plate(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print plate event\n" ; + } + + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + if (preset_bundle.use_bbl_network()) { + // BBS + if (!m_select_machine_dlg) + m_select_machine_dlg = new SelectMachineDialog(q); + m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL); + m_select_machine_dlg->prepare(partplate_list.get_curr_plate_index()); + m_select_machine_dlg->ShowModal(); + } else { + q->send_gcode_legacy(PLATE_CURRENT_IDX, nullptr, true); + } +} + +void Plater::priv::on_action_send_to_multi_machine(SimpleEvent&) +{ + if (!m_send_multi_dlg) + m_send_multi_dlg = new SendMultiMachinePage(q); + m_send_multi_dlg->prepare(partplate_list.get_curr_plate_index()); + m_send_multi_dlg->ShowModal(); +} + +void Plater::priv::on_action_print_plate_from_sdcard(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print plate event\n"; + } + + //BBS + if (!m_select_machine_dlg) m_select_machine_dlg = new SelectMachineDialog(q); + m_select_machine_dlg->set_print_type(PrintFromType::FROM_SDCARD_VIEW); + m_select_machine_dlg->prepare(0); + m_select_machine_dlg->ShowModal(); +} + +void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) +{ + // Ignore event raised by child controls + if (!(main_frame->m_tabpanel && e.GetId() == main_frame->m_tabpanel->GetId())) { + e.Skip(); + return; + } + + const int new_sel = e.GetSelection(); + sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview; + update_sidebar(); + int old_sel = e.GetOldSelection(); + if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->use_bbl_device_tab() && new_sel == MainFrame::tpMonitor) { + if (!Slic3r::NetworkAgent::is_network_module_loaded()) { + e.Veto(); + BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel; + if (q) { + wxCommandEvent* evt = new wxCommandEvent(EVT_INSTALL_PLUGIN_HINT); + wxQueueEvent(q, evt); + } + } + } else { + if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { + auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); + if (main_frame->m_printer_view && url.empty()) { + // It's missing_connection page, reload so that we can replay the gif image + main_frame->m_printer_view->reload(); + } + } + } +} + +int Plater::priv::update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path) +{ + if (!m_select_machine_dlg) m_select_machine_dlg = new SelectMachineDialog(q); + return m_select_machine_dlg->update_print_required_data(config, model, plate_data_list, file_name, file_path); +} + +void Plater::priv::on_action_send_to_printer(bool isall) +{ + if (!m_send_to_sdcard_dlg) m_send_to_sdcard_dlg = new SendToPrinterDialog(q); + if (isall) { + m_send_to_sdcard_dlg->prepare(PLATE_ALL_IDX); + } + else { + m_send_to_sdcard_dlg->prepare(partplate_list.get_curr_plate_index()); + } + + m_send_to_sdcard_dlg->ShowModal(); +} + + +void Plater::priv::on_action_select_sliced_plate(wxCommandEvent &evt) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received select sliced plate event\n" ; + } + q->select_sliced_plate(evt.GetInt()); +} + +void Plater::priv::on_action_print_all(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print all event\n" ; + } + + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + if (preset_bundle.use_bbl_network()) { + // BBS + if (!m_select_machine_dlg) + m_select_machine_dlg = new SelectMachineDialog(q); + m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL); + m_select_machine_dlg->prepare(PLATE_ALL_IDX); + m_select_machine_dlg->ShowModal(); + } else { + q->send_gcode_legacy(PLATE_ALL_IDX, nullptr, true); + } +} + +void Plater::priv::on_action_export_gcode(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n" ; + q->export_gcode(false); + } +} + +void Plater::priv::on_action_send_gcode(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n" ; + q->send_gcode_legacy(); + } +} + +void Plater::priv::on_action_export_sliced_file(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n" ; + q->export_gcode_3mf(); + } +} + +void Plater::priv::on_action_export_all_sliced_file(SimpleEvent &) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export all sliced file event\n"; + q->export_gcode_3mf(true); + } +} + +void Plater::priv::on_action_export_to_sdcard(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n"; + q->send_to_printer(); + } +} + +void Plater::priv::on_action_export_to_sdcard_all(SimpleEvent&) +{ + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n"; + q->send_to_printer(true); + } +} + +//BBS: add plate select logic +void Plater::priv::on_plate_selected(SimpleEvent&) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received plate selected event\n" ; + sidebar->obj_list()->on_plate_selected(partplate_list.get_curr_plate_index()); +} + +void Plater::priv::on_action_request_model_id(wxCommandEvent& evt) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received import model id event\n" ; + if (q != nullptr) { + q->import_model_id(evt.GetString()); + } +} + +void Plater::priv::on_action_download_project(wxCommandEvent& evt) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received download project event\n" ; + if (q != nullptr) { + q->download_project(evt.GetString()); + } +} + +//BBS: add slice button status update logic +void Plater::priv::on_slice_button_status(bool enable) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": enable = "<update_slice_print_status(MainFrame::eEventObjectUpdate, enable); +} + +void Plater::priv::on_action_split_objects(SimpleEvent&) +{ + split_object(); +} + +void Plater::priv::on_action_split_volumes(SimpleEvent&) +{ + split_volume(); +} + +void Plater::priv::on_object_select(SimpleEvent& evt) +{ + if (wxGetApp().is_closing()) + return; + + wxGetApp().obj_list()->update_selections(); + selection_changed(); +} + +//BBS: repair model through cgal +void Plater::priv::on_repair_model(wxCommandEvent &event) +{ + wxGetApp().obj_list()->fix_through_cgal(); +} + +void Plater::priv::on_filament_color_changed(wxCommandEvent &event) +{ + //q->update_all_plate_thumbnails(true); + //q->get_preview_canvas3D()->update_plate_thumbnails(); + int modify_id = event.GetInt(); + + auto& ams_multi_color_filment = wxGetApp().preset_bundle->ams_multi_color_filment; + if (modify_id >= 0 && modify_id < ams_multi_color_filment.size()) + ams_multi_color_filment[modify_id].clear(); + + if (wxGetApp().app_config->get("auto_calculate_flush") != "disabled") { + sidebar->auto_calc_flushing_volumes(modify_id); + } +} + +void Plater::priv::install_network_plugin(wxCommandEvent &event) +{ + wxGetApp().ShowDownNetPluginDlg(); + return; +} + +void Plater::priv::update_plugin_when_launch(wxCommandEvent &event) +{ + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto cache_folder = data_dir_path / "ota"; + std::string changelog_file = cache_folder.string() + "/plugins/network_plugins.json"; + + UpdatePluginDialog dlg(wxGetApp().mainframe); + dlg.update_info(changelog_file); + auto result = dlg.ShowModal(); + + auto app_config = wxGetApp().app_config; + if (!app_config) return; + + if (result == wxID_OK) { + app_config->set("update_network_plugin", "true"); + } + else if (result == wxID_NO) { + app_config->set("update_network_plugin", "false"); + } +} + +void Plater::priv::show_install_plugin_hint(wxCommandEvent &event) +{ + notification_manager->bbl_show_plugin_install_notification(into_u8(_L("The network plug-in was not detected. Network related features are unavailable."))); +} + +void Plater::priv::show_preview_only_hint(wxCommandEvent &event) +{ + notification_manager->bbl_show_preview_only_notification(into_u8(_L("Preview only mode:\nThe loaded file contains G-code only, cannot enter the Prepare page."))); +} + +void Plater::priv::on_apple_change_color_mode(wxSysColourChangedEvent& evt) { + m_is_dark = wxSystemSettings::GetAppearance().IsDark(); + if (view3D->get_canvas3d() && view3D->get_canvas3d()->is_initialized()) { + view3D->get_canvas3d()->on_change_color_mode(m_is_dark); + preview->get_canvas3d()->on_change_color_mode(m_is_dark); + assemble_view->get_canvas3d()->on_change_color_mode(m_is_dark); + } + + apply_color_mode(); +} + +void Plater::priv::on_change_color_mode(SimpleEvent& evt) { + m_is_dark = wxGetApp().app_config->get("dark_color_mode") == "1"; + sidebar->on_change_color_mode(m_is_dark); + view3D->get_canvas3d()->on_change_color_mode(m_is_dark); + preview->get_canvas3d()->on_change_color_mode(m_is_dark); + assemble_view->get_canvas3d()->on_change_color_mode(m_is_dark); + if (m_send_to_sdcard_dlg) m_send_to_sdcard_dlg->on_change_color_mode(); + + apply_color_mode(); +} + +void Plater::priv::apply_color_mode() +{ + const bool is_dark = wxGetApp().dark_mode(); + wxColour orca_color = wxColour(59, 68, 70);//wxColour(ColorRGBA::ORCA().r_uchar(), ColorRGBA::ORCA().g_uchar(), ColorRGBA::ORCA().b_uchar()); + orca_color = is_dark ? StateColor::darkModeColorFor(orca_color) : StateColor::lightModeColorFor(orca_color); + wxColour sash_color = is_dark ? wxColour(38, 46, 48) : wxColour(206, 206, 206); + m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, sash_color); + m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE); + m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_SASH_COLOUR, sash_color); + m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165)); +} + +static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) { + int res_x = 0, res_y = 0; + + while (child != until_parent && child != nullptr) { + int _x, _y; + child->GetPosition(&_x, &_y); + res_x += _x; + res_y += _y; + + child = child->GetParent(); + } + + x = res_x; + y = res_y; +} + +void Plater::priv::show_right_click_menu(Vec2d mouse_position, wxMenu *menu) +{ + // BBS: GUI refactor: move sidebar to the left + int x, y; + get_position(current_panel, wxGetApp().mainframe, x, y); + wxPoint position(static_cast(mouse_position.x() + x), static_cast(mouse_position.y() + y)); +#ifdef __linux__ + // For some reason on Linux the menu isn't displayed if position is + // specified (even though the position is sane). + position = wxDefaultPosition; +#endif + GLCanvas3D &canvas = *q->canvas3D(); + canvas.apply_retina_scale(mouse_position); + canvas.set_popup_menu_position(mouse_position); + q->PopupMenu(menu, position); + canvas.clear_popup_menu_position(); +} + +void Plater::priv::on_right_click(RBtnEvent& evt) +{ + int obj_idx = get_selected_object_idx(); + + wxMenu* menu = nullptr; + + if (obj_idx == -1) { // no one or several object are selected + if (evt.data.second) { // right button was clicked on empty space + if (!get_selection().is_empty()) // several objects are selected in 3DScene + return; + menu = menus.default_menu(); + } + else { + if (current_panel == assemble_view) { + menu = menus.assemble_multi_selection_menu(); + } + else { + menu = menus.multi_selection_menu(); + } + } + } + else { + // If in 3DScene is(are) selected volume(s), but right button was clicked on empty space + if (evt.data.second) + return; + + // Each context menu respects to the selected item in ObjectList, + // so this selection should be updated before menu agyuicreation + wxGetApp().obj_list()->update_selections(); + + if (printer_technology == ptSLA) + menu = menus.sla_object_menu(); + else { + const Selection& selection = get_selection(); + // show "Object menu" for each one or several FullInstance instead of FullObject + const bool is_some_full_instances = selection.is_single_full_instance() || + selection.is_single_full_object() || + selection.is_multiple_full_instance(); + const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); + + //BBS get assemble view menu + if (current_panel == assemble_view) { + menu = is_some_full_instances ? menus.assemble_object_menu() : + is_part ? menus.assemble_part_menu() : menus.assemble_multi_selection_menu(); + } else { + if (is_some_full_instances) + menu = printer_technology == ptSLA ? menus.sla_object_menu() : menus.object_menu(); + else if (is_part) { + const GLVolume* gl_volume = selection.get_first_volume(); + const ModelVolume *model_volume = get_model_volume(*gl_volume, selection.get_model()->objects); + menu = (model_volume != nullptr && model_volume->is_text()) ? menus.text_part_menu() : + (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : + menus.part_menu(); + } else + menu = menus.multi_selection_menu(); + } + } + } + + if (q != nullptr && menu) { + show_right_click_menu(evt.data.first, menu); + } +} + +//BBS: add part plate related logic +void Plater::priv::on_plate_right_click(RBtnPlateEvent& evt) +{ + wxMenu *menu = menus.plate_menu(); + show_right_click_menu(evt.data.first, menu); +} + +void Plater::priv::on_update_geometry(Vec3dsEvent<2>&) +{ + // TODO +} + +void Plater::priv::on_3dcanvas_mouse_dragging_started(SimpleEvent&) +{ + view3D->get_canvas3d()->reset_sequential_print_clearance(); +} + +// Update the scene from the background processing, +// if the update message was received during mouse manipulation. +void Plater::priv::on_3dcanvas_mouse_dragging_finished(SimpleEvent&) +{ + if (delayed_scene_refresh) { + delayed_scene_refresh = false; + update_sla_scene(); + } + + //partplate_list.reload_all_objects(); +} + +//BBS: add plate id for thumbnail generate param +void Plater::priv::generate_thumbnail(ThumbnailData & data, + unsigned int w, + unsigned int h, + const ThumbnailsParams &thumbnail_params, + Camera::EType camera_type, + Camera::ViewAngleType camera_view_angle_type, + bool for_picking, + bool ban_light) +{ + view3D->get_canvas3d()->render_thumbnail(data, w, h, thumbnail_params, camera_type, camera_view_angle_type, for_picking, ban_light); +} + +//BBS: add plate id for thumbnail generate param +ThumbnailsList Plater::priv::generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type) +{ + ThumbnailsList thumbnails; + for (const Vec2d& size : params.sizes) { + thumbnails.push_back(ThumbnailData()); + Point isize(size); // round to ints + generate_thumbnail(thumbnails.back(), isize.x(), isize.y(), params, camera_type); + if (!thumbnails.back().is_valid()) + thumbnails.pop_back(); + } + return thumbnails; +} + +PlateBBoxData Plater::priv::generate_first_layer_bbox() +{ + PlateBBoxData bboxdata; + std::vector& id_bboxes = bboxdata.bbox_objs; + BoundingBoxf bbox_all; + auto print = this->background_process.m_fff_print; + auto curr_plate = this->partplate_list.get_curr_plate(); + auto curr_plate_seq = curr_plate->get_real_print_seq(); + bboxdata.is_seq_print = (curr_plate_seq == PrintSequence::ByObject); + bboxdata.first_extruder = print->get_tool_ordering().first_extruder(); + bboxdata.bed_type = bed_type_to_gcode_string(print->config().curr_bed_type.value); + bboxdata.first_layer_time = partplate_list.get_curr_plate()->get_slice_result()->initial_layer_time; + // get nozzle diameter + auto opt_nozzle_diameters = print->config().option("nozzle_diameter"); + if (opt_nozzle_diameters != nullptr) + bboxdata.nozzle_diameter = float(opt_nozzle_diameters->get_at(bboxdata.first_extruder)); + //PrintObjectPtrs objects; + //if (this->printer_technology == ptFFF) { + // objects = this->background_process.m_fff_print->objects().vector(); + //} + //else { + // objects = this->background_process.m_sla_print->objects(); + //} + auto objects = print->objects(); + auto orig = this->partplate_list.get_curr_plate()->get_origin(); + Vec2d orig2d = { orig[0], orig[1] }; + + BBoxData data; + for (auto obj : objects) + { + auto bb_scaled = obj->get_first_layer_bbox(data.area, data.layer_height, data.name); + auto bb = unscaled(bb_scaled); + bb.min -= orig2d; + bb.max -= orig2d; + bbox_all.merge(bb); + data.area *= (SCALING_FACTOR * SCALING_FACTOR); // unscale area + data.id = obj->id().id; + data.bbox = { bb.min.x(),bb.min.y(),bb.max.x(),bb.max.y() }; + id_bboxes.emplace_back(data); + } + + // add wipe tower bounding box + if (print->has_wipe_tower()) { + auto wt_corners = print->first_layer_wipe_tower_corners(); + // when loading gcode.3mf, wipe tower info may not be correct + if (!wt_corners.empty()) { + BoundingBox bb_scaled = {wt_corners[0], wt_corners[2]}; + auto bb = unscaled(bb_scaled); + bb.min -= orig2d; + bb.max -= orig2d; + bbox_all.merge(bb); + data.name = "wipe_tower"; + data.id = partplate_list.get_curr_plate()->get_index() + 1000; + data.bbox = {bb.min.x(), bb.min.y(), bb.max.x(), bb.max.y()}; + id_bboxes.emplace_back(data); + } + } + + bboxdata.bbox_all = { bbox_all.min.x(),bbox_all.min.y(),bbox_all.max.x(),bbox_all.max.y() }; + return bboxdata; +} + +wxString Plater::priv::get_project_filename(const wxString& extension) const +{ + if (m_project_name.empty()) + return ""; + else { + auto full_filename = m_project_folder / std::string((m_project_name + extension).mb_str(wxConvUTF8)); + return m_project_folder.empty() ? "" : from_path(full_filename); + } +} + +wxString Plater::priv::get_export_gcode_filename(const wxString& extension, bool only_filename, bool export_all) const +{ + std::string plate_index_str; + auto plate_name = partplate_list.get_curr_plate()->get_plate_name(); + if (!plate_name.empty()) + plate_index_str = (boost::format("_%1%") % plate_name).str(); + else if (partplate_list.get_plate_count() > 1) + plate_index_str = (boost::format("_plate_%1%") % std::to_string(partplate_list.get_curr_plate_index() + 1)).str(); + + if (!m_project_folder.empty()) { + if (!only_filename) { + if (export_all) { + auto full_filename = m_project_folder / std::string((m_project_name + extension).mb_str(wxConvUTF8)); + return from_path(full_filename); + } else { + auto full_filename = m_project_folder / std::string((m_project_name + from_u8(plate_index_str) + extension).mb_str(wxConvUTF8)); + return from_path(full_filename); + } + } else { + if (export_all) + return m_project_name + extension; + else + return m_project_name + from_u8(plate_index_str) + extension; + } + } else { + if (only_filename) { + if(!model.objects.empty() && m_project_name == _L("Untitled")) + return wxString(fs::path(model.objects.front()->name).replace_extension().c_str()) + from_u8(plate_index_str) + extension; + + if (export_all) + return m_project_name + extension; + else + return m_project_name + from_u8(plate_index_str) + extension; + } + else + return ""; + } +} + +wxString Plater::priv::get_project_name() +{ + return m_project_name; +} + +//BBS +void Plater::priv::set_project_name(const wxString& project_name) +{ + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " project is:" << project_name; + m_project_name = project_name; + //update topbar title +#ifdef __APPLE__ + wxGetApp().mainframe->SetTitle(m_project_name); + if (!m_project_name.IsEmpty()) + wxGetApp().mainframe->update_title_colour_after_set_title(); +#else + wxGetApp().mainframe->SetTitle(m_project_name + " - OrcaSlicer"); + wxGetApp().mainframe->topbar()->SetTitle(m_project_name); +#endif +} + +void Plater::priv::update_title_dirty_status() +{ + if (m_project_name.empty()) + return; + + wxString title; + if (is_project_dirty()) + title = "*" + m_project_name; + else + title = m_project_name; + +#ifdef __APPLE__ + wxGetApp().mainframe->SetTitle(title); + wxGetApp().mainframe->update_title_colour_after_set_title(); +#else + wxGetApp().mainframe->SetTitle(title + " - OrcaSlicer"); + wxGetApp().mainframe->topbar()->SetTitle(title); +#endif +} + +void Plater::priv::set_project_filename(const wxString& filename) +{ + boost::filesystem::path full_path = into_path(filename); + boost::filesystem::path ext = full_path.extension(); + //if (boost::iequals(ext.string(), ".amf")) { + // // Remove the first extension. + // full_path.replace_extension(""); + // // It may be ".zip.amf". + // if (boost::iequals(full_path.extension().string(), ".zip")) + // // Remove the 2nd extension. + // full_path.replace_extension(""); + //} else { + // // Remove just one extension. + // full_path.replace_extension(""); + //} + full_path.replace_extension(""); + + m_project_folder = full_path.parent_path(); + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " project folder is:" << m_project_folder.string(); + + //BBS + wxString project_name = from_u8(full_path.filename().string()); + set_project_name(project_name); + // record filename for hint when open exported file/.gcode + if (q->m_only_gcode) + q->m_preview_only_filename = std::string((project_name + ".gcode").mb_str()); + if (q->m_exported_file) + q->m_preview_only_filename = std::string((project_name + ".3mf").mb_str()); + + wxGetApp().mainframe->update_title(); + + if (!m_project_folder.empty() && !q->m_only_gcode) + wxGetApp().mainframe->add_to_recent_projects(filename); +} + +void Plater::priv::init_notification_manager() +{ + if (!notification_manager) + return; + notification_manager->init(); + + auto cancel_callback = [this]() { + if (this->background_process.idle()) + return false; + this->background_process.stop(); + return true; + }; + notification_manager->init_slicing_progress_notification(cancel_callback); + notification_manager->set_fff(printer_technology == ptFFF); + notification_manager->init_progress_indicator(); +} + +void Plater::priv::update_objects_position_when_select_preset(const std::function &select_prest) +{ + select_prest(); + + wxGetApp().obj_list()->update_object_list_by_printer_technology(); + + // Re-clamp wipe tower positions to new bed boundaries after preset change + PartPlateList &cur_plate_list = this->partplate_list; + for (size_t plate_id = 0; plate_id < cur_plate_list.get_plate_list().size(); ++plate_id) { + cur_plate_list.set_default_wipe_tower_pos_for_plate(plate_id); + } + + update(); +} + +void Plater::orient() +{ + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Orient")); + replace_job(w, std::make_unique()); + } +} + +//BBS: add job state related functions +void Plater::set_prepare_state(int state) +{ + p->m_job_prepare_state = state; +} + +int Plater::get_prepare_state() +{ + return p->m_job_prepare_state; +} + +void Plater::get_print_job_data(PrintPrepareData* data) +{ + if (data) { + data->plate_idx = p->m_print_job_data.plate_idx; + data->_3mf_path = p->m_print_job_data._3mf_path; + data->_3mf_config_path = p->m_print_job_data._3mf_config_path; + } +} + +void Plater::set_print_job_plate_idx(int plate_idx) +{ + if (plate_idx == PLATE_CURRENT_IDX) { + p->m_print_job_data.plate_idx = get_partplate_list().get_curr_plate_index(); + } + else { + p->m_print_job_data.plate_idx = plate_idx; + } +} + + +int Plater::get_send_calibration_finished_event() +{ + return EVT_SEND_CALIBRATION_FINISHED; +} + +int Plater::get_print_finished_event() +{ + return EVT_PRINT_FINISHED; +} + +int Plater::get_send_finished_event() +{ + return EVT_SEND_FINISHED; +} + +int Plater::get_publish_finished_event() +{ + return EVT_PUBLISH_FINISHED; +} + +void Plater::priv::set_current_canvas_as_dirty() +{ + if (current_panel == view3D) + view3D->set_as_dirty(); + else if (current_panel == preview) + preview->set_as_dirty(); + else if (current_panel == assemble_view) + assemble_view->set_as_dirty(); +} + +GLCanvas3D* Plater::priv::get_current_canvas3D(bool exclude_preview) +{ + if (current_panel == view3D) + return view3D->get_canvas3d(); + else if (!exclude_preview && (current_panel == preview)) + return preview->get_canvas3d(); + else if (current_panel == assemble_view) + return assemble_view->get_canvas3d(); + else //BBS default set to view3D + return view3D->get_canvas3d(); + + //return (current_panel == view3D) ? view3D->get_canvas3d() : ((current_panel == preview) ? preview->get_canvas3d() : nullptr); +} + +void Plater::priv::unbind_canvas_event_handlers() +{ + if (view3D != nullptr) + view3D->get_canvas3d()->unbind_event_handlers(); + + if (preview != nullptr) + preview->get_canvas3d()->unbind_event_handlers(); + + if (assemble_view != nullptr) + assemble_view->get_canvas3d()->unbind_event_handlers(); +} + +void Plater::priv::reset_canvas_volumes() +{ + if (view3D != nullptr) + view3D->get_canvas3d()->reset_volumes(); + + if (preview != nullptr) + preview->get_canvas3d()->reset_volumes(); +} + +bool Plater::priv::check_ams_status_impl(bool is_slice_all) +{ + Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) + return true; + + MachineObject* obj = dev->get_selected_machine(); + if (!obj || !obj->is_multi_extruders()) + return true; + if (q->is_gcode_3mf() || q->only_gcode_mode() || q->get_partplate_list().get_curr_plate()->get_objects().empty()) { + return true; + } + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type()) { + bool is_same_as_printer = true; + auto nozzle_volumes_values = preset_bundle->project_config.option("nozzle_volume_type")->values; + assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2); + if (obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2) { + NozzleVolumeType right_nozzle_type = NozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0) - 1); + NozzleVolumeType left_nozzle_type = NozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(1) - 1); + NozzleVolumeType preset_left_type = NozzleVolumeType(nozzle_volumes_values[0]); + NozzleVolumeType preset_right_type = NozzleVolumeType(nozzle_volumes_values[1]); + is_same_as_printer = (left_nozzle_type == preset_left_type && right_nozzle_type == preset_right_type); + } + + std::vector> ams_count_info; + ams_count_info.resize(2); + int deputy_4 = 0, main_4 = 0, deputy_1 = 0, main_1 = 0; + for (auto ams : obj->GetFilaSystem()->GetAmsList()) { + // Main (first) extruder at right + if (ams.second->GetExtruderId() == 0) { + if (ams.second->GetAmsType() == DevAms::N3S) // N3S + ++main_1; + else + ++main_4; + } else if (ams.second->GetExtruderId() == 1) { + if (ams.second->GetAmsType() == DevAms::N3S) // N3S + ++deputy_1; + else + ++deputy_4; + } + } + + int left_4 = main_4; + int left_1 = main_1; + int right_4 = deputy_4; + int right_1 = deputy_1; + if (!obj->is_main_extruder_on_left()) { + left_4 = deputy_4; + left_1 = deputy_1; + right_4 = main_4; + right_1 = main_1; + } + + if (!preset_bundle->extruder_ams_counts.empty() && !preset_bundle->extruder_ams_counts.front().empty()) { + is_same_as_printer &= preset_bundle->extruder_ams_counts[0][4] == left_4 + && preset_bundle->extruder_ams_counts[0][1] == left_1 + && preset_bundle->extruder_ams_counts[1][4] == right_4 + && preset_bundle->extruder_ams_counts[1][1] == right_1; + } + + if (!is_same_as_printer) { + struct SyncInfoDialog : MessageDialog + { + SyncInfoDialog(wxWindow *parent) + : MessageDialog(parent, + _L("The nozzle type and AMS quantity information has not been synced from the connected printer.\n" + "After syncing, software can optimize printing time and filament usage when slicing.\n" + "Would you like to sync now?"), + _L("Warning"), 0) + { + add_button(wxID_YES, true, _L("Sync now")); + add_button(wxID_NO, true, _L("Later")); + } + } dlg(q); + dlg.Fit(); + if (dlg.ShowModal() == wxID_YES) { + if (GUI::wxGetApp().sidebar().sync_extruder_list()) { + if (is_slice_all) + wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); + else + wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); + wxGetApp().mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tpPreview); + } + return false; + } + } + } + + return true; +} + +bool Plater::priv::get_machine_sync_status() +{ + Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) + return false; + + MachineObject* obj = dev->get_selected_machine(); + if (!obj) + return false; + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + return preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type(); +} + +bool Plater::priv::init_collapse_toolbar() +{ + if (wxGetApp().is_gcode_viewer()) + return true; + + if (collapse_toolbar.get_items_count() > 0) + // already initialized + return true; + + BackgroundTexture::Metadata background_data; + background_data.filename = m_is_dark ? "toolbar_background_dark.png" : "toolbar_background.png"; + background_data.left = 16; + background_data.top = 16; + background_data.right = 16; + background_data.bottom = 16; + + if (!collapse_toolbar.init(background_data)) + return false; + + collapse_toolbar.set_layout_type(GLToolbar::Layout::Vertical); + collapse_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Right); + collapse_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top); + collapse_toolbar.set_border(4.0f); + collapse_toolbar.set_separator_size(4); + collapse_toolbar.set_gap_size(2); + + collapse_toolbar.del_all_item(); + + GLToolbarItem::Data item; + + item.name = "collapse_sidebar"; + // set collapse svg name + item.icon_filename = "collapse.svg"; + item.sprite_id = 0; + item.left.action_callback = []() { + wxGetApp().plater()->collapse_sidebar(!wxGetApp().plater()->is_sidebar_collapsed()); + }; + + if (!collapse_toolbar.add_item(item)) + return false; + + // Now "collapse" sidebar to current state. This is done so the tooltip + // is updated before the toolbar is first used. + wxGetApp().plater()->collapse_sidebar(wxGetApp().plater()->is_sidebar_collapsed()); + return true; +} + +void Plater::priv::update_preview_bottom_toolbar() +{ + ; +} + +#if 0 +void Plater::update_partplate() +{ + sidebar().update_partplate(p->partplate_list); +} +#endif + +void Plater::priv::reset_gcode_toolpaths() +{ + preview->get_canvas3d()->reset_gcode_toolpaths(); +} + +bool Plater::priv::can_set_instance_to_object() const +{ + const int obj_idx = get_selected_object_idx(); + return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->instances.size() > 1; +} + +bool Plater::priv::can_split(bool to_objects) const +{ + return sidebar->obj_list()->is_splittable(to_objects); +} + +bool Plater::priv::can_fillcolor() const +{ + //BBS TODO + return true; +} + +bool Plater::priv::has_assemble_view() const +{ + for (auto object: model.objects) + { + for (auto instance : object->instances) + if (instance->is_assemble_initialized()) + return true; + + int part_cnt = 0; + for (auto volume : object->volumes) { + if (volume->is_model_part()) + part_cnt++; + } + + if (part_cnt > 1) + return true; + } + return false; +} + +#if ENABLE_ENHANCED_PRINT_VOLUME_FIT +bool Plater::priv::can_scale_to_print_volume() const +{ + const BuildVolume_Type type = this->bed.build_volume().type(); + return !sidebar->obj_list()->has_selected_cut_object() + && !view3D->get_canvas3d()->get_selection().is_empty() + && (type == BuildVolume_Type::Rectangle || type == BuildVolume_Type::Circle); +} +#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT + +bool Plater::priv::can_mirror() const +{ + return !sidebar->obj_list()->has_selected_cut_object() + && get_selection().is_from_single_instance(); +} + +bool Plater::priv::can_replace_with_stl() const +{ + return !sidebar->obj_list()->has_selected_cut_object() + && get_selection().get_volume_idxs().size() == 1; +} + +bool Plater::priv::can_replace_all_with_stl() const +{ + return !sidebar->obj_list()->has_selected_cut_object() + && get_selection().get_volume_idxs().size() != 1; +} + +bool Plater::priv::can_reload_from_disk() const +{ + if (sidebar->obj_list()->has_selected_cut_object()) + return false; + +#if ENABLE_RELOAD_FROM_DISK_REWORK + // collect selected reloadable ModelVolumes + std::vector> selected_volumes = reloadable_volumes(model, get_selection()); + // nothing to reload, return + if (selected_volumes.empty()) + return false; +#else + // struct to hold selected ModelVolumes by their indices + struct SelectedVolume + { + int object_idx; + int volume_idx; + + // operators needed by std::algorithms + bool operator < (const SelectedVolume& other) const { return (object_idx < other.object_idx) || ((object_idx == other.object_idx) && (volume_idx < other.volume_idx)); } + bool operator == (const SelectedVolume& other) const { return (object_idx == other.object_idx) && (volume_idx == other.volume_idx); } + }; + std::vector selected_volumes; + + const Selection& selection = get_selection(); + + // collects selected ModelVolumes + const std::set& selected_volumes_idxs = selection.get_volume_idxs(); + for (unsigned int idx : selected_volumes_idxs) { + const GLVolume* v = selection.get_volume(idx); + int v_idx = v->volume_idx(); + if (v_idx >= 0) { + int o_idx = v->object_idx(); + if (0 <= o_idx && o_idx < (int)model.objects.size()) + selected_volumes.push_back({ o_idx, v_idx }); + } + } +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + +#if ENABLE_RELOAD_FROM_DISK_REWORK + std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { + return (v1.first < v2.first) || (v1.first == v2.first && v1.second < v2.second); + }); + selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { + return (v1.first == v2.first) && (v1.second == v2.second); + }), selected_volumes.end()); + + // collects paths of files to load + std::vector paths; + for (auto [obj_idx, vol_idx] : selected_volumes) { + paths.push_back(model.objects[obj_idx]->volumes[vol_idx]->source.input_file); + } +#else + std::sort(selected_volumes.begin(), selected_volumes.end()); + selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end()), selected_volumes.end()); + + // collects paths of files to load + std::vector paths; + for (const SelectedVolume& v : selected_volumes) { + const ModelObject* object = model.objects[v.object_idx]; + const ModelVolume* volume = object->volumes[v.volume_idx]; + if (!volume->source.input_file.empty()) + paths.push_back(volume->source.input_file); + else if (!object->input_file.empty() && !volume->name.empty() && !volume->source.is_from_builtin_objects) + paths.push_back(volume->name); + } +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + std::sort(paths.begin(), paths.end()); + paths.erase(std::unique(paths.begin(), paths.end()), paths.end()); + + return !paths.empty(); +} + +void Plater::priv::update_publish_dialog_status(wxString &msg, int percent) +{ + if (m_publish_dlg) + m_publish_dlg->UpdateStatus(msg, percent); +} + +bool Plater::priv::show_publish_dlg(bool show) +{ + if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":recevied publish event\n"; } + + if (!m_publish_dlg) m_publish_dlg = new PublishDialog(q); + if (show) { + m_publish_dlg->reset(); + m_publish_dlg->start_slicing(); + //m_publish_dlg->Show(); + m_publish_dlg->ShowModal(); + } else { + m_publish_dlg->EndModal(wxID_OK); + //cancel the slicing + if (this->background_process.running()) + this->background_process.stop(); + } + return true; +} + +//BBS: add bed exclude area +void Plater::priv::set_bed_shape(const Pointfs &shape, + const Pointfs &exclude_areas, + const Pointfs &wrapping_exclude_areas, + const double printable_height, + std::vector extruder_areas, + std::vector extruder_heights, + const std::string &custom_texture, + const std::string &custom_model, + bool force_as_custom) +{ + //Orca: reduce resolution for large bed printer + BoundingBoxf bed_size = get_extents(shape); + if (bed_size.size().maxCoeff() <= LARGE_BED_THRESHOLD) + SCALING_FACTOR = SCALING_FACTOR_INTERNAL; + else + SCALING_FACTOR = SCALING_FACTOR_INTERNAL_LARGE_PRINTER; + + //BBS: add shape position + Vec2d shape_position = partplate_list.get_current_shape_position(); + bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); + + float prev_height_lid, prev_height_rod; + partplate_list.get_height_limits(prev_height_lid, prev_height_rod); + double height_to_lid = config->opt_float("extruder_clearance_height_to_lid"); + double height_to_rod = config->opt_float("extruder_clearance_height_to_rod"); + + Pointfs prev_exclude_areas = partplate_list.get_exclude_area(); + Pointfs prev_wrapping_exclude_areas = partplate_list.get_wrapping_exclude_area(); + new_shape |= (height_to_lid != prev_height_lid) || (height_to_rod != prev_height_rod) || (prev_exclude_areas != exclude_areas) + || (prev_wrapping_exclude_areas != wrapping_exclude_areas); + if (!new_shape && partplate_list.get_logo_texture_filename() != custom_texture) { + partplate_list.update_logo_texture_filename(custom_texture); + } + if (new_shape) { + if (view3D) view3D->bed_shape_changed(); + if (preview) preview->bed_shape_changed(); + + //BBS: update part plate's size + // BBS: to be checked + Vec3d max = bed.printable_bounding_box().max; + Vec3d min = bed.printable_bounding_box().min; + double z = config->opt_float("printable_height"); + + partplate_list.reset_size(max.x() - min.x() - Bed3D::Axes::DefaultTipRadius, max.y() - min.y() - Bed3D::Axes::DefaultTipRadius, z); + partplate_list.set_shapes(shape, exclude_areas, wrapping_exclude_areas, extruder_areas, extruder_heights, custom_texture, height_to_lid, height_to_rod); + + Vec2d new_shape_position = partplate_list.get_current_shape_position(); + if (shape_position != new_shape_position) + bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, new_shape_position); + } +} + +bool Plater::priv::can_delete() const +{ + return !get_selection().is_empty() && !get_selection().is_wipe_tower(); +} + +bool Plater::priv::can_delete_all() const +{ + return !model.objects.empty(); +} + +bool Plater::priv::can_add_plate() const +{ + return q->get_partplate_list().get_plate_count() < PartPlateList::MAX_PLATES_COUNT; +} + +bool Plater::priv::can_delete_plate() const +{ + return q->get_partplate_list().get_plate_count() > 1; +} + +bool Plater::priv::can_fix_through_cgal() const +{ + std::vector obj_idxs, vol_idxs; + sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs); + +#if FIX_THROUGH_CGAL_ALWAYS + // Fixing always. + return ! obj_idxs.empty() || ! vol_idxs.empty(); +#else // FIX_THROUGH_CGAL_ALWAYS + // Fixing only if the model is not manifold. + if (vol_idxs.empty()) { + for (auto obj_idx : obj_idxs) + if (model.objects[obj_idx]->get_repaired_errors_count() > 0) + return true; + return false; + } + + int obj_idx = obj_idxs.front(); + for (auto vol_idx : vol_idxs) + if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0) + return true; + return false; +#endif // FIX_THROUGH_CGAL_ALWAYS +} + +bool Plater::priv::can_simplify() const +{ + // is object for simplification selected + if (get_selected_object_idx() < 0) return false; + // is already opened? + if (q->get_view3D_canvas3D()->get_gizmos_manager().get_current_type() == + GLGizmosManager::EType::Simplify) + return false; + return true; +} + +bool Plater::priv::can_smooth_mesh() const +{ + std::vector obj_idxs, vol_idxs; + sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs); + if (vol_idxs.empty()) { + for (auto obj_idx : obj_idxs) + if (model.objects[obj_idx]->get_object_stl_stats().open_edges > 0) + return false; + return true; + } + + int obj_idx = obj_idxs.front(); + for (auto vol_idx : vol_idxs) + if (model.objects[obj_idx]->get_object_stl_stats().open_edges > 0) + return false; + return true; +} + +bool Plater::priv::can_increase_instances() const +{ + if (!m_worker.is_idle() + || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) + return false; + + int obj_idx = get_selected_object_idx(); + return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) + && !sidebar->obj_list()->has_selected_cut_object() + && std::all_of(model.objects[obj_idx]->instances.begin(), model.objects[obj_idx]->instances.end(), [](auto& inst) {return inst->printable; }); +} + +bool Plater::priv::can_decrease_instances() const +{ + if (!m_worker.is_idle() + || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) + return false; + + int obj_idx = get_selected_object_idx(); + return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) && (model.objects[obj_idx]->instances.size() > 1) + && !sidebar->obj_list()->has_selected_cut_object(); +} + +bool Plater::priv::can_split_to_objects() const +{ + return q->can_split(true); +} + +bool Plater::priv::can_split_to_volumes() const +{ + return (printer_technology != ptSLA) && q->can_split(false); +} + +bool Plater::priv::can_arrange() const +{ + return !model.objects.empty() && m_worker.is_idle(); +} + +bool Plater::priv::layers_height_allowed() const +{ + if (printer_technology != ptFFF) + return false; + + int obj_idx = get_selected_object_idx(); + return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->max_z() > SINKING_Z_THRESHOLD && view3D->is_layers_editing_allowed(); +} + +bool Plater::priv::can_layers_editing() const +{ + return layers_height_allowed(); +} + +void Plater::priv::on_action_layersediting(SimpleEvent&) +{ + view3D->enable_layers_editing(!view3D->is_layers_editing_enabled()); + notification_manager->set_move_from_overlay(view3D->is_layers_editing_enabled()); +} + +void Plater::priv::on_create_filament(SimpleEvent &) +{ + CreateFilamentPresetDialog dlg(wxGetApp().mainframe); + int res = dlg.ShowModal(); + if (wxID_OK == res) { + wxGetApp().mainframe->update_side_preset_ui(); + update_ui_from_settings(); + sidebar->update_all_preset_comboboxes(); + CreatePresetSuccessfulDialog success_dlg(wxGetApp().mainframe, SuccessType::FILAMENT); + int res = success_dlg.ShowModal(); + } +} + +void Plater::priv::on_modify_filament(SimpleEvent &evt) +{ + Filamentinformation *filament_info = static_cast(evt.GetEventObject()); + int res; + std::shared_ptr need_edit_preset; + { + EditFilamentPresetDialog dlg(wxGetApp().mainframe, filament_info); + res = dlg.ShowModal(); + need_edit_preset = dlg.get_need_edit_preset(); + } + wxGetApp().mainframe->update_side_preset_ui(); + update_ui_from_settings(); + sidebar->update_all_preset_comboboxes(); + if (wxID_EDIT == res) { + Tab *tab = wxGetApp().get_tab(Preset::Type::TYPE_FILAMENT); + //tab->restore_last_select_item(); + if (tab == nullptr) { return; } + // Popup needs to be called before "restore_last_select_item", otherwise the page may not be updated + wxGetApp().params_dialog()->Popup(); + tab->restore_last_select_item(); + // Opening Studio and directly accessing the Filament settings interface through the edit preset button will not take effect and requires manual settings. + tab->set_just_edit(true); + tab->select_preset(need_edit_preset->name); + // when some preset have modified, if the printer is not need_edit_preset_name compatible printer, the preset will jump to other preset, need select again + if (!need_edit_preset->is_compatible) tab->select_preset(need_edit_preset->name); + } + +} + +void Plater::priv::on_add_filament(SimpleEvent &evt) { + sidebar->add_filament(); +} + +void Plater::priv::on_delete_filament(SimpleEvent &evt) { + sidebar->delete_filament(); +} + +void Plater::priv::on_add_custom_filament(ColorEvent &evt) +{ + sidebar->add_custom_filament(evt.data); +} + +void Plater::priv::enter_gizmos_stack() +{ + assert(m_undo_redo_stack_active == &m_undo_redo_stack_main); + if (m_undo_redo_stack_active == &m_undo_redo_stack_main) { + m_undo_redo_stack_active = &m_undo_redo_stack_gizmos; + assert(m_undo_redo_stack_active->empty()); + // Take the initial snapshot of the gizmos. + // Not localized on purpose, the text will never be shown to the user. + this->take_snapshot(std::string("Gizmos-Initial")); + } +} + +bool Plater::priv::leave_gizmos_stack() +{ + bool changed = false; + assert(m_undo_redo_stack_active == &m_undo_redo_stack_gizmos); + if (m_undo_redo_stack_active == &m_undo_redo_stack_gizmos) { + assert(! m_undo_redo_stack_active->empty()); + changed = m_undo_redo_stack_gizmos.has_undo_snapshot(); + m_undo_redo_stack_active->clear(); + m_undo_redo_stack_active = &m_undo_redo_stack_main; + } + return changed; +} + +int Plater::priv::get_active_snapshot_index() +{ + const size_t active_snapshot_time = this->undo_redo_stack().active_snapshot_time(); + const std::vector& ss_stack = this->undo_redo_stack().snapshots(); + const auto it = std::lower_bound(ss_stack.begin(), ss_stack.end(), UndoRedo::Snapshot(active_snapshot_time)); + return it - ss_stack.begin(); +} + +void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRedo::SnapshotType snapshot_type) +{ + if (m_prevent_snapshots > 0) + return; + assert(m_prevent_snapshots >= 0); + // BBS: single snapshot + if (m_single && !m_single->check(snapshot_modifies_project(snapshot_type) && (snapshot_name.empty() || snapshot_name.back() != '!'))) + return; + UndoRedo::SnapshotData snapshot_data; + snapshot_data.snapshot_type = snapshot_type; + snapshot_data.printer_technology = this->printer_technology; + if (this->view3D->is_layers_editing_enabled()) + snapshot_data.flags |= UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE; + if (this->sidebar->obj_list()->is_selected(itSettings)) { + snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR; + snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); + } + else if (this->sidebar->obj_list()->is_selected(itLayer)) { + snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR; + snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); + } + else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) + snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR; + + // If SLA gizmo is active, ask it if it wants to trigger support generation + // on loading this snapshot. + if (view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo()) + snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS; + + //FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. + // BBS: add partplate logic + if (this->printer_technology == ptFFF) { + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; + const ConfigOptionFloats* tower_x_opt = proj_cfg.option("wipe_tower_x"); + const ConfigOptionFloats* tower_y_opt = proj_cfg.option("wipe_tower_y"); + assert(tower_x_opt->values.size() == tower_y_opt->values.size()); + model.wipe_tower.positions.clear(); + model.wipe_tower.positions.resize(tower_x_opt->values.size()); + for (int plate_idx = 0; plate_idx < tower_x_opt->values.size(); plate_idx++) { + ModelWipeTower& tower = model.wipe_tower; + + tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); + tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + } + } + const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : view3D->get_canvas3d()->get_gizmos_manager(); + + if (snapshot_type == UndoRedo::SnapshotType::ProjectSeparator) + this->undo_redo_stack().clear(); + this->undo_redo_stack().take_snapshot(snapshot_name, model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection() : view3D->get_canvas3d()->get_selection(), gizmos, partplate_list, snapshot_data); + if (snapshot_type == UndoRedo::SnapshotType::LeavingGizmoWithAction) { + // Filter all but the last UndoRedo::SnapshotType::GizmoAction in a row between the last UndoRedo::SnapshotType::EnteringGizmo and UndoRedo::SnapshotType::LeavingGizmoWithAction. + // The remaining snapshot will be renamed to a more generic name, + // depending on what gizmo is being left. + if (gizmos.get_current() != nullptr) { + std::string new_name = gizmos.get_current()->get_action_snapshot_name(); + this->undo_redo_stack().reduce_noisy_snapshots(new_name); + } + } else if (snapshot_type == UndoRedo::SnapshotType::ProjectSeparator) { + // Reset the "dirty project" flag. + m_undo_redo_stack_main.mark_current_as_saved(); + } + //BBS: add PartPlateList as the paremeter for take_snapshot + this->undo_redo_stack().release_least_recently_used(); + + dirty_state.update_from_undo_redo_stack(m_undo_redo_stack_main.project_modified()); + + // Save the last active preset name of a particular printer technology. + ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); + 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(); +} + +void Plater::priv::undo() +{ + const std::vector &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())); + // BBS: undo-redo until modify record + while (--it_current != snapshots.begin() && !snapshot_modifies_project(*it_current)); + if (it_current == snapshots.begin()) return; + if (get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView) { + if (it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::GizmoAction && + it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::EnteringGizmo && + it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::LeavingGizmoNoAction && + it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::LeavingGizmoWithAction) + return; + } + this->undo_redo_to(it_current); +} + +void Plater::priv::redo() +{ + const std::vector &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())); + // BBS: undo-redo until modify record + while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)); + if (it_current != snapshots.end()) { + while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)); + this->undo_redo_to(--it_current); + } +} + +void Plater::priv::undo_redo_to(size_t time_to_load) +{ + const std::vector &snapshots = this->undo_redo_stack().snapshots(); + auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(time_to_load)); + assert(it_current != snapshots.end()); + this->undo_redo_to(it_current); +} + +// BBS: check need save or backup +bool Plater::priv::up_to_date(bool saved, bool backup) +{ + size_t& last_time = backup ? m_backup_timestamp : m_saved_timestamp; + if (saved) { + last_time = undo_redo_stack_main().active_snapshot_time(); + if (!backup) + undo_redo_stack_main().mark_current_as_saved(); + return true; + } + else { + return !undo_redo_stack_main().has_real_change_from(last_time); + } +} + +void Plater::priv::undo_redo_to(std::vector::const_iterator it_snapshot) +{ + // Make sure that no updating function calls take_snapshot until we are done. + SuppressSnapshots snapshot_supressor(q); + + bool temp_snapshot_was_taken = this->undo_redo_stack().temp_snapshot_active(); + PrinterTechnology new_printer_technology = it_snapshot->snapshot_data.printer_technology; + bool printer_technology_changed = this->printer_technology != new_printer_technology; + if (printer_technology_changed) { + //BBS do not support SLA + } + // Save the last active preset name of a particular printer technology. + ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); + //FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. + // BBS: add partplate logic + if (this->printer_technology == ptFFF) { + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; + const ConfigOptionFloats* tower_x_opt = proj_cfg.option("wipe_tower_x"); + const ConfigOptionFloats* tower_y_opt = proj_cfg.option("wipe_tower_y"); + assert(tower_x_opt->values.size() == tower_y_opt->values.size()); + model.wipe_tower.positions.clear(); + model.wipe_tower.positions.resize(tower_x_opt->values.size()); + for (int plate_idx = 0; plate_idx < tower_x_opt->values.size(); plate_idx++) { + ModelWipeTower& tower = model.wipe_tower; + + tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); + tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + } + } + const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx; + // Flags made of Snapshot::Flags enum values. + unsigned int new_flags = it_snapshot->snapshot_data.flags; + UndoRedo::SnapshotData top_snapshot_data; + top_snapshot_data.printer_technology = this->printer_technology; + if (this->view3D->is_layers_editing_enabled()) + top_snapshot_data.flags |= UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE; + if (this->sidebar->obj_list()->is_selected(itSettings)) { + top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR; + top_snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); + } + else if (this->sidebar->obj_list()->is_selected(itLayer)) { + top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR; + top_snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); + } + else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) + top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR; + bool new_variable_layer_editing_active = (new_flags & UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE) != 0; + bool new_selected_settings_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR) != 0; + bool new_selected_layer_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR) != 0; + bool new_selected_layerroot_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR) != 0; + + if (this->view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo()) + top_snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS; + + // Disable layer editing before the Undo / Redo jump. + if (!new_variable_layer_editing_active && view3D->is_layers_editing_enabled()) + view3D->get_canvas3d()->force_main_toolbar_left_action(view3D->get_canvas3d()->get_main_toolbar_item_id("layersediting")); + + // Make a copy of the snapshot, undo/redo could invalidate the iterator + const UndoRedo::Snapshot snapshot_copy = *it_snapshot; + // Do the jump in time. + if (it_snapshot->timestamp < this->undo_redo_stack().active_snapshot_time() ? + this->undo_redo_stack().undo(model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection() : this->view3D->get_canvas3d()->get_selection(), get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : this->view3D->get_canvas3d()->get_gizmos_manager(), this->partplate_list, top_snapshot_data, it_snapshot->timestamp) : + this->undo_redo_stack().redo(model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : this->view3D->get_canvas3d()->get_gizmos_manager(), this->partplate_list, it_snapshot->timestamp)) { + if (printer_technology_changed) { + // Switch to the other printer technology. Switch to the last printer active for that particular technology. + AppConfig *app_config = wxGetApp().app_config; + app_config->set("presets", PRESET_PRINTER_NAME, (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name); + //FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive. + // Anyways, don't report any config value substitutions, they have been already reported to the user at application start up. + wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); + // load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> Object_list::update_and_show_object_settings_item(), + // but the Object list still keeps pointer to the old Model. Avoid a crash by removing selection first. + this->sidebar->obj_list()->unselect_objects(); + // Load the currently selected preset into the GUI, update the preset selection box. + // This also switches the printer technology based on the printer technology of the active printer profile. + wxGetApp().load_current_presets(); + } + //FIXME updating the Print config from the Wipe tower config values at the ModelWipeTower. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. + // BBS: add partplate logic + if (this->printer_technology == ptFFF) { + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; + ConfigOptionFloats* tower_x_opt = const_cast(proj_cfg.option("wipe_tower_x")); + ConfigOptionFloats* tower_y_opt = const_cast(proj_cfg.option("wipe_tower_y")); + // BBS: don't support wipe tower rotation + //double current_rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + bool need_update = false; + if (tower_x_opt->values.size() != model.wipe_tower.positions.size()) { + tower_x_opt->clear(); + ConfigOptionFloat default_tower_x(40.f); + tower_x_opt->resize(model.wipe_tower.positions.size(), &default_tower_x); + need_update = true; + } + + if (tower_y_opt->values.size() != model.wipe_tower.positions.size()) { + tower_y_opt->clear(); + ConfigOptionFloat default_tower_y(200.f); + tower_y_opt->resize(model.wipe_tower.positions.size(), &default_tower_y); + need_update = true; + } + + for (int plate_idx = 0; plate_idx < model.wipe_tower.positions.size(); plate_idx++) { + if (Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)) != model.wipe_tower.positions[plate_idx]) { + ConfigOptionFloat tower_x_new(model.wipe_tower.positions[plate_idx].x()); + ConfigOptionFloat tower_y_new(model.wipe_tower.positions[plate_idx].y()); + tower_x_opt->set_at(&tower_x_new, plate_idx, 0); + tower_y_opt->set_at(&tower_y_new, plate_idx, 0); + need_update = true; + break; + } + } + + if (need_update) { + // update print to current plate (preview->m_process) + this->partplate_list.update_slice_context_to_current_plate(this->background_process); + this->preview->update_gcode_result(this->partplate_list.get_current_slice_result()); + this->update(); + } + } + // set selection mode for ObjectList on sidebar + this->sidebar->obj_list()->set_selection_mode(new_selected_settings_on_sidebar ? ObjectList::SELECTION_MODE::smSettings : + new_selected_layer_on_sidebar ? ObjectList::SELECTION_MODE::smLayer : + new_selected_layerroot_on_sidebar ? ObjectList::SELECTION_MODE::smLayerRoot : + ObjectList::SELECTION_MODE::smUndef); + if (new_selected_settings_on_sidebar || new_selected_layer_on_sidebar) + this->sidebar->obj_list()->set_selected_layers_range_idx(layer_range_idx); + + this->update_after_undo_redo(snapshot_copy, temp_snapshot_was_taken); + // Enable layer editing after the Undo / Redo jump. + if (!view3D->is_layers_editing_enabled() && this->layers_height_allowed() && new_variable_layer_editing_active) + view3D->get_canvas3d()->force_main_toolbar_left_action(view3D->get_canvas3d()->get_main_toolbar_item_id("layersediting")); + } + + dirty_state.update_from_undo_redo_stack(m_undo_redo_stack_main.project_modified()); + update_title_dirty_status(); +} + +void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool /* temp_snapshot_was_taken */) +{ + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection().clear() : this->view3D->get_canvas3d()->get_selection().clear(); + // Update volumes from the deserializd model, always stop / update the background processing (for both the SLA and FFF technologies). + this->update((unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE | (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); + // Release old snapshots if the memory allocated is excessive. This may remove the top most snapshot if jumping to the very first snapshot. + //if (temp_snapshot_was_taken) + // Release the old snapshots always, as it may have happened, that some of the triangle meshes got deserialized from the snapshot, while some + // triangle meshes may have gotten released from the scene or the background processing, therefore now being calculated into the Undo / Redo stack size. + this->undo_redo_stack().release_least_recently_used(); + //YS_FIXME update obj_list from the deserialized model (maybe store ObjectIDs into the tree?) (no selections at this point of time) + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), this->undo_redo_stack().selection_deserialized().volumes_and_instances) : + this->view3D->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), this->undo_redo_stack().selection_deserialized().volumes_and_instances); + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d()->get_gizmos_manager().update_after_undo_redo(snapshot) : + this->view3D->get_canvas3d()->get_gizmos_manager().update_after_undo_redo(snapshot); + + wxGetApp().obj_list()->update_after_undo_redo(); + + if (wxGetApp().get_mode() == comSimple && model_has_advanced_features(this->model)) { + // If the user jumped to a snapshot that require user interface with advanced features, switch to the advanced mode without asking. + // There is a little risk of surprising the user, as he already must have had the advanced or advanced mode active for such a snapshot to be taken. + Slic3r::GUI::wxGetApp().save_mode(comAdvanced); + view3D->set_as_dirty(); + } + + // this->update() above was called with POSTPONE_VALIDATION_ERROR_MESSAGE, so that if an error message was generated when updating the back end, it would not open immediately, + // but it would be saved to be show later. Let's do it now. We do not want to display the message box earlier, because on Windows & OSX the message box takes over the message + // queue pump, which in turn executes the rendering function before a full update after the Undo / Redo jump. + this->show_delayed_error_message(); + + //FIXME what about the state of the manipulators? + //FIXME what about the focus? Cursor in the side panel? + + BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot reloaded. Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); +} + +void Plater::priv::bring_instance_forward() const +{ +#ifdef __APPLE__ + wxGetApp().other_instance_message_handler()->bring_instance_forward(); + return; +#endif //__APPLE__ + if (main_frame == nullptr) { + BOOST_LOG_TRIVIAL(debug) << "Couldnt bring instance forward - mainframe is null"; + return; + } + BOOST_LOG_TRIVIAL(debug) << "Orca Slicer window going forward"; + //this code maximize app window on Fedora + { + main_frame->Iconize(false); + if (main_frame->IsMaximized()) + main_frame->Maximize(true); + else + main_frame->Maximize(false); + } + //this code maximize window on Ubuntu + { + main_frame->Restore(); + wxGetApp().GetTopWindow()->SetFocus(); // focus on my window + wxGetApp().GetTopWindow()->Show(true); // show the window + wxGetApp().GetTopWindow()->Raise(); // bring window to front + } +} + +//BBS: popup object table +bool Plater::priv::PopupObjectTable(int object_id, int volume_id, const wxPoint& position) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, create ObjectTableDialog"); + int max_width{1920}, max_height{1080}; + + max_width = q->GetMaxWidth(); + max_height = q->GetMaxHeight(); + ObjectTableDialog table_dialog(q, q, &model, wxSize(max_width, max_height)); + //m_popup_table = new ObjectTableDialog(q, q, &model); + + wxRect rect = sidebar->GetRect(); + wxPoint pos = sidebar->ClientToScreen(wxPoint(rect.x, rect.y)); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": show ObjectTableDialog"); + table_dialog.Popup(object_id, volume_id, pos); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, will destroy ObjectTableDialog"); + return true; +} + +void Sidebar::set_btn_label(const ActionButtonType btn_type, const wxString& label) const +{ + switch (btn_type) + { + case ActionButtonType::abReslice: p->btn_reslice->SetLabelText(label); break; + case ActionButtonType::abExport: p->btn_export_gcode->SetLabelText(label); break; + case ActionButtonType::abSendGCode: /*p->btn_send_gcode->SetLabelText(label);*/ break; + } +} + +// Plater / Public + +Plater::Plater(wxWindow *parent, MainFrame *main_frame) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size()) + , p(new priv(this, main_frame)) +{ + // Initialization performed in the private c-tor + enable_wireframe(true); + m_only_gcode = false; +} + +bool Plater::Show(bool show) +{ + if (wxGetApp().mainframe) + wxGetApp().mainframe->show_option(show); + return wxPanel::Show(show); +} + +bool Plater::is_project_dirty() const { return p->is_project_dirty(); } +bool Plater::is_presets_dirty() const { return p->is_presets_dirty(); } +void Plater::set_plater_dirty(bool is_dirty) { p->set_plater_dirty(is_dirty); } +void Plater::update_project_dirty_from_presets() { p->update_project_dirty_from_presets(); } +int Plater::save_project_if_dirty(const wxString& reason) { return p->save_project_if_dirty(reason); } +void Plater::reset_project_dirty_after_save() { p->reset_project_dirty_after_save(); } +void Plater::reset_project_dirty_initial_presets() { p->reset_project_dirty_initial_presets(); } +#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW +void Plater::render_project_state_debug_window() const { p->render_project_state_debug_window(); } +#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + +Sidebar& Plater::sidebar() { return *p->sidebar; } +const Model& Plater::model() const { return p->model; } +Model& Plater::model() { return p->model; } +const Print& Plater::fff_print() const { return p->fff_print; } +Print& Plater::fff_print() { return p->fff_print; } +const SLAPrint& Plater::sla_print() const { return p->sla_print; } +SLAPrint& Plater::sla_print() { return p->sla_print; } + +int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_name) +{ + model().calib_pa_pattern.reset(nullptr); + model().plates_custom_gcodes.clear(); + + bool transfer_preset_changes = false; + // BBS: save confirm + auto check = [this,&transfer_preset_changes](bool yes_or_no) { + m_new_project_and_check_state = true; + wxString header = _L("Some presets are modified.") + "\n" + + (yes_or_no ? _L("You can keep the modified presets to the new project or discard them") : + _L("You can keep the modified presets to the new project, discard or save changes as new presets.")); + int act_buttons = ActionButtons::KEEP | ActionButtons::REMEMBER_CHOISE; + if (!yes_or_no) + act_buttons |= ActionButtons::SAVE; + if (m_exported_file) { //.gcode.3mf ignore presets modify + m_exported_file = false; + } + bool result = wxGetApp().check_and_keep_current_preset_changes(_L("Creating a new project"), header, act_buttons, &transfer_preset_changes); + m_new_project_and_check_state = false; + return result; + }; + int result; + if (!skip_confirm && (result = close_with_confirm(check)) == wxID_CANCEL) + return wxID_CANCEL; + + m_only_gcode = false; + m_exported_file = false; + m_loading_project = false; + get_notification_manager()->clear_all(); + + if (!silent) + wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + + //get_partplate_list().reinit(); + //get_partplate_list().update_slice_context_to_current_plate(p->background_process); + //p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + reset(transfer_preset_changes); + reset_project_dirty_after_save(); + reset_project_dirty_initial_presets(); + wxGetApp().update_saved_preset_from_current_preset(); + update_project_dirty_from_presets(); + + //reset project + p->project.reset(); + //set project name + if (project_name.empty()) + p->set_project_name(_L("Untitled")); + else + p->set_project_name(project_name); + + Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator); + + Model m; + model().load_from(m); // new id avoid same path name + + //select first plate + get_partplate_list().select_plate(0); + SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); + p->on_plate_selected(event); + + p->load_auxiliary_files(); + wxGetApp().app_config->update_last_backup_dir(model().get_backup_path()); + + // BBS set default view and zoom + p->select_view_3D("3D"); + p->select_view("topfront"); + p->camera.requires_zoom_to_bed = true; + enable_sidebar(!m_only_gcode); + + up_to_date(true, false); + up_to_date(true, true); + return wxID_YES; +} + +LoadType determine_load_type(std::string filename, std::string override_setting = ""); + +// BBS: FIXME, missing resotre logic +void Plater::load_project(wxString const& filename2, + wxString const& originfile) +{ + model().calib_pa_pattern.reset(nullptr); + model().plates_custom_gcodes.clear(); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "filename is: " << filename2 << "and originfile is: " << originfile; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__; + auto filename = filename2; + auto check = [&filename, this] (bool yes_or_no) { + if (!yes_or_no && !wxGetApp().check_and_save_current_preset_changes(_L("Load project"), + _L("Some presets are modified."))) + return false; + if (filename.empty()) { + // Ask user for a project file name. + wxGetApp().load_project(this, filename); + } + return !filename.empty(); + }; + + // BSS: save project, force close + int result; + if ((result = close_with_confirm(check)) == wxID_CANCEL) { + return; + } + + // BBS + if (m_loading_project) { + //some error cases happens + //return directly + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": current loading other project, return directly"); + return; + } + else + m_loading_project = true; + + m_only_gcode = false; + m_exported_file = false; + get_notification_manager()->bbl_close_plateinfo_notification(); + get_notification_manager()->bbl_close_preview_only_notification(); + get_notification_manager()->bbl_close_3mf_warn_notification(); + get_notification_manager()->close_notification_of_type(NotificationType::PlaterError); + get_notification_manager()->close_notification_of_type(NotificationType::PlaterWarning); + get_notification_manager()->close_notification_of_type(NotificationType::SlicingError); + get_notification_manager()->close_notification_of_type(NotificationType::SlicingSeriousWarning); + get_notification_manager()->close_notification_of_type(NotificationType::SlicingWarning); + + auto path = into_path(filename); + + auto strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig; + if (originfile == "") { + strategy = strategy | LoadStrategy::Silence; + } else if (originfile == "") { + // Do nothing + } else if (originfile != "-") { + strategy = strategy | LoadStrategy::Restore; + } else { + switch (determine_load_type(filename.ToStdString())) { + case LoadType::OpenProject: break; // Do nothing + case LoadType::LoadGeometry:; strategy = LoadStrategy::LoadModel; break; + default: return; // User cancelled + } + } + bool load_restore = strategy & LoadStrategy::Restore; + + // Take the Undo / Redo snapshot. + reset(); + + Plater::TakeSnapshot snapshot(this, "Load Project", UndoRedo::SnapshotType::ProjectSeparator); + + std::vector input_paths; + input_paths.push_back(path); + if (strategy & LoadStrategy::Restore) + input_paths.push_back(into_u8(originfile)); + + std::vector res = load_files(input_paths, strategy); + + reset_project_dirty_initial_presets(); + update_project_dirty_from_presets(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + + // if res is empty no data has been loaded + if (!res.empty() && (load_restore || !(strategy & LoadStrategy::Silence))) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << (load_restore ? originfile : filename); + p->set_project_filename(load_restore ? originfile : filename); + if (load_restore && originfile.IsEmpty()) { + p->set_project_name(_L("Untitled")); + } + + } else { + if (using_exported_file()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename; + p->set_project_filename(filename); + } + + } + + // BBS set default 3D view and direction after loading project + //p->select_view_3D("3D"); + if (!m_exported_file) { + p->select_view("topfront"); + p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + } + else { + p->partplate_list.select_plate_view(); + } + + enable_sidebar(!m_only_gcode); + + wxGetApp().app_config->update_last_backup_dir(model().get_backup_path()); + if (load_restore && !originfile.empty()) { + wxGetApp().app_config->update_skein_dir(into_path(originfile).parent_path().string()); + wxGetApp().app_config->update_config_dir(into_path(originfile).parent_path().string()); + } + + if (!load_restore) + up_to_date(true, false); + else + p->dirty_state.update_from_undo_redo_stack(true); + up_to_date(true, true); + + wxGetApp().params_panel()->switch_to_object_if_has_object_configs(); + + auto has_modify = is_flush_config_modified(); + sidebar().set_flushing_volume_warning(has_modify); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load project done"; + m_loading_project = false; +} + +// BBS: save logic +int Plater::save_project(bool saveAs) +{ + //if (up_to_date(false, false)) // should we always save + // return; + auto filename = get_project_filename(".3mf"); + if (!saveAs && filename.IsEmpty()) + saveAs = true; + if (saveAs) + filename = p->get_export_file(FT_3MF); + if (filename.empty()) + return wxID_NO; + if (filename == "") + return wxID_CANCEL; + + //BBS export 3mf without gcode + auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh; + bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); + if (full_pathnames) { + save_strategy = save_strategy | SaveStrategy::FullPathSources; + } + if (export_3mf(into_path(filename), save_strategy) < 0) { + MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs open the project file."), + _L("Save project"), wxOK | wxICON_WARNING).ShowModal(); + return wxID_CANCEL; + } + + Slic3r::remove_backup(model(), false); + + p->set_project_filename(filename); + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << filename; + + up_to_date(true, false); + up_to_date(true, true); + + wxGetApp().update_saved_preset_from_current_preset(); + reset_project_dirty_after_save(); + try { + json j; + boost::uintmax_t size = boost::filesystem::file_size(into_path(filename)); + j["file_size"] = size; + j["file_name"] = std::string(filename.mb_str()); + + NetworkAgent* agent = wxGetApp().getAgent(); + } + catch (...) {} + + update_title_dirty_status(); + return wxID_YES; +} + +//BBS import model by model id +void Plater::import_model_id(wxString download_info) +{ + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " download info: " << download_info; + + wxString download_origin_url = download_info; + wxString download_url; + wxString filename; + wxString separator = "&name="; + + try + { + size_t namePos = download_info.Find(separator); + if (namePos != wxString::npos) { + download_url = download_info.Mid(0, namePos); + filename = download_info.Mid(namePos + separator.Length()); + + } + else { + fs::path download_path = fs::path(download_origin_url.wx_str()); + download_url = download_origin_url; + filename = download_path.filename().string(); + } + + } + catch (const std::exception&) + { + //wxString sError = error.what(); + } + + bool download_ok = false; + int retry_count = 0; + const int max_retries = 3; + + /* jump to 3D eidtor */ + wxGetApp().mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + + /* prepare progress dialog */ + bool cont = true; + bool cont_dlg = true; + bool cancel = false; + wxString msg; + wxString dlg_title = _L("Importing Model"); + + int percent = 0; + ProgressDialog dlg(dlg_title, + wxString(' ', 100) + "\n\n\n\n", + 100, // range + this, // parent + wxPD_CAN_ABORT | + wxPD_APP_MODAL | + wxPD_AUTO_HIDE | + wxPD_SMOOTH); + + boost::filesystem::path target_path; + + //reset params + p->project.reset(); + + /* prepare project and profile */ + boost::thread import_thread = Slic3r::create_thread([&percent, &cont, &cancel, &retry_count, max_retries, &msg, &target_path, &download_ok, download_url, &filename] { + + // Orca: NetworkAgent is not needed and only prevents this from running +// NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent(); +// if (!m_agent) return; + + int res = 0; + std::string http_body; + + msg = _L("Preparing 3MF file..."); + + //gets the number of files with the same name + std::vector vecFiles; + bool is_already_exist = false; + + + target_path = fs::path(wxGetApp().app_config->get("download_path")); + + try + { + vecFiles.clear(); + wxString extension = fs::path(filename.wx_str()).extension().c_str(); + + + //check file suffix + if (!extension.Contains(".3mf")) { + msg = _L("Download failed, unknown file format."); + return; + } + + auto name = filename.substr(0, filename.length() - extension.length() - 1); + + for (const auto& iter : boost::filesystem::directory_iterator(target_path)) + { + if (boost::filesystem::is_directory(iter.path())) + continue; + + wxString sFile = iter.path().filename().string().c_str(); + if (strstr(sFile.c_str(), name.c_str()) != NULL) { + vecFiles.push_back(sFile); + } + + if (sFile == filename) is_already_exist = true; + } + } + catch (const std::exception&) + { + //wxString sError = error.what(); + } + + //update filename + if (is_already_exist && vecFiles.size() >= 1) { + wxString extension = fs::path(filename.wx_str()).extension().c_str(); + wxString name = filename.substr(0, filename.length() - extension.length()); + filename = wxString::Format("%s(%d)%s", name, vecFiles.size() + 1, extension).ToStdString(); + } + + + msg = _L("Downloading project..."); + + //target_path = wxStandardPaths::Get().GetTempDir().utf8_str().data(); + + + //target_path = wxGetApp().get_local_models_path().c_str(); + boost::uuids::uuid uuid = boost::uuids::random_generator()(); + std::string unique = to_string(uuid).substr(0, 6); + + if (filename.empty()) { + filename = "untitled.3mf"; + } + + //target_path /= (boost::format("%1%_%2%.3mf") % filename % unique).str(); + target_path /= fs::path(filename.wc_str()); + + fs::path tmp_path = target_path; + tmp_path += format(".%1%", ".download"); + + auto filesize = 0; + bool size_limit = false; + auto http = Http::get(download_url.ToStdString()); + + while (cont && retry_count < max_retries) { + retry_count++; + http.on_progress([&percent, &cont, &msg, &filesize, &size_limit](Http::Progress progress, bool& cancel) { + + if (!cont) cancel = true; + if (progress.dltotal != 0) { + + if (filesize == 0) { + filesize = progress.dltotal; + double megabytes = static_cast(progress.dltotal) / (1024 * 1024); + //The maximum size of a 3mf file is 500mb + if (megabytes > 500) { + cont = false; + size_limit = true; + } + } + percent = progress.dlnow * 100 / progress.dltotal; + } + + if (size_limit) { + msg = _L("Download failed, File size exception."); + } + else { + msg = wxString::Format(_L("Project downloaded %d%%"), percent); + } + }) + .on_error([&msg, &cont, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) { + (void)body; + BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%", + body, + http_status, + error); + + if (retry_count == max_retries) { + msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it."); + cont = false; + } + }) + .on_complete([&cont, &download_ok, tmp_path, target_path](std::string body, unsigned /* http_status */) { + fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(body.c_str(), body.size()); + file.close(); + fs::rename(tmp_path, target_path); + cont = false; + download_ok = true; + }).perform_sync(); + + // for break while + //cont = false; + } + + }); + + while (cont && cont_dlg) { + wxMilliSleep(50); + cont_dlg = dlg.Update(percent, msg); + if (!cont_dlg) { + cont = cont_dlg; + cancel = true; + } + + if (download_ok) + break; + } + + if (import_thread.joinable()) + import_thread.join(); + + dlg.Hide(); + +#ifdef _WIN32 + dlg.Close(); +#endif + + if (download_ok) { + BOOST_LOG_TRIVIAL(trace) << "import_model_id: target_path = " << target_path.string(); + /* load project */ + // Orca: If download is a zip file, treat it as if file has been drag and dropped on the plater + if (target_path.extension() == ".zip") + { wxArrayString arr; arr.Add(from_path(target_path)); this->load_files(arr); } + else + this->load_project(from_path(target_path)); + /*BBS set project info after load project, project info is reset in load project */ + //p->project.project_model_id = model_id; + //p->project.project_design_id = design_id; + AppConfig* config = wxGetApp().app_config; + if (config) { + p->project.project_country_code = config->get_country_code(); + } + + // show save new project + p->set_project_filename(target_path.wstring()); + p->notification_manager->push_import_finished_notification(target_path.string(), target_path.parent_path().string(), false); + } + else { + if (!msg.empty()) { + MessageDialog msg_wingow(nullptr, msg, wxEmptyString, wxICON_WARNING | wxOK); + msg_wingow.SetSize(wxSize(FromDIP(480), -1)); + msg_wingow.ShowModal(); + } + return; + } +} +//BBS download project by project id +void Plater::download_project(const wxString& project_id) +{ + return; +} + +void Plater::request_model_download(wxString url) +{ + wxCommandEvent* event = new wxCommandEvent(EVT_IMPORT_MODEL_ID); + event->SetString(url); + wxQueueEvent(this, event); +} + +void Plater::request_download_project(std::string project_id) +{ + wxCommandEvent* event = new wxCommandEvent(EVT_DOWNLOAD_PROJECT); + event->SetString(project_id); + wxQueueEvent(this, event); +} + +// BBS: save logic +bool Plater::up_to_date(bool saved, bool backup) +{ + if (saved) { + Slic3r::clear_other_changes(backup); + return p->up_to_date(saved, backup); + } + return p->model.objects.empty() || (p->up_to_date(saved, backup) && + !Slic3r::has_other_changes(backup)); +} + +void Plater::add_model(bool imperial_units, std::string fname) +{ + wxArrayString input_files; + + std::vector paths; + if (fname.empty()) { + wxGetApp().import_model(this, input_files); + if (input_files.empty()) + return; + + for (const auto& file : input_files) + paths.emplace_back(into_path(file)); + } + else { + paths.emplace_back(fname); + } + + std::string snapshot_label; + assert(! paths.empty()); + if (paths.size() == 1) { + snapshot_label = "Import Object"; + snapshot_label += ": "; + snapshot_label += encode_path(paths.front().filename().string().c_str()); + } else { + snapshot_label = "Import Objects"; + snapshot_label += ": "; + snapshot_label += paths.front().filename().string().c_str(); + for (size_t i = 1; i < paths.size(); ++ i) { + snapshot_label += ", "; + snapshot_label += encode_path(paths[i].filename().string().c_str()); + } + } + + Plater::TakeSnapshot snapshot(this, snapshot_label); + + // BBS: check file types + auto loadfiles_type = LoadFilesType::NoFile; + auto amf_files_count = get_3mf_file_count(paths); + + if (paths.size() > 1 && amf_files_count < paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } + if (paths.size() > 1 && amf_files_count == paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } + if (paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } + if (paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; + if (paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; + + bool ask_multi = false; + + if (loadfiles_type == LoadFilesType::MultipleOther) + ask_multi = true; + + auto strategy = LoadStrategy::LoadModel; + if (imperial_units) strategy = strategy | LoadStrategy::ImperialUnits; + if (!load_files(paths, strategy, ask_multi).empty()) { + + if (get_project_name() == _L("Untitled") && paths.size() > 0) { + boost::filesystem::path full_path(paths[0].string()); + p->set_project_name(from_u8(full_path.stem().string())); + } + + wxGetApp().mainframe->update_title(); + } +} + +void Plater::calib_pa(const Calib_Params& params) +{ + const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); + new_project(false, false, calib_pa_name); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false)); + print_config->set_key_value("precise_z_height", new ConfigOptionBool(false)); + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + switch (params.mode) { + case CalibMode::Calib_PA_Line: + add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/pressure_advance_test.drc"); + break; + case CalibMode::Calib_PA_Pattern: + _calib_pa_pattern(params); + break; + case CalibMode::Calib_PA_Tower: + _calib_pa_tower(params); + break; + default: break; + } + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::_calib_pa_pattern(const Calib_Params& params) +{ + std::vector speeds{params.speeds}; + std::vector accels{params.accelerations}; + std::vector object_idxs{}; + /* Set common parameters */ + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + DynamicPrintConfig& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + double nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); + filament_config->set_key_value("filament_retract_when_changing_layer", new ConfigOptionBoolsNullable{false}); + filament_config->set_key_value("filament_wipe", new ConfigOptionBoolsNullable{false}); + printer_config->set_key_value("wipe", new ConfigOptionBools{false}); + printer_config->set_key_value("retract_when_changing_layer", new ConfigOptionBools{false}); + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + + //Orca: find acceleration to use in the test + auto accel = print_config.option("outer_wall_acceleration")->value; // get the outer wall acceleration + if (accel == 0) // if outer wall accel isnt defined, fall back to inner wall accel + accel = print_config.option("inner_wall_acceleration")->value; + if (accel == 0) // if inner wall accel is not defined fall back to default accel + accel = print_config.option("default_acceleration")->value; + // Orca: Set all accelerations except first layer, as the first layer accel doesnt affect the PA test since accel + // is set to the travel accel before printing the pattern. + if (accels.empty()) { + accels.assign({accel}); + const auto msg{_L("INFO:") + "\n" + + _L("No accelerations provided for calibration. Use default acceleration value ") + std::to_string(long(accel)) + _L(u8"mm/s²")}; + get_notification_manager()->push_notification(msg.ToStdString()); + } else { + // set max acceleration in case of batch mode to get correct test pattern size + accel = *std::max_element(accels.begin(), accels.end()); + } + print_config.set_key_value( "outer_wall_acceleration", new ConfigOptionFloat(accel)); + print_config.set_key_value( "print_sequence", new ConfigOptionEnum(PrintSequence::ByLayer)); + + //Orca: find jerk value to use in the test + if(!has_junction_deviation(printer_config) && print_config.option("default_jerk")->value > 0){ // we have set a jerk value + auto jerk = print_config.option("outer_wall_jerk")->value; // get outer wall jerk + if (jerk == 0) // if outer wall jerk is not defined, get inner wall jerk + jerk = print_config.option("inner_wall_jerk")->value; + if (jerk == 0) // if inner wall jerk is not defined, get the default jerk + jerk = print_config.option("default_jerk")->value; + + //Orca: Set jerk values. Again first layer jerk should not matter as it is reset to the travel jerk before the + // first PA pattern is printed. + print_config.set_key_value( "default_jerk", new ConfigOptionFloat(jerk)); + print_config.set_key_value( "outer_wall_jerk", new ConfigOptionFloat(jerk)); + print_config.set_key_value( "inner_wall_jerk", new ConfigOptionFloat(jerk)); + print_config.set_key_value( "top_surface_jerk", new ConfigOptionFloat(jerk)); + print_config.set_key_value( "infill_jerk", new ConfigOptionFloat(jerk)); + print_config.set_key_value( "travel_jerk", new ConfigOptionFloat(jerk)); + } + + if (has_junction_deviation(printer_config)){ + print_config.set_key_value("default_junction_deviation", new ConfigOptionFloat(0)); + } + + for (const auto& opt : SuggestedConfigCalibPAPattern().float_pairs) { + print_config.set_key_value( + opt.first, + new ConfigOptionFloat(opt.second) + ); + } + + for (const auto& opt : SuggestedConfigCalibPAPattern().nozzle_ratio_pairs) { + print_config.set_key_value( + opt.first, + new ConfigOptionFloatOrPercent(nozzle_diameter * opt.second / 100, false) + ); + } + + for (const auto& opt : SuggestedConfigCalibPAPattern().int_pairs) { + print_config.set_key_value( + opt.first, + new ConfigOptionInt(opt.second) + ); + } + + print_config.set_key_value(SuggestedConfigCalibPAPattern().brim_pair.first, + new ConfigOptionEnum(SuggestedConfigCalibPAPattern().brim_pair.second)); + + print_config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + + // Orca: Set the outer wall speed to the optimal speed for the test, cap it with max volumetric speed + if (speeds.empty()) { + double speed = CalibPressureAdvance::find_optimal_PA_speed( + wxGetApp().preset_bundle->full_config(), + print_config.get_abs_value("line_width", nozzle_diameter), + print_config.get_abs_value("layer_height"), 0, 0); + print_config.set_key_value("outer_wall_speed", new ConfigOptionFloat(speed)); + + speeds.assign({speed}); + const auto msg{_L("INFO:") + "\n" + + _L("No speeds provided for calibration. Use default optimal speed ") + std::to_string(long(speed)) + _L("mm/s")}; + get_notification_manager()->push_notification(msg.ToStdString()); + } else if (speeds.size() == 1) { + // If we have single value provided, set speed using global configuration. + // per-object config is not set in this case + print_config.set_key_value("outer_wall_speed", new ConfigOptionFloat(speeds.front())); + } + + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); + + const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(); + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + const bool is_bbl_machine = preset_bundle->is_bbl_vendor(); + auto cur_plate = get_partplate_list().get_plate(0); + + // add "handle" cube + sidebar().obj_list()->load_generic_subobject("Cube", ModelVolumeType::INVALID); + auto *cube = model().objects[0]; + + CalibPressureAdvancePattern pa_pattern( + params, + full_config, + is_bbl_machine, + *cube, + cur_plate->get_origin() + ); + + /* Having PA pattern configured, we could make a set of polygons resembling N test patterns. + * We'll arrange this set of polygons, so we would know position of each test pattern and + * could position test cubes later on + * + * We'll take advantage of already existing cube: scale it up to test pattern size to use + * as a reference for objects arrangement. Polygon is slightly oversized to add spaces between patterns. + * That arrangement will be used to place 'handle cubes' for each test. */ + auto cube_bb = cube->raw_bounding_box(); + cube->scale((pa_pattern.print_size_x() + 4) / cube_bb.size().x(), + (pa_pattern.print_size_y() + 4) / cube_bb.size().y(), + pa_pattern.max_layer_z() / cube_bb.size().z()); + + arrangement::ArrangePolygons arranged_items; + { + arrangement::ArrangeParams ap; + Points bedpts = arrangement::get_shrink_bedpts(&full_config, ap); + + for(size_t i = 0; i < speeds.size() * accels.size(); i++) { + arrangement::ArrangePolygon p; + cube->instances[0]->get_arrange_polygon(&p); + p.bed_idx = 0; + arranged_items.emplace_back(p); + } + + arrangement::arrange(arranged_items, bedpts, ap); + } + + /* scale cube back to the size of test pattern 'handle' */ + cube_bb = cube->raw_bounding_box(); + cube->scale(pa_pattern.handle_xy_size() / cube_bb.size().x(), + pa_pattern.handle_xy_size() / cube_bb.size().y(), + pa_pattern.max_layer_z() / cube_bb.size().z()); + + /* Set speed and acceleration on per-object basis and arrange anchor object on the plates. + * Test gcode will be genecated during plate slicing */ + for(size_t test_idx = 0; test_idx < arranged_items.size(); test_idx++) { + const auto &ai = arranged_items[test_idx]; + size_t plate_idx = arranged_items[test_idx].bed_idx; + auto tspd = speeds[test_idx % speeds.size()]; + auto tacc = accels[test_idx / speeds.size()]; + + /* make an own copy of anchor cube for each test */ + auto obj = test_idx == 0 ? cube : model().add_object(*cube); + auto obj_idx = std::distance(model().objects.begin(), std::find(model().objects.begin(), model().objects.end(), obj)); + obj->name.assign(std::string("pa_pattern_") + std::to_string(int(tspd)) + std::string("_") + std::to_string(int(tacc))); + + auto &obj_config = obj->config; + if (speeds.size() > 1) + obj_config.set_key_value("outer_wall_speed", new ConfigOptionFloat(tspd)); + if (accels.size() > 1) + obj_config.set_key_value("outer_wall_acceleration", new ConfigOptionFloat(tacc)); + + auto cur_plate = get_partplate_list().get_plate(plate_idx); + if (!cur_plate) { + plate_idx = get_partplate_list().create_plate(); + cur_plate = get_partplate_list().get_plate(plate_idx); + } + + object_idxs.emplace_back(obj_idx); + get_partplate_list().add_to_plate(obj_idx, 0, plate_idx); + const Vec3d obj_offset{unscale(ai.translation(X)), + unscale(ai.translation(Y)), + 0}; + obj->instances[0]->set_offset(cur_plate->get_origin() + obj_offset + pa_pattern.handle_pos_offset()); + obj->ensure_on_bed(); + + if (obj_idx == 0) + sidebar().obj_list()->update_name_for_items(); + else + sidebar().obj_list()->add_object_to_list(obj_idx); + } + + model().calib_pa_pattern = std::make_unique(pa_pattern); + changed_objects(object_idxs); +} + +void Plater::_calib_pa_pattern_gen_gcode() +{ + if (!model().calib_pa_pattern) + return; + + auto cur_plate = get_partplate_list().get_curr_plate(); + if (cur_plate->empty()) + return; + + /* Container to store custom g-codes genereted by the test generator. + * We'll store gcode for all tests on a single plate here. Once the plate handling is done, + * all the g-codes will be merged into a single one on per-layer basis */ + std::vector mgc; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + + /* iterate over all cubes on current plate and generate gcode for them */ + for (auto obj : cur_plate->get_objects_on_this_plate()) { + auto gcode = model().calib_pa_pattern->generate_custom_gcodes( + preset_bundle->full_config(), + preset_bundle->is_bbl_vendor(), + *obj, + cur_plate->get_origin() + ); + mgc.emplace_back(gcode); + } + + // move first item into model custom gcode + auto &pcgc = model().plates_custom_gcodes[get_partplate_list().get_curr_plate_index()]; + pcgc = std::move(mgc[0]); + mgc.erase(mgc.begin()); + + // concat layer gcodes for each test + for (size_t i = 0; i < pcgc.gcodes.size(); i++) { + for (auto &gc : mgc) { + pcgc.gcodes[i].extra += gc.gcodes[i].extra; + } + } +} + +void Plater::cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes) +{ + wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); + auto *object = p->model.objects[obj_idx]; + + wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); + + if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) + return; + + wxBusyCursor wait; + + const Vec3d instance_offset = object->instances[instance_idx]->get_offset(); + Cut cut(object, instance_idx, Geometry::translation_transform(z * Vec3d::UnitZ() - instance_offset), attributes); + const auto new_objects = cut.perform_with_plane(); + + apply_cut_object_to_model(obj_idx, new_objects); +} + +void Plater::_calib_pa_tower(const Calib_Params& params) { + add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/tower_with_seam.drc"); + + auto& print_config = wxGetApp().preset_bundle->prints.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; + + const double nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); + + print_config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{ 1.0f }); + + + auto& obj_cfg = model().objects[0]->config; + + obj_cfg.set_key_value("alternate_extra_wall", new ConfigOptionBool(false)); + auto full_config = wxGetApp().preset_bundle->full_config(); + auto wall_speed = CalibPressureAdvance::find_optimal_PA_speed( + full_config, full_config.get_abs_value("line_width", nozzle_diameter), + full_config.get_abs_value("layer_height"), 0, 0); + obj_cfg.set_key_value("outer_wall_speed", new ConfigOptionFloat(wall_speed)); + obj_cfg.set_key_value("inner_wall_speed", new ConfigOptionFloat(wall_speed)); + obj_cfg.set_key_value("seam_position", new ConfigOptionEnum(spRear)); + obj_cfg.set_key_value("wall_loops", new ConfigOptionInt(2)); + obj_cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + obj_cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + obj_cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(0)); + obj_cfg.set_key_value("brim_type", new ConfigOptionEnum(btEar)); + obj_cfg.set_key_value("brim_object_gap", new ConfigOptionFloat(.0f)); + obj_cfg.set_key_value("brim_ears_max_angle", new ConfigOptionFloat(135.f)); + obj_cfg.set_key_value("brim_width", new ConfigOptionFloat(6.f)); + obj_cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + print_config.set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0)); + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); + + auto new_height = std::ceil((params.end - params.start) / params.step) + 1; + auto obj_bb = model().objects[0]->bounding_box_exact(); + if (new_height < obj_bb.size().z()) { + cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower); + } + + _calib_pa_select_added_objects(); +} + +void Plater::_calib_pa_select_added_objects() { + // update printable state for new volumes on canvas3D + wxGetApp().plater()->canvas3D()->update_instance_printable_state_for_objects({0}); + + Selection& selection = p->view3D->get_canvas3d()->get_selection(); + selection.clear(); + selection.add_object(0, false); + + // BBS: update object list selection + p->sidebar->obj_list()->update_selections(); + selection.notify_instance_update(-1, -1); + if (p->view3D->get_canvas3d()->get_gizmos_manager().is_enabled()) { + // this is required because the selected object changed and the flatten on face an sla support gizmos need to be updated accordingly + p->view3D->get_canvas3d()->update_gizmos_on_off_state(); + } +} + +// Adjust settings for flowrate calibration +// For linear mode, pass 1 means normal version while pass 2 mean "for perfectionists" version +// ORCA: Add pattern parameter +void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, int pass, InfillPattern pattern) +{ + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto printerConfig = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + + /// --- scale --- + // model is created for a 0.4 nozzle, scale z with nozzle size. + const ConfigOptionFloats* nozzle_diameter_config = printerConfig->option("nozzle_diameter"); + assert(nozzle_diameter_config->values.size() > 0); + float nozzle_diameter = nozzle_diameter_config->values[0]; + float xyScale = nozzle_diameter / 0.6; + //scale z to have 10 layers + // 2 bottom, 5 top, 3 sparse infill + double first_layer_height = print_config->option("initial_layer_print_height")->value; + double layer_height = nozzle_diameter / 2.0; // prefer 0.2 layer height for 0.4 nozzle + first_layer_height = std::max(first_layer_height, layer_height); + + const auto canvas = wxGetApp().plater()->canvas3D(); + auto& selection = canvas->get_selection(); + selection.setup_cache(); + TransformationType transformation_type; + transformation_type.set_relative(); + float zscale = (first_layer_height + 9 * layer_height) / 2; + // only enlarge + if (xyScale > 1.2) { + selection.scale({xyScale, xyScale, zscale}, transformation_type); + } else { + selection.scale({1, 1, zscale}, transformation_type); + } + canvas->do_scale(""); + + auto cur_flowrate = filament_config->option("filament_flow_ratio")->get_at(0); + Flow infill_flow = Flow(nozzle_diameter * 1.2f, layer_height, nozzle_diameter); + double filament_max_volumetric_speed = filament_config->option("filament_max_volumetric_speed")->get_at(0); + double max_infill_speed; + if (linear) + max_infill_speed = filament_max_volumetric_speed / + (infill_flow.mm3_per_mm() * (cur_flowrate + (pass == 2 ? 0.035 : 0.05)) / cur_flowrate); + else + max_infill_speed = filament_max_volumetric_speed / (infill_flow.mm3_per_mm() * (pass == 1 ? 1.2 : 1)); + double internal_solid_speed = std::floor(std::min(print_config->opt_float("internal_solid_infill_speed"), max_infill_speed)); + double top_surface_speed = std::floor(std::min(print_config->opt_float("top_surface_speed"), max_infill_speed)); + + // adjust parameters + for (auto _obj : objects) { + _obj->ensure_on_bed(); + _obj->config.set_key_value("wall_loops", new ConfigOptionInt(1)); + _obj->config.set_key_value("only_one_wall_top", new ConfigOptionBool(true)); + _obj->config.set_key_value("thick_internal_bridges", new ConfigOptionBool(false)); + _obj->config.set_key_value("enable_extra_bridge_layer", new ConfigOptionEnum(eblDisabled)); + _obj->config.set_key_value("internal_bridge_density", new ConfigOptionPercent(100)); + _obj->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(35)); + _obj->config.set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(100,true)); + _obj->config.set_key_value("bottom_shell_layers", new ConfigOptionInt(2)); + _obj->config.set_key_value("top_shell_layers", new ConfigOptionInt(5)); + _obj->config.set_key_value("top_shell_thickness", new ConfigOptionFloat(0)); + _obj->config.set_key_value("bottom_shell_thickness", new ConfigOptionFloat(0)); + _obj->config.set_key_value("detect_thin_wall", new ConfigOptionBool(true)); + _obj->config.set_key_value("filter_out_gap_fill", new ConfigOptionFloat(0)); + _obj->config.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + _obj->config.set_key_value("top_surface_line_width", new ConfigOptionFloatOrPercent(nozzle_diameter * 1.2f, false)); + _obj->config.set_key_value("internal_solid_infill_line_width", new ConfigOptionFloatOrPercent(nozzle_diameter * 1.2f, false)); + // ORCA: use the pattern parameter + _obj->config.set_key_value("top_surface_pattern", new ConfigOptionEnum(pattern)); + _obj->config.set_key_value("top_solid_infill_flow_ratio", new ConfigOptionFloat(1.0f)); + _obj->config.set_key_value("infill_direction", new ConfigOptionFloat(45)); + _obj->config.set_key_value("solid_infill_direction", new ConfigOptionFloat(135)); + _obj->config.set_key_value("align_infill_direction_to_model", new ConfigOptionBool(true)); + _obj->config.set_key_value("ironing_type", new ConfigOptionEnum(IroningType::NoIroning)); + _obj->config.set_key_value("internal_solid_infill_speed", new ConfigOptionFloat(internal_solid_speed)); + _obj->config.set_key_value("top_surface_speed", new ConfigOptionFloat(top_surface_speed)); + _obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + _obj->config.set_key_value("gap_fill_target", new ConfigOptionEnum(GapFillTarget::gftNowhere)); + print_config->set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0)); + _obj->config.set_key_value("calib_flowrate_topinfill_special_order", new ConfigOptionBool(true)); + + // extract flowrate from name, filename format: flowrate_xxx + std::string obj_name = _obj->name; + assert(obj_name.length() > 9); + obj_name = obj_name.substr(9); + if (obj_name[0] == 'm') + obj_name[0] = '-'; + // Orca: force set locale to C to avoid parsing error + const std::string _loc = std::setlocale(LC_NUMERIC, nullptr); + std::setlocale(LC_NUMERIC,"C"); + auto modifier = 1.0f; + try { + modifier = stof(obj_name); + } catch (...) { + } + // restore locale + std::setlocale(LC_NUMERIC, _loc.c_str()); + + if(linear) + _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat((cur_flowrate + modifier)/cur_flowrate)); + else + _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat(1.0f + modifier/100.f)); + + } + + print_config->set_key_value("layer_height", new ConfigOptionFloat(layer_height)); + print_config->set_key_value("alternate_extra_wall", new ConfigOptionBool(false)); + print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(first_layer_height)); + print_config->set_key_value("reduce_crossing_wall", new ConfigOptionBool(true)); + print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); +} + +// ORCA: Add pattern parameter +void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { + if (pass != 1 && pass != 2) + return; + wxString calib_name; + if (is_linear) { + calib_name = L"Orca YOLO Flow Calibration"; + if (pass == 2) + calib_name += L" - Perfectionist version"; + } else + calib_name = wxString::Format(L"Flowrate Test - Pass%d", pass); + + if (new_project(false, false, calib_name) == wxID_CANCEL) + return; + + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + + if (is_linear) { + if (pass == 1) + add_model(false, + (boost::filesystem::path(Slic3r::resources_dir()) / "calib" / "filament_flow" / "Orca-LinearFlow.3mf").string()); + else + add_model(false, + (boost::filesystem::path(Slic3r::resources_dir()) / "calib" / "filament_flow" / "Orca-LinearFlow_fine.3mf").string()); + } else { + if (pass == 1) + add_model(false, + (boost::filesystem::path(Slic3r::resources_dir()) / "calib" / "filament_flow" / "flowrate-test-pass1.3mf").string()); + else + add_model(false, + (boost::filesystem::path(Slic3r::resources_dir()) / "calib" / "filament_flow" / "flowrate-test-pass2.3mf").string()); + } + + // ORCA: pass the pattern + adjust_settings_for_flowrate_calib(model().objects, is_linear, pass, pattern); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + + // Refresh object after scaling + const std::vector object_idx(boost::counting_iterator(0), boost::counting_iterator(model().objects.size())); + changed_objects(object_idx); +} + + +void Plater::calib_temp(const Calib_Params& params) { + constexpr double base_temp_tower_nozzle_diameter = 0.4; + constexpr double base_temp_tower_block_height = 10.0; + constexpr int base_temp_tower_temp_step = 5; + + const auto calib_temp_name = wxString::Format(L"Nozzle temperature test"); + new_project(false, false, calib_temp_name); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + if (params.mode != CalibMode::Calib_Temp_Tower) + return; + + add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc"); + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto start_temp = lround(params.start); + const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); + size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); + double nozzle_diameter = base_temp_tower_nozzle_diameter; + if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) { + nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); + nozzle_diameter = nozzle_diameter_config->values[nozzle_id]; + } + if (nozzle_diameter <= 0.0) + nozzle_diameter = base_temp_tower_nozzle_diameter; + + const double nozzle_scale = nozzle_diameter / base_temp_tower_nozzle_diameter; + const double block_height = base_temp_tower_block_height; + + // cut upper + auto obj_bb = model().objects[0]->bounding_box_exact(); + auto block_count = lround((500 - params.end) / base_temp_tower_temp_step + 1); + if (block_count > 0) { + // subtract EPSILON offset to avoid cutting at the exact location where the flat surface is + auto new_height = block_count * block_height - EPSILON; + if (new_height < obj_bb.size().z()) { + cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower); + } + } + + // cut bottom + obj_bb = model().objects[0]->bounding_box_exact(); + block_count = lround((500 - params.start) / base_temp_tower_temp_step); + if (block_count > 0) { + auto new_height = block_count * block_height + EPSILON; + if (new_height < obj_bb.size().z()) { + cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepUpper); + } + } + + if (std::abs(nozzle_scale - 1.0) > EPSILON) + model().objects[0]->scale(nozzle_scale, nozzle_scale, nozzle_scale); + + model().objects[0]->ensure_on_bed(); + + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + filament_config->set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(1,(int)start_temp)); + filament_config->set_key_value("nozzle_temperature", new ConfigOptionInts(1,(int)start_temp)); + model().objects[0]->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter/2)); + model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum(btOuterOnly)); + model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(5.0)); + model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); + model().objects[0]->config.set_key_value("alternate_extra_wall", new ConfigOptionBool(false)); + model().objects[0]->config.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + model().objects[0]->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); + model().objects[0]->config.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter/2)); + + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::calib_max_vol_speed(const Calib_Params& params) +{ + const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test"); + new_project(false, false, calib_vol_speed_name); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + if (params.mode != CalibMode::Calib_Vol_speed_Tower) + return; + add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc"); + + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto obj = model().objects[0]; + auto& obj_cfg = obj->config; + + auto bed_shape = printer_config->option("printable_area")->values; + BoundingBoxf bed_ext = get_extents(bed_shape); + auto scale_obj = (bed_ext.size().x() - 10) / obj->bounding_box_exact().size().x(); + if (scale_obj < 1.0) + obj->scale(scale_obj, 1, 1); + + const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); + assert(nozzle_diameter_config->values.size() > 0); + double nozzle_diameter = nozzle_diameter_config->values[0]; + double line_width = nozzle_diameter * 1.75; + double layer_height = nozzle_diameter * 0.8; + + auto max_lh = printer_config->option("max_layer_height"); + if (max_lh->values[0] < layer_height) + max_lh->values[0] = { layer_height }; + + filament_config->set_key_value("filament_max_volumetric_speed", new ConfigOptionFloats { 200 }); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + obj_cfg.set_key_value("enable_overhang_speed", new ConfigOptionBool { false }); + obj_cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); + obj_cfg.set_key_value("alternate_extra_wall", new ConfigOptionBool(false)); + obj_cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + obj_cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + obj_cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(0)); + obj_cfg.set_key_value("outer_wall_line_width", new ConfigOptionFloatOrPercent(line_width, false)); + obj_cfg.set_key_value("layer_height", new ConfigOptionFloat(layer_height)); + obj_cfg.set_key_value("brim_type", new ConfigOptionEnum(btOuterAndInner)); + obj_cfg.set_key_value("brim_width", new ConfigOptionFloat(5.0)); + obj_cfg.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); + obj_cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); + print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); + print_config->set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0)); + print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); + + // cut upper + auto obj_bb = obj->bounding_box_exact(); + auto height = (params.end - params.start + 1) / params.step; + if (height < obj_bb.size().z()) { + cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); + } + + auto new_params = params; + auto mm3_per_mm = Flow(line_width, layer_height, nozzle_diameter).mm3_per_mm() * filament_config->option("filament_flow_ratio")->get_at(0); + new_params.end = params.end / mm3_per_mm; + new_params.start = params.start / mm3_per_mm; + new_params.step = params.step / mm3_per_mm; + + p->background_process.fff_print()->set_calib_params(new_params); +} + +void Plater::calib_retraction(const Calib_Params& params) +{ + const auto calib_retraction_name = wxString::Format(L"Retraction"); + new_project(false, false, calib_retraction_name); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + if (params.mode != CalibMode::Calib_Retraction_tower) + return; + + add_model(false, Slic3r::resources_dir() + "/calib/retraction/retraction_tower.drc"); + + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto obj = model().objects[0]; + + print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + + float nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); + float layer_height; + if (nozzle_diameter <= 0.1f) { + layer_height = 0.05f; + } else if (nozzle_diameter <= 0.2f) { + layer_height = 0.1f; + } else { + layer_height = 0.2f; + } + + auto max_lh = printer_config->option("max_layer_height"); + if (max_lh->values[0] < layer_height) + max_lh->values[0] = { layer_height }; + + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + printer_config->set_key_value("use_firmware_retraction", new ConfigOptionBool(false)); + obj->config.set_key_value("wall_loops", new ConfigOptionInt(2)); + obj->config.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + obj->config.set_key_value("bottom_shell_layers", new ConfigOptionInt(3)); + obj->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(0)); + print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(layer_height)); + obj->config.set_key_value("layer_height", new ConfigOptionFloat(layer_height)); + obj->config.set_key_value("alternate_extra_wall", new ConfigOptionBool(false)); + obj->config.set_key_value("seam_position", new ConfigOptionEnum(spAligned)); + obj->config.set_key_value("wall_sequence", new ConfigOptionEnum(WallSequence::InnerOuter)); + obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); + obj->config.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + + changed_objects({ 0 }); + + // cut upper + auto obj_bb = obj->bounding_box_exact(); + auto height = 1.0 + 0.4 + ((params.end - params.start)) / params.step - EPSILON; + if (height < obj_bb.size().z()) { + cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); + } + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::calib_VFA(const Calib_Params& params) +{ + const auto calib_vfa_name = wxString::Format(L"VFA test"); + new_project(false, false, calib_vfa_name); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + if (params.mode != CalibMode::Calib_VFA_Tower) + return; + + add_model(false, Slic3r::resources_dir() + "/calib/vfa/vfa.drc"); + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true}); + printer_config->set_key_value("input_shaping_type", new ConfigOptionEnum(InputShaperType::Disable)); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); + print_config->set_key_value("enable_overhang_speed", new ConfigOptionBool { false }); + print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); + print_config->set_key_value("wall_loops", new ConfigOptionInt(1)); + print_config->set_key_value("alternate_extra_wall", new ConfigOptionBool(false)); + print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0)); + print_config->set_key_value("bottom_shell_layers", new ConfigOptionInt(1)); + print_config->set_key_value("sparse_infill_density", new ConfigOptionPercent(0)); + print_config->set_key_value("detect_thin_wall", new ConfigOptionBool(false)); + print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); + print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + print_config->set_key_value("precise_z_height", new ConfigOptionBool(false)); + model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum(btOuterOnly)); + model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); + model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + + // cut upper + auto obj_bb = model().objects[0]->bounding_box_exact(); + auto height = 5 * ((params.end - params.start) / params.step + 1); + if (height < obj_bb.size().z()) { + cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); + } + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::calib_input_shaping_freq(const Calib_Params& params) +{ + const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test"); + new_project(false, false, calib_input_shaping_name); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + if (params.mode != CalibMode::Calib_Input_shaping_freq) + return; + + add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc")); + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto gcode_flavor_option = printer_config->option>("gcode_flavor"); + + if (has_junction_deviation(printer_config)) { + printer_config->set_key_value("machine_max_junction_deviation", new ConfigOptionFloats {(std::max(printer_config->option("machine_max_junction_deviation")->values.front(), 0.25))}); + print_config->set_key_value("default_junction_deviation", new ConfigOptionFloat(0)); + } else { + const double jerk_value = (gcode_flavor_option && gcode_flavor_option->value == GCodeFlavor::gcfKlipper) ? 5.0 : 10.0; + printer_config->set_key_value("machine_max_jerk_x", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_x")->values.front(), jerk_value)}); + printer_config->set_key_value("machine_max_jerk_y", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_y")->values.front(), jerk_value)}); + print_config->set_key_value("default_jerk", new ConfigOptionFloat(0)); + } + + if (!filament_config->option("enable_pressure_advance")->get_at(0)) { + filament_config->set_key_value("enable_pressure_advance", new ConfigOptionBools {true}); + filament_config->set_key_value("pressure_advance", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("adaptive_pressure_advance", new ConfigOptionBools{false}); + } + + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); + print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2)); + print_config->set_key_value("enable_overhang_speed", new ConfigOptionBool{false}); + print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); + print_config->set_key_value("wall_loops", new ConfigOptionInt(1)); + print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0)); + print_config->set_key_value("bottom_shell_layers", new ConfigOptionInt(1)); + print_config->set_key_value("sparse_infill_density", new ConfigOptionPercent(0)); + print_config->set_key_value("detect_thin_wall", new ConfigOptionBool(false)); + print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); + print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); + print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); + print_config->set_key_value("outer_wall_speed", new ConfigOptionFloat(200)); + print_config->set_key_value("default_acceleration", new ConfigOptionFloat(20000)); + print_config->set_key_value("outer_wall_acceleration", new ConfigOptionFloat(20000)); + print_config->set_key_value("precise_z_height", new ConfigOptionBool(false)); + model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum(btOuterOnly)); + model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); + model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::calib_input_shaping_damp(const Calib_Params& params) +{ + const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test"); + new_project(false, false, calib_input_shaping_name); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + if (params.mode != CalibMode::Calib_Input_shaping_damp) + return; + + add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc")); + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto gcode_flavor_option = printer_config->option>("gcode_flavor"); + + if (has_junction_deviation(printer_config)) { + printer_config->set_key_value("machine_max_junction_deviation", new ConfigOptionFloats {(std::max(printer_config->option("machine_max_junction_deviation")->values.front(), 0.25))}); + print_config->set_key_value("default_junction_deviation", new ConfigOptionFloat(0)); + } else { + const double jerk_value = (gcode_flavor_option && gcode_flavor_option->value == GCodeFlavor::gcfKlipper) ? 5.0 : 10.0; + printer_config->set_key_value("machine_max_jerk_x", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_x")->values.front(), jerk_value)}); + printer_config->set_key_value("machine_max_jerk_y", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_y")->values.front(), jerk_value)}); + print_config->set_key_value("default_jerk", new ConfigOptionFloat(0)); + } + + if (!filament_config->option("enable_pressure_advance")->get_at(0)) { + filament_config->set_key_value("enable_pressure_advance", new ConfigOptionBools {true}); + filament_config->set_key_value("pressure_advance", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("adaptive_pressure_advance", new ConfigOptionBools{false}); + } + + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); + print_config->set_key_value("enable_overhang_speed", new ConfigOptionBool{false}); + print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); + print_config->set_key_value("wall_loops", new ConfigOptionInt(1)); + print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0)); + print_config->set_key_value("bottom_shell_layers", new ConfigOptionInt(1)); + print_config->set_key_value("sparse_infill_density", new ConfigOptionPercent(0)); + print_config->set_key_value("detect_thin_wall", new ConfigOptionBool(false)); + print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); + print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); + print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); + print_config->set_key_value("outer_wall_speed", new ConfigOptionFloat(200)); + print_config->set_key_value("default_acceleration", new ConfigOptionFloat(20000)); + print_config->set_key_value("outer_wall_acceleration", new ConfigOptionFloat(20000)); + print_config->set_key_value("precise_z_height", new ConfigOptionBool(false)); + model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum(btOuterOnly)); + model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); + model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::Calib_Cornering(const Calib_Params& params) +{ + const auto Calib_Cornering = wxString::Format(L"Cornering test"); + new_project(false, false, Calib_Cornering); + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + if (params.mode != CalibMode::Calib_Cornering) + return; + + const std::string cornering_model_path = params.test_model == 0 + ? "/calib/input_shaping/ringing_tower.drc" + : (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : "/calib/cornering/SCV-V2.drc"); + add_model(false, Slic3r::resources_dir() + cornering_model_path); + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + + if (has_junction_deviation(printer_config)) { + printer_config->set_key_value("machine_max_junction_deviation", new ConfigOptionFloats{params.end}); + print_config->set_key_value("default_junction_deviation", new ConfigOptionFloat(0.0)); + } else { + printer_config->set_key_value("machine_max_jerk_x", new ConfigOptionFloats{params.end}); + printer_config->set_key_value("machine_max_jerk_y", new ConfigOptionFloats{params.end}); + print_config->set_key_value("default_jerk", new ConfigOptionFloat(0)); + } + + if (!filament_config->option("enable_pressure_advance")->get_at(0)) { + filament_config->set_key_value("enable_pressure_advance", new ConfigOptionBools {true}); + filament_config->set_key_value("pressure_advance", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("adaptive_pressure_advance", new ConfigOptionBools{false}); + } + + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true}); + printer_config->set_key_value("input_shaping_type", new ConfigOptionEnum(InputShaperType::Disable)); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); + filament_config->set_key_value("filament_max_volumetric_speed", new ConfigOptionFloats{200}); + print_config->set_key_value("enable_overhang_speed", new ConfigOptionBool{false}); + print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); + print_config->set_key_value("wall_loops", new ConfigOptionInt(1)); + print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0)); + print_config->set_key_value("bottom_shell_layers", new ConfigOptionInt(1)); + print_config->set_key_value("sparse_infill_density", new ConfigOptionPercent(0)); + print_config->set_key_value("detect_thin_wall", new ConfigOptionBool(false)); + print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); + print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); + print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); + print_config->set_key_value("outer_wall_speed", new ConfigOptionFloat(200)); + print_config->set_key_value("default_acceleration", new ConfigOptionFloat(2000)); + print_config->set_key_value("outer_wall_acceleration", new ConfigOptionFloat(2000)); + print_config->set_key_value("precise_z_height", new ConfigOptionBool(false)); + model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum(btOuterOnly)); + model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); + model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + + p->background_process.fff_print()->set_calib_params(params); +} + +BuildVolume_Type Plater::get_build_volume_type() const { return p->bed.get_build_volume_type(); } + +void Plater::import_zip_archive() +{ + wxString input_file; + wxGetApp().import_zip(this, input_file); + if (input_file.empty()) + return; + + wxArrayString arr; + arr.Add(input_file); + load_files(arr); +} + +void Plater::import_sl1_archive() +{ + auto &w = get_ui_job_worker(); + if (w.is_idle() && p->m_sla_import_dlg->ShowModal() == wxID_OK) { + p->take_snapshot(_u8L("Import SLA archive")); + replace_job(w, std::make_unique(p->m_sla_import_dlg)); + } +} + +void Plater::extract_config_from_project() +{ + wxString input_file; + wxGetApp().load_project(this, input_file); + + if (! input_file.empty()) + load_files({ into_path(input_file) }, LoadStrategy::LoadConfig); +} + +void Plater::load_gcode() +{ + // Ask user for a gcode file name. + wxString input_file; + wxGetApp().load_gcode(this, input_file); + // And finally load the gcode file. + load_gcode(input_file); +} + +//BBS: remove GCodeViewer as seperate APP logic +void Plater::load_gcode(const wxString& filename) +{ + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " entry and filename: " << filename; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__; + if (! is_gcode_file(into_u8(filename)) + || (m_last_loaded_gcode == filename && m_only_gcode) + ) + return; + + m_last_loaded_gcode = filename; + + // BSS: create a new project when load_gcode, force close previous one + if (new_project(false, true) != wxID_YES) + return; + + m_only_gcode = true; + + // cleanup view before to start loading/processing + //BBS: update gcode to current partplate's + GCodeProcessorResult* current_result = p->partplate_list.get_current_slice_result(); + Print& current_print = p->partplate_list.get_current_fff_print(); + //BBS:already reset in new_project + //current_result->reset(); + //p->gcode_result.reset(); + //reset_gcode_toolpaths(); + p->preview->reload_print(m_only_gcode); + wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + p->set_current_panel(p->preview, true); + p->get_current_canvas3D()->render(); + //p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); + + wxBusyCursor wait; + + // process gcode + GCodeProcessor processor; + processor.init_filament_maps_and_nozzle_type_when_import_only_gcode(); + try + { + GCodeProcessor::s_IsBBLPrinter = wxGetApp().preset_bundle->is_bbl_vendor(); + processor.process_file(filename.ToUTF8().data()); + } + catch (const std::exception& ex) + { + show_error(this, ex.what()); + return; + } + *current_result = std::move(processor.extract_result()); + //current_result->filename = filename; + + BedType bed_type = current_result->bed_type; + if (bed_type != BedType::btCount) { + DynamicPrintConfig &proj_config = wxGetApp().preset_bundle->project_config; + proj_config.set_key_value("curr_bed_type", new ConfigOptionEnum(bed_type)); + on_bed_type_change(bed_type); + } + + current_print.apply(this->model(), wxGetApp().preset_bundle->full_config()); + + current_print.apply_config_for_render(processor.export_config_for_render()); + + //BBS: add cost info when drag in gcode + auto& ps = current_result->print_statistics; + double total_cost = 0.0; + for (auto volume : ps.total_volumes_per_extruder) { + size_t extruder_id = volume.first; + double density = current_result->filament_densities.at(extruder_id); + double cost = current_result->filament_costs.at(extruder_id); + double weight = volume.second * density * 0.001; + total_cost += weight * cost * 0.001; + } + current_print.print_statistics().total_cost = total_cost; + + current_print.set_gcode_file_ready(); + + // show results + p->preview->reload_print(m_only_gcode); + //BBS: zoom to bed 0 for gcode preview + //p->preview->get_canvas3d()->zoom_to_gcode(); + p->preview->get_canvas3d()->zoom_to_plate(0); + + if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) { + MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("does not contain valid G-code."), + wxString(GCODEVIEWER_APP_NAME) + " - " + _L("Error occurs while loading G-code file"), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal(); + set_project_filename(DEFAULT_PROJECT_NAME); + } else { + set_project_filename(filename); + } + + // Orca: Fix crash when loading gcode file multiple times + if (m_only_gcode) { + p->view3D->get_canvas3d()->remove_raycasters_for_picking(SceneRaycaster::EType::Bed); + } + + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false, false); //20250416 ban gcode to send print +} + +void Plater::reload_gcode_from_disk() +{ + wxString filename(m_last_loaded_gcode); + m_last_loaded_gcode.clear(); + load_gcode(filename); +} + +void Plater::reload_print() +{ + p->preview->reload_print(); +} + +// BBS +wxString Plater::get_project_name() +{ + return p->get_project_name(); +} + +void Plater::update_all_plate_thumbnails(bool force_update) +{ + for (int i = 0; i < get_partplate_list().get_plate_count(); i++) { + PartPlate* plate = get_partplate_list().get_plate(i); + ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i}; + if (force_update || !plate->thumbnail_data.is_valid()) { + get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho); + } + if (force_update || !plate->no_light_thumbnail_data.is_valid()) { + get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, + Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true); + } + } +} + +void Plater::update_obj_preview_thumbnail(ModelObject *mo, int obj_idx, int vol_idx, std::vector colors, int camera_view_angle_type) +{ + PartPlate * plate = get_partplate_list().get_plate(0); + ThumbnailsParams thumbnail_params = {{}, false, true, true, true, 0, false}; + GLVolumeCollection cur_volumes; + cur_volumes.load_object_volume(mo, obj_idx, vol_idx, 0, "volume", true, false, false, false); + ModelObjectPtrs model_objects; + model_objects.emplace_back(mo); + get_view3D_canvas3D()->render_thumbnail(plate->obj_preview_thumbnail_data, colors, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, + model_objects, cur_volumes, Camera::EType::Ortho, (Camera::ViewAngleType) camera_view_angle_type, false, false); +} + +//invalid all plate's thumbnails +void Plater::invalid_all_plate_thumbnails() +{ + if (using_exported_file() || skip_thumbnail_invalid) + return; + BOOST_LOG_TRIVIAL(info) << "thumb: invalid all"; + for (int i = 0; i < get_partplate_list().get_plate_count(); i++) { + PartPlate* plate = get_partplate_list().get_plate(i); + plate->thumbnail_data.reset(); + plate->no_light_thumbnail_data.reset(); + } +} + +void Plater::force_update_all_plate_thumbnails() +{ + if (using_exported_file() || skip_thumbnail_invalid) { + } + else { + invalid_all_plate_thumbnails(); + update_all_plate_thumbnails(true); + } + get_preview_canvas3D()->update_plate_thumbnails(); +} + +// BBS: backup +std::vector Plater::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi) { + //BBS: wish to reset state when load a new file + p->m_slice_all_only_has_gcode = false; + //BBS: wish to reset all plates stats item selected state when load a new file + p->preview->get_canvas3d()->reset_select_plate_toolbar_selection(); + return p->load_files(input_files, strategy, ask_multi); +} + +// To be called when providing a list of files to the GUI slic3r on command line. +std::vector Plater::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi) +{ + std::vector paths; + paths.reserve(input_files.size()); + for (const std::string& path : input_files) + paths.emplace_back(path); + return p->load_files(paths, strategy, ask_multi); +} + +bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) +{ + //std::vector unzipped_paths; + std::vector non_project_paths; + std::vector project_paths; + try + { + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, archive_path.string())) { + // TRN %1% is archive path + std::string err_msg = GUI::format(_u8L("Loading of a ZIP archive on path %1% has failed."), archive_path.string()); + throw Slic3r::FileIOError(err_msg); + } + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + mz_zip_archive_file_stat stat; + // selected_paths contains paths and its uncompressed size. The size is used to distinguish between files with same path. + std::vector> selected_paths; + FileArchiveDialog dlg(static_cast(wxGetApp().mainframe), &archive, selected_paths); + if (dlg.ShowModal() == wxID_OK) + { + std::string archive_path_string = archive_path.string(); + archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4); + fs::path archive_dir(wxStandardPaths::Get().GetTempDir().utf8_str().data()); + + for (auto& path_w_size : selected_paths) { + const fs::path& path = path_w_size.first; + size_t size = path_w_size.second; + // find path in zip archive + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + if (size != stat.m_uncomp_size) // size must fit + continue; + wxString wname = boost::nowide::widen(stat.m_filename); + std::string name = into_u8(wname); + fs::path archive_path(name); + + std::string extra(1024, 0); + size_t extra_size = mz_zip_reader_get_filename_from_extra(&archive, i, extra.data(), extra.size()); + if (extra_size > 0) { + archive_path = fs::path(extra.substr(0, extra_size)); + name = archive_path.string(); + } + + if (archive_path.empty()) + continue; + if (path != archive_path) + continue; + // decompressing + try + { + std::replace(name.begin(), name.end(), '\\', '/'); + // rename if file exists + std::string filename = path.filename().string(); + std::string extension = path.extension().string(); + std::string just_filename = filename.substr(0, filename.size() - extension.size()); + std::string final_filename = just_filename; + + size_t version = 0; + while (fs::exists(archive_dir / (final_filename + extension))) + { + ++version; + final_filename = just_filename + "(" + std::to_string(version) + ")"; + } + filename = final_filename + extension; + fs::path final_path = archive_dir / filename; + std::string buffer((size_t)stat.m_uncomp_size, 0); + // Decompress action. We already has correct file index in stat structure. + mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + // TRN: First argument = path to file, second argument = error description + wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2%"), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive))); + BOOST_LOG_TRIVIAL(error) << error_log; + show_error(nullptr, error_log); + break; + } + // write buffer to file + fs::fstream file(final_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(buffer.c_str(), buffer.size()); + file.close(); + if (!fs::exists(final_path)) { + wxString error_log = GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), final_path.string()); + BOOST_LOG_TRIVIAL(error) << error_log; + show_error(nullptr, error_log); + break; + } + BOOST_LOG_TRIVIAL(info) << "Unzipped " << final_path; + if (!boost::algorithm::iends_with(filename, ".3mf") && !boost::algorithm::iends_with(filename, ".amf")) { + non_project_paths.emplace_back(final_path); + break; + } + // if 3mf - read archive headers to find project file + if (/*(boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(final_path.string())) ||*/ + (boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) { + non_project_paths.emplace_back(final_path); + break; + } + + project_paths.emplace_back(final_path); + break; + } + catch (const std::exception& e) + { + // ensure the zip archive is closed and rethrow the exception + close_zip_reader(&archive); + throw Slic3r::FileIOError(e.what()); + } + } + } + } + close_zip_reader(&archive); + if (non_project_paths.size() + project_paths.size() != selected_paths.size()) + BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: " + << selected_paths.size() + << " Decopressed files: " + << non_project_paths.size() + project_paths.size(); + } else { + close_zip_reader(&archive); + return false; + } + + } + catch (const Slic3r::FileIOError& e) { + // zip reader should be already closed or not even opened + GUI::show_error(this, e.what()); + return false; + } + // none selected + if (project_paths.empty() && non_project_paths.empty()) + { + return false; + } + + // 1 project file and some models - behave like drag n drop of 3mf and then load models + if (project_paths.size() == 1) + { + wxArrayString aux; + aux.Add(from_u8(project_paths.front().string())); + bool loaded3mf = load_files(aux); + load_files(non_project_paths, LoadStrategy::LoadModel); + boost::system::error_code ec; + if (loaded3mf) { + fs::remove(project_paths.front(), ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + for (const fs::path& path : non_project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + return true; + } + + // load all projects and all models as geometry + load_files(project_paths, LoadStrategy::LoadModel); + load_files(non_project_paths, LoadStrategy::LoadModel); + + + for (const fs::path& path : project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + for (const fs::path& path : non_project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + + return true; +} + +#define PROJECT_DROP_DIALOG_SELECT_PLANE_SIZE wxSize(FromDIP(350), FromDIP(120)) + +class ProjectDropDialog : public DPIDialog +{ +private: + wxColour m_def_color = wxColour(255, 255, 255); + int m_action{1}; + bool m_remember_choice{false}; + +public: + ProjectDropDialog(const std::string &filename); + + wxPanel * m_top_line; + wxStaticText *m_fname_title; + wxStaticText *m_fname_f; + StaticBox * m_panel_select; + + void on_select_ok(wxCommandEvent &event); + void on_select_cancel(wxCommandEvent &event); + + int get_action() const { return m_action; } + void set_action(int index) { m_action = index; } + + wxBoxSizer *create_remember_checkbox(wxString title, wxWindow* parent, wxString tooltip); + +protected: + void on_dpi_changed(const wxRect &suggested_rect) override; +}; + +ProjectDropDialog::ProjectDropDialog(const std::string &filename) + : DPIDialog(static_cast(wxGetApp().mainframe), + wxID_ANY, + from_u8((boost::format(_utf8(L("Drop project file")))).str()), + wxDefaultPosition, + wxDefaultSize, + wxCAPTION | wxCLOSE_BOX) + , m_action(2) +{ + // def setting + SetBackgroundColour(m_def_color); + + wxBoxSizer *m_sizer_main = new wxBoxSizer(wxVERTICAL); + + m_top_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + m_top_line->SetBackgroundColour(wxColour(166, 169, 170)); + + m_sizer_main->Add(m_top_line, 0, wxEXPAND, 0); + + m_sizer_main->AddSpacer(FromDIP(15)); + + // ORCA use file name on new line to create room for longer names + m_fname_title = new wxStaticText(this, wxID_ANY, _L("Please select an action"), wxDefaultPosition, wxDefaultSize, 0); + m_fname_title->SetFont(::Label::Body_14); + m_fname_title->SetForegroundColour(wxColour("#363636")); + + m_fname_f = new wxStaticText(this, wxID_ANY, filename); + m_fname_f->SetFont(::Label::Head_14); + m_fname_f->SetMaxSize(wxSize(FromDIP(300),-1)); + m_fname_f->Wrap(FromDIP(300)); + m_fname_f->SetForegroundColour(wxColour("#363636")); + + m_sizer_main->Add(m_fname_title, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); + m_sizer_main->AddSpacer(FromDIP(10)); + m_sizer_main->Add(m_fname_f , 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); + m_sizer_main->AddSpacer(FromDIP(10)); + + auto radio_group = new RadioGroup(this, { + _L("Open as project"), // 0 + _L("Import geometry only") // 1 + }, wxVERTICAL); + radio_group->SetMinSize(wxSize(FromDIP(300),-1)); + radio_group->SetSelection(get_action() - 1); + radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, [this, radio_group](wxCommandEvent &e) { + set_action(radio_group->GetSelection() + 1); + }); + + m_sizer_main->Add(radio_group, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); + + m_sizer_main->AddSpacer(FromDIP(10)); + + // wxBoxSizer *m_sizer_bottom = new wxBoxSizer(wxHORIZONTAL); + // Orca: hide the "Don't show again" checkbox, people keeps accidentally checked this then forgot + // wxBoxSizer *m_sizer_left = new wxBoxSizer(wxHORIZONTAL); + // + // auto dont_show_again = create_remember_checkbox(_L("Remember my choice."), this, _L("This option can be changed later in preferences, under 'Load Behaviour'.")); + // m_sizer_left->Add(dont_show_again, 0, wxALL, 5); + // + // m_sizer_bottom->Add(m_sizer_left, 0, wxEXPAND, 5); + + auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); + + dlg_btns->GetOK()->Bind(wxEVT_BUTTON, &ProjectDropDialog::on_select_ok, this); + + dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, &ProjectDropDialog::on_select_cancel, this); + + m_sizer_main->Add(dlg_btns, 0, wxEXPAND); + + SetSizer(m_sizer_main); + Layout(); + Fit(); + Centre(wxBOTH); + + wxGetApp().UpdateDlgDarkUI(this); +} + +wxBoxSizer *ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow *parent, wxString tooltip) +{ + wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); + m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 5); + + auto checkbox = new ::CheckBox(parent); + checkbox->SetValue(m_remember_choice); + checkbox->SetToolTip(tooltip); + m_sizer_checkbox->Add(checkbox, 0, wxALIGN_CENTER, 0); + m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 8); + + auto checkbox_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxSize(-1, -1), 0); + checkbox_title->SetForegroundColour(wxColour(144,144,144)); + checkbox_title->SetFont(::Label::Body_13); + checkbox_title->Wrap(-1); + checkbox_title->SetToolTip(tooltip); + m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxALL, 3); + + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox](wxCommandEvent &e) { + m_remember_choice = checkbox->GetValue(); + e.Skip(); + }); + + return m_sizer_checkbox; +} + +void ProjectDropDialog::on_select_ok(wxCommandEvent &event) +{ + if (m_remember_choice) { + LoadType load_type = static_cast(get_action()); + switch (load_type) + { + case LoadType::OpenProject: + wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL); + break; + case LoadType::LoadGeometry: + wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY); + break; + } + } + + EndModal(wxID_OK); +} + +void ProjectDropDialog::on_select_cancel(wxCommandEvent &event) +{ + EndModal(wxID_CANCEL); +} + +void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + Fit(); + Refresh(); +} + +//BBS: remove GCodeViewer as seperate APP logic +bool Plater::load_files(const wxArrayString& filenames) +{ + const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip|drc)", std::regex::icase); + const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase); + + std::vector normal_paths; + std::vector gcode_paths; + + for (const auto& filename : filenames) { + fs::path path(into_path(filename)); + if (std::regex_match(path.string(), pattern_drop)) + normal_paths.push_back(std::move(path)); + else if (std::regex_match(path.string(), pattern_gcode_drop)) + gcode_paths.push_back(std::move(path)); + else + continue; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": normal_paths %1%, gcode_paths %2%")%normal_paths.size() %gcode_paths.size(); + if (normal_paths.empty() && gcode_paths.empty()) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": can not find valid path, return directly"); + // Likely no supported files + return false; + } + else if (normal_paths.empty()){ + //only gcode files + if (gcode_paths.size() > 1) { + show_info(this, _L("Only one G-code file can be opened at the same time."), _L("G-code loading")); + return false; + } + load_gcode(from_path(gcode_paths.front())); + return true; + } + + if (!gcode_paths.empty()) { + show_info(this, _L("G-code files cannot be loaded with models together!"), _L("G-code loading")); + return false; + } + + //// searches for project files + //for (std::vector::const_reverse_iterator it = normal_paths.rbegin(); it != normal_paths.rend(); ++it) { + // std::string filename = (*it).filename().string(); + // ////BBS: only 3mf will be treated as project file + // if (open_3mf_file((*it))) + // return true; + //} + + //// other files + std::string snapshot_label; + assert(!normal_paths.empty()); + if (normal_paths.size() == 1) { + snapshot_label = "Load File"; + snapshot_label += ": "; + snapshot_label += encode_path(normal_paths.front().filename().string().c_str()); + } else { + snapshot_label = "Load Files"; + snapshot_label += ": "; + snapshot_label += encode_path(normal_paths.front().filename().string().c_str()); + for (size_t i = 1; i < normal_paths.size(); ++i) { + snapshot_label += ", "; + snapshot_label += encode_path(normal_paths[i].filename().string().c_str()); + } + } + + //Plater::TakeSnapshot snapshot(this, snapshot_label); + //load_files(normal_paths, LoadStrategy::LoadModel); + + // BBS: check file types + std::sort(normal_paths.begin(), normal_paths.end(), [](fs::path obj1, fs::path obj2) { return obj1.filename().string() < obj2.filename().string(); }); + + auto loadfiles_type = LoadFilesType::NoFile; + auto amf_files_count = get_3mf_file_count(normal_paths); + + if (normal_paths.size() > 1 && amf_files_count < normal_paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } + if (normal_paths.size() > 1 && amf_files_count == normal_paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } + if (normal_paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } + if (normal_paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; + if (normal_paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; + + auto first_file = std::vector{}; + auto tmf_file = std::vector{}; + auto other_file = std::vector{}; + auto res = true; + + if (this->m_only_gcode || this->m_exported_file) { + if ((loadfiles_type == LoadFilesType::SingleOther) + || (loadfiles_type == LoadFilesType::MultipleOther)) { + show_info(this, _L("Cannot add models when in preview mode!"), _L("Add Models")); + return false; + } + } + + // Orca: Iters through given paths and imports files from zip then remove zip from paths + // returns true if zip files were found + auto handle_zips = [this](vector& paths) { // NOLINT(*-no-recursion) - Recursion is intended and should be managed properly + bool res = false; + for (auto it = paths.begin(); it != paths.end();) { + if (boost::algorithm::iends_with(it->string(), ".zip")) { + res = true; + preview_zip_archive(*it); + it = paths.erase(it); + } else + it++; + } + return res; + }; + + switch (loadfiles_type) { + case LoadFilesType::Single3MF: + open_3mf_file(normal_paths[0]); + break; + + case LoadFilesType::SingleOther: { + Plater::TakeSnapshot snapshot(this, snapshot_label); + if (handle_zips(normal_paths)) return true; + if (load_files(normal_paths, LoadStrategy::LoadModel, false).empty()) { res = false; } + break; + } + case LoadFilesType::Multiple3MF: + first_file = std::vector{normal_paths[0]}; + for (auto i = 0; i < normal_paths.size(); i++) { + if (i > 0) { other_file.push_back(normal_paths[i]); } + }; + + open_3mf_file(first_file[0]); + if (load_files(other_file, LoadStrategy::LoadModel).empty()) { res = false; } + break; + + case LoadFilesType::MultipleOther: { + Plater::TakeSnapshot snapshot(this, snapshot_label); + if (handle_zips(normal_paths)) { + if (normal_paths.empty()) return true; + } + if (load_files(normal_paths, LoadStrategy::LoadModel, true).empty()) { res = false; } + break; + } + + case LoadFilesType::Multiple3MFOther: + for (const auto &path : normal_paths) { + if (boost::iends_with(path.filename().string(), ".3mf")){ + if (first_file.size() <= 0) + first_file.push_back(path); + else + tmf_file.push_back(path); + } else { + other_file.push_back(path); + } + } + + open_3mf_file(first_file[0]); + if (load_files(tmf_file, LoadStrategy::LoadModel).empty()) { res = false; } + if (res && handle_zips(other_file)) { + if (normal_paths.empty()) return true; + } + if (load_files(other_file, LoadStrategy::LoadModel, false).empty()) { res = false; } + break; + default: break; + } + + return res; +} + +LoadType determine_load_type(std::string filename, std::string override_setting) +{ + std::string setting; + + if (override_setting != "") { + setting = override_setting; + } else { + setting = wxGetApp().app_config->get(SETTING_PROJECT_LOAD_BEHAVIOUR); + } + + if (setting == OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY) { + return LoadType::LoadGeometry; + } else if (setting == OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK) { + ProjectDropDialog dlg(filename); + if (dlg.ShowModal() == wxID_OK) { + int choice = dlg.get_action(); + LoadType load_type = static_cast(choice); + wxGetApp().app_config->set("import_project_action", std::to_string(choice)); + + // BBS: jump to plater panel + wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + return load_type; + } + + return LoadType::Unknown; // Cancel + } else { + return LoadType::OpenProject; + } +} + +bool Plater::open_3mf_file(const fs::path &file_path) +{ + std::string filename = encode_path(file_path.filename().string().c_str()); + if (!boost::algorithm::iends_with(filename, ".3mf")) { + return false; + } + + bool not_empty_plate = !model().objects.empty(); + bool load_setting_ask_when_relevant = wxGetApp().app_config->get(SETTING_PROJECT_LOAD_BEHAVIOUR) == OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT; + LoadType load_type = determine_load_type(filename, (not_empty_plate && load_setting_ask_when_relevant) ? OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK : ""); + + if (load_type == LoadType::Unknown) return false; + + switch (load_type) { + case LoadType::OpenProject: { + if (wxGetApp().can_load_project()) + load_project(from_path(file_path), ""); + break; + } + case LoadType::LoadGeometry: { + Plater::TakeSnapshot snapshot(this, "Import Object"); + load_files({file_path}, LoadStrategy::LoadModel); + break; + } + case LoadType::LoadConfig: { + load_files({file_path}, LoadStrategy::LoadConfig); + break; + } + case LoadType::Unknown: { + assert(false); + break; + } + } + + return true; +} + +int Plater::get_3mf_file_count(std::vector paths) +{ + auto count = 0; + for (const auto &path : paths) { + if (boost::iends_with(path.filename().string(), ".3mf")) { + count++; + } + } + return count; +} + +void Plater::add_file() +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " entry"; + wxArrayString input_files; + wxGetApp().import_model(this, input_files); + if (input_files.empty()) return; + + std::vector paths; + for (const auto &file : input_files) paths.emplace_back(into_path(file)); + + std::string snapshot_label; + assert(!paths.empty()); + + snapshot_label = "Import Objects"; + snapshot_label += ": "; + snapshot_label += encode_path(paths.front().filename().string().c_str()); + for (size_t i = 1; i < paths.size(); ++i) { + snapshot_label += ", "; + snapshot_label += encode_path(paths[i].filename().string().c_str()); + } + + // BBS: check file types + auto loadfiles_type = LoadFilesType::NoFile; + auto amf_files_count = get_3mf_file_count(paths); + + if (paths.size() > 1 && amf_files_count < paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } + if (paths.size() > 1 && amf_files_count == paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } + if (paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } + if (paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; + if (paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; + + auto first_file = std::vector{}; + auto tmf_file = std::vector{}; + auto other_file = std::vector{}; + + switch (loadfiles_type) + { + case LoadFilesType::Single3MF: + open_3mf_file(paths[0]); + break; + + case LoadFilesType::SingleOther: { + Plater::TakeSnapshot snapshot(this, snapshot_label); + if (!load_files(paths, LoadStrategy::LoadModel, false).empty()) { + if (get_project_name() == _L("Untitled") && paths.size() > 0) { + boost::filesystem::path full_path(paths[0].string()); + p->set_project_name(from_u8(full_path.stem().string())); + } + wxGetApp().mainframe->update_title(); + if (wxGetApp().app_config->get("recent_models") == "true") + wxGetApp().mainframe->add_to_recent_projects(paths[0].wstring()); + } + break; + } + case LoadFilesType::Multiple3MF: + first_file = std::vector{paths[0]}; + for (auto i = 0; i < paths.size(); i++) { + if (i > 0) { other_file.push_back(paths[i]); } + }; + + open_3mf_file(first_file[0]); + if (!load_files(other_file, LoadStrategy::LoadModel).empty()) { wxGetApp().mainframe->update_title(); } + break; + + case LoadFilesType::MultipleOther: { + Plater::TakeSnapshot snapshot(this, snapshot_label); + if (!load_files(paths, LoadStrategy::LoadModel, true).empty()) { + if (get_project_name() == _L("Untitled") && paths.size() > 0) { + boost::filesystem::path full_path(paths[0].string()); + p->set_project_name(from_u8(full_path.stem().string())); + } + wxGetApp().mainframe->update_title(); + if (wxGetApp().app_config->get("recent_models") == "true") + for (auto &path : paths) + wxGetApp().mainframe->add_to_recent_projects(path.wstring()); + } + break; + } + case LoadFilesType::Multiple3MFOther: + for (const auto &path : paths) { + if (boost::iends_with(path.filename().string(), ".3mf")) { + if (first_file.size() <= 0) + first_file.push_back(path); + else + tmf_file.push_back(path); + } else { + other_file.push_back(path); + } + } + + open_3mf_file(first_file[0]); + load_files(tmf_file, LoadStrategy::LoadModel); + if (!load_files(other_file, LoadStrategy::LoadModel, false).empty()) { + wxGetApp().mainframe->update_title(); + if (wxGetApp().app_config->get("recent_models") == "true") + for (auto &file : other_file) + wxGetApp().mainframe->add_to_recent_projects(file.wstring()); + } + break; + default:break; + } +} + +void Plater::update(bool conside_update_flag, bool force_background_processing_update) +{ + if (is_new_project_and_check_state()) { + return; + } + unsigned int flag = force_background_processing_update ? (unsigned int)Plater::priv::UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE : 0; + if (conside_update_flag) { + if (need_update()) { + p->update(flag); + p->set_need_update(false); + } + } + else + p->update(flag); +} + +void Plater::object_list_changed() { p->object_list_changed(); } + +Worker &Plater::get_ui_job_worker() { return p->m_worker; } + +const Worker &Plater::get_ui_job_worker() const { return p->m_worker; } + +void Plater::update_ui_from_settings() { p->update_ui_from_settings(); } + +void Plater::select_view(const std::string& direction) { p->select_view(direction); } + +//BBS: add no_slice logic +void Plater::select_view_3D(const std::string& name, bool no_slice) { p->select_view_3D(name, no_slice); } + +void Plater::reload_paint_after_background_process_apply() { + p->preview->set_reload_paint_after_background_process_apply(true); +} + +bool Plater::is_preview_shown() const { return p->is_preview_shown(); } +bool Plater::is_preview_loaded() const { return p->is_preview_loaded(); } +bool Plater::is_view3D_shown() const { return p->is_view3D_shown(); } + +bool Plater::are_view3D_labels_shown() const { return p->are_view3D_labels_shown(); } +void Plater::show_view3D_labels(bool show) { p->show_view3D_labels(show); } + +bool Plater::is_view3D_overhang_shown() const { return p->is_view3D_overhang_shown(); } +void Plater::show_view3D_overhang(bool show) { p->show_view3D_overhang(show); } + +bool Plater::is_sidebar_enabled() const { return p->sidebar_layout.is_enabled; } +void Plater::enable_sidebar(bool enabled) { p->enable_sidebar(enabled); } +bool Plater::is_sidebar_collapsed() const { return p->sidebar_layout.is_collapsed; } +void Plater::collapse_sidebar(bool collapse) { p->collapse_sidebar(collapse); } +Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_sidebar_docking_state(); } + +void Plater::reset_window_layout() { p->reset_window_layout(); } + +//BBS +void Plater::select_curr_plate_all() { p->select_curr_plate_all(); } +void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); } + +void Plater::select_all() { p->select_all(); } +void Plater::deselect_all() { p->deselect_all(); } +void Plater::exit_gizmo() { p->exit_gizmo(); } + +void Plater::remove(size_t obj_idx) { p->remove(obj_idx); } +void Plater::reset(bool apply_presets_change) { p->reset(apply_presets_change); } +void Plater::reset_with_confirm() +{ + if (p->model.objects.empty() || MessageDialog(static_cast(this), _L("All objects will be removed, continue?"), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Delete all"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE) + .ShowModal() == wxID_YES) { + reset(); + // BBS: jump to plater panel + wxGetApp().mainframe->select_tab(size_t(0)); + } +} + +// BBS: save logic +int GUI::Plater::close_with_confirm(std::function second_check) +{ + if (up_to_date(false, false)) { + if (second_check && !second_check(false)) return wxID_CANCEL; + model().set_backup_path(""); + return wxID_NO; + } + + MessageDialog dlg(static_cast(this), _L("The current project has unsaved changes, save it before continue?"), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Save"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE); + dlg.show_dsa_button(_L("Remember my choice.")); + auto choise = wxGetApp().app_config->get("save_project_choise"); + auto result = choise.empty() ? dlg.ShowModal() : choise == "yes" ? wxID_YES : wxID_NO; + if (result == wxID_CANCEL) + return result; + else { + if (dlg.get_checkbox_state()) + wxGetApp().app_config->set("save_project_choise", result == wxID_YES ? "yes" : "no"); + if (result == wxID_YES) { + result = save_project(); + if (result == wxID_CANCEL) { + if (choise.empty()) + return result; + else + result = wxID_NO; + } + } + } + + if (second_check && !second_check(result == wxID_YES)) return wxID_CANCEL; + + model().set_backup_path(""); + up_to_date(true, false); + up_to_date(true, true); + + return result; +} + +//BBS: trigger a restore project event +void Plater::trigger_restore_project(int skip_confirm) +{ + auto evt = new wxCommandEvent(EVT_RESTORE_PROJECT, this->GetId()); + evt->SetInt(skip_confirm); + wxQueueEvent(this, evt); + //wxPostEvent(this, *evt); +} + +//BBS +bool Plater::delete_object_from_model(size_t obj_idx, bool refresh_immediately) { return p->delete_object_from_model(obj_idx, refresh_immediately); } + +//BBS: delete all from model +void Plater::delete_all_objects_from_model() +{ + p->delete_all_objects_from_model(); +} + +void Plater::set_selected_visible(bool visible) +{ + if (p->get_curr_selection().is_empty()) + return; + + Plater::TakeSnapshot snapshot(this, "Set Selected Objects Visible in AssembleView"); + get_ui_job_worker().cancel_all(); + + p->get_current_canvas3D()->set_selected_visible(visible); +} + + +void Plater::remove_selected() +{ + /*if (p->get_selection().is_empty()) + return;*/ + if (p->get_curr_selection().is_empty()) + return; + + // BBS: check before deleting object + if (!p->can_delete()) + return; + + Plater::TakeSnapshot snapshot(this, "Delete Selected Objects"); + get_ui_job_worker().cancel_all(); + + //BBS delete current selected + // p->view3D->delete_selected(); + p->get_current_canvas3D()->delete_selected(); +} + +void Plater::increase_instances(size_t num) +{ + if (! can_increase_instances()) { return; } + + Plater::TakeSnapshot snapshot(this, "Increase Instances"); + + int obj_idx = p->get_selected_object_idx(); + + ModelObject* model_object = p->model.objects[obj_idx]; + ModelInstance* model_instance = model_object->instances.back(); + + bool was_one_instance = model_object->instances.size()==1; + + double offset_base = canvas3D()->get_size_proportional_to_max_bed_size(0.05); + double offset = offset_base; + for (size_t i = 0; i < num; i++, offset += offset_base) { + Vec3d offset_vec = model_instance->get_offset() + Vec3d(offset, offset, 0.0); + model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), model_instance->get_mirror()); +// p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec)); + } + +#ifdef SUPPORT_AUTO_CENTER + if (p->get_config("autocenter") == "true") + arrange(); +#endif + + p->update(); + + p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1); + + sidebar().obj_list()->increase_object_instances(obj_idx, was_one_instance ? num + 1 : num); + + p->selection_changed(); + this->p->schedule_background_process(); + //if (wxGetApp().app_config->get("auto_arrange") == "true") { + // this->set_prepare_state(Job::PREPARE_STATE_MENU); + // this->arrange(); + //} +} + +void Plater::decrease_instances(size_t num) +{ + if (! can_decrease_instances()) { return; } + + Plater::TakeSnapshot snapshot(this, "Decrease Instances"); + + int obj_idx = p->get_selected_object_idx(); + + ModelObject* model_object = p->model.objects[obj_idx]; + if (model_object->instances.size() > num) { + for (size_t i = 0; i < num; ++ i) + model_object->delete_last_instance(); + p->update(); + // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. + sidebar().obj_list()->decrease_object_instances(obj_idx, num); + } + else { + remove(obj_idx); + } + + if (!model_object->instances.empty()) + p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1); + + p->selection_changed(); + this->p->schedule_background_process(); + //if (wxGetApp().app_config->get("auto_arrange") == "true") { + // this->set_prepare_state(Job::PREPARE_STATE_MENU); + // this->arrange(); + //} +} + +static long GetNumberFromUser( const wxString& msg, + const wxString& prompt, + const wxString& title, + long value, + long min, + long max, + wxWindow* parent) +{ +#ifdef _WIN32 + wxNumberEntryDialog dialog(parent, msg, prompt, title, value, min, max, wxDefaultPosition); + wxGetApp().UpdateDlgDarkUI(&dialog); + if (dialog.ShowModal() == wxID_OK) + return dialog.GetValue(); + + return -1; +#else + return wxGetNumberFromUser(msg, prompt, title, value, min, max, parent); +#endif +} + +void Plater::set_number_of_copies(/*size_t num*/) +{ + int obj_idx = p->get_selected_object_idx(); + if (obj_idx == -1) + return; + + ModelObject* model_object = p->model.objects[obj_idx]; + + const int num = GetNumberFromUser( " ", _L("Number of copies:"), + _L("Copies of the selected object"), model_object->instances.size(), 0, 1000, this ); + if (num < 0) + return; + + Plater::TakeSnapshot snapshot(this, (boost::format("Set numbers of copies to %1%")%num).str()); + + int diff = num - (int)model_object->instances.size(); + if (diff > 0) + increase_instances(diff); + else if (diff < 0) + decrease_instances(-diff); +} + +void Plater::fill_bed_with_copies() +{ + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Arrange")); + replace_job(w, std::make_unique()); + } +} + +void Plater::fill_bed_with_instances() +{ + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Arrange")); + replace_job(w, std::make_unique(true)); + } +} + +bool Plater::is_selection_empty() const +{ + return p->get_selection().is_empty() || p->get_selection().is_wipe_tower(); +} + +void Plater::scale_selection_to_fit_print_volume() +{ + p->scale_selection_to_fit_print_volume(); +} + +void Plater::convert_unit(ConversionType conv_type) +{ + std::vector obj_idxs, volume_idxs; + wxGetApp().obj_list()->get_selection_indexes(obj_idxs, volume_idxs); + if (obj_idxs.empty() && volume_idxs.empty()) + return; + + TakeSnapshot snapshot(this, conv_type == ConversionType::CONV_FROM_INCH ? "Convert from imperial units" : + conv_type == ConversionType::CONV_TO_INCH ? "Revert conversion from imperial units" : + conv_type == ConversionType::CONV_FROM_METER ? "Convert from meters" : "Revert conversion from meters"); + wxBusyCursor wait; + + ModelObjectPtrs objects; + std::reverse(obj_idxs.begin(), obj_idxs.end()); + for (int obj_idx : obj_idxs) { + ModelObject *object = p->model.objects[obj_idx]; + object->convert_units(objects, conv_type, volume_idxs); + remove(obj_idx); + } + std::reverse(objects.begin(), objects.end()); + p->load_model_objects(objects); + + Selection& selection = p->view3D->get_canvas3d()->get_selection(); + size_t last_obj_idx = p->model.objects.size() - 1; + + if (volume_idxs.empty()) { + for (size_t i = 0; i < objects.size(); ++i) + selection.add_object((unsigned int)(last_obj_idx - i), i == 0); + } + else { + for (int vol_idx : volume_idxs) + selection.add_volume(last_obj_idx, vol_idx, 0, false); + } +} + +void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& new_objects) +{ + model().delete_object(obj_idx); + sidebar().obj_list()->delete_object_from_list(obj_idx); + + // suppress to call selection update for Object List to avoid call of early Gizmos on/off update + p->load_model_objects(new_objects, false, false); + + // now process all updates of the 3d scene + update(); + // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), + // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call + for (size_t idx = 0; idx < p->model.objects.size(); idx++) + wxGetApp().obj_list()->update_info_items(idx); + + Selection& selection = p->get_selection(); + size_t last_id = p->model.objects.size() - 1; + for (size_t i = 0; i < new_objects.size(); ++i) + selection.add_object((unsigned int)(last_id - i), i == 0); + + // UIThreadWorker w; + // arrange(w, true); + // w.wait_for_idle(); +} + +void Plater::export_gcode(bool prefer_removable) +{ + if (p->model.objects.empty()) + return; + + //if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) + // return; + + if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) + return; + + // If possible, remove accents from accented latin characters. + // This function is useful for generating file names to be processed by legacy firmwares. + fs::path default_output_file; + try { + // Update the background processing, so that the placeholder parser will get the correct values for the ouput file template. + // Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed. + unsigned int state = this->p->update_restart_background_process(false, false); + if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) + return; + default_output_file = this->p->background_process.output_filepath_for_project( + into_path(this->p->get_project_filename(".3mf"))); + } catch (const Slic3r::PlaceholderParserError &ex) { + // Show the error with monospaced font. + show_error(this, ex.what(), true); + return; + } catch (const std::exception &ex) { + show_error(this, ex.what(), false); + return; + } + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + AppConfig &appconfig = *wxGetApp().app_config; + RemovableDriveManager &removable_drive_manager = *wxGetApp().removable_drive_manager(); + // Get a last save path, either to removable media or to an internal media. + std::string start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), prefer_removable); + if (prefer_removable) { + // Returns a path to a removable media if it exists, prefering start_dir. Update the internal removable drives database. + start_dir = removable_drive_manager.get_removable_drive_path(start_dir); + if (start_dir.empty()) + // Direct user to the last internal media. + start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), false); + } + + fs::path output_path; + { + std::string ext = default_output_file.extension().string(); + wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SLA file as:"), + start_dir, + from_path(default_output_file.filename()), + GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : FT_SL1, ext), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + if (dlg.ShowModal() == wxID_OK) { + output_path = into_path(dlg.GetPath()); + while (has_illegal_filename_characters(output_path.filename().string())) { + show_error(this, _L("The provided file name is not valid.") + "\n" + + _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""); + dlg.SetFilename(from_path(output_path.filename())); + if (dlg.ShowModal() == wxID_OK) + output_path = into_path(dlg.GetPath()); + else { + output_path.clear(); + break; + } + } + } + } + + if (! output_path.empty()) { + bool path_on_removable_media = removable_drive_manager.set_and_verify_last_save_path(output_path.string()); + //bool path_on_removable_media = false; + p->notification_manager->new_export_began(path_on_removable_media); + p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; + p->last_output_path = output_path.string(); + p->last_output_dir_path = output_path.parent_path().string(); + p->export_gcode(output_path, path_on_removable_media); + // Storing a path to AppConfig either as path to removable media or a path to internal media. + // is_path_on_removable_drive() is called with the "true" parameter to update its internal database as the user may have shuffled the external drives + // while the dialog was open. + appconfig.update_last_output_dir(output_path.parent_path().string(), path_on_removable_media); + + try { + json j; + auto printer_config = Slic3r::GUI::wxGetApp().preset_bundle->printers.get_edited_preset_with_vendor_profile().preset; + if (printer_config.is_system) { + j["printer_preset"] = printer_config.name; + } else { + j["printer_preset"] = printer_config.config.opt_string("inherits"); + } + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (preset_bundle) { + j["gcode_printer_model"] = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); + } + NetworkAgent *agent = wxGetApp().getAgent(); + } catch (...) {} + + } +} + +void Plater::send_to_printer(bool isall) +{ + p->on_action_send_to_printer(isall); +} + +//BBS export gcode 3mf to file +void Plater::export_gcode_3mf(bool export_all) +{ + if (p->model.objects.empty()) + return; + + if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) + return; + + //calc default_output_file, get default output file from background process + fs::path default_output_file; + AppConfig& appconfig = *wxGetApp().app_config; + std::string start_dir; + try { + // Update the background processing, so that the placeholder parser will get the correct values for the ouput file template. + // Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed. + unsigned int state = this->p->update_restart_background_process(false, false); + if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) + return; + default_output_file = this->p->background_process.output_filepath_for_project( + into_path(this->p->get_project_filename(".3mf"))); + } + catch (const Slic3r::PlaceholderParserError& ex) { + // Show the error with monospaced font. + show_error(this, ex.what(), true); + return; + } + catch (const std::exception& ex) { + show_error(this, ex.what(), false); + return; + } + default_output_file.replace_extension(".gcode.3mf"); + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + + //Get a last save path + start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), false); + + fs::path output_path; + { + std::string ext = default_output_file.extension().string(); + wxFileDialog dlg(this, _L("Save Sliced file as:"), + start_dir, + from_path(default_output_file.filename()), + GUI::file_wildcards(FT_GCODE_3MF, ""), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + if (dlg.ShowModal() == wxID_OK) { + output_path = into_path(dlg.GetPath()); + ext = output_path.extension().string(); + if (ext != ".3mf") + output_path = output_path.string() + ".3mf"; + } + } + + if (!output_path.empty()) { + //BBS do not set to removable media path + bool path_on_removable_media = false; + p->notification_manager->new_export_began(path_on_removable_media); + p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; + //BBS do not save last output path + p->last_output_path = output_path.string(); + p->last_output_dir_path = output_path.parent_path().string(); + int plate_idx = get_partplate_list().get_curr_plate_index(); + if (export_all) + plate_idx = PLATE_ALL_IDX; + export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, plate_idx); // BBS: silence + + RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager(); + + + bool on_removable = removable_drive_manager.is_path_on_removable_drive(p->last_output_dir_path); + + + // update last output dir + appconfig.update_last_output_dir(output_path.parent_path().string(), false); + p->notification_manager->push_exporting_finished_notification(output_path.string(), p->last_output_dir_path, on_removable); + } +} + +void Plater::send_gcode_finish(wxString name) +{ + auto out_str = GUI::format(_L("The file %s has been sent to the printer's storage space and can be viewed on the printer."), name); + p->notification_manager->push_exporting_finished_notification(out_str, "", false); +} + +void Plater::export_core_3mf() +{ + wxString path = p->get_export_file(FT_3MF); + if (path.empty()) { return; } + const std::string path_u8 = into_u8(path); + export_3mf(path_u8, SaveStrategy::Silence); +} + +Preset *get_printer_preset(const MachineObject *obj) +{ + if (!obj) + return nullptr; + + Preset *printer_preset = nullptr; + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + for (auto printer_it = preset_bundle->printers.begin(); printer_it != preset_bundle->printers.end(); printer_it++) { + // only use system printer preset + if (!printer_it->is_system) + continue; + + ConfigOption *printer_nozzle_opt = printer_it->config.option("nozzle_diameter"); + ConfigOptionFloats *printer_nozzle_vals = nullptr; + if (printer_nozzle_opt) printer_nozzle_vals = dynamic_cast(printer_nozzle_opt); + std::string model_id = printer_it->get_current_printer_type(preset_bundle); + + std::string printer_type = obj->get_show_printer_type(); + bool nozzle_diameter_matches_or_unknown = printer_nozzle_vals && obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(0, printer_nozzle_vals->get_at(0)); + if (model_id.compare(printer_type) == 0 && nozzle_diameter_matches_or_unknown) { + printer_preset = &(*printer_it); + } + } + return printer_preset; +} + +bool Plater::check_printer_initialized(MachineObject *obj, bool only_warning, bool popup_warning) +{ + if (!obj) + return false; + + bool has_been_initialized = true; + + const auto& extruders = obj->GetExtderSystem()->GetExtruders(); + for (const DevExtder& extruder : extruders) { + + // Skip check if nozzle type is unknown + if (extruder.GetNozzleType() == NozzleType::ntUndefine) { + continue; + } + + if (extruder.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) { + has_been_initialized = false; + break; + } + } + + if (!has_been_initialized) { + if (popup_warning) { + if (!only_warning) { + if (DevPrinterConfigUtil::get_printer_can_set_nozzle(obj->get_show_printer_type())) { + MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please set the nozzle and try again."), _L("Warning"), wxOK | wxICON_WARNING); + dlg.ShowModal(); + } else { + MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please check."), _L("Warning"), wxOK | wxICON_WARNING); + dlg.ShowModal(); + } + + PrinterPartsDialog *print_parts_dlg = new PrinterPartsDialog(nullptr); + print_parts_dlg->update_machine_obj(obj); + print_parts_dlg->ShowModal(); + } else { + auto printer_name = get_selected_printer_name_in_combox(); // wxString(obj->get_preset_printer_model_name(machine_print_name)) + pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, _L("Sync printer information")); + } + } + return false; + } + return true; +} + +// Following lambda generates a combined mesh for export with normals pointing outwards. +TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, std::function notify_func) +{ + TriangleMesh mesh; + + std::vector csgmesh; + csgmesh.reserve(2 * mo.volumes.size()); + bool has_splitable_volume = csg::model_to_csgmesh(mo, Transform3d::Identity(), std::back_inserter(csgmesh), + csg::mpartsPositive | csg::mpartsNegative); + + std::string fail_msg = _u8L("Unable to perform boolean operation on model meshes. " + "Only positive parts will be kept. You may fix the meshes and try again."); + if (auto fail_reason_name = csg::check_csgmesh_booleans(Range{ std::begin(csgmesh), std::end(csgmesh) }); std::get<0>(fail_reason_name) != csg::BooleanFailReason::OK) { + std::string name = std::get<1>(fail_reason_name); + std::map fail_reasons = { + {csg::BooleanFailReason::OK, "OK"}, + {csg::BooleanFailReason::MeshEmpty, Slic3r::format( _u8L("Reason: part \"%1%\" is empty."), name)}, + {csg::BooleanFailReason::NotBoundAVolume, Slic3r::format(_u8L("Reason: part \"%1%\" does not bound a volume."), name)}, + {csg::BooleanFailReason::SelfIntersect, Slic3r::format(_u8L("Reason: part \"%1%\" has self intersection."), name)}, + {csg::BooleanFailReason::NoIntersection, Slic3r::format(_u8L("Reason: \"%1%\" and another part have no intersection."), name)} }; + fail_msg += " " + fail_reasons[std::get<0>(fail_reason_name)]; + } + else { + try { + MeshBoolean::mcut::McutMeshPtr meshPtr = csg::perform_csgmesh_booleans_mcut(Range{ std::begin(csgmesh), std::end(csgmesh) }); + mesh = MeshBoolean::mcut::mcut_to_triangle_mesh(*meshPtr); + } + catch (...) {} +#if 0 + // if mcut fails, try again with CGAL + if (mesh.empty()) { + try { + auto meshPtr = csg::perform_csgmesh_booleans(Range{ std::begin(csgmesh), std::end(csgmesh) }); + mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*meshPtr); + } + catch (...) {} + } +#endif + } + + if (mesh.empty()) { + if (notify_func) + notify_func(fail_msg); + + for (const ModelVolume* v : mo.volumes) + if (v->is_model_part()) { + TriangleMesh vol_mesh(v->mesh()); + vol_mesh.transform(v->get_matrix(), true); + mesh.merge(vol_mesh); + } + } + + if (instance_id == -1) { + TriangleMesh vols_mesh(std::move(mesh)); + mesh = TriangleMesh(); + for (const ModelInstance* i : mo.instances) { + TriangleMesh m = vols_mesh; + m.transform(i->get_matrix(), true); + mesh.merge(m); + } + } + else if (0 <= instance_id && instance_id < int(mo.instances.size())) + mesh.transform(mo.instances[instance_id]->get_matrix(), true); + return mesh; +} + +// BBS export with/without boolean, however, stil merge mesh +#define EXPORT_WITH_BOOLEAN 0 +void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, FileType file_type) +{ + if (p->model.objects.empty()) { return; } + + int quality = 0; + + switch (file_type) { + case FT_DRC: + AppConfig* app_config = wxGetApp().app_config; + if (app_config) + quality = stoi(app_config->get("drc_bits")); + else + quality = DRC_BITS_DEFAULT; + break; + } + + wxString path; + if (multi_stls) { + wxDirDialog dlg(this, _L("Choose a directory"), from_u8(wxGetApp().app_config->get_last_dir()), + wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + if (dlg.ShowModal() == wxID_OK) { + path = dlg.GetPath() + "/"; + } + } else { + path = p->get_export_file(file_type); + } + if (path.empty()) { return; } + const std::string path_u8 = into_u8(path); + + wxBusyCursor wait; + const auto& selection = p->get_selection(); + const auto obj_idx = selection.get_object_idx(); + +#if EXPORT_WITH_BOOLEAN + if (selection_only && (obj_idx == -1 || selection.is_wipe_tower())) + return; +#else + // BBS support selecting multiple objects + if (selection_only && selection.is_wipe_tower()) return; + + // BBS + if (selection_only) { + // only support selection single full object and mulitiple full object + if (!selection.is_single_full_object() && !selection.is_multiple_full_object()) return; + } + + // Following lambda generates a combined mesh for export with normals pointing outwards. + auto mesh_to_export_fff_no_boolean = [this](const ModelObject &mo, int instance_id) { + TriangleMesh mesh; + + //Prusa export negative parts + std::vector csgmesh; + csgmesh.reserve(2 * mo.volumes.size()); + csg::model_to_csgmesh(mo, Transform3d::Identity(), std::back_inserter(csgmesh), + csg::mpartsPositive | csg::mpartsNegative | csg::mpartsDoSplits); + + auto csgrange = range(csgmesh); + if (csg::is_all_positive(csgrange)) { + mesh = TriangleMesh{csg::csgmesh_merge_positive_parts(csgrange)}; + } else if (std::get<2>(csg::check_csgmesh_booleans(csgrange)) == csgrange.end()) { + try { + auto cgalm = csg::perform_csgmesh_booleans(csgrange); + mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*cgalm); + } catch (...) {} + } + + if (mesh.empty()) { + get_notification_manager()->push_plater_error_notification( + _u8L("Unable to perform boolean operation on model meshes. " + "Only positive parts will be exported.")); + + for (const ModelVolume* v : mo.volumes) + if (v->is_model_part()) { + TriangleMesh vol_mesh(v->mesh()); + vol_mesh.transform(v->get_matrix(), true); + mesh.merge(vol_mesh); + } + } + if (instance_id == -1) { + TriangleMesh vols_mesh(mesh); + mesh = TriangleMesh(); + for (const ModelInstance *i : mo.instances) { + TriangleMesh m = vols_mesh; + m.transform(i->get_matrix(), true); + mesh.merge(m); + } + } else if (0 <= instance_id && instance_id < int(mo.instances.size())) + mesh.transform(mo.instances[instance_id]->get_matrix(), true); + return mesh; + }; +#endif + auto mesh_to_export_sla = [&, this](const ModelObject& mo, int instance_id) { + TriangleMesh mesh; + + const SLAPrintObject *object = this->p->sla_print.get_print_object_by_model_object_id(mo.id()); + + if (auto m = object->get_mesh_to_print(); m.empty()) + mesh = combine_mesh_fff(mo, instance_id, [this](const std::string& msg) {return get_notification_manager()->push_general_error_notification(msg); }); + else { + const Transform3d mesh_trafo_inv = object->trafo().inverse(); + const bool is_left_handed = object->is_left_handed(); + + auto pad_mesh = extended? object->pad_mesh() : TriangleMesh{}; + pad_mesh.transform(mesh_trafo_inv); + + auto supports_mesh = extended ? object->support_mesh() : TriangleMesh{}; + supports_mesh.transform(mesh_trafo_inv); + + const std::vector& obj_instances = object->instances(); + for (const SLAPrintObject::Instance& obj_instance : obj_instances) { + auto it = std::find_if(object->model_object()->instances.begin(), object->model_object()->instances.end(), + [&obj_instance](const ModelInstance *mi) { return mi->id() == obj_instance.instance_id; }); + assert(it != object->model_object()->instances.end()); + + if (it != object->model_object()->instances.end()) { + const bool one_inst_only = selection_only && ! selection.is_single_full_object(); + + const int instance_idx = it - object->model_object()->instances.begin(); + const Transform3d& inst_transform = one_inst_only + ? Transform3d::Identity() + : object->model_object()->instances[instance_idx]->get_transformation().get_matrix(); + + TriangleMesh inst_mesh; + + if (!pad_mesh.empty()) { + TriangleMesh inst_pad_mesh = pad_mesh; + inst_pad_mesh.transform(inst_transform, is_left_handed); + inst_mesh.merge(inst_pad_mesh); + } + + if (!supports_mesh.empty()) { + TriangleMesh inst_supports_mesh = supports_mesh; + inst_supports_mesh.transform(inst_transform, is_left_handed); + inst_mesh.merge(inst_supports_mesh); + } + + TriangleMesh inst_object_mesh = object->get_mesh_to_print(); + + inst_object_mesh.transform(mesh_trafo_inv); + inst_object_mesh.transform(inst_transform, is_left_handed); + + inst_mesh.merge(inst_object_mesh); + + // ensure that the instance lays on the bed + inst_mesh.translate(0.0f, 0.0f, -inst_mesh.bounding_box().min.z()); + + // merge instance with global mesh + mesh.merge(inst_mesh); + + if (one_inst_only) + break; + } + } + } + + return mesh; + }; + + std::function + mesh_to_export; + + if (p->printer_technology == ptFFF) +#if EXPORT_WITH_BOOLEAN + mesh_to_export = [this](const ModelObject& mo, int instance_id) {return Plater::combine_mesh_fff(mo, instance_id, + [this](const std::string& msg) {return get_notification_manager()->push_general_error_notification(msg); }); }; +#else + mesh_to_export = mesh_to_export_fff_no_boolean; +#endif + else + mesh_to_export = mesh_to_export_sla; + + auto get_save_file = [file_type](std::string const & dir, std::string const & name) { + std::string ext = ""; + switch (file_type) { + case FT_STL: ext = ".stl"; break; + case FT_DRC: ext = ".drc"; break; + } + + auto path = dir + name + ext; + int n = 1; + while (boost::filesystem::exists(path)) + path = dir + name + "(" + std::to_string(n++) + ")"+ext; + return path; + }; + + TriangleMesh mesh; + if (selection_only) { + if (selection.is_single_full_object()) { + const auto obj_idx = selection.get_object_idx(); + const ModelObject* model_object = p->model.objects[obj_idx]; + if (selection.get_mode() == Selection::Instance) + mesh = mesh_to_export(*model_object, (model_object->instances.size() > 1) ? -1 : selection.get_instance_idx()); + else { + const GLVolume* volume = selection.get_first_volume(); + mesh = model_object->volumes[volume->volume_idx()]->mesh(); + mesh.transform(volume->get_volume_transformation().get_matrix(), true); + } + + if (model_object->instances.size() == 1) mesh.translate(-model_object->origin_translation.cast()); + } + else if (selection.is_multiple_full_object() && !multi_stls) { + const std::set>& instances_idxs = p->get_selection().get_selected_object_instances(); + for (const std::pair& i : instances_idxs) { + ModelObject* object = p->model.objects[i.first]; + mesh.merge(mesh_to_export(*object, i.second)); + } + } + else if (selection.is_multiple_full_object() && multi_stls) { + const std::set> &instances_idxs = p->get_selection().get_selected_object_instances(); + for (const std::pair &i : instances_idxs) { + ModelObject *object = p->model.objects[i.first]; + auto mesh = mesh_to_export(*object, i.second); + mesh.translate(-object->origin_translation.cast()); + + switch (file_type) { + case FT_STL: Slic3r::store_stl(get_save_file(path_u8, object->name).c_str(), &mesh, true); break; + case FT_DRC: Slic3r::store_drc(get_save_file(path_u8, object->name).c_str(), &mesh, quality); break; + } + } + return; + } + } + else if (!multi_stls) { + for (const ModelObject* o : p->model.objects) { + mesh.merge(mesh_to_export(*o, -1)); + } + } else { + for (const ModelObject* o : p->model.objects) { + auto mesh = mesh_to_export(*o, -1); + mesh.translate(-o->origin_translation.cast()); + + switch (file_type) { + case FT_STL: Slic3r::store_stl(get_save_file(path_u8, o->name).c_str(), &mesh, true); break; + case FT_DRC: Slic3r::store_drc(get_save_file(path_u8, o->name).c_str(), &mesh, quality); break; + } + } + return; + } + + switch (file_type) { + case FT_STL: Slic3r::store_stl(path_u8.c_str(), &mesh, true); break; + case FT_DRC: Slic3r::store_drc(path_u8.c_str(), &mesh, quality); break; + } +} + +//BBS: remove amf export +/*void Plater::export_amf() +{ + if (p->model.objects.empty()) { return; } + + wxString path = p->get_export_file(FT_AMF); + if (path.empty()) { return; } + const std::string path_u8 = into_u8(path); + + wxBusyCursor wait; + bool export_config = true; + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); + bool full_pathnames = false; + if (Slic3r::store_amf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, full_pathnames)) { + ; //store success + } else { + ; // store failed + } +}*/ + +namespace { +std::string get_file_name(const std::string &file_path) +{ + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + size_t pos_point = file_path.find_last_of('.'); + size_t offset = pos_last_delimiter + 1; + size_t count = pos_point - pos_last_delimiter - 1; + return file_path.substr(offset, count); +} +using SvgFile = EmbossShape::SvgFile; +using SvgFiles = std::vector; +std::string create_unique_3mf_filepath(const std::string &file, const SvgFiles svgs) +{ + // const std::string MODEL_FOLDER = "3D/"; // copy from file 3mf.cpp + std::string path_in_3mf = "3D/" + file + ".svg"; + size_t suffix_number = 0; + bool is_unique = false; + do{ + is_unique = true; + path_in_3mf = "3D/" + file + ((suffix_number++)? ("_" + std::to_string(suffix_number)) : "") + ".svg"; + for (SvgFile *svgfile : svgs) { + if (svgfile->path_in_3mf.empty()) + continue; + if (svgfile->path_in_3mf.compare(path_in_3mf) == 0) { + is_unique = false; + break; + } + } + } while (!is_unique); + return path_in_3mf; +} + +bool set_by_local_path(SvgFile &svg, const SvgFiles& svgs) +{ + // Try to find already used svg file + for (SvgFile *svg_ : svgs) { + if (svg_->path_in_3mf.empty()) + continue; + if (svg.path.compare(svg_->path) == 0) { + svg.path_in_3mf = svg_->path_in_3mf; + return true; + } + } + return false; +} + +/// +/// Function to secure private data before store to 3mf +/// +/// Data(also private) to clean before publishing +void publish(Model &model, SaveStrategy strategy) { + + // SVG file publishing + bool exist_new = false; + SvgFiles svgfiles; + for (ModelObject *object: model.objects){ + for (ModelVolume *volume : object->volumes) { + if (!volume->emboss_shape.has_value()) + continue; + if (volume->text_configuration.has_value()) + continue; // text dosen't have svg path + + assert(volume->emboss_shape->svg_file.has_value()); + if (!volume->emboss_shape->svg_file.has_value()) + continue; + + SvgFile* svg = &(*volume->emboss_shape->svg_file); + if (svg->path_in_3mf.empty()) + exist_new = true; + svgfiles.push_back(svg); + } + } + + for (SvgFile *svgfile : svgfiles) { + if (!svgfile->path_in_3mf.empty()) + continue; // already suggested path (previous save) + + // create unique name for svgs, when local path differ + std::string filename = "unknown"; + if (!svgfile->path.empty()) { + if (set_by_local_path(*svgfile, svgfiles)) + continue; + // check whether original filename is already in: + filename = get_file_name(svgfile->path); + } + svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles); + } +} +} + +// BBS: backup +int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn) +{ + int ret = 0; + //if (p->model.objects.empty()) { + // MessageDialog dialog(nullptr, _L("No objects to export."), _L("Save project"), wxYES); + // if (dialog.ShowModal() == wxYES) + // return -1; + //} + + if (output_path.empty()) + return -1; + + bool export_config = true; + wxString path = from_path(output_path); + + if (!path.Lower().EndsWith(".3mf")) + return -1; + + // take care about private data stored into .3mf + // modify model + publish(p->model, strategy); + + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); + const std::string path_u8 = into_u8(path); + wxBusyCursor wait; + + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") + %output_path.string()%(strategy & SaveStrategy::Backup)%export_plate_idx %(unsigned int)strategy; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") + % std::string("") % (strategy & SaveStrategy::Backup) % export_plate_idx % (unsigned int)strategy; + + //BBS: add plate logic for thumbnail generate + std::vector thumbnails; + std::vector no_light_thumbnails; + std::vector calibration_thumbnails; + std::vector top_thumbnails; + std::vector picking_thumbnails; + std::vector plate_bboxes; + // BBS: backup + if (!(strategy & SaveStrategy::Backup)) { + for (int i = 0; i < p->partplate_list.get_plate_count(); i++) { + ThumbnailData* thumbnail_data = &p->partplate_list.get_plate(i)->thumbnail_data; + if (p->partplate_list.get_plate(i)->thumbnail_data.is_valid() && using_exported_file()) { + //no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i; + } + else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; + const ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i }; + p->generate_thumbnail(p->partplate_list.get_plate(i)->thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, + thumbnail_params, Camera::EType::Ortho); + } + thumbnails.push_back(thumbnail_data); + + ThumbnailData *no_light_thumbnail_data = &p->partplate_list.get_plate(i)->no_light_thumbnail_data; + if (p->partplate_list.get_plate(i)->no_light_thumbnail_data.is_valid() && using_exported_file()) { + // no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; + const ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; + p->generate_thumbnail(p->partplate_list.get_plate(i)->no_light_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, + Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true); + } + no_light_thumbnails.push_back(no_light_thumbnail_data); + //ThumbnailData* calibration_data = &p->partplate_list.get_plate(i)->cali_thumbnail_data; + //calibration_thumbnails.push_back(calibration_data); + PlateBBoxData* plate_bbox_data = &p->partplate_list.get_plate(i)->cali_bboxes_data; + plate_bboxes.push_back(plate_bbox_data); + + //generate top and picking thumbnails + ThumbnailData* top_thumbnail = &p->partplate_list.get_plate(i)->top_thumbnail_data; + if (top_thumbnail->is_valid() && using_exported_file()) { + //no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i; + } + else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i; + const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; + p->generate_thumbnail(p->partplate_list.get_plate(i)->top_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, + Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, false); + } + top_thumbnails.push_back(top_thumbnail); + + ThumbnailData* picking_thumbnail = &p->partplate_list.get_plate(i)->pick_thumbnail_data; + if (picking_thumbnail->is_valid() && using_exported_file()) { + //no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i; + } + else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i; + const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; + p->generate_thumbnail(p->partplate_list.get_plate(i)->pick_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, + Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, true,true); + } + picking_thumbnails.push_back(picking_thumbnail); + } + + if (p->partplate_list.get_curr_plate()->is_slice_result_valid()) { + //BBS generate BBS calibration thumbnails + int index = p->partplate_list.get_curr_plate_index(); + //ThumbnailData* calibration_data = calibration_thumbnails[index]; + //const ThumbnailsParams calibration_params = { {}, false, true, true, true, p->partplate_list.get_curr_plate_index() }; + //p->generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, calibration_params); + if (using_exported_file()) { + //do nothing + } + else + *plate_bboxes[index] = p->generate_first_layer_bbox(); + } + } + + //BBS: add bbs 3mf logic + PlateDataPtrs plate_data_list; + p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx); + + // BBS: backup + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + std::vector project_presets = preset_bundle.get_current_project_embedded_presets(); + + StoreParams store_params; + store_params.path = path_u8.c_str(); + store_params.model = &p->model; + store_params.plate_data_list = plate_data_list; + store_params.export_plate_idx = export_plate_idx; + store_params.project_presets = project_presets; + store_params.config = export_config ? &cfg : nullptr; + store_params.thumbnail_data = thumbnails; + store_params.no_light_thumbnail_data = no_light_thumbnails; + store_params.top_thumbnail_data = top_thumbnails; + store_params.pick_thumbnail_data = picking_thumbnails; + store_params.calibration_thumbnail_data = calibration_thumbnails; + store_params.proFn = proFn; + store_params.id_bboxes = plate_bboxes;//BBS + store_params.project = &p->project; + store_params.strategy = strategy | SaveStrategy::Zip64; + + + // get type and color for platedata + auto* filament_color = dynamic_cast(cfg.option("filament_colour")); + auto* nozzle_diameter_option = dynamic_cast(cfg.option("nozzle_diameter")); + auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); + std::string nozzle_diameter_str; + if (nozzle_diameter_option) + nozzle_diameter_str = nozzle_diameter_option->serialize(); + + std::string printer_model_id = preset_bundle.printers.get_edited_preset().get_printer_type(&preset_bundle); + + for (int i = 0; i < plate_data_list.size(); i++) { + PlateData *plate_data = plate_data_list[i]; + plate_data->printer_model_id = printer_model_id; + plate_data->nozzle_diameters = nozzle_diameter_str; + for (auto it = plate_data->slice_filaments_info.begin(); it != plate_data->slice_filaments_info.end(); it++) { + std::string display_filament_type; + it->type = cfg.get_filament_type(display_filament_type, it->id); + it->filament_id = filament_id_opt ? filament_id_opt->get_at(it->id) : ""; + it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF"; + // save filament info used in curr plate + int index = p->partplate_list.get_curr_plate_index(); + if (store_params.id_bboxes.size() > index) { + store_params.id_bboxes[index]->filament_ids.push_back(it->id); + store_params.id_bboxes[index]->filament_colors.push_back(it->color); + } + } + } + + // handle Design Info + bool has_design_info = false; + ModelDesignInfo designInfo; + if (p->model.design_info != nullptr) { + if (!p->model.design_info->Designer.empty()) { + BOOST_LOG_TRIVIAL(trace) << "design_info, found designer = " << p->model.design_info->Designer; + has_design_info = true; + } + } + if (!has_design_info) { + // add Designed Info + if (p->model.design_info == nullptr) { + // set designInfo before export and reset after export + if (wxGetApp().is_user_login()) { + p->model.design_info = std::make_shared(); + //p->model.design_info->Designer = wxGetApp().getAgent()->get_user_nickname(); + p->model.design_info->Designer = ""; + p->model.design_info->DesignerUserId = wxGetApp().getAgent()->get_user_id(); + BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer = "<< ""; + BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer_user_id = " << p->model.design_info->DesignerUserId; + } + } + } + + bool store_result = Slic3r::store_bbs_3mf(store_params); + // reset designed info + if (!has_design_info) + p->model.design_info = nullptr; + + if (store_result) { + if (!(store_params.strategy & SaveStrategy::Silence)) { + // Success + p->set_project_filename(path); + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << path; + } + } + else { + ret = -1; + } + + if (project_presets.size() > 0) + { + for (unsigned int i = 0; i < project_presets.size(); i++) + { + delete project_presets[i]; + } + project_presets.clear(); + } + + release_PlateData_list(plate_data_list); + + for (unsigned int i = 0; i < calibration_thumbnails.size(); i++) + { + //release the data here, as it will always be generated when export + calibration_thumbnails[i]->reset(); + } + for (unsigned int i = 0; i < no_light_thumbnails.size(); i++) { + // release the data here, as it will always be generated when export + no_light_thumbnails[i]->reset(); + } + for (unsigned int i = 0; i < top_thumbnails.size(); i++) + { + //release the data here, as it will always be generated when export + top_thumbnails[i]->reset(); + } + top_thumbnails.clear(); + for (unsigned int i = 0; i < picking_thumbnails.size(); i++) + { + //release the data here, as it will always be generated when export + picking_thumbnails[i]->reset();; + } + picking_thumbnails.clear(); + + return ret; +} + +void Plater::publish_project() +{ + return; +} + + +void Plater::reload_from_disk() +{ + p->reload_from_disk(); +} + +void Plater::replace_with_stl() +{ + p->replace_with_stl(); +} + +void Plater::replace_all_with_stl() +{ + p->replace_all_with_stl(); +} + +void Plater::reload_all_from_disk() +{ + p->reload_all_from_disk(); +} + +bool Plater::has_toolpaths_to_export() const +{ + return p->preview->get_canvas3d()->has_toolpaths_to_export(); +} + +void Plater::export_toolpaths_to_obj() const +{ + if ((printer_technology() != ptFFF) || !is_preview_loaded()) + return; + + wxString path = p->get_export_file(FT_OBJ); + if (path.empty()) + return; + + wxBusyCursor wait; + p->preview->get_canvas3d()->export_toolpaths_to_obj(into_u8(path).c_str()); +} + +bool Plater::is_empty_project() { + return model().objects.empty(); +} +bool Plater::is_multi_extruder_ams_empty() +{ + std::vector extruder_ams_count_str = p->config->option("extruder_ams_count", true)->values; + std::vector> extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); + for (auto extruder_ams_count : extruder_ams_counts) { + for (auto iter = extruder_ams_count.begin(); iter != extruder_ams_count.end(); ++iter) { + if (iter->second != 0) + return false; + } + } + return true; +} + +//BBS: add multiple plate reslice logic +void Plater::reslice() +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: enter, process_completed_with_error=%2%")%__LINE__ %p->process_completed_with_error; + // There is "invalid data" button instead "slice now" + if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": process_completed_with_error, return directly"); + reset_gcode_toolpaths(); + return; + } + + // In case SLA gizmo is in editing mode, refuse to continue + // and notify user that he should leave it first. + if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) + return; + + // Stop the running (and queued) UI jobs and only proceed if they actually + // get stopped. + unsigned timeout_ms = 10000; + if (!stop_queue(this->get_ui_job_worker(), timeout_ms)) { + BOOST_LOG_TRIVIAL(error) << "Could not stop UI job within " + << timeout_ms << " milliseconds timeout!"; + return; + } + + // Orca: regenerate CalibPressureAdvancePattern custom G-code to apply changes + if (model().calib_pa_pattern) { + _calib_pa_pattern_gen_gcode(); + } + + if (printer_technology() == ptSLA) { + for (auto& object : model().objects) + if (object->sla_points_status == sla::PointsStatus::NoPoints) + object->sla_points_status = sla::PointsStatus::Generating; + } + + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState + unsigned int state = this->p->update_background_process(true); + if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) + this->p->view3D->reload_scene(false); + // If the SLA processing of just a single object's supports is running, restart slicing for the whole object. + this->p->background_process.set_task(PrintBase::TaskParams()); + // Only restarts if the state is valid. + //BBS: jusdge the result + bool result = this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: restart background,state=%2%, result=%3%")%__LINE__%state %result; + if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) + { + //BBS: add logs + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": state %1% is UPDATE_BACKGROUND_PROCESS_INVALID, can not slice") % state; + p->update_fff_scene_only_shells(); + return; + } + + if ((!result) && p->m_slice_all && (p->m_cur_slice_plate < (p->partplate_list.get_plate_count() - 1))) + { + //slice next + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": in slicing all, current plate %1% already sliced, skip to next") % p->m_cur_slice_plate ; + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Finished, nullptr); + // Post the "complete" callback message, so that it will slice the next plate soon + wxQueueEvent(this, evt.Clone()); + p->m_is_slicing = true; + if (p->m_cur_slice_plate == 0) + reset_gcode_toolpaths(); + return; + } + + if (result) { + p->m_is_slicing = true; + } + + bool clean_gcode_toolpaths = true; + // BBS + if (p->background_process.running()) + { + //p->ready_to_slice = false; + p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process is running, m_is_slicing is true"); + } + else if (!p->background_process.empty() && !p->background_process.idle()) { + //p->show_action_buttons(true); + //p->ready_to_slice = true; + p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process changes to not_idle, set ready_to_slice back to true"); + } + else { + //BBS: add reset logic for empty plate + PartPlate * current_plate = p->background_process.get_current_plate(); + + if (!current_plate->has_printable_instances()) { + clean_gcode_toolpaths = true; + current_plate->update_slice_result_valid_state(false); + } + else { + clean_gcode_toolpaths = false; + current_plate->update_slice_result_valid_state(true); + } + p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process in idle state, use previous result, clean_gcode_toolpaths=%1%")%clean_gcode_toolpaths; + } + + if (clean_gcode_toolpaths) + reset_gcode_toolpaths(); + + p->preview->reload_print(); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index(); + + record_slice_preset("slicing"); +} + +void Plater::record_slice_preset(std::string action) +{ + // record slice preset + try + { + json j; + auto printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset_with_vendor_profile().preset; + if (printer_preset.is_system) { + j["printer_preset_name"] = printer_preset.name; + } + else { + j["printer_preset_name"] = printer_preset.config.opt_string("inherits"); + } + const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map; + if (keys_map) { + for (auto item : *keys_map) { + if (item.second == wxGetApp().preset_bundle->project_config.opt_enum("curr_bed_type")) { + j["curr_bed_type"] = item.first; + break; + } + } + } + auto filament_presets = wxGetApp().preset_bundle->filament_presets; + for (int i = 0; i < filament_presets.size(); ++i) { + auto filament_preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[i]); + if (filament_preset->is_system) { + j["filament_preset_" + std::to_string(i)] = filament_preset->name; + } + else { + j["filament_preset_" + std::to_string(i)] = filament_preset->config.opt_string("inherits"); + } + } + + Preset& print_preset = wxGetApp().preset_bundle->prints.get_edited_preset(); + if (print_preset.is_system) { + j["process_preset"] = print_preset.name; + } + else { + j["process_preset"] = print_preset.config.opt_string("inherits"); + } + j["support_type"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("support_type")); + j["sparse_infill_pattern"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("sparse_infill_pattern")); + j["sparse_infill_density"] = print_preset.config.opt("sparse_infill_density")->value; + + j["brim_type"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("brim_type")); + j["user_mode"] = wxGetApp().get_mode_str(); + + if (p->background_process.fff_print()) { + const DynamicPrintConfig& full_config = p->background_process.fff_print()->full_print_config(); + json values = json::array(); + if (full_config.has("different_settings_to_system")) { + std::vector different_values = full_config.option("different_settings_to_system")->values; + for (auto& item : different_values) { + values.push_back(item); + } + } + j["different_settings_to_system"] = values; + } + + j["record_event"] = action; + NetworkAgent* agent = wxGetApp().getAgent(); + } + catch (...) + { + return; + } +} + +//BBS: add project slicing related logic +int Plater::start_next_slice() +{ + // Stop arrange and (or) optimize rotation tasks. + //this->stop_jobs(); + + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState + unsigned int state = this->p->update_background_process(true, false, false); + if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) + this->p->view3D->reload_scene(false); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": update_background_process returns %1%")%state; + if (!p->partplate_list.get_curr_plate()->can_slice()) { + p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": found invalidated apply in update_background_process."); + return -1; + } + + // Only restarts if the state is valid. + bool result = this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART); + if (!result) + { + //slice next + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Finished, nullptr); + // Post the "complete" callback message, so that it will slice the next plate soon + wxQueueEvent(this, evt.Clone()); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": restart_background_process returns %1%")%result; + + return 0; +} + + +void Plater::reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages) +{ + reslice_SLA_until_step(slaposPad, object, postpone_error_messages); +} + +void Plater::reslice_SLA_hollowing(const ModelObject &object, bool postpone_error_messages) +{ + reslice_SLA_until_step(slaposDrillHoles, object, postpone_error_messages); +} + +void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages) +{ + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState + unsigned int state = this->p->update_background_process(true, postpone_error_messages); + if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) + this->p->view3D->reload_scene(false); + + if (this->p->background_process.empty() || (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID)) + // Nothing to do on empty input or invalid configuration. + return; + + // Limit calculation to the single object only. + PrintBase::TaskParams task; + task.single_model_object = object.id(); + // If the background processing is not enabled, calculate supports just for the single instance. + // Otherwise calculate everything, but start with the provided object. + if (!this->p->background_processing_enabled()) { + task.single_model_instance_only = true; + task.to_object_step = step; + } + this->p->background_process.set_task(task); + // and let the background processing start. + this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART); +} +void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool use_3mf) +{ + // if physical_printer is selected, send gcode for this printer + // DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); + DynamicPrintConfig* physical_printer_config = &Slic3r::GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (! physical_printer_config || p->model.objects.empty()) + return; + + PrintHostJob upload_job(physical_printer_config); + if (upload_job.empty()) + return; + + upload_job.upload_data.use_3mf = use_3mf; + + // Obtain default output path + fs::path default_output_file; + try { + // Update the background processing, so that the placeholder parser will get the correct values for the ouput file template. + // Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed. + unsigned int state = this->p->update_restart_background_process(false, false); + if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) + return; + default_output_file = this->p->background_process.output_filepath_for_project( + into_path(this->p->get_project_filename(".3mf"))); + } catch (const Slic3r::PlaceholderParserError& ex) { + // Show the error with monospaced font. + show_error(this, ex.what(), true); + return; + } catch (const std::exception& ex) { + show_error(this, ex.what(), false); + return; + } + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + if (use_3mf) { + default_output_file.replace_extension("3mf"); + } + + // Repetier specific: Query the server for the list of file groups. + wxArrayString groups; + { + wxBusyCursor wait; + upload_job.printhost->get_groups(groups); + } + + // PrusaLink specific: Query the server for the list of file groups. + wxArrayString storage_paths; + wxArrayString storage_names; + { + wxBusyCursor wait; + try { + upload_job.printhost->get_storage(storage_paths, storage_names); + } catch (const Slic3r::IOError& ex) { + show_error(this, ex.what(), false); + return; + } + } + + { + auto preset_bundle = wxGetApp().preset_bundle; + const auto opt = physical_printer_config->option>("host_type"); + const auto host_type = opt != nullptr ? opt->value : htElegooLink; + auto config = get_app_config(); + + std::unique_ptr pDlg; + if (host_type == htElegooLink) { + pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, + storage_paths, storage_names, + config->get_bool("open_device_tab_post_upload")); + } else { + pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, + storage_paths, storage_names, config->get_bool("open_device_tab_post_upload")); + } + + pDlg->init(); + if (pDlg->ShowModal() != wxID_OK) { + return; + } + + config->set_bool("open_device_tab_post_upload", pDlg->switch_to_device_tab()); + // PrintHostUpload upload_data; + upload_job.switch_to_device_tab = pDlg->switch_to_device_tab(); + upload_job.upload_data.upload_path = pDlg->filename(); + upload_job.upload_data.post_action = pDlg->post_action(); + upload_job.upload_data.group = pDlg->group(); + upload_job.upload_data.storage = pDlg->storage(); + upload_job.upload_data.extended_info = pDlg->extendedInfo(); + } + + // Show "Is printer clean" dialog for PrusaConnect - Upload and print. + if (std::string(upload_job.printhost->get_name()) == "PrusaConnect" && upload_job.upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + GUI::MessageDialog dlg(nullptr, _L("Is the printer ready? Is the print sheet in place, empty and clean?"), _L("Upload and Print"), wxOK | wxCANCEL); + if (dlg.ShowModal() != wxID_OK) + return; + } + + if (use_3mf) { + // Process gcode + const int result = send_gcode(plate_idx, nullptr); + + if (result < 0) { + wxString msg = _L("Abnormal print file data. Please slice again"); + show_error(this, msg, false); + return; + } + + upload_job.upload_data.source_path = p->m_print_job_data._3mf_path; + } + + p->export_gcode(fs::path(), false, std::move(upload_job)); +} +int Plater::send_gcode(int plate_idx, Export3mfProgressFn proFn) +{ + int result = 0; + /* generate 3mf */ + set_print_job_plate_idx(plate_idx); + + PartPlate* plate = get_partplate_list().get_curr_plate(); + try { + p->m_print_job_data._3mf_path = fs::path(plate->get_tmp_gcode_path()); + p->m_print_job_data._3mf_path.replace_extension("3mf"); + } + catch (std::exception&) { + BOOST_LOG_TRIVIAL(error) << "generate 3mf path failed"; + return -1; + } + + SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SkipModel | SaveStrategy::WithGcode | SaveStrategy::SkipAuxiliary; +#if !BBL_RELEASE_TO_PUBLIC + //only save model in QA environment + std::string sel = get_app_config()->get("iot_environment"); + if (sel == ENV_PRE_HOST) + strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode; +#endif + + result = export_3mf(p->m_print_job_data._3mf_path, strategy, plate_idx, proFn); + + return result; +} + +int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn) +{ + int result = 0; + /* generate 3mf */ + set_print_job_plate_idx(plate_idx); + + PartPlate* plate = get_partplate_list().get_curr_plate(); + try { + p->m_print_job_data._3mf_config_path = fs::path(plate->get_temp_config_3mf_path()); + } + catch (std::exception&) { + BOOST_LOG_TRIVIAL(error) << "generate 3mf path failed"; + return -1; + } + + SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SkipModel | SaveStrategy::WithSliceInfo | SaveStrategy::SkipAuxiliary; + result = export_3mf(p->m_print_job_data._3mf_config_path, strategy, plate_idx, proFn); + + return result; +} + +//BBS +void Plater::send_calibration_job_finished(wxCommandEvent & evt) +{ + p->main_frame->request_select_tab(MainFrame::TabPosition::tpCalibration); + auto calibration_panel = p->main_frame->m_calibration; + if (calibration_panel) { + auto curr_wizard = static_cast(calibration_panel->get_tabpanel()->GetPage(evt.GetInt())); + wxCommandEvent event(EVT_CALIBRATION_JOB_FINISHED); + event.SetString(evt.GetString()); + event.SetEventObject(curr_wizard); + wxPostEvent(curr_wizard, event); + } + evt.Skip(); +} + +void Plater::print_job_finished(wxCommandEvent &evt) +{ + //start print failed + if (p) { +#ifdef __APPLE__ + p->hide_select_machine_dlg(); +#else + if (Slic3r::GUI::wxGetApp().get_inf_dialog_contect().empty()) { + p->hide_select_machine_dlg(); + } else { + p->enter_prepare_mode(); + } +#endif // __APPLE__ + } + + + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) return; + + dev->set_selected_machine(evt.GetString().ToStdString()); + p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor); + //jump to monitor and select device status panel + MonitorPanel* curr_monitor = p->main_frame->m_monitor; + if(curr_monitor) + curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); +} + +void Plater::send_job_finished(wxCommandEvent& evt) +{ + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) return; + //dev->set_selected_machine(evt.GetString().ToStdString()); + + send_gcode_finish(evt.GetString()); + p->hide_send_to_printer_dlg(); + //p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor); + ////jump to monitor and select device status panel + //MonitorPanel* curr_monitor = p->main_frame->m_monitor; + //if (curr_monitor) + // curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); +} + +void Plater::publish_job_finished(wxCommandEvent &evt) +{ + p->m_publish_dlg->EndModal(wxID_OK); + // GUI::wxGetApp().load_url(evt.GetString()); + //GUI::wxGetApp().open_publish_page_dialog(evt.GetString()); +} + +// Called when the Eject button is pressed. +void Plater::eject_drive() +{ + wxBusyCursor wait; + wxGetApp().removable_drive_manager()->set_and_verify_last_save_path(p->last_output_dir_path); + wxGetApp().removable_drive_manager()->eject_drive(); +} + +void Plater::take_snapshot(const std::string &snapshot_name) { p->take_snapshot(snapshot_name); } +//void Plater::take_snapshot(const wxString &snapshot_name) { p->take_snapshot(snapshot_name); } +void Plater::take_snapshot(const std::string &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, snapshot_type); } +//void Plater::take_snapshot(const wxString &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, snapshot_type); } +void Plater::suppress_snapshots() { p->suppress_snapshots(); } +void Plater::allow_snapshots() { p->allow_snapshots(); } +// BBS: single snapshot +void Plater::single_snapshots_enter(SingleSnapshot *single) +{ + p->single_snapshots_enter(single); +} +void Plater::single_snapshots_leave(SingleSnapshot *single) +{ + p->single_snapshots_leave(single); +} +void Plater::undo() { p->undo(); } +void Plater::redo() { p->redo(); } +void Plater::undo_to(int selection) +{ + if (selection == 0) { + p->undo(); + return; + } + + const int idx = p->get_active_snapshot_index() - selection - 1; + p->undo_redo_to(p->undo_redo_stack().snapshots()[idx].timestamp); +} +void Plater::redo_to(int selection) +{ + if (selection == 0) { + p->redo(); + return; + } + + const int idx = p->get_active_snapshot_index() + selection + 1; + p->undo_redo_to(p->undo_redo_stack().snapshots()[idx].timestamp); +} +bool Plater::undo_redo_string_getter(const bool is_undo, int idx, const char** out_text) +{ + const std::vector& ss_stack = p->undo_redo_stack().snapshots(); + const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -(++idx) : idx); + + if (0 < idx_in_ss_stack && (size_t)idx_in_ss_stack < ss_stack.size() - 1) { + *out_text = ss_stack[idx_in_ss_stack].name.c_str(); + return true; + } + + return false; +} + +int Plater::update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path) +{ + return p->update_print_required_data(config, model, plate_data_list, file_name, file_path); +} + + +void Plater::undo_redo_topmost_string_getter(const bool is_undo, std::string& out_text) +{ + const std::vector& ss_stack = p->undo_redo_stack().snapshots(); + const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -1 : 0); + + if (0 < idx_in_ss_stack && (size_t)idx_in_ss_stack < ss_stack.size() - 1) { + out_text = ss_stack[idx_in_ss_stack].name; + return; + } + + out_text = ""; +} + +bool Plater::search_string_getter(int idx, const char** label, const char** tooltip) +{ + const Search::OptionsSearcher& search_list = p->sidebar->get_searcher(); + + if (0 <= idx && (size_t)idx < search_list.size()) { + search_list[idx].get_marked_label_and_tooltip(label, tooltip); + return true; + } + + return false; +} + +void Plater::on_filament_change(size_t filament_idx) +{ + auto& filament_presets = wxGetApp().preset_bundle->filament_presets; + if (filament_idx >= filament_presets.size()) + return; + Slic3r::Preset* filament = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[filament_idx]); + if (filament == nullptr) + return; + std::string filament_type = filament->config.option("filament_type")->values[0]; +} + +// BBS. +void Plater::on_filament_count_change(size_t num_filaments) +{ + // only update elements in plater + update_filament_colors_in_full_config(); + sidebar().on_filament_count_change(num_filaments); + sidebar().obj_list()->update_objects_list_filament_column(num_filaments); + + Slic3r::GUI::PartPlateList &plate_list = get_partplate_list(); + plate_list.set_filament_count(num_filaments); + for (int i = 0; i < plate_list.get_plate_count(); ++i) { + PartPlate* part_plate = plate_list.get_plate(i); + part_plate->update_first_layer_print_sequence(num_filaments); + } + + for (ModelObject* mo : wxGetApp().model().objects) { + for (ModelVolume* mv : mo->volumes) { + mv->update_extruder_count(num_filaments); + } + } +} + +void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id) +{ + // only update elements in plater + update_filament_colors_in_full_config(); + + // update fisrt print sequence and other layer sequence + //move to partplate->on_filament_deleted + /*Slic3r::GUI::PartPlateList &plate_list = get_partplate_list(); + for (int i = 0; i < plate_list.get_plate_count(); ++i) { + PartPlate *part_plate = plate_list.get_plate(i); + part_plate->update_first_layer_print_sequence_when_delete_filament(filament_id); + }*/ + + // update mmu info + for (ModelObject *mo : wxGetApp().model().objects) { + for (ModelVolume *mv : mo->volumes) { + mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, replace_filament_id + 1); // this function is 1 base + } + } + + // update UI + sidebar().on_filaments_delete(filament_id); + + // update global support filament + static const char *keys[] = {"support_filament", "support_interface_filament"}; + for (auto key : keys) + if (p->config->has(key)) { + if(p->config->opt_int(key) == filament_id + 1) + (*(p->config)).erase(key); + else { + int new_value = p->config->opt_int(key) > filament_id ? p->config->opt_int(key) - 1 : p->config->opt_int(key); + (*(p->config)).set_key_value(key, new ConfigOptionInt(new_value)); + } + } + + // update object/volume/support(object and volume) filament id + sidebar().obj_list()->update_objects_list_filament_column_when_delete_filament(filament_id, num_filaments, replace_filament_id); + + // update customize gcode + for (auto item = p->model.plates_custom_gcodes.begin(); item != p->model.plates_custom_gcodes.end(); ++item) { + auto iter = std::remove_if(item->second.gcodes.begin(), item->second.gcodes.end(), [filament_id](const Item& gcode_item) { + return (gcode_item.type == CustomGCode::Type::ToolChange && gcode_item.extruder == filament_id + 1); + }); + if (replace_filament_id == -1) + item->second.gcodes.erase(iter, item->second.gcodes.end()); + else if(iter != item->second.gcodes.end()) { + iter->extruder = replace_filament_id + 1; + } + + for (auto& item : item->second.gcodes) { + if (item.type == CustomGCode::Type::ToolChange && item.extruder > filament_id) + item.extruder--; + } + } +} + +std::vector Plater::get_extruders_colors() +{ + unsigned char rgba_color[4] = {}; + std::vector colors = get_extruder_colors_from_plater_config(); + std::vector colors_out(colors.size()); + for (const std::string &color : colors) { + Slic3r::GUI::BitmapCache::parse_color4(color, rgba_color); + size_t color_idx = &color - &colors.front(); + colors_out[color_idx] = { + float(rgba_color[0]) / 255.f, + float(rgba_color[1]) / 255.f, + float(rgba_color[2]) / 255.f, + float(rgba_color[3]) / 255.f, + }; + } + return colors_out; +} + +void Plater::on_bed_type_change(BedType bed_type) +{ + sidebar().on_bed_type_change(bed_type); +} + +bool Plater::update_filament_colors_in_full_config() +{ + DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + ConfigOptionStrings* color_opt = project_config.option("filament_colour"); + const ConfigOptionStrings* type_opt = full_config.option("filament_type"); + + p->config->option("filament_colour")->values = color_opt->values; + p->config->option("filament_type")->values = type_opt->values; + return true; +} + +void Plater::config_change_notification(const DynamicPrintConfig &config, const std::string& key) +{ + GLCanvas3D* view3d_canvas = get_view3D_canvas3D(); + if (key == std::string("print_sequence")) { + auto seq_print = config.option>("print_sequence"); + if (seq_print && view3d_canvas && view3d_canvas->is_initialized() && view3d_canvas->is_rendering_enabled()) { + NotificationManager* notify_manager = get_notification_manager(); + if (seq_print->value == PrintSequence::ByObject) { + std::string info_text = _u8L("Print By Object: \nSuggest to use auto-arrange to avoid collisions when printing."); + notify_manager->bbl_show_seqprintinfo_notification(info_text); + } + else + notify_manager->bbl_close_seqprintinfo_notification(); + } + } + // notification for more options +} + +void Plater::on_config_change(const DynamicPrintConfig &config) +{ + bool update_scheduled = false; + bool bed_shape_changed = false; + //bool print_sequence_changed = false; + t_config_option_keys diff_keys = p->config->diff(config); + + size_t old_nozzle_size = 1, new_nozzle_size = 1; + auto * opt_old = p->config->option("nozzle_diameter"); + auto * opt_new = config.option("nozzle_diameter"); + if (opt_old && opt_new) { + old_nozzle_size = opt_old->values.size(); + new_nozzle_size = opt_new->values.size(); + } + + for (auto opt_key : diff_keys) { + if (opt_key == "filament_colour") { + update_scheduled = true; // update should be scheduled (for update 3DScene) #2738 + + if (update_filament_colors_in_full_config()) { + p->sidebar->obj_list()->update_filament_colors(); + p->sidebar->update_dynamic_filament_list(); + continue; + } + } + if (opt_key == "filament_type") { + update_filament_colors_in_full_config(); + continue; + } + if (opt_key == "material_colour") { + update_scheduled = true; // update should be scheduled (for update 3DScene) + } + + p->config->set_key_value(opt_key, config.option(opt_key)->clone()); + if (opt_key == "printer_technology") { + this->set_printer_technology(config.opt_enum(opt_key)); + // print technology is changed, so we should to update a search list + p->sidebar->update_searcher(); + p->reset_gcode_toolpaths(); + p->view3D->get_canvas3d()->reset_sequential_print_clearance(); + p->preview->get_canvas3d()->reset_volumes(); + //BBS: invalid all the slice results + p->partplate_list.invalid_all_slice_result(); + } + //BBS: add bed_exclude_area + else if (opt_key == "printable_area" || opt_key == "bed_exclude_area" + || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model" + || opt_key == "extruder_clearance_height_to_lid" + || opt_key == "extruder_clearance_height_to_rod") { + bed_shape_changed = true; + update_scheduled = true; + } + else if (opt_key == "bed_shape" || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model") { + bed_shape_changed = true; + update_scheduled = true; + } + else if (boost::starts_with(opt_key, "enable_prime_tower") || + boost::starts_with(opt_key, "prime_tower") || + boost::starts_with(opt_key, "wipe_tower") || + opt_key == "filament_minimal_purge_on_wipe_tower" || + opt_key == "single_extruder_multi_material" || + // BBS + opt_key == "prime_volume") { + update_scheduled = true; + } + else if(opt_key == "extruder_colour") { + update_scheduled = true; + //p->sidebar->obj_list()->update_extruder_colors(); + } + else if (opt_key == "printable_height") { + bed_shape_changed = true; + update_scheduled = true; + } + else if (opt_key == "print_sequence") { + update_scheduled = true; + //print_sequence_changed = true; + } + else if (opt_key == "printer_model") { + p->reset_gcode_toolpaths(); + if (old_nozzle_size != new_nozzle_size) { + update_flush_volume_matrix(old_nozzle_size, new_nozzle_size); + } + + // update to force bed selection(for texturing) + bed_shape_changed = true; + update_scheduled = true; + } + // Orca: update when *_filament changed + else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "wall_filament" || + opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") { + update_scheduled = true; + } + } + + if (bed_shape_changed) + set_bed_shape(); + + config_change_notification(config, std::string("print_sequence")); + + if (update_scheduled) + update(); + + if (p->main_frame->is_loaded()) { + this->p->schedule_background_process(); + update_title_dirty_status(); + p->schedule_auto_reslice_if_needed(); + } +} + +void Plater::update_flush_volume_matrix(size_t old_nozzle_size, size_t new_nozzle_size) +{ + size_t nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + Slic3r::DynamicPrintConfig *project_config = &wxGetApp().preset_bundle->project_config; + + // Verify whether it is the first time start Studio + size_t filament_nums = project_config->option("filament_colour")->values.size(); + size_t flush_volume_size = project_config->option("flush_volumes_matrix")->values.size(); + + assert(nozzle_nums == new_nozzle_size); + if (old_nozzle_size < new_nozzle_size) { + + std::vector first_flush_volume_mtx = get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); + if (first_flush_volume_mtx.size() == filament_nums * filament_nums * new_nozzle_size) { // load file + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, first_flush_volume_mtx, -1, new_nozzle_size); + } else { + first_flush_volume_mtx.resize(filament_nums * filament_nums, 0); + std::vector flush_volume_mtx; + for (size_t i = 0; i < new_nozzle_size; ++i) { + flush_volume_mtx.insert(flush_volume_mtx.end(), first_flush_volume_mtx.begin(), first_flush_volume_mtx.end()); + } + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, flush_volume_mtx, -1, new_nozzle_size); + } + std::vector flush_multipliers = project_config->option("flush_multiplier")->values; + flush_multipliers.resize(nozzle_nums, 1.f); + project_config->option("flush_multiplier")->values = flush_multipliers; + } + else if (old_nozzle_size > new_nozzle_size) { + std::vector new_flush_volume_mtx; + for (size_t i = 0; i < new_nozzle_size; ++i) { + std::vector flush_volume_mtx = get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); + flush_volume_mtx.resize(filament_nums * filament_nums, 0); + new_flush_volume_mtx.insert(new_flush_volume_mtx.end(), flush_volume_mtx.begin(), flush_volume_mtx.end()); + } + + std::vector flush_multipliers = project_config->option("flush_multiplier")->values; + flush_multipliers.resize(nozzle_nums, 1.f); + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, new_flush_volume_mtx, -1, new_nozzle_size); + project_config->option("flush_multiplier")->values = flush_multipliers; + } +} + +void Plater::set_bed_shape() const +{ + std::string texture_filename; + auto bundle = wxGetApp().preset_bundle; + if (bundle != nullptr) { + const Preset* curr = &bundle->printers.get_selected_preset(); + if (curr->is_system) + texture_filename = PresetUtils::system_printer_bed_texture(*curr); + else { + auto *printer_model = curr->config.opt("printer_model"); + if (printer_model != nullptr && ! printer_model->value.empty()) { + texture_filename = bundle->get_texture_for_printer_model(printer_model->value); + } + } + } + set_bed_shape(p->config->option("printable_area")->values, + //BBS: add bed exclude areas + p->config->option("bed_exclude_area")->values, + p->config->option("wrapping_exclude_area")->values, + p->config->option("printable_height")->value, + p->config->option("extruder_printable_area")->values, + p->config->option("extruder_printable_height")->values, + p->config->option("bed_custom_texture")->value.empty() ? texture_filename : p->config->option("bed_custom_texture")->value, + p->config->option("bed_custom_model")->value); +} + +//BBS: add bed exclude area +void Plater::set_bed_shape(const Pointfs& shape, const Pointfs& exclude_area, const Pointfs& wrapping_exclude_area, const double printable_height, std::vector extruder_areas, std::vector extruder_heights, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) const +{ + p->set_bed_shape(make_counter_clockwise(shape), exclude_area, wrapping_exclude_area, printable_height, extruder_areas, extruder_heights, custom_texture, custom_model, force_as_custom); +} + +void Plater::force_filament_colors_update() +{ +//BBS: filament_color logic has been moved out of filament setting +#if 0 + bool update_scheduled = false; + DynamicPrintConfig* config = p->config; + const std::vector filament_presets = wxGetApp().preset_bundle->filament_presets; + if (filament_presets.size() > 1 && + p->config->option("filament_colour")->values.size() == filament_presets.size()) + { + const PresetCollection& filaments = wxGetApp().preset_bundle->filaments; + std::vector filament_colors; + filament_colors.reserve(filament_presets.size()); + + for (const std::string& filament_preset : filament_presets) + filament_colors.push_back(filaments.find_preset(filament_preset, true)->config.opt_string("filament_colour", (unsigned)0)); + + if (config->option("filament_colour")->values != filament_colors) { + config->option("filament_colour")->values = filament_colors; + update_scheduled = true; + } + } + + if (update_scheduled) { + update(); + p->sidebar->obj_list()->update_filament_colors(); + } + + if (p->main_frame->is_loaded()) + this->p->schedule_background_process(); +#endif +} + +void Plater::force_print_bed_update() +{ + // Fill in the printer model key with something which cannot possibly be valid, so that Plater::on_config_change() will update the print bed + // once a new Printer profile config is loaded. + p->config->opt_string("printer_model", true) = "bbl_empty"; +} + +void Plater::on_activate() +{ + this->p->show_delayed_error_message(); +} + +// Get vector of extruder colors considering filament color, if extruder color is undefined. +std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const +{ + if (wxGetApp().is_gcode_viewer() && result != nullptr) + return result->extruder_colors; + else { + const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; + std::vector filament_colors; + if (!config->has("filament_colour")) // in case of a SLA print + return filament_colors; + + filament_colors = (config->option("filament_colour"))->values; + return filament_colors; + } +} + +std::vector Plater::get_filament_colors_render_info() const +{ + const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; + std::vector color_packs; + if (!config->has("filament_multi_colour")) return color_packs; + + color_packs = (config->option("filament_multi_colour"))->values; + return color_packs; +} + +std::vector Plater::get_filament_color_render_type() const +{ + const Slic3r::DynamicPrintConfig *config = &wxGetApp().preset_bundle->project_config; + std::vector ctype; + if (!config->has("filament_colour_type")) return ctype; + + ctype = (config->option("filament_colour_type"))->values; + return ctype; +} + +/* Get vector of colors used for rendering of a Preview scene in "Color print" mode + * It consists of extruder colors and colors, saved in model.custom_gcode_per_print_z + */ +std::vector Plater::get_colors_for_color_print(const GCodeProcessorResult* const result) const +{ + std::vector colors = get_extruder_colors_from_plater_config(result); + + if (wxGetApp().is_gcode_viewer() && result != nullptr) { + for (const CustomGCode::Item& code : result->custom_gcode_per_print_z) { + if (code.type == CustomGCode::ColorChange) + colors.emplace_back(code.color); + } + } + else { + //BBS + colors.reserve(colors.size() + p->model.get_curr_plate_custom_gcodes().gcodes.size()); + for (const CustomGCode::Item& code : p->model.get_curr_plate_custom_gcodes().gcodes) { + if (code.type == CustomGCode::ColorChange) + colors.emplace_back(code.color); + } + } + + return colors; +} + +void Plater::set_global_filament_map_mode(FilamentMapMode mode) +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + auto mode_ptr = project_config.option>("filament_map_mode"); + FilamentMapMode old_mode = mode_ptr->value; + if(mode != old_mode) + on_filament_map_mode_change(); + mode_ptr->value = mode; +} + +void Plater::set_global_filament_map(const std::vector& filament_map) +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + project_config.option("filament_map")->values = filament_map; +} + +std::vector Plater::get_global_filament_map() const +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + return project_config.option("filament_map")->values; +} + + +FilamentMapMode Plater::get_global_filament_map_mode() const +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + return project_config.option>("filament_map_mode")->value; +} + +void Plater::on_filament_map_mode_change() +{ + auto& plate_list = this->get_partplate_list(); + int plate_count = plate_list.get_plate_count(); + for (int idx = 0; idx < plate_count; ++idx) { + auto plate=plate_list.get_plate(idx); + auto plate_map_mode = plate->get_filament_map_mode(); + if (plate_map_mode == fmmDefault) + plate->clear_filament_map(); + } +} + +wxWindow* Plater::get_select_machine_dialog() +{ + return p->m_select_machine_dlg; +} + +void Plater::update_print_error_info(int code, std::string msg, std::string extra) +{ + if (p->m_select_machine_dlg) { + p->m_select_machine_dlg->update_print_error_info(code, msg, extra); + } + + if (p->m_send_to_sdcard_dlg) { + p->m_send_to_sdcard_dlg->update_print_error_info(code, msg, extra); + } + if (p->main_frame->m_calibration) + p->main_frame->m_calibration->update_print_error_info(code, msg, extra); +} + +wxString Plater::get_project_filename(const wxString& extension) const +{ + return p->get_project_filename(extension); +} + +wxString Plater::get_export_gcode_filename(const wxString & extension, bool only_filename, bool export_all) const +{ + return p->get_export_gcode_filename(extension, only_filename, export_all); +} + +void Plater::set_project_filename(const wxString& filename) +{ + p->set_project_filename(filename); +} + +bool Plater::is_export_gcode_scheduled() const +{ + return p->background_process.is_export_scheduled(); +} + +const Selection &Plater::get_selection() const +{ + return p->get_selection(); +} + +int Plater::get_selected_object_idx() +{ + return p->get_selected_object_idx(); +} + +bool Plater::is_single_full_object_selection() const +{ + return p->get_selection().is_single_full_object(); +} + +GLCanvas3D* Plater::canvas3D() +{ + // BBS modify view3D->get_canvas3d() to current canvas + return p->get_current_canvas3D(); +} + +const GLCanvas3D* Plater::canvas3D() const +{ + // BBS modify view3D->get_canvas3d() to current canvas + return p->get_current_canvas3D(); +} + +GLCanvas3D* Plater::get_view3D_canvas3D() +{ + return p ? p->view3D->get_canvas3d() : nullptr; +} + +GLCanvas3D* Plater::get_preview_canvas3D() +{ + return p->preview->get_canvas3d(); +} + +GLCanvas3D* Plater::get_assmeble_canvas3D() +{ + if (p->assemble_view) + return p->assemble_view->get_canvas3d(); + return nullptr; +} + +GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview) +{ + return p->get_current_canvas3D(exclude_preview); +} + +void Plater::arrange() +{ + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Arrange")); + replace_job(w, std::make_unique()); + } +} + +void Plater::set_current_canvas_as_dirty() +{ + p->set_current_canvas_as_dirty(); +} + +void Plater::unbind_canvas_event_handlers() +{ + p->unbind_canvas_event_handlers(); +} + +void Plater::reset_canvas_volumes() +{ + p->reset_canvas_volumes(); +} + +PrinterTechnology Plater::printer_technology() const +{ + return p->printer_technology; +} + +const DynamicPrintConfig * Plater::config() const { return p->config; } + +bool Plater::set_printer_technology(PrinterTechnology printer_technology) +{ + p->printer_technology = printer_technology; + bool ret = p->background_process.select_technology(printer_technology); + if (ret) { + // Update the active presets. + } + //FIXME for SLA synchronize + //p->background_process.apply(Model)! + + if (printer_technology == ptSLA) { + for (ModelObject* model_object : p->model.objects) { + model_object->ensure_on_bed(); + } + } + + p->label_btn_export = printer_technology == ptFFF ? L("Export G-code") : L("Export"); + p->label_btn_send = printer_technology == ptFFF ? L("Send G-code") : L("Send to printer"); + + if (wxGetApp().mainframe != nullptr) + wxGetApp().mainframe->update_menubar(); + + p->sidebar->get_searcher().set_printer_technology(printer_technology); + + p->notification_manager->set_fff(printer_technology == ptFFF); + p->notification_manager->set_slicing_progress_hidden(); + + return ret; +} + +void Plater::clear_before_change_mesh(int obj_idx) +{ + ModelObject* mo = model().objects[obj_idx]; + + // If there are custom supports/seams/mmu/fuzzy skin segmentation, remove them. Fixed mesh + // may be different and they would make no sense. + bool paint_removed = false; + for (ModelVolume* mv : mo->volumes) { + paint_removed |= ! mv->supported_facets.empty() || ! mv->seam_facets.empty() || ! mv->mmu_segmentation_facets.empty() || !mv->fuzzy_skin_facets.empty(); + mv->supported_facets.reset(); + mv->seam_facets.reset(); + mv->mmu_segmentation_facets.reset(); + mv->fuzzy_skin_facets.reset(); + } + if (paint_removed) { + // snapshot_time is captured by copy so the lambda knows where to undo/redo to. + get_notification_manager()->push_notification( + NotificationType::CustomSupportsAndSeamRemovedAfterRepair, + NotificationManager::NotificationLevel::PrintInfoNotificationLevel, + _u8L("Custom supports and color painting were removed before repairing.")); + } +} + +void Plater::changed_mesh(int obj_idx) +{ + ModelObject* mo = model().objects[obj_idx]; + sla::reproject_points_and_holes(mo); + update(); + p->object_list_changed(); + p->schedule_background_process(); +} + +void Plater::changed_object(ModelObject &object){ + assert(object.get_model() == &p->model); // is object from same model? + object.invalidate_bounding_box(); + + // recenter and re - align to Z = 0 + object.ensure_on_bed(p->printer_technology != ptSLA); + + if (p->printer_technology == ptSLA) { + // Update the SLAPrint from the current Model, so that the reload_scene() + // pulls the correct data, update the 3D scene. + p->update_restart_background_process(true, false); + } else + p->view3D->reload_scene(false); + + // update print + p->schedule_background_process(); + + // Check outside bed + get_current_canvas3D()->requires_check_outside_state(); +} + +void Plater::changed_object(int obj_idx) +{ + if (obj_idx < 0) + return; + ModelObject *object = p->model.objects[obj_idx]; + if (object == nullptr) + return; + changed_object(*object); +} + +void Plater::changed_objects(const std::vector& object_idxs) +{ + if (object_idxs.empty()) + return; + + for (size_t obj_idx : object_idxs) { + if (obj_idx < p->model.objects.size()) { + if (p->model.objects[obj_idx]->min_z() >= SINKING_Z_THRESHOLD) + // re - align to Z = 0 + p->model.objects[obj_idx]->ensure_on_bed(); + } + } + if (this->p->printer_technology == ptSLA) { + // Update the SLAPrint from the current Model, so that the reload_scene() + // pulls the correct data, update the 3D scene. + this->p->update_restart_background_process(true, false); + } + else { + p->view3D->reload_scene(false); + p->view3D->get_canvas3d()->update_instance_printable_state_for_objects(object_idxs); + } + + // update print + this->p->schedule_background_process(); +} + +void Plater::schedule_background_process(bool schedule/* = true*/) +{ + if (schedule) + this->p->schedule_background_process(); + + this->p->suppressed_backround_processing_update = false; +} + +bool Plater::is_background_process_update_scheduled() const +{ + return this->p->background_process_timer.IsRunning(); +} + +void Plater::suppress_background_process(const bool stop_background_process) +{ + if (stop_background_process) + this->p->background_process_timer.Stop(); + + this->p->suppressed_backround_processing_update = true; +} + +void Plater::center_selection() { p->center_selection(); } +void Plater::drop_selection() { p->drop_selection(); } +void Plater::mirror(Axis axis) { p->mirror(axis); } +void Plater::split_object(bool auto_drop) { p->split_object(auto_drop); } +void Plater::split_volume() { p->split_volume(); } +void Plater::optimize_rotation() +{ + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Optimize Rotation")); + replace_job(w, std::make_unique()); + } +} +void Plater::update_menus() { p->menus.update(); } + +wxString Plater::get_selected_printer_name_in_combox() { + PresetBundle * preset_bundle = wxGetApp().preset_bundle; + std::string printer_model = preset_bundle->printers.get_selected_preset().config.option("printer_model")->value; + return printer_model; +} + +void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWarningType type, const wxString &title) +{ + printer_name.Replace("Bambu Lab", "", false); + wxString content; + bool device_page = (wxGetApp().mainframe == nullptr) && (wxGetApp().mainframe->m_monitor->IsShown()); + if (type == PrinterWarningType::NOT_CONNECTED) { + if (device_page) { + content = wxString::Format(_L("Printer not connected. Please go to the device page to connect %s before syncing."), + printer_name); + } else { + content = wxString::Format( + _L("OrcaSlicer can't connect to %s. Please check if the printer is powered on and connected to the network."), printer_name); + } + } else if (type == PrinterWarningType::INCONSISTENT) { + content = wxString::Format(_L("The currently connected printer on the device page is not %s. Please switch to %s before syncing."), printer_name, printer_name); + } else if (type == PrinterWarningType::UNINSTALL_FILAMENT) { + content = _L("There are no filaments on the printer. Please load the filaments on the printer first."); + } else if (type == PrinterWarningType::EMPTY_FILAMENT) { + content = _L("The filaments on the printer are all unknown types. Please go to the printer screen or software device page to set the filament type."); + } + MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page")); + auto result = dlg.ShowModal(); + if (result == wxFORWARD) { + wxGetApp().mainframe->select_tab(size_t(MainFrame::tpMonitor)); + } +} + +bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning) +{ + if (!wxGetApp().getDeviceManager()) { + return false; + } + MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); + if (obj == nullptr) { + return false; + } + if (!check_printer_initialized(obj, true, popup_warning)) + return false; + Preset * machine_preset = get_printer_preset(obj); + if (!machine_preset) + return false; + + if (wxGetApp().is_blocking_printing()) { + if (popup_warning) { + auto printer_name = get_selected_printer_name_in_combox(); // wxString(obj->get_preset_printer_model_name(machine_print_name)) + pop_warning_and_go_to_device_page(printer_name, PrinterWarningType::INCONSISTENT, _L("Synchronize AMS Filament Information")); + } + return false; + } + return true; +} +// BBS +//void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); } + +void Plater::fill_color(int extruder_id) +{ + if (can_fillcolor()) { + p->assemble_view->get_canvas3d()->get_selection().fill_color(extruder_id); + } +} + +//BBS +void Plater::cut_selection_to_clipboard() +{ + Plater::TakeSnapshot snapshot(this, "Cut Selected Objects"); + if (can_cut_to_clipboard() && !p->sidebar->obj_list()->cut_to_clipboard()) { + p->view3D->get_canvas3d()->get_selection().cut_to_clipboard(); + } +} + +void Plater::copy_selection_to_clipboard() +{ + // At first try to copy selected values to the ObjectList's clipboard + // to check if Settings or Layers are selected in the list + // and then copy to 3DCanvas's clipboard if not + if (can_copy_to_clipboard() && !p->sidebar->obj_list()->copy_to_clipboard()) + p->view3D->get_canvas3d()->get_selection().copy_to_clipboard(); +} + +void Plater::paste_from_clipboard() +{ + if (!can_paste_from_clipboard()) + return; + + Plater::TakeSnapshot snapshot(this, "Paste From Clipboard"); + + // At first try to paste values from the ObjectList's clipboard + // to check if Settings or Layers were copied + // and then paste from the 3DCanvas's clipboard if not + if (!p->sidebar->obj_list()->paste_from_clipboard()) + p->view3D->get_canvas3d()->get_selection().paste_from_clipboard(); +} + +//BBS: add clone +void Plater::clone_selection() +{ + if (is_selection_empty()) + return; + CloneDialog dlg(this); + dlg.ShowModal(); +} + +std::vector Plater::get_empty_cells(const Vec2f step) +{ + PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); + BoundingBoxf3 build_volume = plate->get_build_volume(true); + Vec2d vmin(build_volume.min.x(), build_volume.min.y()), vmax(build_volume.max.x(), build_volume.max.y()); + BoundingBoxf bbox(vmin, vmax); + std::vector cells; + auto min_x = step(0)/2;// start_point.x() - step(0) * int((start_point.x() - bbox.min.x()) / step(0)); + auto min_y = step(1)/2;// start_point.y() - step(1) * int((start_point.y() - bbox.min.y()) / step(1)); + auto& exclude_box3s = plate->get_exclude_areas(); + std::vector exclude_boxs; + for (auto& box : exclude_box3s) { + Vec2d vmin(box.min.x(), box.min.y()), vmax(box.max.x(), box.max.y()); + exclude_boxs.emplace_back(vmin, vmax); + } + for (float x = min_x + bbox.min.x(); x < bbox.max.x() - step(0) / 2; x += step(0)) + for (float y = min_y + bbox.min.y(); y < bbox.max.y() - step(1) / 2; y += step(1)) { + bool in_exclude = false; + BoundingBoxf cell(Vec2d(x - step(0) / 2, y - step(1) / 2), Vec2d(x + step(0) / 2, y + step(1) / 2)); + for (auto& box : exclude_boxs) { + if (box.overlap(cell)) { + in_exclude = true; + break; + } + } + if(in_exclude) + continue; + cells.emplace_back(x, y); + } + return cells; +} + +void Plater::search(bool plater_is_active, Preset::Type type, wxWindow *tag, TextInput *etag, wxWindow *stag) +{ + if (plater_is_active) { + if (is_preview_shown()) + return; + // plater should be focused for correct navigation inside search window + this->SetFocus(); + + wxKeyEvent evt; +#ifdef __APPLE__ + evt.m_keyCode = 'f'; +#else /* __APPLE__ */ + evt.m_keyCode = WXK_CONTROL_F; +#endif /* __APPLE__ */ + evt.SetControlDown(true); + canvas3D()->on_char(evt); + } + else + p->sidebar->get_searcher().show_dialog(type, tag, etag, stag); +} + +void Plater::msw_rescale() +{ + p->preview->msw_rescale(); + + p->view3D->get_canvas3d()->msw_rescale(); + + p->sidebar->msw_rescale(); + + p->menus.msw_rescale(); + + Layout(); + GetParent()->Layout(); +} + +void Plater::sys_color_changed() +{ + p->preview->sys_color_changed(); + p->sidebar->sys_color_changed(); + p->menus.sys_color_changed(); + if (p->m_select_machine_dlg) p->m_select_machine_dlg->sys_color_changed(); + + Layout(); + GetParent()->Layout(); +} + +// BBS +#if 0 +bool Plater::init_view_toolbar() +{ + return p->init_view_toolbar(); +} + +void Plater::enable_view_toolbar(bool enable) +{ + p->view_toolbar.set_enabled(enable); +} +#endif + +bool Plater::init_collapse_toolbar() +{ + return p->init_collapse_toolbar(); +} + +const Camera& Plater::get_camera() const +{ + return p->camera; +} + +Camera& Plater::get_camera() +{ + return p->camera; +} + +//BBS: partplate list related functions +PartPlateList& Plater::get_partplate_list() +{ + return p->partplate_list; +} + +void Plater::apply_background_progress() +{ + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + int plate_index = p->partplate_list.get_curr_plate_index(); + bool result_valid = part_plate->is_slice_result_valid(); + const auto& preset_bundle = wxGetApp().preset_bundle; + //always apply the current plate's print + Print::ApplyStatus invalidated; + if (preset_bundle->get_printer_extruder_count() > 1) { + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps)); + } + else + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ") % __LINE__ % plate_index % invalidated % result_valid; + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) + { + part_plate->update_slice_result_valid_state(false); + //p->ready_to_slice = true; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); + } +} + +//BBS: select Plate +int Plater::select_plate(int plate_index, bool need_slice) +{ + int ret; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, need_slice %3% ")%__LINE__ %plate_index %need_slice; + take_snapshot("select partplate!"); + ret = p->partplate_list.select_plate(plate_index); + if (!ret) { + if (is_view3D_shown()) + wxGetApp().plater()->canvas3D()->render(); + } + const auto& preset_bundle = wxGetApp().preset_bundle; + + if ((!ret) && (p->background_process.can_switch_print())) + { + //select successfully + p->partplate_list.update_slice_context_to_current_plate(p->background_process); + p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + p->update_print_volume_state(); + + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + bool result_valid = part_plate->is_slice_result_valid(); + PrintBase* print = nullptr; + GCodeResult* gcode_result = nullptr; + Print::ApplyStatus invalidated; + + part_plate->get_print(&print, &gcode_result, NULL); + + //always apply the current plate's print + if (preset_bundle->get_printer_extruder_count() > 1) { + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps)); + } + else + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); + bool model_fits, validate_err; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ")%__LINE__ %plate_index %invalidated %result_valid; + if (result_valid) + { + if (is_preview_shown()) + { + if (need_slice) { //from preview's thumbnail + if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) || (gcode_result->moves.empty())){ + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) + part_plate->update_slice_result_valid_state(false); + p->process_completed_with_error = -1; + p->m_slice_all = false; + reset_gcode_toolpaths(); + reslice(); + } + else { + validate_current_plate(model_fits, validate_err); + //just refresh_print + reload_print(); + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false, true); + } + } + else {// from multiple slice's next + //do nothing + } + } + else + { + validate_current_plate(model_fits, validate_err); + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) + { + part_plate->update_slice_result_valid_state(false); + // BBS + //p->show_action_buttons(true); + //p->ready_to_slice = true; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); + } + else + { + // BBS + //p->show_action_buttons(false); + //p->ready_to_slice = false; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); + + reload_print(); + } + } + } + else + { + //check inside status + //model_fits = p->view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; + //bool validate_err = false; + validate_current_plate(model_fits, validate_err); + if (model_fits && !validate_err) { + p->process_completed_with_error = -1; + } + else { + p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); + } + if (is_preview_shown()) + { + if (need_slice) + { + //p->process_completed_with_error = -1; + p->m_slice_all = false; + reset_gcode_toolpaths(); + if (model_fits && !validate_err) { + if (!check_ams_status(false)){ + return ret; + } + reslice(); + } + else { + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); + //sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary + //then the print toolpath will be shown, so we should not refresh print here, only onload shell + //refresh_print(); + p->update_fff_scene_only_shells(); + } + } + else { + //p->ready_to_slice = false; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); + reload_print(); + } + } + else + { + //validate_current_plate(model_fits, validate_err); + //check inside status + /*if (model_fits && !validate_err){ + p->process_completed_with_error = -1; + } + else { + p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); + }*/ + + // BBS: don't show action buttons + //p->show_action_buttons(true); + //p->ready_to_slice = true; + if (model_fits && part_plate->has_printable_instances()) + { + //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); + } + else + { + //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); + } + } + } + } + + SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); + p->on_plate_selected(event); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; + return ret; +} + +int Plater::select_sliced_plate(int plate_index) +{ + int ret = 0; + BOOST_LOG_TRIVIAL(info) << "select_sliced_plate plate_idx=" << plate_index; + + Freeze(); + ret = select_plate(plate_index, true); + if (ret) + { + BOOST_LOG_TRIVIAL(error) << "select_plate error for plate_idx=" << plate_index; + Thaw(); + return -1; + } + p->partplate_list.select_plate_view(); + Thaw(); + + return ret; +} + +extern std::string& get_object_limited_text(); +extern std::string& get_object_clashed_text(); +extern std::string& get_left_extruder_unprintable_text(); +extern std::string& get_right_extruder_unprintable_text(); + +void Plater::validate_current_plate(bool& model_fits, bool& validate_error) +{ + ObjectFilamentResults object_results; + ModelInstanceEPrintVolumeState state = p->view3D->get_canvas3d()->check_volumes_outside_state(&object_results); + model_fits = (state != ModelInstancePVS_Partly_Outside); + + PartPlate *cur_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); + if (model_fits) { // TPU check + bool tpu_valid = cur_plate->check_tpu_printable_status(wxGetApp().preset_bundle->full_config(), wxGetApp().preset_bundle->get_used_tpu_filaments(cur_plate->get_extruders(true))); + model_fits &= tpu_valid; + } + + if (model_fits) { // Filament printable check + wxString filament_printable_error_msg; + bool filament_printable = cur_plate->check_filament_printable(wxGetApp().preset_bundle->full_config(), filament_printable_error_msg); + model_fits &= filament_printable; + } + + model_fits = model_fits && object_results.filaments.empty(); + validate_error = false; + if (p->printer_technology == ptFFF) { + //std::string plater_text = _u8L("An object is laid over the boundary of plate or exceeds the height limit.\n" + // "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume.");; + StringObjectException warning; + Polygons polygons; + std::vector> height_polygons; + p->background_process.fff_print()->set_check_multi_filaments_compatibility(wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); + StringObjectException err = p->background_process.validate(&warning, &polygons, &height_polygons); + // update string by type + post_process_string_object_exception(err); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warning=%2%, model_fits %3%")%err.string%warning.string %model_fits; + + if (err.string.empty()) { + p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); + p->notification_manager->set_all_slicing_errors_gray(true); + p->notification_manager->close_notification_of_type(NotificationType::ValidateError); + p->notification_manager->bbl_close_3mf_warn_notification(); + + // Pass a warning from validation and either show a notification, + // or hide the old one. + p->process_validation_warning(warning); + p->view3D->get_canvas3d()->reset_sequential_print_clearance(); + p->view3D->get_canvas3d()->set_as_dirty(); + p->view3D->get_canvas3d()->request_extra_frame(); + } + else { + // The print is not valid. + p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); + // Show error as notification. + p->notification_manager->push_validate_error_notification(err); + p->process_validation_warning(warning); + //model_fits = false; + validate_error = true; + p->view3D->get_canvas3d()->set_sequential_print_clearance_visible(true); + p->view3D->get_canvas3d()->set_sequential_print_clearance_render_fill(true); + p->view3D->get_canvas3d()->set_sequential_print_clearance_polygons(polygons, height_polygons); + } + + std::string clashed_text = get_object_clashed_text(); + if (state == ModelInstancePVS_Partly_Outside) { + p->notification_manager->push_plater_error_notification(clashed_text); + } + else { + p->notification_manager->close_plater_error_notification(clashed_text); + } + std::string left_unprintable_text = get_left_extruder_unprintable_text(), right_unprintable_text = get_right_extruder_unprintable_text(); + if (!left_unprintable_text.empty()) + { + p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::LeftExtruderUnprintableError, left_unprintable_text); + } + else { + p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice(NotificationType::LeftExtruderUnprintableError); + } + + if (!right_unprintable_text.empty()) + { + p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::RightExtruderUnprintableError,right_unprintable_text); + } + else { + p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice(NotificationType::RightExtruderUnprintableError); + } + + /*if (state == ModelInstancePVS_Limited) { + p->notification_manager->push_plater_warning_notification(get_object_limited_text()); + } + else { + p->notification_manager->close_plater_warning_notification(get_object_limited_text()); + }*/ + } + + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + part_plate->update_slice_ready_status(model_fits); + + return; +} + +void Plater::open_platesettings_dialog(wxCommandEvent& evt) { + int plate_index = evt.GetInt(); + PlateSettingsDialog dlg(this, _L("Plate Settings"), evt.GetString() == "only_layer_sequence"); + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + dlg.sync_bed_type(curr_plate->get_bed_type()); + + auto curr_print_seq = curr_plate->get_print_seq(); + if (curr_print_seq != PrintSequence::ByDefault) { + dlg.sync_print_seq(int(curr_print_seq) + 1); + } + else + dlg.sync_print_seq(0); + + auto first_layer_print_seq = curr_plate->get_first_layer_print_sequence(); + if (first_layer_print_seq.empty()) + dlg.sync_first_layer_print_seq(0); + else + dlg.sync_first_layer_print_seq(1, curr_plate->get_first_layer_print_sequence()); + + auto other_layers_print_seq = curr_plate->get_other_layers_print_sequence(); + if (other_layers_print_seq.empty()) + dlg.sync_other_layers_print_seq(0, {}); + else { + dlg.sync_other_layers_print_seq(1, curr_plate->get_other_layers_print_sequence()); + } + + dlg.sync_spiral_mode(curr_plate->get_spiral_vase_mode(), !curr_plate->has_spiral_mode_config()); + + dlg.Bind(EVT_SET_BED_TYPE_CONFIRM, [this, plate_index, &dlg](wxCommandEvent& e) { + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + BedType old_bed_type = curr_plate->get_bed_type(); + auto bt_sel = dlg.get_bed_type_choice(); + if (old_bed_type != bt_sel) { + curr_plate->set_bed_type(bt_sel); + update_project_dirty_from_presets(); + set_plater_dirty(true); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("select bed type %1% for plate %2% at plate side") % bt_sel % plate_index; + + if (dlg.get_first_layer_print_seq_choice() != 0) + curr_plate->set_first_layer_print_sequence(dlg.get_first_layer_print_seq()); + else + curr_plate->set_first_layer_print_sequence({}); + + if (dlg.get_other_layers_print_seq_choice() != 0) + curr_plate->set_other_layers_print_sequence(dlg.get_other_layers_print_seq_infos()); + else + curr_plate->set_other_layers_print_sequence({}); + + int ps_sel = dlg.get_print_seq_choice(); + if (ps_sel != 0) + curr_plate->set_print_seq(PrintSequence(ps_sel - 1)); + else + curr_plate->set_print_seq(PrintSequence::ByDefault); + + int spiral_sel = dlg.get_spiral_mode_choice(); + if (spiral_sel == 1) { + curr_plate->set_spiral_vase_mode(true, false); + } + else if (spiral_sel == 2) { + curr_plate->set_spiral_vase_mode(false, false); + } + else { + curr_plate->set_spiral_vase_mode(false, true); + } + + update_project_dirty_from_presets(); + set_plater_dirty(true); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("select print sequence %1% for plate %2% at plate side") % ps_sel % plate_index; + auto plate_config = *(curr_plate->config()); + wxGetApp().plater()->config_change_notification(plate_config, std::string("print_sequence")); + update(); + wxGetApp().obj_list()->update_selections(); + }); + dlg.set_plate_name(from_u8(curr_plate->get_plate_name())); + + dlg.ShowModal(); + curr_plate->set_plate_name(dlg.get_plate_name().ToUTF8().data()); +} + +void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt) +{ + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + int value = evt.GetInt(); //1 means from gcode view + bool need_slice = value ==1; // If from gcode view, should slice + + const auto& project_config = wxGetApp().preset_bundle->project_config; + auto filament_colors = config()->option("filament_colour")->values; + auto filament_types = config()->option("filament_type")->values; + + auto plate_filament_maps = curr_plate->get_real_filament_maps(project_config); + auto plate_filament_map_mode = curr_plate->get_filament_map_mode(); + if (plate_filament_maps.size() != filament_colors.size()) // refine it later, save filament map to app config + plate_filament_maps.resize(filament_colors.size(), 1); + + FilamentMapDialog filament_dlg(this, + filament_colors, + filament_types, + plate_filament_maps, + curr_plate->get_extruders(true), + plate_filament_map_mode, + this->get_machine_sync_status(), + false + ); + + if (filament_dlg.ShowModal() == wxID_OK) { + std::vector new_filament_maps = filament_dlg.get_filament_maps(); + std::vector old_filament_maps = curr_plate->get_real_filament_maps(project_config); + + FilamentMapMode old_map_mode = curr_plate->get_filament_map_mode(); + FilamentMapMode new_map_mode = filament_dlg.get_mode(); + + if (new_map_mode != old_map_mode) { + curr_plate->set_filament_map_mode(new_map_mode); + } + + if (new_map_mode == fmmManual){ + curr_plate->set_filament_maps(new_filament_maps); + } + + bool need_invalidate = (old_map_mode != new_map_mode || + old_filament_maps != new_filament_maps); + + if (need_invalidate) { + if (need_slice) { + wxPostEvent(this, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); + } + else { + curr_plate->update_slice_result_valid_state(false); + set_plater_dirty(true); + update(); + } + } + } + return; +} + + +//BBS: select Plate by hover_id +int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModidyPlateName) +{ + int ret; + int action, plate_index; + + plate_index = hover_id / PartPlate::GRABBER_COUNT; + action = isModidyPlateName ? PartPlate::PLATE_NAME_HOVER_ID : hover_id % PartPlate::GRABBER_COUNT; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, hover_id %1%, plate_index %2%, action %3%")%hover_id % plate_index %action; + if (action == 0) + { + //select plate + ret = p->partplate_list.select_plate(plate_index); + if (!ret) { + SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); + p->on_plate_selected(event); + } + if ((!ret)&&(p->background_process.can_switch_print())) + { + //select successfully + p->partplate_list.update_slice_context_to_current_plate(p->background_process); + p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + p->update_print_volume_state(); + + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + bool result_valid = part_plate->is_slice_result_valid(); + PrintBase* print = nullptr; + GCodeResult* gcode_result = nullptr; + Print::ApplyStatus invalidated; + + const auto& preset_bundle = wxGetApp().preset_bundle; + + part_plate->get_print(&print, &gcode_result, NULL); + //always apply the current plate's print + if (preset_bundle->get_printer_extruder_count() > 1) { + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps)); + } + else + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); + bool model_fits, validate_err; + validate_current_plate(model_fits, validate_err); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: after apply, invalidated= %2%, previous result_valid %3% ")%__LINE__ % invalidated %result_valid; + if (result_valid) + { + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) + { + //bool model_fits, validate_err; + //validate_current_plate(model_fits, validate_err); + part_plate->update_slice_result_valid_state(false); + + // BBS + //p->show_action_buttons(true); + //p->ready_to_slice = true; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); + } + else + { + // BBS + //p->show_action_buttons(false); + //validate_current_plate(model_fits, validate_err); + //p->ready_to_slice = false; + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); + + reload_print(); + } + } + else + { + //check inside status + if (model_fits && !validate_err){ + p->process_completed_with_error = -1; + } + else { + p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); + } + + // BBS: don't show action buttons + //p->show_action_buttons(true); + //p->ready_to_slice = true; + if (model_fits && part_plate->has_printable_instances()) + { + //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": will set can_slice to true"); + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); + } + else + { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": will set can_slice to false, has_printable_instances %1%")%part_plate->has_printable_instances(); + //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); + } + } + } + } + else if ((action == 1)&&(!right_click)) + { + //delete plate + ret = delete_plate(plate_index); + } + else if ((action == 2)&&(!right_click)) + { + //arrange the plate + //take_snapshot("select_orient partplate"); + ret = select_plate(plate_index); + if (!ret) + { + set_prepare_state(Job::PREPARE_STATE_MENU); + orient(); + } + else + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } + else if ((action == 3)&&(!right_click)) + { + //arrange the plate + //take_snapshot("select_arrange partplate"); + ret = select_plate(plate_index); + if (!ret) + { + if (last_arrange_job_is_finished()) { + set_prepare_state(Job::PREPARE_STATE_MENU); + arrange(); + } + } + else + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } + else if ((action == 4)&&(!right_click)) + { + //lock the plate + take_snapshot("lock partplate"); + ret = p->partplate_list.lock_plate(plate_index, !p->partplate_list.is_locked(plate_index)); + } + else if ((action == 5)&&(!right_click)) + { + // set the plate type + ret = select_plate(plate_index); + if (!ret) { + wxCommandEvent evt(EVT_OPEN_PLATESETTINGSDIALOG); + evt.SetInt(plate_index); + evt.SetEventObject(this); + wxPostEvent(this, evt); + + this->schedule_background_process(); + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } + else if ((action == PartPlate::PLATE_FILAMENT_MAP_ID) && (!right_click)) { + ret = select_plate(plate_index); + if (!ret) { + PartPlate * curr_plate = p->partplate_list.get_curr_plate(); + wxCommandEvent evt(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG); + evt.SetInt(0); // 0 means not from gcodeviewer + evt.SetEventObject(this); + wxPostEvent(this, evt); + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } + else if ((action == 6) && (!right_click)) { + // set the plate type + ret = select_plate(plate_index); + if (!ret) { + PlateNameEditDialog dlg(this, wxID_ANY, _L("Edit Plate Name")); + PartPlate * curr_plate = p->partplate_list.get_curr_plate(); + + wxString curr_plate_name = from_u8(curr_plate->get_plate_name()); + dlg.set_plate_name(curr_plate_name); + + int result=dlg.ShowModal(); + if (result == wxID_YES) { + wxString dlg_plate_name = dlg.get_plate_name(); + curr_plate->set_plate_name(dlg_plate_name.ToUTF8().data()); + } + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } else if ((action == 7) && (!right_click)) { + // move plate to the front + take_snapshot("move plate to the front"); + ret = p->partplate_list.move_plate_to_index(plate_index,0); + p->partplate_list.update_slice_context_to_current_plate(p->background_process); + p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + p->sidebar->obj_list()->reload_all_plates(); + p->partplate_list.update_plates(); + update(); + p->partplate_list.select_plate(0); + } + + else + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click; + ret = -1; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: return %2%")%__LINE__ % ret; + return ret; +} + +int Plater::duplicate_plate(int plate_index) +{ + int index = plate_index, ret; + if (plate_index == -1) + index = p->partplate_list.get_curr_plate_index(); + + ret = p->partplate_list.duplicate_plate(index); + + //need to call update + update(); + return ret; +} + +//BBS: delete the plate, index= -1 means the current plate +int Plater::delete_plate(int plate_index) +{ + int index = plate_index, ret; + + if (plate_index == -1) + index = p->partplate_list.get_curr_plate_index(); + + take_snapshot("delete partplate"); + ret = p->partplate_list.delete_plate(index); + + //BBS: update the current print to the current plate + p->partplate_list.update_slice_context_to_current_plate(p->background_process); + p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + p->sidebar->obj_list()->reload_all_plates(); + + // BBS update default view + //get_camera().select_view("topfront"); + //get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + + //need to call update + update(); + return ret; +} + +//BBS: set bed positions +void Plater::set_bed_position(Vec2d& pos) +{ + p->bed.set_position(pos); +} + +//BBS: is the background process slicing currently +bool Plater::is_background_process_slicing() const +{ + return p->m_is_slicing; +} + +//BBS: update slicing context +void Plater::update_slicing_context_to_current_partplate() +{ + p->partplate_list.update_slice_context_to_current_plate(p->background_process); + p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); +} + +//BBS: show object info +void Plater::show_object_info() +{ + NotificationManager *notify_manager = get_notification_manager(); + const Selection& selection = get_selection(); + int selCount = selection.get_volume_idxs().size(); + ModelObjectPtrs objects = model().objects; + int obj_idx = selection.get_object_idx(); + std::string info_text; + + if (selCount > 1 && !selection.is_single_full_object()) { + notify_manager->bbl_close_objectsinfo_notification(); + if (selection.get_mode() == Selection::EMode::Volume) { + info_text += (boost::format(_utf8(L("Number of currently selected parts: %1%\n"))) % selCount).str(); + } else if (selection.get_mode() == Selection::EMode::Instance) { + int content_count = selection.get_content().size(); + info_text += (boost::format(_utf8(L("Number of currently selected objects: %1%\n"))) % content_count).str(); + } + notify_manager->bbl_show_objectsinfo_notification(info_text, false, !(p->current_panel == p->view3D)); + return; + } + else if (objects.empty() || (obj_idx < 0) || (obj_idx >= objects.size()) || + objects[obj_idx]->volumes.empty() ||// hack to avoid crash when deleting the last object on the bed + (selection.is_single_full_object() && objects[obj_idx]->instances.size()> 1) || + !(selection.is_single_full_instance() || selection.is_single_volume())) + { + notify_manager->bbl_close_objectsinfo_notification(); + return; + } + + const ModelObject* model_object = objects[obj_idx]; + int inst_idx = selection.get_instance_idx(); + if ((inst_idx < 0) || (inst_idx >= model_object->instances.size())) + { + notify_manager->bbl_close_objectsinfo_notification(); + return; + } + bool imperial_units = wxGetApp().app_config->get("use_inches") == "1"; + double koef = imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0f; + + ModelVolume* vol = nullptr; + Transform3d t; + int face_count; + Vec3d size; + if (selection.is_single_volume()) { + std::vector obj_idxs, vol_idxs; + wxGetApp().obj_list()->get_selection_indexes(obj_idxs, vol_idxs); + if (vol_idxs.size() != 1) + { + //corner case when merge/split/remove + return; + } + vol = model_object->volumes[vol_idxs[0]]; + t = model_object->instances[inst_idx]->get_matrix() * vol->get_matrix(); + info_text += (boost::format(_utf8(L("Part name: %1%\n"))) % vol->name).str(); + face_count = static_cast(vol->mesh().facets_count()); + size = vol->get_convex_hull().transformed_bounding_box(t).size(); + } + else { + //int obj_idx, vol_idx; + //wxGetApp().obj_list()->get_selected_item_indexes(obj_idx, vol_idx); + //if (obj_idx < 0) { + // //corner case when merge/split/remove + // return; + //} + info_text += (boost::format(_utf8(L("Object name: %1%\n"))) % model_object->name).str(); + face_count = static_cast(model_object->facets_count()); + size = model_object->instance_convex_hull_bounding_box(inst_idx).size(); + } + + //Vec3d size = vol ? vol->mesh().transformed_bounding_box(t).size() : model_object->instance_bounding_box(inst_idx).size(); + if (imperial_units) + info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% in\n"))) %(size(0)*koef) %(size(1)*koef) %(size(2)*koef)).str(); + else + info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% mm\n"))) %size(0) %size(1) %size(2)).str(); + + const TriangleMeshStats& stats = vol ? vol->mesh().stats() : model_object->get_object_stl_stats(); + double volume_val = stats.volume; + if (vol) + volume_val *= std::fabs(t.matrix().block(0, 0, 3, 3).determinant()); + volume_val = volume_val * pow(koef,3); + if (imperial_units) + info_text += (boost::format(_utf8(L("Volume: %1% in³\n"))) %volume_val).str(); + else + info_text += (boost::format(_utf8(L("Volume: %1% mm³\n"))) %volume_val).str(); + info_text += (boost::format(_utf8(L("Triangles: %1%\n"))) %face_count).str(); + + wxString info_manifold; + int non_manifold_edges = 0; + auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); + + if (non_manifold_edges > 0) { + info_manifold += into_u8("\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh.")); + } + + info_manifold = "" + info_manifold + ""; + info_text += into_u8(info_manifold); + notify_manager->bbl_show_objectsinfo_notification(info_text, non_manifold_edges > 0, !(p->current_panel == p->view3D)); +} + +bool Plater::show_publish_dialog(bool show) +{ + return p->show_publish_dlg(show); +} + +void Plater::post_process_string_object_exception(StringObjectException &err) +{ + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + if (err.type == StringExceptionType::STRING_EXCEPT_FILAMENT_NOT_MATCH_BED_TYPE) { + try { + int extruder_id = atoi(err.params[2].c_str()) - 1; + if (extruder_id < preset_bundle->filament_presets.size()) { + std::string filament_name = preset_bundle->filament_presets[extruder_id]; + // ORCA: Prefer the selected preset's alias/name and trim any @Printer suffix for display. + for (auto filament_it = preset_bundle->filaments.begin(); filament_it != preset_bundle->filaments.end(); filament_it++) { + if (filament_it->name == filament_name) { + if (!filament_it->alias.empty()) { + filament_name = filament_it->alias; + } else { + char target = '@'; + size_t pos = filament_name.find(target); + if (pos != std::string::npos) { + filament_name = filament_name.substr(0, pos - 1); + } + } + break; + } + } + err.string = format(_L("Plate %d: %s is not suggested to be used to print filament %s (%s). " + "If you still want to do this print job, please set this filament's bed temperature to non-zero."), + err.params[0], err.params[1], err.params[2], filament_name); + err.string += "\n"; + } + } catch (...) { + ; + } + } + + return; +} + +void Plater::update_objects_position_when_select_preset(const std::function &select_prest) +{ + p->update_objects_position_when_select_preset(select_prest); +} + +bool Plater::check_ams_status(bool is_slice_all) +{ + if (m_check_status == 0) { + if (!p->check_ams_status_impl(is_slice_all)) { + m_check_status = 0; + return false; + } + else { + m_check_status = 1; + } + } + + return true; +} + +void Plater::update_machine_sync_status() +{ + DeviceManager *dev_maneger = wxGetApp().getDeviceManager(); + if (!dev_maneger) { + GUI::wxGetApp().sidebar().update_sync_status(nullptr); + return; + } + MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); + GUI::wxGetApp().sidebar().update_sync_status(obj); +} + +bool Plater::get_machine_sync_status() +{ + return p->get_machine_sync_status(); +} + +#if ENABLE_ENVIRONMENT_MAP +void Plater::init_environment_texture() +{ + if (p->environment_texture.get_id() == 0) + p->environment_texture.load_from_file(resources_dir() + "/images/Pmetal_001.png", false, GLTexture::SingleThreaded, false); +} + +unsigned int Plater::get_environment_texture_id() const +{ + return p->environment_texture.get_id(); +} +#endif // ENABLE_ENVIRONMENT_MAP + +const BuildVolume& Plater::build_volume() const +{ + return p->bed.build_volume(); +} + +// BBS +#if 0 +const GLToolbar& Plater::get_view_toolbar() const +{ + return p->view_toolbar; +} + +GLToolbar& Plater::get_view_toolbar() +{ + return p->view_toolbar; +} +#endif + +const GLToolbar& Plater::get_collapse_toolbar() const +{ + return p->collapse_toolbar; +} + +GLToolbar& Plater::get_collapse_toolbar() +{ + return p->collapse_toolbar; +} + +void Plater::update_preview_bottom_toolbar() +{ + p->update_preview_bottom_toolbar(); +} + +void Plater::reset_gcode_toolpaths() +{ + //BBS: add some logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": reset the gcode viewer's toolpaths"); + p->reset_gcode_toolpaths(); +} + +const Mouse3DController& Plater::get_mouse3d_controller() const +{ + return p->mouse3d_controller; +} + +Mouse3DController& Plater::get_mouse3d_controller() +{ + return p->mouse3d_controller; +} + +NotificationManager * Plater::get_notification_manager() +{ + return p->notification_manager.get(); +} + +DailyTipsWindow* Plater::get_dailytips() const +{ + static DailyTipsWindow* dailytips_win = new DailyTipsWindow(); + return dailytips_win; +} + +const NotificationManager * Plater::get_notification_manager() const +{ + return p->notification_manager.get(); +} + +void Plater::init_notification_manager() +{ + p->init_notification_manager(); +} + +void Plater::show_status_message(std::string s) +{ + BOOST_LOG_TRIVIAL(trace) << "show_status_message:" << s; +} + +bool Plater::can_delete() const { return p->can_delete(); } +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_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_increase_instances() const { return p->can_increase_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_fix_through_cgal() const { return p->can_fix_through_cgal(); } +bool Plater::can_simplify() const { return p->can_simplify(); } +bool Plater::can_smooth_mesh() const { return p->can_smooth_mesh(); } +bool Plater::can_split_to_objects() const { return p->can_split_to_objects(); } +bool Plater::can_split_to_volumes() const { return p->can_split_to_volumes(); } +bool Plater::can_arrange() const { return p->can_arrange(); } +bool Plater::can_layers_editing() const { return p->can_layers_editing(); } +bool Plater::can_paste_from_clipboard() const +{ + if (!IsShown() || !p->is_view3D_shown()) return false; + + const Selection& selection = p->view3D->get_canvas3d()->get_selection(); + const Selection::Clipboard& clipboard = selection.get_clipboard(); + + if (clipboard.is_empty() && p->sidebar->obj_list()->clipboard_is_empty()) + return false; + + if ((wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) && !clipboard.is_sla_compliant()) + return false; + + Selection::EMode mode = clipboard.get_mode(); + if ((mode == Selection::Volume) && !selection.is_from_single_instance()) + return false; + + if ((mode == Selection::Instance) && (selection.get_mode() != Selection::Instance)) + return false; + + return true; +} + +//BBS support cut +bool Plater::can_cut_to_clipboard() const +{ + if (is_selection_empty()) + return false; + return true; +} + +bool Plater::can_copy_to_clipboard() const +{ + if (!IsShown() || !p->is_view3D_shown()) + return false; + + if (is_selection_empty()) + return false; + + const Selection& selection = p->view3D->get_canvas3d()->get_selection(); + if ((wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) && !selection.is_sla_compliant()) + return false; + + return true; +} + +bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && 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_reload_from_disk() const { return p->can_reload_from_disk(); } +//BBS +bool Plater::can_fillcolor() const { return p->can_fillcolor(); } +bool Plater::has_assmeble_view() const { return p->has_assemble_view(); } +bool Plater::can_replace_with_stl() const { return p->can_replace_with_stl(); } +bool Plater::can_replace_all_with_stl() const { return p->can_replace_all_with_stl(); } +bool Plater::can_mirror() const { return p->can_mirror(); } +bool Plater::can_split(bool to_objects) const { return p->can_split(to_objects); } +#if ENABLE_ENHANCED_PRINT_VOLUME_FIT +bool Plater::can_scale_to_print_volume() const { return p->can_scale_to_print_volume(); } +#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT + +const UndoRedo::Stack& Plater::undo_redo_stack_main() const { return p->undo_redo_stack_main(); } +void Plater::clear_undo_redo_stack_main() { p->undo_redo_stack_main().clear(); } +void Plater::enter_gizmos_stack() { p->enter_gizmos_stack(); } +bool Plater::leave_gizmos_stack() { return p->leave_gizmos_stack(); } // BBS: return false if not changed +bool Plater::inside_snapshot_capture() { return p->inside_snapshot_capture(); } + +void Plater::toggle_render_statistic_dialog() +{ + p->show_render_statistic_dialog = !p->show_render_statistic_dialog; +} + +bool Plater::is_render_statistic_dialog_visible() const +{ + return p->show_render_statistic_dialog; +} + +void Plater::toggle_show_wireframe() +{ + p->show_wireframe = !p->show_wireframe; +} + +bool Plater::is_show_wireframe() const +{ + return p->show_wireframe; +} + +void Plater::enable_wireframe(bool status) +{ + p->wireframe_enabled = status; +} + +bool Plater::is_wireframe_enabled() const +{ + return p->wireframe_enabled; +} + + +/*Plater::TakeSnapshot::TakeSnapshot(Plater *plater, const std::string &snapshot_name) +: TakeSnapshot(plater, from_u8(snapshot_name)) {} +Plater::TakeSnapshot::TakeSnapshot(Plater* plater, const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type) +: TakeSnapshot(plater, from_u8(snapshot_name), snapshot_type) {}*/ + + +// Wrapper around wxWindow::PopupMenu to suppress error messages popping out while tracking the popup menu. +bool Plater::PopupMenu(wxMenu *menu, const wxPoint& pos) +{ + // Don't want to wake up and trigger reslicing while tracking the pop-up menu. + SuppressBackgroundProcessingUpdate sbpu; + // When tracking a pop-up menu, postpone error messages from the slicing result. + m_tracking_popup_menu = true; + bool out = wxGetApp().mainframe->PopupMenu(menu, pos); + m_tracking_popup_menu = false; + if (! m_tracking_popup_menu_error_message.empty()) { + // Don't know whether the CallAfter is necessary, but it should not hurt. + // The menus likely sends out some commands, so we may be safer if the dialog is shown after the menu command is processed. + wxString message = std::move(m_tracking_popup_menu_error_message); + wxTheApp->CallAfter([message, this]() { show_error(this, message); }); + m_tracking_popup_menu_error_message.clear(); + } + return out; +} +void Plater::bring_instance_forward() +{ + p->bring_instance_forward(); +} + +bool Plater::need_update() const +{ + return p->need_update(); +} + +void Plater::set_need_update(bool need_update) +{ + p->set_need_update(need_update); +} + +// BBS +//BBS: add popup logic for table object +bool Plater::PopupObjectTable(int object_id, int volume_id, const wxPoint& position) +{ + if (dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER))->m_extruders_count > 1) { + MessageDialog dlg(this, _L("Currently, the object configuration form cannot be used with a multiple-extruder printer."), _L("Not available"), wxOK | wxICON_WARNING); + dlg.ShowModal(); + return false; + } + return p->PopupObjectTable(object_id, volume_id, position); +} + +bool Plater::PopupObjectTableBySelection() +{ + wxDataViewItem item; + int obj_idx, vol_idx; + const wxPoint pos = wxPoint(0, 0); //Fake position + wxGetApp().obj_list()->get_selected_item_indexes(obj_idx, vol_idx, item); + return p->PopupObjectTable(obj_idx, vol_idx, pos); +} + +void Plater::update_title_dirty_status() +{ + p->update_title_dirty_status(); +} + + +wxMenu* Plater::plate_menu() { return p->menus.plate_menu(); } +wxMenu* Plater::object_menu() { return p->menus.object_menu(); } +wxMenu* Plater::part_menu() { return p->menus.part_menu(); } +wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } +wxMenu* Plater::svg_part_menu() { return p->menus.svg_part_menu(); } +wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } +wxMenu* Plater::default_menu() { return p->menus.default_menu(); } +wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } +wxMenu* Plater::layer_menu() { return p->menus.layer_menu(); } +wxMenu* Plater::multi_selection_menu() { return p->menus.multi_selection_menu(); } +wxMenu *Plater::filament_action_menu(int active_filament_menu_id) { return p->menus.filament_action_menu(active_filament_menu_id); } +int Plater::GetPlateIndexByRightMenuInLeftUI() { return p->m_is_RightClickInLeftUI; } +void Plater::SetPlateIndexByRightMenuInLeftUI(int index) { p->m_is_RightClickInLeftUI = index; } +SuppressBackgroundProcessingUpdate::SuppressBackgroundProcessingUpdate() : + m_was_scheduled(wxGetApp().plater()->is_background_process_update_scheduled()) +{ + wxGetApp().plater()->suppress_background_process(m_was_scheduled); +} + +SuppressBackgroundProcessingUpdate::~SuppressBackgroundProcessingUpdate() +{ + wxGetApp().plater()->schedule_background_process(m_was_scheduled); +} +wxString get_view_type_string(Camera::ViewAngleType type) { + switch (type) { + case Slic3r::GUI::Camera::ViewAngleType::Iso: return _L("isometric"); + case Slic3r::GUI::Camera::ViewAngleType::Top_Front: return _L("top_front"); + case Slic3r::GUI::Camera::ViewAngleType::Left: return _L("left"); + case Slic3r::GUI::Camera::ViewAngleType::Right: return _L("right"); + case Slic3r::GUI::Camera::ViewAngleType::Top: return _L("top"); + case Slic3r::GUI::Camera::ViewAngleType::Bottom: return _L("bottom"); + case Slic3r::GUI::Camera::ViewAngleType::Front: return _L("front"); + case Slic3r::GUI::Camera::ViewAngleType::Rear: return _L("rear"); + default: return ""; + } +} +wxArrayString get_all_camera_view_type() { + wxArrayString all_types; + for (size_t i = 0; i < (int)Camera::ViewAngleType::Count_ViewAngleType; i++) { + all_types.Add(get_view_type_string((Camera::ViewAngleType) i)); + } + return all_types; +} +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp new file mode 100644 index 0000000000..0f35e7ab93 --- /dev/null +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -0,0 +1,2102 @@ +#include "PresetComboBoxes.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#endif + +#include "libslic3r/libslic3r.h" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Color.hpp" + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "MainFrame.hpp" +#include "format.hpp" +#include "Tab.hpp" +#include "ConfigWizard.hpp" +#include "../Utils/ASCIIFolding.hpp" +#include "../Utils/UndoRedo.hpp" +#include "../Utils/ColorSpaceConvert.hpp" +#include "BitmapCache.hpp" +#include "SavePresetDialog.hpp" +#include "MsgDialog.hpp" +#include "ParamsDialog.hpp" +#include "FilamentPickerDialog.hpp" +#include "wxExtensions.hpp" + +#include "DeviceCore/DevManager.h" + +// A workaround for a set of issues related to text fitting into gtk widgets: +#if defined(__WXGTK20__) || defined(__WXGTK3__) + #include + #include + #include +#endif + +using Slic3r::GUI::format_wxstr; + +namespace Slic3r { +namespace GUI { + +#define BORDER_W 10 + +// --------------------------------- +// *** PresetComboBox *** +// --------------------------------- + +/* For PresetComboBox we use bitmaps that are created from images that are already scaled appropriately for Retina + * (Contrary to the intuition, the `scale` argument for Bitmap's constructor doesn't mean + * "please scale this to such and such" but rather + * "the wxImage is already sized for backing scale such and such". ) + * Unfortunately, the constructor changes the size of wxBitmap too. + * Thus We need to use unscaled size value for bitmaps that we use + * to avoid scaled size of control items. + * For this purpose control drawing methods and + * control size calculation methods (virtual) are overridden. + **/ + +PresetComboBox::PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size, PresetBundle* preset_bundle/* = nullptr*/) : + ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, 0, nullptr, wxCB_READONLY), + m_type(preset_type), + m_last_selected(wxNOT_FOUND), + m_em_unit(em_unit(this)), + m_preset_bundle(preset_bundle ? preset_bundle : wxGetApp().preset_bundle) +{ +#ifdef __WXMSW__ + if (preset_type == Preset::TYPE_FILAMENT) + SetFont(Label::Body_13); +#endif // __WXMSW__ + + switch (m_type) + { + case Preset::TYPE_PRINT: { + m_collection = &m_preset_bundle->prints; + m_main_bitmap_name = "cog"; + break; + } + case Preset::TYPE_FILAMENT: { + m_collection = &m_preset_bundle->filaments; + m_main_bitmap_name = "spool"; + break; + } + case Preset::TYPE_SLA_PRINT: { + m_collection = &m_preset_bundle->sla_prints; + m_main_bitmap_name = "cog"; + break; + } + case Preset::TYPE_SLA_MATERIAL: { + m_collection = &m_preset_bundle->sla_materials; + m_main_bitmap_name = "blank_16"; + break; + } + case Preset::TYPE_PRINTER: { + m_collection = &m_preset_bundle->printers; + m_main_bitmap_name = "printer"; + break; + } + default: break; + } + + m_bitmapCompatible = ScalableBitmap(this, "flag_green"); + m_bitmapIncompatible = ScalableBitmap(this, "flag_red"); + + // parameters for an icon's drawing + fill_width_height(); + Bind(wxEVT_COMBOBOX, &PresetComboBox::OnSelect, this); +} + +void PresetComboBox::OnSelect(wxCommandEvent& evt) +{ + // Under OSX: in case of use of a same names written in different case (like "ENDER" and "Ender") + // m_presets_choice->GetSelection() will return first item, because search in PopupListCtrl is case-insensitive. + // So, use GetSelection() from event parameter + auto selected_item = evt.GetSelection(); + + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + if (marker >= LABEL_ITEM_DISABLED && marker < LABEL_ITEM_MAX) + this->SetSelection(m_last_selected); + else if (on_selection_changed && (m_last_selected != selected_item || m_collection->current_is_dirty())) { + m_last_selected = selected_item; + on_selection_changed(selected_item); + evt.StopPropagation(); + } + evt.Skip(); +} + +PresetComboBox::~PresetComboBox() +{ +} + +BitmapCache& PresetComboBox::bitmap_cache() +{ + static BitmapCache bmps; + return bmps; +} + +void PresetComboBox::set_label_marker(int item, LabelItemType label_item_type) +{ + this->SetClientData(item, (void*)label_item_type); +} + +bool PresetComboBox::set_printer_technology(PrinterTechnology pt) +{ + if (printer_technology != pt) { + printer_technology = pt; + return true; + } + return false; +} + +void PresetComboBox::invalidate_selection() +{ + m_last_selected = INT_MAX; // this value means that no one item is selected +} + +void PresetComboBox::validate_selection(bool predicate/*=false*/) +{ + if (predicate && + // just in case: mark m_last_selected as a first added element + m_last_selected == INT_MAX) + m_last_selected = GetCount() - 1; +} + +void PresetComboBox::update_selection() +{ + /* If selected_preset_item is still equal to INT_MAX, it means that + * there is no presets added to the list. + * So, select last combobox item ("Add/Remove preset") + */ + //validate_selection(); + if (m_last_selected == INT_MAX) + m_last_selected = 1; + + SetSelection(m_last_selected); +#ifdef __WXMSW__ + // From the Windows 2004 the tooltip for preset combobox doesn't work after next call of SetTooltip() + // (There was an issue, when tooltip doesn't appears after changing of the preset selection) + // But this workaround seems to work: We should to kill tooltip and than set new tooltip value + SetToolTip(NULL); +#endif + SetToolTip(GetString(m_last_selected)); + +// A workaround for a set of issues related to text fitting into gtk widgets: +#if defined(__WXGTK20__) || defined(__WXGTK3__) + GtkWidget* widget = m_widget; + if (GTK_IS_CONTAINER(widget)) { + GList* children = gtk_container_get_children(GTK_CONTAINER(widget)); + if (children) { + widget = GTK_WIDGET(children->data); + g_list_free(children); + } + } + if (GTK_IS_ENTRY(widget)) { + // Set ellipsization for the entry + gtk_entry_set_width_chars(GTK_ENTRY(widget), 20); // Adjust this value as needed + gtk_entry_set_max_width_chars(GTK_ENTRY(widget), 20); // Adjust this value as needed + // Create a PangoLayout for the entry and set ellipsization + PangoLayout* layout = gtk_entry_get_layout(GTK_ENTRY(widget)); + if (layout) { + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + } else { + g_warning("Unable to get PangoLayout from GtkEntry"); + } + } else { + g_warning("Expected GtkEntry, but got %s", G_OBJECT_TYPE_NAME(widget)); + } +#endif +} + +int PresetComboBox::update_ams_color() +{ + if (m_filament_idx < 0) return -1; + int idx = selected_ams_filament(); + std::string color; + std::string ctype; + std::vector colors; + if (idx < 0) { + auto name = Preset::remove_suffix_modified(GetValue().ToUTF8().data()); + auto *preset = m_collection->find_preset(name); + if (preset) + color = preset->config.opt_string("default_filament_colour", 0u); + if (color.empty()) return -1; + } else { + auto &ams_list = wxGetApp().preset_bundle->filament_ams_list; + auto iter = ams_list.find(idx); + if (iter == ams_list.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": ams %1% out of range %2%") % idx % ams_list.size(); + return -1; + } + color = iter->second.opt_string("filament_colour", 0u); + ctype = iter->second.opt_string("filament_colour_type", 0u); + colors = iter->second.opt("filament_multi_colour")->values; + } + DynamicPrintConfig *cfg = &wxGetApp().preset_bundle->project_config; + auto color_head = static_cast(cfg->option("filament_colour")->clone()); // single color (the first color if multi-color filament) + auto color_pack = static_cast(cfg->option("filament_multi_colour")->clone()); // multi color (all colors in all kinds of filament) + auto color_type = static_cast(cfg->option("filament_colour_type")->clone()); // color type + + if (m_filament_idx >= color_head->values.size()) color_head->values.resize(m_filament_idx + 1); + if (m_filament_idx >= color_pack->values.size()) color_pack->values.resize(m_filament_idx + 1); + if (m_filament_idx >= color_type->values.size()) color_type->values.resize(m_filament_idx + 1); + if (ctype.empty()) ctype = "1"; + + color_head->values[m_filament_idx] = color; + color_type->values[m_filament_idx] = ctype; + std::string color_str = ""; // Translate multi color info to config storage format + for (auto &c : colors) { + if (c.empty()) continue; + color_str += c + " "; + } + if (color_str.empty()) color_str = color; + else color_str.erase(color_str.size() - 1); + color_pack->values[m_filament_idx] = color_str; + + // Update color informations in config + DynamicPrintConfig new_cfg; + new_cfg.set_key_value("filament_colour", color_head); + new_cfg.set_key_value("filament_colour_type", color_type); + new_cfg.set_key_value("filament_multi_colour", color_pack); + cfg->apply(new_cfg); + wxGetApp().plater()->on_config_change(new_cfg); + //trigger the filament color changed + wxCommandEvent *evt = new wxCommandEvent(EVT_FILAMENT_COLOR_CHANGED); + evt->SetInt(m_filament_idx); + wxQueueEvent(wxGetApp().plater(), evt); + return idx; +} + +wxColor PresetComboBox::different_color(wxColor const &clr) +{ + if (clr.GetLuminance() < 0.51) return *wxWHITE; + return *wxBLACK; +} + +wxString PresetComboBox::get_tooltip(const Preset &preset) +{ + wxString tooltip = from_u8(preset.name); + + // Add filament notes if available for filament presets + if (m_type == Preset::TYPE_FILAMENT) { + const DynamicConfig* config = &preset.config; + Tab* tab = wxGetApp().get_tab(m_type); + if (tab && tab->current_preset_is_dirty() && tab->get_presets()->get_selected_preset().name == preset.name) { + config = tab->get_config(); + } + + if (config->has("filament_notes")) { + const ConfigOptionStrings* notes_opt = config->option("filament_notes"); + if (notes_opt && !notes_opt->values.empty() && !notes_opt->values[0].empty()) { + std::string notes = notes_opt->values[0]; + // Truncate if longer than 200 characters + if (notes.length() > 200) { + notes = notes.substr(0, 197) + "..."; + } + tooltip += "\n" + from_u8(notes); + } + } + } + + // BBS: FIXME +#if 0 + if (m_type == Preset::TYPE_FILAMENT) { + int temperature[4] = { 0,0,0,0 }; + if (preset.config.has("nozzle_temperature_initial_layer")) //get the nozzle_temperature_initial_layer + temperature[0] = preset.config.opt_int("nozzle_temperature_initial_layer", 0); + if (preset.config.has("nozzle_temperature")) //get the nozzle temperature + temperature[1] = preset.config.opt_int("nozzle_temperature", 0); + if (preset.config.has("bed_temperature_initial_layer")) //get the bed_temperature_initial_layer + temperature[2] = preset.config.opt_int("bed_temperature_initial_layer", 0); + if (preset.config.has("bed_temperature")) //get the bed_temperature + temperature[3] = preset.config.opt_int("bed_temperature", 0); + + tooltip += wxString::Format("\nNozzle First Layer:%d, Other Layer:%d\n Bed First Layer:%d, Other Layers:%d", + temperature[0], temperature[1], temperature[2], temperature[3]); + } +#endif + return tooltip; +} + +wxString PresetComboBox::get_preset_item_name(unsigned int index) +{ + if (m_type == Preset::TYPE_PRINTER) { + int idx = selected_connected_printer(); + if (idx < 0) { + m_selected_dev_id.clear(); + return GetString(index); + } + else { + DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) { + assert(false); + m_selected_dev_id.clear(); + return GetString(index); + } + + std::map machine_list = dev->get_my_machine_list(); + if (machine_list.empty()) { + assert(false); + m_selected_dev_id.clear(); + return GetString(index); + } + + auto iter = machine_list.begin(); + std::advance(iter, idx); + if (iter != machine_list.end()) { + m_selected_dev_id = iter->first; + Preset* machine_preset = get_printer_preset(iter->second); + if (machine_preset) { + return from_u8(machine_preset->name); + } + } + } + } + + m_selected_dev_id.clear(); + return GetString(index); +} + +wxString PresetComboBox::get_preset_name(const Preset & preset) +{ + return from_u8(preset.name/* + suffix(preset)*/); +} + +void PresetComboBox::update(std::string select_preset_name) +{ + Freeze(); + Clear(); + invalidate_selection(); + + const std::deque& presets = m_collection->get_presets(); + + std::map> nonsys_presets; + std::map incomp_presets; + + wxString selected = ""; + if (!presets.front().is_visible) + set_label_marker(Append(_L("System presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + + for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) + { + const Preset& preset = presets[i]; + if (!m_show_all && (!preset.is_visible || !preset.is_compatible)) + continue; + + // marker used for disable incompatible printer models for the selected physical printer + bool is_enabled = m_type == Preset::TYPE_PRINTER && printer_technology != ptAny ? preset.printer_technology() == printer_technology : true; + if (select_preset_name.empty() && is_enabled) + select_preset_name = preset.name; + + wxBitmap* bmp = get_bmp(preset); + assert(bmp); + + if (!is_enabled) + incomp_presets.emplace(get_preset_name(preset), bmp); + else if (preset.is_default || preset.is_system) + { + Append(get_preset_name(preset), *bmp); + validate_selection(preset.name == select_preset_name); + } + else + { + nonsys_presets.emplace(get_preset_name(preset), std::pair(bmp, is_enabled)); + if (preset.name == select_preset_name || (select_preset_name.empty() && is_enabled)) + selected = get_preset_name(preset); + } + if (i + 1 == m_collection->num_default_presets()) + set_label_marker(Append(_L("System presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + } + if (!nonsys_presets.empty()) + { + set_label_marker(Append(_L("User presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + int item_id = Append(it->first, *it->second.first); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + if (!incomp_presets.empty()) + { + set_label_marker(Append(_L("Incompatible presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + for (std::map::iterator it = incomp_presets.begin(); it != incomp_presets.end(); ++it) { + set_label_marker(Append(it->first, *it->second), LABEL_ITEM_DISABLED); + } + } + + update_selection(); + Thaw(); +} + +void PresetComboBox::show_all(bool show_all) +{ + m_show_all = show_all; + update(); +} + +void PresetComboBox::update() +{ + this->update(into_u8(this->GetString(this->GetSelection()))); +} + +void PresetComboBox::update_from_bundle() +{ + this->update(m_collection->get_selected_preset().name); +} + +void PresetComboBox::add_connected_printers(std::string selected, bool alias_name) +{ + DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) + return; + + std::map machine_list = dev->get_my_machine_list(); + if (machine_list.empty()) + return; + + set_label_marker(Append(_L("My Printer"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + m_first_printer_idx = GetCount(); + for (auto iter = machine_list.begin(); iter != machine_list.end(); ++iter) { + Preset* printer_preset = get_printer_preset(iter->second); + if (!printer_preset) + continue; + printer_preset->is_visible = true; + auto printer_model = printer_preset->config.opt_string("printer_model"); + boost::replace_all(printer_model, "Bambu Lab ", ""); + auto text = iter->second->get_dev_name() + " (" + printer_model + ")"; + int item_id = Append(from_u8(text), wxNullBitmap, &m_first_printer_idx + std::distance(machine_list.begin(), iter)); + validate_selection(m_selected_dev_id == iter->first); + } + m_last_printer_idx = GetCount(); +} + +int PresetComboBox::selected_connected_printer() const +{ + if (m_first_printer_idx && m_last_selected >= m_first_printer_idx && m_last_selected < m_last_printer_idx) { + return reinterpret_cast(GetClientData(m_last_selected)) - &m_first_printer_idx; + } + return -1; +} + +bool PresetComboBox::add_ams_filaments(std::string selected, bool alias_name) +{ + bool selected_in_ams = false; + bool is_bbl_vendor_preset = m_preset_bundle->is_bbl_vendor(); + if (is_bbl_vendor_preset && !m_preset_bundle->filament_ams_list.empty()) { + bool dual_extruder = (m_preset_bundle->filament_ams_list.begin()->first & 0x10000) == 0; + set_label_marker(Append(dual_extruder ? _L("Left filaments") : _L("AMS filaments"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + m_first_ams_filament = GetCount(); + auto &filaments = m_collection->get_presets(); + + int icon_width = 24; + for (auto &entry : m_preset_bundle->filament_ams_list) { + auto & tray = entry.second; + auto name = tray.opt_string("tray_name", 0u); + if (name.size() > 3) + icon_width = 32; + } + + for (auto &entry : m_preset_bundle->filament_ams_list) { + if (dual_extruder && (entry.first & 0x10000)) { + dual_extruder = false; + set_label_marker(Append(_L("Right filaments"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + } + auto & tray = entry.second; + std::string filament_id = tray.opt_string("filament_id", 0u); + auto name = tray.opt_string("tray_name", 0u); + if (filament_id.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% 's filament_id is empty.") % name; + continue; + } + auto iter = std::find_if(filaments.begin(), filaments.end(), + [&filament_id, this](auto &f) { return f.is_compatible && m_collection->get_preset_base(f) == &f && f.filament_id == filament_id; }); + if (iter == filaments.end()) { + auto filament_type = tray.opt_string("filament_type", 0u); + if (!filament_type.empty()) { + filament_type = "Generic " + filament_type; + iter = std::find_if(filaments.begin(), filaments.end(), + [&filament_type](auto &f) { return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); }); + } + } + if (iter == filaments.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; + continue; + } + const_cast(*iter).is_visible = true; + auto color = tray.opt_string("filament_colour", 0u); + auto multi_color = tray.opt("filament_multi_colour")->values; + wxBitmap bmp(*get_extruder_color_icon(color, name, icon_width, 16)); + auto text = get_preset_name(*iter); + int item_id = Append(text, bmp.ConvertToImage(), &m_first_ams_filament + entry.first); + SetFlag(GetCount() - 1, (int) FilamentAMSType::FROM_AMS); + if (text == selected) { + DynamicPrintConfig *cfg = &wxGetApp().preset_bundle->project_config; + if (cfg) { + auto colors = static_cast(cfg->option("filament_colour")->clone()); + if (m_filament_idx < colors->values.size()) { + auto cur_color = colors->values[m_filament_idx]; + if (color == cur_color) { + selected_in_ams = true; + } + } + } + } + //validate_selection(id->value == selected); // can not select + } + m_last_ams_filament = GetCount(); + } + return selected_in_ams; +} + +int PresetComboBox::selected_ams_filament() const +{ + if (m_first_ams_filament && m_last_selected >= m_first_ams_filament && m_last_selected < m_last_ams_filament) { + return reinterpret_cast(GetClientData(m_last_selected)) - &m_first_ams_filament; + } + return -1; +} + +void PresetComboBox::msw_rescale() +{ + m_em_unit = em_unit(this); + Rescale(); + + m_bitmapIncompatible.msw_rescale(); + m_bitmapCompatible.msw_rescale(); + + // parameters for an icon's drawing + fill_width_height(); + + // update the control to redraw the icons + update(); +} + +void PresetComboBox::sys_color_changed() +{ + wxGetApp().UpdateDarkUI(this); + msw_rescale(); +} + +void PresetComboBox::fill_width_height() +{ + // To avoid asserts, each added bitmap to wxBitmapCombobox should be the same size, so + // set a bitmap's height to m_bitmapCompatible->GetHeight() and norm_icon_width to m_bitmapCompatible->GetWidth() + icon_height = m_bitmapCompatible.GetBmpHeight(); + norm_icon_width = m_bitmapCompatible.GetBmpWidth(); + + /* It's supposed that standard size of an icon is 16px*16px for 100% scaled display. + * So set sizes for solid_colored icons used for filament preset + * and scale them in respect to em_unit value + */ + const float scale_f = (float)m_em_unit * 0.1f; + + thin_icon_width = lroundf(8 * scale_f); // analogue to 8px; + wide_icon_width = norm_icon_width + thin_icon_width; + + space_icon_width = lroundf(2 * scale_f); + thin_space_icon_width = lroundf(4 * scale_f); + wide_space_icon_width = lroundf(6 * scale_f); +} + +wxString PresetComboBox::separator(const std::string& label) +{ + return wxString::FromUTF8(separator_head()) + _(label) + wxString::FromUTF8(separator_tail()); +} + +wxBitmap* PresetComboBox::get_bmp( std::string bitmap_key, bool wide_icons, const std::string& main_icon_name, + bool is_compatible/* = true*/, bool is_system/* = false*/, bool is_single_bar/* = false*/, + const std::string& filament_rgb/* = ""*/, const std::string& extruder_rgb/* = ""*/, const std::string& material_rgb/* = ""*/) +{ + // BBS: no icon +#if 1 + static wxBitmap bmp; + return &bmp; +#else + // If the filament preset is not compatible and there is a "red flag" icon loaded, show it left + // to the filament color image. + if (wide_icons) + bitmap_key += is_compatible ? ",cmpt" : ",ncmpt"; + + bitmap_key += is_system ? ",syst" : ",nsyst"; + bitmap_key += ",h" + std::to_string(icon_height); + bool dark_mode = wxGetApp().dark_mode(); + if (dark_mode) + bitmap_key += ",dark"; + bitmap_key += material_rgb; + + wxBitmap* bmp = bitmap_cache().find(bitmap_key); + if (bmp == nullptr) { + // Create the bitmap with color bars. + std::vector bmps; + if (wide_icons) + // Paint a red flag for incompatible presets. + bmps.emplace_back(is_compatible ? bitmap_cache().mkclear(norm_icon_width, icon_height) : m_bitmapIncompatible.bmp()); + + if (m_type == Preset::TYPE_FILAMENT && !filament_rgb.empty()) + { + // BBS + // Paint a lock at the system presets. + bmps.emplace_back(bitmap_cache().mkclear(space_icon_width, icon_height)); + } + else + { + // BBS +#if 0 + // Paint the color bars. + bmps.emplace_back(bitmap_cache().mkclear(thin_space_icon_width, icon_height)); + if (m_type == Preset::TYPE_SLA_MATERIAL) + bmps.emplace_back(create_scaled_bitmap(main_icon_name, this, 16, false, material_rgb)); + else + bmps.emplace_back(create_scaled_bitmap(main_icon_name)); +#endif + // Paint a lock at the system presets. + bmps.emplace_back(bitmap_cache().mkclear(wide_space_icon_width, icon_height)); + } + bmps.emplace_back(is_system ? create_scaled_bitmap("unlock_normal") : bitmap_cache().mkclear(norm_icon_width, icon_height)); + bmp = bitmap_cache().insert(bitmap_key, bmps); + } + + return bmp; +#endif +} + +wxBitmap *PresetComboBox::get_bmp(Preset const &preset) +{ + static wxBitmap sbmp; + if (m_type == Preset::TYPE_FILAMENT) { + Preset const & preset2 = &m_collection->get_selected_preset() == &preset ? m_collection->get_edited_preset() : preset; + wxString color = preset2.config.opt_string("default_filament_colour", 0); + wxColour clr(color); + if (clr.IsOk()) { + std::string bitmap_key = "default_filament_colour_" + color.ToStdString(); + wxBitmap *bmp = bitmap_cache().find(bitmap_key); + if (bmp == nullptr) { + wxImage img(16, 16); + if (clr.Red() > 224 && clr.Blue() > 224 && clr.Green() > 224) { + img.SetRGB(wxRect({0, 0}, img.GetSize()), 128, 128, 128); + img.SetRGB(wxRect({1, 1}, img.GetSize() - wxSize{2, 2}), clr.Red(), clr.Green(), clr.Blue()); + } else { + img.SetRGB(wxRect({0, 0}, img.GetSize()), clr.Red(), clr.Green(), clr.Blue()); + } + bmp = new wxBitmap(img); + bmp = bitmap_cache().insert(bitmap_key, *bmp); + } + return bmp; + } + } + return &sbmp; +} + +wxBitmap *PresetComboBox::get_bmp(std::string bitmap_key, + const std::string &main_icon_name, + const std::string &next_icon_name, + bool is_enabled/* = true*/, bool is_compatible/* = true*/, bool is_system/* = false*/) +{ + // BBS: no icon +#if 1 + static wxBitmap bmp; + return &bmp; +#else + bitmap_key += !is_enabled ? "_disabled" : ""; + bitmap_key += is_compatible ? ",cmpt" : ",ncmpt"; + bitmap_key += is_system ? ",syst" : ",nsyst"; + bitmap_key += ",h" + std::to_string(icon_height); + if (wxGetApp().dark_mode()) + bitmap_key += ",dark"; + + wxBitmap* bmp = bitmap_cache().find(bitmap_key); + if (bmp == nullptr) { + // Create the bitmap with color bars. + std::vector bmps; + bmps.emplace_back(m_type == Preset::TYPE_PRINTER ? create_scaled_bitmap(main_icon_name, this, 16, !is_enabled) : + is_compatible ? m_bitmapCompatible.bmp() : m_bitmapIncompatible.bmp()); + // Paint a lock at the system presets. + bmps.emplace_back(is_system ? create_scaled_bitmap(next_icon_name, this, 16, !is_enabled) : bitmap_cache().mkclear(norm_icon_width, icon_height)); + bmp = bitmap_cache().insert(bitmap_key, bmps); + } + + return bmp; +#endif +} + +bool PresetComboBox::is_selected_physical_printer() +{ + auto selected_item = this->GetSelection(); + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + return marker == LABEL_ITEM_PHYSICAL_PRINTER; +} + +bool PresetComboBox::is_selected_printer_model() +{ + auto selected_item = this->GetSelection(); + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + return marker == LABEL_ITEM_PRINTER_MODELS; +} + +bool PresetComboBox::selection_is_changed_according_to_physical_printers() +{ + if (m_type != Preset::TYPE_PRINTER || !is_selected_physical_printer()) + return false; + + PhysicalPrinterCollection& physical_printers = m_preset_bundle->physical_printers; + + std::string selected_string = this->GetString(this->GetSelection()).ToUTF8().data(); + + std::string old_printer_full_name, old_printer_preset; + if (physical_printers.has_selection()) { + old_printer_full_name = physical_printers.get_selected_full_printer_name(); + old_printer_preset = physical_printers.get_selected_printer_preset_name(); + } + else + old_printer_preset = m_collection->get_edited_preset().name; + // Select related printer preset on the Printer Settings Tab + physical_printers.select_printer(selected_string); + std::string preset_name = physical_printers.get_selected_printer_preset_name(); + + // if new preset wasn't selected, there is no need to call update preset selection + if (old_printer_preset == preset_name) { + // we need just to update according Plater<->Tab PresetComboBox + if (dynamic_cast(this)!=nullptr) { + wxGetApp().get_tab(m_type)->update_preset_choice(); + // Synchronize config.ini with the current selections. + m_preset_bundle->export_selections(*wxGetApp().app_config); + } + else if (dynamic_cast(this)!=nullptr) + wxGetApp().sidebar().update_presets(m_type); + + this->update(); + return true; + } + + Tab* tab = wxGetApp().get_tab(Preset::TYPE_PRINTER); + if (tab) + tab->select_preset(preset_name, false, old_printer_full_name); + return true; +} + +// --------------------------------- +// *** PlaterPresetComboBox *** +// --------------------------------- + +PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset_type) : + PresetComboBox(parent, preset_type, wxSize(25 * wxGetApp().em_unit(), 30 * wxGetApp().em_unit() / 10)) +{ + GetDropDown().SetUseContentWidth(true,true); + + if (m_type == Preset::TYPE_FILAMENT) + { + // BBS: not show color picker +#if 0 + Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &event) { + const Preset* selected_preset = m_collection->find_preset(m_preset_bundle->filament_presets[m_filament_idx]); + // Wide icons are shown if the currently selected preset is not compatible with the current printer, + // and red flag is drown in front of the selected preset. + bool wide_icons = selected_preset && !selected_preset->is_compatible; + float scale = m_em_unit*0.1f; + + int shifl_Left = wide_icons ? int(scale * 16 + 0.5) : 0; +#if defined(wxBITMAPCOMBOBOX_OWNERDRAWN_BASED) + shifl_Left += int(scale * 4 + 0.5f); // IMAGE_SPACING_RIGHT = 4 for wxBitmapComboBox -> Space left of image +#endif + int icon_right_pos = shifl_Left + int(scale * (24+4) + 0.5); + int mouse_pos = event.GetLogicalPosition(wxClientDC(this)).x; + if (mouse_pos < shifl_Left || mouse_pos > icon_right_pos ) { + // Let the combo box process the mouse click. + event.Skip(); + return; + } + + // BBS + // Swallow the mouse click and open the color picker. + //change_extruder_color(); + }); +#endif + } + + // BBS + if (m_type == Preset::TYPE_FILAMENT) { + int em = wxGetApp().em_unit(); + clr_picker = new wxBitmapButton(parent, wxID_ANY, {}, wxDefaultPosition, wxSize(FromDIP(20), FromDIP(20)), wxBU_EXACTFIT | wxBU_AUTODRAW | wxBORDER_NONE); + clr_picker->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + clr_picker->SetToolTip(_L("Click to select filament color")); + clr_picker->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + // Check if it's an official filament + auto fila_type = Preset::remove_suffix_modified(GetValue().ToUTF8().data()); + bool is_official = boost::algorithm::starts_with(fila_type, "Bambu"); + if (is_official) { + // Get filament_id from filament_presets + const std::string& preset_name = m_preset_bundle->filament_presets[m_filament_idx]; + const Preset* selected_preset = m_collection->find_preset(preset_name); + wxString fila_id = selected_preset ? wxString::FromUTF8(selected_preset->filament_id) : "GFA00"; + FilamentColor fila_color = get_cur_color_info(); + + // Show filament picker dialog + FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type); + + if (!dialog.IsDataLoaded()) { + // If FilamentPicker fails, fallback to default color picker + show_default_color_picker(); + } else if (dialog.ShowModal() == wxID_OK) { + // Get selected filament color data + FilamentColor fila_color = dialog.GetSelectedFilamentColor(); + + // Check if we have valid color data + if (!fila_color.m_colors.empty()) { + // Convert to storage format + std::vector colors; + for (const wxColour& color : fila_color.m_colors) { + colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + } + + bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR); + this->sync_colour_config(colors, is_gradient); + } else { + // Fallback to basic color if no FilamentColor data + wxColour selected_color = dialog.GetSelectedColour(); + if (selected_color.IsOk()) { + std::vector color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()}; + this->sync_colour_config(color, false); + } + } + } + } else { + show_default_color_picker(); + } + wxCommandEvent *evt = new wxCommandEvent(EVT_FILAMENT_COLOR_CHANGED); + evt->SetInt(m_filament_idx); + wxQueueEvent(wxGetApp().plater(), evt); + }); + } + else { + edit_btn = new ScalableButton(parent, wxID_ANY, "cog"); + edit_btn->SetToolTip(_L("Click to edit preset")); + + edit_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent) + { + // In a case of a physical printer, for its editing open PhysicalPrinterDialog + if (m_type == Preset::TYPE_PRINTER +#ifdef __linux__ + // To edit extruder color from the sidebar + || m_type == Preset::TYPE_FILAMENT +#endif //__linux__ + ) + show_edit_menu(); + else + switch_to_tab(); + }); +#ifdef __linux__ + edit_btn->Hide(); +#endif //__linux__ + } +} + +PlaterPresetComboBox::~PlaterPresetComboBox() +{ + if (edit_btn) + edit_btn->Destroy(); + + // BBS. + if (clr_picker) + clr_picker->Destroy(); +} + +static void run_wizard(ConfigWizard::StartPage sp) +{ + wxGetApp().run_wizard(ConfigWizard::RR_USER, sp); +} + +void PlaterPresetComboBox::OnSelect(wxCommandEvent &evt) +{ + auto selected_item = evt.GetSelection(); + + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + if (marker >= LABEL_ITEM_DISABLED && marker < LABEL_ITEM_MAX) { + this->SetSelection(m_last_selected); + if (LABEL_ITEM_WIZARD_ADD_PRINTERS == marker) { + evt.Skip(); + return; + } + evt.StopPropagation(); + if (marker == LABEL_ITEM_MARKER || marker == LABEL_ITEM_DISABLED) + return; + //if (marker == LABEL_ITEM_WIZARD_PRINTERS) + // show_add_menu(); + //else { + ConfigWizard::StartPage sp = ConfigWizard::SP_WELCOME; + switch (marker) { + case LABEL_ITEM_WIZARD_PRINTERS: sp = ConfigWizard::SP_PRINTERS; break; + case LABEL_ITEM_WIZARD_FILAMENTS: sp = ConfigWizard::SP_FILAMENTS; break; + case LABEL_ITEM_WIZARD_MATERIALS: sp = ConfigWizard::SP_MATERIALS; break; + default: break; + } + wxTheApp->CallAfter([sp]() { run_wizard(sp); }); + //} + return; + } else if (marker == LABEL_ITEM_PHYSICAL_PRINTER || selected_item >= 0 || m_collection->current_is_dirty()) { + m_last_selected = selected_item; + if (m_type == Preset::TYPE_FILAMENT) + update_ams_color(); + } + + evt.Skip(); +} + +void PlaterPresetComboBox::update_badge_according_flag() { + auto selection = GetSelection(); + auto select_flag = GetFlag(selection); + auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; + ShowBadge(ok); +} + +bool PlaterPresetComboBox::switch_to_tab() +{ + Tab* tab = wxGetApp().get_tab(m_type); + if (!tab) + return false; + + const Preset* selected_filament_preset = nullptr; + if (m_type == Preset::TYPE_FILAMENT) + { + const std::string& selected_preset = GetString(GetSelection()).ToUTF8().data(); + if (!boost::algorithm::starts_with(selected_preset, Preset::suffix_modified())) + { + const std::string& preset_name = wxGetApp().preset_bundle->filaments.get_preset_name_by_alias(selected_preset); + if (wxGetApp().get_tab(m_type)->select_preset(preset_name)) + wxGetApp().get_tab(m_type)->get_combo_box()->set_filament_idx(m_filament_idx); + else { + return false; + } + } + } + + /* + if (int page_id = wxGetApp().tab_panel()->FindPage(tab); page_id != wxNOT_FOUND) + { + wxGetApp().tab_panel()->SetSelection(page_id); + // Switch to Settings NotePad + wxGetApp().mainframe->select_tab(); + + //In a case of a multi-material printing, for editing another Filament Preset + //it's needed to select this preset for the "Filament settings" Tab + if (m_type == Preset::TYPE_FILAMENT && wxGetApp().extruders_edited_cnt() > 1) + { + const std::string& selected_preset = GetString(GetSelection()).ToUTF8().data(); + // Call select_preset() only if there is new preset and not just modified + if (!boost::algorithm::ends_with(selected_preset, Preset::suffix_modified())) + { + const std::string& preset_name = wxGetApp().preset_bundle->filaments.get_preset_name_by_alias(selected_preset); + wxGetApp().get_tab(m_type)->select_preset(preset_name); + } + } + } + */ + + //BBS Select NoteBook Tab params + if (tab->GetParent() == wxGetApp().params_panel()) + wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + else { + wxGetApp().params_dialog()->Popup(); + tab->OnActivate(); + } + tab->restore_last_select_item(); + + return true; +} + +void PlaterPresetComboBox::change_extruder_color() +{ + // get current color + DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config; + auto colors = static_cast(cfg->option("filament_colour")->clone()); + wxColour clr(colors->values[m_filament_idx]); + if (!clr.IsOk()) + clr = wxColour(0, 0, 0); // Don't set alfa to transparence + + auto data = new wxColourData(); + data->SetChooseFull(1); + data->SetColour(clr); + + wxColourDialog dialog(this, data); + dialog.CenterOnParent(); + if (dialog.ShowModal() == wxID_OK) + { + colors->values[m_filament_idx] = dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + + DynamicPrintConfig cfg_new = *cfg; + cfg_new.set_key_value("filament_colour", colors); + + wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new); + this->update(); + wxGetApp().plater()->on_config_change(cfg_new); + } +} + +void PlaterPresetComboBox::show_add_menu() +{ + wxMenu* menu = new wxMenu(); + + append_menu_item(menu, wxID_ANY, _L("Add/Remove presets"), "", + [](wxCommandEvent&) { + wxTheApp->CallAfter([]() { run_wizard(ConfigWizard::SP_PRINTERS); }); + }, "menu_edit_preset", menu, []() { return true; }, wxGetApp().plater()); + + wxGetApp().plater()->PopupMenu(menu); +} + +void PlaterPresetComboBox::show_edit_menu() +{ + wxMenu* menu = new wxMenu(); + + append_menu_item(menu, wxID_ANY, _L("Edit preset"), "", + [this](wxCommandEvent&) { this->switch_to_tab(); }, "cog", menu, []() { return true; }, wxGetApp().plater()); + +#ifdef __linux__ + // To edit extruder color from the sidebar + if (m_type == Preset::TYPE_FILAMENT) { + append_menu_item(menu, wxID_ANY, _devL("Change extruder color"), "", + [this](wxCommandEvent&) { this->change_extruder_color(); }, "blank_14", menu, []() { return true; }, wxGetApp().plater()); + wxGetApp().plater()->PopupMenu(menu); + return; + } +#endif //__linux__ + + append_menu_item(menu, wxID_ANY, _L("Add/Remove presets"), "", + [](wxCommandEvent&) { + wxTheApp->CallAfter([]() { run_wizard(ConfigWizard::SP_PRINTERS); }); + }, "menu_edit_preset", menu, []() { return true; }, wxGetApp().plater()); + + wxGetApp().plater()->PopupMenu(menu); +} + +wxString PlaterPresetComboBox::get_preset_name(const Preset& preset) +{ + return from_u8(preset.label(false)); +} + +// Only the compatible presets are shown. +// If an incompatible preset is selected, it is shown as well. +void PlaterPresetComboBox::update() +{ + if (m_type == Preset::TYPE_FILAMENT && + (m_preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA || + m_preset_bundle->filament_presets.size() <= (size_t)m_filament_idx) ) + return; + + // Otherwise fill in the list from scratch. + this->Freeze(); + this->Clear(); + invalidate_selection(); + + const Preset* selected_filament_preset = nullptr; + if (m_type == Preset::TYPE_FILAMENT) + { + std::vector bitmaps = get_extruder_color_icons(true); + if (m_filament_idx < bitmaps.size()) { + clr_picker->SetBitmap(*bitmaps[m_filament_idx]); + } else { + return; + } +#ifdef __WXOSX__ + clr_picker->SetLabel(clr_picker->GetLabel()); // Let setBezelStyle: be called + clr_picker->Refresh(); +#endif + selected_filament_preset = m_collection->find_preset(m_preset_bundle->filament_presets[m_filament_idx]); + if (!selected_filament_preset) { + //can not find this filament, should be caused by project embedded presets, will be updated later + Thaw(); + return; + } + //assert(selected_filament_preset); + } + + bool has_selection = m_collection->get_selected_idx() != size_t(-1); + const Preset* selected_preset = m_type == Preset::TYPE_FILAMENT ? selected_filament_preset : has_selection ? &m_collection->get_selected_preset() : nullptr; + // Show wide icons if the currently selected preset is not compatible with the current printer, + // and draw a red flag in front of the selected preset. + bool wide_icons = selected_preset && !selected_preset->is_compatible; + + std::map nonsys_presets; + //BBS: add project embedded presets logic + std::map project_embedded_presets; + // ORCA: add bundle presets + std::map bundle_presets; + std::map system_presets; + std::map uncompatible_presets; + std::unordered_set system_printer_models; + std::map preset_descriptions; + std::map preset_filament_vendors; + std::map preset_filament_types; + std::map preset_aliases; // ORCA + std::map preset_bundle_ids; + std::map preset_bundle_names; + //BBS: move system to the end + wxString selected_system_preset; + wxString selected_user_preset; + wxString selected_bundle_preset; + wxString tooltip; + const std::deque& presets = m_collection->get_presets(); + + //BBS: move system to the end + /*if (!presets.front().is_visible) + this->set_label_marker(this->Append(separator(L("System presets")), wxNullBitmap));*/ + + for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) + { + const Preset& preset = presets[i]; + bool is_selected = m_type == Preset::TYPE_FILAMENT ? + m_preset_bundle->filament_presets[m_filament_idx] == preset.name : + // The case, when some physical printer is selected + m_type == Preset::TYPE_PRINTER && m_preset_bundle->physical_printers.has_selection() ? false : + i == m_collection->get_selected_idx(); + + if (!is_selected && !preset.is_visible) + { + continue; + } + else if (is_selected && !preset.is_visible) + { + Preset& selected_preset = const_cast(preset); + selected_preset.is_visible = true; + } + + bool single_bar = false; + wxString name = from_u8(preset.name); + preset_aliases[name] = get_preset_name(preset).utf8_string(); // ORCA + + // Track bundle names for bundled presets + if (preset.is_from_bundle()) { + m_preset_bundle->bundles.ReadLock(); + auto bundle_it = m_preset_bundle->bundles.m_bundles.find(preset.bundle_id); + if (bundle_it != m_preset_bundle->bundles.m_bundles.end()) { + preset_bundle_ids[name] = bundle_it->second.id; + preset_bundle_names[name] = bundle_it->second.name; + } + m_preset_bundle->bundles.ReadUnlock(); + + } + + if (m_type == Preset::TYPE_FILAMENT) + { +#if 0 + // Assign an extruder color to the selected item if the extruder color is defined. + filament_rgb = is_selected ? selected_filament_preset->config.opt_string("filament_colour", 0) : + preset.config.opt_string("filament_colour", 0); + extruder_rgb = (is_selected && !filament_color.empty()) ? filament_color : filament_rgb; + single_bar = filament_rgb == extruder_rgb; + + bitmap_key += single_bar ? filament_rgb : filament_rgb + extruder_rgb; +#endif + // ORCA allow caching vendor and type values for all presets instead just system ones + // if (preset.is_system) { + if (!preset.is_compatible && preset_filament_vendors.count(name) > 0) + continue; + else if (preset.is_compatible && preset_filament_vendors.count(name) > 0) + uncompatible_presets.erase(name); + preset_filament_vendors[name] = preset.config.option("filament_vendor")->values.at(0); + if (preset_filament_vendors[name] == "Bambu Lab") + preset_filament_vendors[name] = "Bambu"; + preset_filament_types[name] = preset.config.option("filament_type")->values.at(0); + //} + } + wxBitmap* bmp = get_bmp(preset); + assert(bmp); + + preset_descriptions.emplace(name, from_u8(preset.description)); + + if (!preset.is_compatible) { + if (boost::ends_with(name, " template")) + continue; + uncompatible_presets.emplace(name, bmp); + } + else if (preset.is_default || preset.is_system) { + //BBS: move system to the end + if (m_type == Preset::TYPE_PRINTER) { + auto printer_model = preset.config.opt_string("printer_model"); + + // ORCA: Make system printer presets display the dirty "*" prefix when edited. + name = from_u8(is_selected && preset.is_dirty ? Preset::suffix_modified() + printer_model : printer_model); + + if (system_printer_models.count(printer_model) == 0) { + preset_aliases[name] = name.utf8_string(); // ORCA + system_presets.emplace(name, bmp); + system_printer_models.insert(printer_model); + } + else if (is_selected) { + const wxString alternate_name = from_u8(preset.is_dirty ? printer_model : Preset::suffix_modified() + printer_model); + // Remove the old preset name if exists, and add the new one with the same name but with modified suffix if needed. + if (system_presets.erase(alternate_name)) + system_presets.emplace(name, bmp); + + preset_aliases.erase(alternate_name); // ORCA: do this to aliases too + preset_aliases[name] = name.utf8_string(); + } + } else { + system_presets.emplace(name, bmp); + } + + if (is_selected) { + tooltip = get_tooltip(preset); + selected_system_preset = name; + } + //Append(get_preset_name(preset), *bmp); + //validate_selection(is_selected); + //if (is_selected) + //BBS set tooltip + // tooltip = get_tooltip(preset); + } + //BBS: add project embedded preset logic + else if (preset.is_project_embedded) + { + project_embedded_presets.emplace(name, bmp); + if (is_selected) { + selected_user_preset = name; + tooltip = wxString::FromUTF8(preset.name.c_str()); + } + } + // ORCA: add bundle presets + else if (preset.is_from_bundle()) + { + bundle_presets.emplace(name, bmp); + if (is_selected) { + selected_bundle_preset = name; + tooltip = get_tooltip(preset); + } + } + else + { + nonsys_presets.emplace(name, bmp); + if (is_selected) { + selected_user_preset = name; + //BBS set tooltip + tooltip = get_tooltip(preset); + } + } + //BBS: move system to the end + //if (i + 1 == m_collection->num_default_presets()) + // set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + } + //if (m_type == Preset::TYPE_PRINTER) + // add_connected_printers("", true); + bool selected_in_ams = false; + if (m_type == Preset::TYPE_FILAMENT) { + set_replace_text("Bambu", "BambuStudioBlack"); + selected_in_ams = add_ams_filaments(into_u8(selected_user_preset.empty() ? selected_system_preset : selected_user_preset), true); + } + + std::vector filament_orders = {"Bambu PLA Basic", "Bambu PLA Matte", "Bambu PETG HF", "Bambu ABS", "Bambu PLA Silk", "Bambu PLA-CF", + "Bambu PLA Galaxy", "Bambu PLA Metal", "Bambu PLA Marble", "Bambu PETG-CF", "Bambu PETG Translucent", "Bambu ABS-GF"}; + std::vector first_vendors = {"", "Bambu", "Generic"}; // Empty vendor for non-system presets + std::vector first_types = {"PLA", "PETG", "ABS", "TPU"}; + auto add_presets = [this, &preset_descriptions, &filament_orders, &preset_filament_vendors, &first_vendors, &preset_filament_types, &preset_aliases, &preset_bundle_ids, &preset_bundle_names, &first_types, &selected_in_ams] + (std::map const &presets, wxString const &selected, std::string const &group, wxString const &groupName) { + if (!presets.empty()) { + set_label_marker(Append(_L(group), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + if (m_type == Preset::TYPE_FILAMENT || m_type == Preset::TYPE_PRINTER) { + std::vector::value_type const*> list(presets.size(), nullptr); + std::transform(presets.begin(), presets.end(), list.begin(), [](auto & pair) { return &pair; }); + bool groupByGroup = group != "System presets"; + //if (groupByGroup) { + // if (GetCount() == 1) Clear(); + // else SetString(GetCount() - 1, ""); + //} + if (m_type == Preset::TYPE_FILAMENT && (group == "System presets" || group == "Unsupported presets")) + std::sort(list.begin(), list.end(), [&filament_orders, &preset_filament_vendors, &first_vendors, &preset_filament_types, &first_types](auto *l, auto *r) { + { // Compare order + auto iter1 = std::find(filament_orders.begin(), filament_orders.end(), l->first); + auto iter2 = std::find(filament_orders.begin(), filament_orders.end(), r->first); + if (iter1 != iter2) + return iter1 < iter2; + } + { // Compare vendor + auto iter1 = std::find(first_vendors.begin(), first_vendors.end(), preset_filament_vendors[l->first]); + auto iter2 = std::find(first_vendors.begin(), first_vendors.end(), preset_filament_vendors[r->first]); + if (iter1 != iter2) + return iter1 < iter2; + } + { // Compare type + auto iter1 = std::find(first_types.begin(), first_types.end(), preset_filament_types[l->first]); + auto iter2 = std::find(first_types.begin(), first_types.end(), preset_filament_types[r->first]); + if (iter1 != iter2) + return iter1 < iter2; + } + return l->first < r->first; + }); + // ORCA add sorting support for vendor / type for user presets. also non grouped items + if (groupName == "by_bundle" || groupName == "by_vendor" || groupName == "by_type" || groupName == ""){ + auto by = groupName == "by_bundle" ? preset_bundle_names + : groupName == "by_vendor" ? preset_filament_vendors + : groupName == "by_type" ? preset_filament_types + : preset_aliases; + std::sort(list.begin(), list.end(), [&by](auto *l, auto *r) { + auto get_key = [&](auto* item) -> std::pair { + std::string str = by.count(item->first) ? by.at(item->first) : ""; + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::tolower(c);}); + return {!str.empty(), str}; // is_valid, lower_case + }; + auto [l_valid, l_lower] = get_key(l); + auto [r_valid, r_lower] = get_key(r); + return (l_valid != r_valid) ? l_valid > r_valid + : (l_lower != r_lower) ? l_lower < r_lower + : l->first < r->first; + }); + } + bool unsupported = group == "Unsupported presets"; + for (auto it : list) { + // ORCA add sorting support for vendor / type for user presets + auto groupName2 = groupName == "by_bundle" ? (preset_bundle_names[it->first].empty() ? _L("Unspecified") : from_u8(preset_bundle_names[it->first])) + : groupName == "by_type" ? (preset_filament_types[it->first].empty() ? _L("Unspecified") : from_u8(preset_filament_types[it->first])) + : groupName == "by_vendor" ? (preset_filament_vendors[it->first].empty() ? _L("Unspecified") : from_u8(preset_filament_vendors[it->first])) + : groupByGroup ? groupName + : from_u8(preset_filament_vendors[it->first]); + int index = groupName == "by_bundle" + ? Append(from_u8(preset_aliases[it->first]), *it->second, + from_u8(preset_bundle_ids[it->first]), groupName2, nullptr, + unsupported ? DD_ITEM_STYLE_DISABLED : 0) + : Append(from_u8(preset_aliases[it->first]), *it->second, groupName2, nullptr, + unsupported ? DD_ITEM_STYLE_DISABLED : 0); + SetItemAlias(index, it->first); + if (unsupported) + set_label_marker(index, LABEL_ITEM_DISABLED); + else if (m_type == Preset::TYPE_PRINTER && group == "System presets" ) + set_label_marker(index, LABEL_ITEM_PRINTER_MODELS); + SetItemTooltip(index, preset_descriptions[it->first]); + bool is_selected = it->first == selected; + validate_selection(is_selected); + if (is_selected && selected_in_ams) { + SetFlag(GetCount() - 1, (int) FilamentAMSType::FROM_AMS); + } + } + } else { + for (std::map::const_iterator it = presets.begin(); it != presets.end(); ++it) { + int index = Append(from_u8(preset_aliases[it->first]), *it->second); + SetItemAlias(index, it->first); + SetItemTooltip(index, preset_descriptions[it->first]); + if (group == "System presets") + set_label_marker(GetCount() - 1, LABEL_ITEM_PRINTER_MODELS); + validate_selection(it->first == selected); + } + } + } + }; + + //BBS: add project embedded preset logic + add_presets(project_embedded_presets, selected_user_preset, L("Project-inside presets"), _L("Project") + " "); + // ORCA add sorting support for vendor / type for user presets + auto group_filament_presets = wxGetApp().app_config->get("group_filament_presets"); + auto group_filament_presets_by = group_filament_presets == "0" ? (_L("Custom") + " ") // Append all to "Custom" sub menu + : group_filament_presets == "2" ? "by_type" // Create sub menus with filament type + : group_filament_presets == "3" ? "by_vendor" // Create sub menus with filament vendor + : ""; // Use without sub menu + add_presets(nonsys_presets, selected_user_preset, L("User presets"), group_filament_presets_by); + // ORCA: add bundle presets with sub-dropdown grouping for filament and printer + auto bundle_group_name = (m_type == Preset::TYPE_FILAMENT || m_type == Preset::TYPE_PRINTER) ? "by_bundle" : ""; + add_presets(bundle_presets, selected_bundle_preset, L("Bundle presets"), bundle_group_name); + // BBS: move system to the end + add_presets(system_presets, selected_system_preset, L("System presets"), _L("System")); + add_presets(uncompatible_presets, {}, L("Unsupported presets"), _L("Unsupported") + " "); + + //BBS: remove unused pysical printer logic + /*if (m_type == Preset::TYPE_PRINTER) + { + // add Physical printers, if any exists + if (!m_preset_bundle->physical_printers.empty()) { + set_label_marker(Append(separator(L("Physical printers")), wxNullBitmap)); + const PhysicalPrinterCollection& ph_printers = m_preset_bundle->physical_printers; + + for (PhysicalPrinterCollection::ConstIterator it = ph_printers.begin(); it != ph_printers.end(); ++it) { + for (const std::string& preset_name : it->get_preset_names()) { + Preset* preset = m_collection->find_preset(preset_name); + if (!preset || !preset->is_visible) + continue; + std::string main_icon_name, bitmap_key = main_icon_name = preset->printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; + wxBitmap* bmp = get_bmp(main_icon_name, wide_icons, main_icon_name); + assert(bmp); + + set_label_marker(Append(from_u8(it->get_full_name(preset_name) + suffix(preset)), *bmp), LABEL_ITEM_PHYSICAL_PRINTER); + validate_selection(ph_printers.is_selected(it, preset_name)); + } + } + } + }*/ + + if (m_type == Preset::TYPE_PRINTER || m_type == Preset::TYPE_FILAMENT || m_type == Preset::TYPE_SLA_MATERIAL) { + wxBitmap* bmp = get_bmp("edit_preset_list", wide_icons, "edit_uni"); + assert(bmp); + + if (m_type == Preset::TYPE_FILAMENT) + set_label_marker(Append(separator(L("Add/Remove filaments")), *bmp), LABEL_ITEM_WIZARD_FILAMENTS); + else if (m_type == Preset::TYPE_SLA_MATERIAL) + set_label_marker(Append(separator(L("Add/Remove materials")), *bmp), LABEL_ITEM_WIZARD_MATERIALS); + else { + set_label_marker(Append(separator(L("Select/Remove printers (system presets)")), *bmp), LABEL_ITEM_WIZARD_PRINTERS); + set_label_marker(Append(separator(L("Create printer")), *bmp), LABEL_ITEM_WIZARD_ADD_PRINTERS); + } + } + + update_selection(); + if (m_type == Preset::TYPE_FILAMENT) { + if (wxGetApp().plater()->is_same_printer_for_connected_and_selected(false)) { + update_badge_according_flag(); + } + } + Thaw(); + + if (!tooltip.IsEmpty()) { +#ifdef __WXMSW__ + // From the Windows 2004 the tooltip for preset combobox doesn't work after next call of SetTooltip() + // (There was an issue, when tooltip doesn't appears after changing of the preset selection) + // But this workaround seems to work: We should to kill tooltip and than set new tooltip value + // See, https://groups.google.com/g/wx-users/c/mOEe3fgHrzk + SetToolTip(NULL); +#endif + SetToolTip(tooltip); + } + +#ifdef __WXMSW__ + // Use this part of code just on Windows to avoid of some layout issues on Linux + // Update control min size after rescale (changed Display DPI under MSW) + if (GetMinWidth() != 10 * m_em_unit) + SetMinSize(wxSize(10 * m_em_unit, GetSize().GetHeight())); +#endif //__WXMSW__ +} + +void PlaterPresetComboBox::msw_rescale() +{ + PresetComboBox::msw_rescale(); + SetMinSize({-1, 30 * m_em_unit / 10}); + + if (clr_picker) + clr_picker->SetSize(20 * m_em_unit / 10, 20 * m_em_unit / 10); + // BBS + if (edit_btn != nullptr) + edit_btn->msw_rescale(); +} + + +FilamentColor PlaterPresetComboBox::get_cur_color_info() +{ + std::vector filaments_multi_color = Slic3r::GUI::wxGetApp().plater()->get_filament_colors_render_info(); + std::vector filament_color_type = Slic3r::GUI::wxGetApp().plater()->get_filament_color_render_type(); + + if (m_filament_idx < 0 || m_filament_idx >= static_cast(filaments_multi_color.size())) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": m_filament_idx %1% out of range %2%") % m_filament_idx % filaments_multi_color.size(); + return FilamentColor(); + } + + if (m_filament_idx >= static_cast(filament_color_type.size())) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": m_filament_idx %1% out of range for color_type %2%") % m_filament_idx % filament_color_type.size(); + return FilamentColor(); + } + std::string filament_color_info = filaments_multi_color[m_filament_idx]; + std::vector colors; + colors = Slic3r::split_string(filament_color_info, ' '); + FilamentColor fila_color; + for (const std::string& color_str : colors) { + if (!color_str.empty()) { + wxColour color(color_str); + if (color.IsOk()) { + fila_color.m_colors.insert(color); + } + } + } + + fila_color.EndSet(filament_color_type[m_filament_idx] == "0" ? 0 : 1); + return fila_color; +} + +void PlaterPresetComboBox::show_default_color_picker() +{ + DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config; + auto colors = static_cast(cfg->option("filament_colour")->clone()); + wxColour current_clr(colors->values[m_filament_idx]); + if (!current_clr.IsOk()) + current_clr = wxColour(0, 0, 0); // Don't set alfa to transparence + + m_clrData.SetColour(current_clr); + + wxColourData data = show_sys_picker_dialog(this, m_clrData); + if (m_clrData.GetColour() != data.GetColour()) { + std::vector color = {data.GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString()}; + m_clrData.SetColour(data.GetColour()); + sync_colour_config(color, false); + } +} + +void PlaterPresetComboBox::sync_colour_config(const std::vector &clrs, bool is_gradient) +{ + DynamicPrintConfig *cfg = &wxGetApp().preset_bundle->project_config; + + // Clone the string vector and patch the value at current extruder index. + auto multi_colour_opt = static_cast(cfg->option("filament_multi_colour")->clone()); + auto colour_type_opt = static_cast(cfg->option("filament_colour_type")->clone()); + auto colour_opt = static_cast(cfg->option("filament_colour")->clone()); + + if (m_filament_idx >= multi_colour_opt->values.size()) multi_colour_opt->values.resize(m_filament_idx + 1); + if (m_filament_idx >= colour_type_opt->values.size()) colour_type_opt->values.resize(m_filament_idx + 1); + if (m_filament_idx >= colour_opt->values.size()) colour_opt->values.resize(m_filament_idx + 1); + + std::string clr_str = ""; + for(auto &clr : clrs) { + clr_str += clr + " "; + } + clr_str.pop_back(); + + multi_colour_opt->values[m_filament_idx] = clr_str; + colour_opt->values[m_filament_idx] = clrs[0]; + colour_type_opt->values[m_filament_idx] = is_gradient ? "0" : "1"; + DynamicPrintConfig cfg_new = *cfg; + cfg_new.set_key_value("filament_multi_colour", multi_colour_opt); + cfg_new.set_key_value("filament_colour", colour_opt); + cfg_new.set_key_value("filament_colour_type", colour_type_opt); + cfg->apply(cfg_new); + + wxGetApp().plater()->update_project_dirty_from_presets(); + + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + update(); // refresh the preset combobox with new config + + wxGetApp().plater()->on_config_change(cfg_new); +} + +// --------------------------------- +// *** TabPresetComboBox *** +// --------------------------------- + +TabPresetComboBox::TabPresetComboBox(wxWindow* parent, Preset::Type preset_type) : + // BBS: new layout + PresetComboBox(parent, preset_type, wxSize(20 * wxGetApp().em_unit(), 30 * wxGetApp().em_unit() / 10)) +{ +} + +void TabPresetComboBox::OnSelect(wxCommandEvent &evt) +{ + // Under OSX: in case of use of a same names written in different case (like "ENDER" and "Ender") + // m_presets_choice->GetSelection() will return first item, because search in PopupListCtrl is case-insensitive. + // So, use GetSelection() from event parameter + auto selected_item = evt.GetSelection(); + + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + if (marker >= LABEL_ITEM_DISABLED && marker < LABEL_ITEM_MAX) { + this->SetSelection(m_last_selected); + // BBS: Add/Remove filaments + ConfigWizard::StartPage sp = ConfigWizard::SP_WELCOME; + switch (marker) { + case LABEL_ITEM_WIZARD_PRINTERS: sp = ConfigWizard::SP_PRINTERS; break; + case LABEL_ITEM_WIZARD_FILAMENTS: sp = ConfigWizard::SP_FILAMENTS; break; + case LABEL_ITEM_WIZARD_MATERIALS: sp = ConfigWizard::SP_MATERIALS; break; + default: break; + } + if (sp != ConfigWizard::SP_WELCOME) { + wxTheApp->CallAfter([this, sp]() { + run_wizard(sp); + }); + } + } + else if (on_selection_changed && (m_last_selected != selected_item || m_collection->current_is_dirty())) { + m_last_selected = selected_item; + // BBS: ams + update_ams_color(); + on_selection_changed(selected_item); + } + + evt.StopPropagation(); +#ifdef __WXMSW__ + // From the Win 2004 preset combobox lose a focus after change the preset selection + // and that is why the up/down arrow doesn't work properly + // So, set the focus to the combobox explicitly + this->SetFocus(); +#endif +} + +wxString TabPresetComboBox::get_preset_name(const Preset& preset) +{ + if (preset.is_from_bundle()) + return from_u8(preset.label(false)); + else + return from_u8(preset.label(true)); +} + +// Update the choice UI from the list of presets. +// If show_incompatible, all presets are shown, otherwise only the compatible presets are shown. +// If an incompatible preset is selected, it is shown as well. +void TabPresetComboBox::update() +{ + Freeze(); + Clear(); + invalidate_selection(); + + const std::deque& presets = m_collection->get_presets(); + + std::map> nonsys_presets; + //BBS: add project embedded presets logic + std::map> project_embedded_presets; + //BBS: move system to the end + std::map> system_presets; + // ORCA: add bundle presets + std::map> bundle_presets; + std::map preset_descriptions; + std::map preset_aliases; // ORCA + std::map preset_bundle_ids; + std::map preset_bundle_names; + + wxString selected = ""; + //BBS: move system to the end + /*if (!presets.front().is_visible) + set_label_marker(Append(separator(L("System presets")), wxNullBitmap));*/ + size_t idx_selected = m_collection->get_selected_idx(); + + if (m_type == Preset::TYPE_PRINTER && m_preset_bundle->physical_printers.has_selection()) { + std::string sel_preset_name = m_preset_bundle->physical_printers.get_selected_printer_preset_name(); + Preset* preset = m_collection->find_preset(sel_preset_name); + if (!preset) + m_preset_bundle->physical_printers.unselect_printer(); + } + + for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) + { + const Preset& preset = presets[i]; + if (!preset.is_visible || (!show_incompatible && !preset.is_compatible && i != idx_selected)) + continue; + + // marker used for disable incompatible printer models for the selected physical printer + bool is_enabled = true; + + wxBitmap* bmp = get_bmp(preset); + assert(bmp); + + const wxString name = from_u8(preset.name); + preset_aliases[name] = get_preset_name(preset).utf8_string(); + if (preset.is_system) + preset_descriptions.emplace(name, from_u8(preset.description)); + + // ORCA: Track bundle names for bundled presets + if (preset.is_from_bundle()) { + m_preset_bundle->bundles.ReadLock(); + auto bundle_it = m_preset_bundle->bundles.m_bundles.find(preset.bundle_id); + if (bundle_it != m_preset_bundle->bundles.m_bundles.end()) { + preset_bundle_ids[name] = bundle_it->second.id; + preset_bundle_names[name] = bundle_it->second.name; + } + m_preset_bundle->bundles.ReadUnlock(); + + } + + if (preset.is_default || preset.is_system) { + //BBS: move system to the end + system_presets.emplace(name, std::pair(bmp, is_enabled)); + if (i == idx_selected) + selected = name; + //int item_id = Append(get_preset_name(preset), *bmp); + //if (!is_enabled) + // set_label_marker(item_id, LABEL_ITEM_DISABLED); + //validate_selection(i == idx_selected); + } + //BBS: add project embedded preset logic + else if (preset.is_project_embedded) + { + //std::pair pair(bmp, is_enabled); + project_embedded_presets.emplace(name, std::pair(bmp, is_enabled)); + if (i == idx_selected) + selected = name; + } + // ORCA: add bundle presets + else if (preset.is_from_bundle()) + { + bundle_presets.emplace(name, std::pair(bmp, is_enabled)); + if (i == idx_selected) + selected = name; + } + else + { + std::pair pair(bmp, is_enabled); + nonsys_presets.emplace(name, std::pair(bmp, is_enabled)); + if (i == idx_selected) + selected = name; + } + //BBS: move system to the end + //if (i + 1 == m_collection->num_default_presets()) + // set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + } + + if (m_type == Preset::TYPE_FILAMENT && m_preset_bundle->is_bbl_vendor()) + add_ams_filaments(into_u8(selected)); + + //BBS: add project embedded preset logic + if (!project_embedded_presets.empty()) + { + set_label_marker(Append(_L("Project-inside presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + for (std::map>::iterator it = project_embedded_presets.begin(); it != project_embedded_presets.end(); ++it) { + int item_id = Append(it->first, *it->second.first); + SetItemTooltip(item_id, preset_descriptions[it->first]); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + if (!nonsys_presets.empty()) + { + set_label_marker(Append(_L("User presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + int item_id = Append(it->first, *it->second.first); + SetItemAlias(item_id, it->first); + SetItemTooltip(item_id, preset_descriptions[it->first]); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + // ORCA: add bundle presets with sub-dropdown grouping + if (!bundle_presets.empty()) + { + set_label_marker(Append(_L("Bundle presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + for (std::map>::iterator it = bundle_presets.begin(); it != bundle_presets.end(); ++it) { + // Get bundle name for grouping + wxString bundle_name = _L("Unspecified"); + if (preset_bundle_names.count(it->first) > 0 && !preset_bundle_names[it->first].empty()) { + bundle_name = from_u8(preset_bundle_names[it->first]); + } + // Use Append with group parameter for sub-dropdown grouping + int item_id = Append(from_u8(preset_aliases[it->first]), *it->second.first, from_u8(preset_bundle_ids[it->first]), bundle_name); + SetItemAlias(item_id, it->first); + SetItemTooltip(item_id, preset_descriptions[it->first]); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + //BBS: move system to the end + if (!system_presets.empty()) + { + set_label_marker(Append(_L("System presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + for (std::map>::iterator it = system_presets.begin(); it != system_presets.end(); ++it) { + int item_id = Append(it->first, *it->second.first); + SetItemAlias(item_id, it->first); + SetItemTooltip(item_id, preset_descriptions[it->first]); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + + if (m_type == Preset::TYPE_PRINTER) + { + //BBS: remove unused pysical printer logic + /*// add Physical printers, if any exists + if (!m_preset_bundle->physical_printers.empty()) { + set_label_marker(Append(separator(L("Physical printers")), wxNullBitmap)); + const PhysicalPrinterCollection& ph_printers = m_preset_bundle->physical_printers; + + for (PhysicalPrinterCollection::ConstIterator it = ph_printers.begin(); it != ph_printers.end(); ++it) { + for (const std::string& preset_name : it->get_preset_names()) { + Preset* preset = m_collection->find_preset(preset_name); + if (!preset || !preset->is_visible) + continue; + std::string main_icon_name = preset->printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; + + wxBitmap* bmp = get_bmp(main_icon_name, main_icon_name, "", true, true, false); + assert(bmp); + + set_label_marker(Append(from_u8(it->get_full_name(preset_name) + suffix(preset)), *bmp), LABEL_ITEM_PHYSICAL_PRINTER); + validate_selection(ph_printers.is_selected(it, preset_name)); + } + } + }*/ + + // add "Add/Remove printers" item + //std::string icon_name = "edit_uni"; + //wxBitmap* bmp = get_bmp("edit_preset_list, tab,", icon_name, ""); + //assert(bmp); + + //set_label_marker(Append(separator(L("Add/Remove printers")), *bmp), LABEL_ITEM_WIZARD_PRINTERS); + } + + // BBS Add/Remove filaments select + //wxBitmap* bmp = get_bmp("edit_preset_list", false, "edit_uni"); + //assert(bmp); + //if (m_type == Preset::TYPE_FILAMENT) + // set_label_marker(Append(separator(L("Add/Remove filaments")), *bmp), LABEL_ITEM_WIZARD_FILAMENTS); + //else if (m_type == Preset::TYPE_SLA_MATERIAL) + // set_label_marker(Append(separator(L("Add/Remove materials")), *bmp), LABEL_ITEM_WIZARD_MATERIALS); + + update_selection(); + Thaw(); +} + +void TabPresetComboBox::msw_rescale() +{ + PresetComboBox::msw_rescale(); + // BBS: new layout + wxSize sz = wxSize(20 * m_em_unit, 30 * m_em_unit / 10); + SetMinSize(sz); + SetSize(sz); +} + +void TabPresetComboBox::update_dirty() +{ + // 1) Update the dirty flag of the current preset. + m_collection->update_dirty(); + + // 2) Update the labels. + wxWindowUpdateLocker noUpdates(this); + for (unsigned int ui_id = 0; ui_id < GetCount(); ++ui_id) { + auto marker = reinterpret_cast(this->GetClientData(ui_id)); + if (marker >= LABEL_ITEM_MARKER) + continue; + + std::string old_label = GetString(ui_id).utf8_str().data(); + std::string preset_name = Preset::remove_suffix_modified(old_label); + std::string ph_printer_name; + + if (marker == LABEL_ITEM_PHYSICAL_PRINTER) { + ph_printer_name = PhysicalPrinter::get_short_name(preset_name); + preset_name = PhysicalPrinter::get_preset_name(preset_name); + } + + Preset* preset = m_collection->find_preset(preset_name, false); + if (preset) { + std::string new_label = preset->label(true); + + if (marker == LABEL_ITEM_PHYSICAL_PRINTER) + new_label = ph_printer_name + PhysicalPrinter::separator() + new_label; + + if (old_label != new_label) { + SetString(ui_id, from_u8(new_label)); + SetItemBitmap(ui_id, *get_bmp(*preset)); + if (ui_id == GetSelection()) SetToolTip(wxString::FromUTF8(new_label.c_str())); // BBS + } + } + } +#ifdef __APPLE__ + // wxWidgets on OSX do not upload the text of the combo box line automatically. + // Force it to update by re-selecting. + SetSelection(GetSelection()); +#endif /* __APPLE __ */ +} + +} // namespace GUI +GUI::CalibrateFilamentComboBox::CalibrateFilamentComboBox(wxWindow *parent) +: PlaterPresetComboBox(parent, Preset::TYPE_FILAMENT) +{ + clr_picker->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + clr_picker->SetToolTip(""); + clr_picker->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {}); +} + +GUI::CalibrateFilamentComboBox::~CalibrateFilamentComboBox() +{ +} + +void GUI::CalibrateFilamentComboBox::load_tray(DynamicPrintConfig &config) +{ + m_tray_name = config.opt_string("tray_name", 0u); + size_t pos = m_tray_name.find("HT-"); + if (pos != std::string::npos) { + m_tray_name = m_tray_name.substr(pos + 3); + } + m_filament_id = config.opt_string("filament_id", 0u); + m_tag_uid = config.opt_string("tag_uid", 0u); + m_filament_type = config.opt_string("filament_type", 0u); + m_filament_color = config.opt_string("filament_colour", 0u); + m_filament_exist = config.opt_bool("filament_exist", 0u); + wxColor clr(m_filament_color); + clr_picker->SetBitmap(*get_extruder_color_icon(m_filament_color, m_tray_name, FromDIP(20), FromDIP(20))); +#ifdef __WXOSX__ + clr_picker->SetLabel(clr_picker->GetLabel()); // Let setBezelStyle: be called + clr_picker->Refresh(); +#endif + if (!m_filament_exist) { + SetValue(_L("Empty")); + m_selected_preset = nullptr; + m_is_compatible = false; + clr_picker->SetBitmap(*get_extruder_color_icon("#F0F0F0FF", m_tray_name, FromDIP(20), FromDIP(20))); + } else { + auto &filaments = m_collection->get_presets(); + auto iter = std::find_if(filaments.begin(), filaments.end(), [this](auto &f) { + if (!f.is_system) // Only match system preset + return false; + bool is_compatible = m_preset_bundle->calibrate_filaments.find(&f) != m_preset_bundle->calibrate_filaments.end(); + return is_compatible && f.filament_id == m_filament_id; + }); + + // Prioritize matching system presets. If there are no system presets, match all presets. + if (iter == filaments.end()) { + iter = std::find_if(filaments.begin(), filaments.end(), [this](auto &f) { + if (f.is_system) // Only match system preset + return false; + bool is_compatible = m_preset_bundle->calibrate_filaments.find(&f) != m_preset_bundle->calibrate_filaments.end(); + return is_compatible && f.filament_id == m_filament_id; + }); + } + //if (iter == filaments.end() && !m_filament_type.empty()) { + // auto filament_type = "Generic " + m_filament_type; + // iter = std::find_if(filaments.begin(), filaments.end(), + // [this , &filament_type](auto &f) { + // bool is_compatible = m_preset_bundle->calibrate_filaments.find(&f) != m_preset_bundle->calibrate_filaments.end(); + // return is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); }); + //} + if (iter != filaments.end()) { + m_selected_preset = &*iter; + m_is_compatible = true; + SetValue(get_preset_name(*iter)); + } + else { + m_selected_preset = nullptr; + m_is_compatible = false; + SetValue(_L("Incompatible")); + } + Enable(); + } +} + +void GUI::CalibrateFilamentComboBox::update() +{ + if (m_preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) + return; + + // Otherwise fill in the list from scratch. + this->Freeze(); + this->Clear(); + invalidate_selection(); + + const Preset* selected_filament_preset = nullptr; + + m_nonsys_presets.clear(); + m_system_presets.clear(); + + wxString selected_preset = m_selected_preset ? get_preset_name(*m_selected_preset) : GetValue(); + + wxString tooltip; + const std::deque& presets = m_collection->get_presets(); + + for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) + { + const Preset& preset = presets[i]; + auto display_name = get_preset_name(preset); + bool is_selected = m_selected_preset == &preset; + if (m_preset_bundle->calibrate_filaments.empty()) { + Thaw(); + return; + } + bool is_compatible = m_preset_bundle->calibrate_filaments.find(&preset) != m_preset_bundle->calibrate_filaments.end(); + ; + if (!preset.is_visible || (!is_compatible && !is_selected)) + continue; + + if (is_selected) { + tooltip = get_tooltip(preset); + } + + wxBitmap* bmp = get_bmp(preset); + assert(bmp); + + if (preset.is_default || preset.is_system) { + m_system_presets.emplace(display_name, std::make_pair( preset.name, bmp )); + } + else { + m_nonsys_presets.emplace(display_name, std::make_pair( preset.name, bmp )); + } + + } + + if (!m_nonsys_presets.empty()) + { + set_label_marker(Append(_L("User presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + for (auto it = m_nonsys_presets.begin(); it != m_nonsys_presets.end(); ++it) { + Append(it->first, *(it->second.second)); + validate_selection(it->first == selected_preset); + } + } + if (!m_system_presets.empty()) + { + set_label_marker(Append(_L("System presets"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + for (auto it = m_system_presets.begin(); it != m_system_presets.end(); ++it) { + Append(it->first, *(it->second.second)); + validate_selection(it->first == selected_preset); + } + } + + update_selection(); + Thaw(); + + SetToolTip(NULL); +} + +void GUI::CalibrateFilamentComboBox::msw_rescale() +{ + if (clr_picker) { + clr_picker->SetSize(FromDIP(20), FromDIP(20)); + clr_picker->SetBitmap(*get_extruder_color_icon(m_filament_color, m_tray_name, FromDIP(20), FromDIP(20))); + } + // BBS + if (edit_btn != nullptr) + edit_btn->msw_rescale(); +} + +void GUI::CalibrateFilamentComboBox::OnSelect(wxCommandEvent &evt) +{ + auto marker = reinterpret_cast(this->GetClientData(evt.GetSelection())); + if (marker >= LABEL_ITEM_DISABLED && marker < LABEL_ITEM_MAX) { + this->SetSelection(evt.GetSelection() + 1); + wxCommandEvent event(wxEVT_COMBOBOX); + event.SetInt(evt.GetSelection() + 1); + event.SetString(GetString(evt.GetSelection() + 1)); + wxPostEvent(this, event); + return; + } + m_is_compatible = true; + static_cast(m_parent)->Enable(true); + + wxString display_name = evt.GetString(); + std::string preset_name; + if (m_system_presets.find(evt.GetString()) != m_system_presets.end()) { + preset_name = m_system_presets.at(display_name).first; + } + else if (m_nonsys_presets.find(evt.GetString()) != m_nonsys_presets.end()) { + preset_name = m_nonsys_presets.at(display_name).first; + } + m_selected_preset = m_collection->find_preset(preset_name); + + // if the selected preset is null, do not send tray_change event + if (!m_selected_preset) { + MessageDialog msg_dlg(nullptr, _L("The selected preset is null!"), wxEmptyString, wxICON_WARNING | wxOK); + msg_dlg.ShowModal(); + return; + } + + wxCommandEvent e(EVT_CALI_TRAY_CHANGED); + e.SetEventObject(m_parent); + wxPostEvent(m_parent, e); +} + +void PlaterPresetComboBox::sys_color_changed() +{ + PresetComboBox::sys_color_changed(); + if (clr_picker) { + clr_picker->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + } +} +} // namespace Slic3r diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp new file mode 100644 index 0000000000..6ff593a16e --- /dev/null +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -0,0 +1,2404 @@ +#include "MoonrakerPrinterAgent.hpp" +#include "Http.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" +#include "slic3r/GUI/DeviceCore/DevManager.h" +#include "../GUI/DeviceCore/DevStorage.h" +#include "../GUI/DeviceCore/DevFirmware.h" +#include "nlohmann/json.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace beast = boost::beast; +namespace http = beast::http; +namespace websocket = beast::websocket; +namespace net = boost::asio; +using tcp = net::ip::tcp; + +struct WsEndpoint +{ + std::string host; + std::string port; + std::string target; + bool secure = false; +}; + +bool parse_ws_endpoint(const std::string& base_url, WsEndpoint& endpoint) +{ + if (base_url.empty()) { + return false; + } + + std::string url = base_url; + if (boost::istarts_with(url, "https://")) { + endpoint.secure = true; + url = url.substr(8); + } else if (boost::istarts_with(url, "http://")) { + url = url.substr(7); + } + + auto slash = url.find('/'); + if (slash != std::string::npos) { + url = url.substr(0, slash); + } + if (url.empty()) { + return false; + } + + endpoint.host = url; + endpoint.port = endpoint.secure ? "443" : "80"; + if (auto colon = url.rfind(':'); colon != std::string::npos && url.find(']') == std::string::npos) { + endpoint.host = url.substr(0, colon); + endpoint.port = url.substr(colon + 1); + } + + endpoint.target = "/websocket"; + return !endpoint.host.empty() && !endpoint.port.empty(); +} + +std::string map_moonraker_state(std::string state) +{ + boost::algorithm::to_lower(state); + if (state == "printing") { + return "RUNNING"; + } + if (state == "paused") { + return "PAUSE"; + } + if (state == "complete") { + return "FINISH"; + } + if (state == "error" || state == "cancelled") { + return "FAILED"; + } + return "IDLE"; +} + +std::string normalize_filament_name_for_match(const std::string& input) +{ + std::string normalized = input; + boost::trim(normalized); + // Ignore profile suffixes like " @0.4 nozzle" for name matching. + if (const auto suffix_pos = normalized.find(" @"); suffix_pos != std::string::npos) { + normalized = normalized.substr(0, suffix_pos); + } + // Remove non-name symbols (e.g. trademark signs) while preserving separators + // commonly used in filament names. + std::string cleaned; + cleaned.reserve(normalized.size()); + for (unsigned char c : normalized) { + if (std::isalnum(c) || c == '-' || c == '+' || c == '/' || std::isspace(c)) { + cleaned.push_back(static_cast(std::toupper(c))); + } else { + cleaned.push_back(' '); + } + } + + // Collapse repeated whitespace. + std::string collapsed; + collapsed.reserve(cleaned.size()); + bool prev_space = true; + for (unsigned char c : cleaned) { + if (std::isspace(c)) { + if (!prev_space) { + collapsed.push_back(' '); + } + prev_space = true; + } else { + collapsed.push_back(static_cast(c)); + prev_space = false; + } + } + boost::trim(collapsed); + return collapsed; +} + +bool filament_name_match_relaxed(const std::string& wanted, const std::string& candidate) +{ + if (wanted == candidate) { + return true; + } + + // Allow lane names with trailing color descriptors, e.g.: + // "ELEGOO RAPID PETG GREY" -> "ELEGOO RAPID PETG". + if (!candidate.empty() && boost::starts_with(wanted, candidate + " ")) { + return true; + } + return false; +} + +std::vector vendor_match_candidates(std::string vendor) +{ + std::vector candidates; + boost::trim(vendor); + if (vendor.empty()) { + return candidates; + } + + candidates.push_back(vendor); + + // Also try first token (e.g. "Bambu Lab" -> "Bambu") without hardcoded aliases. + const auto first_space = vendor.find_first_of(" \t"); + if (first_space != std::string::npos) { + std::string first = vendor.substr(0, first_space); + boost::trim(first); + if (!first.empty() && !boost::iequals(first, vendor)) { + candidates.push_back(first); + } + } + return candidates; +} + +std::string filament_id_by_name(const Slic3r::PresetCollection& filaments, + const std::string& filament_name, + const std::vector& vendor_filters = {}) +{ + if (filament_name.empty()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher received empty filament name"; + return ""; + } + + const std::string wanted = normalize_filament_name_for_match(filament_name); + std::vector normalized_vendor_filters; + normalized_vendor_filters.reserve(vendor_filters.size()); + for (const auto& vendor_filter : vendor_filters) { + const std::string normalized_vendor = normalize_filament_name_for_match(vendor_filter); + if (!normalized_vendor.empty()) { + normalized_vendor_filters.push_back(normalized_vendor); + } + } + + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher lookup requested='" << filament_name + << "' normalized='" << wanted << "' vendor_filters=" << normalized_vendor_filters.size(); + + // Two-pass search: Pass 1 = compatible presets only (ideal), Pass 2 = all visible presets + // (fallback for vendors like eSUN/Eryone that have no printer-specific Kobra X profile). + for (int pass = 1; pass <= 3; ++pass) { + for (size_t i = 0; i < filaments.size(); ++i) { + const auto& preset = filaments.preset(i); + // Pass 1: compatible + visible only + // Pass 2: visible but not necessarily compatible (vendors without printer-specific profile) + // Pass 3: any preset including invisible (vendors not installed as printer) + if (pass <= 2 && !preset.is_visible) { + continue; + } + // User presets (created via "Save As") may have no filament_id yet. + // We still match them by name and use their name as the identifier. + const std::string preset_id = preset.filament_id.empty() ? preset.name : preset.filament_id; + if (pass == 1 && !preset.is_compatible) { + continue; // Pass 1: only compatible presets + } + if (!normalized_vendor_filters.empty()) { + const std::string preset_vendor = normalize_filament_name_for_match(preset.config.opt_string("filament_vendor", 0u)); + bool vendor_match = false; + for (const auto& vendor_filter : normalized_vendor_filters) { + if (preset_vendor == vendor_filter) { + vendor_match = true; + break; + } + } + if (!vendor_match) { + if (pass == 1) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher skip preset='" << preset.name + << "' reason=vendor_filter_miss preset_vendor='" << preset_vendor << "'"; + } + continue; + } + } + const std::string candidate = normalize_filament_name_for_match(preset.name); + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent: filament matcher compare (pass=" << pass << ") preset='" << preset.name + << "' normalized='" << candidate << "' preset_id='" << preset_id << "'"; + if (filament_name_match_relaxed(wanted, candidate)) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher matched (pass=" << pass << ") requested='" << filament_name + << "' normalized='" << wanted << "' to preset='" << preset.name + << "' preset_id='" << preset_id << "'"; + return preset_id; + } + } + if (pass == 1) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher pass 1 (compatible) found no match for '" + << filament_name << "', trying pass 2 (all visible)"; + } else if (pass == 2) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher pass 2 (all visible) found no match for '" + << filament_name << "', trying pass 3 (all presets incl. invisible)"; + } + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: filament matcher found no match for requested='" << filament_name + << "' normalized='" << wanted << "'"; + return ""; +} + +} // namespace + +namespace Slic3r { + +const std::string MoonrakerPrinterAgent_VERSION = "1.0.0"; + +MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) { (void) log_dir; } + +MoonrakerPrinterAgent::~MoonrakerPrinterAgent() +{ + { + std::lock_guard lock(connect_mutex); + device_info = MoonrakerDeviceInfo{}; + ++connect_generation; + } + if (connect_thread.joinable()) { + connect_thread.join(); + } + stop_status_stream(); +} + +AgentInfo MoonrakerPrinterAgent::get_agent_info_static() +{ + return AgentInfo{"moonraker", "Moonraker", MoonrakerPrinterAgent_VERSION, "Klipper/Moonraker printer agent"}; +} + +void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr cloud) +{ + std::lock_guard lock(state_mutex); + m_cloud_agent = cloud; +} + +int MoonrakerPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) +{ + (void) qos; + (void) flag; + return handle_request(dev_id, json_str); +} + +int MoonrakerPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) +{ + (void) qos; + (void) flag; + return handle_request(dev_id, json_str); +} + +int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +{ + if (dev_id.empty() || dev_ip.empty()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + std::string base_url; + std::string api_key; + uint64_t gen; + { + std::lock_guard lock(connect_mutex); + init_device_info(dev_id, dev_ip, username, password, use_ssl); + gen = ++connect_generation; + base_url = device_info.base_url; + api_key = device_info.api_key; + if (connect_thread.joinable()) { + connect_thread.detach(); + } + } + + // Stop existing status stream and clear state + stop_status_stream(); + { + std::lock_guard lock(payload_mutex); + status_cache = nlohmann::json::object(); + } + ws_last_emit_ms.store(0); + ws_last_dispatch_ms.store(0); + last_print_state.clear(); + + // Launch connection in background thread (capture by value to avoid data races) + { + std::lock_guard lock(connect_mutex); + connect_thread = std::thread([this, dev_id, base_url, api_key, gen]() { perform_connection_async(dev_id, base_url, api_key, gen); }); + } + + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::disconnect_printer() +{ + { + std::lock_guard lock(connect_mutex); + device_info = MoonrakerDeviceInfo{}; + ++connect_generation; // Invalidate any in-flight connection + if (connect_thread.joinable()) { + connect_thread.detach(); + } + } + + stop_status_stream(); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::check_cert() { return BAMBU_NETWORK_SUCCESS; } + +void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) +{ + (void) dev_id; + (void) lan_only; +} + +bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending) +{ + (void) sending; + if (start) { + announce_printhost_device(); + } + return true; +} + +int MoonrakerPrinterAgent::ping_bind(std::string ping_code) +{ + (void) ping_code; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) +{ + (void) sec_link; + + detect.dev_id = device_info.dev_id.empty() ? dev_ip : device_info.dev_id; + detect.model_id = device_info.model_id.empty() ? device_info.model_name : device_info.model_id; + // Prefer fetched hostname, then preset model name, then generic fallback + detect.dev_name = device_info.dev_name; + detect.model_id = device_info.model_id; + detect.version = device_info.version; + detect.connect_type = "lan"; + detect.bind_state = "free"; + + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::bind( + std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) +{ + (void) dev_ip; + (void) dev_id; + (void) sec_link; + (void) timezone; + (void) improved; + (void) update_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::unbind(std::string dev_id) +{ + (void) dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::request_bind_ticket(std::string* ticket) +{ + if (ticket) + *ticket = ""; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_server_callback(OnServerErrFn fn) +{ + std::lock_guard lock(state_mutex); + on_server_err_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +std::string MoonrakerPrinterAgent::get_user_selected_machine() +{ + std::lock_guard lock(state_mutex); + return selected_machine; +} + +int MoonrakerPrinterAgent::set_user_selected_machine(std::string dev_id) +{ + std::lock_guard lock(state_mutex); + selected_machine = dev_id; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) +{ + (void) params; + (void) update_fn; + (void) cancel_fn; + (void) wait_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_local_print_with_record(PrintParams params, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn, + OnWaitFn wait_fn) +{ + (void) params; + (void) update_fn; + (void) cancel_fn; + (void) wait_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn, + OnWaitFn wait_fn) +{ + (void) wait_fn; + + if (update_fn) + update_fn(PrintingStageCreate, 0, "Preparing..."); + + std::string filename = params.filename; + if (filename.empty()) { + filename = params.task_name; + } + if (!boost::iends_with(filename, ".gcode")) { + filename += ".gcode"; + } + + // Sanitize filename to prevent path traversal attacks + std::string safe_filename = sanitize_filename(filename); + + // Upload only, don't start print + if (!upload_gcode(params.filename, safe_filename, device_info.base_url, device_info.api_key, update_fn, cancel_fn)) { + return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; + } + + if (update_fn) + update_fn(PrintingStageFinished, 100, "File uploaded"); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + if (update_fn) + update_fn(PrintingStageCreate, 0, "Preparing..."); + + // Check cancellation + if (cancel_fn && cancel_fn()) { + return BAMBU_NETWORK_ERR_CANCELED; + } + // Determine the G-code file to upload + // params.filename may be .3mf, params.dst_file contains actual G-code + std::string gcode_path = params.filename; + if (!params.dst_file.empty()) { + gcode_path = params.dst_file; + } + + // Check if file exists and has .gcode extension + namespace fs = boost::filesystem; + fs::path source_path(gcode_path); + if (!fs::exists(source_path)) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: G-code file does not exist: " << gcode_path; + return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; + } + + // Extract filename for upload (relative to gcodes root) + std::string upload_filename = source_path.filename().string(); + if (!boost::iends_with(upload_filename, ".gcode")) { + upload_filename += ".gcode"; + } + // Sanitize filename to prevent path traversal attacks (extra safety) + upload_filename = sanitize_filename(upload_filename); + + // Upload file + if (update_fn) + update_fn(PrintingStageUpload, 0, "Uploading G-code..."); + if (!upload_gcode(gcode_path, upload_filename, device_info.base_url, device_info.api_key, update_fn, cancel_fn)) { + return BAMBU_NETWORK_ERR_PRINT_LP_UPLOAD_FTP_FAILED; + } + + // Check cancellation + if (cancel_fn && cancel_fn()) { + return BAMBU_NETWORK_ERR_CANCELED; + } + + // Start print via gcode script (simpler than JSON-RPC) + if (update_fn) + update_fn(PrintingStageSending, 0, "Starting print..."); + std::string gcode = "SDCARD_PRINT_FILE FILENAME=" + upload_filename; + if (!send_gcode(device_info.dev_id, gcode)) { + return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; + } + + if (update_fn) + update_fn(PrintingStageFinished, 100, "Print started"); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) +{ + (void) params; + (void) update_fn; + (void) cancel_fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn) +{ + { + std::lock_guard lock(state_mutex); + on_ssdp_msg_fn = fn; + } + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_printer_connected_fn(OnPrinterConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_printer_connected_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) +{ + std::lock_guard lock(state_mutex); + on_subscribe_failure_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_user_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_user_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn) +{ + std::lock_guard lock(state_mutex); + on_local_connect_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_on_local_message_fn(OnMessageFn fn) +{ + std::lock_guard lock(state_mutex); + on_local_message_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) +{ + std::lock_guard lock(state_mutex); + queue_on_main_fn = fn; + return BAMBU_NETWORK_SUCCESS; +} + +void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays, int active_lane_index) +{ + + // Look up MachineObject via DeviceManager + auto* dev_manager = GUI::wxGetApp().getDeviceManager(); + if (!dev_manager) { + return; + } + MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id); + if (!obj) { + return; + } + + // Build BBL-format JSON for DevFilaSystemParser::ParseV1_0 + nlohmann::json ams_json = nlohmann::json::object(); + nlohmann::json ams_array = nlohmann::json::array(); + + // Calculate ams_exist_bits and tray_exist_bits + unsigned long ams_exist_bits = 0; + unsigned long tray_exist_bits = 0; + + for (int ams_id = 0; ams_id < ams_count; ++ams_id) { + ams_exist_bits |= (1 << ams_id); + + nlohmann::json ams_unit = nlohmann::json::object(); + ams_unit["id"] = std::to_string(ams_id); + ams_unit["info"] = "0002"; // treat as AMS_LITE + + nlohmann::json tray_array = nlohmann::json::array(); + int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4); + for (int slot_id = 0; slot_id <= max_slot_in_this_ams; ++slot_id) { + int slot_index = ams_id * 4 + slot_id; + + // Find tray with matching slot_index + const AmsTrayData* tray = nullptr; + for (const auto& t : trays) { + if (t.slot_index == slot_index) { + tray = &t; + break; + } + } + + nlohmann::json tray_json = nlohmann::json::object(); + tray_json["id"] = std::to_string(slot_id); + tray_json["tag_uid"] = "0000000000000000"; + + if (tray && tray->has_filament) { + tray_exist_bits |= (1 << slot_index); + + tray_json["tray_info_idx"] = tray->tray_info_idx; + tray_json["tray_type"] = tray->tray_type; + tray_json["tray_sub_brands"] = tray->tray_sub_brands.empty() ? tray->tray_type : tray->tray_sub_brands; + tray_json["tray_color"] = normalize_color_value(tray->tray_color); + + // Add temperature data if provided + if (tray->bed_temp > 0) { + tray_json["bed_temp"] = std::to_string(tray->bed_temp); + } + if (tray->nozzle_temp > 0) { + tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp); + } + } else { + tray_json["tray_info_idx"] = ""; + tray_json["tray_type"] = ""; + tray_json["tray_color"] = "00000000"; + tray_json["tray_slot_placeholder"] = "1"; + } + + tray_array.push_back(tray_json); + } + ams_unit["tray"] = tray_array; + ams_array.push_back(ams_unit); + } + + // Format as hex strings (matching BBL protocol) + std::ostringstream ams_exist_ss; + ams_exist_ss << std::hex << std::uppercase << ams_exist_bits; + std::ostringstream tray_exist_ss; + tray_exist_ss << std::hex << std::uppercase << tray_exist_bits; + + ams_json["ams"] = ams_array; + ams_json["ams_exist_bits"] = ams_exist_ss.str(); + ams_json["tray_exist_bits"] = tray_exist_ss.str(); + if (active_lane_index >= 0) { + const std::string active_lane = std::to_string(active_lane_index); + ams_json["tray_now"] = active_lane; + ams_json["tray_tar"] = active_lane; + } + + // Wrap in the expected structure for ParseV1_0 + nlohmann::json print_json = nlohmann::json::object(); + print_json["ams"] = ams_json; + + // Call the parser to populate DevFilaSystem + DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + ExtderSystemParser::ParseV1_0(print_json, obj->GetExtderSystem()); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; + + // Set printer_type so update_sync_status() can match it against the preset's printer type. + // Without this, the comparison fails and all sync badges are cleared. + obj->printer_type = device_info.model_id; + + // Set push counters so is_info_ready() returns true for pull-mode agents. + if (obj->m_push_count == 0) { + obj->m_push_count = 1; + } + if (obj->m_full_msg_count == 0) { + obj->m_full_msg_count = 1; + } + obj->last_push_time = std::chrono::system_clock::now(); + + // Set storage state - Moonraker printers use virtual_sdcard, storage is always available. + // This is required for SelectMachineDialog to allow printing (otherwise it blocks with "No SD card"). + obj->GetStorage()->set_sdcard_state(DevStorage::HAS_SDCARD_NORMAL); + + // Populate module_vers so is_info_ready() passes the version check. + // Moonraker printers don't have BBL-style version info, but we need a non-empty map. + if (obj->module_vers.empty()) { + DevFirmwareVersionInfo ota_info; + ota_info.name = "ota"; + ota_info.sw_ver = "1.0.0"; // Placeholder version for Moonraker printers + obj->module_vers.emplace("ota", ota_info); + } +} + +bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) +{ + std::vector trays; + int max_lane_index = 0; + int active_lane_index = -1; + + // Try Moonraker filament data (more generic, supports any filament changer + // software that reports lane data to Moonraker like AFC and recent Happy + // Hare as of Feb 15, 2026) + if (fetch_moonraker_filament_data(trays, max_lane_index)) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with " + << (max_lane_index + 1) << " lanes"; + int ams_count = (max_lane_index + 4) / 4; + build_ams_payload(ams_count, max_lane_index, trays, active_lane_index); + return true; + } + + // Attempt Happy Hare first (more widely adopted, supports more filament changers) + if (fetch_hh_filament_info(trays, max_lane_index, active_lane_index)) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with " + << (max_lane_index + 1) << " gates"; + int ams_count = (max_lane_index + 4) / 4; + build_ams_payload(ams_count, max_lane_index, trays, active_lane_index); + return true; + } + + // No MMU detected - this is normal for printers without MMU, not an error + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: No MMU system detected (neither HH nor Moonraker)"; + return false; +} + +std::string MoonrakerPrinterAgent::trim_and_upper(const std::string& input) +{ + std::string result = input; + boost::trim(result); + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return static_cast(std::toupper(c)); }); + return result; +} + +std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::string& filament_type) +{ + const std::string upper = trim_and_upper(filament_type); + + // Map to OrcaFilamentLibrary preset IDs (compatible with all printers) + // Source: resources/profiles/OrcaFilamentLibrary/filament/ + + // PLA variants + if (upper == "PLA") return "OGFL99"; + if (upper == "PLA-CF") return "OGFL98"; + if (upper == "PLA SILK" || upper == "PLA-SILK") return "OGFL96"; + if (upper == "PLA HIGH SPEED" || upper == "PLA-HS" || upper == "PLA HS") return "OGFL95"; + + // ABS/ASA variants + if (upper == "ABS") return "OGFB99"; + if (upper == "ASA") return "OGFB98"; + + // PETG/PET variants + if (upper == "PETG" || upper == "PET") return "OGFG99"; + if (upper == "PCTG") return "OGFG97"; + + // PA/Nylon variants + if (upper == "PA" || upper == "NYLON") return "OGFN99"; + if (upper == "PA-CF") return "OGFN98"; + if (upper == "PPA" || upper == "PPA-CF") return "OGFN97"; + if (upper == "PPA-GF") return "OGFN96"; + + // PC variants + if (upper == "PC") return "OGFC99"; + + // PP/PE variants + if (upper == "PE") return "OGFP99"; + if (upper == "PP") return "OGFP97"; + + // Support materials + if (upper == "PVA") return "OGFS99"; + if (upper == "HIPS") return "OGFS98"; + if (upper == "BVOH") return "OGFS97"; + + // TPU variants + if (upper == "TPU") return "OGFU99"; + + // Other materials + if (upper == "EVA") return "OGFR99"; + if (upper == "PHA") return "OGFR98"; + if (upper == "COPE") return "OGFLC99"; + if (upper == "SBS") return "OFLSBS99"; + + // Unknown material + return UNKNOWN_FILAMENT_ID; +} + +// JSON helper methods - null-safe accessors +std::string MoonrakerPrinterAgent::safe_json_string(const nlohmann::json& obj, const char* key) +{ + auto it = obj.find(key); + if (it != obj.end() && it->is_string()) + return it->get(); + return ""; +} + +int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char* key) +{ + auto it = obj.find(key); + if (it != obj.end() && it->is_number()) + return it->get(); + return 0; +} + +std::string MoonrakerPrinterAgent::safe_array_string(const nlohmann::json& arr, int idx) +{ + if (arr.is_array() && idx >= 0 && idx < static_cast(arr.size()) && arr[idx].is_string()) + return arr[idx].get(); + return ""; +} + +int MoonrakerPrinterAgent::safe_array_int(const nlohmann::json& arr, int idx) +{ + if (arr.is_array() && idx >= 0 && idx < static_cast(arr.size()) && arr[idx].is_number()) + return arr[idx].get(); + return 0; +} + +std::string MoonrakerPrinterAgent::normalize_color_value(const std::string& color) +{ + std::string value = color; + boost::trim(value); + + // Remove 0x or 0X prefix if present + if (value.size() >= 2 && (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0)) { + value = value.substr(2); + } + // Remove # prefix if present + if (!value.empty() && value[0] == '#') { + value = value.substr(1); + } + + // Extract only hex digits + std::string normalized; + for (char c : value) { + if (std::isxdigit(static_cast(c))) { + normalized.push_back(static_cast(std::toupper(static_cast(c)))); + } + } + + // If 6 hex digits, add FF alpha + if (normalized.size() == 6) { + normalized += "FF"; + } + + // Validate length - return default if invalid + if (normalized.size() != 8) { + return "00000000"; + } + + return normalized; +} + +// Fetch filament info from moonraker database +bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index) +{ + // Fetch lane data from Moonraker database + std::string url = join_url(device_info.base_url, "/server/database/item?namespace=lane_data"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Failed to fetch lane data: " << http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Invalid JSON response"; + return false; + } + + // Expected structure: { "result": { "namespace": "lane_data", "value": { "lane1": {...}, ... } } } + if (!json.contains("result") || !json["result"].contains("value") || !json["result"]["value"].is_object()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Unexpected JSON structure or no lane_data found"; + return false; + } + + // Parse response into AmsTrayData + const auto& value = json["result"]["value"]; + trays.clear(); + max_lane_index = 0; + + for (const auto& [lane_key, lane_obj] : value.items()) { + if (!lane_obj.is_object()) { + continue; + } + + // Extract lane index from the "lane" field (tool number, 0-based) + std::string lane_str = safe_json_string(lane_obj, "lane"); + int lane_index = -1; + if (!lane_str.empty()) { + try { + lane_index = std::stoi(lane_str); + } catch (...) { + lane_index = -1; + } + } + + if (lane_index < 0) { + continue; + } + + AmsTrayData tray; + tray.slot_index = lane_index; + tray.tray_color = safe_json_string(lane_obj, "color"); + tray.tray_type = safe_json_string(lane_obj, "material"); + const std::string lane_name = safe_json_string(lane_obj, "name"); + const std::string lane_vendor = safe_json_string(lane_obj, "vendor_name"); + tray.tray_sub_brands = lane_name; + tray.bed_temp = safe_json_int(lane_obj, "bed_temp"); + tray.nozzle_temp = safe_json_int(lane_obj, "nozzle_temp"); + tray.has_filament = !tray.tray_type.empty(); + auto* bundle = GUI::wxGetApp().preset_bundle; + if (bundle) { + const auto vendor_candidates = vendor_match_candidates(lane_vendor); + auto match_with_vendor_prefix = [&](const std::string& suffix) -> std::string { + if (suffix.empty()) { + return ""; + } + for (const auto& vendor_candidate : vendor_candidates) { + const std::string requested = vendor_candidate + " " + suffix; + std::string match_id = filament_id_by_name(bundle->filaments, requested, vendor_candidates); + if (!match_id.empty()) { + return match_id; + } + } + return ""; + }; + + // Prefer the most specific lane identity first, then broader vendor/material mapping. + tray.tray_info_idx = match_with_vendor_prefix(lane_name); + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = match_with_vendor_prefix(tray.tray_type); + } + if (tray.tray_info_idx.empty() && !lane_name.empty()) { + tray.tray_info_idx = filament_id_by_name(bundle->filaments, lane_name, vendor_candidates); + } + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane='" << lane_key + << "' index=" << lane_index << " material='" << tray.tray_type + << "' vendor='" << lane_vendor << "' vendor_candidates=" << vendor_candidates.size() + << "' name='" << lane_name + << "' mapped_by='preset_bundle' tray_info_idx='" << tray.tray_info_idx << "'"; + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane='" << lane_key + << "' index=" << lane_index << " material='" << tray.tray_type + << "' mapped_by='generic_fallback' tray_info_idx='" << tray.tray_info_idx << "'"; + } + + max_lane_index = std::max(max_lane_index, lane_index); + trays.push_back(tray); + } + + if (trays.empty()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: No lanes found"; + return false; + } + + return true; +} + +// Fetch filament info from Happy Hare MMU +bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector& trays, int& max_lane_index, int& active_lane_index) +{ + // Query Happy Hare MMU status + std::string url = join_url(device_info.base_url, "/printer/objects/query?mmu"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: Failed to fetch HH data: " << http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: Invalid JSON response"; + return false; + } + + // Expected structure: { "result": { "status": { "mmu": { ... } } } } + if (!json.contains("result") || !json["result"].contains("status") || + !json["result"]["status"].contains("mmu") || !json["result"]["status"]["mmu"].is_object()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No mmu object in response"; + return false; + } + + const auto& mmu = json["result"]["status"]["mmu"]; + + // Check if HH is installed (empty mmu object means HH not installed) + if (mmu.empty()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: Empty mmu object (HH not installed)"; + return false; + } + + // Get num_gates + if (!mmu.contains("num_gates") || !mmu["num_gates"].is_number()) { + BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No num_gates field"; + return false; + } + + int num_gates = mmu["num_gates"].get(); + if (num_gates <= 0) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_hh_filament_info: Invalid num_gates: " << num_gates; + return false; + } + + // Get arrays + const auto& gate_status = mmu.contains("gate_status") ? mmu["gate_status"] : nlohmann::json::array(); + const auto& gate_material = mmu.contains("gate_material") ? mmu["gate_material"] : nlohmann::json::array(); + const auto& gate_filament_name = mmu.contains("gate_filament_name") ? mmu["gate_filament_name"] : nlohmann::json::array(); + const auto& gate_color = mmu.contains("gate_color") ? mmu["gate_color"] : nlohmann::json::array(); + const auto& gate_temperature = mmu.contains("gate_temperature") ? mmu["gate_temperature"] : nlohmann::json::array(); + const int active_gate = safe_json_int(mmu, "gate"); + active_lane_index = active_gate; + std::string active_filament_name; + if (mmu.contains("active_filament") && mmu["active_filament"].is_object()) { + active_filament_name = safe_json_string(mmu["active_filament"], "filament_name"); + if (active_filament_name.empty()) { + active_filament_name = safe_json_string(mmu["active_filament"], "material"); + } + } + + if (!gate_status.is_array() || !gate_material.is_array() || + !gate_color.is_array() || !gate_temperature.is_array()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_hh_filament_info: HH arrays not found or invalid type"; + return false; + } + + // Parse gate data + trays.clear(); + max_lane_index = 0; + + for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) { + // Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available + int status = safe_array_int(gate_status, gate_idx); + if (status <= 0) { + continue; // Skip unknown or empty gates + } + + // Extract gate data + std::string material = safe_array_string(gate_material, gate_idx); + std::string filament_name = safe_array_string(gate_filament_name, gate_idx); + std::string color = safe_array_string(gate_color, gate_idx); + int nozzle_temp = safe_array_int(gate_temperature, gate_idx); + // For the active gate, prefer active_filament from MMU state. + if (gate_idx == active_gate && !active_filament_name.empty()) { + filament_name = active_filament_name; + } + + // Skip if no material type (empty gate) + if (material.empty()) { + continue; + } + + AmsTrayData tray; + tray.slot_index = gate_idx; + tray.tray_type = material; + tray.tray_sub_brands = filament_name; + tray.tray_color = color; + tray.nozzle_temp = nozzle_temp; + tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays + tray.has_filament = true; + + auto* bundle = GUI::wxGetApp().preset_bundle; + if (bundle) { + tray.tray_info_idx = filament_id_by_name(bundle->filaments, filament_name); + if (tray.tray_info_idx.empty()) { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + } + + max_lane_index = std::max(max_lane_index, gate_idx); + trays.push_back(tray); + } + + if (trays.empty()) { + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No valid HH gates found"; + return false; + } + + return true; +} + +int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str) +{ + auto json = nlohmann::json::parse(json_str, nullptr, false); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Invalid JSON request"; + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } + + // Handle info commands + if (json.contains("info") && json["info"].contains("command")) { + const auto& command = json["info"]["command"]; + if (command.is_string() && command.get() == "get_version") { + return send_version_info(dev_id); + } + } + + // Handle system commands + if (json.contains("system") && json["system"].contains("command")) { + const auto& command = json["system"]["command"]; + if (command.is_string() && command.get() == "get_access_code") { + return send_access_code(dev_id); + } + } + + // Handle print commands + if (json.contains("print") && json["print"].contains("command")) { + const auto& command = json["print"]["command"]; + if (!command.is_string()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: print command is not a string"; + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } + + const std::string cmd = command.get(); + + // Handle gcode_line command - this is how G-code commands are sent from OrcaSlicer + if (cmd == "gcode_line") { + if (!json["print"].contains("param") || !json["print"]["param"].is_string()) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: gcode_line missing param value, full json: " << json_str; + return BAMBU_NETWORK_ERR_INVALID_RESULT; + } + std::string gcode = json["print"]["param"].get(); + + // Extract sequence_id from request if present + std::string sequence_id; + if (json["print"].contains("sequence_id") && json["print"]["sequence_id"].is_string()) { + sequence_id = json["print"]["sequence_id"].get(); + } + + nlohmann::json response; + response["print"]["command"] = "gcode_line"; + if (!sequence_id.empty()) { + response["print"]["sequence_id"] = sequence_id; + } + response["print"]["param"] = gcode; + + if (send_gcode(dev_id, gcode)) { + response["print"]["result"] = "success"; + dispatch_message(dev_id, response.dump()); + return BAMBU_NETWORK_SUCCESS; + } + response["print"]["result"] = "failed"; + dispatch_message(dev_id, response.dump()); + return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + } + + // Print control commands + if (cmd == "pause") { + return pause_print(dev_id); + } + if (cmd == "resume") { + return resume_print(dev_id); + } + if (cmd == "stop") { + return cancel_print(dev_id); + } + + // Bed temperature - UI sends "temp" field + if (cmd == "set_bed_temp") { + if (json["print"].contains("temp") && json["print"]["temp"].is_number()) { + int temp = json["print"]["temp"].get(); + std::string gcode = "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=" + std::to_string(temp); + send_gcode(dev_id, gcode); + return BAMBU_NETWORK_SUCCESS; + } + } + + // Nozzle temperature - UI sends "target_temp" and "extruder_index" fields + if (cmd == "set_nozzle_temp") { + if (json["print"].contains("target_temp") && json["print"]["target_temp"].is_number()) { + int temp = json["print"]["target_temp"].get(); + int extruder_idx = 0; // Default to main extruder + if (json["print"].contains("extruder_index") && json["print"]["extruder_index"].is_number()) { + extruder_idx = json["print"]["extruder_index"].get(); + } + std::string heater = (extruder_idx == 0) ? "extruder" : "extruder" + std::to_string(extruder_idx); + std::string gcode = "SET_HEATER_TEMPERATURE HEATER=" + heater + " TARGET=" + std::to_string(temp); + send_gcode(dev_id, gcode); + return BAMBU_NETWORK_SUCCESS; + } + } + + if (cmd == "home") { + return send_gcode(dev_id, "G28") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + } + } + + return BAMBU_NETWORK_SUCCESS; +} + +bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +{ + device_info = MoonrakerDeviceInfo{}; + auto* preset_bundle = GUI::wxGetApp().preset_bundle; + if (!preset_bundle) { + return false; + } + + auto& preset = preset_bundle->printers.get_edited_preset(); + const auto& printer_cfg = preset.config; + device_info.dev_ip = dev_ip; + + device_info.api_key = password; + device_info.model_name = printer_cfg.opt_string("printer_model"); + device_info.model_id = preset.get_printer_type(preset_bundle); + device_info.base_url = use_ssl ? "https://" + dev_ip : "http://" + dev_ip; + device_info.dev_id = dev_id; + device_info.version = ""; + device_info.dev_name = device_info.dev_id; + + return true; +} + +bool MoonrakerPrinterAgent::fetch_device_info(const std::string& base_url, + const std::string& api_key, + MoonrakerDeviceInfo& info, + std::string& error) const +{ + auto fetch_json = [&](const std::string& url, nlohmann::json& out) { + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + out = nlohmann::json::parse(response_body, nullptr, false, true); + if (out.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + return true; + }; + + nlohmann::json json; + std::string url = join_url(base_url, "/server/info"); + if (!fetch_json(url, json)) { + return false; + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + info.dev_name = result.value("machine_name", result.value("hostname", "")); + info.version = result.value("moonraker_version", ""); + info.klippy_state = result.value("klippy_state", ""); + + return true; +} + +bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, + const std::string& api_key, + nlohmann::json& status, + std::string& error) const +{ + std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&extruder&heater_bed&fan"); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status_code) { + if (status_code == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status_code); + } + }) + .on_error([&](std::string body, std::string err, unsigned status_code) { + http_error = err; + if (status_code > 0) { + http_error += " (HTTP " + std::to_string(status_code) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + if (!json.contains("result") || !json["result"].contains("status")) { + error = "Unexpected JSON structure"; + return false; + } + + status = json["result"]["status"]; + return true; +} + +bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::string& gcode) const +{ + nlohmann::json payload; + payload["script"] = gcode; + std::string payload_str = payload.dump(); + + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::post(join_url(device_info.base_url, "/printer/gcode/script")); + if (!device_info.api_key.empty()) { + http.header("X-Api-Key", device_info.api_key); + } + http.header("Content-Type", "application/json") + .set_post_body(payload_str) + .timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status_code) { + if (status_code == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status_code); + } + }) + .on_error([&](std::string body, std::string err, unsigned status_code) { + http_error = err; + if (status_code > 0) { + http_error += " (HTTP " + std::to_string(status_code) + ")"; + } + }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode failed: " << http_error; + return false; + } + + return true; +} + +bool MoonrakerPrinterAgent::fetch_object_list(const std::string& base_url, + const std::string& api_key, + std::set& objects, + std::string& error) const +{ + std::string response_body; + bool success = false; + std::string http_error; + + auto http = Http::get(join_url(base_url, "/printer/objects/list")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; + } else { + http_error = "HTTP error: " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); + + if (!success) { + error = http_error.empty() ? "Connection failed" : http_error; + return false; + } + + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + nlohmann::json result = json.contains("result") ? json["result"] : json; + if (!result.contains("objects") || !result["objects"].is_array()) { + error = "Unexpected JSON structure"; + return false; + } + + objects.clear(); + for (const auto& entry : result["objects"]) { + if (entry.is_string()) { + objects.insert(entry.get()); + } + } + + return !objects.empty(); +} + +int MoonrakerPrinterAgent::send_version_info(const std::string& dev_id) +{ + nlohmann::json payload; + payload["info"]["command"] = "get_version"; + payload["info"]["result"] = "success"; + payload["info"]["module"] = nlohmann::json::array(); + + nlohmann::json module; + module["name"] = "ota"; + module["sw_ver"] = device_info.version; + module["product_name"] = "Moonraker"; + payload["info"]["module"].push_back(module); + + dispatch_message(dev_id, payload.dump()); + return BAMBU_NETWORK_SUCCESS; +} + +int MoonrakerPrinterAgent::send_access_code(const std::string& dev_id) +{ + nlohmann::json payload; + payload["system"]["command"] = "get_access_code"; + payload["system"]["access_code"] = device_info.api_key; + dispatch_message(dev_id, payload.dump()); + return BAMBU_NETWORK_SUCCESS; +} + +void MoonrakerPrinterAgent::announce_printhost_device() +{ + OnMsgArrivedFn ssdp_fn; + { + std::lock_guard lock(state_mutex); + ssdp_fn = on_ssdp_msg_fn; + if (!ssdp_fn) { + return; + } + if (ssdp_announced_host == device_info.base_url && !ssdp_announced_id.empty()) { + return; + } + } + + // Try to fetch actual device name from Moonraker + // Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback + std::string dev_name; + MoonrakerDeviceInfo info; + std::string fetch_error; + if (fetch_device_info(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) { + dev_name = info.dev_name; + } else { + dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name; + } + + const std::string model_id = device_info.model_id; + + if (auto* app_config = GUI::wxGetApp().app_config) { + const std::string access_code = device_info.api_key.empty() ? "88888888" : device_info.api_key; + app_config->set_str("access_code", device_info.dev_id, access_code); + app_config->set_str("user_access_code", device_info.dev_id, access_code); + } + + nlohmann::json payload; + payload["dev_name"] = dev_name; + payload["dev_id"] = device_info.dev_id; + payload["dev_ip"] = device_info.dev_ip; + payload["dev_type"] = model_id.empty() ? dev_name : model_id; + payload["dev_signal"] = "0"; + payload["connect_type"] = "lan"; + payload["bind_state"] = "free"; + payload["sec_link"] = "secure"; + payload["ssdp_version"] = "v1"; + + ssdp_fn(payload.dump()); + + { + std::lock_guard lock(state_mutex); + ssdp_announced_host = device_info.base_url; + ssdp_announced_id = device_info.dev_id; + + // Set this as the selected machine if nothing is currently selected + if (selected_machine.empty()) { + selected_machine = device_info.dev_id; + } + } +} + +void MoonrakerPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg) +{ + OnLocalConnectedFn local_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + local_fn = on_local_connect_fn; + queue_fn = queue_on_main_fn; + } + if (!local_fn) { + return; + } + + auto dispatch = [state, dev_id, msg, local_fn]() { local_fn(state, dev_id, msg); }; + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +void MoonrakerPrinterAgent::dispatch_printer_connected(const std::string& dev_id) +{ + OnPrinterConnectedFn connected_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + connected_fn = on_printer_connected_fn; + queue_fn = queue_on_main_fn; + } + if (!connected_fn) { + return; + } + + auto dispatch = [dev_id, connected_fn]() { connected_fn(dev_id); }; + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +void MoonrakerPrinterAgent::start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key) +{ + stop_status_stream(); + if (base_url.empty()) { + return; + } + + ws_stop.store(false); + ws_thread = std::thread([this, dev_id, base_url, api_key]() { run_status_stream(dev_id, base_url, api_key); }); +} + +void MoonrakerPrinterAgent::stop_status_stream() +{ + ws_stop.store(true); + if (ws_thread.joinable()) { + ws_thread.join(); + } +} + +void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string base_url, std::string api_key) +{ + WsEndpoint endpoint; + if (!parse_ws_endpoint(base_url, endpoint)) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket endpoint invalid for base_url=" << base_url; + return; + } + if (endpoint.secure) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket wss not supported for base_url=" << base_url; + return; + } + + // Reconnection logic + ws_reconnect_requested.store(false); // Reset reconnect flag + int retry_count = 0; + const int max_retries = 10; + const int base_delay_ms = 1000; + + while (!ws_stop.load() && retry_count < max_retries) { + bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect + + try { + net::io_context ioc; + tcp::resolver resolver{ioc}; + beast::tcp_stream stream{ioc}; + + stream.expires_after(std::chrono::seconds(10)); + auto const results = resolver.resolve(endpoint.host, endpoint.port); + stream.connect(results); + + websocket::stream ws{std::move(stream)}; + ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { + req.set(http::field::user_agent, "OrcaSlicer"); + if (!api_key.empty()) { + req.set("X-Api-Key", api_key); + } + })); + + std::string host_header = endpoint.host; + if (!endpoint.port.empty() && endpoint.port != "80") { + host_header += ":" + endpoint.port; + } + ws.handshake(host_header, endpoint.target); + ws.text(true); + + // Send client identification + nlohmann::json identify; + identify["jsonrpc"] = "2.0"; + identify["method"] = "server.connection.identify"; + identify["params"]["client_name"] = "OrcaSlicer"; + identify["params"]["version"] = MoonrakerPrinterAgent_VERSION; + identify["params"]["type"] = "agent"; + identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer"; + identify["id"] = 0; + ws.write(net::buffer(identify.dump())); + + std::set subscribe_objects = {"print_stats", "virtual_sdcard"}; + std::set available_objects; + std::string list_error; + if (fetch_object_list(base_url, api_key, available_objects, list_error)) { + { + std::lock_guard lock(payload_mutex); + this->available_objects = std::move(available_objects); + } + + if (this->available_objects.count("heater_bed") != 0) { + subscribe_objects.insert("heater_bed"); + } + if (this->available_objects.count("fan") != 0) { + subscribe_objects.insert("fan"); + } + + // Add toolhead for homing status + if (this->available_objects.count("toolhead") != 0) { + subscribe_objects.insert("toolhead"); + } + + // Add display_status for layer info (if available) + if (this->available_objects.count("display_status") != 0) { + subscribe_objects.insert("display_status"); + } + + for (const auto& name : this->available_objects) { + if (name == "extruder" || name.rfind("extruder", 0) == 0) { + subscribe_objects.insert(name); + if (name == "extruder") { + break; + } + } + } + } else { + subscribe_objects.insert("extruder"); + subscribe_objects.insert("heater_bed"); + subscribe_objects.insert("toolhead"); // Add toolhead as fallback + subscribe_objects.insert("fan"); // Try to subscribe to fan as fallback + } + + nlohmann::json subscribe; + subscribe["jsonrpc"] = "2.0"; + subscribe["method"] = "printer.objects.subscribe"; + nlohmann::json objects = nlohmann::json::object(); + for (const auto& name : subscribe_objects) { + objects[name] = nullptr; + } + subscribe["params"]["objects"] = std::move(objects); + subscribe["id"] = 1; + ws.write(net::buffer(subscribe.dump())); + + // Read loop + while (!ws_stop.load()) { + ws.next_layer().expires_after(std::chrono::seconds(2)); + beast::flat_buffer buffer; + beast::error_code ec; + ws.read(buffer, ec); + if (ec == beast::error::timeout) { + const auto now_ms = static_cast( + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + const auto last_ms = ws_last_emit_ms.load(); + if (last_ms == 0 || now_ms - last_ms >= 10000) { + nlohmann::json message; + { + std::lock_guard lock(payload_mutex); + message = build_print_payload_locked(); + } + dispatch_message(dev_id, message.dump()); + ws_last_emit_ms.store(now_ms); + } + continue; + } + if (ec == websocket::error::closed) { + connection_lost = true; + break; + } + if (ec) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << ec.message(); + connection_lost = true; + break; + } + handle_ws_message(dev_id, beast::buffers_to_string(buffer.data())); + // Check if handle_ws_message triggered reconnection request + if (ws_reconnect_requested.exchange(false)) { + connection_lost = true; + break; + } + } + + beast::error_code ec; + ws.close(websocket::close_code::normal, ec); + + // Only reset retry count on clean shutdown (not connection_lost) + if (!connection_lost && !ws_stop.load()) { + retry_count = 0; + } + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket disconnected: " << e.what(); + connection_lost = true; + } + + // Exit loop on clean shutdown + if (!connection_lost) { + break; + } + + // Check if we should stop reconnection attempts + if (ws_stop.load()) { + break; + } + + // Exponential backoff before reconnection + int delay_ms = base_delay_ms * (1 << std::min(retry_count, 5)); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Reconnecting in " << delay_ms << "ms (attempt " << (retry_count + 1) << ")"; + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + retry_count++; + } + + if (retry_count >= max_retries) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Max reconnection attempts reached"; + dispatch_local_connect(ConnectStatusLost, dev_id, "max_retries"); + } +} + +void MoonrakerPrinterAgent::handle_ws_message(const std::string& dev_id, const std::string& payload) +{ + auto json = nlohmann::json::parse(payload, nullptr, false); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Invalid WebSocket message JSON"; + return; + } + + bool updated = false; + bool is_critical = false; // Track if this is a critical update that bypasses throttle + + // Check for subscription response (has "result.status") - initial subscription is critical + if (json.contains("result") && json["result"].contains("status") && json["result"]["status"].is_object()) { + update_status_cache(json["result"]["status"]); + updated = true; + is_critical = true; // Initial subscription response - dispatch immediately + } + + // Check for status update notifications + if (json.contains("method") && json["method"].is_string()) { + const std::string method = json["method"].get(); + if (method == "notify_status_update" && json.contains("params") && json["params"].is_array() && !json["params"].empty() && + json["params"][0].is_object()) { + update_status_cache(json["params"][0]); + updated = true; + // Note: is_critical stays false for regular status updates (telemetry) + } else if (method == "notify_klippy_ready") { + nlohmann::json updates; + updates["print_stats"]["state"] = "standby"; + update_status_cache(updates); + updated = true; + is_critical = true; // Klippy events are critical + } else if (method == "notify_klippy_shutdown") { + nlohmann::json updates; + updates["print_stats"]["state"] = "error"; + update_status_cache(updates); + updated = true; + is_critical = true; // Klippy events are critical + } + // Handle Klippy disconnect - update status and trigger reconnection + else if (method == "notify_klippy_disconnected") { + // Klippy disconnected - update status to reflect disconnect state + nlohmann::json updates; + updates["print_stats"]["state"] = "error"; + update_status_cache(updates); + updated = true; + is_critical = true; // Klippy events are critical + // Set flag to trigger reconnection after dispatching the status update + ws_reconnect_requested.store(true); + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Klippy disconnected, triggering reconnection"; + } + } + + // Check for print state changes (critical - always dispatch immediately) + if (updated && !is_critical) { + std::string current_state; + { + std::lock_guard lock(payload_mutex); + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state") && + status_cache["print_stats"]["state"].is_string()) { + current_state = status_cache["print_stats"]["state"].get(); + } + } + + if (!current_state.empty() && current_state != last_print_state) { + is_critical = true; + last_print_state = current_state; + } + } + + if (updated) { + const auto now_ms = static_cast( + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + const auto last_dispatch_ms = ws_last_dispatch_ms.load(); + + // Dispatch if: critical change OR throttle interval elapsed + const bool should_dispatch = is_critical || last_dispatch_ms == 0 || now_ms - last_dispatch_ms >= STATUS_UPDATE_INTERVAL_MS; + + if (should_dispatch) { + nlohmann::json message; + { + std::lock_guard lock(payload_mutex); + message = build_print_payload_locked(); + } + + dispatch_message(dev_id, message.dump()); + ws_last_dispatch_ms.store(now_ms); + ws_last_emit_ms.store(now_ms); // Also update heartbeat timer + } + // else: skip dispatch, cache is updated for next dispatch cycle + } +} + +void MoonrakerPrinterAgent::update_status_cache(const nlohmann::json& updates) +{ + if (!updates.is_object()) { + return; + } + + std::lock_guard lock(payload_mutex); + if (!status_cache.is_object()) { + status_cache = nlohmann::json::object(); + } + + for (const auto& item : updates.items()) { + if (item.value().is_object()) { + nlohmann::json& target = status_cache[item.key()]; + if (!target.is_object()) { + target = nlohmann::json::object(); + } + for (const auto& field : item.value().items()) { + target[field.key()] = field.value(); + } + } else { + status_cache[item.key()] = item.value(); + } + } +} + +nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const +{ + nlohmann::json payload; + payload["print"]["command"] = "push_status"; + payload["print"]["msg"] = 0; + payload["print"]["support_mqtt_alive"] = true; + + std::string state = "IDLE"; + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state") && + status_cache["print_stats"]["state"].is_string()) { + state = map_moonraker_state(status_cache["print_stats"]["state"].get()); + } + payload["print"]["gcode_state"] = state; + + // Map Moonraker state to Bambu stage numbers + int mc_print_stage = 0; + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("state")) { + std::string mr_state = status_cache["print_stats"]["state"].get(); + if (mr_state == "printing") + mc_print_stage = 1; + else if (mr_state == "paused") + mc_print_stage = 2; + else if (mr_state == "complete") + mc_print_stage = 3; + else if (mr_state == "error") + mc_print_stage = 4; + } + payload["print"]["mc_print_stage"] = mc_print_stage; + + // Leave mc_print_error_code and print_error at 0 + // UI expects numeric HMS codes - setting to 1 shows generic error dialog + // Only set if real mapping from Moonraker error strings to HMS codes is defined + payload["print"]["mc_print_error_code"] = 0; + payload["print"]["print_error"] = 0; + + // Map homed axes to bit field: X=bit0, Y=bit1, Z=bit2 + // WARNING: This only sets bits 0-2, clearing support flags (bit 3+) + // Bit 3 = 220V voltage, bit 4 = auto recovery, etc. + // This is acceptable for Moonraker (no AMS, different feature set) + int home_flag = 0; + if (status_cache.contains("toolhead") && status_cache["toolhead"].contains("homed_axes")) { + std::string homed = status_cache["toolhead"]["homed_axes"].get(); + if (homed.find('X') != std::string::npos) + home_flag |= 1; // bit 0 + if (homed.find('Y') != std::string::npos) + home_flag |= 2; // bit 1 + if (homed.find('Z') != std::string::npos) + home_flag |= 4; // bit 2 + } + payload["print"]["home_flag"] = home_flag; + + // Moonraker doesn't provide temperature ranges via API - use hardcoded defaults + payload["print"]["nozzle_temp_range"] = {100, 370}; // Typical Klipper range + payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range + + payload["print"]["support_send_to_sd"] = true; + // Detect bed_leveling support from available objects (bed_mesh or probe) + // Default to 0 (not supported) if neither object exists + bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || available_objects.count("probe") != 0); + payload["print"]["support_bed_leveling"] = has_bed_leveling ? 1 : 0; + + const nlohmann::json* extruder = nullptr; + if (status_cache.contains("extruder") && status_cache["extruder"].is_object()) { + extruder = &status_cache["extruder"]; + } else { + for (const auto& item : status_cache.items()) { + if (item.value().is_object() && item.key().rfind("extruder", 0) == 0) { + extruder = &item.value(); + break; + } + } + } + + if (extruder) { + if (extruder->contains("temperature") && (*extruder)["temperature"].is_number()) { + payload["print"]["nozzle_temper"] = (*extruder)["temperature"].get(); + } + if (extruder->contains("target") && (*extruder)["target"].is_number()) { + payload["print"]["nozzle_target_temper"] = (*extruder)["target"].get(); + } + } + + if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) { + const auto& bed = status_cache["heater_bed"]; + if (bed.contains("temperature") && bed["temperature"].is_number()) { + payload["print"]["bed_temper"] = bed["temperature"].get(); + } + if (bed.contains("target") && bed["target"].is_number()) { + payload["print"]["bed_target_temper"] = bed["target"].get(); + } + } + + // Handle fan speed - only if Moonraker provides "fan" object (standard API) + if (status_cache.contains("fan") && status_cache["fan"].is_object() && !status_cache["fan"].empty()) { + const auto& fan = status_cache["fan"]; + if (fan.contains("speed") && fan["speed"].is_number()) { + double speed = fan["speed"].get(); + int pwm = 0; + if (speed <= 1.0) { + pwm = static_cast(speed * 255.0 + 0.5); + } else { + pwm = static_cast(speed + 0.5); + } + pwm = std::clamp(pwm, 0, 255); + payload["print"]["fan_gear"] = pwm; + } else if (fan.contains("power") && fan["power"].is_number()) { + double power = fan["power"].get(); + int pwm = static_cast(power * 255.0 + 0.5); + pwm = std::clamp(pwm, 0, 255); + payload["print"]["fan_gear"] = pwm; + } + } + // If "fan" object doesn't exist, don't include fan_gear in payload + + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename") && + status_cache["print_stats"]["filename"].is_string()) { + payload["print"]["subtask_name"] = status_cache["print_stats"]["filename"].get(); + } + + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("filename")) { + payload["print"]["gcode_file"] = status_cache["print_stats"]["filename"]; + } + + int mc_percent = -1; + if (status_cache.contains("virtual_sdcard") && status_cache["virtual_sdcard"].contains("progress") && + status_cache["virtual_sdcard"]["progress"].is_number()) { + const double progress = status_cache["virtual_sdcard"]["progress"].get(); + if (progress >= 0.0) { + mc_percent = std::clamp(static_cast(progress * 100.0 + 0.5), 0, 100); + } + } + if (mc_percent >= 0) { + payload["print"]["mc_percent"] = mc_percent; + } + + if (status_cache.contains("print_stats") && status_cache["print_stats"].contains("total_duration") && + status_cache["print_stats"].contains("print_duration") && status_cache["print_stats"]["total_duration"].is_number() && + status_cache["print_stats"]["print_duration"].is_number()) { + const double total = status_cache["print_stats"]["total_duration"].get(); + const double elapsed = status_cache["print_stats"]["print_duration"].get(); + if (total > 0.0 && elapsed >= 0.0) { + const auto remaining_minutes = std::max(0, static_cast((total - elapsed) / 60.0)); + payload["print"]["mc_remaining_time"] = remaining_minutes; + } + } + + const auto now_ms = static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); + payload["t_utc"] = now_ms; + + return payload; +} + +void MoonrakerPrinterAgent::dispatch_message(const std::string& dev_id, const std::string& payload) +{ + OnMessageFn local_fn; + OnMessageFn cloud_fn; + QueueOnMainFn queue_fn; + { + std::lock_guard lock(state_mutex); + local_fn = on_local_message_fn; + cloud_fn = on_message_fn; + queue_fn = queue_on_main_fn; + } + + if (!local_fn && !cloud_fn) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: dispatch_message - no message callback registered!"; + return; + } + + auto dispatch = [dev_id, payload, local_fn, cloud_fn]() { + if (local_fn) { + local_fn(dev_id, payload); + return; + } + if (cloud_fn) { + cloud_fn(dev_id, payload); + } + }; + + if (queue_fn) { + queue_fn(dispatch); + } else { + dispatch(); + } +} + +bool MoonrakerPrinterAgent::upload_gcode(const std::string& local_path, + const std::string& filename, + const std::string& base_url, + const std::string& api_key, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn) +{ + namespace fs = boost::filesystem; + + // Validate file exists + fs::path source_path(local_path); + if (!fs::exists(source_path)) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: File does not exist: " << local_path; + return false; + } + + // Check file size + std::uintmax_t file_size = fs::file_size(source_path); + if (file_size > 1024 * 1024 * 1024) { // 1GB limit + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: File too large: " << file_size << " bytes"; + return false; + } + + // Sanitize filename to prevent path traversal attacks + std::string safe_filename = sanitize_filename(filename); + + bool result = true; + std::string http_error; + + // Use Http::form_add and Http::form_add_file + auto http = Http::post(join_url(base_url, "/server/files/upload")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.form_add("root", "gcodes") // Upload to gcodes directory + .form_add("print", "false") // Don't auto-start print + .form_add_file("file", source_path.string(), safe_filename) + .timeout_connect(5) + .timeout_max(300) // 5 minutes for large files + .on_complete([&](std::string body, unsigned status) { + (void) body; + (void) status; + }) + .on_error([&](std::string body, std::string err, unsigned status) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Upload error: " << err << " HTTP " << status; + http_error = err; + result = false; + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + // Check for cancellation via WasCancelledFn + if (cancel_fn && cancel_fn()) { + cancel = true; + result = false; + return; + } + // Report progress via OnUpdateStatusFn + if (update_fn && progress.ultotal > 0) { + int percent = static_cast((progress.ulnow * 100) / progress.ultotal); + update_fn(PrintingStageUpload, percent, "Uploading..."); + } + }) + .perform_sync(); + + if (!result) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Upload failed: " << http_error; + return false; + } + + return true; +} + +int MoonrakerPrinterAgent::pause_print(const std::string& dev_id) +{ + return send_gcode(dev_id, "PAUSE") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +int MoonrakerPrinterAgent::resume_print(const std::string& dev_id) +{ + return send_gcode(dev_id, "RESUME") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +int MoonrakerPrinterAgent::cancel_print(const std::string& dev_id) +{ + return send_gcode(dev_id, "CANCEL_PRINT") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; +} + +bool MoonrakerPrinterAgent::send_jsonrpc_command(const std::string& base_url, + const std::string& api_key, + const nlohmann::json& request, + std::string& response) const +{ + std::string request_str = request.dump(); + std::string url = join_url(base_url, "/printer/print/start"); + + bool success = false; + std::string http_error; + + auto http = Http::post(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.header("Content-Type", "application/json") + .set_post_body(request_str) + .timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response = body; + success = true; + } else { + http_error = "HTTP " + std::to_string(status); + } + }) + .on_error([&](std::string body, std::string err, unsigned status) { http_error = err; }) + .perform_sync(); + + if (!success) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: JSON-RPC command failed: " << http_error; + } + + return success; +} + +void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, const std::string& base_url, const std::string& api_key, uint64_t generation) +{ + auto is_stale = [&]() { return generation != connect_generation.load(); }; + + int result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + std::string error_msg; + + // Early exit if a newer connection was started before we begin + if (is_stale()) { + return; + } + + try { + MoonrakerDeviceInfo fetched_info; + if (!fetch_device_info(base_url, api_key, fetched_info, error_msg)) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Failed to fetch server info: " << error_msg; + // Orca todo: revist here, for now don't send error, this is set current MachineObject to null + // dispatch_local_connect(ConnectStatusFailed, dev_id, "server_info_failed"); + return; + } + + // Commit fetched info back to device_info under lock, only if still current + { + std::lock_guard lock(connect_mutex); + if (is_stale()) { + return; + } + device_info.dev_name = fetched_info.dev_name; + device_info.version = fetched_info.version; + device_info.klippy_state = fetched_info.klippy_state; + } + +// Orca todo: disable websocket for now, as we don't use MonitorPanel for Moonraker printers yet +#if 0 + // Query initial status + nlohmann::json initial_status; + if (query_printer_status(base_url, api_key, initial_status, error_msg)) { + { + update_status_cache(initial_status); + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Initial status queried successfully"; + } else { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Initial status query failed: " << error_msg; + } + + // Start WebSocket status stream + start_status_stream(dev_id, base_url, api_key); +#endif + + // Success! + result = BAMBU_NETWORK_SUCCESS; + + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: Connection exception: " << e.what(); + error_msg = std::string("exception: ") + e.what(); + result = BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + } + + // Only dispatch if this connection is still the current one + if (result == BAMBU_NETWORK_SUCCESS && !is_stale()) { + dispatch_local_connect(ConnectStatusOk, dev_id, "0"); + dispatch_printer_connected(dev_id); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: connect_printer completed - dev_id=" << dev_id; + } else if (result != BAMBU_NETWORK_SUCCESS && result != BAMBU_NETWORK_ERR_CANCELED) { + // Orca todo: revist here, for now don't send error, this is set current MachineObject to null + // dispatch_local_connect(ConnectStatusFailed, dev_id, error_msg); + } +} + +bool MoonrakerPrinterAgent::is_numeric(const std::string& value) +{ + return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); +} + +std::string MoonrakerPrinterAgent::normalize_base_url(std::string host, const std::string& port) +{ + boost::trim(host); + if (host.empty()) { + return ""; + } + + std::string value = host; + if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { + value += ":" + port; + } + + if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { + value = "http://" + value; + } + + if (value.size() > 1 && value.back() == '/') { + value.pop_back(); + } + + return value; +} + +std::string MoonrakerPrinterAgent::join_url(const std::string& base_url, const std::string& path) const +{ + if (base_url.empty()) { + return ""; + } + if (path.empty()) { + return base_url; + } + if (base_url.back() == '/' && path.front() == '/') { + return base_url.substr(0, base_url.size() - 1) + path; + } + if (base_url.back() != '/' && path.front() != '/') { + return base_url + "/" + path; + } + return base_url + path; +} + +// Sanitize filename to prevent path traversal attacks +// Extracts only the basename, removing any path components +std::string MoonrakerPrinterAgent::sanitize_filename(const std::string& filename) +{ + if (filename.empty()) { + return "print.gcode"; + } + namespace fs = boost::filesystem; + fs::path p(filename); + std::string basename = p.filename().string(); + if (basename.empty() || basename == "." || basename == "..") { + return "print.gcode"; + } + return basename; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp new file mode 100644 index 0000000000..2eeb1099df --- /dev/null +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -0,0 +1,216 @@ +#ifndef __MOONRAKER_PRINTER_AGENT_HPP__ +#define __MOONRAKER_PRINTER_AGENT_HPP__ + +#include "IPrinterAgent.hpp" +#include "ICloudServiceAgent.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Slic3r { + +class MoonrakerPrinterAgent : public IPrinterAgent +{ +public: + explicit MoonrakerPrinterAgent(std::string log_dir); + ~MoonrakerPrinterAgent() override; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + // Cloud Agent Dependency + void set_cloud_agent(std::shared_ptr cloud) override; + + // Communication + int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; + int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; + int disconnect_printer() override; + int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; + + // Certificates + int check_cert() override; + void install_device_cert(std::string dev_id, bool lan_only) override; + + // Discovery + bool start_discovery(bool start, bool sending) override; + + // Binding + int ping_bind(std::string ping_code) override; + int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; + int bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; + int unbind(std::string dev_id) override; + int request_bind_ticket(std::string* ticket) override; + int set_server_callback(OnServerErrFn fn) override; + + // Machine Selection + std::string get_user_selected_machine() override; + int set_user_selected_machine(std::string dev_id) override; + + // Print Job Operations + int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; + int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; + int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; + + // Callbacks + int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override; + int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override; + int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override; + int set_on_message_fn(OnMessageFn fn) override; + int set_on_user_message_fn(OnMessageFn fn) override; + int set_on_local_connect_fn(OnLocalConnectedFn fn) override; + int set_on_local_message_fn(OnMessageFn fn) override; + int set_queue_on_main_fn(QueueOnMainFn fn) override; + + // Pull-mode agent (on-demand filament sync) + FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } + bool fetch_filament_info(std::string dev_id) override; + +protected: + struct MoonrakerDeviceInfo + { + std::string dev_id; + std::string dev_ip; + std::string api_key; + std::string base_url; + std::string model_id; + std::string model_name; + std::string dev_name; + std::string version; + std::string klippy_state; + bool use_ssl = false; + } device_info; + + // Tray data for AMS payload building + struct AmsTrayData { + int slot_index = 0; // 0-based slot index + bool has_filament = false; + std::string tray_type; // Material type (e.g., "PLA", "ASA") + std::string tray_sub_brands; // Human-readable filament name + std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA) + std::string tray_info_idx; // Setting ID (optional) + std::string filament_vendor; // Vendor hint from bridge (optional, KX-Bridge sendet das) + int bed_temp = 0; // Optional + int nozzle_temp = 0; // Optional + }; + + // Build ams JSON and call parser + void build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays, int active_lane_index = -1); + + // Methods that derived classes may need to override or access + virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); + virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; + + // State access for derived classes + mutable std::recursive_mutex state_mutex; + + // Helpers + bool is_numeric(const std::string& value); + std::string normalize_base_url(std::string host, const std::string& port); + std::string sanitize_filename(const std::string& filename); + std::string join_url(const std::string& base_url, const std::string& path) const; + + // Trim whitespace and convert to uppercase + static std::string trim_and_upper(const std::string& input); + + // Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility + static std::string map_filament_type_to_generic_id(const std::string& filament_type); + +private: + int handle_request(const std::string& dev_id, const std::string& json_str); + int send_version_info(const std::string& dev_id); + int send_access_code(const std::string& dev_id); + + bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; + bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; + bool send_gcode(const std::string& dev_id, const std::string& gcode) const; + + void announce_printhost_device(); + void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); + void dispatch_printer_connected(const std::string& dev_id); + void dispatch_message(const std::string& dev_id, const std::string& payload); + void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); + void stop_status_stream(); + void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); + void handle_ws_message(const std::string& dev_id, const std::string& payload); + void update_status_cache(const nlohmann::json& updates); + nlohmann::json build_print_payload_locked() const; + + // Print control helpers + int pause_print(const std::string& dev_id); + int resume_print(const std::string& dev_id); + int cancel_print(const std::string& dev_id); + + // File upload + bool upload_gcode(const std::string& local_path, const std::string& filename, + const std::string& base_url, const std::string& api_key, + OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); + + // JSON-RPC helper + bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key, + const nlohmann::json& request, std::string& response) const; + + // Connection thread management + void perform_connection_async(const std::string& dev_id, + const std::string& base_url, + const std::string& api_key, + uint64_t generation); + + // System-specific filament fetch methods + bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index, int& active_lane_index); + bool fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index); + + // JSON helper methods + static std::string safe_json_string(const nlohmann::json& obj, const char* key); + static int safe_json_int(const nlohmann::json& obj, const char* key); + static std::string safe_array_string(const nlohmann::json& arr, int idx); + static int safe_array_int(const nlohmann::json& arr, int idx); + static std::string normalize_color_value(const std::string& color); + + std::string ssdp_announced_host; + std::string ssdp_announced_id; + std::shared_ptr m_cloud_agent; + std::string selected_machine; + + OnMsgArrivedFn on_ssdp_msg_fn; + OnPrinterConnectedFn on_printer_connected_fn; + GetSubscribeFailureFn on_subscribe_failure_fn; + OnMessageFn on_message_fn; + OnMessageFn on_user_message_fn; + OnLocalConnectedFn on_local_connect_fn; + OnMessageFn on_local_message_fn; + QueueOnMainFn queue_on_main_fn; + OnServerErrFn on_server_err_fn; + + mutable std::recursive_mutex payload_mutex; + nlohmann::json status_cache; + + std::atomic next_jsonrpc_id{1}; + std::set available_objects; // Track for feature detection + + std::atomic ws_stop{false}; + std::atomic ws_reconnect_requested{false}; // Flag to trigger reconnection + std::atomic ws_last_emit_ms{0}; + std::thread ws_thread; + + // Throttling configuration for WebSocket updates + // Critical changes (state transitions) dispatch immediately; telemetry is throttled + static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry + std::atomic ws_last_dispatch_ms{0}; + std::string last_print_state; // Track state for immediate dispatch on change + + // Connection thread management + std::atomic connect_generation{0}; + std::thread connect_thread; + std::recursive_mutex connect_mutex; +}; + +} // namespace Slic3r + +#endif