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

Parameter registry

A parameter is declared once for the stitch types that read it, next to the generator that uses it. Everything else that needs to know about parameters is generated from that declaration:

                        ┌──▶ typed params struct used by the generator
                        ├──▶ validation (types, ranges, applicability) with coded diagnostics
params! { … }  ─────────┼──▶ VectorCraft plug-in manifests (schema per plug-in, ≤ 64 params)
(in the generator)      ├──▶ CLI: `stitch params`, `--param key=value`
                        ├──▶ SVG mapping (`inkstitch:<key>` attributes, read and write)
                        ├──▶ reference docs pages + JSON Schema (generated, checked in CI)
                        └──▶ proptest strategies for conformance and fuzzing

Nothing about a declaration is written twice, so its label, help, range and default cannot drift apart between the code, the docs and the user interfaces. The diagram is the design’s aim. Generated outputs says which outputs exist today and which steps add the others.

Declaring parameters

Parameters are declared with a macro_rules! macro (no proc-macro crate, so contributors read plain Rust and builds stay fast). The field name is the key; the doc comment is the help text, written for an embroiderer. The real declaration of the settings every stitch type shares, in crates/stitchcraft-engine/src/common.rs, starts like this:

params! {
    /// Settings every stitch type shares: lock stitches where the element's stitching starts and
    /// ends, a trim or a stop after it, and the shortest stitch and jump.
    pub struct CommonParams for StitchType::ALL;

    "Lock stitches" {
        /// Where the element gets lock stitches: …
        ties: Choice = "0", label "Lock stitches",
            options ["0" => "At the start and the end", "1" => "At the start", "2" => "At the end", "3" => "None"];

        /// The shape of the lock stitches at the start. …
        lock_start: Choice = "half_stitch", label "Start lock", choices LOCKS,
            origin Origin::InkStitchDeviates { deviation: "DEV-LCK-001" };

        /// The shape of a custom start lock: …
        lock_custom_start: Text = "", label "Custom start lock", when lock_start == "custom";

        /// How long each step of a start lock made of steps (back and forth, or custom) is.
        lock_start_scale_mm: Length = "0.7", label "Start lock size", range (0.1, 10.0),
            when lock_start in SIZED_IN_MM;
        …
    }
}

Each parameter is key: Kind = "default", label "Label", followed by what its kind needs and what is not the usual: range (min, max) for numbers and lists, options [...] or choices CONST for choices, applies when it covers fewer stitch types than its struct, defaults [Family::Fill => "4"] when a family of stitch types has a default of its own, when other == "value" (or when other in VALUES, a list of them) when a user interface shows it, and an element reads it, only then, origin (default Origin::InkStitch) and stability (default Stability::Stable). A combination a kind does not take does not compile. Defaults are written as a design stores them, so they are checked by the same code as a design’s values. The grammar is in the macros module’s documentation.

The macro expands to:

  • the typed struct, pub struct CommonParams { pub ties: &'static str, pub lock_start_scale_mm: Mm, … }, each field’s type given by its kind (kinds module), so a field and its validation cannot disagree;
  • CommonParams::SPECS (a const slice of ParamSpec) and CommonParams::GROUP;
  • CommonParams::from_set(&ParamSet) -> Result<Validated<CommonParams>, Vec<Diagnostic>>: every parameter read from its text or its default, with the clamping warnings beside the values;
  • CommonParams::from_set_as(&ParamSet, StitchType): the same as an element sewn as that stitch type reads it, with its family’s own defaults and only the parameters it uses (The parameters an element reads). The engine reads an element’s settings with it.

The registry itself is stitchcraft_engine::registry::PARAMETERS, a plain const list of every declaration’s GROUP in the engine, next to the generators (no life-before-main tricks; adding a generator’s parameters is one line there). stitchcraft-params knows no stitch algorithm.

ParamSpec

pub struct ParamSpec {
    pub key: &'static str,          // registry key == Ink/Stitch attribute name when one exists
    pub label: &'static str,        // short UI label (English source string, translation key)
    pub help: &'static str,         // the doc comment; `help()` gives the Markdown
    pub kind: Kind,                 // with its limits: range, options, maximum length
    pub default: &'static str,      // as a design stores it; "" for an empty optional value
    pub family_defaults: &'static [(Family, &'static str)], // a family's own default: `default_for`, `in_family`
    pub group: &'static str,        // UI and docs section
    pub applies_to: &'static [StitchType],
    pub visible_when: Option<Condition>,   // another key of the declaration and the values it shows and reads this for: lock_custom_start only for lock_start custom
    pub stability: Stability,       // Stable | Experimental | Deprecated { use_instead }
    pub origin: Origin,             // InkStitch | InkStitchDeviates { deviation } | StitchCraft
}

