Early docs These pages are very early. Structure and tone are still being worked out, so anything here may change drastically.
Skip to content
Rendered from src/infill/README.md in the repository — edit it there.

Infill — Pattern Generation Inside a Boundary ​

This module turns a closed boundary Paths into a Paths of infill line segments. It is pure 2D geometry: no slicing, no G-code, no per-layer bookkeeping.

Boundary in. Lines out. The boundary is sacred — never inflate it again.


Why it exists ​

Sparse infill is what fills the inside of a print so it isn't a pile of loose walls. There are several established patterns, each a tradeoff between print speed, strength, and isotropy. Rather than scatter that geometry across the slicing pipeline, all of it lives behind one entry point — generate_infill — that takes a FillParams and produces line segments. The slicer doesn't know how a gyroid is computed; the gyroid doesn't know what a SliceLayer is.

The boundary handed in is the final infill region: it has already been inset to account for every wall bead and the configured infill overlap. The infill generators must fill it as-is, with no further offsetting.


The contract ​

  1. Boundaries are not inflated. core::infill::calculate_interior_region has already done that work. A second inward inflate inside this module was the cause of the "missing infill on cabin transition layers" bug — features narrower than ~2× the extra offset collapsed entirely.
  2. Output is line segments, not closed contours. Each emitted Path is a polyline that the G-code generator strokes once. They are clipped to the boundary by utils::clip_lines_to_region; patterns themselves emit the raw, unclipped pattern across the boundary's bounding box.
  3. Density is a fraction in [0.0, 1.0], measured against the bead spacing. 0.0 returns empty paths; values outside the range are clamped. See The density unit — this is the single most important invariant in the module.
  4. Layer rotation comes in as angle_offset (radians), and only Rectilinear alternates it. See Per-layer rotation — applying the 90° flip to any other pattern breaks it. 3D patterns (gyroid, TPMS-D) ignore the angle and use z_height instead.

The density unit ​

Every pattern derives its geometry from one number: FillParams::spacing_mm, the flow spacing of a single bead —

text
spacing = extrusion_width − layer_height × (1 − π/4)

which is libslic3r's Flow::spacing(), computed once by core::extrusion_flow_spacing_mm from core::sparse_infill_nominal_width_mm. Lines are laid

text
line_pitch = spacing / density

apart, and the G-code generator charges each of them spacing × layer_height of filament (resolve_width_mm). Because the pitch and the flow come from the same quantity, "20 % density" deposits exactly 20 % of a solid layer's volume.

Patterns that lay several sweeps across the region divide the density first. libslic3r's fill_surface_by_multilines does density /= sweeps, so N sweeps together deposit density, not N × density. Use rectilinear::generate_multiline rather than calling generate_rectilinear twice — an earlier grid implementation did exactly that and deposited double the requested density.

Every generator used to hardcode a 0.4 mm reference width instead, which made density wrong for any nozzle or sparse_infill_line_width that was not 0.4 mm (a 0.6 mm nozzle printed "20 %" as roughly 13 %).


Pattern catalog ​

Sparse — InfillPattern ​

PatternFileDimensionalityCell relation
Rectilinearrectilinear.rs2D, per layerpitch = spacing / density
AlignedRectilinearrectilinear.rs2D, fixed angleas rectilinear, but the per-layer 90° flip is suppressed
Gridgrid.rs2D, per layertwo sweeps at density / 2
Trianglesrectilinear.rs2D, per layerthree sweeps 60° apart at density / 3
TriHexagonrectilinear.rs2D, per layertriangles, third sweep shifted 1.5 × spacing / density
Cubicrectilinear.rs3D, uses ztriangles, sweeps shifted ± √0.5 · z
Honeycombhoneycomb.rs2D, per layerdistance = spacing / density, side = distance / (√3/2)
Concentricconcentric.rs2D, per layerloops stepping inward spacing / density
Gyroidgyroid.rs3D, uses zperiod = 2π · spacing / (density × 2.44)
TpmsDtpms_d.rs3D, uses zperiod = 2π · spacing / (density × 13.2)

InfillPattern::parse accepts OrcaSlicer's spellings alongside our own (line / linear → Rectilinear, alignedrectilinear, stars → TriHexagon, hexagonal → Honeycomb, tpmsd → TpmsD), so an imported profile maps without a translation table.

Every one of them deposits the density it is asked for, pinned by every_line_pattern_deposits_the_density_it_is_asked_for. Concentric is the one exception by design: it traces the whole outline of a thin sliver where a line fill would cross it once, so it over-fills complex cross-sections.

A marching-squares tracer must chain its output. TpmsD walks a level set cell by cell, emitting one short segment per crossing. Left unchained those are isolated sub-cell dabs, and the splat filter deletes almost all of them, so the pattern deposits a fraction of the requested density. utils::chain_segments_into_polylines joins them back into the curves the tracer actually found.

Solid — SurfacePattern ​

Solid fill is always 100 % dense, so these variants differ only in the order and connectivity of the lines — which is what decides how a visible surface looks. Generated by core::surfaces::generate_solid_infill, not here; the enum lives in this module because it is a fill pattern.

PatternBehaviour
RectilinearBack-and-forth serpentine, every second line reversed
AlignedRectilinearSerpentine with the per-layer 90° alternation suppressed
MonotonicOne-way sweep; consecutive spans joined where they already abut
MonotonicLineOne-way sweep, lines never joined (default for top surfaces)
ConcentricLoops following the outline, stepping inward one bead per loop

