Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Data model

The types every crate shares. Code sketches are illustrative (ignore); the crates’ rustdoc is the API reference once the types exist. Invariants listed here are checked in code (constructors return Result) and by conformance level L0.

Units and coordinates (stitchcraft-core)

/// A length in millimetres. Always finite; construction from untrusted input is checked.
pub struct Mm(f64);

/// A point in millimetres, y pointing down (SVG and VectorCraft convention).
pub struct Point { pub x: f64, pub y: f64 }
  • Internal unit: millimetres. Embroidery parameters are specified in mm and machine files use 0.1 mm, so mm keeps the numbers people read and the numbers we compute the same.
  • Host units are converted at the adapter: VectorCraft points ×25.4/72; SVG user units through the root viewBox/width/height, with CSS pixels at 96 per inch.
  • Quantization happens once, in the encoder, on absolute positions: round_half_even(x * 10). Deltas are differences of quantized absolutes, so rounding never accumulates.
  • Axis conventions per format are the encoder’s job (DST is y-up). Test sheet TS-01 (an asymmetric “F” with a ruler) proves orientation and scale on the machine.

Engine input: Design (stitchcraft-engine)

Hosts translate their documents into a Design (stitchcraft_engine::design), so the engine never sees a host document and every host gets the same stitches for the same design. Since M3.3:

pub struct Design {                  // checked by Design::new
    elements: Vec<Element>,
    pub settings: DesignSettings,
    pub sort_colours: bool,          // since M13.3: the elements with settings sew in the order StitchCraft chooses too
}

pub struct Element {
    pub id: ElementId,               // stable host id: "svg:path123:fill", "vc:42:fill"
    pub name: Option<String>,        // the name the user gave it (Inkscape's label)
    pub shape: Shape,
    pub thread: Thread,
    pub params: ParamSet,            // as the host stores them; validated against the registry when planned
    pub automatic: bool,             // since M13.1: a shape without settings, read with --auto, whose stitches StitchCraft chooses
}

pub struct Reading {                 // since M13.1: what every host's reader is asked
    pub auto: bool,                  // --auto
    pub sort_colours: bool,          // since M13.3: --sort-colours, which the reader passes on (Design::read_as)
}

pub struct DesignSettings {          // since M3.8; read from an Ink/Stitch file since M4.8, its commands from M8,
                                     // and from a VectorCraft document since M6.11
    pub collapse_len: Mm,            // 3 mm: same-thread moves no longer than this are sewn on, not jumped
    pub min_stitch_len: Option<Mm>,  // the design's shortest stitch, for elements that set none
    pub origin: Option<Point>,       // where the hoop's centre goes; None: the centre of the stitches
    pub stop_position: Option<Point>,// where the frame moves before each stop; None: it stays
    pub min_satin_stroke_width: Mm,  // 1 mm: a satin column drawn as one path no wider is sewn as a stroke (M4.8)
}

pub enum Shape {
    Stroke { path: Path, width: Mm, join: Join, cap: Cap },  // an outline: running stitch and the other stroke methods
    Fill { path: Path, rule: FillRule },           // an area: its boundary, and which parts are inside
}

pub enum Join { Miter { limit: f64 }, Round, Bevel }   // a stroke's corners; Join::UNSET is a miter limited at 5
pub enum Cap { Butt, Round, Square }                  // since M13.1: how a stroke ends where an open subpath ends

pub struct Path { pub subpaths: Vec<Subpath> }   // in millimetres, y down
pub struct Subpath { pub start: Point, pub segments: Vec<Segment>, pub closed: bool }
pub enum Segment { Line(Point), Quad(Point, Point), Cubic(Point, Point, Point) }

Elements are in the host’s paint order, bottom first. They sew in that order unless the design has automatic elements or sort_colours is set: since M13.3 automatic digitizing then chooses the order (image to embroidery). Curves keep their control points, and the generators flatten them with the tolerance their parameters give.

A host reads 3 of the settings by Ink/Stitch’s names, collapse_len_mm, min_stitch_len_mm and min_satin_stroke_width_mm: an SVG file’s metadata, and since M6.11 the data a VectorCraft document keeps for StitchCraft’s Design Settings filter. SETTINGS lists them with the label and help that filter’s dialog shows, and Setting::get and Setting::set read and write each in millimetres. A shortest stitch of 0 is None, which leaves it to the machine.

A stroke’s width and join, since M4.8, are as the host draws them: in SVG, stroke-width scaled by the transforms, and stroke-linejoin with stroke-miterlimit (SVG input). Only a satin column drawn as one path uses them, as its width and its corners. Shape::stroke(path) makes a stroke that says nothing of either: 1 CSS pixel wide, with Join::UNSET. Its cap, since M13.1, is SVG’s stroke-linecap or VectorCraft’s cap, Butt when nothing sets it. Ink/Stitch sews no cap, and neither does an element with settings. An automatic element’s stitches follow it (image to embroidery).

