diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp new file mode 100644 index 000000000..9c9633921 --- /dev/null +++ b/src/libslic3r/AppConfig.cpp @@ -0,0 +1,1283 @@ +#include "libslic3r/libslic3r.h" +#include "libslic3r/Utils.hpp" +#include "AppConfig.hpp" +//BBS +#include "Preset.hpp" +#include "Exception.hpp" +#include "LocalesUtils.hpp" +#include "Thread.hpp" +#include "format.hpp" +#include "nlohmann/json.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +//FIXME replace the two following includes with after it becomes mainstream. +#include +#include +#endif + +#define USE_JSON_CONFIG + +using namespace nlohmann; + +namespace Slic3r { + +static const std::string VERSION_CHECK_URL = ""; +static const std::string MODELS_STR = "models"; + +const std::string AppConfig::SECTION_FILAMENTS = "filaments"; +const std::string AppConfig::SECTION_MATERIALS = "sla_materials"; + +std::string AppConfig::get_language_code() +{ + std::string get_lang = get("language"); + if (get_lang.empty()) return ""; + + if (get_lang == "zh_CN") + { + get_lang = "zh-cn"; + } + else + { + if (get_lang.length() >= 2) { get_lang = get_lang.substr(0, 2); } + } + + return get_lang; +} + +std::string AppConfig::get_hms_host() +{ + std::string sel = get("iot_environment"); + std::string host = ""; +#if !BBL_RELEASE_TO_PUBLIC + if (sel == ENV_DEV_HOST) + host = "e-dev.bambulab.net"; + else if (sel == ENV_QAT_HOST) + host = "e-qa.bambulab.net"; + else if (sel == ENV_PRE_HOST) + host = "e-pre.bambulab.net"; + else if (sel == ENV_PRODUCT_HOST) + host = "e.bambulab.com"; + return host; +#else + return "e.bambulab.com"; +#endif +} + +void AppConfig::reset() +{ + m_storage.clear(); + set_defaults(); +}; + +// Override missing or keys with their defaults. +void AppConfig::set_defaults() +{ + if (m_mode == EAppMode::Editor) { +#ifdef SUPPORT_AUTO_CENTER + // Reset the empty fields to defaults. + if (get("autocenter").empty()) + set_bool("autocenter", true); +#endif + +#ifdef SUPPORT_BACKGROUND_PROCESSING + // Disable background processing by default as it is not stable. + if (get("background_processing").empty()) + set_bool("background_processing", false); +#endif + +#ifdef SUPPORT_SHOW_DROP_PROJECT + if (get("show_drop_project_dialog").empty()) + set_bool("show_drop_project_dialog", true); +#endif + + if (get("drop_project_action").empty()) + set_bool("drop_project_action", true); + +#ifdef _WIN32 + if (get("associate_3mf").empty()) + set_bool("associate_3mf", false); + if (get("associate_stl").empty()) + set_bool("associate_stl", false); + if (get("associate_step").empty()) + set_bool("associate_step", false); + +#endif // _WIN32 + + // remove old 'use_legacy_opengl' parameter from this config, if present + if (!get("use_legacy_opengl").empty()) + erase("app", "use_legacy_opengl"); + +#ifdef __APPLE__ + if (get("use_retina_opengl").empty()) + set_bool("use_retina_opengl", true); +#endif + + if (get("single_instance").empty()) + set_bool("single_instance", false); + +#ifdef SUPPORT_REMEMBER_OUTPUT_PATH + if (get("remember_output_path").empty()) + set_bool("remember_output_path", true); + + if (get("remember_output_path_removable").empty()) + set_bool("remember_output_path_removable", true); +#endif + if (get("toolkit_size").empty()) + set("toolkit_size", "100"); + +#if ENABLE_ENVIRONMENT_MAP + if (get("use_environment_map").empty()) + set("use_environment_map", false); +#endif // ENABLE_ENVIRONMENT_MAP + + if (get("use_inches").empty()) + set("use_inches", "0"); + } + else { +#ifdef _WIN32 + if (get("associate_gcode").empty()) + set_bool("associate_gcode", false); +#endif // _WIN32 + } + + if (get("use_perspective_camera").empty()) + set_bool("use_perspective_camera", true); + +#ifdef SUPPORT_FREE_CAMERA + if (get("use_free_camera").empty()) + set_bool("use_free_camera", false); +#endif + +#ifdef SUPPORT_REVERSE_MOUSE_ZOOM + if (get("reverse_mouse_wheel_zoom").empty()) + set_bool("reverse_mouse_wheel_zoom", false); +#endif + + if (get("zoom_to_mouse").empty()) + set_bool("zoom_to_mouse", false); + if (get("user_bed_type").empty()) + set_bool("user_bed_type", true); +//#ifdef SUPPORT_SHOW_HINTS + if (get("show_hints").empty()) + set_bool("show_hints", true); +//#endif + if (get("enable_multi_machine").empty()) + set_bool("enable_multi_machine", false); + + +#ifdef _WIN32 + +//#ifdef SUPPORT_3D_CONNEXION + if (get("use_legacy_3DConnexion").empty()) + set_bool("use_legacy_3DConnexion", true); +//#endif + +#ifdef SUPPORT_DARK_MODE + if (get("dark_color_mode").empty()) + set("dark_color_mode", "0"); +#endif + +//#ifdef SUPPORT_SYS_MENU + if (get("sys_menu_enabled").empty()) + set("sys_menu_enabled", "1"); +//#endif +#endif // _WIN32 + + // BBS + /*if (get("3mf_include_gcode").empty()) + set_bool("3mf_include_gcode", true);*/ + + if (get("developer_mode").empty()) + set_bool("developer_mode", false); + + if (get("enable_ssl_for_mqtt").empty()) + set_bool("enable_ssl_for_mqtt", true); + + if (get("enable_ssl_for_ftp").empty()) + set_bool("enable_ssl_for_ftp", true); + + if (get("severity_level").empty()) + set("severity_level", "info"); + + if (get("internal_developer_mode").empty()) + set_bool("internal_developer_mode", false); + + // BBS + if (get("preset_folder").empty()) + set("preset_folder", ""); + + // BBS + if (get("slicer_uuid").empty()) { + boost::uuids::uuid uuid = boost::uuids::random_generator()(); + set("slicer_uuid", to_string(uuid)); + } + + if (get("show_model_mesh").empty()) { + set_bool("show_model_mesh", false); + } + + if (get("show_model_shadow").empty()) { + set_bool("show_model_shadow", true); + } + + if (get("show_build_edges").empty()) { + set_bool("show_build_edgets", false); + } + + if (get("show_daily_tips").empty()) { + set_bool("show_daily_tips", true); + } + //true is auto calculate + if (get("auto_calculate").empty()) { + set_bool("auto_calculate", true); + } + + if (get("auto_calculate_when_filament_change").empty()){ + set_bool("auto_calculate_when_filament_change", true); + } + + if (get("show_home_page").empty()) { + set_bool("show_home_page", true); + } + + if (get("show_printable_box").empty()) { + set_bool("show_printable_box", true); + } + + if (get("units").empty()) { + set("units", "0"); + } + + if (get("sync_user_preset").empty()) { + set_bool("sync_user_preset", false); + } + + if (get("keyboard_supported").empty()) { + set("keyboard_supported", std::string("none/alt/control/shift")); + } + + if (get("mouse_supported").empty()) { + set("mouse_supported", "mouse left/mouse middle/mouse right"); + } + + if (get("privacy_version").empty()) { + set("privacy_version", "00.00.00.00"); + } + + if (get("rotate_view").empty()) { + set("rotate_view", "none/mouse left"); + } + + if (get("move_view").empty()) { + set("move_view", "none/mouse left"); + } + + if (get("zoom_view").empty()) { + set("zoom_view", "none/mouse left"); + } + + if (get("precise_control").empty()) { + set("precise_control", "none/mouse left"); + } + + if (get("download_path").empty()) { + set("download_path", ""); + } + + if (get("mouse_wheel").empty()) { + set("mouse_wheel", "0"); + } + + if (get("max_recent_count").empty()) { + set("max_recent_count", "18"); + } + + if (get("staff_pick_switch").empty()) { + set_bool("staff_pick_switch", true); + } + + if (get("sync_system_preset").empty()) { + set_bool("sync_system_preset", true); + } + + if (get("backup_switch").empty() || get("version") < "01.06.00.00") { + set_bool("backup_switch", true); + } + + if (get("liveview", "auto_stop_liveview").empty()) { + set("liveview", "auto_stop_liveview", true); + } + + if (get("backup_interval").empty()) { + set("backup_interval", "10"); + } + + if (get("curr_bed_type").empty()) { + set("curr_bed_type", "1"); + } + + if (get("sending_interval").empty()) { + set("sending_interval", "5"); + } + + if (get("max_send").empty()) { + set("max_send", "3"); + } + +#if BBL_RELEASE_TO_PUBLIC + if (get("iot_environment").empty()) { + set("iot_environment", "3"); + } +#else + if (get("iot_environment").empty()) { + set("iot_environment", "2"); + } +#endif + + if (get("print", "bed_leveling").empty()) { + set_str("print", "bed_leveling", "1"); + } + if (get("print", "flow_cali").empty()) { + set_str("print", "flow_cali", "1"); + } + if (get("print", "timelapse").empty()) { + set_str("print", "timelapse", "1"); + } + + // Remove legacy window positions/sizes + erase("app", "main_frame_maximized"); + erase("app", "main_frame_pos"); + erase("app", "main_frame_size"); + erase("app", "object_settings_maximized"); + erase("app", "object_settings_pos"); + erase("app", "object_settings_size"); +} + +#ifdef WIN32 +static std::string appconfig_md5_hash_line(const std::string_view data) +{ + //FIXME replace the two following includes with after it becomes mainstream. + // return boost::md5(data).hex_str_value(); + // boost::uuids::detail::md5 is an internal namespace thus it may change in the future. + // Also this implementation is not the fastest, it was designed for short blocks of text. + using boost::uuids::detail::md5; + md5 md5_hash; + // unsigned int[4], 128 bits + md5::digest_type md5_digest{}; + std::string md5_digest_str; + md5_hash.process_bytes(data.data(), data.size()); + md5_hash.get_digest(md5_digest); + boost::algorithm::hex(md5_digest, md5_digest + std::size(md5_digest), std::back_inserter(md5_digest_str)); + // MD5 hash is 32 HEX digits long. + assert(md5_digest_str.size() == 32); + // This line will be emited at the end of the file. + return "# MD5 checksum " + md5_digest_str + "\n"; +} + +// Assume that the last line with the comment inside the config file contains a checksum and that the user didn't modify the config file. +static bool verify_config_file_checksum(boost::nowide::ifstream &ifs) +{ + auto read_whole_config_file = [&ifs]() -> std::string { + std::stringstream ss; + ss << ifs.rdbuf(); + return ss.str(); + }; + + ifs.seekg(0, boost::nowide::ifstream::beg); + std::string whole_config = read_whole_config_file(); + + // The checksum should be on the last line in the config file. + if (size_t last_comment_pos = whole_config.find_last_of('#'); last_comment_pos != std::string::npos) { + // Split read config into two parts, one with checksum, and the second part is part with configuration from the checksum was computed. + // Verify existence and validity of the MD5 checksum line at the end of the file. + // When the checksum isn't found, the checksum was not saved correctly, it was removed or it is an older config file without the checksum. + // If the checksum is incorrect, then the file was either not saved correctly or modified. + if (std::string_view(whole_config.c_str() + last_comment_pos, whole_config.size() - last_comment_pos) == appconfig_md5_hash_line({ whole_config.data(), last_comment_pos })) + return true; + } + return false; +} +#endif + + + +#ifdef USE_JSON_CONFIG +std::string AppConfig::load() +{ + json j; + + // 1) Read the complete config file into a boost::property_tree. + namespace pt = boost::property_tree; + pt::ptree tree; + boost::nowide::ifstream ifs; + bool recovered = false; + std::string error_message; + + try { + ifs.open(AppConfig::loading_path()); + +#ifdef WIN32 + std::stringstream input_stream; + input_stream << ifs.rdbuf(); + std::string total_string = input_stream.str(); + size_t last_pos = total_string.find_last_of('}'); + std::string left_string = total_string.substr(0, last_pos+1); + //skip the "\n" + std::string right_string = total_string.substr(last_pos+2); + + std::string md5_str = appconfig_md5_hash_line({left_string.data()}); + // Verify the checksum of the config file without taking just for debugging purpose. + if (md5_str != right_string) + BOOST_LOG_TRIVIAL(info) << "The configuration file " << AppConfig::loading_path() << + " has a wrong MD5 checksum or the checksum is missing. This may indicate a file corruption or a harmless user edit."; + j = json::parse(left_string); +#else + ifs >> j; +#endif + } + catch(nlohmann::detail::parse_error &err) { +#ifdef WIN32 + // The configuration file is corrupted, try replacing it with the backup configuration. + ifs.close(); + std::string backup_path = (boost::format("%1%.bak") % AppConfig::loading_path()).str(); + if (boost::filesystem::exists(backup_path)) { + // Compute checksum of the configuration backup file and try to load configuration from it when the checksum is correct. + boost::nowide::ifstream backup_ifs(backup_path); + std::stringstream back_input_stream; + back_input_stream << backup_ifs.rdbuf(); + std::string back_total_string = back_input_stream.str(); + size_t back_last_pos = back_total_string.find_last_of('}'); + std::string back_left_string = back_total_string.substr(0, back_last_pos+1); + std::string back_right_string = back_total_string.substr(back_last_pos+2); + + std::string back_md5_str = appconfig_md5_hash_line({back_left_string.data()}); + // Verify the checksum of the config file without taking just for debugging purpose. + if (back_md5_str != back_right_string) { + BOOST_LOG_TRIVIAL(error) << format("Both \"%1%\" and \"%2%\" are corrupted. It isn't possible to restore configuration from the backup.", AppConfig::loading_path(), backup_path); + backup_ifs.close(); + boost::filesystem::remove(backup_path); + } + else if (std::string error_message; copy_file(backup_path, AppConfig::loading_path(), error_message, false) != SUCCESS) { + BOOST_LOG_TRIVIAL(error) << format("Configuration file \"%1%\" is corrupted. Failed to restore from backup \"%2%\": %3%", AppConfig::loading_path(), backup_path, error_message); + backup_ifs.close(); + boost::filesystem::remove(backup_path); + } + else { + BOOST_LOG_TRIVIAL(info) << format("Configuration file \"%1%\" was corrupted. It has been succesfully restored from the backup \"%2%\".", AppConfig::loading_path(), backup_path); + // Try parse configuration file after restore from backup. + j = json::parse(back_left_string); + recovered = true; + } + } + else +#endif // WIN32 + BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\": %2%", AppConfig::loading_path(), err.what()); + + if (!recovered) + return err.what(); + } + + try { + for (auto it = j.begin(); it != j.end(); it++) { + if (it.key() == MODELS_STR) { + for (auto& j_model : it.value()) { + // This is a vendor section listing enabled model / variants + const auto vendor_name = j_model["vendor"].get(); + auto& vendor = m_vendors[vendor_name]; + const auto model_name = j_model["model"].get(); + std::vector variants; + if (!unescape_strings_cstyle(j_model["nozzle_diameter"], variants)) { continue; } + for (const auto& variant : variants) { + vendor[model_name].insert(variant); + } + } + } else if (it.key() == SECTION_FILAMENTS) { + json j_filaments = it.value(); + for (auto& element : j_filaments) { + m_storage[it.key()][element] = "true"; + } + } else if (it.key() == "presets") { + for (auto iter = it.value().begin(); iter != it.value().end(); iter++) { + if (iter.key() == "filaments") { + int idx = 0; + for(auto& element: iter.value()) { + if (idx == 0) + m_storage[it.key()]["filament"] = element; + else { + auto n = std::to_string(idx); + if (n.length() == 1) n = "0" + n; + m_storage[it.key()]["filament_" + n] = element; + } + idx++; + } + } else { + m_storage[it.key()][iter.key()] = iter.value().get(); + } + } + } else if (it.key() == "calis") { + for (auto &calis_j : it.value()) { + PrinterCaliInfo cali_info; + if (calis_j.contains("dev_id")) + cali_info.dev_id = calis_j["dev_id"].get(); + if (calis_j.contains("cali_finished")) + cali_info.cali_finished = bool(calis_j["cali_finished"].get()); + if (calis_j.contains("flow_ratio")) + cali_info.cache_flow_ratio = calis_j["flow_ratio"].get(); + if (calis_j.contains("cache_flow_rate_calibration_type")) + cali_info.cache_flow_rate_calibration_type = static_cast(calis_j["cache_flow_rate_calibration_type"].get()); + if (calis_j.contains("presets")) { + cali_info.selected_presets.clear(); + for (auto cali_it = calis_j["presets"].begin(); cali_it != calis_j["presets"].end(); cali_it++) { + CaliPresetInfo preset_info; + preset_info.tray_id = cali_it.value()["tray_id"].get(); + preset_info.nozzle_diameter = cali_it.value()["nozzle_diameter"].get(); + preset_info.filament_id = cali_it.value()["filament_id"].get(); + preset_info.setting_id = cali_it.value()["setting_id"].get(); + preset_info.name = cali_it.value()["name"].get(); + cali_info.selected_presets.push_back(preset_info); + } + } + m_printer_cali_infos.emplace_back(cali_info); + } + } else { + if (it.value().is_object()) { + for (auto iter = it.value().begin(); iter != it.value().end(); iter++) { + if (iter.value().is_boolean()) { + if (iter.value()) { + m_storage[it.key()][iter.key()] = "true"; + } else { + m_storage[it.key()][iter.key()] = "false"; + } + } else if (iter.key() == "filament_presets") { + m_filament_presets = iter.value().get>(); + } else if (iter.key() == "filament_colors") { + m_filament_colors = iter.value().get>(); + } + else { + if (iter.value().is_string()) + m_storage[it.key()][iter.key()] = iter.value().get(); + else { + BOOST_LOG_TRIVIAL(trace) << "load config warning..."; + } + } + } + } + } + } + } catch(std::exception err) { + BOOST_LOG_TRIVIAL(info) << format("parse app config \"%1%\", error: %2%", AppConfig::loading_path(), err.what()); + + return err.what(); + } + + // Figure out if datadir has legacy presets + auto ini_ver = Semver::parse(get("version")); + m_legacy_datadir = false; + if (ini_ver) { + m_orig_version = *ini_ver; + ini_ver->set_metadata(boost::none); + ini_ver->set_prerelease(boost::none); + } + + // Legacy conversion + if (m_mode == EAppMode::Editor) { + // Convert [extras] "physical_printer" to [presets] "physical_printer", + // remove the [extras] section if it becomes empty. + if (auto it_section = m_storage.find("extras"); it_section != m_storage.end()) { + if (auto it_physical_printer = it_section->second.find("physical_printer"); it_physical_printer != it_section->second.end()) { + m_storage["presets"]["physical_printer"] = it_physical_printer->second; + it_section->second.erase(it_physical_printer); + } + if (it_section->second.empty()) + m_storage.erase(it_section); + } + } + + // Override missing or keys with their defaults. + this->set_defaults(); + m_dirty = false; + return ""; +} + +void AppConfig::save() +{ + if (! is_main_thread_active()) + throw CriticalException("Calling AppConfig::save() from a worker thread!"); + + // The config is first written to a file with a PID suffix and then moved + // to avoid race conditions with multiple instances of Slic3r + const auto path = config_path(); + std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str(); + + json j; + + std::stringstream config_ss; + if (m_mode == EAppMode::Editor) + j["header"] = Slic3r::header_slic3r_generated(); + else + j["header"] = Slic3r::header_gcodeviewer_generated(); + + // Make sure the "no" category is written first. + for (const auto& kvp : m_storage["app"]) { + if (kvp.second == "true") { + j["app"][kvp.first] = true; + continue; + } + if (kvp.second == "false") { + j["app"][kvp.first] = false; + continue; + } + j["app"][kvp.first] = kvp.second; + } + + for (const auto &filament_preset : m_filament_presets) { + j["app"]["filament_presets"].push_back(filament_preset); + } + + for (const auto &filament_color : m_filament_colors) { + j["app"]["filament_colors"].push_back(filament_color); + } + + for (const auto &cali_info : m_printer_cali_infos) { + json cali_json; + cali_json["dev_id"] = cali_info.dev_id; + cali_json["flow_ratio"] = cali_info.cache_flow_ratio; + cali_json["cali_finished"] = cali_info.cali_finished ? 1 : 0; + cali_json["cache_flow_rate_calibration_type"] = static_cast(cali_info.cache_flow_rate_calibration_type); + for (auto filament_preset : cali_info.selected_presets) { + json preset_json; + preset_json["tray_id"] = filament_preset.tray_id; + preset_json["nozzle_diameter"] = filament_preset.nozzle_diameter; + preset_json["filament_id"] = filament_preset.filament_id; + preset_json["setting_id"] = filament_preset.setting_id; + preset_json["name"] = filament_preset.name; + cali_json["presets"].push_back(preset_json); + } + j["calis"].push_back(cali_json); + } + + // Write the other categories. + for (const auto& category : m_storage) { + if (category.first.empty()) + continue; + if (category.first == SECTION_FILAMENTS) { + json j_filaments; + for (const auto& kvp: category.second) { + j_filaments.push_back(kvp.first); + } + j[category.first] = j_filaments; + continue; + } else if (category.first == "presets") { + json j_filament_array; + for(const auto& kvp : category.second) { + if (boost::starts_with(kvp.first, "filament") && kvp.first != "filament_colors") { + j_filament_array.push_back(kvp.second); + } else { + j[category.first][kvp.first] = kvp.second; + } + } + j["presets"]["filaments"] = j_filament_array; + continue; + } + for (const auto& kvp : category.second) { + if (kvp.second == "true") { + j[category.first][kvp.first] = true; + continue; + } + if (kvp.second == "false") { + j[category.first][kvp.first] = false; + continue; + } + j[category.first][kvp.first] = kvp.second; + } + } + + // Write vendor sections + for (const auto& vendor : m_vendors) { + size_t size_sum = 0; + for (const auto& model : vendor.second) { size_sum += model.second.size(); } + if (size_sum == 0) { continue; } + + for (const auto& model : vendor.second) { + if (model.second.empty()) { continue; } + const std::vector variants(model.second.begin(), model.second.end()); + const auto escaped = escape_strings_cstyle(variants); + //j[VENDOR_PREFIX + vendor.first][MODEL_PREFIX + model.first] = escaped; + json j_model; + j_model["vendor"] = vendor.first; + j_model["model"] = model.first; + j_model["nozzle_diameter"] = escaped; + j[MODELS_STR].push_back(j_model); + } + } + + boost::nowide::ofstream c; + c.open(path_pid, std::ios::out | std::ios::trunc); + c << std::setw(4) << j << std::endl; + +#ifdef WIN32 + // WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API + // provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition, + // we save the config file into a backup first before moving it to the final destination. + c << appconfig_md5_hash_line({j.dump(4)}); +#endif + + c.close(); + +#ifdef WIN32 + // Make a backup of the configuration file before copying it to the final destination. + std::string error_message; + std::string backup_path = (boost::format("%1%.bak") % path).str(); + // Copy configuration file with PID suffix into the configuration file with "bak" suffix. + if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS) + BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration."; +#endif + + // Rename the config atomically. + // On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile. + // To cope with that, we already made a backup of the config on Windows. + rename_file(path_pid, path); + m_dirty = false; +} + +#else + +std::string AppConfig::load() +{ + // 1) Read the complete config file into a boost::property_tree. + namespace pt = boost::property_tree; + pt::ptree tree; + boost::nowide::ifstream ifs; + bool recovered = false; + + try { + ifs.open(AppConfig::loading_path()); +#ifdef WIN32 + // Verify the checksum of the config file without taking just for debugging purpose. + if (!verify_config_file_checksum(ifs)) + BOOST_LOG_TRIVIAL(info) << "The configuration file " << AppConfig::loading_path() << + " has a wrong MD5 checksum or the checksum is missing. This may indicate a file corruption or a harmless user edit."; + + ifs.seekg(0, boost::nowide::ifstream::beg); +#endif + pt::read_ini(ifs, tree); + } + catch (pt::ptree_error& ex) { +#ifdef WIN32 + // The configuration file is corrupted, try replacing it with the backup configuration. + ifs.close(); + std::string backup_path = (boost::format("%1%.bak") % AppConfig::loading_path()).str(); + if (boost::filesystem::exists(backup_path)) { + // Compute checksum of the configuration backup file and try to load configuration from it when the checksum is correct. + boost::nowide::ifstream backup_ifs(backup_path); + if (!verify_config_file_checksum(backup_ifs)) { + BOOST_LOG_TRIVIAL(error) << format("Both \"%1%\" and \"%2%\" are corrupted. It isn't possible to restore configuration from the backup.", AppConfig::loading_path(), backup_path); + backup_ifs.close(); + boost::filesystem::remove(backup_path); + } + else if (std::string error_message; copy_file(backup_path, AppConfig::loading_path(), error_message, false) != SUCCESS) { + BOOST_LOG_TRIVIAL(error) << format("Configuration file \"%1%\" is corrupted. Failed to restore from backup \"%2%\": %3%", AppConfig::loading_path(), backup_path, error_message); + backup_ifs.close(); + boost::filesystem::remove(backup_path); + } + else { + BOOST_LOG_TRIVIAL(info) << format("Configuration file \"%1%\" was corrupted. It has been succesfully restored from the backup \"%2%\".", AppConfig::loading_path(), backup_path); + // Try parse configuration file after restore from backup. + try { + ifs.open(AppConfig::loading_path()); + pt::read_ini(ifs, tree); + recovered = true; + } + catch (pt::ptree_error& ex) { + BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\" after it has been restored from backup: %2%", AppConfig::loading_path(), ex.what()); + } + } + } + else +#endif // WIN32 + BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\": %2%", AppConfig::loading_path(), ex.what()); + if (!recovered) { + // Report the initial error of parsing PrusaSlicer.ini. + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + // ! But to avoid the use of _utf8 (related to use of wxWidgets) + // we will rethrow this exception from the place of load() call, if returned value wouldn't be empty + /* + throw Slic3r::RuntimeError( + _utf8(L("Error parsing Prusa config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.")) + + "\n\n" + AppConfig::config_path() + "\n\n" + ex.what()); + */ + return ex.what(); + } + } + + // 2) Parse the property_tree, extract the sections and key / value pairs. + for (const auto& section : tree) { + if (section.second.empty()) { + // This may be a top level (no section) entry, or an empty section. + std::string data = section.second.data(); + if (!data.empty()) + // If there is a non-empty data, then it must be a top-level (without a section) config entry. + m_storage[""][section.first] = data; + } + else if (boost::starts_with(section.first, VENDOR_PREFIX)) { + // This is a vendor section listing enabled model / variants + const auto vendor_name = section.first.substr(VENDOR_PREFIX.size()); + auto& vendor = m_vendors[vendor_name]; + for (const auto& kvp : section.second) { + if (!boost::starts_with(kvp.first, MODEL_PREFIX)) { continue; } + const auto model_name = kvp.first.substr(MODEL_PREFIX.size()); + std::vector variants; + if (!unescape_strings_cstyle(kvp.second.data(), variants)) { continue; } + for (const auto& variant : variants) { + vendor[model_name].insert(variant); + } + } + } + else { + // This must be a section name. Read the entries of a section. + std::map& storage = m_storage[section.first]; + for (auto& kvp : section.second) + storage[kvp.first] = kvp.second.data(); + } + } + + // Figure out if datadir has legacy presets + auto ini_ver = Semver::parse(get("version")); + m_legacy_datadir = false; + if (ini_ver) { + m_orig_version = *ini_ver; + // Make 1.40.0 alphas compare well + ini_ver->set_metadata(boost::none); + ini_ver->set_prerelease(boost::none); + //m_legacy_datadir = ini_ver < Semver(1, 40, 0); + } + + // Legacy conversion + if (m_mode == EAppMode::Editor) { + // Convert [extras] "physical_printer" to [presets] "physical_printer", + // remove the [extras] section if it becomes empty. + if (auto it_section = m_storage.find("extras"); it_section != m_storage.end()) { + if (auto it_physical_printer = it_section->second.find("physical_printer"); it_physical_printer != it_section->second.end()) { + m_storage["presets"]["physical_printer"] = it_physical_printer->second; + it_section->second.erase(it_physical_printer); + } + if (it_section->second.empty()) + m_storage.erase(it_section); + } + } + + // Override missing or keys with their defaults. + this->set_defaults(); + m_dirty = false; + return ""; +} + +void AppConfig::save() +{ + if (! is_main_thread_active()) + throw CriticalException("Calling AppConfig::save() from a worker thread!"); + + // The config is first written to a file with a PID suffix and then moved + // to avoid race conditions with multiple instances of Slic3r + const auto path = config_path(); + std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str(); + + std::stringstream config_ss; + if (m_mode == EAppMode::Editor) + config_ss << "# " << Slic3r::header_slic3r_generated() << std::endl; + else + config_ss << "# " << Slic3r::header_gcodeviewer_generated() << std::endl; + // Make sure the "no" category is written first. + for (const auto& kvp : m_storage[""]) + config_ss << kvp.first << " = " << kvp.second << std::endl; + // Write the other categories. + for (const auto& category : m_storage) { + if (category.first.empty()) + continue; + config_ss << std::endl << "[" << category.first << "]" << std::endl; + for (const auto& kvp : category.second) + config_ss << kvp.first << " = " << kvp.second << std::endl; + } + // Write vendor sections + for (const auto &vendor : m_vendors) { + size_t size_sum = 0; + for (const auto &model : vendor.second) { size_sum += model.second.size(); } + if (size_sum == 0) { continue; } + + config_ss << std::endl << "[" << VENDOR_PREFIX << vendor.first << "]" << std::endl; + + for (const auto &model : vendor.second) { + if (model.second.empty()) { continue; } + const std::vector variants(model.second.begin(), model.second.end()); + const auto escaped = escape_strings_cstyle(variants); + config_ss << MODEL_PREFIX << model.first << " = " << escaped << std::endl; + } + } + // One empty line before the MD5 sum. + config_ss << std::endl; + + std::string config_str = config_ss.str(); + boost::nowide::ofstream c; + c.open(path_pid, std::ios::out | std::ios::trunc); + c << config_str; +#ifdef WIN32 + // WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API + // provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition, + // we save the config file into a backup first before moving it to the final destination. + c << appconfig_md5_hash_line(config_str); +#endif + c.close(); + +#ifdef WIN32 + // Make a backup of the configuration file before copying it to the final destination. + std::string error_message; + std::string backup_path = (boost::format("%1%.bak") % path).str(); + // Copy configuration file with PID suffix into the configuration file with "bak" suffix. + if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS) + BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration."; +#endif + + // Rename the config atomically. + // On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile. + // To cope with that, we already made a backup of the config on Windows. + rename_file(path_pid, path); + m_dirty = false; +} +#endif + +bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const +{ + const auto it_v = m_vendors.find(vendor); + if (it_v == m_vendors.end()) { return false; } + const auto it_m = it_v->second.find(model); + return it_m == it_v->second.end() ? false : it_m->second.find(variant) != it_m->second.end(); +} + +void AppConfig::set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable) +{ + if (enable) { + if (get_variant(vendor, model, variant)) { return; } + m_vendors[vendor][model].insert(variant); + } else { + auto it_v = m_vendors.find(vendor); + if (it_v == m_vendors.end()) { return; } + auto it_m = it_v->second.find(model); + if (it_m == it_v->second.end()) { return; } + auto it_var = it_m->second.find(variant); + if (it_var == it_m->second.end()) { return; } + it_m->second.erase(it_var); + } + // If we got here, there was an update + m_dirty = true; +} + +void AppConfig::set_vendors(const AppConfig &from) +{ + m_vendors = from.m_vendors; + m_dirty = true; +} + +void AppConfig::save_printer_cali_infos(const PrinterCaliInfo &cali_info, bool need_change_status) +{ + auto iter = std::find_if(m_printer_cali_infos.begin(), m_printer_cali_infos.end(), [&cali_info](const PrinterCaliInfo &cali_info_item) { + return cali_info_item.dev_id == cali_info.dev_id; + }); + + if (iter == m_printer_cali_infos.end()) { + m_printer_cali_infos.emplace_back(cali_info); + } else { + if (need_change_status) { + (*iter).cali_finished = cali_info.cali_finished; + } + (*iter).cache_flow_ratio = cali_info.cache_flow_ratio; + (*iter).selected_presets = cali_info.selected_presets; + (*iter).cache_flow_rate_calibration_type = cali_info.cache_flow_rate_calibration_type; + } + m_dirty = true; +} + +std::string AppConfig::get_last_dir() const +{ + const auto it = m_storage.find("recent"); + if (it != m_storage.end()) { + { + const auto it2 = it->second.find("last_opened_folder"); + if (it2 != it->second.end() && ! it2->second.empty()) + return it2->second; + } + { + const auto it2 = it->second.find("settings_folder"); + if (it2 != it->second.end() && ! it2->second.empty()) + return it2->second; + } + } + return std::string(); +} + +std::vector AppConfig::get_recent_projects() const +{ + std::vector ret; + const auto it = m_storage.find("recent_projects"); + if (it != m_storage.end()) + { + for (const std::map::value_type& item : it->second) + { + ret.push_back(item.second); + } + } + return ret; +} + +void AppConfig::set_recent_projects(const std::vector& recent_projects) +{ + auto it = m_storage.find("recent_projects"); + if (it == m_storage.end()) + it = m_storage.insert(std::map>::value_type("recent_projects", std::map())).first; + + it->second.clear(); + for (unsigned int i = 0; i < (unsigned int)recent_projects.size(); ++i) + { + auto n = std::to_string(i + 1); + if (n.length() == 1) n = "0" + n; + it->second[n] = recent_projects[i]; + } +} + +void AppConfig::set_mouse_device(const std::string& name, double translation_speed, double translation_deadzone, + float rotation_speed, float rotation_deadzone, double zoom_speed, bool swap_yz) +{ + std::string key = std::string("mouse_device:") + name; + auto it = m_storage.find(key); + if (it == m_storage.end()) + it = m_storage.insert(std::map>::value_type(key, std::map())).first; + + it->second.clear(); + it->second["translation_speed"] = float_to_string_decimal_point(translation_speed); + it->second["translation_deadzone"] = float_to_string_decimal_point(translation_deadzone); + it->second["rotation_speed"] = float_to_string_decimal_point(rotation_speed); + it->second["rotation_deadzone"] = float_to_string_decimal_point(rotation_deadzone); + it->second["zoom_speed"] = float_to_string_decimal_point(zoom_speed); + it->second["swap_yz"] = swap_yz ? "1" : "0"; +} + +std::vector AppConfig::get_mouse_device_names() const +{ + static constexpr const char *prefix = "mouse_device:"; + static const size_t prefix_len = strlen(prefix); + std::vector out; + for (const auto& key_value_pair : m_storage) + if (boost::starts_with(key_value_pair.first, prefix) && key_value_pair.first.size() > prefix_len) + out.emplace_back(key_value_pair.first.substr(prefix_len)); + return out; +} + +void AppConfig::update_config_dir(const std::string &dir) +{ + this->set("recent", "settings_folder", dir); +} + +void AppConfig::update_skein_dir(const std::string &dir) +{ + if (is_shapes_dir(dir)) + return; // do not save "shapes gallery" directory + this->set("recent", "last_opened_folder", dir); +} +/* +std::string AppConfig::get_last_output_dir(const std::string &alt) const +{ + + const auto it = m_storage.find(""); + if (it != m_storage.end()) { + const auto it2 = it->second.find("last_export_path"); + const auto it3 = it->second.find("remember_output_path"); + if (it2 != it->second.end() && it3 != it->second.end() && ! it2->second.empty() && it3->second == "1") + return it2->second; + } + return alt; +} + +void AppConfig::update_last_output_dir(const std::string &dir) +{ + this->set("", "last_export_path", dir); +} +*/ +std::string AppConfig::get_last_output_dir(const std::string& alt, const bool removable) const +{ + std::string s1 = ("last_export_path"); + const auto it = m_storage.find("app"); + if (it != m_storage.end()) { + const auto it2 = it->second.find(s1); + if (it2 != it->second.end() && !it2->second.empty()) + return it2->second; + } + return is_shapes_dir(alt) ? get_last_dir() : alt; +} + +void AppConfig::update_last_output_dir(const std::string& dir, const bool removable) +{ + this->set("app", ("last_export_path"), dir); +} + +// BBS: backup +std::string AppConfig::get_last_backup_dir() const +{ + const auto it = m_storage.find("app"); + if (it != m_storage.end()) { + const auto it2 = it->second.find("last_backup_path"); + if (it2 != it->second.end()) + return it2->second; + } + return ""; +} + +// BBS: backup +void AppConfig::update_last_backup_dir(const std::string& dir) +{ + this->set("app", "last_backup_path", dir); + this->save(); +} + +std::string AppConfig::get_region() +{ +#if BBL_RELEASE_TO_PUBLIC + return this->get("region"); +#else + std::string sel = get("iot_environment"); + std::string region; + if (sel == ENV_DEV_HOST) + region = "ENV_CN_DEV"; + else if (sel == ENV_QAT_HOST) + region = "ENV_CN_QA"; + else if (sel == ENV_PRE_HOST) + region = "ENV_CN_PRE"; + if (region.empty()) + return this->get("region"); + return region; +#endif +} + +std::string AppConfig::get_country_code() +{ + std::string region = get_region(); +#if !BBL_RELEASE_TO_PUBLIC + if (is_engineering_region()) { return region; } +#endif + if (region == "CHN" || region == "China") + return "CN"; + else if (region == "USA") + return "US"; + else if (region == "Asia-Pacific") + return "Others"; + else if (region == "Europe") + return "US"; + else if (region == "North America") + return "US"; + else + return "Others"; + return ""; + +} + +bool AppConfig::is_engineering_region(){ + std::string sel = get("iot_environment"); + std::string region; + if (sel == ENV_DEV_HOST + || sel == ENV_QAT_HOST + ||sel == ENV_PRE_HOST) + return true; + return false; +} + +void AppConfig::save_custom_color_to_config(const std::vector &colors) +{ + auto set_colors = [](std::map &data, const std::vector &colors) { + for (size_t i = 0; i < colors.size(); i++) { + data[std::to_string(10 + i)] = colors[i]; // for map sort:10 begin + } + }; + if (colors.size() > 0) { + if (!has_section("custom_color_list")) { + std::map data; + set_colors(data, colors); + set_section("custom_color_list", data); + } else { + auto data = get_section("custom_color_list"); + auto data_modify = const_cast *>(&data); + set_colors(*data_modify, colors); + set_section("custom_color_list", *data_modify); + } + } +} + +std::vector AppConfig::get_custom_color_from_config() +{ + std::vector colors; + if (has_section("custom_color_list")) { + auto data = get_section("custom_color_list"); + for (auto iter : data) { + colors.push_back(iter.second); + } + } + return colors; +} + +void AppConfig::reset_selections() +{ + auto it = m_storage.find("presets"); + if (it != m_storage.end()) { + it->second.erase(PRESET_PRINT_NAME); + it->second.erase(PRESET_FILAMENT_NAME); + it->second.erase("sla_print"); + it->second.erase("sla_material"); + it->second.erase(PRESET_PRINTER_NAME); + it->second.erase(PRESET_CONFIG_NAME); + it->second.erase("physical_printer"); + m_dirty = true; + } +} + +std::string AppConfig::config_path() +{ +#ifdef USE_JSON_CONFIG + std::string path = (m_mode == EAppMode::Editor) ? + (boost::filesystem::path(Slic3r::data_dir()) / (SLIC3R_APP_KEY ".conf")).make_preferred().string() : + (boost::filesystem::path(Slic3r::data_dir()) / (GCODEVIEWER_APP_KEY ".conf")).make_preferred().string(); +#else + std::string path = (m_mode == EAppMode::Editor) ? + (boost::filesystem::path(Slic3r::data_dir()) / (SLIC3R_APP_KEY ".ini")).make_preferred().string() : + (boost::filesystem::path(Slic3r::data_dir()) / (GCODEVIEWER_APP_KEY ".ini")).make_preferred().string(); +#endif + + return path; +} + +std::string AppConfig::version_check_url() const +{ + auto from_settings = get("version_check_url"); + return from_settings.empty() ? VERSION_CHECK_URL : from_settings; +} + +bool AppConfig::exists() +{ + return boost::filesystem::exists(config_path()); +} + +}; // namespace Slic3r diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp new file mode 100644 index 000000000..a5835fdec --- /dev/null +++ b/src/libslic3r/Preset.cpp @@ -0,0 +1,3598 @@ +#include + +#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 "libslic3r.h" +#include "Utils.hpp" +#include "Time.hpp" +#include "PlaceholderParser.hpp" + +using boost::property_tree::ptree; + +namespace Slic3r { + +//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" }, +}}; + +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) +{ + // BBS + auto* filament_diameter = dynamic_cast(config.option("filament_diameter")); + if (filament_diameter != nullptr) + // Loaded the FFF Printer settings. Verify, that all extruder dependent values have enough values. + config.set_num_filaments((unsigned int)filament_diameter->values.size()); + + 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. + // BBS + size_t n = (filament_diameter == nullptr) ? 1 : filament_diameter->values.size(); + const auto &defaults = FullPrintConfig::defaults(); + for (const std::string &key : Preset::filament_options()) { + if (key == "compatible_prints" || key == "compatible_printers") + 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_CONFIG: + return PRESET_CONFIG_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 if (type_str.compare(PRESET_CONFIG_NAME) == 0 || type_str.compare(PRESET_IOT_CONFIG_TYPE) == 0) + return Preset::Type::TYPE_CONFIG; + 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; + } + + //TODO: workaround for current info file convert, will remove it later + if (this->updated_time == 0) { + this->updated_time = (long long)Slic3r::Utils::get_current_time_utc(); + //this->sync_info = "update"; + BOOST_LOG_TRIVIAL(info) << boost::format("old info file, updated time to %1%") % this->updated_time; + save_info(); + } +} + +void Preset::save_info(std::string file) +{ + //BBS: add project embedded preset logic + if (this->is_project_embedded) + 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() +{ + //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)) + 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; + //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_system) + from_str = std::string("System"); + else + from_str = std::string("Default"); + + boost::filesystem::create_directories(fs::path(this->file).parent_path()); + + //BBS: only save difference if it has parent + if (parent_config) { + DynamicPrintConfig temp_config; + std::vector dirty_options = config.diff(*parent_config); + + for (auto option: dirty_options) + { + ConfigOption *opt_src = config.option(option); + ConfigOption *opt_dst = temp_config.option(option, true); + opt_dst->set(opt_src); + } + temp_config.save_to_json(this->file, this->name, from_str, this->version.to_string(), this->custom_defined); + } 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, this->name, from_str, this->version.to_string(), this->custom_defined); + } else { + this->config.save_to_json(this->file, this->name, from_str, this->version.to_string(), this->custom_defined); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id; + + 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) +{ + 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) +{ + 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_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". + printf("Preset::is_compatible_with_printer - parsing error of compatible_printers_condition %s:\n%s\n", active_printer.preset.name.c_str(), 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() + //BBS + || (!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(debug) << __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 ""; +} + +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; +} + +bool Preset::is_custom_defined() +{ + if (custom_defined == "1") + return true; + return false; +} + +// The method previously only supports to be called on preset_bundle->printers.get_edited_preset() +// I extened to support call on all presets +bool Preset::is_bbl_vendor_preset(PresetBundle *preset_bundle) +{ + bool is_bbl_vendor_preset = false; + if (preset_bundle) { + auto config = &this->config; + if (type != TYPE_PRINTER) { + auto printers = config->opt("compatible_printers"); + if (printers && !printers->values.empty()) { + auto printer = preset_bundle->printers.find_preset(printers->values.front()); + if (printer) + config = &printer->config; + } + } + auto printer_model_opt = config->opt("printer_model"); + if (printer_model_opt) { + std::string vendor_name; + for (auto vendor_profile : preset_bundle->vendors) { + for (auto vendor_model : vendor_profile.second.models) + if (vendor_model.name == printer_model_opt->value) + { + vendor_name = vendor_profile.first; + break; + } + } + if (!vendor_name.empty()) + is_bbl_vendor_preset = (vendor_name.compare("BBL") == 0); + } + } + return is_bbl_vendor_preset; +} + +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"); + int bed_type_value = atoi(str_bed_type.c_str()); + return BedType(bed_type_value); + } 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", "slice_closing_radius", "spiral_mode", "spiral_mode_smooth", "spiral_mode_max_xy_smoothing", "slicing_mode", + "top_shell_layers", "top_shell_thickness", "bottom_shell_layers", "bottom_shell_thickness", "ensure_vertical_shell_thickness", "reduce_crossing_wall", "detect_thin_wall", + "detect_overhang_wall", + "smooth_speed_discontinuity_area","smooth_coefficient", + "seam_position", "wall_sequence", "is_infill_first", "sparse_infill_density", "sparse_infill_pattern", "sparse_infill_anchor", "sparse_infill_anchor_max", + "top_surface_pattern", "bottom_surface_pattern", "internal_solid_infill_pattern", "infill_direction", "bridge_angle", + "minimum_sparse_infill_area", "reduce_infill_retraction", "ironing_pattern", "ironing_type", + "ironing_flow", "ironing_speed", "ironing_spacing","ironing_direction", + "max_travel_detour_distance", + "fuzzy_skin", "fuzzy_skin_thickness", "fuzzy_skin_point_distance", +#ifdef HAS_PRESSURE_EQUALIZER + "max_volumetric_extrusion_rate_slope_positive", "max_volumetric_extrusion_rate_slope_negative", +#endif /* HAS_PRESSURE_EQUALIZER */ + "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", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed", "outer_wall_acceleration", + "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "inner_wall_acceleration", "sparse_infill_acceleration", + "accel_to_decel_enable", "accel_to_decel_factor", "skirt_loops", "skirt_distance", + "skirt_height", "draft_shield", + "brim_width", "brim_object_gap", "brim_type", "enable_support", "support_type", "support_threshold_angle", "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 + "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", "support_remove_small_overhang", + "bridge_no_support", "thick_bridges", "max_bridge_length", "print_sequence", + "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", "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", + "top_surface_line_width", "support_line_width", "infill_wall_overlap", "bridge_flow", + "elefant_foot_compensation", "xy_contour_compensation", "xy_hole_compensation", "resolution", "enable_prime_tower", + "prime_tower_width", "prime_tower_brim_width", "prime_volume", + "wipe_tower_no_sparse_layers", "compatible_printers", "compatible_printers_condition", "inherits", + "flush_into_infill", "flush_into_objects", "flush_into_support","process_notes", + // BBS + "tree_support_branch_angle", "tree_support_wall_count", "tree_support_branch_distance", + "tree_support_branch_diameter", + "detect_narrow_internal_solid_infill", + "gcode_add_line_number", "enable_arc_fitting", "precise_z_height", "infill_combination", /*"adaptive_layer_height",*/ + "support_bottom_interface_spacing", "enable_overhang_speed", "overhang_1_4_speed", "overhang_2_4_speed", "overhang_3_4_speed", "overhang_4_4_speed", "overhang_totally_speed", + "initial_layer_infill_speed", "top_one_wall_type", "top_area_threshold", "only_one_wall_first_layer", + "timelapse_type", "internal_bridge_support_thickness", + "wall_generator", "wall_transition_length", "wall_transition_filter_deviation", "wall_transition_angle", + "wall_distribution_count", "min_feature_size", "min_bead_width", "post_process", + "seam_gap", "wipe_speed", "top_solid_infill_flow_ratio", "initial_layer_flow_ratio", + "default_jerk", "outer_wall_jerk", "inner_wall_jerk", "infill_jerk", "top_surface_jerk", "initial_layer_jerk", "travel_jerk", + "filter_out_gap_fill", "mmu_segmented_region_max_width", "mmu_segmented_region_interlocking_depth", + "small_perimeter_speed", "small_perimeter_threshold", + // calib + "print_flow_ratio", + //Orca + "exclude_object", "seam_slope_type", "seam_slope_conditional", "scarf_angle_threshold", "seam_slope_start_height", "seam_slope_entire_loop", "seam_slope_min_length", + "seam_slope_steps", "seam_slope_inner_walls"}; + +static std::vector s_Preset_filament_options { + /*"filament_colour", */ "default_filament_colour","required_nozzle_HRC","filament_diameter", "filament_type", "filament_soluble", "filament_is_support", + "filament_max_volumetric_speed", + "filament_flow_ratio", "filament_density", "filament_cost", "filament_minimal_purge_on_wipe_tower", + "nozzle_temperature", "nozzle_temperature_initial_layer", + // BBS + "cool_plate_temp", "eng_plate_temp", "hot_plate_temp", "textured_plate_temp", "cool_plate_temp_initial_layer", "eng_plate_temp_initial_layer", "hot_plate_temp_initial_layer","textured_plate_temp_initial_layer", + // "bed_type", + //BBS:temperature_vitrification + "temperature_vitrification", "reduce_fan_stop_start_freq", "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", "full_fan_speed_layer", "fan_cooling_layer_time", "slow_down_layer_time", "slow_down_min_speed", + "filament_start_gcode", "filament_end_gcode", + //exhaust fan control + "activate_air_filtration","during_print_exhaust_fan_speed","complete_print_exhaust_fan_speed", + // Retract overrides + "filament_retraction_length", "filament_z_hop", "filament_z_hop_types", "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", + //OrcaSlicer + "enable_pressure_advance", "pressure_advance", "chamber_temperatures","filament_notes", + "filament_long_retractions_when_cut","filament_retraction_distances_when_cut" +}; + +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", +}; + +static std::vector s_Preset_printer_options { + "printer_technology", + "printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", + "single_extruder_multi_material", "machine_start_gcode", "machine_end_gcode","printing_by_object_gcode","before_layer_change_gcode", "layer_change_gcode", "time_lapse_gcode", "change_filament_gcode", + "printer_model", "printer_variant", "printable_height", "extruder_clearance_radius", "extruder_clearance_max_radius","extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", + "nozzle_height", + "default_print_profile", "inherits", + "silent_mode", + // BBS + "scan_first_layer", "machine_load_filament_time", "machine_unload_filament_time", "machine_pause_gcode", "template_custom_gcode", + "nozzle_type","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types","support_chamber_temp_control","support_air_filtration","printer_structure","thumbnail_size", + "best_object_pos","head_wrap_detect_zone","printer_notes", + "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", + //OrcaSlicer + "host_type", "print_host", "printhost_apikey", + "print_host_webui", + "printhost_cafile","printhost_port","printhost_authorization_type", + "printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", + "use_relative_e_distances", "extruder_type","use_firmware_retraction" +}; + +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; +} + +// 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) +{ + // 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(); + + // Load custom roots first + if (fs::exists(dir / "base")) { + load_presets(dir.string(), "base", substitutions, substitution_rule); + } + + //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: 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); + if (this->find_preset(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: " << name; + continue; + } + try { + Preset preset(m_type, name, false); + 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; + continue; + } + + std::string version_str = key_values[BBL_JSON_KEY_VERSION]; + boost::optional version = Semver::parse(version_str); + if (!version) continue; + Semver app_version = *(Semver::parse(SLIC3R_VERSION)); + if ( version->maj() > app_version.maj()) { + BOOST_LOG_TRIVIAL(warning) << "Preset incompatibla, not loading: " << name; + 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_IS_CUSTOM) != key_values.end()) + preset.custom_defined = key_values[BBL_JSON_KEY_IS_CUSTOM]; + if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end()) + preset.description = key_values[BBL_JSON_KEY_DESCRIPTION]; + if (key_values.find("instantiation") != key_values.end()) + preset.is_visible = key_values["instantiation"] != "false"; + + //BBS: use 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; + inherit_preset = this->find_preset(inherits_value, false, true); + } else { + ; + } + 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 { + // We support custom root preset now + auto inherits_config2 = dynamic_cast(inherits_config); + if ((inherits_config2 && !inherits_config2->value.empty()) && !preset.is_custom_defined()) { + BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset.file; + continue; + } + // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. + preset.config = default_preset.config; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id; + 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()) + BOOST_LOG_TRIVIAL(error) << "Error in a preset file: The preset \"" << + preset.file << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; + 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) { + 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) { + 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); + } + 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())); + std::sort(m_presets.begin() + m_num_default_presets, m_presets.end()); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __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 = new Preset(); + *new_preset = preset; + + //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) { + 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); + ConfigOption *opt_dst = temp_config.option(option, true); + 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; + 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[BBL_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(); + 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_preset(inherits_value, false, 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. + preset->config = default_preset.config; + BOOST_LOG_TRIVIAL(warning) << boost::format("can not find parent for config %1%!")%preset->file; + //continue; + } + 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()) + 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())); + std::sort(m_presets.begin() + m_num_default_presets, m_presets.end()); + //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); + + 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(); + for (auto it = m_presets.begin(); it != m_presets.end(); it++) { + Preset* preset = &m_presets[it - m_presets.begin()]; + if (preset->name == 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::vector& 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); + + if (preset->is_custom_defined()) { + preset->save(nullptr); + } else { + //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) { + 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) +{ + 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; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, name %1% , total value counts %2%")%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%")%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 %name; + return false; + } + Semver app_version = *(Semver::parse(SLIC3R_VERSION)); + if ( cloud_version->maj() > app_version.maj()) { + BOOST_LOG_TRIVIAL(warning)<< __FUNCTION__ << boost::format("version %1% mismatch with app version %2%, not loading for user preset %3%")%version_str %SLIC3R_VERSION %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%")%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(BBL_JSON_KEY_UPDATE_TIME) != preset_values.end()) { + cloud_update_time = std::atoll(preset_values[BBL_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%")%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(name); + bool need_update = false; + if ((iter != m_presets.end()) && (iter->name == name)) { + BOOST_LOG_TRIVIAL(info) << "Found the Preset locally: " << 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%")%name %cloud_update_time %iter->updated_time; + unlock(); + return false; + } + else { + //update the one from cloud which is newer + need_update = true; + } + } + + // base_id + if (preset_values.find(BBL_JSON_KEY_BASE_ID) == preset_values.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("can not find base_id, not loading for user preset %1%") % name; + unlock(); + return false; + } + std::string cloud_base_id = preset_values[BBL_JSON_KEY_BASE_ID]; + + //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__ << " " << name << " filament_id: " << cloud_filament_id << " base_id: " << cloud_base_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({ name, m_type, PresetConfigSubstitutions::Source::UserCloud, 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; + /*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_preset(inherits_value, false, 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")%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; + } + new_config.apply(std::move(cloud_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()) + BOOST_LOG_TRIVIAL(error) << "Error in a preset file: The preset \"" << + 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)); + } + 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 = cloud_base_id; + iter->filament_id = cloud_filament_id; + //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, name, false); + preset.is_system = false; + preset.loaded = true; + 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 = cloud_base_id; + preset.filament_id = cloud_filament_id; + + 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%")%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; + std::sort(m_presets.begin() + m_num_default_presets, m_presets.end()); + 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) +{ + std::deque::iterator it = this->find_preset_internal(preset_name); + bool found = (it != m_presets.end()) && (it->name == preset_name) && (it->is_system || it->is_default); + if (!found) { + it = this->find_preset_renamed(preset_name); + found = it != m_presets.end() && (it->is_system || it->is_default); + } + if (!found) { + if (!inherit_name.empty()) { + it = this->find_preset_internal(inherit_name); + found = it != m_presets.end() && it->name == inherit_name && (it->is_system || it->is_default); + 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, bool is_custom_defined) +{ + 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, is_custom_defined); +} + +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); + // OrcaSlicer: ignore print connection info from project + cfg.erase("print_host"); + cfg.erase("print_host_webui"); + cfg.erase("printhost_apikey"); + cfg.erase("printhost_cafile"); + cfg.erase("printhost_user"); + cfg.erase("printhost_password"); + cfg.erase("printhost_port"); + + const auto &keys = cfg.keys(); + cfg.apply_only(combined_config, keys, true); + std::string &inherits = Preset::inherits(cfg); + + //BBS: 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(); + } + 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); + for (auto &opt : keys) { + if (different_settings_list.find(opt) != different_settings_list.end()) + continue; + ConfigOption *opt_src = iter->config.option(opt); + ConfigOption *opt_dst = cfg.option(opt); + if (opt_src && opt_dst && (*opt_src != *opt_dst)) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" change key %1% from old_value %2% to inherit's value %3%, preset_name %4%, inherits_name %5%") + %opt %(opt_dst->serialize()) %(opt_src->serialize()) %original_name %inherits; + opt_dst->set(opt_src); + } + } + } + } + else if (found && it->is_system && (different_settings_list.size() > 0)) { + for (auto &opt : keys) { + if (different_settings_list.find(opt) != different_settings_list.end()) + continue; + ConfigOption *opt_src = it->config.option(opt); + ConfigOption *opt_dst = cfg.option(opt); + if (opt_src && opt_dst && (*opt_src != *opt_dst)) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" change key %1% from old_value %2% to new_value %3%, preset_name %4%") + %opt %(opt_dst->serialize()) %(opt_src->serialize()) %original_name; + opt_dst->set(opt_src); + } + } + } + + //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, bool is_custom_defined) +{ + 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; + preset.custom_defined = is_custom_defined ? "1": "0"; + //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.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; + 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.is_default || preset.is_system) { + //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(); + 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; + 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.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.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.is_default) + 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) +{ + auto it = this->find_preset_internal(name); + + Preset& preset = *it; + if (preset.is_default) + return false; + //BBS: add project embedded preset logic and refine is_external + //if (!preset.is_external && !preset.is_system) { + if (! preset.is_system) { + preset.remove_files(); + } + //BBS: add lock logic for sync preset in background + lock(); + m_presets.erase(it); + unlock(); + + return true; +} + +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_preset(child.inherits()); + 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 is not found by its name, null is returned. +Preset* PresetCollection::find_preset(const std::string &name, bool first_visible_if_not_found, bool real) +{ + Preset key(m_type, name, false); + auto it = this->find_preset_internal(name); + // Ensure that a temporary copy is returned if the preset found is currently selected. + return (it != m_presets.end() && it->name == key.name) ? &this->preset(it - m_presets.begin(), real) : + first_visible_if_not_found ? &this->first_visible() : nullptr; +} + +// 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) { + 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) + first_visible = 0; + return first_visible; +} + +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())); + bool some_compatible = false; + 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); + some_compatible |= preset_edited.is_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))) + 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; + 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) +{ + 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]) + 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) +{ + t_config_option_keys diff; + for (const t_config_option_key &opt_key : config_this.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 == "thumbnail_size") { + // 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 { + switch (other_opt->type()) { + case coInts: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; + case coBools: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; + case coFloats: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; + case coStrings: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; + case coPercents:add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; + case coPoints: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; + // BBS + case coEnums: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; + default: diff.emplace_back(opt_key); break; + } + } + } + } + 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(debug) << __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; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __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(debug) << __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); + // 1) Try to find the preset by its name. + auto it = this->find_preset_internal(name); + size_t idx = 0; + if (it != m_presets.end() && it->name == 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(debug) << __FUNCTION__ << boost::format(": %1%, select %2%, success")%Preset::get_type_string(m_type) %name_w_suffix; + return true; + } + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __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(debug) << __FUNCTION__ << boost::format(": %1%, try to select by name %2%")%Preset::get_type_string(m_type) %name; + // 1) Try to find the preset by its name. + auto it = this->find_preset_internal(name); + + size_t idx = (size_t)-1; + if (it != m_presets.end() && it->name == 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(debug) << __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(debug) << __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 = 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_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) + 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) +{ + if (m_type == Preset::Type::TYPE_FILAMENT && preset.config.has(BBL_JSON_KEY_INHERITS) && preset.config.option(BBL_JSON_KEY_INHERITS)->value.empty()) { + std::string alias_name; + std::string preset_name = preset.name; + 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); + boost::trim_right(alias_name); + } + } + if (alias_name.empty() || is_alias_exist(alias_name, &preset)) + preset.alias = ""; + else { + 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) +{ + 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); + if (m_printer_hold_alias.end() == printer_iter) { + m_printer_hold_alias[printer_name].insert(alias); + } else { + auto alias_iter = m_printer_hold_alias[printer_name].find(alias); + if (m_printer_hold_alias[printer_name].end() == alias_iter) { + m_printer_hold_alias[printer_name].insert(alias); + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << printer_name << "already has alias: " << alias << " and the preset name: " << preset.name; + } + } + } +} + +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); + case Preset::TYPE_CONFIG: return L(PRESET_CONFIG_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; + case Preset::TYPE_CONFIG: return PRESET_CONFIG_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(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", + "host_type", + "print_host", + "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(); + 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; + } + return out; + } +} // namespace PresetUtils + +} // namespace Slic3r diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp new file mode 100644 index 000000000..f8f224378 --- /dev/null +++ b/src/libslic3r/Preset.hpp @@ -0,0 +1,1022 @@ +#ifndef slic3r_Preset_hpp_ +#define slic3r_Preset_hpp_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PrintConfig.hpp" +#include "Semver.hpp" +#include "ProjectTask.hpp" + +//BBS: change system directories +#define PRESET_SYSTEM_DIR "system" +#define PRESET_USER_DIR "user" +#define PRESET_FILAMENT_NAME "filament" +#define PRESET_PRINT_NAME "process" +#define PRESET_PRINTER_NAME "machine" +#define PRESET_CONFIG_NAME "config" +#define PRESET_SLA_PRINT_NAME "sla_print" +#define PRESET_SLA_MATERIALS_NAME "sla_materials" +#define PRESET_PROFILES_DIR "profiles" +#define PRESET_PROFILES_TEMOLATE_DIR "profiles_template" +#define PRESET_TEMPLATE_DIR "Template" +#define PRESET_CUSTOM_VENDOR "Custom" + +//BBS: iot preset type strings +#define PRESET_IOT_PRINTER_TYPE "printer" +#define PRESET_IOT_FILAMENT_TYPE "filament" +#define PRESET_IOT_PRINT_TYPE "print" +#define PRESET_IOT_CONFIG_TYPE "config" + + +//BBS: add json support +#define BBL_JSON_KEY_VERSION "version" +#define BBL_JSON_KEY_IS_CUSTOM "is_custom_defined" +#define BBL_JSON_KEY_URL "url" +#define BBL_JSON_KEY_NAME "name" +#define BBL_JSON_KEY_DESCRIPTION "description" +#define BBL_JSON_KEY_FORCE_UPDATE "force_update" +#define BBL_JSON_KEY_MACHINE_MODEL_LIST "machine_model_list" +#define BBL_JSON_KEY_PROCESS_LIST "process_list" +#define BBL_JSON_KEY_SUB_PATH "sub_path" +#define BBL_JSON_KEY_FILAMENT_LIST "filament_list" +#define BBL_JSON_KEY_MACHINE_LIST "machine_list" +#define BBL_JSON_KEY_TYPE "type" +#define BBL_JSON_KEY_FROM "from" +#define BBL_JSON_KEY_SETTING_ID "setting_id" +#define BBL_JSON_KEY_BASE_ID "base_id" +#define BBL_JSON_KEY_USER_ID "user_id" +#define BBL_JSON_KEY_FILAMENT_ID "filament_id" +#define BBL_JSON_KEY_UPDATE_TIME "updated_time" +#define BBL_JSON_KEY_INHERITS "inherits" +#define BBL_JSON_KEY_INSTANTIATION "instantiation" +#define BBL_JSON_KEY_NOZZLE_DIAMETER "nozzle_diameter" +#define BBL_JSON_KEY_PRINTER_TECH "machine_tech" +#define BBL_JSON_KEY_FAMILY "family" +#define BBL_JSON_KEY_BED_MODEL "bed_model" +#define BBL_JSON_KEY_BED_TEXTURE "bed_texture" +#define BBL_JSON_KEY_DEFAULT_BED_TYPE "default_bed_type" +#define BBL_JSON_KEY_HOTEND_MODEL "hotend_model" +#define BBL_JSON_KEY_DEFAULT_MATERIALS "default_materials" +#define BBL_JSON_KEY_MODEL_ID "model_id" + +//BBL: json path + + +namespace Slic3r { + +class AppConfig; +class PresetBundle; + +enum ConfigFileType +{ + CONFIG_FILE_TYPE_UNKNOWN, + CONFIG_FILE_TYPE_APP_CONFIG, + CONFIG_FILE_TYPE_CONFIG, + CONFIG_FILE_TYPE_CONFIG_BUNDLE, +}; + +//BBS: add a function to load the version from xxx.json +extern Semver get_version_from_json(std::string file_path); +//BBS: add a function to load the key-values from xxx.json +extern int get_values_from_json(std::string file_path, std::vector& keys, std::map& key_values); + +extern ConfigFileType guess_config_file_type(const boost::property_tree::ptree &tree); + +class VendorProfile +{ +public: + std::string name; + std::string id; + Semver config_version; + std::string config_update_url; + std::string changelog_url; + + struct PrinterVariant { + PrinterVariant() {} + PrinterVariant(const std::string &name) : name(name) {} + std::string name; + }; + + struct PrinterModel { + PrinterModel() {} + std::string id; + std::string name; + //BBS: this is internal id for the printer. Currently only used for searching in database + std::string model_id; + PrinterTechnology technology; + std::string family; + std::vector variants; + std::vector default_materials; + // Vendor & Printer Model specific print bed model & texture. + std::string bed_model; + std::string bed_texture; + std::string default_bed_type; + std::string hotend_model; + + PrinterVariant* variant(const std::string &name) { + for (auto &v : this->variants) + if (v.name == name) + return &v; + return nullptr; + } + + const PrinterVariant* variant(const std::string &name) const { return const_cast(this)->variant(name); } + }; + std::vector models; + + std::set default_filaments; + std::set default_sla_materials; + + VendorProfile() {} + VendorProfile(std::string id) : id(std::move(id)) {} + + bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); } + + // Load VendorProfile from an ini file. + // If `load_all` is false, only the header with basic info (name, version, URLs) is loaded. + static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true); + static VendorProfile from_ini(const boost::property_tree::ptree &tree, const boost::filesystem::path &path, bool load_all=true); + + size_t num_variants() const { size_t n = 0; for (auto &model : models) n += model.variants.size(); return n; } + std::vector families() const; + + bool operator< (const VendorProfile &rhs) const { return this->id < rhs.id; } + bool operator==(const VendorProfile &rhs) const { return this->id == rhs.id; } +}; + +class Preset; + +// Helper to hold a profile with its vendor definition, where the vendor definition may have been extracted from a parent system preset. +// The parent preset is only accessible through PresetCollection, therefore to allow definition of the various is_compatible_with methods +// outside of the PresetCollection, this composite is returned by PresetCollection::get_preset_with_vendor_profile() when needed. +struct PresetWithVendorProfile { + PresetWithVendorProfile(const Preset &preset, const VendorProfile *vendor) : preset(preset), vendor(vendor) {} + const Preset &preset; + const VendorProfile *vendor; +}; + +// Note: it is imporant that map is used here rather than unordered_map, +// because we need iterators to not be invalidated, +// because Preset and the ConfigWizard hold pointers to VendorProfiles. +// XXX: maybe set is enough (cf. changes in Wizard) +typedef std::map VendorMap; + +class Preset +{ +public: + enum Type + { + TYPE_INVALID, + TYPE_PRINT, + TYPE_SLA_PRINT, + TYPE_FILAMENT, + TYPE_SLA_MATERIAL, + TYPE_PRINTER, + TYPE_COUNT, + // This type is here to support PresetConfigSubstitutions for physical printers, however it does not belong to the Preset class, + // PhysicalPrinter class is used instead. + TYPE_PHYSICAL_PRINTER, + // BBS: plate config + TYPE_PLATE, + // BBS: model config + TYPE_MODEL, + TYPE_CONFIG, + }; + + Type type = TYPE_INVALID; + + // The preset represents a "default" set of properties, + // pulled from the default values of the PrintConfig (see PrintConfigDef for their definitions). + bool is_default = false; + // External preset points to a configuration, which has been loaded but not imported + // into the Slic3r default configuration location. + bool is_external = false; + // System preset is read-only. + bool is_system = false; + // Preset is visible, if it is associated with a printer model / variant that is enabled in the AppConfig + // or if it has no printer model / variant association. + // Also the "default" preset is only visible, if it is the only preset in the list. + bool is_visible = true; + // Has this preset been modified? + bool is_dirty = false; + // Is this preset compatible with the currently active printer? + bool is_compatible = true; + + //BBS: add type for project-embedded + bool is_project_embedded = false; + ConfigSubstitutions *loading_substitutions{nullptr}; + bool is_user() const { return ! this->is_default && ! this->is_system && ! this->is_project_embedded; } + //bool is_user() const { return ! this->is_default && ! this->is_system; } + + // Name of the preset, usually derived form the file name. + std::string name; + // File name of the preset. This could be a Print / Filament / Printer preset, + // or a Configuration file bundling the Print + Filament + Printer presets (in that case is_external and possibly is_system will be true), + // or it could be a G-code (again, is_external will be true). + std::string file; + // If this is a system profile, then there should be a vendor data available to display at the UI. + const VendorProfile *vendor = nullptr; + + // Has this profile been loaded? + bool loaded = false; + + // Configuration data, loaded from a file, or set from the defaults. + DynamicPrintConfig config; + + // Alias of the preset + std::string alias; + // List of profile names, from which this profile was renamed at some point of time. + // This list is then used to match profiles by their names when loaded from .gcode, .3mf, .amf, + // and to match the "inherits" field of user profiles with updated system profiles. + std::vector renamed_from; + + //BBS + Semver version; // version of preset + std::string ini_str; // ini string of preset + std::string setting_id; // setting id in cloud database + std::string filament_id; // setting id in cloud database + std::string user_id; // preset user_id + std::string base_id; // base id of preset + std::string sync_info; // enum: "delete", "create", "update", "" + std::string custom_defined; // enum: "1", "0", "" + std::string description; // + long long updated_time{0}; //last updated time + std::map key_values; + + static std::string get_type_string(Preset::Type type); + // get string type for iot + static std::string get_iot_type_string(Preset::Type type); + static Preset::Type get_type_from_string(std::string type_str); + void load_info(const std::string& file); + void save_info(std::string file = ""); + void remove_files(); + + //BBS: add logic for only difference save + //if parent_config is null, save all keys, otherwise, only save difference + void save(DynamicPrintConfig* parent_config); + void reload(Preset const & parent); + + // Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty. + std::string label(bool no_alias) const; + + // Set the is_dirty flag if the provided config is different from the active one. + void set_dirty(const DynamicPrintConfig &config) { this->is_dirty = ! this->config.diff(config).empty(); } + void set_dirty(bool dirty = true) { this->is_dirty = dirty; } + void reset_dirty() { this->is_dirty = false; } + + // Returns the name of the preset, from which this preset inherits. + static std::string& inherits(DynamicPrintConfig &cfg) { return cfg.option("inherits", true)->value; } + std::string& inherits() { return Preset::inherits(this->config); } + const std::string& inherits() const { return Preset::inherits(const_cast(this)->config); } + + // Returns the "compatible_prints_condition". + static std::string& compatible_prints_condition(DynamicPrintConfig &cfg) { return cfg.option("compatible_prints_condition", true)->value; } + std::string& compatible_prints_condition() { + assert(this->type == TYPE_FILAMENT || this->type == TYPE_SLA_MATERIAL); + return Preset::compatible_prints_condition(this->config); + } + const std::string& compatible_prints_condition() const { return const_cast(this)->compatible_prints_condition(); } + + // Returns the "compatible_printers_condition". + static std::string& compatible_printers_condition(DynamicPrintConfig &cfg) { return cfg.option("compatible_printers_condition", true)->value; } + std::string& compatible_printers_condition() { + assert(this->type == TYPE_PRINT || this->type == TYPE_SLA_PRINT || this->type == TYPE_FILAMENT || this->type == TYPE_SLA_MATERIAL); + return Preset::compatible_printers_condition(this->config); + } + const std::string& compatible_printers_condition() const { return const_cast(this)->compatible_printers_condition(); } + + // Return a printer technology, return ptFFF if the printer technology is not set. + static PrinterTechnology printer_technology(const DynamicPrintConfig &cfg) { + auto *opt = cfg.option>("printer_technology"); + // The following assert may trigger when importing some legacy profile, + // but it is safer to keep it here to capture the cases where the "printer_technology" key is queried, where it should not. +// assert(opt != nullptr); + return (opt == nullptr) ? ptFFF : opt->value; + } + PrinterTechnology printer_technology() const { return Preset::printer_technology(this->config); } + // This call returns a reference, it may add a new entry into the DynamicPrintConfig. + PrinterTechnology& printer_technology_ref() { return this->config.option>("printer_technology", true)->value; } + + // Set is_visible according to application config + void set_visible_from_appconfig(const AppConfig &app_config); + + // Resize the extruder specific fields, initialize them with the content of the 1st extruder. + void set_num_extruders(unsigned int n) { this->config.set_num_extruders(n); } + + // Sort lexicographically by a preset name. The preset name shall be unique across a single PresetCollection. + bool operator<(const Preset &other) const { return this->name < other.name; } + + // special for upport G and Support W + std::string get_filament_type(std::string &display_filament_type); + std::string get_printer_type(PresetBundle *preset_bundle); // get edited preset type + std::string get_current_printer_type(PresetBundle *preset_bundle); // get current preset type + + bool has_lidar(PresetBundle *preset_bundle); + bool is_custom_defined(); + + bool is_bbl_vendor_preset(PresetBundle *preset_bundle); + BedType get_default_bed_type(PresetBundle *preset_bundle); + bool has_cali_lines(PresetBundle* preset_bundle); + + + + static const std::vector& print_options(); + static const std::vector& filament_options(); + // Printer options contain the nozzle options. + static const std::vector& printer_options(); + // Nozzle options of the printer options. + static const std::vector& nozzle_options(); + // Printer machine limits, those are contained in printer_options(). + static const std::vector& machine_limits_options(); + + static const std::vector& sla_printer_options(); + static const std::vector& sla_material_options(); + static const std::vector& sla_print_options(); + + static void update_suffix_modified(const std::string& new_suffix_modified); + static const std::string& suffix_modified(); + static std::string remove_suffix_modified(const std::string& name); + static void normalize(DynamicPrintConfig &config); + // Report configuration fields, which are misplaced into a wrong group, remove them from the config. + static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config); + + // BBS: move constructor to public + Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {} + +protected: + Preset() = default; + + friend class PresetCollection; + friend class PresetBundle; +}; + +bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer); +bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config); +bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer); + +enum class PresetSelectCompatibleType { + // Never select a compatible preset if the newly selected profile is not compatible. + Never, + // Only select a compatible preset if the active profile used to be compatible, but it is no more. + OnlyIfWasCompatible, + // Always select a compatible preset if the active profile is no more compatible. + Always +}; + +// Substitutions having been performed during parsing a single configuration file. +struct PresetConfigSubstitutions { + // User readable preset name. + std::string preset_name; + // Type of the preset (Print / Filament / Printer ...) + Preset::Type preset_type; + enum class Source { + UserFile, + ConfigBundle, + //BBS: add cloud and project type + UserCloud, + ProjectFile, + }; + Source preset_source; + // Source of the preset. It may be empty in case of a ConfigBundle being loaded. + std::string preset_file; + // What config value has been substituted with what. + ConfigSubstitutions substitutions; +}; + +// Substitutions having been performed during parsing a set of configuration files, for example when starting up +// PrusaSlicer and reading the user Print / Filament / Printer profiles. +using PresetsConfigSubstitutions = std::vector; + +// Collections of presets of the same type (one of the Print, Filament or Printer type). +class PresetCollection +{ +public: + // Initialize the PresetCollection with the "- default -" preset. + PresetCollection(Preset::Type type, const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name = "Default Setting"); + + typedef std::deque::iterator Iterator; + typedef std::deque::const_iterator ConstIterator; + typedef std::function SyncFunc; + //BBS get m_presets begin + Iterator lbegin() { return m_presets.begin(); } + //BBS: validate_preset + bool validate_preset(const std::string &name, std::string &inherit); + + Iterator begin() { return m_presets.begin() + m_num_default_presets; } + ConstIterator begin() const { return m_presets.cbegin() + m_num_default_presets; } + ConstIterator cbegin() const { return m_presets.cbegin() + m_num_default_presets; } + Iterator end() { return m_presets.end(); } + ConstIterator end() const { return m_presets.cend(); } + ConstIterator cend() const { return m_presets.cend(); } + + //BBS + Iterator erase(Iterator it) { return m_presets.erase(it); } + SyncFunc sync_func{ nullptr }; + void set_sync_func(SyncFunc func) { sync_func = func; } + //BBS: mutex + void lock() { m_mutex.lock(); } + void unlock() { m_mutex.unlock(); } + + void reset(bool delete_files); + + Preset::Type type() const { return m_type; } + // Name, to be used on the screen and in error messages. Not localized. + std::string name() const; + // Name, to be used as a section name in config bundle, and as a folder name for presets. + std::string section_name() const; + const std::deque& operator()() const { return m_presets; } + + // Add default preset at the start of the collection, increment the m_default_preset counter. + void add_default_preset(const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name); + + // Load ini files of the particular type from the provided directory path. + void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule); + + //BBS: update user presets directory + void update_user_presets_directory(const std::string& dir_path, const std::string& type); + void save_user_presets(const std::string& dir_path, const std::string& type, std::vector& need_to_delete_list); + bool load_user_preset(std::string name, std::map preset_values, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule); + void update_after_user_presets_loaded(); + //BBS: get user presets + int get_user_presets(PresetBundle *preset_bundle, std::vector &result_presets); + void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time); + bool need_sync(std::string name, std::string setting_id, long long update_time); + + //BBS: add function to generate differed preset for save + //the pointer should be freed by the caller + Preset* get_preset_differed_for_save(Preset& preset); + //BBS:get the differencen values to update + int get_differed_values_to_update(Preset& preset, std::map& key_values); + + //BBS: add project embedded presets logic + void load_project_embedded_presets(std::vector& project_presets, const std::string& type, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule); + std::vector get_project_embedded_presets(); + bool reset_project_embedded_presets(); + + // Load a preset from an already parsed config file, insert it into the sorted sequence of presets + // and select it, losing previous modifications. + Preset& load_preset(const std::string &path, const std::string &name, const DynamicPrintConfig &config, bool select = true, Semver file_version = Semver(), bool is_custom_defined = false); + Preset& load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select = true, Semver file_version = Semver(), bool is_custom_defined = false); + + bool clone_presets(std::vector const &presets, std::vector &failures, std::function modifier, bool force_rewritten = false); + bool clone_presets_for_printer( + std::vector const &templates, std::vector &failures, std::string const &printer, std::function create_filament_id, bool force_rewritten = false); + bool 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 = false); + + std::map> get_filament_presets() const; + + // Returns a loaded preset, returns true if an existing preset was selected AND modified from config. + // In that case the successive filament loaded for a multi material printer should not be modified, but + // an external preset should be created instead. + enum class LoadAndSelect { + // Never select + Never, + // Always select + Always, + // Select a profile only if it was modified. + OnlyIfModified, + }; + std::pair 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. + const DynamicPrintConfig &config, + //different settings list + const std::set &different_settings_list, + // Select the preset after loading? + LoadAndSelect select = LoadAndSelect::Always, + const Semver file_version = Semver(), + const std::string filament_id = std::string()); + + // Save the preset under a new name. If the name is different from the old one, + // a new preset is stored into the list of presets. + // All presets are marked as not modified and the new preset is activated. + //BBS: add project embedded preset logic + void save_current_preset(const std::string &new_name, bool detach = false, bool save_to_project = false, Preset* _curr_preset = nullptr); + + // Delete the current preset, activate the first visible preset. + // returns true if the preset was deleted successfully. + bool delete_current_preset(); + // Delete the current preset, activate the first visible preset. + // returns true if the preset was deleted successfully. + bool delete_preset(const std::string& name); + + // Enable / disable the "- default -" preset. + void set_default_suppressed(bool default_suppressed); + bool is_default_suppressed() const { return m_default_suppressed; } + + // Select a preset. If an invalid index is provided, the first visible preset is selected. + Preset& select_preset(size_t idx); + // Return the selected preset, without the user modifications applied. + Preset& get_selected_preset() { + //BBS fix crash when m_idx_selected == -1, give a default value + if ((m_idx_selected < 0) || (m_idx_selected >= m_presets.size())) { + select_preset(first_visible_idx()); + } + return m_presets[m_idx_selected]; + } + const Preset& get_selected_preset() const { return m_presets[m_idx_selected]; } + size_t get_selected_idx() const { return m_idx_selected; } + // Returns the name of the selected preset, or an empty string if no preset is selected. + std::string get_selected_preset_name() const { + if (m_idx_selected == size_t(-1) || m_idx_selected >= m_presets.size()) + return std::string(); + return this->get_selected_preset().name; + } + // For the current edited preset, return the parent preset if there is one. + // If there is no parent preset, nullptr is returned. + // The parent preset may be a system preset or a user preset, which will be + // reflected by the UI. + const Preset* get_selected_preset_parent() const; + // Get parent preset for a child preset, based on the "inherits" field of a child, + // where the "inherits" profile name is searched for in both m_presets and m_map_system_profile_renamed. + const Preset* get_preset_parent(const Preset& child) const; + const Preset* get_preset_base(const Preset& child) const; + // Return the selected preset including the user modifications. + Preset& get_edited_preset() { return m_edited_preset; } + const Preset& get_edited_preset() const { return m_edited_preset; } + + // Return the last saved preset. +// const Preset& get_saved_preset() const { return m_saved_preset; } + + // Return vendor of the first parent profile, for which the vendor is defined, or null if such profile does not exist. + PresetWithVendorProfile get_preset_with_vendor_profile(const Preset &preset) const; + PresetWithVendorProfile get_edited_preset_with_vendor_profile() const { return this->get_preset_with_vendor_profile(this->get_edited_preset()); } + + const std::string& get_preset_name_by_alias(const std::string& alias) const; + const std::string* get_preset_name_renamed(const std::string &old_name) const; + bool is_alias_exist(const std::string &alias, Preset* preset = nullptr); + void set_printer_hold_alias(const std::string &alias, Preset &preset); + + // used to update preset_choice from Tab + const std::deque& get_presets() const { return m_presets; } + size_t get_idx_selected() { return m_idx_selected; } + static const std::string& get_suffix_modified(); + + // Return a preset possibly with modifications. + Preset& default_preset(size_t idx = 0) { assert(idx < m_num_default_presets); return m_presets[idx]; } + const Preset& default_preset(size_t idx = 0) const { assert(idx < m_num_default_presets); return m_presets[idx]; } + virtual const Preset& default_preset_for(const DynamicPrintConfig & /* config */) const { return this->default_preset(); } + // Return a preset by an index. If the preset is active, a temporary copy is returned. + Preset& preset(size_t idx, bool real = false) { + if (real) return m_presets[idx]; + return (idx == m_idx_selected) ? m_edited_preset : m_presets[idx]; + } + const Preset& preset(size_t idx) const { return const_cast(this)->preset(idx); } + void discard_current_changes() { + m_presets[m_idx_selected].reset_dirty(); + m_edited_preset = m_presets[m_idx_selected]; +// update_saved_preset_from_current_preset(); + } + + // Return a preset by its name. If the preset is active, a temporary copy is returned. + // If a preset is not found by its name, null is returned. + // BBS return real pointer if set real = true + Preset* find_preset(const std::string &name, bool first_visible_if_not_found = false, bool real = false); + const Preset* find_preset(const std::string &name, bool first_visible_if_not_found = false) const + { return const_cast(this)->find_preset(name, first_visible_if_not_found); } + + size_t first_visible_idx() const; + // Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible. + // If one of the prefered_alternates is compatible, select it. + template + size_t first_compatible_idx(PreferedCondition prefered_condition) const + { + size_t i = m_default_suppressed ? m_num_default_presets : 0; + size_t n = this->m_presets.size(); + size_t i_compatible = n; + int match_quality = -1; + for (; i < n; ++ i) + // Since we use the filament selection from Wizard, it's needed to control the preset visibility too + if (m_presets[i].is_compatible && m_presets[i].is_visible) { + int this_match_quality = prefered_condition(m_presets[i]); + if (this_match_quality > match_quality) { + if (match_quality == std::numeric_limits::max()) + // Better match will not be found. + return i; + // Store the first compatible profile with highest match quality into i_compatible. + i_compatible = i; + match_quality = this_match_quality; + } + } + return (i_compatible == n) ? + // No compatible preset found, return the default preset. + 0 : + // Compatible preset found. + i_compatible; + } + // Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible. + size_t first_compatible_idx() const { return this->first_compatible_idx([](const Preset&) -> int { return 0; }); } + + // Return index of the first visible preset. Certainly at least the '- default -' preset shall be visible. + // Return the first visible preset. Certainly at least the '- default -' preset shall be visible. + Preset& first_visible() { return this->preset(this->first_visible_idx()); } + const Preset& first_visible() const { return this->preset(this->first_visible_idx()); } + Preset& first_compatible() { return this->preset(this->first_compatible_idx()); } + template + Preset& first_compatible(PreferedCondition prefered_condition) { return this->preset(this->first_compatible_idx(prefered_condition)); } + const Preset& first_compatible() const { return this->preset(this->first_compatible_idx()); } + + // Return number of presets including the "- default -" preset. + size_t size() const { return m_presets.size(); } + bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; } + + // For Print / Filament presets, disable those, which are not compatible with the printer. + template + void update_compatible(const PresetWithVendorProfile &active_printer, const PresetWithVendorProfile *active_print, PresetSelectCompatibleType select_other_if_incompatible, PreferedCondition prefered_condition) + { + if (this->update_compatible_internal(active_printer, active_print, select_other_if_incompatible) == (size_t)-1) + // Find some other compatible preset, or the "-- default --" preset. + this->select_preset(this->first_compatible_idx(prefered_condition)); + } + void update_compatible(const PresetWithVendorProfile &active_printer, const PresetWithVendorProfile *active_print, PresetSelectCompatibleType select_other_if_incompatible) + { this->update_compatible(active_printer, active_print, select_other_if_incompatible, [](const Preset&) -> int { return 0; }); } + + size_t num_visible() const { return std::count_if(m_presets.begin(), m_presets.end(), [](const Preset &preset){return preset.is_visible;}); } + + // Compare the content of get_selected_preset() with get_edited_preset() configs, return true if they differ. + bool current_is_dirty() const + { return is_dirty(&this->get_edited_preset(), &this->get_selected_preset()); } + // Compare the content of get_selected_preset() with get_edited_preset() configs, return the list of keys where they differ. + std::vector current_dirty_options(const bool deep_compare = false) const + { return dirty_options(&this->get_edited_preset(), &this->get_selected_preset(), deep_compare); } + // Compare the content of get_selected_preset() with get_edited_preset() configs, return the list of keys where they differ. + std::vector current_different_from_parent_options(const bool deep_compare = false) const + { return dirty_options(&this->get_edited_preset(), this->get_selected_preset_parent(), deep_compare); } + + // Compare the content of get_saved_preset() with get_edited_preset() configs, return true if they differ. + bool saved_is_dirty() const + { return is_dirty(&this->get_edited_preset(), &m_saved_preset); } + // Compare the content of get_saved_preset() with get_edited_preset() configs, return the list of keys where they differ. +// std::vector saved_dirty_options() const +// { return dirty_options(&this->get_edited_preset(), &this->get_saved_preset(), /* deep_compare */ false); } + // Copy edited preset into saved preset. + void update_saved_preset_from_current_preset() { m_saved_preset = m_edited_preset; } + + // Return a sorted list of system preset names. + // 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 system_preset_names() const; + + // Update a dirty flag of the current preset + // Return true if the dirty flag changed. + bool update_dirty(); + + // Select a profile by its name. Return true if the selection changed. + // Without force, the selection is only updated if the index changes. + // With force, the changes are reverted if the new index is the same as the old index. + bool select_preset_by_name(const std::string &name, bool force); + bool is_base_preset(const Preset &preset) const { return preset.is_system || (preset.is_user() && preset.inherits().empty()); } + + // Generate a file path from a profile name. Add the ".ini" suffix if it is missing. + std::string path_from_name(const std::string &new_name, bool detach = false) const; + std::string path_for_preset(const Preset & preset) const; + + size_t num_default_presets() { return m_num_default_presets; } + +protected: + PresetCollection() = default; + // Copy constructor and copy operators are not to be used from outside PresetBundle, + // as the Profile::vendor points to an instance of VendorProfile stored at parent PresetBundle! + PresetCollection(const PresetCollection &other) = default; + //BBS: add operator= logic insteadof default + PresetCollection& operator=(const PresetCollection &other); + // After copying a collection with the default operators above, call this function + // to adjust Profile::vendor pointers. + void update_vendor_ptrs_after_copy(const VendorMap &vendors); + + // Select a preset, if it exists. If it does not exist, select an invalid (-1) index. + // This is a temporary state, which shall be fixed immediately by the following step. + bool select_preset_by_name_strict(const std::string &name); + + // Merge one vendor's presets with the other vendor's presets, report duplicates. + std::vector merge_presets(PresetCollection &&other, const VendorMap &new_vendors); + + // Update m_map_alias_to_profile_name from loaded system profiles. + void update_map_alias_to_profile_name(); + + // Update m_map_system_profile_renamed from loaded system profiles. + void update_map_system_profile_renamed(); + + void set_custom_preset_alias(Preset &preset); + +private: + // Find a preset position in the sorted list of presets. + // The "-- default -- " preset is always the first, so it needs + // to be handled differently. + // If a preset does not exist, an iterator is returned indicating where to insert a preset with the same name. + std::deque::iterator find_preset_internal(const std::string &name) + { + auto it = Slic3r::lower_bound_by_predicate(m_presets.begin() + m_num_default_presets, m_presets.end(), [&name](const auto& l) { return l.name < name; }); + if (it == m_presets.end() || it->name != name) { + // Preset has not been not found in the sorted list of non-default presets. Try the defaults. + for (size_t i = 0; i < m_num_default_presets; ++ i) + if (m_presets[i].name == name) { + it = m_presets.begin() + i; + break; + } + } + return it; + } + std::deque::const_iterator find_preset_internal(const std::string &name) const + { return const_cast(this)->find_preset_internal(name); } + std::deque::iterator find_preset_renamed(const std::string &name) { + auto it_renamed = m_map_system_profile_renamed.find(name); + auto it = (it_renamed == m_map_system_profile_renamed.end()) ? m_presets.end() : this->find_preset_internal(it_renamed->second); + assert((it_renamed == m_map_system_profile_renamed.end()) || (it != m_presets.end() && it->name == it_renamed->second)); + return it; + } + std::deque::const_iterator find_preset_renamed(const std::string &name) const + { return const_cast(this)->find_preset_renamed(name); } + + size_t update_compatible_internal(const PresetWithVendorProfile &active_printer, const PresetWithVendorProfile *active_print, PresetSelectCompatibleType unselect_if_incompatible); +public: + static bool is_dirty(const Preset *edited, const Preset *reference); + static std::vector dirty_options(const Preset *edited, const Preset *reference, const bool deep_compare = false); + //BBS: add function for dirty_options_without_option_list + static std::vector dirty_options_without_option_list(const Preset *edited, const Preset *reference, const std::set& option_ignore_list, const bool deep_compare = false); +private: + // Type of this PresetCollection: TYPE_PRINT, TYPE_FILAMENT or TYPE_PRINTER. + Preset::Type m_type; + // List of presets, starting with the "- default -" preset. + // Use deque to force the container to allocate an object per each entry, + // so that the addresses of the presets don't change during resizing of the container. + std::deque m_presets; + // System profiles may have aliases. Map to the full profile name. + std::map> m_map_alias_to_profile_name; + std::unordered_map> m_printer_hold_alias; + // Map from old system profile name to a current system profile name. + std::map m_map_system_profile_renamed; + // Initially this preset contains a copy of the selected preset. Later on, this copy may be modified by the user. + Preset m_edited_preset; + // Contains a copy of the last saved selected preset. + Preset m_saved_preset; + + // Selected preset. + size_t m_idx_selected; + // Is the "- default -" preset suppressed? + bool m_default_suppressed = true; + size_t m_num_default_presets = 0; + + // Path to the directory to store the config files into. + std::string m_dir_path; + + // to access select_preset_by_name_strict() and the default & copy constructors. + friend class PresetBundle; + + //BBS: mutex + std::mutex m_mutex; +}; + +// Printer supports the FFF and SLA technologies, with different set of configuration values, +// therefore this PresetCollection needs to handle two defaults. +class PrinterPresetCollection : public PresetCollection +{ +public: + PrinterPresetCollection(Preset::Type type, const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name = "Default Printer") : + PresetCollection(type, keys, defaults, default_name) {} + + const Preset& default_preset_for(const DynamicPrintConfig &config) const override; + + const Preset* find_system_preset_by_model_and_variant(const std::string &model_id, const std::string &variant) const; + const Preset* find_custom_preset_by_model_and_variant(const std::string &model_id, const std::string &variant) const; + + bool only_default_printers() const; +private: + PrinterPresetCollection() = default; + PrinterPresetCollection(const PrinterPresetCollection &other) = default; + PrinterPresetCollection& operator=(const PrinterPresetCollection &other) = default; + + friend class PresetBundle; +}; + +namespace PresetUtils { + // PrinterModel of a system profile, from which this preset is derived, or null if it is not derived from a system profile. + const VendorProfile::PrinterModel* system_printer_model(const Preset &preset); + std::string system_printer_bed_model(const Preset& preset); + std::string system_printer_bed_texture(const Preset& preset); + std::string system_printer_hotend_model(const Preset& preset); +} // namespace PresetUtils + + +////////////////////////////////////////////////////////////////////// + +class PhysicalPrinter +{ +public: + PhysicalPrinter(const std::string& name, const DynamicPrintConfig &default_config); + PhysicalPrinter(const std::string& name, const DynamicPrintConfig &default_config, const Preset& preset); + void set_name(const std::string &name); + + // Name of the Physical Printer, usually derived form the file name. + std::string name; + // File name of the Physical Printer. + std::string file; + // Configuration data, loaded from a file, or set from the defaults. + DynamicPrintConfig config; + // set of presets used with this physical printer + std::set preset_names; + + // Has this profile been loaded? + bool loaded = false; + + static std::string separator(); + static const std::vector& printer_options(); + static const std::vector& print_host_options(); + static std::vector presets_with_print_host_information(const PrinterPresetCollection& printer_presets); + static bool has_print_host_information(const DynamicPrintConfig& config); + + const std::set& get_preset_names() const; + + void update_preset_names_in_config(); + + //BBS: change to json format + //void save() { this->config.save(this->file); } + void save(DynamicPrintConfig* parent_config) { this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION)); } + void save(const std::string& file_name_from, const std::string& file_name_to); + + void update_from_preset(const Preset& preset); + void update_from_config(const DynamicPrintConfig &new_config); + + // add preset to the preset_names + // return false, if preset with this name is already exist in the set + bool add_preset(const std::string& preset_name); + bool delete_preset(const std::string& preset_name); + void reset_presets(); + + // Return a printer technology, return ptFFF if the printer technology is not set. + static PrinterTechnology printer_technology(const DynamicPrintConfig& cfg) { + auto* opt = cfg.option>("printer_technology"); + // The following assert may trigger when importing some legacy profile, + // but it is safer to keep it here to capture the cases where the "printer_technology" key is queried, where it should not. + return (opt == nullptr) ? ptFFF : opt->value; + } + PrinterTechnology printer_technology() const { return printer_technology(this->config); } + + // Sort lexicographically by a preset name. The preset name shall be unique across a single PresetCollection. + bool operator<(const PhysicalPrinter& other) const { return this->name < other.name; } + + // get full printer name included a name of the preset + std::string get_full_name(std::string preset_name) const; + + // get printer name from the full name uncluded preset name + static std::string get_short_name(std::string full_name); + + // get preset name from the full name uncluded printer name + static std::string get_preset_name(std::string full_name); + +protected: + friend class PhysicalPrinterCollection; +}; + + +// --------------------------------- +// *** PhysicalPrinterCollection *** +// --------------------------------- + +// Collections of physical printers +class PhysicalPrinterCollection +{ +public: + PhysicalPrinterCollection(const std::vector& keys); + + typedef std::deque::iterator Iterator; + typedef std::deque::const_iterator ConstIterator; + Iterator begin() { return m_printers.begin(); } + ConstIterator begin() const { return m_printers.cbegin(); } + ConstIterator cbegin() const { return m_printers.cbegin(); } + Iterator end() { return m_printers.end(); } + ConstIterator end() const { return m_printers.cend(); } + ConstIterator cend() const { return m_printers.cend(); } + + bool empty() const {return m_printers.empty(); } + + void reset(bool delete_files) {}; + + const std::deque& operator()() const { return m_printers; } + + // Load ini files of the particular type from the provided directory path. + void load_printers(const std::string& dir_path, const std::string& subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule); + void load_printers_from_presets(PrinterPresetCollection &printer_presets); + // Load printer from the loaded configuration + void load_printer(const std::string& path, const std::string& name, DynamicPrintConfig&& config, bool select, bool save=false); + + // Save the printer under a new name. If the name is different from the old one, + // a new printer is stored into the list of printers. + // New printer is activated. + void save_printer(PhysicalPrinter& printer, const std::string& renamed_from = ""); + + // Delete the current preset, activate the first visible preset. + // returns true if the preset was deleted successfully. + bool delete_printer(const std::string& name); + // Delete the selected preset + // returns true if the preset was deleted successfully. + bool delete_selected_printer(); + // Delete preset_name preset from all printers: + // If there is last preset for the printer and first_check== false, then delete this printer + // returns true if all presets were deleted successfully. + bool delete_preset_from_printers(const std::string& preset_name); + + // Get list of printers which have more than one preset and "preset_names" preset is one of them + std::vector get_printers_with_preset( const std::string &preset_name); + // Get list of printers which has only "preset_names" preset + std::vector get_printers_with_only_preset( const std::string &preset_name); + + // Return the selected preset, without the user modifications applied. + PhysicalPrinter& get_selected_printer() { return m_printers[m_idx_selected]; } + const PhysicalPrinter& get_selected_printer() const { return m_printers[m_idx_selected]; } + + size_t get_selected_idx() const { return m_idx_selected; } + // Returns the name of the selected preset, or an empty string if no preset is selected. + std::string get_selected_printer_name() const { return (m_idx_selected == size_t(-1)) ? std::string() : this->get_selected_printer().name; } + // Returns the config of the selected printer, or nullptr if no printer is selected. + DynamicPrintConfig* get_selected_printer_config() { return (m_idx_selected == size_t(-1)) ? nullptr : &(this->get_selected_printer().config); } + // Returns the config of the selected printer, or nullptr if no printer is selected. + PrinterTechnology get_selected_printer_technology() { return (m_idx_selected == size_t(-1)) ? PrinterTechnology::ptAny : this->get_selected_printer().printer_technology(); } + + // Each physical printer can have a several related preset, + // so, use the next functions to get an exact names of selections in the list: + // Returns the full name of the selected printer, or an empty string if no preset is selected. + std::string get_selected_full_printer_name() const; + // Returns the printer model of the selected preset, or an empty string if no preset is selected. + std::string get_selected_printer_preset_name() const { return (m_idx_selected == size_t(-1)) ? std::string() : m_selected_preset; } + + // Select printer by the full printer name, which contains name of printer, separator and name of selected preset + // If full_name doesn't contain name of selected preset, then select first preset in the list for this printer + void select_printer(const std::string& full_name); + void select_printer(const PhysicalPrinter& printer); + void select_printer(const std::string& printer_name, const std::string& preset_name); + bool has_selection() const; + void unselect_printer() ; + bool is_selected(ConstIterator it, const std::string &preset_name) const; + + // Return a printer by an index. If the printer is active, a temporary copy is returned. + PhysicalPrinter& printer(size_t idx) { return m_printers[idx]; } + const PhysicalPrinter& printer(size_t idx) const { return const_cast(this)->printer(idx); } + + // Return a preset by its name. If the preset is active, a temporary copy is returned. + // If a preset is not found by its name, null is returned. + // It is possible case (in)sensitive search + PhysicalPrinter* find_printer(const std::string& name, bool case_sensitive_search = true); + const PhysicalPrinter* find_printer(const std::string& name, bool case_sensitive_search = true) const + { + return const_cast(this)->find_printer(name, case_sensitive_search); + } + + // Generate a file path from a profile name. Add the ".ini" suffix if it is missing. + std::string path_from_name(const std::string& new_name) const; + + const DynamicPrintConfig& default_config() const { return m_default_config; } + +private: + friend class PresetBundle; + PhysicalPrinterCollection() = default; + PhysicalPrinterCollection& operator=(const PhysicalPrinterCollection& other) = default; + + // Find a physical printer position in the sorted list of printers. + // The name of a printer should be unique and case insensitive + // Use this functions with case_sensitive_search = false, when you need case insensitive search + std::deque::iterator find_printer_internal(const std::string& name, bool case_sensitive_search = true); + std::deque::const_iterator find_printer_internal(const std::string& name, bool case_sensitive_search = true) const + { + return const_cast(this)->find_printer_internal(name); + } + + // List of printers + // Use deque to force the container to allocate an object per each entry, + // so that the addresses of the presets don't change during resizing of the container. + std::deque m_printers; + + // Default config for a physical printer containing all key/value pairs of PhysicalPrinter::printer_options(). + DynamicPrintConfig m_default_config; + + // Selected printer. + size_t m_idx_selected = size_t(-1); + // The name of the preset which is currently select for this printer + std::string m_selected_preset; + + // Path to the directory to store the config files into. + std::string m_dir_path; +}; + + +} // namespace Slic3r + +#endif /* slic3r_Preset_hpp_ */ diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp new file mode 100644 index 000000000..5eb43dcf5 --- /dev/null +++ b/src/libslic3r/PresetBundle.cpp @@ -0,0 +1,4022 @@ +#include + +#include "PresetBundle.hpp" +#include "libslic3r.h" +#include "Utils.hpp" +#include "Model.hpp" +#include "format.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +// 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", + "wipe_tower_x", + "wipe_tower_y", + "wipe_tower_rotation_angle", + "curr_bed_type", + "flush_multiplier", +}; + +//BBS: add BBL as default +const char *PresetBundle::BBL_BUNDLE = "BBL"; +const char *PresetBundle::BBL_DEFAULT_PRINTER_MODEL = "Bambu Lab X1 Carbon"; +const char *PresetBundle::BBL_DEFAULT_PRINTER_VARIANT = "0.4"; +const char *PresetBundle::BBL_DEFAULT_FILAMENT = "Generic PLA"; + +PresetBundle::PresetBundle() + : prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast(FullPrintConfig::defaults())) + , filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), static_cast(FullPrintConfig::defaults()), "Default Filament") + , 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. + this->filaments.default_preset().config.null_nullables(); + + 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","thumbnail_size"}) 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; + + // 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, + from_data_dir / PRESET_CONFIG_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; +} + +//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(); + } + try { + this->printers.load_project_embedded_presets(project_presets, PRESET_CONFIG_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()) + filament_presets[i] = prefered_filament_profile; + 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) + { + 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; + } + + 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); + + // 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); + } + 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); + } + 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); + } + 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; +} + +PresetsConfigSubstitutions PresetBundle::import_presets(std::vector & files, + std::function override_confirm, + ForwardCompatibilitySubstitutionRule rule) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " entry"; + PresetsConfigSubstitutions substitutions; + int overwrite = 0; + std::vector result; + 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, ".bbscfg") || boost::iends_with(file, ".bbsflmt") || 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 default_folder(user_folder / DEFAULT_USER_FOLDER_NAME); + if (!fs::exists(default_folder)) fs::create_directory(default_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(default_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; + + /*if (!open_zip_reader(&zip_archive, file)) { + BOOST_LOG_TRIVIAL(info) << "Failed to initialize reader ZIP archive"; + return substitutions; + } else { + BOOST_LOG_TRIVIAL(info) << "Success to initialize reader ZIP archive"; + }*/ + + 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"; + } + + // 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__ << " Form 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); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " import target file: " << target_file_path << " import result" << is_success; + } + } + } + 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(); + } + } + 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) +{ + 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; + Semver app_version = *(Semver::parse(SLIC3R_VERSION)); + if (version->maj() > app_version.maj()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset incompatibla, not loading: " << name; + return false; + } + + PresetCollection *collection = nullptr; + if (config.has("printer_settings_id")) + collection = &printers; + else if (config.has("print_settings_id")) + collection = &prints; + else if (config.has("filament_settings_id")) + collection = &filaments; + if (collection == nullptr) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset type is unknown, not loading: " << name; + return false; + } + if (overwrite == 0) overwrite = 1; + if (auto p = collection->find_preset(name, false)) { + if (p->is_default || p->is_system) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset already present and is system preset, not loading: " << name; + return false; + } + if (overwrite != 2 && overwrite != 3) overwrite = override_confirm(name); //3: yes to all 2: no to all + } + if (overwrite == 0 || overwrite == 2) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset already present, not loading: " << 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_preset(inherits_value, false, true); + } + if (inherit_preset) { + new_config = inherit_preset->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)); + + Preset &preset = collection->load_preset(collection->path_from_name(name, inherit_preset == nullptr), name, std::move(new_config), false); + 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()) + 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)}); + + preset.save(inherit_preset ? &inherit_preset->config : nullptr); + result.push_back(file); + } catch (const std::ifstream::failure &err) { + BOOST_LOG_TRIVIAL(error) << boost::format("The config cannot be loaded: %1%. Reason: %2%") % file % err.what(); + } catch (const std::runtime_error &err) { + 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::vector& 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); + this->configs.save_user_presets(dir_user_presets, PRESET_CONFIG_NAME, need_to_delete_list); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished"); +} + +//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); + this->configs.update_user_presets_directory(dir_user_presets, PRESET_CONFIG_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)) { + 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"}; +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]; + + 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+2; 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(); + 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 = BBL_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(); + } +} + +// Load system presets into this PresetBundle. +// For each vendor, there will be a single PresetBundle loaded. +/*std::pair PresetBundle::load_system_presets(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(); + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; + bool first = true; + for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) + if (Slic3r::is_ini_file(dir_entry)) { + std::string name = dir_entry.path().filename().string(); + // Remove the .ini suffix. + name.erase(name.size() - 4); + try { + // Load the config bundle, flatten it. + if (first) { + // Reset this PresetBundle and load the first vendor config. + append(substitutions, this->load_configbundle(dir_entry.path().string(), PresetBundle::LoadSystem, 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_configbundle(dir_entry.path().string(), PresetBundle::LoadSystem, compatibility_rule).first); + std::vector duplicates = this->merge_presets(std::move(other)); + if (! duplicates.empty()) { + errors_cummulative += "Vendor configuration file " + name + " contains the following presets with names used by other vendors: "; + 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"; + } + } + 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); +}*/ + +//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(); + 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 { + // Load the config bundle, flatten it. + 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, 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, 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"; + } + } + } + + 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)); + 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(); +} + +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 +{ + // there are not aliases for Printers profiles + if (preset_type == Preset::TYPE_PRINTER || 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 : + 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) +{ + //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; + 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; + } + 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) + config.set(AppConfig::SECTION_FILAMENTS, filament->name, "true"); + //} + + for (auto &preset : filaments) + preset.set_visible_from_appconfig(config); +} + +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); +} + +// 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(debug) << __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_print_profile_name = remove_ini_suffix(config.get("presets", PRESET_PRINT_NAME)); + std::string initial_sla_print_profile_name = remove_ini_suffix(config.get("presets", PRESET_SLA_PRINT_NAME)); + std::string initial_filament_profile_name = remove_ini_suffix(config.get("presets", PRESET_FILAMENT_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)); + std::string initial_config_profile_name = remove_ini_suffix(config.get("presets", PRESET_CONFIG_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; + + //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("Default Filament")) && (prefered_filament_profiles.size() > 0)) + initial_filament_profile_name = prefered_filament_profiles[0]; + } + + // 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); + if (! config.has("presets", name)) + break; + this->filament_presets.emplace_back(remove_ini_suffix(config.get("presets", name))); + } + std::vector filament_colors; + if (config.has("presets", "filament_colors")) { + boost::algorithm::split(filament_colors, config.get("presets", "filament_colors"), boost::algorithm::is_any_of(",")); + } + filament_colors.resize(filament_presets.size(), "#00AE42"); + project_config.option("filament_colour")->values = filament_colors; + std::vector matrix; + if (config.has("presets", "flush_volumes_matrix")) { + boost::algorithm::split(matrix, config.get("presets", "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("presets", "flush_volumes_vector")) { + boost::algorithm::split(matrix, config.get("presets", "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("app", "flush_multiplier")) { + std::string str_flush_multiplier = config.get("app", "flush_multiplier"); + if (!str_flush_multiplier.empty()) + project_config.option("flush_multiplier")->set(new ConfigOptionFloat(std::stof(str_flush_multiplier))); + } + + // 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) { + if (auto it = filaments.find_preset_internal(fp); 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; + } + } + + // 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(debug) << __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"); + config.set("presets", PRESET_PRINT_NAME, prints.get_selected_preset_name()); + config.set("presets", PRESET_FILAMENT_NAME, filament_presets.front()); + for (unsigned i = 1; i < filament_presets.size(); ++i) { + char name[64]; + assert(!filament_presets[i].empty()); + sprintf(name, "filament_%02u", i); + config.set("presets", name, filament_presets[i]); + } + CNumericLocalesSetter locales_setter; + std::string filament_colors = boost::algorithm::join(project_config.option("filament_colour")->values, ","); + config.set("presets", "filament_colors", filament_colors); + 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("presets", "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("presets", "flush_volumes_vector", flush_volumes_vector); + + config.set("presets", PRESET_PRINTER_NAME, printers.get_selected_preset_name()); + + auto flush_multi_opt = project_config.option("flush_multiplier"); + config.set("flush_multiplier", std::to_string(flush_multi_opt ? flush_multi_opt->getFloat() : 1.0f)); + // 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(debug) << __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::string new_color) +{ + 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"); + filament_color->resize(n); + ams_multi_color_filment.resize(n); + + //BBS set new filament color to new_color + if (old_filament_count < n) { + if (!new_color.empty()) { + for (int i = old_filament_count; i < n; i++) { + filament_color->values[i] = new_color; + } + } + } + + update_multi_material_filament_presets(); +} + +unsigned int PresetBundle::sync_ams_list(unsigned int &unknowns) +{ + std::vector filament_presets; + std::vector filament_colors; + ams_multi_color_filment.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 filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); + auto filament_multi_color = ams.opt("filament_multi_colors")->values; + if (filament_id.empty()) continue; + if (!filament_changed && this->filament_presets.size() > filament_presets.size()) { + filament_presets.push_back(this->filament_presets[filament_presets.size()]); + filament_colors.push_back(filament_color); + ams_multi_color_filment.push_back(filament_multi_color); + 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 (filament_presets.size() < this->filament_presets.size()) { + filament_presets.push_back(this->filament_presets[filament_presets.size()]); + filament_colors.push_back(filament_color); + ams_multi_color_filment.push_back(filament_multi_color); + ++unknowns; + 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; + } + ++unknowns; + filament_id = iter->filament_id; + } + filament_presets.push_back(iter->name); + filament_colors.push_back(filament_color); + ams_multi_color_filment.push_back(filament_multi_color); + } + if (filament_presets.empty()) + return 0; + this->filament_presets = filament_presets; + ConfigOptionStrings *filament_color = project_config.option("filament_colour"); + filament_color->resize(filament_presets.size()); + filament_color->values = filament_colors; + update_multi_material_filament_presets(); + return filament_presets.size(); +} + +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::set PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str) +{ + 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 (!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); + + if (filament_list.find(setting_id) == filament_list.end()) return is_equation; + 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; +} + +//BBS: check whether this is the only edited filament +bool PresetBundle::is_the_only_edited_filament(unsigned int filament_index) +{ + int 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; + + int 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; +} + +DynamicPrintConfig PresetBundle::full_config() const +{ + return (this->printers.get_edited_preset().printer_technology() == ptFFF) ? + this->full_fff_config() : + this->full_sla_config(); +} + +DynamicPrintConfig PresetBundle::full_config_secure() const +{ + DynamicPrintConfig config = this->full_config(); + //BBS example: 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() 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(); + 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); + + if (num_filaments <= 1) { + out.apply(this->filaments.get_edited_preset().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); + } 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); + } + + // loop through options and apply them to the resulting config. + 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_configs.front()->option(key); + if (opt_src != nullptr) + opt_dst->set(opt_src); + } else { + // BBS + ConfigOptionVectorBase* opt_vec_dst = static_cast(opt_dst); + { + 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_configs[i]->option(key); + opt_vec_dst->set(filament_opts); + } + } + } + } + + //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" }; + 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)); + } + + 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; + // 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("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); + // 1) Try to load the config file into a boost property tree. + /*boost::property_tree::ptree tree; + try { + boost::nowide::ifstream ifs(path); + boost::property_tree::read_ini(ifs, tree); + } catch (const std::ifstream::failure &err) { + throw Slic3r::RuntimeError(std::string("The Config Bundle cannot be loaded: ") + path + "\n\tReason: " + err.what()); + } catch (const boost::property_tree::file_parser_error &err) { + throw Slic3r::RuntimeError(format("Failed loading the Config Bundle \"%1%\": %2% at line %3%", + err.filename(), err.message(), err.line())); + } catch (const std::runtime_error &err) { + throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + path + "\n\tReason: " + err.what()); + } + + // 2) Continue based on the type of the configuration file. + ConfigFileType config_file_type = guess_config_file_type(tree); + ConfigSubstitutions config_substitutions; + try { + switch (config_file_type) { + case CONFIG_FILE_TYPE_UNKNOWN: + throw Slic3r::RuntimeError(std::string("Unknown configuration file type: ") + path); + case CONFIG_FILE_TYPE_APP_CONFIG: + throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + path + ". This is an application config file."); + case CONFIG_FILE_TYPE_CONFIG: + { + // Initialize a config from full defaults. + DynamicPrintConfig config; + config.apply(FullPrintConfig::defaults()); + config_substitutions = config.load(tree, compatibility_rule); + Preset::normalize(config); + load_config_file_config(path, true, std::move(config)); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": normal config, finished, got %1% substitutions")%config_substitutions.size(); + return config_substitutions; + } + case CONFIG_FILE_TYPE_CONFIG_BUNDLE: + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": found config bundle"); + return load_config_file_config_bundle(path, tree, compatibility_rule); + } + } catch (const ConfigurationError &e) { + throw Slic3r::RuntimeError(format("Invalid configuration file %1%: %2%", path, e.what())); + } + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(" finished, should not be here"); + // This shall never happen. Suppres compiler warnings. + assert(false);*/ + return ConfigSubstitutions{}; +} + +// 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, bool is_custom_defined) +{ + 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 + size_t num_filaments = config.option("filament_colour")->size(); +#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; + } + + // 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, is_custom_defined] + (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, is_custom_defined).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()); + + if (num_filaments <= 1) { + // Split the "compatible_printers_condition" and "inherits" 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) + 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, is_custom_defined); + 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()) { + const 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) + 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. + 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"); +} + +// Load the active configuration of a config bundle from a boost property_tree. This is a private method called from load_config_file. +/*ConfigSubstitutions PresetBundle::load_config_file_config_bundle( + const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule) +{ + // 1) Load the config bundle into a temp data. + PresetBundle tmp_bundle; + // Load the config bundle, but don't save the loaded presets to user profile directory, as only the presets marked as active in the loaded preset bundle + // will be loaded into the master PresetBundle and activated. + auto [presets_substitutions, presets_imported] = tmp_bundle.load_configbundle(path, {}, compatibility_rule); + UNUSED(presets_imported); + + std::string bundle_name = std::string(" - ") + boost::filesystem::path(path).filename().string(); + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, bundle_name %1%")%bundle_name; + // 2) Extract active configs from the config bundle, copy them and activate them in this bundle. + ConfigSubstitutions config_substitutions; + auto load_one = [&path, &bundle_name, &presets_substitutions = presets_substitutions, &config_substitutions]( + PresetCollection &collection_dst, PresetCollection &collection_src, const std::string &preset_name_src, bool activate) -> std::string { + // If there are substitutions reported for this preset, move them to config_substitutions. + if (auto it = std::find_if(presets_substitutions.begin(), presets_substitutions.end(), [&preset_name_src](const PresetConfigSubstitutions& subs){ return subs.preset_name == preset_name_src; }); + it != presets_substitutions.end() && ! it->substitutions.empty()) + append(config_substitutions, std::move(it->substitutions)); + Preset *preset_src = collection_src.find_preset(preset_name_src, false); + Preset *preset_dst = collection_dst.find_preset(preset_name_src, false); + assert(preset_src != nullptr); + std::string preset_name_dst; + if (preset_dst != nullptr && preset_dst->is_default) { + // No need to copy a default preset, it always exists in collection_dst. + if (activate) + collection_dst.select_preset(0); + return preset_name_src; + } else if (preset_dst != nullptr && preset_src->config == preset_dst->config) { + // Don't save as the config exists in the current bundle and its content is the same. + return preset_name_src; + } else { + // Generate a new unique name. + preset_name_dst = preset_name_src + bundle_name; + Preset *preset_dup = nullptr; + for (size_t i = 1; (preset_dup = collection_dst.find_preset(preset_name_dst, false)) != nullptr; ++ i) { + if (preset_src->config == preset_dup->config) + // The preset has been already copied into collection_dst. + return preset_name_dst; + // Try to generate another name. + char buf[64]; + sprintf(buf, " (%d)", (int)i); + preset_name_dst = preset_name_src + buf + bundle_name; + } + } + assert(! preset_name_dst.empty()); + // Save preset_src->config into collection_dst under preset_name_dst. + // The "compatible_printers" field should not have been exported into a config.ini or a G-code anyway, + // but some of the alpha versions of Slic3r did. + ConfigOption *opt_compatible = preset_src->config.optptr("compatible_printers"); + if (opt_compatible != nullptr) { + assert(opt_compatible->type() == coStrings); + if (opt_compatible->type() == coStrings) + static_cast(opt_compatible)->values.clear(); + } + (collection_dst.type() == Preset::TYPE_FILAMENT ? + collection_dst.load_preset(path, preset_name_dst, preset_src->config, activate) : + // Only move the source config for non filament profiles, as single filament profile may be referenced multiple times. + collection_dst.load_preset(path, preset_name_dst, std::move(preset_src->config), activate)) + .is_external = true; + return preset_name_dst; + }; + load_one(this->prints, tmp_bundle.prints, tmp_bundle.prints .get_selected_preset_name(), true); + load_one(this->sla_prints, tmp_bundle.sla_prints, tmp_bundle.sla_prints .get_selected_preset_name(), true); + load_one(this->filaments, tmp_bundle.filaments, tmp_bundle.filaments .get_selected_preset_name(), true); + load_one(this->sla_materials, tmp_bundle.sla_materials, tmp_bundle.sla_materials.get_selected_preset_name(), true); + load_one(this->printers, tmp_bundle.printers, tmp_bundle.printers .get_selected_preset_name(), true); + this->update_multi_material_filament_presets(); + for (size_t i = 1; i < std::min(tmp_bundle.filament_presets.size(), this->filament_presets.size()); ++ i) + this->filament_presets[i] = load_one(this->filaments, tmp_bundle.filaments, tmp_bundle.filament_presets[i], false); + + this->update_compatible(PresetSelectCompatibleType::Never); + + sort_remove_duplicates(config_substitutions); + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": finished, got %1% substitutions")%config_substitutions.size(); + return config_substitutions; +}*/ + +// Process the Config Bundle loaded as a Boost property tree. +// For each print, filament and printer preset (group defined by group_name), apply the inherited presets. +// The presets starting with '*' are considered non-terminal and they are +// removed through the flattening process by this function. +// This function will never fail, but it will produce error messages through boost::log. +// system_profiles will not be flattened, and they will be kept inside the "inherits" field +/*static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree, const std::string &group_name, const std::vector &system_profiles) +{ + namespace pt = boost::property_tree; + + // 1) For the group given by group_name, initialize the presets. + struct Prst { + Prst(const std::string &name, pt::ptree *node) : name(name), node(node) {} + // Name of this preset. If the name starts with '*', it is an intermediate preset, + // which will not make it into the result. + const std::string name; + // Link to the source boost property tree node, owned by tree. + pt::ptree *node; + // Link to the presets, from which this preset inherits. + std::vector inherits; + // Link to the presets, for which this preset is a direct parent. + std::vector parent_of; + // When running the Kahn's Topological sorting algorithm, this counter is decreased from inherits.size() to zero. + // A cycle is indicated, if the number does not drop to zero after the Kahn's algorithm finishes. + size_t num_incoming_edges_left = 0; + // Sorting by the name, to be used when inserted into std::set. + bool operator==(const Prst &rhs) const { return this->name == rhs.name; } + bool operator< (const Prst &rhs) const { return this->name < rhs.name; } + }; + // Find the presets, store them into a std::map, addressed by their names. + std::set presets; + std::string group_name_preset = group_name + ":"; + for (auto §ion : tree) + if (boost::starts_with(section.first, group_name_preset) && section.first.size() > group_name_preset.size()) + presets.emplace(section.first.substr(group_name_preset.size()), §ion.second); + // Fill in the "inherits" and "parent_of" members, report invalid inheritance fields. + for (const Prst &prst : presets) { + // Parse the list of comma separated values, possibly enclosed in quotes. + std::vector inherits_names; + std::vector inherits_system; + if (Slic3r::unescape_strings_cstyle(prst.node->get("inherits", ""), inherits_names)) { + // Resolve the inheritance by name. + std::vector &inherits_nodes = const_cast(prst).inherits; + for (const std::string &node_name : inherits_names) { + auto it_system = std::lower_bound(system_profiles.begin(), system_profiles.end(), node_name); + if (it_system != system_profiles.end() && *it_system == node_name) { + // Loading a user config budnle, this preset is derived from a system profile. + inherits_system.emplace_back(node_name); + } else { + auto it = presets.find(Prst(node_name, nullptr)); + if (it == presets.end()) + BOOST_LOG_TRIVIAL(error) << "flatten_configbundle_hierarchy: The preset " << prst.name << " inherits an unknown preset \"" << node_name << "\""; + else { + inherits_nodes.emplace_back(const_cast(&(*it))); + inherits_nodes.back()->parent_of.emplace_back(const_cast(&prst)); + } + } + } + } else { + BOOST_LOG_TRIVIAL(error) << "flatten_configbundle_hierarchy: The preset " << prst.name << " has an invalid \"inherits\" field"; + } + // Remove the "inherits" key, it has no meaning outside of the config bundle. + const_cast(prst.node)->erase("inherits"); + if (! inherits_system.empty()) { + // Loaded a user config bundle, where a profile inherits a system profile. + // User profile should be derived from a single system profile only. + assert(inherits_system.size() == 1); + if (inherits_system.size() > 1) + BOOST_LOG_TRIVIAL(error) << "flatten_configbundle_hierarchy: The preset " << prst.name << " inherits from more than single system preset"; + prst.node->put("inherits", Slic3r::escape_string_cstyle(inherits_system.front())); + } + } + + // 2) Create a linear ordering for the directed acyclic graph of preset inheritance. + // https://en.wikipedia.org/wiki/Topological_sorting + // Kahn's algorithm. + std::vector sorted; + { + // Initialize S with the set of all nodes with no incoming edge. + std::deque S; + for (const Prst &prst : presets) + if (prst.inherits.empty()) + S.emplace_back(const_cast(&prst)); + else + const_cast(&prst)->num_incoming_edges_left = prst.inherits.size(); + while (! S.empty()) { + Prst *n = S.front(); + S.pop_front(); + sorted.emplace_back(n); + for (Prst *m : n->parent_of) { + assert(m->num_incoming_edges_left > 0); + if (-- m->num_incoming_edges_left == 0) { + // We have visited all parents of m. + S.emplace_back(m); + } + } + } + if (sorted.size() < presets.size()) { + for (const Prst &prst : presets) + if (prst.num_incoming_edges_left) + BOOST_LOG_TRIVIAL(error) << "flatten_configbundle_hierarchy: The preset " << prst.name << " has cyclic dependencies"; + } + } + + // Apply the dependencies in their topological ordering. + for (Prst *prst : sorted) { + // Merge the preset nodes in their order of application. + // Iterate in a reverse order, so the last change will be placed first in merged. + for (auto it_inherits = prst->inherits.rbegin(); it_inherits != prst->inherits.rend(); ++ it_inherits) + for (auto it = (*it_inherits)->node->begin(); it != (*it_inherits)->node->end(); ++ it) + if (it->first == "renamed_from") { + // Don't inherit "renamed_from" flag, it does not make sense. The "renamed_from" flag only makes sense for a concrete preset. + if (boost::starts_with((*it_inherits)->name, "*")) + BOOST_LOG_TRIVIAL(error) << boost::format("Nonpublic intermediate preset %1% contains a \"renamed_from\" field, which is ignored") % (*it_inherits)->name; + } else if (prst->node->find(it->first) == prst->node->not_found()) + prst->node->add_child(it->first, it->second); + } + + // Remove the "internal" presets from the ptree. These presets are marked with '*'. + group_name_preset += '*'; + for (auto it_section = tree.begin(); it_section != tree.end(); ) { + if (boost::starts_with(it_section->first, group_name_preset) && it_section->first.size() > group_name_preset.size()) + // Remove the "internal" preset from the ptree. + it_section = tree.erase(it_section); + else + // Keep the preset. + ++ it_section; + } +}*/ + +// preset_bundle is set when loading user config bundles, which must not overwrite the system profiles. +/*static void flatten_configbundle_hierarchy(boost::property_tree::ptree &tree, const PresetBundle *preset_bundle) +{ + flatten_configbundle_hierarchy(tree, "print", preset_bundle ? preset_bundle->prints.system_preset_names() : std::vector()); + flatten_configbundle_hierarchy(tree, "filament", preset_bundle ? preset_bundle->filaments.system_preset_names() : std::vector()); + flatten_configbundle_hierarchy(tree, "sla_print", preset_bundle ? preset_bundle->sla_prints.system_preset_names() : std::vector()); + flatten_configbundle_hierarchy(tree, "sla_material", preset_bundle ? preset_bundle->sla_materials.system_preset_names() : std::vector()); + flatten_configbundle_hierarchy(tree, "printer", preset_bundle ? preset_bundle->printers.system_preset_names() : std::vector()); +}*/ + +// Load a config bundle file, into presets and store the loaded presets into separate files +// of the local configuration directory. +/*std::pair PresetBundle::load_configbundle( + const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule) +{ + // 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(debug) << __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) Read the complete config file into a boost::property_tree. + namespace pt = boost::property_tree; + pt::ptree tree; + boost::nowide::ifstream ifs(path); + try { + pt::read_ini(ifs, tree); + } catch (const boost::property_tree::ini_parser::ini_parser_error &err) { + throw Slic3r::RuntimeError(format("Failed loading config bundle \"%1%\"\nError: \"%2%\" at line %3%", path, err.message(), err.line()).c_str()); + } + + const VendorProfile *vendor_profile = nullptr; + if (flags.has(LoadConfigBundleAttribute::LoadSystem) || flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) { + auto vp = VendorProfile::from_ini(tree, path); + if (vp.models.size() == 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer model defined.") % path; + return std::make_pair(PresetsConfigSubstitutions{}, 0); + } else if (vp.num_variants() == 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer variant defined") % path; + return std::make_pair(PresetsConfigSubstitutions{}, 0); + } + vendor_profile = &this->vendors.insert({vp.id, vp}).first->second; + } + + //BBS: add config related logs + if (vendor_profile) + 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); + + // 1.5) Flatten the config bundle by applying the inheritance rules. Internal profiles (with names starting with '*') are removed. + // If loading a user config bundle, do not flatten with the system profiles, but keep the "inherits" flag intact. + flatten_configbundle_hierarchy(tree, flags.has(LoadConfigBundleAttribute::LoadSystem) ? nullptr : this); + + // 2) Parse the property_tree, extract the active preset names and the profiles, save them into local config files. + // Parse the obsolete preset names, to be deleted when upgrading from the old configuration structure. + std::vector loaded_prints; + std::vector loaded_filaments; + std::vector loaded_sla_prints; + std::vector loaded_sla_materials; + std::vector loaded_printers; + std::vector loaded_physical_printers; + std::string active_print; + std::vector active_filaments; + std::string active_sla_print; + std::string active_sla_material; + std::string active_printer; + std::string active_physical_printer; + size_t presets_loaded = 0; + size_t ph_printers_loaded = 0; + + for (const auto §ion : tree) { + PresetCollection *presets = nullptr; + std::string preset_name; + PhysicalPrinterCollection *ph_printers = nullptr; + std::string ph_printer_name; + if (boost::starts_with(section.first, "print:")) { + presets = &this->prints; + preset_name = section.first.substr(6); + } else if (boost::starts_with(section.first, "filament:")) { + presets = &this->filaments; + preset_name = section.first.substr(9); + } else if (boost::starts_with(section.first, "sla_print:")) { + presets = &this->sla_prints; + preset_name = section.first.substr(10); + } else if (boost::starts_with(section.first, "sla_material:")) { + presets = &this->sla_materials; + preset_name = section.first.substr(13); + } else if (boost::starts_with(section.first, "printer:")) { + presets = &this->printers; + preset_name = section.first.substr(8); + } else if (boost::starts_with(section.first, "physical_printer:")) { + ph_printers = &this->physical_printers; + ph_printer_name = section.first.substr(17); + } else if (section.first == "presets") { + // Load the names of the active presets. + for (auto &kvp : section.second) { + if (kvp.first == "print") { + active_print = kvp.second.data(); + } else if (boost::starts_with(kvp.first, "filament")) { + int idx = 0; + if (kvp.first == "filament" || sscanf(kvp.first.c_str(), "filament_%d", &idx) == 1) { + if (int(active_filaments.size()) <= idx) + active_filaments.resize(idx + 1, std::string()); + active_filaments[idx] = kvp.second.data(); + } + } else if (kvp.first == "sla_print") { + active_sla_print = kvp.second.data(); + } else if (kvp.first == "sla_material") { + active_sla_material = kvp.second.data(); + } else if (kvp.first == "printer") { + active_printer = kvp.second.data(); + } else if (kvp.first == "physical_printer") { + active_physical_printer = kvp.second.data(); + } + } + } else if (section.first == "obsolete_presets") { + // Parse the names of obsolete presets. These presets will be deleted from user's + // profile directory on installation of this vendor preset. + for (auto &kvp : section.second) { + std::vector *dst = nullptr; + if (kvp.first == "print") + dst = &this->obsolete_presets.prints; + else if (kvp.first == "filament") + dst = &this->obsolete_presets.filaments; + else if (kvp.first == "sla_print") + dst = &this->obsolete_presets.sla_prints; + else if (kvp.first == "sla_material") + dst = &this->obsolete_presets.sla_materials; + else if (kvp.first == "printer") + dst = &this->obsolete_presets.printers; + if (dst) + unescape_strings_cstyle(kvp.second.data(), *dst); + } + } else if (section.first == "settings") { +#ifdef SUPPORT_AUTO_CENTER + // Load the settings. + for (auto &kvp : section.second) { + if (kvp.first == "autocenter") { + } + } +#endif + } else + // Ignore an unknown section. + continue; + if (presets != nullptr) { + // Load the print, filament or printer preset. + const DynamicPrintConfig *default_config = nullptr; + DynamicPrintConfig config; + std::string alias_name; + std::vector renamed_from; + try { + auto parse_config_section = [§ion, &alias_name, &renamed_from, &substitution_context, &path](DynamicPrintConfig &config) { + substitution_context.substitutions.clear(); + for (auto &kvp : section.second) { + if (kvp.first == "alias") + alias_name = kvp.second.data(); + else if (kvp.first == "renamed_from") { + if (! unescape_strings_cstyle(kvp.second.data(), renamed_from)) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The preset \"" << + section.first << "\" contains invalid \"renamed_from\" key, which is being ignored."; + } + } + // Throws on parsing error. For system presets, no substituion is being done, but an exception is thrown. + config.set_deserialize(kvp.first, kvp.second.data(), substitution_context); + } + }; + if (presets == &this->printers) { + // Select the default config based on the printer_technology field extracted from kvp. + DynamicPrintConfig config_src; + parse_config_section(config_src); + default_config = &presets->default_preset_for(config_src).config; + config = *default_config; + config.apply(config_src); + } else { + default_config = &presets->default_preset().config; + config = *default_config; + parse_config_section(config); + } + } catch (const ConfigurationError &e) { + throw ConfigurationError(format("Invalid configuration bundle \"%1%\", section [%2%]: ", path, section.first) + e.what()); + } + Preset::normalize(config); + // 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()) + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + section.first << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; + if (flags.has(LoadConfigBundleAttribute::LoadSystem) && presets == &printers) { + // 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()) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + section.first << "\" defines no printer model, it will be ignored."; + continue; + } + auto printer_variant = config.opt_string("printer_variant"); + if (printer_variant.empty()) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + section.first << "\" defines no printer variant, it will be ignored."; + continue; + } + auto it_model = std::find_if(vendor_profile->models.cbegin(), vendor_profile->models.cend(), + [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } + ); + if (it_model == vendor_profile->models.end()) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + section.first << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; + continue; + } + auto it_variant = it_model->variant(printer_variant); + if (it_variant == nullptr) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + section.first << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; + continue; + } + const Preset *preset_existing = presets->find_preset(section.first, false); + if (preset_existing != nullptr) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + section.first << "\" has already been loaded from another Confing Bundle."; + continue; + } + } else if (! flags.has(LoadConfigBundleAttribute::LoadSystem)) { + // This is a user config bundle. + const Preset *existing = presets->find_preset(preset_name, false); + if (existing != nullptr) { + if (existing->is_system) { + assert(existing->vendor != nullptr); + BOOST_LOG_TRIVIAL(error) << "Error in a user provided Config Bundle \"" << path << "\": The " << presets->name() << " preset \"" << + existing->name << "\" is a system preset of vendor " << existing->vendor->name << " and it will be ignored."; + continue; + } else { + assert(existing->vendor == nullptr); + BOOST_LOG_TRIVIAL(trace) << "A " << presets->name() << " preset \"" << existing->name << "\" was overwritten with a preset from user Config Bundle \"" << path << "\""; + } + } else { + BOOST_LOG_TRIVIAL(trace) << "A new " << presets->name() << " preset \"" << preset_name << "\" was imported from user Config Bundle \"" << path << "\""; + } + } + // Decide a full path to this .ini file. + auto file_name = boost::algorithm::iends_with(preset_name, ".json") ? preset_name : preset_name + ".json"; + //BBS: change directoties by design + auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ presets->section_name() / file_name).make_preferred(); + // Load the preset into the list of presets, save it to disk. + Preset &loaded = presets->load_preset(file_path.string(), preset_name, std::move(config), false); + if (flags.has(LoadConfigBundleAttribute::SaveImported)) + loaded.save(nullptr); + if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { + loaded.is_system = true; + loaded.vendor = vendor_profile; + } + + // 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); + loaded.renamed_from = std::move(renamed_from); + if (! substitution_context.empty()) + substitutions.push_back({ + preset_name, presets->type(), PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); + ++ presets_loaded; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %path; + } + + if (ph_printers != nullptr) { + // Load the physical printer + const DynamicPrintConfig& default_config = ph_printers->default_config(); + DynamicPrintConfig config = default_config; + + substitution_context.substitutions.clear(); + try { + for (auto& kvp : section.second) + config.set_deserialize(kvp.first, kvp.second.data(), substitution_context); + } catch (const ConfigurationError &e) { + throw ConfigurationError(format("Invalid configuration bundle \"%1%\", section [%2%]: ", path, section.first) + e.what()); + } + + // 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()) + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The physical printer \"" << + section.first << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; + + const PhysicalPrinter* ph_printer_existing = ph_printers->find_printer(ph_printer_name, false); + if (ph_printer_existing != nullptr) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The physical printer \"" << + section.first << "\" has already been loaded from another Confing Bundle."; + continue; + } + + // Decide a full path to this .ini file. + //BBS: change directoties by design + auto file_name = boost::algorithm::iends_with(ph_printer_name, ".json") ? ph_printer_name : ph_printer_name + ".json"; + auto file_path = (boost::filesystem::path(data_dir())/PRESET_SYSTEM_DIR/"physical_printer" / file_name).make_preferred(); + // Load the preset into the list of presets, save it to disk. + ph_printers->load_printer(file_path.string(), ph_printer_name, std::move(config), false, flags.has(LoadConfigBundleAttribute::SaveImported)); + if (! substitution_context.empty()) + substitutions.push_back({ + ph_printer_name, Preset::TYPE_PHYSICAL_PRINTER, PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); + ++ ph_printers_loaded; + } + } + + // 3) Activate the presets and physical printer if any exists. + if (! flags.has(LoadConfigBundleAttribute::LoadSystem)) { + if (! active_print.empty()) + prints.select_preset_by_name(active_print, true); + if (! active_sla_print.empty()) + sla_prints.select_preset_by_name(active_sla_print, true); + if (! active_sla_material.empty()) + sla_materials.select_preset_by_name(active_sla_material, true); + if (! active_printer.empty()) + printers.select_preset_by_name(active_printer, true); + if (! active_physical_printer.empty()) + physical_printers.select_printer(active_physical_printer, active_printer); + // Activate the first filament preset. + if (! active_filaments.empty() && ! active_filaments.front().empty()) + filaments.select_preset_by_name(active_filaments.front(), true); + this->update_multi_material_filament_presets(); + for (size_t i = 0; i < std::min(this->filament_presets.size(), active_filaments.size()); ++ i) + this->filament_presets[i] = filaments.find_preset(active_filaments[i], true)->name; + this->update_compatible(PresetSelectCompatibleType::Never); + } + + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", finished, presets_loaded %1%, ph_printers_loaded %2%")%presets_loaded %ph_printers_loaded; + return std::make_pair(std::move(substitutions), presets_loaded + ph_printers_loaded); +}*/ + +//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) +{ + // 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 = [](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 { + 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 + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": invalid type for " << iter1.key(); + } + } + else + 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) { + 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 { + 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_DEFAULT_BED_TYPE)) { + // get bed type + model.default_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 { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid default_materials %1% for Vendor %1%") % default_materials_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](\ + 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) -> 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; + config_src.load_from_json(subfile, substitution_context, false, key_values, reason); + if (!reason.empty()) { + 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); + if (it2 != config_maps.end()) { + default_config = &(it2->second); + 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; + } + } + } + else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find inherits "<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); + if (instantiation == "false" && "Template" != vendor_name) { + 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 (config.has("renamed_from")) { + const ConfigOptionVectorBase *vec = static_cast(config.option("renamed_from")); + renamed_from = vec->vserialize(); + } + Preset::normalize(config); + } + catch(nlohmann::detail::parse_error &err) { + 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()) + 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()) { + 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()) { + 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()) { + 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) { + 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) { + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" has already been loaded from another Confing 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(); + // 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; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; + if (presets_collection->type() == Preset::TYPE_FILAMENT) { + if (filament_id.empty() && "Template" != vendor_name) { + 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()) { + //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(); + for (auto& subfile : filament_subfiles) + { + std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); + if (!reason.empty()) { + //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()); + } + } + + //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()) { + //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::update_multi_material_filament_presets() +{ + if (printers.get_edited_preset().printer_technology() != ptFFF) + return; + + // BBS +#if 0 + // Verify and select the filament presets. + auto *nozzle_diameter = static_cast(printers.get_edited_preset().config.option("nozzle_diameter")); + size_t num_extruders = nozzle_diameter->values.size(); + // 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()); +#else + size_t num_filaments = this->filament_presets.size(); +#endif + + // 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_number_of_filaments = size_t(sqrt(old_matrix.size())+EPSILON); + if (num_filaments != old_number_of_filaments) { + // 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(); + } + + std::vector new_matrix; + for (unsigned int i=0;i< num_filaments;++i) + for (unsigned int j=0;j< num_filaments;++j) { + // append the value for this pair from the old matrix (if it's there): + if (i < old_number_of_filaments && j < old_number_of_filaments) + new_matrix.push_back(old_matrix[i* old_number_of_filaments + j]); + else + new_matrix.push_back( i == j ? 0. : filaments[2 * i] + filaments[2 * j + 1]); // so it matches new extruder volumes + } + 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 (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 (! 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) + 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"); + 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. + 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"); +} + +//BBS: add a API to dump current configbundle as default configbundle +/*void PresetBundle::export_current_configbundle(const std::string &path) +{ + boost::nowide::ofstream c; + c.open(path, std::ios::out | std::ios::trunc); + + c << "# generate the configbundle for BBL" << std::endl; + + const Preset& print_selected_preset = this->prints.get_selected_preset(); + const Preset* system_print = NULL; + const Preset& filament_selected_preset = this->filaments.get_selected_preset(); + const Preset* system_filament = NULL; + const Preset& printer_selected_preset = this->printers.get_selected_preset(); + const Preset* system_printer = NULL; + + // Export the print + std::string print_bbl = "@BBL-3DP"; + size_t pos = print_selected_preset.name.find(print_bbl); + if (std::string::npos != pos) + { + std::string system_print_name = print_selected_preset.name.substr(0, pos + print_bbl.size()); + system_print = this->prints.find_preset(system_print_name); + if (system_print) + { + c << std::endl << "[" << this->prints.section_name() << ":" << system_print->name << "]" << std::endl; + for (const std::string &opt_key : print_selected_preset.config.keys()) + { + if (!opt_key.compare("inherits")) + continue; + const ConfigOption *option1 = system_print->config.option(opt_key); + const ConfigOption *option2 = print_selected_preset.config.option(opt_key); + if (option1 && option2 && (*option1 != *option2)) + c << opt_key << " = " << print_selected_preset.config.opt_serialize(opt_key) << std::endl; + } + c << std::endl; + } + } + + // Export the filament + std::string filament_bbl = "@BBL-3DP"; + pos = filament_selected_preset.name.find(filament_bbl); + if (std::string::npos != pos) + { + std::string system_filamant_name = filament_selected_preset.name.substr(0, pos + filament_bbl.size()); + system_filament = this->filaments.find_preset(system_filamant_name); + if (system_filament) + { + c << std::endl << "[" << this->filaments.section_name() << ":" << system_filament->name << "]" << std::endl; + for (const std::string &opt_key : filament_selected_preset.config.keys()) + { + if (!opt_key.compare("inherits")) + continue; + const ConfigOption *option1 = system_filament->config.option(opt_key); + const ConfigOption *option2 = filament_selected_preset.config.option(opt_key); + if (option1 && option2 && (*option1 != *option2)) + c << opt_key << " = " << filament_selected_preset.config.opt_serialize(opt_key) << std::endl; + } + c << std::endl; + } + } + + // Export the printer + std::string printer_bbl = "BBL-3DP-001"; + pos = printer_selected_preset.name.find(printer_bbl); + if (std::string::npos != pos) + { + std::string system_printer_name = printer_selected_preset.name.substr(0, pos + printer_bbl.size()); + system_printer = this->printers.find_preset(system_printer_name); + if (system_printer) + { + c << std::endl << "[" << this->printers.section_name() << ":" << system_printer->name << "]" << std::endl; + for (const std::string &opt_key : printer_selected_preset.config.keys()) + { + c << opt_key << " = " << printer_selected_preset.config.opt_serialize(opt_key) << std::endl; + } + c << std::endl; + } + } + + c.close(); +}*/ + + +//void PresetBundle::export_configbundle(const std::string &path, bool export_system_settings, bool export_physical_printers/* = false*/) +//{ +// boost::nowide::ofstream c; +// c.open(path, std::ios::out | std::ios::trunc); +// +// // Put a comment at the first line including the time stamp and Slic3r version. +// c << "# " << Slic3r::header_slic3r_generated() << std::endl; +// +// // Export the print, filament and printer profiles. +// +// for (const PresetCollection *presets : { +// (const PresetCollection*)&this->prints, (const PresetCollection*)&this->filaments, +// (const PresetCollection*)&this->sla_prints, (const PresetCollection*)&this->sla_materials, +// (const PresetCollection*)&this->printers }) { +// for (const Preset &preset : (*presets)()) { +// //BBS: add project embedded preset logic and refine is_external +// if (preset.is_default || preset.is_project_embedded || (preset.is_system && ! export_system_settings)) +// //if (preset.is_default || preset.is_external || (preset.is_system && ! export_system_settings)) +// // Only export the common presets, not external files or the default preset. +// continue; +// c << std::endl << "[" << presets->section_name() << ":" << preset.name << "]" << std::endl; +// for (const std::string &opt_key : preset.config.keys()) +// c << opt_key << " = " << preset.config.opt_serialize(opt_key) << std::endl; +// } +// } +// +// if (export_physical_printers) { +// for (const PhysicalPrinter& ph_printer : this->physical_printers) { +// c << std::endl << "[physical_printer:" << ph_printer.name << "]" << std::endl; +// for (const std::string& opt_key : ph_printer.config.keys()) +// c << opt_key << " = " << ph_printer.config.opt_serialize(opt_key) << std::endl; +// } +// } +// +// // Export the names of the active presets. +// c << std::endl << "[presets]" << std::endl; +// c << "print = " << this->prints.get_selected_preset_name() << std::endl; +// c << "sla_print = " << this->sla_prints.get_selected_preset_name() << std::endl; +// c << "sla_material = " << this->sla_materials.get_selected_preset_name() << std::endl; +// c << "printer = " << this->printers.get_selected_preset_name() << std::endl; +// for (size_t i = 0; i < this->filament_presets.size(); ++ i) { +// char suffix[64]; +// if (i > 0) +// sprintf(suffix, "_%d", (int)i); +// else +// suffix[0] = 0; +// c << "filament" << suffix << " = " << this->filament_presets[i] << std::endl; +// } +// +// if (export_physical_printers && this->physical_printers.get_selected_idx() >= 0) +// c << "physical_printer = " << this->physical_printers.get_selected_printer_name() << std::endl; +//#if 0 +// // Export the following setting values from the provided setting repository. +// static const char *settings_keys[] = { "autocenter" }; +// c << "[settings]" << std::endl; +// for (size_t i = 0; i < sizeof(settings_keys) / sizeof(settings_keys[0]); ++ i) +// c << settings_keys[i] << " = " << settings.serialize(settings_keys[i]) << std::endl; +//#endif +// +// c.close(); +//} + +//BBS: add export system preset functions +/*void PresetBundle::export_system_configs(const std::string &path) +{ + // Export the print, filament and printer profiles. + for (const PresetCollection *presets : { + (const PresetCollection*)&this->prints, (const PresetCollection*)&this->filaments, + (const PresetCollection*)&this->sla_prints, (const PresetCollection*)&this->sla_materials, + (const PresetCollection*)&this->printers }) { + for (const Preset &preset : (*presets)()) { + if (preset.is_system) + { + // Only export the system presets + boost::nowide::ofstream c; + std::string file_path = path + std::string("\\") + preset.name; + + c.open(file_path, std::ios::out | std::ios::trunc); + + // Put a comment at the first line including the time stamp and Slic3r version. + c << "# " << Slic3r::header_slic3r_generated() << std::endl; + //c << std::endl << "[" << presets->section_name() << ":" << preset.name << "]" << std::endl; + for (const std::string &opt_key : preset.config.keys()) + c << opt_key << " = " << preset.config.opt_serialize(opt_key) << std::endl; + + c.close(); + } + } + } +}*/ + +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); +} + +} // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp new file mode 100644 index 000000000..d15eef922 --- /dev/null +++ b/src/libslic3r/PresetBundle.hpp @@ -0,0 +1,277 @@ +#ifndef slic3r_PresetBundle_hpp_ +#define slic3r_PresetBundle_hpp_ + +#include "Preset.hpp" +#include "AppConfig.hpp" +#include "enum_bitmask.hpp" + +#include +#include +#include + +#define DEFAULT_USER_FOLDER_NAME "default" +#define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json" + +#define VALIDATE_PRESETS_SUCCESS 0 +#define VALIDATE_PRESETS_PRINTER_NOT_FOUND 1 +#define VALIDATE_PRESETS_FILAMENTS_NOT_FOUND 2 +#define VALIDATE_PRESETS_MODIFIED_GCODES 3 + + +namespace Slic3r { + +// Bundle of Print + Filament + Printer presets. +class PresetBundle +{ +public: + PresetBundle(); + PresetBundle(const PresetBundle &rhs); + PresetBundle& operator=(const PresetBundle &rhs); + + // Remove all the presets but the "-- default --". + // Optionally remove all the files referenced by the presets from the user profile directory. + void reset(bool delete_files); + + void setup_directories(); + void copy_files(const std::string& from); + + struct PresetPreferences { + std::string printer_model_id;// name of a preferred printer model + std::string printer_variant; // name of a preferred printer variant + std::string filament; // name of a preferred filament preset + std::string sla_material; // name of a preferred sla_material preset + }; + + // Load ini files of all types (print, filament, printer) from Slic3r::data_dir() / presets. + // Load selections (current print, current filaments, current printer) from config.ini + // select preferred presets, if any exist + PresetsConfigSubstitutions load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule rule, + const PresetPreferences& preferred_selection = PresetPreferences()); + + // Load selections (current print, current filaments, current printer) from config.ini + // This is done just once on application start up. + //BBS: change it to public + void load_selections(AppConfig &config, const PresetPreferences& preferred_selection = PresetPreferences()); + + // BBS Load user presets + PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule); + PresetsConfigSubstitutions load_user_presets(AppConfig &config, std::map>& my_presets, ForwardCompatibilitySubstitutionRule rule); + PresetsConfigSubstitutions import_presets(std::vector &files, std::function override_confirm, ForwardCompatibilitySubstitutionRule rule); + bool import_json_presets(PresetsConfigSubstitutions & substitutions, + std::string & file, + std::function override_confirm, + ForwardCompatibilitySubstitutionRule rule, + int & overwrite, + std::vector & result); + void save_user_presets(AppConfig& config, std::vector& need_to_delete_list); + void remove_users_preset(AppConfig &config, std::map> * my_presets = nullptr); + void update_user_presets_directory(const std::string preset_folder); + void remove_user_presets_directory(const std::string preset_folder); + void update_system_preset_setting_ids(std::map>& system_presets); + + //BBS: add API to get previous machine + int validate_presets(const std::string &file_name, DynamicPrintConfig& config, std::set& different_gcodes); + + //BBS: add function to generate differed preset for save + //the pointer should be freed by the caller + Preset* get_preset_differed_for_save(Preset& preset); + int get_differed_values_to_update(Preset& preset, std::map& key_values); + + //BBS: get vendor's current version + Semver get_vendor_profile_version(std::string vendor_name); + + //BBS: project embedded preset logic + PresetsConfigSubstitutions load_project_embedded_presets(std::vector project_presets, ForwardCompatibilitySubstitutionRule substitution_rule); + std::vector get_current_project_embedded_presets(); + void reset_project_embedded_presets(); + + //BBS: find printer model + std::string get_texture_for_printer_model(std::string model_name); + std::string get_stl_model_for_printer_model(std::string model_name); + std::string get_hotend_model_for_printer_model(std::string model_name); + + // Export selections (current print, current filaments, current printer) into config.ini + void export_selections(AppConfig &config); + + // BBS + void set_num_filaments(unsigned int n, std::string new_col = ""); + unsigned int sync_ams_list(unsigned int & unknowns); + //BBS: check whether this is the only edited filament + bool is_the_only_edited_filament(unsigned int filament_index); + + void set_calibrate_printer(std::string name); + + std::set get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str); + bool 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); + + PresetCollection prints; + PresetCollection sla_prints; + PresetCollection filaments; + PresetCollection configs; + PresetCollection sla_materials; + PresetCollection& materials(PrinterTechnology pt) { return pt == ptFFF ? this->filaments : this->sla_materials; } + const PresetCollection& materials(PrinterTechnology pt) const { return pt == ptFFF ? this->filaments : this->sla_materials; } + PrinterPresetCollection printers; + PhysicalPrinterCollection physical_printers; + // Filament preset names for a multi-extruder or multi-material print. + // extruders.size() should be the same as printers.get_edited_preset().config.nozzle_diameter.size() + std::vector filament_presets; + // BBS: ams + std::map filament_ams_list; + std::vector> ams_multi_color_filment; + // Calibrate + Preset const * calibrate_printer = nullptr; + std::set calibrate_filaments; + + // The project configuration values are kept separated from the print/filament/printer preset, + // they are being serialized / deserialized from / to the .amf, .3mf, .config, .gcode, + // and they are being used by slicing core. + DynamicPrintConfig project_config; + + // There will be an entry for each system profile loaded, + // and the system profiles will point to the VendorProfile instances owned by PresetBundle::vendors. + VendorMap vendors; + + struct ObsoletePresets { + std::vector prints; + std::vector sla_prints; + std::vector filaments; + std::vector sla_materials; + std::vector printers; + std::vector configs; + }; + ObsoletePresets obsolete_presets; + + bool has_defauls_only() const + { return prints.has_defaults_only() && filaments.has_defaults_only() && printers.has_defaults_only(); } + + DynamicPrintConfig full_config() const; + // full_config() with the some "useless" config removed. + DynamicPrintConfig full_config_secure() const; + + // Load user configuration and store it into the user profiles. + // This method is called by the configuration wizard. + void load_config_from_wizard(const std::string &name, DynamicPrintConfig config, Semver file_version, bool is_custom_defined = false) + { this->load_config_file_config(name, false, std::move(config), file_version, true, is_custom_defined); } + + // Load configuration that comes from a model file containing configuration, such as 3MF et al. + // This method is called by the Plater. + void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver()) + { this->load_config_file_config(name, true, std::move(config), file_version); } + + // 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 load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule); + + // Load a config bundle file, into presets and store the loaded presets into separate files + // of the local configuration directory. + // Load settings into the provided settings instance. + // Activate the presets stored in the config bundle. + // Returns the number of presets loaded successfully. + enum LoadConfigBundleAttribute { + // Save the profiles, which have been loaded. + SaveImported, + // Delete all old config profiles before loading. + ResetUserProfile, + // Load a system config bundle. + LoadSystem, + LoadVendorOnly, + LoadFilamentOnly, + }; + using LoadConfigBundleAttributes = enum_bitmask; + // Load the config bundle based on the flags. + // Don't do any config substitutions when loading a system profile, perform and report substitutions otherwise. + /*std::pair load_configbundle( + const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/ + //BBS: add json related logic + std::pair load_vendor_configs_from_json( + const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule); + + // Export a config bundle file containing all the presets and the names of the active presets. + //void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); + //BBS: add a function to export current configbundle as default + //void export_current_configbundle(const std::string &path); + //BBS: add a function to export system presets for cloud-slicer + //void export_system_configs(const std::string &path); + std::vector export_current_configs(const std::string &path, std::function override_confirm, + bool include_modify, bool export_system_settings = false); + + // Enable / disable the "- default -" preset. + void set_default_suppressed(bool default_suppressed); + + // 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 set_filament_preset(size_t idx, const std::string &name); + + // Read out the number of extruders from an active printer preset, + // update size and content of filament_presets. + void update_multi_material_filament_presets(); + + // Update the is_compatible flag of all print and filament presets depending on whether they are marked + // as compatible with the currently selected printer (and print in case of filament presets). + // Also updates the is_visible flag of each preset. + // If select_other_if_incompatible is true, then the print or filament preset is switched to some compatible + // preset if the current print or filament preset is not compatible. + void update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible); + void update_compatible(PresetSelectCompatibleType select_other_if_incompatible) { this->update_compatible(select_other_if_incompatible, select_other_if_incompatible); } + + // Set the is_visible 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 load_installed_printers(const AppConfig &config); + + const std::string& get_preset_name_by_alias(const Preset::Type& preset_type, const std::string& alias) const; + + const int get_required_hrc_by_filament_type(const std::string& filament_type) const; + // Save current preset of a provided type under a new name. If the name is different from the old one, + // Unselected option would be reverted to the beginning values + //BBS: add project embedded preset logic + void save_changes_for_preset(const std::string& new_name, Preset::Type type, const std::vector& unselected_options, bool save_to_project = false); + + std::pair load_system_models_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule); + std::pair load_system_filaments_json(ForwardCompatibilitySubstitutionRule compatibility_rule); + VendorProfile get_custom_vendor_models() const; + + //BBS: add BBL as default + static const char *BBL_BUNDLE; + static const char *BBL_DEFAULT_PRINTER_MODEL; + static const char *BBL_DEFAULT_PRINTER_VARIANT; + static const char *BBL_DEFAULT_FILAMENT; +private: + //std::pair load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule); + //BBS: add json related logic + std::pair load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule); + // Merge one vendor's presets with the other vendor's presets, report duplicates. + std::vector merge_presets(PresetBundle &&other); + // Update renamed_from and alias maps of system profiles. + void update_system_maps(); + + // Set the is_visible flag for filaments and sla materials, + // apply defaults based on enabled printers when no filaments/materials are installed. + void load_installed_filaments(AppConfig &config); + void load_installed_sla_materials(AppConfig &config); + + // Load print, filament & printer presets from a config. If it is an external config, then the name is extracted from the external path. + // and the external config is just referenced, not stored into user profile directory. + // If it is not an external config, then the config will be stored into the user profile directory. + void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false, bool is_custom_defined = false); + /*ConfigSubstitutions load_config_file_config_bundle( + const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/ + + DynamicPrintConfig full_fff_config() const; + DynamicPrintConfig full_sla_config() const; +}; + +ENABLE_ENUM_BITMASK_OPERATORS(PresetBundle::LoadConfigBundleAttribute) + +} // namespace Slic3r + +#endif /* slic3r_PresetBundle_hpp_ */ diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5998a7c51..fff501a80 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1786,13 +1786,13 @@ void PrintConfigDef::init_fff_params() // "Note that this option only takes effect if no prime tower is generated in current plate."); //def->set_default_value(new ConfigOptionBool(0)); - /* def = this->add("default_print_speed", coFloat); + def = this->add("default_print_speed", coFloat); def->label = L("Default print speed"); def->tooltip = L("Speed of initial layer except the solid infill part"); def->sidetext = L("mm/s"); def->min = 0; def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(30));todo*/ + def->set_default_value(new ConfigOptionFloat(30)); def = this->add("initial_layer_speed", coFloat); def->label = L("Initial layer"); @@ -4680,7 +4680,8 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } else if (opt_key == "support_material_enforce_layers") { opt_key = "enforce_support_layers"; } else if ((opt_key == "initial_layer_print_height" || - opt_key == "initial_layer_speed" || + opt_key == "default_print_speed" || + opt_key == "initial_layer_speed" || opt_key == "internal_solid_infill_speed" || opt_key == "top_surface_speed" || opt_key == "support_interface_speed" || diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 8b5d3f370..927e6bf3e 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1019,7 +1019,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, initial_layer_acceleration)) ((ConfigOptionFloat, initial_layer_line_width)) ((ConfigOptionFloat, initial_layer_print_height)) - //((ConfigOptionFloat, default_print_speed)) + ((ConfigOptionFloat, default_print_speed)) ((ConfigOptionFloat, initial_layer_speed)) //BBS ((ConfigOptionFloat, initial_layer_infill_speed)) diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp new file mode 100644 index 000000000..5679dbb1f --- /dev/null +++ b/src/slic3r/GUI/GUI.cpp @@ -0,0 +1,597 @@ +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "format.hpp" +#include "I18N.hpp" + +#include "libslic3r/LocalesUtils.hpp" +#ifdef __APPLE__ +#include "slic3r/Utils/MacDarkMode.hpp" +#endif +#include + +#include +#include +#include + +#if __APPLE__ +#import +#elif _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include "boost/nowide/convert.hpp" +#endif + +#include "AboutDialog.hpp" +#include "MsgDialog.hpp" +#include "format.hpp" + +#include "WebUserLoginDialog.hpp" + +#include "libslic3r/Print.hpp" + +namespace Slic3r { + +class AppConfig; + +namespace GUI { + +#if __APPLE__ +IOPMAssertionID assertionID; +#endif + +void disable_screensaver() +{ + #if __APPLE__ + CFStringRef reasonForActivity = CFSTR("Slic3r"); + [[maybe_unused]]IOReturn success = IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep, + kIOPMAssertionLevelOn, reasonForActivity, &assertionID); + // ignore result: success == kIOReturnSuccess + #elif _WIN32 + SetThreadExecutionState(ES_DISPLAY_REQUIRED | ES_CONTINUOUS); + #endif +} + +void enable_screensaver() +{ + #if __APPLE__ + IOPMAssertionRelease(assertionID); + #elif _WIN32 + SetThreadExecutionState(ES_CONTINUOUS); + #endif +} + +bool debugged() +{ + #ifdef _WIN32 + return IsDebuggerPresent() == TRUE; + #else + return false; + #endif /* _WIN32 */ +} + +void break_to_debugger() +{ + #ifdef _WIN32 + if (IsDebuggerPresent()) + DebugBreak(); + #endif /* _WIN32 */ +} + +const std::string& shortkey_ctrl_prefix() +{ + static const std::string str = +#ifdef __APPLE__ + "⌘+" +#else + _u8L("Ctrl+") +#endif + ; + return str; +} + +const std::string& shortkey_alt_prefix() +{ + static const std::string str = +#ifdef __APPLE__ + "⌥+" +#else + "Alt+" +#endif + ; + return str; +} + +// opt_index = 0, by the reason of zero-index in ConfigOptionVector by default (in case only one element) +void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt_key, const boost::any& value, int opt_index /*= 0*/) +{ + try{ + + if (config.def()->get(opt_key)->type == coBools && config.def()->get(opt_key)->nullable) { + ConfigOptionBoolsNullable* vec_new = new ConfigOptionBoolsNullable{ boost::any_cast(value) }; + config.option(opt_key)->set_at(vec_new, opt_index, 0); + return; + } + + const ConfigOptionDef *opt_def = config.def()->get(opt_key); + switch (opt_def->type) { + case coFloatOrPercent:{ + std::string str = boost::any_cast(value); + bool percent = false; + if (str.back() == '%') { + str.pop_back(); + percent = true; + } + double val = std::stod(str); // locale-dependent (on purpose - the input is the actual content of the field) + config.set_key_value(opt_key, new ConfigOptionFloatOrPercent(val, percent)); + break;} + case coPercent: + config.set_key_value(opt_key, new ConfigOptionPercent(boost::any_cast(value))); + break; + case coFloat:{ + double& val = config.opt_float(opt_key); + val = boost::any_cast(value); + break; + } + case coPercents:{ + ConfigOptionPercents* vec_new = new ConfigOptionPercents{ boost::any_cast(value) }; + config.option(opt_key)->set_at(vec_new, opt_index, opt_index); + break; + } + case coFloats:{ + ConfigOptionFloats* vec_new = new ConfigOptionFloats{ boost::any_cast(value) }; + config.option(opt_key)->set_at(vec_new, opt_index, opt_index); + break; + } + case coString: + config.set_key_value(opt_key, new ConfigOptionString(boost::any_cast(value))); + break; + case coStrings:{ + if (opt_key == "compatible_prints" || opt_key == "compatible_printers") { + config.option(opt_key)->values = + boost::any_cast>(value); + } + else if (config.def()->get(opt_key)->gui_flags.compare("serialized") == 0) { + std::string str = boost::any_cast(value); + std::vector values {}; + if (!str.empty()) { + if (str.back() == ';') str.pop_back(); + // Split a string to multiple strings by a semi - colon.This is the old way of storing multi - string values. + // Currently used for the post_process config value only. + boost::split(values, str, boost::is_any_of(";")); + if (values.size() == 1 && values[0] == "") + values.resize(0); + } + config.option(opt_key)->values = values; + } + else{ + ConfigOptionStrings* vec_new = new ConfigOptionStrings{ boost::any_cast(value) }; + config.option(opt_key)->set_at(vec_new, opt_index, 0); + } + } + break; + case coBool: + config.set_key_value(opt_key, new ConfigOptionBool(boost::any_cast(value))); + break; + case coBools:{ + ConfigOptionBools* vec_new = new ConfigOptionBools{ boost::any_cast(value) != 0 }; + config.option(opt_key)->set_at(vec_new, opt_index, 0); + break;} + case coInt: + config.set_key_value(opt_key, new ConfigOptionInt(boost::any_cast(value))); + break; + case coInts:{ + ConfigOptionInts* vec_new = new ConfigOptionInts{ boost::any_cast(value) }; + config.option(opt_key)->set_at(vec_new, opt_index, 0); + } + break; + case coEnum:{ + auto *opt = opt_def->default_value.get()->clone(); + opt->setInt(boost::any_cast(value)); + config.set_key_value(opt_key, opt); + } + break; + // BBS + case coEnums:{ + ConfigOptionEnumsGeneric* vec_new = new ConfigOptionEnumsGeneric{ boost::any_cast(value) }; + if (config.has(opt_key)) + config.option(opt_key)->set_at(vec_new, opt_index, 0); + } + break; + case coPoint:{ + config.set_key_value(opt_key, new ConfigOptionPoint(boost::any_cast(value))); + } + break; + case coPoints:{ + if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key=="thumbnail_size") { + config.option(opt_key)->values = boost::any_cast>(value); + break; + } + ConfigOptionPoints* vec_new = new ConfigOptionPoints{ boost::any_cast(value) }; + config.option(opt_key)->set_at(vec_new, opt_index, 0); + } + break; + case coNone: + break; + default: + break; + } + } + catch (const std::exception &e) + { + wxLogError(format_wxstr("Internal error when changing value for %1%: %2%", opt_key, e.what())); + } +} + +void show_error(wxWindow* parent, const wxString& message, bool monospaced_font) +{ + wxGetApp().CallAfter([=] { + ErrorDialog msg(parent, message, monospaced_font); + msg.ShowModal(); + }); +} + +void show_error(wxWindow* parent, const char* message, bool monospaced_font) +{ + assert(message); + show_error(parent, wxString::FromUTF8(message), monospaced_font); +} + +void show_error_id(int id, const std::string& message) +{ + auto *parent = id != 0 ? wxWindow::FindWindowById(id) : nullptr; + show_error(parent, message); +} + +void show_info(wxWindow* parent, const wxString& message, const wxString& title) +{ + //wxMessageDialog msg_wingow(parent, message, wxString(SLIC3R_APP_NAME " - ") + (title.empty() ? _L("Notice") : title), wxOK | wxICON_INFORMATION); + MessageDialog msg_wingow(parent, message, wxString(SLIC3R_APP_FULL_NAME " - ") + (title.empty() ? _L("Notice") : title), wxOK | wxICON_INFORMATION); + msg_wingow.ShowModal(); +} + +void show_info(wxWindow* parent, const char* message, const char* title) +{ + assert(message); + show_info(parent, wxString::FromUTF8(message), title ? wxString::FromUTF8(title) : wxString()); +} + +void warning_catcher(wxWindow* parent, const wxString& message) +{ + MessageDialog msg(parent, message, _L("Warning"), wxOK | wxICON_WARNING); + msg.ShowModal(); +} + +static wxString bold(const wxString& str) +{ + return wxString::Format("%s", str); +}; + +static wxString bold_string(const wxString& str) +{ + return wxString::Format("\"%s\"", str); +}; + +static void add_config_substitutions(const ConfigSubstitutions& conf_substitutions, wxString& changes) +{ + changes += ""; + for (const ConfigSubstitution& conf_substitution : conf_substitutions) { + wxString new_val; + const ConfigOptionDef* def = conf_substitution.opt_def; + if (!def) + continue; + switch (def->type) { + case coEnum: + { + const std::vector& labels = def->enum_labels; + const std::vector& values = def->enum_values; + int val = conf_substitution.new_value->getInt(); + + bool is_infill = def->opt_key == "top_surface_pattern" || + def->opt_key == "bottom_surface_pattern" || + def->opt_key == "internal_solid_infill_pattern" || + def->opt_key == "sparse_infill_pattern"; + + // Each infill doesn't use all list of infill declared in PrintConfig.hpp. + // So we should "convert" val to the correct one + if (is_infill) { + for (const auto& key_val : *def->enum_keys_map) + if ((int)key_val.second == val) { + auto it = std::find(values.begin(), values.end(), key_val.first); + if (it == values.end()) + break; + auto idx = it - values.begin(); + new_val = wxString("\"") + values[idx] + "\"" + " (" + from_u8(_utf8(labels[idx])) + ")"; + break; + } + if (new_val.IsEmpty()) { + assert(false); + new_val = _L("Undefined"); + } + } + else + new_val = wxString("\"") + values[val] + "\"" + " (" + from_u8(_utf8(labels[val])) + ")"; + break; + } + case coBool: + new_val = conf_substitution.new_value->getBool() ? "true" : "false"; + break; + case coBools: + if (conf_substitution.new_value->nullable()) + for (const char v : static_cast(conf_substitution.new_value.get())->values) + new_val += std::string(v == ConfigOptionBoolsNullable::nil_value() ? "nil" : v ? "true" : "false") + ", "; + else + for (const char v : static_cast(conf_substitution.new_value.get())->values) + new_val += std::string(v ? "true" : "false") + ", "; + if (! new_val.empty()) + new_val.erase(new_val.begin() + new_val.size() - 2, new_val.end()); + break; + default: + assert(false); + } + + changes += format_wxstr(""; + } + changes += "
\"%1%\" (%2%): ", def->opt_key, _(def->label)) + + format_wxstr(_L("%1% was replaced with %2%"), bold_string(conf_substitution.old_value), bold(new_val)) + + "
"; +} + +static wxString substitution_message(const wxString& changes) +{ + return + _L("The configuration may be generated by a newer version of BambuStudio.") + " " + + _L("Some values have been replaced. Please check them:") + "\n" + changes + "\n"; +} + +void show_substitutions_info(const PresetsConfigSubstitutions& presets_config_substitutions) +{ + wxString changes; + + auto preset_type_name = [](Preset::Type type) { + switch (type) { + case Preset::TYPE_PRINT: return _L("Process"); + // BBS: remove TYPE_SLA_PRINT + case Preset::TYPE_FILAMENT: return _L("Filament"); + // BBS: remove TYPE_SLA_MATERIAL + case Preset::TYPE_PRINTER: return _L("Machine"); + // BBS: remove TYPE_PHYSICAL_PRINTER + case Preset::TYPE_CONFIG: return _L("Configuration"); + default: assert(false); return wxString(); + } + }; + + for (const PresetConfigSubstitutions& substitution : presets_config_substitutions) { + changes += "\n\n" + format_wxstr("%1% : %2%", preset_type_name(substitution.preset_type), bold_string(substitution.preset_name)); + if (!substitution.preset_file.empty()) + changes += format_wxstr(" (%1%)", substitution.preset_file); + + add_config_substitutions(substitution.substitutions, changes); + } + + InfoDialog msg(nullptr, _L("Configuration package was loaded, but some values were not recognized."), substitution_message(changes), true); + msg.ShowModal(); +} + +void show_substitutions_info(const ConfigSubstitutions& config_substitutions, const std::string& filename) +{ + wxString changes = "\n"; + add_config_substitutions(config_substitutions, changes); + + InfoDialog msg(nullptr, + format_wxstr(_L("Configuration file \"%1%\" was loaded, but some values were not recognized."), from_u8(filename)), + substitution_message(changes), true); + msg.ShowModal(); +} + +void create_combochecklist(wxComboCtrl* comboCtrl, const std::string& text, const std::string& items) +{ + if (comboCtrl == nullptr) + return; + wxGetApp().UpdateDarkUI(comboCtrl); + + wxCheckListBoxComboPopup* popup = new wxCheckListBoxComboPopup; + if (popup != nullptr) { + // FIXME If the following line is removed, the combo box popup list will not react to mouse clicks. + // On the other side, with this line the combo box popup cannot be closed by clicking on the combo button on Windows 10. + comboCtrl->UseAltPopupWindow(); + + int max_width = 0; + + // the following line messes up the popup size the first time it is shown on wxWidgets 3.1.3 +// comboCtrl->EnablePopupAnimation(false); +#ifdef _WIN32 + popup->SetFont(comboCtrl->GetFont()); +#endif // _WIN32 + comboCtrl->SetPopupControl(popup); + wxString title = from_u8(text); + max_width = std::max(max_width, 60 + comboCtrl->GetTextExtent(title).x); + popup->SetStringValue(title); + popup->Bind(wxEVT_CHECKLISTBOX, [popup](wxCommandEvent& evt) { popup->OnCheckListBox(evt); }); + popup->Bind(wxEVT_LISTBOX, [popup](wxCommandEvent& evt) { popup->OnListBoxSelection(evt); }); + popup->Bind(wxEVT_KEY_DOWN, [popup](wxKeyEvent& evt) { popup->OnKeyEvent(evt); }); + popup->Bind(wxEVT_KEY_UP, [popup](wxKeyEvent& evt) { popup->OnKeyEvent(evt); }); + + std::vector items_str; + boost::split(items_str, items, boost::is_any_of("|"), boost::token_compress_off); + + // each item must be composed by 2 parts + assert(items_str.size() %2 == 0); + + for (size_t i = 0; i < items_str.size(); i += 2) { + wxString label = from_u8(items_str[i]); + max_width = std::max(max_width, 60 + popup->GetTextExtent(label).x); + popup->Append(label); + popup->Check(i / 2, items_str[i + 1] == "1"); + } + + comboCtrl->SetMinClientSize(wxSize(max_width, -1)); + wxGetApp().UpdateDarkUI(popup); + } +} + +unsigned int combochecklist_get_flags(wxComboCtrl* comboCtrl) +{ + unsigned int flags = 0; + + wxCheckListBoxComboPopup* popup = wxDynamicCast(comboCtrl->GetPopupControl(), wxCheckListBoxComboPopup); + if (popup != nullptr) { + for (unsigned int i = 0; i < popup->GetCount(); ++i) { + if (popup->IsChecked(i)) + flags |= 1 << i; + } + } + + return flags; +} + +void combochecklist_set_flags(wxComboCtrl* comboCtrl, unsigned int flags) +{ + wxCheckListBoxComboPopup* popup = wxDynamicCast(comboCtrl->GetPopupControl(), wxCheckListBoxComboPopup); + if (popup != nullptr) { + for (unsigned int i = 0; i < popup->GetCount(); ++i) { + popup->Check(i, (flags & (1 << i)) != 0); + } + } +} + +AppConfig* get_app_config() +{ + return wxGetApp().app_config; +} + +wxString from_u8(const std::string &str) +{ + return wxString::FromUTF8(str.c_str()); +} + +std::string into_u8(const wxString &str) +{ + auto buffer_utf8 = str.utf8_str(); + return std::string(buffer_utf8.data()); +} + +wxString from_path(const boost::filesystem::path &path) +{ +#ifdef _WIN32 + return wxString(path.string()); +#else + return from_u8(path.string()); +#endif +} + +boost::filesystem::path into_path(const wxString &str) +{ + return boost::filesystem::path(str.wx_str()); +} + +void about() +{ + AboutDialog dlg; + dlg.ShowModal(); +} + +void login() +{ + //LoginDialog dlg; + //dlg.ShowModal(); + + ZUserLogin dlg; + dlg.run(); +} + +void desktop_open_datadir_folder() +{ + // Execute command to open a file explorer, platform dependent. + // FIXME: The const_casts aren't needed in wxWidgets 3.1, remove them when we upgrade. + + const auto path = data_dir(); +#ifdef _WIN32 + const wxString widepath = from_u8(path); + const wchar_t *argv[] = { L"explorer", widepath.GetData(), nullptr }; + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr); +#elif __APPLE__ + const char *argv[] = { "open", path.data(), nullptr }; + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr); +#else + const char *argv[] = { "xdg-open", path.data(), nullptr }; + + // Check if we're running in an AppImage container, if so, we need to remove AppImage's env vars, + // because they may mess up the environment expected by the file manager. + // Mostly this is about LD_LIBRARY_PATH, but we remove a few more too for good measure. + if (wxGetEnv("APPIMAGE", nullptr)) { + // We're running from AppImage + wxEnvVariableHashMap env_vars; + wxGetEnvMap(&env_vars); + + env_vars.erase("APPIMAGE"); + env_vars.erase("APPDIR"); + env_vars.erase("LD_LIBRARY_PATH"); + env_vars.erase("LD_PRELOAD"); + env_vars.erase("UNION_PRELOAD"); + + wxExecuteEnv exec_env; + exec_env.env = std::move(env_vars); + + wxString owd; + if (wxGetEnv("OWD", &owd)) { + // This is the original work directory from which the AppImage image was run, + // set it as CWD for the child process: + exec_env.cwd = std::move(owd); + } + + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, &exec_env); + } else { + // Looks like we're NOT running from AppImage, we'll make no changes to the environment. + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, nullptr); + } +#endif +} + +void desktop_open_any_folder( const std::string path ) +{ + // Execute command to open a file explorer, platform dependent. + // FIXME: The const_casts aren't needed in wxWidgets 3.1, remove them when we upgrade. + +#ifdef _WIN32 + const wxString widepath = from_u8(path); + ::wxExecute(L"explorer /select," + widepath, wxEXEC_ASYNC, nullptr); +#elif __APPLE__ + openFolderForFile(from_u8(path)); +#else + const char *argv[] = {"nautilus", path.data(), nullptr}; + + // Check if we're running in an AppImage container, if so, we need to remove AppImage's env vars, + // because they may mess up the environment expected by the file manager. + // Mostly this is about LD_LIBRARY_PATH, but we remove a few more too for good measure. + if (wxGetEnv("APPIMAGE", nullptr)) { + // We're running from AppImage + wxEnvVariableHashMap env_vars; + wxGetEnvMap(&env_vars); + + env_vars.erase("APPIMAGE"); + env_vars.erase("APPDIR"); + env_vars.erase("LD_LIBRARY_PATH"); + env_vars.erase("LD_PRELOAD"); + env_vars.erase("UNION_PRELOAD"); + + wxExecuteEnv exec_env; + exec_env.env = std::move(env_vars); + + wxString owd; + if (wxGetEnv("OWD", &owd)) { + // This is the original work directory from which the AppImage image was run, + // set it as CWD for the child process: + exec_env.cwd = std::move(owd); + } + + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, &exec_env); + } else { + // Looks like we're NOT running from AppImage, we'll make no changes to the environment. + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, nullptr); + } +#endif +} + + +} } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp new file mode 100644 index 000000000..f10a6a072 --- /dev/null +++ b/src/slic3r/GUI/GUI_App.cpp @@ -0,0 +1,6999 @@ +#include "libslic3r/Technologies.hpp" +#include "GUI_App.hpp" +#include "GUI_Init.hpp" +#include "GUI_ObjectList.hpp" +#include "GUI_Factories.hpp" +#include "slic3r/GUI/UserManager.hpp" +#include "slic3r/GUI/TaskManager.hpp" +#include "format.hpp" + +// Localization headers: include libslic3r version first so everything in this file +// uses the slic3r/GUI version (the macros will take precedence over the functions). +// Also, there is a check that the former is not included from slic3r module. +// This is the only place where we want to allow that, so define an override macro. +#define SLIC3R_ALLOW_LIBSLIC3R_I18N_IN_SLIC3R +#include "libslic3r/I18N.hpp" +#undef SLIC3R_ALLOW_LIBSLIC3R_I18N_IN_SLIC3R +#include "slic3r/GUI/I18N.hpp" + +#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 "libslic3r/Utils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/I18N.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Thread.hpp" +#include "libslic3r/miniz_extension.hpp" +#include "libslic3r/Utils.hpp" + +#include "GUI.hpp" +#include "GUI_Utils.hpp" +#include "3DScene.hpp" +#include "MainFrame.hpp" +#include "Plater.hpp" +#include "GLCanvas3D.hpp" + +#include "../Utils/PresetUpdater.hpp" +#include "../Utils/PrintHost.hpp" +#include "../Utils/Process.hpp" +#include "../Utils/MacDarkMode.hpp" +#include "../Utils/Http.hpp" +#include "../Utils/UndoRedo.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "Preferences.hpp" +#include "Tab.hpp" +#include "SysInfoDialog.hpp" +#include "UpdateDialogs.hpp" +#include "Mouse3DController.hpp" +#include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "UnsavedChangesDialog.hpp" +#include "SavePresetDialog.hpp" +#include "PrintHostDialogs.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "SendSystemInfoDialog.hpp" +#include "ParamsDialog.hpp" +#include "KBShortcutsDialog.hpp" +#include "DownloadProgressDialog.hpp" + +#include "BitmapCache.hpp" +#include "Notebook.hpp" +#include "Widgets/Label.hpp" +#include "Widgets/ProgressDialog.hpp" + +//BBS: DailyTip and UserGuide Dialog +#include "WebDownPluginDlg.hpp" +#include "WebGuideDialog.hpp" +#include "ReleaseNote.hpp" +#include "PrivacyUpdateDialog.hpp" +#include "ModelMall.hpp" +#include "HintNotification.hpp" + +//#ifdef WIN32 +//#include "BaseException.h" +//#endif + + +#ifdef __WXMSW__ +#include +#include + +#ifdef __WINDOWS__ +#ifdef _MSW_DARK_MODE +#include "dark_mode.hpp" +#include "wx/headerctrl.h" +#include "wx/msw/headerctrl.h" +#endif // _MSW_DARK_MODE +#endif // __WINDOWS__ + +#endif +#ifdef _WIN32 +#include +#endif + +#ifdef WIN32 +#include "BaseException.h" +#endif + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +#include +#include +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +// Needed for forcing menu icons back under gtk2 and gtk3 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + #include +#endif + +using namespace std::literals; +namespace pt = boost::property_tree; + +namespace Slic3r { +namespace GUI { + +class MainFrame; + +void start_ping_test() +{ + return; + wxArrayString output; + wxExecute("ping www.amazon.com", output, wxEXEC_NODISABLE); + + wxString output_i; + std::string output_temp; + + for (int i = 0; i < output.size(); i++) { + output_i = output[i].To8BitData(); + output_temp = output_i.ToStdString(wxConvUTF8); + BOOST_LOG_TRIVIAL(info) << "ping amazon:" << output_temp; + + } + wxExecute("ping www.apple.com", output, wxEXEC_NODISABLE); + for (int i = 0; i < output.size(); i++) { + output_i = output[i].To8BitData(); + output_temp = output_i.ToStdString(wxConvUTF8); + BOOST_LOG_TRIVIAL(info) << "ping www.apple.com:" << output_temp; + } + wxExecute("ping www.bambulab.com", output, wxEXEC_NODISABLE); + for (int i = 0; i < output.size(); i++) { + output_i = output[i].To8BitData(); + output_temp = output_i.ToStdString(wxConvUTF8); + BOOST_LOG_TRIVIAL(info) << "ping bambulab:" << output_temp; + } + //Get GateWay IP + wxExecute("ping 192.168.0.1", output, wxEXEC_NODISABLE); + for (int i = 0; i < output.size(); i++) { + output_i = output[i].To8BitData(); + output_temp = output_i.ToStdString(wxConvUTF8); + BOOST_LOG_TRIVIAL(info) << "ping 192.168.0.1:" << output_temp; + } +} + +std::string VersionInfo::convert_full_version(std::string short_version) +{ + std::string result = ""; + std::vector items; + boost::split(items, short_version, boost::is_any_of(".")); + if (items.size() == VERSION_LEN) { + for (int i = 0; i < VERSION_LEN; i++) { + std::stringstream ss; + ss << std::setw(2) << std::setfill('0') << items[i]; + result += ss.str(); + if (i != VERSION_LEN - 1) + result += "."; + } + return result; + } + return result; +} + +std::string VersionInfo::convert_short_version(std::string full_version) +{ + full_version.erase(std::remove(full_version.begin(), full_version.end(), '0'), full_version.end()); + return full_version; +} + +static std::string convert_studio_language_to_api(std::string lang_code) +{ + boost::replace_all(lang_code, "_", "-"); + return lang_code; + + /*if (lang_code == "zh_CN") + return "zh-hans"; + else if (lang_code == "zh_TW") + return "zh-hant"; + else + return "en";*/ +} + +#ifdef _WIN32 +bool is_associate_files(std::wstring extend) +{ + wchar_t app_path[MAX_PATH]; + ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); + + std::wstring prog_id = L" Bambu.Studio.1"; + std::wstring reg_base = L"Software\\Classes"; + std::wstring reg_extension = reg_base + L"\\." + extend; + + wchar_t szValueCurrent[1000]; + DWORD dwType; + DWORD dwSize = sizeof(szValueCurrent); + + int iRC = ::RegGetValueW(HKEY_CURRENT_USER, reg_extension.c_str(), nullptr, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize); + + bool bDidntExist = iRC == ERROR_FILE_NOT_FOUND; + + if (!bDidntExist && ::wcscmp(szValueCurrent, prog_id.c_str()) == 0) + return true; + + return false; +} +#endif + +class BBLSplashScreen : public wxSplashScreen +{ +public: + BBLSplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) + : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, +#ifdef __APPLE__ + wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP +#else + wxBORDER_NONE | wxFRAME_NO_TASKBAR +#endif // !__APPLE__ + ) + { + int init_dpi = get_dpi_for_window(this); + this->SetPosition(pos); + this->CenterOnScreen(); + int new_dpi = get_dpi_for_window(this); + + m_scale = (float)(new_dpi) / (float)(init_dpi); + + m_main_bitmap = bitmap; + + scale_bitmap(m_main_bitmap, m_scale); + + // init constant texts and scale fonts + m_constant_text.init(Label::Body_16); + scale_font(m_constant_text.title_font, 2.0f); + scale_font(m_constant_text.version_font, 1.2f); + + // this font will be used for the action string + m_action_font = m_constant_text.credits_font; + + // draw logo and constant info text + Decorate(m_main_bitmap); + wxGetApp().UpdateFrameDarkUI(this); + } + + void SetText(const wxString& text) + { + set_bitmap(m_main_bitmap); + if (!text.empty()) { + wxBitmap bitmap(m_main_bitmap); + + wxMemoryDC memDC; + memDC.SelectObject(bitmap); + memDC.SetFont(m_action_font); + memDC.SetTextForeground(StateColor::darkModeColorFor(wxColour(144, 144, 144))); + int width = bitmap.GetWidth(); + int text_height = memDC.GetTextExtent(text).GetHeight(); + int text_width = memDC.GetTextExtent(text).GetWidth(); + wxRect text_rect(wxPoint(0, m_action_line_y_position), wxPoint(width, m_action_line_y_position + text_height)); + memDC.DrawLabel(text, text_rect, wxALIGN_CENTER); + + memDC.SelectObject(wxNullBitmap); + set_bitmap(bitmap); +#ifdef __WXOSX__ + // without this code splash screen wouldn't be updated under OSX + wxYield(); +#endif + } + } + + void Decorate(wxBitmap& bmp) + { + if (!bmp.IsOk()) + return; + + // use a memory DC to draw directly onto the bitmap + wxMemoryDC memDc(bmp); + + int groundWidth = FromDIP(1000, nullptr); + int groundHeight = FromDIP(550, nullptr); + + wxImage groundImage(groundWidth, groundHeight); + for (int y = 0; y < groundHeight; ++y) { + for (int x = 0; x < groundWidth; ++x) { + groundImage.SetRGB(x, y, 255, 255, 255); + } + } + wxBitmap new_ground(groundImage); + /* wxMask* mask = new wxMask(new_ground, *wxWHITE); + wxMemoryDC memDC(mask->GetBitmap()); + wxBrush brush(*wxWHITE); + memDC.SetBrush(brush); + memDC.SetPen(*wxTRANSPARENT_PEN); + int radius = 20; + memDC.DrawRoundedRectangle(0, 0, groundWidth, groundHeight, radius); + new_ground.SetMask(mask);*/ + + memDc.DrawBitmap(new_ground, 0, 50, true); + + BitmapCache logo_cache; + + wxBitmap dalian_logo = *logo_cache.load_svg("dalian_logo", FromDIP(122 * m_scale), FromDIP(122 * m_scale)); + + memDc.DrawBitmap(dalian_logo, 50 , 100, true); + + int top_margin = FromDIP(75 * m_scale); + int width = bmp.GetWidth(); + + // draw title and version + int text_padding = FromDIP(3 * m_scale); + memDc.SetFont(m_constant_text.title_font); + int title_height = memDc.GetTextExtent(m_constant_text.title).GetHeight(); + int title_width = memDc.GetTextExtent(m_constant_text.title).GetWidth(); + memDc.SetFont(m_constant_text.version_font); + int version_height = memDc.GetTextExtent(m_constant_text.version).GetHeight(); + int version_width = memDc.GetTextExtent(m_constant_text.version).GetWidth(); + int split_width = (width + title_width - version_width) / 2; + wxRect title_rect(wxPoint(150, 300), wxPoint(400,450)); + memDc.SetTextForeground(StateColor::darkModeColorFor(wxColour(38, 46, 48))); + memDc.SetFont(m_constant_text.title_font); + //memDc.DrawLabel(m_constant_text.title, title_rect, wxALIGN_RIGHT | wxALIGN_BOTTOM); + memDc.DrawLabel(_L("Ui title"), title_rect, wxALIGN_CENTER_VERTICAL); + //BBS align bottom of title and version text + //wxRect version_rect(wxPoint(split_width + text_padding, top_margin), wxPoint(width, top_margin + title_height - text_padding)); + wxRect version_rect(wxPoint(50,550), wxPoint(300,550)); + memDc.SetFont(m_constant_text.version_font); + memDc.SetTextForeground(StateColor::darkModeColorFor(wxColor(134, 134, 134))); + memDc.DrawLabel(m_constant_text.version, version_rect, wxALIGN_LEFT | wxALIGN_BOTTOM); + + // draw title and version + //int text_padding = FromDIP(3 * m_scale); + //memDc.SetFont(m_constant_text.title_font); + //int title_height = memDc.GetTextExtent(m_constant_text.title).GetHeight(); + //int title_width = memDc.GetTextExtent(m_constant_text.title).GetWidth(); + //memDc.SetFont(m_constant_text.version_font); + //int version_height = memDc.GetTextExtent(m_constant_text.version).GetHeight(); + //int version_width = memDc.GetTextExtent(m_constant_text.version).GetWidth(); + //int split_width = (width + title_width - version_width) / 2; + //wxRect title_rect(wxPoint(0, top_margin), wxPoint(split_width - text_padding, top_margin + title_height)); + //memDc.SetTextForeground(StateColor::darkModeColorFor(wxColour(38, 46, 48))); + //memDc.SetFont(m_constant_text.title_font); + ////memDc.DrawLabel(m_constant_text.title, title_rect, wxALIGN_RIGHT | wxALIGN_BOTTOM); + //memDc.DrawLabel(_L("Ui title"), title_rect, wxALIGN_CENTER_VERTICAL); + ////BBS align bottom of title and version text + ////wxRect version_rect(wxPoint(split_width + text_padding, top_margin), wxPoint(width, top_margin + title_height - text_padding)); + //wxRect version_rect(wxPoint(split_width + text_padding, top_margin+40), wxPoint(width, top_margin + title_height - text_padding+40)); + //memDc.SetFont(m_constant_text.version_font); + //memDc.SetTextForeground(StateColor::darkModeColorFor(wxColor(134, 134, 134))); + //memDc.DrawLabel(m_constant_text.version, version_rect, wxALIGN_LEFT | wxALIGN_BOTTOM); + + //memDc.DrawLabel(m_constant_text.version, version_rect, wxALIGN_TOP | wxALIGN_LEFT); + +//#if BBL_INTERNAL_TESTING +// wxString versionText = BBL_INTERNAL_TESTING == 1 ? _L("Internal Version") : _L("Beta Version"); +// wxSize text_rect = memDc.GetTextExtent(versionText); +// int start_x = (title_rect.GetLeft() + version_rect.GetRight()) / 2 - text_rect.GetWidth(); +// int start_y = version_rect.GetBottom() + 10; +// wxRect internal_sign_rect(wxPoint(start_x, start_y), wxSize(text_rect)); +// memDc.SetFont(m_constant_text.title_font); +// memDc.DrawLabel(versionText, internal_sign_rect, wxALIGN_TOP | wxALIGN_LEFT); +//#endif + // load bitmap for logo + BitmapCache bmp_cache; + int logo_margin = FromDIP(72 * m_scale); + int logo_size = FromDIP(122 * m_scale); + int logo_width = FromDIP(94 * m_scale); + wxBitmap logo_bmp = *bmp_cache.load_svg("splash_logo", logo_size, logo_size); + int logo_y = top_margin + title_rect.GetHeight() + logo_margin; + memDc.DrawBitmap(logo_bmp, 900, -100, true); + + // calculate position for the dynamic text + int text_margin = FromDIP(80 * m_scale); + m_action_line_y_position = logo_y + logo_size + text_margin; + } + + static wxBitmap MakeBitmap() + { + int width = FromDIP(1000, nullptr); + int height = FromDIP(550, nullptr); + + /*wxImage image(width, height); + wxBitmap new_bmp(image); + + wxMemoryDC memDC; + memDC.SelectObject(new_bmp); + memDC.SetBrush(StateColor::darkModeColorFor(wxColor(0, 157, 244,128))); + memDC.DrawRectangle(-1, -1, width + 2, height + 2); + memDC.DrawBitmap(new_bmp, 0, 0, true);*/ + // StateColor::darkModeColorFor(wxColor(0, 157, 244) + //fangfa1 + /* wxImage image(width, height, true); + unsigned char* alpha = image.GetAlpha(); + if (alpha) { + for (int i = 0; i < width * height; ++i) { + alpha[i] = 0; + } + } + wxBitmap new_bmp(image); + + wxMemoryDC memDC; + memDC.SelectObject(new_bmp); + wxBrush brush(wxColor(254, 254, 254, 128)); + memDC.SetBrush(brush); + memDC.DrawRectangle(-1, -1, width + 2, height + 2); + memDC.DrawBitmap(new_bmp, 0, 0, true);*/ + + //fangfa2 + //wxBitmap new_bmp(width, height, 32); + //wxMemoryDC memDC; + //memDC.SelectObject(new_bmp); + //memDC.SetBackground(*wxTRANSPARENT_BRUSH); + //memDC.Clear(); + //wxGraphicsContext* gc = wxGraphicsContext::Create(memDC); + //if (gc) { + // gc->SetAntialiasMode(wxANTIALIAS_DEFAULT); + // //wxColour semiTransparent(255, 255, 255, 128); + // //gc->SetBrush(semiTransparent); + // gc->DrawRectangle(0, 0, width, height); + // delete gc; + //} + //fangfa3 + wxBitmap bitmap(width, height,32); + { + wxMemoryDC dc(bitmap); + dc.SetBackground(*wxTRANSPARENT_BRUSH); + dc.Clear(); + } + { + wxMemoryDC dc(bitmap); + dc.SetBackground(*wxTRANSPARENT_BRUSH); + dc.Clear(); + dc.SetPen(wxPen(wxColour(255, 0, 0, 128))); + dc.SetBrush(wxBrush(wxColour(255, 0, 0, 128))); + dc.DrawRectangle(0, 0, width, height); + } + + /* wxBitmap mask(100, 100); + wxMemoryDC maskDC(mask); + maskDC.SetBackground(*wxWHITE_BRUSH); + maskDC.Clear(); + maskDC.SetPen(wxPen(wxColour(0, 0, 0))); + maskDC.SetBrush(wxBrush(wxColour(0, 0, 0))); + maskDC.DrawRectangle(0, 0, width, height); + + bitmap.SetMask(new wxMask(mask, wxColour(0, 0, 0)));*/ + + return bitmap; + } + + void set_bitmap(wxBitmap& bmp) + { + m_window->SetBitmap(bmp); + m_window->Refresh(); + m_window->Update(); + } + + void scale_bitmap(wxBitmap& bmp, float scale) + { + if (scale == 1.0) + return; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) + return; + + int width = int(scale * image.GetWidth()); + int height = int(scale * image.GetHeight()); + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + bmp = wxBitmap(std::move(image)); + } + + void scale_font(wxFont& font, float scale) + { +#ifdef __WXMSW__ + // Workaround for the font scaling in respect to the current active display, + // not for the primary display, as it's implemented in Font.cpp + // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp + // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) + wxNativeFontInfo nfi= *font.GetNativeFontInfo(); + float pointSizeNew = scale * font.GetPointSize(); + nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); + nfi.pointSize = pointSizeNew; + font = wxFont(nfi); +#else + font.Scale(scale); +#endif //__WXMSW__ + } + + +private: + wxStaticText* m_staticText_slicer_name; + wxStaticText* m_staticText_slicer_version; + wxStaticBitmap* m_bitmap; + wxStaticText* m_staticText_loading; + + wxBitmap m_main_bitmap; + wxFont m_action_font; + int m_action_line_y_position; + float m_scale {1.0}; + + struct ConstantText + { + wxString title; + wxString version; + wxString credits; + + wxFont title_font; + wxFont version_font; + wxFont credits_font; + + void init(wxFont init_font) + { + // title + title = wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME; + + // dynamically get the version to display + version = _L("V") + " " + GUI_App::format_display_version(); + + // credits infornation + credits = ""; + + title_font = Label::Head_16; + version_font = Label::Body_16; + credits_font = init_font; + } + } + m_constant_text; +}; + +class SplashScreen : public wxSplashScreen +{ +public: + SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) + : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, +#ifdef __APPLE__ + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP +#else + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR +#endif // !__APPLE__ + ) + { + wxASSERT(bitmap.IsOk()); + + int init_dpi = get_dpi_for_window(this); + this->SetPosition(pos); + this->CenterOnScreen(); + int new_dpi = get_dpi_for_window(this); + + m_scale = (float)(new_dpi) / (float)(init_dpi); + + m_main_bitmap = bitmap; + + scale_bitmap(m_main_bitmap, m_scale); + + // init constant texts and scale fonts + init_constant_text(); + + // this font will be used for the action string + m_action_font = m_constant_text.credits_font.Bold(); + + // draw logo and constant info text + Decorate(m_main_bitmap); + } + + void SetText(const wxString& text) + { + set_bitmap(m_main_bitmap); + if (!text.empty()) { + wxBitmap bitmap(m_main_bitmap); + + wxMemoryDC memDC; + memDC.SelectObject(bitmap); + + memDC.SetFont(m_action_font); + memDC.SetTextForeground(wxColour(237, 107, 33)); + memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); + + memDC.SelectObject(wxNullBitmap); + set_bitmap(bitmap); +#ifdef __WXOSX__ + // without this code splash screen wouldn't be updated under OSX + wxYield(); +#endif + } + } + + static wxBitmap MakeBitmap(wxBitmap bmp) + { + if (!bmp.IsOk()) + return wxNullBitmap; + + // create dark grey background for the splashscreen + // It will be 5/3 of the weight of the bitmap + int width = lround((double)5 / 3 * bmp.GetWidth()); + int height = bmp.GetHeight(); + + wxImage image(width, height); + unsigned char* imgdata_ = image.GetData(); + for (int i = 0; i < width * height; ++i) { + *imgdata_++ = 51; + *imgdata_++ = 51; + *imgdata_++ = 51; + } + + wxBitmap new_bmp(image); + + wxMemoryDC memDC; + memDC.SelectObject(new_bmp); + memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); + + return new_bmp; + } + + void Decorate(wxBitmap& bmp) + { + if (!bmp.IsOk()) + return; + + // draw text to the box at the left of the splashscreen. + // this box will be 2/5 of the weight of the bitmap, and be at the left. + int width = lround(bmp.GetWidth() * 0.4); + + // load bitmap for logo + BitmapCache bmp_cache; + int logo_size = lround(width * 0.25); + wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); + + wxCoord margin = int(m_scale * 20); + + wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); + banner_rect.Deflate(margin, 2 * margin); + + // use a memory DC to draw directly onto the bitmap + wxMemoryDC memDc(bmp); + + // draw logo + memDc.DrawBitmap(logo_bmp, margin, margin, true); + + // draw the (white) labels inside of our black box (at the left of the splashscreen) + memDc.SetTextForeground(wxColour(255, 255, 255)); + + memDc.SetFont(m_constant_text.title_font); + memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + + int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); + banner_rect.SetTop(banner_rect.GetTop() + title_height); + banner_rect.SetHeight(banner_rect.GetHeight() - title_height); + + memDc.SetFont(m_constant_text.version_font); + memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); + + memDc.SetFont(m_constant_text.credits_font); + memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); + int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); + int text_height = memDc.GetTextExtent("text").GetY(); + + // calculate position for the dynamic text + int logo_and_header_height = margin + logo_size + title_height + version_height; + m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); + } + +private: + wxBitmap m_main_bitmap; + wxFont m_action_font; + int m_action_line_y_position; + float m_scale {1.0}; + + struct ConstantText + { + wxString title; + wxString version; + wxString credits; + + wxFont title_font; + wxFont version_font; + wxFont credits_font; + + void init(wxFont init_font) + { + // title + title = wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME; + + // dynamically get the version to display + auto version_text = GUI_App::format_display_version(); +#if BBL_INTERNAL_TESTING + version = _L("Internal Version") + " " + std::string(version_text); +#else + version = _L("Version") + " " + std::string(version_text); +#endif + + // credits infornation + credits = title; + + title_font = version_font = credits_font = init_font; + } + } + m_constant_text; + + void init_constant_text() + { + m_constant_text.init(get_default_font(this)); + + // As default we use a system font for current display. + // Scale fonts in respect to banner width + + int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins + + float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); + scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); + + float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); + scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); + + // The width of the credits information string doesn't respect to the banner width some times. + // So, scale credits_font in the respect to the longest string width + int longest_string_width = word_wrap_string(m_constant_text.credits); + float font_scale = (float)text_banner_width / longest_string_width; + scale_font(m_constant_text.credits_font, font_scale); + } + + void set_bitmap(wxBitmap& bmp) + { + m_window->SetBitmap(bmp); + m_window->Refresh(); + m_window->Update(); + } + + void scale_bitmap(wxBitmap& bmp, float scale) + { + if (scale == 1.0) + return; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) + return; + + int width = int(scale * image.GetWidth()); + int height = int(scale * image.GetHeight()); + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + bmp = wxBitmap(std::move(image)); + } + + void scale_font(wxFont& font, float scale) + { +#ifdef __WXMSW__ + // Workaround for the font scaling in respect to the current active display, + // not for the primary display, as it's implemented in Font.cpp + // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp + // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) + wxNativeFontInfo nfi= *font.GetNativeFontInfo(); + float pointSizeNew = scale * font.GetPointSize(); + nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); + nfi.pointSize = pointSizeNew; + font = wxFont(nfi); +#else + font.Scale(scale); +#endif //__WXMSW__ + } + + // wrap a string for the strings no longer then 55 symbols + // return extent of the longest string + int word_wrap_string(wxString& input) + { + size_t line_len = 55;// count of symbols in one line + int idx = -1; + size_t cur_len = 0; + + wxString longest_sub_string; + auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { + if (cur_len > longest_sub_str.Len()) + longest_sub_str = input.SubString(i - cur_len + 1, i); + }; + + for (size_t i = 0; i < input.Len(); i++) + { + cur_len++; + if (input[i] == ' ') + idx = i; + if (input[i] == '\n') + { + get_longest_sub_string(longest_sub_string, cur_len, i); + idx = -1; + cur_len = 0; + } + if (cur_len >= line_len && idx >= 0) + { + get_longest_sub_string(longest_sub_string, cur_len, i); + input[idx] = '\n'; + cur_len = i - static_cast(idx); + } + } + + return GetTextExtent(longest_sub_string).GetX(); + } +}; + + +#ifdef __linux__ +bool static check_old_linux_datadir(const wxString& app_name) { + // If we are on Linux and the datadir does not exist yet, look into the old + // location where the datadir was before version 2.3. If we find it there, + // tell the user that he might wanna migrate to the new location. + // (https://github.com/prusa3d/PrusaSlicer/issues/2911) + // To be precise, the datadir should exist, it is created when single instance + // lock happens. Instead of checking for existence, check the contents. + + namespace fs = boost::filesystem; + + std::string new_path = Slic3r::data_dir(); + + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + std::string default_path = (dir + "/" + app_name).ToUTF8().data(); + + if (new_path != default_path) { + // This happens when the user specifies a custom --datadir. + // Do not show anything in that case. + return true; + } + + fs::path data_dir = fs::path(new_path); + if (! fs::is_directory(data_dir)) + return true; // This should not happen. + + int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); + + if (file_count <= 1) { // just cache dir with an instance lock + // BBS + } else { + // If the new directory exists, be silent. The user likely already saw the message. + } + return true; +} +#endif + +struct FileWildcards { + std::string_view title; + std::vector file_extensions; +}; + +static const FileWildcards file_wildcards_by_type[FT_SIZE] = { + /* FT_STEP */ { "STEP files"sv, { ".stp"sv, ".step"sv } }, + /* FT_STL */ { "STL files"sv, { ".stl"sv } }, + /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, + /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, + /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, + /* FT_GCODE_3MF */ {"Gcode 3MF files"sv, {".gcode.3mf"sv}}, + /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv } }, +#ifdef __APPLE__ + /* FT_MODEL */ + {"Supported files"sv, {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv}}, +#else + /* FT_MODEL */ + {"Supported files"sv, {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv}}, +#endif + /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv,".stl"sv}}, + /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, + + /* FT_INI */ { "INI files"sv, { ".ini"sv } }, + /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, + /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, + /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv } }, +}; + +// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. +// The function accepts a custom extension parameter. If the parameter is provided, the custom extension +// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips +// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). +wxString file_wildcards(FileType file_type, const std::string &custom_extension) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + std::string custom_ext_lower; + + if (! custom_extension.empty()) { + // Generate an extension into the title mask and into the list of extensions. + custom_ext_lower = boost::to_lower_copy(custom_extension); + const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); + if (custom_ext_lower == custom_extension) { + // Add a lower case version. + title = std::string("*") + custom_ext_lower; + mask = title; + // Add an upper case version. + mask += ";*"; + mask += custom_ext_upper; + } else if (custom_ext_upper == custom_extension) { + // Add an upper case version. + title = std::string("*") + custom_ext_upper; + mask = title; + // Add a lower case version. + mask += ";*"; + mask += custom_ext_lower; + } else { + // Add the mixed case version only. + title = std::string("*") + custom_extension; + mask = title; + } + } + + for (const std::string_view &ext : data.file_extensions) + // Only add an extension if it was not added first as the custom extension. + if (ext != custom_ext_lower) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); +} + +static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) +static void register_win32_dpi_event() +{ + enum { WM_DPICHANGED_ = 0x02e0 }; + + wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { + const int dpi = wParam & 0xffff; + const auto rect = reinterpret_cast(lParam); + const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); + + DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); + win->GetEventHandler()->AddPendingEvent(evt); + + return true; + }); +} +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + +static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + +static void register_win32_device_notification_event() +{ + wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; + switch (wParam) { + case DBT_DEVICEARRIVAL: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { +// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + case DBT_DEVICEREMOVECOMPLETE: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) +// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + default: + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + wchar_t sPath[MAX_PATH]; + if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { + struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); + if (! SHGetPathFromIDList(pidl, sPath)) { + BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; + return false; + } + } + switch (lParam) { + case SHCNE_MEDIAINSERTED: + { + //printf("SHCNE_MEDIAINSERTED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + break; + } + case SHCNE_MEDIAREMOVED: + { + //printf("SHCNE_MEDIAREMOVED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + break; + } + default: +// printf("Unknown\n"); + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); +// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { + if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { + RAWINPUT raw; + UINT rawSize = sizeof(RAWINPUT); + ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); + if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) + return true; + } + return false; + }); + + wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + COPYDATASTRUCT* copy_data_structure = { 0 }; + copy_data_structure = (COPYDATASTRUCT*)lParam; + if (copy_data_structure->dwData == 1) { + LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); + } + return true; + }); +} +#endif // WIN32 + +static void generic_exception_handle() +{ + // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage + // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 + // + // wxLogError typically goes around exception handling and display an error dialog some time + // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. + // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates + // errors if multiple have been collected and displays just one error message for all of them. + // Otherwise we would get multiple error messages for one missing png, for example. + // + // If a custom error message window (or some other solution) were to be used, it would be necessary + // to turn off wxLogError() usage in wx APIs, most notably in wxImage + // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a +/*#ifdef WIN32 + //LPEXCEPTION_POINTERS exception_pointers = nullptr; + __try { + throw; + } + __except (CBaseException::UnhandledExceptionFilter2(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER) { + //__except (exception_pointers = GetExceptionInformation(), EXCEPTION_EXECUTE_HANDLER) { + // if (exception_pointers) { + // CBaseException::UnhandledExceptionFilter(exception_pointers); + // } + // else + throw; + } +#else*/ + try { + throw; + } catch (const std::bad_alloc& ex) { + // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) + // and terminate the app so it is at least certain to happen now. + BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); + flush_logs(); + wxString errmsg = wxString::Format(_L("BambuStudio will terminate because of running out of memory." + "It may be a bug. It will be appreciated if you report the issue to our team.")); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); + + std::terminate(); + //throw; + } catch (const boost::io::bad_format_string& ex) { + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + flush_logs(); + wxString errmsg = _L("BambuStudio will terminate because of a localization error. " + "It will be appreciated if you report the specific scenario this issue happened."); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); + std::terminate(); + //throw; + } catch (const std::exception& ex) { + wxLogError(format_wxstr(_L("BambuStudio got an unhandled exception: %1%"), ex.what())); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + flush_logs(); + throw; + } +//#endif +} + +std::vector GUI_App::split_str(std::string src, std::string separator) +{ + std::string::size_type pos; + std::vector result; + src += separator; + int size = src.size(); + + for (int i = 0; i < size; i++) + { + pos = src.find(separator, i); + if (pos < size) + { + std::string s = src.substr(i, pos - i); + result.push_back(s); + i = pos + separator.size() - 1; + } + } + return result; +} + +void GUI_App::post_init() +{ + assert(initialized()); + if (! this->initialized()) + throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); + + if (app_config->get("sync_user_preset") == "true") { + // BBS loading user preset + // Always async, not such startup step + // BOOST_LOG_TRIVIAL(info) << "Loading user presets..."; + // scrn->SetText(_L("Loading user presets...")); + if (m_agent) { start_sync_user_preset(); } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: true"; + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: false"; + } + + m_open_method = "double_click"; + bool switch_to_3d = false; + if (!this->init_params->input_files.empty()) { + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", init with input files, size %1%, input_gcode %2%") + %this->init_params->input_files.size() %this->init_params->input_gcode; + + if (this->init_params->input_files.size() == 1 && + boost::starts_with(this->init_params->input_files.front(), "bambustudio://open")) { + + std::string download_params_url = url_decode(this->init_params->input_files.front()); + auto input_str_arr = split_str(download_params_url, "file="); + + + std::string download_url; +#if BBL_RELEASE_TO_PUBLIC + for (auto input_str : input_str_arr) { + if (boost::starts_with(input_str, "http://makerworld") || + boost::starts_with(input_str, "https://makerworld") || + boost::starts_with(input_str, "http://public-cdn.bblmw.com") || + boost::starts_with(input_str, "https://public-cdn.bblmw.com") || + boost::algorithm::contains(input_str, "amazonaws.com") || + boost::algorithm::contains(input_str, "aliyuncs.com")) { + download_url = input_str; + } + } +#else + for (auto input_str : input_str_arr) { + download_url = input_str; + } +#endif + try + { + //filter relative directories + std::regex pattern("\\.\\.[\\/\\\\]|\\.\\.[\\/\\\\][\\/\\\\]|\\.\\/[\\/\\\\]|\\.[\\/\\\\]"); + download_url = std::regex_replace(download_url, pattern, ""); + } + catch (...){} + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", download_url %1%") % download_url; + + if (!download_url.empty()) { + m_download_file_url = from_u8(download_url); + } + + m_open_method = "makerworld"; + } + else { + switch_to_3d = true; + if (this->init_params->input_gcode) { + mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + plater_->select_view_3D("3D"); + this->plater()->load_gcode(from_u8(this->init_params->input_files.front())); + m_open_method = "gcode"; + } + else { + mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + plater_->select_view_3D("3D"); + wxArrayString input_files; + for (auto & file : this->init_params->input_files) { + input_files.push_back(wxString::FromUTF8(file)); + } + this->plater()->set_project_filename(_L("Untitled")); + this->plater()->load_files(input_files); + try { + if (!input_files.empty()) { + std::string file_path = input_files.front().ToStdString(); + std::filesystem::path path(file_path); + m_open_method = "file_" + path.extension().string(); + } + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", file path exception!"; + m_open_method = "file"; + } + } + } + } + +//#if BBL_HAS_FIRST_PAGE + bool slow_bootup = false; + if (app_config->get("slow_bootup") == "true") { + slow_bootup = true; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", slow bootup, won't render gl here."; + } + if (!switch_to_3d) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", begin load_gl_resources"; + mainframe->Freeze(); + plater_->canvas3D()->enable_render(false); + mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + plater_->select_view_3D("3D"); + //BBS init the opengl resource here +//#ifdef __linux__ + if (plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen()&&plater_->canvas3D()->make_current_for_postinit()) { +//#endif + Size canvas_size = plater_->canvas3D()->get_canvas_size(); + wxGetApp().imgui()->set_display_size(static_cast(canvas_size.get_width()), static_cast(canvas_size.get_height())); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", start to init opengl"; + wxGetApp().init_opengl(); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished init opengl"; + plater_->canvas3D()->init(); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished init canvas3D"; + wxGetApp().imgui()->new_frame(); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished init imgui frame"; + plater_->canvas3D()->enable_render(true); + + if (!slow_bootup) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", start to render a first frame for test"; + plater_->canvas3D()->render(false); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished rendering a first frame for test"; + } +//#ifdef __linux__ + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "Found glcontext not ready, postpone the init"; + } +//#endif + if (is_editor()) + mainframe->select_tab(size_t(0)); + mainframe->Thaw(); + plater_->trigger_restore_project(1); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", end load_gl_resources"; + } +//#endif + + //BBS: remove GCodeViewer as seperate APP logic + /*if (this->init_params->start_as_gcodeviewer) { + if (! this->init_params->input_files.empty()) + this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); + } + else + { + if (! this->init_params->preset_substitutions.empty()) + show_substitutions_info(this->init_params->preset_substitutions); + +#if 0 + // Load the cummulative config over the currently active profiles. + //FIXME if multiple configs are loaded, only the last one will have an effect. + // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). + // As of now only the full configs are supported here. + if (!m_print_config.empty()) + this->gui->mainframe->load_config(m_print_config); +#endif + if (! this->init_params->load_configs.empty()) + // Load the last config to give it a name at the UI. The name of the preset may be later + // changed by loading an AMF or 3MF. + //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. + this->mainframe->load_config_file(this->init_params->load_configs.back()); + // If loading a 3MF file, the config is loaded from the last one. + if (!this->init_params->input_files.empty()) { + const std::vector res = this->plater()->load_files(this->init_params->input_files); + if (!res.empty() && this->init_params->input_files.size() == 1) { + // Update application titlebar when opening a project file + const std::string& filename = this->init_params->input_files.front(); + //BBS: remove amf logic as project + if (boost::algorithm::iends_with(filename, ".3mf")) + this->plater()->set_project_filename(filename); + } + } + if (! this->init_params->extra_config.empty()) + this->mainframe->load_config(this->init_params->extra_config); + }*/ + + //BBS: check crash log + auto log_dir_path = boost::filesystem::path(data_dir()) / "log"; + if (boost::filesystem::exists(log_dir_path)) + { + boost::filesystem::directory_iterator end_iter; + for (boost::filesystem::directory_iterator iter(log_dir_path); iter != end_iter; ++iter) + { + std::string file_name = iter->path().stem().string(); + if (boost::starts_with(file_name, "crash")) { + if (file_name.find("done") == std::string::npos) { + std::ifstream ifs(iter->path().string(), ios::in); + std::stringstream data; + data << ifs.rdbuf(); + ifs.close(); + + NetworkAgent* agent = wxGetApp().getAgent(); + json j; + j["time"] = file_name.substr(file_name.find("crash") + strlen("crash") + 1); + j["verion"] = std::string(SLIC3R_VERSION); + j["content"] = decode_path(data.str().c_str()); + try { + if (agent) { + agent->track_event("studio_crash", j.dump()); + } + } + catch (...) {} + std::string new_file_name = file_name.append("_done"); + boost::filesystem::rename(iter->path(), iter->path().parent_path() / boost::filesystem::path(new_file_name + iter->path().extension().string())); + } + } + } + } + + if (m_networking_need_update) { + //updating networking + int ret = updating_bambu_networking(); + if (!ret) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<<":networking plugin updated successfully"; + //restart_networking(); + } + else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<<":networking plugin updated failed"; + } + } + + // The extra CallAfter() is needed because of Mac, where this is the only way + // to popup a modal dialog on start without screwing combo boxes. + // This is ugly but I honestly found no better way to do it. + // Neither wxShowEvent nor wxWindowCreateEvent work reliably. + if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. + BOOST_LOG_TRIVIAL(info) << "before check_updates"; + this->check_updates(false); + BOOST_LOG_TRIVIAL(info) << "after check_updates"; + CallAfter([this] { + bool cw_showed = this->config_wizard_startup(); + + std::string http_url = get_http_url(app_config->get_country_code()); + std::string language = GUI::into_u8(current_language_code()); + std::string network_ver = Slic3r::NetworkAgent::get_version(); + bool sys_preset = app_config->get("sync_system_preset") == "true"; + this->preset_updater->sync(http_url, language, network_ver, sys_preset ? preset_bundle : nullptr); + + //BBS: check new version + this->check_new_version(); + //BBS: check privacy version + if (is_user_login()) { + this->check_privacy_version(0); + + this->check_track_enable(); + } + }); + } + + if(!m_networking_need_update && m_agent) { + m_agent->set_on_ssdp_msg_fn( + [this](std::string json_str) { + if (m_is_closing) { + return; + } + GUI::wxGetApp().CallAfter([this, json_str] { + if (m_device_manager) { + m_device_manager->on_machine_alive(json_str); + } + }); + } + ); + m_agent->set_on_http_error_fn([this](unsigned int status, std::string body) { + this->handle_http_error(status, body); + }); + m_agent->start_discovery(true, false); + } + + //update the plugin tips + CallAfter([this] { + mainframe->refresh_plugin_tips(); + }); + + // update hms info + CallAfter([this] { + if (hms_query) + hms_query->check_hms_info(); + }); + + + DeviceManager::load_filaments_blacklist_config(); + + // remove old log files over LOG_FILES_MAX_NUM + std::string log_addr = data_dir(); + if (!log_addr.empty()) { + auto log_folder = boost::filesystem::path(log_addr) / "log"; + if (boost::filesystem::exists(log_folder)) { + std::vector> files_vec; + for (auto& it : boost::filesystem::directory_iterator(log_folder)) { + auto temp_path = it.path(); + try { + std::time_t lw_t = boost::filesystem::last_write_time(temp_path) ; + files_vec.push_back({ lw_t, temp_path.filename().string() }); + } catch (const std::exception &ex) { + } + } + std::sort(files_vec.begin(), files_vec.end(), []( + std::pair &a, std::pair &b) { + return a.first > b.first; + }); + + while (files_vec.size() > LOG_FILES_MAX_NUM) { + auto full_path = log_folder / boost::filesystem::path(files_vec[files_vec.size() - 1].second); + BOOST_LOG_TRIVIAL(info) << "delete log file over " << LOG_FILES_MAX_NUM << ", filename: "<< files_vec[files_vec.size() - 1].second; + try { + boost::filesystem::remove(full_path); + } + catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << "failed to delete log file: "<< files_vec[files_vec.size() - 1].second << ". Error: " << ex.what(); + } + files_vec.pop_back(); + } + } + } + BOOST_LOG_TRIVIAL(info) << "finished post_init"; +#ifdef _WIN32 + // Sets window property to mainframe so other instances can indentify it. + OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); +#endif //WIN32 +} + +wxDEFINE_EVENT(EVT_ENTER_FORCE_UPGRADE, wxCommandEvent); +wxDEFINE_EVENT(EVT_SHOW_NO_NEW_VERSION, wxCommandEvent); +wxDEFINE_EVENT(EVT_SHOW_DIALOG, wxCommandEvent); +wxDEFINE_EVENT(EVT_CONNECT_LAN_MODE_PRINT, wxCommandEvent); +IMPLEMENT_APP(GUI_App) + +//BBS: remove GCodeViewer as seperate APP logic +//GUI_App::GUI_App(EAppMode mode) +GUI_App::GUI_App() + : wxApp() + //, m_app_mode(mode) + , m_app_mode(EAppMode::Editor) + , m_em_unit(10) + , m_imgui(new ImGuiWrapper()) + , hms_query(new HMSQuery()) + , m_removable_drive_manager(std::make_unique()) + , m_other_instance_message_handler(std::make_unique()) +{ + //app config initializes early becasuse it is used in instance checking in BambuStudio.cpp + this->init_app_config(); + if (app_config) { + ::Label::initSysFont(app_config->get_language_code(), false); + } + this->init_download_path(); + + reset_to_active(); +} + +void GUI_App::shutdown() +{ + BOOST_LOG_TRIVIAL(info) << "GUI_App::shutdown enter"; + + if (m_removable_drive_manager) { + removable_drive_manager()->shutdown(); + } + + // destroy login dialog + if (login_dlg != nullptr) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": destroy login dialog"); + delete login_dlg; + login_dlg = nullptr; + } + + if (m_is_recreating_gui) return; + m_is_closing = true; + BOOST_LOG_TRIVIAL(info) << "GUI_App::shutdown exit"; +} + + +std::string GUI_App::get_http_url(std::string country_code, std::string path) +{ + std::string url; + if (country_code == "US") { + url = "https://api.bambulab.com/"; + } + else if (country_code == "CN") { + url = "https://api.bambulab.cn/"; + } + else if (country_code == "ENV_CN_DEV") { + url = "https://api-dev.bambu-lab.com/"; + } + else if (country_code == "ENV_CN_QA") { + url = "https://api-qa.bambu-lab.com/"; + } + else if (country_code == "ENV_CN_PRE") { + url = "https://api-pre.bambu-lab.com/"; + } + else if (country_code == "NEW_ENV_DEV_HOST") + { + url = "https://api-dev.bambulab.net/"; + } + else if (country_code == "NEW_ENV_QAT_HOST") + { + url = "https://api-qa.bambulab.net/"; + } + else if (country_code == "NEW_ENV_PRE_HOST") + { + url = "https://api-pre.bambulab.net/"; + } + else { + url = "https://api.bambulab.com/"; + } + + url += path.empty() ? "v1/iot-service/api/slicer/resource" : path; + return url; +} + +std::string GUI_App::get_model_http_url(std::string country_code) +{ + std::string url; + if (country_code == "US") { + url = "https://makerworld.com/"; + } + else if (country_code == "CN") { + url = "https://makerworld.com.cn/"; + } + else if (country_code == "ENV_CN_DEV") { + url = "https://makerhub-dev.bambu-lab.com/"; + } + else if (country_code == "ENV_CN_QA") { + url = "https://makerhub-qa.bambu-lab.com/"; + } + else if (country_code == "ENV_CN_PRE") { + url = "https://makerhub-pre.bambu-lab.com/"; + } + else if (country_code == "NEW_ENV_DEV_HOST") + { + url = "https://makerhub-dev.bambulab.net/"; + } + else if (country_code == "NEW_ENV_QAT_HOST") + { + url = "https://makerhub-qa.bambulab.net/"; + } + else if (country_code == "NEW_ENV_PRE_HOST") + { + url = "https://makerhub-pre.bambulab.net/"; + } + else { + url = "https://makerworld.com/"; + } + + return url; +} + + +std::string GUI_App::get_plugin_url(std::string name, std::string country_code) +{ + std::string url = get_http_url(country_code); + + std::string curr_version = SLIC3R_VERSION; + std::string using_version = curr_version.substr(0, 9) + "00"; + if (name == "cameratools") + using_version = curr_version.substr(0, 6) + "00.00"; + url += (boost::format("?slicer/%1%/cloud=%2%") % name % using_version).str(); + //url += (boost::format("?slicer/plugins/cloud=%1%") % "01.01.00.00").str(); + return url; +} + +static std::string decode(std::string const& extra, std::string const& path = {}) { + char const* p = extra.data(); + char const* e = p + extra.length(); + while (p + 4 < e) { + boost::uint16_t len = ((boost::uint16_t)p[2]) | ((boost::uint16_t)p[3] << 8); + if (p[0] == '\x75' && p[1] == '\x70' && len >= 5 && p + 4 + len < e && p[4] == '\x01') { + return std::string(p + 9, p + 4 + len); + } + else { + p += 4 + len; + } + } + return Slic3r::decode_path(path.c_str()); +} + +int GUI_App::download_plugin(std::string name, std::string package_name, InstallProgressFn pro_fn, WasCancelledFn cancel_fn) +{ + int result = 0; + json j; + std::string err_msg; + + // get country_code + AppConfig* app_config = wxGetApp().app_config; + if (!app_config) { + j["result"] = "failed"; + j["error_msg"] = "app_config is nullptr"; + if (m_agent) { + m_agent->track_event("networkplugin_download", j.dump()); + } + return -1; + } + + BOOST_LOG_TRIVIAL(info) << "[download_plugin]: enter"; + m_networking_cancel_update = false; + // get temp path + fs::path target_file_path = (fs::temp_directory_path() / package_name); + fs::path tmp_path = target_file_path; + tmp_path += format(".%1%%2%", get_current_pid(), ".tmp"); + + // get_url + std::string url = get_plugin_url(name, app_config->get_country_code()); + std::string download_url; + Slic3r::Http http_url = Slic3r::Http::get(url); + BOOST_LOG_TRIVIAL(info) << "[download_plugin]: check the plugin from " << url; + http_url.timeout_connect(TIMEOUT_CONNECT) + .timeout_max(TIMEOUT_RESPONSE) + .on_complete( + [&download_url](std::string body, unsigned status) { + try { + json j = json::parse(body); + std::string message = j["message"].get(); + + if (message == "success") { + json resource = j.at("resources"); + if (resource.is_array()) { + for (auto iter = resource.begin(); iter != resource.end(); iter++) { + Semver version; + std::string url; + std::string type; + std::string vendor; + std::string description; + for (auto sub_iter = iter.value().begin(); sub_iter != iter.value().end(); sub_iter++) { + if (boost::iequals(sub_iter.key(), "type")) { + type = sub_iter.value(); + BOOST_LOG_TRIVIAL(info) << "[download_plugin]: get version of settings's type, " << sub_iter.value(); + } + else if (boost::iequals(sub_iter.key(), "version")) { + version = *(Semver::parse(sub_iter.value())); + } + else if (boost::iequals(sub_iter.key(), "description")) { + description = sub_iter.value(); + } + else if (boost::iequals(sub_iter.key(), "url")) { + url = sub_iter.value(); + } + } + BOOST_LOG_TRIVIAL(info) << "[download_plugin 1]: get type " << type << ", version " << version.to_string() << ", url " << url; + download_url = url; + } + } + } + else { + BOOST_LOG_TRIVIAL(info) << "[download_plugin 1]: get version of plugin failed, body=" << body; + } + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "[download_plugin 1]: catch unknown exception"; + ; + } + }).on_error( + [&result, &err_msg](std::string body, std::string error, unsigned int status) { + BOOST_LOG_TRIVIAL(error) << "[download_plugin 1] on_error: " << error<<", body = " << body; + err_msg += "[download_plugin 1] on_error: " + error + ", body = " + body; + result = -1; + }).perform_sync(); + + bool cancel = false; + if (result < 0) { + j["result"] = "failed"; + j["error_msg"] = err_msg; + if (m_agent) { + m_agent->track_event("networkplugin_download", j.dump()); + } + if (pro_fn) pro_fn(InstallStatusDownloadFailed, 0, cancel); + return result; + } + + + if (download_url.empty()) { + BOOST_LOG_TRIVIAL(info) << "[download_plugin 1]: no available plugin found for this app version: " << SLIC3R_VERSION; + if (pro_fn) pro_fn(InstallStatusDownloadFailed, 0, cancel); + j["result"] = "failed"; + j["error_msg"] = "[download_plugin 1]: no available plugin found for this app version: " + std::string(SLIC3R_VERSION); + if (m_agent) { + m_agent->track_event("networkplugin_download", j.dump()); + } + return -1; + } + else if (pro_fn) { + pro_fn(InstallStatusNormal, 5, cancel); + } + + if (m_networking_cancel_update || cancel) { + BOOST_LOG_TRIVIAL(info) << boost::format("[download_plugin 1]: %1%, cancelled by user") % __LINE__; + j["result"] = "failed"; + j["error_msg"] = (boost::format("[download_plugin 1]: %1%, cancelled by user") % __LINE__).str(); + if (m_agent) { + m_agent->track_event("networkplugin_download", j.dump()); + } + return -1; + } + BOOST_LOG_TRIVIAL(info) << "[download_plugin] get_url = " << download_url; + + // download + Slic3r::Http http = Slic3r::Http::get(download_url); + int reported_percent = 0; + http.on_progress( + [this, &pro_fn, cancel_fn, &result, &reported_percent, &err_msg](Slic3r::Http::Progress progress, bool& cancel) { + int percent = 0; + if (progress.dltotal != 0) + percent = progress.dlnow * 50 / progress.dltotal; + bool was_cancel = false; + if (pro_fn && ((percent - reported_percent) >= 10)) { + pro_fn(InstallStatusNormal, percent, was_cancel); + reported_percent = percent; + BOOST_LOG_TRIVIAL(info) << "[download_plugin 2] progress: " << reported_percent; + } + cancel = m_networking_cancel_update || was_cancel; + if (cancel_fn) + if (cancel_fn()) + cancel = true; + + if (cancel) { + err_msg += "[download_plugin] cancel"; + result = -1; + } + }) + .on_complete([&pro_fn, tmp_path, target_file_path](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(info) << "[download_plugin 2] completed"; + bool cancel = false; + int percent = 0; + 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_file_path); + if (pro_fn) pro_fn(InstallStatusDownloadCompleted, 80, cancel); + }) + .on_error([&pro_fn, &result, &err_msg](std::string body, std::string error, unsigned int status) { + bool cancel = false; + if (pro_fn) pro_fn(InstallStatusDownloadFailed, 0, cancel); + BOOST_LOG_TRIVIAL(error) << "[download_plugin 2] on_error: " << error<<", body = " << body; + err_msg += "[download_plugin 2] on_error: " + error + ", body = " + body; + result = -1; + }); + http.perform_sync(); + j["result"] = result < 0 ? "failed" : "success"; + j["error_msg"] = err_msg; + if (m_agent) { + m_agent->track_event("networkplugin_download", j.dump()); + } + return result; +} + +int GUI_App::install_plugin(std::string name, std::string package_name, InstallProgressFn pro_fn, WasCancelledFn cancel_fn) +{ + bool cancel = false; + std::string target_file_path = (fs::temp_directory_path() / package_name).string(); + + BOOST_LOG_TRIVIAL(info) << "[install_plugin] enter"; + // get plugin folder + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto plugin_folder = data_dir_path / name; + //auto plugin_folder = boost::filesystem::path(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()) / "plugins"; + auto backup_folder = plugin_folder/"backup"; + if (!boost::filesystem::exists(plugin_folder)) { + BOOST_LOG_TRIVIAL(info) << "[install_plugin] will create directory "< 0) { + std::string dest_file; + if (stat.m_is_utf8) { + dest_file = stat.m_filename; + } + else { + std::string extra(1024, 0); + size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size()); + dest_file = decode(extra.substr(0, n), stat.m_filename); + } + auto dest_file_path = boost::filesystem::path(dest_file); + dest_file = dest_file_path.filename().string(); + auto dest_path = boost::filesystem::path(plugin_folder.string() + "/" + dest_file); + std::string dest_zip_file = encode_path(dest_path.string().c_str()); + try { + if (fs::exists(dest_path)) + fs::remove(dest_path); + mz_bool res = mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_zip_file.c_str(), 0); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", extract %1% from plugin zip %2%\n") % dest_file % stat.m_filename; + if (res == 0) { +#ifdef WIN32 + std::wstring new_dest_zip_file = boost::locale::conv::utf_to_utf(dest_path.generic_string()); + res = mz_zip_reader_extract_to_file_w(&archive, stat.m_file_index, new_dest_zip_file.c_str(), 0); +#endif + if (res == 0) { + mz_zip_error zip_error = mz_zip_get_last_error(&archive); + BOOST_LOG_TRIVIAL(error) << "[install_plugin]Archive read error:" << mz_zip_get_error_string(zip_error) << std::endl; + close_zip_reader(&archive); + if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); } + return InstallStatusUnzipFailed; + } + } + else { + if (pro_fn) { + pro_fn(InstallStatusNormal, 50 + i/num_entries, cancel); + } + try { + auto backup_path = boost::filesystem::path(backup_folder.string() + "/" + dest_file); + if (fs::exists(backup_path)) + fs::remove(backup_path); + std::string error_message; + CopyFileResult cfr = copy_file(dest_path.string(), backup_path.string(), error_message, false); + if (cfr != CopyFileResult::SUCCESS) { + BOOST_LOG_TRIVIAL(error) << "Copying to backup failed(" << cfr << "): " << error_message; + } + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Copying to backup failed: " << e.what(); + //continue + } + } + } + catch (const std::exception& e) + { + // ensure the zip archive is closed and rethrow the exception + close_zip_reader(&archive); + BOOST_LOG_TRIVIAL(error) << "[install_plugin]Archive read exception:"<set_str("app", "installed_networking", "1"); + BOOST_LOG_TRIVIAL(info) << "[install_plugin] success"; + return 0; +} + +void GUI_App::restart_networking() +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, mainframe %1%")%mainframe; + on_init_network(true); + if(m_agent) { + init_networking_callbacks(); + m_agent->set_on_ssdp_msg_fn( + [this](std::string json_str) { + if (m_is_closing) { + return; + } + GUI::wxGetApp().CallAfter([this, json_str] { + if (m_device_manager) { + m_device_manager->on_machine_alive(json_str); + } + }); + } + ); + m_agent->set_on_http_error_fn([this](unsigned int status, std::string body) { + this->handle_http_error(status, body); + }); + m_agent->start_discovery(true, false); + if (mainframe) + mainframe->refresh_plugin_tips(); + if (plater_) + plater_->get_notification_manager()->bbl_close_plugin_install_notification(); + + if (m_agent->is_user_login()) { + remove_user_presets(); + enable_user_preset_folder(true); + preset_bundle->load_user_presets(m_agent->get_user_id(), ForwardCompatibilitySubstitutionRule::Enable); + mainframe->update_side_preset_ui(); + } + + if (app_config->get("sync_user_preset") == "true") { + start_sync_user_preset(); + } + if (mainframe && this->app_config->get("staff_pick_switch") == "true") { + if (mainframe->m_webview) { mainframe->m_webview->SendDesignStaffpick(has_model_mall()); } + } + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" exit, m_agent=%1%")%m_agent; +} + +void GUI_App::remove_old_networking_plugins() +{ + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto plugin_folder = data_dir_path / "plugins"; + //auto plugin_folder = boost::filesystem::path(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()) / "plugins"; + if (boost::filesystem::exists(plugin_folder)) { + BOOST_LOG_TRIVIAL(info) << "[remove_old_networking_plugins] remove the directory "<set_on_user_login_fn([this](int online_login, bool login) { + // GUI::wxGetApp().request_user_handle(online_login); + // }); + + m_agent->set_on_server_connected_fn([this](int return_code, int reason_code) { + if (m_is_closing) { + return; + } + if (return_code == 5) { + GUI::wxGetApp().CallAfter([this] { + this->request_user_logout(); + MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK); + if (msg_dlg.ShowModal() == wxOK) { + return; + } + }); + return; + } + GUI::wxGetApp().CallAfter([this] { + if (m_is_closing) + return; + BOOST_LOG_TRIVIAL(trace) << "static: server connected"; + m_agent->set_user_selected_machine(m_agent->get_user_selected_machine()); + if (this->is_enable_multi_machine()) { + auto evt = new wxCommandEvent(EVT_UPDATE_MACHINE_LIST); + wxQueueEvent(this, evt); + } + m_agent->set_user_selected_machine(m_agent->get_user_selected_machine()); + //subscribe device + if (m_agent->is_user_login()) { + m_agent->start_device_subscribe(); + /* resubscribe the cache dev list */ + if (this->is_enable_multi_machine()) { + DeviceManager* dev = this->getDeviceManager(); + if (dev && !dev->subscribe_list_cache.empty()) { + dev->subscribe_device_list(dev->subscribe_list_cache); + } + } + } + }); + }); + + m_agent->set_on_printer_connected_fn([this](std::string dev_id) { + if (m_is_closing) { + return; + } + GUI::wxGetApp().CallAfter([this, dev_id] { + if (m_is_closing) + return; + bool tunnel = boost::algorithm::starts_with(dev_id, "tunnel/"); + /* request_pushing */ + MachineObject* obj = m_device_manager->get_my_machine(tunnel ? dev_id.substr(7) : dev_id); + if (obj) { +//#if !BBL_RELEASE_TO_PUBLIC && defined(__WINDOWS__) +// if (obj->is_tunnel_mqtt && !tunnel) +// boost::thread ping_thread = Slic3r::create_thread([] { +// start_ping_test(); +// }); +//#endif + obj->is_tunnel_mqtt = tunnel; + obj->command_request_push_all(true); + obj->command_get_version(); + obj->erase_user_access_code(); + obj->command_get_access_code(); + if (!is_enable_multi_machine()) { + GUI::wxGetApp().sidebar().load_ams_list(obj->dev_id, obj); + } + } + }); + }); + + m_agent->set_get_country_code_fn([this]() { + if (app_config) + return app_config->get_country_code(); + return std::string(); + } + ); + + m_agent->set_on_subscribe_failure_fn([this](std::string dev_id) { + CallAfter([this, dev_id] { + on_start_subscribe_again(dev_id); + }); + }); + + m_agent->set_on_local_connect_fn( + [this](int state, std::string dev_id, std::string msg) { + if (m_is_closing) { + return; + } + CallAfter([this, state, dev_id, msg] { + if (m_is_closing) { + return; + } + /* request_pushing */ + MachineObject* obj = m_device_manager->get_my_machine(dev_id); + wxCommandEvent event(EVT_CONNECT_LAN_MODE_PRINT); + + if (obj) { + + if (obj->is_lan_mode_printer()) { + if (state == ConnectStatus::ConnectStatusOk) { + obj->command_request_push_all(true); + obj->command_get_version(); + event.SetInt(0); + event.SetString(obj->dev_id); + GUI::wxGetApp().sidebar().load_ams_list(obj->dev_id, obj); + } else if (state == ConnectStatus::ConnectStatusFailed) { + obj->set_access_code(""); + obj->erase_user_access_code(); + m_device_manager->set_selected_machine("", true); + wxString text; + if (msg == "5") { + text = wxString::Format(_L("Incorrect password")); + wxGetApp().show_dialog(text); + } else { + text = wxString::Format(_L("Connect %s failed! [SN:%s, code=%s]"), from_u8(obj->dev_name), obj->dev_id, msg); + wxGetApp().show_dialog(text); + } + event.SetInt(-1); + } else if (state == ConnectStatus::ConnectStatusLost) { + obj->set_access_code(""); + obj->erase_user_access_code(); + m_device_manager->localMachineList.erase(obj->dev_id); + m_device_manager->set_selected_machine("", true); + event.SetInt(-1); + BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost"; + } else { + event.SetInt(-1); + BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state; + } + + obj->set_lan_mode_connection_state(false); + } + else { + if (state == ConnectStatus::ConnectStatusOk) { + event.SetInt(1); + event.SetString(obj->dev_id); + } + else if(msg == "5") { + event.SetInt(5); + event.SetString(obj->dev_id); + } + else { + event.SetInt(-2); + event.SetString(obj->dev_id); + } + } + } + if (wxGetApp().plater()->get_select_machine_dialog()) { + wxPostEvent(wxGetApp().plater()->get_select_machine_dialog(), event); + } + }); + } + ); + + auto message_arrive_fn = [this](std::string dev_id, std::string msg) { + if (m_is_closing) { + return; + } + CallAfter([this, dev_id, msg] { + if (m_is_closing) + return; + MachineObject* obj = this->m_device_manager->get_user_machine(dev_id); + if (obj) { + obj->is_ams_need_update = false; + + auto sel = this->m_device_manager->get_selected_machine(); + + if (sel && sel->dev_id == dev_id) { + obj->parse_json(msg); + } + else { + obj->parse_json(msg, true); + } + + + if (!this->is_enable_multi_machine()) { + if ((sel == obj || sel == nullptr) && obj->is_ams_need_update) { + GUI::wxGetApp().sidebar().load_ams_list(obj->dev_id, obj); + } + } + } + }); + }; + + m_agent->set_on_message_fn(message_arrive_fn); + + auto user_message_arrive_fn = [this](std::string user_id, std::string msg) { + if (m_is_closing) { + return; + } + CallAfter([this, user_id, msg] { + if (m_is_closing) + return; + + //check user + if (user_id == m_agent->get_user_id()) { + this->m_user_manager->parse_json(msg); + } + + }); + }; + + m_agent->set_on_user_message_fn(user_message_arrive_fn); + + + auto lan_message_arrive_fn = [this](std::string dev_id, std::string msg) { + if (m_is_closing) { + return; + } + CallAfter([this, dev_id, msg] { + if (m_is_closing) + return; + + MachineObject* obj = m_device_manager->get_my_machine(dev_id); + if (!obj || !obj->is_lan_mode_printer()) { + obj = m_device_manager->get_local_machine(dev_id); + } + + if (obj) { + obj->parse_json(msg, DeviceManager::key_field_only); + if (this->m_device_manager->get_selected_machine() == obj && obj->is_ams_need_update) { + GUI::wxGetApp().sidebar().load_ams_list(obj->dev_id, obj); + } + } + obj = m_device_manager->get_local_machine(dev_id); + if (obj) { + obj->parse_json(msg, DeviceManager::key_field_only); + } + }); + }; + m_agent->set_on_local_message_fn(lan_message_arrive_fn); + m_agent->set_queue_on_main_fn([this](std::function callback) { + CallAfter(callback); + }); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": exit, m_agent=%1%")%m_agent; +} + +GUI_App::~GUI_App() +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": enter"); + if (app_config != nullptr) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": destroy app_config"); + delete app_config; + } + + if (preset_bundle != nullptr) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": destroy preset_bundle"); + delete preset_bundle; + } + + if (preset_updater != nullptr) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": destroy preset updater"); + delete preset_updater; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": exit"); +} + +// If formatted for github, plaintext with OpenGL extensions enclosed into
. +// Otherwise HTML formatted for the system info dialog. +std::string GUI_App::get_gl_info(bool for_github) +{ + return OpenGLManager::get_gl_info().to_string(for_github); +} + +wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) +{ + return m_opengl_mgr.init_glcontext(canvas); +} + +bool GUI_App::init_opengl() +{ +#ifdef __linux__ + bool status = m_opengl_mgr.init_gl(); + m_opengl_initialized = true; + return status; +#else + return m_opengl_mgr.init_gl(); +#endif +} + +// gets path to PrusaSlicer.ini, returns semver from first line comment +static boost::optional parse_semver_from_ini(std::string path) +{ + std::ifstream stream(path); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string body = buffer.str(); + size_t start = body.find("BambuStudio "); + if (start == std::string::npos) + return boost::none; + body = body.substr(start + 12); + size_t end = body.find_first_of(" \n"); + if (end < body.size()) + body.resize(end); + return Semver::parse(body); +} + +void GUI_App::init_download_path() +{ + std::string down_path = app_config->get("download_path"); + + if (down_path.empty()) { + std::string user_down_path = wxStandardPaths::Get().GetUserDir(wxStandardPaths::Dir_Downloads).ToUTF8().data(); + app_config->set("download_path", user_down_path); + } + else { + fs::path dp(down_path); + if (!fs::exists(dp)) { + + std::string user_down_path = wxStandardPaths::Get().GetUserDir(wxStandardPaths::Dir_Downloads).ToUTF8().data(); + app_config->set("download_path", user_down_path); + } + } +} + +void GUI_App::init_app_config() +{ + // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. + SetAppName(SLIC3R_APP_KEY); +// SetAppName(SLIC3R_APP_KEY "-alpha"); +// SetAppName(SLIC3R_APP_KEY "-beta"); +// SetAppDisplayName(SLIC3R_APP_NAME); + + // Set the Slic3r data directory at the Slic3r XS module. + // Unix: ~/ .Slic3r + // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" + // Mac : "~/Library/Application Support/Slic3r" + + if (data_dir().empty()) { + #ifndef __linux__ + std::string data_dir = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); + #else + // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. + // https://github.com/prusa3d/PrusaSlicer/issues/2911 + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + std::string data_dir = (dir + "/" + GetAppName()).ToUTF8().data(); + #endif +#if BBL_INTERNAL_TESTING + data_dir += BBL_INTERNAL_TESTING == 1 ? "Internal" : "Beta"; +#endif + //BBS create folder if not exists + boost::filesystem::path data_dir_path(data_dir); + if (!boost::filesystem::exists(data_dir_path)) + boost::filesystem::create_directory(data_dir_path); + set_data_dir(data_dir); + // Change current dirtory of application + chdir(encode_path((data_dir + "/log").c_str()).c_str()); + } else { + m_datadir_redefined = true; + } + + // start log here + std::time_t t = std::time(0); + std::tm * now_time = std::localtime(&t); + std::stringstream buf; + buf << std::put_time(now_time, "debug_%a_%b_%d_%H_%M_%S_"); + buf << get_current_pid() << ".log"; + std::string log_filename = buf.str(); +#if !BBL_RELEASE_TO_PUBLIC + set_log_path_and_level(log_filename, 5); +#else + set_log_path_and_level(log_filename, 3); +#endif + + //BBS: remove GCodeViewer as seperate APP logic + if (!app_config) + app_config = new AppConfig(); + //app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); + + // load settings + m_app_conf_exists = app_config->exists(); + if (m_app_conf_exists) { + std::string error = app_config->load(); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + throw Slic3r::RuntimeError( + _u8L("BambuStudio configuration file may be corrupted and is not able to be parsed." + "Please delete the file and try again.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + // Save orig_version here, so its empty if no app_config existed before this run. + m_last_config_version = app_config->orig_version();//parse_semver_from_ini(app_config->config_path()); + } + else { +#ifdef _WIN32 + // update associate files from registry information + if (is_associate_files(L"3mf")) { + app_config->set("associate_3mf", "true"); + } + if (is_associate_files(L"stl")) { + app_config->set("associate_stl", "true"); + } + if (is_associate_files(L"step") && is_associate_files(L"stp")) { + app_config->set("associate_step", "true"); + } +#endif // _WIN32 + } +} + +// returns true if found newer version and user agreed to use it +bool GUI_App::check_older_app_config(Semver current_version, bool backup) +{ + //BBS: current no need these logic + return false; +} + +void GUI_App::copy_older_config() +{ + preset_bundle->copy_files(m_older_data_dir_path); +} + +std::map GUI_App::get_extra_header() +{ + std::map extra_headers; + extra_headers.insert(std::make_pair("X-BBL-Client-Type", "slicer")); + extra_headers.insert(std::make_pair("X-BBL-Client-Name", SLIC3R_APP_NAME)); + extra_headers.insert(std::make_pair("X-BBL-Client-Version", VersionInfo::convert_full_version(SLIC3R_VERSION))); +#if defined(__WINDOWS__) + extra_headers.insert(std::make_pair("X-BBL-OS-Type", "windows")); +#elif defined(__APPLE__) + extra_headers.insert(std::make_pair("X-BBL-OS-Type", "macos")); +#elif defined(__LINUX__) + extra_headers.insert(std::make_pair("X-BBL-OS-Type", "linux")); +#endif + int major = 0, minor = 0, micro = 0; + wxGetOsVersion(&major, &minor, µ); + std::string os_version = (boost::format("%1%.%2%.%3%") % major % minor % micro).str(); + extra_headers.insert(std::make_pair("X-BBL-OS-Version", os_version)); + if (app_config) + extra_headers.insert(std::make_pair("X-BBL-Device-ID", app_config->get("slicer_uuid"))); + extra_headers.insert(std::make_pair("X-BBL-Language", convert_studio_language_to_api(into_u8(current_language_code_safe())))); + return extra_headers; +} + +//BBS +void GUI_App::init_http_extra_header() +{ + std::map extra_headers = get_extra_header(); + + if (m_agent) + m_agent->set_extra_http_header(extra_headers); +} + +void GUI_App::update_http_extra_header() +{ + std::map extra_headers = get_extra_header(); + Slic3r::Http::set_extra_headers(extra_headers); + if (m_agent) + m_agent->set_extra_http_header(extra_headers); +} + +void GUI_App::on_start_subscribe_again(std::string dev_id) +{ + auto start_subscribe_timer = new wxTimer(this, wxID_ANY); + Bind(wxEVT_TIMER, [this, start_subscribe_timer, dev_id](auto& e) { + start_subscribe_timer->Stop(); + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) return; + MachineObject* obj = dev->get_selected_machine(); + if (!obj) return; + + if ( (dev_id == obj->dev_id) && obj->is_connecting() && obj->subscribe_counter > 0) { + obj->subscribe_counter--; + if(wxGetApp().getAgent()) wxGetApp().getAgent()->set_user_selected_machine(dev_id); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": dev_id=" << obj->dev_id; + } + }); + start_subscribe_timer->Start(4000, wxTIMER_ONE_SHOT); +} + +std::string GUI_App::get_local_models_path() +{ + std::string local_path = ""; + if (data_dir().empty()) { + return local_path; + } + + auto models_folder = (boost::filesystem::path(data_dir()) / "models"); + local_path = models_folder.string(); + + if (!fs::exists(models_folder)) { + if (!fs::create_directory(models_folder)) { + local_path = ""; + } + BOOST_LOG_TRIVIAL(info) << "create models folder:" << models_folder.string(); + } + return local_path; +} + +void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) +{ + BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; + m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); +} + +bool GUI_App::OnInit() +{ + try { + return on_init_inner(); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(fatal) << "OnInit Got Fatal error: " << e.what(); + generic_exception_handle(); + return false; + } +} + +int GUI_App::OnExit() +{ + stop_sync_user_preset(); + + if (m_device_manager) { + delete m_device_manager; + m_device_manager = nullptr; + } + + if (m_user_manager) { + delete m_user_manager; + m_user_manager = nullptr; + } + + if (m_agent) { + // BBS avoid a crash on mac platform +#ifdef __WINDOWS__ + m_agent->start_discovery(false, false); +#endif + delete m_agent; + m_agent = nullptr; + } + + return wxApp::OnExit(); +} + +class wxBoostLog : public wxLog +{ + void DoLogText(const wxString &msg) override { + + BOOST_LOG_TRIVIAL(warning) << msg.ToUTF8().data(); + } + ~wxBoostLog() override + { + // This is a hack. Prevent thread logs from going to wxGuiLog on app quit. + auto t = wxLog::SetActiveTarget(this); + wxLog::FlushActive(); + wxLog::SetActiveTarget(t); + } +}; + +bool GUI_App::on_init_inner() +{ + wxLog::SetActiveTarget(new wxBoostLog()); +#if BBL_RELEASE_TO_PUBLIC + wxLog::SetLogLevel(wxLOG_Message); +#endif + + // Set initialization of image handlers before any UI actions - See GH issue #7469 + wxInitAllImageHandlers(); +#ifdef NDEBUG + wxImage::SetDefaultLoadFlags(0); // ignore waring in release build +#endif + +#if defined(_WIN32) && ! defined(_WIN64) + // BBS: remove 32bit build prompt + // Win32 32bit build. +#endif // _WIN64 + + // Forcing back menu icons under gtk2 and gtk3. Solution is based on: + // https://docs.gtk.org/gtk3/class.Settings.html + // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb + // TODO: Find workaround for GTK4 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); +#endif + +#ifdef WIN32 + //BBS set crash log folder + CBaseException::set_log_folder(data_dir()); +#endif + + wxGetApp().Bind(wxEVT_QUERY_END_SESSION, [this](auto & e) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< "received wxEVT_QUERY_END_SESSION"; + if (mainframe) { + wxCloseEvent e2(wxEVT_CLOSE_WINDOW); + e2.SetCanVeto(true); + mainframe->GetEventHandler()->ProcessEvent(e2); + if (e2.GetVeto()) { + e.Veto(); + return; + } + } + for (auto d : dialogStack) + d->EndModal(wxID_ABORT); + }); + + // Verify resources path + const wxString resources_dir = from_u8(Slic3r::resources_dir()); + wxCHECK_MSG(wxDirExists(resources_dir), false, + wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); + +#ifdef __linux__ + if (! check_old_linux_datadir(GetAppName())) { + std::cerr << "Quitting, user chose to move their data to new location." << std::endl; + return false; + } +#endif + + BOOST_LOG_TRIVIAL(info) << boost::format("gui mode, Current BambuStudio Version %1%")%SLIC3R_VERSION; + // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. +// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); + // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible + // performance when working on high resolution multi-display setups. +// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); + +// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; + if (is_editor()) { + std::string msg = Slic3r::Http::tls_global_init(); + std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); + bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Slic3r::Http::tls_system_cert_store(); + + if (!msg.empty() && !ssl_accept) { + RichMessageDialog + dlg(nullptr, + wxString::Format(_L("%s\nDo you want to continue?"), msg), + "BambuStudio", wxICON_QUESTION | wxYES_NO); + dlg.ShowCheckBox(_L("Remember my choice")); + if (dlg.ShowModal() != wxID_YES) return false; + + app_config->set("tls_cert_store_accepted", + dlg.IsCheckBoxChecked() ? "yes" : "no"); + app_config->set("tls_accepted_cert_store_location", + dlg.IsCheckBoxChecked() ? Slic3r::Http::tls_system_cert_store() : ""); + } + } + + // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. + // Like here, before the show InfoDialog in check_older_app_config() + + // If load_language() fails, the application closes. + load_language(wxString(), true); +#ifdef _MSW_DARK_MODE + +#ifndef __WINDOWS__ + wxSystemAppearance app = wxSystemSettings::GetAppearance(); + GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0"); + GUI::wxGetApp().app_config->save(); +#endif // __APPLE__ + + + bool init_dark_color_mode = dark_mode(); + bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; +#ifdef __WINDOWS__ + NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); +#endif // __WINDOWS__ + +#endif + // initialize label colors and fonts + init_label_colours(); + init_fonts(); + wxGetApp().Update_dark_mode_flag(); + + +#ifdef _MSW_DARK_MODE + // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed + if (bool new_dark_color_mode = dark_mode(); + init_dark_color_mode != new_dark_color_mode) { + +#ifdef __WINDOWS__ + NppDarkMode::SetDarkMode(new_dark_color_mode); +#endif // __WINDOWS__ + + init_label_colours(); + //update_label_colours_from_appconfig(); + } + if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + init_sys_menu_enabled != new_sys_menu_enabled) +#ifdef __WINDOWS__ + NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); +#endif +#endif + + if (m_last_config_version) { + int last_major = m_last_config_version->maj(); + int last_minor = m_last_config_version->min(); + int last_patch = m_last_config_version->patch()/100; + std::string studio_ver = SLIC3R_VERSION; + int cur_major = atoi(studio_ver.substr(0,2).c_str()); + int cur_minor = atoi(studio_ver.substr(3,2).c_str()); + int cur_patch = atoi(studio_ver.substr(6,2).c_str()); + BOOST_LOG_TRIVIAL(info) << boost::format("last app version {%1%.%2%.%3%}, current version {%4%.%5%.%6%}") + %last_major%last_minor%last_patch%cur_major%cur_minor%cur_patch; + if ((last_major != cur_major) + ||(last_minor != cur_minor) + ||(last_patch != cur_patch)) { + remove_old_networking_plugins(); + } + } + + app_config->set("version", SLIC3R_VERSION); + app_config->save(); + + BBLSplashScreen * scrn = nullptr; + const bool show_splash_screen = true; + if (show_splash_screen) { + // make a bitmap with dark grey banner on the left side + //BBS make BBL splash screen bitmap + wxBitmap bmp = BBLSplashScreen::MakeBitmap(); + // Detect position (display) to show the splash screen + // Now this position is equal to the mainframe position + wxPoint splashscreen_pos = wxDefaultPosition; + if (app_config->has("window_mainframe")) { + auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); + if (metrics) + splashscreen_pos = metrics->get_rect().GetPosition(); + } + + BOOST_LOG_TRIVIAL(info) << "begin to show the splash screen..."; + //BBS use BBL splashScreen + scrn = new BBLSplashScreen(bmp, wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 10000, wxDefaultPosition); +#ifndef __linux__ + wxYield(); +#endif + scrn->SetText(_L("Loading configuration")+ dots); + } + + BOOST_LOG_TRIVIAL(info) << "loading systen presets..."; + preset_bundle = new PresetBundle(); + + // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory + // supplied as argument to --datadir; in that case we should still run the wizard + preset_bundle->setup_directories(); + + + if (m_init_app_config_from_older) + copy_older_config(); + + if (is_editor()) { +#ifdef __WXMSW__ + if (app_config->get("associate_3mf") == "true") + associate_files(L"3mf"); + if (app_config->get("associate_stl") == "true") + associate_files(L"stl"); + if (app_config->get("associate_step") == "true") { + associate_files(L"step"); + associate_files(L"stp"); + } + if (app_config->get("associate_gcode") == "true") + associate_files(L"gcode"); +#endif // __WXMSW__ + + preset_updater = new PresetUpdater(); + Bind(EVT_SLIC3R_VERSION_ONLINE, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr) { + // this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable); + //BBS show msg box to download new version + /* wxString tips = wxString::Format(_L("Click to download new version in default browser: %s"), version_info.version_str); + DownloadDialog dialog(this->mainframe, + tips, + _L("New version of Bambu Studio"), + false, + wxCENTER | wxICON_INFORMATION); + + + dialog.SetExtendedMessage(extmsg);*/ + std::string skip_version_str = this->app_config->get("app", "skip_version"); + bool skip_this_version = false; + if (!skip_version_str.empty()) { + BOOST_LOG_TRIVIAL(info) << "new version = " << version_info.version_str << ", skip version = " << skip_version_str; + if (version_info.version_str <= skip_version_str) { + skip_this_version = true; + } else { + app_config->set("skip_version", ""); + skip_this_version = false; + } + } + /*if (!skip_this_version + || evt.GetInt() != 0) { + UpdateVersionDialog dialog(this->mainframe); + wxString extmsg = wxString::FromUTF8(version_info.description); + dialog.update_version_info(extmsg, version_info.version_str); + //dialog.update_version_info(version_info.description); + if (evt.GetInt() != 0) { + dialog.m_button_skip_version->Hide(); + } + switch (dialog.ShowModal()) + { + case wxID_YES: + wxLaunchDefaultBrowser(version_info.url); + break; + case wxID_NO: + break; + default: + ; + } + }*/ + } + }); + + Bind(EVT_ENTER_FORCE_UPGRADE, [this](const wxCommandEvent& evt) { + wxString version_str = wxString::FromUTF8(this->app_config->get("upgrade", "version")); + wxString description_text = wxString::FromUTF8(this->app_config->get("upgrade", "description")); + std::string download_url = this->app_config->get("upgrade", "url"); + wxString tips = wxString::Format(_L("Click to download new version in default browser: %s"), version_str); + DownloadDialog dialog(this->mainframe, + tips, + _L("The Bambu Studio needs an upgrade"), + false, + wxCENTER | wxICON_INFORMATION); + dialog.SetExtendedMessage(description_text); + + int result = dialog.ShowModal(); + switch (result) + { + case wxID_YES: + wxLaunchDefaultBrowser(download_url); + break; + case wxID_NO: + wxGetApp().mainframe->Close(true); + break; + default: + wxGetApp().mainframe->Close(true); + } + }); + + Bind(EVT_SHOW_NO_NEW_VERSION, [this](const wxCommandEvent& evt) { + wxString msg = _L("This is the newest version."); + InfoDialog dlg(nullptr, _L("Info"), msg); + dlg.ShowModal(); + }); + + Bind(EVT_SHOW_DIALOG, [this](const wxCommandEvent& evt) { + wxString msg = evt.GetString(); + InfoDialog dlg(this->mainframe, _L("Info"), msg); + dlg.Bind(wxEVT_DESTROY, [this](auto& e) { + m_info_dialog_content = wxEmptyString; + }); + dlg.ShowModal(); + }); + } + else { +#ifdef __WXMSW__ + if (app_config->get("associate_gcode") == "true") + associate_files(L"gcode"); +#endif // __WXMSW__ + } + + // Suppress the '- default -' presets. + preset_bundle->set_default_suppressed(true); + + Bind(EVT_SET_SELECTED_MACHINE, &GUI_App::on_set_selected_machine, this); + Bind(EVT_UPDATE_MACHINE_LIST, &GUI_App::on_update_machine_list, this); + Bind(EVT_USER_LOGIN, &GUI_App::on_user_login, this); + Bind(EVT_USER_LOGIN_HANDLE, &GUI_App::on_user_login_handle, this); + Bind(EVT_CHECK_PRIVACY_VER, &GUI_App::on_check_privacy_update, this); + Bind(EVT_CHECK_PRIVACY_SHOW, &GUI_App::show_check_privacy_dlg, this); + + Bind(EVT_SHOW_IP_DIALOG, &GUI_App::show_ip_address_enter_dialog_handler, this); + + + std::map extra_headers = get_extra_header(); + Slic3r::Http::set_extra_headers(extra_headers); + + copy_network_if_available(); + on_init_network(); + + if (m_agent && m_agent->is_user_login()) { + enable_user_preset_folder(true); + } else { + enable_user_preset_folder(false); + } + + // BBS if load user preset failed + //if (loaded_preset_result != 0) { + try { + // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. + // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force + // installation of a compatible system preset, thus nullifying the system preset substitutions. + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); + } + catch (const std::exception& ex) { + show_error(nullptr, ex.what()); + } + //} + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) + register_win32_dpi_event(); +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + register_win32_device_notification_event(); +#endif // WIN32 + + // Let the libslic3r know the callback, which will translate messages on demand. + Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); + + BOOST_LOG_TRIVIAL(info) << "create the main window"; + mainframe = new MainFrame(); + // hide settings tabs after first Layout + if (is_editor()) { + mainframe->select_tab(size_t(0)); + } + + sidebar().obj_list()->init(); + //sidebar().aux_list()->init_auxiliary(); + //mainframe->m_auxiliary->init_auxiliary(); + +// update_mode(); // !!! do that later + SetTopWindow(mainframe); + + plater_->init_notification_manager(); + + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + + if (is_gcode_viewer()) { + mainframe->update_layout(); + if (plater_ != nullptr) + // ensure the selected technology is ptFFF + plater_->set_printer_technology(ptFFF); + } + else + load_current_presets(); + + if (plater_ != nullptr) { + plater_->reset_project_dirty_initial_presets(); + plater_->update_project_dirty_from_presets(); + } + + // BBS: +#ifdef __WINDOWS__ + mainframe->topbar()->SaveNormalRect(); +#endif + mainframe->Show(true); + BOOST_LOG_TRIVIAL(info) << "main frame firstly shown"; + +//#if BBL_HAS_FIRST_PAGE + //BBS: set tp3DEditor firstly + /*plater_->canvas3D()->enable_render(false); + mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + scrn->SetText(_L("Loading Opengl resourses...")); + plater_->select_view_3D("3D"); + //BBS init the opengl resource here + Size canvas_size = plater_->canvas3D()->get_canvas_size(); + wxGetApp().imgui()->set_display_size(static_cast(canvas_size.get_width()), static_cast(canvas_size.get_height())); + wxGetApp().init_opengl(); + plater_->canvas3D()->init(); + wxGetApp().imgui()->new_frame(); + plater_->canvas3D()->enable_render(true); + plater_->canvas3D()->render(); + if (is_editor()) + mainframe->select_tab(size_t(0));*/ +//#else + //plater_->trigger_restore_project(1); +//#endif + + obj_list()->set_min_height(); + + update_mode(); // update view mode after fix of the object_list size + +#ifdef __APPLE__ + other_instance_message_handler()->bring_instance_forward(); +#endif //__APPLE__ + + Bind(EVT_HTTP_ERROR, &GUI_App::on_http_error, this); + + + Bind(wxEVT_IDLE, [this](wxIdleEvent& event) + { + bool curr_studio_active = this->is_studio_active(); + if (m_studio_active != curr_studio_active) { + if (curr_studio_active) { + BOOST_LOG_TRIVIAL(info) << "studio is active, start to subscribe"; + if (m_agent) { + json j = json::object(); + m_agent->start_subscribe("app"); + m_agent->track_event("mqtt_active", j.dump()); + } + } else { + BOOST_LOG_TRIVIAL(info) << "studio is inactive, stop to subscribe"; + if (m_agent) { + json j = json::object(); + m_agent->stop_subscribe("app"); + m_agent->track_event("mqtt_inactive", j.dump()); + } + } + m_studio_active = curr_studio_active; + } + + + if (! plater_) + return; + + if (app_config->dirty()) + app_config->save(); + + // BBS + //this->obj_manipul()->update_if_dirty(); + + //use m_post_initialized instead + //static bool update_gui_after_init = true; + + // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT + // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. +//#ifdef __linux__ +// if (!m_post_initialized && m_opengl_initialized) { +//#else + if (!m_post_initialized && !m_adding_script_handler) { +//#endif + m_post_initialized = true; +#ifdef WIN32 + this->mainframe->register_win32_callbacks(); +#endif + this->post_init(); + + if (!m_download_file_url.empty()) { + request_model_download(m_download_file_url); + m_download_file_url = ""; + } + + update_publish_status(); + + } + }); + + m_initialized = true; + + flush_logs(); + + BOOST_LOG_TRIVIAL(info) << "finished the gui app init"; + //BBS: delete splash screen + delete scrn; + return true; +} + +void GUI_App::copy_network_if_available() +{ + if (app_config->get("update_network_plugin") != "true") + return; + std::string network_library, player_library, live555_library, network_library_dst, player_library_dst, live555_library_dst; + std::string data_dir_str = data_dir(); + boost::filesystem::path data_dir_path(data_dir_str); + auto plugin_folder = data_dir_path / "plugins"; + auto cache_folder = data_dir_path / "ota"; + std::string changelog_file = cache_folder.string() + "/network_plugins.json"; +#if defined(_MSC_VER) || defined(_WIN32) + network_library = cache_folder.string() + "/bambu_networking.dll"; + player_library = cache_folder.string() + "/BambuSource.dll"; + live555_library = cache_folder.string() + "/live555.dll"; + network_library_dst = plugin_folder.string() + "/bambu_networking.dll"; + player_library_dst = plugin_folder.string() + "/BambuSource.dll"; + live555_library_dst = plugin_folder.string() + "/live555.dll"; +#elif defined(__WXMAC__) + network_library = cache_folder.string() + "/libbambu_networking.dylib"; + player_library = cache_folder.string() + "/libBambuSource.dylib"; + live555_library = cache_folder.string() + "/liblive555.dylib"; + network_library_dst = plugin_folder.string() + "/libbambu_networking.dylib"; + player_library_dst = plugin_folder.string() + "/libBambuSource.dylib"; + live555_library_dst = plugin_folder.string() + "/liblive555.dylib"; +#else + network_library = cache_folder.string() + "/libbambu_networking.so"; + player_library = cache_folder.string() + "/libBambuSource.so"; + live555_library = cache_folder.string() + "/liblive555.so"; + network_library_dst = plugin_folder.string() + "/libbambu_networking.so"; + player_library_dst = plugin_folder.string() + "/libBambuSource.so"; + live555_library_dst = plugin_folder.string() + "/liblive555.so"; +#endif + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< ": checking network_library " << network_library << ", player_library " << player_library; + if (!boost::filesystem::exists(plugin_folder)) { + BOOST_LOG_TRIVIAL(info)<< __FUNCTION__ << ": create directory "<set("update_network_plugin", "false"); +} + +bool GUI_App::on_init_network(bool try_backup) +{ + int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(); + bool create_network_agent = false; +__retry: + if (!load_agent_dll) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok"; + if (check_networking_version()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version"; + auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry(); + if (!bambu_source) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!"; + m_networking_compatible = false; + if (app_config->get("installed_networking") == "1") { + m_networking_need_update = true; + } + } + else + create_network_agent = true; + } else { + if (try_backup) { + int result = Slic3r::NetworkAgent::unload_network_module(); + BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result; + load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true); + try_backup = false; + goto __retry; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module"; + if (app_config->get("installed_networking") == "1") { + m_networking_need_update = true; + } + } + } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed"; + if (app_config->get("installed_networking") == "1") { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module"; + m_networking_need_update = true; + } + } + + if (create_network_agent) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", create network agent..."); + //std::string data_dir = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); + std::string data_directory = data_dir(); + + m_agent = new Slic3r::NetworkAgent(data_directory); + + if (!m_device_manager) + m_device_manager = new Slic3r::DeviceManager(m_agent); + else + m_device_manager->set_agent(m_agent); + + if (!m_user_manager) + m_user_manager = new Slic3r::UserManager(m_agent); + else + m_user_manager->set_agent(m_agent); + + if (this->is_enable_multi_machine()) { + if (!m_task_manager) { + m_task_manager = new Slic3r::TaskManager(m_agent); + m_task_manager->start(); + } + m_agent->enable_multi_machine(true); + DeviceManager::EnableMultiMachine = true; + } else { + m_agent->enable_multi_machine(false); + DeviceManager::EnableMultiMachine = false; + } + + //BBS set config dir + if (m_agent) { + m_agent->set_config_dir(data_directory); + } + //BBS start http log + if (m_agent) { + m_agent->init_log(); + } + + //BBS set cert dir + if (m_agent) + m_agent->set_cert_file(resources_dir() + "/cert", "slicer_base64.cer"); + + init_http_extra_header(); + + if (m_agent) { + init_networking_callbacks(); + std::string country_code = app_config->get_country_code(); + m_agent->set_country_code(country_code); + m_agent->start(); + } + } + else { + int result = Slic3r::NetworkAgent::unload_network_module(); + BOOST_LOG_TRIVIAL(info) << "on_init_network, unload_network_module, result = " << result; + + if (!m_device_manager) + m_device_manager = new Slic3r::DeviceManager(); + + if (!m_user_manager) + m_user_manager = new Slic3r::UserManager(); + } + + return true; +} + +unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) +{ + double r = colour.Red(); + double g = colour.Green(); + double b = colour.Blue(); + + return std::round(std::sqrt( + r * r * .241 + + g * g * .691 + + b * b * .068 + )); +} + +bool GUI_App::dark_mode() +{ +#ifdef SUPPORT_DARK_MODE +#if __APPLE__ + // The check for dark mode returns false positive on 10.12 and 10.13, + // which allowed setting dark menu bar and dock area, which is + // is detected as dark mode. We must run on at least 10.14 where the + // proper dark mode was first introduced. + return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); +#else + return wxGetApp().app_config->get("dark_color_mode") == "1" ? true : check_dark_mode(); + //const unsigned luma = get_colour_approx_luma(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + //return luma < 128; +#endif +#else + //BBS disable DarkUI mode + return false; +#endif +} + +const wxColour GUI_App::get_label_default_clr_system() +{ + return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); +} + +const wxColour GUI_App::get_label_default_clr_modified() +{ + return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); +} + +void GUI_App::init_label_colours() +{ + bool is_dark_mode = dark_mode(); + m_color_label_modified = is_dark_mode ? wxColour("#F1754E") : wxColour("#F1754E"); + m_color_label_sys = is_dark_mode ? wxColour("#B2B3B5") : wxColour("#363636"); + +#ifdef _WIN32 + m_color_label_default = is_dark_mode ? wxColour(250, 250, 250) : m_color_label_sys; // wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); + m_color_hovered_btn_label = is_dark_mode ? wxColour(255, 255, 254) : wxColour(0,0,0); + m_color_default_btn_label = is_dark_mode ? wxColour(255, 255, 254): wxColour(0,0,0); + m_color_selected_btn_bg = is_dark_mode ? wxColour(84, 84, 91) : wxColour(206, 206, 206); +#else + m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); +#endif + m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + StateColor::SetDarkMode(is_dark_mode); +} + +void GUI_App::update_label_colours_from_appconfig() +{ + if (app_config->has("label_clr_sys")) { + auto str = app_config->get("label_clr_sys"); + if (str != "") + m_color_label_sys = wxColour(str); + } + + if (app_config->has("label_clr_modified")) { + auto str = app_config->get("label_clr_modified"); + if (str != "") + m_color_label_modified = wxColour(str); + } +} + +void GUI_App::update_publish_status() +{ + mainframe->show_publish_button(has_model_mall()); + + mainframe->m_webview->ResetWholePage(); +} + +bool GUI_App::has_model_mall() +{ + /*if (auto cc = app_config->get_region(); cc == "CNH" || cc == "China" || cc == "") + return false;*/ + return true; +} + +void GUI_App::update_label_colours() +{ + for (Tab* tab : tabs_list) + tab->update_label_colours(); +} + +#ifdef _WIN32 +static bool is_focused(HWND hWnd) +{ + HWND hFocusedWnd = ::GetFocus(); + return hFocusedWnd && hWnd == hFocusedWnd; +} + +static bool is_default(wxWindow* win) +{ + wxTopLevelWindow* tlw = find_toplevel_parent(win); + if (!tlw) + return false; + + return win == tlw->GetDefaultItem(); +} +#endif + +void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) +{ + if (wxButton *btn = dynamic_cast(window)) { + if (btn->GetWindowStyleFlag() & wxBU_AUTODRAW) + return; + else { +#ifdef _WIN32 + if (btn->GetId() == wxID_OK || btn->GetId() == wxID_CANCEL) { + bool is_focused_button = false; + bool is_default_button = false; + + if (!(btn->GetWindowStyle() & wxNO_BORDER)) { + btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); + highlited = true; + } + + auto mark_button = [this, btn, highlited](const bool mark) { + btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); + btn->SetForegroundColour(mark ? m_color_hovered_btn_label :m_color_default_btn_label); + btn->Refresh(); + btn->Update(); + }; + + // hovering + btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); + // focusing + btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); + + is_focused_button = is_focused(btn->GetHWND()); + is_default_button = is_default(btn); + mark_button(is_focused_button); + } +#endif + } + } + + if (Button* btn = dynamic_cast(window)) { + if (btn->GetWindowStyleFlag() & wxBU_AUTODRAW) + return; + } + + + /*if (m_is_dark_mode != dark_mode() ) + m_is_dark_mode = dark_mode();*/ + + + if (m_is_dark_mode) { + auto original_col = window->GetBackgroundColour(); + auto bg_col = StateColor::darkModeColorFor(original_col); + + if (bg_col != original_col) { + window->SetBackgroundColour(bg_col); + } + + original_col = window->GetForegroundColour(); + auto fg_col = StateColor::darkModeColorFor(original_col); + + if (fg_col != original_col) { + window->SetForegroundColour(fg_col); + } + } + else { + auto original_col = window->GetBackgroundColour(); + auto bg_col = StateColor::lightModeColorFor(original_col); + + if (bg_col != original_col) { + window->SetBackgroundColour(bg_col); + } + + original_col = window->GetForegroundColour(); + auto fg_col = StateColor::lightModeColorFor(original_col); + + if (fg_col != original_col) { + window->SetForegroundColour(fg_col); + } + } +} + +// recursive function for scaling fonts for all controls in Window +static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) +{ + /*bool is_btn = dynamic_cast(window) != nullptr; + is_btn = false;*/ + if (!window) return; + + wxGetApp().UpdateDarkUI(window); + + auto children = window->GetChildren(); + for (auto child : children) { + update_dark_children_ui(child); + } +} + +// Note: Don't use this function for Dialog contains ScalableButtons +void GUI_App::UpdateDarkUIWin(wxWindow* win) +{ + update_dark_children_ui(win); +} + +void GUI_App::Update_dark_mode_flag() +{ + m_is_dark_mode = dark_mode(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": switch the current dark mode status to %1% ")%m_is_dark_mode; +} + +void GUI_App::UpdateDlgDarkUI(wxDialog* dlg) +{ +#ifdef __WINDOWS__ + NppDarkMode::SetDarkExplorerTheme(dlg->GetHWND()); + NppDarkMode::SetDarkTitleBar(dlg->GetHWND()); +#endif + update_dark_children_ui(dlg); +} + +void GUI_App::UpdateFrameDarkUI(wxFrame* dlg) +{ +#ifdef __WINDOWS__ + NppDarkMode::SetDarkExplorerTheme(dlg->GetHWND()); + NppDarkMode::SetDarkTitleBar(dlg->GetHWND()); +#endif + update_dark_children_ui(dlg); +} + +void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) +{ +#ifdef __WINDOWS__ + UpdateDarkUI(dvc, highlited ? dark_mode() : false); +#ifdef _MSW_DARK_MODE + //dvc->RefreshHeaderDarkMode(&m_normal_font); + HWND hwnd = (HWND)dvc->GenericGetHeader()->GetHandle(); + hwnd = GetWindow(hwnd, GW_CHILD); + if (hwnd != NULL) + NppDarkMode::SetDarkListViewHeader(hwnd); + wxItemAttr attr; + attr.SetTextColour(NppDarkMode::GetTextColor()); + attr.SetFont(m_normal_font); + dvc->SetHeaderAttr(attr); +#endif //_MSW_DARK_MODE + if (dvc->HasFlag(wxDV_ROW_LINES)) + dvc->SetAlternateRowColour(m_color_highlight_default); + if (dvc->GetBorder() != wxBORDER_SIMPLE) + dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); +#endif +} + +void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) +{ +#ifdef __WINDOWS__ + wxGetApp().UpdateDarkUI(parent); + + auto children = parent->GetChildren(); + for (auto child : children) { + if (dynamic_cast(child)) + child->SetForegroundColour(m_color_label_default); + } +#endif +} + +void GUI_App::init_fonts() +{ + // BBS: modify font + m_small_font = Label::Body_10; + m_bold_font = Label::Body_10.Bold(); + m_normal_font = Label::Body_10; + +#ifdef __WXMAC__ + m_small_font.SetPointSize(11); + m_bold_font.SetPointSize(13); +#endif /*__WXMAC__*/ + + // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as + // DEFAULT in wxGtk. Use the TELETYPE family as a work-around + m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::update_fonts(const MainFrame *main_frame) +{ + /* Only normal and bold fonts are used for an application rescale, + * because of under MSW small and normal fonts are the same. + * To avoid same rescaling twice, just fill this values + * from rescaled MainFrame + */ + if (main_frame == nullptr) + main_frame = this->mainframe; + m_normal_font = Label::Body_14; // BBS: larger font size + m_small_font = m_normal_font; + m_bold_font = m_normal_font.Bold(); + m_link_font = m_bold_font.Underlined(); + m_em_unit = main_frame->em_unit(); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::set_label_clr_modified(const wxColour& clr) +{ + return; + //BBS + /* + if (m_color_label_modified == clr) + return; + m_color_label_modified = clr; + auto clr_str = wxString::Format(wxT("#%02X%02X%02X"), clr.Red(), clr.Green(), clr.Blue()); + std::string str = clr_str.ToStdString(); + app_config->save(); + */ +} + +void GUI_App::set_label_clr_sys(const wxColour& clr) +{ + return; + //BBS + /* + if (m_color_label_sys == clr) + return; + m_color_label_sys = clr; + auto clr_str = wxString::Format(wxT("#%02X%02X%02X"), clr.Red(), clr.Green(), clr.Blue()); + std::string str = clr_str.ToStdString(); + app_config->save(); + */ +} + +bool GUI_App::get_side_menu_popup_status() +{ + return m_side_popup_status; +} + +void GUI_App::set_side_menu_popup_status(bool status) +{ + m_side_popup_status = status; +} + +void GUI_App::link_to_network_check() +{ + std::string url; + std::string country_code = app_config->get_country_code(); + + + if (country_code == "US") { + url = "https://status.bambulab.com"; + } + else if (country_code == "CN") { + url = "https://status.bambulab.cn"; + } + else { + url = "https://status.bambulab.com"; + } + wxLaunchDefaultBrowser(url); +} + +bool GUI_App::tabs_as_menu() const +{ + return false; +} + +wxSize GUI_App::get_min_size() const +{ + return wxSize(76*m_em_unit, 49 * m_em_unit); +} + +float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit * 0.1f; +#endif // __APPLE__ + + //return icon_sc; + + const std::string& auto_val = app_config->get("toolkit_size"); + + if (auto_val.empty()) + return icon_sc; + + int int_val = 100; + // correct value in respect to toolkit_size + int_val = std::min(atoi(auto_val.c_str()), int_val); + + if (is_limited && int_val < 50) + int_val = 50; + + return 0.01f * int_val * icon_sc; +} + +void GUI_App::set_auto_toolbar_icon_scale(float scale) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit * 0.1f; +#endif // __APPLE__ + + long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); + std::string val = std::to_string(int_val); + + app_config->set("toolkit_size", val); +} + +// check user printer_presets for the containing information about "Print Host upload" +void GUI_App::check_printer_presets() +{ +//BBS +#if 0 + std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); + if (preset_names.empty()) + return; + + // BBS: remove "print host upload" message dialog + preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); +#endif +} + +void switch_window_pools(); +void release_window_pools(); + +void GUI_App::recreate_GUI(const wxString &msg_name) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI enter"; + m_is_recreating_gui = true; + + update_http_extra_header(); + + mainframe->shutdown(); + ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); + dlg.Pulse(); + dlg.Update(10, _L("Rebuild") + dots); + + MainFrame *old_main_frame = mainframe; + struct ClientData : wxClientData + { + ~ClientData() { release_window_pools(); } + }; + old_main_frame->SetClientObject(new ClientData); + + switch_window_pools(); + mainframe = new MainFrame(); + if (is_editor()) + // hide settings tabs after first Layout + mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + // Propagate model objects to object list. + sidebar().obj_list()->init(); + //sidebar().aux_list()->init_auxiliary(); + //mainframe->m_auxiliary->init_auxiliary(); + SetTopWindow(mainframe); + + dlg.Update(30, _L("Rebuild") + dots); + old_main_frame->Destroy(); + + dlg.Update(80, _L("Loading current presets") + dots); + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + load_current_presets(); + mainframe->Show(true); + //mainframe->refresh_plugin_tips(); + + dlg.Update(90, _L("Loading a mode view") + dots); + + obj_list()->set_min_height(); + update_mode(); + + //check hms info for different language + if (hms_query) + hms_query->check_hms_info(); + + //BBS: trigger restore project logic here, and skip confirm + plater_->trigger_restore_project(1); + + // #ys_FIXME_delete_after_testing Do we still need this ? +// CallAfter([]() { +// // Run the config wizard, don't offer the "reset user profile" checkbox. +// config_wizard_startup(true); +// }); + + + update_publish_status(); + + m_is_recreating_gui = false; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit"; +} + +void GUI_App::system_info() +{ + //SysInfoDialog dlg; + //dlg.ShowModal(); +} + +void GUI_App::keyboard_shortcuts() +{ + KBShortcutsDialog dlg; + dlg.ShowModal(); +} + + +void GUI_App::ShowUserGuide() { + // BBS:Show NewUser Guide + try { + bool res = false; + GuideFrame GuideDlg(this); + //if (GuideDlg.IsFirstUse()) + res = GuideDlg.run(); + if (res) { + load_current_presets(); + update_publish_status(); + mainframe->refresh_plugin_tips(); + // BBS: remove SLA related message + } + } catch (std::exception &e) { + // wxMessageBox(e.what(), "", MB_OK); + } +} + +void GUI_App::ShowDownNetPluginDlg() { + try { + auto iter = std::find_if(dialogStack.begin(), dialogStack.end(), [](auto dialog) { + return dynamic_cast(dialog) != nullptr; + }); + if (iter != dialogStack.end()) + return; + DownloadProgressDialog dlg(_L("Downloading Bambu Network Plug-in")); + dlg.ShowModal(); + } catch (std::exception &e) { + ; + } +} + +void GUI_App::ShowUserLogin(bool show) +{ + // BBS: User Login Dialog + if (show) { + try { + if (!login_dlg) + login_dlg = new ZUserLogin(); + else { + delete login_dlg; + login_dlg = new ZUserLogin(); + } + login_dlg->ShowModal(); + } catch (std::exception &e) { + ; + } + } else { + if (login_dlg) + login_dlg->EndModal(wxID_OK); + } +} + + +void GUI_App::ShowOnlyFilament() { + // BBS:Show NewUser Guide + try { + bool res = false; + GuideFrame GuideDlg(this); + GuideDlg.SetStartPage(GuideFrame::GuidePage::BBL_FILAMENT_ONLY); + res = GuideDlg.run(); + if (res) { + load_current_presets(); + + // BBS: remove SLA related message + } + } catch (std::exception &e) { + // wxMessageBox(e.what(), "", MB_OK); + } +} + + + +// static method accepting a wxWindow object as first parameter +bool GUI_App::catch_error(std::function cb, + // wxMessageDialog* message_dialog, + const std::string& err /*= ""*/) +{ + if (!err.empty()) { + if (cb) + cb(); + // if (message_dialog) + // message_dialog->(err, "Error", wxOK | wxICON_ERROR); + show_error(/*this*/nullptr, err); + return true; + } + return false; +} + +// static method accepting a wxWindow object as first parameter +void fatal_error(wxWindow* parent) +{ + show_error(parent, ""); + // exit 1; // #ys_FIXME +} + +#ifdef __WINDOWS__ +#ifdef _MSW_DARK_MODE +static void update_scrolls(wxWindow* window) +{ + wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); + while (node) + { + wxWindow* win = node->GetData(); + if (dynamic_cast(win) || + dynamic_cast(win) || + dynamic_cast(win)) + NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); + + update_scrolls(win); + node = node->GetNext(); + } +} +#endif //_MSW_DARK_MODE + + +#ifdef _MSW_DARK_MODE +void GUI_App::force_menu_update() +{ + NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); +} +#endif //_MSW_DARK_MODE +#endif //__WINDOWS__ + +void GUI_App::force_colors_update() +{ +#ifdef _MSW_DARK_MODE +#ifdef __WINDOWS__ + NppDarkMode::SetDarkMode(dark_mode()); + if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) + NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); + NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); + + + //NppDarkMode::SetDarkExplorerTheme((HWND)mainframe->m_settings_dialog.GetHWND()); + //NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); + +#endif // __WINDOWS__ +#endif //_MSW_DARK_MODE + m_force_colors_update = true; +} + +// Called after the Preferences dialog is closed and the program settings are saved. +// Update the UI based on the current preferences. +void GUI_App::update_ui_from_settings() +{ + update_label_colours(); + // Upadte UI colors before Update UI from settings + if (m_force_colors_update) { + m_force_colors_update = false; + //UpdateDlgDarkUI(&mainframe->m_settings_dialog); + //mainframe->m_settings_dialog.Refresh(); + //mainframe->m_settings_dialog.Update(); + + if (mainframe) { +#ifdef __WINDOWS__ + mainframe->force_color_changed(); + update_scrolls(mainframe); + update_scrolls(&mainframe->m_settings_dialog); +#endif //_MSW_DARK_MODE + update_dark_children_ui(mainframe); + } + } + + if (mainframe) {mainframe->update_ui_from_settings();} +} + +void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) +{ + const std::string name = into_u8(window->GetName()); + + window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< ": received wxEVT_CLOSE_WINDOW, trigger save for window_mainframe"; + window_pos_save(window, "mainframe"); + event.Skip(); + }); + + if (window_pos_restore(window, "mainframe", default_maximized)) { + on_window_geometry(window, [=]() { + window_pos_sanitize(window); + }); + } else { + on_window_geometry(window, [=]() { + window_pos_center(window); + }); + } +} + +void GUI_App::load_project(wxWindow *parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (3mf):"), + app_config->get_last_dir(), "", + file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const +{ + input_files.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), +#ifdef __APPLE__ + _L("Choose one or more files (3mf/step/stl/svg/obj/amf/usd*/abc/ply):"), +#else + _L("Choose one or more files (3mf/step/stl/svg/obj/amf):"), +#endif + from_u8(app_config->get_last_dir()), "", + file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + dialog.GetPaths(input_files); +} + +void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (gcode/.gco/.g/.ngc/ngc):"), + app_config->get_last_dir(), "", + file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +wxString GUI_App::transition_tridid(int trid_id) +{ + wxString maping_dict[8] = { "A", "B", "C", "D", "E", "F", "G" }; + int id_index = ceil(trid_id / 4); + int id_suffix = (trid_id + 1) % 4 == 0 ? 4 : (trid_id + 1) % 4; + return wxString::Format("%s%d", maping_dict[id_index], id_suffix); +} + +//BBS +void GUI_App::request_login(bool show_user_info) +{ + ShowUserLogin(); + + if (show_user_info) { + get_login_info(); + } +} + +void GUI_App::get_login_info() +{ + if (m_agent) { + if (m_agent->is_user_login()) { + std::string login_cmd = m_agent->build_login_cmd(); + wxString strJS = wxString::Format("window.postMessage(%s)", login_cmd); + GUI::wxGetApp().run_script_left(strJS); + } + else { + m_agent->user_logout(); + std::string logout_cmd = m_agent->build_logout_cmd(); + wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); + GUI::wxGetApp().run_script_left(strJS); + } + } +} + +bool GUI_App::is_user_login() +{ + if (m_agent) { + return m_agent->is_user_login(); + } + return false; +} + + +bool GUI_App::check_login() +{ + bool result = false; + if (m_agent) { + result = m_agent->is_user_login(); + } + + if (!result) { + ShowUserLogin(); + } + return result; +} + +void GUI_App::request_user_handle(int online_login) +{ + auto evt = new wxCommandEvent(EVT_USER_LOGIN_HANDLE); + evt->SetInt(online_login); + wxQueueEvent(this, evt); +} + +void GUI_App::request_user_login(int online_login) +{ + auto evt = new wxCommandEvent(EVT_USER_LOGIN); + evt->SetInt(online_login); + wxQueueEvent(this, evt); +} + +void GUI_App::request_user_logout() +{ + if (m_agent && m_agent->is_user_login()) { + // Update data first before showing dialogs + m_agent->user_logout(); + m_agent->set_user_selected_machine(""); + /* delete old user settings */ + bool transfer_preset_changes = false; + wxString header = _L("Some presets are modified.") + "\n" + + _L("You can keep the modified presets for the new project, discard or save changes as new presets."); + using ab = UnsavedChangesDialog::ActionButtons; + wxGetApp().check_and_keep_current_preset_changes(_L("User logged out"), header, ab::KEEP | ab::SAVE, &transfer_preset_changes); + + m_device_manager->clean_user_info(); + GUI::wxGetApp().sidebar().load_ams_list({}, {}); + remove_user_presets(); + enable_user_preset_folder(false); + preset_bundle->load_user_presets(DEFAULT_USER_FOLDER_NAME, ForwardCompatibilitySubstitutionRule::Enable); + mainframe->update_side_preset_ui(); + + GUI::wxGetApp().stop_sync_user_preset(); + } +} + +int GUI_App::request_user_unbind(std::string dev_id) +{ + int result = -1; + if (m_agent) { + result = m_agent->unbind(dev_id); + BOOST_LOG_TRIVIAL(info) << "request_user_unbind, dev_id = " << dev_id << ", result = " << result; + return result; + } + return result; +} + +std::string GUI_App::handle_web_request(std::string cmd) +{ + try { + //BBS use nlohmann json format + std::stringstream ss(cmd), oss; + pt::ptree root, response; + pt::read_json(ss, root); + if (root.empty()) + return ""; + + boost::optional sequence_id = root.get_optional("sequence_id"); + boost::optional command = root.get_optional("command"); + if (command.has_value()) { + std::string command_str = command.value(); + if (command_str.compare("request_project_download") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional project_id = data_node.get_optional("project_id"); + if (project_id.has_value()) { + this->request_project_download(project_id.value()); + } + } + } + else if (command_str.compare("open_project") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional project_id = data_node.get_optional("project_id"); + if (project_id.has_value()) { + this->request_open_project(project_id.value()); + } + } + } + else if (command_str.compare("get_login_info") == 0) { + CallAfter([this] { + get_login_info(); + }); + } + else if (command_str.compare("homepage_login_or_register") == 0) { + + if (root.get_child_optional("makerworld_model_id") != boost::none) { + boost::optional ModelID = root.get_optional("makerworld_model_id"); + if (ModelID.has_value()) { + if (mainframe) { + if (mainframe->m_webview) + { + mainframe->m_webview->SetMakerworldModelID(ModelID.value()); + } + } + } + } + + CallAfter([this] { + this->request_login(true); + }); + } + else if (command_str.compare("homepage_logout") == 0) { + CallAfter([this] { + wxGetApp().request_user_logout(); + }); + } + else if (command_str.compare("homepage_modeldepot") == 0) { + CallAfter([this] { + wxGetApp().open_mall_page_dialog(); + }); + } + else if (command_str.compare("homepage_newproject") == 0) { + this->request_open_project(""); + } + else if (command_str.compare("homepage_openproject") == 0) { + this->request_open_project({}); + } + else if (command_str.compare("get_recent_projects") == 0) { + if (mainframe) { + if (mainframe->m_webview) { + mainframe->m_webview->SendRecentList(INT_MAX); + } + } + } + else if (command_str.compare("modelmall_model_advise_get") == 0) { + if (mainframe && this->app_config->get("staff_pick_switch") == "true") { + if (mainframe->m_webview) { + mainframe->m_webview->SendDesignStaffpick(has_model_mall()); + } + } + } + else if (command_str.compare("modelmall_model_open") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional id = data_node.get_optional("id"); + if (id.has_value() && mainframe && mainframe->m_webview) { + mainframe->m_webview->OpenModelDetail(id.value(), m_agent); + } + } + } + else if (command_str.compare("homepage_open_recentfile") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional path = data_node.get_optional("path"); + if (path.has_value()) { + this->request_open_project(path.value()); + } + } + } + else if (command_str.compare("homepage_delete_recentfile") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional path = data_node.get_optional("path"); + if (path.has_value()) { + this->request_remove_project(path.value()); + } + } + } + else if (command_str.compare("homepage_delete_all_recentfile") == 0) { + this->request_remove_project(""); + } + else if (command_str.compare("homepage_explore_recentfile") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional path = data_node.get_optional("path"); + if (path.has_value()) + { + boost::filesystem::path NowFile(path.value()); + + std::string FilePath = NowFile.make_preferred().string(); + desktop_open_any_folder(FilePath); + } + } + } + else if (command_str.compare("homepage_open_hotspot") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional url = data_node.get_optional("url"); + if (url.has_value()) { + this->request_open_project(url.value()); + } + } + } + else if (command_str.compare("begin_network_plugin_download") == 0) { + CallAfter([this] { wxGetApp().ShowDownNetPluginDlg(); }); + } + else if (command_str.compare("get_web_shortcut") == 0) { + if (root.get_child_optional("key_event") != boost::none) { + pt::ptree key_event_node = root.get_child("key_event"); + auto keyCode = key_event_node.get("key"); + auto ctrlKey = key_event_node.get("ctrl"); + auto shiftKey = key_event_node.get("shift"); + auto cmdKey = key_event_node.get("cmd"); + + wxKeyEvent e(wxEVT_CHAR_HOOK); +#ifdef __APPLE__ + e.SetControlDown(cmdKey); + e.SetRawControlDown(ctrlKey); +#else + e.SetControlDown(ctrlKey); +#endif + e.SetShiftDown(shiftKey); + keyCode = keyCode == 188 ? ',' : keyCode; + e.m_keyCode = keyCode; + e.SetEventObject(mainframe); + wxPostEvent(mainframe, e); + } + } + else if (command_str.compare("userguide_wiki_open") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional path = data_node.get_optional("url"); + if (path.has_value()) { + wxLaunchDefaultBrowser(path.value()); + if (m_agent) { + json j; + j["user_guide"] = path.value(); + m_agent->track_event("user_guide", j.dump()); + } + } + } + } + else if (command_str.compare("homepage_open_ccabin") == 0) { + if (root.get_child_optional("data") != boost::none) { + pt::ptree data_node = root.get_child("data"); + boost::optional path = data_node.get_optional("file"); + if (path.has_value()) { + std::string Fullpath = resources_dir() + "/web/homepage/model/" + path.value(); + + this->request_open_project(Fullpath); + } + } + } + else if (command_str.compare("common_openurl") == 0) { + boost::optional path = root.get_optional("url"); + if (path.has_value()) { + wxLaunchDefaultBrowser(path.value()); + } + } + else if (command_str.compare("homepage_leftmenu_clicked") == 0) { + if (root.get_child_optional("menu") != boost::none) { + std::string strMenu = root.get_optional("menu").value(); + int nRefresh = root.get_child_optional("refresh") == boost::none ? 0 : root.get_optional("refresh").value(); + + CallAfter([this,strMenu, nRefresh] { + if (mainframe && mainframe->m_webview) + { + mainframe->m_webview->SwitchWebContent(strMenu, nRefresh); + } + } + ); + } + } + else if (command_str.compare("homepage_leftmenu_switch") == 0) { + if (root.get_child_optional("menu") != boost::none) { + std::string strMenu = root.get_optional("menu").value(); + + if (mainframe && mainframe->m_webview) { mainframe->m_webview->SwitchLeftMenu(strMenu); } + } + } + else if (command_str.compare("homepage_makerlab_get") == 0) { + if (mainframe && mainframe->m_webview) { mainframe->m_webview->SendMakerlabList(); } + } + else if (command_str.compare("homepage_makerlab_open") == 0) { + if (root.get_child_optional("url") != boost::none) { + std::string strUrl = root.get_optional("url").value(); + + if (mainframe && mainframe->m_webview) { mainframe->m_webview->OpenOneMakerlab(strUrl); } + } + } + else if (command_str.compare("makerworld_model_open") == 0) + { + if (root.get_child_optional("model") != boost::none) { + pt::ptree data_node = root.get_child("model"); + boost::optional path = data_node.get_optional("url"); + if (path.has_value()) + { + wxString realurl = from_u8(url_decode(path.value())); + wxGetApp().request_model_download(realurl); + } + } + } + } + } + catch (...) { + BOOST_LOG_TRIVIAL(trace) << "parse json cmd failed " << cmd; + return ""; + } + return ""; +} + +void GUI_App::handle_script_message(std::string msg) +{ + try { + json j = json::parse(msg); + if (j.contains("command")) { + wxString cmd = j["command"]; + if (cmd == "user_login") { + if (m_agent) { + m_agent->change_user(j.dump()); + if (m_agent->is_user_login()) { + request_user_login(1); + } + } + } + } + } + catch (...) { + ; + } +} + +void GUI_App::request_model_download(wxString url) +{ + if (plater_) { + plater_->request_model_download(url); + } +} + +//BBS download project by project id +void GUI_App::download_project(std::string project_id) +{ + if (plater_) { + plater_->request_download_project(project_id); + } +} + +void GUI_App::request_project_download(std::string project_id) +{ + if (!check_login()) return; + + download_project(project_id); +} + +void GUI_App::request_open_project(std::string project_id) +{ + if (plater()->is_background_process_slicing()) { + Slic3r::GUI::show_info(nullptr, _L("new or open project file is not allowed during the slicing process!"), _L("Open Project")); + return; + } + + if (project_id == "") + plater()->new_project(); + else if (project_id.empty()) + plater()->load_project(); + else if (std::find_if_not(project_id.begin(), project_id.end(), + [](char c) { return std::isdigit(c); }) == project_id.end()) + ; + else if (boost::algorithm::starts_with(project_id, "http")) + ; + else + CallAfter([this, project_id] { mainframe->open_recent_project(-1, wxString::FromUTF8(project_id)); }); +} + +void GUI_App::request_remove_project(std::string project_id) +{ + mainframe->remove_recent_project(-1, wxString::FromUTF8(project_id)); +} + +void GUI_App::handle_http_error(unsigned int status, std::string body) +{ + // tips body size must less than 1024 + auto evt = new wxCommandEvent(EVT_HTTP_ERROR); + evt->SetInt(status); + evt->SetString(wxString(body)); + wxQueueEvent(this, evt); +} + +void GUI_App::on_http_error(wxCommandEvent &evt) +{ + int status = evt.GetInt(); + + int code = 0; + std::string error; + wxString result; + if (status >= 400 && status < 500) { + try { + wxString body_str = evt.GetString(); + bool found_json = false; + for (int i = 0; i < body_str.size(); i++) { + if (body_str[i] == '{') { + found_json = true; + break; + } + } + if (found_json) { + json j = json::parse(body_str); + if (j.contains("code")) { + if (!j["code"].is_null()) + code = j["code"].get(); + } + if (j.contains("error")) { + if (!j["error"].is_null()) + error = j["error"].get(); + } + } + } + catch (...) {} + } + + // Version limit + if (code == HttpErrorVersionLimited) { + if (!m_show_http_errpr_msgdlg) { + MessageDialog msg_dlg(nullptr, _L("The Bambu Studio version is too old to enable cloud service. Please download the latest version from Bambu Lab website."), "", wxAPPLY | wxOK); + m_show_http_errpr_msgdlg = true; + auto modal_result = msg_dlg.ShowModal(); + if (modal_result == wxOK || modal_result == wxCLOSE) { + m_show_http_errpr_msgdlg = false; + return; + } + } + + } + + // request login + if (status == 401) { + if (m_agent) { + if (m_agent->is_user_login()) { + this->request_user_logout(); + + if (!m_show_http_errpr_msgdlg) { + MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK); + m_show_http_errpr_msgdlg = true; + auto modal_result = msg_dlg.ShowModal(); + if (modal_result == wxOK || modal_result == wxCLOSE) { + m_show_http_errpr_msgdlg = false; + return; + } + } + } + } + return; + } +} + +void GUI_App::enable_user_preset_folder(bool enable) +{ + if (enable) { + std::string user_id = m_agent->get_user_id(); + app_config->set("preset_folder", user_id); + GUI::wxGetApp().preset_bundle->update_user_presets_directory(user_id); + } else { + BOOST_LOG_TRIVIAL(info) << "preset_folder: set to empty"; + app_config->set("preset_folder", ""); + GUI::wxGetApp().preset_bundle->update_user_presets_directory(DEFAULT_USER_FOLDER_NAME); + } +} + +void GUI_App::on_set_selected_machine(wxCommandEvent &evt) +{ + DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (dev) { + dev->set_selected_machine(m_agent->get_user_selected_machine()); + } +} + +void GUI_App::on_update_machine_list(wxCommandEvent &evt) +{ + /* DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (dev) { + dev->add_user_subscribe(); + }*/ +} + +void GUI_App::on_user_login_handle(wxCommandEvent &evt) +{ + if (!m_agent) { return; } + + int online_login = evt.GetInt(); + m_agent->connect_server(); + + // get machine list + DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) return; + + boost::thread update_thread = boost::thread([this, dev] { + dev->update_user_machine_list_info(); + auto evt = new wxCommandEvent(EVT_SET_SELECTED_MACHINE); + wxQueueEvent(this, evt); + }); + + if (online_login) { + remove_user_presets(); + enable_user_preset_folder(true); + preset_bundle->load_user_presets(m_agent->get_user_id(), ForwardCompatibilitySubstitutionRule::Enable); + mainframe->update_side_preset_ui(); + + GUI::wxGetApp().mainframe->show_sync_dialog(); + } +} + + +void GUI_App::check_track_enable() +{ + if (app_config && app_config->get("firstguide", "privacyuse") == "true") { + //enable track event + json header_json; + header_json["ver"] = SLIC3R_VERSION; + wxString os_desc = wxGetOsDescription(); + int major = 0, minor = 0, micro = 0; + header_json["os"] = std::string(os_desc.ToUTF8()); + header_json["name"] = std::string(SLIC3R_APP_NAME); + header_json["uuid"] = app_config->get("slicer_uuid"); + if (m_agent) { + m_agent->track_enable(true); + m_agent->track_header(header_json.dump()); + } + /* record studio start event */ + json j; + j["user_mode"] = this->get_mode_str(); + j["open_method"] = m_open_method; + if (m_agent) { + m_agent->track_event("studio_launch", j.dump()); + } + } + else { + if (m_agent) { + m_agent->track_remove_files(); + } + } +} + +void GUI_App::on_user_login(wxCommandEvent &evt) +{ + if (!m_agent) { return; } + int online_login = evt.GetInt(); + // check privacy before handle + check_privacy_version(online_login); + check_track_enable(); +} + +bool GUI_App::is_studio_active() +{ + auto curr_time = std::chrono::system_clock::now(); + auto diff = std::chrono::duration_cast(curr_time - last_active_point); + if (diff.count() < STUDIO_INACTIVE_TIMEOUT) { + return true; + } + return false; +} + +void GUI_App::reset_to_active() +{ + last_active_point = std::chrono::system_clock::now(); +} + +void GUI_App::check_update(bool show_tips, int by_user) +{ + if (version_info.version_str.empty()) return; + if (version_info.url.empty()) return; + + auto curr_version = Semver::parse(SLIC3R_VERSION); + auto remote_version = Semver::parse(version_info.version_str); + if (curr_version && remote_version && (*remote_version > *curr_version)) { + if (version_info.force_upgrade) { + wxGetApp().app_config->set_bool("force_upgrade", version_info.force_upgrade); + wxGetApp().app_config->set("upgrade", "force_upgrade", true); + wxGetApp().app_config->set("upgrade", "description", version_info.description); + wxGetApp().app_config->set("upgrade", "version", version_info.version_str); + wxGetApp().app_config->set("upgrade", "url", version_info.url); + GUI::wxGetApp().enter_force_upgrade(); + } + else { + GUI::wxGetApp().request_new_version(by_user); + } + } else { + wxGetApp().app_config->set("upgrade", "force_upgrade", false); + if (show_tips) + this->no_new_version(); + } +} + +void GUI_App::check_new_version(bool show_tips, int by_user) +{ + std::string platform = "windows"; + +#ifdef __WINDOWS__ + platform = "windows"; +#endif +#ifdef __APPLE__ + platform = "macos"; +#endif +#ifdef __LINUX__ + platform = "linux"; +#endif + std::string query_params = (boost::format("?name=slicer&version=%1%&guide_version=%2%") + % VersionInfo::convert_full_version(SLIC3R_VERSION) + % VersionInfo::convert_full_version("0.0.0.1") + ).str(); + + std::string url = get_http_url(app_config->get_country_code()) + query_params; + Slic3r::Http http = Slic3r::Http::get(url); + + http.header("accept", "application/json") + .timeout_connect(TIMEOUT_CONNECT) + .timeout_max(TIMEOUT_RESPONSE) + .on_complete([this, show_tips, by_user](std::string body, unsigned) { + try { + json j = json::parse(body); + if (j.contains("message")) { + if (j["message"].get() == "success") { + if (j.contains("software")) { + if (j["software"].empty() && show_tips) { + this->no_new_version(); + } + else { + if (j["software"].contains("url") + && j["software"].contains("version") + && j["software"].contains("description")) { + version_info.url = j["software"]["url"].get(); + version_info.version_str = j["software"]["version"].get(); + version_info.description = j["software"]["description"].get(); + } + if (j["software"].contains("force_update")) { + version_info.force_upgrade = j["software"]["force_update"].get(); + } + CallAfter([this, show_tips, by_user](){ + this->check_update(show_tips, by_user); + }); + } + } + } + } + } + catch (...) { + ; + } + }) + .on_error([this](std::string body, std::string error, unsigned int status) { + handle_http_error(status, body); + BOOST_LOG_TRIVIAL(error) << "check new version error" << body; + }).perform(); +} + + +//BBS pop up a dialog and download files +void GUI_App::request_new_version(int by_user) +{ + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version_info.version_str)); + evt->SetInt(by_user); + GUI::wxGetApp().QueueEvent(evt); +} + +void GUI_App::enter_force_upgrade() +{ + wxCommandEvent *evt = new wxCommandEvent(EVT_ENTER_FORCE_UPGRADE); + GUI::wxGetApp().QueueEvent(evt); +} + +void GUI_App::set_skip_version(bool skip) +{ + BOOST_LOG_TRIVIAL(info) << "set_skip_version, skip = " << skip << ", version = " <set("skip_version", version_info.version_str); + }else { + app_config->set("skip_version", ""); + } +} + +void GUI_App::show_check_privacy_dlg(wxCommandEvent& evt) +{ + int online_login = evt.GetInt(); + PrivacyUpdateDialog privacy_dlg(this->mainframe, wxID_ANY, _L("Privacy Policy Update")); + privacy_dlg.Bind(EVT_PRIVACY_UPDATE_CONFIRM, [this, online_login](wxCommandEvent &e) { + app_config->set("privacy_version", privacy_version_info.version_str); + app_config->set_bool("privacy_update_checked", true); + app_config->save(); + request_user_handle(online_login); + }); + privacy_dlg.Bind(EVT_PRIVACY_UPDATE_CANCEL, [this](wxCommandEvent &e) { + app_config->set_bool("privacy_update_checked", false); + app_config->save(); + if (m_agent) { + m_agent->user_logout(); + } + }); + + privacy_dlg.set_text(privacy_version_info.description); + privacy_dlg.on_show(); +} + +void GUI_App::on_show_check_privacy_dlg(int online_login) +{ + auto evt = new wxCommandEvent(EVT_CHECK_PRIVACY_SHOW); + evt->SetInt(online_login); + wxQueueEvent(this, evt); +} + +bool GUI_App::check_privacy_update() +{ + if (privacy_version_info.version_str.empty() || privacy_version_info.description.empty() + || privacy_version_info.url.empty()) { + return false; + } + + std::string local_privacy_ver = app_config->get("privacy_version"); + auto curr_version = Semver::parse(local_privacy_ver); + auto remote_version = Semver::parse(privacy_version_info.version_str); + if (curr_version && remote_version) { + if (*remote_version > *curr_version || app_config->get("privacy_update_checked") != "true") { + return true; + } + } + return false; +} + +void GUI_App::on_check_privacy_update(wxCommandEvent& evt) +{ + int online_login = evt.GetInt(); + bool result = check_privacy_update(); + if (result) + on_show_check_privacy_dlg(online_login); + else + request_user_handle(online_login); +} + +void GUI_App::check_privacy_version(int online_login) +{ + update_http_extra_header(); + std::string query_params = "?policy/privacy=00.00.00.00"; + std::string url = get_http_url(app_config->get_country_code()) + query_params; + Slic3r::Http http = Slic3r::Http::get(url); + + http.header("accept", "application/json") + .timeout_connect(TIMEOUT_CONNECT) + .timeout_max(TIMEOUT_RESPONSE) + .on_complete([this, online_login](std::string body, unsigned) { + try { + json j = json::parse(body); + if (j.contains("message")) { + if (j["message"].get() == "success") { + if (j.contains("resources")) { + for (auto it = j["resources"].begin(); it != j["resources"].end(); it++) { + if (it->contains("type")) { + if ((*it)["type"] == std::string("policy/privacy") + && it->contains("version") + && it->contains("description") + && it->contains("url") + && it->contains("force_update")) { + privacy_version_info.version_str = (*it)["version"].get(); + privacy_version_info.description = (*it)["description"].get(); + privacy_version_info.url = (*it)["url"].get(); + privacy_version_info.force_upgrade = (*it)["force_update"].get(); + break; + } + } + } + CallAfter([this, online_login]() { + auto evt = new wxCommandEvent(EVT_CHECK_PRIVACY_VER); + evt->SetInt(online_login); + wxQueueEvent(this, evt); + }); + } + } + } + } + catch (...) { + request_user_handle(online_login); + } + }) + .on_error([this, online_login](std::string body, std::string error, unsigned int status) { + request_user_handle(online_login); + BOOST_LOG_TRIVIAL(error) << "check privacy version error" << body; + }).perform(); +} + +void GUI_App::no_new_version() +{ + wxCommandEvent* evt = new wxCommandEvent(EVT_SHOW_NO_NEW_VERSION); + GUI::wxGetApp().QueueEvent(evt); +} + +std::string GUI_App::version_display = ""; +std::string GUI_App::format_display_version() +{ + if (!version_display.empty()) return version_display; + + auto version_text = std::string(SLIC3R_VERSION); + int len = version_text.length(); + for (int i = 0, j = 0; i < len; ++i) { + if (!(version_text[i] == '0' && j == 0)) + version_display += version_text[i]; + + if (version_text[i] == '.') + j = 0; + else + ++j; + } + return version_display; +} + +std::string GUI_App::format_IP(const std::string& ip) +{ + std::string format_ip = ip; + size_t pos_st = 0; + size_t pos_en = 0; + + for (int i = 0; i < 2; i++) { + pos_en = format_ip.find('.', pos_st + 1); + if (pos_en == std::string::npos) { + return ip; + } + format_ip.replace(pos_st, pos_en - pos_st, "***"); + pos_st = pos_en + 1; + } + + return format_ip; +} + +void GUI_App::show_dialog(wxString msg) +{ + if (m_info_dialog_content.empty()) { + wxCommandEvent* evt = new wxCommandEvent(EVT_SHOW_DIALOG); + evt->SetString(msg); + GUI::wxGetApp().QueueEvent(evt); + m_info_dialog_content = msg; + } +} + +void GUI_App::push_notification(wxString msg, wxString title, UserNotificationStyle style) +{ + if (!this->is_enable_multi_machine()) { + if (style == UserNotificationStyle::UNS_NORMAL) { + if (m_info_dialog_content.empty()) { + wxCommandEvent* evt = new wxCommandEvent(EVT_SHOW_DIALOG); + evt->SetString(msg); + GUI::wxGetApp().QueueEvent(evt); + m_info_dialog_content = msg; + } + } + else if (style == UserNotificationStyle::UNS_WARNING_CONFIRM) { + GUI::wxGetApp().CallAfter([msg, title] { + GUI::MessageDialog msg_dlg(nullptr, msg, title, wxICON_WARNING | wxOK); + msg_dlg.ShowModal(); + }); + } + } +} + +void GUI_App::reload_settings() +{ + if (preset_bundle && m_agent) { + std::map> user_presets; + m_agent->get_user_presets(&user_presets); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size(); + preset_bundle->load_user_presets(*app_config, user_presets, ForwardCompatibilitySubstitutionRule::Enable); + preset_bundle->save_user_presets(*app_config, get_delete_cache_presets()); + mainframe->update_side_preset_ui(); + } +} + +//BBS reload when logout +void GUI_App::remove_user_presets() +{ + if (preset_bundle && m_agent) { + preset_bundle->remove_users_preset(*app_config); + + // Not remove user preset cache + //std::string user_id = m_agent->get_user_id(); + //preset_bundle->remove_user_presets_directory(user_id); + + //update ui + mainframe->update_side_preset_ui(); + } +} + +void GUI_App::sync_preset(Preset* preset) +{ + int result = -1; + unsigned int http_code = 200; + std::string updated_info; + long long update_time = 0; + // only sync user's preset + if (!preset->is_user()) return; + if (preset->is_custom_defined()) return; + + auto setting_id = preset->setting_id; + std::map values_map; + if (setting_id.empty() && preset->sync_info.empty()) { + if (m_create_preset_blocked[preset->type]) + return; + int ret = preset_bundle->get_differed_values_to_update(*preset, values_map); + if (!ret) { + std::string new_setting_id = m_agent->request_setting_id(preset->name, &values_map, &http_code); + if (!new_setting_id.empty()) { + setting_id = new_setting_id; + result = 0; + auto update_time_str = values_map[BBL_JSON_KEY_UPDATE_TIME]; + if (!update_time_str.empty()) + update_time = std::atoll(update_time_str.c_str()); + } + else { + BOOST_LOG_TRIVIAL(trace) << "[sync_preset]init: request_setting_id failed, http code "<= 400 + if (http_code >= 400) { + result = 0; + updated_info = "hold"; + } + else + result = -1; + } + } + else { + BOOST_LOG_TRIVIAL(trace) << "[sync_preset]init: can not generate differed key-values"; + result = 0; + updated_info = "hold"; + } + } + else if (preset->sync_info.compare("create") == 0) { + if (m_create_preset_blocked[preset->type]) + return; + int ret = preset_bundle->get_differed_values_to_update(*preset, values_map); + if (!ret) { + std::string new_setting_id = m_agent->request_setting_id(preset->name, &values_map, &http_code); + if (!new_setting_id.empty()) { + setting_id = new_setting_id; + result = 0; + auto update_time_str = values_map[BBL_JSON_KEY_UPDATE_TIME]; + if (!update_time_str.empty()) + update_time = std::atoll(update_time_str.c_str()); + } + else { + BOOST_LOG_TRIVIAL(trace) << "[sync_preset]create: request_setting_id failed, http code "<= 400 + if (http_code >= 400) { + result = 0; + updated_info = "hold"; + } + else + result = -1; + } + } + else { + BOOST_LOG_TRIVIAL(trace) << "[sync_preset]create: can not generate differed preset"; + } + } + else if (preset->sync_info.compare("update") == 0) { + if (!setting_id.empty()) { + int ret = preset_bundle->get_differed_values_to_update(*preset, values_map); + if (!ret) { + if (auto iter = values_map.find(BBL_JSON_KEY_BASE_ID); iter != values_map.end() && iter->second == setting_id) { + //clear the setting_id in this case ??? + setting_id.clear(); + result = 0; + } + else { + result = m_agent->put_setting(setting_id, preset->name, &values_map, &http_code); + if (http_code >= 400) { + result = 0; + updated_info = "hold"; + BOOST_LOG_TRIVIAL(error) << "[sync_preset] put setting_id = " << setting_id << " failed, http_code = " << http_code; + } else { + auto update_time_str = values_map[BBL_JSON_KEY_UPDATE_TIME]; + if (!update_time_str.empty()) + update_time = std::atoll(update_time_str.c_str()); + } + } + + } + else { + BOOST_LOG_TRIVIAL(trace) << "[sync_preset]update: can not generate differed key-values, we need to skip this preset "<< preset->name; + result = 0; + } + } + else { + //clear the sync_info + result = 0; + } + } + + if (http_code >= 400 && values_map["code"] == "14") { // Limit + m_create_preset_blocked[preset->type] = true; + CallAfter([this] { + plater()->get_notification_manager()->push_notification(NotificationType::BBLUserPresetExceedLimit); + static bool dialog_notified = false; + if (dialog_notified) + return; + dialog_notified = true; + if (mainframe == nullptr) + return; + auto msg = _L("The number of user presets cached in the cloud has exceeded the upper limit, newly created user presets can only be used locally."); + MessageDialog(mainframe, msg, _L("Sync user presets"), wxICON_WARNING | wxOK).ShowModal(); + }); + return; // this error not need hold, and should not hold + } + + // update sync_info preset info in file + if (result == 0) { + //PresetBundle* preset_bundle = wxGetApp().preset_bundle; + if (!this->preset_bundle) return; + + BOOST_LOG_TRIVIAL(trace) << "sync_preset: sync operation: " << preset->sync_info << " success! preset = " << preset->name; + if (preset->type == Preset::Type::TYPE_FILAMENT) { + preset_bundle->filaments.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time); + } else if (preset->type == Preset::Type::TYPE_PRINT) { + preset_bundle->prints.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time); + } else if (preset->type == Preset::Type::TYPE_PRINTER) { + preset_bundle->printers.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time); + } + } +} + +void GUI_App::start_sync_user_preset(bool with_progress_dlg) +{ + if (!m_agent || !m_agent->is_user_login()) return; + + // has already start sync + if (m_user_sync_token) return; + + ProgressFn progressFn; + WasCancelledFn cancelFn; + std::function finishFn; + + BOOST_LOG_TRIVIAL(info) << "start_sync_service..."; + // BBS + m_user_sync_token.reset(new int(0)); + if (with_progress_dlg) { + auto dlg = new ProgressDialog(_L("Loading"), "", 100, this->mainframe, wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT); + dlg->Update(0, _L("Loading user preset")); + progressFn = [this, dlg](int percent) { + CallAfter([=]{ + dlg->Update(percent, _L("Loading user preset")); + }); + }; + cancelFn = [this, dlg]() { + return m_is_closing || dlg->WasCanceled(); + }; + finishFn = [this, userid = m_agent->get_user_id(), dlg, t = std::weak_ptr(m_user_sync_token)](bool ok) { + CallAfter([=]{ + dlg->Destroy(); + if (ok && m_agent && t.lock() == m_user_sync_token && userid == m_agent->get_user_id()) reload_settings(); + }); + }; + } + else { + finishFn = [this, userid = m_agent->get_user_id(), t = std::weak_ptr(m_user_sync_token)](bool ok) { + CallAfter([=] { + if (ok && m_agent && t.lock() == m_user_sync_token && userid == m_agent->get_user_id()) reload_settings(); + }); + }; + } + + m_sync_update_thread = Slic3r::create_thread( + [this, progressFn, cancelFn, finishFn, t = std::weak_ptr(m_user_sync_token)] { + // get setting list, update setting list + std::string version = preset_bundle->get_vendor_profile_version(PresetBundle::BBL_BUNDLE).to_string(); + int ret = m_agent->get_setting_list2(version, [this](auto info) { + auto type = info[BBL_JSON_KEY_TYPE]; + auto name = info[BBL_JSON_KEY_NAME]; + auto setting_id = info[BBL_JSON_KEY_SETTING_ID]; + auto update_time_str = info[BBL_JSON_KEY_UPDATE_TIME]; + long long update_time = 0; + if (!update_time_str.empty()) + update_time = std::atoll(update_time_str.c_str()); + if (type == "filament") { + return preset_bundle->filaments.need_sync(name, setting_id, update_time); + } else if (type == "print") { + return preset_bundle->prints.need_sync(name, setting_id, update_time); + } else if (type == "printer") { + return preset_bundle->printers.need_sync(name, setting_id, update_time); + } else { + return true; + } + }, progressFn, cancelFn); + finishFn(ret == 0); + + int count = 0, sync_count = 0; + std::vector presets_to_sync; + while (!t.expired()) { + count++; + if (count % 20 == 0) { + if (m_agent) { + if (!m_agent->is_user_login()) { + continue; + } + //sync preset + if (!preset_bundle) continue; + + int total_count = 0; + sync_count = preset_bundle->prints.get_user_presets(preset_bundle, presets_to_sync); + if (sync_count > 0) { + for (Preset& preset : presets_to_sync) { + sync_preset(&preset); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + } + } + total_count += sync_count; + + sync_count = preset_bundle->filaments.get_user_presets(preset_bundle, presets_to_sync); + if (sync_count > 0) { + for (Preset& preset : presets_to_sync) { + sync_preset(&preset); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + } + } + total_count += sync_count; + + sync_count = preset_bundle->printers.get_user_presets(preset_bundle, presets_to_sync); + if (sync_count > 0) { + for (Preset& preset : presets_to_sync) { + sync_preset(&preset); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + } + } + total_count += sync_count; + + if (total_count == 0) { + CallAfter([this] { + if (!m_is_closing) + plater()->get_notification_manager()->close_notification_of_type(NotificationType::BBLUserPresetExceedLimit); + }); + } + + unsigned int http_code = 200; + + /* get list witch need to be deleted*/ + std::vector delete_cache_presets = get_delete_cache_presets_lock(); + for (auto it = delete_cache_presets.begin(); it != delete_cache_presets.end();) { + if ((*it).empty()) continue; + std::string del_setting_id = *it; + int result = m_agent->delete_setting(del_setting_id); + if (result == 0) { + preset_deleted_from_cloud(del_setting_id); + it = delete_cache_presets.erase(it); + m_create_preset_blocked = { false, false, false, false, false, false }; + BOOST_LOG_TRIVIAL(trace) << "sync_preset: sync operation: delete success! setting id = " << del_setting_id; + } + else { + BOOST_LOG_TRIVIAL(info) << "delete setting = " <m_webview->SendDesignStaffpick(on); +} + +bool GUI_App::switch_language() +{ + if (select_language()) { + recreate_GUI(_L("Switching application language") + dots); + return true; + } else { + return false; + } +} + +#ifdef __linux__ +static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, + const wxLanguageInfo* system_language) +{ + constexpr size_t max_len = 50; + char path[max_len] = ""; + std::vector locales; + const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); + + // Call locale -a so we can parse the output to get the list of available locales + // We expect lines such as "en_US.utf8". Pick ones starting with the language code + // we are switching to. Lines with different formatting will be removed later. + FILE* fp = popen("locale -a", "r"); + if (fp != NULL) { + while (fgets(path, max_len, fp) != NULL) { + std::string line(path); + line = line.substr(0, line.find('\n')); + if (boost::starts_with(line, lang_prefix)) + locales.push_back(line); + } + pclose(fp); + } + + // locales now contain all candidates for this language. + // Sort them so ones containing anything about UTF-8 are at the end. + std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) + { + auto has_utf8 = [](const std::string & s) { + auto S = boost::to_upper_copy(s); + return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; + }; + return ! has_utf8(a) && has_utf8(b); + }); + + // Remove the suffix behind a dot, if there is one. + for (std::string& s : locales) + s = s.substr(0, s.find(".")); + + // We just hope that dear Linux "locale -a" returns country codes + // in ISO 3166-1 alpha-2 code (two letter) format. + // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes + // To be sure, remove anything not looking as expected + // (any number of lowercase letters, underscore, two uppercase letters). + locales.erase(std::remove_if(locales.begin(), + locales.end(), + [](const std::string& s) { + return ! std::regex_match(s, + std::regex("^[a-z]+_[A-Z]{2}$")); + }), + locales.end()); + + // Is there a candidate matching a country code of a system language? Move it to the end, + // while maintaining the order of matches, so that the best match ends up at the very end. + std::string temp_local = into_u8(system_language->CanonicalName.AfterFirst('_')); + if (temp_local.size() >= 2) { + temp_local = temp_local.substr(0, 2); + } + std::string system_country = "_" + temp_local; + int cnt = locales.size(); + for (int i=0; iempty()) { + const std::string &locale = *it; + const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); + if (wxLocale::IsAvailable(lang->Language)) + return lang; + } + return language; +} +#endif + +int GUI_App::GetSingleChoiceIndex(const wxString& message, + const wxString& caption, + const wxArrayString& choices, + int initialSelection) +{ +#ifdef _WIN32 + wxSingleChoiceDialog dialog(nullptr, message, caption, choices); + dialog.SetBackgroundColour(*wxWHITE); + wxGetApp().UpdateDlgDarkUI(&dialog); + + dialog.SetSelection(initialSelection); + return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; +#else + return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); +#endif +} + +// select language from the list of installed languages +bool GUI_App::select_language() +{ + wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); + std::vector language_infos; + language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); + for (size_t i = 0; i < translations.GetCount(); ++ i) { + const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); + if (langinfo != nullptr) + language_infos.emplace_back(langinfo); + } + sort_remove_duplicates(language_infos); + std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); + + wxArrayString names; + names.Alloc(language_infos.size()); + + // Some valid language should be selected since the application start up. + const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); + int init_selection = -1; + int init_selection_alt = -1; + int init_selection_default = -1; + for (size_t i = 0; i < language_infos.size(); ++ i) { + if (wxLanguage(language_infos[i]->Language) == current_language) + // The dictionary matches the active language and country. + init_selection = i; + else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || + // if the active language is Slovak, mark the Czech language as active. + (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) + // The dictionary matches the active language, it does not necessarily match the country. + init_selection_alt = i; + if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") + // This will be the default selection if the active language does not match any dictionary. + init_selection_default = i; + names.Add(language_infos[i]->Description); + } + if (init_selection == -1) + // This is the dictionary matching the active language. + init_selection = init_selection_alt; + if (init_selection != -1) + // This is the language to highlight in the choice dialog initially. + init_selection_default = init_selection; + + const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); + // Try to load a new language. + if (index != -1 && (init_selection == -1 || init_selection != index)) { + const wxLanguageInfo *new_language_info = language_infos[index]; + if (this->load_language(new_language_info->CanonicalName, false)) { + // Save language at application config. + // Which language to save as the selected dictionary language? + // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its + // stability in the future: + // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + // 2) Current locale language may not match the dictionary name, see GH issue #3901 + // m_wxLocale->GetCanonicalName() + // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. + app_config->set("language", new_language_info->CanonicalName.ToUTF8().data()); + app_config->save(); + return true; + } + } + + return false; +} + +// Load gettext translation files and activate them at the start of the application, +// based on the "language" key stored in the application config. +bool GUI_App::load_language(wxString language, bool initial) +{ + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: language %2%, initial: %3%") %__FUNCTION__ %language %initial; + if (initial) { + // There is a static list of lookup path prefixes in wxWidgets. Add ours. + wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); + // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. + language = app_config->get("language"); + if (! language.empty()) + BOOST_LOG_TRIVIAL(info) << boost::format("language provided by BambuStudio.conf: %1%") % language; + else { + // Get the system language. + const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); + if (lang_system != wxLANGUAGE_UNKNOWN) { + m_language_info_system = wxLocale::GetLanguageInfo(lang_system); +#ifdef __WXMSW__ + WCHAR wszLanguagesBuffer[LOCALE_NAME_MAX_LENGTH]; + ::LCIDToLocaleName(LOCALE_USER_DEFAULT, wszLanguagesBuffer, LOCALE_NAME_MAX_LENGTH, 0); + wxString lang(wszLanguagesBuffer); + lang.Replace('-', '_'); + if (auto info = wxLocale::FindLanguageInfo(lang)) + m_language_info_system = info; +#endif + BOOST_LOG_TRIVIAL(info) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); + // BBS set language to app config + app_config->set("language", m_language_info_system->CanonicalName.ToUTF8().data()); + } else { + { + // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. + wxLocale temp_locale; + temp_locale.Init(); + // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). + wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); + // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer + // and try to match them with the system specific "preferred languages". + // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). + // The last parameter gets added to the list of detected dictionaries. This is a workaround + // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. + wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + if (!best_language.IsEmpty()) { + m_language_info_best = wxLocale::FindLanguageInfo(best_language); + BOOST_LOG_TRIVIAL(info) << boost::format("Best translation language detected (may be different from user locales): %1%") % + m_language_info_best->CanonicalName.ToUTF8().data(); + app_config->set("language", m_language_info_best->CanonicalName.ToUTF8().data()); + } +#ifdef __linux__ + wxString lc_all; + if (wxGetEnv("LC_ALL", &lc_all) && !lc_all.IsEmpty()) { + // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. + // Disregard the "best" suggestion in case LC_ALL is provided. + m_language_info_best = nullptr; + } +#endif + } + } + } + } + + const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); + if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { + // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). + language_info = nullptr; + BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); + } + + if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { + BOOST_LOG_TRIVIAL(info) << boost::format("The following language code requires right to left layout, which is not supported by BambuStudio: %1%") % language_info->CanonicalName.ToUTF8().data(); + language_info = nullptr; + } + + if (language_info == nullptr) { + // PrusaSlicer does not support the Right to Left languages yet. + if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_system; + if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_best; + if (language_info == nullptr) + language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); + } + + BOOST_LOG_TRIVIAL(info) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); + + // Select language for locales. This language may be different from the language of the dictionary. + //if (language_info == m_language_info_best || language_info == m_language_info_system) { + // // The current language matches user's default profile exactly. That's great. + //} else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { + // // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. + // // This allows a Swiss guy to use a German dictionary without forcing him to German locales. + // language_info = m_language_info_best; + //} else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) + // language_info = m_language_info_system; + + // Alternate language code. + wxLanguage language_dict = wxLanguage(language_info->Language); + if (language_info->CanonicalName.BeforeFirst('_') == "sk") { + // Slovaks understand Czech well. Give them the Czech translation. + language_dict = wxLANGUAGE_CZECH; + BOOST_LOG_TRIVIAL(info) << "Using Czech dictionaries for Slovak language"; + } + +#ifdef __linux__ + // If we can't find this locale , try to use different one for the language + // instead of just reporting that it is impossible to switch. + if (! wxLocale::IsAvailable(language_info->Language) && m_language_info_system) { + std::string original_lang = into_u8(language_info->CanonicalName); + language_info = linux_get_existing_locale_language(language_info, m_language_info_system); + BOOST_LOG_TRIVIAL(info) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") + % original_lang % language_info->CanonicalName.ToUTF8().data(); + } +#endif + + if (! wxLocale::IsAvailable(language_info->Language)&&initial) { + language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_UK); + app_config->set("language", language_info->CanonicalName.ToUTF8().data()); + } + else if (initial) { + // bbs supported languages + //TODO: use a global one with Preference + //wxLanguage supported_languages[]{ + // wxLANGUAGE_ENGLISH, + // wxLANGUAGE_CHINESE_SIMPLIFIED, + // wxLANGUAGE_GERMAN, + // wxLANGUAGE_FRENCH, + // wxLANGUAGE_SPANISH, + // wxLANGUAGE_SWEDISH, + // wxLANGUAGE_DUTCH, + // wxLANGUAGE_HUNGARIAN, + // wxLANGUAGE_JAPANESE, + // wxLANGUAGE_ITALIAN + //}; + //std::string cur_language = app_config->get("language"); + //if (cur_language != "") { + // //cleanup the language wrongly set before + // const wxLanguageInfo *langinfo = nullptr; + // bool embedded_language = false; + // int language_num = sizeof(supported_languages) / sizeof(supported_languages[0]); + // for (auto index = 0; index < language_num; index++) { + // langinfo = wxLocale::GetLanguageInfo(supported_languages[index]); + // std::string temp_lan = langinfo->CanonicalName.ToUTF8().data(); + // if (cur_language == temp_lan) { + // embedded_language = true; + // break; + // } + // } + // if (!embedded_language) + // app_config->erase("app", "language"); + //} + } + + if (! wxLocale::IsAvailable(language_info->Language)) { + // Loading the language dictionary failed. + wxString message = "Switching Bambu Studio to language " + language_info->CanonicalName + " failed."; +#if !defined(_WIN32) && !defined(__APPLE__) + // likely some linux system + message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; +#endif + if (initial) + message + "\n\nApplication will close."; + wxMessageBox(message, "Bambu Studio - Switching language failed", wxOK | wxICON_ERROR); + if (initial) + std::exit(EXIT_FAILURE); + else + return false; + } + + // Release the old locales, create new locales. + //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. + m_wxLocale.release(); + m_wxLocale = Slic3r::make_unique(); + m_wxLocale->Init(language_info->Language); + // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) + // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. + wxTranslations::Get()->SetLanguage(language_dict); + m_wxLocale->AddCatalog(SLIC3R_APP_KEY); + m_imgui->set_language(into_u8(language_info->CanonicalName)); + + //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. + //wxSetlocale(LC_NUMERIC, "C"); + Preset::update_suffix_modified((_L("*") + " ").ToUTF8().data()); + HintDatabase::get_instance().reinit(); + return true; +} + +Tab* GUI_App::get_tab(Preset::Type type) +{ + for (Tab* tab: tabs_list) + if (tab->type() == type) + return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab + return nullptr; +} + +Tab* GUI_App::get_plate_tab() +{ + return plate_tab; +} + +Tab* GUI_App::get_model_tab(bool part) +{ + return model_tabs_list[part ? 1 : 0]; +} + +Tab* GUI_App::get_layer_tab() +{ + return model_tabs_list[2]; +} + +ConfigOptionMode GUI_App::get_mode() +{ + if (!app_config->has("user_mode")) + return comSimple; + //BBS + const auto mode = app_config->get("user_mode"); + return mode == "advanced" ? comAdvanced : + mode == "simple" ? comSimple : + mode == "develop" ? comDevelop : comSimple; +} + +std::string GUI_App::get_mode_str() +{ + if (!app_config->has("user_mode")) + return "simple"; + return app_config->get("user_mode"); +} + +void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) +{ + //BBS + const std::string mode_str = mode == comAdvanced ? "advanced" : + mode == comSimple ? "simple" : + mode == comDevelop ? "develop" : "simple"; + app_config->set("user_mode", mode_str); + app_config->save(); + update_mode(); +} + +// Update view mode according to selected menu +void GUI_App::update_mode() +{ + sidebar().update_mode(); + + //BBS: GUI refactor + if (mainframe->m_param_panel) + mainframe->m_param_panel->update_mode(); + if (mainframe->m_param_dialog) + mainframe->m_param_dialog->panel()->update_mode(); + +#ifdef _MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); +#endif + + for (auto tab : tabs_list) + tab->update_mode(); + for (auto tab : model_tabs_list) + tab->update_mode(); + + //BBS plater()->update_menus(); + + plater()->canvas3D()->update_gizmos_on_off_state(); +} + +void GUI_App::update_internal_development() { + mainframe->m_webview->update_mode(); +} + +void GUI_App::show_ip_address_enter_dialog(wxString title) +{ + auto evt = new wxCommandEvent(EVT_SHOW_IP_DIALOG); + evt->SetString(title); + wxQueueEvent(this, evt); +} + +bool GUI_App::show_modal_ip_address_enter_dialog(wxString title) +{ + DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) return false; + if (!dev->get_selected_machine()) return false; + auto obj = dev->get_selected_machine(); + + InputIpAddressDialog dlg(nullptr); + dlg.set_machine_obj(obj); + if (!title.empty()) dlg.update_title(title); + + dlg.Bind(EVT_ENTER_IP_ADDRESS, [this, obj](wxCommandEvent& e) { + auto selection_data_arr = wxSplit(e.GetString().ToStdString(), '|'); + + if (selection_data_arr.size() == 2) { + auto ip_address = selection_data_arr[0]; + auto access_code = selection_data_arr[1]; + + BOOST_LOG_TRIVIAL(info) << "User enter IP address is " << format_IP(ip_address.ToStdString()); + if (!ip_address.empty()) { + wxGetApp().app_config->set_str("ip_address", obj->dev_id, ip_address.ToStdString()); + wxGetApp().app_config->save(); + + obj->dev_ip = ip_address.ToStdString(); + obj->set_user_access_code(access_code.ToStdString()); + } + } + }); + + if (dlg.ShowModal() == wxID_YES) { + return true; + } + return false; +} + +void GUI_App::show_ip_address_enter_dialog_handler(wxCommandEvent& evt) +{ + wxString title = evt.GetString(); + show_modal_ip_address_enter_dialog(title); +} + +//void GUI_App::add_config_menu(wxMenuBar *menu) +//void GUI_App::add_config_menu(wxMenu *menu) +//{ +// auto local_menu = new wxMenu(); +// wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); +// +// const auto config_wizard_name = _(ConfigWizard::name(true)); +// const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Open %s"))) % config_wizard_name).str()); +// // Cmd+, is standard on OS X - what about other operating systems? +// if (is_editor()) { +// local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); +// local_menu->Append(config_id_base + ConfigMenuUpdate, _L("Check for Configuration Updates"), _L("Check for configuration updates")); +// local_menu->AppendSeparator(); +// } +// local_menu->Append(config_id_base + ConfigMenuPreferences, _L("Preferences") + dots + +//#ifdef __APPLE__ +// "\tCtrl+,", +//#else +// "\tCtrl+P", +//#endif +// _L("Application preferences")); +// wxMenu* mode_menu = nullptr; +// if (is_editor()) { +// local_menu->AppendSeparator(); +// mode_menu = new wxMenu(); +// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple Mode")); +// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced Mode")); +// Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); +// Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); +// +// local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s Mode"), SLIC3R_APP_NAME)); +// } +// local_menu->AppendSeparator(); +// local_menu->Append(config_id_base + ConfigMenuLanguage, _L("Language")); +// if (is_editor()) { +// local_menu->AppendSeparator(); +// } +// +// local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { +// switch (event.GetId() - config_id_base) { +// case ConfigMenuWizard: +// run_wizard(ConfigWizard::RR_USER); +// break; +// case ConfigMenuUpdate: +// check_updates(true); +// break; +//#ifdef __linux__ +// case ConfigMenuDesktopIntegration: +// show_desktop_integration_dialog(); +// break; +//#endif +// case ConfigMenuSnapshots: +// //BBS do not support task snapshot +// break; +// case ConfigMenuPreferences: +// { +// //BBS GUI refactor: remove unuse layout logic +// //bool app_layout_changed = false; +// { +// // the dialog needs to be destroyed before the call to recreate_GUI() +// // or sometimes the application crashes into wxDialogBase() destructor +// // so we put it into an inner scope +// PreferencesDialog dlg(mainframe); +// dlg.ShowModal(); +// //BBS GUI refactor: remove unuse layout logic +// //app_layout_changed = dlg.settings_layout_changed(); +// if (dlg.seq_top_layer_only_changed()) +// this->plater_->refresh_print(); +// +// if (dlg.recreate_GUI()) { +// recreate_GUI(_L("Restart application") + dots); +// return; +// } +//#ifdef _WIN32 +// if (is_editor()) { +// if (app_config->get("associate_3mf") == "true") +// associate_3mf_files(); +// if (app_config->get("associate_stl") == "true") +// associate_stl_files(); +// } +// else { +// if (app_config->get("associate_gcode") == "true") +// associate_gcode_files(); +// } +//#endif // _WIN32 +// } +// //BBS GUI refactor: remove unuse layout logic +// /*if (app_layout_changed) { +// // hide full main_sizer for mainFrame +// mainframe->GetSizer()->Show(false); +// mainframe->update_layout(); +// mainframe->select_tab(size_t(0)); +// }*/ +// break; +// } +// case ConfigMenuLanguage: +// { +// /* Before change application language, let's check unsaved changes on 3D-Scene +// * and draw user's attention to the application restarting after a language change +// */ +// { +// // the dialog needs to be destroyed before the call to switch_language() +// // or sometimes the application crashes into wxDialogBase() destructor +// // so we put it into an inner scope +// wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); +// title += " - " + _L("Choose language"); +// //wxMessageDialog dialog(nullptr, +// MessageDialog dialog(nullptr, +// _L("Switching the language requires application restart.\n") + "\n\n" + +// _L("Do you want to continue?"), +// title, +// wxICON_QUESTION | wxOK | wxCANCEL); +// if (dialog.ShowModal() == wxID_CANCEL) +// return; +// } +// +// switch_language(); +// break; +// } +// case ConfigMenuFlashFirmware: +// //BBS FirmwareDialog::run(mainframe); +// break; +// default: +// break; +// } +// }); +// +// using std::placeholders::_1; +// +// if (mode_menu != nullptr) { +// auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; +// mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); +// mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); +// } +// +// // BBS +// //menu->Append(local_menu, _L("Configuration")); +// menu->AppendSubMenu(local_menu, _L("Configuration")); +//} + +void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_option) +{ + bool app_layout_changed = false; + { + // the dialog needs to be destroyed before the call to recreate_GUI() + // or sometimes the application crashes into wxDialogBase() destructor + // so we put it into an inner scope + PreferencesDialog dlg(mainframe, open_on_tab, highlight_option); + dlg.ShowModal(); + // BBS + //app_layout_changed = dlg.settings_layout_changed(); +#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER + if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) +#else + if (dlg.seq_top_layer_only_changed()) +#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER + this->plater_->refresh_print(); +#ifdef _WIN32 + if (is_editor()) { + if (app_config->get("associate_3mf") == "true") + associate_files(L"3mf"); + if (app_config->get("associate_stl") == "true") + associate_files(L"stl"); + if (app_config->get("associate_step") == "true") { + associate_files(L"step"); + associate_files(L"stp"); + } + } + else { + if (app_config->get("associate_gcode") == "true") + associate_files(L"gcode"); + } +#endif // _WIN32 + } + + // BBS + /* + if (app_layout_changed) { + // hide full main_sizer for mainFrame + mainframe->GetSizer()->Show(false); + mainframe->update_layout(); + mainframe->select_tab(size_t(0)); + }*/ +} + +bool GUI_App::has_unsaved_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) + return true; + } + return false; +} + +bool GUI_App::has_current_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + return true; + } + return false; +} + +void GUI_App::update_saved_preset_from_current_preset() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->update_saved_preset_from_current_preset(); + } +} + +std::vector> GUI_App::get_selected_presets() const +{ + std::vector> ret; + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) { + const PresetCollection* presets = tab->get_presets(); + ret.push_back({ static_cast(presets->type()), presets->get_selected_preset_name() }); + } + } + return ret; +} + +// To notify the user whether he is aware that some preset changes will be lost, +// UnsavedChangesDialog: "Discard / Save / Cancel" +// This is called when: +// - Close Application & Current project isn't saved +// - Load Project & Current project isn't saved +// - Undo / Redo with change of print technologie +// - Loading snapshot +// - Loading config_file/bundle +// UnsavedChangesDialog: "Don't save / Save / Cancel" +// This is called when: +// - Exporting config_bundle +// - Taking snapshot +bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) +{ + if (has_current_preset_changes()) { + int act_buttons = UnsavedChangesDialog::ActionButtons::SAVE; + if (dont_save_insted_of_discard) + act_buttons |= UnsavedChangesDialog::ActionButtons::DONT_SAVE; + if (remember_choice) + act_buttons |= UnsavedChangesDialog::ActionButtons::REMEMBER_CHOISE; + UnsavedChangesDialog dlg(caption, header, "", act_buttons); + if (dlg.ShowModal() == wxID_CANCEL) + return false; + + if (dlg.save_preset()) // save selected changes + { + //BBS: add project embedded preset relate logic + for (const UnsavedChangesDialog::PresetData& nt : dlg.get_names_and_types()) + preset_bundle->save_changes_for_preset(nt.name, nt.type, dlg.get_unselected_options(nt.type), nt.save_to_project); + //for (const std::pair& nt : dlg.get_names_and_types()) + // preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + load_current_presets(false); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + //MessageDialog(nullptr, _L_PLURAL("Modifications to the preset have been saved", + // "Modifications to the presets have been saved", dlg.get_names_and_types().size())).ShowModal(); + } + } + + return true; +} + +void GUI_App::apply_keeped_preset_modifications() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->apply_config_from_cache(); + } + load_current_presets(false); +} + +// This is called when creating new project or load another project +// OR close ConfigWizard +// to ask the user what should we do with unsaved changes for presets. +// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" +// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed +bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) +{ + if (has_current_preset_changes()) { + bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; + + UnsavedChangesDialog dlg(caption, header, "", action_buttons); + if (dlg.ShowModal() == wxID_CANCEL) + return false; + + auto reset_modifications = [this, is_called_from_configwizard]() { + //if (is_called_from_configwizard) + // return; // no need to discared changes. It will be done fromConfigWizard closing + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + tab->m_presets->discard_current_changes(); + } + load_current_presets(false); + }; + + if (dlg.discard()) + reset_modifications(); + else // save selected changes + { + //BBS: add project embedded preset relate logic + const auto& preset_names_and_types = dlg.get_names_and_types(); + if (dlg.save_preset()) { + for (const UnsavedChangesDialog::PresetData& nt : preset_names_and_types) + preset_bundle->save_changes_for_preset(nt.name, nt.type, dlg.get_unselected_options(nt.type), nt.save_to_project); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + //wxString text = _L_PLURAL("Modifications to the preset have been saved", + // "Modifications to the presets have been saved", preset_names_and_types.size()); + //if (!is_called_from_configwizard) + // text += "\n\n" + _L("All modifications will be discarded for new project."); + + //MessageDialog(nullptr, text).ShowModal(); + reset_modifications(); + } + else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { + // execute this part of code only if not all modifications are keeping to the new project + // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected + for (const UnsavedChangesDialog::PresetData& nt : preset_names_and_types) { + Preset::Type type = nt.type; + Tab* tab = get_tab(type); + std::vector selected_options = dlg.get_selected_options(type); + if (type == Preset::TYPE_PRINTER) { + auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); + if (it != selected_options.end()) { + // erase "extruders_count" option from the list + selected_options.erase(it); + // cache the extruders count + static_cast(tab)->cache_extruder_cnt(); + } + } + std::vector selected_options2; + std::transform(selected_options.begin(), selected_options.end(), std::back_inserter(selected_options2), [](auto & o) { + auto i = o.find('#'); + return i != std::string::npos ? o.substr(0, i) : o; + }); + tab->cache_config_diff(selected_options2); + if (!is_called_from_configwizard) + tab->m_presets->discard_current_changes(); + } + if (is_called_from_configwizard) + *postponed_apply_of_keeped_changes = true; + else + apply_keeped_preset_modifications(); + } + } + } + + return true; +} + +bool GUI_App::can_load_project() +{ + return true; +} + +bool GUI_App::check_print_host_queue() +{ + wxString dirty; + std::vector> jobs; + // Get ongoing jobs from dialog + mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); + if (jobs.empty()) + return true; + // Show dialog + wxString job_string = wxString(); + for (const auto& job : jobs) { + job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); + } + wxString message; + message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); + //wxMessageDialog dialog(mainframe, + MessageDialog dialog(mainframe, + message, + wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), + wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); + if (dialog.ShowModal() == wxID_YES) + return true; + + // TODO: If already shown, bring forward + mainframe->m_printhost_queue_dlg->Show(); + return false; +} + +bool GUI_App::checked_tab(Tab* tab) +{ + bool ret = true; + if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end() && + find(model_tabs_list.begin(), model_tabs_list.end(), tab) == model_tabs_list.end()) + ret = false; + return ret; +} + +// Update UI / Tabs to reflect changes in the currently loaded presets +//BBS: add preset combo box re-activate logic +void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool check_printer_presets_ /*= true*/) +{ + // check printer_presets for the containing information about "Print Host upload" + // and create physical printer from it, if any exists + if (check_printer_presets_) + check_printer_presets(); + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + this->plater()->set_printer_technology(printer_technology); + for (Tab *tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) { + if (tab->type() == Preset::TYPE_PRINTER) { + static_cast(tab)->update_pages(); + // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). + this->plater()->force_print_bed_update(); + } + tab->load_current_preset(); + //BBS: add preset combox re-active logic + if (active_preset_combox) + tab->reactive_preset_combo_box(); + } + // BBS: model config + for (Tab *tab : model_tabs_list) + if (tab->supports_printer_technology(printer_technology)) { + tab->rebuild_page_tree(); + } +} + +static std::mutex mutex_delete_cache_presets; + +std::vector & GUI_App::get_delete_cache_presets() +{ + return need_delete_presets; +} + +std::vector GUI_App::get_delete_cache_presets_lock() +{ + std::scoped_lock l(mutex_delete_cache_presets); + return need_delete_presets; +} + +void GUI_App::delete_preset_from_cloud(std::string setting_id) +{ + std::scoped_lock l(mutex_delete_cache_presets); + need_delete_presets.push_back(setting_id); +} + +void GUI_App::preset_deleted_from_cloud(std::string setting_id) +{ + std::scoped_lock l(mutex_delete_cache_presets); + need_delete_presets.erase(std::remove(need_delete_presets.begin(), need_delete_presets.end(), setting_id), need_delete_presets.end()); +} + +wxString GUI_App::filter_string(wxString str) +{ + std::string result = str.utf8_string(); + std::string input = str.utf8_string(); + + + std::regex domainRegex(R"(([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:\.[a-zA-Z]{2,})?))"); + std::sregex_iterator it(input.begin(), input.end(), domainRegex); + std::sregex_iterator end; + + while (it != end) { + std::smatch match = *it; + std::string domain = match.str(); + result.replace(match.position(), domain.length(), "[***]"); + ++it; + } + + return wxString::FromUTF8(result); +} + +bool GUI_App::OnExceptionInMainLoop() +{ + generic_exception_handle(); + return false; +} + +#ifdef __APPLE__ +// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run +// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App +// to a G-code viewer. +void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) +{ + //BBS: remove GCodeViewer as seperate APP logic + /*size_t num_gcodes = 0; + for (const wxString &filename : fileNames) + if (is_gcode_file(into_u8(filename))) + ++ num_gcodes; + if (fileNames.size() == num_gcodes) { + // Opening PrusaSlicer by drag & dropping a G-Code onto BambuStudio icon in Finder, + // just G-codes were passed. Switch to G-code viewer mode. + m_app_mode = EAppMode::GCodeViewer; + unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); + if(app_config != nullptr) + delete app_config; + app_config = nullptr; + init_app_config(); + }*/ + wxApp::OSXStoreOpenFiles(fileNames); +} + +void GUI_App::MacOpenURL(const wxString& url) +{ + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << "get mac url " << url; + + if (!url.empty() && boost::starts_with(url, "bambustudioopen://")) { + auto input_str_arr = split_str(url.ToStdString(), "bambustudioopen://"); + + std::string download_origin_url; + for (auto input_str : input_str_arr) { + if (!input_str.empty()) download_origin_url = input_str; + } + + std::string download_file_url = url_decode(download_origin_url); + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << download_file_url; + if (!download_file_url.empty() && (boost::starts_with(download_file_url, "http://") || boost::starts_with(download_file_url, "https://"))) { + + if (m_post_initialized) { + request_model_download(download_file_url); + } + else { + m_download_file_url = download_file_url; + } + } + } +} + +// wxWidgets override to get an event on open files. +void GUI_App::MacOpenFiles(const wxArrayString &fileNames) +{ + bool single_instance = app_config->get("app", "single_instance") == "true"; + if (m_post_initialized && !single_instance) { + bool has3mf = false; + std::vector names; + for (auto & n : fileNames) { + has3mf |= n.EndsWith(".3mf"); + names.push_back(n); + } + if (has3mf) { + start_new_slicer(names); + return; + } + } + std::vector files; + std::vector gcode_files; + std::vector non_gcode_files; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", open files, size " << fileNames.size(); + for (const auto& filename : fileNames) { + if (is_gcode_file(into_u8(filename))) + gcode_files.emplace_back(filename); + else { + files.emplace_back(into_u8(filename)); + non_gcode_files.emplace_back(filename); + } + } + //BBS: remove GCodeViewer as seperate APP logic + /*if (m_app_mode == EAppMode::GCodeViewer) { + // Running in G-code viewer. + // Load the first G-code into the G-code viewer. + // Or if no G-codes, send other files to slicer. + if (! gcode_files.empty()) + this->plater()->load_gcode(gcode_files.front()); + if (!non_gcode_files.empty()) + start_new_slicer(non_gcode_files, true); + } else*/ + { + if (! files.empty()) { + if (m_post_initialized) { + wxArrayString input_files; + for (size_t i = 0; i < non_gcode_files.size(); ++i) { + input_files.push_back(non_gcode_files[i]); + } + this->plater()->load_files(input_files); + } + else { + for (size_t i = 0; i < files.size(); ++i) { + this->init_params->input_files.emplace_back(files[i]); + } + } + } + else { + if (m_post_initialized) { + this->plater()->load_gcode(gcode_files.front()); + } + else { + this->init_params->input_gcode = true; + this->init_params->input_files = { into_u8(gcode_files.front()) }; + } + } + /*for (const wxString &filename : gcode_files) + start_new_gcodeviewer(&filename);*/ + } +} + +#endif /* __APPLE */ + +Sidebar& GUI_App::sidebar() +{ + return plater_->sidebar(); +} + +GizmoObjectManipulation *GUI_App::obj_manipul() +{ + // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) + return (plater_ != nullptr) ? &plater_->get_view3D_canvas3D()->get_gizmos_manager().get_object_manipulation() : nullptr; +} + +ObjectSettings* GUI_App::obj_settings() +{ + return sidebar().obj_settings(); +} + +ObjectList* GUI_App::obj_list() +{ + return sidebar().obj_list(); +} + +ObjectLayers* GUI_App::obj_layers() +{ + return sidebar().obj_layers(); +} + +Plater* GUI_App::plater() +{ + return plater_; +} + +const Plater* GUI_App::plater() const +{ + return plater_; +} + +ParamsPanel* GUI_App::params_panel() +{ + if (mainframe) + return mainframe->m_param_panel; + return nullptr; +} + +ParamsDialog* GUI_App::params_dialog() +{ + if (mainframe) + return mainframe->m_param_dialog; + return nullptr; +} + +Model& GUI_App::model() +{ + return plater_->model(); +} + +void GUI_App::load_url(wxString url) +{ + if (mainframe) + return mainframe->load_url(url); +} + +void GUI_App::open_mall_page_dialog() +{ + std::string host_url; + std::string model_url; + std::string link_url; + + int result = -1; + + //model api url + host_url = get_model_http_url(app_config->get_country_code()); + + //model url + + wxString language_code = this->current_language_code().BeforeFirst('_'); + model_url = language_code.ToStdString(); + + if (getAgent() && mainframe) { + + //login already + if (getAgent()->is_user_login()) { + std::string ticket; + result = getAgent()->request_bind_ticket(&ticket); + + if(result == 0){ + link_url = host_url + "api/sign-in/ticket?to=" + host_url + url_encode(model_url) + "&ticket=" + ticket; + } + } + } + + if (result < 0) { + link_url = host_url + model_url; + } + + if (link_url.find("?") != std::string::npos) { + link_url += "&from=bambustudio"; + } else { + link_url += "?from=bambustudio"; + } + + wxLaunchDefaultBrowser(link_url); +} + +void GUI_App::open_publish_page_dialog() +{ + std::string host_url; + std::string model_url; + std::string link_url; + + int result = -1; + + //model api url + host_url = get_model_http_url(app_config->get_country_code()); + + //publish url + wxString language_code = this->current_language_code().BeforeFirst('_'); + model_url += (language_code.ToStdString() + "/my/models/publish"); + + if (getAgent() && mainframe) { + + //login already + if (getAgent()->is_user_login()) { + std::string ticket; + result = getAgent()->request_bind_ticket(&ticket); + + if (result == 0) { + link_url = host_url + "api/sign-in/ticket?to=" + host_url + url_encode(model_url) + "&ticket=" + ticket; + } + } + } + + if (result < 0) { + link_url = host_url + model_url; + } + + wxLaunchDefaultBrowser(link_url); +} + +char GUI_App::from_hex(char ch) { + return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10; +} + +std::string GUI_App::url_decode(std::string value) { + return Http::url_decode(value); +} + +std::string GUI_App::url_encode(std::string value) { + return Http::url_encode(value); +} + +void GUI_App::popup_ping_bind_dialog() +{ + if (m_ping_code_binding_dialog == nullptr) { + m_ping_code_binding_dialog = new PingCodeBindDialog(); + m_ping_code_binding_dialog->ShowModal(); + remove_ping_bind_dialog(); + } +} + +void GUI_App::remove_ping_bind_dialog() +{ + if (m_ping_code_binding_dialog != nullptr) { + m_ping_code_binding_dialog->Destroy(); + delete m_mall_publish_dialog; + m_ping_code_binding_dialog = nullptr; + } +} + + +void GUI_App::remove_mall_system_dialog() +{ + if (m_mall_publish_dialog != nullptr) { + m_mall_publish_dialog->Destroy(); + delete m_mall_publish_dialog; + } +} + +void GUI_App::run_script(wxString js) +{ + if (mainframe) + return mainframe->RunScript(js); +} + +void GUI_App::run_script_left(wxString js) +{ + if (mainframe) + return mainframe->RunScriptLeft(js); +} + +Notebook* GUI_App::tab_panel() const +{ + if (mainframe) + return mainframe->m_tabpanel; + return nullptr; +} + +NotificationManager * GUI_App::notification_manager() +{ + if (plater_) + return plater_->get_notification_manager(); + return nullptr; +} + +// extruders count from selected printer preset +int GUI_App::extruders_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_selected_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +// extruders count from edited printer preset +int GUI_App::extruders_edited_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_edited_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +// BBS +int GUI_App::filaments_cnt() const +{ + return preset_bundle->filament_presets.size(); +} + +PrintSequence GUI_App::global_print_sequence() const +{ + PrintSequence global_print_seq = PrintSequence::ByDefault; + auto curr_preset_config = preset_bundle->prints.get_edited_preset().config; + if (curr_preset_config.has("print_sequence")) + global_print_seq = curr_preset_config.option>("print_sequence")->value; + return global_print_seq; +} + +wxString GUI_App::current_language_code_safe() const +{ + // Translate the language code to a code, for which Prusa Research maintains translations. + const std::map mapping { + { "cs", "cs_CZ", }, + { "sk", "cs_CZ", }, + { "de", "de_DE", }, + { "nl", "nl_NL", }, + { "sv", "sv_SE", }, + { "es", "es_ES", }, + { "fr", "fr_FR", }, + { "it", "it_IT", }, + { "ja", "ja_JP", }, + { "ko", "ko_KR", }, + { "pl", "pl_PL", }, + { "uk", "uk_UA", }, + { "zh", "zh_CN", }, + { "ru", "ru_RU", }, + { "tr", "tr_TR", }, + { "pt", "pt_BR", }, + { "hu", "hu_HU", }, + }; + wxString language_code = this->current_language_code().BeforeFirst('_'); + auto it = mapping.find(language_code); + if (it != mapping.end()) + language_code = it->second; + else + language_code = "en_US"; + return language_code; +} + +void GUI_App::open_web_page_localized(const std::string &http_address) +{ + open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe()); +} + +// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). +// Because of we can't to print the multi-part objects with SLA technology. +bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) +{ + if (model_has_multi_part_objects(model())) { + // BBS: remove SLA related message + return false; + } + return true; +} + +bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) +{ + wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + + //if (reason == ConfigWizard::RR_USER) { + // //TODO: turn off it currently, maybe need to turn on in the future + // if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) + // return false; + //} + + //auto wizard_t = new ConfigWizard(mainframe); + //const bool res = wizard_t->run(reason, start_page); + + std::string strFinish = wxGetApp().app_config->get("firstguide", "finish"); + long pStyle = wxCAPTION | wxCLOSE_BOX | wxSYSTEM_MENU; + if (strFinish == "false" || strFinish.empty()) + pStyle = wxCAPTION | wxTAB_TRAVERSAL; + + GuideFrame wizard(this, pStyle); + auto page = start_page == ConfigWizard::SP_WELCOME ? GuideFrame::BBL_WELCOME : + start_page == ConfigWizard::SP_FILAMENTS ? GuideFrame::BBL_FILAMENT_ONLY : + start_page == ConfigWizard::SP_PRINTERS ? GuideFrame::BBL_MODELS_ONLY : + GuideFrame::BBL_MODELS; + wizard.SetStartPage(page); + bool res = wizard.run(); + + if (res) { + load_current_presets(); + update_publish_status(); + mainframe->refresh_plugin_tips(); + // BBS: remove SLA related message + } + + return res; +} + +void GUI_App::show_desktop_integration_dialog() +{ +#ifdef __linux__ + //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + DesktopIntegrationDialog dialog(mainframe); + dialog.ShowModal(); +#endif //__linux__ +} + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +void GUI_App::gcode_thumbnails_debug() +{ + const std::string BEGIN_MASK = "; thumbnail begin"; + const std::string END_MASK = "; thumbnail end"; + std::string gcode_line; + bool reading_image = false; + unsigned int width = 0; + unsigned int height = 0; + + wxFileDialog dialog(GetTopWindow(), _L("Select a G-code file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + std::string in_filename = into_u8(dialog.GetPath()); + std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); + + boost::nowide::ifstream in_file(in_filename.c_str()); + std::vector rows; + std::string row; + if (in_file.good()) + { + while (std::getline(in_file, gcode_line)) + { + if (in_file.good()) + { + if (boost::starts_with(gcode_line, BEGIN_MASK)) + { + reading_image = true; + gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); + std::string::size_type x_pos = gcode_line.find('x'); + std::string width_str = gcode_line.substr(0, x_pos); + width = (unsigned int)::atoi(width_str.c_str()); + std::string height_str = gcode_line.substr(x_pos + 1); + height = (unsigned int)::atoi(height_str.c_str()); + row.clear(); + } + else if (reading_image && boost::starts_with(gcode_line, END_MASK)) + { + std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; + boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); + if (out_file.good()) + { + std::string decoded; + decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); + decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); + + out_file.write(decoded.c_str(), decoded.size()); + out_file.close(); + } + + reading_image = false; + width = 0; + height = 0; + rows.clear(); + } + else if (reading_image) + row += gcode_line.substr(2); + } + } + + in_file.close(); + } +} +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + WindowMetrics metrics = WindowMetrics::from_window(window); + app_config->set(config_key, metrics.serialize()); + app_config->save(); +} + +bool GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) +{ + if (name.empty()) { return false; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + if (! app_config->has(config_key)) { + //window->Maximize(default_maximized); + return false; + } + + auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); + if (! metrics) { + window->Maximize(default_maximized); + return true; + } + + const wxRect& rect = metrics->get_rect(); + window->SetPosition(rect.GetPosition()); + window->SetSize(rect.GetSize()); + window->Maximize(metrics->get_maximized()); + return true; +} + +void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) +{ + /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); + wxRect display; + if (display_idx == wxNOT_FOUND) { + display = wxDisplay(0u).GetClientArea(); + window->Move(display.GetTopLeft()); + } else { + display = wxDisplay(display_idx).GetClientArea(); + } + + auto metrics = WindowMetrics::from_window(window); + metrics.sanitize_for_display(display); + if (window->GetScreenRect() != metrics.get_rect()) { + window->SetSize(metrics.get_rect()); + } +} + +void GUI_App::window_pos_center(wxTopLevelWindow *window) +{ + /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); + wxRect display; + if (display_idx == wxNOT_FOUND) { + display = wxDisplay(0u).GetClientArea(); + window->Move(display.GetTopLeft()); + } else { + display = wxDisplay(display_idx).GetClientArea(); + } + + auto metrics = WindowMetrics::from_window(window); + metrics.center_for_display(display); + if (window->GetScreenRect() != metrics.get_rect()) { + window->SetSize(metrics.get_rect()); + } +} + +bool GUI_App::config_wizard_startup() +{ + if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { + BOOST_LOG_TRIVIAL(info) << "run wizard..."; + run_wizard(ConfigWizard::RR_DATA_EMPTY); + BOOST_LOG_TRIVIAL(info) << "finished run wizard"; + return true; + } /*else if (get_app_config()->legacy_datadir()) { + // Looks like user has legacy pre-vendorbundle data directory, + // explain what this is and run the wizard + + MsgDataLegacy dlg; + dlg.ShowModal(); + + run_wizard(ConfigWizard::RR_DATA_LEGACY); + return true; + }*/ + return false; +} + +void GUI_App::check_updates(const bool verbose) +{ + PresetUpdater::UpdateResult updater_result; + try { + updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); + if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { + mainframe->Close(); + } + else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { + m_app_conf_exists = true; + } + else if (verbose && updater_result == PresetUpdater::R_NOOP) { + MsgNoUpdates dlg; + dlg.ShowModal(); + } + } + catch (const std::exception & ex) { + show_error(nullptr, ex.what()); + } +} + +bool GUI_App::open_browser_with_warning_dialog(const wxString& url, int flags/* = 0*/) +{ + return wxLaunchDefaultBrowser(url, flags); +} + +// static method accepting a wxWindow object as first parameter +// void warning_catcher{ +// my($self, $message_dialog) = @_; +// return sub{ +// my $message = shift; +// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; +// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); +// $message_dialog +// ? $message_dialog->(@params) +// : Wx::MessageDialog->new($self, @params)->ShowModal; +// }; +// } + +// Do we need this function??? +// void GUI_App::notify(message) { +// auto frame = GetTopWindow(); +// // try harder to attract user attention on OS X +// if (!frame->IsActive()) +// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); +// +// // There used to be notifier using a Growl application for OSX, but Growl is dead. +// // The notifier also supported the Linux X D - bus notifications, but that support was broken. +// //TODO use wxNotificationMessage ? +// } + + +#ifdef __WXMSW__ +static bool set_into_win_registry(HKEY hkeyHive, const wchar_t* pszVar, const wchar_t* pszValue) +{ + // see as reference: https://stackoverflow.com/questions/20245262/c-program-needs-an-file-association + wchar_t szValueCurrent[1000]; + DWORD dwType; + DWORD dwSize = sizeof(szValueCurrent); + + int iRC = ::RegGetValueW(hkeyHive, pszVar, nullptr, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize); + + bool bDidntExist = iRC == ERROR_FILE_NOT_FOUND; + + if ((iRC != ERROR_SUCCESS) && !bDidntExist) + // an error occurred + return false; + + if (!bDidntExist) { + if (dwType != REG_SZ) + // invalid type + return false; + + if (::wcscmp(szValueCurrent, pszValue) == 0) + // value already set + return false; + } + + DWORD dwDisposition; + HKEY hkey; + iRC = ::RegCreateKeyExW(hkeyHive, pszVar, 0, 0, 0, KEY_ALL_ACCESS, nullptr, &hkey, &dwDisposition); + bool ret = false; + if (iRC == ERROR_SUCCESS) { + iRC = ::RegSetValueExW(hkey, L"", 0, REG_SZ, (BYTE*)pszValue, (::wcslen(pszValue) + 1) * sizeof(wchar_t)); + if (iRC == ERROR_SUCCESS) + ret = true; + } + + RegCloseKey(hkey); + return ret; +} + +static bool del_win_registry(HKEY hkeyHive, const wchar_t *pszVar, const wchar_t *pszValue) +{ + wchar_t szValueCurrent[1000]; + DWORD dwType; + DWORD dwSize = sizeof(szValueCurrent); + + int iRC = ::RegGetValueW(hkeyHive, pszVar, nullptr, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize); + + bool bDidntExist = iRC == ERROR_FILE_NOT_FOUND; + + if ((iRC != ERROR_SUCCESS) && !bDidntExist) + return false; + + if (!bDidntExist) { + DWORD dwDisposition; + HKEY hkey; + iRC = ::RegDeleteKeyExW(hkeyHive, pszVar, KEY_ALL_ACCESS, 0); + if (iRC == ERROR_SUCCESS) { + return true; + } + } + + return false; +} + + +void GUI_App::associate_files(std::wstring extend) +{ + wchar_t app_path[MAX_PATH]; + ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); + + std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; + std::wstring prog_id = L" Bambu.Studio.1"; + std::wstring prog_desc = L"BambuStudio"; + std::wstring prog_command = prog_path + L" \"%1\""; + std::wstring reg_base = L"Software\\Classes"; + std::wstring reg_extension = reg_base + L"\\." + extend; + std::wstring reg_prog_id = reg_base + L"\\" + prog_id; + std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; + + bool is_new = false; + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); + is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); + if (is_new) + // notify Windows only when any of the values gets changed + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); +} + +void GUI_App::disassociate_files(std::wstring extend) +{ + wchar_t app_path[MAX_PATH]; + ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); + + std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; + std::wstring prog_id = L" Bambu.Studio.1"; + std::wstring prog_desc = L"BambuStudio"; + std::wstring prog_command = prog_path + L" \"%1\""; + std::wstring reg_base = L"Software\\Classes"; + std::wstring reg_extension = reg_base + L"\\." + extend; + std::wstring reg_prog_id = reg_base + L"\\" + prog_id; + std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; + + bool is_new = false; + is_new |= del_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); + + bool is_associate_3mf = app_config->get("associate_3mf") == "true"; + bool is_associate_stl = app_config->get("associate_stl") == "true"; + bool is_associate_step = app_config->get("associate_step") == "true"; + if (!is_associate_3mf && !is_associate_stl && !is_associate_step) + { + is_new |= del_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); + is_new |= del_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); + } + + if (is_new) + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); +} + + +#endif // __WXMSW__ + +bool is_support_filament(int extruder_id) +{ + auto &filament_presets = Slic3r::GUI::wxGetApp().preset_bundle->filament_presets; + auto &filaments = Slic3r::GUI::wxGetApp().preset_bundle->filaments; + + if (extruder_id >= filament_presets.size()) return false; + + Slic3r::Preset *filament = filaments.find_preset(filament_presets[extruder_id]); + if (filament == nullptr) return false; + + Slic3r::ConfigOptionBools *support_option = dynamic_cast(filament->config.option("filament_is_support")); + if (support_option == nullptr) return false; + + return support_option->get_at(0); +}; + +} // GUI +} //Slic3r diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 1c828b528..f9aa28e47 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -950,7 +950,7 @@ void MainFrame::update_title() void MainFrame::show_publish_button(bool show) { - m_publish_btn->Show(show); + //m_publish_btn->Show(show); Layout(); } @@ -1308,6 +1308,7 @@ void MainFrame::create_preset_tabs() //add_created_tab(new TabSLAPrint(m_param_panel)); //add_created_tab(new TabSLAMaterial(m_param_panel)); add_created_tab(new TabPrinter(m_param_dialog->panel()), "printer"); + //add_created_tab(new TabConfig(m_param_dialog->panel()), "config"); m_param_panel->rebuild_panels(); m_param_dialog->panel()->rebuild_panels(); @@ -1571,7 +1572,7 @@ wxBoxSizer* MainFrame::create_side_tools() m_publish_btn->Hide(); m_slice_option_btn->Enable(); m_print_option_btn->Enable(); - sizer->Add(m_publish_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(1)); + //sizer->Add(m_publish_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(1)); sizer->Add(FromDIP(15), 0, 0, 0, 0); sizer->Add(m_slice_option_btn, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(1)); sizer->Add(m_slice_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(1)); @@ -1651,19 +1652,19 @@ wxBoxSizer* MainFrame::create_side_tools() m_slice_option_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { SidePopup* p = new SidePopup(this); - SideButton* slice_all_btn = new SideButton(p, _L("Slice all"), ""); - slice_all_btn->SetCornerRadius(0); + //SideButton* slice_all_btn = new SideButton(p, _L("Slice all"), ""); + //slice_all_btn->SetCornerRadius(0); SideButton* slice_plate_btn = new SideButton(p, _L("Slice plate"), ""); slice_plate_btn->SetCornerRadius(0); - slice_all_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + /* slice_all_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_slice_btn->SetLabel(_L("Slice all")); m_slice_select = eSliceAll; m_slice_enable = get_enable_slice_status(); m_slice_btn->Enable(m_slice_enable); this->Layout(); p->Dismiss(); - }); + });*/ slice_plate_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_slice_btn->SetLabel(_L("Slice plate")); @@ -1673,7 +1674,8 @@ wxBoxSizer* MainFrame::create_side_tools() this->Layout(); p->Dismiss(); }); - p->append_button(slice_all_btn); + //xiamian- + //p->append_button(slice_all_btn); p->append_button(slice_plate_btn); p->Popup(m_slice_btn); } @@ -1727,28 +1729,29 @@ wxBoxSizer* MainFrame::create_side_tools() } else { //Bambu Studio Buttons - SideButton* print_plate_btn = new SideButton(p, _L("Print plate"), ""); - print_plate_btn->SetCornerRadius(0); + //xiamian- + //SideButton* print_plate_btn = new SideButton(p, _L("Print plate"), ""); + //print_plate_btn->SetCornerRadius(0); - SideButton* send_to_printer_btn = new SideButton(p, _L("Send"), ""); - send_to_printer_btn->SetCornerRadius(0); + //SideButton* send_to_printer_btn = new SideButton(p, _L("Send"), ""); + //send_to_printer_btn->SetCornerRadius(0); SideButton* export_sliced_file_btn = new SideButton(p, _L("Export plate sliced file"), ""); export_sliced_file_btn->SetCornerRadius(0); - SideButton* export_all_sliced_file_btn = new SideButton(p, _L("Export all sliced file"), ""); - export_all_sliced_file_btn->SetCornerRadius(0); + /* SideButton* export_all_sliced_file_btn = new SideButton(p, _L("Export all sliced file"), ""); + export_all_sliced_file_btn->SetCornerRadius(0);*/ - print_plate_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + /* print_plate_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_print_btn->SetLabel(_L("Print plate")); m_print_select = ePrintPlate; m_print_enable = get_enable_print_status(); m_print_btn->Enable(m_print_enable); this->Layout(); p->Dismiss(); - }); + });*/ - SideButton* print_all_btn = new SideButton(p, _L("Print all"), ""); + /* SideButton* print_all_btn = new SideButton(p, _L("Print all"), ""); print_all_btn->SetCornerRadius(0); print_all_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_print_btn->SetLabel(_L("Print all")); @@ -1757,18 +1760,18 @@ wxBoxSizer* MainFrame::create_side_tools() m_print_btn->Enable(m_print_enable); this->Layout(); p->Dismiss(); - }); + });*/ - send_to_printer_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + /* send_to_printer_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_print_btn->SetLabel(_L("Send")); m_print_select = eSendToPrinter; m_print_enable = get_enable_print_status(); m_print_btn->Enable(m_print_enable); this->Layout(); p->Dismiss(); - }); + });*/ - SideButton* send_to_printer_all_btn = new SideButton(p, _L("Send all"), ""); + /* SideButton* send_to_printer_all_btn = new SideButton(p, _L("Send all"), ""); send_to_printer_all_btn->SetCornerRadius(0); send_to_printer_all_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_print_btn->SetLabel(_L("Send all")); @@ -1777,7 +1780,7 @@ wxBoxSizer* MainFrame::create_side_tools() m_print_btn->Enable(m_print_enable); this->Layout(); p->Dismiss(); - }); + });*/ export_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_print_btn->SetLabel(_L("Export plate sliced file")); @@ -1788,22 +1791,22 @@ wxBoxSizer* MainFrame::create_side_tools() p->Dismiss(); }); - export_all_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { + /* export_all_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { m_print_btn->SetLabel(_L("Export all sliced file")); m_print_select = eExportAllSlicedFile; m_print_enable = get_enable_print_status(); m_print_btn->Enable(m_print_enable); this->Layout(); p->Dismiss(); - }); + });*/ - p->append_button(print_plate_btn); - p->append_button(print_all_btn); - p->append_button(send_to_printer_btn); - p->append_button(send_to_printer_all_btn); + //p->append_button(print_plate_btn); + //p->append_button(print_all_btn); + //p->append_button(send_to_printer_btn); + //p->append_button(send_to_printer_all_btn); p->append_button(export_sliced_file_btn); - p->append_button(export_all_sliced_file_btn); - if (enable_multi_machine) { + //p->append_button(export_all_sliced_file_btn); + /*if (enable_multi_machine) { SideButton* print_multi_machine_btn = new SideButton(p, _L("Send to Multi-device"), ""); print_multi_machine_btn->SetCornerRadius(0); print_multi_machine_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) { @@ -1815,7 +1818,7 @@ wxBoxSizer* MainFrame::create_side_tools() p->Dismiss(); }); p->append_button(print_multi_machine_btn); - } + }*/ } p->Popup(m_print_btn); @@ -3815,6 +3818,7 @@ void MainFrame::update_side_preset_ui() //BBS: update the preset m_plater->sidebar().update_presets(Preset::TYPE_PRINTER); m_plater->sidebar().update_presets(Preset::TYPE_FILAMENT); + //m_plater->sidebar().update_presets(Preset::TYPE_CONFIG); //take off multi machine diff --git a/src/slic3r/GUI/ParamsPanel.cpp b/src/slic3r/GUI/ParamsPanel.cpp new file mode 100644 index 000000000..8cbef04c8 --- /dev/null +++ b/src/slic3r/GUI/ParamsPanel.cpp @@ -0,0 +1,836 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.0-4761b0c) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Preset.hpp" +#include "ParamsPanel.hpp" +#include "Tab.hpp" +#include "format.hpp" +#include "MainFrame.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" + +#include "Widgets/Label.hpp" +#include "Widgets/SwitchButton.hpp" +#include "Widgets/Button.hpp" +#include "GUI_Factories.hpp" + + +namespace Slic3r { +namespace GUI { + + +TipsDialog::TipsDialog(wxWindow *parent, const wxString &title, const wxString &description, std::string app_key) + : DPIDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX), + m_app_key(app_key) +{ + SetBackgroundColour(*wxWHITE); + std::string icon_path = (boost::format("%1%/images/BambuStudioTitle.ico") % resources_dir()).str(); + SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO)); + + 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->Add(0, 0, 0, wxEXPAND | wxTOP, FromDIP(20)); + + m_msg = new wxStaticText(this, wxID_ANY, description, wxDefaultPosition, wxDefaultSize, 0); + m_msg->Wrap(-1); + m_msg->SetFont(::Label::Body_13); + m_msg->SetForegroundColour(wxColour(107, 107, 107)); + m_msg->SetBackgroundColour(wxColour(255, 255, 255)); + + m_sizer_main->Add(m_msg, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(40)); + + m_sizer_main->Add(0, 0, 0, wxEXPAND | wxTOP, FromDIP(5)); + + wxBoxSizer *m_sizer_bottom = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer *m_sizer_left = new wxBoxSizer(wxHORIZONTAL); + + auto dont_show_again = create_item_checkbox(_L("Don't show again"), this, _L("Don't show again"), "do_not_show_tips"); + m_sizer_left->Add(dont_show_again, 1, wxALL, FromDIP(5)); + + m_sizer_bottom->Add(m_sizer_left, 1, wxEXPAND, FromDIP(5)); + + wxBoxSizer *m_sizer_right = new wxBoxSizer(wxHORIZONTAL); + + m_confirm = new Button(this, _L("OK")); + StateColor btn_bg_green(std::pair(wxColour(27, 136, 68), StateColor::Pressed), std::pair(wxColour(61, 203, 115), StateColor::Hovered), + std::pair(wxColour(0, 174, 66), StateColor::Normal)); + + m_confirm->SetBackgroundColor(btn_bg_green); + m_confirm->SetBorderColor(wxColour(0, 174, 66)); + m_confirm->SetTextColor(wxColour(255, 255, 255)); + m_confirm->SetSize(TIPS_DIALOG_BUTTON_SIZE); + m_confirm->SetMinSize(TIPS_DIALOG_BUTTON_SIZE); + m_confirm->SetCornerRadius(FromDIP(12)); + m_confirm->Bind(wxEVT_LEFT_DOWN, &TipsDialog::on_ok, this); + m_sizer_right->Add(m_confirm, 0, wxALL, FromDIP(5)); + + m_sizer_bottom->Add(m_sizer_right, 0, wxEXPAND, FromDIP(5)); + m_sizer_main->Add(m_sizer_bottom, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(40)); + m_sizer_main->Add(0, 0, 0, wxEXPAND | wxTOP, FromDIP(20)); + + SetSizer(m_sizer_main); + Layout(); + Fit(); + Centre(wxBOTH); + + wxGetApp().UpdateDlgDarkUI(this); +} + +wxBoxSizer *TipsDialog::create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, std::string param) +{ + wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); + + m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 5); + + auto checkbox = new ::CheckBox(parent); + 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); + m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxALL, 3); + + m_show_again = wxGetApp().app_config->get(param) == "true" ? true : false; + checkbox->SetValue(m_show_again); + + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param](wxCommandEvent &e) { + m_show_again = m_show_again ? false : true; + e.Skip(); + }); + + return m_sizer_checkbox; +} + +void TipsDialog::on_dpi_changed(const wxRect &suggested_rect) +{ + if (m_confirm) m_confirm->SetMinSize(TIPS_DIALOG_BUTTON_SIZE); + if (m_cancel) m_cancel->SetMinSize(TIPS_DIALOG_BUTTON_SIZE); + Fit(); + Refresh(); +} + +void TipsDialog::on_ok(wxMouseEvent &event) +{ + if (m_show_again) { + if (!m_app_key.empty()) + wxGetApp().app_config->set_bool(m_app_key, m_show_again); + } + EndModal(wxID_OK); +} + +void ParamsPanel::Highlighter::set_timer_owner(wxEvtHandler *owner, int timerid /* = wxID_ANY*/) +{ + m_timer.SetOwner(owner, timerid); +} + +void ParamsPanel::Highlighter::init(std::pair params, wxWindow *parent) + { + if (m_timer.IsRunning()) invalidate(); + if (!params.first || !params.second) return; + + m_timer.Start(300, false); + + m_bitmap = params.first; + m_show_blink_ptr = params.second; + m_parent = parent; + + *m_show_blink_ptr = true; + } + +void ParamsPanel::Highlighter::invalidate() +{ + m_timer.Stop(); + + if (m_bitmap && m_show_blink_ptr) { + *m_show_blink_ptr = false; + m_bitmap->Show(*m_show_blink_ptr); + if (m_parent) { + m_parent->Layout(); + m_parent->Refresh(); + } + m_show_blink_ptr = nullptr; + m_bitmap = nullptr; + m_parent = nullptr; + } + + m_blink_counter = 0; +} + +void ParamsPanel::Highlighter::blink() +{ + if (m_bitmap && m_show_blink_ptr) { + *m_show_blink_ptr = !*m_show_blink_ptr; + m_bitmap->Show(*m_show_blink_ptr); + if (m_parent) { + m_parent->Layout(); + m_parent->Refresh(); + } + } else + return; + + if ((++m_blink_counter) == 11) invalidate(); +} + +ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name ) + : wxPanel( parent, id, pos, size, style, name ) +{ + // BBS: new layout + SetBackgroundColour(*wxWHITE); +#if __WXOSX__ + m_top_sizer = new wxBoxSizer(wxHORIZONTAL); + m_top_sizer->SetSizeHints(this); + this->SetSizer(m_top_sizer); + + // Create additional panel to Fit() it from OnActivate() + // It's needed for tooltip showing on OSX + m_tmp_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + m_tmp_panel->SetSizer(sizer); + m_tmp_panel->Layout(); + +#else + ParamsPanel*panel = this; + m_top_sizer = new wxBoxSizer(wxHORIZONTAL); + m_top_sizer->SetSizeHints(panel); + panel->SetSizer(m_top_sizer); +#endif //__WXOSX__ + + if (dynamic_cast(parent)) { + // BBS: new layout + m_top_panel = new StaticBox(this, wxID_ANY, wxDefaultPosition); + m_top_panel->SetBackgroundColor(0xF8F8F8); + m_top_panel->SetBackgroundColor2(0xF1F1F1); + + m_process_icon = new ScalableButton(m_top_panel, wxID_ANY, "process"); + m_process_icon->Hide(); + m_title_label = new Label(m_top_panel, _L("Process")); + m_title_label->Hide(); + //int width, height; + // BBS: new layout + m_mode_region = new SwitchButton(m_top_panel); + m_mode_region->SetMaxSize({em_unit(this) * 12, -1}); + m_mode_region->SetLabels(_L("Global"), _L("Objects")); + //m_mode_region->Hide(); + //m_mode_region->GetSize(&width, &height); + m_tips_arrow = new ScalableButton(m_top_panel, wxID_ANY, "tips_arrow"); + m_tips_arrow->Hide(); + + m_title_view = new Label(m_top_panel, _L("Advance")); + m_mode_view = new SwitchButton(m_top_panel, wxID_ABOUT); + m_title_view->Hide(); + m_mode_view->Hide(); + // BBS: new layout + //m_search_btn = new ScalableButton(m_top_panel, wxID_ANY, "search", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true); + //m_search_btn->SetToolTip(format_wxstr(_L("Search in settings [%1%]"), "Ctrl+F")); + //m_search_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { wxGetApp().plater()->search(false); }); + + m_compare_btn = new ScalableButton(m_top_panel, wxID_ANY, "compare", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true); + m_compare_btn->SetToolTip(_L("Compare presets")); + m_compare_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e) { wxGetApp().mainframe->diff_dialog.show(); })); + m_compare_btn->Hide(); + m_setting_btn = new ScalableButton(m_top_panel, wxID_ANY, "table", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true); + m_setting_btn->SetToolTip(_L("View all object's settings")); + m_setting_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { wxGetApp().plater()->PopupObjectTable(-1, -1, {0, 0}); }); + m_setting_btn->Hide(); + m_highlighter.set_timer_owner(this, 0); + this->Bind(wxEVT_TIMER, [this](wxTimerEvent &) + { + m_highlighter.blink(); + }); + } + + //m_export_to_file = new Button( this, wxT("Export To File"), ""); + //m_import_from_file = new Button( this, wxT("Import From File") ); + + // Initialize the page. +#if __WXOSX__ + auto page_parent = m_tmp_panel; +#else + auto page_parent = this; +#endif + + // BBS: fix scroll to tip view + class PageScrolledWindow : public wxScrolledWindow + { + public: + PageScrolledWindow(wxWindow *parent) + : wxScrolledWindow(parent, + wxID_ANY, + wxDefaultPosition, + wxDefaultSize, + wxVSCROLL) // hide hori-bar will cause hidden field mis-position + { + // ShowScrollBar(GetHandle(), SB_BOTH, FALSE); + Bind(wxEVT_SCROLL_CHANGED, [this](auto &e) { + wxWindow *child = dynamic_cast(e.GetEventObject()); + if (child != this) + EnsureVisible(child); + }); + } + virtual bool ShouldScrollToChildOnFocus(wxWindow *child) + { + EnsureVisible(child); + return false; + } + void EnsureVisible(wxWindow* win) + { + const wxRect viewRect(m_targetWindow->GetClientRect()); + const wxRect winRect(m_targetWindow->ScreenToClient(win->GetScreenPosition()), win->GetSize()); + if (viewRect.Contains(winRect)) { + return; + } + if (winRect.GetWidth() > viewRect.GetWidth() || winRect.GetHeight() > viewRect.GetHeight()) { + return; + } + int stepx, stepy; + GetScrollPixelsPerUnit(&stepx, &stepy); + + int startx, starty; + GetViewStart(&startx, &starty); + // first in vertical direction: + if (stepy > 0) { + int diff = 0; + + if (winRect.GetTop() < 0) { + diff = winRect.GetTop(); + } else if (winRect.GetBottom() > viewRect.GetHeight()) { + diff = winRect.GetBottom() - viewRect.GetHeight() + 1; + // round up to next scroll step if we can't get exact position, + // so that the window is fully visible: + diff += stepy - 1; + } + starty = (starty * stepy + diff) / stepy; + } + // then horizontal: + if (stepx > 0) { + int diff = 0; + if (winRect.GetLeft() < 0) { + diff = winRect.GetLeft(); + } else if (winRect.GetRight() > viewRect.GetWidth()) { + diff = winRect.GetRight() - viewRect.GetWidth() + 1; + // round up to next scroll step if we can't get exact position, + // so that the window is fully visible: + diff += stepx - 1; + } + startx = (startx * stepx + diff) / stepx; + } + Scroll(startx, starty); + } + }; + + m_page_view = new PageScrolledWindow(page_parent); + m_page_view->SetBackgroundColour(*wxWHITE); + m_page_sizer = new wxBoxSizer(wxVERTICAL); + + m_page_view->SetSizer(m_page_sizer); + m_page_view->SetScrollbars(1, 20, 1, 2); + //m_page_view->SetScrollRate( 5, 5 ); + + if (m_mode_region) + m_mode_region->Bind(wxEVT_TOGGLEBUTTON, &ParamsPanel::OnToggled, this); + if (m_mode_view) + m_mode_view->Bind(wxEVT_TOGGLEBUTTON, &ParamsPanel::OnToggled, this); + Bind(wxEVT_TOGGLEBUTTON, &ParamsPanel::OnToggled, this); // For Tab's mode switch + //Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { wxGetApp().plater()->search(false); }, wxID_FIND); + //m_export_to_file->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { wxGetApp().mainframe->export_config(); }); + //m_import_from_file->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { wxGetApp().mainframe->load_config_file(); }); +} + +void ParamsPanel::create_layout() +{ +#ifdef __WINDOWS__ + this->SetDoubleBuffered(true); + m_page_view->SetDoubleBuffered(true); +#endif //__WINDOWS__ + + m_left_sizer = new wxBoxSizer( wxVERTICAL ); + // BBS: new layout + m_left_sizer->SetMinSize( wxSize(40 * em_unit(this), -1 ) ); + + if (m_top_panel) { + m_mode_sizer = new wxBoxSizer( wxHORIZONTAL ); + m_mode_sizer->AddSpacer(FromDIP(11)); + //m_mode_sizer->Add(m_process_icon, 0, wxALIGN_CENTER); + //m_mode_sizer->AddSpacer(FromDIP(11)); + //m_mode_sizer->Add( m_title_label, 0, wxALIGN_CENTER ); + //m_mode_sizer->AddStretchSpacer(2); + m_mode_sizer->Add(m_mode_region, 0, wxALIGN_CENTER); + m_mode_sizer->AddStretchSpacer(1); + //m_mode_sizer->Add(m_tips_arrow, 0, wxALIGN_CENTER); + // m_mode_sizer->AddStretchSpacer(8); + //m_mode_sizer->Add( m_title_view, 0, wxALIGN_CENTER ); + //m_mode_sizer->AddSpacer(FromDIP(2)); + //m_mode_sizer->Add(m_mode_view, 0, wxALIGN_CENTER); + //m_mode_sizer->AddStretchSpacer(2); + //m_mode_sizer->Add(m_setting_btn, 0, wxALIGN_CENTER); + //m_mode_sizer->AddSpacer(FromDIP(2)); + //m_mode_sizer->Add(m_compare_btn, 0, wxALIGN_CENTER); + + m_mode_sizer->AddSpacer(FromDIP(8)); + //m_mode_sizer->Add( m_search_btn, 0, wxALIGN_CENTER ); + //m_mode_sizer->AddSpacer(16); + m_mode_sizer->SetMinSize(-1, FromDIP(30)); + m_top_panel->SetSizer(m_mode_sizer); + //m_left_sizer->Add( m_top_panel, 0, wxEXPAND ); + } + + if (m_tab_print) { + //m_print_sizer = new wxBoxSizer( wxHORIZONTAL ); + //m_print_sizer->Add( m_tab_print, 1, wxEXPAND | wxALL, 5 ); + //m_left_sizer->Add( m_print_sizer, 1, wxEXPAND, 5 ); + m_left_sizer->Add( m_tab_print, 0, wxEXPAND ); + } + + if (m_tab_print_plate) { + m_left_sizer->Add(m_tab_print_plate, 0, wxEXPAND); + } + + if (m_tab_print_object) { + m_left_sizer->Add( m_tab_print_object, 0, wxEXPAND ); + } + + if (m_tab_print_part) { + m_left_sizer->Add( m_tab_print_part, 0, wxEXPAND ); + } + + if (m_tab_print_layer) { + m_left_sizer->Add(m_tab_print_layer, 0, wxEXPAND); + } + + if (m_tab_filament) { + //m_filament_sizer = new wxBoxSizer( wxVERTICAL ); + //m_filament_sizer->Add( m_tab_filament, 1, wxEXPAND | wxALL, 5 ); + // m_left_sizer->Add( m_filament_sizer, 1, wxEXPAND, 5 ); + m_left_sizer->Add( m_tab_filament, 0, wxEXPAND ); + } + + if (m_tab_printer) { + //m_printer_sizer = new wxBoxSizer( wxVERTICAL ); + //m_printer_sizer->Add( m_tab_printer, 1, wxEXPAND | wxALL, 5 ); + m_left_sizer->Add( m_tab_printer, 0, wxEXPAND ); + } + + //m_left_sizer->Add( m_printer_sizer, 1, wxEXPAND, 1 ); + + //m_button_sizer = new wxBoxSizer( wxHORIZONTAL ); + + //m_button_sizer->Add( m_export_to_file, 0, wxALL, 5 ); + + //m_button_sizer->Add( m_import_from_file, 0, wxALL, 5 ); + + //m_left_sizer->Add( m_button_sizer, 0, wxALIGN_CENTER, 5 ); + + m_top_sizer->Add(m_left_sizer, 1, wxEXPAND); + + //m_right_sizer = new wxBoxSizer( wxVERTICAL ); + + //m_right_sizer->Add( m_page_view, 1, wxEXPAND | wxALL, 5 ); + + //m_top_sizer->Add( m_right_sizer, 1, wxEXPAND, 5 ); + // BBS: new layout + m_left_sizer->AddSpacer(6 * em_unit(this) / 10); +#if __WXOSX__ + m_left_sizer->Add(m_tmp_panel, 1, wxEXPAND | wxALL, 0); + m_tmp_panel->GetSizer()->Add( m_page_view, 1, wxEXPAND ); +#else + m_left_sizer->Add( m_page_view, 1, wxEXPAND ); +#endif + + //this->SetSizer( m_top_sizer ); + this->Layout(); +} + +void ParamsPanel::rebuild_panels() +{ + refresh_tabs(); + free_sizers(); + create_layout(); +} + +void ParamsPanel::refresh_tabs() +{ + 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)) + { + if (tab->GetParent() != this) continue; + switch (tab->type()) + { + case Preset::TYPE_PRINT: + case Preset::TYPE_SLA_PRINT: + m_tab_print = tab; + break; + + case Preset::TYPE_FILAMENT: + case Preset::TYPE_SLA_MATERIAL: + m_tab_filament = tab; + break; + + case Preset::TYPE_PRINTER: + m_tab_printer = tab; + break; + default: + break; + } + } + if (m_top_panel) { + m_tab_print_plate = wxGetApp().get_plate_tab(); + m_tab_print_object = wxGetApp().get_model_tab(); + m_tab_print_part = wxGetApp().get_model_tab(true); + m_tab_print_layer = wxGetApp().get_layer_tab(); + } + return; +} + +void ParamsPanel::clear_page() +{ + if (m_page_sizer) + m_page_sizer->Clear(true); +} + + +void ParamsPanel::OnActivate() +{ + if (m_current_tab == NULL) + { + //the first time + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": first time opened, set current tab to print"); + // BBS: open/close tab + //m_current_tab = m_tab_print; + set_active_tab(m_tab_print ? m_tab_print : m_tab_filament); + } + Tab* cur_tab = dynamic_cast (m_current_tab); + if (cur_tab) + cur_tab->OnActivate(); +} + +void ParamsPanel::OnToggled(wxCommandEvent& event) +{ + if (m_mode_region && m_mode_region->GetId() == event.GetId()) { + wxWindowUpdateLocker locker(GetParent()); + set_active_tab(nullptr); + event.Skip(); + return; + } + + if (wxID_ABOUT != event.GetId()) { + return; + } + + // this is from tab's mode switch + bool value = dynamic_cast(event.GetEventObject())->GetValue(); + int mode_id; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": Advanced mode toogle to %1%") % value; + + if (value) + { + //m_mode_region->SetBitmap(m_toggle_on_icon); + mode_id = comAdvanced; + } + else + { + //m_mode_region->SetBitmap(m_toggle_off_icon); + mode_id = comSimple; + } + + Slic3r::GUI::wxGetApp().save_mode(mode_id); +} + +// This is special, DO NOT call it from outer except from Tab +void ParamsPanel::set_active_tab(wxPanel* tab) +{ + Tab* cur_tab = dynamic_cast (tab); + + if (cur_tab == nullptr) { + if (!m_mode_region->GetValue()) { + cur_tab = (Tab*) m_tab_print; + } else if (m_tab_print_part && ((TabPrintModel*) m_tab_print_part)->has_model_config()) { + cur_tab = (Tab*) m_tab_print_part; + } else if (m_tab_print_layer && ((TabPrintModel*)m_tab_print_layer)->has_model_config()) { + cur_tab = (Tab*)m_tab_print_layer; + } else if (m_tab_print_object && ((TabPrintModel*) m_tab_print_object)->has_model_config()) { + cur_tab = (Tab*) m_tab_print_object; + } else if (m_tab_print_plate && ((TabPrintPlate*)m_tab_print_plate)->has_model_config()) { + cur_tab = (Tab*)m_tab_print_plate; + } + Show(cur_tab != nullptr); + wxGetApp().sidebar().show_object_list(m_mode_region->GetValue()); + if (m_current_tab == cur_tab) + return; + if (cur_tab) + cur_tab->restore_last_select_item(); + return; + } + + m_current_tab = tab; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": set current to %1%, type=%2%") % cur_tab % cur_tab?cur_tab->type():-1; + update_mode(); + + // BBS: open/close tab + for (auto t : std::vector>({ + {m_tab_print, m_staticline_print}, + {m_tab_print_object, m_staticline_print_object}, + {m_tab_print_part, m_staticline_print_part}, + {m_tab_print_layer, nullptr}, + {m_tab_print_plate, nullptr}, + {m_tab_filament, m_staticline_filament}, + {m_tab_printer, m_staticline_printer}})) { + if (!t.first) continue; + t.first->Show(tab == t.first); + if (!t.second) continue; + t.second->Show(tab == t.first); + //m_left_sizer->GetItem(t)->SetProportion(tab == t ? 1 : 0); + } + m_left_sizer->Layout(); + if (auto dialog = dynamic_cast(GetParent())) { + //"Configuration settings" + wxString title; + if (cur_tab->type() == Preset::TYPE_FILAMENT) { + title = _L("Filament settings"); + }else if (cur_tab->type() == Preset::TYPE_PRINTER) { + title = _L("Printer settings"); + } else { + title = _L("Configuration settings"); + } + //wxString title = cur_tab->type() == Preset::TYPE_FILAMENT ? _L("Filament settings") : _L("Printer settings"); + dialog->SetTitle(title); + } +} + +bool ParamsPanel::is_active_and_shown_tab(wxPanel* tab) +{ + if (m_current_tab == tab) + return true; + else + return false; +} + +void ParamsPanel::update_mode() +{ + int app_mode = Slic3r::GUI::wxGetApp().get_mode(); + SwitchButton * mode_view = m_current_tab ? dynamic_cast(m_current_tab)->m_mode_view : nullptr; + if (mode_view == nullptr) mode_view = m_mode_view; + if (mode_view == nullptr) return; + + //BBS: disable the mode tab and return directly when enable develop mode + if (app_mode == comDevelop) + { + mode_view->Disable(); + return; + } + if (!mode_view->IsEnabled()) + mode_view->Enable(); + + if (app_mode == comAdvanced) + { + mode_view->SetValue(true); + } + else + { + mode_view->SetValue(false); + } +} + +void ParamsPanel::msw_rescale() +{ + if (m_process_icon) m_process_icon->msw_rescale(); + if (m_setting_btn) m_setting_btn->msw_rescale(); + if (m_search_btn) m_search_btn->msw_rescale(); + if (m_compare_btn) m_compare_btn->msw_rescale(); + if (m_tips_arrow) m_tips_arrow->msw_rescale(); + if (m_left_sizer) m_left_sizer->SetMinSize(wxSize(40 * em_unit(this), -1)); + if (m_mode_sizer) + m_mode_sizer->SetMinSize(-1, 3 * em_unit(this)); + if (m_mode_region) + ((SwitchButton* )m_mode_region)->Rescale(); + if (m_mode_view) + ((SwitchButton* )m_mode_view)->Rescale(); + for (auto tab : {m_tab_print, m_tab_print_plate, m_tab_print_object, m_tab_print_part, m_tab_print_layer, m_tab_filament, m_tab_printer}) { + if (tab) dynamic_cast(tab)->msw_rescale(); + } + //((Button*)m_export_to_file)->Rescale(); + //((Button*)m_import_from_file)->Rescale(); +} + +void ParamsPanel::switch_to_global() +{ + m_mode_region->SetValue(false); + set_active_tab(nullptr); +} + +void ParamsPanel::switch_to_object(bool with_tips) +{ + m_mode_region->SetValue(true); + set_active_tab(nullptr); + if (with_tips) { + m_highlighter.init(std::pair(m_tips_arrow, &m_tips_arror_blink), m_top_panel); + m_highlighter.blink(); + } +} + +void ParamsPanel::notify_object_config_changed() +{ + auto & model = wxGetApp().model(); + bool has_config = false; + for (auto obj : model.objects) { + if (!obj->config.empty()) { + SettingsFactory::Bundle cat_options = SettingsFactory::get_bundle(&obj->config.get(), true); + if (cat_options.size() > 0) { + has_config = true; + break; + } + } + for (auto volume : obj->volumes) { + if (!volume->config.empty()) { + SettingsFactory::Bundle cat_options = SettingsFactory::get_bundle(&volume->config.get(), true); + if (cat_options.size() > 0) { + has_config = true; + break; + } + } + } + if (has_config) break; + } + if (has_config == m_has_object_config) return; + m_has_object_config = has_config; + if (has_config) + m_mode_region->SetTextColor2(StateColor(std::pair{0xfffffe, (int) StateColor::Checked}, std::pair{wxGetApp().get_label_clr_modified(), 0})); + else + m_mode_region->SetTextColor2(StateColor()); + m_mode_region->Rescale(); +} + +void ParamsPanel::switch_to_object_if_has_object_configs() +{ + if (m_has_object_config) + m_mode_region->SetValue(true); + set_active_tab(nullptr); +} + +void ParamsPanel::free_sizers() +{ + if (m_top_sizer) + { + m_top_sizer->Clear(false); + //m_top_sizer = nullptr; + } + + m_left_sizer = nullptr; + //m_right_sizer = nullptr; + m_mode_sizer = nullptr; + //m_print_sizer = nullptr; + //m_filament_sizer = nullptr; + //m_printer_sizer = nullptr; + m_button_sizer = nullptr; +} + +void ParamsPanel::delete_subwindows() +{ + if (m_title_label) + { + delete m_title_label; + m_title_label = nullptr; + } + + if (m_mode_region) + { + delete m_mode_region; + m_mode_region = nullptr; + } + + if (m_mode_view) + { + delete m_mode_view; + m_mode_view = nullptr; + } + + if (m_title_view) + { + delete m_title_view; + m_title_view = nullptr; + } + + if (m_search_btn) + { + delete m_search_btn; + m_search_btn = nullptr; + } + + if (m_staticline_print) + { + delete m_staticline_print; + m_staticline_print = nullptr; + } + + if (m_staticline_print_part) + { + delete m_staticline_print_part; + m_staticline_print_part = nullptr; + } + + if (m_staticline_print_object) + { + delete m_staticline_print_object; + m_staticline_print_object = nullptr; + } + + if (m_staticline_filament) + { + delete m_staticline_filament; + m_staticline_filament = nullptr; + } + + if (m_staticline_printer) + { + delete m_staticline_printer; + m_staticline_printer = nullptr; + } + + if (m_export_to_file) + { + delete m_export_to_file; + m_export_to_file = nullptr; + } + + if (m_import_from_file) + { + delete m_import_from_file; + m_import_from_file = nullptr; + } + + if (m_page_view) + { + delete m_page_view; + m_page_view = nullptr; + } +} + +ParamsPanel::~ParamsPanel() +{ +#if 0 + free_sizers(); + delete m_top_sizer; + + delete_subwindows(); +#endif + // BBS: fix double destruct of OG_CustomCtrl + Tab* cur_tab = dynamic_cast (m_current_tab); + if (cur_tab) + cur_tab->clear_pages(); +} + +} // GUI +} // Slic3r diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp new file mode 100644 index 000000000..af7d987f6 --- /dev/null +++ b/src/slic3r/GUI/Plater.cpp @@ -0,0 +1,14108 @@ +#include "Plater.hpp" +#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 "libslic3r/libslic3r.h" +#include "libslic3r/Format/STL.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" +#include "GUI_Utils.hpp" +#include "GUI_Factories.hpp" +#include "wxExtensions.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/PrintJob.hpp" +#include "Jobs/NotificationProgressIndicator.hpp" +#include "BackgroundSlicingProcess.hpp" +#include "SelectMachine.hpp" +#include "SendMultiMachinePage.hpp" +#include "SendToPrinter.hpp" +#include "PublishDialog.hpp" +#include "ModelMall.hpp" +#include "ConfigWizard.hpp" +#include "../Utils/ASCIIFolding.hpp" +#include "../Utils/FixModelByWin10.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 + +// BBS +#include "Widgets/ProgressDialog.hpp" +#include "BBLStatusBar.hpp" +#include "BitmapCache.hpp" +#include "ParamsDialog.hpp" +#include "Widgets/Label.hpp" +#include "Widgets/RoundedRectangle.hpp" +#include "Widgets/RadioBox.hpp" +#include "Widgets/CheckBox.hpp" +#include "Widgets/Button.hpp" + +#include "GUI_ObjectTable.hpp" +#include "libslic3r/Thread.hpp" + +#ifdef __APPLE__ +#include "Gizmos/GLGizmosManager.hpp" +#endif // __APPLE__ + +#include // Needs to be last because reasons :-/ +#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" + +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); +// 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); +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:") + " <>:/\\|?*\""); +} + +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; + +// Sidebar / private + +enum class ActionButtonType : int { + abReslice, + abExport, + abSendGCode +}; + +struct Sidebar::priv +{ + Plater *plater; + + wxPanel *scrolled; + PlaterPresetComboBox *combo_print; + std::vector combos_filament; + int editing_filament = -1; + wxBoxSizer *sizer_filaments; + PlaterPresetComboBox *combo_sla_print; + PlaterPresetComboBox *combo_sla_material; + PlaterPresetComboBox* combo_printer = nullptr; + wxBoxSizer *sizer_params; + + //BBS Sidebar widgets + wxPanel* m_panel_print_title; + wxStaticText* m_staticText_print_title; + wxPanel* m_panel_print_content; + 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; + wxPanel* 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; + wxSearchCtrl* 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_setting = nullptr; + wxStaticText* m_text_printer_settings = nullptr; + wxPanel* m_panel_printer_content = nullptr; + + // config + StaticBox* m_panel_config_title = nullptr; + ScalableButton* m_config_icon = nullptr; + ScalableButton* m_config_setting = nullptr; + wxStaticText* m_text_config_settings = nullptr; + wxPanel* m_panel_config_content = nullptr; + wxStaticText* m_staticText_config_settings; + PlaterPresetComboBox* combo_config = 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_collapsed {false}; + Search::OptionsSearcher searcher; + std::string ams_list_device; + + priv(Plater *plater) : plater(plater) {} + ~priv(); + + void show_preset_comboboxes(); + void on_search_update(); + void jump_to_object(ObjectDataViewModelNode* item); + void can_search(); + +#ifdef _WIN32 + wxString btn_reslice_tip; + void show_rich_tip(const wxString& tooltip, wxButton* btn); + void hide_rich_tip(wxButton* btn); +#endif +}; + +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::on_search_update() +{ + m_object_list->assembly_plate_object_name(); + + wxString search_text = m_search_bar->GetValue(); + m_object_list->GetModel()->search_object(search_text); + dia->update_list(); +} + +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_bar->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) +{ + 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 ConfigOption* nozzle_volume_opt = full_config.option("nozzle_volume"); + int nozzle_volume_val = nozzle_volume_opt ? (int)nozzle_volume_opt->getFloat() : 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[0] == 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[0] : 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[0]; + } + + 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 + +static struct DynamicFilamentList : DynamicList +{ + std::vector> items; + + void apply_on(Choice *c) override + { + if (items.empty()) + update(true); + auto cb = dynamic_cast(c->window); + auto n = cb->GetSelection(); + cb->Clear(); + cb->Append(_L("Default")); + 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; + 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, icons[i]}); + } + DynamicList::update(); + } +} dynamic_filament_list; + +Sidebar::Sidebar(Plater *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(42 * 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); + + 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); + }); + + + 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(); + m_soft_first_start = false; + 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 | wxRIGHT, em); + h_sizer_title->Add(p->m_text_printer_settings, 0, wxALIGN_CENTER); + h_sizer_title->AddStretchSpacer(); + h_sizer_title->Add(p->m_printer_setting, 0, wxALIGN_CENTER); + h_sizer_title->Add(15 * em / 10, 0, 0, 0, 0); + h_sizer_title->SetMinSize(-1, 3 * em); + //h_sizer_title->Hide(); + 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 + // xiamian- + scrolled_sizer->Add(p->m_panel_printer_title, 0, wxEXPAND | wxALL, 0); + p->m_panel_printer_title->Bind(wxEVT_LEFT_UP, [this] (auto & e) { + if (p->m_panel_printer_content->GetMaxHeight() == 0) + p->m_panel_printer_content->SetMaxSize({-1, -1}); + else + p->m_panel_printer_content->SetMaxSize({-1, 0}); + m_scrolled_sizer->Layout(); + }); + + // add spliter 2 + auto spliter_2 = new ::StaticLine(p->scrolled); + spliter_2->SetLineColour("#CECECE"); + //xiamian- + 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)); + + PlaterPresetComboBox* combo_printer = new PlaterPresetComboBox(p->m_panel_printer_content, Preset::TYPE_PRINTER); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_printer_content, wxID_ANY, "edit"); + edit_btn->SetToolTip(_L("Click to edit preset")); + edit_btn->Bind(wxEVT_BUTTON, [this, combo_printer](wxCommandEvent) + { + m_soft_first_start = false; + p->editing_filament = -1; + if (combo_printer->switch_to_tab()) + p->editing_filament = 0; + }); + combo_printer->edit_btn = edit_btn; + p->combo_printer = combo_printer; + + connection_btn = new ScalableButton(p->m_panel_printer_content, wxID_ANY, "monitor_signal_strong"); + connection_btn->SetBackgroundColour(wxColour(255, 255, 255)); + connection_btn->SetToolTip(_L("Connection")); + connection_btn->Bind(wxEVT_BUTTON, [this, combo_printer](wxCommandEvent) + { + PhysicalPrinterDialog dlg(this->GetParent()); + dlg.ShowModal(); + }); + + wxBoxSizer* vsizer_printer = new wxBoxSizer(wxVERTICAL); + wxBoxSizer* hsizer_printer = new wxBoxSizer(wxHORIZONTAL); + + vsizer_printer->AddSpacer(FromDIP(16)); + hsizer_printer->Add(combo_printer, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + hsizer_printer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + hsizer_printer->Add(FromDIP(8), 0, 0, 0, 0); + hsizer_printer->Add(connection_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + hsizer_printer->Add(FromDIP(8), 0, 0, 0, 0); + vsizer_printer->Add(hsizer_printer, 0, wxEXPAND, 0); + + // Bed type selection + wxBoxSizer* bed_type_sizer = new wxBoxSizer(wxHORIZONTAL); + wxStaticText* bed_type_title = new wxStaticText(p->m_panel_printer_content, wxID_ANY, _L("Bed type")); + //bed_type_title->SetBackgroundColour(); + bed_type_title->Wrap(-1); + bed_type_title->SetFont(Label::Body_14); + m_bed_type_list = new ComboBox(p->m_panel_printer_content, wxID_ANY, wxString(""), wxDefaultPosition, {-1, FromDIP(30)}, 0, nullptr, wxCB_READONLY); + const ConfigOptionDef* bed_type_def = print_config_def.get("curr_bed_type"); + if (bed_type_def && bed_type_def->enum_keys_map) { + for (auto item : bed_type_def->enum_labels) { + m_bed_type_list->AppendString(_L(item)); + } + } + + bed_type_title->Bind(wxEVT_ENTER_WINDOW, [bed_type_title, this](wxMouseEvent &e) { + e.Skip(); + auto font = bed_type_title->GetFont(); + font.SetUnderlined(true); + bed_type_title->SetFont(font); + SetCursor(wxCURSOR_HAND); + }); + bed_type_title->Bind(wxEVT_LEAVE_WINDOW, [bed_type_title, this](wxMouseEvent &e) { + e.Skip(); + auto font = bed_type_title->GetFont(); + font.SetUnderlined(false); + bed_type_title->SetFont(font); + SetCursor(wxCURSOR_ARROW); + }); + bed_type_title->Bind(wxEVT_LEFT_UP, [bed_type_title, this](wxMouseEvent &e) { + wxLaunchDefaultBrowser("https://wiki.bambulab.com/en/x1/manual/compatibility-and-parameter-settings-of-filaments"); + }); + + 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; + m_bed_type_list->Select(bed_type_idx); + bed_type_sizer->Add(bed_type_title, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + bed_type_sizer->Add(m_bed_type_list, 1, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(10)); + vsizer_printer->Add(bed_type_sizer, 0, wxEXPAND | wxTOP, FromDIP(5)); + vsizer_printer->AddSpacer(FromDIP(16)); + + 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)); + + p->m_panel_printer_content->SetSizer(vsizer_printer); + p->m_panel_printer_content->Layout(); + //xiamian- + scrolled_sizer->Add(p->m_panel_printer_content, 0, wxEXPAND, 0); + } + + { + // 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)) + return; + if (p->m_panel_filament_content->GetMaxHeight() == 0) + p->m_panel_filament_content->SetMaxSize({-1, -1}); + else + p->m_panel_filament_content->SetMaxSize({-1, 0}); + 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("Filament"), LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(10)); + 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->SetFont(Label::Body_10); + p->m_flushing_volume_btn->SetPaddingSize(wxSize(FromDIP(8),FromDIP(3))); + p->m_flushing_volume_btn->SetCornerRadius(FromDIP(8)); + + StateColor flush_bg_col(std::pair(wxColour(219, 253, 231), StateColor::Pressed), + std::pair(wxColour(238, 238, 238), StateColor::Hovered), + std::pair(wxColour(238, 238, 238), StateColor::Normal)); + + StateColor flush_fg_col(std::pair(wxColour(107, 107, 106), StateColor::Pressed), + std::pair(wxColour(107, 107, 106), StateColor::Hovered), + std::pair(wxColour(107, 107, 106), StateColor::Normal)); + + StateColor flush_bd_col(std::pair(wxColour(0, 174, 66), StateColor::Pressed), + std::pair(wxColour(0, 174, 66), StateColor::Hovered), + std::pair(wxColour(172, 172, 172), StateColor::Normal)); + + p->m_flushing_volume_btn->SetBackgroundColor(flush_bg_col); + p->m_flushing_volume_btn->SetBorderColor(flush_bd_col); + p->m_flushing_volume_btn->SetTextColor(flush_fg_col); + p->m_flushing_volume_btn->SetFocus(); + p->m_flushing_volume_btn->SetId(wxID_RESET); + p->m_flushing_volume_btn->Rescale(); + + p->m_flushing_volume_btn->Bind(wxEVT_BUTTON, ([parent](wxCommandEvent &e) + { + auto& project_config = wxGetApp().preset_bundle->project_config; + const std::vector& init_matrix = (project_config.option("flush_volumes_matrix"))->values; + const std::vector& init_extruders = (project_config.option("flush_volumes_vector"))->values; + ConfigOptionFloat* flush_multi_opt = project_config.option("flush_multiplier"); + float flush_multiplier = flush_multi_opt ? flush_multi_opt->getFloat() : 1.f; + + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + const auto& full_config = wxGetApp().preset_bundle->full_config(); + const auto& extra_flush_volumes = get_min_flush_volumes(full_config); + WipingDialog dlg(parent, cast(init_matrix), cast(init_extruders), extruder_colours, extra_flush_volumes, flush_multiplier); + if (dlg.ShowModal() == wxID_OK) { + std::vector matrix = dlg.get_matrix(); + std::vector extruders = dlg.get_extruders(); + (project_config.option("flush_volumes_matrix"))->values = std::vector(matrix.begin(), matrix.end()); + (project_config.option("flush_volumes_vector"))->values = std::vector(extruders.begin(), extruders.end()); + (project_config.option("flush_multiplier"))->set(new ConfigOptionFloat(dlg.get_flush_multiplier())); + + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + + wxGetApp().plater()->update_project_dirty_from_presets(); + wxPostEvent(parent, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, parent)); + } + })); + + bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); + bSizer39->Hide(p->m_flushing_volume_btn); + bSizer39->Add(FromDIP(10), 0, 0, 0, 0 ); + + 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; + + bSizer39->Add(add_btn, 0, wxALIGN_CENTER|wxALL, FromDIP(5)); + bSizer39->Add(FromDIP(10), 0, 0, 0, 0 ); + + 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_VERTICAL, FromDIP(5)); + bSizer39->Add(FromDIP(20), 0, 0, 0, 0); + + ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, false, 18); + ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); + ams_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { + sync_ams_list(); + }); + p->m_bpButton_ams_filament = ams_btn; + + bSizer39->Add(ams_btn, 0, wxALIGN_CENTER|wxALL, FromDIP(5)); + 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); + bSizer39->Add(FromDIP(15), 0, 0, 0, 0); + + // add filament content + p->m_panel_filament_content = new wxPanel( p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + p->m_panel_filament_content->SetBackgroundColour( wxColour( 255, 255, 255 ) ); + + //wxBoxSizer* bSizer_filament_content; + //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); + + // BBS: filament double columns + 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 */ + p->combos_filament[0] = new PlaterPresetComboBox(p->m_panel_filament_content, Preset::TYPE_FILAMENT); + auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); + // BBS: filament double columns + combo_and_btn_sizer->Add(FromDIP(8), 0, 0, 0, 0); + if (p->combos_filament[0]->clr_picker) { + p->combos_filament[0]->clr_picker->SetLabel("1"); + combo_and_btn_sizer->Add(p->combos_filament[0]->clr_picker, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(3)); + } + combo_and_btn_sizer->Add(p->combos_filament[0], 1, wxALL | wxEXPAND, FromDIP(2))->SetMinSize({-1, FromDIP(30) }); + + ScalableButton* edit_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "edit"); + edit_btn->SetBackgroundColour(wxColour(255, 255, 255)); + edit_btn->SetToolTip(_L("Click to edit preset")); + + PlaterPresetComboBox* combobox = p->combos_filament[0]; + edit_btn->Bind(wxEVT_BUTTON, [this, combobox](wxCommandEvent) + { + p->editing_filament = 0; + combobox->switch_to_tab(); + }); + combobox->edit_btn = edit_btn; + + combo_and_btn_sizer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + combo_and_btn_sizer->Add(FromDIP(8), 0, 0, 0, 0); + + p->combos_filament[0]->set_filament_idx(0); + p->sizer_filaments->GetItem((size_t)0)->GetSizer()->Add(combo_and_btn_sizer, 1, wxEXPAND); + + //bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); + wxSizer *sizer_filaments2 = new wxBoxSizer(wxVERTICAL); + sizer_filaments2->AddSpacer(FromDIP(16)); + sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); + sizer_filaments2->AddSpacer(FromDIP(16)); + p->m_panel_filament_content->SetSizer(sizer_filaments2); + p->m_panel_filament_content->Layout(); + scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND, 0); + } + + { + /***************** 1. create config title bar **************/ + // 1.1 create title bar resources + p->m_panel_config_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_config_title->SetBackgroundColor(title_bg); + p->m_panel_config_title->SetBackgroundColor2(0xF1F1F1); + + p->m_config_icon = new ScalableButton(p->m_panel_config_title, wxID_ANY, "config"); + p->m_text_config_settings = new Label(p->m_panel_config_title, _L("Configuration"), LB_PROPAGATE_MOUSE_EVENT); + + p->m_config_icon->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + //auto wizard_t = new ConfigWizard(wxGetApp().mainframe); + //wizard_t->run(ConfigWizard::RR_USER, ConfigWizard::SP_CUSTOM); + }); + + wxBoxSizer* h_sizer_title = new wxBoxSizer(wxHORIZONTAL); + h_sizer_title->Add(p->m_config_icon, 0, wxALIGN_CENTRE | wxLEFT | wxRIGHT, em); + h_sizer_title->Add(p->m_text_config_settings, 0, wxALIGN_CENTER); + h_sizer_title->AddStretchSpacer(); + h_sizer_title->SetMinSize(-1, 3 * em); + + p->m_panel_config_title->SetSizer(h_sizer_title); + p->m_panel_config_title->Layout(); + + // add printer title + scrolled_sizer->Add(p->m_panel_config_title, 0, wxEXPAND | wxALL, 0); + p->m_panel_config_title->Bind(wxEVT_LEFT_UP, [this](auto& e) { + if (p->m_panel_config_content->GetMaxHeight() == 0) + p->m_panel_config_content->SetMaxSize({ -1, -1 }); + else + p->m_panel_config_content->SetMaxSize({ -1, 0 }); + 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 config content ************************/ + p->m_panel_config_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + p->m_panel_config_content->SetBackgroundColour(wxColour(255, 255, 255)); + + PlaterPresetComboBox* combo_config = new PlaterPresetComboBox(p->m_panel_config_content, Preset::TYPE_PRINTER); + ScalableButton* edit_btn = new ScalableButton(p->m_panel_config_content, wxID_ANY, "edit"); + edit_btn->SetToolTip(_L("Click to edit preset")); + edit_btn->Bind(wxEVT_BUTTON, [this, combo_config](wxCommandEvent) + { + m_soft_first_start = false; + p->editing_filament = -1; + if (combo_config->switch_to_tab()) + p->editing_filament = 0; + }); + combo_config->edit_btn = edit_btn; + p->combo_config = combo_config; + + wxBoxSizer* vsizer_config = new wxBoxSizer(wxVERTICAL); + wxBoxSizer* hsizer_config = new wxBoxSizer(wxHORIZONTAL); + + vsizer_config->AddSpacer(FromDIP(16)); + hsizer_config->Add(combo_config, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + hsizer_config->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + hsizer_config->Add(FromDIP(8), 0, 0, 0, 0); + vsizer_config->Add(hsizer_config, 0, wxEXPAND, 0); + vsizer_config->AddSpacer(FromDIP(16)); + + p->m_panel_config_content->SetSizer(vsizer_config); + p->m_panel_config_content->Layout(); + scrolled_sizer->Add(p->m_panel_config_content, 0, wxEXPAND, 0); + + } + + + { + //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); + + p->m_search_bar = new wxSearchCtrl(p->scrolled, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); + p->m_search_bar->ShowSearchButton(true); + p->m_search_bar->ShowCancelButton(true); + p->m_search_bar->SetDescriptiveText(_L("Search plate, object and part.")); + + p->m_search_bar->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent&) { + this->p->on_search_update(); + wxPoint pos = this->p->m_search_bar->ClientToScreen(wxPoint(0, 0)); + pos.y += this->p->m_search_bar->GetRect().height; + p->dia->SetPosition(pos); + p->dia->Popup(); + }); + p->m_search_bar->Bind(wxEVT_COMMAND_TEXT_UPDATED, [this](wxCommandEvent&) { + this->p->on_search_update(); + }); + p->m_search_bar->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { + p->dia->Dismiss(); + e.Skip(); + }); + + p->m_object_list = new ObjectList(p->scrolled); + + 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->m_search_bar); +#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::create_printer_preset() +{ + CreatePrinterPresetDialog dlg(wxGetApp().mainframe); + int res = dlg.ShowModal(); + if (wxID_OK == res) { + wxGetApp().mainframe->update_side_preset_ui(); + update_ui_from_settings(); + update_all_preset_comboboxes(); + wxGetApp().load_current_presets(); + 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(); + combo_and_btn_sizer->Add(FromDIP(8), 0, 0, 0, 0 ); + (*combo)->clr_picker->SetLabel(wxString::Format("%d", filament_idx + 1)); + combo_and_btn_sizer->Add((*combo)->clr_picker, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(3)); + combo_and_btn_sizer->Add(*combo, 1, wxALL | wxEXPAND, FromDIP(2))->SetMinSize({-1, FromDIP(30)}); + + /* 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_filaments_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, "edit"); + edit_btn->SetToolTip(_L("Click to edit preset")); + + PlaterPresetComboBox* combobox = (*combo); + edit_btn->Bind(wxEVT_BUTTON, [this, combobox, filament_idx](wxCommandEvent) + { + p->editing_filament = -1; + if (combobox->switch_to_tab()) + p->editing_filament = filament_idx; // sync with TabPresetComboxBox's m_filament_idx + }); + combobox->edit_btn = edit_btn; + + combo_and_btn_sizer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + + combo_and_btn_sizer->Add(FromDIP(8), 0, 0, 0, 0); + + // 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) { + 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::set_bed_by_curr_bed_type(AppConfig *config) { + if (config && !config->get("curr_bed_type").empty()) { + int bed_type_idx = 0; + std::string str_bed_type = config->get("curr_bed_type"); + int bed_type_value = (int) btPC; + try { + bed_type_value = atoi(str_bed_type.c_str()); + } catch (...) { + } + bed_type_idx = bed_type_value - 1; + m_bed_type_list->SelectAndNotify(bed_type_idx); + } else { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":error:curr_bed_type is empty"; + } +} + +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_preset = preset_bundle.printers.get_edited_preset().is_bbl_vendor_preset(&preset_bundle); + auto cur_preset_name = preset_bundle.printers.get_edited_preset().name; + auto p_mainframe = wxGetApp().mainframe; + + p_mainframe->show_device(is_bbl_preset); + if (is_bbl_preset) { + //only show connection button for not-BBL printer + connection_btn->Hide(); + //only show sync-ams button for BBL printer + ams_btn->Show(); + //update print button default value for bbl or third-party printer + p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); + AppConfig* config = wxGetApp().app_config; + if (config) { + m_update_3d_state = true; + if (m_soft_first_start && !wxGetApp().get_app_conf_exists()) { + use_default_bed_type(); + } else { + auto user_bed_type_flag = config->get("user_bed_type") == "true"; + if (!user_bed_type_flag) { //bed_type not follow machine + set_bed_by_curr_bed_type(config); + } else {//bed_type follow machine + if (m_is_gcode_file) {//.gcode.3mf case + m_is_gcode_file = false; + set_bed_by_curr_bed_type(config); + } + else if (user_bed_type_flag) { + if (config->has_section("user_bed_type_list")) { + auto user_bed_type_list = config->get_section("user_bed_type_list"); + if (user_bed_type_list.size() > 0 && user_bed_type_list[cur_preset_name].size() > 0) { + set_bed_type(user_bed_type_list[cur_preset_name]); + } else { + use_default_bed_type(); + } + } else { + use_default_bed_type(); + } + } + } + } + } else { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":error:AppConfig is nullptr"; + } + m_bed_type_list->Enable(); + } else { + connection_btn->Show(); + ams_btn->Hide(); + p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::eSendGcode); + auto cfg = preset_bundle.printers.get_edited_preset().config; + wxString url; + if (cfg.has("print_host_webui") && !cfg.opt_string("print_host_webui").empty()) { + url = cfg.opt_string("print_host_webui"); + } else if (cfg.has("print_host") && !cfg.opt_string("print_host").empty()) { + url = cfg.opt_string("print_host"); + } else { + ; + } + + if(!url.empty()) + { + if(!url.Lower().starts_with("http")) + url = wxString::Format("http://%s",url); + + p_mainframe->load_printer_url(url); + } + + m_bed_type_list->SelectAndNotify(btPEI - 1); + m_bed_type_list->Disable(); + } + + // 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(); +} + +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(); + + dynamic_filament_list.update(); + 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(); + } + + Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + + bool isBBL = printer_preset.is_bbl_vendor_preset(wxGetApp().preset_bundle); + wxGetApp().mainframe->show_calibration_button(!isBBL); + + 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; + + 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!"); +} + +bool Sidebar::set_bed_type(const std::string &bed_type_name) +{ + auto bed_type_keys = print_config_def.get("curr_bed_type")->enum_values; + for (size_t i = 0; i < bed_type_keys.size(); i++) { + if (bed_type_name == bed_type_keys[i]) { + m_bed_type_list->SelectAndNotify(i); + return true; + } + } + return false; +} + +void Sidebar::save_bed_type_to_config(const std::string &bed_type_name) +{ + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + auto cur_preset_name = preset_bundle.printers.get_edited_preset().name; + if (cur_preset_name.size() > 0) { + if (!wxGetApp().app_config->has_section("user_bed_type_list")) { + std::map data; + data[cur_preset_name] = bed_type_name; + wxGetApp().app_config->set_section("user_bed_type_list", data); + } else { + auto data = wxGetApp().app_config->get_section("user_bed_type_list"); + auto data_modify = const_cast *>(&data); + (*data_modify)[cur_preset_name] = bed_type_name; + wxGetApp().app_config->set_section("user_bed_type_list", *data_modify); + } + } +} + +bool Sidebar::use_default_bed_type(bool is_bbl_preset) +{ + auto bundle = wxGetApp().preset_bundle; + const Preset *curr = &bundle->printers.get_selected_preset(); + const VendorProfile::PrinterModel *pm = PresetUtils::system_printer_model(*curr); + if (is_bbl_preset && pm && pm->default_bed_type.size() > 0) { + return set_bed_type(pm->default_bed_type); + } + int selection = m_bed_type_list->GetSelection(); + std::string bed_type_name = print_config_def.get("curr_bed_type")->enum_values[selection]; + save_bed_type_to_config(bed_type_name); + return false; +} + +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::msw_rescale() +{ + SetMinSize(wxSize(42 * 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_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(); + //BBS + m_bed_type_list->Rescale(); + m_bed_type_list->SetMinSize({-1, 3 * wxGetApp().em_unit()}); +#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(); + p->combo_printer->msw_rescale(); + for (PlaterPresetComboBox* combo : p->combos_filament) + combo->msw_rescale(); + + // 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(); + + // 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 + //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_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(); + + // 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 + for (PlaterPresetComboBox* combo : p->combos_filament) + combo->sys_color_changed(); + + // 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_filaments_change(). +void Sidebar::on_filaments_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); + choices.push_back(choice); + + // initialize selection + choice->update(); + ++i; + } + + // remove unused choices if any + remove_unused_filament_combos(num_filaments); + + auto sizer = p->m_panel_filament_title->GetSizer(); + if (p->m_flushing_volume_btn != nullptr && sizer != nullptr) { + if (num_filaments > 1) + sizer->Show(p->m_flushing_volume_btn); + else + sizer->Hide(p->m_flushing_volume_btn); + } + + Layout(); + p->m_panel_filament_title->Refresh(); + update_ui_from_settings(); + dynamic_filament_list.update(); +} + +void Sidebar::add_filament() { + // BBS: limit filament choices to 16 + if (p->combos_filament.size() >= 16) return; + wxColour new_col = Plater::get_next_color_for_filament(); + add_custom_filament(new_col); +} + +void Sidebar::delete_filament() { + if (p->combos_filament.size() <= 1) return; + + size_t filament_count = p->combos_filament.size() - 1; + if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_count) || (filament_count == 1)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + } + + if (p->editing_filament >= filament_count) { + p->editing_filament = -1; + } + + wxGetApp().preset_bundle->set_num_filaments(filament_count); + wxGetApp().plater()->on_filaments_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); +} + +void Sidebar::add_custom_filament(wxColour new_col) { + if (p->combos_filament.size() >= 16) 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()->on_filaments_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); +} + +void Sidebar::on_bed_type_change(BedType bed_type) +{ + // btDefault option is not included in global bed type setting + int sel_idx = (int)bed_type - 1; + if (m_bed_type_list != nullptr) + m_bed_type_list->SetSelection(sel_idx); +} + +std::map Sidebar::build_filament_ams_list(MachineObject* obj) +{ + std::map filament_ams_list; + if (!obj) return filament_ams_list; + + auto vt_tray = obj->vt_tray; + if (obj->ams_support_virtual_tray) { + DynamicPrintConfig vt_tray_config; + vt_tray_config.set_key_value("filament_id", new ConfigOptionStrings{ vt_tray.setting_id }); + vt_tray_config.set_key_value("tag_uid", new ConfigOptionStrings{ vt_tray.tag_uid }); + vt_tray_config.set_key_value("filament_type", new ConfigOptionStrings{ vt_tray.type }); + vt_tray_config.set_key_value("tray_name", new ConfigOptionStrings{ std::string("Ext") }); + vt_tray_config.set_key_value("filament_colour", new ConfigOptionStrings{ into_u8(wxColour("#" + vt_tray.color).GetAsString(wxC2S_HTML_SYNTAX)) }); + vt_tray_config.set_key_value("filament_exist", new ConfigOptionBools{ true }); + + vt_tray_config.set_key_value("filament_multi_colors", new ConfigOptionStrings{}); + for (int i = 0; i < vt_tray.cols.size(); ++i) { + vt_tray_config.opt("filament_multi_colors")->values.push_back(into_u8(wxColour("#" + vt_tray.cols[i]).GetAsString(wxC2S_HTML_SYNTAX))); + } + filament_ams_list.emplace(VIRTUAL_TRAY_ID, std::move(vt_tray_config)); + } + + auto list = obj->amsList; + for (auto ams : list) { + char n = ams.first.front() - '0' + 'A'; + for (auto tray : ams.second->trayList) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ + << boost::format(": ams %1% tray %2% id %3% color %4%") % ams.first % tray.first % tray.second->setting_id % tray.second->color; + char t = tray.first.front() - '0' + '1'; + DynamicPrintConfig tray_config; + tray_config.set_key_value("filament_id", new ConfigOptionStrings{ tray.second->setting_id }); + tray_config.set_key_value("tag_uid", new ConfigOptionStrings{ tray.second->tag_uid }); + tray_config.set_key_value("filament_type", new ConfigOptionStrings{ tray.second->type }); + tray_config.set_key_value("tray_name", new ConfigOptionStrings{ std::string(1, n) + std::string(1, t) }); + tray_config.set_key_value("filament_colour", new ConfigOptionStrings{ into_u8(wxColour("#" + tray.second->color).GetAsString(wxC2S_HTML_SYNTAX)) }); + tray_config.set_key_value("filament_exist", new ConfigOptionBools{ tray.second->is_exists }); + + tray_config.set_key_value("filament_multi_colors", new ConfigOptionStrings{}); + for (int i = 0; i < tray.second->cols.size(); ++i) { + tray_config.opt("filament_multi_colors")->values.push_back(into_u8(wxColour("#" + tray.second->cols[i]).GetAsString(wxC2S_HTML_SYNTAX))); + } + filament_ams_list.emplace(((n - 'A') * 4 + t - '1'), std::move(tray_config)); + } + } + return filament_ams_list; +} + +void Sidebar::load_ams_list(std::string const &device, MachineObject* obj) +{ + std::map filament_ams_list = build_filament_ams_list(obj); + + p->ams_list_device = device; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size(); + if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list) + return; + wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; + + for (auto c : p->combos_filament) + c->update(); +} + +void Sidebar::sync_ams_list() +{ + auto & list = wxGetApp().preset_bundle->filament_ams_list; + if (list.empty()) { + MessageDialog dlg(this, + _L("No AMS filaments. Please select a printer in 'Device' page to load AMS info."), + _L("Sync filaments with AMS"), wxOK); + dlg.ShowModal(); + 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(",")); + struct SyncAmsDialog : MessageDialog { + SyncAmsDialog(wxWindow * parent, bool first): MessageDialog(parent, + first + ? _L("Sync filaments with AMS will drop all current selected filament presets and colors. Do you want to continue?") + : _L("Already did a synchronization, do you want to sync only changes or resync all?"), + _L("Sync filaments with AMS"), 0) + { + if (first) { + add_button(wxID_YES, true, _L("Yes")); + } else { + add_button(wxID_OK, true, _L("Sync")); + add_button(wxID_YES, false, _L("Resync")); + } + add_button(wxID_CANCEL, false, _L("No")); + } + } dlg(this, ams_filament_ids.empty()); + auto res = dlg.ShowModal(); + if (res == wxID_CANCEL) 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{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]); + } + + unsigned int unknowns = 0; + auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns); + if (n == 0) { + MessageDialog dlg(this, + _L("There are no compatible filaments, and sync is not performed."), + _L("Sync filaments with AMS"), wxOK); + dlg.ShowModal(); + return; + } + ams_filament_ids = boost::algorithm::join(list2, ","); + wxGetApp().app_config ->set("ams_filament_ids", p->ams_list_device, ams_filament_ids); + if (unknowns > 0) { + MessageDialog dlg(this, + _L("There are some unknown or uncompatible filaments mapped to generic preset. Please update Bambu Studio or restart Bambu Studio to check if there is an update to system presets."), + _L("Sync filaments with AMS"), wxOK); + dlg.ShowModal(); + } + wxGetApp().plater()->on_filaments_change(n); + for (auto& c : p->combos_filament) + c->update(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0]); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + dynamic_filament_list.update(); + // Expand filament list + p->m_panel_filament_content->SetMaxSize({-1, -1}); + // 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 changed + if (color_before_sync[i] != color_opt->values[i]) { + auto_calc_flushing_volumes(i); + } + // color don't change, but changes between supporting filament and non supporting filament + else { + bool flag = is_support_filament(i); + if (flag != is_support_before[i]) + auto_calc_flushing_volumes(i); + } + } + } + Layout(); +} + +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::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; +} + +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->is_collapsed; } + +void Sidebar::collapse(bool collapse) +{ + p->is_collapsed = collapse; + + this->Show(!collapse); + p->plater->Layout(); + + // save collapsing state to the AppConfig + //if (wxGetApp().is_editor()) + // wxGetApp().app_config->set_bool("collapsed_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; +} + +Search::OptionsSearcher& Sidebar::get_searcher() +{ + return p->searcher; +} + +std::string& Sidebar::get_search_line() +{ + return p->searcher.search_string(); +} + +void Sidebar::set_is_gcode_file(bool flag) +{ + m_is_gcode_file = flag; + if (m_is_gcode_file) { + wxGetApp().plater()->force_update_all_plate_thumbnails(); + } +} + +void Sidebar::auto_calc_flushing_volumes(const int modify_id) +{ + auto& preset_bundle = wxGetApp().preset_bundle; + auto& project_config = preset_bundle->project_config; + auto& printer_config = preset_bundle->printers.get_edited_preset().config; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; + auto& ams_filament_list = preset_bundle->filament_ams_list; + + const std::vector& init_matrix = (project_config.option("flush_volumes_matrix"))->values; + const std::vector& init_extruders = (project_config.option("flush_volumes_vector"))->values; + + const std::vector& min_flush_volumes= get_min_flush_volumes(full_config); + + ConfigOptionFloat* flush_multi_opt = project_config.option("flush_multiplier"); + float flush_multiplier = flush_multi_opt ? flush_multi_opt->getFloat() : 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); + 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); + 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; + } + } + } + } + (project_config.option("flush_volumes_matrix"))->values = std::vector(matrix.begin(), matrix.end()); + + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + + wxGetApp().plater()->update_project_dirty_from_presets(); + wxPostEvent(this, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, this)); +} + +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(Plater* plater) : m_plater(plater) { this->SetDefaultAction(wxDragCopy); } + + virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames); + + void handleOnIdle(wxIdleEvent & event); + +private: + Plater* m_plater; + wxArrayString m_filenames; +}; + +bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) +{ +#ifdef WIN32 + // hides the system icon + this->MSWUpdateDragImageOnLeave(); +#endif // WIN32 + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": drag %1% files into app")%filenames.size(); + m_filenames = filenames; + wxGetApp().Bind(wxEVT_IDLE, &PlaterDropTarget::handleOnIdle, this); + return true; +} + +void PlaterDropTarget::handleOnIdle(wxIdleEvent &event) +{ + wxGetApp().mainframe->Raise(); + wxGetApp().Unbind(wxEVT_IDLE, &PlaterDropTarget::handleOnIdle, this); + if (m_plater != nullptr) { + m_plater->load_files(m_filenames); + wxGetApp().mainframe->update_title(); + } + //m_filenames.clear(); +} + +// State to manage showing after export notifications and device ejecting +enum ExportingStatus{ + NOT_EXPORTING, + EXPORTING_TO_REMOVABLE, + EXPORTING_TO_LOCAL +}; + +// Plater / private +struct Plater::priv +{ + // PIMPL back pointer ("Q-Pointer") + Plater *q; + 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 + wxSizer* panel_sizer{ nullptr }; + wxPanel* current_panel{ nullptr }; + wxPanel* status_panel{ nullptr }; + std::vector panels; + Sidebar *sidebar; + 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 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 }; + std::chrono::system_clock::time_point start; + std::string file_type; + std::string is_mw{"false"}; + +#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 }; + + // Jobs defined inside the group class will be managed so that only one can + // run at a time. Also, the background process will be stopped if a job is + // started. It is up the the plater to ensure that the background slicing + // can't be restarted while a ui job is still running. + class Jobs: public ExclusiveJobGroup + { + priv *m; + size_t m_arrange_id, m_fill_bed_id, m_rotoptimize_id, m_sla_import_id, m_orient_id; + std::shared_ptr m_pri; + //BBS + size_t m_print_id; + + void before_start() override { m->background_process.stop(); } + + public: + Jobs(priv *_m) : + m(_m), + m_pri{std::make_shared(m->notification_manager.get())} + { + m_arrange_id = add_job(std::make_unique(m_pri, m->q)); + m_orient_id = add_job(std::make_unique(m_pri, m->q)); + m_fill_bed_id = add_job(std::make_unique(m_pri, m->q)); + m_rotoptimize_id = add_job(std::make_unique(m_pri, m->q)); + m_sla_import_id = add_job(std::make_unique(m_pri, m->q)); + //BBS add print id + m_print_id = add_job(std::make_unique(m_pri, m->q)); + } + + void arrange() + { + m->take_snapshot("Arrange"); + start(m_arrange_id); + } + + void orient() + { + m->take_snapshot("Orient"); + start(m_orient_id); + } + + void fill_bed() + { + m->take_snapshot("Fill bed"); + start(m_fill_bed_id); + } + + void optimize_rotation() + { + m->take_snapshot("Optimize Rotation"); + start(m_rotoptimize_id); + } + + void import_sla_arch() + { + m->take_snapshot("Import SLA archive"); + start(m_sla_import_id); + } + + //BBS bbl printing job + void print() + { + start(m_print_id); + } + } m_ui_jobs; + + int m_job_prepare_state; + + bool delayed_scene_refresh; + std::string delayed_error_message; + + wxTimer background_process_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); } + + 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); + } + + bool is_sidebar_collapsed() const { return sidebar->is_collapsed(); } + void collapse_sidebar(bool collapse); + + 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(); + + // 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); + + 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 mirror(Axis axis); + void split_object(); + 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 + } + void update_print_volume_state(); + void schedule_background_process(); + // 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 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 on_plate_name_change(SimpleEvent &); + 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 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&); + + // 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 double printable_height, 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_edit_text() 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_netfabb() const; + bool can_simplify() 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_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, bool use_top_view = false, bool for_picking = false,bool ban_light = false); + ThumbnailsList generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type); + //BBS + void generate_calibration_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params); + 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) ; + void set_project_filename(const wxString& filename); + + //BBS store bbs project name + wxString get_project_name(); + void set_project_name(const wxString& project_name); + + // Call after plater and Canvas#D is initialized + void init_notification_manager(); + + // 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 }; + + //record print preset + void record_start_print_preset(std::string action); +}; + +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); + +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", "bed_custom_texture", "bed_custom_model", "print_sequence", + "extruder_clearance_radius", "extruder_clearance_max_radius", + "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", + "nozzle_height", "skirt_loops", "skirt_distance", + "brim_width", "brim_object_gap", "brim_type", "nozzle_diameter", "single_extruder_multi_material", + "enable_prime_tower", "wipe_tower_x", "wipe_tower_y", "prime_tower_width", "prime_tower_brim_width", "prime_volume", + "extruder_colour", "filament_colour", "material_colour", "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", + "brim_width", "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", + "best_object_pos" + })) + , sidebar(new Sidebar(q)) + , notification_manager(std::make_unique(q)) + , m_ui_jobs(this) + , 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) +{ + 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_ADD_FILAMENT, &priv::on_add_filament, this); + this->q->Bind(EVT_DEL_FILAMENT, &priv::on_delete_filament, this); + this->q->Bind(EVT_ADD_CUSTOM_FILAMENT, &priv::on_add_custom_filament, this); + view3D = new View3D(q, bed, &model, config, &background_process); + //BBS: use partplater's gcode + preview = new Preview(q, bed, &model, config, &background_process, partplate_list.get_current_slice_result(), [this]() { schedule_background_process(); }); + + assemble_view = new AssembleView(q, bed, &model, config, &background_process); + + status_panel = new wxPanel(q, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + + + +#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->q->Bind(wxEVT_TIMER, [this](wxTimerEvent &evt) + { + if (!this->suppressed_backround_processing_update) + this->update_restart_background_process(false, false); + }); + + update(); + + auto* hsizer = new wxBoxSizer(wxHORIZONTAL); + auto* vsizer = new wxBoxSizer(wxVERTICAL); + + // BBS: move sidebar to left side + hsizer->Add(sidebar, 0, wxEXPAND | wxLEFT | wxRIGHT, 0); + auto spliter_1 = new ::StaticLine(q, true); + spliter_1->SetLineColour("#A6A9AA"); + hsizer->Add(spliter_1, 0, wxEXPAND); + + + + panel_sizer = new wxBoxSizer(wxVERTICAL); + 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_sizer->Add(vsizer_status, 2, wxEXPAND | wxALL, 0); + vsizer->Add(panel_sizer, 9, wxEXPAND | wxALL, 0); + status_panel->SetBackgroundColour(wxColor(236, 246, 248)); + vsizer->Add(status_panel, 1, wxEXPAND | wxALL, 2); + //xiamian + //hsizer->Add(vsizer, 1, wxEXPAND | wxALL, 0); + hsizer->Insert(0, vsizer, 1, wxEXPAND | wxALL, 0); + q->SetSizer(hsizer); + 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_PLATE_NAME_CHANGE, &priv::on_plate_name_change, 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 arrage 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 arrage 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 oriant 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 oriant 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_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 arrage 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 arrage 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_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent &) { update(); }); + assemble_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent &) { update(); }); + + 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_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); + } + + // Drop target: + q->SetDropTarget(new PlaterDropTarget(q)); // if my understanding is right, wxWindow takes the owenership + q->Layout(); + + 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 (...) {} + 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->Raise(); + this->q->load_files(input_files); + }); + 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); + //} +} + +Plater::priv::~priv() +{ + if (config != nullptr) + delete config; + // Saves the database of visited (already shown) hints into hints.ini. + notification_manager->deactivate_loaded_hints(); +} + +//void Plater::priv::text_update() { +// +//} +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); + this->preview->reload_print(); + // BBS assemble view + this->assemble_view->reload_scene(false, flags); + + if (current_panel && q->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 +} + +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); + } +} + +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] = { + *wxYELLOW, + * wxRED, + *wxBLUE, + *wxCYAN, + *wxLIGHT_GREY, + *wxWHITE, + *wxBLACK, + wxColour(0,127,255), + wxColour(139,0,255), + wxColour(102,255,0), + wxColour(255,215,0), + wxColour(0,35,100), + wxColour(255,0,255), + wxColour(8,37,103), + wxColour(127,255,212), + wxColour(255,191,0) + }; + 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 { + 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 +#ifdef SUPPORT_FREE_CAMERA + && wxGetApp().app_config->get("use_free_camera") != "1" +#endif + ) + 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::collapse_sidebar(bool collapse) +{ + if (q->m_only_gcode && !collapse) + return; + sidebar->collapse(collapse); + notification_manager->set_sidebar_collapsed(collapse); +} + + +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, current_depth, current_height; + + if (input_files.empty()) { return std::vector(); } + + // 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; + { + DynamicPrintConfig config_loaded; + + // BBS: add part plate related logic + PlateDataPtrs plate_data; + En3mfType en_3mf_file_type = En3mfType::From_BBS; + 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_3mf %3%, file_version %4% \n") % plate_data.size() % + project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS) % 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 < 16 && 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_filaments_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(SLIC3R_VERSION)); + if (en_3mf_file_type == En3mfType::From_Prusa) { + // do not reset the model config + load_config = false; + if(load_type != LoadType::LoadGeometry) + show_info(q, _L("The 3mf is not from Bambu Lab, load geometry data only."), _L("Load 3mf")); + } + else if (load_config && (file_version.maj() > app_version.maj())) { + // version mismatch, only load geometries + load_config = false; + if (!load_model) { + // only load config case, return directly + show_info(q, _L("The Config can not be loaded."), _L("Load 3mf")); + q->skip_thumbnail_invalid = false; + return empty_result; + } + load_old_project = true; + // select view to 3D + q->select_view_3D("3D"); + // select plate 0 as default + q->select_plate(0); + if (load_type != LoadType::LoadGeometry) { + if (en_3mf_file_type == En3mfType::From_BBS) + show_info(q, _L("Due to the lower version of Bambu Studio, this 3mf file cannot be fully loaded. Please update Bambu Studio to the latest version"), _L("Load 3mf")); + else + show_info(q, _L("The 3mf is not from Bambu Lab, load geometry data only."), _L("Load 3mf")); + } + for (ModelObject *model_object : model.objects) { + model_object->config.reset(); + // Is there any modifier or advanced config data? + for (ModelVolume *model_volume : model_object->volumes) model_volume->config.reset(); + } + } else if (load_config && (file_version > app_version)) { + Semver cloud_ver; + if (wxGetApp().app_config->has("app", "cloud_version")) { + std::string cloud_version = wxGetApp().app_config->get("app", "cloud_version"); + cloud_ver = *(Semver::parse(cloud_version)); + } else { + cloud_ver = app_version; + } + if (config_substitutions.unrecogized_keys.size() > 0) { + // std::string context = into_u8(text); + wxString context; + if (wxGetApp().app_config->get("user_mode") == "develop") { + context = _L("Found following keys unrecognized:\n"); + for (auto &key : config_substitutions.unrecogized_keys) { + context += " -"; + context += key; + context += ";\n"; + } + } + context += "\n\n"; + Newer3mfVersionDialog newer_dlg(q, &file_version, &cloud_ver, context); + newer_dlg.ShowModal(); + } + else { + //if the minor version is not matched + if (file_version.min() != app_version.min()) { + Newer3mfVersionDialog newer_dlg(q, &file_version, &cloud_ver, ""); + auto res = newer_dlg.ShowModal(); + } + } + } 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) && (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); + } + 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(); + } 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; + + 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 following modified G-codes in filament or printer presets:") + warning_message+ _L("Please confirm that these modified G-codes are safe to prevent any damage to the machine!"), _L("Modified G-codes")); + + MessageDialog dlg(q, _L("The 3mf has following modified G-codes in filament or printer presets:") + warning_message+ _L("Please confirm that these modified G-codes are safe to prevent any damage to the machine!"), _L("Modified G-codes")); + 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 following customized filament or printer presets:") + warning_message + _L("Please confirm that the G-codes within these presets are safe to prevent any damage to the machine!"), _L("Customized Preset")); + MessageDialog dlg(q, _L("The 3mf has following customized filament or printer presets:") + from_u8(warning_message)+ _L("Please confirm that the G-codes within these presets are 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->printers.get_edited_preset().is_bbl_vendor_preset(&(*preset_bundle)); + 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, (boost::algorithm::ends_with(filename.string(), ".gcode.3mf") || + boost::algorithm::ends_with(filename.string(), ".gcode")) ? + true : + false); + } + + // 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_filaments_change(preset_bundle->filament_presets.size()); + is_project_file = true; + + //BBS: rewrite wipe tower pos stored in 3mf file , the code above should be seriously reconsidered + { + DynamicConfig& proj_cfg = wxGetApp().preset_bundle->project_config; + 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; + } + } + } + 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](std::vector &input_colors, bool is_single_color, std::vector &filament_ids, + unsigned char &first_extruder_id) { + 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, input_colors, is_single_color, extruder_colours, filament_ids, first_extruder_id); + if (color_dlg.ShowModal() != wxID_OK) { + filament_ids.clear(); + } + }; + 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; + }, + [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) + Slic3r::GUI::show_info(nullptr, _L("Name of components inside step file is not UTF8 format!") + "\n\n" + _L("The name may show garbage characters!"), + _L("Attention!")); + }, + 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(); + + 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(); + } + + // 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; + } + + 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 + + if (ask_multi) { + MessageDialog msg_dlg(q, _L("Load these files as a single object with multiple parts?\n"), _L("Object with multiple parts was detected"), + wxICON_WARNING | wxYES | wxNO); + if (msg_dlg.ShowModal() == wxID_YES) { new_model->convert_multipart_object(filaments_cnt); } + } + + auto loaded_idxs = load_model_objects(new_model->objects); + obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); + } + + 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); + } + partplate_list.reset_size(current_width, current_depth, current_height, true, true); + } + + //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 (!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) {} + } + } + std::chrono::system_clock::time_point default_time; + if (start == default_time) { + start = std::chrono::system_clock::now(); + } + if (!input_files.empty()) { + auto path = input_files.front(); + file_type = path.extension().string(); + + if (model.model_info == nullptr) { + is_mw = "false"; + } + else if(model.model_info->description != "") { + is_mw = "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) +{ + 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 heat 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 heat 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; + } + } + } + + 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_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_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("Comfirm 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_volume(*selection.get_volume_idxs().begin()); + 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; + const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; + + 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_ui_jobs.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_ui_jobs.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_ui_jobs.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.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_ui_jobs.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(); + + //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.plates_custom_gcodes.clear(); + + // BBS + m_saved_timestamp = m_backup_timestamp = size_t(-1); +} + +void Plater::priv::center_selection() +{ + view3D->center_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(); +} + +void Plater::priv::split_object() +{ + int obj_idx = get_selected_object_idx(); + 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); + 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"); + + 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); + + // 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::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->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); + auto action_fn = (mo || !warning.opt_key.empty()) ? [id = mo ? mo->id() : 0, opt = warning.opt_key](wxEvtHandler *) { + auto & objects = wxGetApp().model().objects; + 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}}); + } + if (!opt.empty()) { + if (iter != objects.end()) + wxGetApp().params_panel()->switch_to_object(); + wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); + } + return false; + } : std::function(); + auto hypertext = (mo || !warning.opt_key.empty()) ? _u8L("Jump to") : ""; + if (mo) hypertext += std::string(" [") + mo->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) + { + // Update the "out of print bed" state of ModelInstances. + this->update_print_volume_state(); + //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()); + } + Print::ApplyStatus invalidated = background_process.apply(this->model, wxGetApp().preset_bundle->full_config()); + + 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); + 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_ui_jobs.is_any_running()) { + // 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 { + 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->bounding_box().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(true) * (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(); + 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); + 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_volume(*selection.get_volume_idxs().begin()); + 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 STL")) + 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); + } +} + +#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](std::vector &input_colors, bool is_single_color, std::vector &filament_ids, unsigned char &first_extruder_id) { + if (!boost::iends_with(path, ".obj")) { return; } + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + ObjColorDialog color_dlg(nullptr, input_colors, is_single_color, extruder_colours, filament_ids, first_extruder_id); + if (color_dlg.ShowModal() != wxID_OK) { 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 + new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, &plate_data, &project_presets, nullptr, + nullptr, 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->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; + 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; + } + } + + 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()); + + Transform3d transform = Transform3d::Identity(); + transform.translate(new_volume->source.mesh_offset - old_volume->source.mesh_offset); + new_volume->set_transformation(old_volume->get_transformation().get_matrix() * old_volume->source.transform.get_matrix(true) * + transform * new_volume->source.transform.get_matrix(true).inverse()); + + 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[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(true) * (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(); + bool model_fits = this->view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; + //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(true); + else + this->update_fff_scene_only_shells(); + } + + preview->set_as_dirty(); + }; + + //BBS: add the collapse logic + if (panel == preview) { + if (q->only_gcode_mode()) { + this->sidebar->collapse(true); + 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()->clear_select_plate_toolbar_render_flag(); + } + + if (current_panel == panel) + { + //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) { + //BBS: only switch to the first panel when visible + panel->Show(); + //dynamic_cast(panel)->get_canvas3d()->render(); + if (!panel->IsShownOnScreen()) + return; + } +//#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(); + } + + panel_sizer->Layout(); + + 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) +{ + ComboBox *combo = static_cast(evt.GetEventObject()); + if (combo && combo->is_drop_down()) { + sidebar->update_soft_first_start_state(); + } + PlaterPresetComboBox* preset_combo_box = dynamic_cast(evt.GetEventObject()); + if (preset_combo_box) { + this->on_select_preset(evt); + } + else { + this->on_select_bed_type(evt); + } +} + +void Plater::priv::on_select_bed_type(wxCommandEvent &evt) +{ + ComboBox* combo = static_cast(evt.GetEventObject()); + int selection = combo->GetSelection(); + std::string bed_type_name = print_config_def.get("curr_bed_type")->enum_values[selection]; + + 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; + auto user_bed_type_flag = wxGetApp().app_config->get("user_bed_type") == "true"; + if (combo && combo->is_drop_down() && user_bed_type_flag) { // save user behavior + sidebar->save_bed_type_to_config(bed_type_name); + } + 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()); + + // only update curr_bed_type to config when preset is bbl printers + bool is_bbl_preset = preset_bundle.printers.get_edited_preset().is_bbl_vendor_preset(&preset_bundle); + if (is_bbl_preset) { + // update app_config + AppConfig* app_config = wxGetApp().app_config; + app_config->set("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()); + + std::string preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, + Preset::remove_suffix_modified(combo->GetString(selection).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); + dynamic_filament_list.update(); + bool flag_is_change = is_support_filament(idx); + if (flag != flag_is_change) { + sidebar->auto_calc_flushing_volumes(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(); + } + //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 + } + +#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_when_filament_change") == "true") { + 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_ui_jobs.is_any_running()) { + // 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 | 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(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); + } + } + } + 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" ; + } + + //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(); + record_start_print_preset("print_plate"); +} + +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(); +} + +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" ; + } + + //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(); + record_start_print_preset("print_all"); +} + +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) +{ + wxGetApp().obj_list()->update_selections(); + selection_changed(); +} + +void Plater::priv::on_plate_name_change(SimpleEvent &) { + wxGetApp().obj_list()->update_selections(); + selection_changed(); +} + +//BBS: repair model through netfabb +void Plater::priv::on_repair_model(wxCommandEvent &event) +{ + wxGetApp().obj_list()->fix_through_netfabb(); +} + +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") == "true") { + 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() + "/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"); + } + app_config->save(); +} + +void Plater::priv::show_install_plugin_hint(wxCommandEvent &event) +{ + notification_manager->bbl_show_plugin_install_notification(into_u8(_L("Network Plug-in is 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 gcode only, Can not 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); + } +} + +void Plater::priv::on_change_color_mode(SimpleEvent& evt) { + m_is_dark = wxGetApp().app_config->get("dark_color_mode") == "1"; + 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(); +} + +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; + if (current_panel != assemble_view) { + 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 { + menu = is_some_full_instances ? menus.object_menu() : + is_part ? menus.part_menu() : menus.multi_selection_menu(); + } + } + } + + if (q != nullptr && menu) { +#ifdef __linux__ + // For some reason on Linux the menu isn't displayed if position is specified + // (even though the position is sane). + q->PopupMenu(menu); +#else + //BBS: GUI refactor: move sidebar to the left + int x, y; + current_panel->GetPosition(&x, &y); + q->PopupMenu(menu, (int)evt.data.first.x() + x, (int)evt.data.first.y()); + //q->PopupMenu(menu); +#endif + } +} + +//BBS: add part plate related logic +void Plater::priv::on_plate_right_click(RBtnPlateEvent& evt) +{ + wxMenu* menu = menus.plate_menu(); + +#ifdef __linux__ + q->PopupMenu(menu); +#else + //BBS: GUI refactor: move sidebar to the left + int x, y; + current_panel->GetPosition(&x, &y); + q->PopupMenu(menu, (int)evt.data.first.x() + x, (int)evt.data.first.y()); + //q->PopupMenu(menu); +#endif +} + +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, bool use_top_view, bool for_picking, bool ban_light) +{ + view3D->get_canvas3d()->render_thumbnail(data, w, h, thumbnail_params, camera_type, use_top_view, 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; +} + +void Plater::priv::generate_calibration_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params) +{ + preview->get_canvas3d()->render_calibration_thumbnail(data, w, h, thumbnail_params); +} + +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); + // 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); + 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) +{ + wxString curr_project_name = m_project_name; + + std::string plate_index_str = ""; + std::string plate_name = partplate_list.get_curr_plate()->get_plate_name(); + + // remove unsupported characters in filename + curr_project_name = from_u8(filter_characters(curr_project_name.ToUTF8().data(), "<>[]:/\\|?*\"")); + plate_name = filter_characters(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((curr_project_name + extension).mb_str(wxConvUTF8)); + return from_path(full_filename); + } else { + auto full_filename = m_project_folder / std::string((curr_project_name + from_u8(plate_index_str) + extension).mb_str(wxConvUTF8)); + return from_path(full_filename); + } + } else { + if (export_all) + return curr_project_name + extension; + else + return curr_project_name + from_u8(plate_index_str) + extension; + } + } else { + if (only_filename) { + if (export_all) + return curr_project_name + extension; + else + return curr_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 __WINDOWS__ + wxGetApp().mainframe->SetTitle(m_project_name + " - BambuStudio"); + wxGetApp().mainframe->topbar()->SetTitle(m_project_name); +#else + wxGetApp().mainframe->SetTitle(m_project_name); + if (!m_project_name.IsEmpty()) + wxGetApp().mainframe->update_title_colour_after_set_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::orient() +{ + p->m_ui_jobs.orient(); +} + +//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; + } +} + +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::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(5.0f); + collapse_toolbar.set_separator_size(5); + 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 = "*.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_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 double printable_height, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) +{ + //BBS: add shape position + Vec2d shape_position = partplate_list.get_current_shape_position(); + bool new_shape = bed.set_shape(shape, printable_height, 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(); + new_shape |= (height_to_lid != prev_height_lid) || (height_to_rod != prev_height_rod) || (prev_exclude_areas != 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.extended_bounding_box().max; + Vec3d min = bed.extended_bounding_box().min; + double z = config->opt_float("printable_height"); + + //Pointfs& exclude_areas = config->option("bed_exclude_area")->values; + 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, 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, 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_edit_text() const +{ + const Selection &selection = view3D->get_canvas3d()->get_selection(); + if (selection.is_single_full_instance()) + return true; + + if (selection.is_single_volume()) { + const GLVolume *gl_volume = selection.get_volume(*selection.get_volume_idxs().begin()); + int out_object_idx = gl_volume->object_idx(); + ModelObject * model_object = selection.get_model()->objects[out_object_idx]; + int out_volume_idx = gl_volume->volume_idx(); + ModelVolume * model_volume = model_object->volumes[out_volume_idx]; + if (model_volume) + return !model_volume->get_text_info().m_text.empty(); + } + return false; +} + +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_netfabb() const +{ + std::vector obj_idxs, vol_idxs; + sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs); + +#if FIX_THROUGH_NETFABB_ALWAYS + // Fixing always. + return ! obj_idxs.empty() || ! vol_idxs.empty(); +#else // FIX_THROUGH_NETFABB_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_NETFABB_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_increase_instances() const +{ + if (m_ui_jobs.is_any_running() + || 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_ui_jobs.is_any_running() + || 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_ui_jobs.is_any_running(); +} + +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]->bounding_box().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(); + } + wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_FILAMENTS); +} + +void Plater::priv::on_modify_filament(SimpleEvent &evt) +{ + FilamentInfomation *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); + } else + wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_FILAMENTS); + +} + +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()); +} + +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) << "Bambu Studio 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()->Raise(); // bring window to front + wxGetApp().GetTopWindow()->Show(true); // show the window + } +} + +//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 Plater::priv::record_start_print_preset(std::string action) { + // record start print preset + try { + json j; + j["user_mode"] = wxGetApp().get_mode_str(); + int plate_count = partplate_list.get_plate_count(); + j["plate_count"] = plate_count; + unsigned int obj_count = model.objects.size(); + j["obj_count"] = obj_count; + 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"); + } + 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"); + } + + json j_system; + if (background_process.fff_print()) { + const DynamicPrintConfig& full_config = background_process.fff_print()->full_print_config(); + if (full_config.has("different_settings_to_system")) { + std::vector different_values = full_config.option("different_settings_to_system")->values; + std::vector values; + for (int i = 0; i < different_values.size(); ++i) { + if (different_values[i] == "") + continue; + boost::split(values, different_values[i], boost::is_any_of(";")); + for (int k = 0; k < values.size(); ++k) { + std::string str = values[k]; + const ConfigOption* config = full_config.option(str); + if (config) + j_system[str] = config->serialize(); + } + } + } + } + j["global_diff"] = j_system; + + PartPlate* curr_plate = partplate_list.get_curr_plate(); + + json j_object; + if (action == "print_plate") { + std::map modify_object_setting = curr_plate->get_diff_object_setting(); + for (auto it = modify_object_setting.cbegin(); it != modify_object_setting.cend(); ++it) { + j_object[it->first] = it->second; + } + } + else { + for (int i = 0; i < model.objects.size(); ++i) { + const ModelConfigObject& diff_object_config = model.objects[i]->config; + for (auto it = diff_object_config.cbegin(); it != diff_object_config.cend(); ++it) { + std::string config_name = it->first; + std::string config_value = it->second->serialize(); + if (j_object.find(config_name) == j_object.end()) { + j_object[config_name] = config_value; + } + } + } + } + j["object_diff"] = j_object; + + json j_plate; + if (action == "print_plate") { + std::map diff_plate_setting = curr_plate->get_diff_plate_setting(); + for (auto it = diff_plate_setting.cbegin(); it != diff_plate_setting.cend(); ++it) { + j_plate["plate_" + std::to_string(curr_plate->get_index())][it->first] = it->second; + } + } + else { + for (int i = 0; i < plate_count; ++i) { + std::string key = "plate_" + std::to_string(i); + DynamicPrintConfig* diff_plate_config = partplate_list.get_plate(i)->config(); + for (auto it = diff_plate_config->cbegin(); it != diff_plate_config->cend(); ++it) { + std::string diff_config_name = it->first; + std::string diff_config_value; + if (diff_config_name == "first_layer_print_sequence") { + diff_config_value = "cutomize"; + } + else { + diff_config_value = it->second->serialize(); + } + j_plate[key][diff_config_name] = diff_config_value; + } + } + } + j["plate_diff"] = j_plate; + + json j_workflow_debug; + std::chrono::system_clock::time_point end = std::chrono::system_clock::now(); + std::chrono::duration duration = std::chrono::duration_cast>(end - start); + int times = duration.count(); + j_workflow_debug["duration"] = times; + + j["record_event"] = action; + NetworkAgent* agent = wxGetApp().getAgent(); + if (agent) { + j_workflow_debug["file_type"] = file_type; + j_workflow_debug["is_mw"] = is_mw; + + agent->track_event("user_start_print", j.dump()); + agent->track_event("workflow_debug", j_workflow_debug.dump()); + } + } + catch (...) { + return; + } + +} + +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); +} + +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) +{ + bool transfer_preset_changes = false; + // BBS: save confirm + auto check = [&transfer_preset_changes](bool yes_or_no) { + 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 for the new project, discard or save changes as new presets.")); + using ab = UnsavedChangesDialog::ActionButtons; + int act_buttons = ab::KEEP | ab::REMEMBER_CHOISE; + if (!yes_or_no) + act_buttons |= ab::SAVE; + return wxGetApp().check_and_keep_current_preset_changes(_L("Creating a new project"), header, act_buttons, &transfer_preset_changes); + }; + int result; + if (!skip_confirm && (result = close_with_confirm(check)) == wxID_CANCEL) + return wxID_CANCEL; + + //BBS: add only gcode mode + bool previous_gcode = m_only_gcode; + + m_only_gcode = false; + m_exported_file = false; + m_loading_project = 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); + + 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; + if (previous_gcode) + collapse_sidebar(false); + + up_to_date(true, false); + up_to_date(true, true); + return wxID_YES; +} + + + +// BBS: FIXME, missing resotre logic +void Plater::load_project(wxString const& filename2, + wxString const& originfile) +{ + 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: add only gcode mode + bool previous_gcode = m_only_gcode; + + // 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 != "-") { + strategy = strategy | LoadStrategy::Restore; + } + 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(); + } + + if (previous_gcode) + collapse_sidebar(false); + + 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(); + + 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 + if (export_3mf(into_path(filename), SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::FullPathSources) < 0) { + MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs open the project file or if there is enough disk space."), + _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"] = into_path(filename).filename().string(); + + NetworkAgent* agent = wxGetApp().getAgent(); + if (agent) agent->track_event("save_project", j.dump()); + } + catch (...) {} + + 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& error) + { + //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] { + + NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent(); + if (!m_agent) return; + + int res = 0; + unsigned int http_code; + std::string http_body; + + msg = _L("prepare 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& error) + { + //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 Bambu Studio 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(); + dlg.Close(); + if (download_ok) { + BOOST_LOG_TRIVIAL(trace) << "import_model_id: target_path = " << target_path.string(); + /* load project */ + this->load_project(target_path.wstring()); + /*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) { + p->set_project_filename(wxString::FromUTF8(paths[0].string())); + } + + wxGetApp().mainframe->update_title(); + } +} + +std::array get_cut_plane(const BoundingBoxf3 &bbox, const double &cut_height) +{ + std::array plane_pts; + plane_pts[0] = Vec3d(bbox.min(0), bbox.min(1), cut_height); + plane_pts[1] = Vec3d(bbox.max(0), bbox.min(1), cut_height); + plane_pts[2] = Vec3d(bbox.max(0), bbox.max(1), cut_height); + plane_pts[3] = Vec3d(bbox.min(0), bbox.max(1), cut_height); + return plane_pts; +} + +void Plater::calib_pa(const Calib_Params ¶ms) +{ + const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); + if (new_project(false, false, calib_pa_name) == wxID_CANCEL) + return; + wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + switch (params.mode) { + case CalibMode::Calib_PA_Line: + add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/pressure_advance_test.stl"); + break; + case CalibMode::Calib_PA_Pattern: + _calib_pa_pattern(params); + break; + case CalibMode::Calib_PA_Tower: + _calib_pa_tower(params); + break; + default: break; + } + + try { + json js; + if (params.mode == CalibMode::Calib_PA_Line) + js["cali_type"] = "third_cali_pa_line"; + else if (params.mode == CalibMode::Calib_PA_Pattern) + js["cali_type"] = "third_cali_pa_pattern"; + else if (params.mode == CalibMode::Calib_PA_Tower) + js["cali_type"] = "third_cali_pa_tower"; + + std::string filament_id = wxGetApp().preset_bundle->filaments.get_edited_preset().filament_id; + js["filament_id"] = filament_id; + + NetworkAgent *agent = GUI::wxGetApp().getAgent(); + if (agent) + agent->track_event("third_cali", js.dump()); + } catch (...) {} + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::_calib_pa_pattern(const Calib_Params ¶ms) +{ + // add "handle" cube + sidebar().obj_list()->load_generic_subobject("Cube", ModelVolumeType::INVALID); + orient(); + changed_objects({0}); + _calib_pa_select_added_objects(); + + const DynamicPrintConfig &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + DynamicPrintConfig & print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + float nozzle_diameter = printer_config.option("nozzle_diameter")->get_at(0); + + for (const auto opt : SuggestedConfigCalibPAPattern().float_pairs) { + print_config.set_key_value(opt.first, new ConfigOptionFloat(opt.second)); + } + print_config.set_key_value("outer_wall_speed", + new ConfigOptionFloat(CalibPressureAdvance::find_optimal_PA_speed( + wxGetApp().preset_bundle->full_config(), print_config.get_abs_value("line_width"), + print_config.get_abs_value("layer_height"), 0))); + + for (const auto opt : SuggestedConfigCalibPAPattern().nozzle_ratio_pairs) { + print_config.set_key_value(opt.first, new ConfigOptionFloat(nozzle_diameter * opt.second / 100)); + } + + 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)); + + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + + const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(); + PresetBundle * preset_bundle = wxGetApp().preset_bundle; + const bool is_bbl_machine = preset_bundle->printers.get_edited_preset().has_lidar(preset_bundle); + const Vec3d plate_origin = get_partplate_list().get_current_plate_origin(); + CalibPressureAdvancePattern pa_pattern(params, full_config, is_bbl_machine, model(), plate_origin); + + // scale cube to suit test + GizmoObjectManipulation &giz_obj_manip = p->view3D->get_canvas3d()->get_gizmos_manager().get_object_manipulation(); + giz_obj_manip.set_uniform_scaling(true); + giz_obj_manip.on_change("size", 0, pa_pattern.handle_xy_size()); + giz_obj_manip.set_uniform_scaling(false); + giz_obj_manip.on_change("size", 2, pa_pattern.max_layer_z()); + // start with pattern centered on plate + center_selection(); + const Vec3d plate_center = get_partplate_list().get_curr_plate()->get_center_origin(); + giz_obj_manip.on_change("position", 0, plate_center.x() - (pa_pattern.print_size_x() / 2)); + giz_obj_manip.on_change("position", 1, plate_center.y() - (pa_pattern.print_size_y() / 2) - pa_pattern.handle_spacing()); + + pa_pattern.generate_custom_gcodes(full_config, is_bbl_machine, model(), plate_origin); + model().calib_pa_pattern = std::make_unique(pa_pattern); + changed_objects({0}); +} + +void Plater::_calib_pa_tower(const Calib_Params ¶ms) +{ + add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/tower_with_seam.stl"); + + 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 float nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); + + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionInts{1}); + //print_config->set_key_value("default_jerk", new ConfigOptionFloat(1.0f)); + //print_config->set_key_value("outer_wall_jerk", new ConfigOptionFloat(1.0f)); + //print_config->set_key_value("inner_wall_jerk", new ConfigOptionFloat(1.0f)); + 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"), + full_config.get_abs_value("layer_height"), 0); + print_config->set_key_value("outer_wall_speed", new ConfigOptionFloat(wall_speed)); + print_config->set_key_value("inner_wall_speed", new ConfigOptionFloat(wall_speed)); + // print_config->set_key_value("wall_generator", new ConfigOptionEnum(PerimeterGeneratorType::Classic)); + const auto _wall_generator = print_config->option>("wall_generator"); + if (_wall_generator->value == PerimeterGeneratorType::Arachne) print_config->set_key_value("wall_transition_angle", new ConfigOptionFloat(25)); + model().objects[0]->config.set_key_value("seam_position", new ConfigOptionEnum(spRear)); + + 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(); + if (new_height < obj_bb.size().z()) { + std::array plane_pts = get_cut_plane(obj_bb, new_height); + cut(0, 0, plane_pts, 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(); + } +} + + +void Plater::calib_flowrate(int pass) +{ + if (pass != 1 && pass != 2) return; + const auto 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 (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()); + + 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 7 layers + 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); + + float zscale = (first_layer_height + 6 * layer_height) / 1.4; + // only enlarge + if (xyScale > 1.2) { + for (auto _obj : model().objects) _obj->scale(xyScale, xyScale, zscale); + } else { + for (auto _obj : model().objects) _obj->scale(1, 1, zscale); + } + + 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 = 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 : model().objects) { + _obj->ensure_on_bed(); + _obj->config.set_key_value("wall_loops", new ConfigOptionInt(3)); + _obj->config.set_key_value("top_one_wall_type", new ConfigOptionEnum(TopOneWallType::Topmost)); + _obj->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(35)); + _obj->config.set_key_value("top_area_threshold", new ConfigOptionPercent(100)); + _obj->config.set_key_value("bottom_shell_layers", new ConfigOptionInt(1)); + _obj->config.set_key_value("top_shell_layers", new ConfigOptionInt(5)); + _obj->config.set_key_value("detect_thin_wall", new ConfigOptionBool(true)); + _obj->config.set_key_value("filter_out_gap_fill", new ConfigOptionFloat(0)); // todo: OrcaSlicer parameter + _obj->config.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + _obj->config.set_key_value("top_surface_line_width", new ConfigOptionFloat(nozzle_diameter * 1.2f)); + _obj->config.set_key_value("internal_solid_infill_line_width", new ConfigOptionFloat(nozzle_diameter * 1.2f)); + _obj->config.set_key_value("top_surface_pattern", new ConfigOptionEnum(ipMonotonic)); + _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("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)); + + // 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] = '-'; + auto modifier = stof(obj_name); + _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("initial_layer_print_height", new ConfigOptionFloat(first_layer_height)); + print_config->set_key_value("reduce_crossing_wall", new ConfigOptionBool(true)); + // filament_config->set_key_value("filament_max_volumetric_speed", new ConfigOptionFloats{ 9. }); + + 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(); + + try { + json js; + if (pass == 1) + js["cali_type"] = "third_cali_flow_rate_1"; + if (pass == 2) + js["cali_type"] = "third_cali_flow_rate_2"; + + std::string filament_id = wxGetApp().preset_bundle->filaments.get_edited_preset().filament_id; + js["filament_id"] = filament_id; + + NetworkAgent *agent = GUI::wxGetApp().getAgent(); + if (agent) agent->track_event("third_cali", js.dump()); + } catch (...) {} + + Calib_Params params; + params.mode = CalibMode::Calib_Flow_Rate; + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::calib_temp(const Calib_Params ¶ms) +{ + try { + json js; + js["cali_type"] = "third_cali_temp"; + std::string filament_id = wxGetApp().preset_bundle->filaments.get_edited_preset().filament_id; + js["filament_id"] = filament_id; + + NetworkAgent *agent = GUI::wxGetApp().getAgent(); + if (agent) agent->track_event("third_cali", js.dump()); + } catch (...) {} + + const auto calib_temp_name = wxString::Format(L"Nozzle temperature test"); + if (new_project(false, false, calib_temp_name) == wxID_CANCEL) + return; + + 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.stl"); + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto start_temp = lround(params.start); + 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("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)); + + 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(); + + // cut upper + auto obj_bb = model().objects[0]->bounding_box(); + auto block_count = lround((350 - params.end) / 5 + 1); + if (block_count > 0) { + // add EPSILON offset to avoid cutting at the exact location where the flat surface is + auto new_height = block_count * 10.0 + EPSILON; + if (new_height < obj_bb.size().z()) { + std::array plane_pts = get_cut_plane(obj_bb, new_height); + cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepLower); + } + } + + // cut bottom + obj_bb = model().objects[0]->bounding_box(); + block_count = lround((350 - params.start) / 5); + if (block_count > 0) { + auto new_height = block_count * 10.0 + EPSILON; + if (new_height < obj_bb.size().z()) { + std::array plane_pts = get_cut_plane(obj_bb, new_height); + cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepUpper); + } + } + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::calib_max_vol_speed(const Calib_Params ¶ms) +{ + try { + json js; + js["cali_type"] = "third_cali_max_flowrate"; + std::string filament_id = wxGetApp().preset_bundle->filaments.get_edited_preset().filament_id; + js["filament_id"] = filament_id; + + NetworkAgent *agent = GUI::wxGetApp().getAgent(); + if (agent) agent->track_event("third_cali", js.dump()); + } catch (...) {} + + const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test"); + if (new_project(false, false, calib_vol_speed_name) == wxID_CANCEL) + return; + 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.step"); + + 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 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().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 ConfigOptionInts{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("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("spiral_mode", new ConfigOptionBool(true)); + print_config->set_key_value("outer_wall_line_width", new ConfigOptionFloat(line_width)); + print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(layer_height)); + print_config->set_key_value("layer_height", new ConfigOptionFloat(layer_height)); + obj->config.set_key_value("brim_type", new ConfigOptionEnum(btOuterAndInner)); + obj->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); + obj->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_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(); + auto height = (params.end - params.start + 1) / params.step; + if (height < obj_bb.size().z()) { + std::array plane_pts = get_cut_plane(obj_bb, height); + cut(0, 0, plane_pts, 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 ¶ms) +{ + try { + json js; + js["cali_type"] = "third_cali_retraction"; + std::string filament_id = wxGetApp().preset_bundle->filaments.get_edited_preset().filament_id; + js["filament_id"] = filament_id; + + NetworkAgent *agent = GUI::wxGetApp().getAgent(); + if (agent) agent->track_event("third_cali", js.dump()); + } catch (...) {} + + const auto calib_retraction_name = wxString::Format(L"Retraction test"); + if (new_project(false, false, calib_retraction_name) == wxID_CANCEL) + return; + 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.stl"); + + 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]; + + double layer_height = 0.2; + + auto max_lh = printer_config->option("max_layer_height"); + if (max_lh->values[0] < layer_height) max_lh->values[0] = {layer_height}; + + 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)); + obj->config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(layer_height)); + obj->config.set_key_value("layer_height", new ConfigOptionFloat(layer_height)); + + changed_objects({0}); + + // cut upper + auto obj_bb = obj->bounding_box(); + auto height = 1.0 + 0.4 + ((params.end - params.start)) / params.step; + if (height < obj_bb.size().z()) { + std::array plane_pts = get_cut_plane(obj_bb, height); + cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepLower); + } + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::calib_VFA(const Calib_Params ¶ms) +{ + try { + json js; + js["cali_type"] = "third_cali_VFA"; + std::string filament_id = wxGetApp().preset_bundle->filaments.get_edited_preset().filament_id; + js["filament_id"] = filament_id; + + NetworkAgent *agent = GUI::wxGetApp().getAgent(); + if (agent) agent->track_event("third_cali", js.dump()); + } catch (...) {} + + const auto calib_vfa_name = wxString::Format(L"VFA test"); + if (new_project(false, false, calib_vfa_name) == wxID_CANCEL) + return; + 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.stl"); + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionInts{0}); + 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("spiral_mode", new ConfigOptionBool(true)); + 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(); + auto height = 5 * ((params.end - params.start) / params.step + 1); + if (height < obj_bb.size().z()) { + std::array plane_pts = get_cut_plane(obj_bb, height); + cut(0, 0, plane_pts, ModelObjectCutAttribute::KeepLower); + } + + p->background_process.fff_print()->set_calib_params(params); +} + +void Plater::import_sl1_archive() +{ + if (!p->m_ui_jobs.is_any_running()) + p->m_ui_jobs.import_sla_arch(); +} + +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(false, 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; + try + { + 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()); + + //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(false, 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); + p->partplate_list.get_curr_plate()->update_slice_result_valid_state(true); + current_print.apply(this->model(), wxGetApp().preset_bundle->full_config()); + + if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) { + MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("does not contain valid gcode."), + 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); + } + +} + +void Plater::reload_gcode_from_disk() +{ + wxString filename(m_last_loaded_gcode); + m_last_loaded_gcode.clear(); + load_gcode(filename); +} + +void Plater::refresh_print() +{ + p->preview->refresh_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,false,false,true); + } + } +} + +//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()->clear_select_plate_toolbar_render_flag(); + 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); +} + +class RadioBox; +class RadioSelector +{ +public: + int m_select_id; + int m_groupid; + RadioBox *m_radiobox; +}; +WX_DECLARE_LIST(RadioSelector, RadioSelectorList); + +#define PROJECT_DROP_DIALOG_SELECT_PLANE_SIZE wxSize(FromDIP(350), FromDIP(120)) +#define PROJECT_DROP_DIALOG_BUTTON_SIZE wxSize(FromDIP(60), FromDIP(24)) + +class ProjectDropDialog : public DPIDialog +{ +private: + wxColour m_def_color = wxColour(255, 255, 255); + RadioSelectorList m_radio_group; + int m_action{1}; + bool m_show_again; + +public: + ProjectDropDialog(const std::string &filename); + + wxPanel * m_top_line; + wxStaticText *m_fname_title; + wxStaticText *m_fname_f; + wxStaticText *m_fname_s; + StaticBox * m_panel_select; + Button * m_confirm; + Button * m_cancel; + + + void select_radio(int index); + void on_select_radio(wxMouseEvent &event); + void on_select_ok(wxMouseEvent &event); + void on_select_cancel(wxMouseEvent &event); + + int get_select_radio(int groupid); + int get_action() const { return m_action; } + void set_action(int index) { m_action = index; } + + wxBoxSizer *create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, std::string param); + wxBoxSizer *create_item_radiobox(wxString title, wxWindow *parent, int select_id, int groupid); + +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) +{ + // def setting + SetBackgroundColour(m_def_color); + + // icon + std::string icon_path = (boost::format("%1%/images/BambuStudioTitle.ico") % resources_dir()).str(); + SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO)); + + 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->Add(0, 0, 0, wxEXPAND | wxTOP, 20); + + wxBoxSizer *m_sizer_name = new wxBoxSizer(wxVERTICAL); + wxBoxSizer *m_sizer_fline = new wxBoxSizer(wxHORIZONTAL); + + m_fname_title = new wxStaticText(this, wxID_ANY, _L("Please select an action"), wxDefaultPosition, wxDefaultSize, 0); + m_fname_title->Wrap(-1); + m_fname_title->SetFont(::Label::Body_13); + m_fname_title->SetForegroundColour(wxColour(107, 107, 107)); + m_fname_title->SetBackgroundColour(wxColour(255, 255, 255)); + + m_sizer_fline->Add(m_fname_title, 0, wxALL, 0); + m_sizer_fline->Add(0, 0, 0, wxEXPAND | wxLEFT, 5); + + m_fname_f = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); + m_fname_f->SetFont(::Label::Head_13); + m_fname_f->Wrap(-1); + m_fname_f->SetForegroundColour(wxColour(38, 46, 48)); + + m_sizer_fline->Add(m_fname_f, 1, wxALL, 0); + + m_sizer_name->Add(m_sizer_fline, 1, wxEXPAND, 0); + + m_fname_s = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); + m_fname_s->SetFont(::Label::Head_13); + m_fname_s->Wrap(-1); + m_fname_s->SetForegroundColour(wxColour(38, 46, 48)); + + m_sizer_name->Add(m_fname_s, 1, wxALL, 0); + + m_sizer_main->Add(m_sizer_name, 1, wxEXPAND | wxLEFT | wxRIGHT, 40); + + m_sizer_main->Add(0, 0, 0, wxEXPAND | wxTOP, 5); + + m_panel_select = new StaticBox(this, wxID_ANY, wxDefaultPosition, PROJECT_DROP_DIALOG_SELECT_PLANE_SIZE); + StateColor box_colour(std::pair(wxColour("#F8F8F8"), StateColor::Normal)); + StateColor box_border_colour(std::pair(wxColour(*wxWHITE), StateColor::Normal)); + + m_panel_select->SetBackgroundColor(box_colour); + m_panel_select->SetBorderColor(box_border_colour); + m_panel_select->SetCornerRadius(5); + + wxBoxSizer *m_sizer_select_h = new wxBoxSizer(wxHORIZONTAL); + + wxBoxSizer *m_sizer_select_v = new wxBoxSizer(wxVERTICAL); + + + auto select_f = create_item_radiobox(_L("Open as project"), m_panel_select, 1, 0); + auto select_s = create_item_radiobox(_L("Import geometry only"), m_panel_select, 2, 0); + //auto select_t = create_item_radiobox(_L("Import presets only"), m_panel_select,3, 0); + + m_sizer_select_v->Add(select_f, 0, wxEXPAND, 5); + m_sizer_select_v->Add(select_s, 0, wxEXPAND, 5); + //m_sizer_select_v->Add(select_t, 0, wxEXPAND, 5); + select_radio(2); + + m_sizer_select_h->Add(m_sizer_select_v, 0, wxALIGN_CENTER | wxLEFT, 22); + + m_panel_select->SetSizer(m_sizer_select_h); + m_panel_select->Layout(); + m_sizer_main->Add(m_panel_select, 0, wxEXPAND | wxLEFT | wxRIGHT, 40); + + m_sizer_main->Add(0, 0, 0, wxEXPAND | wxTOP, 10); + + wxBoxSizer *m_sizer_bottom = new wxBoxSizer(wxHORIZONTAL); + // hide the "Don't show again" checkbox + //wxBoxSizer *m_sizer_left = new wxBoxSizer(wxHORIZONTAL); + + //auto dont_show_again = create_item_checkbox(_L("Don't show again"), this, _L("Don't show again"), "show_drop_project_dialog"); + //m_sizer_left->Add(dont_show_again, 0, wxALL, 5); + + //m_sizer_bottom->Add(m_sizer_left, 0, wxEXPAND, 5); + + m_sizer_bottom->Add(0, 0, 1, wxEXPAND, 5); + + wxBoxSizer *m_sizer_right = new wxBoxSizer(wxHORIZONTAL); + + m_confirm = new Button(this, _L("OK")); + StateColor btn_bg_green(std::pair(wxColour(27, 136, 68), StateColor::Pressed), std::pair(wxColour(61, 203, 115), StateColor::Hovered), + std::pair(wxColour(0, 174, 66), StateColor::Normal)); + + m_confirm->SetBackgroundColor(btn_bg_green); + m_confirm->SetBorderColor(wxColour(0, 174, 66)); + m_confirm->SetTextColor(wxColour("#FFFFFE")); + m_confirm->SetSize(PROJECT_DROP_DIALOG_BUTTON_SIZE); + m_confirm->SetMinSize(PROJECT_DROP_DIALOG_BUTTON_SIZE); + m_confirm->SetCornerRadius(FromDIP(12)); + m_confirm->Bind(wxEVT_LEFT_DOWN, &ProjectDropDialog::on_select_ok, this); + m_sizer_right->Add(m_confirm, 0, wxALL, 5); + + m_cancel = new Button(this, _L("Cancel")); + m_cancel->SetTextColor(wxColour(107, 107, 107)); + m_cancel->SetSize(PROJECT_DROP_DIALOG_BUTTON_SIZE); + m_cancel->SetMinSize(PROJECT_DROP_DIALOG_BUTTON_SIZE); + m_cancel->SetCornerRadius(FromDIP(12)); + m_cancel->Bind(wxEVT_LEFT_DOWN, &ProjectDropDialog::on_select_cancel, this); + m_sizer_right->Add(m_cancel, 0, wxALL, 5); + + m_sizer_bottom->Add( m_sizer_right, 0, wxEXPAND, 5 ); + m_sizer_main->Add(m_sizer_bottom, 0, wxEXPAND | wxLEFT | wxRIGHT, 40); + m_sizer_main->Add(0, 0, 0, wxEXPAND | wxTOP, 20); + + SetSizer(m_sizer_main); + Layout(); + Fit(); + Centre(wxBOTH); + + + auto limit_width = m_fname_f->GetSize().GetWidth() - 2; + auto current_width = 0; + auto cut_index = 0; + auto fstring = wxString(""); + auto bstring = wxString(""); + + //auto file_name = from_u8(filename.c_str()); + auto file_name = wxString(filename); + for (int x = 0; x < file_name.length(); x++) { + current_width += m_fname_s->GetTextExtent(file_name[x]).GetWidth(); + cut_index = x; + + if (current_width > limit_width) { + bstring += file_name[x]; + } else { + fstring += file_name[x]; + } + } + + m_fname_f->SetLabel(fstring); + m_fname_s->SetLabel(bstring); + + wxGetApp().UpdateDlgDarkUI(this); +} + +wxBoxSizer *ProjectDropDialog ::create_item_radiobox(wxString title, wxWindow *parent, int select_id, int groupid) +{ + wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL); + auto radiobox = new RadioBox(parent); + + radiobox->SetBackgroundColour(wxColour(248,248,248)); + sizer->Add(radiobox, 0, wxALL, 5); + sizer->Add(0, 0, 0, wxEXPAND | wxLEFT, 5); + auto text = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0); + text->Wrap(-1); + text->SetForegroundColour(wxColour(107, 107, 107)); + text->SetBackgroundColour(wxColour(248,248,248)); + sizer->Add(text, 0, wxALL, 5); + + radiobox->Bind(wxEVT_LEFT_DOWN, &ProjectDropDialog::on_select_radio, this); + text->Bind(wxEVT_LEFT_DOWN, [this, radiobox](auto &e) { + e.SetId(radiobox->GetId()); + on_select_radio(e); + }); + + RadioSelector *rs = new RadioSelector; + rs->m_groupid = groupid; + rs->m_radiobox = radiobox; + rs->m_select_id = select_id; + m_radio_group.Append(rs); + + return sizer; +} +wxBoxSizer *ProjectDropDialog::create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, std::string param) +{ + wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); + + m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 5); + + auto checkbox = new ::CheckBox(parent); + 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); + m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxALL, 3); + + m_show_again = wxGetApp().app_config->get(param) == "true" ? true : false; + checkbox->SetValue(m_show_again); + + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param](wxCommandEvent &e) { + m_show_again = m_show_again ? false : true; + e.Skip(); + }); + + return m_sizer_checkbox; +} + +void ProjectDropDialog::select_radio(int index) +{ + m_action = index; + RadioSelectorList::Node *node = m_radio_group.GetFirst(); + auto groupid = 0; + + while (node) { + RadioSelector *rs = node->GetData(); + if (rs->m_select_id == index) groupid = rs->m_groupid; + node = node->GetNext(); + } + + node = m_radio_group.GetFirst(); + while (node) { + RadioSelector *rs = node->GetData(); + if (rs->m_groupid == groupid && rs->m_select_id == index) rs->m_radiobox->SetValue(true); + if (rs->m_groupid == groupid && rs->m_select_id != index) rs->m_radiobox->SetValue(false); + node = node->GetNext(); + } +} + +int ProjectDropDialog::get_select_radio(int groupid) +{ + RadioSelectorList::Node *node = m_radio_group.GetFirst(); + while (node) { + RadioSelector *rs = node->GetData(); + if (rs->m_groupid == groupid && rs->m_radiobox->GetValue()) { return rs->m_select_id; } + node = node->GetNext(); + } + + return 0; +} +void ProjectDropDialog::on_select_radio(wxMouseEvent &event) +{ + RadioSelectorList::Node *node = m_radio_group.GetFirst(); + auto groupid = 0; + + while (node) { + RadioSelector *rs = node->GetData(); + if (rs->m_radiobox->GetId() == event.GetId()) groupid = rs->m_groupid; + node = node->GetNext(); + } + + node = m_radio_group.GetFirst(); + while (node) { + RadioSelector *rs = node->GetData(); + if (rs->m_groupid == groupid && rs->m_radiobox->GetId() == event.GetId()) { + set_action(rs->m_select_id); + rs->m_radiobox->SetValue(true); + } + + + if (rs->m_groupid == groupid && rs->m_radiobox->GetId() != event.GetId()) rs->m_radiobox->SetValue(false); + node = node->GetNext(); + } +} + +void ProjectDropDialog::on_select_ok(wxMouseEvent &event) +{ + wxGetApp().app_config->set_bool("show_drop_project_dialog", m_show_again); + EndModal(wxID_OK); +} + +void ProjectDropDialog::on_select_cancel(wxMouseEvent &event) +{ + EndModal(wxID_CANCEL); +} + +void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + m_confirm->SetMinSize(PROJECT_DROP_DIALOG_BUTTON_SIZE); + m_cancel->SetMinSize(PROJECT_DROP_DIALOG_BUTTON_SIZE); + 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)", 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 can not 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("Can not add models when in preview mode!"), _L("Add Models")); + return false; + } + } + + switch (loadfiles_type) { + case LoadFilesType::Single3MF: + open_3mf_file(normal_paths[0]); + break; + + case LoadFilesType::SingleOther: { + Plater::TakeSnapshot snapshot(this, snapshot_label); + 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 (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 (load_files(other_file, LoadStrategy::LoadModel, false).empty()) { res = false; } + break; + default: break; + } + + return res; +} + + +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; + } + + LoadType load_type = LoadType::Unknown; + if (!model().objects.empty()) { + bool show_drop_project_dialog = true; + if (show_drop_project_dialog) { + ProjectDropDialog dlg(filename); + if (dlg.ShowModal() == wxID_OK) { + int choice = dlg.get_action(); + 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); + } + } else + load_type = static_cast( + std::clamp(std::stoi(wxGetApp().app_config->get("import_project_action")), static_cast(LoadType::OpenProject), static_cast(LoadType::LoadConfig))); + } else + load_type = LoadType::OpenProject; + + 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) { + p->set_project_filename(wxString::FromUTF8(paths[0].string())); + + } + wxGetApp().mainframe->update_title(); + } + 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) { + p->set_project_filename(wxString::FromUTF8(paths[0].string())); + } + wxGetApp().mainframe->update_title(); + } + 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();} + break; + default:break; + } +} + +void Plater::update(bool conside_update_flag, bool force_background_processing_update) +{ + 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(); } + +void Plater::stop_jobs() { p->m_ui_jobs.stop_all(); } + +bool Plater::is_any_job_running() const +{ + return p->m_ui_jobs.is_any_running(); +} + +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_collapsed() const { return p->is_sidebar_collapsed(); } +void Plater::collapse_sidebar(bool show) { p->collapse_sidebar(show); } + +//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"); + p->m_ui_jobs.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"); + p->m_ui_jobs.cancel_all(); + + //BBS delete current selected + // p->view3D->delete_selected(); + p->get_current_canvas3D()->delete_selected(); +} + +void Plater::increase_instances(size_t num) +{ + // BBS +#if 0 + 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(); +#endif +} + +void Plater::decrease_instances(size_t num) +{ + // BBS +#if 0 + 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(); +#endif +} + +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_instances() +{ + if (!p->m_ui_jobs.is_any_running()) + p->m_ui_jobs.fill_bed(); +} + +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); + } +} + +// BBS: replace z with plane_points +void Plater::cut(size_t obj_idx, size_t instance_idx, std::array plane_points, 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; + // BBS: replace z with plane_points + const auto new_objects = object->cut(instance_idx, plane_points, attributes); + + remove(obj_idx); + p->load_model_objects(new_objects); + + // 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); +} + +// BBS +void Plater::segment(size_t obj_idx, size_t instance_idx, double smoothing_alpha, int segment_number) +{ + 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"); + + Plater::TakeSnapshot snapshot(this, "Segment"); + + wxBusyCursor wait; + // real process + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + const size_t filament_cnt = print_tech != ptFFF ? 1 : preset_bundle.filament_presets.size(); + const auto new_objects = object->segment(instance_idx, filament_cnt, smoothing_alpha, segment_number); + + remove(obj_idx); + p->load_model_objects(new_objects); + + 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); + } +} + +void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs &cut_objects) +{ + model().delete_object(obj_idx); + sidebar().obj_list()->delete_object_from_list(obj_idx); + p->partplate_list.notify_instance_removed(obj_idx, -1); + + // suppress to call selection update for Object List to avoid call of early Gizmos on/off update + p->load_model_objects(cut_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 < cut_objects.size(); ++i) + selection.add_object((unsigned int) (last_id - i), i == 0); +} + +// BBS +void Plater::merge(size_t obj_idx, std::vector& vol_indeces) +{ + wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); + auto* object = p->model.objects[obj_idx]; + + Plater::TakeSnapshot snapshot(this, "Merge"); + + wxBusyCursor wait; + // real process + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + // BBS + const size_t filament_cnt = print_tech != ptFFF ? 1 : preset_bundle.filament_presets.size(); + + const auto new_objects = object->merge_volumes(vol_indeces); + + remove(obj_idx); + p->load_model_objects(new_objects); + + 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); + } +} + +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(""); + } 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(); + if (agent) agent->track_event("printer_export_gcode", j.dump()); + } 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; + default_output_file = into_path(get_export_gcode_filename(".gcode.3mf", false, export_all)); + if (default_output_file.empty()) { + try { + start_dir = appconfig.get_last_output_dir("", false); + wxString filename = get_export_gcode_filename(".gcode.3mf", true, export_all); + std::string full_filename = start_dir + "/" + filename.utf8_string(); + default_output_file = boost::filesystem::path(full_filename); + } catch(...) { + ; + } + } + + //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()); + if (boost::iends_with(output_path.string(), ".gcode")) { + std::string path = output_path.string(); + path = path.substr(0, path.size() - 6); + output_path = path + ".gcode.3mf"; + } + else if (!boost::iends_with(output_path.string(), ".gcode.3mf")) { + output_path = output_path.replace_extension(".gcode.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); +} + +// 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 agian."); + 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(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 +void Plater::export_stl(bool extended, bool selection_only, bool multi_stls) +{ + if (p->model.objects.empty()) { return; } + + 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(FT_STL); + } + if (path.empty()) { return; } + const std::string path_u8 = into_u8(path); + + wxBusyCursor wait; + const auto& selection = p->get_selection(); + + //confirm export_with_boolean + bool exist_negive_volume = false; + bool export_with_boolean = false; + if (selection_only && !selection.is_multiple_full_object()) { + const auto obj_idx = selection.get_object_idx(); + if (obj_idx == -1 ||selection.is_wipe_tower()) + return; + // only support selection single full object + if (!selection.is_single_full_object()) + return; + const ModelObject *cur_model_object = p->model.objects[obj_idx]; + for (auto v : cur_model_object->volumes) { + if (v->type() == ModelVolumeType::NEGATIVE_VOLUME) { + exist_negive_volume = true; + break; + } + } + } else {//support mulitiple full object// from file mene to export + for (auto cur_model_object : p->model.objects) { + for (auto v : cur_model_object->volumes) { + if (v->type() == ModelVolumeType::NEGATIVE_VOLUME) { + exist_negive_volume = true; + break; + } + } + } + } + + if (exist_negive_volume) { + MessageDialog dlg(this, _L("Negative parts detected. Would you like to perform mesh boolean before exporting?"), _L("Message"), + wxICON_QUESTION | wxYES_NO); + int answer = dlg.ShowModal(); + if (answer == wxID_YES) { + export_with_boolean = true; + } + } + // Following lambda generates a combined mesh for export with normals pointing outwards. + auto mesh_to_export_fff_no_boolean = [](const ModelObject &mo, int instance_id) { + TriangleMesh mesh; + 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; + }; + 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_plater_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_plater_error_notification(msg); }); }; + } else { + mesh_to_export = mesh_to_export_fff_no_boolean; + } + } + else + mesh_to_export = mesh_to_export_sla; + + auto get_save_file = [](std::string const & dir, std::string const & name) { + auto path = dir + name + ".stl"; + int n = 1; + while (boost::filesystem::exists(path)) + path = dir + name + "(" + std::to_string(n++) + ").stl"; + 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_volume(*selection.get_volume_idxs().begin()); + mesh = model_object->volumes[volume->volume_idx()]->mesh(); + mesh.transform(volume->get_volume_transformation().get_matrix(), true); + } + + if (model_object->instances.size() == 1) { + //coconut: make the mesh's origin=(0,0,0). origin_translation is useless here. + mesh.align_to_origin();//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()); + + Slic3r::store_stl(get_save_file(path_u8, object->name).c_str(), &mesh, true); + } + 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()); + Slic3r::store_stl(get_save_file(path_u8, o->name).c_str(), &mesh, true); + } + return; + } + + Slic3r::store_stl(path_u8.c_str(), &mesh, true); +// p->statusbar()->set_status_text(format_wxstr(_L("STL file exported to %s"), path)); +} + +//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 + } +}*/ + +// 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; + + 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,false,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, true, 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, 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_nickanme(); + 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::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()); +} + +//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 arrange and (or) optimize rotation tasks. + this->stop_jobs(); + + // softfever: regenerate CalibPressureAdvancePattern custom G-code to apply changes + if (model().calib_pa_pattern) { + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + + model().calib_pa_pattern->generate_custom_gcodes( + wxGetApp().preset_bundle->full_config(), + preset_bundle->printers.get_edited_preset().is_bbl_vendor_preset(preset_bundle), + model(), + get_partplate_list().get_current_plate_origin() + ); + } + + + 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; + this->SetDropTarget(nullptr); + if (p->m_cur_slice_plate == 0) + reset_gcode_toolpaths(); + return; + } + + if (result) { + p->m_is_slicing = true; + this->SetDropTarget(nullptr); + } + + 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(!clean_gcode_toolpaths); + + 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(); + if (agent) { + agent->track_event("slice_completed", j.dump()); + agent->track_update_property("different_settings_to_system", j["different_settings_to_system"].dump()); + } + } + 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) +{ + // 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; + + // 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(""); + } 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())); + + // Repetier specific: Query the server for the list of file groups. + wxArrayString groups; + { + wxBusyCursor wait; + upload_job.printhost->get_groups(groups); + } + + PrintHostSendDialog dlg(default_output_file, upload_job.printhost->get_post_upload_actions(), groups); + if (dlg.ShowModal() == wxID_OK) { + upload_job.upload_data.upload_path = dlg.filename(); + upload_job.upload_data.post_action = dlg.post_action(); + upload_job.upload_data.group = dlg.group(); + + p->export_gcode(fs::path(), false, std::move(upload_job)); + + try { + json j; + switch (dlg.post_action()) { + case PrintHostPostUploadAction::None: + j["post_action"] = "Upload"; + break; + case PrintHostPostUploadAction::StartPrint: + j["post_action"] = "StartPrint"; + break; + case PrintHostPostUploadAction::StartSimulation: + j["post_action"] = "StartSimulation"; + break; + } + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (preset_bundle) { + j["gcode_printer_model"] = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); + } + + if (physical_printer_config) { + j["printer_preset"] = physical_printer_config->opt_string("inherits"); + } + + NetworkAgent *agent = wxGetApp().getAgent(); + if (agent) agent->track_event("third_party_printer_job", j.dump()); + } catch (...) { + return; + } + } +} +int Plater::send_gcode(int plate_idx, Export3mfProgressFn proFn) +{ + int result = 0; + /* generate 3mf */ + 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; + } + + 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& e) { + 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 */ + 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; + } + + 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& e) { + 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 (Slic3r::GUI::wxGetApp().get_inf_dialog_contect().empty()) { + p->hide_select_machine_dlg(); + } + else { + p->enter_prepare_mode(); + } + + + 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; +} + +// BBS. +void Plater::on_filaments_change(size_t num_filaments) +{ + // only update elements in plater + update_filament_colors_in_full_config(); + sidebar().on_filaments_change(num_filaments); + sidebar().obj_list()->update_objects_list_filament_column(num_filaments); + + 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(num_filaments); + } + + for (ModelObject* mo : wxGetApp().model().objects) { + for (ModelVolume* mv : mo->volumes) { + mv->update_extruder_count(num_filaments); + } + } +} + +void Plater::on_bed_type_change(BedType bed_type, bool is_gcode_file) { + sidebar().set_is_gcode_file(is_gcode_file); + sidebar().on_bed_type_change(bed_type); +} + +bool Plater::update_filament_colors_in_full_config() +{ + DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; + ConfigOptionStrings* color_opt = project_config.option("filament_colour"); + + p->config->option("filament_colour")->values = color_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); + 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(); + dynamic_filament_list.update(); + 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(); + //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 == "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" // ? #ys_FIXME + 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(); + // update to force bed selection(for texturing) + bed_shape_changed = true; + update_scheduled = true; + } + // BBS + else if (opt_key == "support_interface_filament" || + opt_key == "support_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(); +} + +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("printable_height")->value, + 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 double printable_height, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) const +{ + p->set_bed_shape(shape, exclude_area, printable_height, 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; + } +} + +/* 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; +} + +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->view3D->get_canvas3d(); +} + +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() +{ + if (!p->m_ui_jobs.is_any_running()) { + p->m_ui_jobs.arrange(); + } +} + +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 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->supported_facets.reset(); + mv->seam_facets.reset(); + mv->mmu_segmentation_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(int obj_idx) +{ + if (obj_idx < 0) + return; + // recenter and re - align to Z = 0 + p->model.objects[obj_idx]->ensure_on_bed(p->printer_technology != ptSLA); + 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); + + // update print + this->p->schedule_background_process(); +} + +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]->bounding_box().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::mirror(Axis axis) { p->mirror(axis); } +void Plater::split_object() { p->split_object(); } +void Plater::split_volume() { p->split_volume(); } +void Plater::optimize_rotation() { if (!p->m_ui_jobs.is_any_running()) p->m_ui_jobs.optimize_rotation(); } +void Plater::update_menus() { p->menus.update(); } +// 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; + long res = wxGetNumberFromUser("", + _L("Clone"), + _L("Number of copies:"), + 1, 0, 1000, this); + wxString msg; + if (res == -1) { + msg = _L("Invalid number"); + return; + } + Selection& selection = p->get_selection(); + selection.clone(res); +} + +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(); + 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(); +} + +void Plater::enable_collapse_toolbar(bool enable) +{ + p->collapse_toolbar.set_enabled(enable); +} + +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(); + //always apply the current plate's print + Print::ApplyStatus invalidated = p->background_process.apply(this->model(), wxGetApp().preset_bundle->full_config()); + + 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(); + } + + 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 + invalidated = p->background_process.apply(this->model(), wxGetApp().preset_bundle->full_config()); + 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 + refresh_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); + + refresh_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) + 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); + refresh_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; +} + +void Plater::validate_current_plate(bool& model_fits, bool& validate_error) +{ + model_fits = p->view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; + 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; + 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); + + // 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); + } + + if (!model_fits) { + p->notification_manager->push_plater_error_notification(plater_text); + } + else { + p->notification_manager->close_plater_error_notification(plater_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 = BedType(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.ShowModal(); +} + +//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; + + part_plate->get_print(&print, &gcode_result, NULL); + //always apply the current plate's print + invalidated = p->background_process.apply(this->model(), wxGetApp().preset_bundle->full_config()); + 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); + + refresh_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 == 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 + { + 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; +} + +//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); + bool warning = non_manifold_edges > 0; + wxString hyper_text; + std::function callback; + if (warning) { + hyper_text = _L(" (Repair)"); + callback = [](wxEvtHandler*) { + wxCommandEvent* evt = new wxCommandEvent(EVT_REPAIR_MODEL); + wxQueueEvent(wxGetApp().plater(), evt); + return false; + }; + } + + #ifndef __WINDOWS__ + if (non_manifold_edges > 0) { + info_manifold += into_u8("\n" + _L("Tips:") + "\n" +_L("\"Fix Model\" feature is currently only on Windows. Please use a third-party tool to repair the model before importing it into Bambu Studio, such as ")); + } + if (warning) { + std::string repair_url = "https://www.formware.co/onlinestlrepair"; + hyper_text = repair_url + "."; + callback = [repair_url](wxEvtHandler*) { + wxGetApp().open_browser_with_warning_dialog(repair_url); + return false; + }; + } + #endif //APPLE & LINUX + + info_manifold = "" + info_manifold + ""; + //xiamian+ + wxPanel* s_panel = p->status_panel; + auto staticText = new wxStaticText(s_panel, wxID_ANY, wxString::FromUTF8(info_text), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->AddStretchSpacer(20); + //sizer->InsertStretchSpacer(20); + //sizer->InsertSpacer(20); + //sizer->AddSpacer(20); + sizer->AddSpacer(20); + sizer->Add(staticText, 0, wxLEFT, 20); + //sizer->AddSpacer(20); + //sizer->AddStretchSpacer(5); + s_panel->SetSizer(sizer); + s_panel->Layout(); + info_text += into_u8(info_manifold); + notify_manager->bbl_show_objectsinfo_notification(info_text, warning, !(p->current_panel == p->view3D), into_u8(hyper_text), callback); +} + +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]; + 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->is_system) { + filament_name = filament_it->alias; + } else { + auto preset = preset_bundle->filaments.get_preset_base(*filament_it); + if (preset && !preset->alias.empty()) { + filament_name = preset->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 printing, 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; +} + +#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; +} + +void Plater::edit_text() +{ + auto &manager = get_view3D_canvas3D()->get_gizmos_manager(); + manager.open_gizmo(GLGizmosManager::Text); + update(); +} + +bool Plater::can_edit_text() const { return p->can_edit_text(); } +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_netfabb() const { return p->can_fix_through_netfabb(); } +bool Plater::can_simplify() const { return p->can_simplify(); } +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_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) +{ + 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); +} + + +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::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::assemble_multi_selection_menu() { return p->menus.assemble_multi_selection_menu(); } +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); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp new file mode 100644 index 000000000..68e744924 --- /dev/null +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -0,0 +1,1609 @@ +#include "PresetComboBoxes.hpp" + +#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 "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/FixModelByWin10.hpp" +#include "../Utils/UndoRedo.hpp" +#include "../Utils/ColorSpaceConvert.hpp" +#include "BitmapCache.hpp" +#include "SavePresetDialog.hpp" +#include "MsgDialog.hpp" +#include "ParamsDialog.hpp" + +// 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; + } + case Preset::TYPE_CONFIG: { + m_collection = &m_preset_bundle->configs; + m_main_bitmap_name = "config"; + 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_MOUSEWHEEL, [this](wxMouseEvent& e) { + if (m_suppress_change) + e.StopPropagation(); + else + e.Skip(); + }); + Bind(wxEVT_COMBOBOX_DROPDOWN, [this](wxCommandEvent&) { m_suppress_change = false; }); + Bind(wxEVT_COMBOBOX_CLOSEUP, [this](wxCommandEvent&) { m_suppress_change = true; }); + + 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(); + + 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__) + GList* cells = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(m_widget)); + + // 'cells' contains the GtkCellRendererPixBuf for the icon, + // 'cells->next' contains GtkCellRendererText for the text we need to ellipsize + if (!cells || !cells->next) return; + + auto cell = static_cast(cells->next->data); + + if (!cell) return; + + g_object_set(G_OBJECT(cell), "ellipsize", PANGO_ELLIPSIZE_END, (char*)NULL); + + // Only the list of cells must be freed, the renderer isn't ours to free + g_list_free(cells); +#endif +} + +int PresetComboBox::update_ams_color() +{ + if (m_filament_idx < 0) return -1; + int idx = selected_ams_filament(); + std::string color; + if (idx < 0) { + auto *preset = m_collection->find_preset(Preset::remove_suffix_modified(GetLabel().ToUTF8().data())); + 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); + } + DynamicPrintConfig *cfg = &wxGetApp().preset_bundle->project_config; + auto colors = static_cast(cfg->option("filament_colour")->clone()); + colors->values[m_filament_idx] = color; + DynamicPrintConfig new_cfg; + new_cfg.set_key_value("filament_colour", colors); + 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); + // 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_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(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]; + 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(separator(L("System presets")), wxNullBitmap)); + } + if (!nonsys_presets.empty()) + { + set_label_marker(Append(separator(L("User presets")), wxNullBitmap)); + 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(separator(L("Incompatible presets")), wxNullBitmap)); + 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_ams_filaments(std::string selected, bool alias_name) +{ + bool is_bbl_vendor_preset = m_preset_bundle->printers.get_edited_preset().is_bbl_vendor_preset(m_preset_bundle); + if (is_bbl_vendor_preset && !m_preset_bundle->filament_ams_list.empty()) { + set_label_marker(Append(separator(L("AMS filaments")), wxNullBitmap)); + m_first_ams_filament = GetCount(); + auto &filaments = m_collection->get_presets(); + for (auto &entry : m_preset_bundle->filament_ams_list) { + auto & tray = entry.second; + std::string filament_id = tray.opt_string("filament_id", 0u); + if (filament_id.empty()) 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 name = tray.opt_string("tray_name", 0u); + wxBitmap bmp(*get_extruder_color_icon(color, name, 24, 16)); + int item_id = Append(get_preset_name(*iter), bmp.ConvertToImage(), &m_first_ams_filament + entry.first); + //validate_selection(id->value == selected); // can not select + } + m_last_ams_filament = GetCount(); + } +} + +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::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->SetToolTip(_L("Click to pick filament color")); + clr_picker->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + m_clrData.SetColour(clr_picker->GetBackgroundColour()); + m_clrData.SetChooseFull(true); + m_clrData.SetChooseAlpha(false); + + std::vector colors = wxGetApp().app_config->get_custom_color_from_config(); + for (int i = 0; i < colors.size(); i++) { + m_clrData.SetCustomColour(i, string_to_wxColor(colors[i])); + } + wxColourDialog dialog(this, &m_clrData); + dialog.SetTitle(_L("Please choose the filament colour")); + if ( dialog.ShowModal() == wxID_OK ) + { + m_clrData = dialog.GetColourData(); + if (colors.size() != CUSTOM_COLOR_COUNT) { + colors.resize(CUSTOM_COLOR_COUNT); + } + for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) { + colors[i] = color_to_string(m_clrData.GetCustomColour(i)); + } + wxGetApp().app_config->save_custom_color_to_config(colors); + // 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 + + colors->values[m_filament_idx] = m_clrData.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); + cfg->apply(cfg_new); + wxGetApp().plater()->update_project_dirty_from_presets(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + update(); + wxGetApp().plater()->on_config_change(cfg_new); + + wxCommandEvent *evt = new wxCommandEvent(EVT_FILAMENT_COLOR_CHANGED); + evt->SetInt(m_filament_idx); + wxQueueEvent(wxGetApp().plater(), evt); + } + }); + } + //xiamian+ + else if (m_type == Preset::TYPE_CONFIG) { + edit_btn = new ScalableButton(parent, wxID_ANY, "config"); + edit_btn->SetToolTip(_L("Click to edit preset")); + + edit_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent) + { + show_edit_menu(); + }); + }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(); + std::cout<<"123"<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_MARKER && 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) + 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 || m_last_selected != selected_item || m_collection->current_is_dirty()) { + m_last_selected = selected_item; + if (m_type == Preset::TYPE_FILAMENT) + update_ams_color(); + } + + evt.Skip(); +} + +bool PlaterPresetComboBox::switch_to_tab() +{ + Tab* tab = wxGetApp().get_tab(m_type); + if (!tab) + return false; + + //BBS Select NoteBook Tab params + if (tab->GetParent() == wxGetApp().params_panel()) + wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + else + wxGetApp().params_dialog()->Popup(); + tab->restore_last_select_item(); + + 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 { + wxGetApp().params_dialog()->Hide(); + 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); + } + } + } + */ + + 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; + std::string filament_color; + if (m_type == Preset::TYPE_FILAMENT) + { + //unsigned char rgb[3]; + filament_color = m_preset_bundle->project_config.opt_string("filament_colour", (unsigned int) m_filament_idx); + wxColor clr(filament_color); + clr_picker->SetBackgroundColour(clr); + clr_picker->SetBitmap(*get_extruder_color_icons(true)[m_filament_idx]); +#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 + 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; + std::map system_presets; + std::map preset_descriptions; + + //BBS: move system to the end + wxString selected_system_preset; + wxString selected_user_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 (!preset.is_visible || (!preset.is_compatible && !is_selected)) + continue; + + bool single_bar = false; + 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 + } + + wxBitmap* bmp = get_bmp(preset); + assert(bmp); + + const wxString name = get_preset_name(preset); + preset_descriptions.emplace(name, _L(preset.description)); + + if (preset.is_default || preset.is_system) { + //BBS: move system to the end + 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()); + } + } + 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_FILAMENT) + add_ams_filaments(into_u8(selected_user_preset.empty() ? selected_system_preset : selected_user_preset), true); + + //BBS: add project embedded preset logic + if (!project_embedded_presets.empty()) + { + set_label_marker(Append(separator(L("Project-inside presets")), wxNullBitmap)); + for (std::map::iterator it = project_embedded_presets.begin(); it != project_embedded_presets.end(); ++it) { + SetItemTooltip(Append(it->first, *it->second), preset_descriptions[it->first]); + validate_selection(it->first == selected_user_preset); + } + } + if (!nonsys_presets.empty()) + { + set_label_marker(Append(separator(L("User presets")), wxNullBitmap)); + for (std::map::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + SetItemTooltip(Append(it->first, *it->second), preset_descriptions[it->first]); + validate_selection(it->first == selected_user_preset); + } + } + //BBS: move system to the end + if (!system_presets.empty()) + { + set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + for (std::map::iterator it = system_presets.begin(); it != system_presets.end(); ++it) { + SetItemTooltip(Append(it->first, *it->second), preset_descriptions[it->first]); + validate_selection(it->first == selected_system_preset); + } + } + + //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)); + } + } + } + }*/ + //xiamian+ + if (m_type == Preset::TYPE_PRINTER || m_type == Preset::TYPE_FILAMENT || m_type == Preset::TYPE_SLA_MATERIAL || m_type == Preset::TYPE_CONFIG) { + 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 if (m_type == Preset::TYPE_CONFIG) + set_label_marker(Append(separator(L("Add/Remove filaments")), *bmp), LABEL_ITEM_WIZARD_FILAMENTS); + 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(); + 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(); +} + + +// --------------------------------- +// *** 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) +{ + 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; + std::map preset_descriptions; + + 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 = get_preset_name(preset); + preset_descriptions.emplace(name, _L(preset.description)); + + 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; + } + 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) + add_ams_filaments(into_u8(selected)); + + //BBS: add project embedded preset logic + if (!project_embedded_presets.empty()) + { + set_label_marker(Append(separator(L("Project-inside presets")), wxNullBitmap)); + 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(separator(L("User presets")), wxNullBitmap)); + for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_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); + } + } + //BBS: move system to the end + if (!system_presets.empty()) + { + set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + for (std::map>::iterator it = system_presets.begin(); it != system_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 (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(*wxWHITE); + clr_picker->SetBitmap(*get_extruder_color_icon("#FFFFFFFF", "", FromDIP(20), FromDIP(20))); + 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); + 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) { + 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(separator(L("User presets")), wxNullBitmap)); + 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(separator(L("System presets")), wxNullBitmap)); + 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); +} + +} // namespace Slic3r diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index cd6e92bc2..d6c8766cf 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -148,11 +148,12 @@ Tab::Tab(ParamsPanel* parent, const wxString& title, Preset::Type type) : void Tab::set_type() { if (m_name == PRESET_PRINT_NAME) { m_type = Slic3r::Preset::TYPE_PRINT; } - else if (m_name == "sla_print") { m_type = Slic3r::Preset::TYPE_SLA_PRINT; } + else if (m_name == "sla_print") { m_type = Slic3r::Preset::TYPE_SLA_PRINT; } else if (m_name == PRESET_FILAMENT_NAME) { m_type = Slic3r::Preset::TYPE_FILAMENT; } - else if (m_name == "sla_material") { m_type = Slic3r::Preset::TYPE_SLA_MATERIAL; } + else if (m_name == "sla_material") { m_type = Slic3r::Preset::TYPE_SLA_MATERIAL; } else if (m_name == PRESET_PRINTER_NAME) { m_type = Slic3r::Preset::TYPE_PRINTER; } - else { m_type = Slic3r::Preset::TYPE_INVALID; assert(false); } + else if (m_name == PRESET_CONFIG_NAME) { m_type = Slic3r::Preset::TYPE_CONFIG; } + else { m_type = Slic3r::Preset::TYPE_INVALID; assert(false); } } // sub new @@ -1929,12 +1930,12 @@ void TabPrint::build() //auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height"); auto optgroup = page->new_optgroup("", L"param_layer_height"); //optgroup->append_single_option_line("layer_height", "layer-height"); - optgroup->append_single_option_line("initial_layer_print_height", "layer-height"); + //optgroup->append_single_option_line("initial_layer_print_height", "layer-height"); //xiamian- //optgroup = page->new_optgroup(L("Line width"), L"param_line_width"); //optgroup->append_single_option_line("line_width","parameter/line-width"); - optgroup->append_single_option_line("initial_layer_line_width","parameter/line-width"); - optgroup->append_single_option_line("outer_wall_line_width","parameter/line-width"); + //optgroup->append_single_option_line("initial_layer_line_width","parameter/line-width"); + //optgroup->append_single_option_line("outer_wall_line_width","parameter/line-width"); optgroup->append_single_option_line("inner_wall_line_width","parameter/line-width"); optgroup->append_single_option_line("top_surface_line_width","parameter/line-width"); optgroup->append_single_option_line("sparse_infill_line_width","parameter/line-width"); @@ -2041,7 +2042,7 @@ void TabPrint::build() optgroup = page->new_optgroup("", L"param_speed",15); //xiamian- //optgroup = page->new_optgroup(L("Initial layer speed"), L"param_speed_first", 15); - //optgroup->append_single_option_line("default_print_speed");todo + optgroup->append_single_option_line("default_print_speed"); optgroup->append_single_option_line("initial_layer_speed"); optgroup->append_single_option_line("initial_layer_infill_speed"); //optgroup = page->new_optgroup(L("Other layers speed"), L"param_speed", 15); @@ -4280,6 +4281,49 @@ void TabPrinter::update_fff() void TabPrinter::update_sla() { ; } +void TabConfig::build() { + m_presets = &m_preset_bundle->configs; + load_initial_data(); + + auto page = add_options_page(L("Quality"), "config"); + auto optgroup = page->new_optgroup("", L"param_layer_height"); + //optgroup->append_single_option_line("initial_layer_print_height", "layer-height"); + optgroup->append_single_option_line("initial_layer_line_width"); + optgroup->append_single_option_line("outer_wall_line_width", "parameter/line-width"); + optgroup->append_single_option_line("inner_wall_line_width", "parameter/line-width"); + optgroup->append_single_option_line("top_surface_line_width", "parameter/line-width"); + optgroup->append_single_option_line("sparse_infill_line_width", "parameter/line-width"); + optgroup->append_single_option_line("support_line_width", "parameter/line-width"); + optgroup->append_single_option_line("resolution", "acr-move"); + optgroup->append_single_option_line("enable_arc_fitting", "acr-move"); + + optgroup->append_single_option_line("wall_sequence"); +} +void TabConfig::reload_config() +{ + this->compatible_widget_reload(m_compatible_printers); + this->compatible_widget_reload(m_compatible_prints); + Tab::reload_config(); +} +void TabConfig::update_description_lines() +{ + Tab::update_description_lines(); + + //if (!m_active_page) + // return; + + //if (m_active_page->title() == "Cooling" && m_cooling_description_line) + // m_cooling_description_line->SetText(from_u8(PresetHints::cooling_description(m_presets->get_edited_preset()))); + //BBS + //if (m_active_page->title() == "Filament" && m_volumetric_speed_description_line) + // this->update_volumetric_flow_preset_hints(); +} +void TabConfig::toggle_options() { +} +void TabConfig::update() { +} +void TabConfig::clear_pages() { +} void Tab::update_ui_items_related_on_parent_preset(const Preset* selected_preset_parent) { m_is_default_preset = selected_preset_parent != nullptr && selected_preset_parent->is_default; @@ -4359,7 +4403,8 @@ void Tab::load_current_preset() #ifdef _MSW_DARK_MODE if (!wxGetApp().tabs_as_menu()) { std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" : - tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : "cog"; + tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : + tab->type() == Slic3r::Preset::TYPE_CONFIG ? "config":"cog"; tab->Hide(); // #ys_WORKAROUND : Hide tab before inserting to avoid unwanted rendering of the tab dynamic_cast(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(), bmp_name); } @@ -5218,6 +5263,9 @@ void Tab::save_preset(std::string name /*= ""*/, bool detach, bool save_to_proje else dependent = { Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL }; break; + case Preset::TYPE_CONFIG: + dependent = { Preset::TYPE_CONFIG }; + break; default: break; } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 6e1bb8a42..f92a2c275 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -565,6 +565,33 @@ public: bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } }; +class TabConfig : public Tab +{ +private: + //ogStaticText* m_volumetric_speed_description_line{ nullptr }; + //ogStaticText* m_cooling_description_line{ nullptr }; + + //void add_config_overrides_page(); + //void update_config_overrides_page(); + //void update_volumetric_flow_preset_hints(); + + std::map m_overrides_options; + +public: + //BBS: GUI refactor + TabConfig(ParamsPanel* parent) : + Tab(parent, _(L("Configuration")), Slic3r::Preset::TYPE_CONFIG) {} + ~TabConfig() {} + + void build() override; + void reload_config() override; + void update_description_lines() override; + void toggle_options() override; + void update() override; + void clear_pages() override; + bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } +}; + class TabPrinter : public Tab { private: