diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index cdd567457d..858ccd670b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -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. diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index f9c0f31c3e..0faa7b29b9 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1914,6 +1914,24 @@ static std::vector plan_filament_mapping_and_order_by_combo_ran return results; } +MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::build_sequential_group_result( + Print* print, + std::vector> nozzle_map_per_layer, + const std::vector>& layer_filaments, + const std::vector>& layer_sequences, + const std::vector& used_filaments, + const std::vector>& physical_unprintables, + const std::vector>& geometric_unprintables, + const std::map>& 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 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(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(m_nozzle_group_result)); auto maps_without_group = filament_maps; for (auto& item : maps_without_group) diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index b67fea119a..c77b152fe9 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -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>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector>& physical_unprintables, const std::vector>& geometric_unprintables, const std::map>& unprintable_volumes = {}, const std::unordered_map& 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> nozzle_map_per_layer, + const std::vector>& layer_filaments, + const std::vector>& layer_sequences, + const std::vector& used_filaments, + const std::vector>& physical_unprintables, + const std::vector>& geometric_unprintables, + const std::map>& unprintable_volumes); + // should be called after doing reorder FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode); void cal_most_used_extruder(const PrintConfig &config); diff --git a/src/libslic3r/MultiNozzleUtils.cpp b/src/libslic3r/MultiNozzleUtils.cpp index e8ceadbbb7..d5ce550536 100644 --- a/src/libslic3r/MultiNozzleUtils.cpp +++ b/src/libslic3r/MultiNozzleUtils.cpp @@ -52,6 +52,54 @@ std::vector build_nozzle_list(double diameter, const std::vector> &layer_filament_nozzle_maps, + const std::vector> &layer_filaments) +{ + if (layer_filament_nozzle_maps.empty()) + return; + + const int total_layers = static_cast(layer_filament_nozzle_maps.size()); + int filament_count = 0; + for (const auto &layer_map : layer_filament_nozzle_maps) + filament_count = std::max(filament_count, static_cast(layer_map.size())); + + auto layer_uses_filament = [](const std::vector &filaments, int filament_id) { + return std::find(filaments.begin(), filaments.end(), static_cast(filament_id)) != filaments.end(); + }; + + std::vector last_used_nozzle(filament_count, -1); + std::unordered_map first_used_nozzle; + std::unordered_map first_used_layer; + + // Forward pass: layers that extrude a filament define its nozzle; layers that don't inherit + // the nozzle it last used (carry-forward), remembering the first-ever nozzle for the back-fill. + for (int layer_id = 0; layer_id < total_layers; ++layer_id) { + auto &layer_map = layer_filament_nozzle_maps[layer_id]; + const auto &used = layer_id < static_cast(layer_filaments.size()) ? layer_filaments[layer_id] : std::vector(); + + for (int filament_id = 0; filament_id < static_cast(layer_map.size()); ++filament_id) { + if (layer_uses_filament(used, filament_id)) { + last_used_nozzle[filament_id] = layer_map[filament_id]; + if (first_used_nozzle.count(filament_id) == 0) { + first_used_nozzle[filament_id] = layer_map[filament_id]; + first_used_layer[filament_id] = layer_id; + } + } else if (last_used_nozzle[filament_id] >= 0) { + layer_map[filament_id] = last_used_nozzle[filament_id]; + } + } + } + + // Back-fill pass: layers before a filament's first use inherit the first nozzle it ever uses. + for (int layer_id = 0; layer_id < total_layers; ++layer_id) { + auto &layer_map = layer_filament_nozzle_maps[layer_id]; + for (int filament_id = 0; filament_id < static_cast(layer_map.size()); ++filament_id) { + if (first_used_layer.count(filament_id) != 0 && layer_id < first_used_layer[filament_id]) + layer_map[filament_id] = first_used_nozzle[filament_id]; + } + } +} + // ==================== LayeredNozzleGroupResult ==================== static bool has_filament_mapped_to_multiple_nozzles(const std::vector> &layer_filament_nozzle_maps, const std::vector &used_filaments) diff --git a/src/libslic3r/MultiNozzleUtils.hpp b/src/libslic3r/MultiNozzleUtils.hpp index f9b96bd79a..5ae56e497d 100644 --- a/src/libslic3r/MultiNozzleUtils.hpp +++ b/src/libslic3r/MultiNozzleUtils.hpp @@ -269,6 +269,14 @@ FilamentChangeSimResult simulate_filament_change_time( bool calc_sliced_time = false); // ==================== tool functions ==================== +// Make each filament's per-layer nozzle assignment gap-free: layers where a filament is not +// extruded inherit the nozzle it last used (forward carry); layers before its first use inherit +// the first nozzle it ever uses (back-fill). Entries on layers where the filament is actually +// used stay untouched. Needed for stitched sequential maps, where consumers indexing with an +// object-local layer id must resolve the same nozzle as global-id consumers except across a +// genuine mid-print reassignment. +void normalize_nozzle_map_per_layer(std::vector>& layer_filament_nozzle_maps, + const std::vector>& layer_filaments); std::vector build_nozzle_list(std::vector info); std::vector build_nozzle_list(double diameter, const std::vector& filament_nozzle_map, const std::vector& filament_volume_map, const std::vector& filament_map); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 54609f481b..7373623d7c 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2488,6 +2488,9 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) std::vector::const_iterator print_object_instance_sequential_active; std::vector>> layers_to_print = GCode::collect_layers_to_print(*this); std::vector printExtruders; + // Cleared on every process so a print-sequence or selector-mode change can never leave + // stale object pointers behind; repopulated below only by the sequential selector path. + m_sequential_dynamic_orderings.clear(); if (this->config().print_sequence == PrintSequence::ByObject) { // Order object instances for sequential print. print_object_instances_ordering = sort_object_instances_by_model_order(*this); @@ -2509,53 +2512,104 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments); auto geometric_unprintables = this->get_geometric_unprintable_filaments(); auto filament_unprintable_volumes = this->get_filament_unprintable_flow(used_filaments); - std::vectorfilament_maps = this->get_filament_maps(); - auto map_mode = get_filament_map_mode(); - // Grouping returns a nozzle-aware result; the 1-based extruder map for the by-object - // path is derived from it. It is computed in every static map mode (in manual modes it - // mirrors the user's assignment) and published print-wide: GCode's per-nozzle - // placeholder and config-index lookups read it via get_layered_nozzle_group_result(), - // and without it sequential exports on multi-nozzle printers see an empty nozzle table - // (e.g. nozzle_diameter_at_nozzle_id[]) and custom g-code fails to resolve. - auto grouping_result = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables, filament_unprintable_volumes); - this->set_nozzle_group_result(std::make_shared(grouping_result)); - // Orca: the sequential write-back stays gated to auto modes. In manual modes the - // config maps already carry the user's assignment (the per-object ToolOrdering below - // consumes them directly), so a write-back would only re-store the pre-slice values; - // keeping the gate avoids churning the config on every sequential manual slice. - if (map_mode < FilamentMapMode::fmmManual) { - auto derived_maps = grouping_result.get_extruder_map(false); - if (!derived_maps.empty()) { - filament_maps = derived_maps; - // Write the maps back: used filaments adopt the engine's extruder/nozzle - // choice, unused ones keep their config assignment. - // Orca: the config maps are the merge base; fall back to a synthesized base - // when no producer sized them to the filament count (CLI runs until the - // per-filament synthesis lands there), where indexing per filament would - // run out of bounds. - std::vector base_filament_map = m_config.filament_map.values; - if (base_filament_map.size() != derived_maps.size()) - base_filament_map.assign(derived_maps.size(), 1); - std::vector base_volume_map = m_config.filament_volume_map.values; - if (base_volume_map.size() != derived_maps.size()) - base_volume_map.assign(derived_maps.size(), (int)nvtStandard); - update_filament_maps_to_config(FilamentGroupUtils::update_used_filament_values(base_filament_map, derived_maps, used_filaments), - FilamentGroupUtils::update_used_filament_values(base_volume_map, grouping_result.get_volume_map(), used_filaments), - grouping_result.get_nozzle_map()); + // Selector (per-layer regroup) prints skip the static grouping: their print-wide result + // is stitched from the per-object plans after the ordering loop below. + const bool dynamic_reorder = this->is_dynamic_group_reorder(); + if (!dynamic_reorder) { + std::vectorfilament_maps = this->get_filament_maps(); + auto map_mode = get_filament_map_mode(); + // Grouping returns a nozzle-aware result; the 1-based extruder map for the by-object + // path is derived from it. It is computed in every static map mode (in manual modes it + // mirrors the user's assignment) and published print-wide: GCode's per-nozzle + // placeholder and config-index lookups read it via get_layered_nozzle_group_result(), + // and without it sequential exports on multi-nozzle printers see an empty nozzle table + // (e.g. nozzle_diameter_at_nozzle_id[]) and custom g-code fails to resolve. + auto grouping_result = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables, filament_unprintable_volumes); + this->set_nozzle_group_result(std::make_shared(grouping_result)); + // Orca: the sequential write-back stays gated to auto modes. In manual modes the + // config maps already carry the user's assignment (the per-object ToolOrdering below + // consumes them directly), so a write-back would only re-store the pre-slice values; + // keeping the gate avoids churning the config on every sequential manual slice. + if (map_mode < FilamentMapMode::fmmManual) { + auto derived_maps = grouping_result.get_extruder_map(false); + if (!derived_maps.empty()) { + filament_maps = derived_maps; + // Write the maps back: used filaments adopt the engine's extruder/nozzle + // choice, unused ones keep their config assignment. + // Orca: the config maps are the merge base; fall back to a synthesized base + // when no producer sized them to the filament count (CLI runs until the + // per-filament synthesis lands there), where indexing per filament would + // run out of bounds. + std::vector base_filament_map = m_config.filament_map.values; + if (base_filament_map.size() != derived_maps.size()) + base_filament_map.assign(derived_maps.size(), 1); + std::vector base_volume_map = m_config.filament_volume_map.values; + if (base_volume_map.size() != derived_maps.size()) + base_volume_map.assign(derived_maps.size(), (int)nvtStandard); + update_filament_maps_to_config(FilamentGroupUtils::update_used_filament_values(base_filament_map, derived_maps, used_filaments), + FilamentGroupUtils::update_used_filament_values(base_volume_map, grouping_result.get_volume_map(), used_filaments), + grouping_result.get_nozzle_map()); + } } + // check map valid both in auto and mannual mode + std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; }); } - // check map valid both in auto and mannual mode - std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; }); // print_object_instances_ordering = sort_object_instances_by_max_z(print); + const PrintObject *prev_planned_object = nullptr; + unsigned int seq_last_extruder = (unsigned int)-1; + MultiNozzleUtils::NozzleStatusRecorder nozzle_status; + std::vector> nozzle_map_per_layer; + std::vector> stitched_layer_filaments; print_object_instance_sequential_active = print_object_instances_ordering.begin(); 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); + const PrintObject *print_object = (*print_object_instance_sequential_active)->print_object; + if (dynamic_reorder) { + if (print_object != prev_planned_object) { + // Plan each unique object once, threading the physical nozzle occupancy and + // the previous object's last filament into the next plan; repeated instances + // of an object reuse the plan, mirroring the export loop's reuse. + ToolOrdering ordering(*print_object, seq_last_extruder); + ordering.set_nozzle_status(nozzle_status); + ordering.sort_and_build_data(*print_object, seq_last_extruder); + nozzle_status = ordering.get_nozzle_status(); + if (ordering.last_extruder() != static_cast(-1)) + seq_last_extruder = ordering.last_extruder(); + const auto &object_maps = ordering.get_layered_nozzle_group_result().get_layer_filament_nozzle_maps(); + nozzle_map_per_layer.insert(nozzle_map_per_layer.end(), object_maps.begin(), object_maps.end()); + // Orca: the stitch input comes from the same orderings that produced the + // per-layer maps — the collection loop above is per-instance and seeded -1, + // so its layers are misaligned with these plans. layer_tools() of a sorted + // ordering already carries the planned per-layer filament order. + for (const auto &layer_tool : ordering.layer_tools()) + stitched_layer_filaments.emplace_back(layer_tool.extruders); + m_sequential_dynamic_orderings[print_object] = std::move(ordering); + prev_planned_object = print_object; + } + tool_ordering = m_sequential_dynamic_orderings.at(print_object); + } else { + tool_ordering = ToolOrdering(*print_object, initial_extruder_id); + tool_ordering.sort_and_build_data(*print_object, initial_extruder_id); + } if ((initial_extruder_id = tool_ordering.first_extruder()) != static_cast(-1)) { append(printExtruders, tool_ordering.tools_for_layer(layers_to_print.front().first).extruders); } } + if (dynamic_reorder && m_objects.size() > 1) { + // Stitch the per-object plans into one print-wide selector result. A single-object + // sequential print publishes (and writes back) from its own ordering instead: the + // per-object publish gate treats one object as not sequential. + auto stitched = ToolOrdering::build_sequential_group_result(this, std::move(nozzle_map_per_layer), stitched_layer_filaments, + stitched_layer_filaments, used_filaments, physical_unprintables, + geometric_unprintables, filament_unprintable_volumes); + this->set_nozzle_group_result(std::make_shared(stitched)); + // Orca: the dynamic (per-layer) result carries no single volume/nozzle map, so only + // the extruder map is written back (matching the by-layer selector branch); the full + // per-nozzle config write-back is a follow-up behind the dev flag. + std::vector derived_maps = stitched.get_extruder_map(false); // 1-based + if (!derived_maps.empty()) + update_filament_maps_to_config(derived_maps); + } } else { tool_ordering = this->tool_ordering(); @@ -3475,12 +3529,13 @@ std::shared_ptr Print::get_layered_n } // Dynamic (per-layer selector) regroup predicate. -// Orca: enable_filament_dynamic_map is a develop-only config key registered in the ConfigDef but NOT -// a static PrintConfig member, so it is read from the applied full config; it is absent for every -// shipping printer/profile -> nullptr -> false, which keeps the static grouping path (identical -// output) the only one the current fleet takes. There is no mixed-colour-filament guard (mixed-colour -// filaments are not supported). The remaining gates (auto-for-flush mode, multi-extruder machine) -// read the static PrintConfig members. +// Orca: enable_filament_dynamic_map is a project flag registered in the ConfigDef but NOT a static +// PrintConfig member, so it is read from the applied full config. No profile sets it; it is turned +// on per project by the "smart filament assign" checkbox (shown when a filament track switch is +// ready), so absent-key -> nullptr -> false keeps the static grouping path (identical output) for +// everything else. There is no mixed-colour-filament guard (mixed-colour filaments are not +// supported). The remaining gates (auto-for-flush mode, multi-extruder machine) read the static +// PrintConfig members. bool Print::is_dynamic_group_reorder() const { const auto *opt = m_full_print_config.option("enable_filament_dynamic_map"); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index a64c2bc7f5..00460dbcd8 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -1028,13 +1028,20 @@ public: std::shared_ptr get_nozzle_group_result() const { return m_nozzle_group_result; } std::shared_ptr get_layered_nozzle_group_result() const; - // True only when the printer opts into the per-layer filament selector + // True only when the project opts into the per-layer filament selector // (enable_filament_dynamic_map) in auto-for-flush mode on a multi-extruder machine. Gates the - // dynamic (per-layer) regroup branch in ToolOrdering::reorder_extruders_for_minimum_flush_volume. - // Closed for every current profile, so the static grouping path (byte-identical output) is the - // only one taken by the shipping fleet. + // dynamic (per-layer) regroup branch in ToolOrdering::reorder_extruders_for_minimum_flush_volume, + // the sequential (by-object) plan stitching in Print::process, and GCode's use of the cached + // sequential plans. No profile sets the flag, so the static grouping path (byte-identical + // output) is the only one taken unless the user enables the selector. bool is_dynamic_group_reorder() const; + // Per-object tool orderings planned by the sequential (by-object) selector regroup with + // cross-object nozzle-status threading. GCode export must consume these exact plans: a fresh + // per-object construction would re-plan from a different seed and diverge from the published + // stitched result. Empty on the static path. + const std::map& sequential_dynamic_orderings() const { return m_sequential_dynamic_orderings; } + const std::vector>& get_extruder_filament_info() const { return m_extruder_filament_info; } void set_extruder_filament_info(const std::vector>& filament_info) { m_extruder_filament_info = filament_info; } @@ -1271,6 +1278,10 @@ private: // Logical (extruder, nozzle) grouping result, set by ToolOrdering during reorder. std::shared_ptr m_nozzle_group_result; + // Sequential (by-object) selector plans, keyed by object; see sequential_dynamic_orderings(). + // Rebuilt (or cleared) on every process(). + std::map m_sequential_dynamic_orderings; + // Used to cache filament parameter information FilamentIndexMap m_filament_index_map; // Used to cache printer and process parameter information diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index dcc2ae9521..2caab39c7c 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -13,6 +13,8 @@ #include #include +#include + // H2C/A2L multi-nozzle filament grouping core. // // These tests pin the behaviour of the grouping result type @@ -496,3 +498,167 @@ TEST_CASE("Re-applying an unchanged config after slicing keeps the result valid" REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED); REQUIRE(print.is_step_done(psSlicingFinished)); } + +TEST_CASE("normalize_nozzle_map_per_layer makes per-filament assignments gap-free", "[MultiNozzle][H2C][Dynamic]") +{ + SECTION("gaps inherit the last used nozzle, entries on used layers stay untouched") { + // Filament 1 extrudes on layers 0 (nozzle 1) and 3 (nozzle 2); the planner leaves stale + // entries on the layers in between. + std::vector> maps = { + {0, 1}, + {0, -1}, // filament 1 idle + {0, -1}, // filament 1 idle + {0, 2}, + }; + std::vector> filaments = {{0, 1}, {0}, {0}, {0, 1}}; + + normalize_nozzle_map_per_layer(maps, filaments); + + REQUIRE(maps[0] == std::vector({0, 1})); + REQUIRE(maps[1] == std::vector({0, 1})); // carried forward + REQUIRE(maps[2] == std::vector({0, 1})); // carried forward + REQUIRE(maps[3] == std::vector({0, 2})); // used layer untouched + } + + SECTION("layers before a filament's first use inherit its first nozzle") { + std::vector> maps = { + {0, -1}, + {0, -1}, + {0, 3}, // filament 1 first extrudes here + }; + std::vector> filaments = {{0}, {0}, {0, 1}}; + + normalize_nozzle_map_per_layer(maps, filaments); + + REQUIRE(maps[0] == std::vector({0, 3})); // back-filled + REQUIRE(maps[1] == std::vector({0, 3})); // back-filled + REQUIRE(maps[2] == std::vector({0, 3})); + } + + SECTION("empty and ragged inputs are safe no-ops") { + std::vector> empty_maps; + std::vector> no_filaments; + REQUIRE_NOTHROW(normalize_nozzle_map_per_layer(empty_maps, no_filaments)); + REQUIRE(empty_maps.empty()); + + // Rows of different widths and a filament list shorter than the map list. + std::vector> ragged = {{0}, {0, 1, 2}}; + std::vector> short_filaments = {{0}}; + REQUIRE_NOTHROW(normalize_nozzle_map_per_layer(ragged, short_filaments)); + REQUIRE(ragged[0] == std::vector({0})); + } + + SECTION("a single layer is left unchanged") { + std::vector> maps = {{2, 1, 0}}; + std::vector> filaments = {{0, 1, 2}}; + normalize_nozzle_map_per_layer(maps, filaments); + REQUIRE(maps[0] == std::vector({2, 1, 0})); + } +} + +TEST_CASE("Stitched sequential blocks resolve per-layer after normalization", "[MultiNozzle][H2C][Dynamic]") +{ + // Shape of the sequential (by-object) stitch: two per-object plan blocks concatenated on one + // global layer axis, where the second object's plan moves filament 1 to another physical + // nozzle. After normalization the 4-arg create() must detect the migration (selector result) + // and resolve stable ids inside each object's layer range. + std::vector nozzle_list; + for (int g = 0; g < 3; ++g) { + NozzleInfo n; + n.diameter = "0.4"; + n.volume_type = nvtStandard; + n.extruder_id = (g == 0) ? 0 : 1; + n.group_id = g; + nozzle_list.push_back(n); + } + + // Object A (layers 0-1): filament 1 on nozzle 1, filament 0 idle until layer 1. + // Object B (layers 2-3): filament 1 moved to nozzle 2. + std::vector> stitched_maps = { + {-1, 1}, + {0, 1}, + {0, 2}, + {0, 2}, + }; + std::vector> stitched_filaments = {{1}, {0, 1}, {0, 1}, {0, 1}}; + std::vector used_filaments = {0, 1}; + + normalize_nozzle_map_per_layer(stitched_maps, stitched_filaments); + REQUIRE(stitched_maps[0] == std::vector({0, 1})); // filament 0 back-filled to its first nozzle + + auto group_opt = LayeredNozzleGroupResult::create(stitched_maps, nozzle_list, used_filaments, stitched_filaments); + REQUIRE(group_opt.has_value()); + auto &group = *group_opt; + + // A filament on two physical nozzles across the objects => selector result. + REQUIRE(group.is_support_dynamic_nozzle_map()); + REQUIRE(group.get_nozzle_id(1, 0) == 1); + REQUIRE(group.get_nozzle_id(1, 1) == 1); + REQUIRE(group.get_nozzle_id(1, 2) == 2); // second object's range + REQUIRE(group.get_nozzle_id(1, 3) == 2); + // The default (out-of-range) map is the first layer's normalized row. + REQUIRE(group.get_nozzle_id(0, 999) == 0); + REQUIRE(group.get_nozzle_id(1, 999) == 1); +} + +TEST_CASE("Sequential selector prints publish a stitched result and cache the plans", "[Print][H2C][Dynamic]") +{ + // By-object + smart filament assign: the by-object branch of Print::process must plan each + // object with nozzle-status threading, cache the plans for the g-code export, stitch them + // into the published print-wide result, and write the derived extruder map back once + // (per-object orderings must not churn the config). + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.option("nozzle_diameter", true)->values = {0.4, 0.4}; + config.option("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"}; + config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; + config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard}; + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow"}; + config.option("print_extruder_id", true)->values = {1, 1, 2, 2}; + config.option("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow"}; + config.option("filament_diameter", true)->values = {1.75, 1.75}; + config.option("filament_colour", true)->values = {"#FF0000", "#00FF00"}; + config.option("filament_map", true)->values = {1, 2}; + config.option("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard}; + config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(true)); + config.option>("filament_map_mode", true)->value = FilamentMapMode::fmmAutoForFlush; + config.option>("print_sequence", true)->value = PrintSequence::ByObject; + // Export validates flush_volumes_matrix as filaments^2 values per head. + config.option("flush_volumes_matrix", true)->values = std::vector(8, 140.); + config.option("flush_multiplier", true)->values = {1., 1.}; + + Model model; + ModelObject *object_a = model.add_object("cube_a", "", make_cube(20, 20, 20)); + ModelInstance *instance_a = object_a->add_instance(); + instance_a->set_offset(Vec3d(70., 100., 0.)); + ModelObject *object_b = model.add_object("cube_b", "", make_cube(20, 20, 20)); + object_b->config.set_key_value("extruder", new ConfigOptionInt(2)); + ModelInstance *instance_b = object_b->add_instance(); + instance_b->set_offset(Vec3d(150., 100., 0.)); + // The sequential instance ordering keys on arrange_order, which validate() assigns before + // process() in the real pipeline (instances tying at 0 get dropped from the ordering); + // initialize it here since the test drives process() directly. + instance_a->arrange_order = 1; + instance_b->arrange_order = 2; + + Print print; + print.apply(model, config); + REQUIRE(print.objects().size() == 2); + print.process(); + REQUIRE(print.is_step_done(psSlicingFinished)); + + auto result = print.get_layered_nozzle_group_result(); + REQUIRE(result != nullptr); + // One cached plan per unique object, and a stitched layer axis spanning both objects. + REQUIRE(print.sequential_dynamic_orderings().size() == 2); + REQUIRE(result->get_layer_count() > 0); + // The write-back mirrors the stitched result's extruder map. + REQUIRE(print.config().filament_map.values == result->get_extruder_map(false)); + + // Export must consume the cached plans and produce g-code without throwing. + boost::filesystem::path gcode_path = boost::filesystem::temp_directory_path() / "orca_seq_dynamic_publish_test.gcode"; + REQUIRE_NOTHROW(print.export_gcode(gcode_path.string(), nullptr, nullptr)); + REQUIRE(boost::filesystem::exists(gcode_path)); + boost::filesystem::remove(gcode_path); +}