Toolchange Cyclic Order (#14868)

* Toolchange Cyclic Order

* Apply cyclic order to first layer

* Unit test

* Copilot fixes

---------

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
This commit is contained in:
Ian Bassi
2026-09-16 12:19:03 -03:00
committed by GitHub
co-authored by Rodrigo Faselli
parent a610d2d899
commit 72774e5398
9 changed files with 143 additions and 5 deletions
+60 -5
View File
@@ -2735,6 +2735,28 @@ void ToolOrdering::enforce_mixed_component_order()
}
}
// Declared in ToolOrdering.hpp (exposed for unit testing).
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders)
{
std::vector<unsigned int> order;
for (const std::string& token : split_string(str, ',')) {
try {
size_t pos = 0;
int filament = std::stoi(token, &pos); // stoi skips leading whitespace by itself
// stoi stops at the first non-digit, so "2x" would parse as 2. Require the whole token to be
// consumed (bar trailing whitespace) to drop it like any other garbage.
if (token.find_first_not_of(" \t\r\n", pos) != std::string::npos)
continue;
if (filament >= 1 && (unsigned int)filament <= number_of_extruders
&& std::find(order.begin(), order.end(), (unsigned int)(filament - 1)) == order.end())
order.emplace_back((unsigned int)(filament - 1));
} catch (const std::exception&) {
// Not a number, ignore it.
}
}
return order;
}
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{
const PrintConfig* print_config = m_print_config_ptr;
@@ -2832,11 +2854,41 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
const bool use_cyclic_ordering =
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic);
// By default the first layer keeps its adhesion-optimized order (and any custom first layer
// sequence); the cyclic sequence is only forced onto it when the user opts in.
const bool cyclic_first_layer = use_cyclic_ordering && print_config->toolchange_cyclic_first_layer.value;
// Optional user defined cyclic sequence, given as 1-based filament numbers ("3,2,1,4"). Filaments
// missing from it keep their ascending order after the listed ones, so a partial or bogus entry
// still yields the default cyclic order.
const std::vector<unsigned int> cyclic_order =
use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders)
: std::vector<unsigned int>();
// Reorder a layer's filaments (0-based) for cyclic ordering: ascending by default, or following the
// user defined sequence when one was given. Filaments absent from the sequence keep ascending order
// after the listed ones.
auto apply_cyclic_order = [&cyclic_order](std::vector<unsigned int>& filaments) {
std::sort(filaments.begin(), filaments.end());
if (!cyclic_order.empty())
std::stable_sort(filaments.begin(), filaments.end(), [&cyclic_order](unsigned int lhs, unsigned int rhs) {
auto rank = [&cyclic_order](unsigned int filament) {
return size_t(std::find(cyclic_order.begin(), cyclic_order.end(), filament) - cyclic_order.begin());
};
return rank(lhs) < rank(rhs);
});
};
// other_layers_seq: the layer_idx and extruder_idx are base on 1
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector<int>& out_seq) -> bool {
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering, cyclic_first_layer, &apply_cyclic_order](int layer_idx, std::vector<int>& out_seq) -> bool {
if (!reorder_first_layer && layer_idx == 0) {
out_seq.resize(first_layer_filaments.size());
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; });
// The first layer tool order is already decided (adhesion-optimized, plus any custom first
// layer sequence). Only override it with the cyclic sequence when the user opted in.
std::vector<unsigned int> ordered = first_layer_filaments;
if (cyclic_first_layer)
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) {return int(item) + 1; });
return true;
}
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) {
@@ -2847,9 +2899,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
}
}
if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) {
// Skip the first layer here (layer_idx == 0 only reaches this point on the reorder_first_layer
// path) unless the user asked for cyclic order on it, so it keeps the default flush ordering.
if (use_cyclic_ordering && layer_idx >= 0 && (layer_idx != 0 || cyclic_first_layer)
&& size_t(layer_idx) < layer_filaments.size()) {
std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)];
std::sort(ordered.begin(), ordered.end());
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
return true;