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

@@ -1,5 +1,6 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/FilamentGroupUtils.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/GCode/ToolOrdering.hpp"
@@ -348,3 +349,45 @@ TEST_CASE("NozzleStatusRecorder tracks nozzle/extruder occupancy", "[MultiNozzle
// Clearing a nozzle leaves the extruder->nozzle association intact.
REQUIRE(rec.get_nozzle_in_extruder(1) == 2);
}
TEST_CASE("Hybrid nozzle stats resolve to concrete volume types", "[ToolOrdering][H2C]")
{
// Extruder 0 is Standard-only; extruder 1 carries a mixed Standard + High Flow inventory
// (the "Hybrid" flow selection). The write-back pipeline persists get_volume_map(), so the
// result must always carry concrete per-filament volume types, never the Hybrid seed.
auto stats = get_extruder_nozzle_stats({"Standard#1", "Standard#1|High Flow#1"});
REQUIRE(stats.size() == 2);
REQUIRE(stats[1].size() == 2);
std::vector<unsigned int> used_filaments = {0, 1, 2};
std::vector<int> filament_map = {0, 1, 1}; // 0-based extruder ids
std::vector<int> volume_requests = {(int) nvtStandard, (int) nvtHighFlow, (int) nvtStandard};
std::vector<int> nozzle_requests = {0, 1, 2}; // distinct logical nozzles
auto group = LayeredNozzleGroupResult::create(used_filaments, filament_map, volume_requests, nozzle_requests, stats, 0.4f);
REQUIRE(group.has_value());
auto volume_map = group->get_volume_map();
REQUIRE(volume_map == volume_requests);
for (auto fid : used_filaments)
REQUIRE(volume_map[fid] != (int) nvtHybrid);
// The Hybrid seed itself matches no physical nozzle: such a request is unsatisfiable.
std::vector<int> hybrid_requests = {(int) nvtStandard, (int) nvtHybrid, (int) nvtStandard};
REQUIRE_FALSE(LayeredNozzleGroupResult::create(used_filaments, filament_map, hybrid_requests, nozzle_requests, stats, 0.4f).has_value());
}
TEST_CASE("update_used_filament_values merges only used filaments", "[ToolOrdering][H2C]")
{
// The config write-back merges the engine's per-filament values over the config baseline:
// used filaments adopt the engine value, unused filaments keep their config assignment.
std::vector<int> old_values = {1, 1, 2, 1};
std::vector<int> new_values = {2, 2, 1, 2};
std::vector<unsigned int> used = {0, 2};
auto merged = FilamentGroupUtils::update_used_filament_values(old_values, new_values, used);
REQUIRE(merged == std::vector<int>{2, 1, 1, 1});
// No used filaments => the config baseline is returned untouched.
REQUIRE(FilamentGroupUtils::update_used_filament_values(old_values, new_values, {}) == old_values);
}