An automatic element is a shape without settings that the user asked StitchCraft to digitize: the readers mark it when Reading::auto is set, and the engine turns it into ordinary elements with the settings its width calls for before anything is generated (stitchcraft_engine::auto).

Documents repeat ids, so adapters make theirs unique with stitchcraft_core::Labels, which gives a label repeated in a document ~2, ~3 and so on, in every adapter alike.

Invariants, checked by Design::new: element ids are unique, and every point is finite and within ±10,000 mm, as far as machine-file coordinates go. No stroke is narrower than 0, a miter limit is a finite number of at least 1, and the settings’ lengths are not negative. Adapters drop what they cannot represent with a diagnostic of their own, so a failed check is a bug in the adapter (SC-E0009). Design::moved gives the design moved by an offset, every point of its elements and its origin and stop position, checked the same way. VectorCraft’s Test Sheet filter draws a test sheet in the middle of the artboard with it (REQ-VC-015).

Commands attached to elements come with M8, for Ink/Stitch’s command symbols such as start and end points. Trims and stops are the element parameters trim_after and stop_after, and the origin and stop position are design settings (M3.8).

Region

A fill’s region is not part of the design: the normalizer builds it from the fill’s path and fill rule, since M5.1 (stitchcraft_engine::normalize::region):

pub struct Region { pub parts: Vec<Polygon> }   // in the order the drawing reaches them
pub struct Polygon { pub outline: Vec<Point>, pub holes: Vec<Vec<Point>> }   // closed rings, in millimetres

Each ring starts at its point that comes first in the drawing, and repeats it at its end. Outlines turn clockwise and holes counter-clockwise in y-up axes, and in those axes the region lies on the right of every ring. Parts of 3 square CSS pixels (0.21 mm²) or less are left out, with SC-W0303. The fills design builds the region step by step.

SatinShape

A satin column’s path is recognized, since M4.1, as one of 2 shapes (stitchcraft_engine::normalize::satin):

pub enum Shape {
    Rails(Satin),   // 2 rails as polylines, and what pairs their points: rungs, or the rails' nodes
    CentreLine { line: Vec<Point>, closed: bool },  // one subpath, flattened; closed when the path ends with Z
}

A centre line is made into a Satin of its own, its rails offset from the line by the stroke’s width and join (stitchcraft_engine::normalize::centre_line, since M4.9). How rails and rungs are recognised from a host path, and made from a centre line, is part of the satin design.

Parameters

ParamSet maps registry keys to typed values; generators receive typed views (TatamiParams { row_spacing: Mm, angle: Deg, … }) produced by the registry. See parameter registry.

Engine output: StitchPlan (stitchcraft-plan)

pub struct StitchPlan {
    pub blocks: Vec<ColorBlock>,       // sewing order; a thread change between blocks, the end after the last
    pub elements: Vec<ElementId>,      // what `ElementRef`s point at
}

pub struct ColorBlock {
    pub thread: Thread,
    pub stitches: Vec<Stitch>,
}

pub struct Stitch {
    pub at: Point,                     // where the needle is after this entry (mm, y down, hoop centre = 0,0)
    pub kind: StitchKind,
    pub origin: Provenance,
}

pub enum StitchKind { Normal, Jump, Trim, Stop }

pub struct Provenance {
    pub element: Option<ElementRef>,   // index into `elements`; None for plan-level entries
    pub role: Role,                    // Top, Underlay, Travel, Lock, Command
}
  • Colour changes and the end are structure, not entries. The machine changes thread between two blocks and ends after the last one, so a plan cannot misplace either; encoders write exactly one colour change between blocks and one end.
  • Trim and Stop happen where the needle is; their at repeats the needle position so every entry has one (bounds, previews and reports never special-case commands), and the invariant checker verifies it. Encoders decide how a format expresses them. PEC puts a trim flag on every jump after the first move, and records a stop as a change to the same thread. DST spells a trim as a run of jumps. See formats.
  • The origin is the centre of the hoop, where the needle starts, above the fabric. The first Normal stitch is the first time it goes down.
  • PlanBuilder appends entries with the needle tracked; PlanStats counts stitches, jumps, trims, stops and thread changes for reports.
  • A plan does not name a profile: it is checked against one, invariants::check(&plan, &profile).

Plan invariants

Conformance level L0 checks these on every plan the suite produces. A violation is a bug in whatever built the plan: the command line reports it as SC-E0009 and writes nothing. “While sewing” means the previous movement was a Normal stitch with no trim since — the stitch lays thread between two holes, and its length is what the machine and fabric feel; the first stitch after a jump, a trim or a thread change starts a new run. A length or a position within LENGTH_SLACK (10⁻⁹ mm) of a limit counts as at it, at the hoop’s edge as for a stitch’s length, so rounding cannot push a stitch placed exactly at a limit past it. Generators place stitches by the same test, and so does the check that a design fits the hoop.

