diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp index ed4712230e..b668e29e0a 100644 --- a/src/libslic3r/ExtrusionEntity.hpp +++ b/src/libslic3r/ExtrusionEntity.hpp @@ -339,21 +339,32 @@ public: ExtrusionPaths paths; ExtrusionMultiPath() {} - ExtrusionMultiPath(const ExtrusionMultiPath &rhs) : paths(rhs.paths), m_can_reverse(rhs.m_can_reverse) {} - ExtrusionMultiPath(ExtrusionMultiPath &&rhs) : paths(std::move(rhs.paths)), m_can_reverse(rhs.m_can_reverse) {} - ExtrusionMultiPath(const ExtrusionPaths &paths) : paths(paths) {} - ExtrusionMultiPath(const ExtrusionPath &path) {this->paths.push_back(path); m_can_reverse = path.can_reverse(); } + ExtrusionMultiPath(const ExtrusionMultiPath &rhs) : ExtrusionEntity(rhs), paths(rhs.paths), m_can_reverse(rhs.m_can_reverse) {} + ExtrusionMultiPath(ExtrusionMultiPath &&rhs) : ExtrusionEntity(rhs), paths(std::move(rhs.paths)), m_can_reverse(rhs.m_can_reverse) {} + ExtrusionMultiPath(const ExtrusionPaths &paths) : paths(paths) + { + if (!paths.empty()) + this->inset_idx = paths.front().inset_idx; + } + ExtrusionMultiPath(const ExtrusionPath &path) + { + this->paths.push_back(path); + this->inset_idx = path.inset_idx; + m_can_reverse = path.can_reverse(); + } ExtrusionMultiPath &operator=(const ExtrusionMultiPath &rhs) { - this->paths = rhs.paths; - m_can_reverse = rhs.m_can_reverse; + this->paths = rhs.paths; + this->inset_idx = rhs.inset_idx; + m_can_reverse = rhs.m_can_reverse; return *this; } ExtrusionMultiPath &operator=(ExtrusionMultiPath &&rhs) { - this->paths = std::move(rhs.paths); - m_can_reverse = rhs.m_can_reverse; + this->paths = std::move(rhs.paths); + this->inset_idx = rhs.inset_idx; + m_can_reverse = rhs.m_can_reverse; return *this; } @@ -404,12 +415,27 @@ public: ExtrusionPaths paths; ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {} - ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {} - ExtrusionLoop(ExtrusionPaths &&paths, ExtrusionLoopRole role = elrDefault) : paths(std::move(paths)), m_loop_role(role) {} + ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) + { + if (!paths.empty()) + this->inset_idx = paths.front().inset_idx; + } + ExtrusionLoop(ExtrusionPaths &&paths, ExtrusionLoopRole role = elrDefault) : paths(std::move(paths)), m_loop_role(role) + { + if (!this->paths.empty()) + this->inset_idx = this->paths.front().inset_idx; + } ExtrusionLoop(const ExtrusionPath &path, ExtrusionLoopRole role = elrDefault) : m_loop_role(role) - { this->paths.push_back(path); } + { + this->paths.push_back(path); + this->inset_idx = path.inset_idx; + } ExtrusionLoop(const ExtrusionPath &&path, ExtrusionLoopRole role = elrDefault) : m_loop_role(role) - { this->paths.emplace_back(std::move(path)); } + { + this->paths.emplace_back(std::move(path)); + if (!this->paths.empty()) + this->inset_idx = this->paths.front().inset_idx; + } bool is_loop() const override{ return true; } bool can_reverse() const override { return false; } ExtrusionEntity* clone() const override{ return new ExtrusionLoop (*this); } diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 889fcc73cd..c131bb91d4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -4770,15 +4770,32 @@ LayerResult GCode::process_layer(const Print& print, auto configured_filament_id_1based = [&layer_tools](const GCode::ObjectByExtruder::Island::Region::Type entity_type, const ExtrusionEntityCollection& entities, const PrintRegion& region) -> unsigned int { + auto raw_sparse_infill_filament_id_1based = [&layer_tools, ®ion]() -> unsigned int { + const PrintRegionConfig &config = region.config(); + if (!config.enable_infill_filament_override.value) + return unsigned(config.wall_filament.value); + if (layer_tools.object_layer_count <= 0) + return unsigned(config.sparse_infill_filament.value); + + const int first_layers = std::max(0, config.infill_filament_use_base_first_layers.value); + const int last_layers = std::max(0, config.infill_filament_use_base_last_layers.value); + return (layer_tools.layer_index < first_layers || + layer_tools.layer_index >= layer_tools.object_layer_count - last_layers) + ? unsigned(config.wall_filament.value) + : unsigned(config.sparse_infill_filament.value); + }; + if (entity_type == GCode::ObjectByExtruder::Island::Region::INFILL) { + if (layer_tools.extruder_override != 0) + return layer_tools.extruder_override; const ExtrusionRole role = entities.entities.empty() ? erNone : entities.entities.front()->role(); if (role == erSolidInfill && std::abs(region.config().sparse_infill_density.value - 100.) < EPSILON) - return layer_tools.sparse_infill_filament(region) + 1; + return raw_sparse_infill_filament_id_1based(); if (is_solid_infill(role)) - return layer_tools.solid_infill_filament(region) + 1; - return layer_tools.sparse_infill_filament(region) + 1; + return unsigned(region.config().solid_infill_filament.value); + return raw_sparse_infill_filament_id_1based(); } - return layer_tools.wall_filament(region) + 1; + return layer_tools.extruder_override == 0 ? unsigned(region.config().wall_filament.value) : layer_tools.extruder_override; }; auto configured_extruder_id = [&layer_tools](const GCode::ObjectByExtruder::Island::Region::Type entity_type, @@ -5318,28 +5335,39 @@ LayerResult GCode::process_layer(const Print& print, 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); + bucket_count)) { + if (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; + } + if (bucket_count == 1) { + for (size_t extruder_idx = 0; extruder_idx < split_by_extruder.size(); ++extruder_idx) { + const std::unique_ptr& split_collection = split_by_extruder[extruder_idx]; + if (split_collection && !split_collection->entities.empty()) { + // by_extruder keys and LayerTools runtime extruder IDs are zero-based here. + correct_extruder_id = int(extruder_idx); break; } } } - continue; } } } diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index b8bd078d7d..72520682ee 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -120,6 +120,24 @@ unsigned int grouped_manual_pattern_mixed_filament_id_for_layer(const LayerTools return 0; } +unsigned int grouped_manual_pattern_infill_filament_1based(const LayerTools& layer_tools, + const PrintRegion& region, + unsigned int configured_filament_id_1based) +{ + const unsigned int grouped_id = + grouped_manual_pattern_mixed_filament_id_for_layer(layer_tools, configured_filament_id_1based); + if (grouped_id == 0) + return 0; + + const int innermost_perimeter_index = std::max(0, region.config().wall_loops.value - 1); + return layer_tools.mixed_mgr->resolve_perimeter(grouped_id, + layer_tools.num_physical, + layer_tools.layer_index, + innermost_perimeter_index, + float(layer_tools.print_z), + float(layer_tools.layer_height)); +} + void remove_duplicates_preserve_order(std::vector &values) { std::vector ordered; @@ -279,14 +297,16 @@ unsigned int LayerTools::sparse_infill_filament(const PrintRegion ®ion) const { assert(region.config().wall_filament.value > 0); unsigned int id = (this->extruder_override == 0) ? sparse_infill_filament_id_1based(*this, region) : this->extruder_override; - return resolve_mixed_1based(id) - 1; + const unsigned int grouped = grouped_manual_pattern_infill_filament_1based(*this, region, id); + return ((grouped != 0) ? grouped : resolve_mixed_1based(id)) - 1; } unsigned int LayerTools::solid_infill_filament(const PrintRegion ®ion) const { assert(region.config().solid_infill_filament.value > 0); unsigned int id = (this->extruder_override == 0) ? region.config().solid_infill_filament.value : this->extruder_override; - return resolve_mixed_1based(id) - 1; + const unsigned int grouped = grouped_manual_pattern_infill_filament_1based(*this, region, id); + return ((grouped != 0) ? grouped : resolve_mixed_1based(id)) - 1; } // Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden. @@ -295,19 +315,13 @@ unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, c assert(region.config().wall_filament.value > 0); assert(region.config().sparse_infill_filament.value > 0); assert(region.config().solid_infill_filament.value > 0); - // 1 based extruder ID. - unsigned int extruder = 1; - if (this->extruder_override == 0) { - if (extrusions.has_infill()) { - const ExtrusionRole role = extrusions.entities.empty() ? erNone : extrusions.entities.front()->role(); - extruder = infill_filament_id_1based(*this, region, role); - } else - extruder = region.config().wall_filament.value; - } else - extruder = this->extruder_override; - - extruder = resolve_mixed_1based(extruder); - return (extruder == 0) ? 0 : extruder - 1; + if (extrusions.has_infill()) { + const ExtrusionRole role = extrusions.entities.empty() ? erNone : extrusions.entities.front()->role(); + if (internal_solid_infill_uses_sparse_filament(region, role)) + return sparse_infill_filament(region); + return is_solid_infill(role) ? solid_infill_filament(region) : sparse_infill_filament(region); + } + return wall_filament(region); } static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height) @@ -718,8 +732,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto layerCount, float(layer->print_z), float(layer->height)); - if (ordered.size() >= 2) { - layer_tools.preserve_extruder_order = true; + if (!ordered.empty()) { + 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 && @@ -764,10 +779,7 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto if (something_nonoverriddable || !m_print_config_ptr) { if (extruder_override == 0) { if (has_solid_infill) - layer_tools.extruders.emplace_back(resolve_mixed(region.config().solid_infill_filament, - layerCount, - float(layer->print_z), - float(layer->height))); + layer_tools.extruders.emplace_back(layer_tools.solid_infill_filament(region) + 1); if (has_sparse_infill) layer_tools.extruders.emplace_back(layer_tools.sparse_infill_filament(region) + 1); } else if (has_solid_infill || has_sparse_infill) diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index b0adc22f4a..3e113a4d05 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -46,6 +46,39 @@ unsigned int effective_layer_filament_id(const Layer &layer, unsigned int filame float(layer.height)); } +unsigned int effective_infill_filament_id(const Layer &layer, const PrintRegionConfig &config, unsigned int filament_id) +{ + const unsigned int effective = effective_layer_filament_id(layer, filament_id); + if (effective != filament_id || filament_id == 0) + return effective; + + const PrintObject *object = layer.object(); + const Print *print = object ? object->print() : nullptr; + if (print == nullptr) + return filament_id; + + const size_t num_physical = print->config().filament_diameter.size(); + if (num_physical == 0) + return filament_id; + + const MixedFilamentManager &mixed_mgr = print->mixed_filament_manager(); + const MixedFilament *mixed_row = mixed_mgr.mixed_filament_from_id(filament_id, num_physical); + if (mixed_row == nullptr) + return filament_id; + + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern); + if (normalized_pattern.find(',') == std::string::npos) + return filament_id; + + const int innermost_perimeter_index = std::max(0, config.wall_loops.value - 1); + return mixed_mgr.resolve_perimeter(filament_id, + num_physical, + int(layer.id()), + innermost_perimeter_index, + float(layer.print_z), + float(layer.height)); +} + bool use_base_infill_filament(const PrintRegionConfig &config, int layer_index, int layer_count) { if (!config.enable_infill_filament_override.value) @@ -71,7 +104,9 @@ unsigned int LayerRegion::extruder(FlowRole role) const else filament_id = this->region().extruder(role); - return effective_layer_filament_id(*m_layer, filament_id); + return (role == frInfill || role == frSolidInfill) ? + effective_infill_filament_id(*m_layer, config, filament_id) : + effective_layer_filament_id(*m_layer, filament_id); } Flow LayerRegion::flow(FlowRole role) const diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 383343a8d4..e81dca93d4 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -570,6 +570,10 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p // Append paths to collection. if (!paths.empty()) { + const int inset_idx = int(extrusion->inset_idx); + for (ExtrusionPath &path : paths) + path.inset_idx = inset_idx; + if (extrusion->is_closed) { ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole); extrusion_loop.make_counter_clockwise(); @@ -581,6 +585,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p } assert(extrusion_loop.paths.front().first_point() == extrusion_loop.paths.back().last_point()); + extrusion_loop.inset_idx = inset_idx; extrusion_coll.append(std::move(extrusion_loop)); } else { @@ -593,11 +598,13 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p } ExtrusionMultiPath multi_path; multi_path.paths.emplace_back(std::move(paths.front())); + multi_path.inset_idx = inset_idx; for (auto it_path = std::next(paths.begin()); it_path != paths.end(); ++it_path) { if (multi_path.paths.back().last_point() != it_path->first_point()) { extrusion_coll.append(ExtrusionMultiPath(std::move(multi_path))); multi_path = ExtrusionMultiPath(); + multi_path.inset_idx = inset_idx; } multi_path.paths.emplace_back(std::move(*it_path)); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 53c57a2b96..43fdfa0133 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4566,6 +4566,7 @@ struct MixedFilamentPreviewSettings double preferred_a_height { 0.0 }; double preferred_b_height { 0.0 }; bool local_z_mode { false }; + size_t wall_loops { 1 }; }; // Inline editor panel for configuring a single mixed filament @@ -4634,7 +4635,11 @@ private: // Helper functions (copied from update_mixed_filament_panel) static std::vector decode_gradient_ids(const std::string &s); static std::string encode_gradient_ids(const std::vector &ids); - static std::vector decode_manual_pattern_ids(const std::string &pattern, unsigned int a, unsigned int b); + static std::vector decode_manual_pattern_ids(const std::string &pattern, + unsigned int a, + unsigned int b, + size_t num_physical, + size_t wall_loops = 0); static std::vector decode_gradient_weights(const std::string &s, size_t n); static std::vector normalize_gradient_weights(const std::vector &w, size_t n); static std::string encode_gradient_weights(const std::vector &w); @@ -4784,6 +4789,100 @@ private: }; // Implementation of MixedFilamentConfigPanel helper functions +static std::vector split_manual_pattern_preview_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 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) +{ + std::vector sequence; + if (num_physical == 0) + return sequence; + + const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern); + if (normalized.empty()) + return sequence; + + 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; + 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; +} + std::vector MixedFilamentConfigPanel::decode_gradient_ids(const std::string &s) { std::vector ids; @@ -4816,15 +4915,13 @@ std::string MixedFilamentConfigPanel::encode_gradient_ids(const std::vector MixedFilamentConfigPanel::decode_manual_pattern_ids(const std::string &pattern, unsigned int a, unsigned int b) +std::vector MixedFilamentConfigPanel::decode_manual_pattern_ids(const std::string &pattern, + unsigned int a, + unsigned int b, + size_t num_physical, + size_t wall_loops) { - std::vector seq; - for (char c : pattern) { - if (c == '1' || c == 'A' || c == 'a') seq.emplace_back(a); - else if (c == '2' || c == 'B' || c == 'b') seq.emplace_back(b); - else if (c >= '3' && c <= '9') seq.emplace_back(unsigned(c - '0')); - } - return seq; + return build_grouped_manual_pattern_preview_sequence(pattern, a, b, num_physical, wall_loops); } std::vector MixedFilamentConfigPanel::decode_gradient_weights(const std::string &s, size_t n) @@ -5947,7 +6044,11 @@ void MixedFilamentConfigPanel::build_ui() m_mf.pointillism_all_filaments = false; m_mf.gradient_component_ids.clear(); m_mf.gradient_component_weights.clear(); - preview_sequence = decode_manual_pattern_ids(m_mf.manual_pattern, m_mf.component_a, m_mf.component_b); + preview_sequence = decode_manual_pattern_ids(m_mf.manual_pattern, + m_mf.component_a, + m_mf.component_b, + m_num_physical, + m_preview_settings.wall_loops); } else { std::vector selected_ids; selected_ids.reserve(4); @@ -6254,7 +6355,11 @@ void MixedFilamentConfigPanel::update_preview() std::vector initial_sequence; if (pattern_row_mode) { - initial_sequence = decode_manual_pattern_ids(normalized_pattern, m_mf.component_a, m_mf.component_b); + initial_sequence = decode_manual_pattern_ids(normalized_pattern, + m_mf.component_a, + m_mf.component_b, + m_num_physical, + m_preview_settings.wall_loops); } else { std::vector initial_gradient_ids = simple_mode ? std::vector() : decode_gradient_ids(m_mf.gradient_component_ids); if (initial_gradient_ids.size() >= 3) @@ -6707,24 +6812,11 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) sequence = filtered_ids; return sequence; }; - auto decode_manual_pattern_ids = [num_physical](const std::string &pattern, unsigned int component_a, unsigned int component_b) { - std::vector sequence; - if (num_physical == 0) - return sequence; - const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern); - sequence.reserve(normalized.size()); - for (const char token : normalized) { - 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'); - if (extruder_id >= 1 && extruder_id <= num_physical) - sequence.emplace_back(extruder_id); - } - return sequence; + auto decode_manual_pattern_ids = [num_physical](const std::string &pattern, + unsigned int component_a, + unsigned int component_b, + size_t wall_loops) { + return build_grouped_manual_pattern_preview_sequence(pattern, component_a, component_b, num_physical, wall_loops); }; const bool height_weighted_mode = get_mixed_mode(false); int gradient_mode = height_weighted_mode ? 1 : 0; @@ -6736,6 +6828,9 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) if (print_cfg && print_cfg->has("layer_height")) nominal_layer_height = float(print_cfg->opt_float("layer_height")); nominal_layer_height = std::max(0.01f, nominal_layer_height); + size_t wall_loops = 1; + if (print_cfg && print_cfg->has("wall_loops")) + wall_loops = std::max(1, size_t(std::max(1, print_cfg->opt_int("wall_loops")))); const bool local_z_mode = get_mixed_bool("dithering_local_z_mode", false); float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f)); float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f)); @@ -6748,7 +6843,8 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) upper_bound, preferred_local_z_a, preferred_local_z_b, - local_z_mode + local_z_mode, + wall_loops }; auto summarize_sequence = [num_physical](const std::vector &sequence) { if (sequence.empty() || num_physical == 0) @@ -6814,7 +6910,10 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) build_weighted_multi_sequence, preview_settings](const MixedFilament &entry) { const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); if (!normalized_pattern.empty()) - return decode_manual_pattern_ids(normalized_pattern, entry.component_a, entry.component_b); + return decode_manual_pattern_ids(normalized_pattern, + entry.component_a, + entry.component_b, + preview_settings.wall_loops); const bool simple_mode = entry.distribution_mode == int(MixedFilament::Simple); if (!simple_mode) { diff --git a/tests/libslic3r/test_mixed_filament.cpp b/tests/libslic3r/test_mixed_filament.cpp index 06683054b8..acacd9666d 100644 --- a/tests/libslic3r/test_mixed_filament.cpp +++ b/tests/libslic3r/test_mixed_filament.cpp @@ -2,6 +2,8 @@ #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/GCode/ToolOrdering.hpp" #include #include @@ -221,6 +223,119 @@ TEST_CASE("Mixed filament perimeter resolver uses grouped manual patterns by ins CHECK(ordered_layer1[1] == 1); } +TEST_CASE("Grouped manual perimeter patterns keep grouped resolution on collapsed single-tool layers", "[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("2,12"); + REQUIRE(row.manual_pattern == "2,12"); + + const unsigned int mixed_filament_id = 3; + + // The flattened row cadence resolves this layer to component A, but both + // perimeter groups collapse onto physical filament 2. G-code generation + // and tool ordering must keep using the grouped perimeter result here. + CHECK(mgr.resolve(mixed_filament_id, 2, 1) == 1); + + const std::vector ordered_layer1 = mgr.ordered_perimeter_extruders(mixed_filament_id, 2, 1); + REQUIRE(ordered_layer1.size() == 1); + CHECK(ordered_layer1.front() == 2); + + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 1, 0) == 2); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 1, 1) == 2); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 1, 2) == 2); +} + +TEST_CASE("Grouped manual perimeter patterns resolve overlapping singleton inner groups", "[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,1"); + REQUIRE(row.manual_pattern == "12,1"); + + const unsigned int mixed_filament_id = 3; + + 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() == 1); + CHECK(ordered_layer0.front() == 1); + REQUIRE(ordered_layer1.size() == 2); + CHECK(ordered_layer1[0] == 2); + CHECK(ordered_layer1[1] == 1); + + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 0, 0) == 1); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 0, 1) == 1); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 1, 0) == 2); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 1, 1) == 1); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 2, 0) == 1); + CHECK(mgr.resolve_perimeter(mixed_filament_id, 2, 2, 1) == 1); +} + +TEST_CASE("Grouped manual wall patterns make infill follow the innermost perimeter tool", "[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,1"); + REQUIRE(row.manual_pattern == "12,1"); + + PrintRegionConfig region_config = static_cast(FullPrintConfig::defaults()); + region_config.wall_filament.value = 3; + region_config.wall_loops.value = 2; + region_config.enable_infill_filament_override.value = false; + region_config.sparse_infill_density.value = 15.; + region_config.sparse_infill_filament.value = 2; + region_config.solid_infill_filament.value = 3; + + PrintRegion region(region_config); + + LayerTools layer0(0.2); + layer0.layer_index = 0; + layer0.object_layer_count = 6; + layer0.layer_height = 0.2; + layer0.mixed_mgr = &mgr; + layer0.num_physical = 2; + + LayerTools layer1(0.4); + layer1.layer_index = 1; + layer1.object_layer_count = 6; + layer1.layer_height = 0.2; + layer1.mixed_mgr = &mgr; + layer1.num_physical = 2; + + CHECK(layer0.wall_filament(region) == 0); + CHECK(layer1.wall_filament(region) == 1); + CHECK(layer0.sparse_infill_filament(region) == 0); + CHECK(layer1.sparse_infill_filament(region) == 0); + CHECK(layer0.solid_infill_filament(region) == 0); + CHECK(layer1.solid_infill_filament(region) == 0); + + region_config.enable_infill_filament_override.value = true; + region_config.sparse_infill_filament.value = 2; + region_config.solid_infill_filament.value = 2; + PrintRegion overridden_region(region_config); + + CHECK(layer0.sparse_infill_filament(overridden_region) == 1); + CHECK(layer1.sparse_infill_filament(overridden_region) == 1); + CHECK(layer0.solid_infill_filament(overridden_region) == 1); + CHECK(layer1.solid_infill_filament(overridden_region) == 1); +} + TEST_CASE("Mixed filament painted-region resolver collapses ordinary mixed rows to the active physical extruder", "[MixedFilament]") { const std::vector colors = {"#FF0000", "#00FF00"}; @@ -269,3 +384,26 @@ TEST_CASE("ExtrusionPath copies preserve inset index", "[MixedFilament]") assigned = src; CHECK(assigned.inset_idx == 3); } + +TEST_CASE("Extrusion loop and multipath entities preserve inset index", "[MixedFilament]") +{ + ExtrusionPath src(erPerimeter); + src.inset_idx = 2; + + ExtrusionMultiPath multi_from_path(src); + CHECK(multi_from_path.inset_idx == 2); + + ExtrusionMultiPath multi_copy(multi_from_path); + CHECK(multi_copy.inset_idx == 2); + + ExtrusionMultiPath multi_assigned; + multi_assigned.inset_idx = 0; + multi_assigned = multi_from_path; + CHECK(multi_assigned.inset_idx == 2); + + ExtrusionLoop loop_from_path(src); + CHECK(loop_from_path.inset_idx == 2); + + ExtrusionLoop loop_copy(loop_from_path); + CHECK(loop_copy.inset_idx == 2); +}