feat(engine): stitch per-object selector plans for sequential prints

Sequential (by-object) prints were incoherent with the per-layer filament
selector (enable_filament_dynamic_map): the by-object branch published a
static grouping while each per-object ToolOrdering independently ran the
dynamic planner from an empty nozzle status and wrote its own map to the
config (one write per object, last object wins). The exported toolchange
sequences then disagreed with the published result that drives the
per-layer maps, placeholders, and selector emission.

Now the by-object branch, when the selector is enabled, plans each unique
object once — threading the physical nozzle occupancy and the previous
object's last filament into the next plan — stitches the per-object
per-layer nozzle maps into one print-wide result (gap-filled by the new
normalize_nozzle_map_per_layer so any layer index resolves a filament's
nozzle consistently), publishes it, and writes the derived extruder map
back once. The plans are cached on the Print and g-code export consumes
the cache: the ToolOrdering seed changes the plan input (dontcare
assignment, first-layer reorder), so a fresh export-time construction
could re-plan differently from the published stitch. The per-object
dynamic write-back is gated off for sequential prints.

Every change is gated behind is_dynamic_group_reorder(); no profile sets
the flag, so the static fleet's instruction stream is unchanged (20/20
pinned-slice byte gate identical, incl. the by-object repro sliced twice).

Tests: normalize unit coverage (carry-forward, back-fill, ragged input),
stitched-blocks selector detection, and an end-to-end by-object selector
slice (apply -> process -> export) asserting the published stitched
result, one cached plan per object, the config write-back, and a clean
export. Suites green (libslic3r 48958/165, fff_print 633/60).
This commit is contained in:
SoftFever
2026-07-12 03:24:35 +08:00
parent 780b2f1ebe
commit c8db06b1d4
8 changed files with 412 additions and 63 deletions

View File

@@ -1914,6 +1914,24 @@ static std::vector<FilamentPlanRes> plan_filament_mapping_and_order_by_combo_ran
return results;
}
MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::build_sequential_group_result(
Print* print,
std::vector<std::vector<int>> nozzle_map_per_layer,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<std::vector<unsigned int>>& layer_sequences,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::set<int>>& physical_unprintables,
const std::vector<std::set<int>>& geometric_unprintables,
const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes)
{
MultiNozzleUtils::normalize_nozzle_map_per_layer(nozzle_map_per_layer, layer_filaments);
auto context = build_filament_group_context(print, layer_filaments, physical_unprintables, geometric_unprintables,
unprintable_volumes, FilamentMapMode::fmmAutoForFlush, {});
auto result = MultiNozzleUtils::LayeredNozzleGroupResult::create(nozzle_map_per_layer, context.nozzle_info.nozzle_list,
used_filaments, layer_sequences);
return result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult();
}
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{
const PrintConfig* print_config = m_print_config_ptr;
@@ -2031,6 +2049,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
// whole shipping fleet AND H2C static mode — the static branch below is the only one they take, so
// their g-code is byte-identical. Only an H2C profile that enables the selector opens this branch.
const bool dynamic_reorder = m_print && m_print->is_dynamic_group_reorder();
// Orca: there is no is_sequential_print() helper, so the not-sequential check is mirrored with
// the same predicate the static by-object gate below uses. Sequential prints (with more than
// one object) publish and write back from the by-object branch in Print::process instead of
// from each per-object ordering.
const bool not_sequential = print_config->print_sequence != PrintSequence::ByObject ||
(m_print && m_print->objects().size() == 1);
if (dynamic_reorder) {
// Build the grouping context, plan per-combo-range nozzle maps + filament orders, then wrap the
@@ -2063,7 +2087,10 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
std::vector<int> derived_maps = grouping_result.get_extruder_map(false); // 1-based
if (!derived_maps.empty()) {
filament_maps = derived_maps;
m_print->update_filament_maps_to_config(filament_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);
}
std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value - 1; });
}
@@ -2113,16 +2140,10 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
// placeholders are unchanged; H2C/A2L resolve to a nozzle-granular result (dynamic mode
// resolves per-layer). GCode consumes this via Print::get_layered_nozzle_group_result().
m_nozzle_group_result = grouping_result;
{
// Orca: the ToolOrdering member is stored unconditionally, but the Print-level store is gated
// behind a not-sequential check. There is no is_sequential_print() here, so it is mirrored
// with the same predicate the branch above uses. Sequential prints publish their result
// from the by-object branch in Print::process instead.
const bool not_sequential = print_config->print_sequence != PrintSequence::ByObject ||
(m_print && m_print->objects().size() == 1);
if (m_print != nullptr && not_sequential)
m_print->set_nozzle_group_result(std::make_shared<MultiNozzleUtils::LayeredNozzleGroupResult>(m_nozzle_group_result));
}
// Orca: the ToolOrdering member is stored unconditionally, but the Print-level store is gated
// behind the not-sequential check hoisted above.
if (m_print != nullptr && not_sequential)
m_print->set_nozzle_group_result(std::make_shared<MultiNozzleUtils::LayeredNozzleGroupResult>(m_nozzle_group_result));
auto maps_without_group = filament_maps;
for (auto& item : maps_without_group)

View File

@@ -246,6 +246,13 @@ public:
// For single-nozzle printers this is one logical nozzle per extruder (nozzle id == extruder id).
// Consumed by GCode (get_nozzle_id / get_first_nozzle_for_filament).
const MultiNozzleUtils::LayeredNozzleGroupResult &get_layered_nozzle_group_result() const { return m_nozzle_group_result; }
// Physical nozzle occupancy threading for the sequential (by-object) selector regroup: the
// setter seeds both the initial recorder (the state the per-layer plan starts from) and the
// running recorder (read back after sort_and_build_data via get_nozzle_status()), so each
// object's plan continues from the nozzle state the previous object ended with.
const MultiNozzleUtils::NozzleStatusRecorder &get_nozzle_status() const { return m_nozzle_status; }
void set_nozzle_status(const MultiNozzleUtils::NozzleStatusRecorder &status) { m_initial_nozzle_status = status; m_nozzle_status = status; }
/*
* called in single extruder mode, the value in map are all 0
* called in dual extruder mode, the value in map will be 0 or 1
@@ -257,6 +264,22 @@ public:
// path; the per-layer engine supplies non-empty values.
static MultiNozzleUtils::LayeredNozzleGroupResult get_recommended_filament_maps(const std::vector<std::vector<unsigned int>>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector<std::set<int>>& physical_unprintables, const std::vector<std::set<int>>& geometric_unprintables, const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes = {}, const std::unordered_map<int, int>& nozzle_status = {});
// Wrap stitched per-layer filament->nozzle maps from a sequential (by-object) selector regroup
// into one print-wide result. nozzle_map_per_layer / layer_filaments / layer_sequences are the
// per-object planned layers concatenated in print order; nozzle_map_per_layer is taken by value
// and normalized in place. The nozzle list is rebuilt from the print's grouping context. Returns
// an empty result when the wrap fails. Lives here (not in Print) to reach the file-local
// grouping-context builder.
static MultiNozzleUtils::LayeredNozzleGroupResult build_sequential_group_result(
Print* print,
std::vector<std::vector<int>> nozzle_map_per_layer,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<std::vector<unsigned int>>& layer_sequences,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::set<int>>& physical_unprintables,
const std::vector<std::set<int>>& geometric_unprintables,
const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes);
// should be called after doing reorder
FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode);
void cal_most_used_extruder(const PrintConfig &config);