From 97c7afa2af6f7aca7044d2cb0682f7f0a4039a8c Mon Sep 17 00:00:00 2001 From: GeordieTomo <58905733+GeordieTomo@users.noreply.github.com> Date: Thu, 7 May 2026 01:02:42 +1000 Subject: [PATCH] Feature/fuzzy skin ripple mode (#13471) * fuzzy skin ripple mode add fuzzy skin ripple mode, which is a uniform pattern option. fixes #13325 * remove unused wall width parameter * remove cmath import * style consistency --------- Co-authored-by: SoftFever --- src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp | 245 +++++++++++++++++- src/libslic3r/PerimeterGenerator.hpp | 13 +- src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 42 ++- src/libslic3r/PrintConfig.hpp | 4 + src/slic3r/GUI/ConfigManipulation.cpp | 10 +- src/slic3r/GUI/Tab.cpp | 3 + 7 files changed, 310 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp index 812f9f9d14..e246476dcc 100644 --- a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp +++ b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp @@ -66,9 +66,240 @@ static std::unique_ptr get_noise_module(const FuzzySkinCo } } +// --------------------------------------------------------------------------- +// 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 * 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 * (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 @@ -113,6 +344,14 @@ 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) { + + 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 @@ -190,7 +429,11 @@ void group_region_by_fuzzify(PerimeterGenerator& g) region_config.fuzzy_skin_scale, region_config.fuzzy_skin_octaves, region_config.fuzzy_skin_persistence, - region_config.fuzzy_skin_mode}; + 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& surfaces = regions[cfg]; for (const auto& surface : region->slices.surfaces) { surfaces.push_back(&surface); diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 74be04294e..08ba854d06 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -21,6 +21,10 @@ struct FuzzySkinConfig int noise_octaves; double noise_persistence; FuzzySkinMode mode; + int ripples_per_layer; + double ripple_offset; + int layers_between_ripple_offset; + int layer_id; bool operator==(const FuzzySkinConfig& r) const { @@ -32,7 +36,10 @@ struct FuzzySkinConfig && noise_scale == r.noise_scale && noise_octaves == r.noise_octaves && noise_persistence == r.noise_persistence - && mode == r.mode; + && mode == r.mode + && ripples_per_layer == r.ripples_per_layer + && ripple_offset == r.ripple_offset + && layers_between_ripple_offset == r.layers_between_ripple_offset; } bool operator!=(const FuzzySkinConfig& r) const { return !(*this == r); } @@ -52,6 +59,10 @@ template<> struct hash boost::hash_combine(seed, std::hash{}(c.noise_scale)); boost::hash_combine(seed, std::hash{}(c.noise_octaves)); boost::hash_combine(seed, std::hash{}(c.noise_persistence)); + boost::hash_combine(seed, std::hash{}(c.mode)); + boost::hash_combine(seed, std::hash{}(c.ripples_per_layer)); + boost::hash_combine(seed, std::hash{}(c.ripple_offset)); + boost::hash_combine(seed, std::hash{}(c.layers_between_ripple_offset)); return seed; } }; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 35d780d99b..b5b5dba734 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1043,7 +1043,7 @@ static std::vector s_Preset_print_options{ "support_ironing_flow", "support_ironing_spacing", "max_travel_detour_distance", - "fuzzy_skin", "fuzzy_skin_thickness", "fuzzy_skin_point_distance", "fuzzy_skin_first_layer", "fuzzy_skin_noise_type", "fuzzy_skin_mode", "fuzzy_skin_scale", "fuzzy_skin_octaves", "fuzzy_skin_persistence", + "fuzzy_skin", "fuzzy_skin_thickness", "fuzzy_skin_point_distance", "fuzzy_skin_first_layer", "fuzzy_skin_noise_type", "fuzzy_skin_mode", "fuzzy_skin_scale", "fuzzy_skin_octaves", "fuzzy_skin_persistence", "fuzzy_skin_ripples_per_layer", "fuzzy_skin_ripple_offset", "fuzzy_skin_layers_between_ripple_offset", "max_volumetric_extrusion_rate_slope", "max_volumetric_extrusion_rate_slope_segment_length","extrusion_rate_smoothing_external_perimeter_only", "inner_wall_speed", "outer_wall_speed", "sparse_infill_speed", "internal_solid_infill_speed", "top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f2aad27895..919021cbe5 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -183,7 +183,8 @@ static t_config_enum_values s_keys_map_NoiseType { { "perlin", int(NoiseType::Perlin) }, { "billow", int(NoiseType::Billow) }, { "ridgedmulti", int(NoiseType::RidgedMulti) }, - { "voronoi", int(NoiseType::Voronoi) } + { "voronoi", int(NoiseType::Voronoi) }, + { "ripple", int(NoiseType::Ripple) } }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NoiseType) @@ -3370,7 +3371,7 @@ void PrintConfigDef::init_fff_params() def->tooltip = L("The width within which to jitter. It's advised to be below outer wall line width."); def->sidetext = L("mm"); // millimeters, CIS languages need translation def->min = 0; - def->max = 1; + def->max = 2; def->mode = comSimple; def->set_default_value(new ConfigOptionFloat(0.2)); @@ -3422,18 +3423,21 @@ void PrintConfigDef::init_fff_params() "Perlin: Perlin noise, which gives a more consistent texture.\n" "Billow: Similar to perlin noise, but clumpier.\n" "Ridged Multifractal: Ridged noise with sharp, jagged features. Creates marble-like textures.\n" - "Voronoi: Divides the surface into voronoi cells, and displaces each one by a random amount. Creates a patchwork texture."); + "Voronoi: Divides the surface into voronoi cells, and displaces each one by a random amount. Creates a patchwork texture.\n" + "Ripple: Uniform ripple pattern that ripples left and right of the original path. Repeating pattern, woven appearance."); def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values.push_back("classic"); def->enum_values.push_back("perlin"); def->enum_values.push_back("billow"); def->enum_values.push_back("ridgedmulti"); def->enum_values.push_back("voronoi"); + def->enum_values.push_back("ripple"); def->enum_labels.push_back(L("Classic")); def->enum_labels.push_back(L("Perlin")); def->enum_labels.push_back(L("Billow")); def->enum_labels.push_back(L("Ridged Multifractal")); def->enum_labels.push_back(L("Voronoi")); + def->enum_labels.push_back(L("Ripple")); def->mode = comSimple; def->set_default_value(new ConfigOptionEnum(NoiseType::Classic)); @@ -3465,6 +3469,38 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.5)); + def = this->add("fuzzy_skin_ripples_per_layer", coInt); + def->label = L("Number of ripples per layer"); + def->category = L("Others"); + def->tooltip = L("When using the Ripple noise type, this controls how many full cycles of ripples will be added per layer."); + def->min = 1; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(15)); + + def = this->add("fuzzy_skin_ripple_offset", coFloat); + def->label = L("Ripple offset"); + def->category = L("Others"); + def->tooltip = L("When using the Ripple noise type, shifts the ripple pattern forward along the print path by this amount each " + "layer-period. A value of 0 keeps every layer identical. A value equal to 0.5 shifts by a full " + "half-wavelength, inverting the pattern. The shift is applied once per 'Layers between Ripple offset' layers, " + "so consecutive layers within a period are printed identically on top of each other."); + def->min = 0; + def->max = 1; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.5)); + + def = this->add("fuzzy_skin_layers_between_ripple_offset", coInt); + def->label = L("Layers between ripple offset"); + def->category = L("Others"); + def->tooltip = L("When using the Ripple noise type with a non-zero layer offset, this controls how " + "many consecutive layers share the same ripple phase before the offset is applied. " + "For example, a period of 3 means layers 0, 1 and 2 are identical, then layers 3, 4 " + "and 5 are shifted by one full 'Ripple layer offset', and so on. " + "Set to 1 to shift on every layer."); + def->min = 1; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(1)); + def = this->add("filter_out_gap_fill", coFloat); def->label = L("Filter out tiny gaps"); def->category = L("Layers and Perimeters"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 55586a8adb..da5abe595e 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -56,6 +56,7 @@ enum class NoiseType { Billow, RidgedMulti, Voronoi, + Ripple, }; enum class WipeTowerType { @@ -1084,6 +1085,9 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloat, fuzzy_skin_scale)) ((ConfigOptionInt, fuzzy_skin_octaves)) ((ConfigOptionFloat, fuzzy_skin_persistence)) + ((ConfigOptionInt, fuzzy_skin_ripples_per_layer)) + ((ConfigOptionFloat, fuzzy_skin_ripple_offset)) + ((ConfigOptionInt, fuzzy_skin_layers_between_ripple_offset)) ((ConfigOptionFloat, gap_infill_speed)) ((ConfigOptionInt, sparse_infill_filament)) ((ConfigOptionFloatOrPercent, sparse_infill_line_width)) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 238b6ac690..f3a359f560 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -881,9 +881,13 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co // Show noise type specific options with the same logic NoiseType fuzzy_skin_noise_type = config->opt_enum("fuzzy_skin_noise_type"); - toggle_line("fuzzy_skin_scale", fuzzy_skin_noise_type != NoiseType::Classic && has_fuzzy_skin); - toggle_line("fuzzy_skin_octaves", fuzzy_skin_noise_type != NoiseType::Classic && fuzzy_skin_noise_type != NoiseType::Voronoi && has_fuzzy_skin); - toggle_line("fuzzy_skin_persistence", (fuzzy_skin_noise_type == NoiseType::Perlin || fuzzy_skin_noise_type == NoiseType::Billow) && has_fuzzy_skin); + const bool is_ripple = fuzzy_skin_noise_type == NoiseType::Ripple; + toggle_line("fuzzy_skin_scale", fuzzy_skin_noise_type != NoiseType::Classic && has_fuzzy_skin && !is_ripple); + toggle_line("fuzzy_skin_octaves", fuzzy_skin_noise_type != NoiseType::Classic && fuzzy_skin_noise_type != NoiseType::Voronoi && has_fuzzy_skin && !is_ripple); + toggle_line("fuzzy_skin_persistence", (fuzzy_skin_noise_type == NoiseType::Perlin || fuzzy_skin_noise_type == NoiseType::Billow) && has_fuzzy_skin && !is_ripple); + toggle_line("fuzzy_skin_ripples_per_layer", is_ripple && has_fuzzy_skin); + toggle_line("fuzzy_skin_ripple_offset", is_ripple && has_fuzzy_skin); + toggle_line("fuzzy_skin_layers_between_ripple_offset", is_ripple && has_fuzzy_skin); bool have_arachne = config->opt_enum("wall_generator") == PerimeterGeneratorType::Arachne; for (auto el : {"wall_transition_length", "wall_transition_filter_deviation", "wall_transition_angle", "min_feature_size", "min_length_factor", diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e757fea5a6..707414c500 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2677,6 +2677,9 @@ void TabPrint::build() optgroup->append_single_option_line("fuzzy_skin_scale", "others_settings_fuzzy_skin#skin-feature-size"); optgroup->append_single_option_line("fuzzy_skin_octaves", "others_settings_fuzzy_skin#skin-noise-octaves"); optgroup->append_single_option_line("fuzzy_skin_persistence", "others_settings_fuzzy_skin#skin-noise-persistence"); + optgroup->append_single_option_line("fuzzy_skin_ripples_per_layer", "others_settings_fuzzy_skin#ripples-per-layer"); + optgroup->append_single_option_line("fuzzy_skin_ripple_offset", "others_settings_fuzzy_skin#ripple-offset"); + optgroup->append_single_option_line("fuzzy_skin_layers_between_ripple_offset", "others_settings_fuzzy_skin#layers-between-ripple-offset"); optgroup->append_single_option_line("fuzzy_skin_first_layer", "others_settings_fuzzy_skin#apply-fuzzy-skin-to-first-layer"); optgroup = page->new_optgroup(L("G-code output"), L"param_gcode");