From 0ddc730854d5f54848a4e880495724fb37ff59b2 Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:42:44 -0300 Subject: [PATCH] No fuzzy skin on bridge like overhangs perimeters (#13891) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ian Bassi --- src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp | 33 ++++++- src/libslic3r/PerimeterGenerator.hpp | 3 + tests/fff_print/test_perimeters.cpp | 94 +++++++++++++++++++ 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp index e5fbeb5cdb..be86e92245 100644 --- a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp +++ b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp @@ -464,6 +464,16 @@ void group_region_by_fuzzify(PerimeterGenerator& g) } } + g.fuzzy_supported_area.reset(); + if ((g.has_fuzzy_skin || g.has_fuzzy_hole) && g.lower_slices != nullptr) { + coord_t max_thickness = 0; + for (const auto& region : regions) + if (should_fuzzify(region.config, g.layer_id, 0, true) || should_fuzzify(region.config, g.layer_id, 0, false)) + max_thickness = std::max(max_thickness, region.config.thickness); + // Walls farther than a line width plus the noise amplitude from the layer below are bridging; keep them smooth. + g.fuzzy_supported_area = offset_ex(*g.lower_slices, float(g.ext_perimeter_flow.scaled_width() + max_thickness)); + } + if (regions.size() == 1) { // optimization g.regions_by_fuzzify.push_back({regions.front().config, {}}); return; @@ -560,13 +570,23 @@ static std::vector collect_merged_fuzzy_regions(const std::ve return merged_regions; } +// Afterwards an empty region means nothing to fuzzify, no longer full coverage. +static void restrict_to_supported(std::vector& merged_regions, const std::optional& supported) +{ + if (!supported) + return; + for (auto& merged_region : merged_regions) + merged_region.expolygons = merged_region.expolygons.empty() ? *supported : intersection_ex(merged_region.expolygons, *supported); +} + Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, const size_t loop_idx, const bool is_contour) { Polygon fuzzified; const auto slice_z = perimeter_generator.slice_z; const auto& regions = perimeter_generator.regions_by_fuzzify; - if (regions.size() == 1) { // optimization + const auto& supported = perimeter_generator.fuzzy_supported_area; + if (regions.size() == 1 && !supported) { // optimization const auto& config = regions.begin()->first; const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, loop_idx, is_contour); if (!fuzzify) { @@ -590,7 +610,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim // Fast path: single merged region — apply directly without splitting if (merged_regions.size() == 1) { const auto& mr = merged_regions.front(); - if (mr.expolygons.empty()) { + if (mr.expolygons.empty() && !supported) { fuzzified = polygon; fuzzy_polyline(fuzzified.points, true, slice_z, *mr.config); return fuzzified; @@ -626,6 +646,8 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty()) merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons); + restrict_to_supported(merged_regions, supported); + // Split the loops into lines with different config, and fuzzy them separately fuzzified = polygon; for (const auto& r : merged_regions) { @@ -689,7 +711,8 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato const auto slice_z = perimeter_generator.slice_z; const auto layer_height = perimeter_generator.layer_height; const auto& regions = perimeter_generator.regions_by_fuzzify; - if (regions.size() == 1) { // optimization + const auto& supported = perimeter_generator.fuzzy_supported_area; + if (regions.size() == 1 && !supported) { // optimization const auto& config = regions.begin()->first; const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour); if (fuzzify) @@ -703,7 +726,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato if (!merged_regions.empty()) { // Fast path: single merged region — apply directly without splitting - if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) { + if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty() && !supported) { fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *merged_regions.front().config, closed); return; } @@ -753,6 +776,8 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty()) merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons); + restrict_to_supported(merged_regions, supported); + // Split the loops into lines with different config, and fuzzy them separately for (const auto& r : merged_regions) { const auto splitted = Algorithm::split_line(*extrusion, r.expolygons, false); diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 4b8f84ca5f..cd49b4acdd 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -2,6 +2,7 @@ #define slic3r_PerimeterGenerator_hpp_ #include "libslic3r.h" +#include #include #include "Layer.hpp" #include "Flow.hpp" @@ -105,6 +106,8 @@ public: bool has_fuzzy_hole = false; // Preserve construction order so overlap precedence remains deterministic. std::vector> regions_by_fuzzify; + // Area resting on the layer below, where fuzzy skin is allowed. Unset means no restriction. + std::optional fuzzy_supported_area; PerimeterGenerator( // Input: diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp index 6d9442e5d7..459ba0a178 100644 --- a/tests/fff_print/test_perimeters.cpp +++ b/tests/fff_print/test_perimeters.cpp @@ -630,3 +630,97 @@ TEST_CASE("A lower layer sliver too thin to print does not support the wall abov // A rib that does get printed takes the 20mm outer wall running along it out of the overhangs. CHECK(printable < no_rib - scale_(15.)); } + +namespace { + +// Every setting the fuzzy skin assertions below depend on. +DynamicPrintConfig fuzzy_skin_config(const char *wall_generator) +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "wall_generator", wall_generator }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + // One wall, so every wall point along the long sides belongs to the fuzzed outer wall. + { "wall_loops", 1 }, + { "fuzzy_skin", "external" }, + { "fuzzy_skin_noise_type", "classic" }, + { "fuzzy_skin_thickness", 0.3 }, + { "fuzzy_skin_point_distance", 0.8 }, + }); + return config; +} + +// How far the wall points over the middle 60% of the layer's length stray across its width, worst side. +// A negative result means there is no layer at `print_z`. +double mid_span_wall_spread(const Print &print, double print_z) +{ + for (const Layer *layer : print.objects().front()->layers()) { + if (std::abs(layer->print_z - print_z) > 1e-4) + continue; + const BoundingBox bbox = get_extents(layer->lslices); + const coord_t x_min = bbox.min.x() + bbox.size().x() / 5; + const coord_t x_max = bbox.max.x() - bbox.size().x() / 5; + Points points; + for (const LayerRegion *region : layer->regions()) + region->perimeters.collect_points(points); + coord_t spread = 0; + for (const bool south : { true, false }) { + coord_t lo = bbox.max.y(), hi = bbox.min.y(); + for (const Point &p : points) + if (p.x() > x_min && p.x() < x_max && (p.y() < bbox.center().y()) == south) { + lo = std::min(lo, p.y()); + hi = std::max(hi, p.y()); + } + spread = std::max(spread, hi - lo); + } + return unscale(spread); + } + return -1.; +} + +} // namespace + +// TestMesh::bridge is a 50x10mm deck from z=5 to z=8 on two 5mm-wide pillars, leaving a 40mm span. The deck's +// first layer (print_z 5.2) crosses the span unsupported; the layers above it rest on the deck. +TEST_CASE("Fuzzy skin leaves the walls of a bridge smooth", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(wall_generator); + + Print print; + init_and_process_print({ TestMesh::bridge }, print, fuzzy_skin_config(wall_generator)); + REQUIRE_FALSE(print.objects().empty()); + + // Control: one deck layer up the same walls rest on the deck, so they are fuzzed. + CHECK(mid_span_wall_spread(print, 5.6) > 0.1); + // Over the unsupported span the walls stay straight. + const double bridged = mid_span_wall_spread(print, 5.2); + CHECK(bridged >= 0.); + CHECK(bridged < 0.001); +} + +// One object: a 20x20x3mm block on the bed and a second one floating above it from z=5 to z=8. The layers in +// the gap are empty, so the floating block's first layer (print_z 5.2) has a layer below it with nothing +// printed on it; the layers above rest on the floating block. +TEST_CASE("Fuzzy skin leaves the walls over an empty layer smooth", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(wall_generator); + + TriangleMesh mesh = make_cube(20., 20., 3.); + TriangleMesh floating = make_cube(20., 20., 3.); + floating.translate(0.f, 0.f, 5.f); + mesh.merge(floating); + + Print print; + init_and_process_print({ mesh }, print, fuzzy_skin_config(wall_generator)); + REQUIRE_FALSE(print.objects().empty()); + + // Control: one layer up the walls rest on the floating block, so they are fuzzed. + CHECK(mid_span_wall_spread(print, 5.6) > 0.1); + // Nothing is printed under the first floating layer, so its walls stay straight. + const double floating_first_layer = mid_span_wall_spread(print, 5.2); + CHECK(floating_first_layer >= 0.); + CHECK(floating_first_layer < 0.001); +}