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

@@ -1212,7 +1212,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
//BBS: process the filament_map related logic
std::unordered_set<std::string> print_diff_set(print_diff.begin(), print_diff.end());
if (print_diff_set.find("filament_map_mode") == print_diff_set.end())
if (!print_diff_set.empty() && print_diff_set.find("filament_map_mode") == print_diff_set.end())
{
FilamentMapMode map_mode = new_full_config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value;
if (is_auto_filament_map_mode(map_mode)) {
@@ -1224,9 +1224,29 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
old_opt->set(new_opt);
m_config.filament_map = *new_opt;
}
if (print_diff_set.find("filament_volume_map") != print_diff_set.end()) {
print_diff_set.erase("filament_volume_map");
//full_config_diff.erase("filament_volume_map");
ConfigOptionInts* old_opt = m_full_print_config.option<ConfigOptionInts>("filament_volume_map", true);
ConfigOptionInts* new_opt = new_full_config.option<ConfigOptionInts>("filament_volume_map", true);
old_opt->set(new_opt);
m_config.filament_volume_map = *new_opt;
}
if (print_diff_set.find("filament_nozzle_map") != print_diff_set.end()) {
print_diff_set.erase("filament_nozzle_map");
//full_config_diff.erase("filament_nozzle_map");
ConfigOptionInts* old_opt = m_full_print_config.option<ConfigOptionInts>("filament_nozzle_map", true);
ConfigOptionInts* new_opt = new_full_config.option<ConfigOptionInts>("filament_nozzle_map", true);
old_opt->set(new_opt);
m_config.filament_nozzle_map = *new_opt;
}
}
else {
print_diff_set.erase("extruder_ams_count");
if (map_mode == fmmManual) {
// filament_nozzle_map is an engine output, not a GUI input, in manual mode
print_diff_set.erase("filament_nozzle_map");
}
std::vector<int> old_filament_map = m_config.filament_map.values;
std::vector<int> new_filament_map = new_full_config.option<ConfigOptionInts>("filament_map", true)->values;
@@ -1243,8 +1263,32 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
break;
}
}
if (same_map)
if (same_map) {
print_diff_set.erase("filament_map");
// The extruder retract overrides are keyed by the (unchanged) filament map;
// recompute them and drop diffs whose recomputed value matches the current
// config, so a cosmetic reordering of unused filaments does not invalidate.
const auto& retract_keys = print_config_def.extruder_retract_keys();
const std::string filament_prefix = "filament_";
std::vector<int> old_f_map_indices(old_filament_map.size(), 0);
for (size_t i = 0; i < old_filament_map.size(); i++)
old_f_map_indices[i] = old_filament_map[i] - 1;
for (const auto& rk : retract_keys) {
if (print_diff_set.find(rk) == print_diff_set.end())
continue;
const ConfigOption* opt_old = m_config.option(rk);
const ConfigOption* opt_new_m = new_full_config.option(rk);
const ConfigOption* opt_new_f = new_full_config.option(filament_prefix + rk);
if (opt_old && opt_new_m && opt_new_f) {
std::unique_ptr<ConfigOption> opt_recomputed(opt_new_m->clone());
opt_recomputed->apply_override(opt_new_f, old_f_map_indices);
if (*opt_old == *opt_recomputed)
print_diff_set.erase(rk);
}
}
}
}
}
if (print_diff_set.size() != print_diff.size())
@@ -1265,11 +1309,12 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
{
ExtruderType extruder_type = (ExtruderType)(opt_extruder_type->get_at(filament_maps[index] - 1));
NozzleVolumeType nozzle_volume_type = (NozzleVolumeType)(opt_nozzle_volume_type->get_at(filament_maps[index] - 1));
// Orca: the per-filament volume map is only trustworthy when a producer explicitly sized it
// to the filament count; the registered default is a 1-element vector (see
// Orca: honour the per-filament volume map only when a producer sized it to the filament
// count; mis-sized maps (stale project values, CLI runs until the per-filament synthesis
// lands there) must not be indexed per filament (see
// update_values_to_printer_extruders_for_multiple_filaments for the same guard).
if ((extruder_volume_type_count > extruder_count) && opt_filament_volume_maps
&& filament_maps.size() > 1 && opt_filament_volume_maps->values.size() == filament_maps.size())
&& opt_filament_volume_maps->values.size() == filament_maps.size())
nozzle_volume_type = (NozzleVolumeType)(opt_filament_volume_maps->values[index]);
m_config.filament_map_2.values[index] = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
}
@@ -1694,6 +1739,12 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_default_object_config.apply_only(new_full_config, new_changed_keys, true);
// Handle changes to regions config defaults
m_default_region_config.apply_only(new_full_config, new_changed_keys, true);
// Orca: keep the pre-expansion snapshot in sync with this late normalization pass.
// The engine map write-back rebuilds m_full_print_config from m_ori_full_print_config
// after slicing; a stale snapshot would resurrect the un-normalized values (e.g.
// enable_prime_tower on a single-filament print) in the dumped config and spuriously
// re-invalidate the g-code on the next apply.
m_ori_full_print_config.apply_only(new_full_config, new_changed_keys, true);
m_full_print_config = std::move(new_full_config);
update_filament_self_index_cache();
}