feat(gui): per-filament nozzle volume selection and persistence

- FilamentMapDialog's manual page understands volume types: a mixed
  (Hybrid) extruder shows separate Standard / High Flow drop zones
  with live sub-nozzle counts, a validation timer with an inline
  error + "set nozzle count" suggestion, and composes a per-filament
  volume map on OK (persisted to the plate or globally)
- switching an extruder's Flow type rewrites the affected plate map
  entries; plate maps stay sized across filament add/delete/count
  changes (values keep their filament, no index shift)
- CLI: manual mapping on multi-nozzle printers synthesizes the volume
  map from extruder flow types when absent; nozzle-manual mode
  requires explicit maps; computed maps land on the plate so exported
  projects carry filament_volume_maps
- filament_nozzle_map joins the project options (selection seeding +
  filament-count resizing)
- pot entries for the new dialog strings

All 19 reference fixtures byte-identical; slicing an exported Hybrid
project reproduces its g-code byte-for-byte with the volume map
round-tripped through model_settings.config.
This commit is contained in:
SoftFever
2026-07-11 16:37:50 +08:00
parent 21b46044d0
commit 75c8d0775a
14 changed files with 1131 additions and 83 deletions

View File

@@ -36,6 +36,13 @@ static std::vector<int> get_applied_map(DynamicConfig& proj_config, const Plater
return plater_ref->get_global_filament_map();
}
static std::vector<int> get_applied_volume_map(DynamicConfig& proj_config, const Plater* plater_ref, const PartPlate* partplate_ref, const bool sync_plate)
{
if (sync_plate)
return partplate_ref->get_real_filament_volume_maps(proj_config);
return plater_ref->get_global_filament_volume_map();
}
extern std::string& get_left_extruder_unprintable_text();
extern std::string& get_right_extruder_unprintable_text();
@@ -123,7 +130,9 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p
std::vector<std::string> filament_types = full_config.option<ConfigOptionStrings>("filament_type")->values;
FilamentMapMode applied_mode = get_applied_map_mode(full_config, plater_ref,partplate_ref, sync_plate);
std::vector<int> applied_maps = get_applied_map(full_config, plater_ref, partplate_ref, sync_plate);
std::vector<int> applied_volume_maps = get_applied_volume_map(full_config, plater_ref, partplate_ref, sync_plate);
applied_maps.resize(filament_colors.size(), 1);
applied_volume_maps.resize(filament_colors.size(), 0);
if (!force_pop_up && applied_mode != fmmManual)
return true;
@@ -141,6 +150,7 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p
filament_colors,
filament_types,
applied_maps,
applied_volume_maps,
filament_lists,
applied_mode,
plater_ref->get_machine_sync_status(),
@@ -152,25 +162,32 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p
if (ret == wxID_OK) {
FilamentMapMode new_mode = map_dlg.get_mode();
std::vector<int> new_maps = map_dlg.get_filament_maps();
std::vector<int> new_volume_maps = map_dlg.get_filament_volume_maps();
if (sync_plate) {
if (is_slice_all) {
auto plate_list = plater_ref->get_partplate_list().get_plate_list();
for (int i = 0; i < plate_list.size(); ++i) {
plate_list[i]->set_filament_map_mode(new_mode);
if(new_mode == fmmManual)
if (new_mode == fmmManual) {
plate_list[i]->set_filament_maps(new_maps);
plate_list[i]->set_filament_volume_maps(new_volume_maps);
}
}
}
else {
partplate_ref->set_filament_map_mode(new_mode);
if (new_mode == fmmManual)
if (new_mode == fmmManual) {
partplate_ref->set_filament_maps(new_maps);
partplate_ref->set_filament_volume_maps(new_volume_maps);
}
}
}
else {
plater_ref->set_global_filament_map_mode(new_mode);
if (new_mode == fmmManual)
if (new_mode == fmmManual) {
plater_ref->set_global_filament_map(new_maps);
plater_ref->set_global_filament_volume_map(new_volume_maps);
}
}
plater_ref->update();
// check whether able to slice, if not, return false
@@ -186,12 +203,17 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent,
const std::vector<std::string> &filament_color,
const std::vector<std::string> &filament_type,
const std::vector<int> &filament_map,
const std::vector<int> &filament_volume_map,
const std::vector<int> &filaments,
const FilamentMapMode mode,
bool machine_synced,
bool show_default,
bool with_checkbox)
: wxDialog(parent, wxID_ANY, _L("Filament grouping"), wxDefaultPosition, wxDefaultSize,wxDEFAULT_DIALOG_STYLE), m_filament_color(filament_color), m_filament_type(filament_type), m_filament_map(filament_map)
: wxDialog(parent, wxID_ANY, _L("Filament grouping"), wxDefaultPosition, wxDefaultSize,wxDEFAULT_DIALOG_STYLE)
, m_filament_color(filament_color)
, m_filament_type(filament_type)
, m_filament_map(filament_map)
, m_filament_volume_map(filament_volume_map)
{
SetBackgroundColour(*wxWHITE);
@@ -244,7 +266,20 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent,
mode == fmmAutoForMatch && !auto_match_available ? fmmAutoForFlush :
mode;
m_manual_map_panel = new FilamentMapManualPanel(this, m_filament_color, m_filament_type, filaments, filament_map);
m_manual_map_panel = new FilamentMapManualPanel(this, m_filament_color, m_filament_type, filaments, filament_map, filament_volume_map);
// A manual grouping can point filaments at a nozzle volume the extruder does not physically
// carry; the panel's validation timer reports that here so OK is gated on a printable map.
m_manual_map_panel->Bind(wxEVT_INVALID_MANUAL_MAP, [this](wxCommandEvent &event) {
if (m_page_type != PageType::ptManual) {
if (!m_ok_btn->IsEnabled()) { m_ok_btn->Enable(); }
return;
}
if (event.GetInt()) {
if (!m_ok_btn->IsEnabled()) { m_ok_btn->Enable(); }
} else {
if (m_ok_btn->IsEnabled()) { m_ok_btn->Disable(); }
}
});
m_auto_map_panel = new FilamentMapAutoPanel(this, default_auto_mode, auto_match_available);
if (show_default)
m_default_map_panel = new FilamentMapDefaultPanel(this);
@@ -351,16 +386,8 @@ void FilamentMapDialog::on_checkbox(wxCommandEvent &event)
void FilamentMapDialog::on_ok(wxCommandEvent &event)
{
if (m_page_type == PageType::ptManual) {
std::vector<int> left_filaments = m_manual_map_panel->GetLeftFilaments();
std::vector<int> right_filaments = m_manual_map_panel->GetRightFilaments();
for (int i = 0; i < m_filament_map.size(); ++i) {
if (std::find(left_filaments.begin(), left_filaments.end(), i + 1) != left_filaments.end()) {
m_filament_map[i] = 1;
} else if (std::find(right_filaments.begin(), right_filaments.end(), i + 1) != right_filaments.end()) {
m_filament_map[i] = 2;
}
}
m_filament_map = m_manual_map_panel->GetFilamentMaps();
m_filament_volume_map = m_manual_map_panel->GetFilamentVolumeMaps();
}
EndModal(wxID_OK);
@@ -398,6 +425,11 @@ void FilamentMapDialog::update_panel_status(PageType page)
m_auto_map_panel->Show();
}
// The nozzle-availability gate only constrains manual grouping; every other page must
// leave OK usable even if the manual page had disabled it.
if (page != PageType::ptManual && m_ok_btn && !m_ok_btn->IsEnabled())
m_ok_btn->Enable();
if (m_smart_filament)
m_smart_filament->Show(get_mode() == fmmAutoForFlush);