Worked example: compiling a leaf
A leaf is a friendly example because it has recognizable form, curved boundaries, a natural direction field, layered details, and a pointed tip where naïve algorithms tend to misbehave.
Suppose the design contains:
- a closed leaf outline \(\Omega\);
- a center vein curve \(v(s)\) from stem to tip;
- several secondary vein curves;
- a top-fill color;
- a darker vein color;
- an optional border;
- a material and machine profile.
The dimensions and numbers below are illustrative. They are not universal digitizing recipes.
We can keep the inputs in one Dither value. The code does not hide the assumptions: millimeters, colors, and layer order are visible before compilation begins.
; Describe the leaf as source data. The outline and vein geometry remain
; editable objects in the project; this value records how to compile them.
(define user/leaf-plan
{:outline :leaf-outline
:center-vein :leaf-center-vein
:secondary-veins [:vein-left-1 :vein-left-2
:vein-right-1 :vein-right-2]
:threads {:top-fill :fern-green
:veins :forest-green}
:layers [:center-walk-underlay
:sparse-fill-underlay
:directional-top-fill
:decorative-veins
:finishing-border]
:profile {:material :medium-cotton
:machine :single-needle
:units :millimeters}})
; Inspect the exact sewing order carried by the plan.
(get user/leaf-plan :layers)
15.1 Normalize and inspect the topology
Apply source transforms, convert to millimeters, flatten unsupported effects, and build a valid planar arrangement. Assume the leaf has one connected component and no holes:
Check that the center vein lies within or on \(\Omega\), that it reaches sensible locations near the base and tip, and that secondary veins do not create accidental microscopic regions unless they are intended to split the fill.
The semantic decomposition might be:
- center-walk underlay;
- low-density fill underlay;
- directional top fill;
- decorative veins;
- finishing border.
One valid precedence graph is:
flowchart TD
A[Center-walk underlay] --> B[Low-density fill underlay]
B --> C[Directional top fill]
C --> D[Decorative veins]
D --> E[Finishing border]
The border comes last so that it can clean the outer edge and cover vein endpoints. Another style could place selected veins after the border. That is an artistic decision encoded as a different DAG, not a geometry accident.
Dither can check the expected topology before a long compile starts:
; Accept one connected region with no holes. Returning a labeled map makes a
; failed check easier to diagnose than a bare true or false value.
(define user/leaf-topology-ok?
(fn [connected-components holes]
{:connected-components connected-components
:holes holes
:accepted (and (= connected-components 1)
(= holes 0))}))
; This leaf has b0 = 1 and b1 = 0, so :accepted is true.
(user/leaf-topology-ok? 1 0)
15.2 Construct a leaf-aligned direction field
Let \(s^*(x)\) be the parameter of the closest point on the center vein to \(x\), and let
be the local centerline tangent.
Define a signed cross-leaf coordinate \(r(x)\in[-1,1]\), negative on one side of the center vein and positive on the other. A simple fanning guide angle is
where \(\theta_c\) is the angle of \(t_c\) and \(\phi(s)\) controls how strongly rows fan away from the centerline. Set \(\phi\) smaller near the narrow stem and tip if a strong fan would create short crowded rows.
Because embroidery direction is a line field, solve and blend in doubled-angle form:
The doubled angle gives opposite directions the same representation. A stitch line at \(30^\circ\) and one at \(210^\circ\) therefore agree, as they should.
; Represent an unoriented stitch angle as [cos(2 theta), sin(2 theta)].
; Theta is in radians. Adding pi reverses the arrow but returns the same pair.
(define user/line-field-value
(fn [theta]
[(cos (* 2.0 theta))
(sin (* 2.0 theta))]))
; Fan away from the center vein. cross-position runs from -1 on one edge to
; +1 on the other; fan-angle controls the maximum turn at either edge.
(define user/leaf-guide-angle
(fn [center-angle cross-position fan-angle]
(+ center-angle (* cross-position fan-angle))))
(user/line-field-value
(user/leaf-guide-angle 0.35 0.6 0.28))
A smooth constrained field can be obtained by minimizing
with \(|z(x)|\approx1\). Boundary guides, vein guides, and manual artist strokes can contribute additional weighted terms. Recover the stitch angle with
Near the pointed tip, the field may be forced into a singularity or rapid rotation. The compiler has several legitimate responses:
- taper row spacing and stop rows before the exact tip;
- reserve the tip for the border;
- split the top fill into two regions meeting along a controlled seam;
- use a short satin or manual detail at the tip;
- relax the field constraint locally.
The correct response depends on the style and material profile. “Smooth the field harder” is not always a solution to a topological conflict.
15.3 Choose a spacing field
Let \(h_0\) be the nominal row spacing for the selected thread, material, and coverage target. Increase the spacing slightly where rows converge and reduce it only where the substrate or appearance requires more coverage.
One simple model is
where \(q(x)\) is a crowding predictor. It may combine field divergence, distance to the tip, curvature, and previously predicted penetration density.
For example,
with normalized terms and application-specific weights. This is a heuristic surrogate, not a law of textile physics. Its value is that it makes the reason for spacing changes explicit and testable.
; Keep a value inside a closed interval.
(define user/clamp
(fn [value lower upper]
(min upper (max lower value))))
; Widen the nominal spacing as predicted crowding rises. The minimum and
; maximum come from the chosen material profile, not from this helper.
(define user/leaf-row-spacing
(fn [nominal crowding response minimum maximum]
(user/clamp
(* nominal (+ 1.0 (* response crowding)))
minimum
maximum)))
; A 0.43 mm nominal spacing with moderate crowding becomes 0.4472 mm.
(user/leaf-row-spacing 0.43 0.5 0.08 0.38 0.52)
15.4 Generate continuous rows
One approach is to solve for a scalar potential \(\varphi\) whose gradient is approximately perpendicular to the desired stitch direction and scaled by row density:
where \(n(x)\) is a unit normal to the desired row direction. Then extract level curves
Clip each curve to the compensated region, remove fragments below the primitive’s useful length, and order the fragments. At topology changes, preserve branch information instead of returning an arbitrary list of disconnected polylines.
A streamline method is also reasonable. It may better preserve local direction, while the scalar-potential method often gives more globally regular spacing. The compiler can expose both as fill strategies.
The row filter below shows one small decision made after clipping. Short fragments disappear only when they fall below the primitive's declared useful length.
; Keep a clipped row when its length reaches the profile minimum. A real row
; also carries points and branch identity; this helper decides only length.
(define user/useful-row?
(fn [row minimum-length]
(>= (get row :length-mm) minimum-length)))
(filter
(fn [row] (user/useful-row? row 1.2))
[{:id :row-17 :length-mm 8.4}
{:id :tip-fragment :length-mm 0.5}
{:id :row-18 :length-mm 6.9}])
15.5 Add underlay as a separate plan
For an illustrative medium-size leaf, the compiler might choose:
- a center walk following \(v(s)\);
- a sparse fill underlay whose direction differs from the top fill;
- an inset large enough to keep underlay from peeking beyond the edge;
- wider row spacing and longer stitches than the top layer.
Do not derive the underlay by blindly cloning the top fill. Its job is different. It should stabilize and support the top layer without reproducing every high-frequency detail.
15.6 Apply compensation
Suppose calibration predicts that rows contract primarily along their own direction and that the border covers a margin \(m_b\). For a row endpoint \(p\) with outward unit direction \(\hat t\), a simple compensated endpoint is
subject to a cap imposed by the border and neighboring regions.
The region itself may receive a direction-dependent offset. A convenient approximate support function is
where \(n\) is the boundary normal. This permits more extension where the local stitch direction is likely to pull the boundary inward.
At the stem and pointed tip, taper compensation instead of applying the full amount to an arbitrarily short row. Compensation should be a smooth field, not a discontinuous expansion switch.
; Fade compensation to zero at a fragile endpoint. taper is 0.0 at the exact
; tip and approaches 1.0 where the row is wide enough for full compensation.
(define user/tapered-compensation
(fn [base directional-part alignment taper cap]
(min cap
(* taper
(+ base (* directional-part (abs alignment)))))))
; Apply 70 percent of the local correction, capped by the border allowance.
(user/tapered-compensation 0.12 0.18 0.8 0.7 0.24)
15.7 Sample needle points
Sample each continuous row according to:
- maximum preferred stitch length;
- curvature and corner error;
- endpoint placement;
- material-specific minimum useful stitch length;
- target format quantization.
A row with arc length \(L\) and nominal maximum stitch length \(\ell_{\max}\) starts with
segments, then receives adaptive refinement where curvature or direction changes require it.
Stagger neighboring row penetrations to avoid visible columns and repeated needle lines. The stagger must remain deterministic under a recorded seed.
; Estimate the initial segment count before curvature refinement. ceil keeps
; every segment at or below the preferred maximum stitch length.
(define user/initial-segment-count
(fn [row-length maximum-stitch-length]
(ceil (/ row-length maximum-stitch-length))))
; A 12.7 mm row begins with six segments when 2.2 mm is the preferred maximum.
(user/initial-segment-count 12.7 2.2)
; Alternate a quarter-step offset without randomness. The row index alone
; determines the result, so recompiling the same source gives the same points.
(define user/row-stagger
(fn [row-index]
(if (= (mod row-index 2) 0) 0.25 0.75)))
15.8 Route locally and globally
A practical local fill route alternates rows in a boustrophedon pattern. When adjacent row endpoints are close and the connecting move remains inside an already covered area, join them with a travel path. Otherwise, record a jump or split the fill section.
The global schedule may be:
1. move to leaf base
2. secure thread
3. sew center-walk underlay
4. sew sparse fill underlay
5. sew top-fill sections from base toward tip
6. trim or travel to decorative vein color
7. sew secondary veins and center vein
8. sew finishing border
9. secure and trim
Starting near the base can reduce conspicuous travel and put the first tie-in under later stitching. Sewing generally toward the tip may also reduce the need to drag attached thread across an uncovered point. These are profile- and design-dependent costs, so they belong in routing policy rather than in hard-coded folklore.
; Turn the layer vector into labeled schedule entries. The index is explicit,
; which makes the resulting order easy to inspect and compare with a proof.
(define user/numbered-leaf-schedule
(fn []
(let {:layers (get user/leaf-plan :layers)}
(for [index (range (count layers))]
{:step (+ index 1)
:layer (nth layers index)}))))
(user/numbered-leaf-schedule)
15.9 Inspect the result
Before export, review:
- direction-error heatmap;
- row-spacing and coverage map;
- penetration-density map, especially at the tip and vein junctions;
- underlay inset and exposure risk;
- boundary compensation preview;
- short-stitch clusters;
- visible travel paths;
- precedence and attachment-state validation;
- decoded target-format preview.
An illustrative report might read:
| Metric | Example result | Interpretation |
|---|---|---|
| Top-fill stitches | 2,184 | Informational, not a quality score |
| Row spacing, median | 0.43 mm | Near the requested profile value |
| Stitches below preferred minimum | 11 | Clustered at the tip; inspect |
| Maximum penetration heat | 1.38× profile target | Warning at two vein junctions |
| Uncovered area at chosen tolerance | 0.7% | Mostly reserved beneath border |
| Jumps | 2 | Both hidden under later layers |
| Trims | 1 | Before the vein color block |
| Predicted boundary contraction | 0.18–0.34 mm | Compensation applied spatially |
The point of the example is not the numbers. It is the chain of representations and decisions. Every output stitch should be explainable as the result of a source feature, a primitive, a field, a path, a sampling rule, and a routing decision.
15.10 Compile and compare
Save the source geometry, settings, and Dither helpers before compiling. Then inspect the proof beside the intermediate facts rather than judging the rendered leaf alone.
; Compile the open project after the leaf source and profile are saved. The
; result is a background job, so continue only when its proof matches this edit.
(project/compile)
Change one input at a time: the tip treatment, fan angle, nominal spacing, underlay inset, or compensation taper. Keeping the other values fixed turns each new proof into evidence about that decision.
Continue with Validate Compiler Output for systematic checks, or use the compiler geometry reference when you need the full field and topology model. The stitch compiler reference carries the bibliography and deeper implementation-independent mathematics.