diff --git a/src/libslic3r/Clipper2Utils.cpp b/src/libslic3r/Clipper2Utils.cpp index 12fd867500..d389d25209 100644 --- a/src/libslic3r/Clipper2Utils.cpp +++ b/src/libslic3r/Clipper2Utils.cpp @@ -188,6 +188,18 @@ ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta) return results; } +ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta, Clipper2Lib::JoinType joinType) +{ + Clipper2Lib::Paths64 subject = Slic3rExPolygons_to_Paths64(expolygons); + Clipper2Lib::ClipperOffset offsetter; + offsetter.AddPaths(subject, joinType, Clipper2Lib::EndType::Polygon); + Clipper2Lib::PolyPath64 polytree; + offsetter.Execute(delta, polytree); + ExPolygons results = PolyTreeToExPolygons(std::move(polytree)); + + return results; +} + ExPolygons offset2_ex_2(const ExPolygons& expolygons, double delta1, double delta2) { // 1st offset diff --git a/src/libslic3r/Clipper2Utils.hpp b/src/libslic3r/Clipper2Utils.hpp index 54b48d6bd7..2b4b0fb2ef 100644 --- a/src/libslic3r/Clipper2Utils.hpp +++ b/src/libslic3r/Clipper2Utils.hpp @@ -15,6 +15,7 @@ Slic3r::Polylines diff_pl_2(const Slic3r::Polylines& subject, const Slic3r::Pol ExPolygons union_ex_2(const Polygons &expolygons); ExPolygons union_ex_2(const ExPolygons &expolygons); ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta); +ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta, Clipper2Lib::JoinType joinType); ExPolygons offset2_ex_2(const ExPolygons &expolygons, double delta1, double delta2); } diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 6c29367c01..1c785c3184 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -571,6 +571,19 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p return extrusion_coll; } +// ORCA: only_one_wall_top detects the top as "slice − upper", so a feature rising from the middle of a +// top surface becomes an enclosed hole that gets ringed with extra inner walls. Fill those holes back +// into the top. Only holes that are both covered by the upper layer (excludes bridges) and backed by +// solid material (excludes voids) are filled. +static ExPolygons fill_enclosed_top_feature_holes(const ExPolygons &top, const Polygons &covered_by_upper, const ExPolygons &solid) +{ + ExPolygons filled = top; + for (ExPolygon &ex : filled) + ex.holes.clear(); + const ExPolygons feature_holes = intersection_ex(intersection_ex(diff_ex(filled, top), covered_by_upper), solid); + return feature_holes.empty() ? top : union_ex(top, feature_holes); +} + void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills, ExPolygons &non_top_polygons, ExPolygons &fill_clip) const { // other perimeters @@ -636,6 +649,8 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP ExPolygons delete_bridge = diff_ex(orig_polygons, bridge_checker, ApplySafetyOffset::Yes); ExPolygons top_polygons = diff_ex(delete_bridge, upper_polygons_series_clipped, ApplySafetyOffset::Yes); + top_polygons = fill_enclosed_top_feature_holes(top_polygons, upper_polygons_series_clipped, orig_polygons); + // get the not-top surface, from the "real top" but enlarged by external_infill_margin (and the // min_width_top_surface we removed a bit before) ExPolygons temp_gap = diff_ex(top_polygons, fill_clip); @@ -2194,6 +2209,7 @@ void PerimeterGenerator::process_arachne() upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox); top_expolygons = diff_ex(infill_contour, upper_slices_clipped); + top_expolygons = fill_enclosed_top_feature_holes(top_expolygons, upper_slices_clipped, infill_contour); if (!top_expolygons.empty()) { if (lower_slices != nullptr) { diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index cf8469567f..e045b8c3c3 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1044,6 +1044,9 @@ static std::vector s_Preset_print_options{ "lightning_prune_angle", "lightning_straightening_angle", "top_surface_pattern", + "top_surface_expansion", + "top_surface_expansion_margin", + "top_surface_expansion_direction", "bottom_surface_pattern", "infill_direction", "solid_infill_direction", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d8887b19e2..e626d3e37f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -221,6 +221,13 @@ static t_config_enum_values s_keys_map_FuzzySkinMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FuzzySkinMode) +static t_config_enum_values s_keys_map_TopSurfaceExpansionDirection { + { "inward_and_outward", int(TopSurfaceExpansionDirection::InwardAndOutward) }, + { "inward", int(TopSurfaceExpansionDirection::Inward) }, + { "outward", int(TopSurfaceExpansionDirection::Outward) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TopSurfaceExpansionDirection) + static t_config_enum_values s_keys_map_InfillPattern { { "monotonic", ipMonotonic }, { "monotonicline", ipMonotonicLine }, @@ -2123,6 +2130,47 @@ void PrintConfigDef::init_fff_params() def->max = 100; def->set_default_value(new ConfigOptionPercent(100)); + def = this->add("top_surface_expansion", coFloat); + def->label = L("Top surface expansion"); + def->category = L("Strength"); + def->tooltip = L("Expands the top surfaces by this distance to connect distinct top surfaces and fill gaps.\n" + "Useful for cases where the top surface is interrupted by a raised feature, such as text on a plane." + "Expanding it removes the holes beneath these features and creates a continuous path with a better finish for printing on top." + "The expansion is applied to the original top surface, before any other processing such as bridging or overhang detection."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + + def = this->add("top_surface_expansion_margin", coFloat); + def->label = L("Top expansion wall margin"); + def->category = L("Strength"); + def->tooltip = L("Using “Top surface expansion” may cause a surface that did not previously touch the model's outer walls to now do so.\n" + "This can cause contraction marks (such as the hull line) on the outer walls.\n" + "By adding a small margin, this contraction will not occur directly on the walls, thereby preventing a visible mark."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 10; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + + def = this->add("top_surface_expansion_direction", coEnum); + def->label = L("Top expansion direction"); + def->category = L("Strength"); + def->tooltip = L("Direction in which the top surface expansion grows.\n" + " - Inward grows into the holes and gaps left by features rising from the middle of a top surface.\n" + " - Outward grows the outer edge of the surface, connecting surfaces separated by features that can divide a surface, such as a lattice pattern.\n" + " - Inward and Outward does both."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("inward_and_outward"); + def->enum_values.push_back("inward"); + def->enum_values.push_back("outward"); + def->enum_labels.push_back(L("Inward and Outward")); + def->enum_labels.push_back(L("Inward")); + def->enum_labels.push_back(L("Outward")); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(TopSurfaceExpansionDirection::InwardAndOutward)); + def = this->add("bottom_surface_pattern", coEnum); def->label = L("Bottom surface pattern"); def->category = L("Strength"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index fbeb9f0574..31e3c2cc45 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -62,6 +62,13 @@ enum class FuzzySkinMode { Combined, }; +// ORCA: direction in which top_surface_expansion grows the top surfaces. +enum class TopSurfaceExpansionDirection { + InwardAndOutward, + Inward, + Outward, +}; + enum class NoiseType { Classic, Perlin, @@ -540,6 +547,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrinterTechnology) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TopSurfaceExpansionDirection) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) @@ -1194,6 +1202,9 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloatOrPercent, top_surface_line_width)) ((ConfigOptionInt, top_shell_layers)) ((ConfigOptionFloat, top_shell_thickness)) + ((ConfigOptionFloat, top_surface_expansion)) + ((ConfigOptionFloat, top_surface_expansion_margin)) + ((ConfigOptionEnum, top_surface_expansion_direction)) ((ConfigOptionFloatsNullable, top_surface_speed)) //BBS ((ConfigOptionBoolsNullable, enable_overhang_speed)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 13d7297f93..10cc92013b 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4,6 +4,7 @@ #include "Print.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" +#include "Clipper2Utils.hpp" #include "ElephantFootCompensation.hpp" #include "Geometry.hpp" #include "I18N.hpp" @@ -1297,6 +1298,9 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_combination_max_layer_height" || opt_key == "bottom_shell_thickness" || opt_key == "top_shell_thickness" + || opt_key == "top_surface_expansion" + || opt_key == "top_surface_expansion_margin" + || opt_key == "top_surface_expansion_direction" || opt_key == "minimum_sparse_infill_area" || opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" @@ -1686,6 +1690,69 @@ void PrintObject::detect_surfaces_type() } } + // ORCA: Expand the top surfaces outward by top_surface_expansion in every direction. This + // enlarges the top solid infill and, in particular, grows it over the covered material left + // by features rising from the middle of a top surface (filling holes and joining tops so the + // features rest on it). The expansion stays inside the section it belongs to: each connected + // solid island has its own outer wall, so the top is grown within each island separately and + // clipped to it - growing one island's top across the gap into another island (which may have + // no top surface, leaving a partially filled layer) is never allowed. The top infill sits + // inside the perimeters, so the margin is measured from the walls: the island is inset by the + // band the walls consume (outer wall + inner walls) plus the configured margin, making that + // value the real clearance between the expanded top and the walls (avoiding a hull line). The + // original top is unioned back in, so where it already sits within that band it is kept as-is. + // Never claims a bottom surface. + const double top_expansion = layerm->region().config().top_surface_expansion.value; + if (top_expansion > 0. && ! top.empty()) { + const double d = scale_(top_expansion); + const auto jt = Clipper2Lib::JoinType::Miter; + const ExPolygons T = union_ex(to_expolygons(top)); + const int wall_loops = layerm->region().config().wall_loops.value; + const double wall_band = wall_loops <= 0 ? 0. : + double(layerm->flow(frExternalPerimeter).scaled_width()) + + double(layerm->flow(frPerimeter).scaled_width()) * double(wall_loops - 1); + const double margin = scale_(layerm->region().config().top_surface_expansion_margin.value); + // minimum real top to act on: ignore anything thinner than ~2 top-infill lines + const float min_top = float(layerm->flow(frTopSolidInfill).scaled_width()); + const auto direction = layerm->region().config().top_surface_expansion_direction.value; + + ExPolygons grown; + for (const ExPolygon &island : union_ex(layerm_slices_surfaces)) { + // The top infill only exists inside the perimeters, so seed and measure from the infill + // region (the island minus the wall band), not the raw slice. A section whose only + // exposed top lies in the wall band - i.e. a layer where the top is just the walls + // themselves - has no infill here and is skipped, instead of being flooded inward by + // the expansion. Thin slivers inside the infill region are dropped by the opening too. + const ExPolygons infill_region = wall_band > 0. ? offset_ex(island, -float(wall_band)) : ExPolygons{ island }; + const ExPolygons island_top = intersection_ex(T, infill_region); + if (opening_ex(island_top, min_top).empty()) + continue; // no real top infill in this section - never expand into it + + // grow by d, then keep only the part allowed by the configured direction: inward fills + // the holes/gaps left by features (clip the growth back to the top's own filled outline, + // which leaves the outer edge fixed), outward grows the outer edge toward the walls (drop + // the growth that fell into the original holes), and inward+outward keeps both. + ExPolygons expanded = offset_ex_2(island_top, d, jt); + if (direction != TopSurfaceExpansionDirection::InwardAndOutward) { + ExPolygons outline; // the top with its holes filled (same outer edge) + outline.reserve(island_top.size()); + for (const ExPolygon &ex : island_top) + outline.emplace_back(ex.contour); + outline = union_ex(outline); + expanded = direction == TopSurfaceExpansionDirection::Inward ? + intersection_ex(expanded, outline) : // only growth into the holes + diff_ex(expanded, diff_ex(outline, island_top)); // only growth past the outer edge + } + // hold the expansion clear of the walls by the configured margin + const ExPolygons allowed = margin > 0. ? offset_ex(infill_region, -float(margin)) : infill_region; + append(grown, intersection_ex(expanded, allowed)); + } + + ExPolygons new_top = diff_ex(union_ex(T, grown), to_expolygons(bottom)); + top.clear(); + surfaces_append(top, std::move(new_top), stTop); + } + #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { static int iRun = 0; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 46758e3e64..a41473bc3d 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -713,6 +713,13 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_field("top_layer_direction", has_top_shell); toggle_field("bottom_layer_direction", has_bottom_shell); + toggle_line("top_surface_expansion", has_top_shell); + toggle_line("top_surface_expansion_margin", has_top_shell); + bool has_top_surface_expansion = config->opt_float("top_surface_expansion") > 0; + toggle_field("top_surface_expansion_margin", has_top_surface_expansion); + toggle_line("top_surface_expansion_direction", has_top_shell); + toggle_field("top_surface_expansion_direction", has_top_surface_expansion); + for (auto el : { "infill_direction", "sparse_infill_line_width", "gap_fill_target","filter_out_gap_fill","infill_wall_overlap", "bridge_angle", "internal_bridge_angle", "relative_bridge_angle", "solid_infill_direction", "solid_infill_rotate_template", "internal_solid_infill_pattern", "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id", diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index ccb6dedd29..75dc44add6 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -132,6 +132,9 @@ std::map> SettingsFactory::PART_CATE {"infill_anchor", "", 1}, {"infill_anchor_max", "", 1}, {"top_surface_pattern", "", 1}, + {"top_surface_expansion", "", 1}, + {"top_surface_expansion_margin", "", 1}, + {"top_surface_expansion_direction", "", 1}, {"bottom_surface_pattern", "", 1}, {"internal_solid_infill_pattern", "", 1}, {"align_infill_direction_to_model", "", 1}, @@ -187,7 +190,7 @@ std::vector SettingsFactory::get_visible_options(const std::s //Quality "wall_infill_order", "ironing_type", "inner_wall_line_width", "outer_wall_line_width", "top_surface_line_width", //Shell - "wall_loops", "top_shell_layers", "bottom_shell_layers", "top_shell_thickness", "bottom_shell_thickness", + "wall_loops", "top_shell_layers", "bottom_shell_layers", "top_shell_thickness", "bottom_shell_thickness", "top_surface_expansion", "top_surface_expansion_margin", "top_surface_expansion_direction", //Infill "sparse_infill_density", "sparse_infill_pattern", "top_surface_pattern", "bottom_surface_pattern", "infill_combination", "infill_direction", "infill_wall_overlap", //speed diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index b3fc51c245..0e0adbbdda 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2743,6 +2743,10 @@ void TabPrint::build() optgroup->append_single_option_line("top_surface_density", "strength_settings_top_bottom_shells#surface-density"); optgroup->append_single_option_line("top_surface_pattern", "strength_settings_top_bottom_shells#surface-pattern"); optgroup->append_single_option_line("top_layer_direction", "strength_settings_infill#top-direction"); + optgroup->append_single_option_line("top_surface_hole_contraction", "strength_settings_top_bottom_shells#surface-hole-contraction"); + optgroup->append_single_option_line("top_surface_expansion", "strength_settings_top_bottom_shells#surface-expansion"); + optgroup->append_single_option_line("top_surface_expansion_margin", "strength_settings_top_bottom_shells#surface-expansion-margin"); + optgroup->append_single_option_line("top_surface_expansion_direction", "strength_settings_top_bottom_shells#surface-expansion-direction"); optgroup->append_single_option_line("bottom_shell_layers", "strength_settings_top_bottom_shells#shell-layers"); optgroup->append_single_option_line("bottom_shell_thickness", "strength_settings_top_bottom_shells#shell-thickness"); optgroup->append_single_option_line("bottom_surface_density", "strength_settings_top_bottom_shells#surface-density");