diff --git a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp index 97f8f743fb..e5fbeb5cdb 100644 --- a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp +++ b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp @@ -342,7 +342,7 @@ void fuzzy_polyline(Points& poly, bool closed, coordf_t slice_z, const FuzzySkin } // Thanks Cura developers for this function. -void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, const FuzzySkinConfig& cfg, bool closed) +void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, coordf_t layer_height, const FuzzySkinConfig& cfg, bool closed) { if (cfg.noise_type == NoiseType::Ripple) { @@ -356,7 +356,9 @@ void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice const double min_dist_between_points = cfg.point_distance * 3. / 4.; // hardcoded: the point distance may vary between 3/4 and 5/4 the supplied value const double range_random_point_dist = cfg.point_distance / 2.; - const double min_extrusion_width = 0.01; // workaround for many print options. Need overwrite formula with the layer height parameter. The width must more than >>> layer_height * (1 - 0.25 * PI) * 1.05 <<< (last num is the coeff of overlay error case) + // ExtrusionJunction::w is a scaled coord_t, so this floor must be scaled too. + // Flow::rounded_rectangle_extrusion_spacing() requires width > height * (1 - 0.25 * PI); keep 5% above it. + const double min_extrusion_width = scaled(layer_height * (1. - 0.25 * M_PI) * 1.05); double dist_left_over = random_value() * (min_dist_between_points / 2.); // the distance to be traversed on the line before making the first new point auto* p0 = &ext_lines.front(); @@ -685,12 +687,13 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour, const bool closed) { 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& config = regions.begin()->first; const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour); if (fuzzify) - fuzzy_extrusion_line(extrusion->junctions, slice_z, config, closed); + fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, config, closed); } else { // Merge regions that produce identical fuzzy effects (differ only in type). // When the style (e.g. External) and a painted region (All) both fuzzify this loop @@ -701,7 +704,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato // Fast path: single merged region — apply directly without splitting if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) { - fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config, closed); + fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *merged_regions.front().config, closed); return; } @@ -761,7 +764,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato // Fuzzy splitted extrusion if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) { // The entire polygon is fuzzified - fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config, closed); + fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *r.config, closed); continue; } else { const auto current_ext = extrusion->junctions; @@ -769,12 +772,12 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato segment.reserve(current_ext.size()); extrusion->junctions.clear(); - const auto fuzzy_current_segment = [&segment, &extrusion, &r, slice_z]() { + const auto fuzzy_current_segment = [&segment, &extrusion, &r, slice_z, layer_height]() { // Orca: non fuzzy points to isolate fuzzy region const auto front = segment.front(); const auto back = segment.back(); - fuzzy_extrusion_line(segment, slice_z, *r.config, false); + fuzzy_extrusion_line(segment, slice_z, layer_height, *r.config, false); // Orca: only add non fuzzy point if it's not in the extrusion closing point. if (!extrusion->junctions.empty() && extrusion->junctions.front().p != front.p) { extrusion->junctions.push_back(front); diff --git a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.hpp b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.hpp index 51d503a3c9..ab0c491b38 100644 --- a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.hpp +++ b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.hpp @@ -9,7 +9,7 @@ namespace Slic3r::Feature::FuzzySkin { void fuzzy_polyline(Points& poly, bool closed, coordf_t slice_z, const FuzzySkinConfig& cfg); -void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, const FuzzySkinConfig& cfg, bool closed = true); +void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, coordf_t layer_height, const FuzzySkinConfig& cfg, bool closed = true); void group_region_by_fuzzify(PerimeterGenerator& g); diff --git a/tests/libslic3r/test_arachne_walls.cpp b/tests/libslic3r/test_arachne_walls.cpp index 4ca18400af..2b6580ed11 100644 --- a/tests/libslic3r/test_arachne_walls.cpp +++ b/tests/libslic3r/test_arachne_walls.cpp @@ -22,6 +22,8 @@ #include "libslic3r/Arachne/utils/ExtrusionLine.hpp" #include "libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp" #include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" +#include "libslic3r/Feature/FuzzySkin/FuzzySkin.hpp" +#include "libslic3r/Flow.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/ClipperUtils.hpp" @@ -309,3 +311,71 @@ TEST_CASE("Beading interpolation tolerates a thicker side with fewer insets", "[ CHECK(result.bead_widths[i] == expected.bead_widths[i]); } } + +namespace { + +// Closed 20 mm square loop at a uniform width. +Arachne::ExtrusionJunctions square_loop(coord_t width) +{ + const coord_t s = scaled(20.); + return {{Point(0, 0), width, 0}, {Point(s, 0), width, 0}, {Point(s, s), width, 0}, {Point(0, s), width, 0}, {Point(0, 0), width, 0}}; +} + +FuzzySkinConfig thick_fuzzy_config(FuzzySkinMode mode, NoiseType noise_type, double thickness_mm) +{ + FuzzySkinConfig cfg{}; + cfg.type = FuzzySkinType::All; + cfg.thickness = scaled(thickness_mm); + cfg.point_distance = scaled(0.3); + cfg.fuzzy_first_layer = true; + cfg.noise_type = noise_type; + cfg.noise_scale = 1.0; + cfg.noise_octaves = 4; + cfg.noise_persistence = 0.5; + cfg.mode = mode; + cfg.layer_id = 5; + return cfg; +} + +} // namespace + +// Extrusion and Combined mode add noise to each junction's width. A junction narrower than +// height * (1 - PI/4) makes Flow::rounded_rectangle_extrusion_spacing() throw and fails the slice. +// The fuzz thickness is 3x the line width so the clamp is hit on every run regardless of RNG seed. +// Ridged multifractal is covered because its output is not bounded to [-1, 1], so it scales past +// the configured thickness; the floor has to hold for any noise value, not just an in-range one. +TEST_CASE("Fuzzy skin extrusion width is floored at the minimum the flow accepts", "[Arachne][FuzzySkin]") { + using namespace Slic3r::Feature::FuzzySkin; + + const double layer_height = GENERATE(0.08, 0.2, 0.28); + const auto mode = GENERATE(FuzzySkinMode::Extrusion, FuzzySkinMode::Combined); + const auto noise_type = GENERATE(NoiseType::Classic, NoiseType::Perlin, NoiseType::Billow, NoiseType::RidgedMulti, NoiseType::Voronoi); + CAPTURE(layer_height, int(mode), int(noise_type)); + + const double line_width_mm = 0.42; + auto loop = square_loop(scaled(line_width_mm)); + fuzzy_extrusion_line(loop, /*slice_z*/ 1.0, layer_height, thick_fuzzy_config(mode, noise_type, 3 * line_width_mm)); + + REQUIRE(loop.size() > 100); + + const auto narrowest = std::min_element(loop.begin(), loop.end(), [](const auto& a, const auto& b) { return a.w < b.w; }); + const double narrowest_mm = unscaled(narrowest->w); + const double floor_mm = layer_height * (1. - 0.25 * PI); + CAPTURE(narrowest_mm, floor_mm); + + CHECK(narrowest_mm < line_width_mm); // the clamp was exercised + CHECK(narrowest_mm > floor_mm); + CHECK_NOTHROW(Flow::rounded_rectangle_extrusion_spacing(float(narrowest_mm), float(layer_height))); +} + +// Displacement mode only moves points; widths must pass through unchanged. +TEST_CASE("Fuzzy skin displacement mode leaves widths untouched", "[Arachne][FuzzySkin]") { + using namespace Slic3r::Feature::FuzzySkin; + + const coord_t width = scaled(0.42); + auto loop = square_loop(width); + fuzzy_extrusion_line(loop, /*slice_z*/ 1.0, /*layer_height*/ 0.2, thick_fuzzy_config(FuzzySkinMode::Displacement, NoiseType::Classic, 1.26)); + + REQUIRE(loop.size() > 100); + CHECK(std::all_of(loop.begin(), loop.end(), [width](const auto& j) { return j.w == width; })); +}