InkStitchDeviates names its entry in the deviations ledger (conformance/deviations.toml), which says what differs and why; the reference pages show that entry, so a deviation is described once. A since version and a registry schema version arrive with the first release, together with migrations.

Kinds and validation

KindGenerators getValidationInk/Stitch equivalent
LengthMmfinite, within range; a unit may follow the number with no space: mm, in, ptfloat, unit mm
OptionalLengthOption<Mm>as Length, or empty: the setting then comes from the document or the machine, or there is none. 0 counts as empty, since Ink/Stitch reads 0 as not set, and a length below 0 does too, with SC-W0102 (DEV-PRM-003)float, unit mm, no default
OptionalLengthWithZeroOption<Mm>as OptionalLength, but only empty text is empty, and 0 is a length, as Ink/Stitch reads a satin’s shortest stitch to splitfloat, unit mm, no default
Angledegrees, f64finite; normalized to (−180, 180]float, unit deg
BoundedAngledegrees, f64finite, within range, never turned by whole turns. 270 is above a range that ends at 89, and reads as 89, as Ink/Stitch reads a zigzag stroke’s anglefloat, unit deg
Percentf64finite, within rangefloat, unit %
Numberf64finite, within range, without a unitfloat, no unit
Countu32whole, within rangeint
Togglebooltrue or false in any case, also Ink/Stitch’s yes, y, t and 1 for true and their opposites and off for false. on is SC-E0101, where Ink/Stitch reads it as false (DEV-PRM-001)boolean, toggle
Choice&'static str (the id)one of the declared ids; unknown ids are a diagnostic, not a defaultcombo, dropdown
SeedOption<u64>any; empty means “derive from the element”; text that is not a number is hashed (FNV-1a)random_seed
LengthListVec<Mm>1 to 16 values, each in range (patterns like "2.5 1.0")string/float lists
LengthPair[Mm; 2]1 value for both sides, or 2 for the first side and the second (a satin’s rails, or its start and end). Each is in range, with units as for Lengthfloat, unit mm (each side)
PercentPair[f64; 2]as LengthPair, in percentfloat, unit % (each side)
OptionalLengthPair, OptionalPercentPairOption<[Mm; 2]>, Option<[f64; 2]>as LengthPair and PercentPair, or empty: the setting then comes from another one, as a zigzag underlay’s inset comes from the contour’s. 0 is a value, as Ink/Stitch reads itfloat, unit mm (each side) or % (each side), no default
PercentListVec<f64>1 to 16 percentages, each in range (levels like "15 30")float, unit %, read as a list
CountListVec<u32>1–16 whole numbers, each in rangestring/int lists
OptionalAngleListOption<Vec<f64>>1 to 16 angles, each read as an Angle. Empty, the setting comes from another one, as a fill’s underlay angles come from its anglefloat, unit deg, read as a list, no default
TextStringstripped of the white space round it, as Ink/Stitch strips it, then at most 4,096 bytes. A grammar of its own, such as a custom lock’s, is checked where it is usedstring

Invalid values never fall back silently to defaults: the element gets SC-E0101 (not a value of the kind; the element cannot be stitched) or SC-W0102 (clamped into range), with the key, the value and what is accepted. Keys the registry does not know are kept, so they round-trip, and reported once as SC-W0105. Parsing lives in one place (value module), so every host reads text the same way: an SVG attribute, a command-line key=value, a VectorCraft effect record.

SC-W0102 names the value used, or for a list what the list is read as, since an element may sew only some of a list’s values: a zigzag stroke sews its first spacing.

The ranges are StitchCraft’s. Ink/Stitch sets a floor for most of its numbers and few ceilings, so many ranges here are narrower, and a value outside one is clamped where Ink/Stitch would sew it (DEV-PRM-002). A list holds at most 16 values, a pair 2, and no value is longer than 4,096 bytes, which bounds what reading one costs before any budget is charged. Longer text is SC-E0101, and its message quotes the value’s first 64 characters (DEV-PRM-004).

