From daa75b3fa064f7858d0350f804d74daea271e6a5 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Wed, 10 Jun 2026 08:27:02 -0300 Subject: [PATCH] Revert "Forced pattern , separed to > 9 extruder" This reverts commit 42b506ad85ce90ba6a309c998d6152ba0b5fd94f. --- src/libslic3r/GCode.cpp | 71 ++--- src/libslic3r/GCode/ToolOrdering.cpp | 11 +- src/libslic3r/MixedFilament.cpp | 288 +++++++++++--------- src/libslic3r/MixedFilament.hpp | 13 +- src/libslic3r/PrintApply.cpp | 29 +- src/slic3r/GUI/MixedFilamentConfigPanel.cpp | 108 +++++--- src/slic3r/GUI/Plater.cpp | 2 +- 7 files changed, 303 insertions(+), 219 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8b20ff3e47..85fa96c82d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include #include @@ -3797,35 +3796,25 @@ size_t GCode::get_extruder_id(unsigned int filament_id) const // --------------------------------------------------------------------------- namespace { -// Decode a manual pattern string (e.g. "1,2,12,11") into a sequence of -// 1-based physical extruder IDs. +// Decode a manual pattern string (e.g. "121212") into a sequence of +// 1-based physical extruder IDs, honouring component_a / component_b aliases. static std::vector decode_manual_pattern_sequence_for_gcode( const MixedFilament& mf, size_t num_physical) { std::vector sequence; - if (mf.manual_pattern.empty() || num_physical == 0) + if (mf.manual_pattern.empty()) return sequence; - - const std::string normalized = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); - if (normalized.empty()) - return sequence; - - std::stringstream ss(normalized); - std::string token; - while (std::getline(ss, token, ',')) { - if (token.empty()) - continue; - try { - size_t consumed = 0; - const unsigned long value = std::stoul(token, &consumed); - if (consumed != token.size() || value == 0 || value > std::numeric_limits::max()) - continue; - const unsigned int extruder_id = static_cast(value); - if (extruder_id >= 1 && extruder_id <= num_physical) - sequence.emplace_back(extruder_id); - } catch (...) { - continue; - } + sequence.reserve(mf.manual_pattern.size()); + for (const char token : mf.manual_pattern) { + unsigned int extruder_id = 0; + if (token == '1') + extruder_id = mf.component_a; + else if (token == '2') + extruder_id = mf.component_b; + else if (token >= '3' && token <= '9') + extruder_id = unsigned(token - '0'); + if (extruder_id >= 1 && extruder_id <= num_physical) + sequence.emplace_back(extruder_id); } return sequence; } @@ -5834,16 +5823,32 @@ LayerResult GCode::process_layer( return inserted.first->second.empty() ? nullptr : &inserted.first->second; }; - // Legacy grouped-perimeter pattern split is disabled for numeric comma-delimited - // manual patterns; commas now delimit physical filament IDs. + // Lambda: if `entity_type == PERIMETERS` and the configured filament has a grouped + // (comma-containing) manual pattern, return that filament's virtual 1-based ID so + // that split_extrusion_collection_for_multi_perimeter_pattern() can be used. + // Returns 0 when no grouped pattern applies. auto grouped_manual_pattern_mixed_filament_id = [&](const GCode::ObjectByExtruder::Island::Region::Type entity_type, const ExtrusionEntityCollection& entities, const PrintRegion& region) -> unsigned int { - (void)entity_type; (void)entities; - (void)region; - return 0; + if (layer_tools.mixed_mgr == nullptr || layer_tools.num_physical == 0) + return 0; + if (entity_type != ObjectByExtruder::Island::Region::PERIMETERS) + return 0; + unsigned int filament_id_1based = + layer_tools.extruder_override != 0 ? + layer_tools.extruder_override : + unsigned(region.config().wall_filament.value); + if (!layer_tools.mixed_mgr->is_mixed(filament_id_1based, layer_tools.num_physical)) + return 0; + const MixedFilament* mixed_row = layer_tools.mixed_mgr->mixed_filament_from_id( + filament_id_1based, layer_tools.num_physical); + if (mixed_row == nullptr) + return 0; + const std::string normalized_pattern = + MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern); + return normalized_pattern.find(',') != std::string::npos ? filament_id_1based : 0; }; // Storage for dynamically-created split collections that must outlive the @@ -6112,7 +6117,7 @@ LayerResult GCode::process_layer( correct_extruder_id >= 0) { // 1. Uniform-segment pointillism (SameLayerPointillisme with a - // simple ratio or a numeric manual pattern sequence). + // simple ratio or a non-comma manual pattern). const unsigned int cfg_filament_id_1based = layer_tools.extruder_override != 0 ? layer_tools.extruder_override : @@ -6180,8 +6185,8 @@ LayerResult GCode::process_layer( ++pointillism_path_split_fallbacks; } - // 2. Legacy grouped per-perimeter-index pattern path. - // Kept for compatibility plumbing; currently disabled. + // 2. Grouped per-perimeter-index pattern (comma-separated manual + // pattern, e.g. "12,21"). Uses resolve_perimeter() via inset_idx. if (entity_type == ObjectByExtruder::Island::Region::PERIMETERS) { const unsigned int grouped_id = grouped_manual_pattern_mixed_filament_id( diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 82b0c04d7e..a65892525f 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -81,10 +81,13 @@ bool has_grouped_manual_pattern(const MixedFilamentManager *mixed_mgr, size_t num_physical, unsigned int filament_id_1based) { - (void)mixed_mgr; - (void)num_physical; - (void)filament_id_1based; - return false; + if (!(mixed_mgr && mixed_mgr->is_mixed(filament_id_1based, num_physical))) + return false; + const MixedFilament *mixed_row = mixed_mgr->mixed_filament_from_id(filament_id_1based, num_physical); + if (mixed_row == nullptr) + return false; + const std::string normalized = MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern); + return normalized.find(',') != std::string::npos; } void append_unique_preserve_order(std::vector &dst, unsigned int value) diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index 48b3bb0150..0bba9a4d7e 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -605,108 +604,69 @@ static bool decode_pattern_step(char c, char &out) } } -static std::string trim_manual_pattern_token(const std::string &token) +static std::vector split_manual_pattern_groups(const std::string &pattern) { - size_t lo = 0; - size_t hi = token.size(); - while (lo < hi && std::isspace(static_cast(token[lo]))) - ++lo; - while (hi > lo && std::isspace(static_cast(token[hi - 1]))) - --hi; - return token.substr(lo, hi - lo); -} - -static bool parse_manual_pattern_id_token(const std::string &token, unsigned int &out) -{ - const std::string trimmed = trim_manual_pattern_token(token); - if (trimmed.empty()) - return false; - - for (const char c : trimmed) - if (!std::isdigit(static_cast(c))) - return false; - - try { - size_t consumed = 0; - const unsigned long value = std::stoul(trimmed, &consumed); - if (consumed != trimmed.size() || value == 0 || value > std::numeric_limits::max()) - return false; - out = static_cast(value); - return true; - } catch (...) { - return false; - } -} - -static bool parse_manual_pattern_numeric_ids(const std::string &pattern, std::vector &out) -{ - out.clear(); + std::vector groups; if (pattern.empty()) - return false; + return groups; - std::stringstream ss(pattern); - std::string token; - while (std::getline(ss, token, ',')) { - unsigned int id = 0; - if (!parse_manual_pattern_id_token(token, id)) - return false; - out.emplace_back(id); - } - return !out.empty(); -} - -static std::string encode_manual_pattern_ids(const std::vector &ids) -{ - if (ids.empty()) - return {}; - - std::ostringstream out; - bool first = true; - for (const unsigned int id : ids) { - if (id == 0) + std::string current; + for (const char c : pattern) { + if (c == ',') { + if (!current.empty()) { + groups.emplace_back(std::move(current)); + current.clear(); + } continue; - if (!first) - out << ','; - first = false; - out << id; + } + current.push_back(c); } - return out.str(); + if (!current.empty()) + groups.emplace_back(std::move(current)); + return groups; } -static std::vector decode_manual_pattern_ids_for_physical(const std::string &pattern, size_t num_physical) +static std::string flatten_manual_pattern_groups(const std::string &pattern) { - std::vector parsed; - if (!parse_manual_pattern_numeric_ids(pattern, parsed) || num_physical == 0) - return {}; + std::string flattened; + flattened.reserve(pattern.size()); + for (const char c : pattern) + if (c != ',') + flattened.push_back(c); + return flattened; +} - std::vector out; - out.reserve(parsed.size()); - for (const unsigned int id : parsed) - if (id >= 1 && id <= num_physical) - out.emplace_back(id); - return out; +static unsigned int physical_filament_from_pattern_step(char token, const MixedFilament &mf, size_t num_physical) +{ + if (token == '1') + return mf.component_a; + if (token == '2') + return mf.component_b; + if (token >= '3' && token <= '9') { + const unsigned int direct = unsigned(token - '0'); + if (direct >= 1 && direct <= num_physical) + return direct; + } + return 0; } static int mix_percent_from_normalized_pattern(const std::string &pattern) { - std::vector ids; - if (!parse_manual_pattern_numeric_ids(pattern, ids) || ids.empty()) + const std::vector groups = split_manual_pattern_groups(pattern); + if (groups.empty()) return 50; - const unsigned int first = ids.front(); - unsigned int second = 0; - for (const unsigned int id : ids) { - if (id != first) { - second = id; - break; - } + // For grouped patterns, blend preview is the average of each perimeter + // group's own cadence. This keeps simple outer/inner patterns like + // "12,21" at 50/50 and "11111112,11121111" at 12.5%. + double blend_b = 0.0; + for (const std::string &group : groups) { + if (group.empty()) + continue; + const int count_b = int(std::count(group.begin(), group.end(), '2')); + blend_b += double(count_b) / double(group.size()); } - - if (second == 0) - return 0; - - const int count_second = int(std::count(ids.begin(), ids.end(), second)); - return clamp_int(int(std::lround(100.0 * double(count_second) / double(ids.size()))), 0, 100); + return clamp_int(int(std::lround(100.0 * blend_b / double(groups.size()))), 0, 100); } static std::string normalize_gradient_component_ids(const std::string &components) @@ -924,16 +884,25 @@ static std::vector build_weighted_gradient_sequence(const std::vec return sequence; } +static unsigned int decode_manual_pattern_preview_token(char token, unsigned int component_a, unsigned int component_b, size_t num_physical) +{ + unsigned int extruder_id = 0; + if (token == '1') + extruder_id = component_a; + else if (token == '2') + extruder_id = component_b; + else if (token >= '3' && token <= '9') + extruder_id = unsigned(token - '0'); + + return (extruder_id >= 1 && extruder_id <= num_physical) ? extruder_id : 0; +} + static std::vector build_grouped_manual_pattern_preview_sequence(const std::string &pattern, unsigned int component_a, unsigned int component_b, size_t num_physical, size_t wall_loops) { - (void)component_a; - (void)component_b; - (void)wall_loops; - std::vector sequence; if (num_physical == 0) return sequence; @@ -942,7 +911,49 @@ static std::vector build_grouped_manual_pattern_preview_sequence(c if (normalized.empty()) return sequence; - return decode_manual_pattern_ids_for_physical(normalized, num_physical); + const std::vector groups = split_manual_pattern_groups(normalized); + if (groups.empty()) + return sequence; + + if (groups.size() == 1) { + sequence.reserve(normalized.size()); + for (const char token : normalized) { + const unsigned int extruder_id = + decode_manual_pattern_preview_token(token, component_a, component_b, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + return sequence; + } + + constexpr size_t k_max_preview_cycle = 48; + size_t cycle = 1; + for (const std::string &group : groups) { + if (group.empty()) + continue; + cycle = std::lcm(cycle, group.size()); + if (cycle >= k_max_preview_cycle) { + cycle = k_max_preview_cycle; + break; + } + } + + const size_t preview_wall_loops = std::max(1, wall_loops == 0 ? groups.size() : wall_loops); + sequence.reserve(preview_wall_loops * cycle); + for (size_t layer_idx = 0; layer_idx < cycle; ++layer_idx) { + for (size_t wall_idx = 0; wall_idx < preview_wall_loops; ++wall_idx) { + const std::string &group = groups[std::min(wall_idx, groups.size() - 1)]; + if (group.empty()) + continue; + const char token = group[layer_idx % group.size()]; + const unsigned int extruder_id = + decode_manual_pattern_preview_token(token, component_a, component_b, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + } + + return sequence; } static std::pair effective_pair_preview_ratios(int percent_b) @@ -1471,8 +1482,10 @@ std::string compute_mixed_filament_display_color(const MixedFilament &entry, con std::string mixed_filament_standardized_name(const MixedFilament &entry, size_t num_physical) { const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); - if (!normalized_pattern.empty()) - return std::string("Pattern ") + normalized_pattern; + if (!normalized_pattern.empty()) { + const std::string flattened = flatten_manual_pattern_groups(normalized_pattern); + return std::string("Pattern ") + (flattened.empty() ? normalized_pattern : flattened); + } std::vector> parts; parts.reserve(4); @@ -1682,31 +1695,31 @@ void MixedFilamentManager::clear_custom_entries() std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pattern) { - if (pattern.empty()) - return std::string(); - - std::vector ids; - if (pattern.find(',') != std::string::npos) { - if (!parse_manual_pattern_numeric_ids(pattern, ids)) - return std::string(); - return encode_manual_pattern_ids(ids); - } - - // Backward compatibility for legacy compact patterns like "1212" and "A/B". + std::string normalized; + normalized.reserve(pattern.size()); + bool current_group_has_steps = false; for (char c : pattern) { char step = '\0'; if (decode_pattern_step(c, step)) { - ids.emplace_back(unsigned(step - '0')); + normalized.push_back(step); + current_group_has_steps = true; + continue; + } + if (c == ',') { + if (!current_group_has_steps) + return std::string(); + normalized.push_back(','); + current_group_has_steps = false; continue; } if (is_pattern_separator(c)) continue; + // Unknown token => invalid pattern. return std::string(); } - - if (ids.empty()) + if (!normalized.empty() && normalized.back() == ',') return std::string(); - return encode_manual_pattern_ids(ids); + return normalized; } int MixedFilamentManager::mix_percent_from_manual_pattern(const std::string &pattern) @@ -1973,14 +1986,16 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id, const MixedFilament &mf = m_mixed[size_t(mixed_idx)]; - // Manual pattern takes precedence when provided. - // Pattern stores comma-separated 1-based physical filament IDs. + // Manual pattern takes precedence when provided. Pattern uses repeating + // steps: '1' => component_a, '2' => component_b, '3'..'9' => direct + // physical filament IDs. if (!mf.manual_pattern.empty()) { - const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern); - const std::vector pattern_ids = decode_manual_pattern_ids_for_physical(normalized_pattern, num_physical); - if (!pattern_ids.empty()) { - const int pos = safe_mod(layer_index, int(pattern_ids.size())); - return pattern_ids[size_t(pos)]; + const std::string flattened_pattern = flatten_manual_pattern_groups(mf.manual_pattern); + if (!flattened_pattern.empty()) { + const int pos = safe_mod(layer_index, int(flattened_pattern.size())); + const unsigned int resolved = physical_filament_from_pattern_step(flattened_pattern[size_t(pos)], mf, num_physical); + if (resolved >= 1 && resolved <= num_physical) + return resolved; } return mf.component_a; } @@ -2034,12 +2049,25 @@ unsigned int MixedFilamentManager::resolve_perimeter(unsigned int filament_id, float layer_height, bool force_height_weighted) const { - (void)perimeter_index; - const int mixed_idx = mixed_index_from_filament_id(filament_id, num_physical); if (mixed_idx < 0) return filament_id; + const MixedFilament &mf = m_mixed[size_t(mixed_idx)]; + if (!mf.manual_pattern.empty()) { + const std::vector pattern_groups = split_manual_pattern_groups(mf.manual_pattern); + if (!pattern_groups.empty()) { + const size_t group_idx = size_t(std::max(0, perimeter_index)); + const std::string &group = pattern_groups[std::min(group_idx, pattern_groups.size() - 1)]; + if (!group.empty()) { + const int pos = safe_mod(layer_index, int(group.size())); + const unsigned int resolved = physical_filament_from_pattern_step(group[size_t(pos)], mf, num_physical); + if (resolved >= 1 && resolved <= num_physical) + return resolved; + } + } + } + return resolve(filament_id, num_physical, layer_index, layer_print_z, layer_height, force_height_weighted); } @@ -2060,6 +2088,10 @@ unsigned int MixedFilamentManager::effective_painted_region_filament_id(unsigned if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) return filament_id; + const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern); + if (normalized_pattern.find(',') != std::string::npos) + return filament_id; + const bool is_custom_mixed = mf.custom; if (!is_custom_mixed && (layer_height_a > 0.f || layer_height_b > 0.f)) { const float safe_base = std::max(0.01f, base_layer_height); @@ -2091,7 +2123,7 @@ float MixedFilamentManager::component_surface_offset(unsigned int filament_id, return 0.f; const std::string normalized_pattern = normalize_manual_pattern(mixed_row->manual_pattern); - if (!normalized_pattern.empty()) + if (normalized_pattern.find(',') != std::string::npos) return 0.f; const unsigned int resolved = resolve(filament_id, @@ -2125,11 +2157,19 @@ std::vector MixedFilamentManager::ordered_perimeter_extruders(unsi const MixedFilament &mf = m_mixed[size_t(mixed_idx)]; if (!mf.manual_pattern.empty()) { - const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern); - const std::vector pattern_ids = decode_manual_pattern_ids_for_physical(normalized_pattern, num_physical); - if (!pattern_ids.empty()) { - ordered.reserve(pattern_ids.size()); - for (const unsigned int resolved : pattern_ids) { + const std::vector pattern_groups = split_manual_pattern_groups(mf.manual_pattern); + if (!pattern_groups.empty()) { + ordered.reserve(pattern_groups.size()); + for (size_t group_idx = 0; group_idx < pattern_groups.size(); ++group_idx) { + const unsigned int resolved = resolve_perimeter(filament_id, + num_physical, + layer_index, + int(group_idx), + layer_print_z, + layer_height, + force_height_weighted); + if (resolved < 1 || resolved > num_physical) + continue; if (std::find(ordered.begin(), ordered.end(), resolved) == ordered.end()) ordered.emplace_back(resolved); } diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 5c4610de4f..cad52fa85d 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -40,9 +40,9 @@ struct MixedFilament // Blend percentage of component B in [0..100]. int mix_b_percent = 50; - // Optional manual pattern for this mixed filament. - // Canonical format: comma-separated 1-based physical filament IDs - // (for example "1,2,12,11"). + // Optional manual pattern for this mixed filament. Tokens: + // '1' => component_a, '2' => component_b, '3'..'9' => direct physical + // filament IDs (1-based). Example: "11112222" => AAAABBBB repeating. std::string manual_pattern; // Optional explicit gradient multi-color component list, encoded as @@ -145,7 +145,7 @@ std::pair mixed_filament_apparent_pair_percentages(const MixedFilament std::string compute_mixed_filament_display_color(const MixedFilament &entry, const MixedFilamentDisplayContext &context); // Build a standardized user-facing mixed filament name. -// - Pattern rows: "Pattern " (for example "Pattern 1,2,12,11"). +// - Pattern rows: "Pattern " (for example "Pattern 1212354"). // - Mix rows: ":% + :% ..." (for example "1:30% + 2:20% + 3:50%"). std::string mixed_filament_standardized_name(const MixedFilament &entry, size_t num_physical); @@ -197,9 +197,8 @@ public: std::string serialize_custom_entries(); void load_custom_entries(const std::string &serialized, const std::vector &filament_colours); - // Normalize a manual mixed-pattern string into comma-separated numeric IDs. - // Legacy compact patterns are still accepted for backward compatibility. - // Returns empty string if invalid. + // Normalize a manual mixed-pattern string into compact token form. + // Accepts separators and A/B aliases. Returns empty string if invalid. static std::string normalize_manual_pattern(const std::string &pattern); static int mix_percent_from_manual_pattern(const std::string &pattern); diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index f090bcb2eb..3f1f382e4f 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -4,8 +4,6 @@ #include #include -#include -#include namespace Slic3r { @@ -1142,23 +1140,16 @@ static void append_mixed_component_extruders(const MixedFilamentManager &mixed_m append_unique_painted_extruder(painting_extruders, unsigned(token - '0'), num_physical_extruders); } - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern); - if (!normalized_pattern.empty()) { - std::stringstream ss(normalized_pattern); - std::string token; - while (std::getline(ss, token, ',')) { - if (token.empty()) - continue; - try { - size_t consumed = 0; - const unsigned long value = std::stoul(token, &consumed); - if (consumed != token.size() || value == 0 || value > std::numeric_limits::max()) - continue; - append_unique_painted_extruder(painting_extruders, static_cast(value), num_physical_extruders); - } catch (...) { - continue; - } - } + for (char token : mixed_row->manual_pattern) { + unsigned int extruder_id = 0; + if (token == '1') + extruder_id = mixed_row->component_a; + else if (token == '2') + extruder_id = mixed_row->component_b; + else if (token >= '3' && token <= '9') + extruder_id = unsigned(token - '0'); + + append_unique_painted_extruder(painting_extruders, extruder_id, num_physical_extruders); } } diff --git a/src/slic3r/GUI/MixedFilamentConfigPanel.cpp b/src/slic3r/GUI/MixedFilamentConfigPanel.cpp index 1c44421b4a..ef427f7475 100644 --- a/src/slic3r/GUI/MixedFilamentConfigPanel.cpp +++ b/src/slic3r/GUI/MixedFilamentConfigPanel.cpp @@ -33,7 +33,6 @@ #include #include -#include #include #include #include @@ -51,21 +50,39 @@ namespace Slic3r { namespace GUI { namespace { // -- Plater.cpp:4849 -------------------------------------------------------- -static bool parse_manual_pattern_preview_id_token(const std::string &token, unsigned int &out) +static std::vector split_manual_pattern_preview_groups(const std::string &pattern) { - if (token.empty()) - return false; + std::vector groups; + if (pattern.empty()) + return groups; - try { - size_t consumed = 0; - const unsigned long value = std::stoul(token, &consumed); - if (consumed != token.size() || value == 0 || value > std::numeric_limits::max()) - return false; - out = static_cast(value); - return true; - } catch (...) { - return false; + std::string current; + for (const char c : pattern) { + if (c == ',') { + if (!current.empty()) { + groups.emplace_back(std::move(current)); + current.clear(); + } + continue; + } + current.push_back(c); } + if (!current.empty()) + groups.emplace_back(std::move(current)); + return groups; +} + +static unsigned int decode_manual_pattern_preview_token(char token, unsigned int component_a, unsigned int component_b, size_t num_physical) +{ + unsigned int extruder_id = 0; + if (token == '1') + extruder_id = component_a; + else if (token == '2') + extruder_id = component_b; + else if (token >= '3' && token <= '9') + extruder_id = unsigned(token - '0'); + + return (extruder_id >= 1 && extruder_id <= num_physical) ? extruder_id : 0; } static std::vector build_grouped_manual_pattern_preview_sequence(const std::string &pattern, @@ -74,10 +91,6 @@ static std::vector build_grouped_manual_pattern_preview_sequence(c size_t num_physical, size_t wall_loops) { - (void)component_a; - (void)component_b; - (void)wall_loops; - std::vector sequence; if (num_physical == 0) return sequence; @@ -86,14 +99,46 @@ static std::vector build_grouped_manual_pattern_preview_sequence(c if (normalized.empty()) return sequence; - std::stringstream ss(normalized); - std::string token; - while (std::getline(ss, token, ',')) { - unsigned int id = 0; - if (!parse_manual_pattern_preview_id_token(token, id)) + const std::vector groups = split_manual_pattern_preview_groups(normalized); + if (groups.empty()) + return sequence; + + if (groups.size() == 1) { + sequence.reserve(normalized.size()); + for (const char token : normalized) { + const unsigned int extruder_id = + decode_manual_pattern_preview_token(token, component_a, component_b, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + return sequence; + } + + constexpr size_t k_max_preview_cycle = 48; + size_t cycle = 1; + for (const std::string &group : groups) { + if (group.empty()) continue; - if (id >= 1 && id <= num_physical) - sequence.emplace_back(id); + cycle = std::lcm(cycle, group.size()); + if (cycle >= k_max_preview_cycle) { + cycle = k_max_preview_cycle; + break; + } + } + + const size_t preview_wall_loops = std::max(1, wall_loops == 0 ? groups.size() : wall_loops); + sequence.reserve(preview_wall_loops * cycle); + for (size_t layer_idx = 0; layer_idx < cycle; ++layer_idx) { + for (size_t wall_idx = 0; wall_idx < preview_wall_loops; ++wall_idx) { + const std::string &group = groups[std::min(wall_idx, groups.size() - 1)]; + if (group.empty()) + continue; + const char token = group[layer_idx % group.size()]; + const unsigned int extruder_id = + decode_manual_pattern_preview_token(token, component_a, component_b, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } } return sequence; @@ -1171,9 +1216,10 @@ void MixedFilamentConfigPanel::build_ui() pattern_row->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); m_pattern_ctrl = new wxTextCtrl(this, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER); - m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Enter physical filament IDs as comma-separated numbers. " - "Values greater than 9 are supported. " - "Example: 1,2,12,11.")); + m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Use 1/2 or A/B for component A/B, " + "and 3..9 for direct physical filament IDs. " + "Use commas to define deeper perimeter patterns, for example 12,21. " + "Example: 1/1/1/1/2/2/2/2, 12,21, or 1/2/3/4.")); pattern_row->Add(m_pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); root->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); @@ -1237,7 +1283,7 @@ void MixedFilamentConfigPanel::build_ui() const wxString bias_tooltip = _L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n" "The color chip shows which filament the current value affects.\n\n" - "Manual patterns and Local-Z dithering ignore it."); + "Grouped wall patterns and Local-Z dithering ignore it."); auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias")); surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); @@ -1379,7 +1425,7 @@ void MixedFilamentConfigPanel::build_ui() if (m_pattern_ctrl) { m_mf.distribution_mode = int(MixedFilament::Simple); std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(m_pattern_ctrl->GetValue())); - if (normalized.empty()) normalized = "1,2"; + if (normalized.empty()) normalized = "12"; if (into_u8(m_pattern_ctrl->GetValue()) != normalized) m_pattern_ctrl->ChangeValue(from_u8(normalized)); m_mf.manual_pattern = normalized; @@ -1639,8 +1685,8 @@ void MixedFilamentConfigPanel::build_ui() std::string pattern = into_u8(m_pattern_ctrl->GetValue()); if (!pattern.empty()) { const char last = pattern.back(); - if (last != ',') - pattern.push_back(','); + const bool has_sep = last == '/' || last == '-' || last == '_' || last == '|' || last == ':' || last == ';' || last == ',' || last == ' '; + if (!has_sep) pattern.push_back('/'); } pattern += std::to_string(filament_id); m_pattern_ctrl->ChangeValue(from_u8(pattern)); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index de5107e806..2b5276b7ca 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2471,7 +2471,7 @@ Sidebar::Sidebar(Plater *parent) mgr.add_custom_filament(1, 2, 50, colors); auto &mfs = mgr.mixed_filaments(); if (!mfs.empty()) { - mfs.back().manual_pattern = "1,2"; + mfs.back().manual_pattern = "12"; mfs.back().custom = true; } if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions"))