From 9c4258d43e0a0e41c0263525e83cad9fce35ab6a Mon Sep 17 00:00:00 2001 From: LixNix <105106115+LixNix@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:58:30 +0200 Subject: [PATCH 1/3] This commit adds support for multi nozzle printing. Printing with different nozzle sizes is now possible and each extruder can now print at a different semi fixed / semi adaptive layer height depending on user choice. Includes: - Fix Filament for Features (OrcaSlicer#13607) - Fix + Support 'Default' filament option, index 0 (OrcaSlicer#13887) - Filament for features for SEMM (OrcaSlicer#13937) - Divided filament for features (OrcaSlicer#14042) - Port of LixNix/OrcaSlicer-multi-nozzle-size-printing commit 944da13 --- src/libslic3r/Brim.cpp | 5 +- src/libslic3r/Fill/Fill.cpp | 23 +- src/libslic3r/Flow.cpp | 19 +- src/libslic3r/Flow.hpp | 2 + src/libslic3r/GCode.cpp | 398 ++++++---- src/libslic3r/GCode/ToolOrdering.cpp | 317 ++++++-- src/libslic3r/GCode/ToolOrdering.hpp | 9 +- src/libslic3r/GCode/WipeTower2.cpp | 87 ++- src/libslic3r/GCode/WipeTower2.hpp | 11 +- src/libslic3r/Layer.cpp | 22 +- src/libslic3r/Layer.hpp | 28 +- src/libslic3r/LayerRegion.cpp | 64 +- src/libslic3r/MultiMaterialSegmentation.cpp | 2 +- src/libslic3r/PerimeterGenerator.cpp | 37 +- src/libslic3r/PerimeterGenerator.hpp | 10 +- src/libslic3r/Preset.cpp | 6 +- src/libslic3r/PresetBundle.cpp | 13 +- src/libslic3r/Print.cpp | 287 +++++++- src/libslic3r/Print.hpp | 23 +- src/libslic3r/PrintApply.cpp | 36 +- src/libslic3r/PrintConfig.cpp | 238 +++++- src/libslic3r/PrintConfig.hpp | 22 +- src/libslic3r/PrintObject.cpp | 472 +++++++++++- src/libslic3r/PrintObjectSlice.cpp | 218 +++++- src/libslic3r/PrintRegion.cpp | 54 +- src/libslic3r/Slicing.cpp | 39 +- src/libslic3r/Support/SupportCommon.cpp | 2 +- src/libslic3r/Support/SupportMaterial.cpp | 4 +- src/libslic3r/Support/SupportParameters.hpp | 3 +- src/libslic3r/Support/TreeSupport.cpp | 2 +- src/slic3r/GUI/ConfigManipulation.cpp | 22 +- src/slic3r/GUI/GUI_Factories.cpp | 2 +- src/slic3r/GUI/GUI_ObjectList.cpp | 12 +- src/slic3r/GUI/PartPlate.cpp | 156 +++- src/slic3r/GUI/Plater.cpp | 119 +-- src/slic3r/GUI/PresetHints.cpp | 9 +- src/slic3r/GUI/Tab.cpp | 41 +- tests/fff_print/CMakeLists.txt | 1 + .../test_multi_nozzle_layer_height.cpp | 684 ++++++++++++++++++ tests/libslic3r/test_mixed_filament.cpp | 31 +- 40 files changed, 2915 insertions(+), 615 deletions(-) create mode 100644 tests/fff_print/test_multi_nozzle_layer_height.cpp diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index 5ee502acb89..30368cce989 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -1040,7 +1040,10 @@ static ExPolygons outer_inner_brim_area(const Print& print, } support_material_extruder = object->config().support_filament; if (support_material_extruder == 0 && object->has_support_material()) { - if (print.config().print_sequence == PrintSequence::ByObject) + // ORCA: under the support nozzle diameter restriction the brim uses the support's resolved filament. + if (unsigned int resolved = object->resolved_default_support_filament(); resolved > 0) + support_material_extruder = resolved; + else if (print.config().print_sequence == PrintSequence::ByObject) support_material_extruder = objectWithExtruder.second; else support_material_extruder = printExtruders.front() + 1; diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index ea899aa3b39..7f1187c1d21 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -896,6 +896,14 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p params.extrusion_role = erSolidInfill; } } + // ORCA: per-feature filaments. Top and internal solid fills are already resolved + // by layerm.extruder(extrusion_role) above; bottom surfaces print with the bottom + // surface filament, routed through the same mixed-filament remapping as top surfaces. + // External bridges are bottom surfaces, so they print with the bottom surface filament + // too (internal bridges keep their pre-seeded filament). + if (params.extrusion_role == erBottomSurface || params.extrusion_role == erBridgeInfill) + params.extruder = effective_layer_filament_id(layer, + (unsigned int)std::max(0, region_config.bottom_surface_filament_id.value)); // Orca: apply fill multiline only for sparse infill params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1; @@ -919,9 +927,12 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p params.bridge = is_bridge || Fill::use_bridge_flow(params.pattern); const bool is_thick_bridge = surface.is_bridge() && (surface.is_internal_bridge() ? object_config.thick_internal_bridges : object_config.thick_bridges); params.flow = params.bridge ? - //Orca: enable thick bridge based on config - layerm.bridging_flow(extrusion_role, is_thick_bridge) : - layerm.flow(extrusion_role, (surface.thickness == -1) ? layer.height : surface.thickness); + //Orca: enable thick bridge based on config. Combined layers stamp their full thickness on the surface; the non-thick bridge flow must be based on it. + layerm.bridging_flow(extrusion_role, is_thick_bridge, params.extruder, + (surface.thickness == -1) ? 0. : surface.thickness) : + // Width resolves against the nozzle of the filament that actually prints + // (params.extruder), which may differ from the role's default filament mapping. + layerm.flow(extrusion_role, (surface.thickness == -1) ? layer.height : surface.thickness, params.extruder); // record speed params if (!params.bridge) { if (params.extrusion_role == erInternalInfill) @@ -1530,12 +1541,12 @@ void Layer::make_ironing() ((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) && (config.ironing_type == IroningType::TopSurfaces || (config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) { - if (config.wall_filament == config.solid_infill_filament || config.wall_loops == 0) { + if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) { // Iron the whole face. - ironing_params.extruder = config.solid_infill_filament; + ironing_params.extruder = config.top_surface_filament_id; } else { // Iron just the infill. - ironing_params.extruder = config.solid_infill_filament; + ironing_params.extruder = config.top_surface_filament_id; } } if (ironing_params.extruder != -1) { diff --git a/src/libslic3r/Flow.cpp b/src/libslic3r/Flow.cpp index dbe7e157ab1..0008c13e4d8 100644 --- a/src/libslic3r/Flow.cpp +++ b/src/libslic3r/Flow.cpp @@ -211,21 +211,29 @@ double Flow::mm3_per_mm() const return res; } +// Nozzle diameter driving support / raft flows. A "default" (0) filament prints with the active extruder; the support_nozzle_diameter restriction defines which nozzle that may be. +float support_material_nozzle_diameter(const PrintObject *object, int configured_filament) +{ + if (configured_filament == 0 && object->config().support_nozzle_diameter.value > 0.) + return float(object->config().support_nozzle_diameter.value); + // for configured_filament == 0 (use the current extruder), get_at returns the 0th component. + return float(object->print()->config().nozzle_diameter.get_at(configured_filament - 1)); +} + Flow support_material_flow(const PrintObject *object, float layer_height) { return Flow::new_from_config_width( frSupportMaterial, // The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution. (object->config().support_line_width.value > 0) ? object->config().support_line_width : object->config().line_width, - // if object->config().support_filament == 0 (which means to not trigger tool change, but use the current extruder instead), get_at will return the 0th component. - float(object->print()->config().nozzle_diameter.get_at(object->config().support_filament-1)), + support_material_nozzle_diameter(object, object->config().support_filament), (layer_height > 0.f) ? layer_height : float(object->config().layer_height.value)); } //BBS Flow support_transition_flow(const PrintObject* object) { //BBS: support transition of tree support is bridge flow - float dmr = float(object->print()->config().nozzle_diameter.get_at(object->config().support_filament - 1)); + float dmr = support_material_nozzle_diameter(object, object->config().support_filament); return Flow::bridging_flow(dmr, dmr); } @@ -237,7 +245,7 @@ Flow support_material_1st_layer_flow(const PrintObject *object, float layer_heig frSupportMaterial, // The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution. (width.value > 0) ? width : object->config().line_width, - float(print_config.nozzle_diameter.get_at(object->config().support_filament-1)), + support_material_nozzle_diameter(object, object->config().support_filament), (layer_height > 0.f) ? layer_height : float(print_config.initial_layer_print_height.value)); } @@ -247,8 +255,7 @@ Flow support_material_interface_flow(const PrintObject *object, float layer_heig frSupportMaterialInterface, // The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution. (object->config().support_line_width > 0) ? object->config().support_line_width : object->config().line_width, - // if object->config().support_interface_filament == 0 (which means to not trigger tool change, but use the current extruder instead), get_at will return the 0th component. - float(object->print()->config().nozzle_diameter.get_at(object->config().support_interface_filament-1)), + support_material_nozzle_diameter(object, object->config().support_interface_filament), (layer_height > 0.f) ? layer_height : float(object->config().layer_height.value)); } diff --git a/src/libslic3r/Flow.hpp b/src/libslic3r/Flow.hpp index 79cb1b324d6..a8028a9c6fa 100644 --- a/src/libslic3r/Flow.hpp +++ b/src/libslic3r/Flow.hpp @@ -139,6 +139,8 @@ class Flow bool m_bridge { false }; }; +// ORCA: nozzle driving support / raft flows: the configured filament's, or the support_nozzle_diameter restriction when the filament is left at "default" (0). +extern float support_material_nozzle_diameter(const PrintObject *object, int configured_filament); extern Flow support_material_flow(const PrintObject* object, float layer_height = 0.f); extern Flow support_transition_flow(const PrintObject *object); //BBS extern Flow support_material_1st_layer_flow(const PrintObject *object, float layer_height = 0.f); diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index adfcc0dc651..038d8728437 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1513,7 +1513,12 @@ std::vector GCode::collect_layers_to_print(const PrintObjec } extra_gap = std::max(extra_gap, object.config().raft_contact_distance.value); } - double maximal_print_z = (last_extrusion_layer ? last_extrusion_layer->print_z() : 0.) + layer_to_print.layer()->height + + double layer_span = layer_to_print.layer()->height; + // ORCA: the top layer of a combined group prints the whole group at once, so tolerate a gap of combined_height() (equals the layer height when not combined). + if (layer_to_print.object_layer != nullptr) + for (const LayerRegion *layerm : layer_to_print.object_layer->regions()) + layer_span = std::max(layer_span, layerm->combined_height()); + double maximal_print_z = (last_extrusion_layer ? last_extrusion_layer->print_z() : 0.) + layer_span + std::max(0., extra_gap); // Negative support_contact_z is not taken into account, it can result in false positives in cases @@ -5003,6 +5008,7 @@ LayerResult GCode::process_layer(const Print& print, // Group extrusions by an extruder, then by an object, an island and a region. std::map> by_extruder; + std::vector> split_perimeter_storage; bool is_anything_overridden = const_cast(layer_tools).wiping_extrusions().is_anything_overridden(); const double nozzle_0_mm = m_config.nozzle_diameter.values.empty() ? 0.4 : m_config.nozzle_diameter.get_at(0); const double pointillism_pixel_size_cfg = std::max(0.0, double(m_config.mixed_filament_pointillism_pixel_size.value)); @@ -5024,28 +5030,60 @@ LayerResult GCode::process_layer(const Print& print, 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(); + // gap fill inherits the filament of the surface it fills; derive the role from the + // first non-gap-fill entity (must match ToolOrdering::collect_extruders()). + ExtrusionRole role = erNone; + for (const ExtrusionEntity *fill_entity : entities.entities) + if (fill_entity->role() != erGapFill) { + role = fill_entity->role(); + break; + } + if (role == erNone && ! entities.entities.empty()) + // perimeter-generated gap fill with no sibling surface prints with the outer wall filament. + return unsigned(region.config().outer_wall_filament_id.value); if (role == erSolidInfill && std::abs(region.config().sparse_infill_density.value - 100.) < EPSILON) - return unsigned(region.config().sparse_infill_filament.value); + return unsigned(region.config().sparse_infill_filament_id.value); + if (role == erTopSolidInfill || role == erIroning) + return unsigned(region.config().top_surface_filament_id.value); + if (role == erBottomSurface || role == erBridgeInfill) // ORCA: external bridges print as bottom surfaces (internal bridges stay internal solid) + return unsigned(region.config().bottom_surface_filament_id.value); if (is_solid_infill(role)) - return unsigned(region.config().solid_infill_filament.value); - return unsigned(region.config().sparse_infill_filament.value); + return unsigned(region.config().internal_solid_filament_id.value); + return unsigned(region.config().sparse_infill_filament_id.value); } - return layer_tools.extruder_override == 0 ? unsigned(region.config().wall_filament.value) : layer_tools.extruder_override; + if (layer_tools.extruder_override != 0) + return layer_tools.extruder_override; + return entities.role() == erPerimeter ? unsigned(region.config().inner_wall_filament_id.value) + : unsigned(region.config().outer_wall_filament_id.value); }; auto configured_extruder_id = [&layer_tools](const GCode::ObjectByExtruder::Island::Region::Type entity_type, const ExtrusionEntityCollection& entities, const PrintRegion& region) -> int { if (entity_type == GCode::ObjectByExtruder::Island::Region::INFILL) { - const ExtrusionRole role = entities.entities.empty() ? erNone : entities.entities.front()->role(); + // gap fill inherits the filament of the surface it fills; derive the role from the + // first non-gap-fill entity (must match ToolOrdering::collect_extruders()). + ExtrusionRole role = erNone; + for (const ExtrusionEntity *fill_entity : entities.entities) + if (fill_entity->role() != erGapFill) { + role = fill_entity->role(); + break; + } + if (role == erNone && ! entities.entities.empty()) + // perimeter-generated gap fill with no sibling surface prints with the outer wall filament. + return int(layer_tools.wall_extruder_id(region)); if (role == erSolidInfill && std::abs(region.config().sparse_infill_density.value - 100.) < EPSILON) - return int(layer_tools.sparse_infill_filament(region)); + return int(layer_tools.sparse_infill_filament_id(region)); + if (role == erTopSolidInfill || role == erIroning) + return int(layer_tools.top_surface_filament_id(region)); + if (role == erBottomSurface || role == erBridgeInfill) // ORCA: external bridges print as bottom surfaces (internal bridges stay internal solid) + return int(layer_tools.bottom_surface_filament_id(region)); if (is_solid_infill(role)) - return int(layer_tools.solid_infill_filament(region)); - return int(layer_tools.sparse_infill_filament(region)); + return int(layer_tools.internal_solid_filament_id(region)); + return int(layer_tools.sparse_infill_filament_id(region)); } - return int(layer_tools.wall_filament(region)); + return entities.role() == erPerimeter ? int(layer_tools.inner_wall_extruder_id(region)) + : int(layer_tools.wall_extruder_id(region)); }; auto pointillism_sequence_for_filament = [&](unsigned int filament_id_1based) -> const std::vector* { @@ -5381,6 +5419,29 @@ LayerResult GCode::process_layer(const Print& print, if (interface_dontcare) interface_extruder = dontcare_extruder; } + // ORCA: with a support nozzle diameter restriction, ("don't care") support/interface may only use a nozzle-matching extruder; prefer one scheduled on this layer (as ToolOrdering did). + if (object.config().support_nozzle_diameter.value > 0.) { + auto restrict_to_support_nozzle = [&print, &object, &layer_tools](unsigned int extruder_id) -> unsigned int { + if (object.support_filament_allowed(extruder_id + 1)) + return extruder_id; + unsigned int fallback = extruder_id; + bool have_fallback = false; + for (unsigned int candidate : layer_tools.extruders) // 0 based at this point + if (object.support_filament_allowed(candidate + 1)) { + if (! print.config().filament_soluble.get_at(candidate)) + return candidate; + if (! have_fallback) { + fallback = candidate; + have_fallback = true; + } + } + return fallback; + }; + if (support_dontcare) + support_extruder = restrict_to_support_nozzle(support_extruder); + if (interface_dontcare) + interface_extruder = restrict_to_support_nozzle(interface_extruder); + } // Both the support and the support interface are printed with the same extruder, therefore // the interface may be interleaved with the support base. bool single_extruder = !has_support || support_extruder == interface_extruder; @@ -5585,155 +5646,208 @@ LayerResult GCode::process_layer(const Print& print, local_z_clipped_collections.emplace_back(std::move(clipped_base)); } - const unsigned int configured_filament_id = configured_filament_id_1based(entity_type, *filtered_extrusions, region); - const std::vector* pointillism_sequence = - is_anything_overridden ? nullptr : pointillism_sequence_for_filament(configured_filament_id); - if (pointillism_sequence != nullptr) { - std::vector> split_by_extruder; - PointillismPathSplitStats split_stats; - const size_t sequence_phase = pointillism_sequence->empty() ? - 0 : size_t(std::max(0, layer_tools.layer_index)) % pointillism_sequence->size(); - if (split_extrusion_collection_for_pointillism_paths(*filtered_extrusions, - *pointillism_sequence, - layer_tools.num_physical, - pointillism_segment_len_scaled, - pointillism_line_gap_scaled, - sequence_phase, - split_by_extruder, - split_stats) && - split_stats.bucket_count >= 2) { - ++pointillism_path_split_entities; - pointillism_path_split_segments += split_stats.segment_count; - 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 || entity_matches_surface(island_idx, *split_ptr)) { - 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; + auto process_extrusions = [&](const ExtrusionEntityCollection* current_extrusions, + const ExtrusionEntityCollection* overrides_key, + bool use_overrides) { + const unsigned int configured_filament_id = configured_filament_id_1based(entity_type, *current_extrusions, region); + const std::vector* pointillism_sequence = + is_anything_overridden ? nullptr : pointillism_sequence_for_filament(configured_filament_id); + if (pointillism_sequence != nullptr) { + std::vector> split_by_extruder; + PointillismPathSplitStats split_stats; + const size_t sequence_phase = pointillism_sequence->empty() ? + 0 : size_t(std::max(0, layer_tools.layer_index)) % pointillism_sequence->size(); + if (split_extrusion_collection_for_pointillism_paths(*current_extrusions, + *pointillism_sequence, + layer_tools.num_physical, + pointillism_segment_len_scaled, + pointillism_line_gap_scaled, + sequence_phase, + split_by_extruder, + split_stats) && + split_stats.bucket_count >= 2) { + ++pointillism_path_split_entities; + pointillism_path_split_segments += split_stats.segment_count; + 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 || entity_matches_surface(island_idx, *split_ptr)) { + 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; + } } } + return; } - continue; + ++pointillism_path_split_fallbacks; } - ++pointillism_path_split_fallbacks; - } - // This extrusion is part of certain Region, which tells us which extruder should be used for it: - int correct_extruder_id = configured_extruder_id(entity_type, *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(entity_type, *filtered_extrusions, region); - if (mixed_filament_id != 0) { - std::vector> split_by_extruder; - size_t bucket_count = 0; - const PrintObject* current_object_for_gradient = - layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object(); - 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, - current_object_for_gradient)) { - 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 || entity_matches_surface(island_idx, *split_ptr)) { - 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; + // This extrusion is part of certain Region, which tells us which extruder should be used for it: + int correct_extruder_id = configured_extruder_id(entity_type, *current_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(entity_type, *current_extrusions, region); + if (mixed_filament_id != 0) { + std::vector> split_by_extruder; + size_t bucket_count = 0; + const PrintObject* current_object_for_gradient = + layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object(); + if (split_extrusion_collection_for_multi_perimeter_pattern(*current_extrusions, + *layer_tools.mixed_mgr, + mixed_filament_id, + layer_tools.num_physical, + layer_tools.layer_index, + split_by_extruder, + bucket_count, + current_object_for_gradient)) { + 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 || entity_matches_surface(island_idx, *split_ptr)) { + 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; + } } } + return; } - 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; + 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; + } } } } } } - } - // Let's recover vector of extruder overrides: - const WipingExtrusions::ExtruderPerCopy* entity_overrides = nullptr; - if (!layer_tools.has_extruder(correct_extruder_id)) { - // this entity is not overridden, but its extruder is not in layer_tools - we'll print it - // by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare - // extruders are eradicated from layer_tools) - correct_extruder_id = layer_tools.extruders.back(); - } - printing_extruders.clear(); - if (is_anything_overridden) { - entity_overrides = const_cast(layer_tools) - .wiping_extrusions() - .get_extruder_overrides(filtered_extrusions, layer_to_print.original_object, correct_extruder_id, - layer_to_print.object()->instances().size()); - if (entity_overrides == nullptr) { + // Let's recover vector of extruder overrides: + const WipingExtrusions::ExtruderPerCopy* entity_overrides = nullptr; + if (!layer_tools.has_extruder(correct_extruder_id)) { + // this entity is not overridden, but its extruder is not in layer_tools - we'll print it + // by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare + // extruders are eradicated from layer_tools) + correct_extruder_id = layer_tools.extruders.back(); + } + printing_extruders.clear(); + if (is_anything_overridden && use_overrides) { + entity_overrides = const_cast(layer_tools) + .wiping_extrusions() + .get_extruder_overrides(overrides_key, layer_to_print.original_object, correct_extruder_id, + layer_to_print.object()->instances().size()); + if (entity_overrides == nullptr) { + printing_extruders.emplace_back(correct_extruder_id); + } else { + printing_extruders.reserve(entity_overrides->size()); + for (int extruder : *entity_overrides) + printing_extruders.emplace_back(extruder >= 0 ? + // at least one copy is overridden to use this extruder + extruder : + // at least one copy would normally be printed with this extruder + // (see get_extruder_overrides function for explanation) + static_cast(-extruder - 1)); + Slic3r::sort_remove_duplicates(printing_extruders); + } + } else printing_extruders.emplace_back(correct_extruder_id); - } else { - printing_extruders.reserve(entity_overrides->size()); - for (int extruder : *entity_overrides) - printing_extruders.emplace_back(extruder >= 0 ? - // at least one copy is overridden to use this extruder - extruder : - // at least one copy would normally be printed with this extruder - // (see get_extruder_overrides function for explanation) - static_cast(-extruder - 1)); - Slic3r::sort_remove_duplicates(printing_extruders); + + // Now we must add this extrusion into the by_extruder map, once for each extruder that will print it: + for (unsigned int extruder : printing_extruders) { + std::vector& islands = + object_islands_by_extruder(by_extruder, extruder, layer_to_print_idx, layers.size(), n_slices + 1); + for (size_t i = 0; i <= n_slices; ++i) { + bool last = i == n_slices; + size_t island_idx = last ? n_slices : slices_test_order[i]; + if ( // extrusions->first_point does not fit inside any slice + last || + // extrusions->first_point fits inside ith slice + entity_matches_surface(island_idx, *current_extrusions)) { + 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, current_extrusions, + entity_overrides); + break; + } + } } - } else - printing_extruders.emplace_back(correct_extruder_id); - - // Now we must add this extrusion into the by_extruder map, once for each extruder that will print it: - for (unsigned int extruder : printing_extruders) { - std::vector& islands = - object_islands_by_extruder(by_extruder, extruder, layer_to_print_idx, layers.size(), n_slices + 1); - for (size_t i = 0; i <= n_slices; ++i) { - bool last = i == n_slices; - size_t island_idx = last ? n_slices : slices_test_order[i]; - if ( // extrusions->first_point does not fit inside any slice - last || - // extrusions->first_point fits inside ith slice - entity_matches_surface(island_idx, *filtered_extrusions)) { - 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, filtered_extrusions, - entity_overrides); - break; + }; + + bool split_mixed_perimeters = + entity_type == ObjectByExtruder::Island::Region::PERIMETERS && + region.config().outer_wall_filament_id.value != region.config().inner_wall_filament_id.value && + filtered_extrusions->role() == erMixed; + + if (split_mixed_perimeters) { + auto outer_perimeters = std::make_unique(); + auto inner_perimeters = std::make_unique(); + for (const ExtrusionEntity* entity : filtered_extrusions->entities) { + // ORCA: chaining may put an overhang path first and fully overhanging loops + // have no plain perimeter path: classify by scanning every path; anything without + // an inner perimeter path uses the outer wall filament (matches PerimeterGenerator's + // overhang flows). + bool has_external = false, has_internal = false; + auto classify = [&](const ExtrusionPaths& paths) { + for (const ExtrusionPath& path : paths) { + if (path.role() == erExternalPerimeter) + has_external = true; + else if (path.role() == erPerimeter) + has_internal = true; + } + }; + if (const auto* loop = dynamic_cast(entity)) + classify(loop->paths); + else if (const auto* multi_path = dynamic_cast(entity)) + classify(multi_path->paths); + else { + const ExtrusionRole role = entity->role(); + has_external = role == erExternalPerimeter; + has_internal = role == erPerimeter; } + if (has_external || !has_internal) + outer_perimeters->append(*entity); + else + inner_perimeters->append(*entity); + } + + if (!outer_perimeters->entities.empty()) { + split_perimeter_storage.emplace_back(std::move(outer_perimeters)); + process_extrusions(split_perimeter_storage.back().get(), nullptr, false); } + if (!inner_perimeters->entities.empty()) { + split_perimeter_storage.emplace_back(std::move(inner_perimeters)); + process_extrusions(split_perimeter_storage.back().get(), nullptr, false); + } + } else { + process_extrusions(filtered_extrusions, filtered_extrusions, true); } } } diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 811923daaed..46b9f931eaa 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -94,14 +94,18 @@ bool internal_solid_infill_uses_sparse_filament(const PrintRegion ®ion, Extru unsigned int sparse_infill_filament_id_1based(const PrintRegion ®ion) { - return region.config().sparse_infill_filament.value; + return region.config().sparse_infill_filament_id.value; } unsigned int infill_filament_id_1based(const LayerTools &layer_tools, const PrintRegion ®ion, ExtrusionRole role) { if (internal_solid_infill_uses_sparse_filament(region, role)) return sparse_infill_filament_id_1based(region); - return is_solid_infill(role) ? region.config().solid_infill_filament.value : sparse_infill_filament_id_1based(region); + if (role == erTopSolidInfill || role == erIroning) + return region.config().top_surface_filament_id.value; + if (role == erBottomSurface || role == erBridgeInfill) // ORCA: external bridges print as bottom surfaces (internal bridges stay internal solid) + return region.config().bottom_surface_filament_id.value; + return is_solid_infill(role) ? region.config().internal_solid_filament_id.value : sparse_infill_filament_id_1based(region); } unsigned int grouped_manual_pattern_mixed_filament_id_for_layer(const LayerTools& layer_tools, @@ -284,25 +288,48 @@ unsigned int LayerTools::resolve_mixed_1based(unsigned int filament_id) const } // Return a zero based extruder from the region, or extruder_override if overriden. -unsigned int LayerTools::wall_filament(const PrintRegion ®ion) const +unsigned int LayerTools::wall_extruder_id(const PrintRegion ®ion) const { - assert(region.config().wall_filament.value > 0); - unsigned int id = (this->extruder_override == 0) ? region.config().wall_filament.value : this->extruder_override; + assert(region.config().outer_wall_filament_id.value > 0); + unsigned int id = (this->extruder_override == 0) ? region.config().outer_wall_filament_id.value : this->extruder_override; return resolve_mixed_1based(id) - 1; } -unsigned int LayerTools::sparse_infill_filament(const PrintRegion ®ion) const +unsigned int LayerTools::inner_wall_extruder_id(const PrintRegion ®ion) const { - assert(region.config().wall_filament.value > 0); + assert(region.config().inner_wall_filament_id.value > 0); + unsigned int id = (this->extruder_override == 0) ? region.config().inner_wall_filament_id.value : this->extruder_override; + return resolve_mixed_1based(id) - 1; +} + +unsigned int LayerTools::sparse_infill_filament_id(const PrintRegion ®ion) const +{ + assert(region.config().sparse_infill_filament_id.value > 0); unsigned int id = (this->extruder_override == 0) ? sparse_infill_filament_id_1based(region) : this->extruder_override; 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 +unsigned int LayerTools::internal_solid_filament_id(const PrintRegion ®ion) const +{ + assert(region.config().internal_solid_filament_id.value > 0); + unsigned int id = (this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override; + 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::top_surface_filament_id(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; + assert(region.config().top_surface_filament_id.value > 0); + unsigned int id = (this->extruder_override == 0) ? region.config().top_surface_filament_id.value : this->extruder_override; + 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::bottom_surface_filament_id(const PrintRegion ®ion) const +{ + assert(region.config().bottom_surface_filament_id.value > 0); + unsigned int id = (this->extruder_override == 0) ? region.config().bottom_surface_filament_id.value : this->extruder_override; const unsigned int grouped = grouped_manual_pattern_infill_filament_1based(*this, region, id); return ((grouped != 0) ? grouped : resolve_mixed_1based(id)) - 1; } @@ -310,16 +337,41 @@ unsigned int LayerTools::solid_infill_filament(const PrintRegion ®ion) const // Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden. unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion ®ion) const { - assert(region.config().wall_filament.value > 0); - assert(region.config().sparse_infill_filament.value > 0); - assert(region.config().solid_infill_filament.value > 0); + assert(region.config().outer_wall_filament_id.value > 0); + assert(region.config().inner_wall_filament_id.value > 0); + assert(region.config().sparse_infill_filament_id.value > 0); + assert(region.config().internal_solid_filament_id.value > 0); + assert(region.config().top_surface_filament_id.value > 0); + assert(region.config().bottom_surface_filament_id.value > 0); if (extrusions.has_infill()) { - const ExtrusionRole role = extrusions.entities.empty() ? erNone : extrusions.entities.front()->role(); + ExtrusionRole role = extrusions.entities.empty() ? erNone : extrusions.entities.front()->role(); + // gap fill inherits the filament of the surface it fills; derive the role from the first non-gap-fill entity (must match ToolOrdering::collect_extruders()). + if (role == erGapFill) + for (const ExtrusionEntity *ee : extrusions.entities) + if (ee->role() != erGapFill) { + role = ee->role(); + break; + } 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 sparse_infill_filament_id(region); + if (extrusions.has_solid_infill()) { + ExtrusionRole solid_role = extrusions.role(); + // gap fill inherits the filament of the surface it fills; derive the role from the first non-gap-fill entity (must match ToolOrdering::collect_extruders()). + if (solid_role == erMixed) + for (const ExtrusionEntity *ee : extrusions.entities) + if (ee->role() != erGapFill) { + solid_role = ee->role(); + break; + } + if (solid_role == erTopSolidInfill || solid_role == erIroning) + return top_surface_filament_id(region); + if (solid_role == erBottomSurface || solid_role == erBridgeInfill) // ORCA: external bridges print as bottom surfaces (internal bridges stay internal solid) + return bottom_surface_filament_id(region); + return internal_solid_filament_id(region); + } + return sparse_infill_filament_id(region); } - return wall_filament(region); + return extrusions.role() == erPerimeter ? inner_wall_extruder_id(region) : wall_extruder_id(region); } static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height) @@ -564,8 +616,8 @@ std::vector ToolOrdering::generate_first_layer_tool_order(const Pr for (auto object : print.objects()) { auto first_layer = object->get_layer(0); for (auto layerm : first_layer->regions()) { - int extruder_id = layerm->region().config().option("wall_filament")->getInt(); - + int extruder_id = layerm->region().config().option("outer_wall_filament_id")->getInt(); + for (auto expoly : layerm->raw_slices) { const double nozzle_diameter = print.config().nozzle_diameter.get_at(0); const coordf_t initial_layer_line_width = print.config().get_abs_value("initial_layer_line_width", nozzle_diameter); @@ -610,7 +662,7 @@ std::vector ToolOrdering::generate_first_layer_tool_order(const Pr std::map min_areas_per_extruder; auto first_layer = object.get_layer(0); for (auto layerm : first_layer->regions()) { - int extruder_id = layerm->region().config().option("wall_filament")->getInt(); + int extruder_id = layerm->region().config().option("outer_wall_filament_id")->getInt(); for (auto expoly : layerm->raw_slices) { const double nozzle_diameter = object.print()->config().nozzle_diameter.get_at(0); const coordf_t line_width = object.config().get_abs_value("line_width", nozzle_diameter); @@ -693,6 +745,26 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto float(support_layer->print_z), float(support_layer->height), &object); + // support nozzle diameter restriction, resolve a "default" (0) support filament to a nozzle-matching one here, preferring a filament this layer already prints with to save toolchanges. + // Note: this fork collects support before the object's own layers, so the preference scan + // only sees filaments of previously collected objects and otherwise settles on the + // deterministic resolved_default_support_filament(); GCode::process_layer() re-prefers + // layer-scheduled extruders for "don't care" support when the G-code is emitted. + if (object.config().support_nozzle_diameter.value > 0.) { + auto restrict_default_filament = [&object, &layer_tools](unsigned int configured) -> unsigned int { + if (configured != 0) + return configured; + const PrintConfig &print_config = object.print()->config(); + for (unsigned int filament : layer_tools.extruders) // 1 based at this point + if (filament > 0 && object.support_filament_allowed(filament) && + ! print_config.filament_soluble.get_at(filament - 1)) + return filament; + unsigned int resolved = object.resolved_default_support_filament(); + return resolved > 0 ? resolved : configured; + }; + extruder_support = restrict_default_filament(extruder_support); + extruder_interface = restrict_default_filament(extruder_interface); + } if (has_support) layer_tools.extruders.push_back(extruder_support); if (has_interface) @@ -743,52 +815,79 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto } if (something_nonoverriddable){ - 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), &object); - 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.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 && - std::find(firstLayerExtruders.begin(), firstLayerExtruders.end(), int(extruder_id)) == firstLayerExtruders.end()) - firstLayerExtruders.emplace_back(int(extruder_id)); + // Emplace a wall filament, expanding a grouped manual-pattern mixed filament + // into its per-layer ordered physical extruders (both walls need this so the + // wipe tower planner sees every extruder the grouped split will emit). + auto emplace_wall_filament = [&](unsigned int configured_wall, bool first_layer_candidate) { + unsigned int wall_ext = resolve_mixed(configured_wall, layerCount, float(layer->print_z), float(layer->height), &object); + 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.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 (first_layer_candidate && layerCount == 0 && + std::find(firstLayerExtruders.begin(), firstLayerExtruders.end(), int(extruder_id)) == firstLayerExtruders.end()) + firstLayerExtruders.emplace_back(int(extruder_id)); + } + return; } - } 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) + if (first_layer_candidate && layerCount == 0) firstLayerExtruders.emplace_back(wall_ext); - } + }; + emplace_wall_filament((extruder_override == 0) ? region.config().outer_wall_filament_id.value : extruder_override, true); + // alternate_extra_wall should add an inner loop on odd layers (mirrors PerimeterGenerator's loop_number and the spiral gate of LayerRegion::make_perimeters()); layers dropping the loop again (only_one_wall_top / only_one_wall_first_layer) may reserve the filament unused, which merely costs a toolchange. + const bool spiral_vase_layer = object.print()->config().spiral_mode.value && + layer->id() >= size_t(region.config().bottom_shell_layers.value) && + layer->print_z >= region.config().bottom_shell_thickness - EPSILON; + if (extruder_override == 0 && + (region.config().wall_loops.value > 1 || + (region.config().alternate_extra_wall.value && layer->id() % 2 == 1 && + region.config().sparse_infill_density.value > 0 && !spiral_vase_layer))) + emplace_wall_filament(region.config().inner_wall_filament_id.value, false); } layer_tools.has_object = true; } - bool has_sparse_infill = false; - bool has_solid_infill = false; + bool has_sparse_infill = false; + bool has_internal_solid = false; + bool has_top_solid_surface = false; + bool has_bottom_surface = false; + bool has_pure_gap_fill = false; bool something_nonoverriddable = false; for (const ExtrusionEntity *ee : layerm->fills.entities) { // fill represents infill extrusions of a single island. const auto *fill = dynamic_cast(ee); ExtrusionRole role = fill->entities.empty() ? erNone : fill->entities.front()->role(); + // gap fill inherits its surface's filament; classify by the first non-gap-fill entity (must match LayerTools::extruder()). + if (role == erGapFill) + for (const ExtrusionEntity *fill_entity : fill->entities) + if (fill_entity->role() != erGapFill) { + role = fill_entity->role(); + break; + } if (internal_solid_infill_uses_sparse_filament(region, role)) has_sparse_infill = true; + else if (role == erTopSolidInfill || role == erIroning) + has_top_solid_surface = true; + else if (role == erBottomSurface || role == erBridgeInfill) // ORCA: external bridges print as bottom surfaces + has_bottom_surface = true; else if (is_solid_infill(role)) - has_solid_infill = true; + has_internal_solid = true; + else if (role == erGapFill) + // perimeter-generated gap fill with no sibling surface dispatches to the outer wall filament. + has_pure_gap_fill = true; else if (role != erNone) has_sparse_infill = true; @@ -800,13 +899,23 @@ 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(layer_tools.solid_infill_filament(region) + 1); + if (has_internal_solid) { + layer_tools.extruders.emplace_back(layer_tools.internal_solid_filament_id(region) + 1); + } + if (has_top_solid_surface) { + layer_tools.extruders.emplace_back(layer_tools.top_surface_filament_id(region) + 1); + } + if (has_bottom_surface) { + layer_tools.extruders.emplace_back(layer_tools.bottom_surface_filament_id(region) + 1); } if (has_sparse_infill) { - layer_tools.extruders.emplace_back(layer_tools.sparse_infill_filament(region) + 1); + layer_tools.extruders.emplace_back(layer_tools.sparse_infill_filament_id(region) + 1); } - } else if (has_solid_infill || has_sparse_infill) { + if (has_pure_gap_fill) { + // perimeter-generated gap fill with no sibling surface uses the outer wall filament. + layer_tools.extruders.emplace_back(layer_tools.wall_extruder_id(region) + 1); + } + } else if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_sparse_infill || has_pure_gap_fill) { layer_tools.extruders.emplace_back(resolve_mixed(extruder_override, layerCount, float(layer->print_z), @@ -814,7 +923,7 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto &object)); } } - if (has_solid_infill || has_sparse_infill) + if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_sparse_infill || has_pure_gap_fill) layer_tools.has_object = true; } layerCount++; @@ -1060,12 +1169,23 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ // and maybe other problems. We will therefore go through layer_tools and detect and fix this. // So, if there is a non-object layer starting with different extruder than the last one ended with (or containing more than one extruder), // we'll mark it with has_wipe tower. + // Per-extruder layer height combines layers away, leaving many LayerTools empty; skip them and compare against the last printing layer. + // Without the feature keep the historic behavior of stopping at the first empty layer. + const bool skip_empty_layer_tools = std::any_of(config.extruder_layer_height.values.begin(), + config.extruder_layer_height.values.end(), [](double h) { return h > 0.; }); + const LayerTools *lt_last_printing = nullptr; for (unsigned int i=0; i+1 1)) + if (lt.extruders.empty() || lt_next.extruders.empty()) { + if (! skip_empty_layer_tools) + break; + if (lt_last_printing == nullptr || lt_next.extruders.empty()) + continue; + } + if (!lt_next.has_wipe_tower && (lt_next.extruders.front() != lt_last_printing->extruders.back() || lt_next.extruders.size() > 1)) lt_next.has_wipe_tower = true; // We should also check that the next wipe tower layer is no further than max_layer_height: unsigned int j = i+1; @@ -1405,12 +1525,37 @@ int WipingExtrusions::last_nonsoluble_extruder_on_layer(const PrintConfig& print return (-1); } +// diameter of the nozzle a 0-based filament prints through. +// On classic multi-tool printers the filament index is the extruder index. +static double filament_nozzle_diameter(const Print &print, unsigned int filament_id) +{ + return print.config().nozzle_diameter.get_at(filament_id); +} + // Decides whether this entity could be overridden bool WipingExtrusions::is_overriddable(const ExtrusionEntityCollection& eec, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const { - if (print_config.filament_soluble.get_at(m_layer_tools->extruder(eec, region))) + const unsigned int intended_filament = m_layer_tools->extruder(eec, region); + if (print_config.filament_soluble.get_at(intended_filament)) return false; + // Entity widths/heights were computed for its filament's nozzle, so only a filament with the same nozzle diameter may take it over; with no candidate it must keep its own filament. + if (print_config.nozzle_diameter.values.size() > 1) { + const Print &print = *object.print(); + const double intended_nozzle = filament_nozzle_diameter(print, intended_filament); + bool has_candidate = false; + for (size_t filament = 0; filament < print_config.filament_soluble.values.size(); ++ filament) + if (filament != intended_filament && + !print_config.filament_soluble.get_at(filament) && + !print_config.filament_is_support.get_at(filament) && + std::abs(filament_nozzle_diameter(print, (unsigned int)filament) - intended_nozzle) < EPSILON) { + has_candidate = true; + break; + } + if (!has_candidate) + return false; + } + if (object.config().flush_into_objects) return true; @@ -1504,10 +1649,15 @@ float WipingExtrusions::mark_wiping_extrusions(const Print& print, unsigned int if (!is_overriddable(*fill, print.config(), *object, region)) continue; + // Only wipe into entities computed for this extruder's nozzle diameter. + if (std::abs(filament_nozzle_diameter(print, lt.extruder(*fill, region)) - + filament_nozzle_diameter(print, new_extruder)) > EPSILON) + continue; + if (wipe_into_infill_only && ! is_infill_first) // In this case we must check that the original extruder is used on this layer before the one we are overridding // (and the perimeters will be finished before the infill is printed): - if (!lt.is_extruder_order(lt.wall_filament(region), new_extruder)) + if (!lt.is_extruder_order(lt.wall_extruder_id(region), new_extruder)) continue; if ((!is_entity_overridden(fill, object, copy) && fill->total_volume() > min_infill_volume)) @@ -1525,7 +1675,10 @@ float WipingExtrusions::mark_wiping_extrusions(const Print& print, unsigned int { for (const ExtrusionEntity* ee : layerm->perimeters.entities) { auto* fill = dynamic_cast(ee); - if (is_overriddable(*fill, print.config(), *object, region) && !is_entity_overridden(fill, object, copy) && fill->total_volume() > min_infill_volume) { + if (is_overriddable(*fill, print.config(), *object, region) && !is_entity_overridden(fill, object, copy) && fill->total_volume() > min_infill_volume && + // nozzle diameter must match + std::abs(filament_nozzle_diameter(print, lt.extruder(*fill, region)) - + filament_nozzle_diameter(print, new_extruder)) < EPSILON) { set_extruder_override(fill, object, copy, new_extruder, num_of_copies); if ((volume_to_wipe -= float(fill->total_volume())) <= 0.f) // More material was purged already than asked for. @@ -1544,8 +1697,9 @@ float WipingExtrusions::mark_wiping_extrusions(const Print& print, unsigned int if (this_support_layer == nullptr) break; - bool support_overriddable = object_config.support_filament == 0; - bool support_intf_overriddable = object_config.support_interface_filament == 0; + // Only a nozzle matching the object's support nozzle diameter restriction may flush into its support. + bool support_overriddable = object_config.support_filament == 0 && object->support_filament_allowed(new_extruder + 1); + bool support_intf_overriddable = object_config.support_interface_filament == 0 && object->support_filament_allowed(new_extruder + 1); if (!support_overriddable && !support_intf_overriddable) break; @@ -1595,6 +1749,25 @@ void WipingExtrusions::ensure_perimeters_infills_order(const Print& print) unsigned int first_nonsoluble_extruder = first_nonsoluble_extruder_on_layer(print.config()); unsigned int last_nonsoluble_extruder = last_nonsoluble_extruder_on_layer(print.config()); + // Prefer filaments printing through the nozzle diameter the entity's widths were computed for; returns -1 when the layer offers none. + auto nonsoluble_matching_extruder = [this, &print](bool first, unsigned int intended_filament) -> int { + const PrintConfig &config = print.config(); + const double intended_nozzle = filament_nozzle_diameter(print, intended_filament); + auto matches = [&](unsigned int filament) { + return !config.filament_soluble.get_at(filament) && !config.filament_is_support.get_at(filament) && + std::abs(filament_nozzle_diameter(print, filament) - intended_nozzle) < EPSILON; + }; + const std::vector &extruders = m_layer_tools->extruders; + if (first) { + for (auto it = extruders.begin(); it != extruders.end(); ++ it) + if (matches(*it)) return int(*it); + } else { + for (auto it = extruders.rbegin(); it != extruders.rend(); ++ it) + if (matches(*it)) return int(*it); + } + return -1; + }; + for (const PrintObject* object : print.objects()) { // Finds this layer: const Layer* this_layer = object->get_layer_at_printz(lt.print_z, EPSILON); @@ -1625,9 +1798,14 @@ void WipingExtrusions::ensure_perimeters_infills_order(const Print& print) if (is_infill_first //BBS //|| object->config().flush_into_objects // in this case the perimeter is overridden, so we can override by the last one safely - || lt.is_extruder_order(lt.wall_filament(region), last_nonsoluble_extruder // !infill_first, but perimeter is already printed when last extruder prints - || ! lt.has_extruder(lt.sparse_infill_filament(region)))) // we have to force override - this could violate infill_first (FIXME) - set_extruder_override(fill, object, copy, (is_infill_first ? first_nonsoluble_extruder : last_nonsoluble_extruder), num_of_copies); + || lt.is_extruder_order(lt.wall_extruder_id(region), last_nonsoluble_extruder // !infill_first, but perimeter is already printed when last extruder prints + || ! lt.has_extruder(lt.sparse_infill_filament_id(region)))) { // we have to force override - this could violate infill_first (FIXME) + // Prefer a nozzle-matching filament; fall back so the entity still prints. + int flush_extruder = nonsoluble_matching_extruder(is_infill_first, lt.extruder(*fill, region)); + if (flush_extruder < 0) + flush_extruder = is_infill_first ? first_nonsoluble_extruder : last_nonsoluble_extruder; + set_extruder_override(fill, object, copy, flush_extruder, num_of_copies); + } else { // In this case we can (and should) leave it to be printed normally. // Force overriding would mean it gets printed before its perimeter. @@ -1637,8 +1815,13 @@ void WipingExtrusions::ensure_perimeters_infills_order(const Print& print) // Now the same for perimeters - see comments above for explanation: for (const ExtrusionEntity* ee : layerm->perimeters.entities) { // iterate through all perimeter Collections auto* fill = dynamic_cast(ee); - if (is_overriddable(*fill, print.config(), *object, region) && ! is_entity_overridden(fill, object, copy)) - set_extruder_override(fill, object, copy, (is_infill_first ? last_nonsoluble_extruder : first_nonsoluble_extruder), num_of_copies); + if (is_overriddable(*fill, print.config(), *object, region) && ! is_entity_overridden(fill, object, copy)) { + // Same nozzle-matching preference as above. + int flush_extruder = nonsoluble_matching_extruder(!is_infill_first, lt.extruder(*fill, region)); + if (flush_extruder < 0) + flush_extruder = is_infill_first ? last_nonsoluble_extruder : first_nonsoluble_extruder; + set_extruder_override(fill, object, copy, flush_extruder, num_of_copies); + } } } } diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 070b4c4d210..996d4cdca58 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -106,9 +106,12 @@ class LayerTools bool has_extruder(unsigned int extruder) const { return std::find(this->extruders.begin(), this->extruders.end(), extruder) != this->extruders.end(); } // Return a zero based extruder from the region, or extruder_override if overriden. - unsigned int wall_filament(const PrintRegion ®ion) const; - unsigned int sparse_infill_filament(const PrintRegion ®ion) const; - unsigned int solid_infill_filament(const PrintRegion ®ion) const; + unsigned int wall_extruder_id(const PrintRegion ®ion) const; + unsigned int inner_wall_extruder_id(const PrintRegion ®ion) const; + unsigned int sparse_infill_filament_id(const PrintRegion ®ion) const; + unsigned int internal_solid_filament_id(const PrintRegion ®ion) const; + unsigned int top_surface_filament_id(const PrintRegion ®ion) const; + unsigned int bottom_surface_filament_id(const PrintRegion ®ion) const; // Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden. unsigned int extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion ®ion) const; diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 6a06416fd37..bf222eca77a 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1422,7 +1422,10 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config) if (max_vol_speed != 0.f) m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); - m_perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio; // all extruders are now assumed to have the same diameter + // ORCA: every tool purges with a width matching its own nozzle; the tower's geometric layout + // (m_perimeter_width) uses the widest configured nozzle, not whichever was configured last. + m_filpar[idx].perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio; + m_perimeter_width = idx == 0 ? m_filpar[idx].perimeter_width : std::max(m_perimeter_width, m_filpar[idx].perimeter_width); if (m_semm) { std::istringstream stream{config.filament_ramming_parameters.get_at(idx)}; @@ -1494,7 +1497,7 @@ std::vector WipeTower2::prime( for (size_t idx_tool = 0; idx_tool < tools.size(); ++idx_tool) { size_t old_tool = m_current_tool; - WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); + WipeTowerWriter2 writer(m_layer_height, tool_perimeter_width(m_current_tool), m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); writer.set_extrusion_flow(m_extrusion_flow).set_z(m_z_pos).set_initial_tool(m_current_tool); writer.set_is_prime(true); @@ -1588,7 +1591,7 @@ WipeTower::ToolChangeResult WipeTower2::emit_planned_tool_change(const WipeTower (!is_no_tool_sentinel(tool) ? wipe_area + m_depth_traversed - 0.5f * m_perimeter_width : m_wipe_tower_depth - m_perimeter_width)); - WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); + WipeTowerWriter2 writer(m_layer_height, tool_perimeter_width(m_current_tool), m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) @@ -1701,7 +1704,7 @@ WipeTower::ToolChangeResult WipeTower2::local_z_tool_change(size_t new_tool, { const size_t old_tool = m_current_tool; - WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); + WipeTowerWriter2 writer(m_layer_height, tool_perimeter_width(m_current_tool), m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) @@ -1758,7 +1761,8 @@ WipeTower::ToolChangeResult WipeTower2::local_z_tool_change(size_t new_tool, float WipeTower2::predict_ramming_end_x(int old_tool, float layer_height) const { - float line_width = m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator; + // Must match the per-tool ramming line width used by toolchange_Unload(). + float line_width = tool_perimeter_width(old_tool) * m_filpar[old_tool].ramming_line_width_multiplicator; // Same xl/xr as Unload, using actual ramming line_width for the offset float xl = m_perimeter_width / 2.f + 1.f * line_width; @@ -1798,7 +1802,7 @@ void WipeTower2::toolchange_Unload(WipeTowerWriter2& writer, float xl = cleaning_box.ld.x() + 1.f * m_perimeter_width; float xr = cleaning_box.rd.x() - 1.f * m_perimeter_width; - const float line_width = m_perimeter_width * + const float line_width = tool_perimeter_width(m_current_tool) * m_filpar[m_current_tool].ramming_line_width_multiplicator; // desired ramming line thickness const float y_step = line_width * m_filpar[m_current_tool].ramming_step_multiplicator * m_extra_spacing_ramming; // spacing between lines in mm @@ -1886,7 +1890,7 @@ void WipeTower2::toolchange_Unload(WipeTowerWriter2& writer, } } Vec2f end_of_ramming(writer.x(), writer.y()); - writer.change_analyzer_line_width(m_perimeter_width); // so the next lines are not affected by ramming_line_width_multiplier + writer.change_analyzer_line_width(tool_perimeter_width(m_current_tool)); // so the next lines are not affected by ramming_line_width_multiplier // Retraction: if (m_enable_filament_ramming) @@ -2034,6 +2038,8 @@ void WipeTower2::toolchange_Change(WipeTowerWriter2& writer, const size_t new_to writer.flush_planner_queue(); m_current_tool = new_tool; + // The new toolhead may have a different nozzle, so the extrusion flow must follow the tool change. + m_extrusion_flow = extrusion_flow(m_layer_height); } void WipeTower2::toolchange_Load(WipeTowerWriter2& writer, const WipeTower::box_coordinates& cleaning_box) @@ -2071,14 +2077,15 @@ void WipeTower2::toolchange_Wipe(WipeTowerWriter2& writer, const WipeTower::box_ const float& xr = cleaning_box.rd.x(); writer.set_extrusion_flow(m_extrusion_flow * m_extra_flow); - const float line_width = m_perimeter_width * m_extra_flow; + // Purge lines use the tool's own width; row spacing dy keeps m_perimeter_width so consumed depth matches the plan. + const float line_width = tool_perimeter_width(m_current_tool) * m_extra_flow; writer.change_analyzer_line_width(line_width); // Variables x_to_wipe and traversed_x are here to be able to make sure it always wipes at least // the ordered volume, even if it means violating the box. This can later be removed and simply // wipe until the end of the assigned area. - float x_to_wipe = volume_to_length(wipe_volume, m_perimeter_width, m_layer_height) / m_extra_flow; + float x_to_wipe = volume_to_length(wipe_volume, tool_perimeter_width(m_current_tool), m_layer_height) / m_extra_flow; float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; // Don't use the extra spacing for the first layer, but do use the spacing resulting from increased flow. // All the calculations in all other places take the spacing into account for all the layers. @@ -2192,7 +2199,7 @@ void WipeTower2::toolchange_Wipe(WipeTowerWriter2& writer, const WipeTower::box_ } writer.set_extrusion_flow(m_extrusion_flow); // Reset the extrusion flow. - writer.change_analyzer_line_width(m_perimeter_width); + writer.change_analyzer_line_width(tool_perimeter_width(m_current_tool)); } WipeTower::ToolChangeResult WipeTower2::finish_layer() @@ -2202,7 +2209,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer() size_t old_tool = m_current_tool; - WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); + WipeTowerWriter2 writer(m_layer_height, tool_perimeter_width(m_current_tool), m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); writer.set_extrusion_flow(m_extrusion_flow) .set_z(m_z_pos) .set_initial_tool(m_current_tool) @@ -2424,12 +2431,13 @@ std::vector> WipeTower2::extract_wipe_volumes(const PrintConf return wipe_volumes; } -static float get_wipe_depth(float volume, float layer_height, float perimeter_width, float extra_flow, float extra_spacing, float width) +// purge_line_width is what the purging tool extrudes, row_spacing_width the tower's geometric row pitch - they differ on printers with mixed nozzle diameters. +static float get_wipe_depth(float volume, float layer_height, float purge_line_width, float row_spacing_width, float extra_flow, float extra_spacing, float width) { - float length_to_extrude = (volume_to_length(volume, perimeter_width, layer_height)) / extra_flow; + float length_to_extrude = (volume_to_length(volume, purge_line_width, layer_height)) / extra_flow; length_to_extrude = std::max(length_to_extrude, 0.f); - return (int(length_to_extrude / width) + 1) * perimeter_width * extra_spacing; + return (int(length_to_extrude / width) + 1) * row_spacing_width * extra_spacing; } // Appends a toolchange into m_plan and calculates neccessary depth of the corresponding box @@ -2447,7 +2455,8 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i return; // this is an actual toolchange - let's calculate depth to reserve on the wipe tower - float ramming_lw = m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator; + // ramming uses the OLD tool's own line width and the wipe the NEW tool's; the planned depths must match the widths toolchange_Unload() / toolchange_Wipe() will actually use. + float ramming_lw = tool_perimeter_width(old_tool) * m_filpar[old_tool].ramming_line_width_multiplicator; float width = m_wipe_tower_width - m_perimeter_width - 2 * ramming_lw; // Orca: For non-SEMM multi-toolhead, ramming_speed contains only flow (not speed), so 0.25f * flow is meaningless. // Use the actual multitool_ramming_volume instead. @@ -2461,9 +2470,9 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i m_extra_spacing_ramming; float first_wipe_line = -(width * ((length_to_extrude / width) - int(length_to_extrude / width)) - width); - float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height_par); - float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height_par, m_perimeter_width, m_extra_flow, - m_extra_spacing_wipe, width); + float first_wipe_volume = length_to_volume(first_wipe_line, tool_perimeter_width(new_tool) * m_extra_flow, layer_height_par); + float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height_par, tool_perimeter_width(new_tool), + m_perimeter_width, m_extra_flow, m_extra_spacing_wipe, width); m_plan.back().tool_changes.push_back( WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume)); @@ -2482,7 +2491,8 @@ void WipeTower2::plan_local_z_toolchange(float z_par, float layer_height_par, un if (old_tool == new_tool) return; - float ramming_lw = m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator; + // ramming uses the OLD tool's own line width and the wipe the NEW tool's (see plan_toolchange()). + float ramming_lw = tool_perimeter_width(old_tool) * m_filpar[old_tool].ramming_line_width_multiplicator; float width = m_wipe_tower_width - m_perimeter_width - 2 * ramming_lw; float ramming_volume = m_semm ? 0.25f * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f) @@ -2494,9 +2504,9 @@ void WipeTower2::plan_local_z_toolchange(float z_par, float layer_height_par, un m_extra_spacing_ramming; float first_wipe_line = -(width * ((length_to_extrude / width) - int(length_to_extrude / width)) - width); - float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height_par); - float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height_par, m_perimeter_width, m_extra_flow, - m_extra_spacing_wipe, width); + float first_wipe_volume = length_to_volume(first_wipe_line, tool_perimeter_width(new_tool) * m_extra_flow, layer_height_par); + float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height_par, tool_perimeter_width(new_tool), + m_perimeter_width, m_extra_flow, m_extra_spacing_wipe, width); m_plan.back().local_z_tool_changes.push_back( WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume)); @@ -2514,9 +2524,15 @@ void WipeTower2::plan_local_z_reserve(float z_par, float layer_height_par, size_ const float mini_wipe_depth = m_local_z_wipe_tower_purge_lines * m_perimeter_width * m_extra_spacing_wipe; const float wipe_width = std::max(0.f, m_wipe_tower_width - 3.f * m_perimeter_width); + // The reserving slot does not know which tool will purge into it; plan the wipe length with the + // narrowest tool line width (worst case) while keeping the geometric row pitch (m_perimeter_width). + float min_tool_perimeter_width = m_perimeter_width; + for (const FilamentParameters& filament : m_filpar) + if (filament.perimeter_width > 0.f) + min_tool_perimeter_width = std::min(min_tool_perimeter_width, filament.perimeter_width); const float wiping_depth = wipe_width > WT_EPSILON ? - get_wipe_depth(std::max(0.f, wipe_volume), layer_height_par, m_perimeter_width, m_extra_flow, - m_extra_spacing_wipe, wipe_width) : + get_wipe_depth(std::max(0.f, wipe_volume), layer_height_par, min_tool_perimeter_width, + m_perimeter_width, m_extra_flow, m_extra_spacing_wipe, wipe_width) : 0.f; float max_ramming_depth = 0.f; @@ -2526,9 +2542,11 @@ void WipeTower2::plan_local_z_reserve(float z_par, float layer_height_par, size_ if (!do_ramming || filament.ramming_speed.empty()) continue; - const float line_width = m_perimeter_width * filament.ramming_line_width_multiplicator; + // Each tool rams with its own line width (see toolchange_Unload()). + const float tool_width = filament.perimeter_width > 0.f ? filament.perimeter_width : m_perimeter_width; + const float line_width = tool_width * filament.ramming_line_width_multiplicator; const float line_step = - (m_perimeter_width * filament.ramming_line_width_multiplicator * filament.ramming_step_multiplicator) * + (tool_width * filament.ramming_line_width_multiplicator * filament.ramming_step_multiplicator) * m_extra_spacing_ramming; if (line_width <= WT_EPSILON || line_step <= WT_EPSILON) continue; @@ -2597,15 +2615,17 @@ void WipeTower2::save_on_last_wipe() if (i == idx) { float width = m_wipe_tower_width - 3 * m_perimeter_width; // width we draw into - float volume_to_save = length_to_volume(finish_layer().total_extrusion_length_in_plane(), m_perimeter_width, + // ORCA: the finish layer and the wipe are extruded by the new tool with its own line width. + float volume_to_save = length_to_volume(finish_layer().total_extrusion_length_in_plane(), + tool_perimeter_width(toolchange.new_tool), m_layer_info->height); float volume_left_to_wipe = std::max(m_filpar[toolchange.new_tool].filament_minimal_purge_on_wipe_tower, toolchange.wipe_volume_total - volume_to_save); float volume_we_need_depth_for = std::max(0.f, volume_left_to_wipe - length_to_volume(toolchange.first_wipe_line, - m_perimeter_width * m_extra_flow, + tool_perimeter_width(toolchange.new_tool) * m_extra_flow, m_layer_info->height)); - float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, - m_extra_spacing_wipe, width); + float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, tool_perimeter_width(toolchange.new_tool), + m_perimeter_width, m_extra_flow, m_extra_spacing_wipe, width); toolchange.required_depth = toolchange.ramming_depth + depth_to_wipe; toolchange.wipe_volume = volume_left_to_wipe; @@ -2930,7 +2950,8 @@ WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(int old_tool, i float ramming_volume = m_semm ? 0.25f * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f) : m_filpar[old_tool].multitool_ramming_volume; - float line_width = m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator; + // ramming uses the OLD tool's own line width and the wipe the NEW tool's (see plan_toolchange()). + float line_width = tool_perimeter_width(old_tool) * m_filpar[old_tool].ramming_line_width_multiplicator; float width = m_wipe_tower_width - m_perimeter_width - 2 * line_width; if (std::fabs(width) < EPSILON) { assert(false); @@ -2943,9 +2964,9 @@ WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(int old_tool, i float ramming_depth = num_lines * line_width * m_filpar[old_tool].ramming_step_multiplicator * m_extra_spacing_ramming; float first_wipe_line = -(width * ((length_to_extrude / width) - int(length_to_extrude / width)) - width); - float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height); + float first_wipe_volume = length_to_volume(first_wipe_line, tool_perimeter_width(new_tool) * m_extra_flow, layer_height); float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height, - m_perimeter_width, m_extra_flow, m_extra_spacing_wipe, width); + tool_perimeter_width(new_tool), m_perimeter_width, m_extra_flow, m_extra_spacing_wipe, width); return WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume); } diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 459beddb057..aa23c553cd8 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -159,6 +159,8 @@ class WipeTower2 float max_e_speed = std::numeric_limits::max(); std::vector ramming_speed; float nozzle_diameter; + // ORCA: tower line width this tool extrudes (nozzle_diameter * Width_To_Nozzle_Ratio). + float perimeter_width = 0.f; float filament_area; bool multitool_ramming; float multitool_ramming_time = 0.f; @@ -185,6 +187,10 @@ class WipeTower2 float filament_area() const { return m_filpar[0].filament_area; // all extruders are assumed to have the same filament diameter at this point } + // Line width the given tool extrudes on the tower; m_perimeter_width (widest nozzle) keeps the geometric layout: line spacing, box sizes, depth planning. + float tool_perimeter_width(size_t tool) const { + return tool < m_filpar.size() && m_filpar[tool].perimeter_width > 0.f ? m_filpar[tool].perimeter_width : m_perimeter_width; + } bool m_change_pressure = true; float m_change_pressure_value = 0.0; @@ -272,12 +278,13 @@ class WipeTower2 bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; } - // Calculates extrusion flow needed to produce required line width for given layer height + // Calculates extrusion flow needed to produce required line width for given layer height. + // ORCA: uses the current tool's own line width, matching its nozzle. float extrusion_flow(float layer_height = -1.f) const // negative layer_height - return current m_extrusion_flow { if ( layer_height < 0 ) return m_extrusion_flow; - return layer_height * ( m_perimeter_width - layer_height * (1.f-float(M_PI)/4.f)) / filament_area(); + return layer_height * ( tool_perimeter_width(m_current_tool) - layer_height * (1.f-float(M_PI)/4.f)) / filament_area(); } diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index e428a1673bd..33665a525db 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -74,6 +74,21 @@ LayerRegion* Layer::add_region(const PrintRegion *print_region) return m_regions.back(); } +// ORCA: per-extruder layer height, see PrintObject::apply_extruder_layer_heights(). +double LayerRegion::combined_height() const +{ + return m_combined_height > 0. ? m_combined_height : m_layer->height; +} + +const Layer* LayerRegion::combined_lower_layer() const +{ + const Layer *layer = m_layer; + // count == 0 (layer combined away into a group top) is treated as 1; it produces no extrusions anyway. + for (unsigned short i = 0, count = std::max(m_combined_layer_count, 1); layer != nullptr && i < count; ++ i) + layer = layer->lower_layer; + return layer; +} + // merge all regions' slices to get islands void Layer::make_slices() { @@ -181,7 +196,8 @@ bool Layer::is_perimeter_compatible(const PrintRegion& a, const PrintRegion& b) const PrintRegionConfig& config = a.config(); const PrintRegionConfig& other_config = b.config(); - return config.wall_filament == other_config.wall_filament + return config.outer_wall_filament_id == other_config.outer_wall_filament_id + && config.inner_wall_filament_id == other_config.inner_wall_filament_id && config.wall_loops == other_config.wall_loops && config.wall_sequence == other_config.wall_sequence && config.is_infill_first == other_config.is_infill_first @@ -242,7 +258,9 @@ void Layer::make_perimeters() if (! (*it)->slices.empty()) { LayerRegion* other_layerm = *it; const PrintRegion &other_region = other_layerm->region(); - if (is_perimeter_compatible(this_region, other_region)) + // Regions combined to different extruder layer heights extrude with different heights and must not share a make_perimeters() call. + if ((*layerm)->combined_layer_count() == other_layerm->combined_layer_count() && + is_perimeter_compatible(this_region, other_region)) { other_layerm->perimeters.clear(); other_layerm->fills.clear(); diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index b62144a2fea..cc3021f7dd5 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -16,8 +16,15 @@ using LayerPtrs = std::vector; class LayerRegion; using LayerRegionPtrs = std::vector; class PrintRegion; +class PrintRegionConfig; class PrintObject; +// Snapmaker mixed filament: resolve a configured 1-based filament id to the physical filament +// actually printing on this layer (0 "Default" passes through unchanged). The infill variant +// additionally resolves grouped manual patterns like the innermost perimeter. +unsigned int effective_layer_filament_id(const Layer &layer, unsigned int filament_id); +unsigned int effective_infill_filament_id(const Layer &layer, const PrintRegionConfig &config, unsigned int filament_id); + namespace FillAdaptive { struct Octree; }; @@ -74,8 +81,11 @@ class LayerRegion unsigned int extruder(FlowRole role) const; Flow flow(FlowRole role) const; Flow flow(FlowRole role, double layer_height) const; - Flow flow(FlowRole role, double layer_height, bool use_initial_layer_width) const; - Flow bridging_flow(FlowRole role, bool thick_bridge = false) const; + // filament_id: 1-based filament actually printing this flow when it differs from the role's default mapping (e.g. top/bottom surface fills, external bridges), 0 to resolve from the role. + Flow flow(FlowRole role, double layer_height, unsigned int filament_id) const; + Flow flow(FlowRole role, double layer_height, bool use_initial_layer_width, unsigned int filament_id = 0) const; + // layer_height overrides m_layer->height for the non-thick flow (combined layer groups print thicker), 0 to use m_layer->height. + Flow bridging_flow(FlowRole role, bool thick_bridge = false, unsigned int filament_id = 0, double layer_height = 0.) const; void slices_to_fill_surfaces_clipped(); void prepare_fill_surfaces(); @@ -100,6 +110,17 @@ class LayerRegion //BBS void simplify_infill_extrusion_entity() { simplify_entity_collection(&fills); } void simplify_wall_extrusion_entity() { simplify_entity_collection(&perimeters); } + + // ORCA: per-extruder layer height. Number of object layers this region's extrusions cover here: + // > 1 on the top layer of a group combined by PrintObject::apply_extruder_layer_heights(), 0 on + // the combined-away layers below such a top (their slices are empty and print nothing; the 0 + // tells them apart from layers where the region's geometry is genuinely absent), 1 otherwise. + unsigned short combined_layer_count() const { return m_combined_layer_count; } + // Extrusion height at this layer (sum of the covered layer heights when combined, else layer height). + double combined_height() const; + // Layer right below the covered group; replaces Layer::lower_layer for overhang / bridge + // detection of combined regions. May be nullptr. + const Layer* combined_lower_layer() const; private: void simplify_entity_collection(ExtrusionEntityCollection* entity_collection); void simplify_path(ExtrusionPath* path); @@ -116,6 +137,9 @@ class LayerRegion private: Layer *m_layer; const PrintRegion *m_region; + // ORCA: set by PrintObject::apply_extruder_layer_heights(), see combined_layer_count() / combined_height(). + unsigned short m_combined_layer_count { 1 }; + double m_combined_height { 0. }; }; class Layer diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index 0cb8b681871..4aa45311918 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -20,8 +20,6 @@ namespace Slic3r { -namespace { - unsigned int effective_layer_filament_id(const Layer &layer, unsigned int filament_id) { if (filament_id == 0) @@ -81,16 +79,14 @@ unsigned int effective_infill_filament_id(const Layer &layer, const PrintRegionC object); } -} // namespace - unsigned int LayerRegion::extruder(FlowRole role) const { const PrintRegionConfig &config = this->region().config(); unsigned int filament_id = 0; if (role == frInfill) - filament_id = config.sparse_infill_filament.value; + filament_id = config.sparse_infill_filament_id.value; else if (role == frSolidInfill && std::abs(config.sparse_infill_density.value - 100.) < EPSILON) - filament_id = config.sparse_infill_filament.value; + filament_id = config.sparse_infill_filament_id.value; else filament_id = this->region().extruder(role); @@ -109,7 +105,13 @@ Flow LayerRegion::flow(FlowRole role, double layer_height) const return this->flow(role, layer_height, m_layer->id() == 0); } -Flow LayerRegion::flow(FlowRole role, double layer_height, bool use_initial_layer_width) const +// filament_id: 1-based filament actually printing this flow when it differs from the role's default mapping (e.g. top/bottom surface fills, external bridges), 0 to resolve from the role. +Flow LayerRegion::flow(FlowRole role, double layer_height, unsigned int filament_id) const +{ + return this->flow(role, layer_height, m_layer->id() == 0, filament_id); +} + +Flow LayerRegion::flow(FlowRole role, double layer_height, bool use_initial_layer_width, unsigned int filament_id) const { const PrintConfig &print_config = m_layer->object()->print()->config(); ConfigOptionFloatOrPercent config_width; @@ -134,17 +136,19 @@ Flow LayerRegion::flow(FlowRole role, double layer_height, bool use_initial_laye if (config_width.value == 0) config_width = m_layer->object()->config().line_width; - const auto nozzle_diameter = float(print_config.nozzle_diameter.get_at(this->extruder(role) - 1)); + // Width resolves against the nozzle of the filament that actually prints (filament_id, when given), + // which may differ from the role's default filament mapping. + const auto nozzle_diameter = float(print_config.nozzle_diameter.get_at((filament_id > 0 ? filament_id : this->extruder(role)) - 1)); return Flow::new_from_config_width(role, config_width, nozzle_diameter, float(layer_height)); } -Flow LayerRegion::bridging_flow(FlowRole role, bool thick_bridge) const +Flow LayerRegion::bridging_flow(FlowRole role, bool thick_bridge, unsigned int filament_id, double layer_height) const { const PrintRegion ®ion = this->region(); const PrintRegionConfig ®ion_config = region.config(); const PrintObject &print_object = *this->layer()->object(); Flow bridge_flow; - auto nozzle_diameter = float(print_object.print()->config().nozzle_diameter.get_at(this->extruder(role) - 1)); + auto nozzle_diameter = float(print_object.print()->config().nozzle_diameter.get_at((filament_id > 0 ? filament_id : this->extruder(role)) - 1)); if (thick_bridge) { // The old Slic3r way (different from all other slicers): Use rounded extrusions. // Get the configured nozzle_diameter for the extruder associated to the flow role requested. @@ -153,7 +157,8 @@ Flow LayerRegion::bridging_flow(FlowRole role, bool thick_bridge) const bridge_flow = Flow::bridging_flow(float(sqrt(region_config.bridge_flow)) * nozzle_diameter, nozzle_diameter); } else { // The same way as other slicers: Use normal extrusions. Apply bridge_flow while maintaining the original spacing. - bridge_flow = this->flow(role).with_flow_ratio(region_config.bridge_flow); + // Combined layer groups stamp their full thickness on the surface; the base flow must be resolved at that height. + bridge_flow = this->flow(role, layer_height > 0. ? layer_height : m_layer->height, filament_id).with_flow_ratio(region_config.bridge_flow); } return bridge_flow; @@ -188,27 +193,33 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe const PrintRegionConfig ®ion_config = this->region().config(); const PrintObjectConfig& object_config = this->layer()->object()->config(); PrintRegionConfig perimeter_config = region_config; - perimeter_config.wall_filament.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.wall_filament.value)))); - perimeter_config.sparse_infill_filament.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.sparse_infill_filament.value)))); - perimeter_config.solid_infill_filament.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.solid_infill_filament.value)))); + perimeter_config.outer_wall_filament_id.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.outer_wall_filament_id.value)))); + perimeter_config.inner_wall_filament_id.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.inner_wall_filament_id.value)))); + perimeter_config.sparse_infill_filament_id.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.sparse_infill_filament_id.value)))); + perimeter_config.internal_solid_filament_id.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.internal_solid_filament_id.value)))); + perimeter_config.top_surface_filament_id.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.top_surface_filament_id.value)))); + perimeter_config.bottom_surface_filament_id.value = int(effective_layer_filament_id(*this->layer(), unsigned(std::max(0, region_config.bottom_surface_filament_id.value)))); // This needs to be in sync with PrintObject::_slice() slicing_mode_normal_below_layer! bool spiral_mode = print_config.spiral_mode && //FIXME account for raft layers. (this->layer()->id() >= size_t(region_config.bottom_shell_layers.value) && this->layer()->print_z >= region_config.bottom_shell_thickness - EPSILON); + // ORCA: on the top layer of a combined group all perimeters extrude with the whole group's height. + const double perimeter_height = this->combined_height(); + PerimeterGenerator g( // input: &slices, &compatible_regions, - this->layer()->height, + perimeter_height, this->layer()->slice_z, - this->flow(frPerimeter), + this->flow(frPerimeter, perimeter_height), &perimeter_config, &this->layer()->object()->config(), &print_config, spiral_mode, - + // output: &this->perimeters, &this->thin_fills, @@ -216,10 +227,11 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe //BBS fill_no_overlap ); - - if (this->layer()->lower_layer != nullptr) + + // Detect overhangs / bridges against the layer below the whole combined group (combined_lower_layer() == lower_layer for regular regions). + if (const Layer *lower_layer = this->combined_lower_layer(); lower_layer != nullptr) // Cummulative sum of polygons over all the regions. - g.lower_slices = &this->layer()->lower_layer->lslices; + g.lower_slices = &lower_layer->lslices; if (this->layer()->upper_layer != NULL) g.upper_slices = &this->layer()->upper_layer->lslices; @@ -228,9 +240,15 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe g.upper_slices_same_region = &this->layer()->upper_layer->get_region(region_id)->slices; g.layer_id = (int)this->layer()->id(); - g.ext_perimeter_flow = this->flow(frExternalPerimeter); - g.overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges); - g.solid_infill_flow = this->flow(frSolidInfill); + g.ext_perimeter_flow = this->flow(frExternalPerimeter, perimeter_height); + g.overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges, 0, perimeter_height); + // Overhangs of external / fully overhanging loops dispatch to the outer wall filament (GCode::process_layer() splits mixed perimeters); resolve their width against its nozzle. + // The filament id comes from the resolved perimeter_config (mixed-filament aware), not the raw region config. + g.ext_overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges, + (unsigned int)std::max(0, perimeter_config.outer_wall_filament_id.value), perimeter_height); + g.solid_infill_flow = this->flow(frSolidInfill, perimeter_height); + // Gap fill dispatches to the outer wall filament (LayerTools::extruder()); resolve its width against its nozzle. + g.gap_fill_flow = this->flow(frSolidInfill, perimeter_height, (unsigned int)std::max(0, perimeter_config.outer_wall_filament_id.value)); if (this->layer()->object()->config().wall_generator.value == PerimeterGeneratorType::Arachne && !spiral_mode) g.process_arachne(); diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 055ad377966..dee98e67e1f 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1346,7 +1346,7 @@ static inline std::vector> segmentation_top_and_bottom_l if (const PrintRegionConfig &config = region->region().config(); // color_idx == 0 means "don't know" extruder aka the underlying extruder. // As this region may split existing regions, we collect statistics over all regions for color_idx == 0. - color_idx == 0 || config.wall_filament == int(color_idx)) { + color_idx == 0 || config.outer_wall_filament_id == int(color_idx)) { //BBS: the extrusion line width is outer wall rather than inner wall const double nozzle_diameter = print_object.print()->config().nozzle_diameter.get_at(0); double outer_wall_line_width = config.get_abs_value("outer_wall_line_width", nozzle_diameter); diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 4e0ddd0f3fd..3bf94f0b2ac 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -197,10 +197,14 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime // outside the grown lower slices (thus where the distance between // the loop centerline and original lower slices is >= half nozzle diameter if (remain_polines.size() != 0) { - extrusion_paths_append(paths, std::move(remain_polines), - erOverhangPerimeter, perimeter_generator.mm3_per_mm_overhang(), - perimeter_generator.overhang_flow.width(), - perimeter_generator.overhang_flow.height()); + // External / fully overhanging loops (no supported path at all) dispatch to the outer wall filament (GCode::process_layer()); their overhangs use that filament's flow. + const bool overhang_external = is_external || paths.empty(); + const Flow &loop_overhang_flow = overhang_external ? perimeter_generator.ext_overhang_flow : + perimeter_generator.overhang_flow; + extrusion_paths_append(paths, std::move(remain_polines), erOverhangPerimeter, + overhang_external ? perimeter_generator.ext_mm3_per_mm_overhang() : + perimeter_generator.mm3_per_mm_overhang(), + loop_overhang_flow.width(), loop_overhang_flow.height()); } // Reapply the nearest point search for starting point. @@ -444,8 +448,13 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p // get overhang paths by checking what parts of this loop fall // outside the grown lower slices (thus where the distance between // the loop centerline and original lower slices is >= half nozzle diameter + // External / fully overhanging extrusions (no supported path at all) dispatch to the outer + // wall filament (GCode::process_layer()); their overhangs use that filament's flow. Known + // corner: a fully overhanging fragment of the ExtrusionMultiPath split below keeps the inner + // flow, but widths come from the Arachne junctions either way; only thick-bridge height differs. + const bool overhang_external = is_external || paths.empty(); extrusion_paths_append(paths, clip_extrusion(extrusion_path, lower_slices_paths, ClipperLib_Z::ctDifference), erOverhangPerimeter, - perimeter_generator.overhang_flow); + overhang_external ? perimeter_generator.ext_overhang_flow : perimeter_generator.overhang_flow); // Reapply the nearest point search for starting point. // We allow polyline reversal because Clipper may have randomly reversed polylines during clipping. @@ -605,7 +614,7 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP // get the real top surface ExPolygons grown_lower_slices; ExPolygons bridge_checker; - auto nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1); + auto nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->outer_wall_filament_id - 1); // Check whether surface be bridge or not if (this->lower_slices != NULL) { // BBS: get the Polygons below the polygon this layer @@ -1068,8 +1077,9 @@ void PerimeterGenerator::apply_extra_perimeters(ExPolygons &infill_area) if (!m_spiral_vase && this->lower_slices != nullptr && this->config->detect_overhang_wall && this->config->extra_perimeters_on_overhangs && this->config->wall_loops > 0 && this->layer_id > this->object_config->raft_layers) { // Generate extra perimeters on overhang areas, and cut them to these parts only, to save print time and material + // Pure erOverhangPerimeter entities dispatch to the outer wall filament, so use its flow. auto [extra_perimeters, filled_area] = generate_extra_perimeters_over_overhangs(infill_area, this->lower_slices_polygons(), - this->config->wall_loops, this->overhang_flow, + this->config->wall_loops, this->ext_overhang_flow, this->m_scaled_resolution, *this->object_config, *this->print_config); if (!extra_perimeters.empty()) { @@ -1143,7 +1153,8 @@ void PerimeterGenerator::process_classic() // overhang perimeters m_mm3_per_mm_overhang = this->overhang_flow.mm3_per_mm(); - + m_ext_mm3_per_mm_overhang = this->ext_overhang_flow.mm3_per_mm(); + // solid infill coord_t solid_infill_spacing = this->solid_infill_flow.scaled_spacing(); @@ -1152,7 +1163,7 @@ void PerimeterGenerator::process_classic() // We consider overhang any part where the entire nozzle diameter is not supported by the // lower layer, so we take lower slices and offset them by half the nozzle diameter used // in the current layer - double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1); + double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->outer_wall_filament_id - 1); m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter / 2))); } @@ -1589,7 +1600,8 @@ void PerimeterGenerator::process_classic() if (! polylines.empty()) { ExtrusionEntityCollection gap_fill; - variable_width(polylines, erGapFill, this->solid_infill_flow, gap_fill.entities); + // Gap fill prints with the outer wall filament (LayerTools::extruder()). + variable_width(polylines, erGapFill, this->gap_fill_flow, gap_fill.entities); /* Make sure we don't infill narrow parts that are already gap-filled (we only consider this surface's gaps to reduce the diff() complexity). Growing actual extrusions ensures that gaps not filled by medial axis @@ -2092,6 +2104,7 @@ void PerimeterGenerator::process_arachne() coord_t ext_perimeter_spacing2 = scaled(0.5f * (this->ext_perimeter_flow.spacing() + this->perimeter_flow.spacing())); // overhang perimeters m_mm3_per_mm_overhang = this->overhang_flow.mm3_per_mm(); + m_ext_mm3_per_mm_overhang = this->ext_overhang_flow.mm3_per_mm(); // solid infill coord_t solid_infill_spacing = this->solid_infill_flow.scaled_spacing(); @@ -2101,7 +2114,7 @@ void PerimeterGenerator::process_arachne() // We consider overhang any part where the entire nozzle diameter is not supported by the // lower layer, so we take lower slices and offset them by half the nozzle diameter used // in the current layer - double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1); + double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->outer_wall_filament_id - 1); m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter / 2))); } @@ -2537,7 +2550,7 @@ bool PerimeterGeneratorLoop::is_internal_contour() const std::vector PerimeterGenerator::generate_lower_polygons_series(float width) { - float nozzle_diameter = print_config->nozzle_diameter.get_at(config->wall_filament - 1); + float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1); float start_offset = -0.5 * width; float end_offset = 0.5 * nozzle_diameter; diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 74be04294eb..93346c33f3b 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -73,7 +73,11 @@ class PerimeterGenerator { Flow perimeter_flow; Flow ext_perimeter_flow; Flow overhang_flow; + // Overhangs of external / fully overhanging loops print with the outer wall filament (GCode::process_layer() splits mixed perimeters by filament), whose nozzle may differ. + Flow ext_overhang_flow; Flow solid_infill_flow; + // Gap fill prints with the outer wall filament (LayerTools::extruder()), whose nozzle may differ from the internal solid infill filament's. + Flow gap_fill_flow; const PrintRegionConfig *config; const PrintObjectConfig *object_config; const PrintConfig *print_config; @@ -116,12 +120,12 @@ class PerimeterGenerator { ExPolygons* fill_no_overlap) : slices(slices), compatible_regions(compatible_regions), upper_slices(nullptr), lower_slices(nullptr), layer_height(layer_height), slice_z(slice_z), layer_id(-1), perimeter_flow(flow), ext_perimeter_flow(flow), - overhang_flow(flow), solid_infill_flow(flow), + overhang_flow(flow), ext_overhang_flow(flow), solid_infill_flow(flow), gap_fill_flow(flow), config(config), object_config(object_config), print_config(print_config), m_spiral_vase(spiral_mode), m_scaled_resolution(scaled(print_config->resolution.value > EPSILON ? print_config->resolution.value : EPSILON)), loops(loops), gap_fill(gap_fill), fill_surfaces(fill_surfaces), fill_no_overlap(fill_no_overlap), - m_ext_mm3_per_mm(-1), m_mm3_per_mm(-1), m_mm3_per_mm_overhang(-1), m_ext_mm3_per_mm_smaller_width(-1) + m_ext_mm3_per_mm(-1), m_mm3_per_mm(-1), m_mm3_per_mm_overhang(-1), m_ext_mm3_per_mm_overhang(-1), m_ext_mm3_per_mm_smaller_width(-1) {} void process_classic(); @@ -132,6 +136,7 @@ class PerimeterGenerator { double ext_mm3_per_mm() const { return m_ext_mm3_per_mm; } double mm3_per_mm() const { return m_mm3_per_mm; } double mm3_per_mm_overhang() const { return m_mm3_per_mm_overhang; } + double ext_mm3_per_mm_overhang() const { return m_ext_mm3_per_mm_overhang; } //BBS double smaller_width_ext_mm3_per_mm() const { return m_ext_mm3_per_mm_smaller_width; } Polygons lower_slices_polygons() const { return m_lower_slices_polygons; } @@ -148,6 +153,7 @@ class PerimeterGenerator { double m_ext_mm3_per_mm; double m_mm3_per_mm; double m_mm3_per_mm_overhang; + double m_ext_mm3_per_mm_overhang; //BBS double m_ext_mm3_per_mm_smaller_width; Polygons m_lower_slices_polygons; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 8882ce9a3af..a56fd6a435f 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -892,8 +892,8 @@ static std::vector s_Preset_print_options { "support_angle", "support_interface_top_layers", "support_interface_bottom_layers", "support_interface_pattern", "support_interface_spacing", "support_interface_loop_pattern", "support_top_z_distance", "support_on_build_plate_only","support_critical_regions_only", "bridge_no_support", "thick_bridges", "thick_internal_bridges","dont_filter_internal_bridges","enable_extra_bridge_layer", "max_bridge_length", "print_sequence", "print_order", "support_remove_small_overhang", - "filename_format", "wall_filament", "support_bottom_z_distance", - "sparse_infill_filament", "solid_infill_filament", "support_filament", "support_interface_filament","support_interface_not_for_body", + "filename_format", "outer_wall_filament_id", "inner_wall_filament_id", "support_bottom_z_distance", + "sparse_infill_filament_id", "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", "support_filament", "support_nozzle_diameter", "support_interface_filament","support_interface_not_for_body", "ooze_prevention", "standby_temperature_delta", "preheat_time","delta_temperature","preheat_steps", "interface_shells", "line_width", "initial_layer_line_width", "inner_wall_line_width", "outer_wall_line_width", "sparse_infill_line_width", "internal_solid_infill_line_width", "skin_infill_line_width","skeleton_infill_line_width", @@ -906,6 +906,8 @@ static std::vector s_Preset_print_options { "tree_support_branch_diameter", "tree_support_branch_diameter_angle", "detect_narrow_internal_solid_infill", "gcode_add_line_number", "enable_arc_fitting", "precise_z_height", "infill_combination","infill_combination_max_layer_height", /*"adaptive_layer_height",*/ + // ORCA: per-extruder layer height ("extruder_layer_height"). + "extruder_layer_height_mode", "extruder_layer_height_tolerance", "support_bottom_interface_spacing", "enable_overhang_speed", "slowdown_for_curled_perimeters", "overhang_1_4_speed", "overhang_2_4_speed", "overhang_3_4_speed", "overhang_4_4_speed", "initial_layer_infill_speed", "only_one_wall_top", "timelapse_type", diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index f97f468fe47..0d50076e0e9 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2641,13 +2641,16 @@ DynamicPrintConfig PresetBundle::full_fff_config() const opt->value = boost::algorithm::clamp(opt->value, 0, int(num_filaments)); } - static const char* keys_1based[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"}; - for (size_t i = 0; i < sizeof(keys_1based) / sizeof(keys_1based[0]); ++ i) { - std::string key = std::string(keys_1based[i]); + static const char* keys_with_default[] = { + "outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", + "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id" + }; + for (size_t i = 0; i < sizeof(keys_with_default) / sizeof(keys_with_default[0]); ++ i) { + std::string key = std::string(keys_with_default[i]); auto *opt = dynamic_cast(out.option(key, false)); assert(opt != nullptr); - if(opt->value < 1 || opt->value > int(num_filaments)) - opt->value = 1; + if(opt->value < 0 || opt->value > int(num_filaments)) + opt->value = 0; } out.option("print_settings_id", true)->value = this->prints.get_selected_preset_name(); out.option("filament_settings_id", true)->values = this->filament_presets; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 703e8ed32db..aef944ca1f1 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -697,6 +697,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "filament_shrinkage_compensation_z" || opt_key == "resolution" || opt_key == "precise_z_height" + // ORCA: layer combining for thicker extruder layer heights happens at the slicing step. + || opt_key == "extruder_layer_height" || opt_key == "dithering_z_step_size" || opt_key == "dithering_local_z_mode" || opt_key == "dithering_local_z_whole_objects" @@ -807,6 +809,11 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "enable_arc_fitting" || opt_key == "print_order" || opt_key == "wall_sequence") { + // The min / max layer height limits gate layer combining at the slicing step (see region_layer_height_multiplier()), so re-slice while per-extruder layer heights are active. + if ((opt_key == "min_layer_height" || opt_key == "max_layer_height") && + std::any_of(m_config.extruder_layer_height.values.begin(), m_config.extruder_layer_height.values.end(), + [](double h) { return h > 0.; })) + osteps.emplace_back(posSlice); osteps.emplace_back(posPerimeters); osteps.emplace_back(posEstimateCurledExtrusions); osteps.emplace_back(posInfill); @@ -911,17 +918,27 @@ std::vector Print::support_material_extruders() const for (PrintObject *object : m_objects) { if (object->has_support_material()) { + // Under a support nozzle diameter restriction a "default" (0) support filament resolves to a matching-nozzle filament: one of the object's own or the deterministic fallback. + const unsigned int resolved_default = object->resolved_default_support_filament(); assert(object->config().support_filament >= 0); - if (object->config().support_filament == 0) - support_uses_current_extruder = true; - else { + if (object->config().support_filament == 0) { + if (resolved_default > 0) { + unsigned int i = resolved_default - 1; + extruders.emplace_back((i >= num_extruders) ? 0 : i); + } else + support_uses_current_extruder = true; + } else { unsigned int i = (unsigned int)object->config().support_filament - 1; extruders.emplace_back((i >= num_extruders) ? 0 : i); } assert(object->config().support_interface_filament >= 0); - if (object->config().support_interface_filament == 0) - support_uses_current_extruder = true; - else { + if (object->config().support_interface_filament == 0) { + if (resolved_default > 0) { + unsigned int i = resolved_default - 1; + extruders.emplace_back((i >= num_extruders) ? 0 : i); + } else + support_uses_current_extruder = true; + } else { unsigned int i = (unsigned int)object->config().support_interface_filament - 1; extruders.emplace_back((i >= num_extruders) ? 0 : i); } @@ -1803,6 +1820,19 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* } return true; }; + // ORCA: append per-extruder layer height / support nozzle diagnostics to the single warning out-param. + auto warn = [warning](const std::string &msg, const std::string &opt_key, const PrintObject *object) { + if (warning) { + if (! warning->string.empty()) + warning->string += "\n"; + warning->string += msg; + warning->opt_key = opt_key; + warning->object = object; + } + }; + // Flags for the per-extruder layer height / support nozzle warnings below. + bool warned_unhonored_heights = false, warned_support_mixed_nozzles = false, + warned_below_min_heights = false, warned_pitch_fallbacks = false; for (PrintObject *object : m_objects) { if (object->has_support_material()) { // BBS: remove useless logics and L() @@ -1824,6 +1854,26 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* } #endif + // ORCA: multi-nozzle support restriction ("support_nozzle_diameter"). + if (const double support_nozzle = object->config().support_nozzle_diameter.value; support_nozzle > 0.) { + if (std::none_of(m_config.nozzle_diameter.values.begin(), m_config.nozzle_diameter.values.end(), + [support_nozzle](double d) { return std::abs(d - support_nozzle) < EPSILON; })) + return {L("No extruder has a nozzle matching the support nozzle diameter."), object, "support_nozzle_diameter"}; + if (object->config().support_filament.value > 0 && + std::abs(m_config.nozzle_diameter.get_at(object->config().support_filament.value - 1) - support_nozzle) > EPSILON) + return {L("The support/raft base filament prints with a nozzle that does not match the support nozzle diameter."), object, "support_filament"}; + if (object->config().support_interface_filament.value > 0 && + std::abs(m_config.nozzle_diameter.get_at(object->config().support_interface_filament.value - 1) - support_nozzle) > EPSILON) + return {L("The support/raft interface filament prints with a nozzle that does not match the support nozzle diameter."), object, "support_interface_filament"}; + } else if (! warned_support_mixed_nozzles && max_nozzle_diameter - min_nozzle_diameter > EPSILON && + (object->config().support_filament.value == 0 || object->config().support_interface_filament.value == 0)) { + // Supports left on the "default" filament follow the active extruder, mixing nozzle sizes. + warn(L("Support may print with extruders of differing nozzle diameters. Set the support " + "nozzle diameter (or explicit support and interface filaments) to keep the support " + "on one nozzle size."), "support_nozzle_diameter", object); + warned_support_mixed_nozzles = true; + } + // Prusa: Fixing crashes with invalid tip diameter or branch diameter // https://github.com/prusa3d/PrusaSlicer/commit/96b3ae85013ac363cd1c3e98ec6b7938aeacf46d if (is_tree(object->config().support_type.value) && (object->config().support_style == smsTreeOrganic || @@ -1862,9 +1912,13 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* size_t first_layer_extruder = object->config().raft_layers == 1 ? object->config().support_interface_filament-1 : object->config().support_filament-1; - first_layer_min_nozzle_diameter = (first_layer_extruder == size_t(-1)) ? - min_nozzle_diameter : - m_config.nozzle_diameter.get_at(first_layer_extruder); + if (first_layer_extruder == size_t(-1) && object->config().support_nozzle_diameter.value > 0.) + // ORCA: a "default" support filament under the restriction prints the raft with the restricted nozzle. + first_layer_min_nozzle_diameter = object->config().support_nozzle_diameter.value; + else + first_layer_min_nozzle_diameter = (first_layer_extruder == size_t(-1)) ? + min_nozzle_diameter : + m_config.nozzle_diameter.get_at(first_layer_extruder); } else { // if we don't have raft layers, any nozzle diameter is potentially used in first layer first_layer_min_nozzle_diameter = min_nozzle_diameter; @@ -1889,6 +1943,219 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* for (const PrintRegion ®ion : object->all_regions()) if (!validate_extrusion_width(region.config(), opt_key, layer_height, err_msg)) return {err_msg, object, opt_key}; + + // ORCA: per-extruder layer height ("extruder_layer_height"). + if (layer_height > EPSILON) { + // Gates diagnostics that would otherwise fire for configurations not using the feature. + const bool heights_feature_active = std::any_of(m_config.extruder_layer_height.values.begin(), + m_config.extruder_layer_height.values.end(), [](double v) { return v > 0.; }); + bool object_has_combined_regions = false; + for (const PrintRegion ®ion : object->all_regions()) { + const PrintRegionConfig ®ion_config = region.config(); + // The wall filaments define the part's layer pitch and must satisfy the strict lattice constraints below; must match the gating of PrintObject::region_layer_height_multiplier(). + std::vector lattice_filaments; // 0-based + bool has_mixed_wall = false; + if (region_config.wall_loops.value > 0) { + const int num_filaments = (int)m_config.filament_diameter.size(); + auto emplace_filament = [num_filaments, &lattice_filaments, &has_mixed_wall](int filament_id) { + int i = std::max(0, filament_id - 1); + if (i >= num_filaments) { + // Snapmaker mixed filament: virtual mixed ids resolve to a different + // physical extruder per layer; such walls never combine + // (matches PrintObject::region_layer_height_multiplier()). + has_mixed_wall = true; + return; + } + lattice_filaments.emplace_back(i); + }; + emplace_filament(region_config.outer_wall_filament_id.value); + if (region_config.wall_loops.value > 1) + emplace_filament(region_config.inner_wall_filament_id.value); + } else + PrintRegion::collect_object_printing_extruders(m_config, region_config, false /* has_brim */, lattice_filaments); + unsigned int region_multiplier = 0; + for (const unsigned int filament : lattice_filaments) { + // The filament index is the extruder index on classic multi-tool printers. + const size_t extruder_idx = filament; + const double extruder_height = m_config.extruder_layer_height.get_at(extruder_idx); + unsigned int multiplier = 1; + if (extruder_height > 0.) { + if (extruder_height < layer_height - EPSILON) + return { Slic3r::format(_u8L("The layer height of extruder %1% (%2% mm) is smaller than the object layer height (%3% mm). " + "Lower the object layer height to the finest extruder layer height."), + extruder_idx + 1, extruder_height, layer_height), object, "extruder_layer_height" }; + const int n = int(std::lround(extruder_height / layer_height)); + if (std::abs(extruder_height - n * layer_height) > EPSILON) + return { Slic3r::format(_u8L("The layer height of extruder %1% (%2% mm) must be an integer multiple of the object layer height (%3% mm)."), + extruder_idx + 1, extruder_height, layer_height), object, "extruder_layer_height" }; + if (extruder_height > m_config.nozzle_diameter.get_at(extruder_idx) + EPSILON) + return { Slic3r::format(_u8L("The layer height of extruder %1% (%2% mm) cannot exceed its nozzle diameter."), + extruder_idx + 1, extruder_height), object, "extruder_layer_height" }; + // 0 means "auto" (0.75 of the nozzle diameter for adaptive layer heights), not enforced here. + if (const double max_lh = m_config.max_layer_height.get_at(extruder_idx); max_lh > EPSILON && extruder_height > max_lh + EPSILON) + return { Slic3r::format(_u8L("The layer height of extruder %1% (%2% mm) cannot exceed its maximum layer height (%3% mm)."), + extruder_idx + 1, extruder_height, max_lh), object, "extruder_layer_height" }; + if (const double min_lh = m_config.min_layer_height.get_at(extruder_idx); min_lh > EPSILON && extruder_height < min_lh - EPSILON) + return { Slic3r::format(_u8L("The layer height of extruder %1% (%2% mm) is below its minimum layer height (%3% mm)."), + extruder_idx + 1, extruder_height, min_lh), object, "extruder_layer_height" }; + multiplier = (unsigned int)n; + } + if (region_multiplier == 0) + region_multiplier = multiplier; + else if (region_multiplier != multiplier) { + // Walls print together; differing preferences fall back to the object layer height (warned below). + region_multiplier = 1; + break; + } + } + if (region_multiplier == 0 || has_mixed_wall) + region_multiplier = 1; + // 0-based indices of all filaments printing this region's features. + std::vector used_filaments; + PrintRegion::collect_object_printing_extruders(m_config, region_config, false /* has_brim */, used_filaments); + if (region_multiplier > 1) + // A feature filament that cannot print the walls' pitch makes the part fall back to the object layer height (mirrors PrintObject::region_layer_height_multiplier()). + for (const unsigned int filament : used_filaments) { + const size_t extruder_idx = filament; // filament index == extruder index here + const double pitch = region_multiplier * layer_height; + const double max_lh = m_config.max_layer_height.get_at(extruder_idx); + const double min_lh = m_config.min_layer_height.get_at(extruder_idx); + if (pitch > m_config.nozzle_diameter.get_at(extruder_idx) + EPSILON || + (max_lh > EPSILON && pitch > max_lh + EPSILON) || + (min_lh > EPSILON && pitch < min_lh - EPSILON)) { + if (! warned_pitch_fallbacks) { + warned_pitch_fallbacks = true; + warn(Slic3r::format(_u8L("The walls of some object parts prefer %1% mm layers, but filament %2% printing " + "other features of the part cannot print that height (nozzle diameter or layer " + "height limits). These parts print with the object layer height instead."), + pitch, filament + 1), + "extruder_layer_height", object); + } + region_multiplier = 1; + break; + } + } + const double region_pitch = region_multiplier * layer_height; + // Infill combines up to the preferred height of its printing filament (sparse, or internal solid at 100% density; mirrors PrintObject::combine_infill()); its width must cover the tallest group. + const bool solid_combined_infill = std::fabs(region_config.sparse_infill_density.value - 100.) < EPSILON; + double combined_infill_height = 0.; + unsigned int combined_infill_filament = 0; // 0-based, only valid while combining + if (region_multiplier == 1 && region_config.sparse_infill_density.value > 0) { + // This fork prints internal solid infill at 100% density with the sparse + // filament (internal_solid_infill_uses_sparse_filament), so the sparse + // filament decides in both cases (mirrors PrintObject::combine_infill()). + const int filament_id = region_config.sparse_infill_filament_id.value; + const FlowRole role = solid_combined_infill ? frSolidInfill : frInfill; + const char *width_key = solid_combined_infill ? "internal_solid_infill_line_width" : "sparse_infill_line_width"; + const double preferred = object->extruder_preferred_layer_height((unsigned int)std::max(0, filament_id)); + if (preferred > layer_height + EPSILON) { + const double infill_nozzle = m_config.nozzle_diameter.get_at(region.extruder(role) - 1); + // Mirrors PrintObject::combine_infill(): groups capped by both feature nozzles and the printing extruder's max layer height (0 = unset). + const unsigned int combine_filament0 = (unsigned int)std::max(1, filament_id) - 1; + double combine_cap = std::min({preferred, + m_config.nozzle_diameter.get_at(std::max(1, region_config.sparse_infill_filament_id.value) - 1), + m_config.nozzle_diameter.get_at(std::max(1, region_config.internal_solid_filament_id.value) - 1)}); + if (const double max_lh = m_config.max_layer_height.get_at(combine_filament0); max_lh > EPSILON) + combine_cap = std::min(combine_cap, max_lh); + combined_infill_height = std::floor(combine_cap / layer_height + EPSILON) * layer_height; + combined_infill_filament = combine_filament0; + auto width_opt = *region_config.option(width_key); + if (width_opt.value == 0.) + width_opt = object->config().line_width; + const double width = width_opt.value == 0. ? + double(Flow::auto_extrusion_width(role, float(infill_nozzle))) : + width_opt.get_abs_value(infill_nozzle); + if (combined_infill_height > layer_height + EPSILON && width <= combined_infill_height + EPSILON) + return { Slic3r::format(_u8L("The %1% mm infill line width is too small for infill combined to %2% mm high layers. " + "Increase the line width or lower the preferred layer height of the infill filament."), + width, combined_infill_height), object, width_key }; + } + } + // Warn once when a filament's minimum layer height is above the height its features print at (the part's pitch; the combined infill prints above the minimum by construction). + if (! warned_below_min_heights && heights_feature_active) + for (const unsigned int filament : used_filaments) { + const double min_lh = m_config.min_layer_height.get_at(filament); + if (min_lh <= EPSILON) + continue; + const bool pitch_below_min = region_pitch < min_lh - EPSILON; + // Areas of a combined region that cannot reach the pitch fall back to the object layer height (see apply_extruder_layer_heights()), possibly below the minimum. + const bool fallback_below_min = region_multiplier > 1 && layer_height < min_lh - EPSILON; + if (! pitch_below_min && ! fallback_below_min) + continue; + if (combined_infill_height > 0. && filament == combined_infill_filament && + combined_infill_height >= min_lh - EPSILON && ! fallback_below_min) + continue; + warned_below_min_heights = true; + warn(Slic3r::format(_u8L("Some object parts print %1% mm layers with filament %2% whose minimum layer " + "height is %3% mm. Raise the object layer height, use a filament with a finer " + "nozzle for these features, or accept printing below the extruder's minimum."), + pitch_below_min ? region_pitch : layer_height, filament + 1, min_lh), + "extruder_layer_height", object); + break; + } + // Warn once about preferred heights the part cannot honor: walls set the pitch, infill combines separately, everything else prints with the pitch. + if (! warned_unhonored_heights) + for (const unsigned int filament : used_filaments) { + const double preferred = object->extruder_preferred_layer_height(filament + 1); + if (preferred <= 0. || std::abs(preferred - region_pitch) < EPSILON) + continue; + if (combined_infill_height > 0. && filament == combined_infill_filament) + continue; + // At 100% density no sparse surfaces exist, the sparse filament's preference is moot. + if (solid_combined_infill && + filament == (unsigned int)std::max(1, region_config.sparse_infill_filament_id.value) - 1) + continue; + warned_unhonored_heights = true; + warn(_u8L("Some object parts use filaments whose preferred layer heights cannot all be honored: " + "the wall filaments set a part's layer pitch, the infill can combine to a thicker height, " + "and the remaining features print with the part's pitch."), + "extruder_layer_height", object); + break; + } + if (region_multiplier > 1) { + object_has_combined_regions = true; + // The combined extrusions are thicker, so line widths must stay above the combined + // height, resolved per role against the region's own nozzle (mirrors PrintRegion::flow()), + // unlike validate_extrusion_width() above which uses the object's smallest nozzle. + // (role, 1-based filament printing it, width key). Bottom surfaces print + // the solid infill width with the bottom surface filament (Fill.cpp). + const std::tuple width_roles[] = { + { frExternalPerimeter, region_config.outer_wall_filament_id.value, "outer_wall_line_width" }, + { frPerimeter, region_config.inner_wall_filament_id.value, "inner_wall_line_width" }, + { frInfill, region_config.sparse_infill_filament_id.value, "sparse_infill_line_width" }, + { frSolidInfill, region_config.internal_solid_filament_id.value, "internal_solid_infill_line_width" }, + { frTopSolidInfill, region_config.top_surface_filament_id.value, "top_surface_line_width" }, + { frSolidInfill, region_config.bottom_surface_filament_id.value, "internal_solid_infill_line_width" }, + }; + for (const auto &[role, filament_id, width_key] : width_roles) { + const double nozzle_diameter = m_config.nozzle_diameter.get_at(std::max(1, filament_id) - 1); + auto width_opt = *region_config.option(width_key); + if (width_opt.value == 0.) + width_opt = object->config().line_width; + const double width = width_opt.value == 0. ? + double(Flow::auto_extrusion_width(role, float(nozzle_diameter))) : + width_opt.get_abs_value(nozzle_diameter); + if (width <= region_pitch + EPSILON) + return { Slic3r::format(_u8L("The %1% mm line width is too small for the %2% mm layer height of its extruder. " + "Increase the line width or lower the extruder layer height."), + width, region_pitch), object, width_key }; + } + } + } + if (object_has_combined_regions) { + if (m_config.spiral_mode) + return { _u8L("Per-extruder layer heights are not supported in spiral vase mode."), object, "extruder_layer_height" }; + if (object->config().interface_shells) + return { _u8L("Per-extruder layer heights are not supported together with interface shells."), object, "extruder_layer_height" }; + if (object->model_object()->has_custom_layering()) { + std::vector profile; + PrintObject::update_layer_height_profile(*object->model_object(), object->slicing_parameters(), profile, object); + if (! check_object_layers_fixed(object->slicing_parameters(), profile)) + return { _u8L("Per-extruder layer heights are not supported together with variable layer height."), + object, "extruder_layer_height" }; + } + } + } } } @@ -2186,7 +2453,7 @@ Flow Print::brim_flow() const frPerimeter, // Flow::new_from_config_width takes care of the percent to value substitution width, - (float)m_config.nozzle_diameter.get_at(m_print_regions.front()->config().wall_filament-1), + (float)m_config.nozzle_diameter.get_at(m_print_regions.front()->config().outer_wall_filament_id-1), (float)this->skirt_first_layer_height()); } diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b618641c35e..6632506b945 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -154,7 +154,9 @@ class PrintRegion int print_object_region_id() const throw() { return m_print_object_region_id; } // 1-based extruder identifier for this region and role. unsigned int extruder(FlowRole role) const; - Flow flow(const PrintObject &object, FlowRole role, double layer_height, bool first_layer = false) const; + // filament_id: 1-based filament actually printing this flow when it differs from the role's default + // mapping (top / bottom surface fills), 0 to resolve the filament from the role. + Flow flow(const PrintObject &object, FlowRole role, double layer_height, bool first_layer = false, unsigned int filament_id = 0) const; // Average diameter of nozzles participating on extruding this region. coordf_t nozzle_dmr_avg(const PrintConfig &print_config) const; // Average diameter of nozzles participating on extruding this region. @@ -486,6 +488,23 @@ class PrintObject : public PrintObjectBaseWithState object_extruders() const; + // ORCA: per-extruder layer height ("extruder_layer_height" printer option). + // Preferred layer height (mm) of the extruder printing the given 1-based filament id; 0 when unset or not reliably resolvable. + double extruder_preferred_layer_height(unsigned int filament_id) const; + // Object layers a region printing with the given 1-based filament id combines into one extrusion where its geometry allows it (1 = no combining). + unsigned int layer_height_multiplier_for_filament(unsigned int filament_id) const; + // Layer pitch multiplier of a region, driven by its wall filaments; 1 when no combining is requested or possible. + unsigned int region_layer_height_multiplier(const PrintRegion ®ion) const; + // Any region of this object printing with a layer height multiplier > 1? + bool has_combined_layer_regions() const; + // Multi-nozzle support restriction ("support_nozzle_diameter" print option): may the given 1-based filament print this object's support / raft? + // Always true when the restriction is disabled or for the "default" filament 0. + bool support_filament_allowed(unsigned int filament_id) const; + // 1-based filament a "default" (0) support filament resolves to under the support nozzle + // diameter restriction when none of a layer's own filaments match: the first non-soluble + // matching-nozzle filament, else the first matching one; 0 when the restriction is disabled or nothing matches. + unsigned int resolved_default_support_filament() const; + // Called by make_perimeters() void slice(); @@ -550,6 +569,8 @@ class PrintObject : public PrintObjectBaseWithStateconfig(); - cfg.wall_filament.value = region.extruder_id; - cfg.solid_infill_filament.value = region.extruder_id; - cfg.sparse_infill_filament.value = region.extruder_id; + cfg.outer_wall_filament_id.value = region.extruder_id; + cfg.inner_wall_filament_id.value = region.extruder_id; + cfg.internal_solid_filament_id.value = region.extruder_id; + cfg.top_surface_filament_id.value = region.extruder_id; + cfg.bottom_surface_filament_id.value = region.extruder_id; + cfg.sparse_infill_filament_id.value = region.extruder_id; if (cfg != region.region->config()) { // Region configuration changed. if (print_region_ref_cnt(*region.region) == 0) { @@ -1073,20 +1076,29 @@ static PrintObjectRegions* generate_print_object_regions( (parent_region.model_volume->is_model_part() || parent_region.model_volume->is_modifier()) && mm_paint_applies_to_parent_region(layer_range, parent_region_id)) { PrintRegionConfig cfg = parent_region.region->config(); - cfg.wall_filament.value = painted_extruder_id; - cfg.solid_infill_filament.value = painted_extruder_id; - cfg.sparse_infill_filament.value = painted_extruder_id; + cfg.outer_wall_filament_id.value = painted_extruder_id; + cfg.inner_wall_filament_id.value = painted_extruder_id; + cfg.internal_solid_filament_id.value = painted_extruder_id; + cfg.top_surface_filament_id.value = painted_extruder_id; + cfg.bottom_surface_filament_id.value = painted_extruder_id; + cfg.sparse_infill_filament_id.value = painted_extruder_id; // Keep PrintRegion config-interned. If a painted target resolves to the same // config as its parent, alias it instead of creating a duplicate PrintRegion. PrintRegion *painted_region = get_create_region(std::move(cfg)); - if (painted_region->config().wall_filament.value != painted_extruder_id || - painted_region->config().solid_infill_filament.value != painted_extruder_id || - painted_region->config().sparse_infill_filament.value != painted_extruder_id) { + if (painted_region->config().outer_wall_filament_id.value != painted_extruder_id || + painted_region->config().inner_wall_filament_id.value != painted_extruder_id || + painted_region->config().internal_solid_filament_id.value != painted_extruder_id || + painted_region->config().top_surface_filament_id.value != painted_extruder_id || + painted_region->config().bottom_surface_filament_id.value != painted_extruder_id || + painted_region->config().sparse_infill_filament_id.value != painted_extruder_id) { BOOST_LOG_TRIVIAL(warning) << "Painted region filament mismatch" << " requested_extruder_id=" << painted_extruder_id - << " wall_filament=" << painted_region->config().wall_filament.value - << " solid_infill_filament=" << painted_region->config().solid_infill_filament.value - << " sparse_infill_filament=" << painted_region->config().sparse_infill_filament.value + << " outer_wall_filament_id=" << painted_region->config().outer_wall_filament_id.value + << " inner_wall_filament_id=" << painted_region->config().inner_wall_filament_id.value + << " internal_solid_filament_id=" << painted_region->config().internal_solid_filament_id.value + << " top_surface_filament_id=" << painted_region->config().top_surface_filament_id.value + << " bottom_surface_filament_id=" << painted_region->config().bottom_surface_filament_id.value + << " sparse_infill_filament_id=" << painted_region->config().sparse_infill_filament_id.value << " parent_region_id=" << parent_region_id << " parent_print_region_id=" << parent_region.region->print_object_region_id() << " painted_print_region_id=" << painted_region->print_object_region_id(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c4881c23c9a..e5a1b28e4aa 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -294,6 +294,13 @@ static t_config_enum_values s_keys_map_EnsureVerticalShellThickness{ }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(EnsureVerticalShellThickness) +// ORCA: per-extruder layer height ("extruder_layer_height"). +static t_config_enum_values s_keys_map_ExtruderLayerHeightMode{ + { "consistent", int(ExtruderLayerHeightMode::elhmConsistent) }, + { "adaptive", int(ExtruderLayerHeightMode::elhmAdaptive) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ExtruderLayerHeightMode) + // Orca static t_config_enum_values s_keys_map_InternalBridgeFilter { { "disabled", ibfDisabled }, @@ -3400,14 +3407,14 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(100., true)); - def = this->add("sparse_infill_filament", coInt); + def = this->add("sparse_infill_filament_id", coInt); def->gui_type = ConfigOptionDef::GUIType::i_enum_open; def->label = L("Infill"); def->category = L("Extruders"); - def->tooltip = L("Filament to print internal sparse infill."); - def->min = 1; + def->tooltip = L("Filament to print internal sparse infill.\n\"Default\" uses the active object/part filament."); + def->min = 0; def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(1)); + def->set_default_value(new ConfigOptionInt(0)); def = this->add("sparse_infill_line_width", coFloatOrPercent); def->label = L("Sparse infill"); @@ -3952,12 +3959,61 @@ void PrintConfigDef::init_fff_params() def = this->add("min_layer_height", coFloats); def->label = L("Min"); def->tooltip = L("The lowest printable layer height for the extruder. " - "Used to limit the minimum layer height when enable adaptive layer height."); + "Used to limit the minimum layer height when enable adaptive layer height. " + "Parts printed with a thicker preferred extruder layer height never fall back " + "below this height either (the first layer excepted)."); def->sidetext = "mm"; // milimeters, don't need translation def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloats { 0.07 }); + def = this->add("extruder_layer_height", coFloats); + def->label = L("Preferred layer height"); + def->tooltip = L("Layer height this extruder should print with, used for printers whose extruders have " + "different nozzle sizes. Object parts whose walls are assigned to this extruder are " + "printed only on every Nth layer with correspondingly thicker extrusions, wherever " + "their geometry allows it (near-vertical walls); elsewhere they fall back to the " + "object layer height. It must then be an integer multiple of the object layer height. " + "When only the sparse infill of a part uses this extruder, the infill is combined up " + "to this height instead. 0 means to use the object layer height."); + def->sidetext = "mm"; // milimeters, don't need translation + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloats { 0. }); + + def = this->add("extruder_layer_height_mode", coEnum); + def->label = L("Thick layer regions"); + def->category = L("Quality"); + def->tooltip = L("How aggressively object parts assigned to an extruder with a thicker preferred layer " + "height are combined into thick layers.\n" + "Consistent: parts print with at most two layer heights, the extruder layer height " + "wherever whole runs of layers fit and the object layer height everywhere else. This " + "gives the most uniform walls.\n" + "Adaptive: runs may also be combined at intermediate multiples of the object layer " + "height, so more of the part prints with thicker layers, at the price of bands of " + "varying layer heights on curved part boundaries."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("consistent"); + def->enum_values.push_back("adaptive"); + def->enum_labels.push_back(L("Consistent")); + def->enum_labels.push_back(L("Adaptive")); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(elhmConsistent)); + + def = this->add("extruder_layer_height_tolerance", coPercent); + def->label = L("Thick layer tolerance"); + def->category = L("Quality"); + def->tooltip = L("How far the outline of an object part assigned to an extruder with a thicker preferred " + "layer height may drift sideways across the layers of one thick run and still be combined, " + "as a percentage of that extruder's nozzle diameter. Higher values combine more of curved " + "part boundaries into thick layers, at the price of rougher boundary walls: deviations up " + "to this fraction of the nozzle diameter are swallowed by the thick extrusions."); + def->sidetext = "%"; + def->min = 0; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionPercent(10)); + def = this->add("slow_down_min_speed", coFloats); def->label = L("Min print speed"); def->tooltip = L("The minimum print speed to which the printer slows down to maintain the minimum layer time defined above " @@ -4139,14 +4195,23 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); - def = this->add("wall_filament", coInt); + def = this->add("outer_wall_filament_id", coInt); def->gui_type = ConfigOptionDef::GUIType::i_enum_open; - def->label = L("Walls"); + def->label = L("Outer walls"); def->category = L("Extruders"); - def->tooltip = L("Filament to print walls."); - def->min = 1; + def->tooltip = L("Filament to print outer walls.\n\"Default\" uses the active object/part filament."); + def->min = 0; def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(1)); + def->set_default_value(new ConfigOptionInt(0)); + + def = this->add("inner_wall_filament_id", coInt); + def->gui_type = ConfigOptionDef::GUIType::i_enum_open; + def->label = L("Inner walls"); + def->category = L("Extruders"); + def->tooltip = L("Filament to print inner walls.\n\"Default\" uses the active object/part filament."); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(0)); def = this->add("inner_wall_line_width", coFloatOrPercent); def->label = L("Inner wall"); @@ -4936,14 +5001,32 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(15)); - def = this->add("solid_infill_filament", coInt); + def = this->add("internal_solid_filament_id", coInt); def->gui_type = ConfigOptionDef::GUIType::i_enum_open; - def->label = L("Solid infill"); + def->label = L("Internal solid infill"); def->category = L("Extruders"); - def->tooltip = L("Filament to print solid infill."); - def->min = 1; + def->tooltip = L("Filament to print internal solid infill.\n\"Default\" uses the active object/part filament."); + def->min = 0; def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(1)); + def->set_default_value(new ConfigOptionInt(0)); + + def = this->add("top_surface_filament_id", coInt); + def->gui_type = ConfigOptionDef::GUIType::i_enum_open; + def->label = L("Top surface"); + def->category = L("Extruders"); + def->tooltip = L("Filament to print top surface.\n\"Default\" uses the active object/part filament."); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(0)); + + def = this->add("bottom_surface_filament_id", coInt); + def->gui_type = ConfigOptionDef::GUIType::i_enum_open; + def->label = L("Bottom surface"); + def->category = L("Extruders"); + def->tooltip = L("Filament to print bottom surface.\n\"Default\" uses the active object/part filament."); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(0)); def = this->add("internal_solid_infill_line_width", coFloatOrPercent); def->label = L("Internal solid infill"); @@ -5314,7 +5397,7 @@ void PrintConfigDef::init_fff_params() def->gui_type = ConfigOptionDef::GUIType::i_enum_open; def->label = L("Support/raft base"); def->category = L("Support"); - def->tooltip = L("Filament to print support base and raft. \"Default\" means no specific filament for support and current filament is used."); + def->tooltip = L("Filament to print support base and raft.\n\"Default\" means no specific filament for support and current filament is used."); def->min = 0; def->mode = comSimple; def->set_default_value(new ConfigOptionInt(0)); @@ -5326,6 +5409,19 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionBool(true)); + def = this->add("support_nozzle_diameter", coFloat); + def->label = L("Support nozzle diameter"); + def->category = L("Support"); + def->tooltip = L("On printers whose extruders have different nozzle diameters, only filaments of this " + "nozzle diameter are used to print support, raft and support interface. This keeps " + "filaments of other nozzle sizes - with their different line widths and layer height " + "limits - out of the support. Support filaments set to a non-default value must match " + "this diameter. Value 0 allows any filament to print support."); + def->sidetext = "mm"; // milimeters, don't need translation + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("support_line_width", coFloatOrPercent); def->label = L("Support"); def->category = L("Quality"); @@ -5349,7 +5445,7 @@ void PrintConfigDef::init_fff_params() def->gui_type = ConfigOptionDef::GUIType::i_enum_open; def->label = L("Support/raft interface"); def->category = L("Support"); - def->tooltip = L("Filament to print support interface. \"Default\" means no specific filament for support interface and current filament is used."); + def->tooltip = L("Filament to print support interface.\n\"Default\" means no specific filament for support interface and current filament is used."); def->min = 0; // BBS def->mode = comSimple; @@ -6426,7 +6522,7 @@ void PrintConfigDef::init_extruder_option_keys() { // ConfigOptionFloats, ConfigOptionPercents, ConfigOptionBools, ConfigOptionStrings m_extruder_option_keys = { - "nozzle_diameter", "min_layer_height", "max_layer_height", "extruder_offset", + "nozzle_diameter", "min_layer_height", "max_layer_height", "extruder_layer_height", "extruder_offset", "retraction_length", "z_hop", "z_hop_types", "z_hop_when_prime", "travel_slope", "retract_lift_above", "retract_lift_below", "retract_lift_enforce", "retraction_speed", "deretraction_speed", "retract_before_wipe", "retract_restart_extra", "retraction_minimum_travel", "wipe", "wipe_distance", "retract_when_changing_layer", "retract_length_toolchange", "retract_restart_extra_toolchange", "extruder_colour", @@ -7145,12 +7241,34 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va opt_key = "change_filament_gcode"; } else if (opt_key == "bridge_fan_speed") { opt_key = "overhang_fan_speed"; - } else if (opt_key == "infill_extruder") { - opt_key = "sparse_infill_filament"; - }else if (opt_key == "solid_infill_extruder") { - opt_key = "solid_infill_filament"; - }else if (opt_key == "perimeter_extruder") { - opt_key = "wall_filament"; + } else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") { + // ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the + // default value "1" meant "the first/active filament". The current scheme uses a dedicated + // key where 0 = "Default" (inherit the object/part filament) and 1..N = explicit filament. + // Renaming to the new *_id key here means every config (process presets, 3mf project + // settings, imported gcode) is translated uniformly on load - not just the version-gated + // 3mf paths the old bespoke migration covered - and a brand-new key can never be misread as + // a legacy default. Map the legacy default "1" to "0" (inherit); keep explicit values >1. + opt_key = "sparse_infill_filament_id"; + if (value == "1") value = "0"; + } else if (opt_key == "solid_infill_extruder" || opt_key == "solid_infill_filament") { + opt_key = "internal_solid_filament_id"; + if (value == "1") value = "0"; + } else if (opt_key == "top_solid_infill_filament") { + opt_key = "top_surface_filament_id"; + if (value == "1") value = "0"; + } else if (opt_key == "bottom_solid_infill_filament") { + opt_key = "bottom_surface_filament_id"; + if (value == "1") value = "0"; + } else if (opt_key == "perimeter_extruder" || opt_key == "wall_filament" || opt_key == "wall_filament_id") { + opt_key = "outer_wall_filament_id"; + if (value == "1") value = "0"; + } else if (opt_key == "inner_wall_filament") { + opt_key = "inner_wall_filament_id"; + if (value == "1") value = "0"; + } else if (opt_key == "outer_wall_filament") { + opt_key = "outer_wall_filament_id"; + if (value == "1") value = "0"; }else if(opt_key == "wipe_tower_extruder") { opt_key = "wipe_tower_filament"; }else if (opt_key == "support_material_extruder") { @@ -7398,10 +7516,18 @@ void DynamicPrintConfig::normalize_fdm(int used_filaments) int extruder = this->option("extruder")->getInt(); this->erase("extruder"); if (extruder != 0) { - if (!this->has("sparse_infill_filament")) - this->option("sparse_infill_filament", true)->setInt(extruder); - if (!this->has("wall_filament")) - this->option("wall_filament", true)->setInt(extruder); + if (!this->has("sparse_infill_filament_id") || this->option("sparse_infill_filament_id")->getInt() == 0) + this->option("sparse_infill_filament_id", true)->setInt(extruder); + if (!this->has("outer_wall_filament_id") || this->option("outer_wall_filament_id")->getInt() == 0) + this->option("outer_wall_filament_id", true)->setInt(extruder); + if (!this->has("inner_wall_filament_id") || this->option("inner_wall_filament_id")->getInt() == 0) + this->option("inner_wall_filament_id", true)->setInt(extruder); + if (!this->has("internal_solid_filament_id") || this->option("internal_solid_filament_id")->getInt() == 0) + this->option("internal_solid_filament_id", true)->setInt(extruder); + if (!this->has("top_surface_filament_id") || this->option("top_surface_filament_id")->getInt() == 0) + this->option("top_surface_filament_id", true)->setInt(extruder); + if (!this->has("bottom_surface_filament_id") || this->option("bottom_surface_filament_id")->getInt() == 0) + this->option("bottom_surface_filament_id", true)->setInt(extruder); // Don't propagate the current extruder to support. // For non-soluble supports, the default "0" extruder means to use the active extruder, // for soluble supports one certainly does not want to set the extruder to non-soluble. @@ -7420,8 +7546,24 @@ void DynamicPrintConfig::normalize_fdm(int used_filaments) this->option("wipe_tower_filament")->setInt(0); } - if (!this->has("solid_infill_filament") && this->has("sparse_infill_filament")) - this->option("solid_infill_filament", true)->setInt(this->option("sparse_infill_filament")->getInt()); + if (this->has("sparse_infill_filament_id")) { + int sparse_infill_filament_id = this->option("sparse_infill_filament_id")->getInt(); + if (sparse_infill_filament_id > 0 && (!this->has("internal_solid_filament_id") || this->option("internal_solid_filament_id")->getInt() == 0)) + this->option("internal_solid_filament_id", true)->setInt(sparse_infill_filament_id); + } + + const int internal_solid = this->has("internal_solid_filament_id") ? this->option("internal_solid_filament_id")->getInt() : 0; + const int top_surface = this->has("top_surface_filament_id") ? this->option("top_surface_filament_id")->getInt() : 0; + const int bottom_surface = this->has("bottom_surface_filament_id") ? this->option("bottom_surface_filament_id")->getInt() : 0; + + if (internal_solid == 0 && top_surface > 0) + this->option("internal_solid_filament_id", true)->setInt(top_surface); + if (internal_solid == 0 && bottom_surface > 0) + this->option("internal_solid_filament_id", true)->setInt(bottom_surface); + if (top_surface == 0 && internal_solid > 0) + this->option("top_surface_filament_id", true)->setInt(internal_solid); + if (bottom_surface == 0 && internal_solid > 0) + this->option("bottom_surface_filament_id", true)->setInt(internal_solid); if (this->has("spiral_mode") && this->opt("spiral_mode", true)->value) { { @@ -7479,10 +7621,18 @@ void DynamicPrintConfig::normalize_fdm_1() int extruder = this->option("extruder")->getInt(); this->erase("extruder"); if (extruder != 0) { - if (!this->has("sparse_infill_filament")) - this->option("sparse_infill_filament", true)->setInt(extruder); - if (!this->has("wall_filament")) - this->option("wall_filament", true)->setInt(extruder); + if (!this->has("sparse_infill_filament_id") || this->option("sparse_infill_filament_id")->getInt() == 0) + this->option("sparse_infill_filament_id", true)->setInt(extruder); + if (!this->has("outer_wall_filament_id") || this->option("outer_wall_filament_id")->getInt() == 0) + this->option("outer_wall_filament_id", true)->setInt(extruder); + if (!this->has("inner_wall_filament_id") || this->option("inner_wall_filament_id")->getInt() == 0) + this->option("inner_wall_filament_id", true)->setInt(extruder); + if (!this->has("internal_solid_filament_id") || this->option("internal_solid_filament_id")->getInt() == 0) + this->option("internal_solid_filament_id", true)->setInt(extruder); + if (!this->has("top_surface_filament_id") || this->option("top_surface_filament_id")->getInt() == 0) + this->option("top_surface_filament_id", true)->setInt(extruder); + if (!this->has("bottom_surface_filament_id") || this->option("bottom_surface_filament_id")->getInt() == 0) + this->option("bottom_surface_filament_id", true)->setInt(extruder); // Don't propagate the current extruder to support. // For non-soluble supports, the default "0" extruder means to use the active extruder, // for soluble supports one certainly does not want to set the extruder to non-soluble. @@ -7493,8 +7643,24 @@ void DynamicPrintConfig::normalize_fdm_1() } } - if (!this->has("solid_infill_filament") && this->has("sparse_infill_filament")) - this->option("solid_infill_filament", true)->setInt(this->option("sparse_infill_filament")->getInt()); + if (this->has("sparse_infill_filament_id")) { + int sparse_infill_filament_id = this->option("sparse_infill_filament_id")->getInt(); + if (sparse_infill_filament_id > 0 && (!this->has("internal_solid_filament_id") || this->option("internal_solid_filament_id")->getInt() == 0)) + this->option("internal_solid_filament_id", true)->setInt(sparse_infill_filament_id); + } + + const int internal_solid = this->has("internal_solid_filament_id") ? this->option("internal_solid_filament_id")->getInt() : 0; + const int top_surface = this->has("top_surface_filament_id") ? this->option("top_surface_filament_id")->getInt() : 0; + const int bottom_surface = this->has("bottom_surface_filament_id") ? this->option("bottom_surface_filament_id")->getInt() : 0; + + if (internal_solid == 0 && top_surface > 0) + this->option("internal_solid_filament_id", true)->setInt(top_surface); + if (internal_solid == 0 && bottom_surface > 0) + this->option("internal_solid_filament_id", true)->setInt(bottom_surface); + if (top_surface == 0 && internal_solid > 0) + this->option("top_surface_filament_id", true)->setInt(internal_solid); + if (bottom_surface == 0 && internal_solid > 0) + this->option("bottom_surface_filament_id", true)->setInt(internal_solid); if (this->has("spiral_mode") && this->opt("spiral_mode", true)->value) { { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 68d18bccf2c..b7aa97ea316 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -195,6 +195,12 @@ enum EnsureVerticalShellThickness { evstAll, }; +// ORCA: per-extruder layer height ("extruder_layer_height"). +enum ExtruderLayerHeightMode { + elhmConsistent, + elhmAdaptive, +}; + //Orca enum InternalBridgeFilter { ibfDisabled, ibfLimited, ibfNofilter @@ -460,6 +466,7 @@ static std::string get_bed_temp_1st_layer_key(const BedType type) template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values(); CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrinterTechnology) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ExtruderLayerHeightMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode) @@ -837,6 +844,9 @@ PRINT_CONFIG_CLASS_DEFINE( // Force the generation of solid shells between adjacent materials/volumes. ((ConfigOptionBool, interface_shells)) ((ConfigOptionFloat, layer_height)) + // ORCA: per-extruder layer height ("extruder_layer_height"). + ((ConfigOptionEnum, extruder_layer_height_mode)) + ((ConfigOptionPercent, extruder_layer_height_tolerance)) ((ConfigOptionFloat, mmu_segmented_region_max_width)) ((ConfigOptionFloat, mmu_segmented_region_interlocking_depth)) ((ConfigOptionFloat, raft_contact_distance)) @@ -860,6 +870,8 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloat, support_bottom_z_distance)) ((ConfigOptionInt, enforce_support_layers)) ((ConfigOptionInt, support_filament)) + // ORCA: restrict support/raft/interface printing to filaments of this nozzle diameter (0 = no restriction). + ((ConfigOptionFloat, support_nozzle_diameter)) ((ConfigOptionFloatOrPercent, support_line_width)) ((ConfigOptionBool, support_interface_not_for_body)) ((ConfigOptionBool, support_interface_loop_pattern)) @@ -1010,7 +1022,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionInt, fuzzy_skin_octaves)) ((ConfigOptionFloat, fuzzy_skin_persistence)) ((ConfigOptionFloat, gap_infill_speed)) - ((ConfigOptionInt, sparse_infill_filament)) + ((ConfigOptionInt, sparse_infill_filament_id)) ((ConfigOptionFloatOrPercent, sparse_infill_line_width)) ((ConfigOptionPercent, infill_wall_overlap)) ((ConfigOptionPercent, top_bottom_infill_wall_overlap)) @@ -1036,14 +1048,17 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloat, ironing_angle)) // Detect bridging perimeters ((ConfigOptionBool, detect_overhang_wall)) - ((ConfigOptionInt, wall_filament)) + ((ConfigOptionInt, outer_wall_filament_id)) + ((ConfigOptionInt, inner_wall_filament_id)) ((ConfigOptionFloatOrPercent, inner_wall_line_width)) ((ConfigOptionFloat, inner_wall_speed)) // Total number of perimeters. ((ConfigOptionInt, wall_loops)) ((ConfigOptionBool, alternate_extra_wall)) ((ConfigOptionFloat, minimum_sparse_infill_area)) - ((ConfigOptionInt, solid_infill_filament)) + ((ConfigOptionInt, internal_solid_filament_id)) + ((ConfigOptionInt, top_surface_filament_id)) + ((ConfigOptionInt, bottom_surface_filament_id)) ((ConfigOptionFloatOrPercent, internal_solid_infill_line_width)) ((ConfigOptionFloat, internal_solid_infill_speed)) // Detect thin walls. @@ -1355,6 +1370,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloats, max_layer_height)) ((ConfigOptionFloats, fan_min_speed)) ((ConfigOptionFloats, min_layer_height)) + ((ConfigOptionFloats, extruder_layer_height)) ((ConfigOptionFloat, printable_height)) ((ConfigOptionPoint, best_object_pos)) ((ConfigOptionFloats, slow_down_min_speed)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f649c0e2b1e..b0476125a64 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -223,7 +223,7 @@ void PrintObject::_transform_hole_to_polyholes() bool twist = this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_twisted.value; if (diameter_max - diameter_min < max_variation * 2 && diameter_line_max - diameter_line_min < max_variation * 2) { layerid2center[layer_idx].emplace_back( - std::tuple{center, diameter_max, layer->m_regions[region_idx]->region().config().wall_filament.value, max_variation, twist}, & hole); + std::tuple{center, diameter_max, layer->m_regions[region_idx]->region().config().outer_wall_filament_id.value, max_variation, twist}, & hole); } } } @@ -519,6 +519,18 @@ void PrintObject::prepare_infill() this->combine_infill(); m_print->throw_if_canceled(); + // Per-extruder layer height. Stamp the group height onto fill surfaces of combined LayerRegions (Fill uses Surface::thickness when set); must run last, earlier steps re-create surfaces. + if (this->has_combined_layer_regions()) { + for (Layer *layer : m_layers) + for (LayerRegion *layerm : layer->m_regions) + if (layerm->combined_layer_count() > 1) + for (Surface &surface : layerm->fill_surfaces.surfaces) { + surface.thickness = layerm->combined_height(); + surface.thickness_layers = layerm->combined_layer_count(); + } + m_print->throw_if_canceled(); + } + #ifdef SLIC3R_DEBUG_SLICE_PROCESSING for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { for (const Layer *layer : m_layers) { @@ -881,6 +893,18 @@ bool PrintObject::invalidate_state_by_config_options( std::vector steps; bool invalidated = false; for (const t_config_option_key &opt_key : opt_keys) { + // Per-extruder layer height: these options feed region_layer_height_multiplier(), which decides layer combination at the slicing step. + if (opt_key == "outer_wall_filament_id" || opt_key == "inner_wall_filament_id" + || opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" + || opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id" + || opt_key == "wall_loops" || opt_key == "sparse_infill_density" + || opt_key == "top_shell_layers" || opt_key == "bottom_shell_layers") { + for (double extruder_layer_height : m_print->config().extruder_layer_height.values) + if (extruder_layer_height > 0.) { + steps.emplace_back(posSlice); + break; + } + } if ( opt_key == "brim_width" || opt_key == "brim_object_gap" || opt_key == "brim_type" @@ -947,6 +971,9 @@ bool PrintObject::invalidate_state_by_config_options( steps.emplace_back(posPerimeters); } else if ( opt_key == "layer_height" + // ORCA: per-extruder layer height: the layer combining happens at the slicing step. + || opt_key == "extruder_layer_height_mode" + || opt_key == "extruder_layer_height_tolerance" || opt_key == "dithering_z_step_size" || opt_key == "dithering_local_z_mode" || opt_key == "dithering_local_z_whole_objects" @@ -1019,6 +1046,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "support_remove_small_overhang" || opt_key == "enforce_support_layers" || opt_key == "support_filament" + || opt_key == "support_nozzle_diameter" || opt_key == "support_line_width" || opt_key == "support_interface_top_layers" || opt_key == "support_interface_bottom_layers" @@ -1092,8 +1120,10 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "bottom_shell_thickness" || opt_key == "top_shell_thickness" || opt_key == "minimum_sparse_infill_area" - || opt_key == "sparse_infill_filament" - || opt_key == "solid_infill_filament" + || opt_key == "sparse_infill_filament_id" + || opt_key == "internal_solid_filament_id" + || opt_key == "top_surface_filament_id" + || opt_key == "bottom_surface_filament_id" || opt_key == "sparse_infill_line_width" || opt_key == "skin_infill_line_width" || opt_key == "skeleton_infill_line_width" @@ -1151,7 +1181,8 @@ bool PrintObject::invalidate_state_by_config_options( steps.emplace_back(posPrepareInfill); } else if ( opt_key == "outer_wall_line_width" - || opt_key == "wall_filament" + || opt_key == "outer_wall_filament_id" + || opt_key == "inner_wall_filament_id" || opt_key == "fuzzy_skin" || opt_key == "fuzzy_skin_thickness" || opt_key == "fuzzy_skin_point_distance" @@ -1704,11 +1735,17 @@ void PrintObject::process_external_surfaces() for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); // BOOST_LOG_TRIVIAL(trace) << "Processing external surface, layer" << m_layers[layer_idx]->print_z; - m_layers[layer_idx]->get_region(int(region_id))->process_external_surfaces( + LayerRegion *layerm = m_layers[layer_idx]->get_region(int(region_id)); + // Per-extruder layer height: a combined group's external surfaces relate to the layer below the whole group (lower_idx == layer_idx - 1 for regular regions; + // count == 0 on combined-away layers, hence the max(..., 1)). + const size_t combined_layers = std::max(layerm->combined_layer_count(), 1); + const bool has_lower = layer_idx >= combined_layers; + const size_t lower_idx = has_lower ? layer_idx - combined_layers : 0; + layerm->process_external_surfaces( // lower layer - (layer_idx == 0) ? nullptr : m_layers[layer_idx - 1], + has_lower ? m_layers[lower_idx] : nullptr, // lower layer polygons with density > 0% - (layer_idx == 0 || surfaces_covered.empty() || surfaces_covered[layer_idx - 1].empty()) ? nullptr : &surfaces_covered[layer_idx - 1]); + (! has_lower || surfaces_covered.empty() || surfaces_covered[lower_idx].empty()) ? nullptr : &surfaces_covered[lower_idx]); } } ); @@ -1825,6 +1862,9 @@ void PrintObject::discover_vertical_shells() if (region.config().ensure_vertical_shell_thickness.value != evstAll ) // This region will be handled by discover_horizontal_shells(). continue; + if (this->region_layer_height_multiplier(region) > 1) + // Per-extruder layer height. Combined regions extrude on group tops only; their shells are ensured by discover_horizontal_shells() stepping over the groups instead. + continue; //FIXME Improve the heuristics for a grain size. size_t grain_size = std::max(num_layers / 16, size_t(1)); @@ -3190,6 +3230,15 @@ static void clamp_exturder_to_default(ConfigOptionInt &opt, size_t num_total_fil opt.value = 1; } +// ORCA: feature filament selectors use 0 = "Default" (inherit the active object/part filament). +// Once the object/part/layer-range overrides are resolved, any remaining "Default" or invalid +// value falls back to filament 1. +static void clamp_feature_filament_to_valid(ConfigOptionInt &opt, size_t num_total_filaments) +{ + if (opt.value <= 0 || opt.value > (int)num_total_filaments) + opt.value = 1; +} + PrintObjectConfig PrintObject::object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders) { PrintObjectConfig config = default_object_config; @@ -3205,55 +3254,124 @@ PrintObjectConfig PrintObject::object_config_from_model_object(const PrintObject } const std::string key_extruder { "extruder" }; -static constexpr const std::initializer_list keys_extruders { "sparse_infill_filament"sv, "solid_infill_filament"sv, "wall_filament"sv }; +static constexpr const std::initializer_list keys_extruders { + "sparse_infill_filament_id"sv, + "internal_solid_filament_id"sv, + "top_surface_filament_id"sv, + "bottom_surface_filament_id"sv, + "outer_wall_filament_id"sv, + "inner_wall_filament_id"sv +}; + +struct FeatureFilamentOverrideMask +{ + bool sparse_infill_filament_id = false; + bool internal_solid_filament_id = false; + bool top_surface_filament_id = false; + bool bottom_surface_filament_id = false; + bool outer_wall_filament_id = false; + bool inner_wall_filament_id = false; +}; -static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPrintConfig &in) +static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPrintConfig &in, FeatureFilamentOverrideMask &feature_overrides) { - // 1) Copy the "extruder key to sparse_infill_filament and wall_filament. + // 1) Explicit feature filament values take precedence over base extruder fallback. auto *opt_extruder = in.opt(key_extruder); - if (opt_extruder) - if (int extruder = opt_extruder->value; extruder != 0) { - // Not a default extruder. - out.sparse_infill_filament.value = extruder; - out.solid_infill_filament.value = extruder; - out.wall_filament.value = extruder; - } + int base_extruder = (opt_extruder != nullptr) ? opt_extruder->value : 0; + // 2) Copy the rest of the values. for (auto it = in.cbegin(); it != in.cend(); ++ it) if (it->first != key_extruder) if (ConfigOption* my_opt = out.option(it->first, false); my_opt != nullptr) { if (one_of(it->first, keys_extruders)) { - // Ignore "default" extruders. + // "Default" (0) clears explicit override for this scope and lets fallback apply. int extruder = static_cast(it->second.get())->value; - if (extruder > 0) + if (extruder > 0) { my_opt->setInt(extruder); + if (it->first == "sparse_infill_filament_id") + feature_overrides.sparse_infill_filament_id = true; + else if (it->first == "internal_solid_filament_id") + feature_overrides.internal_solid_filament_id = true; + else if (it->first == "top_surface_filament_id") + feature_overrides.top_surface_filament_id = true; + else if (it->first == "bottom_surface_filament_id") + feature_overrides.bottom_surface_filament_id = true; + else if (it->first == "outer_wall_filament_id") + feature_overrides.outer_wall_filament_id = true; + else if (it->first == "inner_wall_filament_id") + feature_overrides.inner_wall_filament_id = true; + } else { + if (it->first == "sparse_infill_filament_id") + feature_overrides.sparse_infill_filament_id = false; + else if (it->first == "internal_solid_filament_id") + feature_overrides.internal_solid_filament_id = false; + else if (it->first == "top_surface_filament_id") + feature_overrides.top_surface_filament_id = false; + else if (it->first == "bottom_surface_filament_id") + feature_overrides.bottom_surface_filament_id = false; + else if (it->first == "outer_wall_filament_id") + feature_overrides.outer_wall_filament_id = false; + else if (it->first == "inner_wall_filament_id") + feature_overrides.inner_wall_filament_id = false; + } } else my_opt->set(it->second.get()); } + + // 3) Apply base extruder only to features that were not explicitly overridden. + if (base_extruder > 0) { + if (!feature_overrides.sparse_infill_filament_id) + out.sparse_infill_filament_id.value = base_extruder; + if (!feature_overrides.internal_solid_filament_id) + out.internal_solid_filament_id.value = base_extruder; + if (!feature_overrides.top_surface_filament_id) + out.top_surface_filament_id.value = base_extruder; + if (!feature_overrides.bottom_surface_filament_id) + out.bottom_surface_filament_id.value = base_extruder; + if (!feature_overrides.outer_wall_filament_id) + out.outer_wall_filament_id.value = base_extruder; + if (!feature_overrides.inner_wall_filament_id) + out.inner_wall_filament_id.value = base_extruder; + } } PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders) { PrintRegionConfig config = default_or_parent_region_config; + FeatureFilamentOverrideMask feature_overrides; + + // For model parts, non-zero values coming from the print defaults should stay explicit. + if (volume.is_model_part()) { + feature_overrides.sparse_infill_filament_id = (config.sparse_infill_filament_id.value > 0); + feature_overrides.internal_solid_filament_id = (config.internal_solid_filament_id.value > 0); + feature_overrides.top_surface_filament_id = (config.top_surface_filament_id.value > 0); + feature_overrides.bottom_surface_filament_id = (config.bottom_surface_filament_id.value > 0); + feature_overrides.outer_wall_filament_id = (config.outer_wall_filament_id.value > 0); + feature_overrides.inner_wall_filament_id = (config.inner_wall_filament_id.value > 0); + } + if (volume.is_model_part()) { // default_or_parent_region_config contains the Print's PrintRegionConfig. // Override with ModelObject's PrintRegionConfig values. - apply_to_print_region_config(config, volume.get_object()->config.get()); + apply_to_print_region_config(config, volume.get_object()->config.get(), feature_overrides); } else { // default_or_parent_region_config contains parent PrintRegion config, which already contains ModelVolume's config. } - apply_to_print_region_config(config, volume.config.get()); + apply_to_print_region_config(config, volume.config.get(), feature_overrides); if (! volume.material_id().empty()) - apply_to_print_region_config(config, volume.material()->config.get()); + apply_to_print_region_config(config, volume.material()->config.get(), feature_overrides); if (layer_range_config != nullptr) { // Not applicable to modifiers. assert(volume.is_model_part()); - apply_to_print_region_config(config, *layer_range_config); + apply_to_print_region_config(config, *layer_range_config, feature_overrides); } - // Clamp invalid extruders to the default extruder (with index 1). - clamp_exturder_to_default(config.sparse_infill_filament, num_extruders); - clamp_exturder_to_default(config.wall_filament, num_extruders); - clamp_exturder_to_default(config.solid_infill_filament, num_extruders); + // Resolve feature defaults and clamp invalid extruders to index 1. + clamp_feature_filament_to_valid(config.sparse_infill_filament_id, num_extruders); + clamp_feature_filament_to_valid(config.outer_wall_filament_id, num_extruders); + clamp_feature_filament_to_valid(config.inner_wall_filament_id, num_extruders); + clamp_feature_filament_to_valid(config.internal_solid_filament_id, num_extruders); + clamp_feature_filament_to_valid(config.top_surface_filament_id, num_extruders); + clamp_feature_filament_to_valid(config.bottom_surface_filament_id, num_extruders); if (config.sparse_infill_density.value < 0.00011f) // Switch of infill for very low infill rates, also avoid division by zero in infill generator for these very low rates. // See GH issue #5910. @@ -3316,9 +3434,12 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full object_config.brim_type != btNoBrim && object_config.brim_width > 0., object_extruders); for (const std::pair &range_and_config : model_object.layer_config_ranges) - if (range_and_config.second.has("wall_filament") || - range_and_config.second.has("sparse_infill_filament") || - range_and_config.second.has("solid_infill_filament")) + if (range_and_config.second.has("outer_wall_filament_id") || + range_and_config.second.has("inner_wall_filament_id") || + range_and_config.second.has("sparse_infill_filament_id") || + range_and_config.second.has("internal_solid_filament_id") || + range_and_config.second.has("top_surface_filament_id") || + range_and_config.second.has("bottom_surface_filament_id")) PrintRegion::collect_object_printing_extruders( print_config, region_config_from_model_volume(default_region_config, &range_and_config.second.get(), *model_volume, filament_extruders), @@ -3355,6 +3476,138 @@ std::vector PrintObject::object_extruders() const return extruders; } +// Per-extruder layer height ("extruder_layer_height" printer option). +// Raw preferred layer height (mm) of the extruder printing the given 1-based filament id, 0 when unset or not reliably resolvable. +double PrintObject::extruder_preferred_layer_height(unsigned int filament_id) const +{ + const PrintConfig &print_config = m_print->config(); + if (print_config.extruder_layer_height.empty()) + return 0.; + // Region filament ids are 1-based, 0 ("Default") falls back to the first filament. + if (filament_id == 0) + filament_id = 1; + // Snapmaker mixed filament: virtual mixed ids (beyond the physical filaments) resolve to a + // different physical extruder on every layer, so no stable preferred height exists for them. + if (filament_id > print_config.filament_diameter.values.size()) + return 0.; + // Classic multi-tool printers index per-extruder vectors by the filament id directly (like the min/max_layer_height and nozzle_diameter lookups elsewhere). + const size_t extruder_idx = filament_id - 1; + return std::max(0., print_config.extruder_layer_height.get_at(extruder_idx)); +} + +// Returns the number of object layers a region printing with the given 1-based filament id combines into one extrusion (1 = no combining). +// The configured height must be a (near) integer multiple of the object layer height, otherwise it is ignored here and rejected by Print::validate(). +unsigned int PrintObject::layer_height_multiplier_for_filament(unsigned int filament_id) const +{ + const double extruder_height = this->extruder_preferred_layer_height(filament_id); + if (extruder_height <= 0.) + return 1; + const double layer_height = m_config.layer_height.value; + if (layer_height <= EPSILON) + return 1; + const int multiplier = int(std::lround(extruder_height / layer_height)); + if (multiplier <= 1 || std::abs(extruder_height - multiplier * layer_height) > EPSILON) + // Not combining, or not an integer multiple (the latter is rejected by Print::validate()). + return 1; + // Same filament -> extruder mapping as extruder_preferred_layer_height() and Print::validate(). + const PrintConfig &print_config = m_print->config(); + const size_t extruder_idx = size_t((filament_id == 0 ? 1 : filament_id) - 1); + if (extruder_height > print_config.nozzle_diameter.get_at(extruder_idx) + EPSILON) + // Taller than the nozzle can print, rejected by Print::validate(). + return 1; + return (unsigned int)multiplier; +} + +unsigned int PrintObject::region_layer_height_multiplier(const PrintRegion ®ion) const +{ + const PrintRegionConfig &config = region.config(); + // The wall filaments define a region's layer pitch (sparse infill preferring taller layers is combined separately in combine_infill()); wall-less regions need agreement of all their feature filaments. + // Disagreeing filaments keep the object layer height (Print::validate() warns). + std::vector filaments; // 0-based filament indices + if (config.wall_loops.value > 0) { + const int num_filaments = (int)m_print->config().filament_diameter.size(); + bool has_mixed_wall = false; + auto emplace_filament = [num_filaments, &filaments, &has_mixed_wall](int filament_id) { + int i = std::max(0, filament_id - 1); + if (i >= num_filaments) { + // Snapmaker mixed filament: virtual mixed ids resolve to a different physical + // extruder per layer, so such walls need every layer and must never combine + // (matches Print::validate() and extruder_preferred_layer_height()). + has_mixed_wall = true; + return; + } + filaments.emplace_back(i); + }; + emplace_filament(config.outer_wall_filament_id.value); + if (config.wall_loops.value > 1) + emplace_filament(config.inner_wall_filament_id.value); + if (has_mixed_wall) + return 1; + } else + // The brim is not considered, it prints on the first layer which is never combined. + PrintRegion::collect_object_printing_extruders(m_print->config(), config, false /* has_brim */, filaments); + unsigned int multiplier = 0; + for (unsigned int filament : filaments) { + unsigned int m = this->layer_height_multiplier_for_filament(filament + 1); + if (multiplier == 0) + multiplier = m; + else if (m != multiplier) + return 1; + } + if (multiplier <= 1) + return 1; + // A feature filament whose nozzle or layer height limits cannot follow the walls' pitch makes the region fall back to the object layer height (Print::validate() warns about it). + const PrintConfig &print_config = m_print->config(); + const double pitch = multiplier * m_config.layer_height.value; + std::vector used_filaments; // 0-based filament indices + PrintRegion::collect_object_printing_extruders(print_config, config, false /* has_brim */, used_filaments); + for (unsigned int filament : used_filaments) { + // The filament index is the extruder index on classic multi-tool printers. + const size_t extruder_idx = size_t(filament); + if (pitch > print_config.nozzle_diameter.get_at(extruder_idx) + EPSILON) + return 1; + if (const double max_lh = print_config.max_layer_height.get_at(extruder_idx); max_lh > EPSILON && pitch > max_lh + EPSILON) + return 1; + if (const double min_lh = print_config.min_layer_height.get_at(extruder_idx); min_lh > EPSILON && pitch < min_lh - EPSILON) + return 1; + } + return multiplier; +} + +bool PrintObject::has_combined_layer_regions() const +{ + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) + if (this->region_layer_height_multiplier(this->printing_region(region_id)) > 1) + return true; + return false; +} + +bool PrintObject::support_filament_allowed(unsigned int filament_id) const +{ + const double restriction = m_config.support_nozzle_diameter.value; + if (restriction <= 0. || filament_id == 0) + return true; + return std::abs(m_print->config().nozzle_diameter.get_at(filament_id - 1) - restriction) < EPSILON; +} + +unsigned int PrintObject::resolved_default_support_filament() const +{ + if (m_config.support_nozzle_diameter.value <= 0.) + return 0; + const PrintConfig &print_config = m_print->config(); + const size_t num_filaments = std::max(print_config.nozzle_diameter.values.size(), + print_config.filament_diameter.values.size()); + unsigned int soluble_fallback = 0; + for (size_t i = 0; i < num_filaments; ++ i) + if (this->support_filament_allowed((unsigned int)(i + 1))) { + if (! print_config.filament_soluble.get_at(i)) + return (unsigned int)(i + 1); + if (soluble_fallback == 0) + soluble_fallback = (unsigned int)(i + 1); + } + return soluble_fallback; +} + namespace { struct LayerHeightRangeOverride { @@ -4038,6 +4291,9 @@ void PrintObject::clip_fill_surfaces() for (LayerRegion *layerm : lower_layer->m_regions) { if (layerm->region().config().sparse_infill_density.value == 0) continue; + if (layerm->combined_layer_count() > 1) + // Per-extruder layer height: a combined group's infill supports the whole group, don't clip it against the single layer above. + continue; Polygons internal; for (Surface &surface : layerm->fill_surfaces.surfaces) if (surface.surface_type == stInternal || surface.surface_type == stInternalVoid) @@ -4061,6 +4317,9 @@ void PrintObject::discover_horizontal_shells() BOOST_LOG_TRIVIAL(trace) << "discover_horizontal_shells()"; for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { + // ORCA: per-extruder layer height. Combined regions extrude only on the top layers of their + // groups; the empty layers in between must neither receive shells nor count as shell layers. + const bool region_combined = this->region_layer_height_multiplier(this->printing_region(region_id)) > 1; for (size_t i = 0; i < m_layers.size(); ++ i) { m_print->throw_if_canceled(); Layer *layer = m_layers[i]; @@ -4076,7 +4335,8 @@ void PrintObject::discover_horizontal_shells() } // If ensure_vertical_shell_thickness, then the rest has already been performed by discover_vertical_shells(). - if (region_config.ensure_vertical_shell_thickness.value == evstAll) + // ORCA: combined regions are skipped by discover_vertical_shells() and handled here instead. + if (region_config.ensure_vertical_shell_thickness.value == evstAll && ! region_combined) continue; coordf_t print_z = layer->print_z; @@ -4113,17 +4373,26 @@ void PrintObject::discover_horizontal_shells() // Slic3r::debugf "Layer %d has %s surfaces\n", $i, ($type == stTop) ? 'top' : 'bottom'; // Scatter top / bottom regions to other layers. Scattering process is inherently serial, it is difficult to parallelize without locking. + // ORCA: per-extruder layer height. n_region_layers counts only layers the region + // prints on (equals the int(i) - n index distance when nothing is skipped), so + // shells of combined regions propagate across group tops. + int n_region_layers = 0; for (int n = (type == stTop) ? int(i) - 1 : int(i) + 1; (type == stTop) ? - (n >= 0 && (int(i) - n < num_solid_layers || + (n >= 0 && (n_region_layers + 1 < num_solid_layers || print_z - m_layers[n]->print_z < region_config.top_shell_thickness.value - EPSILON)) : - (n < int(m_layers.size()) && (n - int(i) < num_solid_layers || + (n < int(m_layers.size()) && (n_region_layers + 1 < num_solid_layers || m_layers[n]->bottom_z() - bottom_z < region_config.bottom_shell_thickness.value - EPSILON)); (type == stTop) ? -- n : ++ n) { // Slic3r::debugf " looking for neighbors on layer %d...\n", $n; // Reference to the lower layer of a TOP surface, or an upper layer of a BOTTOM surface. LayerRegion *neighbor_layerm = m_layers[n]->regions()[region_id]; + // ORCA: combined-away layers (count == 0) print nothing here: no shells, not + // counted. Genuinely absent geometry (count == 1, empty slices) still counts. + if (region_combined && neighbor_layerm->combined_layer_count() == 0) + continue; + ++ n_region_layers; // find intersection between neighbor and current layer's surfaces // intersections have contours and holes @@ -4274,7 +4543,21 @@ void PrintObject::combine_infill() const PrintRegion ®ion = this->printing_region(region_id); //BBS const bool enable_combine_infill = region.config().infill_combination.value; - if (enable_combine_infill == false || region.config().sparse_infill_density == 0.) + // Per-extruder layer height: infill whose filament prefers a taller layer height is combined up to it, with or without the infill_combination option. + // Only for regions printing at the object layer height; combined-wall regions already print everything over their runs. + if (this->region_layer_height_multiplier(region) > 1) + continue; + // At 100% density the combined surfaces are internal solid infill (see use_solid_infill + // below), but this fork prints them with the sparse infill filament + // (internal_solid_infill_uses_sparse_filament in PrintRegion::extruder()), so the sparse + // filament's preference and limits decide in both cases. + const int combine_filament_id = std::max(1, region.config().sparse_infill_filament_id.value); + // Per-extruder vector index: the filament index is the extruder index on classic multi-tool printers (like extruder_preferred_layer_height()). + const size_t combine_extruder_idx = size_t(combine_filament_id - 1); + double preferred_infill_height = this->extruder_preferred_layer_height((unsigned int)combine_filament_id); + if (preferred_infill_height <= m_config.layer_height.value + EPSILON) + preferred_infill_height = 0.; + if ((enable_combine_infill == false && preferred_infill_height == 0.) || region.config().sparse_infill_density == 0.) continue; // Support internal solid infill when sparse_infill_density is 100% @@ -4285,14 +4568,26 @@ void PrintObject::combine_infill() // Limit the number of combined layers to the maximum height allowed by this regions' nozzle. //FIXME limit the layer height to max_layer_height - double nozzle_diameter = this->print()->config().nozzle_diameter.get_at(region.config().wall_filament.value - 1); - nozzle_diameter = std::min(nozzle_diameter, this->print()->config().nozzle_diameter.get_at(region.config().sparse_infill_filament.value - 1)); - nozzle_diameter = std::min(nozzle_diameter, this->print()->config().nozzle_diameter.get_at(region.config().solid_infill_filament.value - 1)); + double nozzle_diameter = this->print()->config().nozzle_diameter.get_at(region.config().outer_wall_filament_id.value - 1); + nozzle_diameter = std::min(nozzle_diameter, this->print()->config().nozzle_diameter.get_at(region.config().inner_wall_filament_id.value - 1)); + nozzle_diameter = std::min(nozzle_diameter, this->print()->config().nozzle_diameter.get_at(region.config().sparse_infill_filament_id.value - 1)); + nozzle_diameter = std::min(nozzle_diameter, this->print()->config().nozzle_diameter.get_at(region.config().internal_solid_filament_id.value - 1)); //Orca: Limit combination of infill to up to infill_combination_max_layer_height const double infill_combination_max_layer_height = region.config().infill_combination_max_layer_height.get_abs_value(nozzle_diameter); nozzle_diameter = infill_combination_max_layer_height > 0 ? std::min(infill_combination_max_layer_height, nozzle_diameter) : nozzle_diameter; - + + // Per-extruder layer height: the filament's preferred height is an explicit target that overrides the caps above, limited only by the physical nozzle diameters and the extruder's max layer height. + // Plain infill_combination keeps its historical cap untouched. + if (preferred_infill_height > 0.) { + nozzle_diameter = std::min({preferred_infill_height, + this->print()->config().nozzle_diameter.get_at(region.config().sparse_infill_filament_id.value - 1), + this->print()->config().nozzle_diameter.get_at(region.config().internal_solid_filament_id.value - 1)}); + const double combine_max_layer_height = this->print()->config().max_layer_height.get_at(combine_extruder_idx); + if (combine_max_layer_height > EPSILON) + nozzle_diameter = std::min(nozzle_diameter, combine_max_layer_height); + } + // define the combinations std::vector combine(m_layers.size(), 0); { @@ -4387,6 +4682,107 @@ void PrintObject::combine_infill() } } } + + // Per-extruder layer height: leftovers the pass above could not combine to the full group height print at the object layer height. + // Where that is below the infill extruder's min layer height, re-group them over the smallest layer count reaching the minimum (isolated leftovers with no matching neighbors stay as-is; Print::validate() warns). + const double combine_min_layer_height = this->print()->config().min_layer_height.get_at(combine_extruder_idx); + const double min_window = combine_min_layer_height > 0. ? + std::ceil(combine_min_layer_height / m_config.layer_height.value - EPSILON) * m_config.layer_height.value : 0.; + // Re-grouped windows must stay within the main pass' caps (a min_layer_height above them is + // a misconfiguration). + if (preferred_infill_height > 0. && combine_min_layer_height > m_config.layer_height.value + EPSILON && + min_window <= nozzle_diameter + EPSILON) { + // Assign leftover windows exactly like the pass above, just with the shorter target. + std::vector recombine(m_layers.size(), 0); + { + double current_height = 0.; + size_t num_layers = 0; + for (size_t layer_idx = 0; layer_idx < m_layers.size(); ++ layer_idx) { + m_print->throw_if_canceled(); + const Layer *layer = m_layers[layer_idx]; + if (layer->id() == 0) + // Skip first print layer (which may not be first layer in array because of raft). + continue; + if (current_height + layer->height >= min_window + EPSILON) { + recombine[layer_idx - 1] = num_layers; + current_height = 0.; + num_layers = 0; + } + current_height += layer->height; + ++ num_layers; + } + recombine[m_layers.size() - 1] = num_layers; + } + // Leftovers are surfaces of the combined type whose thickness is still unset. + auto leftover_expolygons = [surface_type](const LayerRegion *layerm) { + ExPolygons out; + for (const Surface &surface : layerm->fill_surfaces.surfaces) + if (surface.surface_type == surface_type && surface.thickness < 0.) + out.emplace_back(surface.expolygon); + return out; + }; + for (size_t layer_idx = 0; layer_idx < m_layers.size(); ++ layer_idx) { + m_print->throw_if_canceled(); + size_t num_layers = recombine[layer_idx]; + if (num_layers <= 1) + continue; + std::vector layerms; + layerms.reserve(num_layers); + for (size_t i = layer_idx + 1 - num_layers; i <= layer_idx; ++ i) + layerms.emplace_back(m_layers[i]->regions()[region_id]); + ExPolygons intersection = leftover_expolygons(layerms.front()); + for (size_t i = 1; i < layerms.size() && ! intersection.empty(); ++ i) + intersection = intersection_ex(leftover_expolygons(layerms[i]), intersection); + double area_threshold = layerms.front()->infill_area_threshold(); + if (! intersection.empty() && area_threshold > 0.) + intersection.erase(std::remove_if(intersection.begin(), intersection.end(), + [area_threshold](const ExPolygon &expoly) { return expoly.area() <= area_threshold; }), + intersection.end()); + if (intersection.empty()) + continue; + // Same clearance as the pass above: the leftover fills also grow to overlap perimeters. + Polygons intersection_with_clearance; + intersection_with_clearance.reserve(intersection.size()); + float clearance_offset = + 0.5f * layerms.back()->flow(frPerimeter).scaled_width() + + ((infill_pattern == ipRectilinear || + infill_pattern == ipMonotonic || + infill_pattern == ipGrid || + infill_pattern == ipLateralLattice|| + infill_pattern == ipLine || + infill_pattern == ipHoneycomb || + infill_pattern == ipLateralHoneycomb) ? 1.5f : 0.5f) * + layerms.back()->flow(frSolidInfill).scaled_width(); + for (ExPolygon &expoly : intersection) + polygons_append(intersection_with_clearance, offset(expoly, clearance_offset)); + for (LayerRegion *layerm : layerms) { + // Take out only the leftover surfaces; surfaces combined above keep their heights. + Polygons leftover; + Surfaces kept; + kept.reserve(layerm->fill_surfaces.surfaces.size()); + for (Surface &surface : layerm->fill_surfaces.surfaces) { + if (surface.surface_type == surface_type && surface.thickness < 0.) + polygons_append(leftover, to_polygons(std::move(surface.expolygon))); + else + kept.emplace_back(std::move(surface)); + } + layerm->fill_surfaces.surfaces = std::move(kept); + layerm->fill_surfaces.append(diff_ex(leftover, intersection_with_clearance), surface_type); + if (layerm == layerms.back()) { + Surface templ(surface_type, ExPolygon()); + templ.thickness = 0.; + for (LayerRegion *layerm2 : layerms) + templ.thickness += layerm2->layer()->height; + templ.thickness_layers = (unsigned short)layerms.size(); + layerm->fill_surfaces.append(intersection, templ); + } else { + layerm->fill_surfaces.append( + intersection_ex(leftover, intersection_with_clearance), + stInternalVoid); + } + } + } + } } } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 688c0f2aee4..686916bae39 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -848,6 +848,11 @@ void PrintObject::slice() // BBS: the actual first layer slices stored in layers are re-sorted by volume group and will be used to generate brim groupingVolumesForBrim(this, m_layers, firstLayerReplacedBy); + // Per-extruder layer height: combine region slices into every Nth layer where geometry allows. + // Must run before backup_untyped_slices() below so the combined slices survive the restore_untyped_slices*() calls in make_perimeters() / prepare_infill(). + this->apply_extruder_layer_heights(); + m_print->throw_if_canceled(); + // Update bounding boxes, back up raw slices of complex models. tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), @@ -869,6 +874,191 @@ void PrintObject::slice() this->set_done(posSlice); } +// ORCA: per-extruder layer height ("extruder_layer_height" printer option). +// For every region whose extruder prefers an integer multiple N (> 1) of the object layer height, +// greedily combine bottom-up runs of up to N layers on which the region keeps a (nearly) identical +// shape, merging the run's slices into its top layer to be extruded once at the run's full height +// (see LayerRegion::combined_height()); combined-away layers carry no slices for the region and +// trigger no toolchange. A run requires near-identical slices, uniform layer heights and full support +// from below (no overhang / bridge hidden inside a run). Only full runs plus the clean cap of a +// column ending above are committed, so each region prints at two consistent heights rather than +// ever-changing intermediate bands. Region tops / bottoms and overhangs keep the finer base layers. +void PrintObject::apply_extruder_layer_heights() +{ + if (m_layers.size() < 2 || this->num_printing_regions() == 0) + return; + std::vector multipliers(this->num_printing_regions(), 1); + bool any_combined = false; + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { + multipliers[region_id] = this->region_layer_height_multiplier(this->printing_region(region_id)); + any_combined |= multipliers[region_id] > 1; + } + if (! any_combined) + return; + if (m_print->config().spiral_mode || m_config.interface_shells) + // These combinations are rejected by Print::validate(), fail safe here. + return; + + BOOST_LOG_TRIVIAL(debug) << "Combining region slices to extruder layer heights for " << this->model_object()->name; + + // Never combine the first printed layer: it keeps its own height for bed adhesion (mirrors the + // id() == 0 exclusion in PrintObject::combine_infill()), and with a raft detect_surfaces_type() + // needs its slices to seed the object's bottom surfaces. + const size_t first_idx = 1; + if (m_layers.size() <= first_idx + 1) + return; + + // Adaptive mode commits runs cut short by shape drift at intermediate heights instead of + // falling back to the object layer height. + const bool adaptive = m_config.extruder_layer_height_mode.value == elhmAdaptive; + // The filament index is the extruder index on classic multi-tool printers (same mapping as + // extruder_preferred_layer_height() and Print::validate()). + const PrintConfig &print_config = m_print->config(); + auto extruder_idx_of = [](int filament_id) { + return size_t((unsigned int)std::max(1, filament_id) - 1); + }; + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { + const size_t mult = multipliers[region_id]; + if (mult <= 1) + continue; + // Shapes deviating by less than this fraction of the region's nozzle diameter are considered + // identical; the deviations swallowed stay below what printing N layers at once causes anyway. + const double nozzle_diameter = print_config.nozzle_diameter.get_at( + extruder_idx_of(this->printing_region(region_id).config().outer_wall_filament_id.value)); + const float tolerance = float(scale_(m_config.extruder_layer_height_tolerance.get_abs_value(nozzle_diameter))); + // Where geometry would fall back below the extruders' minimum layer height, runs of at least + // min_run layers are forced instead, ignoring the shape tolerance (the nozzle cannot print + // finer). Both wall filaments print fallback runs, so the coarser of their minimums decides. + const PrintRegionConfig ®ion_config = this->printing_region(region_id).config(); + double min_layer_height = print_config.min_layer_height.get_at( + extruder_idx_of(region_config.outer_wall_filament_id.value)); + if (region_config.wall_loops.value > 1) + min_layer_height = std::max(min_layer_height, print_config.min_layer_height.get_at( + extruder_idx_of(region_config.inner_wall_filament_id.value))); + size_t min_run = 1; + if (min_layer_height > m_config.layer_height.value + EPSILON) + min_run = std::min(mult, (size_t)std::ceil(min_layer_height / m_config.layer_height.value - EPSILON)); + size_t idx = first_idx; + while (idx < m_layers.size()) { + m_print->throw_if_canceled(); + ExPolygons merged = to_expolygons(m_layers[idx]->regions()[region_id]->slices.surfaces); + if (merged.empty()) { + // The region does not exist at this layer. + ++ idx; + continue; + } + // Grow the run upwards while nothing sticks out of the run's common shape by more than + // the tolerance. Uniform layer heights only (variable heights are rejected by Print::validate()). + Polygons unioned = to_polygons(merged); + size_t top_idx = idx; + bool shape_drifted = false; + while (top_idx + 1 < m_layers.size() && top_idx + 1 - idx < mult) { + const size_t next = top_idx + 1; + if (std::abs(m_layers[next]->height - m_layers[idx]->height) > EPSILON) + break; + const ExPolygons expolys = to_expolygons(m_layers[next]->regions()[region_id]->slices.surfaces); + if (expolys.empty()) + // The region ends above, the run is the clean cap of its column. + break; + ExPolygons next_merged = intersection_ex(expolys, merged); + if (next_merged.empty()) + // Laterally displaced (a painted-boundary step): this column ends, the next anchors above. + break; + Polygons next_unioned = unioned; + polygons_append(next_unioned, to_polygons(expolys)); + if (! opening(diff(union_(next_unioned), offset(next_merged, tolerance)), 0.5f * tolerance).empty()) { + shape_drifted = true; + break; + } + merged = std::move(next_merged); + unioned = std::move(next_unioned); + top_idx = next; + } + // Full runs and clean caps commit as grown; a run cut short by shape drift falls back to + // the object layer height (unless adaptive) to avoid bands of ever-changing heights on + // curved boundaries. The fallback is raised to min_run where required. + const size_t grown = top_idx + 1 - idx; + size_t commit_length; + if (grown == mult || ! shape_drifted) + commit_length = grown; + else if (adaptive) + commit_length = std::max(grown, min_run); + else + commit_length = min_run; + if (commit_length >= 2 && min_run > 1) { + // Don't leave a remainder shorter than min_run above: shorten so the next run can reach it. + size_t above = 0; + for (size_t i = idx + commit_length; i < m_layers.size() && above < min_run; ++ i) { + if (m_layers[i]->regions()[region_id]->slices.empty()) + break; + ++ above; + } + if (above > 0 && above < min_run) { + const size_t shift = min_run - above; + if (commit_length >= min_run + shift && commit_length - shift >= 2) + commit_length -= shift; + } + } + if (commit_length < 2) { + // Print this layer with the object layer height. + ++ idx; + continue; + } + size_t commit_top = std::min(idx + commit_length - 1, m_layers.size() - 1); + if (commit_top != top_idx) { + // Forced or shortened run: recompute its shape without the tolerance check, + // shrinking the range where the region ends or jumps. + merged = to_expolygons(m_layers[idx]->regions()[region_id]->slices.surfaces); + for (size_t i = idx + 1; i <= commit_top; ++ i) { + if (std::abs(m_layers[i]->height - m_layers[idx]->height) > EPSILON) { + commit_top = i - 1; + break; + } + const ExPolygons expolys = to_expolygons(m_layers[i]->regions()[region_id]->slices.surfaces); + ExPolygons next_merged = expolys.empty() ? ExPolygons() : intersection_ex(expolys, merged); + if (next_merged.empty()) { + commit_top = i - 1; + break; + } + merged = std::move(next_merged); + } + if (commit_top + 1 - idx < 2) { + // Nothing to force here, the object layer height is the last resort. + ++ idx; + continue; + } + } + // The run must rest on the object below, else the finer per-layer bridge / overhang path + // is needed. Skipped when min_run > 1: no finer path exists then, overhangs are detected + // against the layer below the whole run instead. + if (min_run <= 1) + if (const Layer *below = m_layers[idx]->lower_layer; below != nullptr && + ! opening_ex(diff_ex(merged, below->lslices, ApplySafetyOffset::Yes), 0.5f * tolerance).empty()) { + ++ idx; + continue; + } + // Commit: move the common shape to the top layer of the run, drop the layers below. + double combined_height = 0.; + for (size_t i = idx; i <= commit_top; ++ i) + combined_height += m_layers[i]->height; + LayerRegion *top_layerm = m_layers[commit_top]->regions()[region_id]; + top_layerm->slices.set(std::move(merged), stInternal); + top_layerm->m_combined_layer_count = (unsigned short)(commit_top - idx + 1); + top_layerm->m_combined_height = combined_height; + for (size_t i = idx; i < commit_top; ++ i) { + LayerRegion *combined_away = m_layers[i]->regions()[region_id]; + combined_away->slices.clear(); + // 0 marks "extrudes at the run top above", as opposed to genuinely absent geometry. + combined_away->m_combined_layer_count = 0; + } + // Do not touch Layer::lslices here: they describe the final object and keep driving + // top / bottom detection of the other regions, brim, supports and overhang handling. + idx = commit_top + 1; + } + m_print->throw_if_canceled(); + } +} + static bool bool_from_full_config(const DynamicPrintConfig &full_cfg, const char *key, bool fallback) { if (!full_cfg.has(key)) @@ -2575,7 +2765,7 @@ static bool apply_mixed_region_surface_offsets(PrintObject &print_object) if (layerm == nullptr || layerm->slices.empty()) continue; - const unsigned int filament_id = unsigned(std::max(0, layerm->region().config().wall_filament.value)); + const unsigned int filament_id = unsigned(std::max(0, layerm->region().config().outer_wall_filament_id.value)); if (!mixed_mgr.is_mixed(filament_id, num_physical)) continue; @@ -2769,7 +2959,7 @@ static std::vector> whole_object_local_z_segmentation_by if (layerm == nullptr || layerm->slices.empty()) continue; - const unsigned int filament_id = unsigned(std::max(0, layerm->region().config().wall_filament.value)); + const unsigned int filament_id = unsigned(std::max(0, layerm->region().config().outer_wall_filament_id.value)); if (!mixed_mgr.is_mixed(filament_id, num_physical)) continue; if (filament_id >= segmentation[layer_id].size()) @@ -4469,7 +4659,7 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector< int self_extruder_id = -1; // 1-based extruder ID ExPolygons explicit_self_expolygons; ExPolygons default_self_expolygons; - if (const int cfg_wall = parent_print_region.config().wall_filament.value; + if (const int cfg_wall = parent_print_region.config().outer_wall_filament_id.value; cfg_wall >= 1 && cfg_wall <= int(by_extruder.size())) self_extruder_id = cfg_wall; if (clamp_parent_to_geometry && default_bbox.defined && parent_layer_region_bbox.overlap(default_bbox)) @@ -4732,7 +4922,7 @@ static std::vector collect_surface_emboss_mixe continue; processed_region_ids.emplace_back(region_id); - if (!mixed_mgr.is_mixed(unsigned(std::max(0, volume_region.region->config().wall_filament.value)), num_physical)) + if (!mixed_mgr.is_mixed(unsigned(std::max(0, volume_region.region->config().outer_wall_filament_id.value)), num_physical)) continue; out.push_back({ volume, region_id }); @@ -4767,7 +4957,7 @@ static void export_surface_emboss_mixed_layer_svg( }) != candidates.end(); SVG::ExPolygonAttributes attrs( - "region " + std::to_string(region_id) + " wall=" + std::to_string(layerm->region().config().wall_filament.value), + "region " + std::to_string(region_id) + " wall=" + std::to_string(layerm->region().config().outer_wall_filament_id.value), is_candidate ? "#3b82f6" : "#bfc5cc", is_candidate ? 0.35f : 0.14f); attrs.outline_width = scale_(0.05f); @@ -4829,9 +5019,9 @@ static void dump_surface_emboss_mixed_layer_state( std::ostringstream line; line << std::fixed << std::setprecision(4) << " region=" << region_id - << " wall=" << layerm->region().config().wall_filament.value - << " sparse=" << layerm->region().config().sparse_infill_filament.value - << " solid=" << layerm->region().config().solid_infill_filament.value + << " wall=" << layerm->region().config().outer_wall_filament_id.value + << " sparse=" << layerm->region().config().sparse_infill_filament_id.value + << " solid=" << layerm->region().config().internal_solid_filament_id.value << " area=" << slice_area; append_surface_emboss_mixed_debug_line(print_object, line.str()); } @@ -4848,14 +5038,14 @@ static void dump_surface_emboss_mixed_layer_state( << " candidate region=" << candidate.region_id << " volume_name=" << candidate.volume->name << " volume_extruder=" << candidate.volume->extruder_id() - << " cfg_wall=" << layerm->region().config().wall_filament.value + << " cfg_wall=" << layerm->region().config().outer_wall_filament_id.value << " depth=" << float(candidate.volume->emboss_shape->projection.depth) << " shell_delta_mm=" << unscale(shell_delta_scaled) << " area=" << slice_area; append_surface_emboss_mixed_debug_line(print_object, line.str()); if (segmentation_layer != nullptr) { - const int cfg_wall = layerm->region().config().wall_filament.value; + const int cfg_wall = layerm->region().config().outer_wall_filament_id.value; if (cfg_wall >= 1 && cfg_wall <= int(segmentation_layer->size())) { const double seg_area = std::abs(area((*segmentation_layer)[size_t(cfg_wall - 1)])); std::ostringstream seg_line; @@ -4949,7 +5139,7 @@ static bool apply_surface_emboss_mixed_region_override(PrintObject &print_object continue; processed_region_ids.emplace_back(region_id); - const unsigned int filament_id = unsigned(std::max(0, volume_region.region->config().wall_filament.value)); + const unsigned int filament_id = unsigned(std::max(0, volume_region.region->config().outer_wall_filament_id.value)); if (!mixed_mgr.is_mixed(filament_id, num_physical)) continue; @@ -4982,7 +5172,7 @@ static bool apply_surface_emboss_mixed_region_override(PrintObject &print_object << " region=" << region_id << " volume_name=" << volume->name << " volume_extruder=" << volume->extruder_id() - << " cfg_wall=" << volume_region.region->config().wall_filament.value + << " cfg_wall=" << volume_region.region->config().outer_wall_filament_id.value << " depth=" << float(volume->emboss_shape->projection.depth) << " shell_delta_mm=" << unscale(emboss_surface_mixed_shell_override_delta(*emboss_layerm, *volume)) << " mask_area=" << std::abs(area(override_mask)); @@ -5007,7 +5197,7 @@ static bool apply_surface_emboss_mixed_region_override(PrintObject &print_object LayerRegion *target_layerm = layer.get_region(target_region_id); if (target_layerm == nullptr || target_layerm->slices.empty()) continue; - if (target_layerm->region().config().wall_filament.value == int(filament_id)) + if (target_layerm->region().config().outer_wall_filament_id.value == int(filament_id)) continue; ExPolygons stolen = intersection_ex(target_layerm->slices.surfaces, override_mask); @@ -5024,7 +5214,7 @@ static bool apply_surface_emboss_mixed_region_override(PrintObject &print_object << " layer=" << layer_id << " emboss_region=" << region_id << " from_region=" << target_region_id - << " from_wall=" << target_layerm->region().config().wall_filament.value + << " from_wall=" << target_layerm->region().config().outer_wall_filament_id.value << " stolen_area=" << std::abs(area(stolen)); append_surface_emboss_mixed_debug_line(print_object, line.str()); diff --git a/src/libslic3r/PrintRegion.cpp b/src/libslic3r/PrintRegion.cpp index f4b82faf4ab..a6d842ed74c 100644 --- a/src/libslic3r/PrintRegion.cpp +++ b/src/libslic3r/PrintRegion.cpp @@ -18,20 +18,22 @@ bool internal_solid_infill_uses_sparse_filament(const PrintRegionConfig &config, unsigned int PrintRegion::extruder(FlowRole role) const { size_t extruder = 0; - if (role == frPerimeter || role == frExternalPerimeter) - extruder = m_config.wall_filament; + if (role == frPerimeter) + extruder = m_config.inner_wall_filament_id; + else if (role == frExternalPerimeter) + extruder = m_config.outer_wall_filament_id; else if (role == frInfill) - extruder = m_config.sparse_infill_filament; + extruder = m_config.sparse_infill_filament_id; else if (role == frSolidInfill) - extruder = internal_solid_infill_uses_sparse_filament(m_config, role) ? m_config.sparse_infill_filament : m_config.solid_infill_filament; + extruder = internal_solid_infill_uses_sparse_filament(m_config, role) ? m_config.sparse_infill_filament_id : m_config.internal_solid_filament_id; else if (role == frTopSolidInfill) - extruder = m_config.solid_infill_filament; + extruder = m_config.top_surface_filament_id; else throw Slic3r::InvalidArgument("Unknown role"); return extruder; } -Flow PrintRegion::flow(const PrintObject &object, FlowRole role, double layer_height, bool first_layer) const +Flow PrintRegion::flow(const PrintObject &object, FlowRole role, double layer_height, bool first_layer, unsigned int filament_id) const { const PrintConfig &print_config = object.print()->config(); ConfigOptionFloatOrPercent config_width; @@ -56,17 +58,21 @@ Flow PrintRegion::flow(const PrintObject &object, FlowRole role, double layer_he if (config_width.value == 0) config_width = object.config().line_width; - // Get the configured nozzle_diameter for the extruder associated to the flow role requested. + // Get the configured nozzle_diameter for the extruder associated to the flow role requested, + // or for the explicitly given filament when it differs from the role's default mapping (top / bottom surface fills). // Here this->extruder(role) - 1 may underflow to MAX_INT, but then the get_at() will follback to zero'th element, so everything is all right. - auto nozzle_diameter = float(print_config.nozzle_diameter.get_at(this->extruder(role) - 1)); + auto nozzle_diameter = float(print_config.nozzle_diameter.get_at((filament_id > 0 ? filament_id : this->extruder(role)) - 1)); return Flow::new_from_config_width(role, config_width, nozzle_diameter, float(layer_height)); } coordf_t PrintRegion::nozzle_dmr_avg(const PrintConfig &print_config) const { - return (print_config.nozzle_diameter.get_at(m_config.wall_filament.value - 1) + - print_config.nozzle_diameter.get_at(m_config.sparse_infill_filament.value - 1) + - print_config.nozzle_diameter.get_at(m_config.solid_infill_filament.value - 1)) / 3.; + return (print_config.nozzle_diameter.get_at(m_config.outer_wall_filament_id.value - 1) + + print_config.nozzle_diameter.get_at(m_config.inner_wall_filament_id.value - 1) + + print_config.nozzle_diameter.get_at(m_config.sparse_infill_filament_id.value - 1) + + print_config.nozzle_diameter.get_at(m_config.internal_solid_filament_id.value - 1) + + print_config.nozzle_diameter.get_at(m_config.top_surface_filament_id.value - 1) + + print_config.nozzle_diameter.get_at(m_config.bottom_surface_filament_id.value - 1)) / 6.; } coordf_t PrintRegion::bridging_height_avg(const PrintConfig &print_config) const @@ -83,12 +89,19 @@ void PrintRegion::collect_object_printing_extruders(const PrintConfig &print_con int i = std::max(0, extruder_id - 1); object_extruders.emplace_back((i >= num_extruders) ? 0 : i); }; - if (region_config.wall_loops.value > 0 || has_brim) - emplace_extruder(region_config.wall_filament); + if (region_config.wall_loops.value > 0 || has_brim) { + emplace_extruder(region_config.outer_wall_filament_id); + if (region_config.wall_loops.value > 1) + emplace_extruder(region_config.inner_wall_filament_id); + } if (region_config.sparse_infill_density.value > 0) - emplace_extruder(region_config.sparse_infill_filament); - if (region_config.top_shell_layers.value > 0 || region_config.bottom_shell_layers.value > 0) - emplace_extruder(region_config.solid_infill_filament); + emplace_extruder(region_config.sparse_infill_filament_id); + if (region_config.sparse_infill_density.value > 0 || region_config.top_shell_layers.value > 0 || region_config.bottom_shell_layers.value > 0) + emplace_extruder(region_config.internal_solid_filament_id); + if (region_config.top_shell_layers.value > 0) + emplace_extruder(region_config.top_surface_filament_id); + if (region_config.bottom_shell_layers.value > 0) + emplace_extruder(region_config.bottom_surface_filament_id); } void PrintRegion::collect_object_printing_extruders(const Print &print, std::vector &object_extruders) const @@ -98,9 +111,12 @@ void PrintRegion::collect_object_printing_extruders(const Print &print, std::vec #ifndef NDEBUG // BBS auto num_extruders = int(print.config().filament_diameter.size()); - assert(this->config().wall_filament <= num_extruders); - assert(this->config().sparse_infill_filament <= num_extruders); - assert(this->config().solid_infill_filament <= num_extruders); + assert(this->config().outer_wall_filament_id <= num_extruders); + assert(this->config().inner_wall_filament_id <= num_extruders); + assert(this->config().sparse_infill_filament_id <= num_extruders); + assert(this->config().internal_solid_filament_id <= num_extruders); + assert(this->config().top_surface_filament_id <= num_extruders); + assert(this->config().bottom_surface_filament_id <= num_extruders); #endif collect_object_printing_extruders(print.config(), this->config(), print.has_brim(), object_extruders); } diff --git a/src/libslic3r/Slicing.cpp b/src/libslic3r/Slicing.cpp index f3499b7d361..f8bc5600146 100644 --- a/src/libslic3r/Slicing.cpp +++ b/src/libslic3r/Slicing.cpp @@ -73,8 +73,16 @@ SlicingParameters SlicingParameters::create_from_config( // which is consistent with the requirement that if support_filament == 0 resp. support_interface_filament == 0, // support will not trigger tool change, but it will use the current nozzle instead. // In that case all the nozzles have to be of the same diameter. - coordf_t support_material_extruder_dmr = print_config.nozzle_diameter.get_at(object_config.support_filament.value - 1); - coordf_t support_material_interface_extruder_dmr = print_config.nozzle_diameter.get_at(object_config.support_interface_filament.value - 1); + // Support_nozzle_diameter restricts which extruders / nozzle diameter may print support ("default" = no restriction) + auto restricted_default = [&object_config](int configured) { + return configured == 0 && object_config.support_nozzle_diameter.value > 0.; + }; + coordf_t support_material_extruder_dmr = restricted_default(object_config.support_filament.value) ? + object_config.support_nozzle_diameter.value : + print_config.nozzle_diameter.get_at(object_config.support_filament.value - 1); + coordf_t support_material_interface_extruder_dmr = restricted_default(object_config.support_interface_filament.value) ? + object_config.support_nozzle_diameter.value : + print_config.nozzle_diameter.get_at(object_config.support_interface_filament.value - 1); bool soluble_interface = object_config.support_top_z_distance.value == 0.; SlicingParameters params; @@ -94,12 +102,31 @@ SlicingParameters SlicingParameters::create_from_config( params.max_layer_height = std::numeric_limits::max(); if (object_config.enable_support.value || params.base_raft_layers > 0 || object_config.enforce_support_layers > 0) { // Has some form of support. Add the support layers to the minimum / maximum layer height limits. + auto support_layer_height_limit = [&print_config, &object_config, &restricted_default](int configured, bool min_limit) -> coordf_t { + auto from_nozzle = [&print_config, min_limit](int filament) { + return min_limit ? min_layer_height_from_nozzle(print_config, filament) : max_layer_height_from_nozzle(print_config, filament); + }; + if (restricted_default(configured)) { + // Combine the limits of all filaments the restriction allows. + coordf_t limit = min_limit ? 0. : std::numeric_limits::max(); + bool found = false; + for (size_t i = 0; i < print_config.nozzle_diameter.values.size(); ++ i) + if (std::abs(print_config.nozzle_diameter.values[i] - object_config.support_nozzle_diameter.value) < EPSILON) { + coordf_t l = from_nozzle(int(i + 1)); + limit = min_limit ? std::max(limit, l) : std::min(limit, l); + found = true; + } + if (found) + return limit; + } + return from_nozzle(configured); + }; params.min_layer_height = std::max( - min_layer_height_from_nozzle(print_config, object_config.support_filament), - min_layer_height_from_nozzle(print_config, object_config.support_interface_filament)); + support_layer_height_limit(object_config.support_filament.value, true), + support_layer_height_limit(object_config.support_interface_filament.value, true)); params.max_layer_height = std::min( - max_layer_height_from_nozzle(print_config, object_config.support_filament), - max_layer_height_from_nozzle(print_config, object_config.support_interface_filament)); + support_layer_height_limit(object_config.support_filament.value, false), + support_layer_height_limit(object_config.support_interface_filament.value, false)); params.max_suport_layer_height = params.max_layer_height; } if (object_extruders.empty()) { diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 0de4067770a..903e9a9614a 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -55,7 +55,7 @@ void remove_bridges_from_contacts( // Surface supporting this layer, expanded by 0.5 * nozzle_diameter, as we consider this kind of overhang to be sufficiently supported. Polygons lower_grown_slices = expand(lower_layer.lslices, //FIXME to mimic the decision in the perimeter generator, we should use half the external perimeter width. - 0.5f * float(scale_(print_config.nozzle_diameter.get_at(layerm.region().config().wall_filament - 1))), + 0.5f * float(scale_(print_config.nozzle_diameter.get_at(layerm.region().config().outer_wall_filament_id - 1))), SUPPORT_SURFACES_OFFSET_PARAMETERS); // Collect perimeters of this layer. //FIXME split_at_first_point() could split a bridge mid-way diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index 7580976ac99..e83981f9ac0 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1224,7 +1224,7 @@ namespace SupportMaterialInternal { // Surface supporting this layer, expanded by 0.5 * nozzle_diameter, as we consider this kind of overhang to be sufficiently supported. Polygons lower_grown_slices = expand(lower_layer_polygons, //FIXME to mimic the decision in the perimeter generator, we should use half the external perimeter width. - 0.5f * float(scale_(print_config.nozzle_diameter.get_at(layerm.region().config().wall_filament-1))), + 0.5f * float(scale_(print_config.nozzle_diameter.get_at(layerm.region().config().outer_wall_filament_id-1))), SUPPORT_SURFACES_OFFSET_PARAMETERS); // Collect perimeters of this layer. //FIXME split_at_first_point() could split a bridge mid-way @@ -2149,7 +2149,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::top_contact_layers( // check if the sharp tails should be extended higher bool detect_first_sharp_tail_only = false; - const coordf_t extrusion_width = m_object_config->line_width.get_abs_value(object.print()->config().nozzle_diameter.get_at(object.config().support_interface_filament-1)); + const coordf_t extrusion_width = m_object_config->line_width.get_abs_value(support_material_nozzle_diameter(&object, object.config().support_interface_filament)); const coordf_t extrusion_width_scaled = scale_(extrusion_width); if (is_auto(m_object_config->support_type.value) && g_config_support_sharp_tails && !detect_first_sharp_tail_only) { for (size_t layer_nr = layer_id_start; layer_nr < num_layers; layer_nr++) { diff --git a/src/libslic3r/Support/SupportParameters.hpp b/src/libslic3r/Support/SupportParameters.hpp index 655ee0d8060..8d34dee9413 100644 --- a/src/libslic3r/Support/SupportParameters.hpp +++ b/src/libslic3r/Support/SupportParameters.hpp @@ -158,7 +158,8 @@ struct SupportParameters { assert(slicing_params.raft_layers() == 0); } - const auto nozzle_diameter = print_config.nozzle_diameter.get_at(object_config.support_interface_filament - 1); + // ORCA: honors the support nozzle diameter restriction for "default" support filaments. + const auto nozzle_diameter = support_material_nozzle_diameter(&object, object_config.support_interface_filament); const coordf_t extrusion_width = object_config.line_width.get_abs_value(nozzle_diameter); support_extrusion_width = object_config.support_line_width.get_abs_value(nozzle_diameter); support_extrusion_width = support_extrusion_width > 0 ? support_extrusion_width : extrusion_width; diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 17e7fb044f0..352e46b44b3 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1319,7 +1319,7 @@ void TreeSupport::generate_toolpaths() { const PrintObjectConfig &object_config = m_object->config(); coordf_t support_extrusion_width = m_support_params.support_extrusion_width; - coordf_t nozzle_diameter = m_print_config->nozzle_diameter.get_at(object_config.support_filament - 1); + coordf_t nozzle_diameter = support_material_nozzle_diameter(m_object, object_config.support_filament); coordf_t layer_height = object_config.layer_height.value; const size_t wall_count = object_config.tree_support_wall_count.value; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 11c2bd16482..d9d106bea03 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -557,9 +557,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co toggle_field(el, have_perimeters); bool have_infill = config->option("sparse_infill_density")->value > 0; - // sparse_infill_filament uses the same logic as in Print::extruders() + // sparse_infill_filament_id uses the same logic as in Print::extruders() for (auto el : { "sparse_infill_pattern", "infill_combination", - "minimum_sparse_infill_area", "infill_anchor_max","infill_shift_step","sparse_infill_rotate_template","symmetric_infill_y_axis"}) + "minimum_sparse_infill_area", "sparse_infill_filament_id", "infill_anchor_max","infill_shift_step","sparse_infill_rotate_template","symmetric_infill_y_axis"}) toggle_line(el, have_infill); bool have_combined_infill = config->opt_bool("infill_combination") && have_infill; @@ -614,7 +614,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co for (auto el : { "infill_direction", "sparse_infill_line_width", "fill_multiline","gap_fill_target","filter_out_gap_fill","infill_wall_overlap", "sparse_infill_speed", "bridge_speed", "internal_bridge_speed", "bridge_angle", "internal_bridge_angle", - "solid_infill_direction", "solid_infill_rotate_template", "internal_solid_infill_pattern", "solid_infill_filament", + "solid_infill_direction", "solid_infill_rotate_template", "internal_solid_infill_pattern", "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", }) toggle_field(el, have_infill || has_solid_infill); @@ -653,8 +653,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co bool have_brim_width = (config->opt_enum("brim_type") != btNoBrim) && config->opt_enum("brim_type") != btAutoBrim && config->opt_enum("brim_type") != btPainted; toggle_field("brim_width", have_brim_width); - // wall_filament uses the same logic as in Print::extruders() - toggle_field("wall_filament", have_perimeters || have_brim); + // Wall filament selectors use the same logic as in Print::extruders(). + toggle_field("outer_wall_filament_id", have_perimeters || have_brim); + toggle_field("inner_wall_filament_id", have_perimeters || have_brim); bool have_brim_ear = (config->opt_enum("brim_type") == btEar); const auto brim_width = config->opt_float("brim_width"); @@ -730,6 +731,14 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co toggle_field("inner_wall_line_width", have_perimeters || have_skirt || have_brim); toggle_field("support_filament", have_support_material || have_skirt); + // ORCA: support_nozzle_diameter only applies to printers whose extruders have differing nozzle diameters. + bool mixed_nozzle_sizes = false; + if (const auto *nozzle_diameters = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter"); + nozzle_diameters != nullptr && ! nozzle_diameters->values.empty()) + for (double d : nozzle_diameters->values) + mixed_nozzle_sizes |= std::abs(d - nozzle_diameters->values.front()) > EPSILON; + toggle_line("support_nozzle_diameter", have_support_material && mixed_nozzle_sizes); + toggle_line("raft_contact_distance", have_raft && !have_support_soluble); // Orca: Raft, grid, snug and organic supports use these two parameters to control the size & density of the "brim"/flange @@ -760,9 +769,6 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co for (auto el : { "prime_tower_width", "prime_tower_brim_width"}) toggle_line(el, have_prime_tower); - for (auto el : {"wall_filament", "sparse_infill_filament", "solid_infill_filament", "wipe_tower_filament"}) - toggle_line(el, !bSEMM); - bool purge_in_primetower = preset_bundle->printers.get_edited_preset().config.opt_bool("purge_in_prime_tower"); for (auto el : {"wipe_tower_cone_angle", diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 87a6d909cb5..5ef5b1846b6 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -146,7 +146,7 @@ std::map> SettingsFactory::PART_CAT }}, { L("Strength"), {{"wall_loops", "",1},{"top_shell_layers", L("Top Solid Layers"),1},{"top_shell_thickness", L("Top Minimum Shell Thickness"),1},{"top_surface_density", L("Top Surface Density"),1}, {"bottom_shell_layers", L("Bottom Solid Layers"),1}, {"bottom_shell_thickness", L("Bottom Minimum Shell Thickness"),1},{"bottom_surface_density", L("Bottom Surface Density"),1}, - {"sparse_infill_density", "",1},{"sparse_infill_pattern", "",1},{"sparse_infill_filament", "",1},{"lateral_lattice_angle_1", "",1},{"lateral_lattice_angle_2", "",1},{"infill_overhang_angle", "",1},{"infill_anchor", "",1},{"infill_anchor_max", "",1},{"top_surface_pattern", "",1},{"bottom_surface_pattern", "",1}, {"internal_solid_infill_pattern", "",1}, + {"sparse_infill_density", "",1},{"sparse_infill_pattern", "",1},{"sparse_infill_filament_id", "",1},{"lateral_lattice_angle_1", "",1},{"lateral_lattice_angle_2", "",1},{"infill_overhang_angle", "",1},{"infill_anchor", "",1},{"infill_anchor_max", "",1},{"top_surface_pattern", "",1},{"bottom_surface_pattern", "",1}, {"internal_solid_infill_pattern", "",1}, {"align_infill_direction_to_model", "", 1}, {"extra_solid_infills", "", 1}, {"infill_combination", "",1}, {"infill_combination_max_layer_height", "",1}, {"infill_wall_overlap", "",1},{"top_bottom_infill_wall_overlap", "",1}, {"solid_infill_direction", "",1}, {"infill_direction", "",1}, {"bridge_angle", "",1}, {"internal_bridge_angle", "",1}, {"minimum_sparse_infill_area", "",1} diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index b1130ea33c1..be251b4a450 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -701,7 +701,8 @@ void ObjectList::update_filament_values_for_items(const size_t filaments_count) } m_objects_model->SetExtruder(extruder, item); - static const char *keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", + static const char *keys[] = {"outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", + "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", "support_filament", "support_interface_filament"}; for (auto key : keys) if (object->config.has(key) && object->config.opt_int(key) > filaments_count) @@ -877,7 +878,8 @@ void ObjectList::update_filament_values_for_items_when_delete_filament(const siz } m_objects_model->SetExtruder(extruder, item); - static const char* keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", + static const char* keys[] = {"outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", + "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", "support_filament", "support_interface_filament"}; for (auto key : keys) { if (object->config.has(key)) { @@ -5902,9 +5904,9 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) else config.set_key_value("extruder", new ConfigOptionInt(new_extruder)); - // config.set("sparse_infill_filament", new_extruder); - // config.set("solid_infill_filament", new_extruder); - // config.set("wall_filament", new_extruder); + // config.set("sparse_infill_filament_id", new_extruder); + // config.set("internal_solid_filament_id", new_extruder); + // config.set("outer_wall_filament_id", new_extruder); wxGetApp().obj_list()->update_selections(); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 435b72d0933..87142ca5a41 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -80,7 +80,7 @@ namespace { template int resolve_sparse_infill_filament(const ConfigLike &config, int inherited_sparse_infill_filament) { - const ConfigOption *sparse_opt = config.option("sparse_infill_filament"); + const ConfigOption *sparse_opt = config.option("sparse_infill_filament_id"); return sparse_opt != nullptr && sparse_opt->getInt() > 0 ? sparse_opt->getInt() : inherited_sparse_infill_filament; } @@ -1370,9 +1370,16 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; int glb_support_intf_extr = glb_config.opt_int("support_interface_filament"); int glb_support_extr = glb_config.opt_int("support_filament"); - int glb_wall_extr = glb_config.opt_int("wall_filament"); - int glb_sparse_infill_extr = glb_config.opt_int("sparse_infill_filament"); - int glb_solid_infill_extr = glb_config.opt_int("solid_infill_filament"); + int glb_outer_wall_extr = glb_config.opt_int("outer_wall_filament_id"); + int glb_inner_wall_extr = glb_config.opt_int("inner_wall_filament_id"); + if (glb_outer_wall_extr == 0) glb_outer_wall_extr = glb_inner_wall_extr; + if (glb_inner_wall_extr == 0) glb_inner_wall_extr = glb_outer_wall_extr; + int glb_sparse_infill_extr = glb_config.opt_int("sparse_infill_filament_id"); + int glb_internal_solid_extr = glb_config.opt_int("internal_solid_filament_id"); + int glb_top_surface_extr = glb_config.opt_int("top_surface_filament_id"); + int glb_bottom_surface_extr = glb_config.opt_int("bottom_surface_filament_id"); + if (glb_top_surface_extr == 0) glb_top_surface_extr = glb_internal_solid_extr; + if (glb_bottom_surface_extr == 0) glb_bottom_surface_extr = glb_internal_solid_extr; bool glb_support = glb_config.opt_bool("enable_support"); glb_support |= glb_config.opt_int("raft_layers") > 0; @@ -1426,25 +1433,59 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const plate_extruders.push_back(glb_support_extr); } - int obj_wall_extr = glb_wall_extr; - const ConfigOption* wall_opt = mo->config.option("wall_filament"); - if (wall_opt != nullptr) - obj_wall_extr = wall_opt->getInt(); - if (obj_wall_extr != 1) - plate_extruders.push_back(obj_wall_extr); + int obj_outer_wall_extr = 0; + if (const ConfigOption* wall_opt = mo->config.option("outer_wall_filament_id"); wall_opt != nullptr) + obj_outer_wall_extr = wall_opt->getInt(); + if (obj_outer_wall_extr == 0) + if (const ConfigOption* wall_opt = mo->config.option("inner_wall_filament_id"); wall_opt != nullptr) + obj_outer_wall_extr = wall_opt->getInt(); + if (obj_outer_wall_extr != 0) + plate_extruders.push_back(obj_outer_wall_extr); + else if (glb_outer_wall_extr != 0) + plate_extruders.push_back(glb_outer_wall_extr); + + int obj_inner_wall_extr = 0; + if (const ConfigOption* wall_opt = mo->config.option("inner_wall_filament_id"); wall_opt != nullptr) + obj_inner_wall_extr = wall_opt->getInt(); + if (obj_inner_wall_extr == 0) + if (const ConfigOption* wall_opt = mo->config.option("outer_wall_filament_id"); wall_opt != nullptr) + obj_inner_wall_extr = wall_opt->getInt(); + if (obj_inner_wall_extr != 0) + plate_extruders.push_back(obj_inner_wall_extr); + else if (glb_inner_wall_extr != 0) + plate_extruders.push_back(glb_inner_wall_extr); const int object_sparse_infill_extr = resolve_sparse_infill_filament(mo->config, glb_sparse_infill_extr); - if (object_sparse_infill_extr != 1) + if (object_sparse_infill_extr != 0) plate_extruders.push_back(object_sparse_infill_extr); - int obj_solid_infill_extr = 1; - const ConfigOption* solid_infill_opt = mo->config.option("solid_infill_filament"); - if (solid_infill_opt != nullptr) - obj_solid_infill_extr = solid_infill_opt->getInt(); - if (obj_solid_infill_extr != 1) - plate_extruders.push_back(obj_solid_infill_extr); - else if (glb_solid_infill_extr != 1) - plate_extruders.push_back(glb_solid_infill_extr); + int obj_internal_solid_extr = 0; + if (const ConfigOption* solid_opt = mo->config.option("internal_solid_filament_id"); solid_opt != nullptr) + obj_internal_solid_extr = solid_opt->getInt(); + if (obj_internal_solid_extr != 0) + plate_extruders.push_back(obj_internal_solid_extr); + else if (glb_internal_solid_extr != 0) + plate_extruders.push_back(glb_internal_solid_extr); + + int obj_top_surface_extr = 0; + if (const ConfigOption* top_opt = mo->config.option("top_surface_filament_id"); top_opt != nullptr) + obj_top_surface_extr = top_opt->getInt(); + if (obj_top_surface_extr == 0) + obj_top_surface_extr = obj_internal_solid_extr; + if (obj_top_surface_extr != 0) + plate_extruders.push_back(obj_top_surface_extr); + else if (glb_top_surface_extr != 0) + plate_extruders.push_back(glb_top_surface_extr); + + int obj_bottom_surface_extr = 0; + if (const ConfigOption* bottom_opt = mo->config.option("bottom_surface_filament_id"); bottom_opt != nullptr) + obj_bottom_surface_extr = bottom_opt->getInt(); + if (obj_bottom_surface_extr == 0) + obj_bottom_surface_extr = obj_internal_solid_extr; + if (obj_bottom_surface_extr != 0) + plate_extruders.push_back(obj_bottom_surface_extr); + else if (glb_bottom_surface_extr != 0) + plate_extruders.push_back(glb_bottom_surface_extr); } @@ -1484,9 +1525,16 @@ std::vector PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D // if 3mf file int glb_support_intf_extr = full_config.opt_int("support_interface_filament"); int glb_support_extr = full_config.opt_int("support_filament"); - int glb_wall_extr = full_config.opt_int("wall_filament"); - int glb_sparse_infill_extr = full_config.opt_int("sparse_infill_filament"); - int glb_solid_infill_extr = full_config.opt_int("solid_infill_filament"); + int glb_outer_wall_extr = full_config.opt_int("outer_wall_filament_id"); + int glb_inner_wall_extr = full_config.opt_int("inner_wall_filament_id"); + if (glb_outer_wall_extr == 0) glb_outer_wall_extr = glb_inner_wall_extr; + if (glb_inner_wall_extr == 0) glb_inner_wall_extr = glb_outer_wall_extr; + int glb_sparse_infill_extr = full_config.opt_int("sparse_infill_filament_id"); + int glb_internal_solid_extr = full_config.opt_int("internal_solid_filament_id"); + int glb_top_surface_extr = full_config.opt_int("top_surface_filament_id"); + int glb_bottom_surface_extr = full_config.opt_int("bottom_surface_filament_id"); + if (glb_top_surface_extr == 0) glb_top_surface_extr = glb_internal_solid_extr; + if (glb_bottom_surface_extr == 0) glb_bottom_surface_extr = glb_internal_solid_extr; bool glb_support = full_config.opt_bool("enable_support"); glb_support |= full_config.opt_int("raft_layers") > 0; @@ -1570,25 +1618,59 @@ std::vector PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D else if (glb_support_extr != 0) plate_extruders.push_back(glb_support_extr); - int obj_wall_extr = glb_wall_extr; - const ConfigOption* wall_opt = object->config.option("wall_filament"); - if (wall_opt != nullptr) - obj_wall_extr = wall_opt->getInt(); - if (obj_wall_extr != 1) - plate_extruders.push_back(obj_wall_extr); + int obj_outer_wall_extr = 0; + if (const ConfigOption* wall_opt = object->config.option("outer_wall_filament_id"); wall_opt != nullptr) + obj_outer_wall_extr = wall_opt->getInt(); + if (obj_outer_wall_extr == 0) + if (const ConfigOption* wall_opt = object->config.option("inner_wall_filament_id"); wall_opt != nullptr) + obj_outer_wall_extr = wall_opt->getInt(); + if (obj_outer_wall_extr != 0) + plate_extruders.push_back(obj_outer_wall_extr); + else if (glb_outer_wall_extr != 0) + plate_extruders.push_back(glb_outer_wall_extr); + + int obj_inner_wall_extr = 0; + if (const ConfigOption* wall_opt = object->config.option("inner_wall_filament_id"); wall_opt != nullptr) + obj_inner_wall_extr = wall_opt->getInt(); + if (obj_inner_wall_extr == 0) + if (const ConfigOption* wall_opt = object->config.option("outer_wall_filament_id"); wall_opt != nullptr) + obj_inner_wall_extr = wall_opt->getInt(); + if (obj_inner_wall_extr != 0) + plate_extruders.push_back(obj_inner_wall_extr); + else if (glb_inner_wall_extr != 0) + plate_extruders.push_back(glb_inner_wall_extr); const int object_sparse_infill_extr = resolve_sparse_infill_filament(object->config, glb_sparse_infill_extr); - if (object_sparse_infill_extr != 1) + if (object_sparse_infill_extr != 0) plate_extruders.push_back(object_sparse_infill_extr); - int obj_solid_infill_extr = 1; - const ConfigOption* solid_infill_opt = object->config.option("solid_infill_filament"); - if (solid_infill_opt != nullptr) - obj_solid_infill_extr = solid_infill_opt->getInt(); - if (obj_solid_infill_extr != 1) - plate_extruders.push_back(obj_solid_infill_extr); - else if (glb_solid_infill_extr != 1) - plate_extruders.push_back(glb_solid_infill_extr); + int obj_internal_solid_extr = 0; + if (const ConfigOption* solid_opt = object->config.option("internal_solid_filament_id"); solid_opt != nullptr) + obj_internal_solid_extr = solid_opt->getInt(); + if (obj_internal_solid_extr != 0) + plate_extruders.push_back(obj_internal_solid_extr); + else if (glb_internal_solid_extr != 0) + plate_extruders.push_back(glb_internal_solid_extr); + + int obj_top_surface_extr = 0; + if (const ConfigOption* top_opt = object->config.option("top_surface_filament_id"); top_opt != nullptr) + obj_top_surface_extr = top_opt->getInt(); + if (obj_top_surface_extr == 0) + obj_top_surface_extr = obj_internal_solid_extr; + if (obj_top_surface_extr != 0) + plate_extruders.push_back(obj_top_surface_extr); + else if (glb_top_surface_extr != 0) + plate_extruders.push_back(glb_top_surface_extr); + + int obj_bottom_surface_extr = 0; + if (const ConfigOption* bottom_opt = object->config.option("bottom_surface_filament_id"); bottom_opt != nullptr) + obj_bottom_surface_extr = bottom_opt->getInt(); + if (obj_bottom_surface_extr == 0) + obj_bottom_surface_extr = obj_internal_solid_extr; + if (obj_bottom_surface_extr != 0) + plate_extruders.push_back(obj_bottom_surface_extr); + else if (glb_bottom_surface_extr != 0) + plate_extruders.push_back(glb_bottom_surface_extr); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cc0e8ecef11..90e80661cac 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -247,24 +247,20 @@ static void collect_filament_slots_from_config( int num_filaments, std::set& used_slots) { - static const std::vector keys_1based = { - "wall_filament", - "sparse_infill_filament", - "solid_infill_filament" - }; - for (const char* key : keys_1based) - { - const ConfigOptionInt* option = config.option(key); - if (option != nullptr && option->value >= 1 && option->value <= num_filaments) - used_slots.insert(option->value - 1); - } - - static const std::vector keys_0based = { + // All feature filament keys use 0 = "Default" (inherit the active object/part filament), + // so only explicit selections (>= 1) mark a slot as used. + static const std::vector keys_with_default = { + "outer_wall_filament_id", + "inner_wall_filament_id", + "sparse_infill_filament_id", + "internal_solid_filament_id", + "top_surface_filament_id", + "bottom_surface_filament_id", "support_filament", "support_interface_filament", "wipe_tower_filament" }; - for (const char* key : keys_0based) + for (const char* key : keys_with_default) { const ConfigOptionInt* option = config.option(key); if (option != nullptr && option->value >= 1 && option->value <= num_filaments) @@ -288,12 +284,15 @@ static void collect_filament_slots_from_model_config( used_slots.insert(extruder_id - 1); } - // Per-object feature-specific keys (wall_filament, etc.) may be + // Per-object feature-specific keys (outer_wall_filament_id, etc.) may be // overridden independently of the object's primary extruder. static const std::vector feature_keys = { - "wall_filament", - "sparse_infill_filament", - "solid_infill_filament", + "outer_wall_filament_id", + "inner_wall_filament_id", + "sparse_infill_filament_id", + "internal_solid_filament_id", + "top_surface_filament_id", + "bottom_surface_filament_id", "support_filament", "support_interface_filament", "wipe_tower_filament" @@ -1247,54 +1246,6 @@ struct DynamicFilamentList : DynamicList } }; -struct DynamicFilamentList1Based : DynamicFilamentList -{ - void apply_on(Choice *c) override - { - if (items.empty()) - update(true); - auto cb = dynamic_cast(c->window); - auto n = cb->GetSelection(); - cb->Clear(); - for (auto i : items) { - cb->Append(i.first, *i.second); - } - if (n < cb->GetCount()) - cb->SetSelection(n); - } - wxString get_value(int index) override - { - wxString str; - str << index+1; - return str; - } - int index_of(wxString value) override - { - long n = 0; - if(!value.ToLong(&n)) - return -1; - --n; - return (n >= 0 && n <= items.size()) ? int(n) : -1; - } - void update(bool force = false) - { - items.clear(); - if (!force && m_choices.empty()) - return; - auto icons = get_extruder_color_icons(true); - auto presets = wxGetApp().preset_bundle->filament_presets; - for (int i = 0; i < presets.size(); ++i) { - wxString str; - std::string type; - wxGetApp().preset_bundle->filaments.find_preset(presets[i])->get_filament_type(type); - str << type; - items.push_back({str, icons[i]}); - } - DynamicList::update(); - } - -}; - class MixedFilamentColorMatchDialog : public DPIDialog { public: @@ -1819,7 +1770,6 @@ MixedColorMatchRecipeResult prompt_best_color_match_recipe(wxWindow* } static DynamicFilamentList dynamic_filament_list; -static DynamicFilamentList1Based dynamic_filament_list_1_based; static wxString nozzle_type_key_to_label(const std::string& key) { @@ -1839,9 +1789,12 @@ Sidebar::Sidebar(Plater *parent) { Choice::register_dynamic_list("support_filament", &dynamic_filament_list); Choice::register_dynamic_list("support_interface_filament", &dynamic_filament_list); - Choice::register_dynamic_list("wall_filament", &dynamic_filament_list_1_based); - Choice::register_dynamic_list("sparse_infill_filament", &dynamic_filament_list_1_based); - Choice::register_dynamic_list("solid_infill_filament", &dynamic_filament_list_1_based); + Choice::register_dynamic_list("outer_wall_filament_id", &dynamic_filament_list); + Choice::register_dynamic_list("inner_wall_filament_id", &dynamic_filament_list); + Choice::register_dynamic_list("sparse_infill_filament_id", &dynamic_filament_list); + Choice::register_dynamic_list("internal_solid_filament_id", &dynamic_filament_list); + Choice::register_dynamic_list("top_surface_filament_id", &dynamic_filament_list); + Choice::register_dynamic_list("bottom_surface_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); p->scrolled = new wxPanel(this); @@ -8299,7 +8252,6 @@ void Sidebar::show_SEMM_buttons(bool bshow) void Sidebar::update_dynamic_filament_list() { dynamic_filament_list.update(); - dynamic_filament_list_1_based.update(); } void Sidebar::update_nozzle_settings(bool switch_machine) @@ -9434,7 +9386,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "extruder_colour", "filament_colour", "material_colour", "printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", - "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "solid_infill_filament", "top_shell_layers", + "brim_width", "wall_loops", "outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_density", "sparse_infill_filament_id", + "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", @@ -12302,7 +12255,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // Validate passed, but also check filament temp mixing as a final // guard. check_filament_temp_mixing() reads directly from preset // configs and catches cases that Print::validate() may miss (e.g. - // wall_filament changes that haven't propagated to PrintRegions yet). + // outer_wall_filament_id changes that haven't propagated to PrintRegions yet). bool filament_ok = q->sync_filament_temp_mixing_notification(); if (filament_ok) { this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); @@ -20543,7 +20496,8 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r sidebar().on_filaments_delete(filament_id); // update global feature filament selections - static const char* keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", + static const char* keys[] = {"outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", + "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", "support_filament", "support_interface_filament"}; for (auto key : keys) if (p->config->has(key)) { @@ -20810,7 +20764,7 @@ bool Plater::check_filament_temp_mixing(int plate_index) // Collect from the Plater working config. The approach balances // sensitivity against false positives: // - Global features (wipe tower, support) always apply → always collected. - // - Feature-specific keys (wall_filament, infill) depend on the global + // - Feature-specific keys (outer_wall_filament_id, infill) depend on the global // process defaults. They are only collected when at least one object // on the plate uses the default extruder (e=0), which means those // defaults WILL affect the actual slicing output. @@ -20833,9 +20787,12 @@ bool Plater::check_filament_temp_mixing(int plate_index) if (uses_default_extruder) { static const std::vector default_keys = { - "wall_filament", - "sparse_infill_filament", - "solid_infill_filament" + "outer_wall_filament_id", + "inner_wall_filament_id", + "sparse_infill_filament_id", + "internal_solid_filament_id", + "top_surface_filament_id", + "bottom_surface_filament_id" }; for (const char* key : default_keys) { @@ -21133,8 +21090,10 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_scheduled = true; } // Orca: update when *_filament changed - else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "wall_filament" || - opt_key == "sparse_infill_filament" || opt_key == "solid_infill_filament") { + else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || + opt_key == "outer_wall_filament_id" || opt_key == "inner_wall_filament_id" || + opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" || + opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id") { update_scheduled = true; } } diff --git a/src/slic3r/GUI/PresetHints.cpp b/src/slic3r/GUI/PresetHints.cpp index 483ffdc3e94..490e44c954d 100644 --- a/src/slic3r/GUI/PresetHints.cpp +++ b/src/slic3r/GUI/PresetHints.cpp @@ -128,9 +128,12 @@ std::string PresetHints::maximum_volumetric_flow_description(const PresetBundle auto feature_extruder_active = [idx_extruder, num_extruders](int i) { return i <= 0 || i > num_extruders || idx_extruder == -1 || idx_extruder == i - 1; }; - bool perimeter_extruder_active = feature_extruder_active(print_config.opt_int("wall_filament")); - bool infill_extruder_active = feature_extruder_active(print_config.opt_int("wall_filament")); - bool solid_infill_extruder_active = feature_extruder_active(print_config.opt_int("solid_infill_filament")); + bool perimeter_extruder_active = feature_extruder_active(print_config.opt_int("outer_wall_filament_id")) + && feature_extruder_active(print_config.opt_int("inner_wall_filament_id")); + bool infill_extruder_active = feature_extruder_active(print_config.opt_int("sparse_infill_filament_id")); + bool solid_infill_extruder_active = feature_extruder_active(print_config.opt_int("internal_solid_filament_id")) + && feature_extruder_active(print_config.opt_int("top_surface_filament_id")) + && feature_extruder_active(print_config.opt_int("bottom_surface_filament_id")); bool support_material_extruder_active = feature_extruder_active(print_config.opt_int("support_filament")); bool support_material_interface_extruder_active = feature_extruder_active(print_config.opt_int("support_interface_filament")); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 58f5d8d5b1f..ae87b7bbbd6 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2257,6 +2257,8 @@ void TabPrint::build() auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height"); optgroup->append_single_option_line("layer_height","quality_settings_layer_height"); optgroup->append_single_option_line("initial_layer_print_height","quality_settings_layer_height"); + optgroup->append_single_option_line("extruder_layer_height_mode","quality_settings_layer_height"); + optgroup->append_single_option_line("extruder_layer_height_tolerance","quality_settings_layer_height"); optgroup = page->new_optgroup(L("Line width"), L"param_line_width"); optgroup->append_single_option_line("line_width","quality_settings_line_width"); @@ -2499,6 +2501,7 @@ void TabPrint::build() optgroup->append_single_option_line("raft_contact_distance", "support_settings_raft"); optgroup = page->new_optgroup(L("Support filament"), L"param_support_filament"); + optgroup->append_single_option_line("support_nozzle_diameter", "support_settings_filament"); optgroup->append_single_option_line("support_filament", "support_settings_filament#base"); optgroup->append_single_option_line("support_interface_filament", "support_settings_filament#interface"); optgroup->append_single_option_line("support_interface_not_for_body", "support_settings_filament#avoid-interface-filament-for-base"); @@ -2571,9 +2574,12 @@ void TabPrint::build() optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower"); optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features"); - optgroup->append_single_option_line("wall_filament", "multimaterial_settings_filament_for_features#walls"); - optgroup->append_single_option_line("sparse_infill_filament", "multimaterial_settings_filament_for_features#infill"); - optgroup->append_single_option_line("solid_infill_filament", "multimaterial_settings_filament_for_features#solid-infill"); + optgroup->append_single_option_line("outer_wall_filament_id", "multimaterial_settings_filament_for_features#walls"); + optgroup->append_single_option_line("inner_wall_filament_id", "multimaterial_settings_filament_for_features#walls"); + optgroup->append_single_option_line("sparse_infill_filament_id", "multimaterial_settings_filament_for_features#infill"); + optgroup->append_single_option_line("internal_solid_filament_id", "multimaterial_settings_filament_for_features#solid-infill"); + optgroup->append_single_option_line("top_surface_filament_id", "multimaterial_settings_filament_for_features#solid-infill"); + optgroup->append_single_option_line("bottom_surface_filament_id", "multimaterial_settings_filament_for_features#solid-infill"); optgroup->append_single_option_line("wipe_tower_filament", "multimaterial_settings_filament_for_features#wipe-tower"); optgroup = page->new_optgroup(L("Ooze prevention"), L"param_ooze_prevention"); @@ -2879,16 +2885,24 @@ static DynamicPrintConfig resolved_model_config_for_tab(const DynamicPrintConfig if (const auto* extruder_opt = config.option("extruder"); extruder_opt != nullptr && extruder_opt->value > 0) { const int extruder = extruder_opt->value; - if (!resolved.has("wall_filament")) - resolved.set_key_value("wall_filament", new ConfigOptionInt(extruder)); - if (!resolved.has("sparse_infill_filament")) - resolved.set_key_value("sparse_infill_filament", new ConfigOptionInt(extruder)); - if (!resolved.has("solid_infill_filament")) - resolved.set_key_value("solid_infill_filament", new ConfigOptionInt(extruder)); - } - - if (!resolved.has("solid_infill_filament") && resolved.has("sparse_infill_filament")) - resolved.set_key_value("solid_infill_filament", new ConfigOptionInt(resolved.opt_int("sparse_infill_filament"))); + for (const char* key : {"outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", + "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id"}) + if (!resolved.has(key)) + resolved.set_key_value(key, new ConfigOptionInt(extruder)); + } + + // 0 = "Default" (use the active object/part filament); only explicit selections propagate. + if (resolved.has("sparse_infill_filament_id") && !resolved.has("internal_solid_filament_id")) + if (const int sparse_infill_filament_id = resolved.opt_int("sparse_infill_filament_id"); sparse_infill_filament_id > 0) + resolved.set_key_value("internal_solid_filament_id", new ConfigOptionInt(sparse_infill_filament_id)); + + if (resolved.has("internal_solid_filament_id")) + if (const int internal_solid_filament_id = resolved.opt_int("internal_solid_filament_id"); internal_solid_filament_id > 0) { + if (!resolved.has("top_surface_filament_id")) + resolved.set_key_value("top_surface_filament_id", new ConfigOptionInt(internal_solid_filament_id)); + if (!resolved.has("bottom_surface_filament_id")) + resolved.set_key_value("bottom_surface_filament_id", new ConfigOptionInt(internal_solid_filament_id)); + } return resolved; } @@ -4851,6 +4865,7 @@ if (is_marlin_flavor) optgroup = page->new_optgroup(L("Layer height limits"), L"param_layer_height"); optgroup->append_single_option_line("min_layer_height", "", extruder_idx); optgroup->append_single_option_line("max_layer_height", "", extruder_idx); + optgroup->append_single_option_line("extruder_layer_height", "", extruder_idx); optgroup = page->new_optgroup(L("Position"), L"param_position"); optgroup->append_single_option_line("extruder_offset", "", extruder_idx); diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 5e282b08a56..f4083b14835 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(${_TEST_NAME}_tests test_gcode.cpp test_gcodewriter.cpp test_model.cpp + test_multi_nozzle_layer_height.cpp test_print.cpp test_printgcode.cpp test_printobject.cpp diff --git a/tests/fff_print/test_multi_nozzle_layer_height.cpp b/tests/fff_print/test_multi_nozzle_layer_height.cpp new file mode 100644 index 00000000000..6b711a65fc0 --- /dev/null +++ b/tests/fff_print/test_multi_nozzle_layer_height.cpp @@ -0,0 +1,684 @@ +#include + +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/Print.hpp" +#include "libslic3r/Layer.hpp" +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/ExtrusionEntityCollection.hpp" +#include "libslic3r/Flow.hpp" +#include "libslic3r/Slicing.hpp" +#include "libslic3r/GCode/ToolOrdering.hpp" +#include + +#include "test_data.hpp" + +using namespace Slic3r; +using namespace Slic3r::Test; + +// ORCA: tests for the per-extruder layer height feature ("extruder_layer_height"). + +template +static void for_each_path(const ExtrusionEntityCollection &collection, const PathFn &fn) +{ + for (const ExtrusionEntity *entity : collection.entities) { + if (auto *sub_collection = dynamic_cast(entity)) + for_each_path(*sub_collection, fn); + else if (auto *loop = dynamic_cast(entity)) { + for (const ExtrusionPath &path : loop->paths) + fn(path); + } else if (auto *multi_path = dynamic_cast(entity)) { + for (const ExtrusionPath &path : multi_path->paths) + fn(path); + } else if (auto *path = dynamic_cast(entity)) + fn(*path); + } +} + +static void collect_path_heights(const ExtrusionEntityCollection &collection, std::vector &heights) +{ + for_each_path(collection, [&heights](const ExtrusionPath &path) { heights.emplace_back(path.height); }); +} + +static std::vector region_path_heights(const LayerRegion *layerm) +{ + std::vector heights; + collect_path_heights(layerm->perimeters, heights); + collect_path_heights(layerm->fills, heights); + return heights; +} + +static void collect_path_role_widths(const ExtrusionEntityCollection &collection, std::vector> &widths) +{ + for_each_path(collection, [&widths](const ExtrusionPath &path) { widths.emplace_back(path.role(), path.width); }); +} + +// Two extruders: a 0.4 mm nozzle printing with the 0.2 mm object layer height and a 0.6 mm nozzle +// with a configurable extruder layer height (0.4 => multiplier 2). +// On this fork's classic multi-tool printers a filament index is the extruder index, so no +// filament map configuration applies. +static DynamicPrintConfig two_extruder_config(double second_extruder_layer_height) +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); + config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.4)); + config.set_key_value("enable_prime_tower", new ConfigOptionBool(false)); + config.set_key_value("enable_support", new ConfigOptionBool(false)); + + config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.6})); + config.set_key_value("extruder_layer_height", new ConfigOptionFloats({0., second_extruder_layer_height})); + config.set_key_value("min_layer_height", new ConfigOptionFloats({0.07, 0.07})); + config.set_key_value("max_layer_height", new ConfigOptionFloats({0.3, 0.45})); + config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75})); + config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"})); + config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"})); + config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"})); + config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210})); + config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190})); + config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240})); + // flush_volumes_matrix must be filament_count^2 entries. + config.set_key_value("flush_multiplier", new ConfigOptionFloat(1.)); + config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0})); + // Print::validate() reports motion-ability diagnostics by overwriting the single warning + // out-param; raise the machine limit so the default print accelerations do not clobber the + // layer height warnings under test. + config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({100000., 100000.})); + return config; +} + +// One object made of two 20x20 mm cube parts side by side; the second part prints with filament 2. +// The parts are 20 * z_scale mm tall (10 mm by default). +static void init_two_part_print(Print &print, Model &model, const DynamicPrintConfig &config, float z_scale = 0.5f) +{ + TriangleMesh fine_mesh = mesh(TestMesh::cube_20x20x20); + fine_mesh.scale(Vec3f(1.f, 1.f, z_scale)); + TriangleMesh coarse_mesh = fine_mesh; + coarse_mesh.translate(30.f, 0.f, 0.f); + + ModelObject *object = model.add_object(); + object->name = "two_part_cube"; + object->add_volume(std::move(fine_mesh)); + ModelVolume *coarse_volume = object->add_volume(std::move(coarse_mesh)); + coarse_volume->config.set("extruder", 2); + object->add_instance(); + + arrange_objects(model, InfiniteBed{}, ArrangeParams{scaled(min_object_distance(config))}); + for (ModelObject *mo : model.objects) + mo->ensure_on_bed(); + print.apply(model, config); + print.set_status_silent(); +} + +static void find_regions(const PrintObject &object, int &fine_region, int &coarse_region) +{ + fine_region = coarse_region = -1; + for (size_t i = 0; i < object.num_printing_regions(); ++ i) { + if (object.printing_region(i).config().outer_wall_filament_id.value == 2) + coarse_region = int(i); + else + fine_region = int(i); + } +} + +SCENARIO("Per-extruder layer height combines region layers", "[MultiNozzleLayerHeight]") { + GIVEN("A two-part object, the second part on a 0.6 mm nozzle with a 0.4 mm extruder layer height") { + DynamicPrintConfig config = two_extruder_config(0.4); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("the configuration passes validation and slices as expected") { + REQUIRE(print.validate().string.empty()); + print.process(); + + const PrintObject &object = *print.objects().front(); + int fine_region, coarse_region; + find_regions(object, fine_region, coarse_region); + REQUIRE(fine_region >= 0); + REQUIRE(coarse_region >= 0); + // 0.4 mm first layer + 48 layers of 0.2 mm = 10 mm object height. + REQUIRE(object.layer_count() == 49); + + // The coarse region extrudes on the first layer and then only on every 2nd layer, + // always with 0.4 mm high paths; the layers in between print nothing for it. + size_t coarse_layers = 0, coarse_bad_heights = 0, coarse_unexpected = 0, coarse_missing = 0; + // The fine region extrudes on every layer with the base layer heights. + size_t fine_bad_heights = 0, fine_missing = 0; + for (size_t idx = 0; idx < object.layer_count(); ++ idx) { + const Layer *layer = object.get_layer(int(idx)); + const std::vector coarse_heights = region_path_heights(layer->get_region(coarse_region)); + if (idx % 2 == 0) { + if (coarse_heights.empty()) + ++ coarse_missing; + else + ++ coarse_layers; + for (float height : coarse_heights) + if (std::abs(height - 0.4) > 1e-3) + ++ coarse_bad_heights; + } else if (! coarse_heights.empty()) + ++ coarse_unexpected; + + const std::vector fine_heights = region_path_heights(layer->get_region(fine_region)); + if (fine_heights.empty()) + ++ fine_missing; + const double fine_expected = idx == 0 ? 0.4 : 0.2; + for (float height : fine_heights) + if (std::abs(height - fine_expected) > 1e-3) + ++ fine_bad_heights; + } + CHECK(coarse_missing == 0); + CHECK(coarse_unexpected == 0); + CHECK(coarse_bad_heights == 0); + CHECK(coarse_layers == 25); // layer 0 + the 24 group tops + CHECK(fine_missing == 0); + CHECK(fine_bad_heights == 0); + } + } + + GIVEN("The same object with extruder_layer_height disabled") { + DynamicPrintConfig config = two_extruder_config(0.); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("both regions print on every layer with the base layer heights") { + REQUIRE(print.validate().string.empty()); + print.process(); + + const PrintObject &object = *print.objects().front(); + int fine_region, coarse_region; + find_regions(object, fine_region, coarse_region); + REQUIRE(fine_region >= 0); + REQUIRE(coarse_region >= 0); + + size_t missing = 0, bad_heights = 0; + for (size_t idx = 0; idx < object.layer_count(); ++ idx) { + const Layer *layer = object.get_layer(int(idx)); + const double expected = idx == 0 ? 0.4 : 0.2; + for (int region_id : { fine_region, coarse_region }) { + const std::vector heights = region_path_heights(layer->get_region(region_id)); + if (heights.empty()) + ++ missing; + for (float height : heights) + if (std::abs(height - expected) > 1e-3) + ++ bad_heights; + } + } + CHECK(missing == 0); + CHECK(bad_heights == 0); + } + } +} + +SCENARIO("Per-extruder layer height respects the extruder's minimum layer height", "[MultiNozzleLayerHeight]") { + GIVEN("A 0.6 mm preferred layer height with a 0.4 mm minimum, on a part height leaving a 1-layer tail") { + DynamicPrintConfig config = two_extruder_config(0.6); + config.set_key_value("max_layer_height", new ConfigOptionFloats({0.3, 0.6})); + config.set_key_value("min_layer_height", new ConfigOptionFloats({0.07, 0.4})); + Print print; + Model model; + // 10.2 mm parts: a 0.4 mm first layer + 49 layers of 0.2 mm. 49 is not divisible by the + // multiplier 3, so without the minimum the column would end in a single 0.2 mm layer. + init_two_part_print(print, model, config, 0.51f); + THEN("no layer of the coarse part above the first prints below 0.4 mm") { + REQUIRE(print.validate().string.empty()); + print.process(); + + const PrintObject &object = *print.objects().front(); + int fine_region, coarse_region; + find_regions(object, fine_region, coarse_region); + REQUIRE(coarse_region >= 0); + + size_t below_min = 0, full_runs = 0, forced_runs = 0; + for (size_t idx = 1; idx < object.layer_count(); ++ idx) { + const std::vector heights = region_path_heights(object.get_layer(int(idx))->get_region(coarse_region)); + for (float height : heights) { + if (height < 0.4 - 1e-3) + ++ below_min; + else if (std::abs(height - 0.6) < 1e-3) + ++ full_runs; + else if (std::abs(height - 0.4) < 1e-3) + ++ forced_runs; + } + } + CHECK(below_min == 0); + CHECK(full_runs > 0); + CHECK(forced_runs > 0); + } + } + GIVEN("A preferred layer height below the extruder's minimum layer height") { + DynamicPrintConfig config = two_extruder_config(0.4); + config.set_key_value("min_layer_height", new ConfigOptionFloats({0.07, 0.45})); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("validation fails") { + const StringObjectException err = print.validate(); + REQUIRE(! err.string.empty()); + REQUIRE(err.opt_key == "extruder_layer_height"); + } + } +} + +// Shared check: the fine part's walls print on every layer with the base layer heights while +// combined 0.4 mm high infill appears on some layers above the first. +static void check_plain_walls_combined_infill(Print &print) +{ + REQUIRE(print.validate().string.empty()); + print.process(); + + const PrintObject &object = *print.objects().front(); + int fine_region, coarse_region; + find_regions(object, fine_region, coarse_region); + REQUIRE(fine_region >= 0); + + size_t wall_bad_heights = 0, wall_missing = 0, combined_infill_paths = 0; + for (size_t idx = 0; idx < object.layer_count(); ++ idx) { + const LayerRegion *layerm = object.get_layer(int(idx))->get_region(fine_region); + std::vector wall_heights; + collect_path_heights(layerm->perimeters, wall_heights); + if (wall_heights.empty()) + ++ wall_missing; + const double expected = idx == 0 ? 0.4 : 0.2; + for (float height : wall_heights) + if (std::abs(height - expected) > 1e-3) + ++ wall_bad_heights; + if (idx > 0) { + std::vector fill_heights; + collect_path_heights(layerm->fills, fill_heights); + for (float height : fill_heights) + if (std::abs(height - 0.4) < 1e-3) + ++ combined_infill_paths; + } + } + CHECK(wall_missing == 0); + CHECK(wall_bad_heights == 0); + CHECK(combined_infill_paths > 0); +} + +SCENARIO("Per-extruder layer height honors feature filaments", "[MultiNozzleLayerHeight]") { + GIVEN("A part whose sparse infill uses the filament with a 0.4 mm preferred layer height") { + DynamicPrintConfig config = two_extruder_config(0.4); + // Both parts print their sparse infill with filament 2; the first part's walls stay on + // filament 1 with no preferred layer height. + config.set_key_value("sparse_infill_filament_id", new ConfigOptionInt(2)); + config.set_key_value("sparse_infill_density", new ConfigOptionPercent(15)); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("the first part prints per-layer walls with sparse infill combined to 0.4 mm") { + check_plain_walls_combined_infill(print); + } + } + + GIVEN("A part whose internal solid infill uses the 0.4 mm filament at 100% infill density") { + DynamicPrintConfig config = two_extruder_config(0.4); + // At 100% density the combined infill is internal solid infill: the preference of ITS + // filament must decide the combined height, not the sparse infill filament's. + config.set_key_value("internal_solid_filament_id", new ConfigOptionInt(2)); + config.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("the first part prints per-layer walls with solid infill combined to 0.4 mm") { + check_plain_walls_combined_infill(print); + } + } + + GIVEN("A part whose outer walls use a filament with a different preferred layer height") { + DynamicPrintConfig config = two_extruder_config(0.4); + // The first part's outer walls print with filament 2 while its inner walls stay on + // filament 1: walls print together, so the part keeps the object layer height and the + // unhonored preference is warned about. + config.set_key_value("outer_wall_filament_id", new ConfigOptionInt(2)); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("validation passes with a warning instead of an error") { + // This fork's validate() appends warnings to a single StringObjectException out-param. + StringObjectException warning; + REQUIRE(print.validate(&warning).string.empty()); + REQUIRE(! warning.string.empty()); + REQUIRE(warning.opt_key == "extruder_layer_height"); + } + } +} + +SCENARIO("Fill line width follows the filament that prints the surface", "[MultiNozzleLayerHeight]") { + GIVEN("Internal solid infill mapped to the 0.6 mm filament, bottom surfaces staying on the 0.4 mm filament") { + DynamicPrintConfig config = two_extruder_config(0.); + config.set_key_value("internal_solid_filament_id", new ConfigOptionInt(2)); + config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(125., true)); + config.set_key_value("internal_solid_infill_line_width", new ConfigOptionFloatOrPercent(105., true)); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("bottom surface widths resolve against their own filament's nozzle") { + REQUIRE(print.validate().string.empty()); + print.process(); + + const PrintObject &object = *print.objects().front(); + int fine_region, coarse_region; + find_regions(object, fine_region, coarse_region); + REQUIRE(fine_region >= 0); + + // Bottom surfaces print with filament 1, so their percent line width resolves against + // its 0.4 mm nozzle, while internal solid infill (filament 2) resolves against 0.6 mm. + // Solid fills may stretch line spacing up to 20% to fit a region evenly, so accept + // widths in [nominal, 1.2 * nominal]. + size_t bottom_paths = 0, bottom_bad_widths = 0, solid_paths = 0, solid_bad_widths = 0; + for (size_t idx = 0; idx < object.layer_count(); ++ idx) { + std::vector> widths; + collect_path_role_widths(object.get_layer(int(idx))->get_region(fine_region)->fills, widths); + for (const std::pair &role_width : widths) { + const bool bottom = role_width.first == erBottomSurface; + if (! bottom && role_width.first != erSolidInfill) + continue; + ++ (bottom ? bottom_paths : solid_paths); + const double expected = (idx == 0 ? 1.25 : 1.05) * (bottom ? 0.4 : 0.6); + if (role_width.second < expected - 1e-3 || role_width.second > expected * 1.2 + 1e-3) + ++ (bottom ? bottom_bad_widths : solid_bad_widths); + } + } + CHECK(bottom_paths > 0); + CHECK(bottom_bad_widths == 0); + CHECK(solid_paths > 0); + CHECK(solid_bad_widths == 0); + } + } +} + +SCENARIO("Combined infill respects the printing extruder's layer height limits", "[MultiNozzleLayerHeight]") { + GIVEN("Infill combining to a preferred height above the filament's maximum layer height") { + // Infill on filament 2: preferred layer height 0.6 exceeds its max_layer_height 0.45, + // so combining must stop at 0.4 mm groups instead of building 0.6 mm ones (3 x 0.2). + DynamicPrintConfig config = two_extruder_config(0.6); + config.set_key_value("max_layer_height", new ConfigOptionFloats({0.3, 0.45})); + config.set_key_value("sparse_infill_filament_id", new ConfigOptionInt(2)); + config.set_key_value("internal_solid_filament_id", new ConfigOptionInt(2)); + config.set_key_value("sparse_infill_density", new ConfigOptionPercent(15)); + Print print; + Model model; + // A single part: filament 2 prints only infill, so its preference skips the strict wall checks. + TriangleMesh cube = mesh(TestMesh::cube_20x20x20); + cube.scale(Vec3f(1.f, 1.f, 0.5f)); + ModelObject *object_model = model.add_object(); + object_model->name = "single_cube"; + object_model->add_volume(std::move(cube)); + object_model->add_instance(); + arrange_objects(model, InfiniteBed{}, ArrangeParams{scaled(min_object_distance(config))}); + for (ModelObject *mo : model.objects) + mo->ensure_on_bed(); + print.apply(model, config); + print.set_status_silent(); + THEN("no combined infill group exceeds the maximum layer height") { + REQUIRE(print.validate().string.empty()); + print.process(); + + const PrintObject &object = *print.objects().front(); + size_t over_max = 0, combined = 0; + for (size_t idx = 1; idx < object.layer_count(); ++ idx) + for (const LayerRegion *layerm : object.get_layer(int(idx))->regions()) { + std::vector heights; + collect_path_heights(layerm->fills, heights); + for (float height : heights) { + if (height > 0.45 + 1e-3) + ++ over_max; + else if (height > 0.2 + 1e-3) + ++ combined; + } + } + CHECK(over_max == 0); + CHECK(combined > 0); + } + } + + GIVEN("Walls combining to a pitch a feature filament of the part cannot print") { + // Both wall filaments map to filament 2 at a 0.4 mm pitch, but top/bottom/solid features + // stay on filament 1 whose max_layer_height (0.3) cannot print that pitch: the part + // falls back to the object layer height with a warning, like disagreeing wall filaments. + DynamicPrintConfig config = two_extruder_config(0.4); + config.set_key_value("outer_wall_filament_id", new ConfigOptionInt(2)); + config.set_key_value("inner_wall_filament_id", new ConfigOptionInt(2)); + config.set_key_value("max_layer_height", new ConfigOptionFloats({0.3, 0.45})); + // Keep the line width checks out of the way, this test targets the height fallback. + config.set_key_value("line_width", new ConfigOptionFloatOrPercent(0.5, false)); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("validation warns and the part prints with the object layer height") { + StringObjectException warning; + REQUIRE(print.validate(&warning).string.empty()); + REQUIRE(warning.string.find("cannot print that height") != std::string::npos); + + print.process(); + // Only the first part falls back; the second is all filament 2 and keeps the pitch. + // Both parts' walls print with filament 2, so the parts are told apart by their + // top surface filament. + const PrintObject &object = *print.objects().front(); + int fine_region = -1; + for (size_t i = 0; i < object.num_printing_regions(); ++ i) + if (object.printing_region(i).config().top_surface_filament_id.value == 1) + fine_region = int(i); + REQUIRE(fine_region >= 0); + size_t wall_bad_heights = 0; + for (size_t idx = 0; idx < object.layer_count(); ++ idx) { + const double expected = idx == 0 ? 0.4 : 0.2; + std::vector heights; + collect_path_heights(object.get_layer(int(idx))->get_region(fine_region)->perimeters, heights); + for (float height : heights) + if (std::abs(height - expected) > 1e-3) + ++ wall_bad_heights; + } + CHECK(wall_bad_heights == 0); + } + } + + GIVEN("A feature filament whose minimum layer height is above the pitch it prints at") { + // Internal solid infill on filament 2 (minimum layer height 0.3) prints with the 0.2 mm + // object layer height while filament 2's preference drives infill combining elsewhere. + DynamicPrintConfig config = two_extruder_config(0.4); + config.set_key_value("internal_solid_filament_id", new ConfigOptionInt(2)); + config.set_key_value("min_layer_height", new ConfigOptionFloats({0.07, 0.3})); + // Keep the line width checks out of the way, this test targets the height warning. + config.set_key_value("line_width", new ConfigOptionFloatOrPercent(0.5, false)); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("validation warns about printing below the minimum layer height") { + StringObjectException warning; + REQUIRE(print.validate(&warning).string.empty()); + REQUIRE(warning.string.find("minimum layer") != std::string::npos); + } + } +} + +SCENARIO("Fill collections dispatch to the filament their flow was computed for", "[MultiNozzleLayerHeight]") { + GIVEN("A region with distinct per-feature filaments") { + PrintRegionConfig region_config; + region_config.outer_wall_filament_id.value = 1; + region_config.inner_wall_filament_id.value = 1; + region_config.sparse_infill_filament_id.value = 4; + region_config.internal_solid_filament_id.value = 3; + region_config.top_surface_filament_id.value = 1; + region_config.bottom_surface_filament_id.value = 1; + const PrintRegion region(region_config, region_config.hash(), 0); + LayerTools layer_tools(0.); + + auto collection_extruder = [&](std::initializer_list roles) { + ExtrusionEntityCollection eec; + for (ExtrusionRole role : roles) + eec.entities.push_back(new ExtrusionPath(role)); + return layer_tools.extruder(eec, region); + }; + + THEN("top and bottom surfaces keep their filament when gap fill is mixed in") { + CHECK(collection_extruder({erTopSolidInfill}) == 0); // 0 based filament 1 + CHECK(collection_extruder({erTopSolidInfill, erGapFill}) == 0); + CHECK(collection_extruder({erBottomSurface, erGapFill}) == 0); + CHECK(collection_extruder({erSolidInfill, erGapFill}) == 2); // filament 3 + } + THEN("external bridges print with the bottom surface filament, internal ones stay solid") { + CHECK(collection_extruder({erBridgeInfill}) == 0); // bottom filament 1 + CHECK(collection_extruder({erInternalBridgeInfill}) == 2); // internal solid filament 3 + } + THEN("gap fill with no sibling surface prints with the outer wall filament") { + PrintRegionConfig gap_region_config = region_config; + gap_region_config.outer_wall_filament_id.value = 2; + const PrintRegion gap_region(gap_region_config, gap_region_config.hash(), 0); + ExtrusionEntityCollection eec; + eec.entities.push_back(new ExtrusionPath(erGapFill)); + CHECK(layer_tools.extruder(eec, gap_region) == 1); // 0 based outer wall filament 2 + } + } +} + +SCENARIO("Support nozzle diameter restricts support printing", "[MultiNozzleLayerHeight]") { + // A raft makes the object require support handling without any overhang geometry: the raft + // layers below the object print as support-only layers. + auto raft_config = [](double support_nozzle_diameter) { + DynamicPrintConfig config = two_extruder_config(0.); + config.set_key_value("raft_layers", new ConfigOptionInt(2)); + config.option>("support_type", true)->value = stNormalAuto; + config.set_key_value("support_nozzle_diameter", new ConfigOptionFloat(support_nozzle_diameter)); + config.set_key_value("support_line_width", new ConfigOptionFloatOrPercent(105., true)); + return config; + }; + + GIVEN("A raft restricted to the 0.6 mm nozzle while the default filament prints with 0.4 mm") { + DynamicPrintConfig config = raft_config(0.6); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("support flows, layer height limits and tool ordering follow the 0.6 mm filament") { + REQUIRE(print.validate().string.empty()); + + const PrintObject &object = *print.objects().front(); + // Support / raft flows must resolve width against the restricted nozzle, not against + // extruder 1 that the "default" support filament falls back to. + REQUIRE(double(support_material_flow(&object).width()) == Approx(1.05 * 0.6).margin(1e-4)); + REQUIRE(double(support_material_interface_flow(&object).width()) == Approx(1.05 * 0.6).margin(1e-4)); + + // Support layer height limits follow the restricted nozzle (filament 2: max 0.45). + PrintConfig print_config; + print_config.apply(config, true); + PrintObjectConfig object_config; + object_config.apply(config, true); + const SlicingParameters params = SlicingParameters::create_from_config( + print_config, object_config, 10., std::vector{0, 1}, Vec3d(1., 1., 1.)); + REQUIRE(params.max_suport_layer_height == Approx(0.45).margin(1e-6)); + + // The raft layers below the object print with filament 2 only. + print.process(); + ToolOrdering tool_ordering(print, (unsigned int)-1, false); + size_t support_only_layers = 0, wrong_extruders = 0; + for (const LayerTools &layer_tools : tool_ordering.layer_tools()) + if (layer_tools.has_support && ! layer_tools.has_object) { + ++ support_only_layers; + for (unsigned int extruder_id : layer_tools.extruders) + if (extruder_id != 1) // 0 based: filament 2 prints with the 0.6 mm nozzle + ++ wrong_extruders; + } + CHECK(support_only_layers > 0); + CHECK(wrong_extruders == 0); + } + } + + GIVEN("A support nozzle diameter no extruder has") { + DynamicPrintConfig config = raft_config(0.5); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("validation fails") { + const StringObjectException err = print.validate(); + REQUIRE(! err.string.empty()); + REQUIRE(err.opt_key == "support_nozzle_diameter"); + } + } + + GIVEN("A support filament printing with a different nozzle than the support nozzle diameter") { + DynamicPrintConfig config = raft_config(0.6); + config.set_key_value("support_filament", new ConfigOptionInt(1)); + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("validation fails") { + const StringObjectException err = print.validate(); + REQUIRE(! err.string.empty()); + REQUIRE(err.opt_key == "support_filament"); + } + } +} + +SCENARIO("A raft keeps the bottom surfaces of combined regions", "[MultiNozzleLayerHeight]") { + GIVEN("A combined region printing on a raft") { + DynamicPrintConfig config = two_extruder_config(0.4); + config.set_key_value("raft_layers", new ConfigOptionInt(2)); + config.option>("support_type", true)->value = stNormalAuto; + Print print; + Model model; + init_two_part_print(print, model, config); + THEN("the first layer above the raft prints uncombined and carries the bottom surfaces") { + REQUIRE(print.validate().string.empty()); + print.process(); + + const PrintObject &object = *print.objects().front(); + int fine_region, coarse_region; + find_regions(object, fine_region, coarse_region); + REQUIRE(fine_region >= 0); + REQUIRE(coarse_region >= 0); + + // The first layer above the raft must not be swallowed by a combined group: surface + // detection can only seed the object's bottom shells there. + const Layer *first_layer = object.get_layer(0); + size_t bottom_regions = 0; + for (int region_id : { fine_region, coarse_region }) { + const LayerRegion *layerm = first_layer->get_region(region_id); + const std::vector heights = region_path_heights(layerm); + CHECK(! heights.empty()); + for (float height : heights) + CHECK(double(height) == Approx(0.2).margin(1e-3)); + for (const Surface &surface : layerm->fill_surfaces.surfaces) + if (surface.is_bottom()) { + ++ bottom_regions; + break; + } + } + CHECK(bottom_regions == 2); + + // Combining still happens above the first object layer. + size_t combined_layers = 0; + for (size_t idx = 1; idx < object.layer_count(); ++ idx) + for (float height : region_path_heights(object.get_layer(int(idx))->get_region(coarse_region))) + if (std::abs(height - 0.4) < 1e-3) { + ++ combined_layers; + break; + } + CHECK(combined_layers > 0); + } + } +} + +SCENARIO("Per-extruder layer height validation rejects invalid configurations", "[MultiNozzleLayerHeight]") { + auto expect_error = [](double second_extruder_layer_height) { + DynamicPrintConfig config = two_extruder_config(second_extruder_layer_height); + Print print; + Model model; + init_two_part_print(print, model, config); + const StringObjectException err = print.validate(); + REQUIRE(! err.string.empty()); + REQUIRE(err.opt_key == "extruder_layer_height"); + }; + GIVEN("An extruder layer height that is no integer multiple of the object layer height") { + THEN("validation fails") { expect_error(0.5); } + } + GIVEN("An extruder layer height smaller than the object layer height") { + THEN("validation fails") { expect_error(0.1); } + } + GIVEN("An extruder layer height exceeding the nozzle diameter") { + THEN("validation fails") { expect_error(0.8); } + } + GIVEN("An extruder layer height exceeding the extruder's maximum layer height") { + // 0.6 is a multiple of 0.2 and fits the 0.6 mm nozzle, but exceeds max_layer_height 0.45. + THEN("validation fails") { expect_error(0.6); } + } +} diff --git a/tests/libslic3r/test_mixed_filament.cpp b/tests/libslic3r/test_mixed_filament.cpp index b15592e3766..a4e7b0648f3 100644 --- a/tests/libslic3r/test_mixed_filament.cpp +++ b/tests/libslic3r/test_mixed_filament.cpp @@ -476,11 +476,12 @@ TEST_CASE("Grouped manual wall patterns make infill follow the innermost perimet REQUIRE(row.manual_pattern == "12,1"); PrintRegionConfig region_config = static_cast(FullPrintConfig::defaults()); - region_config.wall_filament.value = 3; + region_config.outer_wall_filament_id.value = 3; + region_config.inner_wall_filament_id.value = 3; region_config.wall_loops.value = 2; region_config.sparse_infill_density.value = 15.; - region_config.sparse_infill_filament.value = 2; - region_config.solid_infill_filament.value = 3; + region_config.sparse_infill_filament_id.value = 2; + region_config.internal_solid_filament_id.value = 3; PrintRegion region(region_config); @@ -498,21 +499,21 @@ TEST_CASE("Grouped manual wall patterns make infill follow the innermost perimet 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) == 1); - CHECK(layer1.sparse_infill_filament(region) == 1); - CHECK(layer0.solid_infill_filament(region) == 0); - CHECK(layer1.solid_infill_filament(region) == 0); + CHECK(layer0.wall_extruder_id(region) == 0); + CHECK(layer1.wall_extruder_id(region) == 1); + CHECK(layer0.sparse_infill_filament_id(region) == 1); + CHECK(layer1.sparse_infill_filament_id(region) == 1); + CHECK(layer0.internal_solid_filament_id(region) == 0); + CHECK(layer1.internal_solid_filament_id(region) == 0); - region_config.sparse_infill_filament.value = 2; - region_config.solid_infill_filament.value = 2; + region_config.sparse_infill_filament_id.value = 2; + region_config.internal_solid_filament_id.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); + CHECK(layer0.sparse_infill_filament_id(overridden_region) == 1); + CHECK(layer1.sparse_infill_filament_id(overridden_region) == 1); + CHECK(layer0.internal_solid_filament_id(overridden_region) == 1); + CHECK(layer1.internal_solid_filament_id(overridden_region) == 1); } TEST_CASE("Mixed filament painted-region resolver collapses ordinary mixed rows to the active physical extruder", "[MixedFilament]") From 0de9b2598b1c33d85670b27a61e113daba160855 Mon Sep 17 00:00:00 2001 From: LixNix <105106115+LixNix@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:01:39 +0200 Subject: [PATCH 2/3] fix: allow per-nozzle diameters in the Prepare sidebar (Snapmaker U1) --- src/slic3r/GUI/Plater.cpp | 112 +++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 90e80661cac..1170745f642 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -8311,61 +8312,72 @@ void Sidebar::update_nozzle_settings(bool switch_machine) } diameter_combo->Bind(wxEVT_COMBOBOX, [this, diameter_combo, i](wxCommandEvent& event) { + // ORCA multi-nozzle-size: set ONLY this nozzle's diameter, mirroring the + // Printer Settings -> Extruder tab. Previously this switched the whole printer preset + // to a single-diameter variant (forcing all nozzles to the same size); that defeats + // per-nozzle sizes, which the slicer now supports. + + // Parse the selected diameter from the "0.4mm" combo item. + wxString sel_num = diameter_combo->GetValue(); + if (sel_num.EndsWith("mm")) + sel_num.RemoveLast(2); + double new_nd = 0.; + if (!sel_num.ToCDouble(&new_nd) || new_nd <= 0.) + return; - //auto* pNotice = p->plater->get_notification_manager(); - //if (pNotice) - //{ - // pNotice->close_notification_of_type(NotificationType::CustomNotification); - // pNotice->push_notification(_u8L("Note: Printing PLA Silk on the hot end of 0.6mm hardened steel is not recommended. 0.4mm or smaller specifications are suggested."), 0); - // pNotice->set_slicing_progress_hidden(); - //} - - auto printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto printer_model_opt = printer_config.option("printer_model"); - if (printer_model_opt) { - std::string printer_model = printer_model_opt->value; - bool is_snapmaker_u1 = boost::icontains(printer_model, "Snapmaker") && boost::icontains(printer_model, "U1"); - - if (is_snapmaker_u1) - { - //check the config has flags to tips switch nozzle and all nozzle will be changed to the same type - auto notShow = wxGetApp().app_config->get("app", "sync_diameter_flags"); - if (notShow != "true") - { - RichMessageDialog dlg(static_cast(wxGetApp().mainframe), - _L("Note: Changing this will sync all other nozzles to the same diameter."), - _L("Set Nozzle Diameter"), - wxOK); - dlg.ShowCheckBox(_L("Don't show this again"), false); - auto res = dlg.ShowModal(); - bool isCheckBox = dlg.IsCheckBoxChecked(); - - if (wxID_OK == res) - wxGetApp().app_config->set("app", "sync_diameter_flags", isCheckBox); - } - } - } - - auto diameter = diameter_combo->GetValue().substr(0, 3); - auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter.ToStdString()); - if (preset == nullptr) { - BOOST_LOG_TRIVIAL(error) << "get the similar printer preset fail"; + Tab* printer_tab = wxGetApp().get_tab(Preset::TYPE_PRINTER); + if (printer_tab == nullptr) return; - } - preset->is_visible = true; // force visible - - for (size_t i = 0; i < p->m_nozzle_diameter_lists.size(); ++i) { - //set all nozzle use the diameter - p->m_nozzle_diameter_lists[i]->SetValue(diameter + "mm"); - } - wxGetApp().get_tab(Preset::TYPE_PRINTER)->select_preset(preset->name); - // Do not event.Skip(): select_preset rebuilds nozzle UI and can destroy this combo; skipping would let sidebar treat this as bed-type combo and use-after-free. + // Write nozzle_diameter[i] into the edited printer config, like Tab.cpp's extruder page. + DynamicPrintConfig new_conf = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto* nozzle_diam_opt = static_cast(new_conf.option("nozzle_diameter")); + if (nozzle_diam_opt == nullptr || i >= nozzle_diam_opt->values.size()) + return; + std::vector nozzle_diameters = nozzle_diam_opt->values; + if (std::abs(nozzle_diameters[i] - new_nd) < EPSILON) + return; // unchanged + nozzle_diameters[i] = new_nd; + new_conf.set_key_value("nozzle_diameter", new ConfigOptionFloats(nozzle_diameters)); + + // load_config marks the printer preset modified and propagates the change without + // rebuilding these combos or switching presets, so the other nozzles keep their sizes. + printer_tab->load_config(new_conf); + // Do not event.Skip(): this is a plain ComboBox; skipping would let the sidebar treat + // it as the bed-type combo (Plater::priv::on_combobox_select) and mishandle it. }); - auto diam_str = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("printer_variant")->value; - - diameter_combo->SetValue(diam_str + "mm"); + // Show THIS nozzle's own diameter (per-nozzle sizes are supported), selecting the matching + // "x.xmm" item so the read-only combo accepts it. Falls back to the uniform printer_variant + // label only when the per-nozzle value is unavailable. + const double this_nd = (nozzle_diameter && i < nozzle_diameter->values.size()) ? nozzle_diameter->values[i] : 0.; + wxString this_label; + for (unsigned int n = 0; n < diameter_combo->GetCount(); ++n) { + wxString item = diameter_combo->GetString(n); + wxString num = item; + if (num.EndsWith("mm")) + num.RemoveLast(2); + double item_nd = 0.; + if (num.ToCDouble(&item_nd) && std::abs(item_nd - this_nd) < EPSILON) { + this_label = item; + break; + } + } + if (this_label.empty() && this_nd > 0.) { + // A per-nozzle diameter not among the printer's variant list (e.g. an imported config): + // add it so the combo can display the true value. Format with the C locale (period + // decimal) to match the other "x.xmm" items and the ToCDouble parsing above. + std::ostringstream oss; + oss.imbue(std::locale::classic()); + oss << this_nd; + this_label = wxString(oss.str()) + "mm"; + diameter_combo->AppendString(this_label); + } + if (this_label.empty()) { + const auto *pv = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("printer_variant"); + this_label = (pv ? wxString(pv->value) : wxString()) + "mm"; + } + diameter_combo->SetValue(this_label); p->m_nozzle_diameter_lists.push_back(diameter_combo); From 9a90ccb55fc00b8ab0b0bf8ea8fa5b778a409ae4 Mon Sep 17 00:00:00 2001 From: LixNix <105106115+LixNix@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:37:46 +0200 Subject: [PATCH 3/3] profiles: express Snapmaker U1 line widths as % of nozzle diameter --- resources/profiles/Snapmaker.json | 2 +- .../Snapmaker/process/fdm_process_U1.json | 16 ++++++++-------- .../process/fdm_process_U1_0.06_nozzle_0.2.json | 16 ++++++++-------- .../process/fdm_process_U1_0.08_nozzle_0.2.json | 16 ++++++++-------- .../process/fdm_process_U1_0.10_nozzle_0.2.json | 16 ++++++++-------- .../process/fdm_process_U1_0.12_nozzle_0.2.json | 16 ++++++++-------- .../process/fdm_process_U1_0.14_nozzle_0.2.json | 16 ++++++++-------- .../process/fdm_process_U1_0.18_nozzle_0.6.json | 16 ++++++++-------- .../Snapmaker/process/fdm_process_U1_0.24.json | 2 +- .../process/fdm_process_U1_0.24_nozzle_0.6.json | 16 ++++++++-------- .../process/fdm_process_U1_0.24_nozzle_0.8.json | 16 ++++++++-------- .../Snapmaker/process/fdm_process_U1_0.28.json | 2 +- .../process/fdm_process_U1_0.30_nozzle_0.6.json | 16 ++++++++-------- .../process/fdm_process_U1_0.32_nozzle_0.8.json | 16 ++++++++-------- .../process/fdm_process_U1_0.36_nozzle_0.6.json | 16 ++++++++-------- .../process/fdm_process_U1_0.40_nozzle_0.8.json | 16 ++++++++-------- .../process/fdm_process_U1_0.42_nozzle_0.6.json | 16 ++++++++-------- .../process/fdm_process_U1_0.48_nozzle_0.8.json | 16 ++++++++-------- .../process/fdm_process_U1_0.56_nozzle_0.8.json | 16 ++++++++-------- .../Snapmaker/process/fdm_process_U1_common.json | 6 +++--- 20 files changed, 134 insertions(+), 134 deletions(-) diff --git a/resources/profiles/Snapmaker.json b/resources/profiles/Snapmaker.json index 268c453c60b..6de5b155eb8 100644 --- a/resources/profiles/Snapmaker.json +++ b/resources/profiles/Snapmaker.json @@ -1,6 +1,6 @@ { "name": "Snapmaker", - "version": "02.02.50.02", + "version": "02.02.50.03", "force_update": "0", "description": "Snapmaker configurations", "machine_model_list": [ diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1.json b/resources/profiles/Snapmaker/process/fdm_process_U1.json index 6c9935cce7b..9c75c70c9c5 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1.json @@ -12,18 +12,18 @@ "default_acceleration": "10000", "bridge_no_support": "0", "elefant_foot_compensation": "0.1", - "outer_wall_line_width": "0.42", + "outer_wall_line_width": "105%", "outer_wall_speed": "120", - "line_width": "0.45", + "line_width": "112.5%", "infill_direction": "45", "sparse_infill_density": "15%", "sparse_infill_pattern": "grid", - "initial_layer_line_width": "0.42", + "initial_layer_line_width": "105%", "initial_layer_print_height": "0.2", "initial_layer_speed": "20", "gap_infill_speed": "30", "infill_combination": "0", - "sparse_infill_line_width": "0.45", + "sparse_infill_line_width": "112.5%", "infill_wall_overlap": "15%", "sparse_infill_speed": "50", "interface_shells": "0", @@ -31,7 +31,7 @@ "reduce_infill_retraction": "0", "filename_format": "{input_filename_base}.gcode", "wall_loops": "2", - "inner_wall_line_width": "0.45", + "inner_wall_line_width": "112.5%", "inner_wall_speed": "40", "print_settings_id": "", "raft_layers": "0", @@ -39,13 +39,13 @@ "skirt_distance": "2", "skirt_height": "2", "minimum_sparse_infill_area": "0", - "internal_solid_infill_line_width": "0.45", + "internal_solid_infill_line_width": "112.5%", "internal_solid_infill_speed": "40", "spiral_mode": "0", "standby_temperature_delta": "-5", "enable_support": "0", "support_filament": "0", - "support_line_width": "0.42", + "support_line_width": "105%", "support_interface_filament": "0", "support_on_build_plate_only": "0", "support_top_z_distance": "0.15", @@ -60,7 +60,7 @@ "support_threshold_angle": "40", "support_object_xy_distance": "0.5", "detect_thin_wall": "0", - "top_surface_line_width": "0.42", + "top_surface_line_width": "105%", "top_surface_speed": "30", "travel_speed": "400", "enable_prime_tower": "0", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.06_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.06_nozzle_0.2.json index b028d9087a0..36e972e066b 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.06_nozzle_0.2.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.06_nozzle_0.2.json @@ -10,14 +10,14 @@ "bottom_shell_layers": "5", "top_shell_layers": "7", "bridge_flow": "1", - "line_width": "0.22", - "outer_wall_line_width": "0.22", - "initial_layer_line_width": "0.25", - "sparse_infill_line_width": "0.22", - "inner_wall_line_width": "0.22", - "internal_solid_infill_line_width": "0.22", - "support_line_width": "0.22", - "top_surface_line_width": "0.22", + "line_width": "110%", + "outer_wall_line_width": "110%", + "initial_layer_line_width": "125%", + "sparse_infill_line_width": "110%", + "inner_wall_line_width": "110%", + "internal_solid_infill_line_width": "110%", + "support_line_width": "110%", + "top_surface_line_width": "110%", "initial_layer_speed": "40", "initial_layer_infill_speed": "70", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.08_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.08_nozzle_0.2.json index 79430d26d2d..255713a98d3 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.08_nozzle_0.2.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.08_nozzle_0.2.json @@ -10,14 +10,14 @@ "bottom_shell_layers": "5", "top_shell_layers": "7", "bridge_flow": "1", - "line_width": "0.22", - "outer_wall_line_width": "0.22", - "initial_layer_line_width": "0.25", - "sparse_infill_line_width": "0.22", - "inner_wall_line_width": "0.22", - "internal_solid_infill_line_width": "0.22", - "support_line_width": "0.22", - "top_surface_line_width": "0.22", + "line_width": "110%", + "outer_wall_line_width": "110%", + "initial_layer_line_width": "125%", + "sparse_infill_line_width": "110%", + "inner_wall_line_width": "110%", + "internal_solid_infill_line_width": "110%", + "support_line_width": "110%", + "top_surface_line_width": "110%", "initial_layer_speed": "40", "initial_layer_infill_speed": "70", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.10_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.10_nozzle_0.2.json index 3e47d1bf5ae..d86cafb29fa 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.10_nozzle_0.2.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.10_nozzle_0.2.json @@ -10,14 +10,14 @@ "bottom_shell_layers": "5", "top_shell_layers": "7", "bridge_flow": "1", - "line_width": "0.22", - "outer_wall_line_width": "0.22", - "initial_layer_line_width": "0.25", - "sparse_infill_line_width": "0.22", - "inner_wall_line_width": "0.22", - "internal_solid_infill_line_width": "0.22", - "support_line_width": "0.22", - "top_surface_line_width": "0.22", + "line_width": "110%", + "outer_wall_line_width": "110%", + "initial_layer_line_width": "125%", + "sparse_infill_line_width": "110%", + "inner_wall_line_width": "110%", + "internal_solid_infill_line_width": "110%", + "support_line_width": "110%", + "top_surface_line_width": "110%", "initial_layer_speed": "40", "initial_layer_infill_speed": "70", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.12_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.12_nozzle_0.2.json index c52683cb8f4..0a14422db8f 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.12_nozzle_0.2.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.12_nozzle_0.2.json @@ -10,14 +10,14 @@ "bottom_shell_layers": "5", "top_shell_layers": "7", "bridge_flow": "1", - "line_width": "0.22", - "outer_wall_line_width": "0.22", - "initial_layer_line_width": "0.25", - "sparse_infill_line_width": "0.22", - "inner_wall_line_width": "0.22", - "internal_solid_infill_line_width": "0.22", - "support_line_width": "0.22", - "top_surface_line_width": "0.22", + "line_width": "110%", + "outer_wall_line_width": "110%", + "initial_layer_line_width": "125%", + "sparse_infill_line_width": "110%", + "inner_wall_line_width": "110%", + "internal_solid_infill_line_width": "110%", + "support_line_width": "110%", + "top_surface_line_width": "110%", "initial_layer_speed": "40", "initial_layer_infill_speed": "70", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.14_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.14_nozzle_0.2.json index 57119f0f022..58a403c7361 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.14_nozzle_0.2.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.14_nozzle_0.2.json @@ -10,14 +10,14 @@ "bottom_shell_layers": "5", "top_shell_layers": "7", "bridge_flow": "1", - "line_width": "0.22", - "outer_wall_line_width": "0.22", - "initial_layer_line_width": "0.25", - "sparse_infill_line_width": "0.22", - "inner_wall_line_width": "0.22", - "internal_solid_infill_line_width": "0.22", - "support_line_width": "0.22", - "top_surface_line_width": "0.22", + "line_width": "110%", + "outer_wall_line_width": "110%", + "initial_layer_line_width": "125%", + "sparse_infill_line_width": "110%", + "inner_wall_line_width": "110%", + "internal_solid_infill_line_width": "110%", + "support_line_width": "110%", + "top_surface_line_width": "110%", "initial_layer_speed": "40", "initial_layer_infill_speed": "70", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.18_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.18_nozzle_0.6.json index 7d10b214291..156b714e941 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.18_nozzle_0.6.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.18_nozzle_0.6.json @@ -7,14 +7,14 @@ "layer_height": "0.18", "initial_layer_print_height": "0.3", "bridge_flow": "1", - "line_width": "0.62", - "outer_wall_line_width": "0.62", - "initial_layer_line_width": "0.62", - "sparse_infill_line_width": "0.62", - "inner_wall_line_width": "0.62", - "internal_solid_infill_line_width": "0.62", - "support_line_width": "0.62", - "top_surface_line_width": "0.62", + "line_width": "103.33%", + "outer_wall_line_width": "103.33%", + "initial_layer_line_width": "103.33%", + "sparse_infill_line_width": "103.33%", + "inner_wall_line_width": "103.33%", + "internal_solid_infill_line_width": "103.33%", + "support_line_width": "103.33%", + "top_surface_line_width": "103.33%", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.24.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24.json index 89224e3a29a..66f83313ca1 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.24.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24.json @@ -6,7 +6,7 @@ "instantiation": "false", "layer_height": "0.24", "elefant_foot_compensation": "0.15", - "top_surface_line_width": "0.45", + "top_surface_line_width": "112.5%", "top_shell_thickness": "1.0", "bridge_flow": "1", "initial_layer_speed": "50", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.6.json index 12104aa0cdf..cd408b77e55 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.6.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.6.json @@ -7,14 +7,14 @@ "layer_height": "0.24", "initial_layer_print_height": "0.3", "bridge_flow": "1", - "line_width": "0.62", - "outer_wall_line_width": "0.62", - "initial_layer_line_width": "0.62", - "sparse_infill_line_width": "0.62", - "inner_wall_line_width": "0.62", - "internal_solid_infill_line_width": "0.62", - "support_line_width": "0.62", - "top_surface_line_width": "0.62", + "line_width": "103.33%", + "outer_wall_line_width": "103.33%", + "initial_layer_line_width": "103.33%", + "sparse_infill_line_width": "103.33%", + "inner_wall_line_width": "103.33%", + "internal_solid_infill_line_width": "103.33%", + "support_line_width": "103.33%", + "top_surface_line_width": "103.33%", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.8.json index 5f0b60b231a..49508e88835 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.8.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.8.json @@ -7,14 +7,14 @@ "layer_height": "0.24", "initial_layer_print_height": "0.4", "bridge_flow": "1", - "line_width": "0.82", - "outer_wall_line_width": "0.82", - "initial_layer_line_width": "0.82", - "sparse_infill_line_width": "0.82", - "inner_wall_line_width": "0.82", - "internal_solid_infill_line_width": "0.82", - "support_line_width": "0.82", - "top_surface_line_width": "0.82", + "line_width": "102.5%", + "outer_wall_line_width": "102.5%", + "initial_layer_line_width": "102.5%", + "sparse_infill_line_width": "102.5%", + "inner_wall_line_width": "102.5%", + "internal_solid_infill_line_width": "102.5%", + "support_line_width": "102.5%", + "top_surface_line_width": "102.5%", "top_surface_pattern": "monotonic", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.28.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.28.json index 189d8d337ce..37fd4f68e0e 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.28.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.28.json @@ -6,7 +6,7 @@ "instantiation": "false", "layer_height": "0.28", "elefant_foot_compensation": "0.15", - "top_surface_line_width": "0.45", + "top_surface_line_width": "112.5%", "top_shell_thickness": "1.0", "bridge_flow": "1", "initial_layer_speed": "50", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.30_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.30_nozzle_0.6.json index fcf87f69513..f753c702d9b 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.30_nozzle_0.6.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.30_nozzle_0.6.json @@ -7,14 +7,14 @@ "layer_height": "0.3", "initial_layer_print_height": "0.3", "bridge_flow": "1", - "line_width": "0.62", - "outer_wall_line_width": "0.62", - "initial_layer_line_width": "0.62", - "sparse_infill_line_width": "0.62", - "inner_wall_line_width": "0.62", - "internal_solid_infill_line_width": "0.62", - "support_line_width": "0.62", - "top_surface_line_width": "0.62", + "line_width": "103.33%", + "outer_wall_line_width": "103.33%", + "initial_layer_line_width": "103.33%", + "sparse_infill_line_width": "103.33%", + "inner_wall_line_width": "103.33%", + "internal_solid_infill_line_width": "103.33%", + "support_line_width": "103.33%", + "top_surface_line_width": "103.33%", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.32_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.32_nozzle_0.8.json index 43b3dc8f9b9..37441608522 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.32_nozzle_0.8.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.32_nozzle_0.8.json @@ -7,14 +7,14 @@ "layer_height": "0.32", "initial_layer_print_height": "0.4", "bridge_flow": "1", - "line_width": "0.82", - "outer_wall_line_width": "0.82", - "initial_layer_line_width": "0.82", - "sparse_infill_line_width": "0.82", - "inner_wall_line_width": "0.82", - "internal_solid_infill_line_width": "0.82", - "support_line_width": "0.82", - "top_surface_line_width": "0.82", + "line_width": "102.5%", + "outer_wall_line_width": "102.5%", + "initial_layer_line_width": "102.5%", + "sparse_infill_line_width": "102.5%", + "inner_wall_line_width": "102.5%", + "internal_solid_infill_line_width": "102.5%", + "support_line_width": "102.5%", + "top_surface_line_width": "102.5%", "top_surface_pattern": "monotonic", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.36_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.36_nozzle_0.6.json index 01c90927b29..61701643e2d 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.36_nozzle_0.6.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.36_nozzle_0.6.json @@ -7,14 +7,14 @@ "layer_height": "0.36", "initial_layer_print_height": "0.3", "bridge_flow": "1", - "line_width": "0.62", - "outer_wall_line_width": "0.62", - "initial_layer_line_width": "0.62", - "sparse_infill_line_width": "0.62", - "inner_wall_line_width": "0.62", - "internal_solid_infill_line_width": "0.62", - "support_line_width": "0.62", - "top_surface_line_width": "0.62", + "line_width": "103.33%", + "outer_wall_line_width": "103.33%", + "initial_layer_line_width": "103.33%", + "sparse_infill_line_width": "103.33%", + "inner_wall_line_width": "103.33%", + "internal_solid_infill_line_width": "103.33%", + "support_line_width": "103.33%", + "top_surface_line_width": "103.33%", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.40_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.40_nozzle_0.8.json index 45bd45c8a54..3850aaf0f55 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.40_nozzle_0.8.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.40_nozzle_0.8.json @@ -7,14 +7,14 @@ "layer_height": "0.4", "initial_layer_print_height": "0.4", "bridge_flow": "1", - "line_width": "0.82", - "outer_wall_line_width": "0.82", - "initial_layer_line_width": "0.82", - "sparse_infill_line_width": "0.82", - "inner_wall_line_width": "0.82", - "internal_solid_infill_line_width": "0.82", - "support_line_width": "0.82", - "top_surface_line_width": "0.82", + "line_width": "102.5%", + "outer_wall_line_width": "102.5%", + "initial_layer_line_width": "102.5%", + "sparse_infill_line_width": "102.5%", + "inner_wall_line_width": "102.5%", + "internal_solid_infill_line_width": "102.5%", + "support_line_width": "102.5%", + "top_surface_line_width": "102.5%", "top_surface_pattern": "monotonic", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.42_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.42_nozzle_0.6.json index 57cc629208c..91224ef8427 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.42_nozzle_0.6.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.42_nozzle_0.6.json @@ -7,14 +7,14 @@ "layer_height": "0.42", "initial_layer_print_height": "0.3", "bridge_flow": "1", - "line_width": "0.62", - "outer_wall_line_width": "0.62", - "initial_layer_line_width": "0.62", - "sparse_infill_line_width": "0.62", - "inner_wall_line_width": "0.62", - "internal_solid_infill_line_width": "0.62", - "support_line_width": "0.62", - "top_surface_line_width": "0.62", + "line_width": "103.33%", + "outer_wall_line_width": "103.33%", + "initial_layer_line_width": "103.33%", + "sparse_infill_line_width": "103.33%", + "inner_wall_line_width": "103.33%", + "internal_solid_infill_line_width": "103.33%", + "support_line_width": "103.33%", + "top_surface_line_width": "103.33%", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", "sparse_infill_speed": "100", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.48_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.48_nozzle_0.8.json index be97fb0672b..2bb250ed9bd 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.48_nozzle_0.8.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.48_nozzle_0.8.json @@ -7,14 +7,14 @@ "layer_height": "0.48", "initial_layer_print_height": "0.4", "bridge_flow": "1", - "line_width": "0.82", - "outer_wall_line_width": "0.82", - "initial_layer_line_width": "0.82", - "sparse_infill_line_width": "0.82", - "inner_wall_line_width": "0.82", - "internal_solid_infill_line_width": "0.82", - "support_line_width": "0.82", - "top_surface_line_width": "0.82", + "line_width": "102.5%", + "outer_wall_line_width": "102.5%", + "initial_layer_line_width": "102.5%", + "sparse_infill_line_width": "102.5%", + "inner_wall_line_width": "102.5%", + "internal_solid_infill_line_width": "102.5%", + "support_line_width": "102.5%", + "top_surface_line_width": "102.5%", "top_surface_pattern": "monotonic", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.56_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.56_nozzle_0.8.json index 2aed528ea3a..30225fb7bb0 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.56_nozzle_0.8.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.56_nozzle_0.8.json @@ -7,14 +7,14 @@ "layer_height": "0.56", "initial_layer_print_height": "0.4", "bridge_flow": "1", - "line_width": "0.82", - "outer_wall_line_width": "0.82", - "initial_layer_line_width": "0.82", - "sparse_infill_line_width": "0.82", - "inner_wall_line_width": "0.82", - "internal_solid_infill_line_width": "0.82", - "support_line_width": "0.82", - "top_surface_line_width": "0.82", + "line_width": "102.5%", + "outer_wall_line_width": "102.5%", + "initial_layer_line_width": "102.5%", + "sparse_infill_line_width": "102.5%", + "inner_wall_line_width": "102.5%", + "internal_solid_infill_line_width": "102.5%", + "support_line_width": "102.5%", + "top_surface_line_width": "102.5%", "top_surface_pattern": "monotonic", "initial_layer_speed": "35", "initial_layer_infill_speed": "55", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_common.json b/resources/profiles/Snapmaker/process/fdm_process_U1_common.json index 4f9c8a34079..4dc5e352b16 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_common.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_common.json @@ -16,10 +16,10 @@ "enable_arc_fitting": "1", "outer_wall_acceleration": "5000", "wall_infill_order": "inner wall/outer wall/infill", - "line_width": "0.42", + "line_width": "105%", "internal_bridge_support_thickness": "0.8", "initial_layer_acceleration": "500", - "initial_layer_line_width": "0.5", + "initial_layer_line_width": "125%", "initial_layer_speed": "30", "gap_infill_speed": "50", "sparse_infill_speed": "250", @@ -41,7 +41,7 @@ "skirt_height": "1", "skirt_loops": "0", "minimum_sparse_infill_area": "15", - "internal_solid_infill_line_width": "0.42", + "internal_solid_infill_line_width": "105%", "internal_solid_infill_speed": "150", "initial_layer_infill_speed": "60", "resolution": "0.012",