Empty text, or spaces alone, is not set, whatever the kind, and the parameter takes its default (REQ-PRM-002). Ink/Stitch strips every value and reads an empty one so, and its settings window stores a field the user clears as empty text. A value is stripped of the white space round it before it is read, whatever its kind, text included. A custom lock’s numbers with a tab or a line end round them are numbers here, as there. Spaces separate the values of a list or a pair, as Ink/Stitch splits them. A comma separates nothing and is no decimal point, so text with one is SC-E0101, with a hint to use spaces and points. Ink/Stitch reads 3,5 or 45,90 as the default instead, without a word (DEV-PRM-001). Ink/Stitch leaves out one comma at the end of each of a fill’s underlay angles, for older files, and so does StitchCraft for an angle of any list.

The parameters an element reads (REQ-PRM-004)

An element reads only the parameters its stitch type uses. Any other parameter takes its default, and so does one whose when is false for the element. Its text is kept and not checked. Ink/Stitch reads a setting only where its stitch type uses it, and a path keeps a satin’s settings when its stroke goes back to running stitch: that path must still sew, with nothing said of what it does not read. A when is read from the other parameter’s value on the element, or its default, as the element reads that parameter. So the audit requires the other parameter to be of the same declaration and never hidden itself. It must also apply to every stitch type the hidden one applies to. A host that checks a whole set, such as a settings panel, reads every parameter with from_set, declaration by declaration. It reads only the declarations of the families it shows: the satin column’s declaration of zigzag_spacing_mm cannot read a zigzag stroke’s 2 spacings (rule 7).

Completeness (REQ-PRM-001)

A declaration that compiles can still be wrong: an empty range, a default outside it, a repeated option, a condition on a parameter that does not exist or that an element could not read it by. stitchcraft_params::audit lists every such problem, and the engine’s req_prm_001_every_registered_parameter_is_complete test requires the list to be empty. cargo xtask docs --check adds the cross-checks with outside data. A parameter of Ink/Stitch origin must be in the compatibility contract with a compatible type and the default Ink/Stitch sews, and one of StitchCraft’s own must not take an Ink/Stitch key. Deviations must name ledger entries, and the lock and method identifiers must equal Ink/Stitch’s.

Naming rules

  1. If Ink/Stitch has the parameter with the same meaning, use its attribute name verbatim (row_spacing_mm). This keeps SVG files compatible with no mapping table.

  2. If our meaning differs, keep the name, mark origin: InkStitchDeviates, and record the difference in the deviations ledger (conformance/deviations.toml). An entry that changes how other keys of Ink/Stitch’s read or sew lists them under params, and each key’s reference entry shows it.

  3. New parameters follow the same style: snake_case, a unit suffix for lengths (_mm), percentages (_percent) and angles where Ink/Stitch does (angle alone for row angles).

  4. Never reuse a removed key with a new meaning.

  5. Defaults are part of the contract. For a parameter of Ink/Stitch origin the registry default is the value Ink/Stitch sews for an SVG that omits the attribute, and the file stitches the same way in both tools. That value is the default Ink/Stitch’s settings window shows, except for the few parameters its code reads with another value. The contract records those as fallback, and conformance/inkstitch/check_params.py compares every one of its getters with the contract. Better starting values for new objects come from presets and machine profiles, never from changing a default.

  6. A key belongs to its stitch types. Ink/Stitch gives some keys to several elements with their own defaults: max_stitch_length_mm is empty on manual stitch and satin columns and 4 mm on fills. The contract is per key and stitch type, so the contract page matches a declaration with the rows for the stitch types it applies to, and a declaration for a stitch type Ink/Stitch does not give the key is reported. A key is declared once for a family (rule 7), and a family whose default differs gets its own with defaults, as fills do for max_stitch_length_mm. A family with a default of its own needs a value (ParamSpec::in_family). There an empty value means its default. A length of 0 or less is raised to the least the key accepts instead of counting as empty, as Ink/Stitch reads a fill’s longest stitch. Elsewhere an optional value left empty is no value. The engine reads an element’s settings with its family’s defaults (from_set_as). The contract page compares each row with the default of the row’s family. The reference page lists each family’s default and what the family accepts, and the JSON Schema gives the defaults under x-stitchcraft.family_defaults.

  7. A family’s own kind and range. Some of Ink/Stitch’s keys differ by more than their default from one family to another. Its zigzag_spacing_mm is a list for a zigzag stroke and one number for a satin column, and a meander fill reads it as a number that defaults to 0. Its ripple stitch reads a staggers of 0 as no stagger, but a tatami fill’s range starts at 0.01. Such a key is declared again for the family that reads it its own way, in a declaration for that family’s stitch types (REQ-PRM-005). The registry’s audit lets a key have one declaration for each family at most, and an element reads the key as its family’s declaration says. find_for finds a family’s declaration, and find the first, which is the only one for every other key. The JSON Schema gives such a key any of its declarations’ schemas, each with the stitch types it applies to. The index lists the key once for each declaration, under its group, and the contract compares each declaration with the rows of Ink/Stitch’s for its stitch types. Every declaration of one of Ink/Stitch’s keys has an Ink/Stitch origin. The reference pages show a ledger entry that names the key with each declaration.

    An SVG node’s fill and its stroke read the same settings. A key of Ink/Stitch’s is never one of StitchCraft’s own, even for a family Ink/Stitch does not give it, because that family would read a value written for Ink/Stitch’s meaning. A node’s fill may also read a value that only its stroke’s declaration can read, such as a zigzag stroke’s 2 spacings read by a meander fill. Ink/Stitch sews that fill with its default, and StitchCraft would refuse it (DEV-PRM-001). The meander fill’s step (M7.8) chooses between the two.

