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: "
( ) [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 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 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 on_click) { if (hover_label) hover_label->SetOnHoverClick(std::move(on_click)); }
void sync_ams(MachineObject const *obj, std::vector const &ams4, std::vector 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 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 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 Plater::priv::load_files(const std::vector& 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("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("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("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("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("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("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("extruder_max_nozzle_count");
+ auto *nozzle_volume_type = preset_bundle->project_config.option("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 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 reset_types{nvtStandard, nvtHighFlow};
@@ -1203,9 +1274,13 @@ std::optional 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 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.