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 <ian.bassi@outlook.com>
This commit is contained in:
Rodrigo Faselli
2026-09-26 16:42:44 -03:00
committed by GitHub
co-authored by Copilot Autofix powered by AI Ian Bassi
parent ea280ba6f6
commit 0ddc730854
3 changed files with 126 additions and 4 deletions
+29 -4
View File
@@ -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<MergedFuzzyRegion> 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<MergedFuzzyRegion>& merged_regions, const std::optional<ExPolygons>& 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);
+3
View File
@@ -2,6 +2,7 @@
#define slic3r_PerimeterGenerator_hpp_
#include "libslic3r.h"
#include <optional>
#include <vector>
#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<std::pair<FuzzySkinConfig, ExPolygons>> regions_by_fuzzify;
// Area resting on the layer below, where fuzzy skin is allowed. Unset means no restriction.
std::optional<ExPolygons> fuzzy_supported_area;
PerimeterGenerator(
// Input:
+94
View File
@@ -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<double>(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);
}