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

@@ -1213,9 +1213,16 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
combo_flow->GetDropDown().SetUseContentWidth(true);
combo_flow->Bind(wxEVT_COMBOBOX, [this, index, combo_flow](wxCommandEvent &evt) {
auto printer_tab = dynamic_cast<TabPrinter *>(wxGetApp().get_tab(Preset::TYPE_PRINTER));
printer_tab->set_extruder_volume_type(index, NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt()))));
if (GUI::wxGetApp().plater())
GUI::wxGetApp().plater()->update_machine_sync_status();
NozzleVolumeType volume_type = NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt())));
printer_tab->set_extruder_volume_type(index, volume_type);
auto plater = GUI::wxGetApp().plater();
if (plater) {
// A new Flow type invalidates the per-filament volume choices stored on the
// plates for this extruder; rewrite them so the next apply/grouping sees the
// selected volume instead of a stale one.
plater->update_filament_volume_map(index, static_cast<int>(volume_type));
plater->update_machine_sync_status();
}
});
this->combo_flow = combo_flow;
@@ -7328,6 +7335,16 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
filament_map->values.resize(filament_count, 1);
}
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("filament_nozzle_map", true);
if (filament_nozzle_map->size() != filament_count) {
filament_nozzle_map->values.resize(filament_count, 0);
}
ConfigOptionInts* filament_volume_map = proj_cfg.opt<ConfigOptionInts>("filament_volume_map", true);
if (filament_volume_map->size() != filament_count) {
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// Sync filament multi colour
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("filament_multi_colour", true);
if (filament_multi_color->size() != filament_count) {
@@ -17799,12 +17816,24 @@ void Plater::set_global_filament_map(const std::vector<int>& filament_map)
project_config.option<ConfigOptionInts>("filament_map")->values = filament_map;
}
void Plater::set_global_filament_volume_map(const std::vector<int>& filament_volume_map)
{
auto& project_config = wxGetApp().preset_bundle->project_config;
project_config.option<ConfigOptionInts>("filament_volume_map")->values = filament_volume_map;
}
std::vector<int> Plater::get_global_filament_map() const
{
auto& project_config = wxGetApp().preset_bundle->project_config;
return project_config.option<ConfigOptionInts>("filament_map")->values;
}
std::vector<int> Plater::get_global_filament_volume_map() const
{
auto& project_config = wxGetApp().preset_bundle->project_config;
return project_config.option<ConfigOptionInts>("filament_volume_map")->values;
}
FilamentMapMode Plater::get_global_filament_map_mode() const
{
@@ -18721,13 +18750,17 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt)
auto plate_filament_maps = curr_plate->get_real_filament_maps(project_config);
auto plate_filament_map_mode = curr_plate->get_filament_map_mode();
auto plate_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config);
if (plate_filament_maps.size() != filament_colors.size()) // refine it later, save filament map to app config
plate_filament_maps.resize(filament_colors.size(), 1);
if (plate_filament_volume_maps.size() != filament_colors.size())
plate_filament_volume_maps.resize(filament_colors.size(), 0);
FilamentMapDialog filament_dlg(this,
filament_colors,
filament_types,
plate_filament_maps,
plate_filament_volume_maps,
curr_plate->get_extruders(true),
plate_filament_map_mode,
this->get_machine_sync_status(),
@@ -18741,16 +18774,21 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt)
FilamentMapMode old_map_mode = curr_plate->get_filament_map_mode();
FilamentMapMode new_map_mode = filament_dlg.get_mode();
std::vector<int> new_filament_volume_maps = filament_dlg.get_filament_volume_maps();
std::vector<int> old_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config);
if (new_map_mode != old_map_mode) {
curr_plate->set_filament_map_mode(new_map_mode);
}
if (new_map_mode == fmmManual){
curr_plate->set_filament_maps(new_filament_maps);
curr_plate->set_filament_volume_maps(new_filament_volume_maps);
}
bool need_invalidate = (old_map_mode != new_map_mode ||
old_filament_maps != new_filament_maps);
old_filament_maps != new_filament_maps ||
old_filament_volume_maps != new_filament_volume_maps);
if (need_invalidate) {
if (need_slice) {
@@ -19216,6 +19254,30 @@ bool Plater::get_machine_sync_status()
return p->get_machine_sync_status();
}
void Plater::update_filament_volume_map(int extruder_id, int volume_type)
{
// Hybrid is a per-extruder mix, not a per-filament volume; reset the affected filaments
// to Standard so the manual grouping dialog starts from a concrete assignment.
int selected_volume_type = volume_type == static_cast<int>(NozzleVolumeType::nvtHybrid) ? static_cast<int>(NozzleVolumeType::nvtStandard) : volume_type;
auto& partplate_list = get_partplate_list();
for (int idx = 0; idx < partplate_list.get_plate_count(); ++idx) {
auto plate = partplate_list.get_plate(idx);
if (!plate) continue;
auto filament_map = plate->get_filament_maps();
auto filament_volume_map = plate->get_filament_volume_maps();
if (filament_map.empty() || filament_volume_map.empty()) continue;
if (filament_volume_map.size() < filament_map.size()) {
filament_volume_map.resize(filament_map.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
}
for (size_t i = 0; i < filament_map.size(); ++i) {
if (filament_map[i] == extruder_id + 1) {
filament_volume_map[i] = selected_volume_type;
}
}
plate->set_filament_volume_maps(filament_volume_map);
}
}
#if ENABLE_ENVIRONMENT_MAP
void Plater::init_environment_texture()
{