From 9f9aebf957cb2ad3b1eee37e6f1227a7acd62fab Mon Sep 17 00:00:00 2001 From: Rad Date: Thu, 12 Mar 2026 03:49:38 +0100 Subject: [PATCH] added support for multi perimeter patterns, issue 34 --- src/libslic3r/ExtrusionEntity.hpp | 18 ++- src/libslic3r/GCode.cpp | 109 ++++++++++++++ src/libslic3r/GCode/ToolOrdering.cpp | 126 +++++++++++++--- src/libslic3r/GCode/ToolOrdering.hpp | 1 + src/libslic3r/MixedFilament.cpp | 187 +++++++++++++++++++++--- src/libslic3r/MixedFilament.hpp | 14 ++ src/slic3r/GUI/Plater.cpp | 6 +- tests/libslic3r/test_mixed_filament.cpp | 69 +++++++++ 8 files changed, 482 insertions(+), 48 deletions(-) diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp index edd7839cc1..ed4712230e 100644 --- a/src/libslic3r/ExtrusionEntity.hpp +++ b/src/libslic3r/ExtrusionEntity.hpp @@ -170,7 +170,9 @@ public: , m_can_reverse(rhs.m_can_reverse) , m_role(rhs.m_role) , m_no_extrusion(rhs.m_no_extrusion) - {} + { + this->inset_idx = rhs.inset_idx; + } ExtrusionPath(ExtrusionPath &&rhs) : polyline(std::move(rhs.polyline)) , mm3_per_mm(rhs.mm3_per_mm) @@ -179,7 +181,9 @@ public: , m_can_reverse(rhs.m_can_reverse) , m_role(rhs.m_role) , m_no_extrusion(rhs.m_no_extrusion) - {} + { + this->inset_idx = rhs.inset_idx; + } ExtrusionPath(const Polyline &polyline, const ExtrusionPath &rhs) : polyline(polyline) , mm3_per_mm(rhs.mm3_per_mm) @@ -188,7 +192,9 @@ public: , m_can_reverse(rhs.m_can_reverse) , m_role(rhs.m_role) , m_no_extrusion(rhs.m_no_extrusion) - {} + { + this->inset_idx = rhs.inset_idx; + } ExtrusionPath(Polyline &&polyline, const ExtrusionPath &rhs) : polyline(std::move(polyline)) , mm3_per_mm(rhs.mm3_per_mm) @@ -197,7 +203,9 @@ public: , m_can_reverse(rhs.m_can_reverse) , m_role(rhs.m_role) , m_no_extrusion(rhs.m_no_extrusion) - {} + { + this->inset_idx = rhs.inset_idx; + } ExtrusionPath& operator=(const ExtrusionPath& rhs) { m_can_reverse = rhs.m_can_reverse; @@ -207,6 +215,7 @@ public: this->width = rhs.width; this->height = rhs.height; this->polyline = rhs.polyline; + this->inset_idx = rhs.inset_idx; return *this; } ExtrusionPath& operator=(ExtrusionPath&& rhs) { @@ -217,6 +226,7 @@ public: this->width = rhs.width; this->height = rhs.height; this->polyline = std::move(rhs.polyline); + this->inset_idx = rhs.inset_idx; return *this; } diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 95d3172eca..565e464166 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -4008,6 +4008,53 @@ static bool split_extrusion_collection_for_pointillism_paths( return out_stats.segment_count > 0; } +static bool split_extrusion_collection_for_multi_perimeter_pattern( + const ExtrusionEntityCollection& source, + const MixedFilamentManager& mixed_mgr, + unsigned int mixed_filament_id, + size_t num_physical, + int layer_index, + std::vector>& out_by_extruder, + size_t& out_bucket_count) +{ + out_by_extruder.clear(); + out_by_extruder.resize(num_physical); + out_bucket_count = 0; + + if (source.entities.empty() || num_physical == 0) + return false; + + size_t split_entities = 0; + ExtrusionEntityCollection flattened = source.flatten(false); + for (const ExtrusionEntity* entity : flattened.entities) { + if (entity == nullptr) + continue; + + int perimeter_index = entity->inset_idx; + if (perimeter_index < 0) + perimeter_index = entity->role() == erExternalPerimeter ? 0 : 1; + + const unsigned int extruder_id = mixed_mgr.resolve_perimeter( + mixed_filament_id, num_physical, layer_index, perimeter_index); + if (extruder_id == 0 || extruder_id > num_physical) + continue; + + std::unique_ptr& bucket = out_by_extruder[extruder_id - 1]; + if (!bucket) { + bucket = std::make_unique(); + bucket->no_sort = source.no_sort; + } + bucket->append(*entity); + ++split_entities; + } + + for (const std::unique_ptr& bucket : out_by_extruder) { + if (bucket && !bucket->entities.empty()) + ++out_bucket_count; + } + return split_entities > 0; +} + inline std::vector& object_islands_by_extruder( std::map>& by_extruder, unsigned int extruder_id, @@ -4750,6 +4797,27 @@ LayerResult GCode::process_layer(const Print& print, auto inserted = pointillism_sequence_cache.emplace(filament_id_1based, std::move(sequence)); return inserted.first->second.empty() ? nullptr : &inserted.first->second; }; + auto grouped_manual_pattern_mixed_filament_id = + [&layer_tools, &configured_filament_id_1based](const ExtrusionEntityCollection& entities, + const PrintRegion& region) -> unsigned int { + if (layer_tools.mixed_mgr == nullptr || layer_tools.num_physical == 0) + return 0; + + auto has_grouped_pattern = [&layer_tools](unsigned int filament_id_1based) -> bool { + if (!layer_tools.mixed_mgr->is_mixed(filament_id_1based, layer_tools.num_physical)) + return false; + const MixedFilament* mixed_row = layer_tools.mixed_mgr->mixed_filament_from_id(filament_id_1based, layer_tools.num_physical); + if (mixed_row == nullptr) + return false; + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern); + return normalized_pattern.find(',') != std::string::npos; + }; + + const unsigned int configured_filament_id = configured_filament_id_1based(entities, region); + if (has_grouped_pattern(configured_filament_id)) + return configured_filament_id; + return 0; + }; // Compensate perimeter clipping at mixed-mask boundaries to avoid cracks from exact centerline clipping. constexpr double LOCAL_Z_PERIMETER_MASK_EXPAND_MM = 0.10; // Keep base exclusion smaller than mixed-pass inclusion to guarantee a slight overlap @@ -5216,6 +5284,47 @@ LayerResult GCode::process_layer(const Print& print, // This extrusion is part of certain Region, which tells us which extruder should be used for it: int correct_extruder_id = layer_tools.extruder(*filtered_extrusions, region); + if (!is_anything_overridden && + entity_type == ObjectByExtruder::Island::Region::PERIMETERS && + layer_tools.mixed_mgr != nullptr && + layer_tools.num_physical > 0 && + correct_extruder_id >= 0) { + const unsigned int mixed_filament_id = + grouped_manual_pattern_mixed_filament_id(*filtered_extrusions, region); + if (mixed_filament_id != 0) { + std::vector> split_by_extruder; + size_t bucket_count = 0; + if (split_extrusion_collection_for_multi_perimeter_pattern(*filtered_extrusions, + *layer_tools.mixed_mgr, + mixed_filament_id, + layer_tools.num_physical, + layer_tools.layer_index, + split_by_extruder, + bucket_count) && + bucket_count >= 2) { + for (size_t extruder_idx = 0; extruder_idx < split_by_extruder.size(); ++extruder_idx) { + std::unique_ptr& split_collection = split_by_extruder[extruder_idx]; + if (!split_collection || split_collection->entities.empty()) + continue; + const ExtrusionEntityCollection* split_ptr = split_collection.get(); + local_z_clipped_collections.emplace_back(std::move(split_collection)); + std::vector& islands = + object_islands_by_extruder(by_extruder, unsigned(extruder_idx), layer_to_print_idx, layers.size(), n_slices + 1); + for (size_t i = 0; i <= n_slices; ++i) { + const bool last = i == n_slices; + const size_t island_idx = last ? n_slices : slices_test_order[i]; + if (last || point_inside_surface(island_idx, split_ptr->first_point())) { + if (islands[island_idx].by_region.empty()) + islands[island_idx].by_region.assign(print.num_print_regions(), ObjectByExtruder::Island::Region()); + islands[island_idx].by_region[region.print_region_id()].append(entity_type, split_ptr, nullptr); + break; + } + } + } + continue; + } + } + } // Let's recover vector of extruder overrides: const WipingExtrusions::ExtruderPerCopy* entity_overrides = nullptr; diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index ccb819a91e..cc6a78890c 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -60,6 +60,45 @@ unsigned int resolve_mixed_with_layer_heights(const MixedFilamentManager *mixed_ return mixed_mgr->resolve(filament_id_1based, num_physical, layer_index, layer_print_z, layer_height); } +bool has_grouped_manual_pattern(const MixedFilamentManager *mixed_mgr, + size_t num_physical, + unsigned int filament_id_1based) +{ + 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) +{ + if (std::find(dst.begin(), dst.end(), value) == dst.end()) + dst.emplace_back(value); +} + +unsigned int grouped_manual_pattern_mixed_filament_id_for_layer(const LayerTools& layer_tools, + unsigned int configured_filament_id_1based) +{ + if (layer_tools.mixed_mgr == nullptr || layer_tools.num_physical == 0) + return 0; + + if (has_grouped_manual_pattern(layer_tools.mixed_mgr, layer_tools.num_physical, configured_filament_id_1based)) + return configured_filament_id_1based; + return 0; +} + +void remove_duplicates_preserve_order(std::vector &values) +{ + std::vector ordered; + ordered.reserve(values.size()); + for (unsigned int value : values) + append_unique_preserve_order(ordered, value); + values = std::move(ordered); +} + } // namespace @@ -639,11 +678,34 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto } if (something_nonoverriddable){ - unsigned int wall_ext = (extruder_override == 0) ? region.config().wall_filament.value : extruder_override; - wall_ext = resolve_mixed(wall_ext, layerCount, float(layer->print_z), float(layer->height)); - layer_tools.extruders.emplace_back(wall_ext); - if (layerCount == 0) { - firstLayerExtruders.emplace_back(wall_ext); + const unsigned int configured_wall = (extruder_override == 0) ? region.config().wall_filament.value : extruder_override; + unsigned int wall_ext = resolve_mixed(configured_wall, layerCount, float(layer->print_z), float(layer->height)); + const unsigned int grouped_id = + grouped_manual_pattern_mixed_filament_id_for_layer(layer_tools, configured_wall); + if (grouped_id != 0) { + const std::vector ordered = + m_mixed_mgr->ordered_perimeter_extruders(grouped_id, + m_num_physical, + layerCount, + float(layer->print_z), + float(layer->height)); + if (ordered.size() >= 2) { + layer_tools.preserve_extruder_order = true; + for (unsigned int extruder_id : ordered) { + layer_tools.extruders.emplace_back(extruder_id); + if (layerCount == 0 && + std::find(firstLayerExtruders.begin(), firstLayerExtruders.end(), int(extruder_id)) == firstLayerExtruders.end()) + firstLayerExtruders.emplace_back(int(extruder_id)); + } + } else { + layer_tools.extruders.emplace_back(wall_ext); + if (layerCount == 0) + firstLayerExtruders.emplace_back(wall_ext); + } + } else { + layer_tools.extruders.emplace_back(wall_ext); + if (layerCount == 0) + firstLayerExtruders.emplace_back(wall_ext); } } @@ -696,8 +758,10 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto const_cast(object).object_first_layer_wall_extruders = firstLayerExtruders; for (auto& layer : m_layer_tools) { - // Sort and remove duplicates - sort_remove_duplicates(layer.extruders); + if (layer.preserve_extruder_order) + remove_duplicates_preserve_order(layer.extruders); + else + sort_remove_duplicates(layer.extruders); // make sure that there are some tools for each object layer (e.g. tall wiping object will result in empty extruders vector) if (layer.extruders.empty() && layer.has_object) @@ -739,6 +803,10 @@ void ToolOrdering::reorder_extruders(unsigned int last_extruder_id) if (lt.extruders.front() == 0) // Pop the "don't care" extruder, the "don't care" region will be merged with the next one. lt.extruders.erase(lt.extruders.begin()); + if (lt.preserve_extruder_order) { + last_extruder_id = lt.extruders.back(); + continue; + } // Reorder the extruders to start with the last one. for (size_t i = 1; i < lt.extruders.size(); ++ i) if (lt.extruders[i] == last_extruder_id) { @@ -789,27 +857,29 @@ void ToolOrdering::reorder_extruders(std::vector tool_order_layer0 // Reorder the extruders of first layer { LayerTools& lt = m_layer_tools[0]; - std::vector layer0_extruders = lt.extruders; - lt.extruders.clear(); - for (unsigned int extruder_id : tool_order_layer0) { - auto iter = std::find(layer0_extruders.begin(), layer0_extruders.end(), extruder_id); - if (iter != layer0_extruders.end()) { - lt.extruders.push_back(extruder_id); - *iter = (unsigned int)-1; + if (!lt.preserve_extruder_order) { + std::vector layer0_extruders = lt.extruders; + lt.extruders.clear(); + for (unsigned int extruder_id : tool_order_layer0) { + auto iter = std::find(layer0_extruders.begin(), layer0_extruders.end(), extruder_id); + if (iter != layer0_extruders.end()) { + lt.extruders.push_back(extruder_id); + *iter = (unsigned int)-1; + } } - } - for (unsigned int extruder_id : layer0_extruders) { - if (extruder_id == 0) - continue; + for (unsigned int extruder_id : layer0_extruders) { + if (extruder_id == 0) + continue; - if (extruder_id != (unsigned int)-1) - lt.extruders.push_back(extruder_id); - } + if (extruder_id != (unsigned int)-1) + lt.extruders.push_back(extruder_id); + } - // all extruders are zero - if (lt.extruders.empty()) { - lt.extruders.push_back(tool_order_layer0[0]); + // all extruders are zero + if (lt.extruders.empty()) { + lt.extruders.push_back(tool_order_layer0[0]); + } } } @@ -825,6 +895,10 @@ void ToolOrdering::reorder_extruders(std::vector tool_order_layer0 if (lt.extruders.front() == 0) // Pop the "don't care" extruder, the "don't care" region will be merged with the next one. lt.extruders.erase(lt.extruders.begin()); + if (lt.preserve_extruder_order) { + last_extruder_id = lt.extruders.back(); + continue; + } // Reorder the extruders to start with the last one. for (size_t i = 1; i < lt.extruders.size(); ++i) if (lt.extruders[i] == last_extruder_id) { @@ -1039,6 +1113,10 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume() LayerTools& lt = m_layer_tools[i]; if (lt.extruders.empty()) continue; + if (lt.preserve_extruder_order) { + current_extruder_id = lt.extruders.back(); + continue; + } std::vector custom_extruder_seq; if (get_custom_seq(i, custom_extruder_seq) && !custom_extruder_seq.empty()) { diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 10a0772551..2513511b2f 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -117,6 +117,7 @@ public: bool has_support = false; // Zero based extruder IDs, ordered to minimize tool switches. std::vector extruders; + bool preserve_extruder_order = false; // If per layer extruder switches are inserted by the G-code preview slider, this value contains the new (1 based) extruder, with which the whole object layer is being printed with. // If not overriden, it is set to 0. unsigned int extruder_override = 0; diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index d53c720124..2d2b068868 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -383,7 +383,7 @@ static bool parse_row_definition(const std::string &row, while (std::getline(ss, token, ',')) tokens.emplace_back(trim_copy(token)); - if (tokens.size() < 4 || tokens.size() > 13) + if (tokens.size() < 4) return false; int values[5] = { 0, 0, 1, 1, 50 }; @@ -436,6 +436,10 @@ static bool parse_row_definition(const std::string &row, } } + std::vector pattern_tokens; + pattern_tokens.reserve(tokens.size() > token_idx ? tokens.size() - token_idx : 1); + if (!manual_pattern.empty()) + pattern_tokens.push_back(manual_pattern); for (size_t i = token_idx; i < tokens.size(); ++i) { const std::string &tok = tokens[i]; if (tok.empty()) @@ -472,7 +476,17 @@ static bool parse_row_definition(const std::string &row, stable_id = parsed_stable_id; continue; } - manual_pattern = tok; + pattern_tokens.push_back(tok); + } + + if (!pattern_tokens.empty()) { + std::ostringstream joined_pattern; + for (size_t i = 0; i < pattern_tokens.size(); ++i) { + if (i != 0) + joined_pattern << ','; + joined_pattern << pattern_tokens[i]; + } + manual_pattern = joined_pattern.str(); } // Compatibility for early same-layer prototype rows. @@ -504,14 +518,69 @@ static bool decode_pattern_step(char c, char &out) } } +static std::vector split_manual_pattern_groups(const std::string &pattern) +{ + std::vector groups; + if (pattern.empty()) + return groups; + + 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 std::string flatten_manual_pattern_groups(const std::string &pattern) +{ + std::string flattened; + flattened.reserve(pattern.size()); + for (const char c : pattern) + if (c != ',') + flattened.push_back(c); + return flattened; +} + +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) { - if (pattern.empty()) + const std::vector groups = split_manual_pattern_groups(pattern); + if (groups.empty()) return 50; - // Legacy blend ratio for UI preview: count component-B aliases only. - // Tokens '3'..'9' are direct physical filament IDs and are ignored here. - const int count_b = int(std::count(pattern.begin(), pattern.end(), '2')); - return clamp_int(int(std::lround(100.0 * double(count_b) / double(pattern.size()))), 0, 100); + + // 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()); + } + 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) @@ -859,10 +928,19 @@ std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pa { 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)) { 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)) @@ -870,9 +948,16 @@ std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pa // Unknown token => invalid pattern. return std::string(); } + if (!normalized.empty() && normalized.back() == ',') + return std::string(); return normalized; } +int MixedFilamentManager::mix_percent_from_manual_pattern(const std::string &pattern) +{ + return mix_percent_from_normalized_pattern(normalize_manual_pattern(pattern)); +} + void MixedFilamentManager::apply_gradient_settings(int gradient_mode, float lower_bound, float upper_bound, @@ -1119,16 +1204,12 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id, // steps: '1' => component_a, '2' => component_b, '3'..'9' => direct // physical filament IDs. if (!mf.manual_pattern.empty()) { - const int pos = safe_mod(layer_index, int(mf.manual_pattern.size())); - const char token = mf.manual_pattern[size_t(pos)]; - if (token == '2') - return mf.component_b; - if (token == '1') - return mf.component_a; - if (token >= '3' && token <= '9') { - const unsigned int direct = unsigned(token - '0'); - if (direct >= 1 && direct <= num_physical) - return direct; + 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; } @@ -1174,6 +1255,78 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id, return (pos < mf.ratio_a) ? mf.component_a : mf.component_b; } +unsigned int MixedFilamentManager::resolve_perimeter(unsigned int filament_id, + size_t num_physical, + int layer_index, + int perimeter_index, + float layer_print_z, + float layer_height, + bool force_height_weighted) const +{ + 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); +} + +std::vector MixedFilamentManager::ordered_perimeter_extruders(unsigned int filament_id, + size_t num_physical, + int layer_index, + float layer_print_z, + float layer_height, + bool force_height_weighted) const +{ + std::vector ordered; + + const int mixed_idx = mixed_index_from_filament_id(filament_id, num_physical); + if (mixed_idx < 0) { + ordered.emplace_back(filament_id); + return ordered; + } + + 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()) { + 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); + } + if (!ordered.empty()) + return ordered; + } + } + + ordered.emplace_back(resolve(filament_id, num_physical, layer_index, layer_print_z, layer_height, force_height_weighted)); + return ordered; +} + int MixedFilamentManager::mixed_index_from_filament_id(unsigned int filament_id, size_t num_physical) const { if (filament_id <= num_physical) diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 53a852b1ac..6a8ca18595 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -146,6 +146,7 @@ public: // 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); // ---- Queries -------------------------------------------------------- @@ -164,6 +165,19 @@ public: float layer_print_z = 0.f, float layer_height = 0.f, bool force_height_weighted = false) const; + unsigned int resolve_perimeter(unsigned int filament_id, + size_t num_physical, + int layer_index, + int perimeter_index, + float layer_print_z = 0.f, + float layer_height = 0.f, + bool force_height_weighted = false) const; + std::vector ordered_perimeter_extruders(unsigned int filament_id, + size_t num_physical, + int layer_index, + float layer_print_z = 0.f, + float layer_height = 0.f, + bool force_height_weighted = false) const; // Map virtual filament ID (1-based, after physical IDs) to index into // m_mixed. Virtual IDs enumerate enabled mixed rows only. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b16d8ec53a..33cc348951 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3467,7 +3467,8 @@ void MixedFilamentConfigPanel::build_ui() wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER); 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. " - "Example: 1/1/1/1/2/2/2/2 or 1/2/3/4.")); + "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); @@ -3581,8 +3582,7 @@ void MixedFilamentConfigPanel::build_ui() if (into_u8(m_pattern_ctrl->GetValue()) != normalized) m_pattern_ctrl->ChangeValue(from_u8(normalized)); m_mf.manual_pattern = normalized; - const int count_b = int(std::count(normalized.begin(), normalized.end(), '2')); - m_mf.mix_b_percent = std::clamp((100 * count_b + int(normalized.size()) / 2) / std::max(1, int(normalized.size())), 0, 100); + m_mf.mix_b_percent = MixedFilamentManager::mix_percent_from_manual_pattern(normalized); m_mf.pointillism_all_filaments = false; m_mf.gradient_component_ids.clear(); m_mf.gradient_component_weights.clear(); diff --git a/tests/libslic3r/test_mixed_filament.cpp b/tests/libslic3r/test_mixed_filament.cpp index d37b69b2fe..8fbeb9fbb4 100644 --- a/tests/libslic3r/test_mixed_filament.cpp +++ b/tests/libslic3r/test_mixed_filament.cpp @@ -1,5 +1,6 @@ #include +#include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/PresetBundle.hpp" #include @@ -123,3 +124,71 @@ TEST_CASE("Mixed filament remap keeps later painted colors stable when an earlie CHECK(remap[7] == virtual_id_for_stable_id(mixed, 4, stable_id_7)); CHECK(remap[8] == virtual_id_for_stable_id(mixed, 4, stable_id_8)); } + +TEST_CASE("Mixed filament grouped manual patterns normalize and round-trip", "[MixedFilament]") +{ + const std::vector colors = {"#FF0000", "#0000FF"}; + + MixedFilamentManager mgr; + mgr.add_custom_filament(1, 2, 50, colors); + REQUIRE(mgr.mixed_filaments().size() == 1); + + MixedFilament &row = mgr.mixed_filaments().front(); + row.manual_pattern = MixedFilamentManager::normalize_manual_pattern("1/1/1/1/1/1/1/2, 1/1/1/2/1/1/1/1"); + REQUIRE(row.manual_pattern == "11111112,11121111"); + + const std::string serialized = mgr.serialize_custom_entries(); + + MixedFilamentManager loaded; + loaded.load_custom_entries(serialized, colors); + REQUIRE(loaded.mixed_filaments().size() == 1); + CHECK(loaded.mixed_filaments().front().manual_pattern == "11111112,11121111"); + CHECK(loaded.mixed_filaments().front().mix_b_percent == 13); +} + +TEST_CASE("Mixed filament perimeter resolver uses grouped manual patterns by inset", "[MixedFilament]") +{ + const std::vector colors = {"#00FFFF", "#FF00FF"}; + + MixedFilamentManager mgr; + mgr.add_custom_filament(1, 2, 50, colors); + REQUIRE(mgr.mixed_filaments().size() == 1); + + MixedFilament &row = mgr.mixed_filaments().front(); + row.manual_pattern = MixedFilamentManager::normalize_manual_pattern("12,21"); + REQUIRE(row.manual_pattern == "12,21"); + + const unsigned int mixed_filament_id = 3; + CHECK(mgr.resolve(mixed_filament_id, 2, 0) == 1); + CHECK(mgr.resolve(mixed_filament_id, 2, 1) == 2); + + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 0, 0) == 1); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 1, 0) == 2); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 0, 1) == 2); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 1, 1) == 1); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 0, 3) == 2); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 1, 3) == 1); + + const std::vector ordered_layer0 = mgr.ordered_perimeter_extruders(mixed_filament_id, 2, 0); + const std::vector ordered_layer1 = mgr.ordered_perimeter_extruders(mixed_filament_id, 2, 1); + REQUIRE(ordered_layer0.size() == 2); + REQUIRE(ordered_layer1.size() == 2); + CHECK(ordered_layer0[0] == 1); + CHECK(ordered_layer0[1] == 2); + CHECK(ordered_layer1[0] == 2); + CHECK(ordered_layer1[1] == 1); +} + +TEST_CASE("ExtrusionPath copies preserve inset index", "[MixedFilament]") +{ + ExtrusionPath src(erPerimeter); + src.inset_idx = 3; + + ExtrusionPath copied(src); + CHECK(copied.inset_idx == 3); + + ExtrusionPath assigned(erExternalPerimeter); + assigned.inset_idx = 0; + assigned = src; + CHECK(assigned.inset_idx == 3); +}