diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 969dcb693a..71d1c3eba0 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -494,6 +494,8 @@ set(SLIC3R_GUI_SOURCES GUI/Widgets/AMSControl.hpp GUI/Widgets/AMSItem.cpp GUI/Widgets/AMSItem.hpp + GUI/Widgets/MultiNozzleSync.cpp + GUI/Widgets/MultiNozzleSync.hpp GUI/Widgets/AxisCtrlButton.cpp GUI/Widgets/AxisCtrlButton.hpp GUI/SafetyOptionsDialog.hpp diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index ec7deeb82a..84709dd13b 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1024,7 +1024,8 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, const Transform3d& projection_matrix, const GUI::Size& cnv_size, std::function filter_func, - bool partly_inside_enable) const + bool partly_inside_enable, + std::vector * printable_heights) const { GLVolumeWithIdAndZList to_render = volumes_to_render(volumes, type, view_matrix, filter_func); if (to_render.empty()) @@ -1108,7 +1109,24 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, //use -1 ad a invalid type shader->set_uniform("print_volume.type", -1); } - + + // Per-extruder printable-height shading. The flag is set to + // 2.0 only for multi-extruder printers (two per-extruder heights); otherwise it is forced to 0.0 + // on every render so no stale flag survives a multi->single-extruder plate switch, keeping the + // shared gouraud shader pixel-identical for single-extruder printers. When active the height + // branch reads print_volume.xy_data (the bed rect), so set it explicitly here. + std::array extruder_printable_heights = {0.0f, 0.0f, 0.0f}; + if (printable_heights != nullptr && printable_heights->size() > 1) { + extruder_printable_heights[0] = 2.0f; + extruder_printable_heights[1] = static_cast((*printable_heights)[0]); + extruder_printable_heights[2] = static_cast((*printable_heights)[1]); + shader->set_uniform("extruder_printable_heights", extruder_printable_heights); + shader->set_uniform("print_volume.xy_data", m_print_volume.data); + } + else { + shader->set_uniform("extruder_printable_heights", extruder_printable_heights); + } + shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower); shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast())); diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index 76d10620d9..1217d32182 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -497,7 +497,10 @@ public: const Transform3d& projection_matrix, const GUI::Size& cnv_size, std::function filter_func = std::function(), - bool partly_inside_enable =true + bool partly_inside_enable =true, + // Per-extruder printable heights (extruder_printable_height); null / size<=1 + // leaves the shader's extruder_printable_heights flag at 0.0 (single-extruder = inert). + std::vector * printable_heights = nullptr ) const; // Clear the geometry diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 4a429a4a5a..f92114b53d 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -16,6 +16,9 @@ #include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevFilaBlackList.h" #include "DeviceCore/DevFilaSystem.h" +#include "DeviceCore/DevFilaSwitch.h" +#include "DeviceCore/DevNozzleSystem.h" // DevNozzle / GetNozzleByPosId / GetNozzleRack +#include "DeviceCore/DevNozzleRack.h" // DevNozzleRack::IsSupported (rack gating) #define FILAMENT_MAX_TEMP 300 #define FILAMENT_MIN_TEMP 120 @@ -531,56 +534,121 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) { Close(); } +// Nozzle-context builder for the AMS-edit blacklist check. Factors the blacklist evaluation out of +// on_select_ok and threads rack-gated per-nozzle context (extruder id + nozzle flow + nozzle diameter) +// into CheckFilamentInfo, returning the accumulate-all CheckResult. As a side effect it resolves +// ams_filament_id/ams_setting_id from the matched preset. fila_name is kept as the preset name (not +// the short alias) so the name / name_suffix rules match on the non-rack fleet (no unintended +// activation of e.g. the PLA Glow rule). +static DevFilaBlacklist::CheckResult +sCheckFilamentInfo(PresetBundle* preset_bundle, + MachineObject* obj, + int ams_id, + int slot_id, + const std::string& filament_id, + std::string& ams_filament_id, + std::string& ams_setting_id) +{ + DevFilaBlacklist::CheckResult result; + if (!preset_bundle || !obj) + return result; + + // Orca: there is no lookup struct that also carries setting_id, so resolve the (root) preset by + // scanning the filaments collection for a matching filament_id. + Preset* fila_preset = nullptr; + for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); ++it) { + if (it->filament_id == filament_id) { + fila_preset = &(*it); + break; + } + } + if (!fila_preset) + return result; + + if (wxGetApp().app_config->get("skip_ams_blacklist_check") != "true") { + std::string filamnt_type; + fila_preset->get_filament_type(filamnt_type); + + DevFilaBlacklist::CheckFilamentInfo check_info; + check_info.dev_id = obj->get_dev_id(); + check_info.model_id = obj->printer_type; + check_info.fila_id = fila_preset->filament_id; + check_info.fila_type = filamnt_type; + check_info.fila_name = fila_preset->name; + check_info.has_filament_switch = obj->GetFilaSwitch()->IsInstalled(); + check_info.ams_id = ams_id; + check_info.slot_id = slot_id; + + if (auto vendor = dynamic_cast(fila_preset->config.option("filament_vendor")); + vendor && !vendor->values.empty()) { + check_info.fila_vendor = vendor->values[0]; + } + + check_info.extruder_id = obj->GetFilaSystem()->GetExtruderIdByAmsId(std::to_string(ams_id)); + + // Rack-gated per-nozzle context. For a multi-nozzle main extruder on a rack printer (H2C) the + // specific nozzle is resolved later by the print-dispatch rack mapping, so leave + // nozzle_flow/nozzle_diameter unset here to keep the high-flow nozzle rules dormant. Every + // non-rack printer takes the else branch, threading the reported nozzle flow + diameter and + // thereby activating the high-flow blacklist warnings in the AMS-edit dialog for H2D/H2S/X2D/P2S + // (and the E3D-kit X1C/P1x). + DevNozzleSystem* nozzle_system = obj->GetNozzleSystem(); + const bool rack_supported = nozzle_system && nozzle_system->GetNozzleRack() && + nozzle_system->GetNozzleRack()->IsSupported(); + if (check_info.extruder_id == MAIN_EXTRUDER_ID && rack_supported) { + ; // multi-nozzle main extruder on a rack printer — nozzle resolved later by print dispatch + } else { + check_info.nozzle_flow = obj->GetFilaSystem()->GetNozzleFlowStringByAmsId(std::to_string(ams_id)); + if (nozzle_system) { + DevNozzle nozzle = nozzle_system->GetNozzleByPosId(check_info.extruder_id.value_or(-1)); + if (!nozzle.IsEmpty()) + check_info.nozzle_diameter = nozzle.GetNozzleDiameter(); + } + } + + result = DevFilaBlacklist::check_filaments_in_blacklist(check_info); + } + + ams_filament_id = fila_preset->filament_id; + ams_setting_id = fila_preset->setting_id; + return result; +} + void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event) { + if (!obj) + return; + //get filament id ams_filament_id = ""; ams_setting_id = ""; + // the combobox item + auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()]; + + // check filament info (rack-gated per-nozzle blacklist evaluation) PresetBundle* preset_bundle = wxGetApp().preset_bundle; - if (preset_bundle) { - for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { + const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id, + filament_item.filament_id, ams_filament_id, ams_setting_id); - auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()]; - std::string filament_id = filament_item.filament_id; - if (it->filament_id.compare(filament_id) == 0) { + if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) { + wxString info_msg; + for (const auto& item : prohibit_items) { info_msg += item.info_msg + "\n"; } + MessageDialog msg_wingow(nullptr, info_msg, _L("Error"), wxICON_WARNING | wxOK); + msg_wingow.ShowModal(); + return; + } - - //check is it in the filament blacklist - if (wxGetApp().app_config->get("skip_ams_blacklist_check") != "true") { - bool in_blacklist = false; - std::string action; - wxString info; - std::string filamnt_type; - std::string filamnt_name; - it->get_filament_type(filamnt_type); - - auto vendor = dynamic_cast(it->config.option("filament_vendor")); - - if (vendor && (vendor->values.size() > 0)) { - std::string vendor_name = vendor->values[0]; - DevFilaBlacklist::check_filaments_in_blacklist(obj->printer_type, vendor_name, filamnt_type, it->filament_id, ams_id, slot_id, it->name, in_blacklist, action, info); - } - - if (in_blacklist) { - if (action == "prohibition") { - MessageDialog msg_wingow(nullptr, info, _L("Error"), wxICON_WARNING | wxOK); - msg_wingow.ShowModal(); - //m_comboBox_filament->SetSelection(m_filament_selection); - return; - } - else if (action == "warning") { - MessageDialog msg_wingow(nullptr, info, _L("Warning"), wxICON_INFORMATION | wxOK); - msg_wingow.ShowModal(); - } - } - } - - ams_filament_id = it->filament_id; - ams_setting_id = it->setting_id; - break; - } + if (const auto& warning_items = fila_check_res.get_items_by_action("warning"); !warning_items.empty()) { + std::vector infos; + for (const auto& item : warning_items) { + FilamentWarningInfo winfo; + winfo.info_msg = item.info_msg; + winfo.wiki_url = item.wiki_url; + infos.emplace_back(winfo); } + FilamentWarningDialog msg_window(nullptr, _L("Warning"), infos); + msg_window.ShowModal(); } wxString nozzle_temp_min = m_input_nozzle_min->GetTextCtrl()->GetValue(); diff --git a/src/slic3r/GUI/AmsMappingPopup.cpp b/src/slic3r/GUI/AmsMappingPopup.cpp index cf417498d5..241c2799a7 100644 --- a/src/slic3r/GUI/AmsMappingPopup.cpp +++ b/src/slic3r/GUI/AmsMappingPopup.cpp @@ -24,6 +24,9 @@ #include "BindDialog.hpp" #include "DeviceCore/DevFilaSystem.h" +#include "DeviceCore/DevNozzleRack.h" +#include "DeviceCore/DevMappingNozzle.h" +#include "DeviceTab/wgtDeviceNozzleSelect.h" namespace Slic3r { namespace GUI { #define MATERIAL_ITEM_SIZE wxSize(FromDIP(65), FromDIP(50)) @@ -64,6 +67,7 @@ static void _add_containers(const AmsMapingPopup * win, //m_ams_wheel_mitem = ScalableBitmap(this, "ams_wheel", FromDIP(25)); m_ams_wheel_mitem = ScalableBitmap(this, "ams_wheel_narrow", 25); m_ams_not_match = ScalableBitmap(this, "filament_not_mactch", 25); + m_rack_nozzle_bitmap = ScalableBitmap(this, "dev_rack_nozzle_print_job", 22); m_material_coloul = mcolour; m_material_name = mname; @@ -73,9 +77,7 @@ static void _add_containers(const AmsMapingPopup * win, SetDoubleBuffered(true); #endif //__WINDOWS__ - SetSize(MATERIAL_ITEM_SIZE); - SetMinSize(MATERIAL_ITEM_SIZE); - SetMaxSize(MATERIAL_ITEM_SIZE); + messure_size(); SetBackgroundColour(*wxWHITE); Bind(wxEVT_PAINT, &MaterialItem::paintEvent, this); @@ -89,6 +91,7 @@ void MaterialItem::msw_rescale() { m_arraw_bitmap_white.msw_rescale(); m_transparent_mitem.msw_rescale(); m_ams_wheel_mitem.msw_rescale(); + m_rack_nozzle_bitmap.msw_rescale(); } void MaterialItem::allow_paint_dropdown(bool flag) { @@ -114,11 +117,22 @@ void MaterialItem::set_ams_info(wxColour col, wxString txt, int ctype, std::vect BOOST_LOG_TRIVIAL(info) << "set_ams_info " << m_ams_name; } +void MaterialItem::set_nozzle_info(const wxString& mapped_nozzle_str) +{ + if (m_mapped_nozzle_str != mapped_nozzle_str) { + m_mapped_nozzle_str = mapped_nozzle_str; + messure_size(); // a mapped nozzle adds/removes a row, changing the card height + Refresh(); + } +} + void MaterialItem::reset_ams_info() { m_ams_name = "-"; m_ams_coloul = wxColour(0xCE, 0xCE, 0xCE); m_ams_cols.clear(); m_ams_ctype = 0; + m_mapped_nozzle_str.Clear(); + messure_size(); Refresh(); } @@ -215,6 +229,11 @@ void MaterialItem::render(wxDC &dc) acolor = wxColour(0x90, 0x90, 0x90); } + // Header + AMS name keep their fixed-card positions; a mapped nozzle only adds a row below, so + // position this text against the base card height, not the grown one (else it slides down onto + // the nozzle row). Identical to GetSize().y when no nozzle row is present. + const float base_y = m_mapped_nozzle_str.IsEmpty() ? (float) GetSize().y : (float) MATERIAL_ITEM_SIZE.y; + // materials name dc.SetFont(::Label::Body_13); @@ -227,7 +246,7 @@ void MaterialItem::render(wxDC &dc) } auto material_txt_size = dc.GetTextExtent(m_material_name); - dc.DrawText(m_material_name, wxPoint((GetSize().x - material_txt_size.x) / 2, ((float)GetSize().y * 2 / 5 - material_txt_size.y) / 2)); + dc.DrawText(m_material_name, wxPoint((GetSize().x - material_txt_size.x) / 2, (base_y * 2 / 5 - material_txt_size.y) / 2)); @@ -238,11 +257,11 @@ void MaterialItem::render(wxDC &dc) if (mapping_txt.size() >= 4) { mapping_txt.insert(mapping_txt.size() / 2, "\n"); mapping_txt_size = dc.GetTextExtent(mapping_txt); - m_text_pos_y = ((float) GetSize().y * 3 / 5 - mapping_txt_size.y) / 2 + (float) GetSize().y * 2 / 5 - mapping_txt_size.y / 2; + m_text_pos_y = (base_y * 3 / 5 - mapping_txt_size.y) / 2 + base_y * 2 / 5 - mapping_txt_size.y / 2; m_text_pos_x = mapping_txt_size.x / 4; } else { mapping_txt_size = dc.GetTextExtent(mapping_txt); - m_text_pos_y = ((float) GetSize().y * 3 / 5 - mapping_txt_size.y) / 2 + (float) GetSize().y * 2 / 5; + m_text_pos_y = (base_y * 3 / 5 - mapping_txt_size.y) / 2 + base_y * 2 / 5; m_text_pos_x = 0; } @@ -258,12 +277,73 @@ void MaterialItem::match(bool mat) Refresh(); } +// A filament can map to several rack nozzles; the joined "R1, R2, ..." string is wrapped to a few +// slots per line so the label stays readable inside the narrow card. +static constexpr size_t MAPPED_NOZZLE_STR_MAX_CHARS_PER_LINE = 7; + +static wxString s_wrap_mapped_nozzle_str(const wxString& text) +{ + if (text.length() <= MAPPED_NOZZLE_STR_MAX_CHARS_PER_LINE) + return text; + + wxArrayString parts = wxSplit(text, ','); + wxString wrapped; + std::vector buffers; + for (auto& part : parts) { + if (!part.empty()) + buffers.push_back(part.Trim(false).Trim(true)); + + if (buffers.size() > 2) { + if (!wrapped.empty()) + wrapped += "\n"; + wrapped += buffers[0] + " " + buffers[1] + " " + buffers[2]; + buffers.clear(); + } + } + + if (!wrapped.empty()) + wrapped += "\n"; + if (buffers.size() > 0) + wrapped += buffers[0]; + if (buffers.size() > 1) + wrapped += " " + buffers[1]; + + return wrapped; +} + +static int s_get_mapped_nozzle_str_line_count(const wxString& text) +{ + if (text.empty()) + return 0; + return std::max(1, static_cast(wxSplit(s_wrap_mapped_nozzle_str(text), '\n', '\0').size())); +} + +void MaterialItem::messure_size() +{ + // Without a mapped nozzle the card is a fixed swatch + AMS wheel; a mapped nozzle adds a row + // below (glyph + slot label), growing the height by one text line per wrapped line. + if (m_mapped_nozzle_str.empty()) { + SetSize(MATERIAL_ITEM_SIZE); + SetMinSize(MATERIAL_ITEM_SIZE); + SetMaxSize(MATERIAL_ITEM_SIZE); + } else { + const int line_count = s_get_mapped_nozzle_str_line_count(m_mapped_nozzle_str); + const wxSize item_size(MATERIAL_ITEM_SIZE.x, FromDIP(84 + std::max(0, line_count - 1) * 10)); + SetSize(item_size); + SetMinSize(item_size); + SetMaxSize(item_size); + } +} + void MaterialItem::doRender(wxDC& dc) { wxSize size = GetSize(); auto mcolor = m_material_coloul; auto acolor = m_ams_coloul; change_the_opacity(acolor); + // Header + wheel keep their fixed-card geometry; when a nozzle row is present the extra height + // below is used for it, so lay out the top of the card against the base height, not the grown one. + const int base_y = m_mapped_nozzle_str.IsEmpty() ? size.y : MATERIAL_ITEM_SIZE.y; if (mcolor.Alpha() == 0) { dc.DrawBitmap(m_transparent_mitem.bmp(), FromDIP(1), FromDIP(1)); @@ -288,7 +368,7 @@ void MaterialItem::doRender(wxDC& dc) //bottom rectangle in wheel bitmap, size is MATERIAL_REC_WHEEL_SIZE(22) auto left = (size.x / 2 - MATERIAL_REC_WHEEL_SIZE.x) / 2 + FromDIP(3); - auto up = (size.y * 0.4 + (size.y * 0.6 - MATERIAL_REC_WHEEL_SIZE.y) / 2); + auto up = (base_y * 0.4 + (base_y * 0.6 - MATERIAL_REC_WHEEL_SIZE.y) / 2); auto right = left + MATERIAL_REC_WHEEL_SIZE.x - FromDIP(3); dc.SetPen(*wxTRANSPARENT_PEN); //bottom @@ -350,12 +430,12 @@ void MaterialItem::doRender(wxDC& dc) } dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRoundedRectangle(FromDIP(0), FromDIP(0), MATERIAL_ITEM_SIZE.x - FromDIP(0), MATERIAL_ITEM_SIZE.y - FromDIP(0), 5); + dc.DrawRoundedRectangle(FromDIP(0), FromDIP(0), MATERIAL_ITEM_SIZE.x - FromDIP(0), size.y - FromDIP(0), 5); if (m_selected) { dc.SetPen(wxPen(AMS_CONTROL_BRAND_COLOUR, FromDIP(2))); dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRoundedRectangle(FromDIP(1), FromDIP(1), MATERIAL_ITEM_SIZE.x - FromDIP(1), MATERIAL_ITEM_SIZE.y - FromDIP(1), 5); + dc.DrawRoundedRectangle(FromDIP(1), FromDIP(1), MATERIAL_ITEM_SIZE.x - FromDIP(1), size.y - FromDIP(1), 5); } //#endif if (m_text_pos_y > 0 && m_match) { @@ -368,7 +448,7 @@ void MaterialItem::doRender(wxDC& dc) } auto wheel_left = (GetSize().x / 2 - m_ams_wheel_mitem.GetBmpSize().x) / 2 + FromDIP(2); - auto wheel_top = ((float)GetSize().y * 0.6 - m_ams_wheel_mitem.GetBmpSize().y) / 2 + (float)GetSize().y * 0.4; + auto wheel_top = ((float)base_y * 0.6 - m_ams_wheel_mitem.GetBmpSize().y) / 2 + (float)base_y * 0.4; if (!m_match) { wheel_left += m_ams_wheel_mitem.GetBmpSize().x; @@ -380,6 +460,29 @@ void MaterialItem::doRender(wxDC& dc) dc.DrawBitmap(m_ams_wheel_mitem.bmp(), wheel_left - FromDIP(LEFT_OFFSET), wheel_top); } } + + // Rack / filament-switcher printers: draw the physical nozzle the print-dispatch mapping assigned + // to this filament — a divider, the rack-nozzle glyph, and the slot label ("R1", "L R", ...) in + // the extra row below the wheel. Nothing is drawn when no nozzle is mapped (all other printers). + if (!m_mapped_nozzle_str.IsEmpty()) { + int row_top = wheel_top + m_ams_wheel_mitem.GetBmpSize().y + FromDIP(4); + dc.SetPen(wxPen(wxColour("#CECECE"), 1, wxPENSTYLE_SHORT_DASH)); // Orca: WXCOLOUR_GREY400 macro not in scope here + dc.DrawLine(FromDIP(1), row_top, FromDIP(size.x), row_top); + + int bitmap_y = row_top + (size.y - row_top - m_rack_nozzle_bitmap.GetBmpHeight()) / 2; + int bitmap_l = wheel_left + (m_ams_wheel_mitem.GetBmpWidth() - m_rack_nozzle_bitmap.GetBmpWidth()) / 2 + FromDIP(2); + dc.DrawBitmap(m_rack_nozzle_bitmap.bmp(), bitmap_l, bitmap_y); + + // Orca: BBS width-fits this label via a get_suitable_font_size overload Orca lacks; the wrapped + // slot string is short, so a fixed label font renders it cleanly without that helper. + const wxString wrapped = s_wrap_mapped_nozzle_str(m_mapped_nozzle_str); + dc.SetFont(::Label::Head_12); + dc.SetTextForeground(StateColor::darkModeColorFor(wxColour(0x26, 0x2E, 0x30))); + const wxSize text_size = dc.GetMultiLineTextExtent(wrapped); + int text_x = (size.x + bitmap_l + m_rack_nozzle_bitmap.GetBmpWidth() - text_size.x) / 2; + int text_y = row_top + (size.y - row_top - text_size.y) / 2; + dc.DrawText(wrapped, text_x, text_y); + } } void MaterialItem::reset_valid_info() { @@ -725,6 +828,16 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) : m_right_first_text_panel->SetMaxSize(wxSize(-1, FromDIP(same_height))); m_sizer_ams_right->Add(m_right_first_text_panel, 0, wxEXPAND | wxBOTTOM | wxTOP, FromDIP(8)); + + // Flush-waste warning (rack printers only). Orca: uses a plain Label here. + // Hidden by default => zero layout effect for non-rack printers. + m_flush_warning_panel = new Label(m_right_marea_panel); + m_flush_warning_panel->SetFont(::Label::Body_12); + m_flush_warning_panel->SetForegroundColour(StateColor::darkModeColorFor("#F09A17")); + m_flush_warning_panel->SetBackgroundColour(StateColor::darkModeColorFor("#FFFFE0")); + m_flush_warning_panel->Show(false); + m_sizer_ams_right->Add(m_flush_warning_panel, 0, wxBOTTOM | wxALIGN_LEFT, FromDIP(8)); + m_right_split_ams_sizer = create_split_sizer(m_right_marea_panel, _L("Right AMS")); m_sizer_ams_right->Add(m_right_split_ams_sizer, 0, wxEXPAND, 0); m_sizer_ams_right->Add(m_sizer_ams_basket_right, 0, wxEXPAND|wxTOP, FromDIP(8)); @@ -740,6 +853,14 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) : m_sizer_ams->Add(0, 0, 0, wxEXPAND, FromDIP(15)); m_sizer_ams->Add(m_right_marea_panel, 1, wxEXPAND, FromDIP(0)); + // Rack nozzle manual-pick, alongside the AMS area (rack printers only). Hidden by default + // => contributes nothing to layout for non-rack printers. + m_rack_nozzle_select = new wgtDeviceNozzleRackSelect(this); + m_rack_nozzle_select->Bind(EVT_NOZZLE_SELECT_CHANGED, &AmsMapingPopup::OnNozzleMappingSelected, this); + m_rack_nozzle_select->Bind(EVT_NOZZLE_SELECT_CLICKED, [this](wxCommandEvent& e) { this->Dismiss(); }); + m_rack_nozzle_select->Show(false); + m_sizer_ams->Add(m_rack_nozzle_select, 0, wxEXPAND | wxTOP | wxLEFT, FromDIP(15)); + m_sizer_main->Add(title_panel, 0, wxEXPAND | wxALL, FromDIP(2)); m_sizer_main->Add(m_sizer_ams, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(14)); m_sizer_main->Add( 0, 0, 0, wxTOP, FromDIP(14)); @@ -782,6 +903,7 @@ void AmsMapingPopup::msw_rescale() m_right_extra_slot->msw_rescale(); for (auto item : m_mapping_item_list) { item->msw_rescale(); } for (auto container : m_amsmapping_container_list) { container->msw_rescale(); } + if (m_rack_nozzle_select) { m_rack_nozzle_select->Rescale(); } Fit(); Refresh(); @@ -1038,7 +1160,7 @@ void AmsMapingPopup::update_items_check_state(const std::vector& a } } -void AmsMapingPopup::update(MachineObject* obj, const std::vector& ams_mapping_result) +void AmsMapingPopup::update(MachineObject* obj, const std::vector& ams_mapping_result, bool use_dynamic_switch, std::optional print_type) { //BOOST_LOG_TRIVIAL(info) << "ams_mapping nozzle count " << obj->get_extder_system()->nozzle.size(); BOOST_LOG_TRIVIAL(info) << "ams_mapping total count " << obj->GetFilaSystem()->GetAmsCount(); @@ -1252,11 +1374,68 @@ void AmsMapingPopup::update(MachineObject* obj, const std::vector& } else { m_right_split_ams_sizer->Show(false); } + + /*rack (manual nozzle pick; rack printers only)*/ + update_rack_select(obj, use_dynamic_switch, print_type); + update_flush_waste(obj); + Layout(); Fit(); Refresh(); } +void AmsMapingPopup::update_rack_select(MachineObject* obj, bool use_dynamic_switch, std::optional print_from_type) +{ + // Orca: MachineObject has no GetNozzleRack() convenience; route through the nozzle system instead. + m_rack = obj ? obj->GetNozzleSystem()->GetNozzleRack() : nullptr; + + bool show_rack_select_area = false; + if (!m_mapping_from_multi_machines && !m_use_in_sync_dialog && + obj && obj->GetNozzleSystem()->GetNozzleRack()->IsSupported() && + print_from_type.has_value() && print_from_type.value() == PrintFromType::FROM_NORMAL) { + const auto& nozzle_pos_vec = obj->get_nozzle_mapping_result()->GetMappedNozzlePosVecByFilaId(m_current_filament_id); + m_rack_nozzle_select->UpdatSelectedNozzles(obj->GetNozzleSystem()->GetNozzleRack(), nozzle_pos_vec, use_dynamic_switch, print_from_type); + show_rack_select_area = true; + } + + if (show_rack_select_area != m_rack_nozzle_select->IsShown()) { + m_right_tip_text = show_rack_select_area ? _L("Select Filament && Hotends") : _L("Select Filament"); + m_right_tips->SetLabel(m_right_tip_text); + m_rack_nozzle_select->Show(show_rack_select_area); + Layout(); + Fit(); + } +} + +void AmsMapingPopup::update_flush_waste(MachineObject* obj) +{ + if (!obj) { + m_flush_warning_panel->Hide(); + return; + }; + + float flush_waste_base = obj->get_nozzle_mapping_result()->GetFlushWeightBase(); + float flush_waste_current = obj->get_nozzle_mapping_result()->GetFlushWeightCurrent(); + if ((flush_waste_base != -1) && (flush_waste_current != -1) && flush_waste_current > flush_waste_base) { + m_flush_warning_panel->SetLabel(wxString::Format(_L("Printing with the current nozzle may produce an extra %0.2f g of waste."), flush_waste_current - flush_waste_base)); + m_flush_warning_panel->Show(); + } else { + m_flush_warning_panel->Hide(); + } +} + +void AmsMapingPopup::OnNozzleMappingSelected(wxCommandEvent& evt) +{ + if (auto ptr = m_rack.lock()) { + MachineObject* obj = ptr->GetNozzleSystem()->GetOwner(); + obj->get_nozzle_mapping_result()->SetManualNozzleMappingByFila(m_current_filament_id, m_rack_nozzle_select->GetSelectedNozzlePosID()); + update_flush_waste(obj); + } + + evt.Skip(); + Dismiss(); +} + std::vector AmsMapingPopup::parse_ams_mapping(const std::map& amsList) { std::vector m_tray_data; @@ -2305,12 +2484,25 @@ _GetBackupStatus(unsigned int fila_back_group) for (int j = 16; j < 32; j++)/* single ams is from 128*/ { + // N9 / AMS-Lite-mixed reports its four backup slots in bits 24-27, which decode directly to + // tray ids 24-27 (handled in the loop below). Keep those bits out of the single-ams (128+) + // range; all other bits keep their existing mapping unchanged. + if (j >= 24 && j <= 27) + continue; if (fila_back_group & (1 << j)) { trayid_group[128 + j - 16] = true; } } + for (int i = 24; i <= 27; i++)/* ams lite for N9 */ + { + if (fila_back_group & (1 << i)) + { + trayid_group[i] = true; + } + } + return trayid_group; } diff --git a/src/slic3r/GUI/AmsMappingPopup.hpp b/src/slic3r/GUI/AmsMappingPopup.hpp index bbfb967822..c264dadcbf 100644 --- a/src/slic3r/GUI/AmsMappingPopup.hpp +++ b/src/slic3r/GUI/AmsMappingPopup.hpp @@ -41,8 +41,15 @@ #include "slic3r/GUI/DeviceCore/DevUtil.h" +#include + #define MAPPING_ITEM_INVALID_REMAIN -1 +namespace Slic3r { +class DevNozzleRack; +namespace GUI { class wgtDeviceNozzleRackSelect; } +} + namespace Slic3r { namespace GUI { @@ -93,6 +100,9 @@ public: //info wxColour m_ams_coloul; wxString m_ams_name; + // Physical nozzle(s) the print-dispatch mapping assigned to this filament (e.g. "R1", "L R"). + // Empty on printers without a nozzle rack or filament switcher; when set, the card grows a row. + wxString m_mapped_nozzle_str; int m_ams_ctype = 0; std::vector m_ams_cols = std::vector(); //reset @@ -107,6 +117,7 @@ public: ScalableBitmap m_filament_wheel_transparent; ScalableBitmap m_ams_wheel_mitem; ScalableBitmap m_ams_not_match; + ScalableBitmap m_rack_nozzle_bitmap; bool m_selected {false}; bool m_warning{false}; @@ -117,6 +128,10 @@ public: void allow_paint_dropdown(bool flag); void set_ams_info(wxColour col, wxString txt, int ctype=0, std::vector cols= std::vector(),bool record_back_info = false); void reset_ams_info(); + // Set the mapped-nozzle label ("R1", "L R", ...); grows/shrinks the card as the row appears/clears. + void set_nozzle_info(const wxString& mapped_nozzle_str); + // Size the card: base swatch height, plus one row when a nozzle label is present. + void messure_size(); void disable(); void enable(); @@ -259,12 +274,19 @@ public: wxBoxSizer* m_sizer_split_ams_right; bool m_mapping_from_multi_machines {false}; + // Rack nozzle manual-pick (rack printers only; hidden by default via Show(false)). + wgtDeviceNozzleRackSelect* m_rack_nozzle_select{nullptr}; + Label* m_flush_warning_panel{nullptr}; // Orca: uses a plain Label for the flush warning + std::weak_ptr m_rack; + void set_sizer_title(wxBoxSizer *sizer, wxString text); wxBoxSizer* create_split_sizer(wxWindow* parent, wxString text); void set_send_win(wxWindow* win) {send_win = win;}; void update_materials_list(std::vector list); void set_tag_texture(std::string texture); - void update(MachineObject* obj, const std::vector& ams_mapping_result); + void update(MachineObject* obj, const std::vector& ams_mapping_result, bool use_dynamic_switch = false, std::optional print_type = std::nullopt); + void update_rack_select(MachineObject* obj, bool use_dynamic_switch, std::optional print_type); + void update_flush_waste(MachineObject* obj); void update_title(MachineObject* obj); void update_items_check_state(const std::vector& ams_mapping_result); void update_ams_data_multi_machines(); @@ -295,6 +317,8 @@ public: void EnableExtMappingFilaTypeCheck(bool to_check = true) { m_ext_mapping_filatype_check = to_check;} ; private: + void OnNozzleMappingSelected(wxCommandEvent& evt); + ResetCallback m_reset_callback{nullptr}; std::string m_material_index; bool m_only_show_ext_spool{false}; diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 46ff3afff9..8c60b136b9 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -241,6 +241,9 @@ void BackgroundSlicingProcess::process_fff() //BBS: add plate index into render params m_temp_output_path = this->get_current_plate()->get_tmp_gcode_path(); m_fff_print->export_gcode(m_temp_output_path, m_gcode_result, [this](const ThumbnailsParams& params) { return this->render_thumbnails(params); }); + // Orca: BBL printers post-process the g-code in place here and never re-parse it into a fresh + // GCodeProcessorResult, so m_gcode_result->nozzle_group_result (consumed by the H2C print-dispatch + // nozzle mapping) survives post-processing. No preservation guard is needed on this path. if(m_fff_print->is_BBL_printer()) run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config()); diff --git a/src/slic3r/GUI/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp index 8ed0305369..2e7f2caf97 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.cpp +++ b/src/slic3r/GUI/CaliHistoryDialog.cpp @@ -13,6 +13,9 @@ #include "Plater.hpp" #include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevManager.h" +#include "DeviceCore/DevConfigUtil.h" +#include "DeviceCore/DevNozzleSystem.h" +#include "DeviceCore/DevNozzleRack.h" namespace Slic3r { namespace GUI { @@ -26,6 +29,7 @@ namespace GUI { enum CaliColumnType : int { Cali_Name = 0, Cali_Filament, + Cali_Nozzle_ID, Cali_Nozzle, Cali_K_Value, Cali_Delete, @@ -33,6 +37,9 @@ enum CaliColumnType : int { Cali_Type_Count }; +// Orca: derive nozzle-volume support from the machine preset's nozzle_volume array +// rather than a live device-capability query, to avoid flipping the Nozzle-Flow column +// visibility on shipping printers (H2D/X1/P1/A1). bool support_nozzle_volume(const MachineObject* obj) { if (!obj) @@ -49,14 +56,17 @@ bool support_nozzle_volume(const MachineObject* obj) return false; } -int get_colume_idx(CaliColumnType type, MachineObject* obj) +// Rack hotend position code -> label: 0 = fixed ("R"); >=0x10 = rack slot number; else N/A. +static wxString nozzle_id_code_to_string(int code) { - if (!support_nozzle_volume(obj) - && (type > CaliColumnType::Cali_Nozzle)) { - return type - 1; + if (code == 0) { + return "R"; + } else if (code >= 0x10) { + return wxString::Format("%d", code - 0x10 + 1); + } else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "Nozzle position id is -1 or invalid."; + return "N/A"; } - - return type; } static wxString get_preset_name_by_filament_id(std::string filament_id) @@ -244,6 +254,10 @@ void HistoryWindow::on_device_connected(MachineObject* obj) } m_comboBox_nozzle_dia->SetSelection(selection); + m_extruder_switch_btn->SetLabels( + _L(DevPrinterConfigUtil::get_toolhead_display_name(obj->printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase)), + _L(DevPrinterConfigUtil::get_toolhead_display_name(obj->printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); + if (obj->is_multi_extruders()) m_extruder_switch_btn->Show(); else @@ -320,7 +334,21 @@ void HistoryWindow::enbale_action_buttons(bool enable) { } } +// The main extruder can carry a nozzle rack (H2C); the Nozzle-ID column only shows for it. +bool HistoryWindow::support_nozzle_id_column() +{ + if (!curr_obj || get_extruder_id() != MAIN_EXTRUDER_ID) + return false; + auto ns = curr_obj->GetNozzleSystem(); + if (!ns) + return false; + auto rack = ns->GetNozzleRack(); + return rack && rack->IsSupported(); +} + void HistoryWindow::sync_history_data() { + int column_idx = 0; + Freeze(); m_history_data_panel->DestroyChildren(); m_history_data_panel->Enable(); @@ -331,24 +359,35 @@ void HistoryWindow::sync_history_data() { m_history_data_panel->SetSizer(gbSizer, true); + const bool has_nozzle_id = support_nozzle_id_column(); + auto title_name = new Label(m_history_data_panel, _L("Name")); title_name->SetFont(Label::Head_14); - gbSizer->Add(title_name, {0, get_colume_idx(CaliColumnType::Cali_Name, curr_obj) }, {1, 1}, wxBOTTOM, FromDIP(15)); - BOOST_LOG_TRIVIAL(info) << "=====================" << title_name->GetLabelText().ToStdString(); + gbSizer->Add(title_name, {0, column_idx++ }, {1, 1}, wxBOTTOM, FromDIP(15)); auto title_preset_name = new Label(m_history_data_panel, _L("Filament")); title_preset_name->SetFont(Label::Head_14); - gbSizer->Add(title_preset_name, { 0, get_colume_idx(CaliColumnType::Cali_Filament, curr_obj) }, { 1, 1 }, wxBOTTOM, FromDIP(15)); + gbSizer->Add(title_preset_name, { 0, column_idx++ }, { 1, 1 }, wxBOTTOM, FromDIP(15)); + + if (has_nozzle_id) { + auto nozzle_id_name = new Label(m_history_data_panel, _L("Nozzle ID")); + nozzle_id_name->SetFont(Label::Head_14); + { + wxString chd_ext_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(curr_obj->printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::LowerCase)); + nozzle_id_name->SetToolTip(wxString::Format(_L("Note: The hotend number on the %s is tied to the holder. When the hotend is moved to a new holder, its number will update automatically."), chd_ext_name)); + } + gbSizer->Add(nozzle_id_name, {0, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); + } if (support_nozzle_volume(curr_obj)) { auto nozzle_name = new Label(m_history_data_panel, _L("Nozzle Flow")); nozzle_name->SetFont(Label::Head_14); - gbSizer->Add(nozzle_name, {0, get_colume_idx(CaliColumnType::Cali_Nozzle, curr_obj)}, {1, 1}, wxBOTTOM, FromDIP(15)); + gbSizer->Add(nozzle_name, {0, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); } auto title_k = new Label(m_history_data_panel, _L("Factor K")); title_k->SetFont(Label::Head_14); - gbSizer->Add(title_k, { 0, get_colume_idx(CaliColumnType::Cali_K_Value,curr_obj) }, { 1, 1 }, wxBOTTOM, FromDIP(15)); + gbSizer->Add(title_k, { 0, column_idx++ }, { 1, 1 }, wxBOTTOM, FromDIP(15)); // Hide //auto title_n = new Label(m_history_data_panel, wxID_ANY, _L("N")); @@ -357,7 +396,12 @@ void HistoryWindow::sync_history_data() { auto title_action = new Label(m_history_data_panel, _L("Action")); title_action->SetFont(Label::Head_14); - gbSizer->Add(title_action, {0, get_colume_idx(CaliColumnType::Cali_Delete, curr_obj)}, {1, 1}); + gbSizer->Add(title_action, {0, column_idx++}, {1, 1}); + + // grid item num in row (Delete + Edit share one "Action" header) + const int column_count = column_idx + 1; + // reset column_idx for the data rows + column_idx = 0; int i = 1; for (auto& result : m_calib_results_history) { @@ -373,13 +417,13 @@ void HistoryWindow::sync_history_data() { n_value->Hide(); auto delete_button = new Button(m_history_data_panel, _L("Delete")); delete_button->SetStyle(ButtonStyle::Alert, ButtonType::Window); - delete_button->Bind(wxEVT_BUTTON, [this, gbSizer, i, &result](auto& e) { + delete_button->Bind(wxEVT_BUTTON, [this, gbSizer, i, &result, column_count](auto& e) { if (m_ui_op_lock) { return; } else { m_ui_op_lock = true; } - for (int j = 0; j < HISTORY_WINDOW_ITEMS_COUNT; j++) { + for (int j = 0; j < column_count; j++) { auto item = gbSizer->FindItemAtPosition({ i, j }); if (item && item->GetWindow()) item->GetWindow()->Hide(); @@ -392,6 +436,8 @@ void HistoryWindow::sync_history_data() { cali_info.cali_idx = result.cali_idx; cali_info.nozzle_diameter = result.nozzle_diameter; cali_info.filament_id = result.filament_id; + cali_info.nozzle_pos_id = result.nozzle_pos_id; + cali_info.nozzle_sn = result.nozzle_sn; CalibUtils::delete_PA_calib_result(cali_info); }); @@ -418,17 +464,23 @@ void HistoryWindow::sync_history_data() { } }); - gbSizer->Add(name_value, {i, get_colume_idx(CaliColumnType::Cali_Name, curr_obj)}, {1, 1}, wxBOTTOM, FromDIP(15)); - gbSizer->Add(preset_name_value, {i, get_colume_idx(CaliColumnType::Cali_Filament, curr_obj)}, {1, 1}, wxBOTTOM, FromDIP(15)); + gbSizer->Add(name_value, {i, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); + gbSizer->Add(preset_name_value, {i, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); + if (has_nozzle_id) { + wxString nozzle_id = nozzle_id_code_to_string(result.nozzle_pos_id); + auto nozzle_id_label = new Label(m_history_data_panel, nozzle_id); + gbSizer->Add(nozzle_id_label, {i, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); + } if (support_nozzle_volume(curr_obj)) { wxString nozzle_name = get_nozzle_volume_type_name(result.nozzle_volume_type); auto nozzle_name_label = new Label(m_history_data_panel, nozzle_name); - gbSizer->Add(nozzle_name_label, {i, get_colume_idx(CaliColumnType::Cali_Nozzle, curr_obj)}, {1, 1}, wxBOTTOM, FromDIP(15)); + gbSizer->Add(nozzle_name_label, {i, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); } - gbSizer->Add(k_value, {i, get_colume_idx(CaliColumnType::Cali_K_Value, curr_obj)}, {1, 1}, wxBOTTOM, FromDIP(15)); + gbSizer->Add(k_value, {i, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); //gbSizer->Add(n_value, { i, 3 }, { 1, 1 }, wxBOTTOM, FromDIP(15)); - gbSizer->Add(delete_button, {i, get_colume_idx(CaliColumnType::Cali_Delete, curr_obj)}, {1, 1}, wxBOTTOM, FromDIP(15)); - gbSizer->Add(edit_button, {i, get_colume_idx(CaliColumnType::Cali_Edit, curr_obj)}, {1, 1}, wxBOTTOM, FromDIP(15)); + gbSizer->Add(delete_button, {i, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); + gbSizer->Add(edit_button, {i, column_idx++}, {1, 1}, wxBOTTOM, FromDIP(15)); + column_idx = 0; i++; m_ui_op_lock = false; } @@ -523,13 +575,22 @@ EditCalibrationHistoryDialog::EditCalibrationHistoryDialog(wxWindow if (obj && obj->is_multi_extruders()) { Label *extruder_name_title = new Label(top_panel, _L("Extruder")); - int extruder_index = obj->is_main_extruder_on_left() ? result.extruder_id : 1 - result.extruder_id; - wxString extruder_name = extruder_index == 0 ? _L("Left") : _L("Right"); + wxString extruder_name = _L(DevPrinterConfigUtil::get_toolhead_display_name( + obj->printer_type, result.extruder_id, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true)); Label *extruder_name_value = new Label(top_panel, extruder_name); flex_sizer->Add(extruder_name_title); flex_sizer->Add(extruder_name_value); } + if (obj && obj->GetNozzleSystem() && obj->GetNozzleSystem()->GetNozzleRack() + && obj->GetNozzleSystem()->GetNozzleRack()->IsSupported() && result.extruder_id == MAIN_EXTRUDER_ID) { + Label* nozzle_id_title = new Label(top_panel, _L("Nozzle ID")); + wxString nozzle_id = nozzle_id_code_to_string(result.nozzle_pos_id); + Label* nozzle_id_value = new Label(top_panel, nozzle_id); + flex_sizer->Add(nozzle_id_title); + flex_sizer->Add(nozzle_id_value); + } + if (support_nozzle_volume(curr_obj)) { Label *nozzle_name_title = new Label(top_panel, _L("Nozzle")); wxString nozzle_name; @@ -768,14 +829,40 @@ NewCalibrationHistoryDialog::NewCalibrationHistoryDialog(wxWindow *parent, const Label *extruder_name_title = new Label(top_panel, _L("Extruder")); m_comboBox_extruder = new ::ComboBox(top_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, NEW_HISTORY_DIALOG_INPUT_SIZE, 0, nullptr, wxCB_READONLY); wxArrayString extruder_items; - extruder_items.push_back(_L("Left")); - extruder_items.push_back(_L("Right")); + extruder_items.push_back(_L(DevPrinterConfigUtil::get_toolhead_display_name( + curr_obj->printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true))); + extruder_items.push_back(_L(DevPrinterConfigUtil::get_toolhead_display_name( + curr_obj->printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true))); m_comboBox_extruder->Set(extruder_items); m_comboBox_extruder->SetSelection(-1); flex_sizer->Add(extruder_name_title); flex_sizer->Add(m_comboBox_extruder); } + // Nozzle ID (rack printers only): choose the hotend position this manual record belongs to. + if (curr_obj->GetNozzleSystem() && curr_obj->GetNozzleSystem()->GetNozzleRack() + && curr_obj->GetNozzleSystem()->GetNozzleRack()->IsSupported()) { + auto rack = curr_obj->GetNozzleSystem()->GetNozzleRack(); + Label *nozzle_id_title = new Label(top_panel, _L("Nozzle ID")); + m_comboBox_nozzle_id = new ::ComboBox(top_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, NEW_HISTORY_DIALOG_INPUT_SIZE, 0, nullptr, wxCB_READONLY); + m_comboBox_nozzle_id->Bind(wxEVT_COMMAND_COMBOBOX_SELECTED, &NewCalibrationHistoryDialog::on_select_nozzle_pos, this); + + auto main_extder = curr_obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); + int r_nozzle_id = main_extder ? main_extder->GetNozzleId() : 0; + auto r_nozzle = curr_obj->GetNozzleSystem()->GetExtNozzle(r_nozzle_id); + if (r_nozzle.IsNormal()) { + m_comboBox_nozzle_id->Append("R", wxNullBitmap, new int{0}); + } + for (auto nozzle_item : rack->GetRackNozzles()) { + if (nozzle_item.second.IsNormal()) { + m_comboBox_nozzle_id->Append(wxString::Format("%d", nozzle_item.first + 1), wxNullBitmap, new int{0x10 | nozzle_item.first}); + } + } + m_comboBox_nozzle_id->SetSelection(-1); + flex_sizer->Add(nozzle_id_title); + flex_sizer->Add(m_comboBox_nozzle_id); + } + if (support_nozzle_volume(curr_obj)) { Label *nozzle_name_title = new Label(top_panel, _L("Nozzle")); m_comboBox_nozzle_type = new ::ComboBox(top_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, NEW_HISTORY_DIALOG_INPUT_SIZE, 0, nullptr, wxCB_READONLY); @@ -839,6 +926,35 @@ int NewCalibrationHistoryDialog::get_extruder_id(int extruder_index) return 0; } +int NewCalibrationHistoryDialog::get_nozzle_combo_id_code() const +{ + if (!m_comboBox_nozzle_id) + return -1; + + auto sel = m_comboBox_nozzle_id->GetSelection(); + if (sel != wxNOT_FOUND && m_comboBox_nozzle_id->GetClientData(sel)) + return *(reinterpret_cast(m_comboBox_nozzle_id->GetClientData(sel))); + + return -1; +} + +void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event) +{ + // Mirror the picked hotend's flow onto the (Orca index-based) nozzle-type combo. + if (!curr_obj || !m_comboBox_nozzle_id || !m_comboBox_nozzle_type || !curr_obj->GetNozzleSystem()) + return; + + int pos = get_nozzle_combo_id_code(); + if (pos < 0) + return; + + DevNozzle nozzle = curr_obj->GetNozzleSystem()->GetNozzleByPosId(pos); + if (nozzle.IsNormal()) { + NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.GetNozzleFlowType()); + m_comboBox_nozzle_type->SetSelection(static_cast(volume_type)); + } +} + void NewCalibrationHistoryDialog::on_ok(wxCommandEvent &event) { wxString name = m_name_value->GetTextCtrl()->GetValue(); @@ -897,6 +1013,7 @@ void NewCalibrationHistoryDialog::on_ok(wxCommandEvent &event) m_new_result.nozzle_diameter = nozzle_value; m_new_result.filament_id = filament_id; m_new_result.setting_id = setting_id; + m_new_result.nozzle_pos_id = get_nozzle_combo_id_code(); // Check for duplicate names from history { diff --git a/src/slic3r/GUI/CaliHistoryDialog.hpp b/src/slic3r/GUI/CaliHistoryDialog.hpp index 3eb0a84f9d..d5159b634c 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.hpp +++ b/src/slic3r/GUI/CaliHistoryDialog.hpp @@ -27,6 +27,7 @@ protected: void enbale_action_buttons(bool enable); float get_nozzle_value(); int get_extruder_id(); + bool support_nozzle_id_column(); void on_click_new_button(wxCommandEvent &event); @@ -76,10 +77,12 @@ public: protected: virtual void on_ok(wxCommandEvent &event); virtual void on_cancel(wxCommandEvent &event); + void on_select_nozzle_pos(wxCommandEvent &event); wxArrayString get_all_filaments(const MachineObject *obj); int get_extruder_id(int extruder_index); // extruder_index 0 : left, 1 : right + int get_nozzle_combo_id_code() const; // rack hotend position code, -1 if none protected: PACalibResult m_new_result; @@ -93,6 +96,7 @@ protected: ComboBox *m_comboBox_filament; ComboBox *m_comboBox_extruder; ComboBox *m_comboBox_nozzle_type; + ComboBox *m_comboBox_nozzle_id{nullptr}; struct FilamentInfos { diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index 970635c770..db97354ad1 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -9,7 +9,9 @@ #include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevFilaBlackList.h" #include "DeviceCore/DevFilaSystem.h" +#include "DeviceCore/DevFilaSwitch.h" #include "DeviceCore/DevManager.h" +#include "DeviceCore/DevNozzleSystem.h" #include "DeviceCore/DevStorage.h" #define CALIBRATION_LABEL_SIZE wxSize(FromDIP(150), FromDIP(24)) @@ -1457,31 +1459,40 @@ bool CalibrationPresetPage::is_filament_in_blacklist(int tray_id, Preset* preset get_tray_ams_and_slot_id(curr_obj, tray_id, ams_id, slot_id, out_tray_id); if (wxGetApp().app_config->get("skip_ams_blacklist_check") != "true") { - bool in_blacklist = false; - std::string action; - wxString info; - std::string filamnt_type; - preset->get_filament_type(filamnt_type); + DevFilaBlacklist::CheckFilamentInfo check_info; + check_info.dev_id = curr_obj->get_dev_id(); + check_info.model_id = curr_obj->printer_type; + check_info.fila_id = preset->filament_id; + preset->get_filament_type(check_info.fila_type); + check_info.ams_id = ams_id; + check_info.slot_id = slot_id; + check_info.has_filament_switch = curr_obj->GetFilaSwitch()->IsInstalled(); + // fila_name intentionally left empty: the engine recovers it from the selected AMS slot, + // preserving the name-match behavior. auto vendor = dynamic_cast (preset->config.option("filament_vendor")); if (vendor && (vendor->values.size() > 0)) { - std::string vendor_name = vendor->values[0]; - DevFilaBlacklist::check_filaments_in_blacklist(curr_obj->printer_type, vendor_name, filamnt_type, preset->filament_id, ams_id, slot_id, "", in_blacklist, action, info); + check_info.fila_vendor = vendor->values[0]; } - if (in_blacklist) { - error_tips = info.ToUTF8().data(); - if (action == "prohibition") { - return false; - } - else if (action == "warning") { - return true; - } + const auto &result = DevFilaBlacklist::check_filaments_in_blacklist(check_info); + + if (const auto &prohibition_items = result.get_items_by_action("prohibition"); !prohibition_items.empty()) { + wxString combined_msg; + for (const auto &item : prohibition_items) { combined_msg += item.info_msg + "\n"; } + error_tips = combined_msg.ToUTF8().data(); + return false; } - else { - error_tips = ""; + + if (const auto &warning_items = result.get_items_by_action("warning"); !warning_items.empty()) { + wxString combined_msg; + for (const auto &item : warning_items) { combined_msg += item.info_msg + "\n"; } + error_tips = combined_msg.ToUTF8().data(); return true; } + + error_tips = ""; + return true; } if (devPrinterUtil::IsVirtualSlot(ams_id)) { if (m_cali_mode == CalibMode::Calib_PA_Line && (m_cali_method == CalibrationMethod::CALI_METHOD_AUTO || m_cali_method == CalibrationMethod::CALI_METHOD_NEW_AUTO)) { @@ -1663,7 +1674,9 @@ bool CalibrationPresetPage::is_nozzle_info_synced() const if (curr_obj->is_nozzle_flow_type_supported()) { if (extruder.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) return false; - if (int(extruder.GetNozzleFlowType()) - 1 != int(get_nozzle_volume_type(extruder_id))) + // Map device flow -> volume type via DevNozzle::ToNozzleVolumeType so U_FLOW resolves to + // nvtTPUHighFlow(3); the naive flow-1 would yield nvtHybrid(2). Identical to flow-1 for S/H flow. + if (int(DevNozzle::ToNozzleVolumeType(extruder.GetNozzleFlowType())) != int(get_nozzle_volume_type(extruder_id))) return false; } } @@ -2124,7 +2137,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) } if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) { - m_left_comboBox_nozzle_volume->SetSelection(obj->GetExtderSystem()->GetNozzleFlowType(i) - 1); + m_left_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)))); } else { m_left_comboBox_nozzle_volume->SetSelection(0); } @@ -2144,7 +2157,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) } if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) { - m_right_comboBox_nozzle_volume->SetSelection(obj->GetExtderSystem()->GetNozzleFlowType(i) - 1); + m_right_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)))); } else { m_right_comboBox_nozzle_volume->SetSelection(0); } @@ -2182,7 +2195,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) else { if ((obj->GetExtderSystem()->GetTotalExtderCount() > 0) && (obj->GetExtderSystem()->GetNozzleFlowType(0) != NozzleFlowType::NONE_FLOWTYPE)) { - m_comboBox_nozzle_volume->SetSelection(obj->GetExtderSystem()->GetNozzleFlowType(0) - 1); + m_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0)))); } else { m_comboBox_nozzle_volume->SetSelection(0); } diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.cpp b/src/slic3r/GUI/CalibrationWizardSavePage.cpp index f14238d990..78bc4bb77f 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.cpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.cpp @@ -2,6 +2,9 @@ #include "I18N.hpp" #include "Widgets/Label.hpp" #include "MsgDialog.hpp" +#include "DeviceCore/DevConfigUtil.h" +#include "DeviceCore/DevNozzleSystem.h" +#include "DeviceCore/DevNozzleRack.h" namespace Slic3r { namespace GUI { @@ -95,35 +98,43 @@ enum class GridTextInputType { class GridTextInput : public TextInput { public: - GridTextInput(wxWindow* parent, wxString text, wxString label, wxSize size, int col_idx, GridTextInputType type); + GridTextInput(wxWindow* parent, wxString text, wxString label, wxSize size, int col_idx, GridTextInputType type, int extruder_id = MAIN_EXTRUDER_ID); int get_col_idx() { return m_col_idx; } void set_col_idx(int idx) { m_col_idx = idx; } + int get_extruder_id() { return m_extruder_id; } + void set_extruder_id(int id) { m_extruder_id = id; } GridTextInputType get_type() { return m_type; } void set_type(GridTextInputType type) { m_type = type; } private: int m_col_idx; + int m_extruder_id{MAIN_EXTRUDER_ID}; GridTextInputType m_type; }; -GridTextInput::GridTextInput(wxWindow* parent, wxString text, wxString label, wxSize size, int col_idx, GridTextInputType type) +GridTextInput::GridTextInput(wxWindow* parent, wxString text, wxString label, wxSize size, int col_idx, GridTextInputType type, int extruder_id) : TextInput(parent, text, label, "", wxDefaultPosition, size, wxTE_PROCESS_ENTER) , m_col_idx(col_idx) + , m_extruder_id(extruder_id) , m_type(type) { } class GridComboBox : public ComboBox { public: - GridComboBox(wxWindow* parent, wxSize size, int col_idx); + GridComboBox(wxWindow* parent, wxSize size, int col_idx, int extruder_id = MAIN_EXTRUDER_ID); int get_col_idx() { return m_col_idx; } void set_col_idx(int idx) { m_col_idx = idx; } + int get_extruder_id() { return m_extruder_id; } + void set_extruder_id(int id) { m_extruder_id = id; } private: int m_col_idx; + int m_extruder_id{MAIN_EXTRUDER_ID}; }; -GridComboBox::GridComboBox(wxWindow* parent, wxSize size, int col_idx) +GridComboBox::GridComboBox(wxWindow* parent, wxSize size, int col_idx, int extruder_id) : ComboBox(parent, wxID_ANY, "", wxDefaultPosition, size, 0, nullptr) , m_col_idx(col_idx) + , m_extruder_id(extruder_id) { } @@ -227,7 +238,7 @@ void CaliPASaveAutoPanel::sync_cali_result(const std::vector& cal m_calib_results.clear(); for (auto& item : cali_result) { if (item.confidence == 0) - m_calib_results[item.tray_id] = item; + m_calib_results.push_back(item); } m_grid_panel->DestroyChildren(); auto grid_sizer = new wxBoxSizer(wxHORIZONTAL); @@ -281,14 +292,14 @@ void CaliPASaveAutoPanel::sync_cali_result(const std::vector& cal wxString tray_name = get_tray_name_by_tray_id(item.tray_id); tray_title->SetLabel(tray_name); - auto k_value = new GridTextInput(m_grid_panel, "", "", CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, GridTextInputType::K); - auto n_value = new GridTextInput(m_grid_panel, "", "", CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, GridTextInputType::N); + auto k_value = new GridTextInput(m_grid_panel, "", "", CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, GridTextInputType::K, item.extruder_id); + auto n_value = new GridTextInput(m_grid_panel, "", "", CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, GridTextInputType::N, item.extruder_id); k_value->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC)); n_value->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC)); auto k_value_failed = new Label(m_grid_panel, _L("Failed")); auto n_value_failed = new Label(m_grid_panel, _L("Failed")); - auto comboBox_tray_name = new GridComboBox(m_grid_panel, CALIBRATION_SAVE_INPUT_SIZE, item.tray_id); + auto comboBox_tray_name = new GridComboBox(m_grid_panel, CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, item.extruder_id); auto tray_name_failed = new Label(m_grid_panel, " - "); wxArrayString selections; static std::vector filtered_results; @@ -395,17 +406,26 @@ void CaliPASaveAutoPanel::save_to_result_from_widgets(wxWindow* window, bool* ou return; //operate + // The grid can hold two rows with the same tray_id under different extruders + // (rack / dual-address configs), so results must be keyed by (tray_id, extruder_id). + auto find_result = [this](int tray_id, int extruder_id) -> PACalibResult* { + auto it = std::find_if(m_calib_results.begin(), m_calib_results.end(), [tray_id, extruder_id](const PACalibResult& r) { + return r.tray_id == tray_id && r.extruder_id == extruder_id; + }); + return it != m_calib_results.end() ? &(*it) : nullptr; + }; auto input = dynamic_cast(window); if (input && input->IsShown()) { - int tray_id = input->get_col_idx(); + int tray_id = input->get_col_idx(); + int extruder_id = input->get_extruder_id(); if (input->get_type() == GridTextInputType::K) { float k = 0.0f; if (!CalibUtils::validate_input_k_value(input->GetTextCtrl()->GetValue(), &k)) { *out_msg = wxString::Format(_L("Please input a valid value (K in %.1f~%.1f)"), MIN_PA_K_VALUE, MAX_PA_K_VALUE); *out_is_valid = false; } - else - m_calib_results[tray_id].k_value = k; + else if (auto* result = find_result(tray_id, extruder_id)) + result->k_value = k; } else if (input->get_type() == GridTextInputType::N) { } @@ -413,7 +433,8 @@ void CaliPASaveAutoPanel::save_to_result_from_widgets(wxWindow* window, bool* ou auto comboBox = dynamic_cast(window); if (comboBox && comboBox->IsShown()) { - int tray_id = comboBox->get_col_idx(); + int tray_id = comboBox->get_col_idx(); + int extruder_id = comboBox->get_extruder_id(); wxString name = comboBox->GetTextCtrl()->GetValue().ToStdString(); if (name.IsEmpty()) { *out_msg = _L("Please enter the name you want to save to printer."); @@ -423,7 +444,8 @@ void CaliPASaveAutoPanel::save_to_result_from_widgets(wxWindow* window, bool* ou *out_msg = _L("The name cannot exceed 40 characters."); *out_is_valid = false; } - m_calib_results[tray_id].name = into_u8(name); + if (auto* result = find_result(tray_id, extruder_id)) + result->name = into_u8(name); } auto childern = window->GetChildren(); @@ -441,52 +463,7 @@ bool CaliPASaveAutoPanel::get_result(std::vector& out_result) { else save_to_result_from_widgets(m_grid_panel, &is_valid, &err_msg); if (is_valid) { - /* - std::vector to_save_result; - for (auto &result : m_calib_results) { - auto iter = std::find_if(to_save_result.begin(), to_save_result.end(), [this, &result](const PACalibResult &item) { - bool has_same_name = (item.name == result.second.name && item.filament_id == result.second.filament_id); - if (m_obj && m_obj->is_multi_extruders()) { - has_same_name &= (item.extruder_id == result.second.extruder_id && item.nozzle_volume_type == result.second.nozzle_volume_type); - } - return has_same_name; - }); - - if (iter != to_save_result.end()) { - MessageDialog msg_dlg(nullptr, _L("Only one of the results with the same name will be saved. Are you sure you want to overwrite the other results?"), - wxEmptyString, wxICON_WARNING | wxYES_NO); - if (msg_dlg.ShowModal() != wxID_YES) { - return false; - } else { - break; - } - } - } - - for (auto &result : m_history_results) { - auto iter = std::find_if(m_history_results.begin(), m_history_results.end(), [this, &result](const PACalibResult &item) { - bool has_same_name = (item.name == result.name && item.filament_id == result.filament_id); - if (m_obj && m_obj->is_multi_extruders()) { - has_same_name &= (item.extruder_id == result.extruder_id && item.nozzle_volume_type == result.nozzle_volume_type); - } - return has_same_name; - }); - - if (iter != m_history_results.end()) { - MessageDialog msg_dlg(nullptr, - wxString::Format(_L("There is already a historical calibration result with the same name: %s. Are you sure you want to override the historical result?"), - result.name), - wxEmptyString, wxICON_WARNING | wxYES_NO); - if (msg_dlg.ShowModal() != wxID_YES) { - return false; - } - } - } - */ - - for (auto& result : m_calib_results) { - out_result.push_back(result.second); - } + out_result = m_calib_results; return true; } else { @@ -517,11 +494,12 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< m_calib_results.clear(); for (auto &item : cali_result) { if (item.confidence == 0) { - int tray_id = 4 * item.ams_id + item.slot_id; + PACalibResult result = item; + result.tray_id = 4 * item.ams_id + item.slot_id; if (item.ams_id == VIRTUAL_TRAY_MAIN_ID || item.ams_id == VIRTUAL_TRAY_DEPUTY_ID) { - tray_id = item.ams_id; + result.tray_id = item.ams_id; } - m_calib_results[tray_id] = item; + m_calib_results.push_back(result); } } m_multi_extruder_grid_panel->DestroyChildren(); @@ -532,14 +510,21 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< m_multi_extruder_grid_panel->SetSizer(grid_sizer, true); m_multi_extruder_grid_panel->Bind(wxEVT_LEFT_DOWN, [this](auto &e) { SetFocusIgnoringChildren(); }); - wxStaticBoxSizer *left_sizer = new wxStaticBoxSizer(wxVERTICAL, m_multi_extruder_grid_panel, _L("Left extruder")); - wxStaticBoxSizer *right_sizer = new wxStaticBoxSizer(wxVERTICAL, m_multi_extruder_grid_panel, _L("Right extruder")); + const std::string& cwsp_pt = m_obj->printer_type; + wxStaticBoxSizer *left_sizer = new wxStaticBoxSizer(wxVERTICAL, m_multi_extruder_grid_panel, _L(DevPrinterConfigUtil::get_toolhead_display_name(cwsp_pt, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::SentenceCase))); + wxStaticBoxSizer *right_sizer = new wxStaticBoxSizer(wxVERTICAL, m_multi_extruder_grid_panel, _L(DevPrinterConfigUtil::get_toolhead_display_name(cwsp_pt, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::SentenceCase))); grid_sizer->Add(left_sizer); grid_sizer->AddSpacer(COLUMN_GAP); grid_sizer->Add(right_sizer); wxFlexGridSizer *left_grid_sizer = new wxFlexGridSizer(3, COLUMN_GAP, ROW_GAP); - wxFlexGridSizer *right_grid_sizer = new wxFlexGridSizer(3, COLUMN_GAP, ROW_GAP); + + // The main extruder can carry a nozzle rack (H2C); non-rack printers keep the 3-column layout. + auto ns = m_obj->GetNozzleSystem(); + auto rack = ns ? ns->GetNozzleRack() : nullptr; + bool has_rack = rack && rack->IsSupported(); + + wxFlexGridSizer *right_grid_sizer = new wxFlexGridSizer(has_rack ? 4 : 3, COLUMN_GAP, ROW_GAP); left_sizer->Add(left_grid_sizer); right_sizer->Add(right_grid_sizer); @@ -567,29 +552,46 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< auto k_title = new Label(m_multi_extruder_grid_panel, _L("Factor K"), 0, CALIBRATION_SAVE_NUMBER_INPUT_SIZE); k_title->SetFont(Label::Head_14); right_grid_sizer->Add(k_title, 1, wxALIGN_CENTER); + + if (has_rack) { + auto nozzle_title = new Label(m_multi_extruder_grid_panel, _L("Nozzle ID"), 0, CALIBRATION_SAVE_NUMBER_INPUT_SIZE); + nozzle_title->SetFont(Label::Head_14); + right_grid_sizer->Add(nozzle_title, 1, wxALIGN_CENTER); + } } - std::vector> preset_names; + // key: (extruder_id, tray_id) + std::vector, std::string>> preset_names; int i = 1; std::unordered_set set; for (auto &info : m_obj->selected_cali_preset) { std::string default_name; - // extruder _id + // Derive extruder_id from the tray so the extruder/tray key stays consistent + // with the calibration result's extruder_id. + int extruder_id = 0; + if (info.tray_id == VIRTUAL_TRAY_MAIN_ID) { + extruder_id = 0; + } else if (info.tray_id == VIRTUAL_TRAY_DEPUTY_ID) { + extruder_id = 1; + } else { + int ams_id = info.tray_id / 4; + extruder_id = m_obj->get_extruder_id_by_ams_id(std::to_string(ams_id)); + } { - int extruder_id = 0; - if (info.tray_id == VIRTUAL_TRAY_MAIN_ID) { - extruder_id = 0; - } else if (info.tray_id == VIRTUAL_TRAY_DEPUTY_ID) { - extruder_id = 1; - } else { - int ams_id = info.tray_id / 4; - extruder_id = m_obj->get_extruder_id_by_ams_id(std::to_string(ams_id)); - } - - if (extruder_id == 0) { - default_name += L("Right Nozzle"); - } else if (extruder_id == 1){ - default_name += L("Left Nozzle"); + if (extruder_id == MAIN_EXTRUDER_ID) { + default_name += DevPrinterConfigUtil::get_toolhead_display_name(cwsp_pt, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase); + if (has_rack) { + default_name += "_"; + if (info.nozzle_pos_id == 0) { + default_name += "R"; + } else if (info.nozzle_pos_id >= 0x10) { + default_name += std::to_string((info.nozzle_pos_id & 0x0f) + 1); + } else { + default_name += "N/A"; + } + } + } else if (extruder_id == DEPUTY_EXTRUDER_ID) { + default_name += DevPrinterConfigUtil::get_toolhead_display_name(cwsp_pt, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase); } } @@ -614,7 +616,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< } } - preset_names.push_back({info.tray_id, default_name}); + preset_names.push_back({{extruder_id, info.tray_id}, default_name}); } bool left_first_add_item = true; @@ -648,14 +650,19 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< tray_title->SetBitmap(*get_extruder_color_icon(full_filament_ams_list[item.tray_id].opt_string("filament_colour", 0u), tray_name.ToStdString(), FromDIP(20), FromDIP(20))); tray_title->SetToolTip(""); - auto k_value = new GridTextInput(m_multi_extruder_grid_panel, "", "", CALIBRATION_SAVE_NUMBER_INPUT_SIZE, item.tray_id, GridTextInputType::K); - auto n_value = new GridTextInput(m_multi_extruder_grid_panel, "", "", CALIBRATION_SAVE_NUMBER_INPUT_SIZE, item.tray_id, GridTextInputType::N); + auto k_value = new GridTextInput(m_multi_extruder_grid_panel, "", "", CALIBRATION_SAVE_NUMBER_INPUT_SIZE, item.tray_id, GridTextInputType::K, item.extruder_id); + auto n_value = new GridTextInput(m_multi_extruder_grid_panel, "", "", CALIBRATION_SAVE_NUMBER_INPUT_SIZE, item.tray_id, GridTextInputType::N, item.extruder_id); k_value->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC)); n_value->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC)); auto k_value_failed = new Label(m_multi_extruder_grid_panel, _L("Failed")); auto n_value_failed = new Label(m_multi_extruder_grid_panel, _L("Failed")); - auto comboBox_tray_name = new GridComboBox(m_multi_extruder_grid_panel, CALIBRATION_SAVE_INPUT_SIZE, item.tray_id); + auto nozzle_id_value = new Label(m_multi_extruder_grid_panel, wxEmptyString); + nozzle_id_value->SetMinSize(wxSize(FromDIP(180), FromDIP(24))); + nozzle_id_value->Wrap(-1); + auto nozzle_id_failed = new Label(m_multi_extruder_grid_panel, _L("Failed")); + + auto comboBox_tray_name = new GridComboBox(m_multi_extruder_grid_panel, CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, item.extruder_id); auto tray_name_failed = new Label(m_multi_extruder_grid_panel, " - "); wxArrayString selections; static std::vector filtered_results; @@ -671,22 +678,26 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< } comboBox_tray_name->Set(selections); - auto set_edit_mode = [this, k_value, n_value, k_value_failed, n_value_failed, comboBox_tray_name, tray_name_failed](std::string str) { + auto set_edit_mode = [this, k_value, n_value, k_value_failed, n_value_failed, nozzle_id_value, nozzle_id_failed, comboBox_tray_name, tray_name_failed](std::string str, bool display_nozzle) { if (str == "normal") { comboBox_tray_name->Show(); tray_name_failed->Show(false); k_value->Show(); n_value->Show(); + nozzle_id_value->Show(display_nozzle); k_value_failed->Show(false); n_value_failed->Show(false); + nozzle_id_failed->Show(false); } if (str == "failed") { comboBox_tray_name->Show(false); tray_name_failed->Show(); k_value->Show(false); n_value->Show(false); + nozzle_id_value->Show(false); k_value_failed->Show(); n_value_failed->Show(); + nozzle_id_failed->Show(display_nozzle); } // hide n value @@ -698,15 +709,39 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< }; if (!result_failed) { - set_edit_mode("normal"); + set_edit_mode("normal", has_rack && item.extruder_id == MAIN_EXTRUDER_ID); auto k_str = wxString::Format("%.3f", item.k_value); auto n_str = wxString::Format("%.3f", item.n_coef); k_value->GetTextCtrl()->SetValue(k_str); n_value->GetTextCtrl()->SetValue(n_str); + if (has_rack && item.extruder_id == MAIN_EXTRUDER_ID) { + wxString nozzle_id_str; + if (item.nozzle_pos_id == 0) { + nozzle_id_str += "R | "; + } else if (item.nozzle_pos_id >= 0x10) { + nozzle_id_str += wxString::Format("%d | ", (item.nozzle_pos_id & 0x0f) + 1); + } else { + nozzle_id_str += "N/A | "; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "Nozzle position id is -1 or invalid."; + } + nozzle_id_str += wxString::Format("%.1f mm ", item.nozzle_diameter); + switch (item.nozzle_volume_type) { + case NozzleVolumeType::nvtStandard: nozzle_id_str += _L("Standard Flow"); break; + case NozzleVolumeType::nvtHighFlow: nozzle_id_str += _L("High Flow"); break; + case NozzleVolumeType::nvtTPUHighFlow: nozzle_id_str += _L("TPU High Flow"); break; + default: break; + } + nozzle_id_value->SetLabel(nozzle_id_str); + } + for (auto &name : preset_names) { - if (item.tray_id == name.first) { comboBox_tray_name->SetValue(from_u8(name.second)); } + int extruder_id = name.first.first; + int tray_id = name.first.second; + if (item.tray_id == tray_id && item.extruder_id == extruder_id) { + comboBox_tray_name->SetValue(from_u8(name.second)); + } } comboBox_tray_name->Bind(wxEVT_COMBOBOX, [this, comboBox_tray_name, k_value, n_value](auto &e) { @@ -714,7 +749,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< auto history = filtered_results[selection]; }); } else { - set_edit_mode("failed"); + set_edit_mode("failed", has_rack && item.extruder_id == MAIN_EXTRUDER_ID); } if ((m_obj->is_main_extruder_on_left() && item.extruder_id == 0) @@ -762,6 +797,14 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< } else { right_grid_sizer->Add(k_value_failed, 1, wxEXPAND); } + + if (has_rack) { + if (nozzle_id_value->IsShown()) { + right_grid_sizer->Add(nozzle_id_value, 1, wxEXPAND); + } else { + right_grid_sizer->Add(nozzle_id_failed, 1, wxEXPAND); + } + } } } diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.hpp b/src/slic3r/GUI/CalibrationWizardSavePage.hpp index 3233116d34..4726cb1230 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.hpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.hpp @@ -107,7 +107,10 @@ protected: wxPanel* m_part_failed_panel; wxPanel* m_grid_panel{ nullptr }; wxPanel* m_multi_extruder_grid_panel{ nullptr }; - std::map m_calib_results;// map + // keyed by (tray_id, extruder_id): the same tray_id can appear under two + // extruders (rack / dual-address configs), so a tray_id-only map would + // silently overwrite one extruder's K/N. + std::vector m_calib_results; std::vector m_history_results; bool m_is_all_failed{ true }; MachineObject* m_obj{ nullptr }; diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 9d7afd13b9..ddf6166d11 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -23,6 +23,7 @@ #include "fast_float/fast_float.h" #include "DeviceCore/DevFilaSystem.h" +#include "DeviceCore/DevFilaSwitch.h" #include "DeviceCore/DevExtensionTool.h" #include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevNozzleSystem.h" @@ -39,6 +40,7 @@ #include "DeviceCore/DevHMS.h" #include "DeviceCore/DevMapping.h" +#include "DeviceCore/DevMappingNozzle.h" #include "DeviceCore/DevManager.h" #include "DeviceCore/DevUtil.h" @@ -567,11 +569,14 @@ MachineObject::MachineObject(DeviceManager* manager, NetworkAgent* agent, std::s m_extension_tool = DevExtensionTool::Create(this); m_nozzle_system = new DevNozzleSystem(this); m_fila_system = new DevFilaSystem(this); + m_fila_switch = new DevFilaSwitch(this); m_hms_system = new DevHMS(this); m_config = new DevConfig(this); m_ctrl = new DevCtrl(this); m_print_options = new DevPrintOptions(this); + + m_nozzle_mapping_ptr = std::make_shared(this); } } @@ -615,6 +620,9 @@ MachineObject::~MachineObject() delete m_fila_system; m_fila_system = nullptr; + delete m_fila_switch; + m_fila_switch = nullptr; + delete m_hms_system; m_hms_system = nullptr; @@ -862,6 +870,9 @@ void MachineObject::clear_version_info() cutting_module_version_info = DevFirmwareVersionInfo(); extinguish_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). + m_nozzle_system->ClearFirmwareInfoWTM(); } void MachineObject::store_version_info(const DevFirmwareVersionInfo& info) @@ -874,6 +885,11 @@ void MachineObject::store_version_info(const DevFirmwareVersionInfo& info) cutting_module_version_info = info; } else if (info.isExtinguishSystem()) { extinguish_version_info = info; + } else if (info.isWTM()) { + // Route rack-hotend / extruder-nozzle firmware into the nozzle system so + // 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); } module_vers.emplace(info.name, info); @@ -1976,6 +1992,10 @@ int MachineObject::command_set_pa_calibration(const std::vector & j["print"]["filaments"][i]["n_coef"] = std::to_string(pa_calib_values[i].n_coef); else j["print"]["filaments"][i]["n_coef"] = "0.0"; + if (pa_calib_values[i].nozzle_pos_id >= 0) { + j["print"]["filaments"][i]["nozzle_pos"] = pa_calib_values[i].nozzle_pos_id; + j["print"]["filaments"][i]["nozzle_sn"] = pa_calib_values[i].nozzle_sn; + } } BOOST_LOG_TRIVIAL(info) << "extrusion_cali_set: " << j.dump(); @@ -1994,6 +2014,10 @@ int MachineObject::command_delete_pa_calibration(const PACalibIndexInfo& pa_cali j["print"]["nozzle_id"] = _generate_nozzle_id(pa_calib.nozzle_volume_type, to_string_nozzle_diameter(pa_calib.nozzle_diameter)).ToStdString(); j["print"]["filament_id"] = pa_calib.filament_id; j["print"]["cali_idx"] = pa_calib.cali_idx; + if (pa_calib.nozzle_pos_id >= 0) { + j["print"]["nozzle_pos"] = pa_calib.nozzle_pos_id; + j["print"]["nozzle_sn"] = pa_calib.nozzle_sn; + } j["print"]["nozzle_diameter"] = to_string_nozzle_diameter(pa_calib.nozzle_diameter); BOOST_LOG_TRIVIAL(info) << "extrusion_cali_del: " << j.dump(); @@ -2014,6 +2038,11 @@ int MachineObject::command_get_pa_calibration_tab(const PACalibExtruderInfo &cal j["print"]["nozzle_id"] = _generate_nozzle_id(calib_info.nozzle_volume_type, to_string_nozzle_diameter(calib_info.nozzle_diameter)).ToStdString(); j["print"]["nozzle_diameter"] = to_string_nozzle_diameter(calib_info.nozzle_diameter); + if (calib_info.nozzle_pos_id >= 0) { + j["print"]["nozzle_pos"] = calib_info.nozzle_pos_id; + j["print"]["nozzle_sn"] = calib_info.nozzle_sn; + } + BOOST_LOG_TRIVIAL(info) << "extrusion_cali_get: " << j.dump(); request_tab_from_bbs = true; return this->publish_json(j); @@ -2040,6 +2069,10 @@ int MachineObject::commnad_select_pa_calibration(const PACalibIndexInfo& pa_cali j["print"]["slot_id"] = pa_calib_info.slot_id; j["print"]["cali_idx"] = pa_calib_info.cali_idx; j["print"]["filament_id"] = pa_calib_info.filament_id; + if (pa_calib_info.nozzle_pos_id >= 0) { + j["print"]["nozzle_pos"] = pa_calib_info.nozzle_pos_id; + j["print"]["nozzle_sn"] = pa_calib_info.nozzle_sn; + } j["print"]["nozzle_diameter"] = to_string_nozzle_diameter(pa_calib_info.nozzle_diameter); BOOST_LOG_TRIVIAL(info) << "extrusion_cali_sel: " << j.dump(); @@ -2379,6 +2412,7 @@ void MachineObject::reset() jobState_ = 0; m_plate_index = -1; device_cert_installed = false; + clear_auto_nozzle_mapping();// reset nozzle mapping // reset print_json json empty_j; @@ -2896,6 +2930,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ } m_fan->ParseV2_0(jj); + m_fila_switch->ParseFilaSwitchInfo(jj); if (jj.contains("support_filament_backup")) { if (jj["support_filament_backup"].is_boolean()) { @@ -3000,6 +3035,8 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ if (jj.contains("command")) { + m_nozzle_mapping_ptr->ParseAutoNozzleMapping(jj); + if (jj["command"].get() == "ams_change_filament") { if (jj.contains("errno")) { if (jj["errno"].is_number()) { @@ -4082,6 +4119,14 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ pa_calib_result.nozzle_volume_type = convert_to_nozzle_type((*it)["nozzle_id"].get()); } + if ((*it).contains("nozzle_pos")) { + pa_calib_result.nozzle_pos_id = (*it)["nozzle_pos"].get(); + } + + if ((*it).contains("nozzle_sn")) { + pa_calib_result.nozzle_sn = (*it)["nozzle_sn"].get(); + } + if (jj["nozzle_diameter"].is_number_float()) { pa_calib_result.nozzle_diameter = jj["nozzle_diameter"].get(); } else if (jj["nozzle_diameter"].is_string()) { @@ -4179,6 +4224,14 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ pa_calib_result.nozzle_volume_type = NozzleVolumeType::nvtStandard; } + if (it->contains("nozzle_pos")) { + pa_calib_result.nozzle_pos_id = (*it)["nozzle_pos"].get(); + } + + if (it->contains("nozzle_sn")) { + pa_calib_result.nozzle_sn = (*it)["nozzle_sn"].get(); + } + if ((*it)["k_value"].is_number_float()) pa_calib_result.k_value = (*it)["k_value"].get(); else if ((*it)["k_value"].is_string()) @@ -5004,6 +5057,7 @@ void MachineObject::parse_new_info(json print) is_support_user_preset = get_flag_bits(fun, 11); is_support_door_open_check = get_flag_bits(fun, 12); is_support_nozzle_blob_detection = get_flag_bits(fun, 13); + m_nozzle_system->SetSupportNozzleRack(get_flag_bits(fun, 60)); // H2C hotend rack support (device-side gate for the sync dialog) is_support_upgrade_kit = get_flag_bits(fun, 14); is_support_internal_timelapse = get_flag_bits(fun, 28); m_support_mqtt_homing = get_flag_bits(fun, 32); @@ -5066,7 +5120,10 @@ void MachineObject::parse_new_info(json print) DevBed::ParseV2_0(device,m_bed); - if (device.contains("nozzle")) { DevNozzleSystemParser::ParseV2_0(device["nozzle"], m_nozzle_system); } + // Pass the whole device json: ParseV2_0 reads ext/rack nozzles from device["nozzle"] and the + // hotend-rack state from device["holder"] (rack data lives outside device["nozzle"]). It is a + // no-op for devices that report neither key, so non-rack machines are unaffected. + DevNozzleSystemParser::ParseV2_0(device, m_nozzle_system); if (device.contains("extruder")) { ExtderSystemParser::ParseV2_0(device["extruder"], m_extder_system);} if (device.contains("ext_tool")) { DevExtensionToolParser::ParseV2_0(device["ext_tool"], m_extension_tool); } diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 0a31c18502..9bb00dfcab 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -83,10 +83,12 @@ class DevExtensionTool; class DevExtderSystem; class DevFan; class DevFilaSystem; +class DevFilaSwitch; class DevPrintOptions; class DevHMS; class DevLamp; class DevNozzleSystem; +class DevNozzleMappingCtrl; class DeviceManager; class DevStorage; struct DevPrintTaskRatingInfo; @@ -115,6 +117,7 @@ private: DevExtderSystem* m_extder_system; DevNozzleSystem* m_nozzle_system; DevFilaSystem* m_fila_system; + DevFilaSwitch* m_fila_switch; DevFan* m_fan; DevBed * m_bed; DevStorage* m_storage; @@ -131,6 +134,11 @@ private: /*Config*/ DevConfig* m_config; + /* print-dispatch nozzle mapping (H2C hotend rack). Created unconditionally in the ctor so + get_nozzle_mapping_result() is always valid; stays empty (no result attached) for every + non-rack printer. */ + std::shared_ptr m_nozzle_mapping_ptr; + public: MachineObject(DeviceManager* manager, NetworkAgent* agent, std::string name, std::string id, std::string ip); ~MachineObject(); @@ -329,7 +337,12 @@ public: DevNozzleSystem* GetNozzleSystem() const { return m_nozzle_system;} + /* print-dispatch nozzle mapping (H2C hotend rack); result stays empty for non-rack printers */ + std::shared_ptr get_nozzle_mapping_result() const { return m_nozzle_mapping_ptr; } + void clear_auto_nozzle_mapping();// defined in DevMappingNozzle.cpp + DevFilaSystem* GetFilaSystem() const { return m_fila_system;} + DevFilaSwitch* GetFilaSwitch() const { return m_fila_switch;} bool HasAms() const; DevLamp* GetLamp() const { return m_lamp; } diff --git a/src/slic3r/GUI/DeviceTab/CMakeLists.txt b/src/slic3r/GUI/DeviceTab/CMakeLists.txt index 40f192ef4e..1438bf139b 100644 --- a/src/slic3r/GUI/DeviceTab/CMakeLists.txt +++ b/src/slic3r/GUI/DeviceTab/CMakeLists.txt @@ -4,5 +4,15 @@ list(APPEND SLIC3R_GUI_SOURCES GUI/DeviceTab/uiAmsHumidityPopup.cpp GUI/DeviceTab/uiDeviceUpdateVersion.h GUI/DeviceTab/uiDeviceUpdateVersion.cpp + GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.h + GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.cpp + GUI/DeviceTab/wgtDeviceNozzleRack.h + GUI/DeviceTab/wgtDeviceNozzleRack.cpp + GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h + GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp + GUI/DeviceTab/wgtDeviceNozzleSelect.h + GUI/DeviceTab/wgtDeviceNozzleSelect.cpp + GUI/DeviceTab/wgtMsgBox.h + GUI/DeviceTab/wgtMsgBox.cpp ) set(SLIC3R_GUI_SOURCES ${SLIC3R_GUI_SOURCES} PARENT_SCOPE) \ No newline at end of file diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp new file mode 100644 index 0000000000..e8fbf37d15 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp @@ -0,0 +1,700 @@ +//**********************************************************/ +/* File: wgtDeviceNozzleRack.cpp +* Description: The Device-tab panel with the toolhead nozzle and the H2C induction hotend rack. +* +* \n class wgtDeviceNozzleRack; +* \n class wgtDeviceNozzleRackToolHead; +* \n class wgtDeviceNozzleRackArea; +* \n class wgtDeviceNozzleRackPos; +* +* The wgtDeviceNozzleRackNozzleItem tile and the EVT_NOZZLE_RACK_NOZZLE_ITEM_SELECTED event live in +* wgtDeviceNozzleRackNozzleItem.{h,cpp}, so they are not redefined here. +//**********************************************************/ + +#include "wgtDeviceNozzleRack.h" +#include "wgtDeviceNozzleRackUpdate.h" + +#include "slic3r/GUI/DeviceCore/DevNozzleSystem.h" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/wxExtensions.hpp" + +#include "slic3r/GUI/Widgets/Button.hpp" +#include "slic3r/GUI/Widgets/Label.hpp" + +#define WX_DIP_SIZE(x, y) wxSize(FromDIP(x), FromDIP(y)) + +#define L_RAW_A_STR _L("Row A") +#define L_RAW_B_STR _L("Row B") + +// Orca: StateColor lacks these grey constants, so mirror the values here. +static const wxColour WGT_GREY200 = wxColour(248, 248, 248); +static const wxColour WGT_GREY300 = wxColour(238, 238, 238); + +static wxColour s_hgreen_clr("#00AE42"); + +namespace Slic3r::GUI +{ + +// Tints the "yellow" template pixels of a nozzle bitmap with the loaded filament colour. +// Duplicates the file-local helper of the same purpose in wgtDeviceNozzleRackNozzleItem.cpp. +static wxBitmap SetNozzleBmpColor(const wxBitmap& bmp, const std::string& color_str) { + if(color_str.empty()) return bmp; + + wxImage img = bmp.ConvertToImage(); + wxColour color("#" + color_str); + + for (int y = 0; y < img.GetHeight(); ++y) { + for (int x = 0; x < img.GetWidth(); ++x) { + unsigned char r = img.GetRed(x, y); + unsigned char g = img.GetGreen(x, y); + unsigned char b = img.GetBlue(x, y); + + /*replace yellow with color*/ + if ( r >= 180 && g >= 180 && b <= 150) { + img.SetRGB(x, y, color.Red(), color.Green(), color.Blue()); + } + } + } + + return wxBitmap(img, -1, bmp.GetScaleFactor()); +} + +// Orca: StateColor has no gray button style, so define one file-local. This panel is its only +// consumer, so keeping it here avoids touching the shared StateColor widget. +static StateColor s_button_style_gray() +{ + return StateColor(std::pair(wxColour(206, 206, 206), StateColor::Pressed), + std::pair(*wxWHITE, StateColor::Focused), + std::pair(wxColour(238, 238, 238), StateColor::Hovered), + std::pair(*wxWHITE, StateColor::Normal)); +} + +wgtDeviceNozzleRack::wgtDeviceNozzleRack(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style) + : wxPanel(parent, id, pos, size, style) +{ + CreateGui(); +} + +void wgtDeviceNozzleRack::CreateGui() +{ + m_toolhead_panel = new wgtDeviceNozzleRackToolHead(this); + m_rack_area = new wgtDeviceNozzleRackArea(this); + + wxPanel* separator = new wxPanel(this); + separator->SetMaxSize(wxSize(FromDIP(1), -1)); + separator->SetMinSize(wxSize(FromDIP(1), -1)); + separator->SetBackgroundColour(WGT_GREY300); + + wxSizer* main_sizer = new wxBoxSizer(wxHORIZONTAL); + main_sizer->AddStretchSpacer(); + main_sizer->Add(m_toolhead_panel, 0, wxEXPAND); + main_sizer->Add(separator, 0, wxEXPAND); + main_sizer->Add(m_rack_area, 0, wxEXPAND); + main_sizer->AddStretchSpacer(); + + SetSizer(main_sizer); + SetMaxSize(WX_DIP_SIZE(586, -1)); + SetMinSize(WX_DIP_SIZE(586, -1)); + SetSize(WX_DIP_SIZE(586, -1)); + Layout(); + + wxGetApp().UpdateDarkUIWin(this); +} + +void wgtDeviceNozzleRack::UpdateRackInfo(std::shared_ptr rack) +{ + if (!rack->IsSupported()) { return; } + + m_nozzle_rack = rack; + if (m_nozzle_rack.expired()) { return; } + + DevNozzleSystem* nozzle_system = m_nozzle_rack.lock()->GetNozzleSystem(); + if (nozzle_system) + { + m_toolhead_panel->UpdateToolHeadInfo(nozzle_system->GetExtNozzle(MAIN_EXTRUDER_ID)); + m_rack_area->UpdateRackInfo(m_nozzle_rack); + } +} + +void wgtDeviceNozzleRack::Rescale() +{ + m_toolhead_panel->Rescale(); + m_rack_area->Rescale(); + Layout(); +} + +class wgtDeviceNozzleRackTitle : public StaticBox +{ +public: + wgtDeviceNozzleRackTitle(wxWindow* parent, const wxString& title) : StaticBox(parent) + { + SetBackgroundColour(WGT_GREY200); + SetBorderColor(*wxWHITE); + SetCornerRadius(0); + + m_title_label = new Label(this, title); + m_title_label->SetFont(Label::Body_14); + m_title_label->SetBackgroundColour(WGT_GREY200); + + wxSizer* title_sizer = new wxBoxSizer(wxHORIZONTAL); + title_sizer->AddStretchSpacer(); + title_sizer->Add(m_title_label, 0, wxEXPAND | wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(5)); + title_sizer->AddStretchSpacer(); + SetSizer(title_sizer); + }; + +public: + void SetLabel(const wxString& new_label) { m_title_label->SetLabel(new_label); } + +private: + Label* m_title_label; +}; + + +void wgtDeviceNozzleRackToolHead::CreateGui() +{ + wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); + + // Create Header + wgtDeviceNozzleRackTitle* title_box = new wgtDeviceNozzleRackTitle(this, _L("Toolhead")); + mainSizer->Add(title_box, 0, wxEXPAND | wxTOP); + mainSizer->AddStretchSpacer(); + + // Image + m_extruder_nozzle_empty = new ScalableBitmap(this, "dev_rack_toolhead_empty", 98); + m_extruder_nozzle_normal = new ScalableBitmap(this, "dev_rack_toolhead_normal", 98); + m_toolhead_icon = new wxStaticBitmap(this, wxID_ANY, m_extruder_nozzle_empty->bmp(), wxDefaultPosition, WX_DIP_SIZE(98, 98)); + mainSizer->Add(m_toolhead_icon, 0, wxALIGN_CENTRE_HORIZONTAL | wxTOP, FromDIP(20)); + + // Nozzle info + m_nozzle_diamenter_label = new Label(this); + m_nozzle_diamenter_label->SetFont(Label::Body_13); + m_nozzle_diamenter_label->SetBackgroundColour(*wxWHITE); + mainSizer->Add(m_nozzle_diamenter_label, 0, wxALIGN_CENTRE_HORIZONTAL | wxBOTTOM | wxTOP, FromDIP(5)); + + m_nozzle_flowtype_label = new Label(this); + m_nozzle_flowtype_label->SetFont(Label::Body_13); + m_nozzle_flowtype_label->SetBackgroundColour(*wxWHITE); + mainSizer->Add(m_nozzle_flowtype_label, 0, wxALIGN_CENTRE_HORIZONTAL); + mainSizer->AddStretchSpacer(); + + // Set sizer + SetSizer(mainSizer); + SetMaxSize(WX_DIP_SIZE(132, -1)); + SetMinSize(WX_DIP_SIZE(132, -1)); + SetSize(WX_DIP_SIZE(132, -1)); +} + +void wgtDeviceNozzleRackToolHead::UpdateToolHeadInfo(const DevNozzle& extruder_nozzle) +{ + /* Labels */ + if (extruder_nozzle.IsEmpty()) + { + m_nozzle_diamenter_label->Show(false); + m_nozzle_flowtype_label->SetLabel(_L("Empty")); + } + else if (extruder_nozzle.IsUnknown()) + { + m_nozzle_diamenter_label->Show(false); + m_nozzle_flowtype_label->SetLabel(_L("Unknown")); + } + else if (extruder_nozzle.IsAbnormal()) + { + m_nozzle_diamenter_label->Show(false); + m_nozzle_flowtype_label->SetLabel(_L("Error")); + } + else /*extruder_nozzle.IsNormal()*/ + { + m_nozzle_diamenter_label->Show(true); + m_nozzle_diamenter_label->SetLabel(extruder_nozzle.GetNozzleDiameterStr()); + m_nozzle_flowtype_label->SetLabel(extruder_nozzle.GetNozzleFlowTypeStr()); + } + + /* Icon*/ + bool extruder_exist = !extruder_nozzle.IsEmpty(); + if (m_extruder_nozzle_exist != extruder_exist) + { + m_extruder_nozzle_exist = extruder_exist; + m_filament_color = extruder_nozzle.GetFilamentColor(); + m_toolhead_icon->SetBitmap(m_extruder_nozzle_exist ? SetNozzleBmpColor(m_extruder_nozzle_normal->bmp(), m_filament_color) : m_extruder_nozzle_empty->bmp()); + m_toolhead_icon->Refresh(); + } +} + +void wgtDeviceNozzleRackToolHead::Rescale() +{ + m_extruder_nozzle_normal->msw_rescale(); + m_extruder_nozzle_empty->msw_rescale(); + m_toolhead_icon->SetBitmap(m_extruder_nozzle_exist ? SetNozzleBmpColor(m_extruder_nozzle_normal->bmp(), m_filament_color) : m_extruder_nozzle_empty->bmp()); + + Layout(); + Refresh(); +} + +void wgtDeviceNozzleRackArea::CreateGui() +{ + wxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + + // Create Header + m_title_nozzle_rack = new wgtDeviceNozzleRackTitle(this, _L("Induction Hotend Rack")); + main_sizer->Add(m_title_nozzle_rack, 0, wxEXPAND | wxTOP); + + // Create Simple Book + m_simple_book = new wxSimplebook(this, wxID_ANY); + + wxSizer* content_sizer = new wxBoxSizer(wxVERTICAL); + + m_panel_content = new wxPanel(m_simple_book, wxID_ANY); + m_panel_refresh = new wxPanel(m_simple_book, wxID_ANY); + + // Create Hotends ans Rack Position Panel + wxSizer* hotends_rack_sizer = new wxBoxSizer(wxHORIZONTAL); + + // Hotends + m_hotends_sizer = new wxBoxSizer(wxVERTICAL); + m_arow_nozzles_box = CreateNozzleBox( { 0, 2, 4}); + m_brow_nozzles_box = CreateNozzleBox( { 1, 3, 5}); + m_hotends_sizer->Add(m_arow_nozzles_box); + m_hotends_sizer->Add(m_brow_nozzles_box); + hotends_rack_sizer->Add(m_hotends_sizer, 0, wxLEFT, FromDIP(8)); + + // Rack + m_rack_pos_panel = new wgtDeviceNozzleRackPos(m_panel_content); + hotends_rack_sizer->Add(m_rack_pos_panel, 0, wxEXPAND); + content_sizer->Add(hotends_rack_sizer, 0); + + wxSizer* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + m_btn_hotends_infos = new Button(m_panel_content, _L("Hotends Info")); + m_btn_hotends_infos->SetFont(Label::Body_12); + m_btn_hotends_infos->SetBackgroundColor(s_button_style_gray()); + m_btn_hotends_infos->SetBackgroundColour(*wxWHITE); + m_btn_hotends_infos->Bind(wxEVT_BUTTON, &wgtDeviceNozzleRackArea::OnBtnHotendsInfos, this); + + m_btn_read_all = new Button(m_panel_content, _L("Read All")); + m_btn_read_all->SetFont(Label::Body_12); + m_btn_read_all->SetBackgroundColor(s_button_style_gray()); + m_btn_read_all->SetBackgroundColour(*wxWHITE); + m_btn_read_all->Bind(wxEVT_BUTTON, &wgtDeviceNozzleRackArea::OnBtnReadAll, this); + + btn_sizer->Add(m_btn_hotends_infos, 0, wxLEFT); + btn_sizer->Add(m_btn_read_all, 0, wxLEFT, FromDIP(5)); + content_sizer->Add(btn_sizer, 0, wxLEFT, FromDIP(10)); + + /* refresh panel */ + wxSizer* refresh_sizer = CreateRefreshBook(m_panel_refresh); + + m_panel_content->SetSizer(content_sizer); + m_panel_refresh->SetSizer(refresh_sizer); + m_simple_book->AddPage(m_panel_content, "Content"); + m_simple_book->AddPage(m_panel_refresh, "Refresh"); + main_sizer->Add(m_simple_book, 1, wxEXPAND); + + m_simple_book->SetSelection(0); + + SetSizer(main_sizer); + Layout(); + Fit(); +} + +wxSizer* wgtDeviceNozzleRackArea::CreateRefreshBook(wxPanel* parent) +{ + wxSizer* refresh_sizer = new wxBoxSizer(wxVERTICAL); + + std::vector list{"ams_rfid_1", "ams_rfid_2", "ams_rfid_3", "ams_rfid_4"}; + m_refresh_icon = new AnimaIcon(parent, wxID_ANY, list, "refresh_printer", 100); + m_refresh_icon->SetMinSize(wxSize(FromDIP(25), FromDIP(25))); + + wxSizer* progress_sizer = new wxBoxSizer(wxHORIZONTAL); + + Label* progress_prefix = new Label(parent, _L("Reading ")); + progress_prefix->SetBackgroundColour(*wxWHITE); + m_progress_refresh = new Label(parent, "(1/6)"); + m_progress_refresh->SetFont(Label::Body_14); + m_progress_refresh->SetBackgroundColour(*wxWHITE); + m_progress_refresh->SetForegroundColour(*wxGREEN); + Label* progress_suffix = new Label(parent, " ..."); + progress_suffix->SetBackgroundColour(*wxWHITE); + + progress_sizer->Add(progress_prefix, 0, wxLEFT); + progress_sizer->Add(m_progress_refresh, 0, wxLEFT); + progress_sizer->Add(progress_suffix, 0, wxLEFT); + + Label* refresh_tip = new Label(parent, _L("Please wait")); + refresh_tip->SetBackgroundColour(*wxWHITE); + + refresh_sizer->Add(0, 0, 1, wxEXPAND, 0); + refresh_sizer->Add(m_refresh_icon, 0, wxALIGN_CENTER_HORIZONTAL, 0); + refresh_sizer->Add(progress_sizer, 0, wxALIGN_CENTER_HORIZONTAL, FromDIP(0)); + refresh_sizer->Add(refresh_tip, 0, wxALIGN_CENTER_HORIZONTAL, FromDIP(0)); + refresh_sizer->Add(0, 0, 1, wxEXPAND, 0); + + return refresh_sizer; +} + +StaticBox* wgtDeviceNozzleRackArea::CreateNozzleBox(const std::vector nozzle_idxes) +{ + StaticBox* nozzle_box = new StaticBox(m_panel_content); + nozzle_box->SetBackgroundColor(*wxWHITE); + nozzle_box->SetBorderColor(*wxWHITE); + nozzle_box->SetCornerRadius(0); + + wxSizer* h_sizer = new wxBoxSizer(wxHORIZONTAL); + for (auto start_idx : nozzle_idxes) + { + wgtDeviceNozzleRackNozzleItem* nozzle_item = new wgtDeviceNozzleRackNozzleItem(nozzle_box, start_idx); + m_nozzle_items[start_idx] = nozzle_item; + h_sizer->Add(nozzle_item, 0, wxALL, FromDIP(8)); + } + + nozzle_box->SetSizer(h_sizer); + return nozzle_box; +} + +void wgtDeviceNozzleRackArea::UpdateNozzleItems(const std::unordered_map& nozzle_items, + std::shared_ptr nozzle_rack) +{ + for (auto iter : nozzle_items) + { + iter.second->Update(nozzle_rack); + } + + /*update nozzle possition and background*/ + if (nozzle_rack->GetReadingCount() != 0) + { + m_progress_refresh->SetLabel(wxString::Format("(%d/%d)", nozzle_rack->GetReadingIdx(), nozzle_rack->GetReadingCount())); + if(!m_refresh_icon->IsPlaying()) { + m_simple_book->SetSelection(1); + m_refresh_icon->Play(); + } + return; + } else{ + m_refresh_icon->Stop(); + m_simple_book->SetSelection(0); + } + + const DevNozzleRack::RackPos new_pos = nozzle_rack->GetPosition(); + const DevNozzleRack::RackStatus new_status = nozzle_rack->GetStatus(); + if (m_rack_pos != new_pos || m_rack_status != new_status) + { + m_rack_pos = new_pos; + m_rack_status = new_status; + if (m_rack_status == DevNozzleRack::RACK_STATUS_IDLE) + { + m_hotends_sizer->Clear(); + if (m_rack_pos == DevNozzleRack::RACK_POS_B_TOP) + { + m_hotends_sizer->Add(m_brow_nozzles_box); + m_hotends_sizer->Add(m_arow_nozzles_box); + } + else if (m_rack_pos == DevNozzleRack::RACK_POS_A_TOP) + { + m_hotends_sizer->Add(m_arow_nozzles_box); + m_hotends_sizer->Add(m_brow_nozzles_box); + } + else + { + m_hotends_sizer->Add(m_arow_nozzles_box); + m_hotends_sizer->Add(m_brow_nozzles_box); + } + } + } +} + +void wgtDeviceNozzleRackArea::UpdateRackInfo(std::weak_ptr rack) +{ + m_nozzle_rack = rack; + const auto& nozzle_rack = rack.lock(); + if (nozzle_rack) + { + UpdateNozzleItems(m_nozzle_items, nozzle_rack); + m_rack_pos_panel->UpdateRackPos(nozzle_rack); + m_btn_read_all->Enable(nozzle_rack->CtrlCanReadAll()); + } + + if (m_rack_upgrade_dlg && m_rack_upgrade_dlg->IsShown()) + { + m_rack_upgrade_dlg->UpdateRackInfo(nozzle_rack); + } +}; + +void wgtDeviceNozzleRackArea::OnBtnHotendsInfos(wxCommandEvent& evt) +{ + const auto& nozzle_rack = m_nozzle_rack.lock(); + if (nozzle_rack) + { + m_rack_upgrade_dlg = new wgtDeviceNozzleRackUpgradeDlg((wxWindow*)wxGetApp().mainframe, nozzle_rack); + m_rack_upgrade_dlg->ShowModal(); + + delete m_rack_upgrade_dlg; + m_rack_upgrade_dlg = nullptr; + } + + evt.Skip(); +} + +void wgtDeviceNozzleRackArea::OnBtnReadAll(wxCommandEvent& evt) +{ + if (const auto nozzle_rack = m_nozzle_rack.lock()) + { + nozzle_rack->CtrlRackReadAll(true); + } + + evt.Skip(); +} + +void wgtDeviceNozzleRackArea::Rescale() +{ + for (auto item : m_nozzle_items) + { + item.second->Rescale(); + } + + m_rack_pos_panel->Rescale(); + m_btn_hotends_infos->Rescale(); + m_btn_read_all->Rescale(); +} + +static void s_set_bg_style(StaticBox* box, + ScalableButton* btn, + Label* label_row, + Label* label_row_status, + const wxColour& clr) +{ + box->SetBorderColor(clr); + box->SetBackgroundColor(clr); + btn->SetBackgroundColour(clr); + label_row->SetBackgroundColour(clr); + label_row_status->SetBackgroundColour(clr); +} + +void wgtDeviceNozzleRackPos::CreateGui() +{ + // RowA + m_rowup_panel = new StaticBox(this, wxID_ANY); + m_rowup_panel->SetCornerRadius(0); + + wxBoxSizer* rowa_sizer = new wxBoxSizer(wxVERTICAL); + rowa_sizer->AddStretchSpacer(); + m_btn_rowup = new ScalableButton(m_rowup_panel, wxID_ANY, "dev_rack_row_up", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 25); + m_btn_rowup->Bind(wxEVT_ENTER_WINDOW, [this](auto&) { SetCursor(wxCURSOR_HAND); }); + m_btn_rowup->Bind(wxEVT_LEAVE_WINDOW, [this](auto&) { SetCursor(wxCURSOR_ARROW); }); + m_btn_rowup->Bind(wxEVT_BUTTON, &wgtDeviceNozzleRackPos::OnMoveRackUp, this); + rowa_sizer->Add(m_btn_rowup, 0, wxALIGN_CENTER | wxEXPAND | wxLEFT | wxRIGHT, FromDIP(10)); + + m_label_rowup_status = new Label(m_rowup_panel); + m_label_rowup_status->SetFont(Label::Body_12); + m_label_rowup_status->Show(false); + rowa_sizer->Add(m_label_rowup_status, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(10)); + + m_label_rowup = new Label(m_rowup_panel); + m_label_rowup->SetFont(Label::Body_14); + rowa_sizer->Add(m_label_rowup, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(10)); + rowa_sizer->AddStretchSpacer(); + + m_rowup_panel->SetSizer(rowa_sizer); + + // homing + m_btn_homing = new ScalableButton(this, wxID_ANY, "dev_rack_home", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 25); + m_btn_homing->SetBackgroundColour(WGT_GREY200); + m_btn_homing->Bind(wxEVT_ENTER_WINDOW, [this](auto&) { SetCursor(wxCURSOR_HAND); }); + m_btn_homing->Bind(wxEVT_LEAVE_WINDOW, [this](auto&) { SetCursor(wxCURSOR_ARROW); }); + m_btn_homing->Bind(wxEVT_BUTTON, &wgtDeviceNozzleRackPos::OnBtnHomingRack, this); + + // Row B + m_rowbottom_panel = new StaticBox(this, wxID_ANY); + m_rowbottom_panel->SetCornerRadius(0); + + wxBoxSizer* rowb_sizer = new wxBoxSizer(wxVERTICAL); + rowb_sizer->AddStretchSpacer(); + + m_btn_rowbottom_up = new ScalableButton(m_rowbottom_panel, wxID_ANY, "dev_rack_row_up", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 25); + m_btn_rowbottom_up->Bind(wxEVT_BUTTON, &wgtDeviceNozzleRackPos::OnMoveRackDown, this); + m_btn_rowbottom_up->Bind(wxEVT_ENTER_WINDOW, [this](auto&) { SetCursor(wxCURSOR_HAND); }); + m_btn_rowbottom_up->Bind(wxEVT_LEAVE_WINDOW, [this](auto&) { SetCursor(wxCURSOR_ARROW); }); + rowb_sizer->Add(m_btn_rowbottom_up, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(10)); + + m_label_rowbottom_status = new Label(m_rowbottom_panel); + m_label_rowbottom_status->SetFont(Label::Body_12); + m_label_rowbottom_status->Show(false); + rowb_sizer->Add(m_label_rowbottom_status, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(10)); + + m_label_rowbottom = new Label(m_rowbottom_panel); + m_label_rowbottom->SetFont(Label::Body_14); + rowb_sizer->Add(m_label_rowbottom, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(10)); + rowb_sizer->AddStretchSpacer(); + + m_rowbottom_panel->SetSizer(rowb_sizer); + + // bg style + SetBackgroundColour(*wxWHITE); + s_set_bg_style(m_rowup_panel, m_btn_rowup, m_label_rowup, m_label_rowup_status, *wxWHITE); + s_set_bg_style(m_rowbottom_panel, m_btn_rowbottom_up, m_label_rowbottom, m_label_rowbottom_status, *wxWHITE); + + // main sizer + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + main_sizer->Add(m_rowup_panel, 1, wxALIGN_TOP | wxEXPAND | wxALIGN_CENTER); + main_sizer->Add(m_btn_homing, 0, wxALIGN_CENTER | wxTOP | wxBOTTOM, FromDIP(10)); + main_sizer->Add(m_rowbottom_panel, 1, wxALIGN_BOTTOM | wxEXPAND | wxALIGN_CENTER); + SetSizer(main_sizer); + + SetMinSize(WX_DIP_SIZE(85, -1)); + + Layout(); + Fit(); +} + +void wgtDeviceNozzleRackPos::UpdateRackPos(const std::shared_ptr& rack) +{ + m_rack = rack; + if (rack) + { + UpdateRackPos(rack->GetPosition(), rack->GetStatus(), rack->GetReadingCount() > 0); + } +} + +static void s_show_label(Label* label, const wxString& text) +{ + label->SetLabel(text); + label->Show(); +} + +static void s_show_label(Label* label, const wxString& text, const wxColour& text_color) +{ + label->SetLabel(text); + label->SetForegroundColour(StateColor::darkModeColorFor(text_color)); + label->Show(); +} + +void wgtDeviceNozzleRackPos::UpdateRackPos(DevNozzleRack::RackPos new_pos, + DevNozzleRack::RackStatus new_status, bool is_reading) +{ + // While reading, both rows show a "Running..." status and the move buttons are hidden. + if (is_reading) + { + s_show_label(m_label_rowup, L_RAW_A_STR, *wxBLACK); + s_show_label(m_label_rowup_status, _L("Running...")); + + s_show_label(m_label_rowbottom, L_RAW_B_STR, *wxBLACK); + s_show_label(m_label_rowbottom_status, _L("Running...")); + + m_btn_rowup->Show(false); + m_btn_rowbottom_up->Show(false); + + m_rack_pos = DevNozzleRack::RACK_POS_UNKNOWN; + m_rack_status = DevNozzleRack::RACK_STATUS_UNKNOWN; + return; + } + + if (new_pos != m_rack_pos || m_rack_status != new_status) + { + m_rack_pos = new_pos; + m_rack_status = new_status; + + if (m_rack_status != DevNozzleRack::RACK_STATUS_IDLE) + { + s_show_label(m_label_rowup, L_RAW_A_STR, *wxBLACK); + s_show_label(m_label_rowup_status, _L("Running...")); + + s_show_label(m_label_rowbottom, L_RAW_B_STR, *wxBLACK); + s_show_label(m_label_rowbottom_status, _L("Running...")); + + m_btn_rowup->Show(false); + m_btn_rowbottom_up->Show(false); + } + else + { + if (new_pos == DevNozzleRack::RACK_POS_A_TOP) + { + s_show_label(m_label_rowup, L_RAW_A_STR, s_hgreen_clr); + s_show_label(m_label_rowup_status, _L("Raised")); + + m_rowbottom_panel->SetBorderColor(*wxWHITE); + m_rowbottom_panel->SetBackgroundColor(*wxWHITE); + s_show_label(m_label_rowbottom, L_RAW_B_STR, *wxBLACK); + m_label_rowbottom_status->Show(false); + + m_btn_rowup->Show(false); + m_btn_rowbottom_up->Show(true); + } + else if (new_pos == DevNozzleRack::RACK_POS_B_TOP) + { + s_show_label(m_label_rowup, L_RAW_B_STR, s_hgreen_clr); + s_show_label(m_label_rowup_status, _L("Raised")); + s_show_label(m_label_rowbottom, L_RAW_A_STR, *wxBLACK); + m_label_rowbottom_status->Show(false); + + m_btn_rowup->Show(false); + m_btn_rowbottom_up->Show(true); + } + else + { + s_show_label(m_label_rowup, L_RAW_A_STR, *wxBLACK); + m_label_rowup_status->Show(false); + + s_show_label(m_label_rowbottom, L_RAW_B_STR, *wxBLACK); + m_label_rowbottom_status->Show(false); + + m_btn_rowup->Show(true); + m_btn_rowbottom_up->Show(true); + } + } + + Layout(); + Refresh(); + } +}; + +void wgtDeviceNozzleRackPos::OnMoveRackUp(wxCommandEvent& evt) +{ + auto rack = m_rack.lock(); + if (rack) + { + if (m_label_rowup->GetLabel() == L_RAW_A_STR) + { + rack->CtrlRackPosMove(DevNozzleRack::RACK_POS_A_TOP); + } + else if (m_label_rowup->GetLabel() == L_RAW_B_STR) + { + rack->CtrlRackPosMove(DevNozzleRack::RACK_POS_B_TOP); + } + } + evt.Skip(); +} + +void wgtDeviceNozzleRackPos::OnMoveRackDown(wxCommandEvent& evt) +{ + auto rack = m_rack.lock(); + if (rack) + { + if (m_label_rowbottom->GetLabel() == L_RAW_A_STR) + { + rack->CtrlRackPosMove(DevNozzleRack::RACK_POS_A_TOP); + } + else if (m_label_rowbottom->GetLabel() == L_RAW_B_STR) + { + rack->CtrlRackPosMove(DevNozzleRack::RACK_POS_B_TOP); + } + } + evt.Skip(); +} + +void wgtDeviceNozzleRackPos::OnBtnHomingRack(wxCommandEvent& evt) +{ + if (auto rack = m_rack.lock()) + { + rack->CtrlRackPosGoHome(); + } + evt.Skip(); +} + +void wgtDeviceNozzleRackPos::Rescale() +{ + m_btn_rowup->msw_rescale(); + m_btn_rowbottom_up->msw_rescale(); + m_btn_homing->msw_rescale(); +} + +};// end of namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h new file mode 100644 index 0000000000..c4fa464e05 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h @@ -0,0 +1,196 @@ +//**********************************************************/ +/* File: wgtDeviceNozzleRack.h +* Description: The Device-tab panel with the toolhead nozzle and the H2C induction hotend rack. +* +* \n class wgtDeviceNozzleRack; // toolhead panel + rack area, side by side +* \n class wgtDeviceNozzleRackToolHead; +* \n class wgtDeviceNozzleRackArea; +* \n class wgtDeviceNozzleRackPos; +* +* The single-nozzle tile class wgtDeviceNozzleRackNozzleItem lives in its own translation unit +* (wgtDeviceNozzleRackNozzleItem.{h,cpp}), so it is included here rather than redeclared. +* The Hotends-Info upgrade dialog (wgtDeviceNozzleRackUpgradeDlg) is intentionally not wired yet +* (see the TODO markers in the .cpp). +//**********************************************************/ + +#pragma once +#include "slic3r/GUI/DeviceCore/DevNozzleRack.h" + +#include "wgtDeviceNozzleRackNozzleItem.h" + +#include "slic3r/GUI/Widgets/StaticBox.hpp" +#include "slic3r/GUI/Widgets/AnimaController.hpp" + +#include +#include +#include +#include +#include + +// Previous definitions +class Button; +class Label; +class ScalableBitmap; +class ScalableButton; +namespace Slic3r +{ + struct DevNozzle; + class DevNozzleRack; +namespace GUI +{ + class wgtDeviceNozzleRackArea; + class wgtDeviceNozzleRackNozzleItem; + class wgtDeviceNozzleRackToolHead; + class wgtDeviceNozzleRackPos; + class wgtDeviceNozzleRackTitle; + class wgtDeviceNozzleRackUpgradeDlg; +} +}; + +namespace Slic3r::GUI +{ +class wgtDeviceNozzleRack : public wxPanel +{ +public: + wgtDeviceNozzleRack(wxWindow* parent, + wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxTAB_TRAVERSAL); + ~wgtDeviceNozzleRack() = default; + +public: + void UpdateRackInfo(std::shared_ptr rack); + void Rescale(); + +private: + void CreateGui(); + +private: + std::weak_ptr m_nozzle_rack; + + // GUI + wgtDeviceNozzleRackToolHead* m_toolhead_panel{ nullptr }; + wgtDeviceNozzleRackArea* m_rack_area{ nullptr }; +}; + + +class wgtDeviceNozzleRackToolHead : public wxPanel +{ +public: + wgtDeviceNozzleRackToolHead(wxWindow* parent) : wxPanel(parent) { CreateGui();} + +public: + void UpdateToolHeadInfo(const DevNozzle& extruder_nozzle); + void Rescale(); + +private: + void CreateGui(); + +private: + bool m_extruder_nozzle_exist = false; + std::string m_filament_color; + + // GUI + ScalableBitmap* m_extruder_nozzle_normal = nullptr; + ScalableBitmap* m_extruder_nozzle_empty = nullptr; + wxStaticBitmap* m_toolhead_icon; + + Label* m_nozzle_diamenter_label; + Label* m_nozzle_flowtype_label; +}; + + +class wgtDeviceNozzleRackArea : public wxPanel +{ +public: + wgtDeviceNozzleRackArea(wxWindow* parent) : wxPanel(parent) { CreateGui();} + +public: + void UpdateRackInfo(std::weak_ptr rack); + void Rescale(); + +private: + void CreateGui(); + StaticBox* CreateNozzleBox(const std::vector nozzle_idxes); + wxSizer* CreateRefreshBook(wxPanel* parent); + + // updates + void UpdateNozzleItems(const std::unordered_map& nozzle_items, + std::shared_ptr nozzle_rack); + + // events + void OnBtnHotendsInfos(wxCommandEvent& evt); + void OnBtnReadAll(wxCommandEvent& evt); + +private: + std::weak_ptr m_nozzle_rack; + DevNozzleRack::RackPos m_rack_pos = DevNozzleRack::RACK_POS_UNKNOWN; + DevNozzleRack::RackStatus m_rack_status = DevNozzleRack::RACK_STATUS_UNKNOWN; + + // GUI + wxSimplebook* m_simple_book{ nullptr }; + wxPanel* m_panel_content{ nullptr }; + wxPanel* m_panel_refresh{ nullptr }; + + wgtDeviceNozzleRackTitle* m_title_nozzle_rack; + wxBoxSizer* m_hotends_sizer; + StaticBox* m_arow_nozzles_box; + StaticBox* m_brow_nozzles_box; + std::unordered_map m_nozzle_items; + + wgtDeviceNozzleRackPos* m_rack_pos_panel; + + Button* m_btn_hotends_infos; + Button* m_btn_read_all; + + /* refresh book */ + Label* m_progress_refresh{ nullptr }; + AnimaIcon* m_refresh_icon{ nullptr }; + + // "Hotends Info" upgrade dialog. Owned for its ShowModal lifetime by + // OnBtnHotendsInfos; UpdateRackInfo forwards live device pushes to it while it is shown. + wgtDeviceNozzleRackUpgradeDlg* m_rack_upgrade_dlg = nullptr; +}; + +class wgtDeviceNozzleRackPos : public wxPanel +{ +public: + explicit wgtDeviceNozzleRackPos(wxWindow* parent) : wxPanel(parent) { CreateGui();} + +public: + void UpdateRackPos(const std::shared_ptr& rack); + void Rescale(); + +private: + void CreateGui(); + + void UpdateRackPos(DevNozzleRack::RackPos new_pos, + DevNozzleRack::RackStatus new_status, + bool is_reading); + + // events + void OnMoveRackUp(wxCommandEvent& evt); + void OnMoveRackDown(wxCommandEvent& evt); + void OnBtnHomingRack(wxCommandEvent& evt); + +private: + std::weak_ptr m_rack; + DevNozzleRack::RackPos m_rack_pos = DevNozzleRack::RACK_POS_UNKNOWN; + DevNozzleRack::RackStatus m_rack_status = DevNozzleRack::RACK_STATUS_UNKNOWN; + + // GUI + StaticBox* m_rowup_panel; + ScalableButton* m_btn_rowup; + Label* m_label_rowup_status{ nullptr }; + Label* m_label_rowup{ nullptr }; + + StaticBox* m_rowbottom_panel; + ScalableButton* m_btn_rowbottom_up; + Label* m_label_rowbottom_status{ nullptr }; + Label* m_label_rowbottom{ nullptr }; + + ScalableButton* m_btn_homing{ nullptr }; +}; + +};// end of namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.cpp new file mode 100644 index 0000000000..23c69bf6e2 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.cpp @@ -0,0 +1,347 @@ +//**********************************************************/ +/* File: wgtDeviceNozzleRackNozzleItem.cpp +* Description: One nozzle cell of the H2C hotend rack view. Holds the +* wgtDeviceNozzleRackNozzleItem widget, the SetNozzleBmpColor helper and the layout +* constants it needs. +//**********************************************************/ + +#include "wgtDeviceNozzleRackNozzleItem.h" + +#include "slic3r/GUI/DeviceCore/DevNozzleSystem.h" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/wxExtensions.hpp" + +#include "slic3r/GUI/Widgets/Label.hpp" + +#define WX_DIP_SIZE_46 wxSize(FromDIP(46), FromDIP(46)) +#define WX_DIP_SIZE(x, y) wxSize(FromDIP(x), FromDIP(y)) + +#define WGT_RACK_NOZZLE_SIZE WX_DIP_SIZE(88, 100) + +static wxColour s_gray_clr("#B0B0B0"); +static wxColour s_hgreen_clr("#00AE42"); +static wxColour s_red_clr("#D01B1B"); + +wxDEFINE_EVENT(EVT_NOZZLE_RACK_NOZZLE_ITEM_SELECTED, wxCommandEvent); + +namespace Slic3r::GUI +{ + +static wxBitmap SetNozzleBmpColor(const wxBitmap& bmp, const std::string& color_str) { + if(color_str.empty()) return bmp; + + wxImage img = bmp.ConvertToImage(); + wxColour color("#" + color_str); + + for (int y = 0; y < img.GetHeight(); ++y) { + for (int x = 0; x < img.GetWidth(); ++x) { + unsigned char r = img.GetRed(x, y); + unsigned char g = img.GetGreen(x, y); + unsigned char b = img.GetBlue(x, y); + + /*replace yellow with color*/ + if ( r >= 180 && g >= 180 && b <= 150) { + img.SetRGB(x, y, color.Red(), color.Green(), color.Blue()); + } + } + } + + return wxBitmap(img, -1, bmp.GetScaleFactor()); +} + +wgtDeviceNozzleRackNozzleItem::wgtDeviceNozzleRackNozzleItem(wxWindow* parent, int nozzle_id) + : StaticBox(parent, wxID_ANY), m_nozzle_id(nozzle_id) +{ + CreateGui(); +} + +void wgtDeviceNozzleRackNozzleItem::CreateGui() +{ + // Background + SetCornerRadius(FromDIP(5)); + SetBackgroundColor(*wxWHITE); + + // Top H + wxSizer *top_h_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_nozzle_label_id = new Label(this); + m_nozzle_label_id->SetFont(Label::Body_12); + m_nozzle_label_id->SetBackgroundColour(*wxWHITE); + m_nozzle_label_id->SetLabel(wxString::Format("%d", m_nozzle_id + 1)); + + m_status = NOZZLE_STATUS::NOZZLE_EMPTY; + m_nozzle_empty_image = new ScalableBitmap(this, "dev_rack_nozzle_empty", 46); + m_nozzle_icon = new wxStaticBitmap(this, wxID_ANY, m_nozzle_empty_image->bmp(), wxDefaultPosition, WX_DIP_SIZE_46); + m_nozzle_icon->SetBackgroundColour(*wxWHITE); + + m_nozzle_selected_bitmap = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap, wxDefaultPosition, WX_DIP_SIZE(20, 20)); + m_nozzle_selected_bitmap->SetBackgroundColour(*wxWHITE); + + top_h_sizer->Add(m_nozzle_label_id, 0, wxTOP | wxLEFT, FromDIP(6)); + top_h_sizer->AddStretchSpacer(1); + top_h_sizer->Add(m_nozzle_icon, 0, wxTOP, FromDIP(10)); + top_h_sizer->AddStretchSpacer(1); + top_h_sizer->Add(m_nozzle_selected_bitmap, 0, wxTOP | wxRIGHT, FromDIP(2)); + + // Bottom V + wxBoxSizer* bottom_v = new wxBoxSizer(wxVERTICAL); + + wxSizer* label_h_sizer = new wxBoxSizer(wxHORIZONTAL); + m_nozzle_label_1 = new Label(this); + m_nozzle_label_1->SetFont(Label::Body_12); + m_nozzle_label_1->SetBackgroundColour(*wxWHITE); + m_nozzle_label_1->SetLabel(_L("Empty")); + + label_h_sizer->Add(m_nozzle_label_1, 0, wxALIGN_LEFT); + + auto status_icon = create_scaled_bitmap("dev_rack_nozzle_error_icon", this, 14); + m_nozzle_status_icon = new wxStaticBitmap(this, wxID_ANY, status_icon, wxDefaultPosition, WX_DIP_SIZE(14, 14)); + m_nozzle_status_icon->Bind(wxEVT_LEFT_DOWN, &wgtDeviceNozzleRackNozzleItem::OnBtnNozzleStatus, this); + m_nozzle_status_icon->Bind(wxEVT_ENTER_WINDOW, [this](auto&) { SetCursor(wxCURSOR_HAND); }); + m_nozzle_status_icon->Bind(wxEVT_LEAVE_WINDOW, [this](auto&) { SetCursor(wxCURSOR_ARROW); }); + m_nozzle_status_icon->SetBackgroundColour(*wxWHITE); + m_nozzle_status_icon->Show(false); + + label_h_sizer->Add(m_nozzle_status_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(2)); + bottom_v->Add(label_h_sizer, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(2)); + + m_nozzle_label_2 = new Label(this); + m_nozzle_label_2->SetFont(Label::Body_12); + m_nozzle_label_2->SetBackgroundColour(*wxWHITE); + bottom_v->Add(m_nozzle_label_2, 0, wxALIGN_CENTER_HORIZONTAL); + + // Main sizer + wxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + main_sizer->Add(top_h_sizer, 0, wxEXPAND); + main_sizer->Add(bottom_v, 0, wxALIGN_CENTER_HORIZONTAL); + SetSizer(main_sizer); + + SetMinSize(WGT_RACK_NOZZLE_SIZE); + SetMaxSize(WGT_RACK_NOZZLE_SIZE); + SetSize(WGT_RACK_NOZZLE_SIZE); + Layout(); +}; + +void wgtDeviceNozzleRackNozzleItem::SetSelected(bool selected) +{ + if (!m_enable_select){ + assert(false && "not support select"); + return; + } + + if (m_is_selected != selected) { + m_is_selected = selected; + if (selected) { + if (!m_nozzle_selected_image) { + m_nozzle_selected_image = new ScalableBitmap(this, "dev_rack_nozzle_selected", 20); + } + + m_nozzle_selected_bitmap->SetBitmap(m_nozzle_selected_image->bmp()); + SetBorderColor(StateColor::darkModeColorFor(s_hgreen_clr)); + } else { + m_nozzle_selected_bitmap->SetBitmap(wxNullBitmap); + SetBorderColor(StateColor::darkModeColorFor(s_gray_clr)); + } + + Refresh(); + } +} + +void wgtDeviceNozzleRackNozzleItem::Update(const std::shared_ptr rack, bool on_rack /*= true*/) +{ + m_rack = rack; + + if (rack) { + const auto &nozzle_info = on_rack ? rack->GetNozzle(m_nozzle_id) : rack->GetNozzleSystem()->GetExtNozzle(m_nozzle_id); + const wxString &diameter_str = nozzle_info.GetNozzleDiameterStr(); + const wxString &flowtype_str = nozzle_info.GetNozzleFlowTypeStr(); + const std::string &color = nozzle_info.GetFilamentColor(); + + /*check empty first*/ + if (nozzle_info.IsEmpty()) { + SetNozzleStatus(NOZZLE_STATUS::NOZZLE_EMPTY, _L("Empty"), wxEmptyString, color); + } else if (nozzle_info.IsNormal()) { + SetNozzleStatus(NOZZLE_STATUS::NOZZLE_NORMAL, diameter_str, flowtype_str, color); + } else if (nozzle_info.IsAbnormal()) { + SetNozzleStatus(NOZZLE_STATUS::NOZZLE_ERROR, _L("Error"), wxEmptyString, color); + } else if (nozzle_info.IsUnknown()) { + SetNozzleStatus(NOZZLE_STATUS::NOZZLE_UNKNOWN, _L("Unknown"), wxEmptyString, color); + } + } +} + +void wgtDeviceNozzleRackNozzleItem::SetNozzleStatus(NOZZLE_STATUS status, const wxString& str1, const wxString& str2, const std::string& color) +{ + if (m_status != status || m_filament_color != color) + { + m_status = status; + m_filament_color = color; + switch (status) + { + case Slic3r::GUI::wgtDeviceNozzleRackNozzleItem::NOZZLE_EMPTY: + { + if (!m_nozzle_empty_image) { m_nozzle_empty_image = new ScalableBitmap(this, "dev_rack_nozzle_empty", 46);} + m_nozzle_icon->SetBitmap(m_nozzle_empty_image->bmp()); + break; + } + case Slic3r::GUI::wgtDeviceNozzleRackNozzleItem::NOZZLE_NORMAL: + { + if (!m_nozzle_normal_image) { m_nozzle_normal_image = new ScalableBitmap(this, "dev_rack_nozzle_normal", 46);} + m_nozzle_icon->SetBitmap(SetNozzleBmpColor(m_nozzle_normal_image->bmp(), m_filament_color)); + break; + } + case Slic3r::GUI::wgtDeviceNozzleRackNozzleItem::NOZZLE_UNKNOWN: + { + if (!m_nozzle_unknown_image) { m_nozzle_unknown_image = new ScalableBitmap(this, "dev_rack_nozzle_unknown", 46);} + m_nozzle_icon->SetBitmap(m_nozzle_unknown_image->bmp()); + break; + } + case Slic3r::GUI::wgtDeviceNozzleRackNozzleItem::NOZZLE_ERROR: + { + if (!m_nozzle_error_image) { m_nozzle_error_image = new ScalableBitmap(this, "dev_rack_nozzle_error", 46);} + m_nozzle_icon->SetBitmap(m_nozzle_error_image->bmp()); + break; + } + default: + { + break; + } + } + + if (status == wgtDeviceNozzleRackNozzleItem::NOZZLE_ERROR) + { + m_nozzle_label_1->SetForegroundColour(StateColor::darkModeColorFor(s_red_clr)); + m_nozzle_status_icon->Show(true); + } + else + { + m_nozzle_label_1->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); + m_nozzle_status_icon->Show(false); + } + } + + bool update_layout = (m_nozzle_label_1->GetLabel() != str1 || m_nozzle_label_2->GetLabel() != str2); + m_nozzle_label_1->SetLabel(str1); + m_nozzle_label_2->SetLabel(str2); + + if (update_layout) { + Layout(); + } +} + +void wgtDeviceNozzleRackNozzleItem::OnBtnNozzleStatus(wxMouseEvent& evt) +{ + if (m_is_disabled) { + return; + } + + auto rack = m_rack.lock(); + if (rack && m_status == wgtDeviceNozzleRackNozzleItem::NOZZLE_ERROR) + { + // Orca: show the abnormal-hotend warning as an informational dialog only, with no + // "Jump to the upgrade page" button, since Orca's MessageDialog and device Upgrade UI + // have no such entry point. Fires when tapping an error-state rack nozzle's status icon. + MessageDialog dlg(nullptr, _L("The hotend is in an abnormal state and currently unavailable. " + "Please go to 'Device -> Upgrade' to upgrade firmware."), _L("Abnormal Hotend"), wxICON_WARNING | wxOK); + dlg.ShowModal(); + } +} + +void wgtDeviceNozzleRackNozzleItem::Rescale() +{ + if (m_nozzle_normal_image) { m_nozzle_normal_image->msw_rescale(); } + if (m_nozzle_empty_image) { m_nozzle_empty_image->msw_rescale(); } + if (m_nozzle_unknown_image) { m_nozzle_unknown_image->msw_rescale(); } + if (m_nozzle_error_image) { m_nozzle_error_image->msw_rescale(); } + + auto status_icon = create_scaled_bitmap("dev_rack_nozzle_error_icon", this, 14); + m_nozzle_status_icon->SetBitmap(status_icon); + m_nozzle_status_icon->Refresh(); + + if (m_nozzle_selected_image) { + m_nozzle_selected_image->msw_rescale(); + if (m_is_selected) { + m_nozzle_selected_bitmap->SetBitmap(m_nozzle_selected_image->bmp()); + } + }; + + switch (m_status) + { + case Slic3r::GUI::wgtDeviceNozzleRackNozzleItem::NOZZLE_EMPTY: + { + m_nozzle_icon->SetBitmap(m_nozzle_empty_image->bmp()); + break; + } + case Slic3r::GUI::wgtDeviceNozzleRackNozzleItem::NOZZLE_NORMAL: + { + m_nozzle_icon->SetBitmap(SetNozzleBmpColor(m_nozzle_normal_image->bmp(), m_filament_color)); + break; + } + case Slic3r::GUI::wgtDeviceNozzleRackNozzleItem::NOZZLE_UNKNOWN: + { + m_nozzle_icon->SetBitmap(m_nozzle_unknown_image->bmp()); + break; + } + case Slic3r::GUI::wgtDeviceNozzleRackNozzleItem::NOZZLE_ERROR: + { + m_nozzle_icon->SetBitmap(m_nozzle_error_image->bmp()); + break; + } + default: + { + break; + } + }; +}; + +void wgtDeviceNozzleRackNozzleItem::EnableSelect() +{ + if (m_enable_select == true) { + return; + }; + + m_enable_select = true; + m_nozzle_icon->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { OnItemSelected(evt); }); + m_nozzle_label_id->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { OnItemSelected(evt); }); + m_nozzle_label_1->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { OnItemSelected(evt); }); + m_nozzle_label_2->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { OnItemSelected(evt); }); + Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { OnItemSelected(evt); }); +} + +void wgtDeviceNozzleRackNozzleItem::OnItemSelected(wxMouseEvent& evt) +{ + if (m_enable_select && !m_is_disabled){ + SetSelected(true); + wxCommandEvent command_evt(EVT_NOZZLE_RACK_NOZZLE_ITEM_SELECTED, GetId()); + command_evt.SetEventObject(this); + ProcessEvent(command_evt); + } + + evt.Skip(); +} + + +void wgtDeviceNozzleRackNozzleItem::SetDisable(bool disabled) +{ + if (m_is_disabled == disabled) { + return; + } + + m_is_disabled = disabled; + + auto bg_clr = disabled ? StateColor::darkModeColorFor("#E5E7EB") : StateColor::darkModeColorFor(*wxWHITE); + m_nozzle_icon->SetBackgroundColour(bg_clr); + m_nozzle_label_id->SetBackgroundColour(bg_clr); + m_nozzle_label_1->SetBackgroundColour(bg_clr); + m_nozzle_status_icon->SetBackgroundColour(bg_clr); + m_nozzle_label_2->SetBackgroundColour(bg_clr); + m_nozzle_selected_bitmap->SetBackgroundColour(bg_clr); + + SetBackgroundColor(bg_clr); + Refresh(); +}; + +};// end of namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.h b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.h new file mode 100644 index 0000000000..2ed2b48d66 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.h @@ -0,0 +1,99 @@ +//**********************************************************/ +/* File: wgtDeviceNozzleRackNozzleItem.h +* Description: One nozzle cell of the H2C hotend rack view. +* +* This header defines only the single-nozzle-cell widget (and the selection event it emits), which +* is the one dependency MultiNozzleSync's HotEndTable needs. The rest of the Device-tab rack panel +* (wgtDeviceNozzleRack / ...Area / ...ToolHead / ...Pos) is not provided here. +//**********************************************************/ + +#pragma once +#include "slic3r/GUI/DeviceCore/DevNozzleRack.h" + +#include "slic3r/GUI/Widgets/StaticBox.hpp" +#include "slic3r/GUI/Widgets/Label.hpp" // complete type required by the inline SetDisplayIdText below + +#include +#include + +// Previous definitions +class ScalableBitmap; +namespace Slic3r +{ + struct DevNozzle; + class DevNozzleRack; +}; + +// Events +wxDECLARE_EVENT(EVT_NOZZLE_RACK_NOZZLE_ITEM_SELECTED, wxCommandEvent); + +namespace Slic3r::GUI +{ +class wgtDeviceNozzleRackNozzleItem : public StaticBox +{ +public: + enum NOZZLE_STATUS + { + NOZZLE_EMPTY, + NOZZLE_NORMAL, + NOZZLE_UNKNOWN, + NOZZLE_ERROR + }; + +public: + wgtDeviceNozzleRackNozzleItem(wxWindow* parent, int nozzle_id); + +public: + void Update(const std::shared_ptr rack, bool on_rack = true); // on_rack is false means extruder nozzle + + int GetNozzleId() const { return m_nozzle_id; } + void SetDisplayIdText(const wxString& text) { m_nozzle_label_id->SetLabel(text);}; + + void EnableSelect();; + void SetSelected(bool selected); + bool IsSelected() const { return m_is_selected; } + + bool IsDisabled() const { return m_is_disabled; } + void SetDisable(bool disabled); + + void Rescale(); + +private: + void CreateGui(); + + void SetNozzleStatus(NOZZLE_STATUS status, const wxString& str1, const wxString& str2, const std::string& color); + + void OnBtnNozzleStatus(wxMouseEvent& evt); + void OnItemSelected(wxMouseEvent& evt); + +private: + std::weak_ptr m_rack; + + int m_nozzle_id; // internal id, from 0 to 5 + std::string m_filament_color; + NOZZLE_STATUS m_status = NOZZLE_STATUS::NOZZLE_EMPTY; + + // select + bool m_is_selected = false; + bool m_enable_select = false; + ScalableBitmap* m_nozzle_selected_image{ nullptr }; + wxStaticBitmap* m_nozzle_selected_bitmap{ nullptr }; + + // enable or disable + bool m_is_disabled = false; + + // Images + ScalableBitmap* m_nozzle_normal_image{ nullptr }; + ScalableBitmap* m_nozzle_empty_image{ nullptr }; + ScalableBitmap* m_nozzle_unknown_image{ nullptr }; + ScalableBitmap* m_nozzle_error_image{ nullptr }; + + // GUI + wxStaticBitmap* m_nozzle_icon{ nullptr }; + Label* m_nozzle_label_id { nullptr }; + Label* m_nozzle_label_1{ nullptr }; + wxStaticBitmap* m_nozzle_status_icon = nullptr; + Label* m_nozzle_label_2{ nullptr }; +}; + +};// end of namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp new file mode 100644 index 0000000000..e992d4b1ab --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp @@ -0,0 +1,696 @@ +//**********************************************************/ +/* File: wgtDeviceNozzleRackUpdate.cpp +* Description: The dialog for reading/upgrading the H2C induction-hotend-rack firmware. +* +* \n class wgtDeviceNozzleRackUpdate +//**********************************************************/ + +#include "wgtDeviceNozzleRackUpdate.h" + +#include "slic3r/GUI/DeviceCore/DevNozzleSystem.h" + +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/wxExtensions.hpp" + +#include "slic3r/GUI/Widgets/Button.hpp" +#include "slic3r/GUI/Widgets/Label.hpp" + +#define WX_DIP_SIZE(x, y) wxSize(FromDIP(x), FromDIP(y)) + +static wxColour s_red_clr("#D01B1B"); + +namespace Slic3r::GUI +{ + +wxDEFINE_EVENT(wxEVT_NOZZLE_JUMP_UPGRADE, wxCommandEvent); + +wgtDeviceNozzleRackUpgradeDlg::wgtDeviceNozzleRackUpgradeDlg(wxWindow* parent, const std::shared_ptr rack) + : DPIDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) +{ + m_rack_upgrade_panel = new wgtDeviceNozzleRackUprade(this); + m_rack_upgrade_panel->UpdateRackInfo(rack); + + Bind(wxEVT_NOZZLE_JUMP_UPGRADE, [this](wxCommandEvent&) { + EndModal(wxID_OK); + }); + + auto main_sizer = new wxBoxSizer(wxVERTICAL); + main_sizer->Add(m_rack_upgrade_panel, 0, wxEXPAND); + SetSizer(main_sizer); + Layout(); + Fit(); + + wxGetApp().UpdateDlgDarkUI(this); +} + +void wgtDeviceNozzleRackUpgradeDlg::UpdateRackInfo(const std::shared_ptr rack) +{ + m_rack_upgrade_panel->UpdateRackInfo(rack); +} + +void wgtDeviceNozzleRackUpgradeDlg::on_dpi_changed(const wxRect& suggested_rect) +{ + m_rack_upgrade_panel->Rescale(); +} + +wgtDeviceNozzleRackUprade::wgtDeviceNozzleRackUprade(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style) + : wxPanel(parent, id, pos, size, style) +{ + CreateGui(); +} + +void wgtDeviceNozzleRackUprade::CreateGui() +{ + SetBackgroundColour(*wxWHITE); + + // Main vertical sizer + auto* main_sizer = new wxBoxSizer(wxVERTICAL); + + // Header: title + buttons + auto* header_sizer = new wxBoxSizer(wxHORIZONTAL); + + // Title label + auto* title_label = new Label(this, _L("Hotends Info")); + title_label->SetFont(Label::Head_14); + header_sizer->Add(title_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + // Spacer + header_sizer->AddStretchSpacer(); + main_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxRIGHT, FromDIP(10)); + + // "Nozzles" + m_extruder_nozzle_item = new wgtDeviceNozzleRackHotendUpdate(this, "R"); + m_extruder_nozzle_item->UpdateColourStyle(wxColour("#F8F8F8")); + m_extruder_nozzle_item->SetExtruderNozzleId(MAIN_EXTRUDER_ID); + + main_sizer->Add(m_extruder_nozzle_item, 0, wxEXPAND | wxALL, FromDIP(12)); + for (int id = 0; id < 6; id ++) + { + auto item = new wgtDeviceNozzleRackHotendUpdate(this, wxString::Format("%d", id + 1)); + item->SetRackNozzleId(id); + m_nozzle_items[id] = item; + + main_sizer->Add(item, 0, wxEXPAND | wxALL, FromDIP(12)); + if (id < 5) + { + wxPanel* separator = new wxPanel(this); + separator->SetMaxSize(wxSize(-1, FromDIP(1))); + separator->SetMinSize(wxSize(-1, FromDIP(1))); + separator->SetBackgroundColour(wxColour(238, 238, 238));// Orca: grey300 spelled out as a literal RGB, since Orca has no equivalent grey-300 colour macro + main_sizer->Add(separator, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(12)); + } + } + + main_sizer->AddSpacer(FromDIP(20)); + + // Set sizer + this->SetSizer(main_sizer); + this->Layout(); +} + +void wgtDeviceNozzleRackUprade::UpdateRackInfo(const std::shared_ptr rack) +{ + m_nozzle_rack = rack; + if (!rack) { return;} + + // update the nozzles + m_extruder_nozzle_item->UpdateExtruderNozzleInfo(rack); + for (auto iter : m_nozzle_items) + { + iter.second->UpdateRackNozzleInfo(rack); + } + + // update layout + Layout(); +} + +void wgtDeviceNozzleRackUprade::OnBtnReadAll(wxCommandEvent& e) +{ + if (auto rack = m_nozzle_rack.lock()) + { + rack->CtrlRackReadAll(true); + } +} + +void wgtDeviceNozzleRackUprade::Rescale() +{ + m_extruder_nozzle_item->Rescale(); + for (auto& iter : m_nozzle_items) + { + iter.second->Rescale(); + } +} + +#define WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG *wxWHITE +wgtDeviceNozzleRackHotendUpdate::wgtDeviceNozzleRackHotendUpdate(wxWindow* parent, const wxString& idx_text) + : StaticBox(parent, wxID_ANY) +{ + CreateGui(); + + m_idx_label->SetLabel(idx_text); +} + +void wgtDeviceNozzleRackHotendUpdate::CreateGui() +{ + SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + SetBorderColor(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + SetCornerRadius(0); + + //load nozzle hs image + for (int i = 1; i <= 4; i++) + { + auto normalImage = new ScalableBitmap(this, "Nozzle_HS_01_0" + std::to_string(i * 2), 46); + auto bigImage = new ScalableBitmap(this, "Big_Nozzle_HS_01_0" + std::to_string(i * 2), 216); + nozzle_hs.push_back({normalImage, bigImage}); + } + for (int i = 2; i <= 4; i++) + { + auto normalImage = new ScalableBitmap(this, "Nozzle_HH_01_0" + std::to_string(i * 2), 46); + auto bigImage = new ScalableBitmap(this, "Big_Nozzle_HH_01_0" + std::to_string(i * 2), 216); + nozzle_hh.push_back({normalImage, bigImage}); + } + + auto* content_sizer = new wxBoxSizer(wxHORIZONTAL); + + // Index + m_idx_label = new Label(this); + m_idx_label->SetFont(Label::Head_14); + m_idx_label->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + content_sizer->Add(m_idx_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(25)); + + // Icon + wxPanel* imagePanel = new wxPanel(this); + imagePanel->SetMaxSize(WX_DIP_SIZE(46, -1)); + imagePanel->SetMinSize(WX_DIP_SIZE(46, -1)); + imagePanel->SetSize(wxSize(FromDIP(46), FromDIP(-1))); + wxBoxSizer* panelSizer = new wxBoxSizer(wxVERTICAL); + imagePanel->SetSizer(panelSizer); + + m_nozzle_empty_image = new ScalableBitmap(imagePanel, "dev_rack_nozzle_empty", 46); + m_icon_bitmap = new wxStaticBitmap(imagePanel, wxID_ANY, m_nozzle_empty_image->bmp()); + panelSizer->Add(m_icon_bitmap, 0, wxALIGN_CENTER); + content_sizer->Add(imagePanel, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(25)); + m_icon_bitmap->Bind(wxEVT_ENTER_WINDOW, &wgtDeviceNozzleRackHotendUpdate::OnBitmapHoverEnter, this); + m_icon_bitmap->Bind(wxEVT_LEAVE_WINDOW, &wgtDeviceNozzleRackHotendUpdate::OnBitmapHoverLeave, this); + + // Diameter/type (vertical) + wxPanel* type_panel = new wxPanel(this); + auto* main_type_sizer = new wxBoxSizer(wxVERTICAL); + auto* type_sizer_row_1 = new wxBoxSizer(wxHORIZONTAL); + auto* type_sizer_row_2 = new wxBoxSizer(wxHORIZONTAL); + + m_material_label = new Label(type_panel); + m_material_label->SetFont(Label::Body_12); + m_material_label->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + + m_colour_box = new StaticBox(type_panel); + m_colour_box->SetMaxSize(WX_DIP_SIZE(16, 16)); + m_colour_box->SetMinSize(WX_DIP_SIZE(16, 16)); + m_colour_box->SetCornerRadius(FromDIP(2)); + m_colour_box->SetSize(wxSize(FromDIP(16), FromDIP(16))); + + type_sizer_row_1->Add(m_colour_box, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT); + type_sizer_row_1->AddStretchSpacer(1); + type_sizer_row_1->Add(m_material_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + + m_diameter_label = new Label(type_panel); + m_diameter_label->SetFont(Label::Body_12); + m_diameter_label->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + + m_flowtype_label = new Label(type_panel); + m_flowtype_label->SetFont(Label::Body_12); + m_flowtype_label->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + + m_type_label = new Label(type_panel); + m_type_label->SetFont(Label::Body_12); + m_type_label->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + + type_sizer_row_2->Add(m_diameter_label, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT ); + type_sizer_row_2->Add(m_flowtype_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + type_sizer_row_2->Add(m_type_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + + main_type_sizer->Add(type_sizer_row_1, 0, wxALIGN_LEFT); + main_type_sizer->Add(type_sizer_row_2, 1, wxALIGN_LEFT | wxEXPAND | wxTOP, FromDIP(4)); + type_panel->SetSizer(main_type_sizer); + type_panel->SetMaxSize(WX_DIP_SIZE(160, 40)); + type_panel->SetMinSize(WX_DIP_SIZE(160, 40)); + type_panel->SetSize(WX_DIP_SIZE(160, 40)); + + content_sizer->Add(type_panel, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(12)); + + // SN and version (vertical) + wxPanel* info_panel = new wxPanel(this); + auto* info_sizer = new wxBoxSizer(wxVERTICAL); + m_sn_label = new Label(info_panel); + m_sn_label->SetFont(Label::Body_12); + m_sn_label->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + + auto* version_h_sizer = new wxBoxSizer(wxHORIZONTAL); + m_version_label = new Label(info_panel); + m_version_label->SetFont(Label::Body_12); + m_version_label->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + + m_version_new_label = new Label(info_panel); + m_version_new_label->SetFont(Label::Body_12); + m_version_new_label->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + m_version_new_label->SetForegroundColour(wxColour(0, 168, 84)); // Green + + version_h_sizer->Add(m_version_label, 0, wxALIGN_CENTER_VERTICAL); + version_h_sizer->Add(m_version_new_label, 0, wxALIGN_CENTER_VERTICAL); + info_sizer->Add(m_sn_label, 0, wxALIGN_LEFT); + info_sizer->Add(version_h_sizer, 0, wxALIGN_LEFT | wxTOP, FromDIP(4)); + info_panel->SetSizer(info_sizer); + info_panel->SetMaxSize(WX_DIP_SIZE(183, 40)); + info_panel->SetMinSize(WX_DIP_SIZE(183, 40)); + info_panel->SetSize(WX_DIP_SIZE(183, 40)); + + //Used Time + m_used_time = new Label(this); + m_used_time->SetFont(Label::Body_12); + m_used_time->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + + m_refresh_icon = new ScalableBitmap(this, "refresh_printer", 12); + m_error_icon = new ScalableBitmap(this, "error", 14); + m_status_bitmap = new wxStaticBitmap(this, wxID_ANY, m_refresh_icon->bmp()); + m_status_bitmap->Bind(wxEVT_LEFT_UP, &wgtDeviceNozzleRackHotendUpdate::OnStatusIconClick, this); + + std::vector list{"refresh_nozzle_1", "refresh_nozzle_2", "refresh_nozzle_3", "refresh_nozzle_4"}; + // Orca: AnimaIcon has no per-instance size argument (it fixes 25px internally), so no size is + // passed here. + m_refreshing_icon = new AnimaIcon(this, wxID_ANY, list, "refresh_nozzle", 100); + m_refreshing_icon->Show(false); + + + m_status_label = new Label(this); + m_status_label->SetFont(Label::Body_12); + + content_sizer->Add(info_panel, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + content_sizer->Add(m_used_time, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + content_sizer->Add(m_status_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + content_sizer->Add(m_status_bitmap, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + content_sizer->Add(m_refreshing_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + content_sizer->AddSpacer(FromDIP(25)); + + auto* main_sizer = new wxBoxSizer(wxHORIZONTAL); + main_sizer->Add(content_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(10)); + + SetSizer(main_sizer); + Layout(); +} + +void wgtDeviceNozzleRackHotendUpdate::OnStatusIconClick(wxMouseEvent& event) +{ + if (m_status_label->GetLabel() == _L("Refresh")) + { + m_status_label->SetForegroundColour(wxColour("#A3A3A3")); + m_status_label->SetLabel(_L("Refreshing")); + m_status_bitmap->Show(false); + if(!m_refreshing_icon->IsPlaying()) + { + m_refreshing_icon->Play(); + m_refreshing_icon->Show(); + } + if (auto shared = m_nozzle_rack.lock()) + { + shared->CrtlRackReadNozzle(m_rack_nozzle_id); + } + Layout(); + } + + if (m_status_label->GetLabel() == _L("Error") /*&& m_nozzle_status == NOZZLE_STATUS_ABNORMAL*/) + { + if (auto shared = m_nozzle_rack.lock()) + { + MessageDialog dlg(nullptr, _L("Hotend status abnormal, unavailable at present. Please upgrade the firmware" + " and try again."), _L("Update"), wxICON_WARNING); + dlg.AddButton(wxID_CANCEL, _L("Cancel"), false); + dlg.AddButton(wxID_OK,_L("Jump to the upgrade page"), true); + + if (dlg.ShowModal() == wxID_OK) + { + wxGetApp().mainframe->m_monitor->jump_to_Upgrade(); + + wxCommandEvent evt(wxEVT_NOZZLE_JUMP_UPGRADE, GetId()); + evt.SetEventObject(this); + wxWindow* target = GetParent(); + if (target) + { + target = target->GetParent(); + if (target) + { + wxPostEvent(target, evt); + } + } + }; + } + } +} + +void wgtDeviceNozzleRackHotendUpdate::OnBitmapHoverEnter(wxMouseEvent& event) +{ + int scaledW = FromDIP(240); + int scaledH = FromDIP(240); + + ScalableBitmap* scaledBmp{nullptr}; + if (m_nozzle_status == NOZZLE_STATUS_EMPTY || m_nozzle_status == NOZZLE_STATUS_UNKNOWN || !findNozzleImage) + { + if (!m_scaled_nozzle_empty_image) {m_scaled_nozzle_empty_image = new ScalableBitmap(this, "dev_rack_nozzle_empty", 216);} + + scaledBmp = m_scaled_nozzle_empty_image; + } + else + { + scaledBmp = m_scaled_nozzle_image; + } + + m_hoverFrame = new wxFrame(nullptr, wxID_ANY, "", + wxDefaultPosition, wxDefaultSize, + wxFRAME_NO_TASKBAR | wxBORDER_NONE | wxTRANSPARENT_WINDOW); + m_hoverFrame->SetBackgroundColour(WGT_DEVICE_NOZZLE_RACK_HOTEND_UPDATE_DEFAULT_BG); + m_hoverFrame->SetSize(scaledW, scaledH); + wxBoxSizer* frameSizer = new wxBoxSizer(wxVERTICAL); + m_hoverFrame->SetSizer(frameSizer); + + wxStaticBitmap* hoverBmp = new wxStaticBitmap(m_hoverFrame, wxID_ANY, scaledBmp->bmp()); + frameSizer->Add(hoverBmp, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxBOTTOM, FromDIP(12)); + + wxPoint mousePos = wxGetMousePosition(); + m_hoverFrame->SetPosition(wxPoint(mousePos.x + 10, mousePos.y)); + m_hoverFrame->Layout(); + m_hoverFrame->Show(true); + + event.Skip(); +} + +void wgtDeviceNozzleRackHotendUpdate::OnBitmapHoverLeave(wxMouseEvent& event) +{ + if (m_hoverFrame) + { + m_hoverFrame->Destroy(); + m_hoverFrame = nullptr; + } + event.Skip(); +} + +void wgtDeviceNozzleRackHotendUpdate::updateNozzleImage(const DevNozzle& nozzle) +{ + if (m_nozzle_status == NOZZLE_STATUS_UNKNOWN || m_nozzle_status == NOZZLE_STATUS_EMPTY) + { + if (!m_nozzle_empty_image) {m_nozzle_empty_image = new ScalableBitmap(this, "dev_rack_nozzle_empty", 46);} + m_icon_bitmap->SetBitmap(m_nozzle_empty_image->bmp()); + m_icon_bitmap->Refresh(); + return; + } + findNozzleImage = false; + int index = -1; + switch (nozzle.GetNozzleDiameterType()) { + case NOZZLE_DIAMETER_0_2: index = 0; break; + case NOZZLE_DIAMETER_0_4: index = 1; break; + case NOZZLE_DIAMETER_0_6: index = 2; break; + case NOZZLE_DIAMETER_0_8: index = 3; break; + default: index = -1; break; + } + NozzleType nozzleType = nozzle.GetNozzleType(); + if (nozzleType == ntHardenedSteel || nozzleType == ntStainlessSteel) + { + if (nozzle.GetNozzleFlowType() == H_FLOW && index >= 1) + { + m_nozzle_image = nozzle_hh[index - 1][0]; + m_icon_bitmap->SetBitmap(m_nozzle_image->bmp()); + m_scaled_nozzle_image = nozzle_hh[index - 1][1]; + findNozzleImage = true; + } + else if (nozzle.GetNozzleFlowType() == S_FLOW && index >= 0) + { + m_nozzle_image = nozzle_hs[index][0]; + m_icon_bitmap->SetBitmap(m_nozzle_image->bmp()); + m_scaled_nozzle_image = nozzle_hs[index][1]; + findNozzleImage = true; + } + } + if (!findNozzleImage) + { + if (!m_nozzle_empty_image) {m_nozzle_empty_image = new ScalableBitmap(this, "dev_rack_nozzle_empty", 46);} + m_icon_bitmap->SetBitmap(m_nozzle_empty_image->bmp()); + } + m_icon_bitmap->Refresh(); +} + +void wgtDeviceNozzleRackHotendUpdate::UpdateColourStyle(const wxColour& clr) +{ + SetBackgroundColour(clr); + SetBorderColor(clr); + + // BFS: Update all children background color + auto children = GetChildren(); + while (!children.IsEmpty()) + { + auto win = children.front(); + children.pop_front(); + win->SetBackgroundColour(clr); + + for (auto child : win->GetChildren()) + { + children.push_back(child); + } + } +} + +void wgtDeviceNozzleRackHotendUpdate::UpdateExtruderNozzleInfo(const std::shared_ptr rack) +{ + m_nozzle_rack = rack; + if (rack) + { + DevNozzleSystem* nozzle_system = rack->GetNozzleSystem(); + if (nozzle_system) + { + UpdateInfo(nozzle_system->GetExtNozzle(m_ext_nozzle_id)); + } + } +} + +void wgtDeviceNozzleRackHotendUpdate::UpdateRackNozzleInfo(const std::shared_ptr rack) +{ + m_nozzle_rack = rack; + if (rack) + { + UpdateInfo(rack->GetNozzle(m_rack_nozzle_id)); + } +} + +void wgtDeviceNozzleRackHotendUpdate::UpdateInfo(const DevNozzle& nozzle) +{ + /*update nozzle possition and background*/ + if (auto share = m_nozzle_rack.lock()) + { + // if (share->GetReadingCount() > 0 && m_status_label->IsShown()) + if (share->GetReadingCount() > 0 && m_status_label->IsShown() && m_status_label->GetLabel() == _L("Refreshing")) + { + m_refreshing_icon->Play(); + m_refreshing_icon->Show(); + m_nozzle_status = NOZZLE_STATUS_DC; + return; + } + else + { + m_refreshing_icon->Stop(); + m_refreshing_icon->Show(false); + } + } + + wxString filamentDisplayName{}; + for (auto iter = GUI::wxGetApp().preset_bundle->filaments.begin(); iter != GUI::wxGetApp().preset_bundle->filaments.end(); ++iter) + { + const Preset& filament_preset = *iter; + // const auto& config = filament_preset.config; + if (filament_preset.filament_id == nozzle.GetFilamentId()) + { + filamentDisplayName = wxString(filament_preset.alias); + } + } + + if (nozzle.IsEmpty() && m_nozzle_status != NOZZLE_STATUS_EMPTY) + { + m_nozzle_status = NOZZLE_STATUS_EMPTY; + + m_material_label->Show(false); + m_colour_box->Show(false); + + m_diameter_label->Show(false); + m_flowtype_label->Show(false); + m_type_label->SetLabel(_L("Empty")); + m_type_label->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); + + m_sn_label->Show(false); + m_version_label->Show(false); + m_version_new_label->Show(false); + + updateNozzleImage(nozzle); + + m_used_time->Show(false); + m_status_label->Show(false); + m_status_bitmap->Show(false); + + } + else if (nozzle.IsNormal() && m_nozzle_status != NOZZLE_STATUS_NORMAL) + { + m_nozzle_status = NOZZLE_STATUS_NORMAL; + + m_material_label->SetLabel(filamentDisplayName); + m_colour_box->SetBackgroundColour(wxColour("#" + nozzle.GetFilamentColor())); + m_colour_box->SetBorderColor(wxColour("#" + nozzle.GetFilamentColor())); + m_material_label->Show(true); + m_colour_box->Show(true); + + m_diameter_label->Show(true); + m_flowtype_label->Show(true); + m_diameter_label->SetLabel(nozzle.GetNozzleDiameterStr()); + m_flowtype_label->SetLabel(nozzle.GetNozzleFlowTypeStr()); + m_type_label->SetLabel(nozzle.GetNozzleTypeStr()); + m_type_label->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); + + m_sn_label->Show(true); + m_version_label->Show(true); + + updateNozzleImage(nozzle); + + m_used_time->Show(true); + m_status_label->Show(false); + m_status_bitmap->Show(false); + } + else if (nozzle.IsAbnormal() && m_nozzle_status != NOZZLE_STATUS_ABNORMAL) + { + m_nozzle_status = NOZZLE_STATUS_ABNORMAL; + + m_material_label->SetLabel(filamentDisplayName); + m_colour_box->SetBackgroundColour(wxColour("#" + nozzle.GetFilamentColor())); + m_colour_box->SetBorderColor(wxColour("#" + nozzle.GetFilamentColor())); + m_material_label->Show(true); + m_colour_box->Show(true); + + m_diameter_label->Show(true); + m_flowtype_label->Show(true); + m_diameter_label->SetLabel(nozzle.GetNozzleDiameterStr()); + m_flowtype_label->SetLabel(nozzle.GetNozzleFlowTypeStr()); + m_type_label->SetLabel(nozzle.GetNozzleTypeStr()); + m_type_label->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); + + updateNozzleImage(nozzle); + + m_status_label->Show(true); + m_status_bitmap->Show(true); + m_status_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#E14747"))); + m_status_label->SetLabel(_L("Error")); + m_status_bitmap->SetBitmap(m_error_icon->bmp()); + m_status_bitmap->Refresh(); + } + else if (nozzle.IsUnknown() && m_nozzle_status != NOZZLE_STATUS_UNKNOWN) + { + m_nozzle_status = NOZZLE_STATUS_UNKNOWN; + + m_colour_box->Show(false); + m_material_label->SetLabel(wxString("--")); + m_material_label->Show(true); + + m_diameter_label->Show(false); + m_flowtype_label->Show(false); + m_type_label->SetLabel(_L("Unknown")); + m_type_label->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); + + m_sn_label->Show(true); + m_version_label->Show(true); + + updateNozzleImage(nozzle); + + m_used_time->Show(true); + m_status_label->Show(true); + m_status_bitmap->Show(true); + m_status_label->SetForegroundColour(wxColour("#00AE42")); + m_status_label->SetLabel(_L("Refresh")); + m_status_bitmap->SetBitmap(m_refresh_icon->bmp()); + m_status_bitmap->Refresh(); + } + + // Update firmware info + const DevFirmwareVersionInfo& firmware = nozzle.GetFirmwareInfo(); + if (nozzle.IsUnknown()) + { + m_sn_label->SetLabel(wxString::Format("%s: --", _L("SN"))); + m_version_label->SetLabel(wxString::Format("%s: --", _L("Version"))); + m_version_new_label->Show(false); + } + if (nozzle.IsNormal() || nozzle.IsAbnormal()) + { + if (firmware.isValid()) + { + m_sn_label->SetLabel(wxString::Format("%s: %s", _L("SN"), firmware.sn)); + + if (!firmware.sw_new_ver.empty() && firmware.sw_new_ver != firmware.sw_ver) + { + m_version_label->SetLabel(wxString::Format("%s: %s > ", _L("Version"), firmware.sw_ver)); + m_version_new_label->SetLabel(wxString::Format("%s", firmware.sw_new_ver)); + m_version_new_label->Show(true); + } + else + { + m_version_label->SetLabel(wxString::Format("%s:%s", _L("Version"), firmware.sw_ver)); + m_version_new_label->Show(false); + } + } + else + { + m_sn_label->SetLabel(wxString::Format("%s: --", _L("SN"))); + m_version_label->SetLabel(wxString::Format("%s: --", _L("Version"))); + m_version_new_label->Show(false); + } + } + + if (!nozzle.IsEmpty()) + { + if (nozzle.IsUnknown()) + { + m_used_time->SetLabel(wxString::Format(_L("Used Time: %s"), "--h")); + } + else + { + // Update used time + int usedSeconds = nozzle.GetNozzlePrintTime(); + if (usedSeconds < 60) + { + m_used_time->SetLabel(wxString::Format(_L("Used Time: %s"), "0 h")); + } + else + { + int printTime = (usedSeconds >= 3600 ? usedSeconds / 3600 : usedSeconds / 60); + std::string printTimeStr = (usedSeconds >= 3600 ? std::to_string(printTime) + " h" : std::to_string(printTime) + " min"); + m_used_time->SetLabel(wxString::Format(_L("Used Time: %s"), printTimeStr.c_str())); + } + } + + } +} + +void wgtDeviceNozzleRackHotendUpdate::Rescale() +{ + // update images + if (m_nozzle_image) { m_nozzle_image->msw_rescale(); } + if (m_nozzle_empty_image) { m_nozzle_empty_image->msw_rescale(); } + if (m_nozzle_status == NOZZLE_STATUS_EMPTY && m_nozzle_empty_image) + { + m_icon_bitmap->SetBitmap(m_nozzle_empty_image->bmp()); + } + else if (m_nozzle_image != nullptr) + { + m_icon_bitmap->SetBitmap(m_nozzle_image->bmp()); + } + m_icon_bitmap->Refresh(); +} + +};// namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h new file mode 100644 index 0000000000..49df93c2c7 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h @@ -0,0 +1,175 @@ +//**********************************************************/ +/* File: wgtDeviceNozzleRackUpdate.h +* Description: The dialog for reading/upgrading the H2C induction-hotend-rack firmware. +* +* \n class wgtDeviceNozzleRackUpgradeDlg; // modal "Hotends Info" dialog +* \n class wgtDeviceNozzleRackUprade; // the dialog's content panel (per-nozzle rows) +* \n class wgtDeviceNozzleRackHotendUpdate; // one hotend row (toolhead "R" + rack 1..6) +* +* Opened from the Device-tab rack panel's "Hotends Info" button (wgtDeviceNozzleRack.cpp) and +* reused by the Device->Upgrade page "Hotends on Rack" section +* (UpgradePanel::createNozzleRackWidgets). All device data + MQTT commands live in +* DevNozzleRack / DevNozzleSystem, so this is additive GUI gated behind +* DevNozzleRack::IsSupported() — dormant for every non-rack printer. +//**********************************************************/ + +#pragma once +#include "slic3r/GUI/DeviceCore/DevNozzleRack.h" + +#include "slic3r/GUI/GUI_Utils.hpp" +#include "slic3r/GUI/Widgets/StaticBox.hpp" +#include "slic3r/GUI/Widgets/AnimaController.hpp" + +#include +#include +#include +#include + +// Previous definitions +class Button; +class Label; +class ScalableBitmap; +class ScalableButton; +namespace Slic3r +{ + struct DevNozzle; + class DevNozzleRack; +namespace GUI +{ + class wgtDeviceNozzleRackUprade; + class wgtDeviceNozzleRackHotendUpdate; +} +}; + +namespace Slic3r::GUI +{ +class wgtDeviceNozzleRackUpgradeDlg : public DPIDialog +{ +public: + wgtDeviceNozzleRackUpgradeDlg(wxWindow* parent, const std::shared_ptr rack); + +public: + void UpdateRackInfo(const std::shared_ptr rack); + +public: + void on_dpi_changed(const wxRect& suggested_rect) override; + +private: + wgtDeviceNozzleRackUprade* m_rack_upgrade_panel; +}; + + +class wgtDeviceNozzleRackUprade : public wxPanel +{ +public: + wgtDeviceNozzleRackUprade(wxWindow* parent, + wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxTAB_TRAVERSAL); + ~wgtDeviceNozzleRackUprade() = default; + +public: + void UpdateRackInfo(const std::shared_ptr rack); + void Rescale(); + +private: + void CreateGui(); + + void OnBtnReadAll(wxCommandEvent& e); + +private: + std::weak_ptr m_nozzle_rack; + + // GUI + wgtDeviceNozzleRackHotendUpdate* m_extruder_nozzle_item; + std::unordered_map m_nozzle_items; +}; + +// Using for rack nozzle id or extruder nozzle id +class wgtDeviceNozzleRackHotendUpdate : public StaticBox +{ +public: + wgtDeviceNozzleRackHotendUpdate(wxWindow* parent, const wxString& idx_text); + +public: + // Color + void UpdateColourStyle(const wxColour& clr); + + // extruder nozzle + int GetExtruderNozzleId() const { return m_ext_nozzle_id; } + void SetExtruderNozzleId(int ext_nozzle_id) { m_ext_nozzle_id = ext_nozzle_id; } + void UpdateExtruderNozzleInfo(const std::shared_ptr rack); + + // rack nozzle + void SetRackNozzleId(int rack_nozzle_id) { m_rack_nozzle_id = rack_nozzle_id; } + int GetRackNozzleId() const { return m_rack_nozzle_id; } + void UpdateRackNozzleInfo(const std::shared_ptr rack); + + // gui + void Rescale(); + +private: + void CreateGui(); + + void UpdateInfo(const DevNozzle& nozzle); + + void OnBitmapHoverEnter(wxMouseEvent& event); + void OnBitmapHoverLeave(wxMouseEvent& event); + void OnStatusIconClick(wxMouseEvent& event); + void updateNozzleImage(const DevNozzle& nozzle); + +private: + enum NozzleStatus : int + { + NOZZLE_STATUS_DC = -1, + NOZZLE_STATUS_EMPTY, + NOZZLE_STATUS_NORMAL, + NOZZLE_STATUS_ABNORMAL, + NOZZLE_STATUS_UNKNOWN, + }; + +private: + int m_ext_nozzle_id = -1; + int m_rack_nozzle_id = -1; + bool m_isRefreshFinish = false; + bool findNozzleImage = false; + + NozzleStatus m_nozzle_status = NOZZLE_STATUS_DC; + std::weak_ptr m_nozzle_rack; + + std::vector> nozzle_hs; + std::vector> nozzle_hh; + // GUI + ScalableBitmap* m_nozzle_image = nullptr; + ScalableBitmap* m_nozzle_empty_image = nullptr; + ScalableBitmap* m_scaled_nozzle_image = nullptr; + ScalableBitmap* m_scaled_nozzle_empty_image = nullptr; + ScalableBitmap* m_refresh_icon = nullptr; + AnimaIcon* m_refreshing_icon = nullptr; + ScalableBitmap* m_error_icon = nullptr; + + Label* m_idx_label; + wxStaticBitmap* m_icon_bitmap{ nullptr }; + wxStaticBitmap* m_status_bitmap{ nullptr }; + + Label* m_material_label{ nullptr }; + StaticBox* m_colour_box{ nullptr }; + wxFrame* m_hoverFrame{ nullptr }; + + Label* m_status_label{ nullptr }; + Label* m_used_time{ nullptr }; + + Label* m_diameter_label; + Label* m_flowtype_label; + Label* m_type_label; + ScalableButton* m_error_button{ nullptr }; + + Label* m_sn_label; + Label* m_version_label; + Label* m_version_new_label; +}; + +wxDECLARE_EVENT(wxEVT_NOZZLE_JUMP_UPGRADE, wxCommandEvent); + +};// end of namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp new file mode 100644 index 0000000000..201b762537 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp @@ -0,0 +1,289 @@ +//**********************************************************/ +/* File: wgtDeviceNozzleSelect.cpp +* Description: The panel to select nozzle +* +* \n class wgtDeviceNozzleSelect; +//**********************************************************/ + +#include "wgtDeviceNozzleSelect.h" +#include "wgtDeviceNozzleRackNozzleItem.h" // the nozzle-item widget lives in its own header + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/DeviceTab/wgtMsgBox.h" +#include "slic3r/GUI/Widgets/Label.hpp" + +static wxColour s_gray_clr("#B0B0B0"); +static wxColour s_hgreen_clr("#00AE42"); +static wxColour s_red_clr("#D01B1B"); + +static std::vector a_nozzle_seq = {16, 18, 20, 17, 19, 21}; + +wxDEFINE_EVENT(EVT_NOZZLE_SELECT_CHANGED, wxCommandEvent); +wxDEFINE_EVENT(EVT_NOZZLE_SELECT_CLICKED, wxCommandEvent); + +namespace Slic3r::GUI { + +wgtDeviceNozzleRackSelect::wgtDeviceNozzleRackSelect(wxWindow *parent) : wxPanel(parent, wxID_ANY) { CreateGui(); } + +static wxPanel* s_create_title(wxWindow *parent, const wxString& text) +{ + wxPanel *panel = new wxPanel(parent, wxID_ANY); + + auto title = new Label(panel, text); + title->SetFont(::Label::Body_13); + title->SetBackgroundColour(*wxWHITE); + title->SetForegroundColour(0x909090); + + auto split_line = new wxPanel(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + split_line->SetBackgroundColour(0xeeeeee); + split_line->SetMinSize(wxSize(-1, 1)); + split_line->SetMaxSize(wxSize(-1, 1)); + + wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(0, 0, 0, wxEXPAND, 0); + sizer->Add(title, 0, wxALIGN_CENTER, 0); + sizer->Add(split_line, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND, 0); + panel->SetSizer(sizer); + panel->Layout(); + return panel; +} + +void wgtDeviceNozzleRackSelect::CreateGui() +{ + wxSizer *main_sizer = new wxBoxSizer(wxVERTICAL); + + wxColour tip_bg_clr("#FFF0E0"); + m_title_tips_dynamic = new wgtMsgBox(this); + m_title_tips_dynamic->SetBackgroundColour(tip_bg_clr); + m_title_tips_dynamic->SetBorderColor(wxColour("#FF6F00")); + m_title_tips_dynamic->SetCornerRadius(1); + m_title_tips_dynamic->SetBorderWidth(1); + auto text_label = m_title_tips_dynamic->GetTextLabel(); + text_label->SetFont(::Label::Body_12); + text_label->SetForegroundColour("#FF6F00"); + text_label->SetBackgroundColour(tip_bg_clr); + text_label->SetLabel(_L("Dynamic nozzles are allocated on the current plate. Picking hotend is not supported.")); + text_label->Wrap(FromDIP(300)); + text_label->Fit(); + m_title_tips_dynamic->Layout(); + m_title_tips_dynamic->Refresh(); + + // nozzles + wxGridSizer *nozzle_sizer = new wxGridSizer(2, 3, FromDIP(10), FromDIP(10)); + for (auto idx : a_nozzle_seq) { + wgtDeviceNozzleRackNozzleItem *nozzle_item = new wgtDeviceNozzleRackNozzleItem(this, idx - 16); + nozzle_item->EnableSelect(); + nozzle_item->Bind(EVT_NOZZLE_RACK_NOZZLE_ITEM_SELECTED, &wgtDeviceNozzleRackSelect::OnNozzleItemSelected, this); + m_nozzle_items[idx] = nozzle_item; + nozzle_sizer->Add(nozzle_item, 0); + } + + // toolhead area + m_toolhead_nozzle_l = new wgtDeviceNozzleRackNozzleItem(this, 1); + m_toolhead_nozzle_l->EnableSelect(); + m_toolhead_nozzle_l->SetDisplayIdText("L"); + m_toolhead_nozzle_l->Bind(EVT_NOZZLE_RACK_NOZZLE_ITEM_SELECTED, &wgtDeviceNozzleRackSelect::OnNozzleItemSelected, this); + + m_toolhead_nozzle_r = new wgtDeviceNozzleRackNozzleItem(this, 0); + m_toolhead_nozzle_r->EnableSelect(); + m_toolhead_nozzle_r->SetDisplayIdText("R"); + m_toolhead_nozzle_r->Bind(EVT_NOZZLE_RACK_NOZZLE_ITEM_SELECTED, &wgtDeviceNozzleRackSelect::OnNozzleItemSelected, this); + + wxSizer* toolhead_sizer = new wxBoxSizer(wxHORIZONTAL); + toolhead_sizer->Add(m_toolhead_nozzle_l, 0, wxRIGHT, FromDIP(5)); + toolhead_sizer->Add(m_toolhead_nozzle_r, 0, wxLEFT, FromDIP(5)); + + main_sizer->Add(m_title_tips_dynamic, 0, wxALIGN_CENTRE_VERTICAL | wxBOTTOM, FromDIP(10)); + main_sizer->Add(s_create_title(this, _L("Hotend Rack")), 0, wxEXPAND); + main_sizer->Add(nozzle_sizer, 0, wxTOP | wxBOTTOM | wxALIGN_LEFT, FromDIP(10)); + main_sizer->Add(s_create_title(this, _L("ToolHead")), 0, wxEXPAND); + main_sizer->AddSpacer(FromDIP(10)); + main_sizer->Add(toolhead_sizer, 0, wxALIGN_LEFT); + + SetBackgroundColour(*wxWHITE); + SetSizer(main_sizer); + Layout(); + Fit(); +} + +static void s_update_nozzle_info(wgtDeviceNozzleRackNozzleItem* item, + std::shared_ptr rack, + const DevNozzle& nozzle_info) +{ + item->Update(rack, nozzle_info.IsOnRack()); + if (nozzle_info.IsUnknown()) { + if (item->GetToolTipText() != _L("Nozzle information needs to be read")) { + item->SetToolTip(_L("Nozzle information needs to be read")); + } + } else { + item->SetToolTip(wxEmptyString); + } +} + +void wgtDeviceNozzleRackSelect::UpdateNozzleInfos(std::shared_ptr rack) +{ + m_nozzle_rack = rack; + if (rack) { + s_update_nozzle_info(m_toolhead_nozzle_l, rack, rack->GetNozzleSystem()->GetNozzleByPosId(DEPUTY_EXTRUDER_ID)); + s_update_nozzle_info(m_toolhead_nozzle_r, rack, rack->GetNozzleSystem()->GetNozzleByPosId(MAIN_EXTRUDER_ID)); + for (const auto& item : m_nozzle_items) { + s_update_nozzle_info(item.second, rack, rack->GetNozzleSystem()->GetNozzleByPosId(item.first)); + } + } +} + +static void s_enable_item_if_match(wgtDeviceNozzleRackNozzleItem* item, + const DevNozzle& nozzle_info, + const DevNozzle& selected_nozzle) +{ + if (item) { + if (nozzle_info.GetLogicExtruderId() != selected_nozzle.GetLogicExtruderId()) { + item->SetDisable(true); + return; + } + + if (!nozzle_info.IsEmpty() && !nozzle_info.IsAbnormal() && !nozzle_info.IsUnknown() && + nozzle_info.GetNozzleType() == selected_nozzle.GetNozzleType() && + nozzle_info.GetNozzleDiameter() == selected_nozzle.GetNozzleDiameter() && + nozzle_info.GetNozzleFlowType() == selected_nozzle.GetNozzleFlowType()) { + item->SetDisable(false); + } else { + item->SetDisable(true); + } + } +} + +void wgtDeviceNozzleRackSelect::UpdatSelectedNozzle(std::shared_ptr rack, int selected_nozzle_pos_id) +{ + m_nozzle_rack = rack; + if (rack) { + SetSelectedNozzle(rack->GetNozzleSystem()->GetNozzleByPosId(selected_nozzle_pos_id)); + if (m_enable_manual_nozzle_pick) { + s_enable_item_if_match(m_toolhead_nozzle_r, rack->GetNozzleSystem()->GetNozzleByPosId(MAIN_EXTRUDER_ID), m_selected_nozzle); + s_enable_item_if_match(m_toolhead_nozzle_l, rack->GetNozzleSystem()->GetNozzleByPosId(DEPUTY_EXTRUDER_ID), m_selected_nozzle); + for (auto& item : m_nozzle_items) { + s_enable_item_if_match(item.second, rack->GetNozzleSystem()->GetNozzleByPosId(item.first), m_selected_nozzle); + } + } + } +} + +void wgtDeviceNozzleRackSelect::UpdatSelectedNozzles(std::shared_ptr rack, + std::vector selected_nozzle_pos_vec, + bool use_dynamic_switch, + std::optional /*print_from_type*/) +{ + m_nozzle_rack = rack; + m_enable_manual_nozzle_pick = !use_dynamic_switch; + m_title_tips_dynamic->Show(use_dynamic_switch); + + UpdateNozzleInfos(rack); + if (!use_dynamic_switch) { + if (selected_nozzle_pos_vec.size() > 0) { + return UpdatSelectedNozzle(rack, selected_nozzle_pos_vec.at(0)); + } else { + return ClearSelection(); + } + } + + if (rack) { + ClearSelection(); + for (const auto& pos_id : selected_nozzle_pos_vec) { + if (pos_id == MAIN_EXTRUDER_ID) { + m_toolhead_nozzle_r->SetSelected(true); + } else if (pos_id == DEPUTY_EXTRUDER_ID) { + m_toolhead_nozzle_l->SetSelected(true); + } else if (auto it = m_nozzle_items.find(pos_id); it != m_nozzle_items.end()) { + it->second->SetSelected(true); + } + } + + if (!m_toolhead_nozzle_l->IsSelected()) { + m_toolhead_nozzle_l->SetDisable(true); + } + + if (!m_toolhead_nozzle_r->IsSelected()) { + m_toolhead_nozzle_r->SetDisable(true); + } + + for (const auto& item : m_nozzle_items) { + if (!item.second->IsSelected()) { + item.second->SetDisable(true); + } + } + } +} + +void wgtDeviceNozzleRackSelect::ClearSelection() +{ + m_selected_nozzle = DevNozzle(); + m_toolhead_nozzle_l->SetSelected(false); + m_toolhead_nozzle_r->SetSelected(false); + for (auto &item : m_nozzle_items) { item.second->SetSelected(false); } +} + +void wgtDeviceNozzleRackSelect::SetSelectedNozzle(const DevNozzle &nozzle) +{ + int new_selected_pos_id = nozzle.GetNozzlePosId(); + if (m_selected_nozzle.GetNozzlePosId() != new_selected_pos_id) { + ClearSelection(); + + m_selected_nozzle = nozzle; + if (new_selected_pos_id == MAIN_EXTRUDER_ID) { + m_toolhead_nozzle_r->SetSelected(true); + } else if (new_selected_pos_id == DEPUTY_EXTRUDER_ID) { + m_toolhead_nozzle_l->SetSelected(true); + } else if (auto it = m_nozzle_items.find(m_selected_nozzle.GetNozzlePosId()); it != m_nozzle_items.end()) { + it->second->SetSelected(true); + } + } +} + +volatile int sGetNozzlePosId(wgtDeviceNozzleRackNozzleItem* item, + wgtDeviceNozzleRackNozzleItem* l_item, + wgtDeviceNozzleRackNozzleItem* r_item) +{ + int to_select_pos_id = -1; + if (item == l_item) { + to_select_pos_id = DEPUTY_EXTRUDER_ID; + } else if (item == r_item) { + to_select_pos_id = MAIN_EXTRUDER_ID; + } else { + to_select_pos_id = item->GetNozzleId() + 0x10; + } + + return to_select_pos_id; +} + +void wgtDeviceNozzleRackSelect::OnNozzleItemSelected(wxCommandEvent &evt) +{ + if (!m_enable_manual_nozzle_pick) { + return; + } + + auto *item = dynamic_cast(evt.GetEventObject()); + if (item; auto ptr = m_nozzle_rack.lock()) { + int to_select_pos_id = sGetNozzlePosId(item, m_toolhead_nozzle_l, m_toolhead_nozzle_r); + if (to_select_pos_id > -1 && to_select_pos_id != GetSelectedNozzlePosID()) { + SetSelectedNozzle(ptr->GetNozzleSystem()->GetNozzleByPosId(to_select_pos_id)); + wxCommandEvent change_evt(EVT_NOZZLE_SELECT_CHANGED, GetId()); + change_evt.SetEventObject(this); + ProcessEvent(change_evt); + evt.Skip(); + } else { + wxCommandEvent change_evt(EVT_NOZZLE_SELECT_CLICKED, GetId()); + change_evt.SetEventObject(this); + ProcessEvent(change_evt); + evt.Skip(); + } + } +} + +void wgtDeviceNozzleRackSelect::Rescale() +{ + m_toolhead_nozzle_l->Rescale(); + m_toolhead_nozzle_r->Rescale(); + for (auto &item : m_nozzle_items) { item.second->Rescale(); } +} + +}; // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.h b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.h new file mode 100644 index 0000000000..43f3378ff2 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.h @@ -0,0 +1,74 @@ +//**********************************************************/ +/* File: wgtDeviceNozzleSelect.h +* Description: The panel to select nozzle +* +* \n class wgtDeviceNozzleSelect; +//**********************************************************/ + +#pragma once + +#include "slic3r/GUI/DeviceCore/DevNozzleSystem.h" // DevNozzle (value member) + DevDefs.h (PrintFromType, MAIN/DEPUTY_EXTRUDER_ID) + +#include + +#include +#include +#include +#include + +// Previous definitions +class Label; +namespace Slic3r +{ + struct DevNozzle; + class DevNozzleRack; +namespace GUI +{ + class wgtDeviceNozzleRackNozzleItem; + class wgtMsgBox; +} +}; + +wxDECLARE_EVENT(EVT_NOZZLE_SELECT_CHANGED, wxCommandEvent); +wxDECLARE_EVENT(EVT_NOZZLE_SELECT_CLICKED, wxCommandEvent); +namespace Slic3r::GUI +{ +class wgtDeviceNozzleRackSelect : public wxPanel +{ +public: + wgtDeviceNozzleRackSelect(wxWindow* parent); + ~wgtDeviceNozzleRackSelect() = default; + +public: + int GetSelectedNozzlePosID() const { return m_selected_nozzle.GetNozzlePosId();} + void UpdatSelectedNozzles(std::shared_ptr rack, std::vector selected_nozzle_pos_vec, bool use_dynamic_switch, std::optional print_from_type);// for slicing with dynamic switch + void Rescale(); + +private: + void CreateGui(); + + void ClearSelection(); + void SetSelectedNozzle(const DevNozzle &nozzle); + void UpdatSelectedNozzle(std::shared_ptr rack, int selected_nozzle_pos_id = -1); // for slicing without dynamic switch + + void UpdateNozzleInfos(std::shared_ptr rack); + + void OnNozzleItemSelected(wxCommandEvent& evt); + +private: + DevNozzle m_selected_nozzle; + std::weak_ptr m_nozzle_rack; + + // pick + bool m_enable_manual_nozzle_pick = true; + + // Label + wgtMsgBox* m_title_tips_dynamic; + + // GUI + wgtDeviceNozzleRackNozzleItem * m_toolhead_nozzle_l{nullptr}; + wgtDeviceNozzleRackNozzleItem * m_toolhead_nozzle_r{nullptr}; + std::unordered_map m_nozzle_items; // from 16 to 21 +}; + +};// end of namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtMsgBox.cpp b/src/slic3r/GUI/DeviceTab/wgtMsgBox.cpp new file mode 100644 index 0000000000..22ee189777 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtMsgBox.cpp @@ -0,0 +1,43 @@ +#include "wgtMsgBox.h" + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Widgets/Label.hpp" +#include "slic3r/GUI/wxExtensions.hpp" + +#include + +namespace Slic3r::GUI +{ + +wgtMsgBox::wgtMsgBox(wxWindow* parent) + : StaticBox(parent, wxID_ANY) +{ + CreateGUI(); +} + +void wgtMsgBox::CreateGUI() +{ + wxBoxSizer* mainSizer = new wxBoxSizer(wxHORIZONTAL); + + m_txt_label = new Label(this); + m_txt_label->SetFont(::Label::Body_13); + m_txt_label->SetBackgroundColour(*wxWHITE); + + auto padding_sizer = new wxBoxSizer(wxHORIZONTAL); + padding_sizer->Add(m_txt_label, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); + mainSizer->Add(padding_sizer, 0, wxALIGN_CENTER_VERTICAL | wxALL, 1); + + //Button* btnClose = CreateCloseButton(); + //wxButton* btnClose = CreateCloseButton(); + //mainSizer->Add(btnClose, 0, wxALIGN_CENTER_VERTICAL); + + SetSizer(mainSizer); + mainSizer->Fit(this); +} + +void wgtMsgBox::OnCloseClicked(wxCommandEvent& evt) +{ + this->Hide(); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtMsgBox.h b/src/slic3r/GUI/DeviceTab/wgtMsgBox.h new file mode 100644 index 0000000000..0668893d09 --- /dev/null +++ b/src/slic3r/GUI/DeviceTab/wgtMsgBox.h @@ -0,0 +1,31 @@ +#ifndef WGTMSGBOX_H +#define WGTMSGBOX_H + +#include +#include +#include +#include + +#include "slic3r/GUI/Widgets/StaticBox.hpp" + +class Label; +namespace Slic3r::GUI +{ +class wgtMsgBox : public StaticBox +{ +public: + wgtMsgBox(wxWindow* parent); + +public: + Label* GetTextLabel() const { return m_txt_label; } + +private: + Label* m_txt_label; + +private: + void CreateGUI(); + void OnCloseClicked(wxCommandEvent& evt); +}; +} + +#endif // WGTMSGBOX_H diff --git a/src/slic3r/GUI/FilamentMapDialog.cpp b/src/slic3r/GUI/FilamentMapDialog.cpp index a514494af1..72428dae02 100644 --- a/src/slic3r/GUI/FilamentMapDialog.cpp +++ b/src/slic3r/GUI/FilamentMapDialog.cpp @@ -138,7 +138,8 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent, if (mode < fmmManual) m_page_type = PageType::ptAuto; - else if (mode == fmmManual) + else if (mode == fmmManual || mode == fmmNozzleManual) + // Orca: there is no dedicated nozzle-manual page, so treat an fmmNozzleManual plate as a manual variant and show the Custom page instead of misfiling it as "Same as Global". m_page_type = PageType::ptManual; else m_page_type = PageType::ptDefault; diff --git a/src/slic3r/GUI/FilamentMapPanel.cpp b/src/slic3r/GUI/FilamentMapPanel.cpp index 59f6e5be6e..250851e556 100644 --- a/src/slic3r/GUI/FilamentMapPanel.cpp +++ b/src/slic3r/GUI/FilamentMapPanel.cpp @@ -1,5 +1,7 @@ #include "FilamentMapPanel.hpp" #include "GUI_App.hpp" +#include "Widgets/MultiNozzleSync.hpp" // manuallySetNozzleCount producer for extruder_nozzle_stats +#include #include #include #include "wx/graphics.h" @@ -72,6 +74,33 @@ FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *p m_switch_btn->Bind(wxEVT_BUTTON, &FilamentMapManualPanel::OnSwitchFilament, this); + // Multi-nozzle: give the user a reachable way to declare, per extruder, how many + // physical nozzles of each volume type a multi-nozzle extruder carries. This is the + // fallback "manual" producer of the extruder_nozzle_stats config (the full device nozzle-rack + // auto-sync is deferred). Gated on the edited printer preset having an extruder with + // extruder_max_nozzle_count > 1, so the trigger is not even created for any single-nozzle or + // dual-extruder ({1,1}, H2D) printer - zero UI change for every existing profile. + auto *max_nozzle_counts_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("extruder_max_nozzle_count"); + // Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate. + if (max_nozzle_counts_opt && + std::any_of(max_nozzle_counts_opt->values.begin(), max_nozzle_counts_opt->values.end(), + [](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); })) { + auto *set_count_link = new Label(this, _L("Set the physical nozzle count...")); + set_count_link->SetFont(Label::Body_14); + set_count_link->SetForegroundColour(BorderSelectedColor); + set_count_link->SetCursor(wxCursor(wxCURSOR_HAND)); + top_sizer->AddSpacer(FromDIP(8)); + top_sizer->Add(set_count_link, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15)); + const std::vector max_counts = max_nozzle_counts_opt->values; + set_count_link->Bind(wxEVT_LEFT_DOWN, [max_counts](wxMouseEvent &evt) { + for (int extruder_id = 0; extruder_id < (int) max_counts.size(); ++extruder_id) { + if (max_counts[extruder_id] > 1) + GUI::manuallySetNozzleCount(extruder_id); + } + evt.Skip(); + }); + } + SetSizer(top_sizer); Layout(); Fit(); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index bb9de4976f..0f86ff903f 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2911,8 +2911,11 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding) float delta_weight_to_single_ext = stats_by_extruder.stats_by_single_extruder.filament_flush_weight - stats_by_extruder.stats_by_multi_extruder_curr.filament_flush_weight; float delta_weight_to_best = stats_by_extruder.stats_by_multi_extruder_curr.filament_flush_weight - stats_by_extruder.stats_by_multi_extruder_best.filament_flush_weight; - int delta_change_to_single_ext = stats_by_extruder.stats_by_single_extruder.filament_change_count - stats_by_extruder.stats_by_multi_extruder_curr.filament_change_count; - int delta_change_to_best = stats_by_extruder.stats_by_multi_extruder_curr.filament_change_count - stats_by_extruder.stats_by_multi_extruder_best.filament_change_count; + // The displayed "hand changes" delta uses the per-nozzle flush_filament_change_count. + // For single-nozzle-per-extruder printers it equals the per-extruder filament_change_count, + // so the shown value is unchanged. + int delta_change_to_single_ext = stats_by_extruder.stats_by_single_extruder.flush_filament_change_count - stats_by_extruder.stats_by_multi_extruder_curr.flush_filament_change_count; + int delta_change_to_best = stats_by_extruder.stats_by_multi_extruder_curr.flush_filament_change_count - stats_by_extruder.stats_by_multi_extruder_best.flush_filament_change_count; bool any_less_to_single_ext = delta_weight_to_single_ext > EPSILON || delta_change_to_single_ext > 0; bool any_more_to_best = delta_weight_to_best > EPSILON || delta_change_to_best > 0; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index c250d96f44..5eda0f7f06 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -8136,6 +8136,11 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with shader = wxGetApp().get_shader("gouraud"); ECanvasType canvas_type = this->m_canvas_type; bool partly_inside_enable = canvas_type == ECanvasType::CanvasAssembleView ? false : true; + // The edited printer's per-extruder printable heights feed the object shader's + // extruder_printable_heights uniform. Empty for single-extruder printers, so the shader flag stays + // 0.0 and rendering is pixel-identical there (see GLVolumeCollection::render). + auto printable_height_option = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("extruder_printable_height"); + std::vector* printable_heights = printable_height_option ? &printable_height_option->values : nullptr; if (shader != nullptr) { shader->start_using(); @@ -8183,7 +8188,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0); } }, - partly_inside_enable); + partly_inside_enable, printable_heights); } } else { @@ -8218,7 +8223,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with return true; } }, - partly_inside_enable); + partly_inside_enable, printable_heights); if (m_canvas_type == CanvasAssembleView && m_gizmos.m_assemble_view_data->model_objects_clipper()->get_position() > 0) { const GLGizmosManager& gm = get_gizmos_manager(); shader->stop_using(); diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index dff8cdc15b..ccec5048f2 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -518,6 +518,16 @@ void MonitorPanel::jump_to_HMS() m_tabpanel->SetSelection(PT_HMS); } +void MonitorPanel::jump_to_Upgrade() +{ + if (this->IsShown()) { + auto page = m_tabpanel->GetCurrentPage(); + if (page && page != m_upgrade_panel) { + m_tabpanel->SetSelection(PT_UPDATE); + } + } +} + void MonitorPanel::jump_to_LiveView() { if (!this->IsShown()) { return; } diff --git a/src/slic3r/GUI/Monitor.hpp b/src/slic3r/GUI/Monitor.hpp index b7502b9d51..cf050068b6 100644 --- a/src/slic3r/GUI/Monitor.hpp +++ b/src/slic3r/GUI/Monitor.hpp @@ -156,6 +156,7 @@ public: void jump_to_HMS(); + void jump_to_Upgrade(); void jump_to_LiveView(); void update_network_version_footer(); }; diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 724e54c2f7..ed76c822f1 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -19,6 +19,7 @@ #include "I18N.hpp" //#include "ConfigWizard.hpp" #include "wxExtensions.hpp" +#include "Widgets/Label.hpp" #include "slic3r/GUI/MainFrame.hpp" #include "GUI_App.hpp" #define MSG_DLG_MAX_SIZE wxSize(-1, FromDIP(464))//notice:ban setting the maximum width value @@ -750,6 +751,47 @@ NetworkErrorDialog::NetworkErrorDialog(wxWindow* parent) Centre(wxBOTH); } + +FilamentWarningDialog::FilamentWarningDialog(wxWindow *parent, const wxString &title, std::vector infos) + : MsgDialog(parent, title.IsEmpty() ? wxString::Format(_L("%s warning"), SLIC3R_APP_FULL_NAME) : title, wxEmptyString, wxOK | wxICON_WARNING), m_messages(infos) +{ + BuildContent(); + finalize(); +} + +void FilamentWarningDialog::BuildContent() +{ + wxBoxSizer *messages_sizer = new wxBoxSizer(wxVERTICAL); + + int message_count = 0; + for (int i = 0; i < m_messages.size(); i++) + { + const wxString &message = m_messages[i].info_msg; + const wxString &wiki_url = m_messages[i].wiki_url; + if (message_count > 0) { messages_sizer->AddSpacer(FromDIP(10)); } + + if (wiki_url.IsEmpty()) { + // No wiki link - just display as regular text + Label *text = new Label(this, message); + text->SetFont(::Label::Body_12); + text->Wrap(FromDIP(400)); + messages_sizer->Add(text, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5)); + } else { + Label *link = new Label(this, message + " " + _L("Please refer to Wiki before use->")); + link->SetForegroundColour(wxColour(8, 153, 46)); + link->SetFont(::Label::Body_12); + link->Wrap(FromDIP(400)); + link->Bind(wxEVT_ENTER_WINDOW, [this](auto &e) { SetCursor(wxCURSOR_HAND); }); + link->Bind(wxEVT_LEAVE_WINDOW, [this](auto &e) { SetCursor(wxCURSOR_ARROW); }); + link->Bind(wxEVT_LEFT_DOWN, [wiki_url](auto &event) { wxLaunchDefaultBrowser(wiki_url); }); + messages_sizer->Add(link, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5)); + } + message_count++; + } + + content_sizer->Add(messages_sizer, 1, wxEXPAND | wxALL, FromDIP(5)); +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index a385fbaa13..174d734336 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -67,6 +67,10 @@ struct MsgDialog : DPIDialog bool get_checkbox_state(); virtual void on_dpi_changed(const wxRect& suggested_rect); void SetButtonLabel(wxWindowID btn_id, const wxString& label, bool set_focus = false); + // Public wrapper around add_button — lets callers append custom-labelled choice buttons to an + // already-constructed dialog (used by the H2C rack hotend "Jump to the upgrade page" prompt). + // Purely additive; existing dialogs are unaffected. + void AddButton(wxWindowID btn_id, const wxString& label, bool set_focus = false) { add_button(btn_id, set_focus, label); } protected: enum { @@ -434,6 +438,32 @@ public: bool m_show_again{false}; }; +// Multi-item filament-blacklist warning dialog with per-item wiki links. Replaces the plain +// MessageDialog used for warnings so stacked accumulate-all warnings render with their own +// optional Wiki link. +struct FilamentWarningInfo +{ + wxString info_msg; + wxString wiki_url; +}; + +class FilamentWarningDialog : public MsgDialog +{ +public: + FilamentWarningDialog(wxWindow *parent, const wxString &title, std::vector infos); + FilamentWarningDialog(FilamentWarningDialog &&) = delete; + FilamentWarningDialog(const FilamentWarningDialog &) = delete; + FilamentWarningDialog &operator=(FilamentWarningDialog &&) = delete; + FilamentWarningDialog &operator=(const FilamentWarningDialog &) = delete; + virtual ~FilamentWarningDialog() = default; + +private: + void BuildContent(); + +private: + std::vector m_messages; +}; + } } diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 18d6e17b33..8dd3ce4d78 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include "libslic3r/MultiNozzleUtils.hpp" #include #include #include @@ -66,6 +68,8 @@ static const int PARTPLATE_PLATENAME_OFFSET_Y = 10; const float WIPE_TOWER_DEFAULT_X_POS = 165.; const float WIPE_TOWER_DEFAULT_Y_POS = 250.; // Max y +const float N9_WIPE_TOWER_DEFAULT_Y_POS = 160.; + const float I3_WIPE_TOWER_DEFAULT_X_POS = 0.; const float I3_WIPE_TOWER_DEFAULT_Y_POS = 250.; // Max y @@ -4221,6 +4225,12 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini y = I3_WIPE_TOWER_DEFAULT_Y_POS; } + std::string printer_type = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle); + // Note: printer_type == "N9" and printer_structure_opt->value == PrinterStructure::psI3 can both be true + if (printer_type == "N9") { + y = N9_WIPE_TOWER_DEFAULT_Y_POS; + } + PartPlate *part_plate = get_plate(plate_idx); Vec3d plate_origin = part_plate->get_origin(); BoundingBoxf3 plate_bbox = part_plate->get_bounding_box(); @@ -6279,6 +6289,58 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f m_plate_list[index]->slice_filaments_info = plate_data_list[i]->slice_filaments_info; gcode_result->warnings = plate_data_list[i]->warnings; gcode_result->filament_maps = plate_data_list[i]->filament_maps; + + // Reconstruct the device-side nozzle grouping from the loaded 3mf so + // the monitor/preview can map filaments to physical nozzles. + // load_nozzle_infos_with_compatibility handles older single-nozzle 3mf (no tags) by + // falling back to the per-filament group_id, then to the extruder volume types / nozzle diameters. + { + // nozzle_volume_types is space-separated ints; nozzle_diameters is comma/space-separated floats. + auto parse_int_tokens = [](const std::string& str) -> std::vector { + std::vector out; + std::istringstream iss(str); + std::string tok; + while (iss >> tok) { try { out.push_back(std::stoi(tok)); } catch (...) {} } + return out; + }; + auto parse_double_tokens = [](const std::string& str) -> std::vector { + std::vector out; + std::string s = str; + std::replace(s.begin(), s.end(), ',', ' '); + std::istringstream iss(s); + std::string tok; + while (iss >> tok) { try { out.push_back(std::stod(tok)); } catch (...) {} } + return out; + }; + + std::vector nozzle_volume_type_values = parse_int_tokens(plate_data_list[i]->nozzle_volume_types); + std::vector nozzle_diameter_values = parse_double_tokens(plate_data_list[i]->nozzle_diameters); + + std::vector extruder_volume_types(nozzle_volume_type_values.size(), NozzleVolumeType::nvtStandard); + for (size_t idx = 0; idx < nozzle_volume_type_values.size(); ++idx) + if (nozzle_volume_type_values[idx] >= 0 && nozzle_volume_type_values[idx] <= nvtMaxNozzleVolumeType) + extruder_volume_types[idx] = static_cast(nozzle_volume_type_values[idx]); + + auto nozzle_infos = MultiNozzleUtils::load_nozzle_infos_with_compatibility( + plate_data_list[i]->nozzles_info, + plate_data_list[i]->slice_filaments_info, + plate_data_list[i]->filament_maps, + extruder_volume_types, + nozzle_diameter_values); + + std::vector fil_seq(plate_data_list[i]->filament_change_sequence.begin(), plate_data_list[i]->filament_change_sequence.end()); + std::vector noz_seq(plate_data_list[i]->nozzle_change_sequence.begin(), plate_data_list[i]->nozzle_change_sequence.end()); + bool enable_filament_dynamic_map = false; + if (plate_data_list[i]->config.has("enable_filament_dynamic_map")) + enable_filament_dynamic_map = plate_data_list[i]->config.option("enable_filament_dynamic_map")->value; + + auto group_result = MultiNozzleUtils::StaticNozzleGroupResult::create( + plate_data_list[i]->slice_filaments_info, nozzle_infos, fil_seq, noz_seq, enable_filament_dynamic_map); + if (group_result) + gcode_result->nozzle_group_result = std::make_shared(group_result.value()); + else + gcode_result->nozzle_group_result = nullptr; + } if (m_plater && !plate_data_list[i]->thumbnail_file.empty()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1%, load thumbnail from %2%.")%(i+1) %plate_data_list[i]->thumbnail_file; if (boost::filesystem::exists(plate_data_list[i]->thumbnail_file)) { @@ -6438,9 +6500,10 @@ void PartPlateList::init_bed_type_info() auto bed_texture_maps = wxGetApp().plater()->get_bed_texture_maps(); std::string bottom_texture_end_name = bed_texture_maps.find("bottom_texture_end_name") != bed_texture_maps.end() ? bed_texture_maps["bottom_texture_end_name"] : ""; std::string bottom_texture_rect_str = bed_texture_maps.find("bottom_texture_rect") != bed_texture_maps.end() ? bed_texture_maps["bottom_texture_rect"] : ""; + std::string bottom_texture_rect_longer_str = bed_texture_maps.find("bottom_texture_rect_longer") != bed_texture_maps.end() ? bed_texture_maps["bottom_texture_rect_longer"] : ""; std::string middle_texture_rect_str = bed_texture_maps.find("middle_texture_rect") != bed_texture_maps.end() ? bed_texture_maps["middle_texture_rect"] : ""; std::string use_double_extruder_default_texture = bed_texture_maps.find("use_double_extruder_default_texture") != bed_texture_maps.end() ? bed_texture_maps["use_double_extruder_default_texture"] : ""; - std::array bottom_texture_rect = {0, 0, 0, 0}, middle_texture_rect = {0, 0, 0, 0}; + std::array bottom_texture_rect = {0, 0, 0, 0}, bottom_texture_rect_longer = {0, 0, 0, 0}, middle_texture_rect = {0, 0, 0, 0}; if (bottom_texture_rect_str.size() > 0) { std::vector items; boost::algorithm::erase_all(bottom_texture_rect_str, " "); @@ -6451,6 +6514,16 @@ void PartPlateList::init_bed_type_info() } } } + if (bottom_texture_rect_longer_str.size() > 0) { + std::vector items; + boost::algorithm::erase_all(bottom_texture_rect_longer_str, " "); + boost::split(items, bottom_texture_rect_longer_str, boost::is_any_of(",")); + if (items.size() == 4) { + for (int i = 0; i < items.size(); i++) { + bottom_texture_rect_longer[i] = std::atof(items[i].c_str()); + } + } + } if (middle_texture_rect_str.size() > 0) { std::vector items; boost::algorithm::erase_all(middle_texture_rect_str, " "); @@ -6471,6 +6544,7 @@ void PartPlateList::init_bed_type_info() } pte_part2 = BedTextureInfo::TexturePart(45, -14.5, 70, 8, "bbl_bed_pte_left_bottom.svg"); auto &bottom_rect = bottom_texture_rect; + auto &bottom_rect_longer = bottom_texture_rect_longer; if (bottom_texture_end_name.size() > 0 && bottom_rect[2] > 0.f) { std::string pte_part2_name = "bbl_bed_pte_bottom_" + bottom_texture_end_name + ".svg"; pte_part2 = BedTextureInfo::TexturePart(bottom_rect[0], bottom_rect[1], bottom_rect[2], bottom_rect[3], pte_part2_name); @@ -6494,6 +6568,9 @@ void PartPlateList::init_bed_type_info() if (bottom_texture_end_name.size() > 0 && bottom_rect[2] > 0.f) { std::string st_part2_name = "bbl_bed_st_bottom_" + bottom_texture_end_name + ".svg"; st_part2 = BedTextureInfo::TexturePart(bottom_rect[0], bottom_rect[1], bottom_rect[2], bottom_rect[3], st_part2_name); + } else if (bottom_rect_longer[2] > 0.f) { + // SuperTack bottom strip uses the wider "longer" rect. + st_part2.update_pos(bottom_rect_longer[0], bottom_rect_longer[1], bottom_rect_longer[2], bottom_rect_longer[3]); } ep_part1 = BedTextureInfo::TexturePart(57, 300, 236.12f, 10.f, "bbl_bed_ep_middle.svg"); @@ -6504,6 +6581,9 @@ void PartPlateList::init_bed_type_info() if (bottom_texture_end_name.size() > 0 && bottom_rect[2] > 0.f) { std::string ep_part2_name = "bbl_bed_ep_bottom_" + bottom_texture_end_name + ".svg"; ep_part2 = BedTextureInfo::TexturePart(bottom_rect[0], bottom_rect[1], bottom_rect[2], bottom_rect[3], ep_part2_name); + } else if (bottom_rect_longer[2] > 0.f) { + // Engineering-plate bottom strip uses the wider "longer" rect. + ep_part2.update_pos(bottom_rect_longer[0], bottom_rect_longer[1], bottom_rect_longer[2], bottom_rect_longer[3]); } pc_part1 = BedTextureInfo::TexturePart(57, 300, 236.12f, 10.f, "bbl_bed_pc_middle.svg"); diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index d4c5399142..bf8304ffb4 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -668,6 +668,12 @@ public: this->filename = part.filename; this->texture = part.texture; } + void update_pos(float xx, float yy, float ww, float hh) { + x = xx; + y = yy; + w = ww; + h = hh; + } void update_file(std::string file) { filename = file; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a4a4b9a98e..e0545a83e9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -122,6 +122,8 @@ #include "NotificationManager.hpp" #include "PresetComboBoxes.hpp" #include "MsgDialog.hpp" +#include "Widgets/MultiNozzleSync.hpp" // NozzleOption, tryPopUpMultiNozzleDialog, setExtruderNozzleCount +#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification #include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file @@ -584,7 +586,11 @@ struct Sidebar::priv void jump_to_object(ObjectDataViewModelNode* item); void can_search(); - bool sync_extruder_list(bool &only_external_material); + bool sync_extruder_list(bool &only_external_material, bool is_manual = false); + // Resolve the nozzle option for a multi-nozzle machine. Returns nullopt (and is a no-op) unless + // extruder_count >= 2 && support_multi_nozzle. When is_manual, always pops the MultiNozzleSyncDialog; + // otherwise reuses the app_config-cached option when the machine's nozzle config is unchanged. + std::optional get_nozzle_options(MachineObject* obj, int extruder_count, bool support_multi_nozzle, bool is_manual); bool switch_diameter(bool single); void update_sync_status(const MachineObject* obj); @@ -1323,7 +1329,288 @@ static bool is_skip_high_flow_printer(const std::string& printer) return invalidate_list.count(printer); }; -bool Sidebar::priv::sync_extruder_list(bool &only_external_material) +// ---- Multi-nozzle sync helpers ---------------------------- +// Serialize/deserialize the machine nozzle config + chosen NozzleOption for the app_config +// "sync_extruder" reuse cache, and Sidebar::priv::get_nozzle_options (the nozzle picker). + +static std::string serialize_nozzle_config(const std::map>& nozzle_cfg_map) +{ + std::ostringstream oss; + + std::vector deputy_nozzles; + auto deputy_it = nozzle_cfg_map.find(1); + if (deputy_it != nozzle_cfg_map.end()) { + deputy_nozzles = deputy_it->second; + } + + std::vector main_nozzles; + auto main_it = nozzle_cfg_map.find(0); + if (main_it != nozzle_cfg_map.end()) { + main_nozzles = main_it->second; + } + + for (size_t i = 0; i < deputy_nozzles.size(); ++i) { + if (i > 0) oss << ";"; + oss << std::fixed << std::setprecision(1) << deputy_nozzles[i].GetNozzleDiameter() << "," + << static_cast(deputy_nozzles[i].GetNozzleFlowType()); + } + + oss << "|"; + + for (size_t i = 0; i < main_nozzles.size(); ++i) { + if (i > 0) oss << ";"; + oss << std::fixed << std::setprecision(1) << main_nozzles[i].GetNozzleDiameter() << "," + << static_cast(main_nozzles[i].GetNozzleFlowType()); + } + + return oss.str(); +} + +static std::map> deserialize_nozzle_config(const std::string &config_str) +{ + std::map> nozzle_cfg_map; + if (config_str.empty()) return nozzle_cfg_map; + + std::vector extruder_parts; + boost::split(extruder_parts, config_str, boost::is_any_of("|")); + + auto get_nozzles_from_string = [](const std::string& part_str) -> std::vector { + std::vector nozzles; + std::vector parts; + boost::split(parts, part_str, boost::is_any_of(";")); + for (const auto &part : parts) { + std::vector values; + boost::split(values, part, boost::is_any_of(",")); + if (values.size() == 2) { + DevNozzle nozzle; + nozzle.m_diameter = std::stof(values[0]); + nozzle.m_nozzle_flow = static_cast(std::stoi(values[1])); + nozzles.push_back(nozzle); + } + } + return nozzles; + }; + + if (extruder_parts.size() != 2) { + auto nozzles = get_nozzles_from_string(config_str); + nozzle_cfg_map[MAIN_EXTRUDER_ID] = nozzles; + nozzle_cfg_map[DEPUTY_EXTRUDER_ID] = { DevNozzle() }; + return nozzle_cfg_map; + } + + if (!extruder_parts[0].empty()) { + auto nozzles = get_nozzles_from_string(extruder_parts[0]); + nozzle_cfg_map[DEPUTY_EXTRUDER_ID] = nozzles; + } + if (!extruder_parts[1].empty()) { + auto nozzles = get_nozzles_from_string(extruder_parts[1]); + nozzle_cfg_map[MAIN_EXTRUDER_ID] = nozzles; + } + + return nozzle_cfg_map; +} + +static bool is_same_nozzle_config(const std::map> &config1, const std::map> &config2) +{ + if (config1.size() != config2.size()) return false; + + for (const auto& [eid, nozzles1] : config1) { + auto it = config2.find(eid); + if (it == config2.end()) return false; + + const auto &nozzles2 = it->second; + if (nozzles1.size() != nozzles2.size()) return false; + + auto sorted_nozzles1 = nozzles1; + auto sorted_nozzles2 = nozzles2; + + auto compare_nozzle = [](const DevNozzle &a, const DevNozzle &b) { + float dia_a = a.GetNozzleDiameter(); + float dia_b = b.GetNozzleDiameter(); + if (std::abs(dia_a - dia_b) > EPSILON) { + return dia_a < dia_b; + } + return static_cast(a.GetNozzleFlowType()) < static_cast(b.GetNozzleFlowType()); + }; + + std::sort(sorted_nozzles1.begin(), sorted_nozzles1.end(), compare_nozzle); + std::sort(sorted_nozzles2.begin(), sorted_nozzles2.end(), compare_nozzle); + + for (size_t i = 0; i < sorted_nozzles1.size(); ++i) { + if (std::abs(sorted_nozzles1[i].GetNozzleDiameter() - sorted_nozzles2[i].GetNozzleDiameter()) > EPSILON || + sorted_nozzles1[i].GetNozzleFlowType() != sorted_nozzles2[i].GetNozzleFlowType()) { + return false; + } + } + } + + return true; +} + +static std::string serialize_nozzle_option(const NozzleOption& option) { + std::ostringstream oss; + oss << option.diameter << "|"; + + bool first = true; + for (const auto& pair : option.extruder_nozzle_stats) { + if (!first) oss << ";"; + first = false; + oss << pair.first << ":"; + + bool first_stat = true; + for (const auto& stat_pair : pair.second) { + if (!first_stat) oss << ","; + first_stat = false; + oss << static_cast(stat_pair.first) << "#" << stat_pair.second; + } + } + return oss.str(); +} + +static std::optional deserialize_nozzle_option(const std::string& option_str) { + if (option_str.empty()) return std::nullopt; + + std::vector parts; + boost::split(parts, option_str, boost::is_any_of("|")); + if (parts.size() != 2) return std::nullopt; + + NozzleOption option; + option.diameter = parts[0]; + + std::vector extruder_parts; + boost::split(extruder_parts, parts[1], boost::is_any_of(";")); + + for (const auto& extruder_part : extruder_parts) { + if (extruder_part.empty()) continue; + + std::vector extruder_data; + boost::split(extruder_data, extruder_part, boost::is_any_of(":")); + if (extruder_data.size() != 2) continue; + + int extruder_id = std::stoi(extruder_data[0]); + std::unordered_map stats; + + std::vector stat_parts; + boost::split(stat_parts, extruder_data[1], boost::is_any_of(",")); + + for (const auto& stat_part : stat_parts) { + std::vector kv; + boost::split(kv, stat_part, boost::is_any_of("#")); + if (kv.size() == 2) { + NozzleVolumeType type = static_cast(std::stoi(kv[0])); + int count = std::stoi(kv[1]); + stats[type] = count; + } + } + + option.extruder_nozzle_stats[extruder_id] = stats; + } + + return option; +} + +std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj, int extruder_count, bool support_multi_nozzle, bool is_manual) +{ + if (extruder_count < 2 || !support_multi_nozzle) { + return std::nullopt; + } + if (!obj || !obj->GetNozzleSystem()) return std::nullopt; + auto nozzle_system = obj->GetNozzleSystem(); + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) return std::nullopt; + + std::string curr_dev_id = obj->get_dev_id(); + std::map> curr_nozzle_cfg; + + for (const auto& ext_nozzle : nozzle_system->GetExtNozzles()) { + int extruder_id = ext_nozzle.first; + curr_nozzle_cfg[extruder_id].emplace_back(ext_nozzle.second); + } + + for (const auto& rack_nozzle : nozzle_system->GetRackNozzles()) { + curr_nozzle_cfg[MAIN_EXTRUDER_ID].emplace_back(rack_nozzle.second); + } + + AppConfig *app_config = wxGetApp().app_config; + std::string saved_dev_id = app_config->get("sync_extruder", "dev_id"); + std::string saved_nozzle_config_str = app_config->get("sync_extruder", "nozzle_config"); + std::string saved_nozzle_option_str = app_config->get("sync_extruder", "nozzle_option"); + std::optional nozzle_option; + + if (is_manual) { + nozzle_option = tryPopUpMultiNozzleDialog(obj); + } else { + bool can_reuse_saved_option = false; + if (!saved_dev_id.empty() && !saved_nozzle_config_str.empty() && !saved_nozzle_option_str.empty() && saved_dev_id == curr_dev_id) { + auto saved_nozzle_config = deserialize_nozzle_config(saved_nozzle_config_str); + if (is_same_nozzle_config(saved_nozzle_config, curr_nozzle_cfg)) { + nozzle_option = deserialize_nozzle_option(saved_nozzle_option_str); + can_reuse_saved_option = nozzle_option.has_value(); + } + if (can_reuse_saved_option) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Reusing saved nozzle option for dev_id: " << curr_dev_id; + + auto &project_config = preset_bundle->project_config; + ConfigOptionEnumsGeneric *nozzle_volume_type_opt = project_config.option("nozzle_volume_type"); + + // Write to preset bundle config + for (int extruder_id = 0; extruder_id < extruder_count; ++extruder_id) { + NozzleVolumeType volume_type; + int nozzle_count; + bool clear_all = true; + + if (!nozzle_option->extruder_nozzle_stats.count(extruder_id)) { + nozzle_count = 0; + // Reset the concrete volume types (Standard/High Flow); Hybrid stays a hidden match-any + // sentinel and is left alone. TPU High Flow is a concrete variant shipped on 0.4/0.6 + // nozzles, so reset it too, but only there (mirrors the High-Flow-skip-for-0.2 guard). + std::vector reset_types{nvtStandard, nvtHighFlow}; + if (extruder_supports_tpu_high_flow(preset_bundle, extruder_id)) + reset_types.push_back(nvtTPUHighFlow); + for (NozzleVolumeType vt : reset_types) { + volume_type = vt; + setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, nozzle_count, clear_all); + clear_all = false; + } + } else { + for (auto &stat : nozzle_option->extruder_nozzle_stats[extruder_id]) { + volume_type = stat.first; + nozzle_count = stat.second; + setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, nozzle_count, clear_all); + clear_all = false; + } + } + } + // Orca: nozzle stats are persisted directly into the preset's `extruder_nozzle_stats` key; + // there is no machine/user data-source flag on the stats. + } + } + + if (!can_reuse_saved_option) { + nozzle_option = tryPopUpMultiNozzleDialog(obj); + } + } + if (nozzle_option) { + std::string current_nozzle_config = serialize_nozzle_config(curr_nozzle_cfg); + std::string current_nozzle_option = serialize_nozzle_option(*nozzle_option); + if (app_config->has_section("sync_extruder")) { + app_config->set("sync_extruder", "dev_id", curr_dev_id); + app_config->set("sync_extruder", "nozzle_config", current_nozzle_config); + app_config->set("sync_extruder", "nozzle_option", current_nozzle_option); + } else { + std::map data; + data["dev_id"] = curr_dev_id; + data["nozzle_config"] = current_nozzle_config; + data["nozzle_option"] = current_nozzle_option; + app_config->set_section("sync_extruder", data); + } + } + + return nozzle_option; +} + +bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_manual) { MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); auto printer_name = plater->get_selected_printer_name_in_combox(); @@ -1377,13 +1664,51 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material) } assert(obj->GetExtderSystem()->GetTotalExtderCount() == extruder_nums); + // Multi-nozzle: resolve the nozzle option (pops MultiNozzleSyncDialog when is_manual). No-op for every + // existing printer — support_multi_nozzle is false unless some extruder_max_nozzle_count entry is a real + // value > 1 (nil-guarded, matching the manual/ToolOrdering gates), which no shipping single-nozzle or + // dual-extruder profile sets. When the machine is multi-nozzle but no option resolves (e.g. user cancelled), + // abort the sync. + const ConfigOptionIntsNullable *extruder_max_nozzle_count = cur_preset.config.option("extruder_max_nozzle_count"); + bool support_multi_nozzle = extruder_max_nozzle_count != nullptr && + std::any_of(extruder_max_nozzle_count->values.begin(), extruder_max_nozzle_count->values.end(), + [](int val) { return val > 1 && val != ConfigOptionIntsNullable::nil_value(); }); + auto nozzle_option = get_nozzle_options(obj, extruder_nums, support_multi_nozzle, is_manual); + if (!nozzle_option && support_multi_nozzle) + return false; + std::vector nozzle_diameters; nozzle_diameters.resize(extruder_nums); + std::vector target_types(extruder_nums, NozzleVolumeType::nvtStandard); for (size_t index = 0; index < extruder_nums; ++index) { int extruder_id = extruder_map[index]; - nozzle_diameters[extruder_id] = obj->GetExtderSystem()->GetNozzleDiameter(index); + nozzle_diameters[extruder_id] = nozzle_option ? atof(nozzle_option->diameter.c_str()) : obj->GetExtderSystem()->GetNozzleDiameter(index); NozzleVolumeType target_type = NozzleVolumeType::nvtStandard; - auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + std::optional select_type; + if (nozzle_option && nozzle_option->extruder_nozzle_stats.count(index)) { + const auto &stats = nozzle_option->extruder_nozzle_stats[index]; + if (stats.size() > 1) { + // Orca: for a mixed extruder, keep Hybrid out of the (still-deferred) nozzle-centric slicing + // pipeline, so collapse to the dominant concrete volume type (tie -> Standard). + // TPU High Flow is shipped on 0.4/0.6 nozzles, so only fold it in there (mirrors the + // High-Flow-skip-for-0.2 guard). Uses the device-reported diameter, same predicate as the preset path. + int std_cnt = 0, hf_cnt = 0, tpu_hf_cnt = 0; + for (const auto &kv : stats) { + if (kv.first == nvtStandard) std_cnt = kv.second; + else if (kv.first == nvtHighFlow) hf_cnt = kv.second; + else if (kv.first == nvtTPUHighFlow) tpu_hf_cnt = kv.second; + } + if (!nozzle_diameter_supports_tpu_high_flow(nozzle_diameters[extruder_id])) + tpu_hf_cnt = 0; + select_type = nvtStandard; + if (hf_cnt > std_cnt && hf_cnt >= tpu_hf_cnt) + select_type = nvtHighFlow; + else if (tpu_hf_cnt > std_cnt && tpu_hf_cnt > hf_cnt) + select_type = nvtTPUHighFlow; + } else { + select_type = stats.begin()->first; + } + } if (obj->is_nozzle_flow_type_supported()) { if (obj->GetExtderSystem()->GetNozzleFlowType(index) == NozzleFlowType::NONE_FLOWTYPE) { MessageDialog dlg(this->plater, _L("There are unset nozzle types. Please set the nozzle types of all extruders before synchronizing."), @@ -1393,9 +1718,13 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material) } // hack code, only use standard flow for 0.2 if (std::fabs(nozzle_diameters[extruder_id] - 0.2) > EPSILON) - target_type = NozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(extruder_id) - 1); + // Map device flow->volume via the table, not `flowtype - 1`. + // The arithmetic only aligns for S_FLOW/H_FLOW; U_FLOW(3)-1 would yield nvtHybrid(2), not nvtTPUHighFlow(3). + target_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(extruder_id)); } - printer_tab->set_extruder_volume_type(index, target_type); + if (select_type) + target_type = *select_type; + target_types[index] = target_type; } int deputy_4 = 0, main_4 = 0, deputy_1 = 0, main_1 = 0; @@ -1439,6 +1768,12 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material) is_switching_diameter = false; } + // set nozzle volume type after switching prset, so this value can override the old value stored in conf + auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + for (size_t idx = 0; idx < target_types.size(); ++idx) { + printer_tab->set_extruder_volume_type(idx, target_types[idx]); + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " finish sync_extruder_list"; return true; } @@ -3745,15 +4080,13 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) { // badge ams filament clear_combos_filament_badge(); if (sync_result.direct_sync) { - auto& ams_list = wxGetApp().preset_bundle->filament_ams_list; - size_t tray_idx = 0; - for (auto& entry : ams_list) { - if (tray_idx >= p->combos_filament.size()) break; - auto filament_id = entry.second.opt_string("filament_id", 0u); - if (!filament_id.empty()) { - badge_combox_filament(p->combos_filament[tray_idx]); - } - tray_idx++; + // Orca: PresetBundle::sync_ams_list rebuilds combos_filament + // 1:1 from the AMS trays that produce a combo (loaded trays + placeholders; non-placeholder + // empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The + // previous per-tray index walked the full filament_ams_list (including the skipped empties), + // so an empty slot before a loaded one dropped the badge for the trailing filaments. + for (auto &c : p->combos_filament) { + badge_combox_filament(c); } } } @@ -3948,7 +4281,8 @@ bool Sidebar::is_multifilament() void Sidebar::deal_btn_sync() { m_begin_sync_printer_status = true; bool only_external_material; - auto ok = p->sync_extruder_list(only_external_material); + // Manual "sync machine" button: is_manual=true so an H2C pops the MultiNozzleSyncDialog to pick a nozzle option. + auto ok = p->sync_extruder_list(only_external_material, true); if (ok) { pop_sync_nozzle_and_ams_dialog(); } @@ -5641,6 +5975,9 @@ std::map Plater::get_bed_texture_maps() if (pm->bottom_texture_rect.size() > 0) { maps["bottom_texture_rect"] = pm->bottom_texture_rect; } + if (pm->bottom_texture_rect_longer.size() > 0) { + maps["bottom_texture_rect_longer"] = pm->bottom_texture_rect_longer; + } if (pm->middle_texture_rect.size() > 0) { maps["middle_texture_rect"] = pm->middle_texture_rect; } @@ -11112,8 +11449,9 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) auto nozzle_volumes_values = preset_bundle->project_config.option("nozzle_volume_type")->values; assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2); if (obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2) { - NozzleVolumeType right_nozzle_type = NozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0) - 1); - NozzleVolumeType left_nozzle_type = NozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(1) - 1); + // Map device flow->volume via the table, not `flowtype - 1` (which mis-maps U_FLOW to nvtHybrid). + NozzleVolumeType right_nozzle_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0)); + NozzleVolumeType left_nozzle_type = DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(1)); NozzleVolumeType preset_left_type = NozzleVolumeType(nozzle_volumes_values[0]); NozzleVolumeType preset_right_type = NozzleVolumeType(nozzle_volumes_values[1]); is_same_as_printer = (left_nozzle_type == preset_left_type && right_nozzle_type == preset_right_type); @@ -17317,6 +17655,9 @@ void Plater::suppress_background_process(const bool stop_background_process) this->p->suppressed_backround_processing_update = true; } +// Expose the slicing process to the device GUI. +BackgroundSlicingProcess& Plater::background_process() { return p->background_process; } + void Plater::center_selection() { p->center_selection(); } void Plater::drop_selection() { p->drop_selection(); } void Plater::mirror(Axis axis) { p->mirror(axis); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index c70c5ca7c1..626d288f46 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -50,6 +50,7 @@ class SLAPrint; //BBS: add partplatelist and SlicingStatusEvent class PartPlateList; class SlicingStatusEvent; +class BackgroundSlicingProcess; enum SLAPrintObjectStep : unsigned int; enum class ConversionType : int; class DevAms; @@ -519,6 +520,9 @@ public: void schedule_background_process(bool schedule = true); bool is_background_process_update_scheduled() const; void suppress_background_process(const bool stop_background_process) ; + // Expose the slicing process so the device GUI can read the current + // GCodeProcessorResult (e.g. the nozzle grouping for print-dispatch mapping). + BackgroundSlicingProcess& background_process(); /* -1: send current gcode if not specified * -2: send all gcode to target machine */ int send_gcode(int plate_idx = -1, Export3mfProgressFn proFn = nullptr); diff --git a/src/slic3r/GUI/PrePrintChecker.cpp b/src/slic3r/GUI/PrePrintChecker.cpp index d66ee53d56..870d3f835d 100644 --- a/src/slic3r/GUI/PrePrintChecker.cpp +++ b/src/slic3r/GUI/PrePrintChecker.cpp @@ -36,6 +36,8 @@ std::string PrePrintChecker::get_print_status_info(PrintDialogStatus status) case PrintStatusNotSupportedPrintAll: return "PrintStatusNotSupportedPrintAll"; case PrintStatusBlankPlate: return "PrintStatusBlankPlate"; case PrintStatusUnsupportedPrinter: return "PrintStatusUnsupportedPrinter"; + case PrintStatusRackNozzleMappingWaiting: return "PrintStatusRackNozzleMappingWaiting"; + case PrintStatusRackNozzleMappingError: return "PrintStatusRackNozzleMappingError"; case PrintStatusColorQuantityExceed: return "PrintStatusColorQuantityExceed"; // Handle filament errors case PrintStatusAmsOnSettingup: return "PrintStatusAmsOnSettingup"; @@ -47,6 +49,7 @@ std::string PrePrintChecker::get_print_status_info(PrintDialogStatus status) case PrintStatusTimelapseNoSdcard: return "PrintStatusTimelapseNoSdcard"; case PrintStatusTimelapseWarning: return "PrintStatusTimelapseWarning"; case PrintStatusMixAmsAndVtSlotWarning: return "PrintStatusMixAmsAndVtSlotWarning"; + case PrintStatusRackNozzleMappingWarning: return "PrintStatusRackNozzleMappingWarning"; 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 8e4d4ed3f4..53ab441df4 100644 --- a/src/slic3r/GUI/PrePrintChecker.hpp +++ b/src/slic3r/GUI/PrePrintChecker.hpp @@ -65,6 +65,8 @@ enum PrintDialogStatus : unsigned int { PrintStatusNotSupportedPrintAll, PrintStatusBlankPlate, PrintStatusUnsupportedPrinter, + PrintStatusRackNozzleMappingWaiting, + PrintStatusRackNozzleMappingError, PrintStatusPrinterErrorEnd, // Errors for filament, Block Print @@ -89,6 +91,7 @@ enum PrintDialogStatus : unsigned int { PrintStatusTimelapseWarning, PrintStatusMixAmsAndVtSlotWarning, PrintStatusToolHeadCoolingFanWarning, + PrintStatusRackNozzleMappingWarning, PrintStatusPrinterWarningEnd, // Warnings for filament diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 6475ae1c88..c52d0eed3a 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -1307,7 +1307,16 @@ void PlaterPresetComboBox::update() bool selected_in_ams = false; if (m_type == Preset::TYPE_FILAMENT) { set_replace_text("Bambu", "BambuStudioBlack"); - selected_in_ams = add_ams_filaments(into_u8(selected_user_preset.empty() ? selected_system_preset : selected_user_preset), true); + // Orca: selected_system/user_preset hold the FULL preset name because Orca keys the maps above by + // full name to avoid alias collisions (BBS keys by alias). add_ams_filaments() compares against + // get_preset_name() which returns the alias, so resolve the selection back to its alias here. + // Without this, e.g. "Bambu PLA Basic @BBL H2C" never equals the AMS tray alias "Bambu PLA Basic", + // so the FROM_AMS flag is never set and update_sync_status() wipes the AMS sync check mark on the + // filament cards for connected Bambu printers. + wxString selected_full = selected_user_preset.empty() ? selected_system_preset : selected_user_preset; + auto alias_it = preset_aliases.find(selected_full); + wxString selected_alias = alias_it != preset_aliases.end() ? from_u8(alias_it->second) : selected_full; + selected_in_ams = add_ams_filaments(into_u8(selected_alias), true); } std::vector filament_orders = {"Bambu PLA Basic", "Bambu PLA Matte", "Bambu PETG HF", "Bambu ABS", "Bambu PLA Silk", "Bambu PLA-CF", diff --git a/src/slic3r/GUI/PrintOptionsDialog.cpp b/src/slic3r/GUI/PrintOptionsDialog.cpp index 562b4cbc24..39f115ed89 100644 --- a/src/slic3r/GUI/PrintOptionsDialog.cpp +++ b/src/slic3r/GUI/PrintOptionsDialog.cpp @@ -1426,6 +1426,7 @@ wxString PrinterPartsDialog::GetString(NozzleFlowType nozzle_flow_type) const { switch (nozzle_flow_type) { case Slic3r::S_FLOW: return _L("Standard"); case Slic3r::H_FLOW: return _L("High flow"); + case Slic3r::U_FLOW: return _L("TPU High flow"); default: break; } diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 9fd0a65087..e66d07d2e9 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -20,12 +20,16 @@ #include "DeviceCore/DevConfig.h" #include "DeviceCore/DevNozzleSystem.h" +#include "DeviceCore/DevNozzleRack.h" #include "DeviceCore/DevExtensionTool.h" #include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevFilaBlackList.h" #include "DeviceCore/DevFilaSystem.h" +#include "DeviceCore/DevFilaSwitch.h" #include "DeviceCore/DevManager.h" #include "DeviceCore/DevMapping.h" +#include "DeviceCore/DevUtilBackend.h" +#include "DeviceCore/DevMappingNozzle.h" #include "DeviceCore/DevStorage.h" #include @@ -62,12 +66,22 @@ std::string get_nozzle_volume_type_cloud_string(NozzleVolumeType nozzle_volume_t else if (nozzle_volume_type == NozzleVolumeType::nvtHighFlow) { return "high_flow"; } + else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) { + return "tpu_high_flow"; + } + else if (nozzle_volume_type == NozzleVolumeType::nvtHybrid) { + // to be supported + return "hybrid_flow"; + } else { assert(false); return ""; } } +// Throttle so the rack print-dispatch nozzle-mapping request isn't re-sent on every status poll. +static int s_nozzle_mapping_last_request_time = 0; + std::vector SelectMachineDialog::MACHINE_BED_TYPE_STRING; std::vector SelectMachineDialog::MachineBedTypeString; void SelectMachineDialog::init_machine_bed_types() @@ -558,7 +572,11 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) m_checkbox_list["flow_cali"] = option_flow_dynamics_cali; m_checkbox_list["nozzle_offset_cali"] = option_nozzle_offset_cali_cali; for (auto print_opt : m_checkbox_list_order) { - print_opt->Bind(EVT_SWITCH_PRINT_OPTION, [this](auto &e) { save_option_vals(); }); + print_opt->Bind(EVT_SWITCH_PRINT_OPTION, [this, print_opt](auto &e) { + save_option_vals(); + // Flow calibration feeds the printer-side rack nozzle mapping; re-request it on change. + if (print_opt == m_checkbox_list["flow_cali"]) on_flow_cali_option_changed(); + }); } option_auto_bed_level->Hide(); @@ -932,13 +950,30 @@ void SelectMachineDialog::finish_mode() void SelectMachineDialog::sync_ams_mapping_result(std::vector &result) { + // A rack / filament-switcher printer grows its filament cards to show the mapped-nozzle row. + // Reflow the grid so the taller cards aren't clipped; no-op for printers that never show it. + auto relayout_nozzle_cards = [this]() { + DeviceManager* dev = wxGetApp().getDeviceManager(); + MachineObject* obj_ = dev ? dev->get_selected_machine() : nullptr; + if (!obj_) return; + DevNozzleSystem* ns = obj_->GetNozzleSystem(); + if (!obj_->GetFilaSwitch()->IsInstalled() && !(ns && ns->GetNozzleRack()->IsSupported())) return; + if (m_sizer_ams_mapping_left) { m_sizer_ams_mapping_left->Layout(); m_filament_panel_left_sizer->Layout(); m_filament_left_panel->Layout(); } + if (m_sizer_ams_mapping_right) { m_sizer_ams_mapping_right->Layout(); m_filament_panel_right_sizer->Layout(); m_filament_right_panel->Layout(); } + if (m_sizer_ams_mapping) { m_sizer_ams_mapping->Layout(); m_filament_panel_sizer->Layout(); m_filament_panel->Layout(); } + if (m_scroll_area) { m_scroll_area->Layout(); } + Layout(); + }; + if (result.empty()) { BOOST_LOG_TRIVIAL(info) << "ams_mapping result is empty"; for (auto it = m_materialList.begin(); it != m_materialList.end(); it++) { wxString ams_id = "Ext";// wxColour ams_col = wxColour(0xCE, 0xCE, 0xCE); it->second->item->set_ams_info(ams_col, ams_id); + it->second->item->set_nozzle_info(get_mapped_nozzle_str(it->first)); } + relayout_nozzle_cards(); return; } @@ -975,11 +1010,13 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector &res cols.push_back(DevAmsTray::decode_color(col)); } m->set_ams_info(ams_col, ams_id,f->ctype, cols); + m->set_nozzle_info(get_mapped_nozzle_str(id)); break; } iter++; } } + relayout_nozzle_cards(); auto tab_index = (MainFrame::TabPosition) dynamic_cast(wxGetApp().tab_panel())->GetSelection(); if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { updata_thumbnail_data_after_connected_printer(); @@ -1378,6 +1415,7 @@ bool SelectMachineDialog::is_nozzle_type_match(DevExtderSystem data, wxString& e if (nozzle_volume_type_opt) { NozzleVolumeType nozzle_volume_type = (NozzleVolumeType) (nozzle_volume_type_opt->get_at(used_extruders[i])); if (nozzle_volume_type == NozzleVolumeType::nvtStandard) { used_extruders_flow[used_extruders[i]] = "Standard";} + else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) { used_extruders_flow[used_extruders[i]] = "TPU High Flow";} else {used_extruders_flow[used_extruders[i]] = "High Flow";} } } @@ -1397,6 +1435,10 @@ bool SelectMachineDialog::is_nozzle_type_match(DevExtderSystem data, wxString& e { flow_type_of_machine.push_back(L("Standard")); } + else if (it.GetNozzleFlowType() == NozzleFlowType::U_FLOW) + { + flow_type_of_machine.push_back(L("TPU High Flow")); + } } //Only when all preset nozzle types and machine nozzle types are exactly the same, return true. @@ -1443,6 +1485,279 @@ int SelectMachineDialog::convert_filament_map_nozzle_id_to_task_nozzle_id(int no } } +// Physical nozzle(s) a mapped filament prints on. key: nozzle pos id, value: nozzle. +// (fila_id is the filament index, i.e. FilamentInfo::id.) Non-rack printers resolve from the +// filament->extruder map; a rack printer (H2C) resolves from the print-dispatch mapping the printer +// returned. Empty when no mapping is available yet — the per-nozzle blacklist check then skips it. +std::map SelectMachineDialog::get_mapped_nozzles(int fila_id) const +{ + std::map nozzle_map; + + DeviceManager* dev = wxGetApp().getDeviceManager(); + if (!dev) { return nozzle_map; } + MachineObject* obj_ = dev->get_selected_machine(); + if (!obj_) { return nozzle_map; } + + int total_ext_count = 0; + if (m_print_type == FROM_NORMAL) { + const auto& full_config = wxGetApp().preset_bundle->full_config(); + const auto opt = full_config.option("nozzle_diameter"); + total_ext_count = opt ? opt->values.size() : 0; + } else { + const auto opt = m_required_data_config.option("nozzle_diameter"); + total_ext_count = opt ? opt->values.size() : 0; + } + + if (total_ext_count != obj_->GetExtderSystem()->GetTotalExtderCount()) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": total_ext_count not match"; + return nozzle_map; + } + + DevNozzleSystem* nozzle_system = obj_->GetNozzleSystem(); + if (!nozzle_system) { return nozzle_map; } + + if (!nozzle_system->GetNozzleRack()->IsSupported()) { + if (total_ext_count == 1) { + nozzle_map[MAIN_EXTRUDER_ID] = nozzle_system->GetNozzleByPosId(MAIN_EXTRUDER_ID); + } else if (total_ext_count == 2) { + if (fila_id >= 0 && (size_t)fila_id < m_filaments_map.size()) { + if (m_filaments_map[fila_id] == 1) { + nozzle_map[DEPUTY_EXTRUDER_ID] = nozzle_system->GetNozzleByPosId(DEPUTY_EXTRUDER_ID); + } else if (m_filaments_map[fila_id] == 2) { + nozzle_map[MAIN_EXTRUDER_ID] = nozzle_system->GetNozzleByPosId(MAIN_EXTRUDER_ID); + } + } + } + } else if (m_print_type == FROM_NORMAL) { + // Rack printer (H2C): resolve the physical rack nozzle(s) the print-dispatch mapping assigned + // to this filament. Empty until the printer returns the auto-mapping result. + for (int nozzle_pos : obj_->get_nozzle_mapping_result()->GetMappedNozzlePosVecByFilaId(fila_id)) + nozzle_map[nozzle_pos] = nozzle_system->GetNozzleByPosId(nozzle_pos); + } + + return nozzle_map; +} + +wxString SelectMachineDialog::get_mapped_nozzle_str(int fila_id) const +{ + if (m_print_type != FROM_NORMAL) + return wxEmptyString; // no slicing data when printing from sdcard + + DeviceManager* dev = wxGetApp().getDeviceManager(); + if (!dev) { return wxEmptyString; } + MachineObject* obj_ = dev->get_selected_machine(); + if (!obj_) { return wxEmptyString; } + + DevNozzleSystem* nozzle_system = obj_->GetNozzleSystem(); + const bool rack_supported = nozzle_system && nozzle_system->GetNozzleRack()->IsSupported(); + const bool fila_switch_installed = obj_->GetFilaSwitch()->IsInstalled(); + + // A dynamic nozzle map routes filaments through the filament switcher; without one it can't map. + if (use_dynamic_nozzle_map() && !fila_switch_installed) + return "?"; + + if (fila_switch_installed || rack_supported) { + if (rack_supported) { + return obj_->get_nozzle_mapping_result()->GetMappedNozzlePosStrByFilaId(fila_id); + } else { + const auto& nozzle_map = get_mapped_nozzles(fila_id); + if (nozzle_map.count(MAIN_EXTRUDER_ID) != 0 && nozzle_map.count(DEPUTY_EXTRUDER_ID) != 0) + return "L R"; + else if (nozzle_map.count(MAIN_EXTRUDER_ID) != 0) + return "R"; + else if (nozzle_map.count(DEPUTY_EXTRUDER_ID) != 0) + return "L"; + } + return "?"; + } + + return wxEmptyString; +} + +bool SelectMachineDialog::use_dynamic_nozzle_map() const +{ + if (m_print_type == FROM_NORMAL) { + auto nozzle_group_res = DevUtilBackend::GetNozzleGroupResult(m_plater); + if (nozzle_group_res && nozzle_group_res->is_support_dynamic_nozzle_map()) + return true; + } else if (m_print_type == FROM_SDCARD_VIEW) { + if (m_required_data_plate_data_list.size() > (size_t) m_print_plate_idx) { + auto dynamic_nozzle_map = m_required_data_plate_data_list[m_print_plate_idx]->config.option("enable_filament_dynamic_map"); + if (dynamic_nozzle_map) + return dynamic_nozzle_map->value; + } + auto dynamic_nozzle_map = m_required_data_config.option("enable_filament_dynamic_map"); + if (dynamic_nozzle_map) + return dynamic_nozzle_map->value; + } + return false; +} + +void SelectMachineDialog::clear_nozzle_mapping() +{ + m_nozzle_mapping_result.clear(); + // Orca: no BBS get_current_machine(); use the selected device (same accessor get_mapped_nozzles uses). + DeviceManager* dev = wxGetApp().getDeviceManager(); + if (MachineObject* obj_ = dev ? dev->get_selected_machine() : nullptr) + obj_->get_nozzle_mapping_result()->Clear(); +} + +void SelectMachineDialog::on_flow_cali_option_changed() +{ + // Flow calibration feeds the printer-side rack nozzle-mapping computation, so a change must + // invalidate the cached mapping and let the next status poll re-request it (V0 path). + DeviceManager* dev = wxGetApp().getDeviceManager(); + MachineObject* obj_ = dev ? dev->get_selected_machine() : nullptr; + if (!obj_ || !(obj_->GetNozzleSystem() && obj_->GetNozzleSystem()->GetNozzleRack()->IsSupported())) return; + if (use_dynamic_nozzle_map()) return; + // Orca: BBS also pops a confirmation dialog and re-fires immediately (and handles a PA-value + // switch Orca lacks); here we just clear + unthrottle so the next poll re-requests promptly. + clear_nozzle_mapping(); + s_nozzle_mapping_last_request_time = 0; +} + +// Rack print-dispatch nozzle mapping, dynamic-map (V1) path. Fires get_auto_nozzle_mapping when no +// result is cached, blocks Send while the printer computes, then surfaces error/warning. Returns +// false (pre-print check fails / keep waiting) only while blocking; true when the chain may proceed. +bool SelectMachineDialog::CheckErrorSyncNozzleMappingResultV1(MachineObject* obj_) +{ + if (m_print_type != FROM_NORMAL) + return true; // there is no slicing data when printing from sdcard + + if (!obj_) + return true; + + if (!(obj_->GetNozzleSystem() && obj_->GetNozzleSystem()->GetNozzleRack()->IsSupported())) + return true; // no need to check if the printer has no nozzle rack + + if (!use_dynamic_nozzle_map()) + return true; + + const auto& obj_nozzle_mapping_ptr = obj_->get_nozzle_mapping_result(); + if (!obj_nozzle_mapping_ptr->HasResult()) { + if (time(nullptr) - s_nozzle_mapping_last_request_time > 10) { // avoid too many requests + int rtn = obj_nozzle_mapping_ptr->CtrlGetAutoNozzleMappingV1(m_plater); + if (rtn == 0) { + s_nozzle_mapping_last_request_time = time(nullptr); + } else { + const auto& err_msg = wxString::Format(_L("Failed to send nozzle auto-mapping request to printer { code: %d }. " + "Please try to refresh the printer information. " + "If it still does not recover, you can try to rebind the printer and check the network connection."), rtn); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingWaiting, {err_msg}); + return false; + } + } + + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingWaiting, {_L("The printer is calculating nozzle mapping.") + " " + _L("Please wait a moment...")}); + return false; + } + + if (obj_nozzle_mapping_ptr->GetResultStr() == "failed") { + const auto& err_msg = wxString::Format(_L("Failed to receive nozzle auto-mapping table from printer { msg: %s }. Please refresh the printer information."), obj_nozzle_mapping_ptr->GetMqttReason()); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingError, {err_msg}); + return false; + } + + if (obj_nozzle_mapping_ptr->GetResultStr() == "fail") { + const wxString& err_msg = wxString::Format(_L("The printer failed to build the nozzle auto-mapping table { code: %d }. Please refresh nozzle information."), obj_nozzle_mapping_ptr->GetErrno()); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingError, {err_msg}); + return false; + } + + const auto& mapping_map = obj_->get_nozzle_mapping_result()->GetNozzleMappingMap(); + if (!mapping_map.empty()) { + if (m_nozzle_mapping_result != mapping_map) { + m_nozzle_mapping_result = mapping_map; + sync_ams_mapping_result(m_ams_mapping_result); + } + + float flush_waste_base = obj_->get_nozzle_mapping_result()->GetFlushWeightBase(); + float flush_waste_current = obj_->get_nozzle_mapping_result()->GetFlushWeightCurrent(); + if ((flush_waste_base != -1) && (flush_waste_current != -1) && flush_waste_current > flush_waste_base) { + const wxString& warning_msg = wxString::Format(_L("The current nozzle mapping may produce an extra %0.2f g of waste."), flush_waste_current - flush_waste_base); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingWarning, {warning_msg}); + } + return true; + } + + const wxString& err_msg = wxString::Format(_L("Failed to receive nozzle auto-mapping table from printer { msg: %s }. Please refresh the printer information."), "empty table"); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingError, {err_msg}); + return true; +} + +// Rack print-dispatch nozzle mapping, static-rack (V0) path — see CheckErrorSyncNozzleMappingResultV1. +bool SelectMachineDialog::CheckErrorSyncNozzleMappingResultV0(MachineObject* obj_) +{ + if (!obj_) + return true; + + if (!(obj_->GetNozzleSystem() && obj_->GetNozzleSystem()->GetNozzleRack()->IsSupported())) + return true; // no need to check if the printer has no nozzle rack + + if (m_print_type != FROM_NORMAL) + return true; // there is no slicing data when printing from sdcard + + if (use_dynamic_nozzle_map()) + return true; // handled by the V1 path + + auto nozzle_group_res = DevUtilBackend::GetNozzleGroupResult(m_plater); + if (nozzle_group_res && nozzle_group_res->get_used_nozzles_in_extruder(LOGIC_R_EXTRUDER_ID).empty()) + return true; // no right-extruder nozzles used in slicing -> nothing to map + + const auto& obj_nozzle_mapping_ptr = obj_->get_nozzle_mapping_result(); + if (!obj_nozzle_mapping_ptr->HasResult()) { + if (time(nullptr) - s_nozzle_mapping_last_request_time > 10) { // avoid too many requests + // Orca: BBS passes m_pa_value_switch->GetValue() ? 0 : 1; Orca has no PA-value switch UI, + // so use the switch-off default (1). + int rtn = obj_nozzle_mapping_ptr->CtrlGetAutoNozzleMappingV0(m_plater, m_ams_mapping_result, m_checkbox_list["flow_cali"]->getValueInt(), 1); + if (rtn == 0) { + s_nozzle_mapping_last_request_time = time(nullptr); + } else { + const auto& err_msg = wxString::Format(_L("Failed to send nozzle auto-mapping request to printer { code: %d }. " + "Please try to refresh the printer information. " + "If it still does not recover, you can try to rebind the printer and check the network connection."), rtn); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingWaiting, {err_msg}); + return false; + } + } + + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingWaiting, {_L("The printer is calculating nozzle mapping.") + " " + _L("Please wait a moment...")}); + return false; + } + + if (obj_nozzle_mapping_ptr->GetResultStr() == "failed") { + const auto& err_msg = wxString::Format(_L("Failed to receive nozzle auto-mapping table from printer { msg: %s }. Please refresh the printer information."), obj_nozzle_mapping_ptr->GetMqttReason()); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingError, {err_msg}); + return false; + } + + if (obj_nozzle_mapping_ptr->GetResultStr() == "fail") { + const wxString& err_msg = wxString::Format(_L("The printer failed to build the nozzle auto-mapping table { code: %d }. Please refresh nozzle information."), obj_nozzle_mapping_ptr->GetErrno()); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingError, {err_msg}); + return false; + } + + const auto& mapping_map = obj_->get_nozzle_mapping_result()->GetNozzleMappingMap(); + if (!mapping_map.empty()) { + if (m_nozzle_mapping_result != mapping_map) { + m_nozzle_mapping_result = mapping_map; + sync_ams_mapping_result(m_ams_mapping_result); + } + + float flush_waste_base = obj_->get_nozzle_mapping_result()->GetFlushWeightBase(); + float flush_waste_current = obj_->get_nozzle_mapping_result()->GetFlushWeightCurrent(); + if ((flush_waste_base != -1) && (flush_waste_current != -1) && flush_waste_current > flush_waste_base) { + const wxString& warning_msg = wxString::Format(_L("The current nozzle mapping may produce an extra %0.2f g of waste."), flush_waste_current - flush_waste_base); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingWarning, {warning_msg}); + } + return true; + } + + const wxString& err_msg = wxString::Format(_L("Failed to receive nozzle auto-mapping table from printer { msg: %s }. Please refresh the printer information."), "empty table"); + show_status(PrintDialogStatus::PrintStatusRackNozzleMappingError, {err_msg}); + return true; +} + void SelectMachineDialog::prepare(int print_plate_idx) { m_print_plate_idx = print_plate_idx; @@ -1684,6 +1999,17 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vectorprinter_type, filament_brand, filament_type, m_ams_mapping_result[i].filament_id, ams_id, slot_id, "", in_blacklist, - action, info, wiki_url); - if (in_blacklist && action == "warning") { - confirm_text.push_back(ConfirmBeforeSendInfo(info, wiki_url)); - has_slice_warnings = true; + DevFilaBlacklist::CheckFilamentInfo check_info; + check_info.dev_id = obj_->get_dev_id(); + check_info.model_id = obj_->printer_type; + check_info.fila_id = m_ams_mapping_result[i].filament_id; + check_info.fila_type = filament_type; + check_info.fila_vendor = filament_brand; + // Populate fila_name so the high-flow warning strings (blacklist rules 14/15/18/19, + // "%s has a risk of nozzle clogging ...") render with the filament name instead of a + // leading blank. Resolves the same preset the engine's internal AMS-name recovery uses, so + // blacklist name-matching is unchanged. + if (auto option = wxGetApp().preset_bundle->get_filament_by_filament_id(check_info.fila_id)) + check_info.fila_name = option->filament_name; + check_info.ams_id = ams_id; + check_info.slot_id = slot_id; + check_info.has_filament_switch = obj_->GetFilaSwitch()->IsInstalled(); + + std::vector mapped_nozzles; + for (const auto &[pos_id, nozzle] : get_mapped_nozzles(m_ams_mapping_result[i].id)) { + if (!nozzle.IsEmpty()) { mapped_nozzles.push_back(nozzle); } + } + + // Evaluate the blacklist once per resolved physical nozzle so the high-flow + // nozzle_flows/nozzle_diameters rules match against the real nozzle. If no nozzle context is + // resolvable (a non-rack printer, or a rack filament not yet mapped by the print-dispatch + // mapping), evaluate once with the nozzle fields unset so the non-nozzle rules still fire as + // before (empty nozzle_flow never matches a nozzle_flows rule). + const size_t iterations = mapped_nozzles.empty() ? 1 : mapped_nozzles.size(); + for (size_t n = 0; n < iterations; ++n) { + if (n < mapped_nozzles.size()) { + const DevNozzle &nozzle = mapped_nozzles[n]; + check_info.extruder_id = nozzle.GetExtruderId(); + check_info.nozzle_flow = DevNozzle::GetNozzleFlowTypeString(nozzle.GetNozzleFlowType()); + check_info.nozzle_diameter = nozzle.GetNozzleDiameter(); + } + + const auto &result = DevFilaBlacklist::check_filaments_in_blacklist(check_info); + if (const auto &warning_items = result.get_items_by_action("warning"); !warning_items.empty()) { + for (const auto &item : warning_items) { + confirm_text.push_back(ConfirmBeforeSendInfo(item.info_msg, item.wiki_url)); + has_slice_warnings = true; + } + } } } @@ -2514,6 +2873,12 @@ void SelectMachineDialog::on_send_print() m_print_job->task_ams_mapping = ams_mapping_array; m_print_job->task_ams_mapping2 = ams_mapping_array2; m_print_job->task_ams_mapping_info = ams_mapping_info; + // Print-dispatch nozzle mapping (H2C hotend rack): attach ONLY when a mapping result exists. + // For every non-rack printer (X1/P1/A1/H2S/H2D) the mapping json is empty, so task_nozzle_mapping + // stays absent and the print payload is unchanged. + if (!obj_->get_nozzle_mapping_result()->GetNozzleMappingJson().empty()) { + m_print_job->task_nozzle_mapping = obj_->get_nozzle_mapping_result()->GetNozzleMappingJson().dump(); + } /* build nozzles info for multi extruders printers */ if (build_nozzles_info(m_print_job->task_nozzles_info)) { @@ -2689,6 +3054,7 @@ void SelectMachineDialog::on_set_finish_mapping(wxCommandEvent &evt) if (item->id == m_current_filament_id) { auto ams_colour = wxColour(wxAtoi(selection_data_arr[0]), wxAtoi(selection_data_arr[1]), wxAtoi(selection_data_arr[2]), wxAtoi(selection_data_arr[3])); m->set_ams_info(ams_colour, selection_data_arr[4], ctype, material_cols); + m->set_nozzle_info(get_mapped_nozzle_str(item->id)); } iter++; } @@ -2993,9 +3359,11 @@ void SelectMachineDialog::on_selection_changed(wxCommandEvent &event) /* reset timeout and reading printer info */ m_status_bar->reset(); m_timeout_count = 0; + s_nozzle_mapping_last_request_time = 0; m_ams_mapping_res = false; m_ams_mapping_valid = false; m_ams_mapping_result.clear(); + clear_nozzle_mapping(); m_pre_print_checker.clear(); m_link_edit_nozzle->Show(false); @@ -3102,7 +3470,9 @@ void SelectMachineDialog::update_filament_change_count() auto best = stats.stats_by_multi_extruder_best; auto curr = stats.stats_by_multi_extruder_curr; - int hand_changes_count = curr.filament_change_count - best.filament_change_count; + // Per-nozzle flush_filament_change_count. Equals the per-extruder filament_change_count for + // single-nozzle-per-extruder printers, so this suggestion's shown value is unchanged. + int hand_changes_count = curr.flush_filament_change_count - best.flush_filament_change_count; int saving_weight = curr.filament_flush_weight - best.filament_flush_weight; if (obj->GetExtderSystem()->GetTotalExtderCount() > 1) { m_link_edit_nozzle->Show(true); } @@ -3425,6 +3795,10 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) } } + // Rack print-dispatch nozzle mapping (dynamic V1): request/await the printer's mapping and block + // Send while it computes. No-op for non-rack printers. + if (!CheckErrorSyncNozzleMappingResultV1(obj_)) return; + if (!DevPrinterConfigUtil::support_ams_ext_mix_print(obj_->printer_type)) { bool useAms = _HasAms(m_ams_mapping_result); bool useExt = _HasExt(m_ams_mapping_result); @@ -3444,6 +3818,10 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) return; } + // Rack print-dispatch nozzle mapping (static rack V0): consumes the validated AMS mapping, so it + // runs after the AMS-validity check and before the per-nozzle blacklist loop (which needs the result). + if (!CheckErrorSyncNozzleMappingResultV0(obj_)) return; + // filaments check for black list for (auto i = 0; i < m_ams_mapping_result.size(); i++) { const auto &ams_id = m_ams_mapping_result[i].get_ams_id(); @@ -3458,22 +3836,53 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) if (fs.id == m_ams_mapping_result[i].id) { filament_brand = m_filaments[i].brand; } } - bool in_blacklist = false; - std::string action; - wxString info; - wxString wiki_url; - DevFilaBlacklist::check_filaments_in_blacklist_url(obj_->printer_type, filament_brand, filament_type, m_ams_mapping_result[i].filament_id, ams_id, slot_id, "", in_blacklist, - action, info, wiki_url); - if (in_blacklist) { + DevFilaBlacklist::CheckFilamentInfo check_info; + check_info.dev_id = obj_->get_dev_id(); + check_info.model_id = obj_->printer_type; + check_info.fila_id = m_ams_mapping_result[i].filament_id; + check_info.fila_type = filament_type; + check_info.fila_vendor = filament_brand; + // Populate fila_name so the high-flow warning strings (blacklist rules 14/15/18/19, + // "%s has a risk of nozzle clogging ...") render with the filament name instead of a + // leading blank. Resolves the same preset the engine's internal AMS-name recovery uses, so + // blacklist name-matching is unchanged. + if (auto option = wxGetApp().preset_bundle->get_filament_by_filament_id(check_info.fila_id)) + check_info.fila_name = option->filament_name; + check_info.ams_id = ams_id; + check_info.slot_id = slot_id; + check_info.has_filament_switch = obj_->GetFilaSwitch()->IsInstalled(); - std::vector error_msg { info }; - if (action == "prohibition") { - show_status(PrintDialogStatus::PrintStatusHasFilamentInBlackListError, error_msg, wiki_url); + std::vector mapped_nozzles; + for (const auto &[pos_id, nozzle] : get_mapped_nozzles(m_ams_mapping_result[i].id)) { + if (!nozzle.IsEmpty()) { mapped_nozzles.push_back(nozzle); } + } + + // Per-physical-nozzle blacklist check — thread the nozzle the filament prints on so the + // high-flow prohibition/warning rules evaluate against the real nozzle flow/diameter. + // With no resolvable nozzle context (a non-rack printer, or a rack filament not yet mapped + // by the print-dispatch mapping), evaluate once with the nozzle fields unset so the + // non-nozzle prohibitions/warnings still fire as before. + const size_t iterations = mapped_nozzles.empty() ? 1 : mapped_nozzles.size(); + for (size_t n = 0; n < iterations; ++n) { + if (n < mapped_nozzles.size()) { + const DevNozzle &nozzle = mapped_nozzles[n]; + check_info.extruder_id = nozzle.GetExtruderId(); + check_info.nozzle_flow = DevNozzle::GetNozzleFlowTypeString(nozzle.GetNozzleFlowType()); + check_info.nozzle_diameter = nozzle.GetNozzleDiameter(); + } + + const auto &result = DevFilaBlacklist::check_filaments_in_blacklist(check_info); + if (const auto &prohibition_items = result.get_items_by_action("prohibition"); !prohibition_items.empty()) { + for (const auto &item : prohibition_items) { + show_status(PrintDialogStatus::PrintStatusHasFilamentInBlackListError, {item.info_msg}, item.wiki_url); + } return; } - else if (action == "warning") { - show_status(PrintDialogStatus::PrintStatusHasFilamentInBlackListWarning, error_msg, wiki_url);/** warning check **/ - // return; + + if (const auto &warning_items = result.get_items_by_action("warning"); !warning_items.empty()) { + for (const auto &item : warning_items) { + show_status(PrintDialogStatus::PrintStatusHasFilamentInBlackListWarning, {item.info_msg}, item.wiki_url);/** warning check **/ + } } } } @@ -3664,6 +4073,7 @@ void SelectMachineDialog::reset_ams_material() wxString ams_id = "-"; wxColour ams_col = wxColour(0xEE, 0xEE, 0xEE); m->set_ams_info(ams_col, ams_id); + m->set_nozzle_info(get_mapped_nozzle_str(id)); iter++; } } @@ -3968,7 +4378,7 @@ void SelectMachineDialog::reset_and_sync_ams_list() m_mapping_popup.set_current_filament_id(extruder); m_mapping_popup.set_tag_texture(materials[extruder]); m_mapping_popup.set_send_win(this);//fix bug:fisrt click is not valid - m_mapping_popup.update(obj_, m_ams_mapping_result); + m_mapping_popup.update(obj_, m_ams_mapping_result, use_dynamic_nozzle_map(), m_print_type); m_mapping_popup.Popup(); } } @@ -4471,7 +4881,7 @@ void SelectMachineDialog::set_default_from_sdcard() m_mapping_popup.set_current_filament_id(fo.id); m_mapping_popup.set_tag_texture(fo.type); m_mapping_popup.set_send_win(this); - m_mapping_popup.update(obj_, m_ams_mapping_result); + m_mapping_popup.update(obj_, m_ams_mapping_result, use_dynamic_nozzle_map(), m_print_type); m_mapping_popup.Popup(); } } diff --git a/src/slic3r/GUI/SelectMachine.hpp b/src/slic3r/GUI/SelectMachine.hpp index ab88240ec0..976f130cb2 100644 --- a/src/slic3r/GUI/SelectMachine.hpp +++ b/src/slic3r/GUI/SelectMachine.hpp @@ -32,6 +32,7 @@ #include "GUI_Utils.hpp" #include "wxExtensions.hpp" #include "DeviceManager.hpp" +#include "DeviceCore/DevNozzleSystem.h" // DevNozzle (get_mapped_nozzles return type) #include "Plater.hpp" #include "BBLStatusBar.hpp" #include "BBLStatusBarPrint.hpp" @@ -61,10 +62,8 @@ namespace Slic3r { namespace GUI { std::string get_nozzle_volume_type_cloud_string(NozzleVolumeType nozzle_volume_type); void print_ams_mapping_result(std::vector &result); -enum PrintFromType { - FROM_NORMAL, - FROM_SDCARD_VIEW, -}; +// enum PrintFromType moved to DeviceCore/DevDefs.h so shared device-mapping GUI +// headers can name it without an include cycle. SelectMachine.hpp still sees it via DeviceManager.hpp. enum PrintPageMode { PrintPageModePrepare = 0, @@ -324,6 +323,7 @@ private: std::vector m_list; std::vector m_filaments; std::vector m_ams_mapping_result; + std::unordered_map m_nozzle_mapping_result; // cached rack print-dispatch mapping: filament/group id -> physical nozzle pos std::vector m_filaments_map; std::shared_ptr m_status_bar; std::unique_ptr m_worker; @@ -511,6 +511,23 @@ public: bool is_nozzle_type_match(DevExtderSystem data, wxString& error_message) const; int convert_filament_map_nozzle_id_to_task_nozzle_id(int nozzle_id) const; + // Physical nozzle(s) a mapped filament (by filament index / FilamentInfo::id) prints on. + // key: nozzle pos id, value: nozzle. Used by the per-nozzle filament blacklist check loop. + std::map get_mapped_nozzles(int fila_id) const; + + // Short label of the physical nozzle(s) a filament prints on, shown on its send-dialog card: + // rack slots ("R1", "R2 R3") for a nozzle-rack printer, or "L"/"R"/"L R" for a filament-switcher + // printer. Empty for printers with neither (the card then shows no nozzle row). + wxString get_mapped_nozzle_str(int fila_id) const; + + // Rack print-dispatch nozzle mapping (H2C): request the printer's auto-mapping and consume the + // result, gating the Send button while the printer computes. Both are no-ops for non-rack printers. + bool CheckErrorSyncNozzleMappingResultV1(MachineObject* obj_); + bool CheckErrorSyncNozzleMappingResultV0(MachineObject* obj_); + void clear_nozzle_mapping(); + bool use_dynamic_nozzle_map() const; + void on_flow_cali_option_changed(); + PrintFromType get_print_type() {return m_print_type;}; wxString format_steel_name(NozzleType type); PrintDialogStatus get_status() { return m_print_status; } diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 5cbf3e2a83..b0cc0819f6 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -34,6 +34,8 @@ #include "DeviceCore/DevManager.h" #include "DeviceCore/DevPrintTaskInfo.h" +#include "DeviceTab/wgtDeviceNozzleRack.h" + #include "PrintOptionsDialog.hpp" @@ -1611,13 +1613,26 @@ wxBoxSizer *StatusBasePanel::create_machine_control_page(wxWindow *parent) wxBoxSizer *bSizer_control = new wxBoxSizer(wxVERTICAL); auto temp_axis_ctrl_sizer = create_temp_axis_group(parent); - auto m_ams_ctrl_sizer = create_ams_group(parent); auto m_filament_load_sizer = create_filament_group(parent); + /* ams control box or live nozzle-rack panel (rack printers switch between the two) */ + wxSizer *ams_rack_sizer = new wxBoxSizer(wxHORIZONTAL); + ams_rack_sizer->Add(create_ams_group(parent), 0, wxEXPAND | wxLEFT); + + m_panel_nozzle_rack = new wgtDeviceNozzleRack(parent); + m_panel_nozzle_rack->Show(false); + ams_rack_sizer->Add(m_panel_nozzle_rack, 0, wxEXPAND | wxLEFT); + + m_ams_rack_switch = new SwitchBoard(parent, _L("Filament"), _L("Hotends"), wxSize(FromDIP(126), FromDIP(26))); + m_ams_rack_switch->updateState("left"); + m_ams_rack_switch->Hide(); + m_ams_rack_switch->Bind(wxCUSTOMEVT_SWITCH_POS, &StatusBasePanel::on_ams_rack_switch, this); + bSizer_control->Add(0, 0, 0, wxTOP, FromDIP(8)); bSizer_control->Add(temp_axis_ctrl_sizer, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); + bSizer_control->Add(m_ams_rack_switch, 0, wxALIGN_CENTRE|wxTOP, FromDIP(6)); bSizer_control->Add(0, 0, 0, wxTOP, FromDIP(6)); - bSizer_control->Add(m_ams_ctrl_sizer, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); + bSizer_control->Add(ams_rack_sizer, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); bSizer_control->Add(0, 0, 0, wxTOP, FromDIP(6)); bSizer_control->Add(m_filament_load_sizer, 0, wxALIGN_CENTER|wxLEFT|wxRIGHT, FromDIP(8)); bSizer_control->Add(0, 0, 0, wxTOP, FromDIP(4)); @@ -2163,7 +2178,19 @@ void StatusBasePanel::expand_filament_loading(wxMouseEvent& e) else if (obj->is_series_o()) { const auto& ext_system = obj->GetExtderSystem(); - if (ext_system->GetTotalExtderCount() == 2) + // Prefer the config-driven filament-load image: H2C ships per-extruder bitmaps, and a printer + // fitted with an induction hotend rack picks the rack-specific set (filament_load_image_nozzle_rack). + // Printers whose profile carries no such key (e.g. H2D) get an empty name here and fall through to + // the generic O-series bitmaps below, so their monitor page stays byte-identical. Rack-driven, so + // there is no behavior change for any non-rack printer. + int cur_ext_id = (ext_system && ext_system->GetTotalExtderCount() > 1) ? ext_system->GetCurrentExtderId() : 0; + bool has_nozzle_rack = obj->GetNozzleSystem()->GetNozzleRack()->IsSupported(); + std::string img_name = DevPrinterConfigUtil::get_filament_load_img(obj->printer_type, cur_ext_id, has_nozzle_rack); + if (!img_name.empty()) + { + m_filament_load_img->SetBitmap(create_scaled_bitmap(img_name, this, load_img_size)); + } + else if (ext_system->GetTotalExtderCount() == 2) { int cur_extder_id = ext_system->GetCurrentExtderId(); if (cur_extder_id == MAIN_EXTRUDER_ID) @@ -2202,6 +2229,10 @@ void StatusBasePanel::show_ams_group(bool show) wxGetApp().mainframe->m_monitor->Layout(); } + // On rack printers, don't clobber the rack view when the user has the switch on "Hotends". + // Inert for every non-rack printer: the switch stays hidden, so this guard never triggers. + if (show && m_ams_rack_switch->IsShown() && (m_ams_rack_switch->switch_left != true)) { return; } + if (m_ams_control_box->IsShown() != show) { m_ams_control_box->Show(show); m_ams_control->Layout(); @@ -2237,6 +2268,31 @@ void StatusBasePanel::show_filament_load_group(bool show) } } +void StatusBasePanel::jump_to_Rack() +{ + if (obj && obj->GetNozzleSystem()->GetNozzleRack()->IsSupported()) { + m_ams_rack_switch->updateState("right"); + m_ams_control_box->Show(false); + m_panel_nozzle_rack->Show(true); + Layout(); + } +} + +void StatusBasePanel::on_ams_rack_switch(wxCommandEvent &e) +{ + if (!m_ams_control_box->IsShown() && e.GetInt() == 1) { + m_ams_control_box->Show(e.GetInt() == 1); + m_panel_nozzle_rack->Show(e.GetInt() == 0); + Layout(); + } else if (!m_panel_nozzle_rack->IsShown() && e.GetInt() == 0) { + m_ams_control_box->Show(e.GetInt() == 1); + m_panel_nozzle_rack->Show(e.GetInt() == 0); + Layout(); + } + + e.Skip(); +} + void StatusPanel::update_camera_state(MachineObject* obj) { if (!obj) return; @@ -2778,6 +2834,8 @@ void StatusPanel::update(MachineObject *obj) update_ams(obj); update_cali(obj); + update_rack(obj); + if (obj) { //nozzle ui //m_button_left_of_extruder->SetSelected(); @@ -3412,7 +3470,14 @@ void StatusPanel::update_ams(MachineObject *obj) int tray_id_int = atoi(tray_id.c_str()); // new protocol if (ams_id_int < 128) { - if ((obj->tray_reading_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0) { + if (ams_it->second->IsAmsLiteMixed()) { + // Mixed AMS-Lite (A2L / N9) RFID-reading bits occupy positions 24..27. + if ((obj->tray_reading_bits & (1 << (AMS_LITE_MIXED_TRAY_INDEX_OFFSET + tray_id_int))) != 0) { + m_ams_control->PlayRridLoading(ams_id, tray_id); + } else { + m_ams_control->StopRridLoading(ams_id, tray_id); + } + } else if ((obj->tray_reading_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0) { m_ams_control->PlayRridLoading(ams_id, tray_id); } else { m_ams_control->StopRridLoading(ams_id, tray_id); @@ -5085,6 +5150,9 @@ void StatusPanel::set_default() m_ams_control->Hide(); m_ams_control_box->Hide(); m_ams_control->Reset(); + m_ams_rack_switch->updateState("left"); + m_ams_rack_switch->Hide(); + m_panel_nozzle_rack->Hide(); m_scale_panel->Hide(); m_filament_load_box->Hide(); m_filament_step->Hide(); @@ -5262,6 +5330,7 @@ void StatusPanel::msw_rescale() m_extruder_switching_status->msw_rescale(); m_ams_control->msw_rescale(); + m_panel_nozzle_rack->Rescale(); // m_filament_step->Rescale(); @@ -5283,6 +5352,20 @@ void StatusPanel::msw_rescale() Refresh(); } +void StatusPanel::update_rack(MachineObject *obj) +{ + // Rack switch + live rack panel are shown only for printers whose nozzle system reports a rack + // (H2C). Every other Bambu printer keeps the switch hidden and the panel collapsed -> the AMS + // monitor page is unchanged for X1/P1/A1/H2D/H2S. + if (obj && obj->GetNozzleSystem()->GetNozzleRack()->IsSupported()) { + m_ams_rack_switch->Show(); + m_panel_nozzle_rack->UpdateRackInfo(obj->GetNozzleSystem()->GetNozzleRack()); + } else { + m_ams_rack_switch->Show(false); + m_panel_nozzle_rack->Show(false); + } +} + void StatusPanel::update_filament_loading_panel(MachineObject* obj) { if (!obj) { diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index 8065f198e4..d286add1b0 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -50,6 +50,7 @@ namespace GUI { // Previous definitions class MessageDialog; +class wgtDeviceNozzleRack; enum CameraRecordingStatus { RECORDING_NONE, @@ -518,11 +519,16 @@ protected: bool m_show_ams_group{false}; bool m_show_filament_group{ false }; + /* AMS control box <-> live nozzle-rack panel toggle (rack printers only) */ + SwitchBoard* m_ams_rack_switch{ nullptr }; + AMSControl* m_ams_control; StaticBox* m_ams_control_box; wxStaticBitmap *m_ams_extruder_img; wxStaticBitmap* m_bitmap_extruder_img; + wgtDeviceNozzleRack* m_panel_nozzle_rack{ nullptr }; + wxPanel * m_panel_separator_right; wxPanel * m_panel_separotor_bottom; wxGridBagSizer *m_tasklist_info_sizer{nullptr}; @@ -611,6 +617,11 @@ public: void show_ams_group(bool show = true); void show_filament_load_group(bool show = true); MediaPlayCtrl* get_media_play_ctrl() {return m_media_play_ctrl;}; + + void jump_to_Rack(); + +private: + void on_ams_rack_switch(wxCommandEvent& event); }; @@ -767,6 +778,7 @@ protected: void update_temp_ctrl(MachineObject *obj); void update_misc_ctrl(MachineObject *obj); void update_ams(MachineObject* obj); + void update_rack(MachineObject* obj); void update_filament_loading_panel(MachineObject* obj); void update_extruder_status(MachineObject* obj); diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 535c561b55..5ffb321a62 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -1547,6 +1547,8 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err NozzleVolumeType nozzle_volume_type = (NozzleVolumeType) (nozzle_volume_type_opt->get_at(used_extruders[i])); if (nozzle_volume_type == NozzleVolumeType::nvtStandard) { used_extruders_flow[used_extruders[i]] = "Standard"; + } else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) { + used_extruders_flow[used_extruders[i]] = "TPU High Flow"; } else { used_extruders_flow[used_extruders[i]] = "High Flow"; } @@ -1562,6 +1564,8 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err flow_type_of_machine.push_back("High Flow"); } else if (it->GetNozzleFlowType() == NozzleFlowType::S_FLOW) { flow_type_of_machine.push_back("Standard"); + } else if (it->GetNozzleFlowType() == NozzleFlowType::U_FLOW) { + flow_type_of_machine.push_back("TPU High Flow"); } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f1eb41b0b4..658d658823 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1847,6 +1847,27 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) if (opt_key == "enable_prime_tower") { auto timelapse_type = m_config->option>("timelapse_type"); bool timelapse_enabled = timelapse_type->value == TimelapseType::tlSmooth; + if (!boost::any_cast(value)) { + // Disabling the prime tower on a multi-nozzle printer degrades quality because nozzle changes rely + // on it. Gate on any extruder having extruder_max_nozzle_count > 1 so single-nozzle and dual-extruder + // (H2D, {1,1}) printers keep their exact existing behavior. + // Orca: gate on any_of(count > 1) rather than the sum of counts >= 2. The sum form would also fire for + // H2D ({1,1} sums to 2); any_of(>1) preserves H2D's current no-dialog behavior. + auto *max_nozzle_counts_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("extruder_max_nozzle_count"); + const bool has_multiple_nozzle = max_nozzle_counts_opt && + std::any_of(max_nozzle_counts_opt->values.begin(), max_nozzle_counts_opt->values.end(), + [](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); }); + if (has_multiple_nozzle) { + MessageDialog dlg(wxGetApp().plater(), + _L("Prime tower is required for nozzle changing. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?"), + _L("Warning"), wxICON_WARNING | wxYES | wxNO); + if (dlg.ShowModal() == wxID_NO) { + DynamicPrintConfig new_conf = *m_config; + new_conf.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); + m_config_manipulation.apply(m_config, &new_conf); + } + } + } if (!boost::any_cast(value) && timelapse_enabled) { bool set_enable_prime_tower = false; MessageDialog dlg(wxGetApp().plater(), _L("A prime tower is required for smooth timelapse mode. There may be flaws on the model without a prime tower. Are you sure you want to disable the prime tower\?"), diff --git a/src/slic3r/GUI/UpgradePanel.cpp b/src/slic3r/GUI/UpgradePanel.cpp index 4a4ad9e947..9ff1d12709 100644 --- a/src/slic3r/GUI/UpgradePanel.cpp +++ b/src/slic3r/GUI/UpgradePanel.cpp @@ -3,6 +3,7 @@ #include #include #include "slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.h" +#include "slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h" #include "GUI.hpp" #include "GUI_App.hpp" @@ -217,6 +218,9 @@ MachineInfoPanel::MachineInfoPanel(wxWindow* parent, wxWindowID id, const wxPoin createAirPumpWidgets(m_main_left_sizer); createExtinguishWidgets(m_main_left_sizer); + // nozzle rack widgets (H2C induction hotend rack; hidden unless GetNozzleRack()->IsSupported()) + createNozzleRackWidgets(m_main_left_sizer); + m_main_sizer->Add(m_main_left_sizer, 1, wxEXPAND, 0); wxBoxSizer *m_main_right_sizer = new wxBoxSizer(wxVERTICAL); @@ -431,6 +435,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_nozzle_rack = ScalableBitmap(this, "nozzle_rack", 160); upgrade_green_icon = ScalableBitmap(this, "monitor_upgrade_online", 5); upgrade_gray_icon = ScalableBitmap(this, "monitor_upgrade_offline", 5); @@ -539,6 +544,7 @@ void MachineInfoPanel::update(MachineObject* obj) update_cut(obj); update_laszer(obj); update_extinguish(obj); + update_nozzle_rack(obj); //update progress int upgrade_percent = obj->get_upgrade_percent(); @@ -1257,6 +1263,80 @@ void MachineInfoPanel::show_extinguish(bool show) } } +void MachineInfoPanel::createNozzleRackWidgets(wxBoxSizer *main_left_sizer) +{ + // horizontal line above + m_nozzle_rack_line_above = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); + m_nozzle_rack_line_above->SetBackgroundColour(wxColour(206, 206, 206)); + main_left_sizer->Add(m_nozzle_rack_line_above, 0, wxEXPAND | wxLEFT, FromDIP(40)); + + m_nozzle_rack_sizer = new wxBoxSizer(wxHORIZONTAL); + + // left placeholder icon (keep consistent spacing with others) + m_nozzle_rack_img = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(200), FromDIP(200))); + m_nozzle_rack_img->SetBitmap(m_img_nozzle_rack.bmp()); + m_nozzle_rack_sizer->Add(m_nozzle_rack_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); + + // right content: label + update button + auto *content_sizer = new wxBoxSizer(wxHORIZONTAL); + m_nozzle_rack_text = new wxStaticText(this, wxID_ANY, _L("Hotends on Rack"), wxDefaultPosition, wxDefaultSize, 0); + m_nozzle_rack_text->Wrap(-1); + m_nozzle_rack_text->SetFont(Label::Head_14); + content_sizer->Add(m_nozzle_rack_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxLEFT, FromDIP(50)); + + m_nozzle_rack_update_btn = new Button(this, _L("Info")); + StateColor btn_bg(std::pair(wxColour(255, 255, 255), StateColor::Disabled), std::pair(wxColour(200, 200, 200), StateColor::Pressed), + std::pair(wxColour(240, 240, 240), StateColor::Hovered), std::pair(wxColour(255, 255, 255), StateColor::Enabled), + std::pair(wxColour(255, 255, 255), StateColor::Normal)); + StateColor btn_bd(std::pair(wxColour(200, 200, 200), StateColor::Disabled), std::pair(wxColour(150, 150, 150), StateColor::Enabled)); + StateColor btn_text(std::pair(wxColour(150, 150, 150), StateColor::Disabled), std::pair(wxColour(0, 0, 0), StateColor::Enabled)); + m_nozzle_rack_update_btn->SetBackgroundColor(btn_bg); + m_nozzle_rack_update_btn->SetBorderColor(btn_bd); + m_nozzle_rack_update_btn->SetTextColor(btn_text); + m_nozzle_rack_update_btn->SetFont(Label::Body_10.Bold()); + m_nozzle_rack_update_btn->SetMinSize(wxSize(FromDIP(-1), FromDIP(24))); + m_nozzle_rack_update_btn->SetCornerRadius(FromDIP(12)); + m_nozzle_rack_update_btn->Bind(wxEVT_BUTTON, &MachineInfoPanel::on_nozzle_rack_update, this); + content_sizer->Add(m_nozzle_rack_update_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(350)); + + m_nozzle_rack_sizer->Add(content_sizer, 1, wxEXPAND, 0); + + main_left_sizer->Add(m_nozzle_rack_sizer, 0, wxEXPAND, 0); +} + +void MachineInfoPanel::update_nozzle_rack(MachineObject* obj) +{ + if (obj && obj->GetNozzleSystem()) { + auto rack = obj->GetNozzleSystem()->GetNozzleRack(); + if (rack && rack->IsSupported()) { + show_nozzle_rack(true); + } + else { + show_nozzle_rack(false); + } + } +} + +void MachineInfoPanel::show_nozzle_rack(bool show) +{ + if (m_nozzle_rack_img->IsShown() != show) { + m_nozzle_rack_line_above->Show(show); + m_nozzle_rack_update_btn->Show(show); + m_nozzle_rack_img->Show(show); + m_nozzle_rack_text->Show(show); + } +} + +void MachineInfoPanel::on_nozzle_rack_update(wxCommandEvent &event) +{ + if (!m_obj || !m_obj->GetNozzleSystem()) return; + auto rack = m_obj->GetNozzleSystem()->GetNozzleRack(); + if (!rack) return; + + wgtDeviceNozzleRackUpgradeDlg dlg(this, rack); + dlg.ShowModal(); +} + void MachineInfoPanel::on_sys_color_changed() { diff --git a/src/slic3r/GUI/UpgradePanel.hpp b/src/slic3r/GUI/UpgradePanel.hpp index 2fb0f8dcb1..ef56bde03d 100644 --- a/src/slic3r/GUI/UpgradePanel.hpp +++ b/src/slic3r/GUI/UpgradePanel.hpp @@ -134,6 +134,13 @@ protected: wxStaticLine* m_extinguish_line_above = nullptr;; uiDeviceUpdateVersion* m_extinguish_version = nullptr; + /* nozzle rack (H2C induction hotend rack) — opens wgtDeviceNozzleRackUpgradeDlg */ + wxBoxSizer* m_nozzle_rack_sizer = nullptr; + wxStaticBitmap* m_nozzle_rack_img = nullptr; + wxStaticLine* m_nozzle_rack_line_above = nullptr; + wxStaticText* m_nozzle_rack_text = nullptr; + Button* m_nozzle_rack_update_btn = nullptr; + /* upgrade widgets */ wxBoxSizer* m_upgrading_sizer; wxStaticText * m_staticText_upgrading_info; @@ -155,6 +162,7 @@ protected: ScalableBitmap m_img_cutting; ScalableBitmap m_img_laser; ScalableBitmap m_img_extinguish; + ScalableBitmap m_img_nozzle_rack; ScalableBitmap upgrade_gray_icon; ScalableBitmap upgrade_green_icon; ScalableBitmap upgrade_yellow_icon; @@ -212,16 +220,21 @@ private: void createCuttingWidgets(wxBoxSizer* main_left_sizer); void createLaserWidgets(wxBoxSizer* main_left_sizer); void createExtinguishWidgets(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_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_nozzle_rack(bool show = true); + + void on_nozzle_rack_update(wxCommandEvent& event); }; //enum UpgradeMode { diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp new file mode 100644 index 0000000000..9bcf3e234c --- /dev/null +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -0,0 +1,1215 @@ +#include "MultiNozzleSync.hpp" + +#include "../GUI_App.hpp" +#include "../I18N.hpp" +#include "../Plater.hpp" +#include "../DeviceManager.hpp" +#include "../DeviceCore/DevConfigUtil.h" +#include "../DeviceCore/DevNozzleSystem.h" +#include "../wxExtensions.hpp" +#include "Button.hpp" +#include "Label.hpp" +#include "StaticBox.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Utils.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Slic3r { namespace GUI { + +wxDEFINE_EVENT(EVT_NOZZLE_SELECTED, wxCommandEvent); + +static const int LeftExtruderIdx = 0; +static const int RightExtruderIdx = 1; + +// ---- extruder_nozzle_stats config helpers --------------------------------------------------------------------- +// Orca: nozzle stats have no live runtime object; they are stored in the printer preset's `extruder_nozzle_stats` +// config key (ConfigOptionStrings, one encoded map per extruder) via get_extruder_nozzle_stats / +// save_extruder_nozzle_stats_to_string. These helpers read and write that config. + +static int extruder_nozzle_count(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type) +{ + if (!preset_bundle) + return 0; + auto *opt = preset_bundle->printers.get_edited_preset().config.option("extruder_nozzle_stats"); + if (!opt) + return 0; + const auto stats = get_extruder_nozzle_stats(opt->values); + if (extruder_id < 0 || extruder_id >= (int) stats.size()) + return 0; + const auto it = stats[extruder_id].find(volume_type); + return it == stats[extruder_id].end() ? 0 : it->second; +} + +static int extruder_nozzle_count_total(PresetBundle *preset_bundle, int extruder_id) +{ + if (!preset_bundle) + return 0; + auto *opt = preset_bundle->printers.get_edited_preset().config.option("extruder_nozzle_stats"); + if (!opt) + return 0; + const auto stats = get_extruder_nozzle_stats(opt->values); + if (extruder_id < 0 || extruder_id >= (int) stats.size()) + return 0; + int sum = 0; + for (const auto &kv : stats[extruder_id]) + sum += kv.second; + return sum; +} + +// ---- ManualNozzleCountDialog ---------------------------------------------------------------------------------- + +ManualNozzleCountDialog::ManualNozzleCountDialog( + wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int max_nozzle_count, bool force_no_zero) + : DPIDialog(parent, wxID_ANY, _L("Set nozzle count"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) +{ + SetBackgroundColour(*wxWHITE); + + wxPanel *content = new wxPanel(this); + content->SetBackgroundColour(*wxWHITE); + wxBoxSizer *content_sizer = new wxBoxSizer(wxHORIZONTAL); + content->SetSizer(content_sizer); + + wxBoxSizer *choice_sizer = new wxBoxSizer(wxVERTICAL); + choice_sizer->Add(new wxStaticText(content, wxID_ANY, _L("Please set nozzle count")), 0, wxALL | wxALIGN_LEFT, FromDIP(10)); + + wxArrayString nozzle_choices; + for (int i = 0; i <= max_nozzle_count; ++i) + nozzle_choices.Add(wxString::Format("%d", i)); + + // Orca's NozzleVolumeType is {Standard, High Flow} (no Hybrid), so exactly one choice is shown per dialog. + if (volume_type == nvtStandard) { + choice_sizer->Add(new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtStandard))), 0, wxALL | wxALIGN_LEFT, FromDIP(5)); + m_standard_choice = new wxChoice(content, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(100), -1), nozzle_choices); + m_standard_choice->SetSelection(standard_count); + choice_sizer->Add(m_standard_choice, 0, wxLEFT | wxBOTTOM | wxRIGHT, FromDIP(10)); + } + if (volume_type == nvtHighFlow) { + choice_sizer->Add(new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtHighFlow))), 0, wxALL | wxALIGN_LEFT, FromDIP(5)); + m_highflow_choice = new wxChoice(content, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(100), -1), nozzle_choices); + m_highflow_choice->SetSelection(highflow_count); + choice_sizer->Add(m_highflow_choice, 0, wxLEFT | wxBOTTOM | wxRIGHT, FromDIP(10)); + } + + m_error_label = new wxStaticText(this, wxID_ANY, ""); + m_error_label->SetForegroundColour(wxColour("#E14747")); + m_error_label->Hide(); + + auto update_nozzle_error = [this, force_no_zero, content, max_nozzle_count](int standard_count, int highflow_count) { + const int total_count = standard_count + highflow_count; + if (0 < total_count && total_count <= max_nozzle_count && m_error_label->IsShown()) { + m_error_label->Hide(); + m_confirm_btn->Enable(); + Layout(); + Fit(); + } else if ((total_count == 0 && force_no_zero) || total_count > max_nozzle_count) { + const wxString error_tip = total_count == 0 ? _L("Error: Can not set both nozzle count to zero.") + : wxString::Format(_L("Error: Nozzle count can not exceed %d."), max_nozzle_count); + m_error_label->SetLabel(error_tip); + m_error_label->Wrap(content->GetSize().x); + m_error_label->Show(); + m_confirm_btn->Disable(); + Layout(); + Fit(); + } + }; + + if (m_standard_choice) + m_standard_choice->Bind(wxEVT_CHOICE, [this, update_nozzle_error](wxCommandEvent &e) { + update_nozzle_error(m_standard_choice->GetSelection(), m_highflow_choice ? m_highflow_choice->GetSelection() : 0); + e.Skip(); + }); + if (m_highflow_choice) + m_highflow_choice->Bind(wxEVT_CHOICE, [this, update_nozzle_error](wxCommandEvent &e) { + update_nozzle_error(m_standard_choice ? m_standard_choice->GetSelection() : 0, m_highflow_choice->GetSelection()); + e.Skip(); + }); + + content_sizer->Add(choice_sizer, 0, wxALIGN_CENTRE_VERTICAL); + + m_confirm_btn = new Button(this, _L("Confirm")); + m_confirm_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Window); + m_confirm_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_OK); }); + + wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL); + main_sizer->Add(content, 1, wxEXPAND); + main_sizer->Add(m_error_label, 0, wxALL, FromDIP(5)); + main_sizer->Add(m_confirm_btn, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, FromDIP(20)); + + SetSizerAndFit(main_sizer); + CentreOnParent(); + wxGetApp().UpdateDlgDarkUI(this); +} + +int ManualNozzleCountDialog::GetNozzleCount(NozzleVolumeType volume_type) const +{ + if (volume_type == nvtStandard) + return m_standard_choice ? m_standard_choice->GetSelection() : 0; + if (volume_type == nvtHighFlow) + return m_highflow_choice ? m_highflow_choice->GetSelection() : 0; + return 0; +} + +// ---- free functions ------------------------------------------------------------------------------------------- + +void setExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type, int nozzle_count, bool clear_all) +{ + if (!preset_bundle) + return; + auto &config = preset_bundle->printers.get_edited_preset().config; + auto *opt = config.option("extruder_nozzle_stats", true); + if (!opt) + return; + + const int extruder_count = preset_bundle->get_printer_extruder_count(); + auto stats = get_extruder_nozzle_stats(opt->values); + if ((int) stats.size() < extruder_count) + stats.resize(extruder_count); + if (extruder_id < 0 || extruder_id >= (int) stats.size()) + return; + + if (clear_all) + stats[extruder_id].clear(); + stats[extruder_id][volume_type] = nozzle_count; + opt->values = save_extruder_nozzle_stats_to_string(stats); +} + +bool nozzle_diameter_supports_tpu_high_flow(double nozzle_diameter) +{ + // "Direct Drive TPU High Flow" is offered only on the H2D/H2D Pro 0.4 and 0.6 nozzles. + constexpr double kEps = 0.01; + return std::fabs(nozzle_diameter - 0.4) < kEps || std::fabs(nozzle_diameter - 0.6) < kEps; +} + +bool extruder_supports_tpu_high_flow(PresetBundle *preset_bundle, int extruder_id) +{ + if (!preset_bundle) + return false; + const auto *nozzle_diameter = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter"); + return nozzle_diameter != nullptr && extruder_id >= 0 && extruder_id < (int) nozzle_diameter->values.size() && + nozzle_diameter_supports_tpu_high_flow(nozzle_diameter->values[extruder_id]); +} + +void updateNozzleCountDisplay(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type) +{ + // Orca: the sidebar ExtruderGroup has no per-extruder nozzle-count badge yet, so this refresh is a no-op. The + // authoritative count already lives in the extruder_nozzle_stats config written by setExtruderNozzleCount; kept + // as the wiring point for that future UI. + (void) preset_bundle; + (void) extruder_id; + (void) volume_type; +} + +void manuallySetNozzleCount(int extruder_id) +{ + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) + return; + + DynamicPrintConfig full_config = preset_bundle->full_config(); + auto *max_nozzle_count = full_config.option("extruder_max_nozzle_count"); + // Multi-nozzle gate: no-op for every single-nozzle / dual-extruder ({1,1}) printer. + // Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate. + if (!max_nozzle_count || + !std::any_of(max_nozzle_count->values.begin(), max_nozzle_count->values.end(), + [](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); })) + return; + + auto *nozzle_volume_type_opt = full_config.option("nozzle_volume_type"); + if (!nozzle_volume_type_opt || extruder_id < 0 || extruder_id >= (int) nozzle_volume_type_opt->values.size() || + extruder_id >= (int) max_nozzle_count->values.size()) + return; + + const NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id]); + const int standard_count = extruder_nozzle_count(preset_bundle, extruder_id, nvtStandard); + const int highflow_count = extruder_nozzle_count(preset_bundle, extruder_id, nvtHighFlow); + + // Require at least one nozzle when the other extruder currently has none. + bool force_no_zero = false; + if (nozzle_volume_type_opt->values.size() > 1) + force_no_zero = extruder_nozzle_count_total(preset_bundle, 1 - extruder_id) == 0; + + ManualNozzleCountDialog dialog(wxGetApp().plater(), volume_type, standard_count, highflow_count, max_nozzle_count->values[extruder_id], force_no_zero); + if (dialog.ShowModal() == wxID_OK) { + setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, dialog.GetNozzleCount(volume_type), true); + updateNozzleCountDisplay(preset_bundle, extruder_id, volume_type); + wxGetApp().plater()->update(); + } +} + +// ==== device-sync half ==== + +ExtruderBadge::ExtruderBadge(wxWindow* parent) : wxPanel(parent) +{ + wxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + SetBackgroundColour("#F8F8F8"); + wxBitmap icon = create_scaled_bitmap("extruder_badge_none_selected", nullptr, FromDIP(90)); + + auto extruder_label = new Label(this, _L("Extruder")); + + badget = new wxStaticBitmap(this, wxID_ANY, icon); + + m_diameter_list = { "0.4","0.4" }; + m_volume_type_list = { NozzleVolumeType::nvtStandard,NozzleVolumeType::nvtStandard }; + + left_diameter_desp = new Label(this, _L(m_diameter_list[LeftExtruderIdx])); + right_diameter_desp = new Label(this, _L(m_diameter_list[RightExtruderIdx])); + left_flow_desp = new Label(this, _L(get_nozzle_volume_type_string(m_volume_type_list[LeftExtruderIdx]))); + right_flow_desp = new Label(this, _L(get_nozzle_volume_type_string(m_volume_type_list[RightExtruderIdx]))); + + + wxBoxSizer* top_h = new wxBoxSizer(wxVERTICAL); + top_h->Add(extruder_label, 0, wxALIGN_CENTER | wxBOTTOM, FromDIP(10)); + + main_sizer->Add(top_h, 0, wxEXPAND | wxTOP, FromDIP(5)); + main_sizer->Add(badget, 0, wxALIGN_CENTER | wxTOP, FromDIP(5)); + + wxBoxSizer* left_extruder = new wxBoxSizer(wxVERTICAL); + + left_extruder->Add(left_diameter_desp, 0, wxALIGN_CENTER | wxLEFT, FromDIP(12)); + left_extruder->Add(left_flow_desp, 0, wxALIGN_CENTER | wxLEFT, FromDIP(12)); + + wxBoxSizer* right_extruder = new wxBoxSizer(wxVERTICAL); + right_extruder->Add(right_diameter_desp, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(12)); + right_extruder->Add(right_flow_desp, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(12)); + + wxBoxSizer* info_sizer = new wxBoxSizer(wxHORIZONTAL); + info_sizer->Add(left_extruder, 0); + info_sizer->AddStretchSpacer(); + info_sizer->Add(right_extruder, 0); + + main_sizer->Add(info_sizer, 0, wxEXPAND | wxTOP, FromDIP(5)); + + SetSizer(main_sizer); + Layout(); + Fit(); + wxGetApp().UpdateDarkUIWin(this); +} + +void ExtruderBadge::SetExtruderInfo(int extruder_id, const std::string& diameter, const NozzleVolumeType& volume_type) +{ + m_diameter_list[extruder_id] = diameter; + m_volume_type_list[extruder_id] = volume_type; + + if (extruder_id == LeftExtruderIdx) { + left_diameter_desp->SetLabel(diameter + " mm"); + left_flow_desp->SetLabel(_L(get_nozzle_volume_type_string(volume_type))); + } + else if (extruder_id == RightExtruderIdx) { + right_diameter_desp->SetLabel(diameter + " mm"); + right_flow_desp->SetLabel(_L(get_nozzle_volume_type_string(volume_type))); + } +} + +void ExtruderBadge::SetExtruderValid(bool right_on) +{ + if (!right_on) { + right_diameter_desp->SetLabel(""); + right_flow_desp->SetLabel(""); + } + std::string badge_name; + if (m_right_on) + badge_name = "extruder_badge_none_selected"; + else + badge_name = "extruder_badge_none_selected_single"; + wxBitmap icon = create_scaled_bitmap(badge_name, nullptr, FromDIP(90)); + badget->SetBitmap(icon); + + m_right_on = right_on; +} + +void ExtruderBadge::SetExtruderStatus(bool left_selected, bool right_selected) +{ + std::string badge_name; + if (m_right_on) { + if (left_selected && right_selected) + badge_name = "extruder_badge_both_selected"; + else if (left_selected) + badge_name = "extruder_badge_left_selected"; + else if (right_selected) + badge_name = "extruder_badge_right_selected"; + else + badge_name = "extruder_badge_none_selected"; + } + else if (left_selected) { + badge_name = "extruder_badge_left_selected_single"; + } + else { + badge_name = "extruder_badge_none_selected_single"; + } + + wxBitmap icon = create_scaled_bitmap(badge_name, nullptr, FromDIP(90)); + badget->SetBitmap(icon); + Layout(); +} + + +void ExtruderBadge::UnMarkRelatedItems(const NozzleOption& option) +{ + bool left_selected = true, right_selected = true; + + if (m_diameter_list[LeftExtruderIdx] == option.diameter && option.extruder_nozzle_stats.count(LeftExtruderIdx) + && option.extruder_nozzle_stats.at(LeftExtruderIdx).count(m_volume_type_list[LeftExtruderIdx]) + && option.extruder_nozzle_stats.at(LeftExtruderIdx).at(m_volume_type_list[LeftExtruderIdx])>0) + left_selected = false; + + if (m_diameter_list[RightExtruderIdx] == option.diameter && option.extruder_nozzle_stats.count(RightExtruderIdx) + && option.extruder_nozzle_stats.at(RightExtruderIdx).count(m_volume_type_list[RightExtruderIdx]) + && option.extruder_nozzle_stats.at(RightExtruderIdx).at(m_volume_type_list[RightExtruderIdx])>0) + right_selected = false; + + SetExtruderStatus(left_selected, right_selected); +} + +void ExtruderBadge::MarkRelatedItems(const NozzleOption& option) +{ + bool left_selected = false, right_selected = false; + + if (m_diameter_list[LeftExtruderIdx] == option.diameter && option.extruder_nozzle_stats.count(LeftExtruderIdx) + && option.extruder_nozzle_stats.at(LeftExtruderIdx).count(m_volume_type_list[LeftExtruderIdx]) + && option.extruder_nozzle_stats.at(LeftExtruderIdx).at(m_volume_type_list[LeftExtruderIdx]) > 0) + left_selected = true; + + if (m_diameter_list[RightExtruderIdx] == option.diameter && option.extruder_nozzle_stats.count(RightExtruderIdx) + && option.extruder_nozzle_stats.at(RightExtruderIdx).count(m_volume_type_list[RightExtruderIdx]) + && option.extruder_nozzle_stats.at(RightExtruderIdx).at(m_volume_type_list[RightExtruderIdx]) > 0) + right_selected = true; + + SetExtruderStatus(left_selected, right_selected); +} + +HotEndTable::HotEndTable(wxWindow* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,wxBORDER_NONE) +{ + Bind(wxEVT_PAINT, &HotEndTable::OnPaint, this); + auto main_sizer = new wxBoxSizer(wxVERTICAL); + auto label = new Label(this, _L("Induction Hotend Rack")); + label->SetBackgroundColour("#F8F8F8"); + + m_arow_nozzle_box = CreateNozzleBox({ 0,2,4 }); + m_brow_nozzle_box = CreateNozzleBox({ 1,3,5 }); + main_sizer->Add(label, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxBOTTOM, FromDIP(5)); + main_sizer->Add(m_arow_nozzle_box, 0, wxLEFT | wxRIGHT, FromDIP(5)); + main_sizer->Add(m_brow_nozzle_box, 0, wxLEFT | wxRIGHT, FromDIP(5)); + SetBackgroundColour("#F8F8F8"); + + SetSizer(main_sizer); + Layout(); + Fit(); + wxGetApp().UpdateDarkUIWin(this); +} + +void HotEndTable::UpdateRackInfo(std::weak_ptr rack) +{ + m_nozzle_rack = rack; + const auto& nozzle_rack = rack.lock(); + if (nozzle_rack) { + UpdateNozzleItems(m_nozzle_items, nozzle_rack); + } +} + +std::vector HotEndTable::FilterHotEnds(const NozzleOption& option) +{ + auto rack = m_nozzle_rack.lock(); + if (!rack) + return {}; + + std::vector nozzles_to_search; + + for (auto& item : option.extruder_nozzle_stats) { + for (auto& nozzle : item.second) { + HotEndAttr info; + info.diameter = option.diameter; + info.extruder_id = item.first; + info.volume_type = nozzle.first; + nozzles_to_search.emplace_back(info); + } + } + + std::vector filtered_nozzles; + + for (auto& info : nozzles_to_search) { + + float diameter = atof(info.diameter.c_str()); + NozzleFlowType flow = DevNozzle::ToNozzleFlowType(info.volume_type); + int extruder_id = 1 - info.extruder_id; //physical + + auto nozzles = rack->GetNozzleSystem()->CollectNozzles(extruder_id, flow, diameter); + + for (auto& nozzle : nozzles) { + if (nozzle.IsOnRack()) + filtered_nozzles.emplace_back(nozzle.GetNozzleId()); + } + } + + return filtered_nozzles; +} + +void HotEndTable::MarkRelatedItems(const NozzleOption& option) +{ + const static StateColor bg_green( + std::pair(wxColour("#DBFDE7"), StateColor::Normal) + ); + + const static StateColor bd_green( + std::pair(wxColour("#00AE42"), StateColor::Normal) + ); + auto filtered_nozzles = FilterHotEnds(option); + for (auto nozzle_id : filtered_nozzles) { + auto iter = m_nozzle_items.find(nozzle_id); + if (iter == m_nozzle_items.end()) + continue; + auto& item = iter->second; + item->SetBackgroundColor(bg_green); + item->SetBorderColor(bd_green); + for (auto child : item->GetChildren()) { + child->SetBackgroundColour("#DBFDE7"); + } + } + wxGetApp().UpdateDarkUIWin(this); +} + +void HotEndTable::UnMarkRelatedItems(const NozzleOption& option) +{ + static const wxColour bg_color("#EEEEEE"); + static const wxColour bd_color("#CECECE"); + const static StateColor bg_green( + std::pair(bg_color, StateColor::Normal) + ); + + const static StateColor bd_green( + std::pair(bd_color, StateColor::Normal) + ); + auto filtered_nozzles = FilterHotEnds(option); + for (auto nozzle_id : filtered_nozzles) { + auto iter = m_nozzle_items.find(nozzle_id); + if (iter == m_nozzle_items.end()) + continue; + auto& item = iter->second; + item->SetBackgroundColor(bg_green); + item->SetBorderColor(bd_green); + for (auto child : item->GetChildren()) { + child->SetBackgroundColour(bg_color); + } + } + wxGetApp().UpdateDarkUIWin(this); +} + + + +StaticBox* HotEndTable::CreateNozzleBox(const std::vector& nozzle_indices) +{ + StaticBox* nozzle_box = new StaticBox(this); + nozzle_box->SetBackgroundColour("#F8F8F8"); + nozzle_box->SetBorderColorNormal("#F8F8F8"); + nozzle_box->SetCornerRadius(0); + + wxSizer* h_sizer = new wxBoxSizer(wxHORIZONTAL); + for (auto idx : nozzle_indices) { + wgtDeviceNozzleRackNozzleItem* nozzle_item = new wgtDeviceNozzleRackNozzleItem(nozzle_box, idx); + nozzle_item->SetBackgroundColorNormal("#EEEEEE"); + for (auto& child : nozzle_item->GetChildren()) + child->SetBackgroundColour("#EEEEEE"); + m_nozzle_items[idx] = nozzle_item; + h_sizer->Add(nozzle_item, 0, wxALL, FromDIP(8)); + } + + nozzle_box->SetSizer(h_sizer); + + return nozzle_box; +} + +void HotEndTable::UpdateNozzleItems(const std::unordered_map& nozzle_items, std::shared_ptr nozzle_rack) +{ + for (auto& item : nozzle_items) + item.second->Update(nozzle_rack); +} + +void HotEndTable::OnPaint(wxPaintEvent& evt) +{ + wxPaintDC dc(this); + wxSize size = GetClientSize(); + + dc.SetPen(wxPen(wxColour("#EEEEEE"), 2)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRoundedRectangle(0, 0, size.GetWidth()-2, size.GetHeight()-2, 5); +} + +NozzleListTable::NozzleListTable(wxWindow* parent) : wxPanel(parent,wxID_ANY,wxDefaultPosition,wxDefaultSize ,wxNO_BORDER) +{ + m_web_view = wxWebView::New(this, wxID_ANY, wxEmptyString, wxDefaultPosition,wxDefaultSize,wxString::FromAscii(wxWebViewBackendDefault),wxNO_BORDER); + m_web_view->AddScriptMessageHandler("nozzleListTable"); + m_web_view->EnableContextMenu(false); + fs::path filepath = fs::path(resources_dir()) / "web/flush/NozzleListTable.html"; + wxFileName fn(wxString::FromUTF8(filepath.string())); + wxString url = wxFileSystem::FileNameToURL(fn); + m_web_view->LoadURL(url); + + auto sizer = new wxBoxSizer(wxVERTICAL); + sizer->AddStretchSpacer(0); + sizer->Add(m_web_view, 1, wxEXPAND); + sizer->AddStretchSpacer(0); + SetSizer(sizer); + Layout(); + + m_web_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this,sizer](wxWebViewEvent& evt) { + std::string message = evt.GetString().ToStdString(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << "Received message: " << message; + try { + json j = json::parse(message); + if (j["msg"].get() == "init") { + auto table_obj_str = BuildTableObjStr(); + auto text_obj_str = BuildTextObjStr(); + CallAfter([table_obj_str, text_obj_str, this] { + wxString script1 = wxString::Format("initText(%s)", text_obj_str); + m_web_view->RunScript(script1); + wxString script2 = wxString::Format("initTable(%s)", table_obj_str); + m_web_view->RunScript(script2); + }); + } + else if (j["msg"].get() == "updateList") { + auto table_obj_str = BuildTableObjStr(); + + CallAfter([table_obj_str, this] { + wxString script1 = wxString::Format("updateTable(%s)", table_obj_str); + m_web_view->RunScript(script1); + }); + + } + else if (j["msg"].get() == "onSelect") { + int idx = j["index"].get(); + m_selected_idx = idx; + SendSelectionChangedEvent(); + } + else if (j["msg"].get() == "layout") { + int height = j["height"].get(); + int width = j["width"].get(); + wxSize table_size = wxSize(-1, FromDIP(height)); + m_web_view->SetSize(table_size); + m_web_view->SetMaxSize(table_size); + m_web_view->SetMinSize(table_size); + this->Layout(); + this->GetParent()->Layout(); + this->GetParent()->Fit(); + } + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Failed to parse json message: " << message; + } + }); + + wxGetApp().UpdateDarkUIWin(this); +} + +wxString NozzleListTable::BuildTableObjStr() +{ + json obj; + obj["darkmode"] = wxGetApp().dark_mode(); + obj["data"] = json::array(); + for(size_t idx = 0; idx < m_nozzle_options.size(); ++idx){ + const auto& option = m_nozzle_options[idx]; + json json_opt; + json_opt["diameter"] = option.diameter; + json_opt["is_selected"] = idx == m_selected_idx; + json_opt["darkmode"] = wxGetApp().dark_mode(); + + json extruders = json::object(); + for (const auto& [extruderId, nozzleInfo] : option.extruder_nozzle_stats) { + nlohmann::json nozzleData; + nozzleData["type"] = ""; + nozzleData["count"] = std::accumulate(nozzleInfo.begin(), nozzleInfo.end(), 0, [](int val, auto elem) {return val + elem.second; }); + extruders[std::to_string(extruderId)] = nozzleData; + } + json_opt["extruders"] = extruders; + obj["data"].push_back(json_opt); + } + return wxString::FromUTF8(obj.dump().c_str()); +} + +void NozzleListTable::SendSelectionChangedEvent() +{ + wxCommandEvent event(EVT_NOZZLE_SELECTED, GetId()); + event.SetInt(m_selected_idx); + event.SetEventObject(this); + ProcessWindowEvent(event); +} +wxString NozzleListTable::BuildTextObjStr() +{ + wxString nozzle_selection = _L("Nozzle Selection"); + wxString nozzle_list = _L("Available Nozzles"); + std::string pt = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle); + wxString Left = _L(DevPrinterConfigUtil::get_toolhead_display_name(pt, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true)); + wxString Right = _L(DevPrinterConfigUtil::get_toolhead_display_name(pt, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::TitleCase, true)); + wxString highflow = _L(get_nozzle_volume_type_string(nvtHighFlow)); + wxString standard = _L(get_nozzle_volume_type_string(nvtStandard)); + + wxString text_obj = "{"; + text_obj += wxString::Format("\"nozzle_selection_label\":\"%s\",", nozzle_selection); + text_obj += wxString::Format("\"nozzle_list_label\":\"%s\",", nozzle_list); + text_obj += wxString::Format("\"left_label\":\"%s\",", Left); + text_obj += wxString::Format("\"right_label\":\"%s\",", Right); + text_obj += wxString::Format("\"highflow_label\":\"%s\",", highflow); + text_obj += wxString::Format("\"standard_label\":\"%s\",", standard); + text_obj += wxString::Format("\"language\":\"%s\"", wxGetApp().app_config->get_language_code()); + text_obj += "}"; + + return text_obj; +} + +void NozzleListTable::SetOptions(const std::vector& options,int default_select) +{ + m_nozzle_options = options; + m_selected_idx = default_select; + auto table_obj_str = BuildTableObjStr(); + wxString script1 = wxString::Format("updateTable(%s)", table_obj_str); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "update table " << script1; + +#if 1 + m_web_view->RunScript(script1); +#else + CallAfter([script1, this]() { + m_web_view->RunScript(script1); + }); +#endif +} + +MultiNozzleStatusTable::MultiNozzleStatusTable(wxWindow* parent): wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) +{ + m_badge = new ExtruderBadge(this); + + SetBackgroundColour(wxColour("#F8F8F8")); + + auto main_sizer = new wxBoxSizer(wxVERTICAL); + + auto title_panel = new wxPanel(this); + title_panel->SetBackgroundColour(0xEEEEEE); + auto title_sizer = new wxBoxSizer(wxHORIZONTAL); + title_panel->SetSizer(title_sizer); + + Label* static_text = new Label(this, _L("Nozzle Info")); + static_text->SetFont(Label::Head_13); + static_text->SetBackgroundColour(0xEEEEEE); + + title_sizer->Add(static_text, 0, wxALIGN_CENTER | wxALL, FromDIP(5)); + + main_sizer->Add(title_panel, 0, wxEXPAND); + main_sizer->AddSpacer(10); + + wxSizer* nozzle_area_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_table = new HotEndTable(this); + + nozzle_area_sizer->Add(m_badge, 0, wxLEFT | wxRIGHT, FromDIP(20)); + nozzle_area_sizer->Add(m_table, 0, wxRIGHT, FromDIP(10)); + + main_sizer->Add(nozzle_area_sizer); + main_sizer->AddSpacer(FromDIP(5)); + + SetSizer(main_sizer); + Layout(); + Fit(); + wxGetApp().UpdateDarkUIWin(this); +} + +void MultiNozzleStatusTable::MarkRelatedItems(const NozzleOption& option) +{ + m_table->MarkRelatedItems(option); + + m_badge->MarkRelatedItems(option); +} + +void MultiNozzleStatusTable::UnMarkRelatedItems(const NozzleOption& option) +{ + m_table->UnMarkRelatedItems(option); + + m_badge->UnMarkRelatedItems(option); +} + +void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr rack) +{ + if (m_table) + m_table->UpdateRackInfo(rack); + if (m_badge) { + auto nozzle_rack = rack.lock(); + if (!nozzle_rack) + return; + auto nozzles_in_extruder = nozzle_rack->GetNozzleSystem()->GetExtNozzles(); + bool has_right = false; + for (auto& elem : nozzles_in_extruder) { + auto& nozzle = elem.second; + int extruder_id = nozzle.AtLeftExtruder() ? 0 : 1; + if (nozzle.AtRightExtruder()) + has_right = true; + + NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow); + + m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type); + } + m_badge->SetExtruderValid(has_right); + } +} + + +Slic3r::GUI::MultiNozzleSyncDialog::MultiNozzleSyncDialog(wxWindow* parent,std::weak_ptr rack) : DPIDialog(parent, wxID_ANY, _L("Sync Nozzle status"),wxDefaultPosition, wxDefaultSize,wxDEFAULT_DIALOG_STYLE) +{ + m_nozzle_rack = rack; + wxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + SetBackgroundColour(*wxWHITE); + + m_tips = new Label(this, ""); + wxBoxSizer* label_sizer = new wxBoxSizer(wxHORIZONTAL); + label_sizer->Add(m_tips, 0, wxLEFT | wxRIGHT, FromDIP(25)); + main_sizer->Add(label_sizer, 0, wxTOP | wxBOTTOM, FromDIP(15)); + + m_list_table = new NozzleListTable(this); + + m_list_table->Bind(EVT_NOZZLE_SELECTED, [this](wxCommandEvent& evt) { + int idx = evt.GetInt(); + this->OnSelectRadio(idx); + }); + + main_sizer->Add(m_list_table, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(25)); + + m_nozzle_table = new MultiNozzleStatusTable(this); + wxBoxSizer* table_sizer = new wxBoxSizer(wxHORIZONTAL); + table_sizer->Add(m_nozzle_table, 0, wxLEFT | wxRIGHT, FromDIP(25)); + main_sizer->Add(table_sizer, 0, wxTOP | wxBOTTOM, FromDIP(15)); + + wxBoxSizer* button_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_cancel_btn = new Button(this, _L("Cancel"), "", 0, 0, wxID_OK); + m_confirm_btn = new Button(this, _L("Confirm"), "", 0, 0, wxID_CANCEL); + + m_caution = new Label(this, _L("Caution: Mixing nozzle diameters in one print is not supported. If the selected size is only on one extruder, single-extruder printing will be enforced.")); + m_caution->SetForegroundColour("#909090"); + main_sizer->Add(m_caution, 0, wxLEFT | wxRIGHT, FromDIP(25)); + + StateColor btn_bg_green( + std::pair(wxColour(144, 144, 144), StateColor::Disabled), + std::pair(wxColour(27, 136, 68), StateColor::Pressed), + std::pair(wxColour(61, 203, 115), StateColor::Hovered), + std::pair(wxColour(0, 174, 66), StateColor::Normal) + ); + + StateColor btn_text_green( + std::pair(wxColour(255, 255, 254), StateColor::Normal) + ); + + + m_confirm_btn->SetBackgroundColor(btn_bg_green); + m_confirm_btn->SetMinSize(wxSize(FromDIP(55), FromDIP(24))); + m_confirm_btn->SetCornerRadius(FromDIP(12)); + m_confirm_btn->SetBackgroundColor(btn_bg_green); + m_confirm_btn->SetTextColor(btn_text_green); + m_confirm_btn->SetFocus(); + + m_cancel_btn->SetMinSize(wxSize(FromDIP(55), FromDIP(24))); + m_cancel_btn->SetCornerRadius(FromDIP(12)); + + button_sizer->AddStretchSpacer(); + button_sizer->Add(m_cancel_btn, 0, wxALL, FromDIP(10)); + button_sizer->Add(m_confirm_btn, 0, wxALL, FromDIP(10)); + + main_sizer->Add(button_sizer, 0, wxEXPAND | wxALL, FromDIP(10)); + + SetSizer(main_sizer); + + //main_sizer->SetSizeHints(this); + main_sizer->Fit(this); + + int table_width = m_nozzle_table->GetBestSize().GetWidth(); + m_tips->Wrap(table_width); + m_tips->SetMaxSize(wxSize(table_width, -1)); + + m_caution->Wrap(table_width); + m_caution->SetMaxSize(wxSize(table_width, -1)); + + Layout(); + + m_refresh_timer = new wxTimer(this); + Bind(wxEVT_TIMER, &MultiNozzleSyncDialog::OnRefreshTimer, this); + CenterOnParent(); + wxGetApp().UpdateDlgDarkUI(this); +} + + +void MultiNozzleSyncDialog::OnRackStatusReadingFinished(wxEvent& evt) { + if (!IsShown()) return; + + m_refreshing = false; + if (m_refresh_timer) + m_refresh_timer->Stop(); +#if 1 + if (!UpdateUi(m_nozzle_rack)) + EndModal(wxID_OK); +#else + if(!UpdateUoi(m_nozzle_rack)){ + CallAfter([this]() { + EndModal(wxID_OK); + }); + } +#endif +} + +void MultiNozzleSyncDialog::OnRefreshTimer(wxTimerEvent& evt){ + if(!m_refreshing) + return; + auto nozzle_rack = m_nozzle_rack.lock(); + if(!nozzle_rack) + return; + + int reading_count = nozzle_rack->GetReadingCount(); + int reading_idx = nozzle_rack->GetReadingIdx(); + + wxString tip = wxString::Format(_L("Refresh %d/%d..."), reading_idx, reading_count); + m_confirm_btn->SetLabel(tip); + m_confirm_btn->Fit(); + if (m_confirm_btn->GetParent()) + m_confirm_btn->GetParent()->Layout(); + + m_confirm_btn->Disable(); + m_cancel_btn->Disable(); +} + +void MultiNozzleSyncDialog::UpdateRackInfo(std::weak_ptr rack) +{ + m_nozzle_rack = rack; + UpdateUi(rack); +} + +void MultiNozzleSyncDialog::OnSelectRadio(int select_idx) +{ + if (m_nozzle_option_idx != -1) + m_nozzle_table->UnMarkRelatedItems(m_nozzle_option_values[m_nozzle_option_idx]); + m_nozzle_option_idx = select_idx; + m_nozzle_table->MarkRelatedItems(m_nozzle_option_values[m_nozzle_option_idx]); +} + +bool MultiNozzleSyncDialog::hasMultiDiameters(const std::vector& group_infos) +{ + if (group_infos.empty()) + return false; + return !std::all_of(group_infos.begin(), group_infos.end(), [val = group_infos.front()](auto& elem) {return val.diameter == elem.diameter; }); +} + + +std::vector MultiNozzleSyncDialog::GetNozzleOptions(const std::vector& nozzle_groups) +{ + std::vector options; + + std::set diameters; + std::multimap groups_mapped_for_diameter; + for (auto& nozzle_group : nozzle_groups) { + groups_mapped_for_diameter.insert({ nozzle_group.diameter,nozzle_group }); + } + +#if ENABLE_MIX_FLOW_PRINT + for (auto it = groups_mapped_for_diameter.begin(); it != groups_mapped_for_diameter.end(); ) { + NozzleOption option; + const auto& diameter = it->first; + auto range = groups_mapped_for_diameter.equal_range(diameter); + + option.diameter = diameter; + for (auto val_it = range.first; val_it != range.second; ++val_it) { + const auto& elem = val_it->second; + option.extruder_nozzle_stats[elem.extruder_id][elem.volume_type] += elem.nozzle_count; + } + options.emplace_back(std::move(option)); + it = range.second; + } +#else + for (auto it = groups_mapped_for_diameter.begin(); it != groups_mapped_for_diameter.end(); ) { + const auto& diameter = it->first; + auto range = groups_mapped_for_diameter.equal_range(diameter); + + std::vector> left_nozzles; + std::vector> right_nozzles; + + for (auto val_it = range.first; val_it != range.second; ++val_it) { + const auto& elem = val_it->second; + if (elem.extruder_id == 0) + left_nozzles.emplace_back(elem.volume_type, elem.nozzle_count); + else + right_nozzles.emplace_back(elem.volume_type, elem.nozzle_count); + } + + for (const auto& left_nozzle : left_nozzles.empty() ? std::vector>{{}} : left_nozzles) { + for (const auto& right_nozzle : right_nozzles.empty() ? std::vector>{{}} : right_nozzles) { + NozzleOption option; + option.diameter = diameter; + + if (!left_nozzles.empty()) { + option.extruder_nozzle_stats[0] = { left_nozzle.first, left_nozzle.second }; + } + + if (!right_nozzles.empty()) { + option.extruder_nozzle_stats[1] = { right_nozzle.first, right_nozzle.second }; + } + + options.emplace_back(std::move(option)); + } + } + + it = range.second; + } +#endif + return options; +} + +bool MultiNozzleSyncDialog::UpdateOptionList(std::weak_ptr rack, bool ignore_unknown, bool ignore_unreliable) +{ + const auto& nozzle_rack = rack.lock(); + if (!nozzle_rack) + return true; + bool has_unknown = nozzle_rack->HasUnknownNozzles() && !ignore_unknown; + bool has_unreliable = nozzle_rack->HasUnreliableNozzles() && !ignore_unreliable; + + if (has_unknown || has_unreliable) { + m_list_table->Hide(); + return true; + } + + m_list_table->Show(); + + m_nozzle_option_values.clear(); + + auto options = GetNozzleOptions(nozzle_rack->GetNozzleSystem()->GetNozzleGroups()); + + int recommend_idx = std::max_element(options.begin(), options.end(), [](const NozzleOption& opt1, const NozzleOption& opt2) { + int count1 = 0, count2 = 0; + for (auto elem : opt1.extruder_nozzle_stats) { + for (auto& stats : elem.second) + count1 += stats.second; + } + for (auto elem : opt2.extruder_nozzle_stats) { + for (auto& stats : elem.second) + count2 += stats.second; + } + return count1 < count2; + }) - options.begin(); + + m_nozzle_option_values = options; + OnSelectRadio(recommend_idx); + m_list_table->SetOptions(options,recommend_idx); + + if (!has_unknown && !has_unreliable && options.size() == 1) { + return false; + } + return true; +} + +void MultiNozzleSyncDialog::UpdateTip(std::weak_ptr rack, bool ignore_unknown, bool ignore_unreliable) +{ + const auto& nozzle_rack = rack.lock(); + if (!nozzle_rack) + return; + + bool has_unknown = nozzle_rack->HasUnknownNozzles() && !ignore_unknown; + bool has_unreliable = nozzle_rack->HasUnreliableNozzles() && !ignore_unreliable; + + if (has_unknown && has_unreliable) { + m_tips->SetLabel(_L("Unknown nozzle detected. Refresh to update info (unrefreshed nozzles will be excluded during slicing). Verify nozzle diameter & flow rate against displayed values.")); + m_caution->Hide(); + } + else if (has_unknown) { + m_tips->SetLabel(_L("Unknown nozzle detected. Refresh to update (unrefreshed nozzles will be skipped in slicing).")); + m_caution->Hide(); + } + else if (has_unreliable) { + m_tips->SetLabel(_L("Please confirm whether the required nozzle diameter and flow rate match the currently displayed values.")); + m_caution->Hide(); + } + else { + m_tips->SetLabel(_L("Your printer has different nozzles installed. Please select a nozzle for this print.")); + m_caution->Show(); + } +} + +int MultiNozzleSyncDialog::ShowModal() +{ + bool res = UpdateUi(m_nozzle_rack); + if (!res) + return wxID_OK; + + return DPIDialog::ShowModal(); + +} + +MultiNozzleSyncDialog::~MultiNozzleSyncDialog() +{ + if (auto rack = m_nozzle_rack.lock()) { + rack->Unbind(DEV_RACK_EVENT_READING_FINISHED, &MultiNozzleSyncDialog::OnRackStatusReadingFinished, this); + } + if (m_refresh_timer) + m_refresh_timer->Stop(); +} + +void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr rack, bool ignore_unknown, bool ignore_unreliable) +{ + const auto& nozzle_rack = rack.lock(); + if (!nozzle_rack) + return; + + if (m_refreshing) + return; + + bool has_unknown = nozzle_rack->HasUnknownNozzles() && !ignore_unknown; + bool has_unreliable = nozzle_rack->HasUnreliableNozzles() && !ignore_unreliable; + + auto refresh_cmd = [rack, this]() { + auto nozzle_rack = m_nozzle_rack.lock(); + if (!nozzle_rack) + return; + nozzle_rack->CtrlRackReadAll(); + m_refreshing = true; + if (m_refresh_timer) + m_refresh_timer->Start(500); + nozzle_rack->Bind(DEV_RACK_EVENT_READING_FINISHED, &MultiNozzleSyncDialog::OnRackStatusReadingFinished, this); + }; + + auto trust_cmd = [rack, this]() { + auto nozzle_rack = rack.lock(); + if (!nozzle_rack) + return; + nozzle_rack->CtrlRackConfirmAll(); + UpdateUi(rack, true, false); + }; + + auto ignore_opt = [rack,this]() { + if (!UpdateUi(rack, true, true)) + EndModal(wxID_OK); + }; + + m_cancel_btn->Enable(); + m_confirm_btn->Enable(); + + if (has_unknown && has_unreliable) { + m_cancel_btn->Show(); + m_confirm_btn->Show(); + + m_cancel_btn->SetLabel(_L("Ignore")); + m_confirm_btn->SetLabel(_L("Refresh")); + + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, ignore_opt](auto& e) {ignore_opt(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); + } + else if (has_unknown) { + m_cancel_btn->Show(); + m_confirm_btn->Show(); + + m_cancel_btn->SetLabel(_L("Ignore")); + m_confirm_btn->SetLabel(_L("Refresh")); + + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, ignore_opt](auto& e) {ignore_opt(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); + } + else if (has_unreliable) { + m_cancel_btn->Show(); + m_confirm_btn->Show(); + + m_cancel_btn->SetLabel(_L("Refresh")); + m_confirm_btn->SetLabel(_L("Confirm")); + + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, trust_cmd](auto& e) {trust_cmd(); }); + + } + else { + m_cancel_btn->Hide(); + m_confirm_btn->Show(); + m_confirm_btn->SetLabel(_L("OK")); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this](auto& e) {EndModal(wxID_OK); }); + } + +} + +bool MultiNozzleSyncDialog::UpdateUi(std::weak_ptr rack, bool ignore_unknown, bool ignore_unreliable) +{ + m_nozzle_table->UpdateRackInfo(rack); + bool res = UpdateOptionList(rack, ignore_unknown, ignore_unreliable); + if (!res) + return false; + + UpdateTip(rack, ignore_unknown, ignore_unreliable); + UpdateButton(rack, ignore_unknown, ignore_unreliable); + + Layout(); + + int table_width = m_nozzle_table->GetBestSize().GetWidth(); + m_tips->Wrap(table_width); + m_tips->SetMaxSize(wxSize(table_width, -1)); + m_caution->Wrap(table_width); + m_caution->SetMaxSize(wxSize(table_width, -1)); + Fit(); + return true; +} + + +std::optional tryPopUpMultiNozzleDialog(MachineObject* obj) +{ + if (!obj) + return std::nullopt; + auto rack = obj->GetNozzleSystem()->GetNozzleRack(); + if (!rack || !rack->IsSupported()) + return std::nullopt; + MultiNozzleSyncDialog dialog(wxGetApp().plater_,rack); + + bool has_unreliable = rack->HasUnreliableNozzles(); + bool has_unknown = rack->HasUnknownNozzles(); + + auto config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + + if (dialog.ShowModal() == wxID_OK) { + auto selected_option = dialog.GetSelectedOption(); + if (!selected_option) + return std::nullopt; + auto& preset_bundle = GUI::wxGetApp().preset_bundle; + auto& project_config = preset_bundle->project_config; + + ConfigOptionEnumsGeneric* nozzle_volume_type_opt = project_config.option("nozzle_volume_type"); + + // write to preset bundle config + for (int extruder_id = 0; extruder_id < 2; ++extruder_id) { + NozzleVolumeType volume_type; + int nozzle_count; + bool clear_all = true; + if (!selected_option->extruder_nozzle_stats.count(extruder_id)) { + nozzle_count = 0; + // Reset the concrete volume types (Standard/High Flow); Hybrid stays a hidden match-any sentinel. + // TPU High Flow is a concrete variant that exists only on 0.4/0.6 nozzles, so reset it too, but + // only there (same guard that skips High Flow on 0.2). + std::vector reset_types{nvtStandard, nvtHighFlow}; + if (extruder_supports_tpu_high_flow(preset_bundle, extruder_id)) + reset_types.push_back(nvtTPUHighFlow); + for (NozzleVolumeType vt : reset_types) { + volume_type = vt; + setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, nozzle_count, clear_all); + clear_all = false; + } + } + else { + for (auto& stat : selected_option->extruder_nozzle_stats[extruder_id]) { + volume_type = stat.first; + nozzle_count = stat.second; + setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, nozzle_count, clear_all); + clear_all = false; + } + } + } + // Orca: there is no ExtruderNozzleStat runtime object, so there is no machine/user data-source flag to mark + // here; nozzle stats persist directly into the printer preset's `extruder_nozzle_stats` config key via + // setExtruderNozzleCount. + return selected_option; + } + + return std::nullopt; +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp new file mode 100644 index 0000000000..7da90f1413 --- /dev/null +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp @@ -0,0 +1,239 @@ +#ifndef slic3r_GUI_MultiNozzleSync_hpp_ +#define slic3r_GUI_MultiNozzleSync_hpp_ + +// Multi-nozzle GUI. +// +// Two surfaces live here: +// - The DEVICE-INDEPENDENT "manual nozzle count" surface (ManualNozzleCountDialog + helpers): lets the +// user declare, per extruder, how many physical nozzles of each volume type a multi-nozzle (H2C) +// extruder carries. Persisted into the printer preset's `extruder_nozzle_stats` config key, which the +// multi-nozzle slicer already consumes (ToolOrdering::build_multi_nozzle_group_result). +// - The DEVICE-SYNC surface (MultiNozzleSyncDialog / HotEndTable / NozzleListTable / +// tryPopUpMultiNozzleDialog): reads the live nozzle rack from the connected machine via +// DevNozzleRack / wgtDeviceNozzleRackNozzleItem and lets the user pick a nozzle option to slice with. +// Popped from the sidebar "sync machine" button when the selected printer is an H2C. +// +// Everything here is inert for existing printers: the manual entry points are gated on the printer having +// any extruder with `extruder_max_nozzle_count > 1` (no shipping single-nozzle/dual-extruder profile sets +// it), and the device-sync path only runs for a connected machine whose rack reports as supported. + +#include "../GUI_Utils.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/MultiNozzleUtils.hpp" +#include "slic3r/GUI/DeviceCore/DevNozzleRack.h" +#include "slic3r/GUI/DeviceTab/wgtDeviceNozzleRackNozzleItem.h" + +#include +#include + +#include +#include +#include +#include + +class wxChoice; +class wxStaticText; +class wxStaticBitmap; +class Button; // global widget (src/slic3r/GUI/Widgets/Button.hpp), not in the Slic3r::GUI namespace +class Label; // global widget (src/slic3r/GUI/Widgets/Label.hpp) +class StaticBox; + +namespace Slic3r { +class PresetBundle; +class MachineObject; + +namespace GUI { + +#define ENABLE_MIX_FLOW_PRINT 1 + +#if ENABLE_MIX_FLOW_PRINT +struct NozzleOption +{ + std::string diameter; + std::unordered_map> extruder_nozzle_stats; +}; +#else +struct NozzleOption +{ + std::string diameter; + std::unordered_map> extruder_nozzle_stats; +}; +#endif + +// Dialog to manually set the per-volume-type physical nozzle count of one multi-nozzle extruder. +class ManualNozzleCountDialog : public DPIDialog +{ +public: + ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int max_nozzle_count, bool force_no_zero); + ~ManualNozzleCountDialog() override = default; + void on_dpi_changed(const wxRect &suggested_rect) override {} + int GetNozzleCount(NozzleVolumeType volume_type) const; + +private: + wxChoice *m_standard_choice{nullptr}; + wxChoice *m_highflow_choice{nullptr}; + Button *m_confirm_btn{nullptr}; + wxStaticText *m_error_label{nullptr}; +}; + +class ExtruderBadge : public wxPanel +{ +public: + ExtruderBadge(wxWindow* parent); + void SetExtruderInfo(int extruder_id, const std::string& label, const NozzleVolumeType& flow); + void UnMarkRelatedItems(const NozzleOption& option); + void MarkRelatedItems(const NozzleOption& option); + void SetExtruderValid(bool right_on); +private: + void SetExtruderStatus(bool left_selected, bool right_selected); + + bool m_right_on{ true }; + wxStaticBitmap* badget; + Label* left; + Label* right; + Label* left_diameter_desp; + Label* right_diameter_desp; + Label* left_flow_desp; + Label* right_flow_desp; + + std::vector m_diameter_list; + std::vector m_volume_type_list; +}; + +class HotEndTable : public wxPanel +{ +public: + HotEndTable(wxWindow* parent); + void UpdateRackInfo(std::weak_ptr rack); + void MarkRelatedItems(const NozzleOption& option); + void UnMarkRelatedItems(const NozzleOption& option); +private: + StaticBox* CreateNozzleBox(const std::vector& nozzle_indices); + void UpdateNozzleItems(const std::unordered_map& nozzle_items, + std::shared_ptr nozzle_rack); + +private: + struct HotEndAttr { + std::string diameter; + int extruder_id; + NozzleVolumeType volume_type; + }; + + std::vector FilterHotEnds(const NozzleOption& option); + +private: + StaticBox* m_arow_nozzle_box{ nullptr }; + StaticBox* m_brow_nozzle_box{ nullptr }; + std::unordered_map m_nozzle_items; + std::weak_ptr m_nozzle_rack; + void OnPaint(wxPaintEvent& event); +}; + + +wxDECLARE_EVENT(EVT_NOZZLE_SELECTED, wxCommandEvent); + +class NozzleListTable : public wxPanel +{ +public: + NozzleListTable(wxWindow* parent); + int GetSelectIdx(); + void SetOptions(const std::vector& options,int default_select); +private: + wxString BuildTableObjStr(); + wxString BuildTextObjStr(); + std::vector m_nozzle_options; + + void SendSelectionChangedEvent(); + + wxWebView* m_web_view; + + int m_selected_idx; +}; + +class MultiNozzleStatusTable : public wxPanel +{ +public: + MultiNozzleStatusTable(wxWindow* parent); + void UpdateRackInfo(std::weak_ptr rack); + void MarkRelatedItems(const NozzleOption& option); + void UnMarkRelatedItems(const NozzleOption& option); +private: + ExtruderBadge* m_badge; + HotEndTable* m_table; +}; + + +class MultiNozzleSyncDialog : public DPIDialog +{ +public: + MultiNozzleSyncDialog(wxWindow* parent, std::weak_ptr rack); + virtual void on_dpi_changed(const wxRect& suggested_rect) {}; + std::vector GetNozzleOptions(const std::vector& group_infos); + + std::optional GetSelectedOption() { + if (m_nozzle_option_idx < 0 || m_nozzle_option_idx >= m_nozzle_option_values.size()) + return std::nullopt; + return m_nozzle_option_values[m_nozzle_option_idx]; + } + + int ShowModal() override; + ~MultiNozzleSyncDialog() override; +private: + void UpdateRackInfo(std::weak_ptr rack); + + bool hasMultiDiameters(const std::vector& group_infos); + void OnSelectRadio(int select_idx); + + bool UpdateUi(std::weak_ptr rack, bool ignore_unknown=false, bool ignore_unreliable=false); + + bool UpdateOptionList(std::weak_ptr rack, bool ignore_unknown, bool ignore_unreliable); + void UpdateTip(std::weak_ptr rack, bool ignore_unknown, bool ignore_unreliable); + void UpdateButton(std::weak_ptr rack, bool ignore_unknown, bool ignore_unreliable); + void OnRackStatusReadingFinished(wxEvent& evt); + void OnRefreshTimer(wxTimerEvent& event); + +private: + MultiNozzleStatusTable* m_nozzle_table; + NozzleListTable* m_list_table; + std::vector m_nozzle_option_values; + int m_nozzle_option_idx{ -1 }; + bool m_refreshing{ false }; + + std::weak_ptr m_nozzle_rack; + Label* m_tips; + Label* m_caution; + + wxTimer* m_refresh_timer {nullptr}; + size_t m_rack_event_token; + Button* m_cancel_btn; + Button* m_confirm_btn; +}; + + +// Entry point for the sidebar "sync machine" button: pops MultiNozzleSyncDialog for a connected H2C and +// returns the chosen nozzle option (nullopt if the machine has no supported rack or the user cancels). +std::optional tryPopUpMultiNozzleDialog(MachineObject* obj); + +// Persist one extruder's per-volume-type nozzle count into the edited printer preset's `extruder_nozzle_stats` +// config key (the value the multi-nozzle slicer reads). When clear_all is true, the extruder's other volume-type +// counts are reset first. +void setExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType type, int nozzle_count, bool clear_all); + +// Refresh the sidebar nozzle-count display for one extruder. Deferred wiring point: Orca's sidebar ExtruderGroup has +// no nozzle-count widget yet, so the multi-nozzle sidebar UI is not wired up. +void updateNozzleCountDisplay(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type); + +// True when a nozzle of this diameter can carry the "Direct Drive TPU High Flow" variant. That variant +// exists on the 0.4 and 0.6 nozzle H2D/H2D Pro leaves (not 0.2/0.8), so this is the single source of truth +// for the diameter set — the counterpart of the High-Flow-skip-for-0.2 guard. +bool nozzle_diameter_supports_tpu_high_flow(double nozzle_diameter); +// Same predicate resolved from the edited printer preset's nozzle_diameter for one extruder. False when unknown. +bool extruder_supports_tpu_high_flow(PresetBundle *preset_bundle, int extruder_id); + +// Entry point: pop the ManualNozzleCountDialog for one extruder of a multi-nozzle printer and persist the result. +// No-op unless the selected printer has an extruder with extruder_max_nozzle_count > 1. +void manuallySetNozzleCount(int extruder_id); + +}} // namespace Slic3r::GUI + +#endif // slic3r_GUI_MultiNozzleSync_hpp_ diff --git a/src/slic3r/Utils/CalibUtils.cpp b/src/slic3r/Utils/CalibUtils.cpp index 8e7de91318..e7d16ffef9 100644 --- a/src/slic3r/Utils/CalibUtils.cpp +++ b/src/slic3r/Utils/CalibUtils.cpp @@ -94,6 +94,10 @@ wxString get_nozzle_volume_type_name(NozzleVolumeType type) return _L("Standard"); } else if (NozzleVolumeType::nvtHighFlow == type) { return _L("High Flow"); + } else if (NozzleVolumeType::nvtHybrid == type) { + return _L("Hybrid"); + } else if (NozzleVolumeType::nvtTPUHighFlow == type) { + return _L("TPU High Flow"); } return wxString(); }