Generated outputs

OutputGenerated byChecked bySince
The reference pages, the index and one page per declaration (docs/src/user/reference/params/*.md), with the deviations ledger’s entriescargo xtask docscargo xtask docs --check, which fails on a stale page and on a page left from a declaration since removed (cargo xtask docs deletes those)M3.1
docs/src/user/reference/params.schema.json: the parameters as JSON values (numbers, booleans, strings, arrays, null for empty), for editors, presets and JSON hostscargo xtask docssameM3.1
The StitchCraft column of the Ink/Stitch compatibility contractcargo xtask docssame, plus the cross-checks aboveM3.1
VectorCraft manifests, one for each stitch family’s module, with each parameter’s label, unit, section, help, help page and the values that show itat runtime from the registry, when VectorCraft asks a module for its manifest (stitchcraft-vc-plugin’s manifest and dialog)plug-in tests validate them against VectorCraft’s manifest rules, cargo xtask docs --check finds each help page, the contract tests install them in VectorCraft’s host, and the window probe checks the dialogs’ rowsM6.2, and the dialogs M6.10
SVG attribute read/writestitchcraft-svg iterates the registryround-trip property test REQ-PRM-003planned for M8
CLI parameter help (stitch params, --param key=value)at runtime from the registrytrycmd examplesplanned, in no step yet
proptest strategiesstitchcraft-testkit iterates the registryused by every generator property testplanned, in no step yet

Splitting for VectorCraft ABI v1

VectorCraft took at most 64 parameters per plug-in, with no groups or conditional visibility, until 0.14. The plug-in crate builds one plug-in per stitch family for that (stroke, satin, fill), each with only the parameters that apply to it; the shared lock/trim parameters are included in each. A test fails if any family’s manifest exceeds 64 parameters or 64 KiB, so adding a parameter that would break the limit is caught in the PR, not by a user. A parameter has one value, of a kind every release takes. A parameter that needs a text, a seed or several values keeps its default in VectorCraft (manifests). VectorCraft 0.14 adopted richer schemas from the RFC: labels, units, help, groups and conditions, texts and lists, and 256 parameters, 64 of them outside groups. Releases before it ignore the labels, units, help, groups and conditions, and refuse the rest. Each manifest gives a parameter the registry’s label, unit, help and group, and the conditions of its declaration’s applies_to and visible_when (M6.10, dialogs). A parameter’s help page is its entry on the reference page of its declaration, which ParamGroup::slug names for the docs generator and the plug-in alike. VectorCraft shows help as plain text, so stitchcraft_core::text::plain takes the marks out of the Markdown.

Presets

A preset is a named partial ParamSet ("denim", "knit, light") stored as a TOML file in a presets directory of the parameters crate. The same code as user input validates it, and a generated page documents it. In VectorCraft, users can also save appearance (including embroidery live effects) as Graphic Styles. Applying one copies its effect records into each object, and a design sews them (integration, REQ-VC-006).

Versioning and migration

The registry has a schema version. A breaking change (renamed key, changed unit) ships a migration function vN → vN+1 and a conformance case with an old-format input. Unknown keys from newer files are preserved on round trips where the host allows it, and reported once as SC-W0105.