diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index c2fd948e57..d4182df666 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -454,7 +454,8 @@ void Preset::set_visible_from_appconfig(const AppConfig &app_config) static std::vector s_Preset_print_options { "layer_height", "first_layer_height", "perimeters", "spiral_vase", "slice_closing_radius", "slicing_mode", "top_solid_layers", "top_solid_min_thickness", "bottom_solid_layers", "bottom_solid_min_thickness", - "extra_perimeters", "extra_perimeters_on_overhangs", "avoid_crossing_curled_overhangs", "avoid_crossing_perimeters", "thin_walls", "overhangs", + "ensure_vertical_shell_thickness", "extra_perimeters", "extra_perimeters_on_overhangs", + "avoid_crossing_curled_overhangs", "avoid_crossing_perimeters", "thin_walls", "overhangs", "seam_position","staggered_inner_seams", "external_perimeters_first", "fill_density", "fill_pattern", "top_fill_pattern", "bottom_fill_pattern", "scarf_seam_placement", "scarf_seam_only_on_smooth", "scarf_seam_start_height", "scarf_seam_entire_loop", "scarf_seam_length", "scarf_seam_max_segment_length", "scarf_seam_on_inner_perimeters", "infill_every_layers", /*"infill_only_where_needed",*/ "solid_infill_every_layers", "fill_angle", "bridge_angle", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d5b2acfa14..9a33bddcec 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -314,6 +314,13 @@ static const t_config_enum_values s_keys_map_TiltSpeeds{ }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TiltSpeeds) +static const t_config_enum_values s_keys_map_EnsureVerticalShellThickness { + { "disabled", int(EnsureVerticalShellThickness::Disabled) }, + { "partial", int(EnsureVerticalShellThickness::Partial) }, + { "enabled", int(EnsureVerticalShellThickness::Enabled) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(EnsureVerticalShellThickness) + static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology) { for (std::pair &kvp : options) @@ -958,6 +965,19 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionStrings { "; Filament-specific end gcode \n;END gcode for filament\n" }); + def = this->add("ensure_vertical_shell_thickness", coEnum); + def->label = L("Ensure vertical shell thickness"); + def->category = L("Layers and Perimeters"); + def->tooltip = L("Add solid infill near sloping surfaces to guarantee the vertical shell thickness " + "(top+bottom solid layers)."); + def->set_enum({ + { "disabled", L("Disabled") }, + { "partial", L("Partial") }, + { "enabled", L("Enabled") }, + }); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(EnsureVerticalShellThickness::Enabled)); + auto def_top_fill_pattern = def = this->add("top_fill_pattern", coEnum); def->label = L("Top fill pattern"); def->category = L("Infill"); @@ -4795,8 +4815,6 @@ static std::set PrintConfigDef_ignore = { "fuzzy_skin_perimeter_mode", "fuzzy_skin_shape", // Introduced in PrusaSlicer 2.3.0-alpha2, later replaced by automatic calculation based on extrusion width. "wall_add_middle_threshold", "wall_split_middle_threshold", - // Replaced by new concentric ensuring in 2.6.0-alpha5 - "ensure_vertical_shell_thickness", // Disabled in 2.6.0-alpha6, this option is problematic "infill_only_where_needed", "gcode_binary", // Introduced in 2.7.0-alpha1, removed in 2.7.1 (replaced by binary_gcode). @@ -4873,16 +4891,17 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va opt_key = "printhost_apikey"; } else if (opt_key == "preset_name") { opt_key = "preset_names"; - } /*else if (opt_key == "material_correction" || opt_key == "relative_correction") { - ConfigOptionFloats p; - p.deserialize(value); - - if (p.values.size() < 3) { - double firstval = p.values.front(); - p.values.emplace(p.values.begin(), firstval); - value = p.serialize(); + } else if (opt_key == "ensure_vertical_shell_thickness") { + if (value == "1") { + value = "enabled"; + } else if (value == "0") { + value = "partial"; + } else if (const t_config_enum_values &enum_keys_map = ConfigOptionEnum::get_enum_values(); enum_keys_map.find(value) == enum_keys_map.end()) { + assert(value == "0" || value == "1"); + // Values other than 0/1 are replaced with "partial" for handling values from different slicers. + value = "partial"; } - }*/ + } // In PrusaSlicer 2.3.0-alpha0 the "monotonous" infill was introduced, which was later renamed to "monotonic". if (value == "monotonous" && (opt_key == "top_fill_pattern" || opt_key == "bottom_fill_pattern" || opt_key == "fill_pattern")) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 1a476f7ff6..da0fa245f1 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -232,6 +232,12 @@ enum TiltSpeeds : int { tsMove8000, }; +enum class EnsureVerticalShellThickness { + Disabled, + Partial, + Enabled, +}; + #define CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NAME) \ template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names(); \ template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values(); @@ -261,6 +267,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeThumbnailsFormat) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TopOnePerimeterType) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(EnsureVerticalShellThickness) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS @@ -683,6 +690,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloat, bottom_solid_min_thickness)) ((ConfigOptionFloat, bridge_flow_ratio)) ((ConfigOptionFloat, bridge_speed)) + ((ConfigOptionEnum, ensure_vertical_shell_thickness)) ((ConfigOptionEnum, top_fill_pattern)) ((ConfigOptionEnum, bottom_fill_pattern)) ((ConfigOptionFloatOrPercent, external_perimeter_extrusion_width)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 52e0612d18..bd0542ca6a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -809,6 +809,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_only_where_needed" || opt_key == "infill_every_layers" || opt_key == "solid_infill_every_layers" + || opt_key == "ensure_vertical_shell_thickness" || opt_key == "bottom_solid_min_thickness" || opt_key == "top_solid_layers" || opt_key == "top_solid_min_thickness" @@ -1296,6 +1297,21 @@ void PrintObject::discover_vertical_shells() if (top_bottom_surfaces_all_regions) { // This is a multi-material print and interface_shells are disabled, meaning that the vertical shell thickness // is calculated over all materials. + // Is the "ensure vertical wall thickness" applicable to any region? + bool has_extra_layers = false; + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) { + const PrintRegionConfig &config = this->printing_region(region_id).config(); + if (config.ensure_vertical_shell_thickness.value == EnsureVerticalShellThickness::Enabled || config.ensure_vertical_shell_thickness.value == EnsureVerticalShellThickness::Partial) { + has_extra_layers = true; + break; + } + } + + if (!has_extra_layers) { + // The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit. + return; + } + BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom"; //FIXME Improve the heuristics for a grain size. size_t grain_size = std::max(num_layers / 16, size_t(1)); @@ -1365,7 +1381,13 @@ void PrintObject::discover_vertical_shells() BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom"; } - for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) { + const PrintRegion ®ion = this->printing_region(region_id); + if (region.config().ensure_vertical_shell_thickness.value != EnsureVerticalShellThickness::Enabled && region.config().ensure_vertical_shell_thickness.value != EnsureVerticalShellThickness::Partial) { + // This region will be handled by discover_horizontal_shells(). + continue; + } + //FIXME Improve the heuristics for a grain size. size_t grain_size = std::max(num_layers / 16, size_t(1)); @@ -1485,7 +1507,10 @@ void PrintObject::discover_vertical_shells() ++ i) { at_least_one_top_projected = true; const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i]; - combine_holes(cache.holes); + if (region_config.ensure_vertical_shell_thickness.value != EnsureVerticalShellThickness::Partial) { + combine_holes(cache.holes); + } + combine_shells(cache.top_surfaces); } if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) { @@ -1514,7 +1539,10 @@ void PrintObject::discover_vertical_shells() -- i) { at_least_one_bottom_projected = true; const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i]; - combine_holes(cache.holes); + if (region_config.ensure_vertical_shell_thickness.value != EnsureVerticalShellThickness::Partial) { + combine_holes(cache.holes); + } + combine_shells(cache.bottom_surfaces); } @@ -2836,9 +2864,177 @@ void PrintObject::discover_horizontal_shells() if (surface.surface_type == stInternal) surface.surface_type = type; } - // The rest has already been performed by discover_vertical_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 != EnsureVerticalShellThickness::Disabled) + continue; + + assert(region_config.ensure_vertical_shell_thickness.value == EnsureVerticalShellThickness::Disabled); + + coordf_t print_z = layer->print_z; + coordf_t bottom_z = layer->bottom_z(); + for (size_t idx_surface_type = 0; idx_surface_type < 3; ++ idx_surface_type) { + m_print->throw_if_canceled(); + SurfaceType type = (idx_surface_type == 0) ? stTop : (idx_surface_type == 1) ? stBottom : stBottomBridge; + int num_solid_layers = (type == stTop) ? region_config.top_solid_layers.value : region_config.bottom_solid_layers.value; + if (num_solid_layers == 0) + continue; + // Find slices of current type for current layer. + // Use slices instead of fill_surfaces, because they also include the perimeter area, + // which needs to be propagated in shells; we need to grow slices like we did for + // fill_surfaces though. Using both ungrown slices and grown fill_surfaces will + // not work in some situations, as there won't be any grown region in the perimeter + // area (this was seen in a model where the top layer had one extra perimeter, thus + // its fill_surfaces were thinner than the lower layer's infill), however it's the best + // solution so far. Growing the external slices by EXTERNAL_INFILL_MARGIN will put + // too much solid infill inside nearly-vertical slopes. + + // Surfaces including the area of perimeters. Everything, that is visible from the top / bottom + // (not covered by a layer above / below). + // This does not contain the areas covered by perimeters! + Polygons solid; + for (const Surface &surface : layerm->slices()) + if (surface.surface_type == type) + polygons_append(solid, to_polygons(surface.expolygon)); + // Infill areas (slices without the perimeters). + for (const Surface &surface : layerm->fill_surfaces()) + if (surface.surface_type == type) + polygons_append(solid, to_polygons(surface.expolygon)); + if (solid.empty()) + continue; + + // Scatter top / bottom regions to other layers. Scattering process is inherently serial, it is difficult to parallelize without locking. + for (int n = (type == stTop) ? int(i) - 1 : int(i) + 1; + (type == stTop) ? + (n >= 0 && (int(i) - n < num_solid_layers || + print_z - m_layers[n]->print_z < region_config.top_solid_min_thickness.value - EPSILON)) : + (n < int(m_layers.size()) && (n - int(i) < num_solid_layers || + m_layers[n]->bottom_z() - bottom_z < region_config.bottom_solid_min_thickness.value - EPSILON)); + (type == stTop) ? -- 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]; + + // find intersection between neighbor and current layer's surfaces + // intersections have contours and holes + // we update $solid so that we limit the next neighbor layer to the areas that were + // found on this one - in other words, solid shells on one layer (for a given external surface) + // are always a subset of the shells found on the previous shell layer + // this approach allows for DWIM in hollow sloping vases, where we want bottom + // shells to be generated in the base but not in the walls (where there are many + // narrow bottom surfaces): reassigning $solid will consider the 'shadow' of the + // upper perimeter as an obstacle and shell will not be propagated to more upper layers + //FIXME How does it work for stInternalBRIDGE? This is set for sparse infill. Likely this does not work. + Polygons new_internal_solid; + { + Polygons internal; + for (const Surface &surface : neighbor_layerm->fill_surfaces()) + if (surface.surface_type == stInternal || surface.surface_type == stInternalSolid) + polygons_append(internal, to_polygons(surface.expolygon)); + new_internal_solid = intersection(solid, internal, ApplySafetyOffset::Yes); + } + if (new_internal_solid.empty()) { + // No internal solid needed on this layer. In order to decide whether to continue + // searching on the next neighbor (thus enforcing the configured number of solid + // layers, use different strategies according to configured infill density: + if (region_config.fill_density.value == 0 || region_config.ensure_vertical_shell_thickness.value == EnsureVerticalShellThickness::Disabled) { + // If user expects the object to be void (for example a hollow sloping vase), + // don't continue the search. In this case, we only generate the external solid + // shell if the object would otherwise show a hole (gap between perimeters of + // the two layers), and internal solid shells are a subset of the shells found + // on each previous layer. + goto EXTERNAL; + } else { + // If we have internal infill, we can generate internal solid shells freely. + continue; + } + } + + const float factor = (region_config.fill_density.value == 0) ? 1.f : 0.5f; + if (factor > 0.0f) { + // if we're printing a hollow object we discard any solid shell thinner + // than a perimeter width, since it's probably just crossing a sloping wall + // and it's not wanted in a hollow print even if it would make sense when + // obeying the solid shell count option strictly (DWIM!) + + // Also use the same strategy if the user has selected to reduce + // the amount of solid infill on walls. However reduce the margin to 20% overhang + // as we want to generate infill on sloped vertical surfaces but still keep a small amount of + // filtering. This is an arbitrary value to make this option safe + // by ensuring that top surfaces, especially slanted ones dont go **completely** unsupported + // especially when using single perimeter top layers. + float margin = float(neighbor_layerm->flow(frExternalPerimeter).scaled_width()) * factor; + Polygons too_narrow = diff(new_internal_solid, + opening(new_internal_solid, margin, margin + ClipperSafetyOffset, jtMiter, 5)); + // Trim the regularized region by the original region. + if (!too_narrow.empty()) + new_internal_solid = solid = diff(new_internal_solid, too_narrow); + } + + // make sure the new internal solid is wide enough, as it might get collapsed + // when spacing is added in Fill.pm + { + //FIXME Vojtech: Disable this and you will be sorry. + // https://github.com/prusa3d/PrusaSlicer/issues/26 bottom + float margin = layerm->flow(frSolidInfill).scaled_width(); // require at least this size + // we use a higher miterLimit here to handle areas with acute angles + // in those cases, the default miterLimit would cut the corner and we'd + // get a triangle in $too_narrow; if we grow it below then the shell + // would have a different shape from the external surface and we'd still + // have the same angle, so the next shell would be grown even more and so on. + Polygons too_narrow = diff( + new_internal_solid, + opening(new_internal_solid, margin, margin + ClipperSafetyOffset, ClipperLib::jtMiter, 5)); + if (! too_narrow.empty()) { + // grow the collapsing parts and add the extra area to the neighbor layer + // as well as to our original surfaces so that we support this + // additional area in the next shell too + // make sure our grown surfaces don't exceed the fill area + Polygons internal; + for (const Surface &surface : neighbor_layerm->fill_surfaces()) + if (surface.is_internal() && !surface.is_bridge()) + polygons_append(internal, to_polygons(surface.expolygon)); + polygons_append(new_internal_solid, + intersection( + expand(too_narrow, +margin), + // Discard bridges as they are grown for anchoring and we can't + // remove such anchors. (This may happen when a bridge is being + // anchored onto a wall where little space remains after the bridge + // is grown, and that little space is an internal solid shell so + // it triggers this too_narrow logic.) + internal)); + // see https://github.com/prusa3d/PrusaSlicer/pull/3426 + // solid = new_internal_solid; + } + } + + // internal-solid are the union of the existing internal-solid surfaces + // and new ones + SurfaceCollection backup = std::move(neighbor_layerm->m_fill_surfaces); + polygons_append(new_internal_solid, to_polygons(backup.filter_by_type(stInternalSolid))); + ExPolygons internal_solid = union_ex(new_internal_solid); + // assign new internal-solid surfaces to layer + neighbor_layerm->m_fill_surfaces.set(internal_solid, stInternalSolid); + // subtract intersections from layer surfaces to get resulting internal surfaces + Polygons polygons_internal = to_polygons(std::move(internal_solid)); + ExPolygons internal = diff_ex(backup.filter_by_type(stInternal), polygons_internal, ApplySafetyOffset::Yes); + // assign resulting internal surfaces to layer + neighbor_layerm->m_fill_surfaces.append(internal, stInternal); + polygons_append(polygons_internal, to_polygons(std::move(internal))); + // assign top and bottom surfaces to layer + backup.keep_types({ stTop, stBottom, stBottomBridge }); + std::vector top_bottom_groups; + backup.group(&top_bottom_groups); + for (SurfacesPtr &group : top_bottom_groups) + neighbor_layerm->m_fill_surfaces.append( + diff_ex(group, polygons_internal), + // Use an existing surface as a template, it carries the bridge angle etc. + *group.front()); + } + EXTERNAL:; + } // foreach type (stTop, stBottom, stBottomBridge) } // for each layer - } // for each region + } // for each region #ifdef SLIC3R_DEBUG_SLICE_PROCESSING for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) { @@ -2847,8 +3043,8 @@ void PrintObject::discover_horizontal_shells() layerm->export_region_slices_to_svg_debug("5_discover_horizontal_shells"); layerm->export_region_fill_surfaces_to_svg_debug("5_discover_horizontal_shells"); } // for each layer - } // for each region -#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ + } // for each region +#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ } // void PrintObject::discover_horizontal_shells() // combine fill surfaces across layers to honor the "infill every N layers" option diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index ee349cd7fd..dca9d2e58e 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -249,8 +249,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) "infill_speed", "bridge_speed" }) toggle_field(el, have_infill || has_solid_infill); - toggle_field("top_solid_min_thickness", ! has_spiral_vase && has_top_solid_infill); - toggle_field("bottom_solid_min_thickness", ! has_spiral_vase && has_bottom_solid_infill); + const bool has_ensure_vertical_shell_thickness = config->opt_enum("ensure_vertical_shell_thickness") != EnsureVerticalShellThickness::Disabled; + toggle_field("top_solid_min_thickness", !has_spiral_vase && has_top_solid_infill && has_ensure_vertical_shell_thickness); + toggle_field("bottom_solid_min_thickness", !has_spiral_vase && has_bottom_solid_infill && has_ensure_vertical_shell_thickness); // Gap fill is newly allowed in between perimeter lines even for empty infill (see GH #1476). toggle_field("gap_fill_speed", have_perimeters); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 88ceaa54be..8b36bd8cec 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1448,6 +1448,7 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Quality (slower slicing)")); optgroup->append_single_option_line("extra_perimeters", category_path + "extra-perimeters-if-needed"); optgroup->append_single_option_line("extra_perimeters_on_overhangs", category_path + "extra-perimeters-on-overhangs"); + optgroup->append_single_option_line("ensure_vertical_shell_thickness", category_path + "ensure-vertical-shell-thickness"); optgroup->append_single_option_line("avoid_crossing_curled_overhangs", category_path + "avoid-crossing-curled-overhangs"); optgroup->append_single_option_line("avoid_crossing_perimeters", category_path + "avoid-crossing-perimeters"); optgroup->append_single_option_line("avoid_crossing_perimeters_max_detour", category_path + "avoid_crossing_perimeters_max_detour");