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 (kindsmodule), so a field and its validation cannot disagree; CommonParams::SPECS(aconstslice ofParamSpec) andCommonParams::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
| Kind | Generators get | Validation | Ink/Stitch equivalent |
|---|---|---|---|
Length | Mm | finite, within range; a unit may follow the number with no space: mm, in, pt | float, unit mm |
OptionalLength | Option<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 |
OptionalLengthWithZero | Option<Mm> | as OptionalLength, but only empty text is empty, and 0 is a length, as Ink/Stitch reads a satin’s shortest stitch to split | float, unit mm, no default |
Angle | degrees, f64 | finite; normalized to (−180, 180] | float, unit deg |
BoundedAngle | degrees, f64 | finite, 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 angle | float, unit deg |
Percent | f64 | finite, within range | float, unit % |
Number | f64 | finite, within range, without a unit | float, no unit |
Count | u32 | whole, within range | int |
Toggle | bool | true 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 default | combo, dropdown |
Seed | Option<u64> | any; empty means “derive from the element”; text that is not a number is hashed (FNV-1a) | random_seed |
LengthList | Vec<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 Length | float, unit mm (each side) |
PercentPair | [f64; 2] | as LengthPair, in percent | float, unit % (each side) |
OptionalLengthPair, OptionalPercentPair | Option<[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 it | float, unit mm (each side) or % (each side), no default |
PercentList | Vec<f64> | 1 to 16 percentages, each in range (levels like "15 30") | float, unit %, read as a list |
CountList | Vec<u32> | 1–16 whole numbers, each in range | string/int lists |
OptionalAngleList | Option<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 angle | float, unit deg, read as a list, no default |
Text | String | stripped 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 used | string |
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
-
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. -
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 underparams, and each key’s reference entry shows it. -
New parameters follow the same style:
snake_case, a unit suffix for lengths (_mm), percentages (_percent) and angles where Ink/Stitch does (anglealone for row angles). -
Never reuse a removed key with a new meaning.
-
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, andconformance/inkstitch/check_params.pycompares 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. -
A key belongs to its stitch types. Ink/Stitch gives some keys to several elements with their own defaults:
max_stitch_length_mmis 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 withdefaults, as fills do formax_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 underx-stitchcraft.family_defaults. -
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_mmis 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 astaggersof 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_forfinds a family’s declaration, andfindthe 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
| Output | Generated by | Checked by | Since |
|---|---|---|---|
The reference pages, the index and one page per declaration (docs/src/user/reference/params/*.md), with the deviations ledger’s entries | cargo xtask docs | cargo 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 hosts | cargo xtask docs | same | M3.1 |
| The StitchCraft column of the Ink/Stitch compatibility contract | cargo xtask docs | same, plus the cross-checks above | M3.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 it | at 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’ rows | M6.2, and the dialogs M6.10 |
| SVG attribute read/write | stitchcraft-svg iterates the registry | round-trip property test REQ-PRM-003 | planned for M8 |
CLI parameter help (stitch params, --param key=value) | at runtime from the registry | trycmd examples | planned, in no step yet |
proptest strategies | stitchcraft-testkit iterates the registry | used by every generator property test | planned, 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.