mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-03 07:16:52 +00:00
The sidebar extruder cards get an interactive title row — "<name> ( <count> )" 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.
1291 lines
50 KiB
C++
1291 lines
50 KiB
C++
#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 <algorithm>
|
||
#include <cmath>
|
||
#include <map>
|
||
#include <numeric>
|
||
#include <set>
|
||
|
||
#include <wx/choice.h>
|
||
#include <wx/sizer.h>
|
||
#include <wx/stattext.h>
|
||
|
||
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<ConfigOptionStrings>("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<ConfigOptionStrings>("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);
|
||
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);
|
||
|
||
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));
|
||
|
||
// 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 || 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);
|
||
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(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"));
|
||
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;
|
||
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)
|
||
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<ConfigOptionFloats>("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)
|
||
{
|
||
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)
|
||
{
|
||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||
if (!preset_bundle)
|
||
return;
|
||
|
||
DynamicPrintConfig full_config = preset_bundle->full_config();
|
||
auto *max_nozzle_count = full_config.option<ConfigOptionIntsNullable>("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<ConfigOptionEnumsGeneric>("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 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;
|
||
|
||
ManualNozzleCountDialog dialog(wxGetApp().plater(), volume_type, standard_count, highflow_count, max_nozzle_count->values[extruder_id], force_no_zero);
|
||
if (dialog.ShowModal() == wxID_OK) {
|
||
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();
|
||
}
|
||
}
|
||
|
||
// ==== 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<DevNozzleRack> rack)
|
||
{
|
||
m_nozzle_rack = rack;
|
||
const auto& nozzle_rack = rack.lock();
|
||
if (nozzle_rack) {
|
||
UpdateNozzleItems(m_nozzle_items, nozzle_rack);
|
||
}
|
||
}
|
||
|
||
std::vector<int> HotEndTable::FilterHotEnds(const NozzleOption& option)
|
||
{
|
||
auto rack = m_nozzle_rack.lock();
|
||
if (!rack)
|
||
return {};
|
||
|
||
std::vector<HotEndAttr> 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<int> 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, int>(wxColour("#E5F0EE"), StateColor::Normal)
|
||
);
|
||
|
||
const static StateColor bd_green(
|
||
std::pair<wxColour, int>(wxColour("#009688"), 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("#E5F0EE");
|
||
}
|
||
}
|
||
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<wxColour, int>(bg_color, StateColor::Normal)
|
||
);
|
||
|
||
const static StateColor bd_green(
|
||
std::pair<wxColour, int>(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<int>& 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<int, wgtDeviceNozzleRackNozzleItem*>& nozzle_items, std::shared_ptr<DevNozzleRack> 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<std::string>() == "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<std::string>() == "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<std::string>() == "onSelect") {
|
||
int idx = j["index"].get<int>();
|
||
m_selected_idx = idx;
|
||
SendSelectionChangedEvent();
|
||
}
|
||
else if (j["msg"].get<std::string>() == "layout") {
|
||
int height = j["height"].get<int>();
|
||
int width = j["width"].get<int>();
|
||
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<NozzleOption>& 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<DevNozzleRack> 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<DevNozzleRack> 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, int>(wxColour(144, 144, 144), StateColor::Disabled),
|
||
std::pair<wxColour, int>(wxColour(0, 137, 123), StateColor::Pressed),
|
||
std::pair<wxColour, int>(wxColour(38, 166, 154), StateColor::Hovered),
|
||
std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Normal)
|
||
);
|
||
|
||
StateColor btn_text_green(
|
||
std::pair<wxColour, int>(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<DevNozzleRack> 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<MultiNozzleUtils::NozzleGroupInfo>& 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<NozzleOption> MultiNozzleSyncDialog::GetNozzleOptions(const std::vector<MultiNozzleUtils::NozzleGroupInfo>& nozzle_groups)
|
||
{
|
||
std::vector<NozzleOption> options;
|
||
|
||
std::set<std::string> diameters;
|
||
std::multimap<std::string, MultiNozzleUtils::NozzleGroupInfo> 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<std::pair<NozzleVolumeType, int>> left_nozzles;
|
||
std::vector<std::pair<NozzleVolumeType, int>> 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<std::pair<NozzleVolumeType, int>>{{}} : left_nozzles) {
|
||
for (const auto& right_nozzle : right_nozzles.empty() ? std::vector<std::pair<NozzleVolumeType, int>>{{}} : 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<DevNozzleRack> 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<DevNozzleRack> 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<DevNozzleRack> 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<DevNozzleRack> 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<NozzleOption> 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<ConfigOptionEnumsGeneric>("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 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};
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
// 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;
|
||
}
|
||
|
||
return std::nullopt;
|
||
}
|
||
|
||
}} // namespace Slic3r::GUI
|