Add Optimized Gyroid infill (auto-tuned wavelength + amplitude) (#13379)

* Add Optimized Gyroid infill (auto-tuned wavelength + amplitude)

New infill geometry derived from FillGyroid. Two parameters are
auto-computed per-region from density, line spacing, and layer height
(no user inputs):

  omega     = sqrt(density_adj) / sqrt(1 + layer_height/spacing)
              clamped to [0.5, 2.0]
              -- Euler-Bernoulli buckling: critical load ~ 1/L^2,
                 so shorter wavelength under higher load (denser infill)
                 raises buckling resistance.

  amplitude = 0.55 / omega^2, clamped to [0.20, 0.65]
              -- Curved-beam bending stress: peak stress ~ A * omega^2,
                 so amplitude is reduced as omega rises to keep peak
                 fiber stress bounded while preserving stiffness.

Files:
  - src/libslic3r/Fill/FillOptimizedGyroid.{hpp,cpp}  (new)
  - src/libslic3r/Fill/FillBase.cpp                   (factory case)
  - src/libslic3r/Fill/Fill.cpp                       (switch case)
  - src/libslic3r/Layer.cpp                           (switch case)
  - src/libslic3r/PrintConfig.{hpp,cpp}               (enum + label)
  - src/libslic3r/CMakeLists.txt                      (build sources)

User-facing: appears as "Optimized Gyroid" in the Fill Pattern dropdown.
Density still chosen by user; omega/amplitude are internal.

* Fix build: layer_height is in FillParams, not Fill base

* Add ipOptimizedGyroid to multiline infill list in ConfigManipulation

* Refactor: replace ipOptimizedGyroid enum with gyroid_optimized boolean

Per @RF47's review feedback, fold the optimized wave math into FillGyroid
itself behind a per-region boolean instead of a separate infill enum.

What changes:
  - New ConfigOptionBool "gyroid_optimized" on PrintRegionConfig (default
    false). When unchecked, gyroid behavior is byte-identical to before.
  - Optimized wave math (compute_omega_factor, compute_amplitude_factor,
    f_opt, make_*_opt, make_optimized_gyroid_waves) lives inside
    FillGyroid.cpp. _fill_surface_single branches on params.gyroid_optimized.
  - FillParams gains a bool gyroid_optimized field, populated in Fill.cpp
    from region_config alongside fill_multiline.
  - UI checkbox added under Strength > Infill in Tab.cpp, label
    "Optimize gyroid wave (experimental)". Toggle is hidden by
    ConfigManipulation when sparse_infill_pattern != ipGyroid.
  - "gyroid_optimized" added to s_Preset_print_options for preset I/O.

What goes away:
  - ipOptimizedGyroid enum value, factory case, switch cases, dropdown
    label, string key.
  - FillOptimizedGyroid.cpp / FillOptimizedGyroid.hpp (math moved into
    FillGyroid.cpp).
  - Net diff drops by ~250 lines.

Existing profiles using gyroid are unaffected.

* Wire gyroid_optimized through SurfaceFillParams to FillParams

Linux build failed because line 921 in Fill.cpp populates a
SurfaceFillParams (the dedup struct), not FillParams directly.
Add the field there, in operator< / operator==, and copy it to
FillParams at both conversion sites.

* Use toggle_line for gyroid_optimized: hide row when pattern != gyroid

* Account for multiline wall thickness in omega correction (per @RF47)

When fill_multiline = N, each gyroid wall is N lines thick, so the
geometric scale fed into the buckling correction term should be
spacing * N rather than spacing. Increases omega (tighter wavelength)
when multiline is enabled, consistent with the thicker wall being
more buckling-resistant.

* Optimized gyroid via marching squares on the implicit scalar field

Per @RF47 review: replace the analytical f_opt / make_one_period_opt
wave generator (which had visible kinks at vertical-horizontal
transitions) with a marching-squares iso-extraction on the gyroid
scalar field, modeled on FillTpmsFK.cpp.

  - New marchsq::GyroidField in FillGyroid.cpp evaluates
    F(x,y,z) = sin(fx*x)cos(fy*y) + sin(fy*y)cos(fz*z) + sin(fz*z)cos(fx*x)
    where fx = omega * baseline (anisotropic in x), fy = fz = baseline.
  - get_gyroid_polylines() runs marching squares at iso=0 and converts
    rings to polylines.
  - _fill_surface_single() optimized branch now builds GyroidField,
    runs marching squares, and skips the bb.min translate (field
    output is already in absolute coords).
  - Dropped: f_opt, make_one_period_opt, make_wave_opt,
    make_optimized_gyroid_waves, compute_amplitude_factor. Amplitude
    has no clean analog in iso-zero extraction.
  - Standard (non-optimized) gyroid path unchanged.

* Mass calibration: compensate period by cbrt(omega) for x-anisotropic field

Per @RF47: optimized vs standard gyroid had different masses at the
same sparse_infill_density setting. Cause: scaling fx by omega while
leaving fy=fz at the baseline raised the surface-area-to-volume ratio
by approximately omega^(1/3) (the geometric mean of the three
frequencies).

Fix: multiply the base period by cbrt(omega) so the geometric mean of
(fx, fy, fz) returns to the standard baseline. Net effect:
  fx = omega^(2/3) * baseline_orig
  fy = fz = omega^(-1/3) * baseline_orig
which preserves total mass at the same density setting while
preserving the load-direction anisotropy this PR introduces.

* Switch optimized gyroid anisotropy from X to Z (per @RF47)

Z is the typical compression-load axis for FFF parts and is not at
delamination risk under compression — so the dominant failure mode
is column buckling of the vertical strands themselves. Tightening
fz directly shortens the effective vertical strand length, which
improves Z-axis buckling resistance.

Mass calibration via cbrt(omega) period compensation still applies
(scaling exactly one of three frequencies by omega; the geometric-
mean preservation argument is symmetric across axes).

* Update src/slic3r/GUI/Tab.cpp

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>

* Address review feedback (Copilot + @RF47)

- Fill.cpp: gate params.gyroid_optimized on (params.pattern == ipGyroid)
  so non-gyroid surfaces don't differ in SurfaceFillParams by an
  irrelevant flag (would unnecessarily split fill batching).
  [Copilot suggestion, RF47 confirmed correct]
- PrintConfig.cpp: drop "amplitude" from the tooltip; only wavelength
  is parameterized (the marching-squares iso=0 extraction is invariant
  to a uniform field scale, so amplitude has no effect).
- FillBase.hpp: shorten gyroid_optimized comment to match the actual
  carried state (no amplitude term).
- FillGyroid.cpp: shorten the marchsq namespace comment block; the
  ODR concern was overstated (FillTpmsFK uses the same pattern fine).

* Drop redundant marchsq bb expansion (Copilot)

bb is already offset by 10 * scale_(spacing) above for edge-artifact
margin; the second offset on bb_field doubled the raster area for no
geometric benefit and hurt CPU time on large parts.

* Update src/slic3r/GUI/Tab.cpp

Co-authored-by: Ian Bassi <ian.bassi@outlook.com>

* Fix density mismatch + rename to Z-buckling bias optimization

Issue (per @ianalexis): at the same sparse_infill_density setting,
the optimized branch produced denser fill than standard. Verified via
Python sim (sim_gyroid_compare.py) using marching squares on the
implicit field across multiple z slices.

Root cause: the omega formula was inverted from the buckling-physics
intent. The naive sqrt(density_adj) factor produced omega < 1 at
typical print densities (10-30%), which LENGTHENED the Z wavelength
instead of shortening it -- net loss in both mass and strength.

Fix:
  - compute_omega_factor: invert to sqrt(1 / density_adj), clamp to
    [1.0, 2.0]. Now omega = 2.0 at low density (long strands need
    most help) and clamps to 1.0 above ~30% density (no-op, since
    standard gyroid is already short enough).
  - Remove the cbrt(omega) period compensation. Empirically (sim
    table embedded in FillGyroid.cpp comment) the inverted formula
    keeps line length per area at ~1.000 of standard across all
    densities with no period scaling needed.

Predicted gains (sim, Z-axis Euler buckling proxy):
  density   line/std   strength/std
    10%      1.000        2.84x
    15%      1.000        1.89x
    20%      1.000        1.42x
    30%+     1.000        1.00x  (no-op)

Rename per @ianalexis: "Optimize gyroid wave" oversells (now no-op
above 30% density and Z-only). Renamed user-facing label to
"Z-buckling bias optimization (experimental)" with updated tooltip
that scopes to vertical compression and discloses the density cutoff.
Internal config key (gyroid_optimized) unchanged for diff size.

Real-world Instron compression tests at Brown's Prince Lab to follow.

---------

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
Peyton Marcotte
2026-05-20 11:42:32 -04:00
committed by GitHub
parent 4c481cef18
commit 34209a7cd7
8 changed files with 208 additions and 10 deletions

View File

@@ -271,6 +271,9 @@ struct SurfaceFillParams
// Params for Lateral honeycomb
float infill_overhang_angle = 60.f;
// For Gyroid: when true, use the parameterized "optimized" wave.
bool gyroid_optimized = false;
bool operator<(const SurfaceFillParams &rhs) const {
#define RETURN_COMPARE_NON_EQUAL(KEY) if (this->KEY < rhs.KEY) return true; if (this->KEY > rhs.KEY) return false;
#define RETURN_COMPARE_NON_EQUAL_TYPED(TYPE, KEY) if (TYPE(this->KEY) < TYPE(rhs.KEY)) return true; if (TYPE(this->KEY) > TYPE(rhs.KEY)) return false;
@@ -303,6 +306,7 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(symmetric_infill_y_axis);
RETURN_COMPARE_NON_EQUAL(infill_lock_depth);
RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
return false;
}
@@ -330,7 +334,8 @@ struct SurfaceFillParams
this->lateral_lattice_angle_2 == rhs.lateral_lattice_angle_2 &&
this->infill_lock_depth == rhs.infill_lock_depth &&
this->skin_infill_depth == rhs.skin_infill_depth &&
this->infill_overhang_angle == rhs.infill_overhang_angle;
this->infill_overhang_angle == rhs.infill_overhang_angle &&
this->gyroid_optimized == rhs.gyroid_optimized;
}
};
@@ -920,6 +925,12 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
// Orca: apply fill multiline only for sparse infill
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
// (which would unnecessarily split fill batching).
// Stored on SurfaceFillParams; copied to FillParams during conversion.
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized;
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
region_config.sparse_infill_rotate_template.value);
@@ -1273,6 +1284,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.lateral_lattice_angle_1 = surface_fill.params.lateral_lattice_angle_1;
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
// BBS
params.flow = surface_fill.params.flow;
@@ -1468,6 +1480,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
for (ExPolygon &expoly : surface_fill.expolygons) {
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.

View File

@@ -79,6 +79,9 @@ struct FillParams
// Layer height for Concentric infill with Arachne.
coordf_t layer_height { 0.f };
// For Gyroid: when true, use the parameterized "optimized" variant.
bool gyroid_optimized { false };
// For Lateral lattice
coordf_t lateral_lattice_angle_1 { 0.f };
coordf_t lateral_lattice_angle_2 { 0.f };

View File

@@ -1,4 +1,5 @@
#include "../ClipperUtils.hpp"
#include "../MarchingSquares.hpp"
#include "../ShortestPath.hpp"
#include "../Surface.hpp"
#include <cmath>
@@ -7,6 +8,99 @@
#include "FillBase.hpp"
#include "FillGyroid.hpp"
// ---------------------------------------------------------------------------
// Marching-squares scalar field for the optimized gyroid branch.
// Modeled after FillTpmsFK.cpp's ScalarField.
//
// The gyroid scalar field is the standard implicit equation
// F(x,y,z) = sin(fx*x)cos(fy*y) + sin(fy*y)cos(fz*z) + sin(fz*z)cos(fx*x)
// Marching squares extracts the iso-zero contour, which gives smoother
// transitions between vertical and horizontal regimes than the analytical
// asin-based wave generator. Setting fz = omega * baseline anisotropically
// tightens the wave along the layer-stacking axis, shortening the effective
// vertical strand length and improving column-buckling resistance under
// Z-axis compression.
// ---------------------------------------------------------------------------
namespace marchsq {
using namespace Slic3r;
using coordr_t = long;
using Pointf = Vec2d;
struct GyroidField
{
static constexpr float gsizef = 0.40f;
static constexpr float rsizef = 0.004f;
const coord_t rsize = scaled(rsizef);
const coordr_t gsize = std::round(gsizef / rsizef);
Point size;
Point offs;
coordf_t z;
float fx;
float fy;
float fz;
float isoval = 0.0f;
explicit GyroidField(const BoundingBox bb, const coordf_t z, const float period, const float omega = 1.0f)
: size{bb.size()}, offs{bb.min}, z{z}
{
const float baseline = float(2.0 * PI) / std::max(period, 1e-3f);
fx = baseline;
fy = baseline;
fz = omega * baseline;
}
float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const
{
const float a = fx * float(x);
const float b = fy * float(y);
const float c = fz * float(z_arg);
return std::sin(a) * std::cos(b) + std::sin(b) * std::cos(c) + std::sin(c) * std::cos(a);
}
float get_scalar(Coord p) const
{
Pointf pf = to_Pointf(p);
return get_scalar(pf.x(), pf.y(), z);
}
inline coord_t to_coord (const coordr_t& x) const { return x * rsize; }
inline coordr_t to_coordr(const coord_t& x) const { return x / rsize; }
inline Point to_Point (const Coord& p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
inline Coord to_Coord (const Point& p) const { return Coord(to_coordr(p.y() - offs.y()), to_coordr(p.x() - offs.x())); }
inline Pointf to_Pointf(const Point& p) const { return Pointf(unscaled(p.x()), unscaled(p.y())); }
inline Pointf to_Pointf(const Coord& p) const { return to_Pointf(to_Point(p)); }
};
template<> struct _RasterTraits<GyroidField>
{
using ValueType = float;
static float get (const GyroidField& sf, size_t row, size_t col) { return sf.get_scalar(Coord(row, col)); }
static size_t rows(const GyroidField& sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const GyroidField& sf) { return sf.to_coordr(sf.size.x()); }
};
inline Polylines get_gyroid_polylines(const GyroidField& sf, const double tolerance = SCALED_EPSILON)
{
std::vector<Ring> rings = execute_with_policy(ex_tbb, sf, sf.isoval, {sf.gsize, sf.gsize});
Polylines polys;
polys.reserve(rings.size());
for (const Ring& ring : rings) {
Polyline poly;
Points& pts = poly.points;
pts.reserve(ring.size() + 1);
for (const Coord& crd : ring)
pts.emplace_back(sf.to_Point(crd));
pts.push_back(pts.front());
if (tolerance >= 0.0)
poly.simplify(tolerance);
polys.emplace_back(poly);
}
return polys;
}
} // namespace marchsq
namespace Slic3r {
static inline double f(double x, double z_sin, double z_cos, bool vertical, bool flip)
@@ -102,6 +196,49 @@ static std::vector<Vec2d> make_one_period(double width, double scaleFactor, doub
return points;
}
// ---------------------------------------------------------------------------
// "Optimized" gyroid wave: marching-squares variant gated on
// params.gyroid_optimized. The wave shape is extracted from the gyroid
// implicit scalar field (see marchsq::GyroidField above) at iso=0, with
// the Z dimension's spatial frequency multiplied by an Euler-Bernoulli
// buckling-derived factor so the vertical strands become shorter columns,
// raising the critical buckling load against Z-axis compression.
//
// The formula is INVERTED from a naive "scale with density" derivation:
// at LOW density the gyroid strands are long and slender (prime buckling
// targets), so they need the most shortening; at high density the strands
// are already short and need little extra help. omega is therefore the
// inverse-square-root of density_adjusted:
//
// omega = sqrt(1 / density_adj) / sqrt(1 + layer_h/spacing),
// clamped [1.0, 2.0]
//
// fx and fy are left at the baseline frequency, so the per-XY-slice line
// length per unit area is preserved -> mass at the same `sparse_infill_density`
// setting matches the standard gyroid path. Strength gain comes purely from
// the shorter vertical column length (P_cr proportional to 1/L^2).
//
// Empirical Python sim (sim_gyroid_compare.py) at layer_h=0.20, spacing=0.45:
//
// density omega line/std strength/std strength_per_mass
// 10% 2.00 1.00 2.84 2.84
// 15% 1.38 1.00 1.89 1.89
// 20% 1.19 1.00 1.42 1.42
// 30% 1.00 1.00 1.00 1.00
// 50%+ 1.00 1.00 1.00 1.00
//
// When gyroid_optimized is false, behavior is byte-identical to the
// standard parametric gyroid path below.
// ---------------------------------------------------------------------------
static inline double compute_omega_factor(double density_adjusted, double line_spacing, double layer_height)
{
double lh_ratio = (line_spacing > 0.) ? layer_height / line_spacing : 0.5;
double correction = 1.0 / std::sqrt(1.0 + lh_ratio);
double raw = std::sqrt(1.0 / std::max(density_adjusted, 0.1)) * correction;
return std::clamp(raw, 1.0, 2.0);
}
static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
{
const double scaleFactor = scale_(line_spacing) / density_adjusted;
@@ -173,16 +310,41 @@ void FillGyroid::_fill_surface_single(
bb.offset(expand);
// generate pattern
Polylines polylines = make_gyroid_waves(
scale_(this->z),
density_adjusted,
this->spacing,
ceil(bb.size()(0) / distance) + 1.,
ceil(bb.size()(1) / distance) + 1.);
Polylines polylines;
if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
// omega >= 1 always, so fz >= baseline -> shorter vertical wavelength ->
// shorter effective column length -> higher buckling resistance.
//
// Mass: fx and fy are left at baseline (same as standard), so the
// per-XY-slice line length per unit area is approximately preserved.
// Empirically (sim_gyroid_compare.py) the optimized line/std ratio is
// ~1.000 across densities, so no period compensation is needed.
const double lh = (params.layer_height > 0.) ? double(params.layer_height) : double(this->spacing);
const double omega = compute_omega_factor(density_adjusted, this->spacing * params.multiline, lh);
// shift the polyline to the grid origin
for (Polyline &pl : polylines)
pl.translate(bb.min);
const float density_factor = std::max(0.001f, float(params.density * DensityAdjust / params.multiline));
const float period = float(2.0 * M_PI) * float(this->spacing) / density_factor;
// bb is already expanded above by 10 * scale_(spacing) for edge artifacts;
// skip a second offset here to avoid raster-area bloat in the marching squares pass.
marchsq::GyroidField sf(bb, this->z, period, float(omega));
polylines = marchsq::get_gyroid_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
} else {
polylines = make_gyroid_waves(
scale_(this->z),
density_adjusted,
this->spacing,
ceil(bb.size()(0) / distance) + 1.,
ceil(bb.size()(1) / distance) + 1.);
// The parametric generator produces wave coords relative to the grid origin;
// shift them into absolute layer coords. The marching-squares branch above
// already emits absolute coords via GyroidField::to_Point, so it skips this.
for (Polyline &pl : polylines)
pl.translate(bb.min);
}
// Apply multiline offset if needed
multiline_fill(polylines, params, spacing);

View File

@@ -1008,6 +1008,7 @@ static std::vector<std::string> s_Preset_print_options{
"is_infill_first",
"sparse_infill_density",
"fill_multiline",
"gyroid_optimized",
"sparse_infill_pattern",
"lateral_lattice_angle_1",
"lateral_lattice_angle_2",

View File

@@ -2912,6 +2912,19 @@ void PrintConfigDef::init_fff_params()
def->max = 10; // Maximum number of lines for infill pattern
def->set_default_value(new ConfigOptionInt(1));
// Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z
// (vertical) axis at low infill density to shorten the effective column length under
// Z-axis compression. Filament use at the same `sparse_infill_density` setting is
// preserved. No effect above ~30% density (formula clamps to no-op).
def = this->add("gyroid_optimized", coBool);
def->label = L("Z-buckling bias optimization (experimental)");
def->category = L("Strength");
def->tooltip = L("Tightens the gyroid wave along the Z (vertical) axis at low infill density "
"to shorten the effective vertical column length and improve Z-axis compression "
"buckling resistance. Filament use is preserved. No effect at ~30% sparse infill "
"density and above. Only applies when Sparse infill pattern is set to Gyroid.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("sparse_infill_pattern", coEnum);
def->label = L("Sparse infill pattern");
def->category = L("Strength");

View File

@@ -1133,6 +1133,7 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca:
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
((ConfigOptionInt, fill_multiline))
((ConfigOptionBool, gyroid_optimized))
// Ironing options
((ConfigOptionEnum<IroningType>, ironing_type))
((ConfigOptionEnum<InfillPattern>, ironing_pattern))

View File

@@ -605,6 +605,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral;
// gyroid_optimized only applies when the sparse infill pattern is gyroid;
// hide the whole line otherwise.
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid);
// If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
if (have_infill) {
toggle_field("fill_multiline", have_multiline_infill_pattern);

View File

@@ -2445,6 +2445,7 @@ void TabPrint::build()
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid_optimized");
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");