RequirementRuleSince
REQ-PLAN-001Every position is finite and inside the profile’s hoop, centred on the origin.M1
REQ-PLAN-002While sewing, every stitch is between the profile’s min_stitch (lock stitches, into or out of a lock point: 0.2 mm) and max_stitch.M1
REQ-PLAN-003Every block sews at least one stitch; trims and stops happen where the needle is.M1
REQ-PLAN-005While sewing, no stitch lands where the needle already is.M1
REQ-PLAN-006Thread changes plus stops fit the format (PES: 255; DST: 999).M1
REQ-PLAN-007Every Normal and Jump entry names an element the plan lists. A trim or a stop need not name one.M3

REQ-PLAN-004, a tie-off before a trim and a tie-in after it, is retired. Whether they belong there depends on the elements’ lock settings, which a plan does not hold. Assembly’s cases check it with the settings (REQ-ASM-003, REQ-LCK-001).

Threads and palettes

pub struct Thread {
    pub color: Rgb,                  // what the user chose
    pub name: Option<String>,        // shown to the operator
}

Palettes are static tables (the Brother PEC palette first) of PaletteEntry { index, name, color, matchable }. Nearest-colour matching uses CIEDE2000 in CIELAB (D65), through stitchcraft_core::math so it is deterministic, with ties going to the lower index; it is tested against the 34 reference pairs of Sharma, Wu & Dalal (2005). Entries that are not threads — Brother’s applique steps 62–64 — are never matched. A format that names threads by entries gives a design’s threads theirs together (Palette::assign, REQ-THREAD-002). Different threads get different entries while they last, the nearest pairs first, and a thread read from such a file gets its own (formats, PES). Each table names its source in its module docs and in a row of NOTICE. The PEC palette is the one MIT-licensed pyembroidery publishes, with entries 62 and 63 from its fork pystitch. Thread catalogues (brand and number) arrive with M11.

Machine profiles

pub struct MachineProfile {
    pub id: &'static str,            // "brother-pe800-5x7"
    pub name: &'static str,          // "Brother PE800 with its 5 × 7 in hoop"
    pub hoop: Size,                  // 130 × 180 mm
    pub comfort: Option<Size>,       // None — a warning beyond it, not an error
    pub format: FormatId,            // PesV1 | Dst
    pub max_stitch: Mm,              // 12.0
    pub min_stitch: Mm,              // 0.3
    pub trims: TrimSupport,          // Command | LongJumps(Mm) | None
    pub palette: PaletteId,          // BrotherPec
    pub evidence: &'static str,      // where the values come from
}

Profiles are data in stitchcraft-plan/src/profiles/, listed by stitch profiles and documented by generated reference pages. A profile’s values are machine facts: changing one requires a machine-testing record that justifies it (machine testing), and evidence says where each value comes from. Format limits that are not machine facts (the most colour changes a format records) live on FormatId, so the invariant checker and the encoders read the same number.

A profile is a machine with one hoop, so a machine with 3 hoops has 3 profiles. profiles::REFERENCE is the reference machine’s, the Brother PE800 with its 5 × 7 in hoop (ADR 0013). Code that means the machine the checkpoints run on imports it rather than naming a hoop: the command line plans for it unless --profile names another or a VectorCraft document keeps one (integration). Each test sheet names the profile of the hoop it is sewn in.

check_fit(bounds) turns the design’s size into SC-E0701 (larger than the hoop) or SC-W0702 (larger than the comfort zone), with “rotate 90°” as a one-click fix when turning the design would fit (REQ-PRF-002).

Budgets

pub struct Budget {
    pub max_stitches: u32,           // per design, default 2,000,000
    pub max_work: u64,               // abstract work units per element, and once more for assembly
}

Work units are counted per inner loop iteration (scanline crossings, graph edge visits, offset steps) so a budget behaves identically on every machine. Each element has the work budget to itself, and so does working out what it offers the element before it to end near. Exhaustion there yields SC-E0004 for the element, and other elements still plan. Assembly and finalize share the budget once more, and running out there plans nothing. SC-E0004 then says so for the whole design.

Work bounds time, not memory: a unit can make a point, and 500 million points are gigabytes. A few settings multiply an element’s needle points. 10 mm of zigzag at the smallest spacing, sewn 100 times with bean stitch 10, is 4.2 million. So the stitch limit bounds what generators hold as well (REQ-GEN-004):

  • A generator spends a stitch for each needle point as it makes it, and for each one a pass after the first or bean stitch sews again.
  • What it lays out before sewing it must fit in the stitches left, though it spends none on it: a satin column’s pairs, say, or a fill’s row segments.
  • Once done, it settles at the needle points it gives. Some it made were dropped on the way, such as points too close together, and some were copied, such as an E-stitch’s way back down its spike.

So a generator never holds much more than the stitches left, and it may stop a little short of the limit where it made many more points than it keeps. Each element is given the stitches the elements before it left. The first that needs more ends planning there, with SC-E0004 naming it: the design cannot be sewn within its limit, and making the rest would only fill memory.