diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index e9be0171e4..64ea4d52b3 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -2735,6 +2735,28 @@ void ToolOrdering::enforce_mixed_component_order() } } +// Declared in ToolOrdering.hpp (exposed for unit testing). +std::vector parse_cyclic_order(const std::string& str, unsigned int number_of_extruders) +{ + std::vector 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 cyclic_order = + use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders) + : std::vector(); + + // 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& 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& 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& 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 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 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; diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 4dc08c0e8b..1b8ce190d7 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -417,6 +417,11 @@ private: int most_used_extruder; }; +// Parse the user defined cyclic toolchange sequence ("3,2 , 1 , 4") into 0-based filament indices. +// Out-of-range entries, duplicates and non-numeric tokens are dropped, so a partially valid string +// still orders the filaments it does name. Exposed for unit testing. +std::vector parse_cyclic_order(const std::string& str, unsigned int number_of_extruders); + } // namespace SLic3r #endif /* slic3r_ToolOrdering_hpp_ */ diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 2358680fbc..deae560d7c 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1320,6 +1320,8 @@ static std::vector s_Preset_print_options{ "wipe_tower_extra_flow", "single_extruder_multi_material_priming", "toolchange_ordering", + "toolchange_cyclic_order", + "toolchange_cyclic_first_layer", "wipe_tower_rotation_angle", "tree_support_branch_distance_organic", "tree_support_branch_diameter_organic", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 9c4edabfdb..13fe7758ff 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -360,6 +360,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "other_layers_print_sequence" || opt_key == "other_layers_print_sequence_nums" || opt_key == "toolchange_ordering" + || opt_key == "toolchange_cyclic_order" + || opt_key == "toolchange_cyclic_first_layer" || opt_key == "extruder_ams_count" || opt_key == "extruder_nozzle_stats" || opt_key == "filament_map_mode" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 77aaeb694a..65f43b3ee5 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6700,6 +6700,34 @@ void PrintConfigDef::init_fff_params() def->enum_labels.emplace_back(L("Cyclic")); def->set_default_value(new ConfigOptionEnum(ToolChangeOrderingType::Default)); + def = this->add("toolchange_cyclic_order", coString); + def->label = L("Cyclic order"); + def->category = L("Advanced"); + def->tooltip = L( + "Custom filament sequence used by the cyclic toolchange ordering, as filament numbers separated by commas (e.g. \"3,2,1,4\").\n" + "Each layer prints its filaments following this sequence; filaments not listed are printed last, in ascending order.\n" + "Leave empty to cycle through the filaments in ascending order." + ); + def->mode = comExpert; + def->set_default_value(new ConfigOptionString("")); + + def = this->add("toolchange_cyclic_first_layer", coBool); + def->label = L("Apply cyclic order to first layer"); + def->category = L("Advanced"); + def->tooltip = L( + "Applies the cyclic toolchange order to the first layer as well.\n" + "By default this is disabled, because the first layer is instead ordered for the best bed " + "adhesion: filaments that print small, fragile first-layer features are printed last, so the " + "following tool changes and travel moves are less likely to knock those weakly anchored parts " + "loose. This first-layer order also honors a custom first layer filament sequence when one is set. " + "The cyclic order's benefit (extra tool changes give each layer more time to cool) does not apply " + "to the first layer, which is printed slowly and hot for adhesion.\n" + "Enable this only if you need the exact same tool sequence on every layer, including the first, at " + "the cost of that adhesion optimization." + ); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("slice_closing_radius", coFloat); def->label = L("Slice gap closing radius"); def->category = L("Quality"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 6beaeed104..3373fbece7 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1627,6 +1627,8 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, manual_filament_change)) ((ConfigOptionBool, single_extruder_multi_material_priming)) ((ConfigOptionEnum, toolchange_ordering)) + ((ConfigOptionString, toolchange_cyclic_order)) + ((ConfigOptionBool, toolchange_cyclic_first_layer)) ((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionString, change_filament_gcode)) ((ConfigOptionString, change_extrusion_role_gcode)) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index ba91ffb7c2..f0f0adfa38 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1055,6 +1055,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2); + bool use_cyclic_ordering = config->opt_enum("toolchange_ordering") == ToolChangeOrderingType::Cyclic; + toggle_line("toolchange_cyclic_order", use_cyclic_ordering); + toggle_line("toolchange_cyclic_first_layer", use_cyclic_ordering); + toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM)); for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"}) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c0f78ee9c5..6279941e13 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3026,6 +3026,8 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Advanced"), L"advanced"); optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam"); optgroup->append_single_option_line("toolchange_ordering", "multimaterial_settings_advanced#toolchange-ordering"); + optgroup->append_single_option_line("toolchange_cyclic_order", "multimaterial_settings_advanced#toolchange-order"); + optgroup->append_single_option_line("toolchange_cyclic_first_layer", "multimaterial_settings_advanced#toolchange-order"); optgroup->append_single_option_line("interface_shells", "multimaterial_settings_advanced#interface-shells"); optgroup->append_single_option_line("mmu_segmented_region_max_width", "multimaterial_settings_advanced#maximum-width-of-segmented-region"); optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region"); diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index d01ccf5856..d9a5d70406 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -1025,3 +1025,41 @@ TEST_CASE("Selector slicing keeps the result valid across re-apply", "[Print][H2 REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED); REQUIRE(print.is_step_done(psSlicingFinished)); } + +TEST_CASE("parse_cyclic_order parses user cyclic toolchange sequences", "[ToolOrdering][Cyclic]") +{ + // Filament numbers are 1-based in the UI; the parser returns 0-based indices. + SECTION("well-formed sequence") { + REQUIRE(parse_cyclic_order("3,2,1,4", 4) == std::vector({2, 1, 0, 3})); + } + + SECTION("surrounding whitespace is tolerated") { + REQUIRE(parse_cyclic_order(" 3 , 2 ,1, 4 ", 4) == std::vector({2, 1, 0, 3})); + } + + SECTION("out-of-range and non-positive entries are dropped") { + // 0 is below the 1-based range, 5 is above it for a 4-filament setup, -1 is invalid. + REQUIRE(parse_cyclic_order("0,5,-1,2", 4) == std::vector({1})); + } + + SECTION("duplicates keep only the first occurrence") { + REQUIRE(parse_cyclic_order("2,2,1,2", 4) == std::vector({1, 0})); + } + + SECTION("garbage tokens are ignored") { + REQUIRE(parse_cyclic_order("3,abc,,2,x1", 4) == std::vector({2, 1})); + } + + SECTION("tokens that only start with a number are ignored") { + // "2x" must be dropped rather than parsed as filament 2. + REQUIRE(parse_cyclic_order("3,2x,1", 4) == std::vector({2, 0})); + } + + SECTION("empty string yields an empty order") { + REQUIRE(parse_cyclic_order("", 4).empty()); + } + + SECTION("a partial sequence only names the filaments it lists") { + REQUIRE(parse_cyclic_order("3,1", 4) == std::vector({2, 0})); + } +}