feat(gui): multi-nozzle UI for H2C/A2L

Multi-nozzle sync widget, AMS rack-nozzle mapping popup, calibration rework, send-dialog nozzle mapping and extruder-count UI. Includes the fix to persist the AMS sync badge on filament cards (H2C/A2L and direct-sync printers).
This commit is contained in:
SoftFever
2026-07-09 00:06:27 +08:00
parent a098b483f6
commit 28b7127150
52 changed files with 6136 additions and 250 deletions

View File

@@ -2,6 +2,9 @@
#include "I18N.hpp"
#include "Widgets/Label.hpp"
#include "MsgDialog.hpp"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevNozzleSystem.h"
#include "DeviceCore/DevNozzleRack.h"
namespace Slic3r { namespace GUI {
@@ -95,35 +98,43 @@ enum class GridTextInputType {
class GridTextInput : public TextInput
{
public:
GridTextInput(wxWindow* parent, wxString text, wxString label, wxSize size, int col_idx, GridTextInputType type);
GridTextInput(wxWindow* parent, wxString text, wxString label, wxSize size, int col_idx, GridTextInputType type, int extruder_id = MAIN_EXTRUDER_ID);
int get_col_idx() { return m_col_idx; }
void set_col_idx(int idx) { m_col_idx = idx; }
int get_extruder_id() { return m_extruder_id; }
void set_extruder_id(int id) { m_extruder_id = id; }
GridTextInputType get_type() { return m_type; }
void set_type(GridTextInputType type) { m_type = type; }
private:
int m_col_idx;
int m_extruder_id{MAIN_EXTRUDER_ID};
GridTextInputType m_type;
};
GridTextInput::GridTextInput(wxWindow* parent, wxString text, wxString label, wxSize size, int col_idx, GridTextInputType type)
GridTextInput::GridTextInput(wxWindow* parent, wxString text, wxString label, wxSize size, int col_idx, GridTextInputType type, int extruder_id)
: TextInput(parent, text, label, "", wxDefaultPosition, size, wxTE_PROCESS_ENTER)
, m_col_idx(col_idx)
, m_extruder_id(extruder_id)
, m_type(type)
{
}
class GridComboBox : public ComboBox {
public:
GridComboBox(wxWindow* parent, wxSize size, int col_idx);
GridComboBox(wxWindow* parent, wxSize size, int col_idx, int extruder_id = MAIN_EXTRUDER_ID);
int get_col_idx() { return m_col_idx; }
void set_col_idx(int idx) { m_col_idx = idx; }
int get_extruder_id() { return m_extruder_id; }
void set_extruder_id(int id) { m_extruder_id = id; }
private:
int m_col_idx;
int m_extruder_id{MAIN_EXTRUDER_ID};
};
GridComboBox::GridComboBox(wxWindow* parent, wxSize size, int col_idx)
GridComboBox::GridComboBox(wxWindow* parent, wxSize size, int col_idx, int extruder_id)
: ComboBox(parent, wxID_ANY, "", wxDefaultPosition, size, 0, nullptr)
, m_col_idx(col_idx)
, m_extruder_id(extruder_id)
{
}
@@ -227,7 +238,7 @@ void CaliPASaveAutoPanel::sync_cali_result(const std::vector<PACalibResult>& cal
m_calib_results.clear();
for (auto& item : cali_result) {
if (item.confidence == 0)
m_calib_results[item.tray_id] = item;
m_calib_results.push_back(item);
}
m_grid_panel->DestroyChildren();
auto grid_sizer = new wxBoxSizer(wxHORIZONTAL);
@@ -281,14 +292,14 @@ void CaliPASaveAutoPanel::sync_cali_result(const std::vector<PACalibResult>& cal
wxString tray_name = get_tray_name_by_tray_id(item.tray_id);
tray_title->SetLabel(tray_name);
auto k_value = new GridTextInput(m_grid_panel, "", "", CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, GridTextInputType::K);
auto n_value = new GridTextInput(m_grid_panel, "", "", CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, GridTextInputType::N);
auto k_value = new GridTextInput(m_grid_panel, "", "", CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, GridTextInputType::K, item.extruder_id);
auto n_value = new GridTextInput(m_grid_panel, "", "", CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, GridTextInputType::N, item.extruder_id);
k_value->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC));
n_value->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC));
auto k_value_failed = new Label(m_grid_panel, _L("Failed"));
auto n_value_failed = new Label(m_grid_panel, _L("Failed"));
auto comboBox_tray_name = new GridComboBox(m_grid_panel, CALIBRATION_SAVE_INPUT_SIZE, item.tray_id);
auto comboBox_tray_name = new GridComboBox(m_grid_panel, CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, item.extruder_id);
auto tray_name_failed = new Label(m_grid_panel, " - ");
wxArrayString selections;
static std::vector<PACalibResult> filtered_results;
@@ -395,17 +406,26 @@ void CaliPASaveAutoPanel::save_to_result_from_widgets(wxWindow* window, bool* ou
return;
//operate
// The grid can hold two rows with the same tray_id under different extruders
// (rack / dual-address configs), so results must be keyed by (tray_id, extruder_id).
auto find_result = [this](int tray_id, int extruder_id) -> PACalibResult* {
auto it = std::find_if(m_calib_results.begin(), m_calib_results.end(), [tray_id, extruder_id](const PACalibResult& r) {
return r.tray_id == tray_id && r.extruder_id == extruder_id;
});
return it != m_calib_results.end() ? &(*it) : nullptr;
};
auto input = dynamic_cast<GridTextInput*>(window);
if (input && input->IsShown()) {
int tray_id = input->get_col_idx();
int tray_id = input->get_col_idx();
int extruder_id = input->get_extruder_id();
if (input->get_type() == GridTextInputType::K) {
float k = 0.0f;
if (!CalibUtils::validate_input_k_value(input->GetTextCtrl()->GetValue(), &k)) {
*out_msg = wxString::Format(_L("Please input a valid value (K in %.1f~%.1f)"), MIN_PA_K_VALUE, MAX_PA_K_VALUE);
*out_is_valid = false;
}
else
m_calib_results[tray_id].k_value = k;
else if (auto* result = find_result(tray_id, extruder_id))
result->k_value = k;
}
else if (input->get_type() == GridTextInputType::N) {
}
@@ -413,7 +433,8 @@ void CaliPASaveAutoPanel::save_to_result_from_widgets(wxWindow* window, bool* ou
auto comboBox = dynamic_cast<GridComboBox*>(window);
if (comboBox && comboBox->IsShown()) {
int tray_id = comboBox->get_col_idx();
int tray_id = comboBox->get_col_idx();
int extruder_id = comboBox->get_extruder_id();
wxString name = comboBox->GetTextCtrl()->GetValue().ToStdString();
if (name.IsEmpty()) {
*out_msg = _L("Please enter the name you want to save to printer.");
@@ -423,7 +444,8 @@ void CaliPASaveAutoPanel::save_to_result_from_widgets(wxWindow* window, bool* ou
*out_msg = _L("The name cannot exceed 40 characters.");
*out_is_valid = false;
}
m_calib_results[tray_id].name = into_u8(name);
if (auto* result = find_result(tray_id, extruder_id))
result->name = into_u8(name);
}
auto childern = window->GetChildren();
@@ -441,52 +463,7 @@ bool CaliPASaveAutoPanel::get_result(std::vector<PACalibResult>& out_result) {
else
save_to_result_from_widgets(m_grid_panel, &is_valid, &err_msg);
if (is_valid) {
/*
std::vector<PACalibResult> to_save_result;
for (auto &result : m_calib_results) {
auto iter = std::find_if(to_save_result.begin(), to_save_result.end(), [this, &result](const PACalibResult &item) {
bool has_same_name = (item.name == result.second.name && item.filament_id == result.second.filament_id);
if (m_obj && m_obj->is_multi_extruders()) {
has_same_name &= (item.extruder_id == result.second.extruder_id && item.nozzle_volume_type == result.second.nozzle_volume_type);
}
return has_same_name;
});
if (iter != to_save_result.end()) {
MessageDialog msg_dlg(nullptr, _L("Only one of the results with the same name will be saved. Are you sure you want to overwrite the other results?"),
wxEmptyString, wxICON_WARNING | wxYES_NO);
if (msg_dlg.ShowModal() != wxID_YES) {
return false;
} else {
break;
}
}
}
for (auto &result : m_history_results) {
auto iter = std::find_if(m_history_results.begin(), m_history_results.end(), [this, &result](const PACalibResult &item) {
bool has_same_name = (item.name == result.name && item.filament_id == result.filament_id);
if (m_obj && m_obj->is_multi_extruders()) {
has_same_name &= (item.extruder_id == result.extruder_id && item.nozzle_volume_type == result.nozzle_volume_type);
}
return has_same_name;
});
if (iter != m_history_results.end()) {
MessageDialog msg_dlg(nullptr,
wxString::Format(_L("There is already a historical calibration result with the same name: %s. Are you sure you want to override the historical result?"),
result.name),
wxEmptyString, wxICON_WARNING | wxYES_NO);
if (msg_dlg.ShowModal() != wxID_YES) {
return false;
}
}
}
*/
for (auto& result : m_calib_results) {
out_result.push_back(result.second);
}
out_result = m_calib_results;
return true;
}
else {
@@ -517,11 +494,12 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
m_calib_results.clear();
for (auto &item : cali_result) {
if (item.confidence == 0) {
int tray_id = 4 * item.ams_id + item.slot_id;
PACalibResult result = item;
result.tray_id = 4 * item.ams_id + item.slot_id;
if (item.ams_id == VIRTUAL_TRAY_MAIN_ID || item.ams_id == VIRTUAL_TRAY_DEPUTY_ID) {
tray_id = item.ams_id;
result.tray_id = item.ams_id;
}
m_calib_results[tray_id] = item;
m_calib_results.push_back(result);
}
}
m_multi_extruder_grid_panel->DestroyChildren();
@@ -532,14 +510,21 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
m_multi_extruder_grid_panel->SetSizer(grid_sizer, true);
m_multi_extruder_grid_panel->Bind(wxEVT_LEFT_DOWN, [this](auto &e) { SetFocusIgnoringChildren(); });
wxStaticBoxSizer *left_sizer = new wxStaticBoxSizer(wxVERTICAL, m_multi_extruder_grid_panel, _L("Left extruder"));
wxStaticBoxSizer *right_sizer = new wxStaticBoxSizer(wxVERTICAL, m_multi_extruder_grid_panel, _L("Right extruder"));
const std::string& cwsp_pt = m_obj->printer_type;
wxStaticBoxSizer *left_sizer = new wxStaticBoxSizer(wxVERTICAL, m_multi_extruder_grid_panel, _L(DevPrinterConfigUtil::get_toolhead_display_name(cwsp_pt, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::SentenceCase)));
wxStaticBoxSizer *right_sizer = new wxStaticBoxSizer(wxVERTICAL, m_multi_extruder_grid_panel, _L(DevPrinterConfigUtil::get_toolhead_display_name(cwsp_pt, MAIN_EXTRUDER_ID, ToolHeadComponent::Extruder, ToolHeadNameCase::SentenceCase)));
grid_sizer->Add(left_sizer);
grid_sizer->AddSpacer(COLUMN_GAP);
grid_sizer->Add(right_sizer);
wxFlexGridSizer *left_grid_sizer = new wxFlexGridSizer(3, COLUMN_GAP, ROW_GAP);
wxFlexGridSizer *right_grid_sizer = new wxFlexGridSizer(3, COLUMN_GAP, ROW_GAP);
// The main extruder can carry a nozzle rack (H2C); non-rack printers keep the 3-column layout.
auto ns = m_obj->GetNozzleSystem();
auto rack = ns ? ns->GetNozzleRack() : nullptr;
bool has_rack = rack && rack->IsSupported();
wxFlexGridSizer *right_grid_sizer = new wxFlexGridSizer(has_rack ? 4 : 3, COLUMN_GAP, ROW_GAP);
left_sizer->Add(left_grid_sizer);
right_sizer->Add(right_grid_sizer);
@@ -567,29 +552,46 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
auto k_title = new Label(m_multi_extruder_grid_panel, _L("Factor K"), 0, CALIBRATION_SAVE_NUMBER_INPUT_SIZE);
k_title->SetFont(Label::Head_14);
right_grid_sizer->Add(k_title, 1, wxALIGN_CENTER);
if (has_rack) {
auto nozzle_title = new Label(m_multi_extruder_grid_panel, _L("Nozzle ID"), 0, CALIBRATION_SAVE_NUMBER_INPUT_SIZE);
nozzle_title->SetFont(Label::Head_14);
right_grid_sizer->Add(nozzle_title, 1, wxALIGN_CENTER);
}
}
std::vector<std::pair<int, std::string>> preset_names;
// key: (extruder_id, tray_id)
std::vector<std::pair<std::pair<int, int>, std::string>> preset_names;
int i = 1;
std::unordered_set<std::string> set;
for (auto &info : m_obj->selected_cali_preset) {
std::string default_name;
// extruder _id
// Derive extruder_id from the tray so the extruder/tray key stays consistent
// with the calibration result's extruder_id.
int extruder_id = 0;
if (info.tray_id == VIRTUAL_TRAY_MAIN_ID) {
extruder_id = 0;
} else if (info.tray_id == VIRTUAL_TRAY_DEPUTY_ID) {
extruder_id = 1;
} else {
int ams_id = info.tray_id / 4;
extruder_id = m_obj->get_extruder_id_by_ams_id(std::to_string(ams_id));
}
{
int extruder_id = 0;
if (info.tray_id == VIRTUAL_TRAY_MAIN_ID) {
extruder_id = 0;
} else if (info.tray_id == VIRTUAL_TRAY_DEPUTY_ID) {
extruder_id = 1;
} else {
int ams_id = info.tray_id / 4;
extruder_id = m_obj->get_extruder_id_by_ams_id(std::to_string(ams_id));
}
if (extruder_id == 0) {
default_name += L("Right Nozzle");
} else if (extruder_id == 1){
default_name += L("Left Nozzle");
if (extruder_id == MAIN_EXTRUDER_ID) {
default_name += DevPrinterConfigUtil::get_toolhead_display_name(cwsp_pt, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase);
if (has_rack) {
default_name += "_";
if (info.nozzle_pos_id == 0) {
default_name += "R";
} else if (info.nozzle_pos_id >= 0x10) {
default_name += std::to_string((info.nozzle_pos_id & 0x0f) + 1);
} else {
default_name += "N/A";
}
}
} else if (extruder_id == DEPUTY_EXTRUDER_ID) {
default_name += DevPrinterConfigUtil::get_toolhead_display_name(cwsp_pt, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase);
}
}
@@ -614,7 +616,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
}
}
preset_names.push_back({info.tray_id, default_name});
preset_names.push_back({{extruder_id, info.tray_id}, default_name});
}
bool left_first_add_item = true;
@@ -648,14 +650,19 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
tray_title->SetBitmap(*get_extruder_color_icon(full_filament_ams_list[item.tray_id].opt_string("filament_colour", 0u), tray_name.ToStdString(), FromDIP(20), FromDIP(20)));
tray_title->SetToolTip("");
auto k_value = new GridTextInput(m_multi_extruder_grid_panel, "", "", CALIBRATION_SAVE_NUMBER_INPUT_SIZE, item.tray_id, GridTextInputType::K);
auto n_value = new GridTextInput(m_multi_extruder_grid_panel, "", "", CALIBRATION_SAVE_NUMBER_INPUT_SIZE, item.tray_id, GridTextInputType::N);
auto k_value = new GridTextInput(m_multi_extruder_grid_panel, "", "", CALIBRATION_SAVE_NUMBER_INPUT_SIZE, item.tray_id, GridTextInputType::K, item.extruder_id);
auto n_value = new GridTextInput(m_multi_extruder_grid_panel, "", "", CALIBRATION_SAVE_NUMBER_INPUT_SIZE, item.tray_id, GridTextInputType::N, item.extruder_id);
k_value->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC));
n_value->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC));
auto k_value_failed = new Label(m_multi_extruder_grid_panel, _L("Failed"));
auto n_value_failed = new Label(m_multi_extruder_grid_panel, _L("Failed"));
auto comboBox_tray_name = new GridComboBox(m_multi_extruder_grid_panel, CALIBRATION_SAVE_INPUT_SIZE, item.tray_id);
auto nozzle_id_value = new Label(m_multi_extruder_grid_panel, wxEmptyString);
nozzle_id_value->SetMinSize(wxSize(FromDIP(180), FromDIP(24)));
nozzle_id_value->Wrap(-1);
auto nozzle_id_failed = new Label(m_multi_extruder_grid_panel, _L("Failed"));
auto comboBox_tray_name = new GridComboBox(m_multi_extruder_grid_panel, CALIBRATION_SAVE_INPUT_SIZE, item.tray_id, item.extruder_id);
auto tray_name_failed = new Label(m_multi_extruder_grid_panel, " - ");
wxArrayString selections;
static std::vector<PACalibResult> filtered_results;
@@ -671,22 +678,26 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
}
comboBox_tray_name->Set(selections);
auto set_edit_mode = [this, k_value, n_value, k_value_failed, n_value_failed, comboBox_tray_name, tray_name_failed](std::string str) {
auto set_edit_mode = [this, k_value, n_value, k_value_failed, n_value_failed, nozzle_id_value, nozzle_id_failed, comboBox_tray_name, tray_name_failed](std::string str, bool display_nozzle) {
if (str == "normal") {
comboBox_tray_name->Show();
tray_name_failed->Show(false);
k_value->Show();
n_value->Show();
nozzle_id_value->Show(display_nozzle);
k_value_failed->Show(false);
n_value_failed->Show(false);
nozzle_id_failed->Show(false);
}
if (str == "failed") {
comboBox_tray_name->Show(false);
tray_name_failed->Show();
k_value->Show(false);
n_value->Show(false);
nozzle_id_value->Show(false);
k_value_failed->Show();
n_value_failed->Show();
nozzle_id_failed->Show(display_nozzle);
}
// hide n value
@@ -698,15 +709,39 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
};
if (!result_failed) {
set_edit_mode("normal");
set_edit_mode("normal", has_rack && item.extruder_id == MAIN_EXTRUDER_ID);
auto k_str = wxString::Format("%.3f", item.k_value);
auto n_str = wxString::Format("%.3f", item.n_coef);
k_value->GetTextCtrl()->SetValue(k_str);
n_value->GetTextCtrl()->SetValue(n_str);
if (has_rack && item.extruder_id == MAIN_EXTRUDER_ID) {
wxString nozzle_id_str;
if (item.nozzle_pos_id == 0) {
nozzle_id_str += "R | ";
} else if (item.nozzle_pos_id >= 0x10) {
nozzle_id_str += wxString::Format("%d | ", (item.nozzle_pos_id & 0x0f) + 1);
} else {
nozzle_id_str += "N/A | ";
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "Nozzle position id is -1 or invalid.";
}
nozzle_id_str += wxString::Format("%.1f mm ", item.nozzle_diameter);
switch (item.nozzle_volume_type) {
case NozzleVolumeType::nvtStandard: nozzle_id_str += _L("Standard Flow"); break;
case NozzleVolumeType::nvtHighFlow: nozzle_id_str += _L("High Flow"); break;
case NozzleVolumeType::nvtTPUHighFlow: nozzle_id_str += _L("TPU High Flow"); break;
default: break;
}
nozzle_id_value->SetLabel(nozzle_id_str);
}
for (auto &name : preset_names) {
if (item.tray_id == name.first) { comboBox_tray_name->SetValue(from_u8(name.second)); }
int extruder_id = name.first.first;
int tray_id = name.first.second;
if (item.tray_id == tray_id && item.extruder_id == extruder_id) {
comboBox_tray_name->SetValue(from_u8(name.second));
}
}
comboBox_tray_name->Bind(wxEVT_COMBOBOX, [this, comboBox_tray_name, k_value, n_value](auto &e) {
@@ -714,7 +749,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
auto history = filtered_results[selection];
});
} else {
set_edit_mode("failed");
set_edit_mode("failed", has_rack && item.extruder_id == MAIN_EXTRUDER_ID);
}
if ((m_obj->is_main_extruder_on_left() && item.extruder_id == 0)
@@ -762,6 +797,14 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
} else {
right_grid_sizer->Add(k_value_failed, 1, wxEXPAND);
}
if (has_rack) {
if (nozzle_id_value->IsShown()) {
right_grid_sizer->Add(nozzle_id_value, 1, wxEXPAND);
} else {
right_grid_sizer->Add(nozzle_id_failed, 1, wxEXPAND);
}
}
}
}