feat(engine): wire the per-filament volume-map producer pipeline

- Print::update_filament_maps_to_config takes filament/volume/nozzle
  maps, backfills an empty volume map from extruder types, rebuilds
  filament_map_2, re-expands the per-filament variant arrays, and
  recomputes retract overrides keyed by resolved slots
- grouping writes its result back in every non-sequential mode;
  manual multi-nozzle grouping validates the user mapping and raises a
  translatable error on deviation; the engine's concrete volume
  assignment is deliberately not merged yet (per-filament arrays are
  already consumed by filament id, so materializing High Flow now
  would change motion before the layer-aware resolvers land)
- Print::apply treats the three map keys as engine outputs in auto
  modes (erased from the diff and adopted), compares them against used
  filaments in manual mode, and keeps the pre-expansion snapshot in
  sync with the late normalization pass so rebuilt headers reflect the
  sliced state instead of resurrecting stale values
- volume/nozzle maps and extruder_nozzle_stats join the invalidation
  group of filament_map (wipe tower + skirt/brim)
- PresetBundle composes full configs with an optional per-filament
  volume map (plate map, else defaults derived from each extruder's
  flow type); project config keeps the map sized across filament
  count changes
- PartPlate stores per-plate volume/nozzle maps; Plater injects them
  at every slice-composition site (incl. g-code reload and wipe-tower
  estimation); BackgroundSlicingProcess reads engine results back to
  the plate in auto modes
- per-filament map trust guards relaxed to size-match everywhere now
  that every producer sizes the map; single-filament explicit flow
  assignments are honored
- tests: grouping volume maps stay concrete, merge semantics of
  update_used_filament_values, single-filament override honoring

Motion g-code is byte-identical fleet-wide including Hybrid projects
(19-fixture gate + repro determinism double-slice). Header deltas:
the map keys now dump real values, and stale pre-normalization values
(e.g. enable_prime_tower on single-used-filament prints) no longer
leak into the config block.
This commit is contained in:
SoftFever
2026-07-11 04:38:31 +08:00
parent 18b9279c26
commit 03a704df7c
15 changed files with 429 additions and 70 deletions

View File

@@ -231,7 +231,20 @@ void BackgroundSlicingProcess::process_fff()
if (m_current_plate->get_real_filament_map_mode(preset_bundle.project_config) < FilamentMapMode::fmmManual) {
std::vector<int> f_maps = m_fff_print->get_filament_maps();
m_current_plate->set_filament_maps(f_maps);
// Orca: the volume map is not read back to the plate yet. The engine's concrete
// volume assignment is not written into the print config until the layer-aware
// g-code resolvers consume it (see the ToolOrdering write-back), so the print-side
// value here is just the injected plate-or-default map echoed back; persisting it
// would freeze the per-extruder defaults (including the transient Hybrid seed) into
// the plate and the saved project. Restore together with the write-back merge.
}
if (m_current_plate->get_real_filament_map_mode(preset_bundle.project_config) != FilamentMapMode::fmmNozzleManual) {
// The engine-resolved nozzle map is read back for every non-nozzle-manual mode so the
// plate config the next apply overlays matches the engine's written-back state
// (otherwise the full-config diff would invalidate the g-code on every apply).
std::vector<int> f_nozzle_maps = m_fff_print->get_filament_nozzle_maps();
m_current_plate->set_filament_nozzle_maps(f_nozzle_maps);
}
wxCommandEvent evt(m_event_slicing_completed_id);
// Post the Slicing Finished message for the G-code viewer to update.
// Passing the timestamp

View File

