Satin Studio Manual

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:

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:

\[b_0=1,\qquad b_1=0,\qquad \chi=1.\]

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:

  1. center-walk underlay;
  2. low-density fill underlay;
  3. directional top fill;
  4. decorative veins;
  5. 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

\[t_c(x)=\frac{v'(s^*(x))}{\|v'(s^*(x))\|}\]

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

\[\theta_0(x) = \theta_c(x)+r(x)\,\phi(s^*(x)),\]

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:

\[z_0(x)=e^{i2\theta_0(x)}.\]

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

\[E(z) = \int_\Omega w_s(x)\|\nabla z(x)\|^2 + w_g(x)\|z(x)-z_0(x)\|^2 \,dx,\]

with \(|z(x)|\approx1\). Boundary guides, vein guides, and manual artist strokes can contribute additional weighted terms. Recover the stitch angle with

\[\theta(x)=\tfrac12\arg z(x).\]

Near the pointed tip, the field may be forced into a singularity or rapid rotation. The compiler has several legitimate responses:

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

\[h(x) = \operatorname{clamp}\!\left( h_0\,[1+\beta q(x)], h_{\min}, h_{\max} \right),\]

where \(q(x)\) is a crowding predictor. It may combine field divergence, distance to the tip, curvature, and previously predicted penetration density.

For example,

\[q(x) = a\,|\nabla\cdot d(x)| +b\,\kappa_{\partial\Omega}(x) +c\,\rho_{\text{pred}}(x),\]

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:

\[\nabla\varphi(x) \approx \frac{n(x)}{h(x)},\]

where \(n(x)\) is a unit normal to the desired row direction. Then extract level curves

\[\Gamma_k=\{x\in\Omega:\varphi(x)=k\}.\]

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:

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

\[p' = p + c_{\text{pull}}(x,\theta,w)\,\hat t,\]

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

\[c(x,n) = c_0(x)+c_a(x)\,|n\cdot d(x)|,\]

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:

A row with arc length \(L\) and nominal maximum stitch length \(\ell_{\max}\) starts with

\[n=\left\lceil\frac{L}{\ell_{\max}}\right\rceil\]

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:

An illustrative report might read:

MetricExample resultInterpretation
Top-fill stitches2,184Informational, not a quality score
Row spacing, median0.43 mmNear the requested profile value
Stitches below preferred minimum11Clustered at the tip; inspect
Maximum penetration heat1.38× profile targetWarning at two vein junctions
Uncovered area at chosen tolerance0.7%Mostly reserved beneath border
Jumps2Both hidden under later layers
Trims1Before the vein color block
Predicted boundary contraction0.18–0.34 mmCompensation 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.