feat(engine): write per-variant filament values back for selector regroups

When the per-layer filament selector (enable_filament_dynamic_map)
migrates a filament across nozzle variants (e.g. Standard -> High Flow),
the config write-back only stored the derived extruder map; every
per-variant filament value (retraction, nozzle temperature, flow,
flush...) kept the numbers resolved from the pre-slice static mapping.

Now both dynamic write-back sites (the by-layer branch and the
sequential stitch) branch on the result's dynamic support. Migrating
results run a mixed-filament expansion that regathers every
filament_options_with_variant key from the pristine per-variant
superset, giving a migrating filament one config slot per (extruder
type x nozzle volume type) it lands on - filament_self_index,
filament_extruder_variant, and all value arrays grow in lockstep - and
recompute the retract overrides with per-slot machine indices so a nil
slot falls back to its own variant's machine value. Non-migrating
dynamic results take the merged three-map write-back so re-applies
reproduce from the written maps. Unrouted filaments resolve from the
result's own default map, so slot resolution never depends on
filament_map round-tripping through the plate config.

Print::apply reproduces the identical expansion from the persisted
group result (shared dedupe helper, expansion function, and slot
indices on both sides): the expanded keys sit in the psWipeTower /
psGCodeExport invalidate lists, so without the reproduction every
re-apply after a selector slice would diff non-empty and permanently
invalidate. cal_non_support_filaments now resolves the extruder per
layer from the published result for dynamic groupings.

filament_map_2 keeps its apply-time static derivation; nothing on the
dynamic path reads it (the per-slot machine indices key the override
merge), and per-(extruder x volume-type) machine limits in the g-code
processor remain a documented follow-up.

Every change is gated behind is_dynamic_group_reorder() or a persisted
result with dynamic support; no profile sets the flag, so the static
fleet's instruction stream is unchanged (20/20 pinned-slice byte gate
identical, incl. the sequential repro sliced twice, deterministic).

Tests: expansion unit coverage (migrating slots, unrouted fallback via
the default map, mis-sized volume map ignored, nullable retract keys in
lockstep, slot machine index layout), an end-to-end stub-driven
write-back asserting expanded slots, per-layer config-index resolution,
the override merge incl. the nil-slot variant fallback, and re-apply
stability, plus a real selector slice staying valid across re-apply.
Suites green (libslic3r 48987/168, fff_print 633/60).
This commit is contained in:
SoftFever
2026-07-12 11:15:58 +08:00
parent c8db06b1d4
commit abbd420f2a
7 changed files with 569 additions and 26 deletions

View File

@@ -1063,12 +1063,24 @@ bool ToolOrdering::cal_non_support_filaments(const PrintConfig &config,
int find_count = 0;
int find_first_filaments_count = 0;
bool has_non_support = has_non_support_filament(config);
for (const LayerTools &layer_tool : m_layer_tools) {
for (const unsigned int &filament : layer_tool.extruders) {
// The selector can move a filament between extruders per layer; resolve the extruder from
// the published result then, so the first filament attributed to an extruder is one it
// actually prints there. Static results keep the cross-layer filament_map arithmetic.
const bool use_dynamic_map = m_nozzle_group_result.is_support_dynamic_nozzle_map() && m_nozzle_group_result.get_layer_count() > 0;
auto extruder_for_filament = [&](unsigned int filament, size_t layer_idx) -> int {
if (use_dynamic_map)
return m_nozzle_group_result.get_extruder_id(static_cast<int>(filament), static_cast<int>(layer_idx));
return config.filament_map.values[filament] - 1;
};
for (size_t layer_idx = 0; layer_idx < m_layer_tools.size(); ++layer_idx) {
for (const unsigned int &filament : m_layer_tools[layer_idx].extruders) {
//check first filament
if (!config.filament_map.values.empty() && initial_filaments[config.filament_map.values[filament] - 1] == -1) {
initial_filaments[config.filament_map.values[filament] - 1] = filament;
find_first_filaments_count++;
if (!config.filament_map.values.empty()) {
const int extruder_id = extruder_for_filament(filament, layer_idx);
if (extruder_id >= 0 && extruder_id < static_cast<int>(initial_filaments.size()) && initial_filaments[extruder_id] == -1) {
initial_filaments[extruder_id] = filament;
find_first_filaments_count++;
}
}
if (has_non_support) {
@@ -1083,8 +1095,9 @@ bool ToolOrdering::cal_non_support_filaments(const PrintConfig &config,
if (config.filament_map.values.empty())
return true;
if (initial_non_support_filaments[config.filament_map.values[filament] - 1] == -1) {
initial_non_support_filaments[config.filament_map.values[filament] - 1] = filament;
const int extruder_id = extruder_for_filament(filament, layer_idx);
if (extruder_id >= 0 && extruder_id < static_cast<int>(initial_non_support_filaments.size()) && initial_non_support_filaments[extruder_id] == -1) {
initial_non_support_filaments[extruder_id] = filament;
find_count++;
}
@@ -2080,17 +2093,15 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
auto result = MultiNozzleUtils::LayeredNozzleGroupResult::create(nozzle_map_per_layer, grouping_context.nozzle_info.nozzle_list, used_filaments, filament_sequences);
grouping_result = result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult();
// Derive the extruder-level map for the stats path + config write-back.
// Orca: the dynamic (per-layer) result carries no single volume/nozzle map, so only the
// extruder map is written back here; the full per-nozzle config write-back
// (a dedicated dynamic-map path) is a follow-up behind the dev flag.
// Derive the extruder-level map for the stats path; the write-back resolves the
// per-variant slots from the full grouping result itself.
std::vector<int> derived_maps = grouping_result.get_extruder_map(false); // 1-based
if (!derived_maps.empty()) {
filament_maps = derived_maps;
// A sequential per-object plan must not write its own map: the objects' plans are
// stitched print-wide afterwards and written back once from there.
if (not_sequential)
m_print->update_filament_maps_to_config(filament_maps);
m_print->update_to_config_by_nozzle_group_result(grouping_result);
}
std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value - 1; });
}