@@ -319,6 +319,18 @@ std::vector<int> PartPlate::get_real_filament_maps(const DynamicConfig& g_config
return g_maps;
}
std::vector<int> PartPlate::get_real_filament_volume_maps(const DynamicConfig& g_config, bool* use_global_param) const
{
auto maps = get_filament_volume_maps();
if (!maps.empty()) {
if (use_global_param) { *use_global_param = false; }
return maps;
}
auto g_maps = g_config.option<ConfigOptionInts>("filament_volume_map")->values;
if (use_global_param) { *use_global_param = true; }
return g_maps;
}
FilamentMapMode PartPlate::get_real_filament_map_mode(const DynamicConfig& g_config, bool* use_global_param) const
{
auto mode = get_filament_map_mode();
@@ -3506,8 +3518,16 @@ int PartPlate::load_gcode_from_file(const std::string& filename)
int ret = 0;
// process gcode
auto& preset_bundle = wxGetApp().preset_bundle;
std::vector<int> filament_maps = this->get_filament_maps();
DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps);
// Inject the plate's volume map (or the per-extruder defaults) exactly like the apply-time
// composition, so the config applied over the loaded slice result matches the next
// background-process apply and does not invalidate the embedded g-code.
std::vector<int> f_volume_maps = this->get_filament_volume_maps();
if (f_volume_maps.empty()) {
f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps);
}
DynamicPrintConfig full_config = preset_bundle->full_config(false, filament_maps, f_volume_maps);
full_config.apply(m_config, true);
m_print->apply(*m_model, full_config, false);
//BBS: need to apply two times, for after the first apply, the m_print got its object,
@@ -3814,6 +3834,40 @@ void PartPlate::clear_filament_map()
m_config.erase("filament_map");
}
std::vector<int> PartPlate::get_filament_volume_maps() const
{
std::string key = "filament_volume_map";
if (m_config.has(key))
return m_config.option<ConfigOptionInts>(key)->values;
return {};
}
void PartPlate::set_filament_volume_maps(const std::vector<int>& f_maps)
{
m_config.option<ConfigOptionInts>("filament_volume_map", true)->values = f_maps;
}
void PartPlate::clear_filament_volume_map()
{
if (m_config.has("filament_volume_map"))
m_config.erase("filament_volume_map");
}
std::vector<int> PartPlate::get_filament_nozzle_maps() const
{
std::string key = "filament_nozzle_map";
if (m_config.has(key))
return m_config.option<ConfigOptionInts>(key)->values;
return {};
}
void PartPlate::set_filament_nozzle_maps(const std::vector<int>& f_maps)
{
m_config.option<ConfigOptionInts>("filament_nozzle_map", true)->values = f_maps;
}
void PartPlate::clear_filament_map_mode()
{
if (m_config.has("filament_map_mode"))
@@ -4247,7 +4301,14 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini
coordf_t plate_bbox_y_max_local_coord = plate_bbox_2d.max(1) - plate_origin(1);
std::vector<int> filament_maps = part_plate->get_real_filament_maps(proj_cfg);
DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps);
// Keep this composition consistent with the apply-time injection (plate volume map, else
// per-extruder defaults); the config below currently only feeds scalar reads, but a
// divergent volume map would silently mis-resolve any future per-filament read here.
std::vector<int> f_volume_maps = part_plate->get_filament_volume_maps();
if (f_volume_maps.empty()) {
f_volume_maps = wxGetApp().preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps);
}
DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps, f_volume_maps);
const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
float w = dynamic_cast<const ConfigOptionFloat *>(print_cfg.option("prime_tower_width"))->value;
float v = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_volume"))->value;

View File

@@ -253,6 +253,7 @@ public:
PrintSequence get_real_print_seq(bool* plate_same_as_global=nullptr) const;
std::vector<int> get_real_filament_maps(const DynamicConfig& g_config, bool* use_global_param = nullptr)const;
std::vector<int> get_real_filament_volume_maps(const DynamicConfig& g_config, bool* use_global_param = nullptr) const;
FilamentMapMode get_real_filament_map_mode(const DynamicConfig& g_config,bool * use_global_param = nullptr) const;
FilamentMapMode get_filament_map_mode() const;
@@ -262,6 +263,15 @@ public:
std::vector<int> get_filament_maps() const;
void set_filament_maps(const std::vector<int>& f_maps);
// per-filament nozzle-volume choice (NozzleVolumeType values, 0 based filament ids)
std::vector<int> get_filament_volume_maps() const;
void set_filament_volume_maps(const std::vector<int>& f_maps);
void clear_filament_volume_map();
// per-filament nozzle-group choice (0 based filament and nozzle ids)
std::vector<int> get_filament_nozzle_maps() const;
void set_filament_nozzle_maps(const std::vector<int>& f_maps);
void clear_filament_map();
void clear_filament_map_mode();

View File

@@ -8818,7 +8818,11 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
if (preset_bundle->get_printer_extruder_count() > 1) {
PartPlate* cur_plate = background_process.get_current_plate();
std::vector<int> f_maps = cur_plate->get_real_filament_maps(preset_bundle->project_config);
invalidated = background_process.apply(this->model, preset_bundle->full_config(false, f_maps));
std::vector<int> f_volume_maps = cur_plate->get_filament_volume_maps();
if (f_volume_maps.empty()) {
f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps);
}
invalidated = background_process.apply(this->model, preset_bundle->full_config(false, f_maps, f_volume_maps));
background_process.fff_print()->set_extruder_filament_info(get_extruder_filament_info());
}
else
@@ -18326,7 +18330,11 @@ void Plater::apply_background_progress()
Print::ApplyStatus invalidated;
if (preset_bundle->get_printer_extruder_count() > 1) {
std::vector<int> f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config);
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps));
std::vector<int> f_volume_maps = part_plate->get_filament_volume_maps();
if (f_volume_maps.empty()) {
f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps);
}
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps));
}
else
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false));
@@ -18371,7 +18379,11 @@ int Plater::select_plate(int plate_index, bool need_slice)
//always apply the current plate's print
if (preset_bundle->get_printer_extruder_count() > 1) {
std::vector<int> f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config);
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps));
std::vector<int> f_volume_maps = part_plate->get_filament_volume_maps();
if (f_volume_maps.empty()) {
f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps);
}
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps));
}
else
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false));
@@ -18792,7 +18804,11 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi
//always apply the current plate's print
if (preset_bundle->get_printer_extruder_count() > 1) {
std::vector<int> f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config);
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps));
std::vector<int> f_volume_maps = part_plate->get_filament_volume_maps();
if (f_volume_maps.empty()) {
f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps);
}
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps));
}
else
invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false));