diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index d71ad5aef1..c3ca1cbf96 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -18656,3 +18656,69 @@ msgstr "" #, possible-c-format, possible-boost-format msgid "The current nozzle mapping may produce an extra %0.2f g of waste." msgstr "" + +msgid "" +"Filament switcher detected. All AMS filaments are now available for both " +"extruders. The slicer will auto-assign for optimal printing. " +msgstr "" + +msgid "" +"A filament switcher is detected but not calibrated and thus currently " +"unavailable. Please calibrate it on the printer and synchronize before use. " +msgstr "" + +msgid "" +"The Filament Track Switch installed on the printer does not match the slicing " +"file. Please re-slice to avoid print quality issues." +msgstr "" + +msgid "This print requires a Filament Track Switch. Please install it first." +msgstr "" + +msgid "The Filament Track Switch has not been setup. Please setup it first." +msgstr "" + +msgid "\"Load\" or \"Unload\" is not supported for external spool while using Filament Track Switch." +msgstr "" + +msgid "The Filament Track Switch has not been setup. Please setup on printer." +msgstr "" + +msgid "Load %s to " +msgstr "" + +msgid "AMS filaments" +msgstr "" + +msgid "Fila Saving" +msgstr "" + +msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" +msgstr "" + +msgid "Filament Track Switch" +msgstr "" + +msgid "AMS has not been initialized. Please initialize it before use." +msgstr "" + +msgid "External spools is not supported since Filament Track Switch has been installed. If you want to use external spool, please uninstall it." +msgstr "" + +msgid "Switch" +msgstr "" + +msgid "hotend" +msgstr "" + +msgid "Wait for AMS cooling" +msgstr "" + +msgid "Switch current filament at Filament Track Switch" +msgstr "" + +msgid "Pull back current filament at Filament Track Switch" +msgstr "" + +msgid "Switch track at Filament Track Switch" +msgstr "" diff --git a/resources/images/fila_switch.svg b/resources/images/fila_switch.svg new file mode 100644 index 0000000000..f9a1ed93d4 --- /dev/null +++ b/resources/images/fila_switch.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/fila_switch_error.svg b/resources/images/fila_switch_error.svg new file mode 100644 index 0000000000..e2be18ce39 --- /dev/null +++ b/resources/images/fila_switch_error.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/filament_track_switch.png b/resources/images/filament_track_switch.png new file mode 100644 index 0000000000..fe64bc442e Binary files /dev/null and b/resources/images/filament_track_switch.png differ diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 311451f774..5d151ca43f 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1326,6 +1326,14 @@ int CLI::run(int argc, char **argv) } set_temporary_dir(temp_path); + // The Filament Track Switch flags are live-device state with no meaning in headless slicing; + // default both off unless explicitly provided on the command line, so an old 3MF that had + // them enabled still slices without the switch behavior. + if (!m_extra_config.has("has_filament_switcher")) + m_extra_config.set_key_value("has_filament_switcher", new ConfigOptionBool(false)); + if (!m_extra_config.has("enable_filament_dynamic_map")) + m_extra_config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(false)); + m_extra_config.apply(m_config, true); m_extra_config.normalize_fdm(); diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 8ff9dcea32..df5b3f5380 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -304,6 +304,10 @@ void AppConfig::set_defaults() if (get("show_3d_navigator").empty()) set_bool("show_3d_navigator", true); + // Show the one-time "Filament Track Switch is ready" tip until it has been seen once. + if (get("show_fila_switch_tips").empty()) + set_bool("show_fila_switch_tips", true); + if (get("show_plate_gridlines").empty()) set_bool("show_plate_gridlines", true); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 610ca41619..ea4ad5bc52 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -57,7 +57,12 @@ static std::vector s_project_options { "prime_volume_mode", "nozzle_volume_type", "filament_map_mode", - "filament_map" + "filament_map", + // Filament Track Switch device state: whether the switch is installed and ready, and + // whether dynamic per-nozzle filament mapping is active. Persisted with the project and + // restored from a saved 3mf; reset to false on load and set true only by live device sync. + "has_filament_switcher", + "enable_filament_dynamic_map" }; //Orca: add custom as default @@ -4323,6 +4328,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool }; clear_compatible_printers(config); + // Dynamic per-nozzle filament mapping reflects live device state, not a stored setting; + // drop it from any imported config so it only comes from the connected printer. + config.erase("enable_filament_dynamic_map"); + #if 0 size_t num_extruders = (printer_technology == ptFFF) ? std::min(config.option("nozzle_diameter" )->values.size(), diff --git a/src/slic3r/GUI/AmsMappingPopup.cpp b/src/slic3r/GUI/AmsMappingPopup.cpp index 241c2799a7..9a3b97b061 100644 --- a/src/slic3r/GUI/AmsMappingPopup.cpp +++ b/src/slic3r/GUI/AmsMappingPopup.cpp @@ -22,6 +22,7 @@ #include "Plater.hpp" #include "BitmapCache.hpp" #include "BindDialog.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" #include "DeviceCore/DevFilaSystem.h" #include "DeviceCore/DevNozzleRack.h" @@ -974,6 +975,8 @@ void AmsMapingPopup::on_left_down(wxMouseEvent &evt) auto left = item->GetSize(); if (pos.x > p_rect.x && pos.y > p_rect.y && pos.x < (p_rect.x + item->GetSize().x) && pos.y < (p_rect.y + item->GetSize().y)) { + // Orca: the external spool is un-pickable while a Filament Track Switch is installed (Apple hit-tests here). + if (m_fila_switch_installed && (item->m_ams_id == VIRTUAL_TRAY_MAIN_ID || item->m_ams_id == VIRTUAL_TRAY_DEPUTY_ID)) { return; } if (item->m_tray_data.type == TrayType::NORMAL) { if (!m_ext_mapping_filatype_check && (item->m_ams_id == VIRTUAL_TRAY_MAIN_ID || item->m_ams_id == VIRTUAL_TRAY_DEPUTY_ID)) { // Do nothing @@ -1169,6 +1172,11 @@ void AmsMapingPopup::update(MachineObject* obj, const std::vector& if (!obj) {return;} m_ams_remain_detect_flag = obj->GetFilaSystem()->IsDetectRemainEnabled(); + // Orca: with a Filament Track Switch installed, filament routes through the switch and the external + // spool cannot be used, so its mapping slots are rendered greyed-out and un-pickable. Inert (false) + // on any printer without a switch — GetFilaSwitch()->IsInstalled() defaults to false. + m_fila_switch_installed = obj->GetFilaSwitch()->IsInstalled(); + for (auto& ams_container : m_amsmapping_container_list) { ams_container->Destroy(); } @@ -1546,6 +1554,20 @@ void AmsMapingPopup::add_ext_ams_mapping(TrayData tray_data, MappingItem* item) #ifdef __APPLE__ m_mapping_item_list.push_back(item); #endif + + // Orca: a Filament Track Switch cannot route the external spool, so grey the ext slot out and make it + // un-pickable, with a tooltip explaining why (the click below is disabled; on_left_down also skips it). + if (m_fila_switch_installed) { + const wxString disp_name = (tray_data.type == THIRD) ? wxString("?") + : (tray_data.type == EMPTY) ? wxString("-") + : wxString(tray_data.name); + item->set_data(m_tag_material, wxColour(0xEE, 0xEE, 0xEE), disp_name, false, tray_data, true); + item->SetToolTip(_L("External spools is not supported since Filament Track Switch has been installed. If you want to use external spool, please uninstall it.")); + item->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { /* un-pickable while the switch is installed */ }); + item->set_tray_index(_L("Ext")); + return; + } + // set button if (tray_data.type == NORMAL) { if (is_match_material(tray_data.filament_type)) { diff --git a/src/slic3r/GUI/AmsMappingPopup.hpp b/src/slic3r/GUI/AmsMappingPopup.hpp index c264dadcbf..0850c0e728 100644 --- a/src/slic3r/GUI/AmsMappingPopup.hpp +++ b/src/slic3r/GUI/AmsMappingPopup.hpp @@ -244,6 +244,9 @@ public: bool m_has_unmatch_filament {false}; int m_current_filament_id; ShowType m_show_type{ShowType::RIGHT}; + // Orca: set from update() — true when a Filament Track Switch is installed, which makes the + // external-spool slots un-pickable (false on any printer without a switch, so behavior is unchanged). + bool m_fila_switch_installed{false}; std::string m_tag_material; wxBoxSizer *m_sizer_main{nullptr}; wxBoxSizer *m_sizer_ams{nullptr}; diff --git a/src/slic3r/GUI/DeviceCore/DevDefs.h b/src/slic3r/GUI/DeviceCore/DevDefs.h index 204064645c..76a1d6843a 100644 --- a/src/slic3r/GUI/DeviceCore/DevDefs.h +++ b/src/slic3r/GUI/DeviceCore/DevDefs.h @@ -41,6 +41,31 @@ enum AmsStatusMain AMS_STATUS_MAIN_UNKNOWN = 0xFF, }; +// Device-numbered filament-change step codes. Newer firmware reports the exact change-step +// sequence through the AMS (ams.cfs), keyed by these codes, instead of the client hardcoding +// steps per model. Distinct from the legacy GUI-side Slic3r::GUI::FilamentStep enum. +// STEP_CHECK_POSITION and STEP_CONFIRM_EXTRUDED share code 0x08 by device design. +enum DevFilamentStep +{ + STEP_IDLE = 0x00, + STEP_PAUSE = 0x01, + STEP_HEAT_NOZZLE = 0x02, + STEP_CUT_FILAMENT = 0x03, + STEP_PULL_CURR_FILAMENT = 0x04, + STEP_PUSH_NEW_FILAMENT = 0x05, + STEP_GRAB_NEW_FILAMENT = 0x06, + STEP_PURGE_OLD_FILAMENT = 0x07, + STEP_CHECK_POSITION = 0x08, + STEP_SWITCH_EXTRUDER = 0x09, + STEP_SWITCH_HOTEND = 0x0A, + STEP_AMS_FILA_COOLING = 0x0B, + STEP_PUSH_SWITCHER_FILA = 0x0C, + STEP_PULL_SWITCHER_FILA = 0x0D, + STEP_SWITCHER_SWITCH = 0x0E, + STEP_CONFIRM_EXTRUDED = 0x08, + STEP_COUNT, +}; + // Slots and Tray #define VIRTUAL_TRAY_MAIN_ID 255 #define VIRTUAL_TRAY_DEPUTY_ID 254 diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSwitch.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSwitch.cpp index 7768030271..1f7a9a7623 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSwitch.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSwitch.cpp @@ -9,6 +9,19 @@ namespace Slic3r { DevFilaSwitch::DevFilaSwitch(MachineObject *owner) { m_owner = owner; } +bool DevFilaSwitch::IsReady() const +{ + if (!m_is_installed) { return false; } + + const auto& ams_list = m_owner->GetFilaSystem()->GetAmsList(); + for (const auto& ams_item : ams_list) { + if (ams_item.second->GetBindedExtruderSet().empty()) { return false; } + if (!ams_item.second->GetSwitcherPos().has_value()) { return false; } + } + + return true; +} + void DevFilaSwitch::Reset() { m_is_installed = false; diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSwitch.h b/src/slic3r/GUI/DeviceCore/DevFilaSwitch.h index 52c3a271b3..e18dea338c 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSwitch.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSwitch.h @@ -49,6 +49,11 @@ public: public: bool IsInstalled() const { return m_is_installed; }; + // Ready once installed and every AMS has resolved both its extruder binding and its + // switcher track position from the device. The send dialog and sidebar sync UX only + // treat the switch as usable when this is true. + bool IsReady() const; + std::optional IsInA_HasFilament() const { return m_in_a_has_filament; }; std::optional IsInB_HasFilament() const { return m_in_b_has_filament; }; @@ -67,12 +72,6 @@ public: void Reset(); void ParseFilaSwitchInfo(const nlohmann::json& print_jj); - // NOTE: an IsReady() accessor is intentionally omitted. It would walk - // GetFilaSystem()->GetAmsList() calling GetBindedExtruderSet()/GetSwitcherPos(), - // AMS filament-switch binding accessors Orca's DevFilaSystem does not yet carry. - // It is unused by the filament blacklist; port it alongside the filament-switch - // device-sync UI that needs those accessors. - private: MachineObject* m_owner; diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 2202a7d689..fccc31c47b 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -346,6 +346,15 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS if (!key_field_only) { + // Newer firmware sends the exact filament-change step sequence in ams.cfs, so the + // client no longer hardcodes the steps per model. Absent on current firmware, which + // keeps the list empty and the legacy hardcoded/ams_status_sub step path in effect. + if (jj["ams"].contains("cfs")) { + system->m_filament_change_steps = DevJsonValParser::GetVal>(jj["ams"], "cfs"); + } else { + system->m_filament_change_steps.clear(); + } + if (jj["ams"].contains("tray_read_done_bits")) { obj->tray_read_done_bits = stol(jj["ams"]["tray_read_done_bits"].get(), nullptr, 16); @@ -403,17 +412,43 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS int type_id = 1; // 0:dummy 1:ams 2:ams-lite 3:n3f 4:n3s /*ams info*/ + std::set binded_extruder_set; + std::optional binded_switcher_pos; if (it->contains("info")) { const std::string& info = (*it)["info"].get(); type_id = DevUtil::get_flag_bits(info, 0, 4); extuder_id = DevUtil::get_flag_bits(info, 8, 4); + if (extuder_id == 0xE && obj->GetFilaSwitch()->IsInstalled()) { + int bind_switch_in = DevUtil::get_flag_bits(info, 24, 4); + if (bind_switch_in == 0 || bind_switch_in == 1) { + binded_extruder_set = { MAIN_EXTRUDER_ID, DEPUTY_EXTRUDER_ID }; + } + + if (bind_switch_in == 0) { + binded_switcher_pos = DevFilaSwitch::SwitchPos::POS_IN_B; + } else if (bind_switch_in == 1) { + binded_switcher_pos = DevFilaSwitch::SwitchPos::POS_IN_A; + } + + // Orca: the switch feeds every AMS to both extruders; pin a deterministic + // single-extruder id so legacy GetExtruderId() consumers keep a valid value + // while switch-aware code reads the binding set instead. + extuder_id = MAIN_EXTRUDER_ID; + } else if (extuder_id != 0xE) { + binded_extruder_set = { extuder_id }; + } } else { if (!obj->is_enable_ams_np && obj->get_printer_ams_type() == "f1") { type_id = DevAms::AMS_LITE; } + binded_extruder_set = { MAIN_EXTRUDER_ID }; } /*AMS without initialization*/ + // Orca: an AMS reporting extruder 0xE without a Filament Track Switch installed has + // no usable extruder binding; drop it, preserving the existing display for + // half-initialized printers. With the switch installed the 0xE case is remapped + // above to both extruders and falls through to normal handling. if (extuder_id == 0xE) { ams_id_set.erase(ams_id); @@ -445,6 +480,13 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS /*set ams type flag*/ curr_ams->SetAmsType(type_id); + // Refresh the switch-aware extruder binding on every push (both create and + // update paths) so it can't go stale after an AMS is re-homed to another + // extruder. Without the switch the set holds the single bound extruder and + // the track position stays empty. + curr_ams->m_binded_extruder_set = binded_extruder_set; + curr_ams->m_binded_switcher_pos = binded_switcher_pos; + /*set ams exist flag*/ try diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h index 8401498a37..604f228c13 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h @@ -4,9 +4,12 @@ #include "DevDefs.h" #include "DevFilaAmsSetting.h" +#include "DevFilaSwitch.h" #include "DevUtil.h" #include +#include +#include #include #include #include @@ -169,6 +172,20 @@ public: // installed on the extruder int GetExtruderId() const { return m_ext_id; } + // Extruders this AMS can currently feed. Without a Filament Track Switch installed this + // is the single {m_ext_id}; with the switch installed the device binds it to both extruders. + const std::set& GetBindedExtruderSet() const { return m_binded_extruder_set; } + + // The bound extruder when exactly one is bound, empty otherwise. Used to attribute an AMS + // to a single extruder on printers without the switch. + std::optional GetUniqueBindedExtruderId() const + { + return m_binded_extruder_set.size() == 1 ? std::optional(*m_binded_extruder_set.begin()) : std::nullopt; + } + + // Which switch input track (A/B) feeds this AMS, set only when a Filament Track Switch is installed. + std::optional GetSwitcherPos() const { return m_binded_switcher_pos; } + // temperature and humidity float GetCurrentTemperature() const { return m_current_temperature; } @@ -185,6 +202,12 @@ private: AmsType m_ams_type = AmsType::AMS; std::string m_ams_id; int m_ext_id;//extruder id + // Orca: keeps the legacy single m_ext_id for existing consumers and carries the + // switch-aware binding alongside it (BambuStudio stores only the set). Without a Filament + // Track Switch the set is {m_ext_id}; with one installed the device binds both extruders and + // records which input track (A/B) feeds this AMS. + std::set m_binded_extruder_set; + std::optional m_binded_switcher_pos; bool m_exist = false; // slots and trays @@ -268,6 +291,11 @@ public: std::weak_ptr GetAmsFirmwareSwitch() const { return m_ams_firmware_switch;} + // filament change steps + // The exact change-step sequence reported by the AMS (ams.cfs). Empty on current firmware, + // which leaves the legacy hardcoded/ams_status_sub step handling in effect. + const std::vector& GetFilamentChangeSteps() const { return m_filament_change_steps; } + public: // ctrls int CtrlAmsReset() const; @@ -281,6 +309,9 @@ private: /* ams properties */ int m_ams_cali_stat = 0; + // Change-step sequence reported by the AMS (ams.cfs); empty when firmware doesn't send it. + std::vector m_filament_change_steps; + std::map amsList;// key: ams[id], start with 0 DevAmsSystemSetting m_ams_system_setting{ this }; diff --git a/src/slic3r/GUI/DeviceCore/DevFirmware.h b/src/slic3r/GUI/DeviceCore/DevFirmware.h index 2339ae9e3a..eff34db0ea 100644 --- a/src/slic3r/GUI/DeviceCore/DevFirmware.h +++ b/src/slic3r/GUI/DeviceCore/DevFirmware.h @@ -45,6 +45,7 @@ public: bool isCuttingModule() const { return product_name.Contains("Cutting Module"); } bool isExtinguishSystem() const { return product_name.Contains("Extinguishing System"); } bool isWTM() const { return name.find("wtm") != std::string::npos; } // nozzle (rack hotend) module firmware + bool isFilaTrackSwitch() const { return product_name.Contains("Filament Track"); } }; diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index ddf6166d11..527fe4f372 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -869,6 +869,7 @@ void MachineObject::clear_version_info() laser_version_info = DevFirmwareVersionInfo(); cutting_module_version_info = DevFirmwareVersionInfo(); extinguish_version_info = DevFirmwareVersionInfo(); + filatrack_version_info = DevFirmwareVersionInfo(); module_vers.clear(); // Drop cached rack-hotend (WTM) firmware alongside the module list. // Inert for non-rack printers (rack firmware map is empty). @@ -890,6 +891,8 @@ void MachineObject::store_version_info(const DevFirmwareVersionInfo& info) // the rack upgrade UI can read per-nozzle versions. isWTM() is false for every non-rack // printer's modules, so this branch never fires outside H2C. m_nozzle_system->AddFirmwareInfoWTM(info); + } else if (info.isFilaTrackSwitch()) { + filatrack_version_info = info; } module_vers.emplace(info.name, info); @@ -1572,7 +1575,7 @@ int MachineObject::check_resume_condition() } return 0; } -int MachineObject::command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp, int new_temp) +int MachineObject::command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp, int new_temp, std::optional extruder_id) { json j; try { @@ -1604,6 +1607,13 @@ int MachineObject::command_ams_change_filament(bool load, std::string ams_id, st j["print"]["slot_id"] = atoi(slot_id.c_str()); } + // Filament Track Switch: route the load to the chosen extruder. Only present when the + // caller supplied a value (FTS-installed+ready path), so every other caller is unchanged. + if (extruder_id.has_value()) + { + j["print"]["extruder_id"] = *extruder_id; + } + } catch (const std::exception &) {} return this->publish_json(j); } @@ -5085,6 +5095,7 @@ void MachineObject::parse_new_info(json print) // fun2 may have infinite length, use get_flag_bits_no_border if (!fun2.empty()) { is_support_print_with_emmc = get_flag_bits_no_border(fun2, 0) == 1; + is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1; } /*aux*/ diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 9bb00dfcab..773bdc200b 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include "nlohmann/json.hpp" @@ -378,6 +379,7 @@ public: DevFirmwareVersionInfo laser_version_info; DevFirmwareVersionInfo cutting_module_version_info; DevFirmwareVersionInfo extinguish_version_info; + DevFirmwareVersionInfo filatrack_version_info; std::map module_vers; std::map new_ver_list; bool m_new_ver_list_exist = false; @@ -633,6 +635,7 @@ public: // fun2 bool is_support_print_with_emmc{false}; + bool is_support_check_track_switch_match_slice_printer{false}; bool installed_upgrade_kit{false}; int bed_temperature_limit = -1; @@ -746,7 +749,7 @@ public: int check_resume_condition(); // ams controls //int command_ams_switch(int tray_index, int old_temp = 210, int new_temp = 210); - int command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp = 210, int new_temp = 210); + int command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp = 210, int new_temp = 210, std::optional extruder_id = std::nullopt); int command_ams_user_settings(bool start_read_opt, bool tray_read_opt, bool remain_flag = false); int command_ams_switch_filament(bool switch_filament); int command_ams_air_print_detect(bool air_print_detect); diff --git a/src/slic3r/GUI/FilamentGroupPopup.cpp b/src/slic3r/GUI/FilamentGroupPopup.cpp index a29e4537fb..e5e49928dc 100644 --- a/src/slic3r/GUI/FilamentGroupPopup.cpp +++ b/src/slic3r/GUI/FilamentGroupPopup.cpp @@ -174,6 +174,9 @@ FilamentGroupPopup::FilamentGroupPopup(wxWindow *parent) : PopupWindow(parent, w button_labels[idx]->Bind(wxEVT_LEAVE_WINDOW, [this](auto &) { UpdateButtonStatus(); }); } + // Smart filament assign section + MakeSmartFilamentSection(top_sizer, horizontal_margin, vertical_padding); + { wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL); // ORCA Unified hyperlinks @@ -257,6 +260,7 @@ void FilamentGroupPopup::Init() SetFilamentMapMode(m_mode); } + UpdateSmartFilamentSection(); UpdateButtonStatus(); GUI::wxGetApp().UpdateDarkUIWin(this); } @@ -408,4 +412,65 @@ void FilamentGroupPopup::UpdateButtonStatus(int hover_idx) Fit(); } +void FilamentGroupPopup::MakeSmartFilamentSection(wxSizer *top_sizer, int horizontal_margin, int vertical_padding) +{ + m_smart_filament_panel = new StaticBox(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0); + m_smart_filament_panel->SetCornerRadius(FromDIP(4)); + m_smart_filament_panel->SetBorderWidth(FromDIP(1)); + m_smart_filament_panel->SetBorderColor(wxColour("#CECECE")); + m_smart_filament_panel->SetBackgroundColor(StateColor(std::pair(wxColour("#F8F8F8"), StateColor::Normal))); + + auto *label = new Label(m_smart_filament_panel, _L("Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings")); + label->SetFont(Label::Body_12); + label->SetForegroundColour(GreyColor); + label->SetBackgroundColour(wxColour("#F8F8F8")); + label->Wrap(FromDIP(240)); + + m_smart_filament_switch = new SwitchButton(m_smart_filament_panel); + m_smart_filament_switch->Bind(wxEVT_TOGGLEBUTTON, &FilamentGroupPopup::OnSmartFilamentToggle, this); +#ifdef __WXOSX__ + // wxEVT_TOGGLEBUTTON event not handled well by PopupWindow on MacOS + // we bind a wxEVT_LEFT_DOWN event as a workaround + m_smart_filament_switch->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) { + wxCommandEvent evt(wxEVT_TOGGLEBUTTON); + evt.SetInt(!m_smart_filament_switch->GetValue()); + m_smart_filament_switch->Command(evt); + }); +#endif + + auto *panel_sizer = new wxBoxSizer(wxHORIZONTAL); + panel_sizer->Add(label, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(10)); + panel_sizer->Add(m_smart_filament_switch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(10)); + m_smart_filament_panel->SetSizer(panel_sizer); + + top_sizer->Add(m_smart_filament_panel, 0, wxEXPAND | wxLEFT | wxRIGHT, horizontal_margin); + m_smart_filament_spacer = top_sizer->AddSpacer(vertical_padding); + + // Hidden by default; shown in Init() when the filament track switch is ready + m_smart_filament_panel->Show(false); + m_smart_filament_spacer->Show(false); +} + +void FilamentGroupPopup::OnSmartFilamentToggle(wxCommandEvent &event) +{ + auto &config = wxGetApp().preset_bundle->project_config; + auto *dynamic_filament = dynamic_cast(config.option("enable_filament_dynamic_map")); + if (dynamic_filament) { dynamic_filament->value = m_smart_filament_switch->GetValue(); } + plater_ref->update(); + event.Skip(); +} + +void FilamentGroupPopup::UpdateSmartFilamentSection() +{ + bool show = wxGetApp().sidebar().is_fila_switch_ready(); + m_smart_filament_panel->Show(show); + m_smart_filament_spacer->Show(show); + + if (show) { + auto &config = wxGetApp().preset_bundle->project_config; + auto *dynamic_filament = dynamic_cast(config.option("enable_filament_dynamic_map")); + if (dynamic_filament) { m_smart_filament_switch->SetValue(dynamic_filament->value); } + } +} + }} // namespace Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/FilamentGroupPopup.hpp b/src/slic3r/GUI/FilamentGroupPopup.hpp index 2355b13e9e..6d0bd43da7 100644 --- a/src/slic3r/GUI/FilamentGroupPopup.hpp +++ b/src/slic3r/GUI/FilamentGroupPopup.hpp @@ -3,10 +3,13 @@ #include #include +#include #include #include "libslic3r/PrintConfig.hpp" #include "Widgets/PopupWindow.hpp" #include "Widgets/Label.hpp" +#include "Widgets/SwitchButton.hpp" +#include "Widgets/StaticBox.hpp" namespace Slic3r { namespace GUI { @@ -42,6 +45,10 @@ private: void Init(); void UpdateButtonStatus(int hover_idx = -1); void DrawRoundedCorner(int radius); + + void MakeSmartFilamentSection(wxSizer *top_sizer, int horizontal_margin, int vertical_padding); + void UpdateSmartFilamentSection(); + void OnSmartFilamentToggle(wxCommandEvent &event); private: FilamentMapMode GetFilamentMapMode() const; void SetFilamentMapMode(const FilamentMapMode mode); @@ -75,6 +82,10 @@ private: wxStaticText *wiki_link; wxStaticText *video_link; + StaticBox *m_smart_filament_panel{nullptr}; + wxSizerItem *m_smart_filament_spacer{nullptr}; + SwitchButton *m_smart_filament_switch{nullptr}; + PartPlate* partplate_ref{ nullptr }; Plater* plater_ref{ nullptr }; }; diff --git a/src/slic3r/GUI/FilamentMapDialog.cpp b/src/slic3r/GUI/FilamentMapDialog.cpp index 72428dae02..19b48ce5f1 100644 --- a/src/slic3r/GUI/FilamentMapDialog.cpp +++ b/src/slic3r/GUI/FilamentMapDialog.cpp @@ -39,6 +39,68 @@ static std::vector get_applied_map(DynamicConfig& proj_config, const Plater extern std::string& get_left_extruder_unprintable_text(); extern std::string& get_right_extruder_unprintable_text(); +// Orca: minimal smart-filament toggle. When a filament track switch is ready every AMS filament is +// reachable from both nozzles, so one filament can be assigned to multiple nozzles to maximize +// savings. The checkbox drives the enable_filament_dynamic_map project flag. +class SmartFilamentPanel : public wxPanel +{ + static constexpr int spacing = 20; + +public: + SmartFilamentPanel(wxWindow *parent) : wxPanel(parent) + { + SetBackgroundColour(*wxWHITE); + wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL); + + main_sizer->AddSpacer(FromDIP(spacing)); + + auto *separator = new wxPanel(this); + separator->SetBackgroundColour(wxColour("#EEEEEE")); + main_sizer->Add(separator, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(15)); + + main_sizer->AddSpacer(FromDIP(spacing)); + + m_smart_filament_checkbox = new CheckBox(this); + if (auto *opt = enable_filament_dynamic_map()) + m_smart_filament_checkbox->SetValue(opt->value); + m_smart_filament_checkbox->Bind(wxEVT_TOGGLEBUTTON, &SmartFilamentPanel::on_smart_filament_checkbox, this); + + auto *label = new Label(this, _L("Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings")); + label->SetFont(Label::Body_12); + + // Orca: dropped the vendor "Learn more" tracking link (no Orca help page for this feature). + + auto *smart_sizer = new wxBoxSizer(wxHORIZONTAL); + smart_sizer->Add(m_smart_filament_checkbox, 0, wxALIGN_CENTER_VERTICAL); + smart_sizer->Add(label, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(3)); + + main_sizer->Add(smart_sizer, 0, wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(15)); + + SetSizer(main_sizer); + Layout(); + Fit(); + wxGetApp().UpdateDarkUIWin(this); + } + +private: + static ConfigOptionBool *enable_filament_dynamic_map() + { + auto &config = wxGetApp().preset_bundle->project_config; + return dynamic_cast(config.option("enable_filament_dynamic_map")); + } + + void on_smart_filament_checkbox(wxCommandEvent &event) + { + if (auto *opt = enable_filament_dynamic_map()) + opt->value = m_smart_filament_checkbox->GetValue(); + wxGetApp().plater()->update(); + event.Skip(); + } + +private: + CheckBox *m_smart_filament_checkbox{nullptr}; +}; + bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* partplate_ref, bool force_pop_up) { @@ -136,6 +198,14 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent, SetMinSize(wxSize(FromDIP(580), -1)); SetMaxSize(wxSize(FromDIP(580), -1)); + // Orca: when a filament track switch is ready, every AMS filament reaches both nozzles, so the + // Match/Convenience sub-mode is dropped and Auto is presented purely as a filament-saving mode. + // Orca has no fmmAutoForQuality, so "only saving" collapses to "switch ready" and the remaining + // auto mode set is { fmmAutoForFlush }. + m_fila_switch_ready = wxGetApp().sidebar().is_fila_switch_ready(); + const bool only_saving_mode = m_fila_switch_ready; + const bool auto_match_available = machine_synced && !m_fila_switch_ready; + if (mode < fmmManual) m_page_type = PageType::ptAuto; else if (mode == fmmManual || mode == fmmNozzleManual) @@ -149,7 +219,7 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent, wxBoxSizer *mode_sizer = new wxBoxSizer(wxHORIZONTAL); - m_auto_btn = new CapsuleButton(this, PageType::ptAuto, _L("Auto"), false); + m_auto_btn = new CapsuleButton(this, PageType::ptAuto, only_saving_mode ? _L("Fila Saving") : _L("Auto"), false); m_manual_btn = new CapsuleButton(this, PageType::ptManual, _L("Custom"), false); if (show_default) m_default_btn = new CapsuleButton(this, PageType::ptDefault, _L("Same as Global"), true); @@ -168,12 +238,14 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent, auto panel_sizer = new wxBoxSizer(wxHORIZONTAL); + // Orca: fall back to the saving mode whenever Match is unavailable, which now also covers the + // filament-track-switch-ready case (auto_match_available folds in !m_fila_switch_ready). FilamentMapMode default_auto_mode = mode >= fmmManual ? fmmAutoForFlush : - mode == fmmAutoForMatch && !machine_synced ? fmmAutoForFlush : + mode == fmmAutoForMatch && !auto_match_available ? fmmAutoForFlush : mode; m_manual_map_panel = new FilamentMapManualPanel(this, m_filament_color, m_filament_type, filaments, filament_map); - m_auto_map_panel = new FilamentMapAutoPanel(this, default_auto_mode, machine_synced); + m_auto_map_panel = new FilamentMapAutoPanel(this, default_auto_mode, auto_match_available); if (show_default) m_default_map_panel = new FilamentMapDefaultPanel(this); else @@ -184,6 +256,13 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent, if (show_default) panel_sizer->Add(m_default_map_panel, 0, wxEXPAND); main_sizer->Add(panel_sizer, 0, wxEXPAND); + // Smart filament section, shown only in filament-saving (flush) mode when the switch is ready. + if (m_fila_switch_ready) { + m_smart_filament = new SmartFilamentPanel(this); + m_smart_filament->Show(get_mode() == fmmAutoForFlush); + main_sizer->Add(m_smart_filament, 0, wxEXPAND); + } + wxPanel* bottom_panel = new wxPanel(this); bottom_panel->SetBackgroundColour(*wxWHITE); wxBoxSizer *bottom_sizer = new wxBoxSizer(wxHORIZONTAL); @@ -319,6 +398,9 @@ void FilamentMapDialog::update_panel_status(PageType page) m_auto_map_panel->Show(); } + if (m_smart_filament) + m_smart_filament->Show(get_mode() == fmmAutoForFlush); + Layout(); Fit(); } diff --git a/src/slic3r/GUI/FilamentMapDialog.hpp b/src/slic3r/GUI/FilamentMapDialog.hpp index 3272f203b3..c0692ec081 100644 --- a/src/slic3r/GUI/FilamentMapDialog.hpp +++ b/src/slic3r/GUI/FilamentMapDialog.hpp @@ -15,6 +15,7 @@ namespace GUI { class DragDropPanel; class Plater; class PartPlate; +class SmartFilamentPanel; /** * @brief Try to pop up the filament map dialog before slicing. @@ -78,6 +79,9 @@ private: Button* m_ok_btn; Button* m_cancel_btn; CheckBox* m_checkbox; + SmartFilamentPanel* m_smart_filament{nullptr}; + + bool m_fila_switch_ready{false}; PageType m_page_type; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index e0545a83e9..c5524ab7f0 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -564,6 +564,9 @@ struct Sidebar::priv ScalableButton* m_printer_setting = nullptr; wxStaticText * m_text_printer_settings = nullptr; wxPanel* m_panel_printer_content = nullptr; + // Filament Track Switch status overlay: an icon floated over the left/single extruder AMS area, + // shown only when the switch is installed (green when ready, red when not calibrated). + wxStaticBitmap* extruder_separator_icon = nullptr; ObjectList *m_object_list{ nullptr }; ObjectSettings *object_settings{ nullptr }; @@ -594,6 +597,19 @@ struct Sidebar::priv bool switch_diameter(bool single); void update_sync_status(const MachineObject* obj); + // Filament Track Switch (H2-family accessory): true only when the connected printer is the + // selected one, online, and reports the switch installed and calibrated. + bool is_fila_switch_ready(); + // One-time info tip when the switch is ready, and a not-calibrated warning otherwise. + void show_filament_switcher_dialog(bool is_ready, bool is_manual); + void show_fila_switch_msg(bool ready); + bool fila_switch_warning_shown = false; + // Show/hide and reposition the switcher status icon; caches the last state to avoid churn. + void update_extruder_separator_icon(bool show, bool ready); + // Orca: BBS resets the switcher UI from DeviceManager::OnSelectedMachineChanged, which Orca lacks. + // Track the last-synced device id here so update_sync_status can detect a machine change. + std::string last_sync_dev_id; + #ifdef _WIN32 wxString btn_reslice_tip; void show_rich_tip(const wxString& tooltip, wxButton* btn); @@ -642,6 +658,18 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) extruder_dual_sizer->AddSpacer(FromDIP(4)); extruder_dual_sizer->Add(right_extruder->sizer, 1, wxEXPAND, 0); + // Filament Track Switch status icon, floated over the extruder AMS area (positioned in + // update_extruder_separator_icon). Created hidden; a click re-shows the ready/not-ready tip. + if (!extruder_separator_icon) { + auto bitmap = ScalableBitmap(m_panel_printer_content, "fila_switch", 10); + extruder_separator_icon = new wxStaticBitmap(m_panel_printer_content, wxID_ANY, bitmap.bmp(), wxDefaultPosition, bitmap.GetBmpSize()); + extruder_separator_icon->Hide(); + extruder_separator_icon->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& evt) { + show_fila_switch_msg(is_fila_switch_ready()); + evt.Skip(); + }); + } + // single extruder_single_sizer = single_extruder->sizer; wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL); @@ -1610,6 +1638,105 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj return nozzle_option; } +bool Sidebar::priv::is_fila_switch_ready() +{ + if (!wxGetApp().plater()->is_same_printer_for_connected_and_selected(false)) + return false; + auto device_manager = wxGetApp().getDeviceManager(); + if (device_manager == nullptr) + return false; + auto obj = device_manager->get_selected_machine(); + if (obj == nullptr || !obj->is_online()) + return false; + auto fila_switch = obj->GetFilaSwitch(); + if (fila_switch == nullptr) + return false; + return fila_switch->IsInstalled() && fila_switch->IsReady(); +} + +void Sidebar::priv::show_fila_switch_msg(bool ready) +{ + wxString msg = ready ? _L("Filament switcher detected. All AMS filaments are now available for both extruders. " + "The slicer will auto-assign for optimal printing. ") : + _L("A filament switcher is detected but not calibrated and thus currently unavailable. " + "Please calibrate it on the printer and synchronize before use. "); + + long style = ready ? (wxICON_INFORMATION | wxOK) : (wxICON_WARNING | wxOK); + // Orca: drop the vendor "Learn more" tracking link; there is no Orca help page for the switch yet. + MessageDialog dlg(static_cast(wxGetApp().mainframe), msg, _L("Tips"), style); + dlg.CenterOnParent(); + dlg.ShowModal(); +} + +void Sidebar::priv::show_filament_switcher_dialog(bool is_ready, bool is_manual) +{ + if (is_ready) { + // Show the "switch is ready" tip only once, ever. + if (wxGetApp().app_config->get("show_fila_switch_tips") == "true") { + wxGetApp().app_config->set("show_fila_switch_tips", "false"); + show_fila_switch_msg(true); + } + } else { + // A manual sync always warns; an automatic update warns once per session. + if (is_manual || !fila_switch_warning_shown) { + fila_switch_warning_shown = true; + show_fila_switch_msg(false); + } + } +} + +void Sidebar::priv::update_extruder_separator_icon(bool show, bool ready) +{ + if (!extruder_separator_icon) + return; + static bool last_show = false; + static bool last_ready = false; + static bool first_call = true; + + if (!first_call && last_show == show && last_ready == ready) + return; + first_call = false; + last_show = show; + last_ready = ready; + + // Never overlay the icon when the connected printer is not the selected preset. + if (!wxGetApp().plater()->is_same_printer_for_connected_and_selected(false)) { + extruder_separator_icon->Hide(); + if (m_panel_printer_content) + m_panel_printer_content->Refresh(); + return; + } + + if (show && left_extruder && left_extruder->hsizer_ams && left_extruder->sizer) { + // Orca: center the icon over the seam between the two extruder cards, vertically aligned with + // the AMS row. left_extruder and the icon share m_panel_printer_content as parent, so + // left_extruder->GetPosition() and the icon position live in the same coordinate space. + wxPoint left_box_pos = left_extruder->GetPosition(); + wxPoint ams_local_pos = left_extruder->hsizer_ams->GetPosition(); + wxSize left_size = left_extruder->sizer->GetSize(); + wxSize ams_size = left_extruder->hsizer_ams->GetSize(); + wxSize icon_size = extruder_separator_icon->GetSize(); + int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); + int center_x = left_size.GetWidth() + FromDIP(6); + int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2; + center_x -= icon_size.GetWidth() / 2; + center_y -= icon_size.GetHeight() / 2; + extruder_separator_icon->SetPosition(wxPoint(center_x, center_y)); + + auto normal_bitmap = ScalableBitmap(m_panel_printer_content, "fila_switch", 10); + auto error_bitmap = ScalableBitmap(m_panel_printer_content, "fila_switch_error", 10); + extruder_separator_icon->SetBitmap(ready ? normal_bitmap.bmp() : error_bitmap.bmp()); + + extruder_separator_icon->Show(); + extruder_separator_icon->Raise(); + } else { + extruder_separator_icon->Hide(); + } + + if (m_panel_printer_content) + m_panel_printer_content->Refresh(); +} + bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_manual) { MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); @@ -1728,18 +1855,29 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man } int deputy_4 = 0, main_4 = 0, deputy_1 = 0, main_1 = 0; + const bool switch_ready = is_fila_switch_ready(); for (auto ams : obj->GetFilaSystem()->GetAmsList()) { - // Main (first) extruder at right - if (ams.second->GetExtruderId() == 0) { - if (ams.second->GetAmsType() == DevAms::N3S) // N3S - ++main_1; - else - ++main_4; - } else if (ams.second->GetExtruderId() == 1) { - if (ams.second->GetAmsType() == DevAms::N3S) // N3S - ++deputy_1; - else - ++deputy_4; + for (int extruder_id : ams.second->GetBindedExtruderSet()) { + // With the switch installed every AMS binds to both extruders; once it is ready, attribute + // each to the extruder its input track feeds so the per-extruder counts stay correct. Without + // a switch the binding set is the single extruder and the filter is skipped, matching the + // pre-switch counts. + if (switch_ready) { + auto switcher_pos = ams.second->GetSwitcherPos(); + if (!switcher_pos) + continue; + int switcher_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) + : static_cast(switcher_pos.value()); + if (extruder_id != switcher_id) + continue; + } + const bool is_n3s = ams.second->GetAmsType() == DevAms::N3S; + // Main (first) extruder is id 0, deputy is id 1. + if (extruder_id == 0) { + if (is_n3s) ++main_1; else ++main_4; + } else if (extruder_id == 1) { + if (is_n3s) ++deputy_1; else ++deputy_4; + } } } only_external_material = !obj->GetFilaSystem()->HasAms(); @@ -1774,6 +1912,22 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man printer_tab->set_extruder_volume_type(idx, target_types[idx]); } + if (extruder_nums > 1) { + auto fila_switch = obj->GetFilaSwitch(); + if (fila_switch->IsInstalled()) + show_filament_switcher_dialog(fila_switch->IsReady(), is_manual); + else + fila_switch_warning_shown = false; + } + + // Copy the live switch state into the project so slicing and the send dialog honor it. Both + // resolve to false unless the switch is installed and calibrated, so nothing changes without one. + auto &project_config = wxGetApp().preset_bundle->project_config; + if (auto *dynamic_filament = project_config.opt("enable_filament_dynamic_map")) + dynamic_filament->value = is_fila_switch_ready(); + if (auto *has_switcher = project_config.opt("has_filament_switcher")) + has_switcher->value = is_fila_switch_ready(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " finish sync_extruder_list"; return true; } @@ -1791,6 +1945,7 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) right_extruder->sync_ams(nullptr, {}, {}); single_extruder->ShowBadge(false); single_extruder->sync_ams(nullptr, {}, {}); + update_extruder_separator_icon(false, false); //btn_sync_printer->SetBorderColor(not_synced_colour); //btn_sync_printer->SetIcon("printer_sync"); m_printer_bbl_sync->SetBitmap_("printer_sync_not"); @@ -1801,6 +1956,16 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) return; } + // Orca: replaces BBS's reset_fila_switch hook on DeviceManager::OnSelectedMachineChanged (absent + // in Orca). Selection/MQTT updates all funnel through here, so a change of the selected device id + // resets the switcher icon and the once-per-session not-ready warning for the new printer. + const std::string cur_dev_id = obj->get_dev_id(); + if (cur_dev_id != last_sync_dev_id) { + last_sync_dev_id = cur_dev_id; + update_extruder_separator_icon(false, false); + fila_switch_warning_shown = false; + } + PresetBundle *preset_bundle = wxGetApp().preset_bundle; if (!preset_bundle) { clear_all_sync_status(); @@ -1852,6 +2017,21 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) if (extruder_nums != obj->GetExtderSystem()->GetTotalExtderCount()) return; + // Filament Track Switch: only ever surfaced on multi-extruder machines. When installed, tip/warn + // and show the status icon (green ready / red not-calibrated); when absent the icon stays hidden. + // Inert on any printer without a switch: GetFilaSwitch()->IsInstalled() is false. + const bool fila_switch_flag = is_fila_switch_ready(); + if (extruder_nums > 1) { + auto fila_switch = obj->GetFilaSwitch(); + if (fila_switch->IsInstalled()) { + show_filament_switcher_dialog(fila_switch->IsReady(), false); + update_extruder_separator_icon(true, fila_switch->IsReady()); + } else { + update_extruder_separator_icon(false, false); + fila_switch_warning_shown = false; + } + } + std::vector extruder_infos(extruder_nums); std::vector nozzle_volume_types = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; //for (size_t i = 0; i < nozzle_volume_types.size(); ++i) { @@ -1893,16 +2073,34 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) machine_extruder_infos[extruder.GetExtId()].diameter = extruder.GetNozzleDiameter(); } for (auto &item : obj->GetFilaSystem()->GetAmsList()) { - if (item.second->GetExtruderId() >= machine_extruder_infos.size()) + int extruder_id; + // With the switch ready every AMS feeds both extruders, so attribute each to the extruder its + // input track feeds. Without a switch the binding is a single extruder + // (GetUniqueBindedExtruderId() == GetExtruderId()) and the switcher position is empty, so this + // yields the same attribution as before. + if (fila_switch_flag) { + auto switcher_pos = item.second->GetSwitcherPos(); + if (!switcher_pos) + continue; + extruder_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) + : static_cast(switcher_pos.value()); + } else { + const auto &uniq_extruder_id = item.second->GetUniqueBindedExtruderId(); + if (!uniq_extruder_id) + continue; + extruder_id = uniq_extruder_id.value(); + } + + if (extruder_id >= machine_extruder_infos.size()) continue; if (item.second->GetAmsType() == DevAms::N3S) { // N3S - machine_extruder_infos[item.second->GetExtruderId()].ams_1++; - machine_extruder_infos[item.second->GetExtruderId()].ams_v1.push_back(item.second); + machine_extruder_infos[extruder_id].ams_1++; + machine_extruder_infos[extruder_id].ams_v1.push_back(item.second); } else { - machine_extruder_infos[item.second->GetExtruderId()].ams_4++; - machine_extruder_infos[item.second->GetExtruderId()].ams_v4.push_back(item.second); + machine_extruder_infos[extruder_id].ams_4++; + machine_extruder_infos[extruder_id].ams_v4.push_back(item.second); } } @@ -1956,6 +2154,20 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) // btn_sync_printer->SetIcon("printer_sync"); m_printer_bbl_sync->SetBitmap_("printer_sync_not"); } + + // When the switch is ready every AMS filament is available to both extruders; refresh the + // filament combos whenever the AMS list changes so both extruders pick up the full set. + if (fila_switch_flag) { + static std::map last_filament_ams_list; + bool is_same_ams_list = last_filament_ams_list == wxGetApp().preset_bundle->filament_ams_list; + if (!is_same_ams_list) + last_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list; + const auto print_tech = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology(); + if (print_tech == ptFFF && !is_same_ams_list) { + for (PlaterPresetComboBox *cb : combos_filament) + cb->update(); + } + } } void Sidebar::update_sync_ams_btn_enable(wxUpdateUIEvent &e) @@ -2408,6 +2620,24 @@ Sidebar::Sidebar(Plater *parent) p->left_extruder = new ExtruderGroup(p->m_panel_printer_content, 0, _L("Left Nozzle")); p->right_extruder = new ExtruderGroup(p->m_panel_printer_content, 1, _L("Right Nozzle")); p->single_extruder = new ExtruderGroup(p->m_panel_printer_content, -1, _L("Nozzle")); + // Orca: keep the floating switcher icon aligned with the left extruder's AMS row when the + // extruder card is resized (the overlay is absolutely positioned, not managed by a sizer). + p->left_extruder->Bind(wxEVT_SIZE, [this](wxSizeEvent &evt) { + if (p->extruder_separator_icon && p->extruder_separator_icon->IsShown()) { + wxPoint left_box_pos = p->left_extruder->GetPosition(); + wxPoint ams_local_pos = p->left_extruder->hsizer_ams->GetPosition(); + wxSize left_size = p->left_extruder->sizer->GetSize(); + wxSize ams_size = p->left_extruder->hsizer_ams->GetSize(); + wxSize icon_size = p->extruder_separator_icon->GetSize(); + int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); + int center_x = left_size.GetWidth() + FromDIP(6) - icon_size.GetWidth() / 2; + int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2 - icon_size.GetHeight() / 2; + p->extruder_separator_icon->SetPosition(wxPoint(center_x, center_y)); + if (p->m_panel_printer_content) + p->m_panel_printer_content->Refresh(); + } + evt.Skip(); + }); auto switch_diameter = [this](wxCommandEvent & evt) { auto extruder = dynamic_cast(dynamic_cast(evt.GetEventObject())->GetParent()); p->is_switching_diameter = true; @@ -3821,6 +4051,16 @@ bool Sidebar::sync_extruder_list() return p->sync_extruder_list(only_external_material); } +bool Sidebar::is_fila_switch_ready() +{ + return p->is_fila_switch_ready(); +} + +void Sidebar::reset_fila_switch() +{ + p->update_extruder_separator_icon(false, false); +} + bool Sidebar::need_auto_sync_extruder_list_after_connect_priner(const MachineObject *obj) { if(!obj) @@ -6947,6 +7187,14 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } } + + // Filament Track Switch state is derived live from the connected + // printer; a loaded project must not carry a stale installed/active + // flag, so clear both and let device sync re-derive them. + if (auto* has_switcher = proj_cfg.opt("has_filament_switcher")) + has_switcher->value = false; + if (auto* dynamic_map = proj_cfg.opt("enable_filament_dynamic_map")) + dynamic_map->value = false; } // Update filament combobox after loading config wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 626d288f46..1234cac325 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -196,6 +196,8 @@ public: std::map build_filament_ams_list(MachineObject* obj); void sync_ams_list(bool is_from_big_sync_btn = false); bool sync_extruder_list(); + bool is_fila_switch_ready(); + void reset_fila_switch(); bool need_auto_sync_extruder_list_after_connect_priner(const MachineObject* obj); void update_sync_status(const MachineObject* obj); int get_sidebar_pos_right_x(); diff --git a/src/slic3r/GUI/PrePrintChecker.cpp b/src/slic3r/GUI/PrePrintChecker.cpp index 870d3f835d..4344e7170c 100644 --- a/src/slic3r/GUI/PrePrintChecker.cpp +++ b/src/slic3r/GUI/PrePrintChecker.cpp @@ -38,6 +38,7 @@ std::string PrePrintChecker::get_print_status_info(PrintDialogStatus status) case PrintStatusUnsupportedPrinter: return "PrintStatusUnsupportedPrinter"; case PrintStatusRackNozzleMappingWaiting: return "PrintStatusRackNozzleMappingWaiting"; case PrintStatusRackNozzleMappingError: return "PrintStatusRackNozzleMappingError"; + case PrintStatusFilaSwitcherError: return "PrintStatusFilaSwitcherError"; case PrintStatusColorQuantityExceed: return "PrintStatusColorQuantityExceed"; // Handle filament errors case PrintStatusAmsOnSettingup: return "PrintStatusAmsOnSettingup"; @@ -50,6 +51,7 @@ std::string PrePrintChecker::get_print_status_info(PrintDialogStatus status) case PrintStatusTimelapseWarning: return "PrintStatusTimelapseWarning"; case PrintStatusMixAmsAndVtSlotWarning: return "PrintStatusMixAmsAndVtSlotWarning"; case PrintStatusRackNozzleMappingWarning: return "PrintStatusRackNozzleMappingWarning"; + case PrintStatusFilaSwitcherSlicingNotMatch: return "PrintStatusFilaSwitcherSlicingNotMatch"; case PrintStatusWarningKvalueNotUsed: return "PrintStatusWarningKvalueNotUsed"; case PrintStatusHasFilamentInBlackListWarning: return "PrintStatusHasFilamentInBlackListWarning"; case PrintStatusFilamentWarningHighChamberTemp: return "PrintStatusFilamentWarningHighChamberTemp"; diff --git a/src/slic3r/GUI/PrePrintChecker.hpp b/src/slic3r/GUI/PrePrintChecker.hpp index 53ab441df4..5996782b1d 100644 --- a/src/slic3r/GUI/PrePrintChecker.hpp +++ b/src/slic3r/GUI/PrePrintChecker.hpp @@ -67,6 +67,7 @@ enum PrintDialogStatus : unsigned int { PrintStatusUnsupportedPrinter, PrintStatusRackNozzleMappingWaiting, PrintStatusRackNozzleMappingError, + PrintStatusFilaSwitcherError, PrintStatusPrinterErrorEnd, // Errors for filament, Block Print @@ -92,6 +93,7 @@ enum PrintDialogStatus : unsigned int { PrintStatusMixAmsAndVtSlotWarning, PrintStatusToolHeadCoolingFanWarning, PrintStatusRackNozzleMappingWarning, + PrintStatusFilaSwitcherSlicingNotMatch, PrintStatusPrinterWarningEnd, // Warnings for filament diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index c52d0eed3a..e24604fd68 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include @@ -510,8 +512,14 @@ bool PresetComboBox::add_ams_filaments(std::string selected, bool alias_name) bool selected_in_ams = false; bool is_bbl_vendor_preset = m_preset_bundle->is_bbl_vendor(); if (is_bbl_vendor_preset && !m_preset_bundle->filament_ams_list.empty()) { + // When a filament track switch is installed and calibrated, every AMS filament is reachable + // from both extruders, so present one deduplicated group instead of the Left/Right split. + bool fila_switch_ready = wxGetApp().sidebar().is_fila_switch_ready(); bool dual_extruder = (m_preset_bundle->filament_ams_list.begin()->first & 0x10000) == 0; - set_label_marker(Append(dual_extruder ? _L("Left filaments") : _L("AMS filament"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + if (fila_switch_ready) + set_label_marker(Append(_L("AMS filaments"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); + else + set_label_marker(Append(dual_extruder ? _L("Left filaments") : _L("AMS filament"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); m_first_ams_filament = GetCount(); auto &filaments = m_collection->get_presets(); @@ -523,8 +531,12 @@ bool PresetComboBox::add_ams_filaments(std::string selected, bool alias_name) icon_width = 32; } + // Deduplicate by (tray_name, filament_id) so a filament shared by both extruders is + // listed once when the switch is ready. Uses Orca's tray naming/lookup, not BBS's. + std::set> added_filaments; + for (auto &entry : m_preset_bundle->filament_ams_list) { - if (dual_extruder && (entry.first & 0x10000)) { + if (!fila_switch_ready && dual_extruder && (entry.first & 0x10000)) { dual_extruder = false; set_label_marker(Append(_L("Right filaments"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM)); } @@ -535,6 +547,13 @@ bool PresetComboBox::add_ams_filaments(std::string selected, bool alias_name) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% 's filament_id is empty.") % name; continue; } + if (fila_switch_ready) { + // skip the external spool and collapse duplicates shared across both extruders + if (name == "Ext") + continue; + if (!added_filaments.insert(std::make_pair(name, filament_id)).second) + 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()) { diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index e66d07d2e9..ec4f3a7cab 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -1593,6 +1593,58 @@ bool SelectMachineDialog::use_dynamic_nozzle_map() const return false; } +bool SelectMachineDialog::slicing_with_fila_switch() const +{ + if (use_dynamic_nozzle_map()) + return true; + + if (m_print_type == FROM_NORMAL) { + auto has_filament_switcher = wxGetApp().preset_bundle->project_config.option("has_filament_switcher"); + if (has_filament_switcher) + return has_filament_switcher->value; + } else if (m_print_type == FROM_SDCARD_VIEW) { + if (m_required_data_plate_data_list.size() > (size_t) m_print_plate_idx) { + auto has_filament_switcher = m_required_data_plate_data_list[m_print_plate_idx]->config.option("has_filament_switcher"); + if (has_filament_switcher) + return has_filament_switcher->value; + } + auto has_filament_switcher = m_required_data_config.option("has_filament_switcher"); + if (has_filament_switcher) + return has_filament_switcher->value; + } + return false; +} + +bool SelectMachineDialog::CheckErrorDynamicSwitchNozzle(MachineObject* obj_) +{ + if (!obj_) + return false; + + // Advisory only (does not block Send): the file was sliced for a switch state that doesn't + // match the installed hardware, so grouping/flush may be suboptimal. Only on firmware that + // reports it can check this. + if (obj_->is_support_check_track_switch_match_slice_printer && slicing_with_fila_switch() != obj_->GetFilaSwitch()->IsInstalled()) { + show_status(PrintDialogStatus::PrintStatusFilaSwitcherSlicingNotMatch, + {_L("The Filament Track Switch installed on the printer does not match the slicing file. Please re-slice to avoid print quality issues.")}); + } + + // The blocking checks below only matter for dynamic nozzle mapping, which requires a switch. + if (!use_dynamic_nozzle_map()) + return true; + + if (!obj_->GetFilaSwitch()->IsInstalled()) { + show_status(PrintDialogStatus::PrintStatusFilaSwitcherError, {_L("This print requires a Filament Track Switch. Please install it first.")}); + return false; + } + + if (!obj_->GetFilaSwitch()->IsReady()) { + show_status(PrintDialogStatus::PrintStatusFilaSwitcherError, {_L("The Filament Track Switch has not been setup. Please setup it first.")}); + return false; + } + + return true; +} + void SelectMachineDialog::clear_nozzle_mapping() { m_nozzle_mapping_result.clear(); @@ -2010,6 +2062,14 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vectorGetExtderSystem()->GetCurrentExtder(); - m_filament_step->SetupSteps(cur_ext ? cur_ext->HasFilamentInExt() : false); + m_filament_step->SetupSteps(obj, cur_ext ? cur_ext->HasFilamentInExt() : false); Layout(); Fit(); @@ -3526,6 +3526,14 @@ void StatusPanel::update_ams_control_state(std::string ams_id, std::string slot_ if (ams_id.empty() || slot_id.empty()) { load_error_info = _L("Choose an AMS slot then press \"Load\" or \"Unload\" button to automatically load or unload filament."); unload_error_info = _L("Choose an AMS slot then press \"Load\" or \"Unload\" button to automatically load or unload filament."); + } else if (obj->GetFilaSwitch()->IsInstalled() && devPrinterUtil::IsVirtualSlot(ams_id)) { + // Orca: with a Filament Track Switch installed the external spool cannot be routed, so both actions are blocked. + load_error_info = _L("\"Load\" or \"Unload\" is not supported for external spool while using Filament Track Switch."); + unload_error_info = load_error_info; + } else if (obj->GetFilaSwitch()->IsInstalled() && !obj->GetFilaSwitch()->IsReady()) { + // Orca: an un-calibrated Filament Track Switch cannot route filament to either extruder, so block both actions. + load_error_info = _L("The Filament Track Switch has not been setup. Please setup on printer."); + unload_error_info = load_error_info; } else if (ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) { for (auto ext : obj->GetExtderSystem()->GetExtruders()) { if (ext.GetSlotNow().ams_id == ams_id && ext.GetSlotNow().slot_id == slot_id) @@ -3535,6 +3543,8 @@ void StatusPanel::update_ams_control_state(std::string ams_id, std::string slot_ } } else { for (auto ext : obj->GetExtderSystem()->GetExtruders()) { + // Orca: with a Filament Track Switch installed a slot is not bound to a single extruder, so skip the "already loaded" identity check. + if (obj->GetFilaSwitch()->IsInstalled()) { continue; } if (ext.GetSlotNow().ams_id == ams_id && ext.GetSlotNow().slot_id == slot_id) { load_error_info = _L("Current slot has already been loaded."); @@ -4231,6 +4241,43 @@ void StatusPanel::on_ams_load_curr() std::string curr_ams_id = m_ams_control->GetCurentAms(); std::string curr_can_id = m_ams_control->GetCurrentCan(curr_ams_id); + std::optional extruder_id = std::nullopt; + if (obj->GetFilaSwitch() && obj->GetFilaSwitch()->IsInstalled()) { + if (!obj->GetFilaSwitch()->IsReady()) { + MessageDialog msg_dlg(nullptr, _L("The Filament Track Switch has not been setup. Please setup on printer."), wxEmptyString, wxICON_WARNING | wxOK); + msg_dlg.ShowModal(); + return; + } + + // Filament Track Switch is calibrated: the load can go to either extruder, so ask the + // user which. Record each extruder's currently-loaded slot so the dialog can grey out a + // side that already holds this filament. + std::vector> extruderSlots(2, {"", ""}); + if (auto ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); ext.has_value()) + { + if (ext->HasFilamentInExt()) + { + extruderSlots[MAIN_EXTRUDER_ID] = {ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id}; + } + } + if (auto ext = obj->GetExtderSystem()->GetExtderById(DEPUTY_EXTRUDER_ID); ext.has_value()) + { + if (ext->HasFilamentInExt()) + { + extruderSlots[DEPUTY_EXTRUDER_ID] = {ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id}; + } + } + + FeedDirectionDialog dialog(nullptr, 2, obj->printer_type); + dialog.SetExtruderMapping(obj, curr_ams_id, curr_can_id, extruderSlots); + auto rtn = dialog.ShowModal(); + + if (rtn != wxID_OK) + { + return; + } + extruder_id = dialog.GetExtruderID(); + } update_load_with_temp(); //virtual tray @@ -4262,11 +4309,11 @@ void StatusPanel::on_ams_load_curr() if (obj->is_enable_np || obj->is_enable_ams_np) { try { if (!curr_ams_id.empty() && !curr_can_id.empty()) { - obj->command_ams_change_filament(true, curr_ams_id, "0", old_temp, new_temp); + obj->command_ams_change_filament(true, curr_ams_id, "0", old_temp, new_temp, extruder_id); } } catch (...) {} } else { - obj->command_ams_change_filament(true, "254", "0", old_temp, new_temp); + obj->command_ams_change_filament(true, "254", "0", old_temp, new_temp, extruder_id); } } @@ -4302,12 +4349,12 @@ void StatusPanel::on_ams_load_curr() if (obj->is_enable_np) { try { if (!curr_ams_id.empty() && !curr_can_id.empty()) { - obj->command_ams_change_filament(true, curr_ams_id, curr_can_id, old_temp, new_temp); + obj->command_ams_change_filament(true, curr_ams_id, curr_can_id, old_temp, new_temp, extruder_id); } } catch (...){} } else { - obj->command_ams_change_filament(true, curr_ams_id, curr_can_id, old_temp, new_temp); + obj->command_ams_change_filament(true, curr_ams_id, curr_can_id, old_temp, new_temp, extruder_id); } } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 658d658823..ca4bfeddbb 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5636,6 +5636,18 @@ void TabPrinter::on_preset_loaded() if (use_default_nozzle_volume_type) { m_preset_bundle->project_config.option("nozzle_volume_type")->values = current_printer.config.option("default_nozzle_volume_type")->values; } + + // Changing printer model drops any Filament Track Switch context from the previous + // printer; clear both device-derived flags so a switch-less printer never inherits a + // stale installed/active state (re-derived by device sync when a printer is connected). + if (auto* has_switcher = m_preset_bundle->project_config.opt("has_filament_switcher")) + has_switcher->value = false; + if (auto* dynamic_map = m_preset_bundle->project_config.opt("enable_filament_dynamic_map")) + dynamic_map->value = false; + // Orca: also clear the sidebar switcher status icon on printer-model change (mirrors BBS's + // reset_fila_switch on machine change); it re-derives from device sync once a printer connects. + if (wxGetApp().plater()) + wxGetApp().plater()->sidebar().reset_fila_switch(); } } diff --git a/src/slic3r/GUI/UpgradePanel.cpp b/src/slic3r/GUI/UpgradePanel.cpp index 9ff1d12709..16fe0420d8 100644 --- a/src/slic3r/GUI/UpgradePanel.cpp +++ b/src/slic3r/GUI/UpgradePanel.cpp @@ -28,6 +28,7 @@ static const std::unordered_map ACCESSORY_DISPLAY_STR = { {"O2L_ACM", "Active Cutting Module"}, {"O2L_UCM", "Ultrasonic Cutting Module"}, {"O2L-AFP", L("Auto Fire Extinguishing System")}, + {"O2L-FTS", L("Filament Track Switch")}, }; enum FIRMWARE_STASUS @@ -217,6 +218,7 @@ MachineInfoPanel::MachineInfoPanel(wxWindow* parent, wxWindowID id, const wxPoin createLaserWidgets(m_main_left_sizer); createAirPumpWidgets(m_main_left_sizer); createExtinguishWidgets(m_main_left_sizer); + createFilaTrackSwitchWidgets(m_main_left_sizer); // nozzle rack widgets (H2C induction hotend rack; hidden unless GetNozzleRack()->IsSupported()) createNozzleRackWidgets(m_main_left_sizer); @@ -406,6 +408,27 @@ void MachineInfoPanel::createExtinguishWidgets(wxBoxSizer* main_left_sizer) main_left_sizer->Add(m_extinguish_sizer, 0, wxEXPAND, 0); } +void MachineInfoPanel::createFilaTrackSwitchWidgets(wxBoxSizer* main_left_sizer) +{ + m_filatrack_line_above = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); + m_filatrack_line_above->SetBackgroundColour(wxColour(206, 206, 206)); + main_left_sizer->Add(m_filatrack_line_above, 0, wxEXPAND | wxLEFT, FromDIP(40)); + + m_filatrack_img = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(200), FromDIP(200))); + m_filatrack_img->SetBitmap(m_img_filatrack.bmp()); + + wxBoxSizer* content_sizer = new wxBoxSizer(wxVERTICAL); + content_sizer->Add(0, 40, 0, wxEXPAND, FromDIP(5)); + m_filatrack_version = new uiDeviceUpdateVersion(this, wxID_ANY); + content_sizer->Add(m_filatrack_version, 0, wxEXPAND, 0); + + m_filatrack_sizer = new wxBoxSizer(wxHORIZONTAL); + m_filatrack_sizer->Add(m_filatrack_img, 0, wxALIGN_TOP | wxALL, FromDIP(5)); + m_filatrack_sizer->Add(content_sizer, 1, wxEXPAND, 0); + + main_left_sizer->Add(m_filatrack_sizer, 0, wxEXPAND, 0); +} + void MachineInfoPanel::msw_rescale() { rescale_bitmaps(); @@ -435,6 +458,7 @@ void MachineInfoPanel::init_bitmaps() m_img_laser = ScalableBitmap(this, "laser", 160); m_img_cutting = ScalableBitmap(this, "cut", 160); m_img_extinguish = ScalableBitmap(this, "extinguish", 160); + m_img_filatrack = ScalableBitmap(this, "filament_track_switch", 160); m_img_nozzle_rack = ScalableBitmap(this, "nozzle_rack", 160); upgrade_green_icon = ScalableBitmap(this, "monitor_upgrade_online", 5); @@ -544,6 +568,7 @@ void MachineInfoPanel::update(MachineObject* obj) update_cut(obj); update_laszer(obj); update_extinguish(obj); + update_filatrack(obj); update_nozzle_rack(obj); //update progress @@ -1129,6 +1154,19 @@ void MachineInfoPanel::update_extinguish(MachineObject* obj) } } +void MachineInfoPanel::update_filatrack(MachineObject* obj) +{ + if (obj && obj->filatrack_version_info.isValid()) + { + m_filatrack_version->UpdateInfo(obj->filatrack_version_info); + show_filatrack(true); + } + else + { + show_filatrack(false); + } +} + void MachineInfoPanel::show_status(int status, std::string upgrade_status_str) { if (last_status == status && last_status_str == upgrade_status_str) return; @@ -1263,6 +1301,16 @@ void MachineInfoPanel::show_extinguish(bool show) } } +void MachineInfoPanel::show_filatrack(bool show) +{ + if (m_filatrack_version->IsShown() != show) + { + m_filatrack_img->Show(show); + m_filatrack_line_above->Show(show); + m_filatrack_version->Show(show); + } +} + void MachineInfoPanel::createNozzleRackWidgets(wxBoxSizer *main_left_sizer) { // horizontal line above diff --git a/src/slic3r/GUI/UpgradePanel.hpp b/src/slic3r/GUI/UpgradePanel.hpp index ef56bde03d..0cf88ae080 100644 --- a/src/slic3r/GUI/UpgradePanel.hpp +++ b/src/slic3r/GUI/UpgradePanel.hpp @@ -134,6 +134,12 @@ protected: wxStaticLine* m_extinguish_line_above = nullptr;; uiDeviceUpdateVersion* m_extinguish_version = nullptr; + /* filament track switch info*/ + wxBoxSizer* m_filatrack_sizer = nullptr; + wxStaticBitmap* m_filatrack_img = nullptr; + wxStaticLine* m_filatrack_line_above = nullptr; + uiDeviceUpdateVersion* m_filatrack_version = nullptr; + /* nozzle rack (H2C induction hotend rack) — opens wgtDeviceNozzleRackUpgradeDlg */ wxBoxSizer* m_nozzle_rack_sizer = nullptr; wxStaticBitmap* m_nozzle_rack_img = nullptr; @@ -162,6 +168,7 @@ protected: ScalableBitmap m_img_cutting; ScalableBitmap m_img_laser; ScalableBitmap m_img_extinguish; + ScalableBitmap m_img_filatrack; ScalableBitmap m_img_nozzle_rack; ScalableBitmap upgrade_gray_icon; ScalableBitmap upgrade_green_icon; @@ -220,18 +227,21 @@ private: void createCuttingWidgets(wxBoxSizer* main_left_sizer); void createLaserWidgets(wxBoxSizer* main_left_sizer); void createExtinguishWidgets(wxBoxSizer* main_left_sizer); + void createFilaTrackSwitchWidgets(wxBoxSizer* main_left_sizer); void createNozzleRackWidgets(wxBoxSizer* main_left_sizer); void update_air_pump(MachineObject* obj); void update_cut(MachineObject* obj); void update_laszer(MachineObject* obj); void update_extinguish(MachineObject* obj); + void update_filatrack(MachineObject* obj); void update_nozzle_rack(MachineObject* obj); void show_air_pump(bool show = true); void show_cut(bool show = true); void show_laszer(bool show = true); void show_extinguish(bool show = true); + void show_filatrack(bool show = true); void show_nozzle_rack(bool show = true); void on_nozzle_rack_update(wxCommandEvent& event); diff --git a/src/slic3r/GUI/Widgets/AMSControl.cpp b/src/slic3r/GUI/Widgets/AMSControl.cpp index a3913dc4e4..ea693fbd4c 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.cpp +++ b/src/slic3r/GUI/Widgets/AMSControl.cpp @@ -9,9 +9,11 @@ #include "slic3r/GUI/DeviceCore/DevManager.h" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" #include #include +#include #include @@ -39,7 +41,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons SetBackgroundColour(*wxWHITE); // normal mode //Freeze(); - wxBoxSizer *m_sizer_body = new wxBoxSizer(wxVERTICAL); + m_sizer_body = new wxBoxSizer(wxVERTICAL); m_amswin = new wxWindow(this, wxID_ANY); m_amswin->SetBackgroundColour(*wxWHITE); m_amswin->SetSize(wxSize(FromDIP(578), -1)); @@ -151,6 +153,12 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons m_extruder = new AMSextruder(m_amswin, wxID_ANY, m_total_ext_count, wxDefaultPosition, AMS_EXTRUDER_SIZE); m_sizer_option_mid->Add( m_extruder, 0, wxALIGN_CENTER, 0 ); + // Orca: filament-switch routing glyph; hidden by default so it stays inert (zero layout impact) + // on any printer without a Filament Track Switch. Shown from UpdateAms only when installed. + m_switcher = new SwitcherImage(m_amswin, wxID_ANY, "fila_switch", wxSize(FromDIP(29), FromDIP(16)), wxDefaultPosition); + m_switcher->Hide(); + m_sizer_option_mid->Add(m_switcher, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6)); + /*option right*/ m_button_extruder_feed = new Button(m_panel_option_right, _L("Load")); @@ -975,6 +983,32 @@ void AMSControl::UpdateAms(const std::string &series_name, { m_amswin->Layout(); } + + /*update switch status*/ + // Orca: inert on any printer without a Filament Track Switch — install is false, so the banner is + // never materialized, the glyph stays hidden, and every ShowRoad(true) below is a no-op. + const auto [install, ready] = isFilaSwitchReady(); + show_switcher_status(install && (!ready)); + bool isShow = install && m_total_ext_count >= 2; + if (m_switcher->IsShown() != isShow) + { + m_switcher->Show(isShow); + m_sizer_body->Layout(); + m_sizer_body->Fit(this); + this->Layout(); + this->Refresh(true); + this->Update(); + } + + /*update ext road visibility when fila switch installed*/ + bool road_visibility_changed = false; + for (auto& [ams_id, ams_item] : m_ams_item_list) { + if (!ams_item) continue; + // Orca: drop the external-spool road (AMSModel::EXT_AMS) while the switch routes all filament. + const bool should_show_road = !(install && ams_item->get_ams_model() == AMSModel::EXT_AMS); + if (ams_item->ShowRoad(should_show_road)) { road_visibility_changed = true; } + } + if (road_visibility_changed) { m_amswin->Layout(); } } void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type) @@ -1596,6 +1630,54 @@ void AMSControl::on_ams_setting_click(wxMouseEvent &event) post_event(SimpleEvent(EVT_AMS_SETTINGS)); } +std::tuple AMSControl::isFilaSwitchReady() +{ + DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (!dev) return {false, false}; + MachineObject* obj = dev->get_selected_machine(); + if (!obj) return {false, false}; + // Orca: GetFilaSwitch() is a raw non-null accessor (BBS returns a shared_ptr). + DevFilaSwitch* fila_switch = obj->GetFilaSwitch(); + if (fila_switch) + { + return {fila_switch->IsInstalled(), fila_switch->IsReady()}; + } + return {false, false}; +} + +void AMSControl::show_switcher_status(bool show) +{ + // Orca: never materialize the banner on printers that never request it, so the AMS control is + // byte-identical without a Filament Track Switch (UpdateAms calls this with show=false every refresh). + if (!show && tipPanel == nullptr) { return; } + + if (tipPanel == nullptr) + { + m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(5)); + tipPanel = new wxPanel(m_amswin); + tipPanel->SetBackgroundColour(wxColour(255, 153, 0)); + tipSizer = new wxBoxSizer(wxHORIZONTAL); + tipPanel->SetSizer(tipSizer); + icon = new wxStaticBitmap(tipPanel, wxID_ANY, + wxArtProvider::GetBitmap(wxART_INFORMATION, wxART_MESSAGE_BOX, wxSize(FromDIP(16), FromDIP(16)))); + tipSizer->Add(icon, 0, wxALL, FromDIP(8)); + tipText = new wxStaticText(tipPanel, wxID_ANY, _L("AMS has not been initialized. Please initialize it before use.")); + tipText->SetForegroundColour(wxColour(255, 255, 255)); + tipText->SetFont(wxFont(10, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD)); + tipText->Wrap(-1); + tipText->SetMinSize(wxSize(-1, -1)); + tipSizer->Add(tipText, 0, wxALL | wxALIGN_CENTER_VERTICAL | wxEXPAND, FromDIP(8)); + m_sizer_body->Add(tipPanel, 1, wxEXPAND, 0); + } + if (tipPanel->IsShown() == show) + { + return; + } + tipPanel->Show(show); + m_amswin->Layout(); + m_amswin->Fit(); +} + void AMSControl::parse_object(MachineObject* obj) { if (!obj || obj->GetFilaSystem()->GetAmsList().size() == 0) { diff --git a/src/slic3r/GUI/Widgets/AMSControl.hpp b/src/slic3r/GUI/Widgets/AMSControl.hpp index 31c6b86feb..7616d269f2 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.hpp +++ b/src/slic3r/GUI/Widgets/AMSControl.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" @@ -51,8 +52,16 @@ protected: int m_total_ext_count = 1; AMSextruder *m_extruder{nullptr}; + SwitcherImage *m_switcher{nullptr}; // Orca: filament-switch routing glyph (hidden unless a switch is installed) AMSRoadDownPart* m_down_road{ nullptr }; + // Orca: "AMS not initialized" warning banner, materialized lazily only when a switch needs it (see show_switcher_status) + wxBoxSizer* m_sizer_body{nullptr}; + wxPanel* tipPanel{nullptr}; + wxBoxSizer* tipSizer{nullptr}; + wxStaticBitmap* icon{nullptr}; + wxStaticText* tipText{nullptr}; + /*items*/ wxBoxSizer* m_sizer_ams_items{nullptr}; wxScrolledWindow* m_panel_prv_left {nullptr}; @@ -151,6 +160,11 @@ public: void StopRridLoading(wxString amsid, wxString canid); void ShowFilamentTip(bool hasams = true); + // Orca: Filament Track Switch awareness. isFilaSwitchReady() resolves the selected machine and + // returns {installed, ready}; show_switcher_status() toggles the "AMS not initialized" banner. + std::tuple isFilaSwitchReady(); + void show_switcher_status(bool show); + void UpdatePassRoad(string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step); void CreateAms(); void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type); diff --git a/src/slic3r/GUI/Widgets/AMSItem.cpp b/src/slic3r/GUI/Widgets/AMSItem.cpp index 129eec9c61..2e47f01d6c 100644 --- a/src/slic3r/GUI/Widgets/AMSItem.cpp +++ b/src/slic3r/GUI/Widgets/AMSItem.cpp @@ -742,6 +742,63 @@ void AMSExtImage::doRender(wxDC& dc) } +// Orca: SwitcherImage — routing glyph drawn when a Filament Track Switch is installed. +void SwitcherImage::paintEvent(wxPaintEvent &evt) +{ + wxPaintDC dc(this); + render(dc); +} + +void SwitcherImage::render(wxDC &dc) +{ +#ifdef __WXMSW__ + wxSize size = GetSize(); + wxMemoryDC memdc; + wxBitmap bmp(size.x, size.y); + memdc.SelectObject(bmp); + memdc.Blit({0, 0}, size, &dc, {0, 0}); + + { + wxGCDC dc2(memdc); + doRender(dc2); + } + + memdc.SelectObject(wxNullBitmap); + dc.DrawBitmap(bmp, 0, 0); +#else + doRender(dc); +#endif +} + +void SwitcherImage::doRender(wxDC &dc) +{ + auto size = GetSize(); + if (m_show_state){ + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(*wxWHITE); + dc.DrawBitmap(m_switcher.bmp(), wxPoint((size.x - m_switcher.GetBmpSize().x) / 2, 0)); + } + Layout(); +} + +SwitcherImage::SwitcherImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos) +{ + wxWindow::Create(parent, id, pos, size); + SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + m_show_state = true; + m_switcher = ScalableBitmap(this, file_name, 16); + m_file_name = file_name; + SetSize(size); + SetMinSize(size); + SetMaxSize(size); + + + Bind(wxEVT_PAINT, &SwitcherImage::paintEvent, this); +} + +SwitcherImage::~SwitcherImage() {} + + //DevAms Extruder AMSextruder::AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos, const wxSize &size) { @@ -3577,6 +3634,18 @@ void AmsItem::doRender(wxDC& dc) } } +// Orca: toggle the road segment below the item (m_panel_road). Used to drop the external-spool +// road when a Filament Track Switch is installed. No-op (returns false) when already in state. +bool AmsItem::ShowRoad(bool show) +{ + if (!m_panel_road) return false; + if (m_panel_road->IsShown() == show) return false; + m_panel_road->Show(show); + Layout(); + Refresh(); + return true; +} + void AmsItem::RenderLiteRoad(wxDC& dc, wxSize size) { auto end_top = size.x - FromDIP(3); if (m_panel_pos == AMSPanelPos::RIGHT_PANEL){ @@ -3706,4 +3775,342 @@ void AmsItem::show_sn_value(bool show) } } +DevExtruderImage::DevExtruderImage(wxWindow *parent, wxWindowID id, int extruder_num, const wxPoint &pos, const wxSize &size) : wxWindow(parent, id, pos, wxDefaultSize), m_extruder_num(extruder_num) +{ + // wxWindow::Create(parent, id, pos, wxSize(FromDIP(45), FromDIP(112))); + SetBackgroundColour(*wxWHITE); + SetSize(wxSize(FromDIP(48), FromDIP(112))); + SetMinSize(wxSize(FromDIP(48), FromDIP(112))); + SetMaxSize(wxSize(FromDIP(48), FromDIP(112))); + + + m_left_extruder_active_filled = new ScalableBitmap(this, "left_extruder_active_filled", 62); + m_left_extruder_active_empty = new ScalableBitmap(this, "left_extruder_active_empty", 62); + m_left_extruder_unactive_filled = new ScalableBitmap(this, "left_extruder_unactive_filled", 62); + m_left_extruder_unactive_empty = new ScalableBitmap(this, "left_extruder_unactive_empty", 62); + m_right_extruder_active_filled = new ScalableBitmap(this, "right_extruder_active_filled", 62); + m_right_extruder_active_empty = new ScalableBitmap(this, "right_extruder_active_empty", 62); + m_right_extruder_unactive_filled = new ScalableBitmap(this, "right_extruder_unactive_filled", 62); + m_right_extruder_unactive_empty = new ScalableBitmap(this, "right_extruder_unactive_empty", 62); + + m_extruder_single_nozzle_empty_load = new ScalableBitmap(this, "monitor_extruder_empty_load", 106); + m_extruder_single_nozzle_empty_unload = new ScalableBitmap(this, "monitor_extruder_empty_unload", 106); + m_extruder_single_nozzle_filled_load = new ScalableBitmap(this, "monitor_extruder_filled_load", 106); + m_extruder_single_nozzle_filled_unload = new ScalableBitmap(this, "monitor_extruder_filled_unload", 106); + + Bind(wxEVT_PAINT, &DevExtruderImage::paintEvent, this); +} + +void DevExtruderImage::msw_rescale() +{ + m_left_extruder_active_filled->msw_rescale(); + m_left_extruder_active_empty->msw_rescale(); + m_left_extruder_unactive_filled->msw_rescale(); + m_left_extruder_unactive_empty->msw_rescale(); + m_right_extruder_active_filled->msw_rescale(); + m_right_extruder_active_empty->msw_rescale(); + m_right_extruder_unactive_filled->msw_rescale(); + m_right_extruder_unactive_empty->msw_rescale(); + + m_extruder_single_nozzle_empty_load->msw_rescale(); + m_extruder_single_nozzle_empty_unload->msw_rescale(); + m_extruder_single_nozzle_filled_load->msw_rescale(); + m_extruder_single_nozzle_filled_unload->msw_rescale(); + Layout(); + Refresh(); +} + +void DevExtruderImage::render(wxDC &dc) +{ +#ifdef __WXMSW__ + wxSize size = GetSize(); + wxMemoryDC memdc; + wxBitmap bmp(size.x, size.y); + memdc.SelectObject(bmp); + memdc.Blit({0, 0}, size, &dc, {0, 0}); + + { + wxGCDC dc2(memdc); + doRender(dc2); + } + + memdc.SelectObject(wxNullBitmap); + dc.DrawBitmap(bmp, 0, 0); +#else + doRender(dc); +#endif +} + +void DevExtruderImage::doRender(wxDC &dc) +{ + auto size = GetSize(); + auto pot = wxPoint(size.x / 2, (size.y - m_left_extruder_active_filled->GetBmpSize().y) / 2); + + if (m_extruder_num >= 2) + { + ScalableBitmap *left_extruder_bmp{nullptr}; + ScalableBitmap *right_extruder_bmp{nullptr}; + + switch (m_right_ext_state) + { + case DevExtruderState::FILLED_LOAD: + right_extruder_bmp = current_extruder_loc == "right" ? m_right_extruder_active_filled : m_right_extruder_unactive_filled; + break; + case DevExtruderState::FILLED_UNLOAD: + right_extruder_bmp = current_extruder_loc == "right" ? m_right_extruder_active_filled : m_right_extruder_unactive_filled; + break; + case DevExtruderState::EMPTY_LOAD: + if (current_extruder_loc.empty()) + { + right_extruder_bmp = m_right_extruder_active_empty; + } + else + { + right_extruder_bmp = current_extruder_loc == "right" ? m_right_extruder_active_empty : m_right_extruder_unactive_empty; + } + break; + case DevExtruderState::EMPTY_UNLOAD: + right_extruder_bmp = current_extruder_loc == "right" ? m_right_extruder_active_empty : m_right_extruder_unactive_empty; + break; + default: break; + } + + switch (m_left_ext_state) + { + case DevExtruderState::FILLED_LOAD: + left_extruder_bmp = current_extruder_loc == "left" ? m_left_extruder_active_filled : m_left_extruder_unactive_filled; + break; + case DevExtruderState::FILLED_UNLOAD: + left_extruder_bmp = current_extruder_loc == "left" ? m_left_extruder_active_filled : m_left_extruder_unactive_filled; + break; + case DevExtruderState::EMPTY_LOAD: + if (current_extruder_loc.empty()) + { + left_extruder_bmp = m_left_extruder_active_empty; + } + else + { + left_extruder_bmp = current_extruder_loc == "left" ? m_left_extruder_active_empty : m_left_extruder_unactive_empty; + } + break; + case DevExtruderState::EMPTY_UNLOAD: + left_extruder_bmp = current_extruder_loc == "left" ? m_left_extruder_active_empty : m_left_extruder_unactive_empty; + break; + default: break; + } + + if (left_extruder_bmp) { dc.DrawBitmap(left_extruder_bmp->bmp(), pot.x - left_extruder_bmp->GetBmpWidth(), pot.y); } + if (right_extruder_bmp) { dc.DrawBitmap(right_extruder_bmp->bmp(), pot.x, pot.y); } + } + else + { + ScalableBitmap *extruder_bmp = nullptr; + switch (m_single_ext_state) + { + case DevExtruderState::FILLED_LOAD: + extruder_bmp = m_extruder_single_nozzle_filled_load; + break; + case DevExtruderState::FILLED_UNLOAD: + extruder_bmp = m_extruder_single_nozzle_filled_unload; + break; + case DevExtruderState::EMPTY_LOAD: + extruder_bmp = m_extruder_single_nozzle_empty_load; + break; + case DevExtruderState::EMPTY_UNLOAD: + extruder_bmp = m_extruder_single_nozzle_empty_unload; + break; + default: break; + } + + if (extruder_bmp) { dc.DrawBitmap(extruder_bmp->bmp(), pot.x - extruder_bmp->GetBmpWidth() / 2, (size.y - extruder_bmp->GetBmpHeight()) / 2); } + } +} + + +FeedDirectionDialog::FeedDirectionDialog(wxWindow* parent, + const int extruderNum, + const std::string& printer_type) + : wxDialog(parent, wxID_ANY, "", wxDefaultPosition, wxDefaultSize), + m_extruder_num(extruderNum), + m_printer_type(printer_type) +{ + SetBackgroundColour(wxColour("#FFFFFF")); + SetMaxSize(wxSize(FromDIP(360), FromDIP(207))); + SetMinSize(wxSize(FromDIP(360), FromDIP(207))); + SetSize(wxSize(FromDIP(360), FromDIP(207))); + + wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); + + wxGridSizer* topSizer = new wxGridSizer (1, 3, FromDIP(5), 0); + + m_radioHelper = new wxRadioButton(this, wxID_ANY, wxT(""), wxDefaultPosition, wxDefaultSize, wxRB_GROUP); + m_leftRadio = new wxRadioButton(this, wxID_ANY, + _L(DevPrinterConfigUtil::get_toolhead_display_name(m_printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true))); + m_rightRadio = new wxRadioButton(this, wxID_ANY, + _L(DevPrinterConfigUtil::get_toolhead_display_name(m_printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true))); + m_radioHelper->Show(false); + m_radioHelper->SetCanFocus(false); + + m_leftRadio->SetForegroundColour(*wxBLACK); + m_rightRadio->SetForegroundColour(*wxBLACK); + + topSizer->Add(m_leftRadio, 0, wxALIGN_CENTER | wxALL, FromDIP(20)); + m_extruderImage = new DevExtruderImage(this, wxID_ANY, m_extruder_num); + topSizer->Add(m_extruderImage, 0, wxALIGN_CENTER | wxALL, FromDIP(5)); + topSizer->Add(m_rightRadio, 0, wxALIGN_CENTER | wxALL, FromDIP(20)); + + mainSizer->AddStretchSpacer(1); + mainSizer->Add(topSizer, 1, wxEXPAND); + mainSizer->AddStretchSpacer(1); + + wxBoxSizer* bottomSizer = new wxBoxSizer(wxHORIZONTAL); + m_confirmBtn = new Button(this, _L("Confirm")); + m_confirmBtn->SetSize(wxSize(FromDIP(80), FromDIP(32))); + m_confirmBtn->Enable(false); + m_confirmBtn->SetFont(::Label::Body_14); + bottomSizer->Add(m_confirmBtn, 0, wxALIGN_RIGHT); + + mainSizer->Add(bottomSizer, 0, wxALIGN_RIGHT | wxBOTTOM | wxRIGHT, FromDIP(10)); + + SetSizer(mainSizer); + Layout(); + Centre(wxBOTH); + + m_lastChecked = m_radioHelper; + m_confirmBtn->Bind(wxEVT_BUTTON, &FeedDirectionDialog::OnConfirm, this); + m_leftRadio->Bind(wxEVT_RADIOBUTTON, &FeedDirectionDialog::OnRadioClicked, this); + m_rightRadio->Bind(wxEVT_RADIOBUTTON, &FeedDirectionDialog::OnRadioClicked, this); +} + +void FeedDirectionDialog::OnConfirm(wxCommandEvent& event) +{ + EndModal(wxID_OK); +} + +void FeedDirectionDialog::OnRadioClicked(wxCommandEvent& evt) +{ + auto clicked = static_cast(evt.GetEventObject()); + m_load_extruder_id = std::nullopt; + if (clicked == m_lastChecked) + { + m_radioHelper->SetValue(true); + m_lastChecked = m_radioHelper; + m_confirmBtn->Enable(false); + m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::EMPTY_LOAD); + m_extruderImage->setExtruderUsed(""); + + } + else + { + clicked->SetValue(true); + m_lastChecked = clicked; + m_confirmBtn->Enable(true); + + if (clicked == m_leftRadio) + { + m_extruderImage->update(DevExtruderState::FILLED_LOAD, DevExtruderState::EMPTY_LOAD); + m_extruderImage->setExtruderUsed("left"); + m_load_extruder_id = 1; + { + SetTitle(wxString::Format(_L("Load %s to ") + _L(DevPrinterConfigUtil::get_toolhead_display_name(m_printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::LowerCase)), m_filament_id)); + } + } + else if (clicked == m_rightRadio) + { + m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::FILLED_LOAD); + m_extruderImage->setExtruderUsed("right"); + m_load_extruder_id = 0; + { + SetTitle(wxString::Format(_L("Load %s to ") + _L(DevPrinterConfigUtil::get_toolhead_display_name(m_printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::LowerCase)), m_filament_id)); + } + } + } + m_leftRadio->Refresh(); + m_rightRadio->Refresh(); + Fit(); + Update(); +} + +std::optional FeedDirectionDialog::GetExtruderID() +{ + if (m_extruderImage) + { + return m_load_extruder_id; + } + return std::nullopt; +} + +wxString FeedDirectionDialog::calcTrayName(MachineObject* obj, const std::string& amsID, const std::string& slotID) +{ + if (amsID.empty() || slotID.empty() || !obj) + return wxString(); + + auto filaSys = obj->GetFilaSystem(); + if (!filaSys) + return wxString(); + + DevAms* ams = filaSys->GetAmsById(amsID); + if (!ams) + return wxString(); + + int ams_id_int = std::stoi(amsID); + int slot_id_int = std::stoi(slotID); + int tray_id = 0; + + // Orca: DevAms::AmsType is the local enum (AMS / AMS_LITE / N3F / N3S); it has no EXT_SPOOL + // member, and external/virtual spools are not real DevAms objects (GetAmsById returns nullptr + // above), so the EXT_SPOOL branch from upstream is unnecessary here. + if (ams->GetAmsType() == DevAms::AMS || ams->GetAmsType() == DevAms::AMS_LITE || ams->GetAmsType() == DevAms::N3F) { + tray_id = ams_id_int * 4 + slot_id_int; + } else if (ams->GetAmsType() == DevAms::N3S) { + tray_id = ams_id_int + slot_id_int; + } else { + return wxString(); + } + + return wxGetApp().transition_tridid(tray_id); +} + +void FeedDirectionDialog::SetExtruderMapping(MachineObject* obj, + const std::string& currAmsId, + const std::string& currSlotId, + const std::vector>& extruderSlots) +{ + wxString filamentID = calcTrayName(obj, currAmsId, currSlotId); + if (filamentID.empty()) + return; + + m_filament_id = filamentID; + SetTitle(wxString::Format(_L("Load %s to "), filamentID)); + + std::vector extruderMapping(extruderSlots.size()); + for (size_t i = 0; i < extruderSlots.size(); ++i) { + if (!extruderSlots[i].first.empty()) + extruderMapping[i] = calcTrayName(obj, extruderSlots[i].first, extruderSlots[i].second); + } + + if (extruderMapping.size() > 1 && extruderMapping[1] == filamentID) //left extruder + { + m_leftRadio->Enable(false); + m_confirmBtn->Enable(false); + m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::EMPTY_LOAD); + m_extruderImage->setExtruderUsed("right"); + } + else if (!extruderMapping.empty() && extruderMapping[0] == filamentID) //right extruder + { + m_rightRadio->Enable(false); + m_confirmBtn->Enable(false); + m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::EMPTY_LOAD); + m_extruderImage->setExtruderUsed("left"); + } + else + { + m_radioHelper->SetValue(true); + m_lastChecked = m_radioHelper; + m_confirmBtn->Enable(false); + m_extruderImage->update(DevExtruderState::EMPTY_LOAD, DevExtruderState::EMPTY_LOAD); + m_extruderImage->setExtruderUsed(""); + } +} + }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Widgets/AMSItem.hpp b/src/slic3r/GUI/Widgets/AMSItem.hpp index 676fec6ec6..526fa82f36 100644 --- a/src/slic3r/GUI/Widgets/AMSItem.hpp +++ b/src/slic3r/GUI/Widgets/AMSItem.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #define AMS_CONTROL_BRAND_COLOUR wxColour(0, 150, 136) @@ -400,6 +401,26 @@ private: }; +// Orca: routing glyph shown on the AMS control when a Filament Track Switch is installed. +class SwitcherImage: public wxWindow +{ +public: + void setShowState(bool show_state) { m_show_state = show_state; }; + // void msw_rescale(); + void paintEvent(wxPaintEvent &evt); + + void render(wxDC &dc); + bool m_show_state = {false}; + wxColour m_colour; + ScalableBitmap m_switcher; + string m_file_name; + // bool m_ams_loading{ false }; + void doRender(wxDC &dc); + SwitcherImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition); + ~SwitcherImage(); +}; + + class AMSextruder : public wxWindow { private: @@ -771,6 +792,9 @@ public: void PlayRridLoading(wxString canid); void StopRridLoading(wxString canid); void msw_rescale(); + // Orca: hide/show the road segment below the item; used to drop the external-spool road + // when a Filament Track Switch is installed. Returns true if the visibility actually changed. + bool ShowRoad(bool show); void show_sn_value(bool show); void SetAmsStepExtra(wxString canid, AMSPassRoadType type, AMSPassRoadSTEP step); void SetAmsStep(std::string amsid, std::string canid, AMSPassRoadType type, AMSPassRoadSTEP step); @@ -849,6 +873,111 @@ wxDECLARE_EVENT(EVT_AMS_UNSELETED_AMS, wxCommandEvent); wxDECLARE_EVENT(EVT_VAMS_ON_FILAMENT_EDIT, wxCommandEvent); wxDECLARE_EVENT(EVT_AMS_SWITCH, SimpleEvent); +enum class DevExtruderState { + FILLED_LOAD, + FILLED_UNLOAD, + EMPTY_LOAD, + EMPTY_UNLOAD +}; + +class DevExtruderImage : public wxWindow +{ + ScalableBitmap *m_left_extruder_active_filled; + ScalableBitmap *m_left_extruder_active_empty; + ScalableBitmap *m_left_extruder_unactive_filled; + ScalableBitmap *m_left_extruder_unactive_empty; + ScalableBitmap *m_right_extruder_active_filled; + ScalableBitmap *m_right_extruder_active_empty; + ScalableBitmap *m_right_extruder_unactive_filled; + ScalableBitmap *m_right_extruder_unactive_empty; + + ScalableBitmap *m_extruder_single_nozzle_empty_load; + ScalableBitmap *m_extruder_single_nozzle_empty_unload; + ScalableBitmap *m_extruder_single_nozzle_filled_load; + ScalableBitmap *m_extruder_single_nozzle_filled_unload; + + DevExtruderState m_left_ext_state = {DevExtruderState::EMPTY_LOAD}; + DevExtruderState m_right_ext_state = {DevExtruderState::EMPTY_LOAD}; + DevExtruderState m_single_ext_state = {DevExtruderState::EMPTY_LOAD}; + +public: + DevExtruderImage(wxWindow *parent, wxWindowID id, + int extruder_num, + const wxPoint &pos = wxDefaultPosition, + const wxSize &size = wxDefaultSize); + ~DevExtruderImage() + { + + } + void update(DevExtruderState single_state) + { + m_single_ext_state = single_state; + } + void update(DevExtruderState left_state, DevExtruderState right_state) + { + m_left_ext_state = left_state; + m_right_ext_state = right_state; + } + + void msw_rescale(); + void setExtruderCount(int extruder_num) + { + m_extruder_num = extruder_num; + } + void setExtruderUsed(const std::string& loc) + { + if (current_extruder_loc == loc) { return; } + current_extruder_loc = loc; + Refresh(); + } +private: + void paintEvent(wxPaintEvent &evt) + { + wxPaintDC dc(this); + render(dc); + } + void render(wxDC &dc); + void doRender(wxDC &dc); + int m_extruder_num = 1; + std::string current_extruder_loc = ""; + +}; + +// Filament Track Switch: when the switch is installed and calibrated a filament can be routed to +// either extruder, so this dialog lets the user pick. GetExtruderID() returns the chosen extruder +// (1 = deputy/left, 0 = main/right) or nullopt if none was picked; that value is passed to +// MachineObject::command_ams_change_filament. Only constructed on the FTS-ready path. +class FeedDirectionDialog : public wxDialog +{ +public: + FeedDirectionDialog(wxWindow* parent, const int extruderNum, const std::string& printer_type = ""); + + std::optional GetExtruderID(); + + void SetExtruderMapping(MachineObject* obj, + const std::string& currAmsId, + const std::string& currSlotId, + const std::vector>& extruderSlots); + +private: + static wxString calcTrayName(MachineObject* obj, const std::string& amsID, const std::string& slotID); + + int m_extruder_num{}; + std::string m_printer_type; + wxString m_filament_id{}; + wxRadioButton* m_radioHelper{nullptr}; + wxRadioButton* m_leftRadio{nullptr}; + wxRadioButton* m_rightRadio{nullptr}; + wxRadioButton* m_lastChecked{nullptr}; + DevExtruderImage* m_extruderImage{nullptr}; + Button* m_confirmBtn{nullptr}; + std::optional m_load_extruder_id = std::nullopt; + + void OnConfirm(wxCommandEvent& event); + void OnRadioClicked(wxCommandEvent& evt); + +}; + }} // namespace Slic3r::GUI #endif // !slic3r_GUI_amscontrol_hpp_ diff --git a/src/slic3r/GUI/Widgets/FilamentLoad.cpp b/src/slic3r/GUI/Widgets/FilamentLoad.cpp index 502b153fef..2f2ef4dd92 100644 --- a/src/slic3r/GUI/Widgets/FilamentLoad.cpp +++ b/src/slic3r/GUI/Widgets/FilamentLoad.cpp @@ -3,6 +3,7 @@ #include "../BitmapCache.hpp" #include "../I18N.hpp" #include "../GUI_App.hpp" +#include "../DeviceCore/DevFilaSystem.h" #include #include @@ -37,6 +38,28 @@ FilamentLoad::FilamentLoad(wxWindow* parent, wxWindowID id, const wxPoint& pos, //FILAMENT_CHANGE_STEP_STRING[FilamentStep::STEP_FEED_FILAMENT] = _L("Feed Filament"); FILAMENT_CHANGE_STEP_STRING[FilamentStep::STEP_CONFIRM_EXTRUDED] = _L("Confirm extruded"); FILAMENT_CHANGE_STEP_STRING[FilamentStep::STEP_CHECK_POSITION] = _L("Check filament location"); + + // Orca: labels for the device-numbered steps carried by the AMS (ams.cfs). Overlapping steps + // reuse the wording above so the current-step highlight (driven by the legacy ams_status_sub + // path) still text-matches; the switch/hotend/cooling and Filament Track Switch steps are new. + // STEP_CHECK_POSITION and STEP_CONFIRM_EXTRUDED share code 0x08, so CONFIRM is assigned first + // and CHECK_POSITION last, leaving code 0x08 mapped to "Check filament location" as on device. + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_IDLE] = _L("Idling..."); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_PAUSE] = _L("Pause"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_HEAT_NOZZLE] = _L("Heat the nozzle"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_CUT_FILAMENT] = _L("Cut filament"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_PULL_CURR_FILAMENT] = _L("Pull back the current filament"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_PUSH_NEW_FILAMENT] = _L("Push new filament into extruder"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_GRAB_NEW_FILAMENT] = _L("Grab new filament"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_PURGE_OLD_FILAMENT] = _L("Purge old filament"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_SWITCH_EXTRUDER] = _L("Switch") + " " + _L("extruder"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_SWITCH_HOTEND] = _L("Switch") + " " + _L("hotend"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_AMS_FILA_COOLING] = _L("Wait for AMS cooling"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_PUSH_SWITCHER_FILA] = _L("Switch current filament at Filament Track Switch"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_PULL_SWITCHER_FILA] = _L("Pull back current filament at Filament Track Switch"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_SWITCHER_SWITCH] = _L("Switch track at Filament Track Switch"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_CONFIRM_EXTRUDED] = _L("Confirm extruded"); + DEV_FILAMENT_CHANGE_STEP_STRING[DevFilamentStep::STEP_CHECK_POSITION] = _L("Check filament location"); } void FilamentLoad::SetFilamentStep(FilamentStep item_idx, FilamentStepType f_type) @@ -124,11 +147,34 @@ void FilamentLoad::SetFilamentStep(FilamentStep item_idx, FilamentStepType f_typ step_control->SetSlotInformation(slot_info); } -void FilamentLoad::SetupSteps(bool has_fila_to_switch) { +void FilamentLoad::SetupSteps(MachineObject* obj_, bool has_fila_to_switch) { m_filament_load_steps->DeleteAllItems(); m_filament_unload_steps->DeleteAllItems(); m_filament_vt_load_steps->DeleteAllItems(); + // Orca: newer firmware sends the exact change-step sequence through the AMS (ams.cfs). When + // present, build the visible steps straight from that list and skip the hardcoded per-model + // sequences below. Inert on current firmware: the list is empty, so this branch is skipped and + // the legacy logic runs unchanged. + if (obj_ && obj_->GetFilaSystem() && !obj_->GetFilaSystem()->GetFilamentChangeSteps().empty()) { + const auto& steps = obj_->GetFilaSystem()->GetFilamentChangeSteps(); + for (auto step : steps) { + auto iter = DEV_FILAMENT_CHANGE_STEP_STRING.find(step); + if (iter == DEV_FILAMENT_CHANGE_STEP_STRING.end()) { + BOOST_LOG_TRIVIAL(error) << "Unknown filament change step: " << static_cast(step); + continue; + } + + m_filament_load_steps->AppendItem(iter->second); + m_filament_unload_steps->AppendItem(iter->second); + m_filament_vt_load_steps->AppendItem(iter->second); + } + + Layout(); + Fit(); + return; + } + if (m_ams_model == AMSModel::GENERIC_AMS || m_ext_model == AMSModel::N3F_AMS || m_ext_model == AMSModel::N3S_AMS) { if (has_fila_to_switch) { m_filament_load_steps->AppendItem(FILAMENT_CHANGE_STEP_STRING[FilamentStep::STEP_HEAT_NOZZLE]); diff --git a/src/slic3r/GUI/Widgets/FilamentLoad.hpp b/src/slic3r/GUI/Widgets/FilamentLoad.hpp index c358816b77..e3cc6daf67 100644 --- a/src/slic3r/GUI/Widgets/FilamentLoad.hpp +++ b/src/slic3r/GUI/Widgets/FilamentLoad.hpp @@ -35,6 +35,10 @@ protected: public: std::map FILAMENT_CHANGE_STEP_STRING; + // Labels for the device-numbered steps reported by the AMS (ams.cfs). Keyed by the device + // enum, separate from the legacy FilamentStep map above. Only used when the AMS provides a + // step list; empty AMS list falls back to the legacy sequences below. + std::map DEV_FILAMENT_CHANGE_STEP_STRING; AMSModel m_ams_model{ AMSModel::GENERIC_AMS }; AMSModel m_ext_model{ AMSModel::AMS_LITE }; AMSModel m_is_none_ams_mode{ AMSModel::AMS_LITE }; @@ -44,7 +48,7 @@ public: void SetFilamentStep(FilamentStep item_idx, FilamentStepType f_type); void ShowFilamentTip(bool hasams = true); - void SetupSteps(bool is_extrusion_exist); + void SetupSteps(MachineObject* obj_, bool is_extrusion_exist); void show_nofilament_mode(bool show); void updateID(int ams_id, int slot_id) { m_ams_id = ams_id; m_slot_id = slot_id; };