From 7c8d086d9e59d524c1f1cd5d41758004cbcfb068 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 10 Jul 2026 19:47:21 +0800 Subject: [PATCH] feat(gui): show per-extruder nozzle count with inline editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar extruder cards get an interactive title row — " ( )" with an edit button — showing the extruder's physical nozzle count on multi-nozzle printers (hidden elsewhere). Clicking it opens the existing "Set nozzle count" dialog, which now also handles a Hybrid extruder by offering both Standard and High Flow counts (an empty mix is rejected) and shows a hotend thumbnail. Because `extruder_nozzle_stats` is session-only (saved presets never carry it, so preset switches rebuild the edited config without it), the stats are re-baselined whenever they are missing: each extruder starts with extruder_max_nozzle_count nozzles of its selected volume type. Switching an extruder's flow type carries its total count over to the new type, except when the stats came from a device sync — the machine-reported per-type breakdown must survive a manual flow switch. The badge refreshes on preset load, flow-type change, manual edit, device sync, and project load. Single-extruder cards keep the title row but never enable editing. --- src/slic3r/GUI/Plater.cpp | 175 +++++++++++++++++++-- src/slic3r/GUI/Plater.hpp | 3 + src/slic3r/GUI/Tab.cpp | 19 +++ src/slic3r/GUI/Widgets/MultiNozzleSync.cpp | 109 +++++++++++-- src/slic3r/GUI/Widgets/MultiNozzleSync.hpp | 18 ++- 5 files changed, 292 insertions(+), 32 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6535d457e7..5558e77cbe 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -432,10 +432,111 @@ enum class ActionButtonType : int { abSendGCode }; +// Interactive title row for the sidebar extruder cards: " ( <count> ) [edit]". The count shows the +// extruder's physical nozzle count on multi-nozzle printers (hidden elsewhere, SetCount(-1)), and the +// trailing button opens the manual nozzle-count editor when enabled (a plain dot otherwise). +class HoverLabel : public wxPanel +{ +public: + HoverLabel(wxWindow *parent, const wxString &label) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + { +#ifdef __WXOSX__ + SetBackgroundColour("#F7F7F7"); +#else + SetBackgroundColour(*wxWHITE); +#endif + auto sizer = new wxBoxSizer(wxHORIZONTAL); + + m_label = new wxStaticText(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + m_label->SetFont(Label::Body_13); + m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); + + m_brace_left = new wxStaticText(this, wxID_ANY, "(", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + m_brace_left->SetFont(Label::Body_13); + m_brace_left->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + m_brace_left->Hide(); + + m_count = new wxStaticText(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + m_count->SetFont(Label::Body_13.Bold()); + m_count->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + m_count->Hide(); + + m_brace_right = new wxStaticText(this, wxID_ANY, ")", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + m_brace_right->SetFont(Label::Body_13); + m_brace_right->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + m_brace_right->Hide(); + + m_hover_btn = new ScalableButton(this, wxID_ANY, "dot"); + m_hover_btn->SetMinSize(wxSize(FromDIP(25), -1)); +#ifdef __WXOSX__ + m_hover_btn->SetBackgroundColour("#F7F7F7"); +#else + m_hover_btn->SetBackgroundColour(*wxWHITE); +#endif + m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { + if (m_enabled && m_hover_on_click) + m_hover_on_click(); + }); + + sizer->Add(m_label, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(m_brace_left, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(m_count, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(m_brace_right, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(m_hover_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5)); + + SetSizerAndFit(sizer); + Layout(); + } + + void EnableEdit(bool enable) + { + m_enabled = enable; + m_hover_btn->SetBitmap_(enable ? "edit" : "dot"); + } + + void SetOnHoverClick(std::function<void()> on_click) { m_hover_on_click = std::move(on_click); } + + void SetCount(int count) + { + if (count < 0) { + m_count->Hide(); + m_brace_left->Hide(); + m_brace_right->Hide(); + } else { + m_count->SetLabel(wxString::Format("%d", count)); + m_count->Show(); + m_brace_left->Show(); + m_brace_right->Show(); + } + Layout(); + Fit(); + } + + void SetTitle(const wxString &title) + { + m_label->SetLabel(title); + Layout(); + Fit(); + } + + void Rescale() { m_hover_btn->msw_rescale(); } + +private: + wxStaticText *m_label; + wxStaticText *m_brace_left; + wxStaticText *m_count; + wxStaticText *m_brace_right; + ScalableButton *m_hover_btn; + + std::function<void()> m_hover_on_click; + bool m_enabled{false}; +}; + struct ExtruderGroup : StaticGroup { ExtruderGroup(wxWindow * parent, int index, wxString const &title); wxStaticBoxSizer *sizer = nullptr; + HoverLabel * hover_label = nullptr; ScalableButton * btn_edit = nullptr; ComboBox * combo_diameter = nullptr; ComboBox * combo_flow = nullptr; @@ -465,11 +566,16 @@ struct ExtruderGroup : StaticGroup void update_ams(); void SetTitle(const wxString& title); + void SetCount(int count) { if (hover_label) hover_label->SetCount(count); } + void SetEditEnabled(bool enable) { if (hover_label) hover_label->EnableEdit(enable); } + void SetOnHoverClick(std::function<void()> on_click) { if (hover_label) hover_label->SetOnHoverClick(std::move(on_click)); } void sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1); void Rescale() { + if (hover_label) + hover_label->Rescale(); if (btn_edit) btn_edit->msw_rescale(); btn_up->msw_rescale(); @@ -1080,13 +1186,18 @@ public: }; ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title) - : StaticGroup(parent, wxID_ANY, title) + : StaticGroup(parent, wxID_ANY, wxString()) { SetFont(Label::Body_10); SetForegroundColour(wxColour("#CECECE")); SetBorderColor(wxColour("#EEEEEE")); SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); // ORCA match radius with other boxes ShowBadge(true); + + // The title lives in an interactive row inside the card (with the nozzle-count badge and its edit + // button) instead of being painted on the border by StaticGroup. + hover_label = new HoverLabel(this, title); + // Nozzle wxStaticText *label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter")); label_diameter->SetFont(Label::Body_14); @@ -1181,15 +1292,19 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title if (index < 0) { label_ams->Hide(); ams_not_installed_msg->Hide(); - wxStaticBoxSizer *hsizer = new wxStaticBoxSizer(this, wxHORIZONTAL); + wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL); + wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP| wxBOTTOM, FromDIP(8)); hsizer->Add(hsizer_nozzle, 1, wxEXPAND | wxALL, FromDIP(8)); hsizer->AddSpacer(FromDIP(2)); // Avoid badge - this->sizer = hsizer; + vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2)); + vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(2)); + this->sizer = vsizer; } else { wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL); - vsizer->Add(hsizer_ams, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(2)); - vsizer->Add(hsizer_diameter, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT | wxBOTTOM, FromDIP(2)); + vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2)); + vsizer->Add(hsizer_ams, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2)); + vsizer->Add(hsizer_diameter, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2)); vsizer->Add(hsizer_nozzle, 0, wxEXPAND | wxALL, FromDIP(2)); this->sizer = vsizer; } @@ -1289,12 +1404,8 @@ void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector<DevAms *> con void ExtruderGroup::SetTitle(const wxString& title) { - m_label = title; - int tW, tH, descent, externalLeading; - GetTextExtent(m_label.IsEmpty() ? "Orca" : m_label, &tW, &tH, &descent, &externalLeading, &m_font); - m_label_height = tH - externalLeading; - m_label_width = tW; - Refresh(); + if (hover_label) + hover_label->SetTitle(title); } bool Sidebar::priv::switch_diameter(bool single) @@ -1613,8 +1724,13 @@ std::optional<NozzleOption> Sidebar::priv::get_nozzle_options(MachineObject* obj } } } - // 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. + // The stats now hold the device's per-type breakdown: protect it from being collapsed by + // a manual flow switch, and refresh the sidebar badges. + setNozzleStatsFromMachine(true); + if (nozzle_volume_type_opt) { + for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volume_type_opt->values.size(); ++extruder_id) + updateNozzleCountDisplay(preset_bundle, extruder_id, NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id])); + } } } @@ -2607,6 +2723,12 @@ Sidebar::Sidebar(Plater *parent) p->left_extruder = new ExtruderGroup(p->m_panel_printer_content, 0, _L("Left Nozzle")); p->right_extruder = new ExtruderGroup(p->m_panel_printer_content, 1, _L("Right Nozzle")); p->single_extruder = new ExtruderGroup(p->m_panel_printer_content, -1, _L("Nozzle")); + // manuallySetNozzleCount refreshes the badge and the plater itself; it is a no-op unless the + // printer has a multi-nozzle extruder (and the edit button is only enabled then, see + // enable_nozzle_count_edit). + p->left_extruder->SetOnHoverClick([]() { GUI::manuallySetNozzleCount(0); }); + p->right_extruder->SetOnHoverClick([]() { GUI::manuallySetNozzleCount(1); }); + p->single_extruder->SetEditEnabled(false); // Orca: keep the floating switcher icon aligned with the left extruder's AMS row when the // extruder card is resized (the overlay is absolutely positioned, not managed by a sizer). p->left_extruder->Bind(wxEVT_SIZE, [this](wxSizeEvent &evt) { @@ -4449,6 +4571,26 @@ void Sidebar::enable_purge_mode_btn(bool enable) }); } +void Sidebar::set_extruder_nozzle_count(int extruder_id, int nozzle_count) +{ + if (!p) + return; + if (extruder_id == 0) { + p->left_extruder->SetCount(nozzle_count); + p->single_extruder->SetCount(nozzle_count); + } else if (extruder_id == 1) { + p->right_extruder->SetCount(nozzle_count); + } +} + +void Sidebar::enable_nozzle_count_edit(bool enable) +{ + if (!p) + return; + p->left_extruder->SetEditEnabled(enable); + p->right_extruder->SetEditEnabled(enable); +} + void Sidebar::update_dynamic_filament_list() { dynamic_filament_list.update(); @@ -7221,6 +7363,13 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ } // Update filament combobox after loading config wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT); + // The loaded project supplies nozzle_volume_type; refresh the sidebar + // nozzle-count badges against it. + if (auto *nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")) { + const int extruder_count = wxGetApp().preset_bundle->get_printer_extruder_count(); + for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volumes->values.size(); ++extruder_id) + updateNozzleCountDisplay(wxGetApp().preset_bundle, extruder_id, NozzleVolumeType(nozzle_volumes->values[extruder_id])); + } } } if (!silence) wxGetApp().app_config->update_config_dir(path.parent_path().string()); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index e96dd7804f..0e4f992935 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -209,6 +209,9 @@ public: static bool should_show_SEMM_buttons(); void show_SEMM_buttons(); void enable_purge_mode_btn(bool enable); + // Sidebar nozzle-count badge on the extruder cards (multi-nozzle printers only). + void set_extruder_nozzle_count(int extruder_id, int nozzle_count); + void enable_nozzle_count_edit(bool enable); void update_dynamic_filament_list(); PlaterPresetComboBox * printer_combox(); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index a6ff7fba97..db5725bb13 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -46,6 +46,7 @@ #include "Widgets/ComboBox.hpp" #include "Widgets/Label.hpp" +#include "Widgets/MultiNozzleSync.hpp" #include "Widgets/SwitchButton.hpp" #include "Widgets/TabCtrl.hpp" #include "Widgets/ComboBox.hpp" @@ -5677,6 +5678,19 @@ void TabPrinter::on_preset_loaded() } if (wxGetApp().plater()) wxGetApp().plater()->sidebar().enable_purge_mode_btn(show_purge_mode); + + // Sidebar nozzle-count badge on the extruder cards. The `extruder_nozzle_stats` key is session-only — + // saved presets never carry it, so any preset switch rebuilds the edited config without it — so + // re-baseline it whenever it is missing (each extruder gets extruder_max_nozzle_count nozzles of its + // selected volume type), then refresh the badges. Idempotent, so run on every preset load. + if (wxGetApp().plater()) + wxGetApp().plater()->sidebar().enable_nozzle_count_edit(has_multiple_nozzle); + const auto *nozzle_stats = m_preset_bundle->printers.get_edited_preset().config.option<ConfigOptionStrings>("extruder_nozzle_stats"); + if (nozzle_stats == nullptr || nozzle_stats->values.empty()) + seedExtruderNozzleStats(m_preset_bundle); + if (auto *nozzle_volume_type = m_preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")) + for (size_t idx = 0; idx < nozzle_volume_type->values.size(); ++idx) + updateNozzleCountDisplay(m_preset_bundle, idx, NozzleVolumeType(nozzle_volume_type->values[idx])); } void TabPrinter::update_pages() @@ -7551,6 +7565,11 @@ void TabPrinter::set_extruder_volume_type(int extruder_id, NozzleVolumeType type auto nozzle_volumes = m_preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type"); assert(nozzle_volumes->values.size() > (size_t)extruder_id); nozzle_volumes->values[extruder_id] = type; + + // Carry the extruder's nozzle count over to the new flow type and refresh the sidebar badge. + onNozzleVolumeTypeSwitch(m_preset_bundle, extruder_id, type); + updateNozzleCountDisplay(m_preset_bundle, extruder_id, type); + on_value_change((boost::format("nozzle_volume_type#%1%") % extruder_id).str(), int(type)); //save to app config diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index cf7e5743b1..f27beec657 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -75,6 +75,8 @@ ManualNozzleCountDialog::ManualNozzleCountDialog( wxPanel *content = new wxPanel(this); content->SetBackgroundColour(*wxWHITE); + wxBitmap nozzle_bmp = create_scaled_bitmap("hotend_thumbnail", nullptr, FromDIP(60)); + auto *nozzle_icon = new wxStaticBitmap(content, wxID_ANY, nozzle_bmp); wxBoxSizer *content_sizer = new wxBoxSizer(wxHORIZONTAL); content->SetSizer(content_sizer); @@ -85,14 +87,15 @@ ManualNozzleCountDialog::ManualNozzleCountDialog( 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) { + // A Hybrid extruder mixes Standard and High Flow nozzles, so it gets both count choices; the concrete + // types get exactly one. + if (volume_type == nvtStandard || volume_type == nvtHybrid) { 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) { + if (volume_type == nvtHighFlow || volume_type == nvtHybrid) { 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); @@ -133,6 +136,7 @@ ManualNozzleCountDialog::ManualNozzleCountDialog( e.Skip(); }); + content_sizer->Add(nozzle_icon, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(15)); content_sizer->Add(choice_sizer, 0, wxALIGN_CENTRE_VERTICAL); m_confirm_btn = new Button(this, _L("Confirm")); @@ -155,15 +159,29 @@ int ManualNozzleCountDialog::GetNozzleCount(NozzleVolumeType volume_type) const return m_standard_choice ? m_standard_choice->GetSelection() : 0; if (volume_type == nvtHighFlow) return m_highflow_choice ? m_highflow_choice->GetSelection() : 0; + if (volume_type == nvtHybrid) + return (m_standard_choice ? m_standard_choice->GetSelection() : 0) + + (m_highflow_choice ? m_highflow_choice->GetSelection() : 0); return 0; } // ---- free functions ------------------------------------------------------------------------------------------- +// Session-scoped provenance of the current `extruder_nozzle_stats` values: device sync sets it so a manual +// flow switch keeps the machine-reported per-type breakdown; seeding clears it. The stats themselves do not +// survive a preset switch (the key is absent from saved presets, so the edited config is rebuilt without it), +// so this flag only protects the currently loaded values. +static bool s_nozzle_stats_from_machine = false; + +void setNozzleStatsFromMachine(bool from_machine) { s_nozzle_stats_from_machine = from_machine; } + void setExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type, int nozzle_count, bool clear_all) { if (!preset_bundle) return; + // Hybrid is a mix marker, not a physical nozzle type — never a stats key. + if (volume_type == nvtHybrid) + return; auto &config = preset_bundle->printers.get_edited_preset().config; auto *opt = config.option<ConfigOptionStrings>("extruder_nozzle_stats", true); if (!opt) @@ -200,12 +218,59 @@ bool extruder_supports_tpu_high_flow(PresetBundle *preset_bundle, int extruder_i 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; + auto *plater = wxGetApp().plater(); + if (!preset_bundle || !plater) + return; + + auto *max_nozzle_count = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count"); + // Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate. + const bool support_multi_nozzle = 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(); }); + + int display_count = -1; // hides the badge + if (support_multi_nozzle) + display_count = volume_type == nvtHybrid ? extruder_nozzle_count_total(preset_bundle, extruder_id) + : extruder_nozzle_count(preset_bundle, extruder_id, volume_type); + plater->sidebar().set_extruder_nozzle_count(extruder_id, display_count); +} + +void seedExtruderNozzleStats(PresetBundle *preset_bundle) +{ + if (!preset_bundle) + return; + auto *max_nozzle_count = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count"); + auto *nozzle_volume_type = preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type"); + + const int extruder_count = preset_bundle->get_printer_extruder_count(); + for (int eid = 0; eid < extruder_count; ++eid) { + NozzleVolumeType type = nvtStandard; + if (nozzle_volume_type && eid < (int) nozzle_volume_type->values.size()) + type = NozzleVolumeType(nozzle_volume_type->values[eid]); + // A fresh seed has no per-type breakdown to draw on, so a Hybrid extruder starts as all-Standard. + if (type == nvtHybrid) + type = nvtStandard; + int count = 1; + if (max_nozzle_count && eid < (int) max_nozzle_count->values.size() && + max_nozzle_count->values[eid] != ConfigOptionIntsNullable::nil_value()) + count = max_nozzle_count->values[eid]; + setExtruderNozzleCount(preset_bundle, eid, type, count, true); + } + s_nozzle_stats_from_machine = false; +} + +void onNozzleVolumeTypeSwitch(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType type) +{ + if (!preset_bundle) + return; + // Machine-synced stats carry the device's per-type breakdown; a flow switch must not collapse it. + if (s_nozzle_stats_from_machine) + return; + // Hybrid is a mix, not a type every nozzle shares, so there is nothing to carry over. + if (type == nvtHybrid) + return; + const int total = extruder_nozzle_count_total(preset_bundle, extruder_id); + setExtruderNozzleCount(preset_bundle, extruder_id, type, total, true); } void manuallySetNozzleCount(int extruder_id) @@ -232,14 +297,20 @@ void manuallySetNozzleCount(int 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; + // Require at least one nozzle for a Hybrid extruder (an empty mix is meaningless) and when the other + // extruder currently has none. + bool force_no_zero = volume_type == nvtHybrid; if (nozzle_volume_type_opt->values.size() > 1) - force_no_zero = extruder_nozzle_count_total(preset_bundle, 1 - extruder_id) == 0; + 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); + if (volume_type == nvtHybrid) { + setExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard, dialog.GetNozzleCount(nvtStandard), true); + setExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow, dialog.GetNozzleCount(nvtHighFlow), false); + } else { + setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, dialog.GetNozzleCount(volume_type), true); + } updateNozzleCountDisplay(preset_bundle, extruder_id, volume_type); wxGetApp().plater()->update(); } @@ -1182,7 +1253,7 @@ std::optional<NozzleOption> tryPopUpMultiNozzleDialog(MachineObject* obj) 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. + // Reset the concrete volume types (Standard/High Flow); Hybrid is a mix marker, never a stats key. // 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<NozzleVolumeType> reset_types{nvtStandard, nvtHighFlow}; @@ -1203,9 +1274,13 @@ std::optional<NozzleOption> tryPopUpMultiNozzleDialog(MachineObject* obj) } } } - // 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. + // The stats now hold the device's per-type breakdown: protect it from being collapsed by a manual + // flow switch, and refresh the sidebar badges. + setNozzleStatsFromMachine(true); + if (nozzle_volume_type_opt) { + for (int extruder_id = 0; extruder_id < 2 && extruder_id < (int) nozzle_volume_type_opt->values.size(); ++extruder_id) + updateNozzleCountDisplay(preset_bundle, extruder_id, NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id])); + } return selected_option; } diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp index 7da90f1413..2ee65ea554 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp @@ -219,10 +219,24 @@ std::optional<NozzleOption> tryPopUpMultiNozzleDialog(MachineObject* obj); // 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. +// Refresh the sidebar nozzle-count badge for one extruder: shows the extruder's physical nozzle count on +// multi-nozzle printers, hides it (count -1) everywhere else. void updateNozzleCountDisplay(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type); +// Reset `extruder_nozzle_stats` to its baseline for the selected printer: each extruder gets +// extruder_max_nozzle_count nozzles of its currently selected volume type. Called whenever the stats are +// absent (the key is session-only — preset switches rebuild the edited config without it). Clears the +// machine-provenance flag. +void seedExtruderNozzleStats(PresetBundle *preset_bundle); + +// React to the user switching one extruder's nozzle volume type: carry the extruder's total nozzle count +// over to the new type. No-op for Hybrid (which is a mix, not a type all nozzles share) and for +// machine-synced stats (the device-reported per-type breakdown must survive a flow switch). +void onNozzleVolumeTypeSwitch(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType type); + +// Mark the current `extruder_nozzle_stats` values as machine-reported (device sync) or manual/seeded. +void setNozzleStatsFromMachine(bool from_machine); + // 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.