#include #include "libslic3r/Algorithm/LineSplit.hpp" #include "libslic3r/Arachne/utils/ExtrusionJunction.hpp" #include "libslic3r/Arachne/utils/ExtrusionLine.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/PerimeterGenerator.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/PrintConfig.hpp" #include "FuzzySkin.hpp" #include "libnoise/noise.h" // #define DEBUG_FUZZY using namespace Slic3r; namespace Slic3r::Feature::FuzzySkin { // Produces a random value between 0 and 1. Thread-safe. static double random_value() { thread_local std::random_device rd; // Hash thread ID for random number seed if no hardware rng seed is available thread_local std::mt19937 gen(rd.entropy() > 0 ? rd() : std::hash()(std::this_thread::get_id())); thread_local std::uniform_real_distribution dist(0.0, 1.0); return dist(gen); } class UniformNoise: public noise::module::Module { public: UniformNoise(): Module (GetSourceModuleCount ()) {}; virtual int GetSourceModuleCount() const { return 0; } virtual double GetValue(double x, double y, double z) const { return random_value() * 2 - 1; } }; static std::unique_ptr get_noise_module(const FuzzySkinConfig& cfg) { if (cfg.noise_type == NoiseType::Perlin) { auto perlin_noise = noise::module::Perlin(); perlin_noise.SetFrequency(1 / cfg.noise_scale); perlin_noise.SetOctaveCount(cfg.noise_octaves); perlin_noise.SetPersistence(cfg.noise_persistence); return std::make_unique(perlin_noise); } else if (cfg.noise_type == NoiseType::Billow) { auto billow_noise = noise::module::Billow(); billow_noise.SetFrequency(1 / cfg.noise_scale); billow_noise.SetOctaveCount(cfg.noise_octaves); billow_noise.SetPersistence(cfg.noise_persistence); return std::make_unique(billow_noise); } else if (cfg.noise_type == NoiseType::RidgedMulti) { auto ridged_multi_noise = noise::module::RidgedMulti(); ridged_multi_noise.SetFrequency(1 / cfg.noise_scale); ridged_multi_noise.SetOctaveCount(cfg.noise_octaves); return std::make_unique(ridged_multi_noise); } else if (cfg.noise_type == NoiseType::Voronoi) { auto voronoi_noise = noise::module::Voronoi(); voronoi_noise.SetFrequency(1 / cfg.noise_scale); voronoi_noise.SetDisplacement(1.0); return std::make_unique(voronoi_noise); } else { return std::make_unique(); } } // --------------------------------------------------------------------------- // Ripple noise — deterministic sine-wave displacement along the path arc length. // // Unlike the other noise types, the ripple pattern is driven by cumulative arc // length along the print path rather than world-space (x, y, z) coordinates. // This gives a uniform wave period regardless of the polygon's geometry. // // A consistent visual anchor is established by finding the leftmost Y=0 crossing // of the polygon (the point where the sine wave always peaks when phase shift is // zero), ensuring the pattern aligns across layers. // // Per-layer-group phase shifting works as follows: // period_index = floor(layer_id / layers_between_ripple_offset) // phase_shift = period_index * (ripple_offset / 100) * 2π [radians] // // Setting layers_between_ripple_offset = 1 shifts the phase on every layer; // setting it to N makes N consecutive layers share the same pattern. // --------------------------------------------------------------------------- // Compute the per-layer-group phase shift in radians. static double ripple_phase_shift_rad(const FuzzySkinConfig& cfg) { if (cfg.ripple_offset == 0.0 || cfg.layers_between_ripple_offset <= 0) return 0.0; const int effective_layer = std::max(cfg.layer_id, 0); const int period_index = effective_layer / std::max(cfg.layers_between_ripple_offset, 1); const double raw_shift = period_index * (cfg.ripple_offset/100) * (2.0 * M_PI); return fmod(raw_shift, 2.0 * M_PI); } // Find the arc-length (in mm) of the visual anchor point along the polygon perimeter. // The anchor is the leftmost Y=0 crossing, falling back to the vertex with the // smallest |y| if no crossing exists. The anchor is where sin(phase) = 1 (a peak) // when the phase shift is zero, giving a stable reference across layers. static double ripple_anchor_arc_mm(const Points& poly) { const size_t np = poly.size(); // Find anchor world position: leftmost Y=0 crossing. Vec2d anchor_world(std::numeric_limits::max(), std::numeric_limits::max()); bool found_crossing = false; for (size_t i = 0; i < np; ++i) { const double ya = unscale_(poly[i].y()); const double yb = unscale_(poly[(i + 1) % np].y()); if ((ya <= 0.0 && yb >= 0.0) || (ya >= 0.0 && yb <= 0.0)) { const double t = (std::abs(yb - ya) < 1e-9) ? 0.0 : ya / (ya - yb); const double x_cross = unscale_(poly[i].x()) + std::max(0.0, std::min(1.0, t)) * (unscale_(poly[(i + 1) % np].x()) - unscale_(poly[i].x())); if (!found_crossing || x_cross < anchor_world.x()) { anchor_world = Vec2d(x_cross, 0.0); found_crossing = true; } } } if (!found_crossing) { double best_abs_y = std::numeric_limits::max(); for (const Point& p : poly) { const double ay = std::abs(unscale_(p.y())); if (ay < best_abs_y) { best_abs_y = ay; anchor_world = Vec2d(unscale_(p.x()), unscale_(p.y())); } } } // Find the arc-length of the closest point on the polyline to anchor_world. double anchor_arc_mm = 0.0; double best_dist_sq = std::numeric_limits::max(); double accum_mm = 0.0; for (size_t i = 0; i < np; ++i) { const Vec2d pa_mm(unscale_(poly[i].x()), unscale_(poly[i].y())); const Vec2d pb_mm(unscale_(poly[(i + 1) % np].x()), unscale_(poly[(i + 1) % np].y())); const Vec2d seg = pb_mm - pa_mm; const double seg_len = seg.norm(); if (seg_len > 1e-9) { const double t = std::max(0.0, std::min(1.0, (anchor_world - pa_mm).dot(seg) / (seg_len * seg_len))); const double dist_sq = (pa_mm + seg * t - anchor_world).squaredNorm(); if (dist_sq < best_dist_sq) { best_dist_sq = dist_sq; anchor_arc_mm = accum_mm + t * seg_len; } } accum_mm += seg_len; } return anchor_arc_mm; } // Apply a sine-wave ripple displacement to a closed polygon. // Points are resampled at cfg.point_distance intervals along the perimeter. static void fuzzy_polyline_ripple(Points& poly, const FuzzySkinConfig& cfg) { const double amplitude = unscale_(cfg.thickness); const double N = static_cast(cfg.ripples_per_layer); const double fill_step_mm = unscale_(cfg.point_distance); if (N <= 0.0 || fill_step_mm < 1e-6) return; // Compute total perimeter length in mm. const size_t np = poly.size(); double perimeter_mm = 0.0; for (size_t i = 0; i < np; ++i) perimeter_mm += unscale_((poly[(i + 1) % np] - poly[i]).cast().norm()); if (perimeter_mm < 1e-6) return; const double anchor_arc_mm = ripple_anchor_arc_mm(poly); const double phase_shift_rad = ripple_phase_shift_rad(cfg); // Phase function: φ(s) = N·2π·(s - anchor_arc) / perimeter + π/2 + phase_shift // Adding π/2 ensures sin(φ) = 1 at the anchor when phase_shift = 0 (a peak). const double phase_at_anchor = M_PI * 2.0 + phase_shift_rad; auto arc_phase = [&](double arc_mm) -> double { return N * (2.0 * M_PI) * (arc_mm - anchor_arc_mm) / perimeter_mm + phase_at_anchor; }; Points out; out.reserve(static_cast(perimeter_mm / fill_step_mm) + np * 2); double accum_mm = 0.0; for (size_t i = 0; i < np; ++i) { const Point& p0 = poly[i]; const Point& p1 = poly[(i + 1) % np]; const Vec2d seg = (p1 - p0).cast(); const double seg_len = seg.norm(); if (seg_len < EPSILON) continue; const double seg_len_mm = unscale_(seg_len); const Vec2d seg_unit = seg / seg_len; const Vec2d seg_perp = perp(seg_unit); const double seg_end_mm = accum_mm + seg_len_mm; const double first_s = std::ceil(accum_mm / fill_step_mm) * fill_step_mm; for (double s = first_s; s < seg_end_mm; s += fill_step_mm) { const double t = (s - accum_mm) / seg_len_mm; const double disp = std::sin(arc_phase(s)) * amplitude; const Point pt = p0 + (seg * t).cast(); out.emplace_back(pt + (seg_perp * scale_(disp)).cast()); } accum_mm = seg_end_mm; } while (out.size() < 3) out.emplace_back(poly[poly.size() - 2]); if (out.size() >= 3) poly = std::move(out); } // Apply a sine-wave ripple displacement to an Arachne extrusion line. // Mirrors fuzzy_polyline_ripple but operates on ExtrusionJunction vectors so // that per-point line width (j.w) is preserved correctly. static void fuzzy_extrusion_line_ripple(Arachne::ExtrusionJunctions& ext_lines, const FuzzySkinConfig& cfg) { const double amplitude = unscale_(cfg.thickness); const double N = static_cast(cfg.ripples_per_layer); const double fill_step_mm = unscale_(cfg.point_distance); if (N <= 0.0 || fill_step_mm < 1e-6) return; // Build a Points vector for perimeter/anchor calculations. Points poly; poly.reserve(ext_lines.size()); for (const auto& j : ext_lines) poly.push_back(j.p); // Compute total length in mm. const size_t np = poly.size(); double perimeter_mm = 0.0; for (size_t i = 0; i + 1 < np; ++i) perimeter_mm += unscale_((poly[i + 1] - poly[i]).cast().norm()); if (perimeter_mm < 1e-6) return; const double anchor_arc_mm = ripple_anchor_arc_mm(poly); const double phase_shift_rad = ripple_phase_shift_rad(cfg); const double phase_at_anchor = M_PI * 2.0 + phase_shift_rad; auto arc_phase = [&](double arc_mm) -> double { return N * (2.0 * M_PI) * (arc_mm - anchor_arc_mm) / perimeter_mm + phase_at_anchor; }; Arachne::ExtrusionJunctions out; out.reserve(static_cast(perimeter_mm / fill_step_mm) + np * 2); double accum_mm = 0.0; for (size_t i = 0; i + 1 < np; ++i) { const Arachne::ExtrusionJunction& j0 = ext_lines[i]; const Arachne::ExtrusionJunction& j1 = ext_lines[i + 1]; const Vec2d seg = (j1.p - j0.p).cast(); const double seg_len = seg.norm(); if (seg_len < EPSILON) continue; const double seg_len_mm = unscale_(seg_len); const Vec2d seg_unit = seg / seg_len; const Vec2d seg_perp = perp(seg_unit); const double seg_end_mm = accum_mm + seg_len_mm; const double first_s = std::ceil(accum_mm / fill_step_mm) * fill_step_mm; for (double s = first_s; s < seg_end_mm; s += fill_step_mm) { const double t = (s - accum_mm) / seg_len_mm; const double disp = std::sin(arc_phase(s)) * amplitude; const Point pt = j0.p + (seg * t).cast(); out.emplace_back(pt + (seg_perp * scale_(disp)).cast(), j1.w, j1.perimeter_index); } accum_mm = seg_end_mm; } while (out.size() < 3) { size_t point_idx = ext_lines.size() - 2; out.emplace_back(ext_lines[point_idx].p, ext_lines[point_idx].w, ext_lines[point_idx].perimeter_index); if (point_idx == 0) break; --point_idx; } if (out.size() >= 3) ext_lines = std::move(out); } // Thanks Cura developers for this function. void fuzzy_polyline(Points& poly, bool closed, coordf_t slice_z, const FuzzySkinConfig& cfg) { if (cfg.noise_type == NoiseType::Ripple) { if (poly.size() < 3) return; fuzzy_polyline_ripple(poly, cfg); return; } std::unique_ptr noise = get_noise_module(cfg); 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.; 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 Point* p0 = &poly.back(); Points out; out.reserve(poly.size()); for (Point &p1 : poly) { if (!closed) { // Skip the first point for open path closed = true; p0 = &p1; continue; } // 'a' is the (next) new point between p0 and p1 Vec2d p0p1 = (p1 - *p0).cast(); double p0p1_size = p0p1.norm(); double p0pa_dist = dist_left_over; for (; p0pa_dist < p0p1_size; p0pa_dist += min_dist_between_points + random_value() * range_random_point_dist) { Point pa = *p0 + (p0p1 * (p0pa_dist / p0p1_size)).cast(); double r = noise->GetValue(unscale_(pa.x()), unscale_(pa.y()), slice_z) * cfg.thickness; out.emplace_back(pa + (perp(p0p1).cast().normalized() * r).cast()); } dist_left_over = p0pa_dist - p0p1_size; p0 = &p1; } while (out.size() < 3) { size_t point_idx = poly.size() - 2; out.emplace_back(poly[point_idx]); if (point_idx == 0) break; -- point_idx; } if (out.size() >= 3) poly = std::move(out); } // Thanks Cura developers for this function. void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, const FuzzySkinConfig& cfg, bool closed) { if (cfg.noise_type == NoiseType::Ripple) { if (ext_lines.size() < 3) return; fuzzy_extrusion_line_ripple(ext_lines, cfg); return; } std::unique_ptr noise = get_noise_module(cfg); 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) 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(); Arachne::ExtrusionJunctions out; out.reserve(ext_lines.size()); for (auto& p1 : ext_lines) { // Orca: only skip the first point for closed path, open path should not skip any point if (closed) { if (p0->p == p1.p) { // Connect endpoints. out.emplace_back(p1.p, p1.w, p1.perimeter_index); continue; } } // 'a' is the (next) new point between p0 and p1 Vec2d p0p1 = (p1.p - p0->p).cast(); double p0p1_size = p0p1.norm(); double p0pa_dist = dist_left_over; for (; p0pa_dist < p0p1_size; p0pa_dist += min_dist_between_points + random_value() * range_random_point_dist) { Point pa = p0->p + (p0p1 * (p0pa_dist / p0p1_size)).cast(); double r = noise->GetValue(unscale_(pa.x()), unscale_(pa.y()), slice_z) * cfg.thickness; switch (cfg.mode) { //the curly code for testing case FuzzySkinMode::Displacement : out.emplace_back(pa + (perp(p0p1).cast().normalized() * r).cast(), p1.w, p1.perimeter_index); break; case FuzzySkinMode::Extrusion : out.emplace_back(pa, std::max(p1.w + r + min_extrusion_width, min_extrusion_width), p1.perimeter_index); break; case FuzzySkinMode::Combined : double rad = std::max(p1.w + r + min_extrusion_width, min_extrusion_width); out.emplace_back(pa + (perp(p0p1).cast().normalized() * ((rad - p1.w) / 2)).cast(), rad, p1.perimeter_index); //0.05 - minimum width of extruded line break; } } dist_left_over = p0pa_dist - p0p1_size; p0 = &p1; } while (out.size() < 3) { size_t point_idx = ext_lines.size() - 2; out.emplace_back(ext_lines[point_idx].p, ext_lines[point_idx].w, ext_lines[point_idx].perimeter_index); if (point_idx == 0) break; --point_idx; } if (ext_lines.back().p == ext_lines.front().p) { // Connect endpoints. out.front().p = out.back().p; out.front().w = out.back().w; } if (out.size() >= 3) ext_lines = std::move(out); } void group_region_by_fuzzify(PerimeterGenerator& g) { g.regions_by_fuzzify.clear(); g.has_fuzzy_skin = false; g.has_fuzzy_hole = false; struct ConfigSurfaces { FuzzySkinConfig config; SurfacesPtr surfaces; }; std::vector regions; regions.reserve(g.compatible_regions->size()); for (auto region : *g.compatible_regions) { const auto& region_config = region->region().config(); const FuzzySkinConfig cfg{region_config.fuzzy_skin, scaled(region_config.fuzzy_skin_thickness.value), scaled(region_config.fuzzy_skin_point_distance.value), region_config.fuzzy_skin_first_layer, region_config.fuzzy_skin_noise_type, region_config.fuzzy_skin_scale, region_config.fuzzy_skin_octaves, region_config.fuzzy_skin_persistence, region_config.fuzzy_skin_mode, region_config.fuzzy_skin_ripples_per_layer, region_config.fuzzy_skin_ripple_offset, region_config.fuzzy_skin_layers_between_ripple_offset, g.layer_id}; auto it = std::find_if(regions.begin(), regions.end(), [&cfg](const ConfigSurfaces& item) { return item.config == cfg; }); if (it == regions.end()) { regions.push_back({cfg, {}}); it = regions.end() - 1; } auto& surfaces = it->surfaces; for (const auto& surface : region->slices.surfaces) { surfaces.push_back(&surface); } if (should_fuzzify(cfg, g.layer_id, 0, true)) { g.has_fuzzy_skin = true; } if (should_fuzzify(cfg, g.layer_id, 0, false)) { g.has_fuzzy_hole = true; } } if (regions.size() == 1) { // optimization g.regions_by_fuzzify.push_back({regions.front().config, {}}); return; } g.regions_by_fuzzify.reserve(regions.size()); for (const auto& region : regions) { g.regions_by_fuzzify.push_back({region.config, offset_ex(region.surfaces, ClipperSafetyOffset)}); } } bool should_fuzzify(const FuzzySkinConfig& config, const int layer_id, const size_t loop_idx, const bool is_contour) { const auto fuzziy_type = config.type; if (fuzziy_type == FuzzySkinType::None|| fuzziy_type == FuzzySkinType::Disabled_fuzzy) { return false; } if (!config.fuzzy_first_layer && layer_id <= 0) { // Do not fuzzy first layer unless told to return false; } const bool fuzzify_contours = (loop_idx == 0 && fuzziy_type != FuzzySkinType::Hole) || fuzziy_type == FuzzySkinType::AllWalls; const bool fuzzify_holes = (fuzziy_type == FuzzySkinType::Hole || fuzziy_type == FuzzySkinType::All || fuzziy_type == FuzzySkinType::AllWalls) && (loop_idx == 0 || fuzziy_type == FuzzySkinType::AllWalls); return is_contour ? fuzzify_contours : fuzzify_holes; } struct MergedFuzzyRegion { const FuzzySkinConfig *config; ExPolygons expolygons; }; // Compare whether two configs produce the same fuzzy effect (ignoring type/first_layer // which only control which loops get fuzzified, not the noise itself). static bool same_fuzzy_effect(const FuzzySkinConfig& a, const FuzzySkinConfig& b) { return a.thickness == b.thickness && a.point_distance == b.point_distance && a.noise_type == b.noise_type && a.noise_scale == b.noise_scale && a.noise_octaves == b.noise_octaves && a.noise_persistence == b.noise_persistence && a.mode == b.mode && a.ripples_per_layer == b.ripples_per_layer && a.ripple_offset == b.ripple_offset && a.layers_between_ripple_offset == b.layers_between_ripple_offset; } static std::vector collect_merged_fuzzy_regions(const std::vector>& regions, const int layer_id, const size_t loop_idx, const bool is_contour) { // 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 // with the same noise parameters, merging their ExPolygons avoids splitting the // perimeter at the painted boundary — eliminating discontinuity artifacts. std::vector merged_regions; merged_regions.reserve(regions.size()); for (const auto& region : regions) { if (!should_fuzzify(region.first, layer_id, loop_idx, is_contour)) { continue; } bool merged = false; for (auto& merged_region : merged_regions) { if (same_fuzzy_effect(*merged_region.config, region.first)) { if (merged_region.expolygons.empty()) { // Already full coverage, nothing to add. } else if (region.second.empty()) { merged_region.expolygons.clear(); } else { append(merged_region.expolygons, region.second); } merged = true; break; } } if (!merged) { merged_regions.push_back({®ion.first, region.second}); } } for (auto& merged_region : merged_regions) { if (!merged_region.expolygons.empty()) { merged_region.expolygons = union_ex(merged_region.expolygons); } } return merged_regions; } 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& config = regions.begin()->first; const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, loop_idx, is_contour); if (!fuzzify) { return polygon; } fuzzified = polygon; fuzzy_polyline(fuzzified.points, true, slice_z, config); return fuzzified; } // 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 // with the same noise parameters, merging their ExPolygons avoids splitting the // perimeter at the painted boundary — eliminating discontinuity artifacts. auto merged_regions = collect_merged_fuzzy_regions(regions, perimeter_generator.layer_id, loop_idx, is_contour); if (merged_regions.empty()) { return polygon; } // Fast path: single merged region — apply directly without splitting if (merged_regions.size() == 1) { const auto& mr = merged_regions.front(); if (mr.expolygons.empty()) { fuzzified = polygon; fuzzy_polyline(fuzzified.points, true, slice_z, *mr.config); return fuzzified; } // Fall through to split_line with a single region below } #ifdef DEBUG_FUZZY { int i = 0; for (const auto& r : merged_regions) { BoundingBox bbox = get_extents(perimeter_generator.slices->surfaces); bbox.offset(scale_(1.)); ::Slic3r::SVG svg(debug_out_path("fuzzy_traverse_loops_%d_%d_%d_region_%d.svg", perimeter_generator.layer_id, is_contour ? 0 : 1, loop_idx, i) .c_str(), bbox); svg.draw_outline(perimeter_generator.slices->surfaces); svg.draw_outline(polygon, "green"); svg.draw(r.expolygons, "red", 0.5); svg.draw_outline(r.expolygons, "red"); svg.Close(); i++; } } #endif // Make each region's ExPolygons exclusive so overlapping regions don't double-fuzz // the same perimeter section. Later regions in the list take priority over earlier ones // in overlapping areas (matching modifier precedence order). for (size_t i = 0; i < merged_regions.size(); ++i) for (size_t j = i + 1; j < merged_regions.size(); ++j) 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); // Split the loops into lines with different config, and fuzzy them separately fuzzified = polygon; for (const auto& r : merged_regions) { auto splitted = Algorithm::split_line(fuzzified, r.expolygons, true); if (splitted.empty()) { // No intersection, skip continue; } // Fuzzy splitted polygon if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) { // The entire polygon is fuzzified fuzzy_polyline(fuzzified.points, true, slice_z, *r.config); } else { // Start from a non-clipped junction so wrapped clipped segments do // not need an artificial reconnection across the seam. const auto first_non_clipped = std::find_if(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return !j.clipped; }); if (first_non_clipped != splitted.begin()) { std::rotate(splitted.begin(), first_non_clipped, splitted.end()); } Points segment; segment.reserve(splitted.size()); fuzzified.points.clear(); const auto fuzzy_current_segment = [&segment, &fuzzified, &r, slice_z]() { fuzzified.points.push_back(segment.front()); const auto back = segment.back(); fuzzy_polyline(segment, false, slice_z, *r.config); fuzzified.points.insert(fuzzified.points.end(), segment.begin(), segment.end()); fuzzified.points.push_back(back); segment.clear(); }; for (const auto& p : splitted) { if (p.clipped) { segment.push_back(p.p); } else { if (segment.empty()) { fuzzified.points.push_back(p.p); } else { segment.push_back(p.p); fuzzy_current_segment(); } } } if (!segment.empty()) { // Close the loop segment.push_back(splitted.front().p); fuzzy_current_segment(); } } } return fuzzified; } 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& 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); } 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 // with the same noise parameters, merging avoids splitting the perimeter at the // painted boundary — eliminating discontinuity artifacts. auto merged_regions = collect_merged_fuzzy_regions(regions, perimeter_generator.layer_id, extrusion->inset_idx, is_contour); if (!merged_regions.empty()) { // 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); return; } // Open path means this is a thin wall that collapsed into a single thick line, in this case the path will go exactly // between the middle two sides of the object. And since the paint segmentation never goes beyond the middle line because // it uses voronoi diagram, we need to expand the segmentation a little bit to make sure it covers the path. if (!closed) { for (auto& r : merged_regions) { r.expolygons = offset_ex(r.expolygons, perimeter_generator.ext_perimeter_flow.scaled_width() / 10); } } #ifdef DEBUG_FUZZY { int i = 0; for (const auto& r : merged_regions) { BoundingBox bbox = get_extents(perimeter_generator.slices->surfaces); bbox.offset(scale_(1.)); ::Slic3r::SVG svg(debug_out_path("fuzzy_traverse_loops_%d_%d_%d_region_%d.svg", perimeter_generator.layer_id, is_contour ? 0 : 1, extrusion->inset_idx, i) .c_str(), bbox); // Convert extrusion line to polygon for visualization Polygon extrusion_polygon; extrusion_polygon.points.reserve(extrusion->junctions.size()); for (const auto& junction : extrusion->junctions) { extrusion_polygon.points.push_back(junction.p); } svg.draw_outline(perimeter_generator.slices->surfaces); svg.draw_outline(extrusion_polygon, "green"); svg.draw(r.expolygons, "red", 0.5); svg.draw_outline(r.expolygons, "red"); svg.Close(); i++; } } #endif // Make each region's ExPolygons exclusive so overlapping regions don't double-fuzz // the same perimeter section. Later regions in the list take priority over earlier ones // in overlapping areas. for (size_t i = 0; i < merged_regions.size(); ++i) for (size_t j = i + 1; j < merged_regions.size(); ++j) 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); // 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); if (splitted.empty()) { // No intersection, skip continue; } // 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); continue; } else { const auto current_ext = extrusion->junctions; std::vector segment; segment.reserve(current_ext.size()); extrusion->junctions.clear(); const auto fuzzy_current_segment = [&segment, &extrusion, &r, slice_z]() { // 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); // 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); } extrusion->junctions.insert(extrusion->junctions.end(), segment.begin(), segment.end()); // Orca: only add non fuzzy point if it's not in the extrusion closing point. if (!extrusion->junctions.empty() && extrusion->junctions.back().p != front.p) { extrusion->junctions.push_back(back); } segment.clear(); }; const auto to_ex_junction = [¤t_ext](const Algorithm::SplitLineJunction& j) -> Arachne::ExtrusionJunction { Arachne::ExtrusionJunction res = current_ext[j.get_src_index()]; if (!j.is_src()) { res.p = j.p; } return res; }; for (const auto& p : splitted) { if (p.clipped) { segment.push_back(to_ex_junction(p)); } else { if (segment.empty()) { extrusion->junctions.push_back(to_ex_junction(p)); } else { segment.push_back(to_ex_junction(p)); fuzzy_current_segment(); } } } if (!segment.empty()) { fuzzy_current_segment(); } //Orca: ensure the loop is closed after fuzzy if (closed && !extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) { extrusion->junctions.back().p = extrusion->junctions.front().p; extrusion->junctions.back().w = extrusion->junctions.front().w; } } } } } } } // namespace Slic3r::Feature::FuzzySkin