From da074043f2b9fa1cebd617b15a6a2f316df28a9e Mon Sep 17 00:00:00 2001 From: SoftFever Date: Thu, 25 Jun 2026 10:52:19 +0800 Subject: [PATCH] Fix fuzzy skin artifacting regression (#14376) (#14382) Fix fuzzy skin artifacting (#14376) --- .../WideningBeadingStrategy.cpp | 8 ++- tests/libslic3r/test_arachne_walls.cpp | 58 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp index 5bef586d66..ab8b070604 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp @@ -32,7 +32,13 @@ WideningBeadingStrategy::Beading WideningBeadingStrategy::compute(coord_t thickn // Previously used optimal_width which could differ from the outer wall width used in // bead count calculations, causing inconsistency where bead_count=2 was requested but // only 1 bead was produced. - if (thickness < getTransitionThickness(1)) { + // + // Orca #14376: only collapse to a single bead when at most one bead is requested. This + // branch emits one bead at the full wall thickness, so honoring it when bead_count >= 2 + // (which happens inside 1<->2 transition bands) produces an over-wide extrusion that shows + // up as a surface bulge. Deferring to the parent keeps the requested two beads and also + // keeps compute() consistent with the bead count the skeletal graph asked for. + if (bead_count <= 1 && thickness < getTransitionThickness(1)) { Beading ret; ret.total_thickness = thickness; if (thickness >= min_input_width) { diff --git a/tests/libslic3r/test_arachne_walls.cpp b/tests/libslic3r/test_arachne_walls.cpp index dc5d7143f6..2d8ee3bc03 100644 --- a/tests/libslic3r/test_arachne_walls.cpp +++ b/tests/libslic3r/test_arachne_walls.cpp @@ -19,11 +19,14 @@ #include "libslic3r/Arachne/WallToolPaths.hpp" #include "libslic3r/Arachne/utils/ExtrusionLine.hpp" +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp" +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Point.hpp" +#include #include using namespace Slic3r; @@ -207,3 +210,58 @@ TEST_CASE("Arachne wall generation - 60% min_bead_width", "[Arachne]") { size_t duplicates = run_arachne_test(60); REQUIRE(duplicates == 0); } + +// Regression test for #14376 ("Fuzzy skin artifacting" — a surface bulge at a fixed height). +// +// PR #14031 changed WideningBeadingStrategy::compute() to take the thin-wall single-bead +// branch whenever thickness < getTransitionThickness(1). That branch emits a single bead at +// the full wall thickness and ignores the requested bead_count. When the skeletal graph asks +// for 2 beads at a thickness inside the 1<->2 transition band (between the inner wall width and +// getTransitionThickness(1)), the request was collapsed into one over-wide bead — an +// over-extruded line that shows up as a bulge on curved surfaces at a deterministic height. +// +// Profile mirrors the reporter's project ("0.20mm Standard @BBL X1C", 0.4mm nozzle): +// outer 0.42mm / inner 0.45mm, min_bead_width 85% (0.34mm), 2 walls (max_bead_count 4). +// For these numbers wall_split_middle_threshold = 2*0.34/0.42 - 1 = 0.619, so +// getTransitionThickness(1) = (1 + 0.619) * 0.42 = 0.68mm. A 0.5mm-thick wall therefore sits +// in the transition band: alpha produced 2 beads here, beta collapses it to 1 fat bead. +TEST_CASE("Arachne widening keeps two beads in transition band (#14376)", "[Arachne]") { + using namespace Slic3r::Arachne; + + // Widths in mm; the scaled coord_t values and the thresholds below are both derived from + // these so a width change cannot silently desync the transition-band math. + const double outer_mm = 0.42, inner_mm = 0.45, min_bead_mm = 0.34; // min_bead = 85% of 0.4mm nozzle + + const coord_t outer_width = scaled(outer_mm); + const coord_t inner_width = scaled(inner_mm); + const coord_t min_bead_width = scaled(min_bead_mm); + const coord_t min_feature_size = scaled(0.10); // 25% of 0.4mm nozzle + const coord_t transition_length = scaled(0.40); + const coord_t max_bead_count = 4; // 2 * wall_loops + + // Same derivation as WallToolPaths.cpp. + const double split_middle_threshold = std::clamp(2.0 * min_bead_mm / outer_mm - 1.0, 0.01, 0.99); + const double add_middle_threshold = std::clamp(min_bead_mm / inner_mm, 0.01, 0.99); + + auto strategy = BeadingStrategyFactory::makeStrategy( + outer_width, inner_width, transition_length, + /*transitioning_angle*/ float(M_PI / 4.0), /*print_thin_walls*/ true, + min_bead_width, min_feature_size, + split_middle_threshold, add_middle_threshold, + max_bead_count, /*outer_wall_offset*/ 0, /*inward_distributed_center_wall_count*/ 1); + + // A wall thickness inside the 1<->2 bead transition band (inner_width < t < transition). + const coord_t thickness = scaled(0.50); + REQUIRE(thickness > inner_width); + REQUIRE(thickness < strategy->getTransitionThickness(1)); + + // When the graph requests 2 beads, the strategy must produce 2 beads — not collapse them + // into a single full-thickness (bulge) bead. + const BeadingStrategy::Beading beading = strategy->compute(thickness, 2); + REQUIRE(beading.bead_widths.size() == 2); + + // And neither bead may be over-wide: a single collapsed bead would be ~0.5mm (the full + // thickness), well above the configured wall widths. + for (const coord_t w : beading.bead_widths) + CHECK(w <= inner_width); +}