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

@@ -2954,6 +2954,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
first_non_support_filaments.resize(print.config().nozzle_diameter.size(), -1);
first_filaments.resize(print.config().nozzle_diameter.size(), -1);
float max_additional_fan = 0.f;
// Sequential selector prints consume the per-object plans cached by Print::process — they were
// planned with cross-object nozzle-status threading and match the published stitched result; a
// fresh construction here would re-plan from a different seed. Static sequential prints keep
// the fresh per-object construction (byte-identical output).
const auto &seq_dynamic_orderings = print.sequential_dynamic_orderings();
const bool use_seq_dynamic_cache = print.is_dynamic_group_reorder() && !seq_dynamic_orderings.empty();
if (print.config().print_sequence == PrintSequence::ByObject) {
// Order object instances for sequential print.
print_object_instances_ordering = sort_object_instances_by_model_order(print);
@@ -2963,9 +2969,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
first_has_extrude_print_object = print_object_instance_sequential_active;
bool find_fist_non_support_filament = false;
for (; print_object_instance_sequential_active != print_object_instances_ordering.end(); ++ print_object_instance_sequential_active) {
tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
tool_ordering.sort_and_build_data(*(*print_object_instance_sequential_active)->print_object,initial_extruder_id);
auto cached_ordering = use_seq_dynamic_cache ? seq_dynamic_orderings.find((*print_object_instance_sequential_active)->print_object) : seq_dynamic_orderings.end();
if (cached_ordering != seq_dynamic_orderings.end()) {
tool_ordering = cached_ordering->second;
} else {
tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
tool_ordering.sort_and_build_data(*(*print_object_instance_sequential_active)->print_object,initial_extruder_id);
}
float temp_max_additional_fan = tool_ordering.cal_max_additional_fan(print.config());
if(temp_max_additional_fan > max_additional_fan )
max_additional_fan = temp_max_additional_fan;
@@ -3568,8 +3578,15 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
for (; print_object_instance_sequential_active != print_object_instances_ordering.end(); ++ print_object_instance_sequential_active) {
const PrintObject &object = *(*print_object_instance_sequential_active)->print_object;
if (&object != prev_object || tool_ordering.first_extruder() != final_extruder_id) {
tool_ordering = ToolOrdering(object, final_extruder_id);
tool_ordering.sort_and_build_data(object, final_extruder_id);
auto cached_ordering = use_seq_dynamic_cache ? seq_dynamic_orderings.find(&object) : seq_dynamic_orderings.end();
if (cached_ordering != seq_dynamic_orderings.end()) {
// Never re-plan a selector object mid-export: the cached plan is what the
// published stitched result was built from.
tool_ordering = cached_ordering->second;
} else {
tool_ordering = ToolOrdering(object, final_extruder_id);
tool_ordering.sort_and_build_data(object, final_extruder_id);
}
unsigned int new_extruder_id = tool_ordering.first_extruder();
if (new_extruder_id == (unsigned int)-1)
// Skip this object.