SurfacePattern::parse accepts both slicers' spellings — PrusaSlicer writes monotoniclines, OrcaSlicer monotonicline — so an imported profile maps cleanly either way.

Monotonic means every line is drawn in the same direction. The nozzle never returns across a line it just laid, so each bead is squished by its neighbour identically and the direction-dependent sheen a serpentine leaves on a top surface disappears. MonotonicLine is exactly Monotonic with joining switched off, which libslic3r encodes as anchor_length_max = 0.

Because that ordering is the feature, core::pipeline skips the greedy-TSP path reordering for a monotonic surface group (monotonic_surface_role) — the TSP is free to reverse an open path, which would scramble the sweep and leave the surface looking no different from a plain serpentine.


Per-layer rotation ​

InfillPattern::alternates_per_layer() is true for Rectilinear alone.

Alternation exists for one reason: a layer of parallel lines that stacked directly on the identical layer below would build unsupported walls instead of a lattice, so consecutive layers are crossed by 90°. That question only arises when a layer draws its lines in one direction.

Every other pattern is actively harmed by the flip, which is why libslic3r's multi-sweep and cellular fills all override _layer_angle to 0:

  • Honeycomb is cellular. Its walls have to stack layer over layer to form tubes — that vertical structure is the pattern. Rotating the lattice 90° drops each layer's walls onto the previous layer's voids, and consecutive layers end up sharing almost none of their infill geometry.
  • Triangles / TriHexagon / Cubic already sweep three directions, so rotating only misregisters the lattice against the layer below.
  • Grid sweeps 0° and 90°, so a 90° rotation maps it onto itself.
  • Concentric, Gyroid, TpmsD ignore the angle entirely.

The companion invariant is world-anchored phase: a fixed orientation is not enough if the lattice slides. Both are needed for cells to stack, and both are pinned by tests.


Anchors — anchor.rs ​

Every sparse-infill line stops dead where it meets the inner wall. Letting it turn and follow the wall welds it to the shell instead of merely touching it, and two lines that meet around a short stretch of wall become one continuous move. This is a port of libslic3r's Fill::connect_infill, driven by two knobs:

SettingMeaning
infill_anchor_max_mmlongest wall stretch that may join two lines; 0 disables all anchoring
infill_anchor_percenthow far a single unpaired end may run along the wall, as a % of the bead spacing; 0 disables open anchors

The walk always follows the boundary, never cuts across the region, so an anchor can only be laid where the fill area already reaches.

This is not a cosmetic feature. On a hollow-box cross-section it turns a layer's worth of isolated sub-millimetre dashes — each costing a full retract → travel → un-retract — into one continuous serpentine. The extra material the QA baselines record is the connectors that make those dashes printable at all.

Anchoring must run before the splat and minimum-length filters in core::infill::add_infill_to_layers. A line that anchoring would have merged into a long path must not be discarded as an isolated dash first.

Anchors are for sparse infill only. libslic3r gives solid and bridge fill unlimited anchors, but our monotonic surface fill already joins abutting spans itself, and running the generic pass over it would reverse lines to make the join — destroying the uniform sweep the pattern exists to produce.


Anatomy ​


Role in the wider system ​

The infill module sits at the leaves of the pipeline. Everything upstream — surface detection, wall stripping, region calculation — exists so the boundary handed to generate_infill is exactly right.


Critical invariants ​

1. Do not inflate the input boundary ​

The block comment in generate_infill calls this out explicitly. A second inward offset inside this module collapses thin features and silently loses infill. If a pattern needs internal padding, it must do so in its own coordinate space, not by shrinking the boundary.

2. Pattern outputs are unclipped polylines ​

Per-pattern generators (generate_rectilinear, generate_grid, …) emit straight line segments across the boundary's bounding box without any clipping. clip_lines_to_region does the boolean intersection at the end. This keeps each pattern simple and pushes the Clipper2 dependency to one shared utility.

3. Line phase is anchored to world coordinates ​

generate_lines seeds its first line from a world-aligned grid, not from the region's own centre. Successive layers have slightly different interior regions, so a centre-relative phase would drift the infill a fraction of a pitch per layer and the lattice would never stack.

generate_honeycomb obeys the same rule: its lattice is built in a pattern space rotated about the world origin, and the region's bounding box only decides which cells to emit. Keying the phase to the region's centre instead made the cells wander as the cross-section changed — fatal for a pattern whose walls are supposed to stack into tubes.

4. Scanline fill correctly handles holes ​

For boundaries containing CW hole sub-paths (annular cross-sections like a hollow box), the scanline algorithm relies on Clipper2's standard winding to produce the right parity of crossings — solid CCW outer ring + CW hole naturally toggles the even-odd count to skip the void. No special-casing is needed as long as the input is canonical Clipper2 output.


What this module deliberately does not do ​

  • No slicing. It doesn't know what a layer is; it operates on a single boundary at a time.
  • No solid-surface fill generation. Top/bottom solid lines are generated by core::surfaces, which owns the scanline used for both. Only the SurfacePattern vocabulary lives here.
  • No G-code. Output is Paths. The G-code generator strokes them.
  • No layer combining. Stacking sparse infill across layers is a per-layer bookkeeping problem, so it lives in core::infill.
  • No travel optimisation. Path ordering and combing are out of scope; anchoring only ever connects lines along the boundary.

See also ​