spine-rigc 1.5.1 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -1306,6 +1306,18 @@ past that line would be emitted as Infinity, which JSON writes as `null`: `1e309
1306
1306
  both refused by the field they are in, before anything is compiled (§5.1). The cut
1307
1307
  manifest and the motion spec are held to the same line.
1308
1308
 
1309
+ 🔤 **Every field below holds one JSON type, and a value of another is refused.**
1310
+ Where a table says a field is a position, an angle, a scale, a mix, a count, a
1311
+ size or an index, it is a JSON **number** — `5`, never `"5"`; a flag is `true` or
1312
+ `false`, never `"true"` or `1`; a name or a path is a string; a list of vertices,
1313
+ uvs or triangles is an array of numbers, typed element by element. A JSON `null`
1314
+ is a value, not an absence, and is accepted only where a table below says what
1315
+ `null` means. The type of every key, shape by shape, is the table `RIG_TYPES` in
1316
+ [`src/rig.ts`](../src/rig.ts) (`MOTION_TYPES` in [`src/motion.ts`](../src/motion.ts),
1317
+ `MANIFEST_TYPES` in [`src/types.ts`](../src/types.ts)), and the refusal names the
1318
+ field, the type it found and the type it required (§5.1) — so that sentence, not a
1319
+ column restated beside each row here, is what to act on.
1320
+
1309
1321
  🚫 **Every example value below is invented.** Names, coordinates, vertex lists and
1310
1322
  payloads in this guide are written to illustrate a field, never copied out of a
1311
1323
  reference export — an example lifted from one would be handing an authoring agent an
@@ -1411,7 +1423,7 @@ it would simply be a second root.
1411
1423
  | `skin` | `BoneData.skinRequired` | `false` |
1412
1424
  | `color` | `rrggbbaa`, editor affordance | — |
1413
1425
  | `icon` | the editor's icon for this bone, e.g. `arrowsB`; editor affordance, no rendering effect. Copied through verbatim — no assertion checks the name, because the icon vocabulary is the editor's and an unknown one is not an error | — |
1414
- | `from` | **rigc extension** — take `x`/`y` (and optionally `rotation`) from a cut manifest | — |
1426
+ | `from` | **rigc extension** — take `x`/`y` from a cut manifest: `from.anchor`, `from.slotWindow` or `from.meshCenter`, exactly one. `from.rotation` is `"axis"` (the manifest's `axis.deg`, negated into y-up) or `"anchor"` (the third element of the named anchor, a screen-space facing angle); any other value is refused by name — it used to build as no rotation at all | — |
1415
1427
 
1416
1428
  ⚠️ Spine 4.0/4.1 called `inherit` **`transform`**. That old key still *loads* in 4.3
1417
1429
  and the inheritance silently falls back to Normal — assertion `A02` refuses it.
@@ -1791,7 +1803,7 @@ region, a trigger volume — that moves with the skeleton and draws nothing.
1791
1803
  | `vertexCount` | **required**, 3 or more. No default — the paragraph above is why |
1792
1804
  | `vertices` | the unweighted `x, y` run, in the slot bone's local space; or the index-encoded weighted run, behind `boneIndexing` |
1793
1805
  | `weights` | the by-name form: one entry per vertex, each a list of `{ "bone": …, "x": …, "y": …, "weight": … }`, each pair in that bone's local space. Never beside `vertices` |
1794
- | `boneIndexing` | `"name"` (the default) or `"raw"`, which opts a weighted `vertices` run into Spine's index encoding. `"raw"` beside `weights` is refused: `weights` always binds by name |
1806
+ | `boneIndexing` | `"name"` (the default) or `"raw"`, which opts a weighted `vertices` run into Spine's index encoding. `"raw"` beside `weights` is refused: `weights` always binds by name. Any other value — `"named"` included — is refused by name; it used to build as `"name"` |
1795
1807
  | `color` | `rrggbbaa`. **No default in the file** — omitted, the parser never calls `setFromString` and the attachment keeps the runtime's own colour. It is an editor affordance, the colour the polygon is drawn in there; rigc emits it verbatim when stated and leaves the key out when not |
1796
1808
 
1797
1809
  **Clipping polygon** ([Spine: clipping](http://esotericsoftware.com/spine-clipping)) —
@@ -1849,7 +1861,8 @@ is the wrap-around curve from `x = 180` back to `x = 0`, which nothing reads. Th
1849
1861
  `worldX = 45.000000`.
1850
1862
 
1851
1863
  The generators are `ring`, `ribbon`, `contour`, `grid` and `segments` (see
1852
- [`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
1864
+ [`src/mesh.ts`](../src/mesh.ts)), and a generator's `kind` is required: any other
1865
+ value, or none, is refused naming the five; the first two encode a deformation model rather
1853
1866
  than a table of numbers, which is why they are code invoked by data. `contour`
1854
1867
  and `grid` are geometry: they pin every vertex to the slot bone and exist to give
1855
1868
  a `deform` timeline (§4.12) somewhere to push. `segments` is both at once — a
@@ -5438,11 +5451,12 @@ out of it.)
5438
5451
 
5439
5452
  🔢 **A fourth surface is a pose that is not finite**, which `render` and `check`
5440
5453
  refuse before drawing anything, naming the bone or vertex and its value.
5441
- `A10_NO_NAN_AFTER_STEPPING` reads the same scan, so on a skeleton with an animation
5442
- `validate` gets there first and in the same words: a bone `rotation` of `1e309`, in
5443
- the setup pose or in a key, leaves the bone's world position finite and its matrix
5444
- `NaN`, and A10 names the bone and the term. What it does not see is a skeleton with
5445
- no animation (A10 SKIPs, so `render` is where that one is named) and a skin other
5454
+ `A10_NO_NAN_AFTER_STEPPING` reads the same scan, so `validate` gets there first and
5455
+ in the same words: a bone `rotation` of `1e309`, in the setup pose or in a key,
5456
+ leaves the bone's world position finite and its matrix `NaN`, and A10 names the bone
5457
+ and the term. That holds on a skeleton with no animation too — A10 reads its setup
5458
+ pose, which is the whole of what a static rig shows, and says on the stats line that
5459
+ it had nothing to step (`nanStepping=skipped`). What it does not see is a skin other
5446
5460
  than the default, which A10 does not pose. The sentence, and where to look for the
5447
5461
  number, is under *nothing to draw* in §3.4.
5448
5462
 
@@ -5569,7 +5583,9 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5569
5583
  | `a rig spec needs a non-empty "bones" array` | §3.2 — a rig with no bones has nothing to hang a slot on; every one has at least a root |
5570
5584
  | `a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)` | §3.3 — write `[]` for a rig that draws nothing. ⚠️ These three arrive only when the key is really absent: **misspelt**, it is the unknown-key refusal above, naming what you wrote |
5571
5585
  | `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
5572
- | `<file>: <where> is Infinity; a number in this file is finite at float32 precision, at most ±3.4028234663852886e+38, because the skeleton is written as float32 — …` (also `is -Infinity`, `is 1e+308`, and `is NaN` from a library caller) | §3 — state the number you meant, inside float32's range. One walk over the whole file, run right after the key scan and before any other rig-spec check, so a range rule never prints `NaN`. `<where>` is every number in the rig spec — a bone (`bone "hip" x`), a slot, a skin's attachment down to its arrays (`skin "default" slot "s" attachment "a" weights[3][1].x`), a constraint (`constraint "aim" mixRotate`), an event, `skeleton`, `invariants.meshTriangles` — and the dotted path in the cut manifest (`crop.h`) and in the motion spec (`` `animations.idle.tracks[0].keys[1].v[0]` ``, after the motion spec's own field checks, which keep their sentences). ⚠️ It sees numbers only: `"x": "NaN"` is a string and is not refused by this row — write a number as a JSON number |
5586
+ | `<file>: <where> is a string "5"; a number is required` (also `is a boolean true`, `is an array [1]`, `is null`, `is a number 7; a string is required`, `…; an array of numbers is required`, `…; a boolean (true or false) is required`, `…; a string or null is required`) | §3 / §4 — write the value as the JSON type its field holds: a number as a JSON number (`5`, not `"5"`), a flag as `true`/`false`, a name as a string. Every field of the three input files has one type, and a value of another is refused by the field before any reader does arithmetic on it — which it used to: `"x": "5"` built as `5`, `true` as `1` and `[1]` as `1`, and a `null` built too, all green. An array is typed element by element, so the refusal names the index (`weights[3][1].x`, `vertices[0]`). `null` is refused as `null` except where the field states it means something (`skeleton.width`/`height`, `skeleton.audio`, a slot's and a setup entry's `attachment`, a deform key's `vertices`, the manifest's `image`, `states`, `contact_depth` and `cap_containment_ceiling`). `<where>` is the rig spec's wording (`bone "hip" x`, `skin "default" slot "s" attachment "a" rotation`, `constraint "aim" mix`), the dotted path in the cut manifest (`crop.h`, `parts[2].offset[0]`), and the motion spec's own path (`` `animations."walk".ik[0].keys[0].mix` ``). In the rig spec and the manifest it runs right after the key scan and before every other check, so a field check that used to name a type (a mesh name that is not a string, a `lengths` entry spelt as a string) now reads as this row; in the motion spec it runs after that file's own field checks, which keep their sentences. A closed set of names is not a type, and is the next row's |
5587
+ | `<file>: <where> is "foo"; one of "name", "raw" — anything else was read as …` (also `bone "b" from.rotation is 5; one of "axis", "anchor"`, the manifest's `parts[1].mesh.kind is "foo"; one of "ring", "ribbon"`, and `skin "s" slot "x" attachment "a" generator.kind is "foo"; one of "ring", "ribbon", "contour", "grid", "segments"`, or `… generator.kind is absent; …`) | §3.2 / §3.4 — write one of the names listed. A field that holds one of a closed set of names is refused, with the set, when it holds anything else — a number too, since a number is not a name. Most such fields have a reader that refuses by name with its own sentence (`blend`, `inherit`, a constraint's `type`, `scaleY`); these four had none, and each built green or crashed: `boneIndexing` read as `"name"`, `from.rotation` as no rotation, the manifest's `mesh.kind` as a ribbon by one reader and a ring by two others, and a generator `kind` threw a `TypeError`. The sentence after the dash says what the value used to be read as, which is the build you had before this refusal. The first three run right after the type row's walk, before every other check; the generator's `kind` is refused earlier, by the key scan, because it chooses which keys the rest of the generator may hold |
5588
+ | `<file>: <where> is Infinity; a number in this file is finite at float32 precision, at most ±3.4028234663852886e+38, because the skeleton is written as float32 — …` (also `is -Infinity`, `is 1e+308`, and `is NaN` from a library caller) | §3 — state the number you meant, inside float32's range. One walk over the whole file, run right after the key scan and before any other rig-spec check, so a range rule never prints `NaN`. `<where>` is every number in the rig spec — a bone (`bone "hip" x`), a slot, a skin's attachment down to its arrays (`skin "default" slot "s" attachment "a" weights[3][1].x`), a constraint (`constraint "aim" mixRotate`), an event, `skeleton`, `invariants.meshTriangles` — and the dotted path in the cut manifest (`crop.h`) and in the motion spec (`` `animations.idle.tracks[0].keys[1].v[0]` ``, after the motion spec's own field checks, which keep their sentences). ⚠️ It sees numbers only: `"x": "NaN"` is a string, and the row above refuses it as one |
5573
5589
  | `two bones are called "X"` | bone names are the join key; rename one |
5574
5590
  | `two ik constraints are called "X" — a constraint resolves by name AND type (\`SkeletonData.findConstraint\`), so names are unique PER KIND: an ik and a transform constraint may share one, two of a kind may not` | §3.5 — rename one of the two. The kind in the sentence is the pair's own, so `two transform constraints are called "X"` is the same refusal on another kind; a name shared **across** kinds is not this error |
5575
5591
  | `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | §4.6 — the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
@@ -5795,7 +5811,7 @@ Fix A00 and run it again.
5795
5811
  | `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` |
5796
5812
  | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. There is no `spine-html` clause here — a placeholder is free to differ from the region its `path` names **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once |
5797
5813
  | `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
5798
- | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked **SKIP** when the skeleton carries no animation: the NaN is produced by stepping, and a static rig is never stepped — the same subject `A09` skips on |
5814
+ | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked. 🧍 **A skeleton with no animation is still read**: its setup pose is the whole of what it shows, so A10 reads it — the world transform and the vertices as above, and the bone's mode and the slot colours a stepped frame is read for, which on a static rig nothing else would reach — and names it `the setup pose` (`the setup pose: slot "x" colour is non-finite`). The stepping half had nothing, and a PASS row carries no detail, so the stats line says so: `nanStepping=skipped`, beside `animations=0`. `A09` does not follow — a declared duration is a fact about an animation and nothing else, so a static rig still gives it nothing. **SKIP** only when the skeleton carries **no bone and no animation**: there is then no pose to read and nothing to step |
5799
5815
  | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
5800
5816
  | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
5801
5817
  | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
@@ -8936,6 +8952,21 @@ quarter alone found 234, losing the native arms #877 was made for. The one lost
8936
8952
  margin reading again: the blurred 32 px front shin (walk) now reports its truth as its
8937
8953
  best (0.07 px, 0.00968), with its old 0.51 px answer 0.0045 above it.
8938
8954
 
8955
+ Issue #892 asked whether the coarse field, which keeps **one rotation per anchor
8956
+ cell**, loses the truth more often than on that post, and the answer is no. The
8957
+ tool now records, per trial, the coarse seed nearest the truth, the rotation the
8958
+ field kept for its cell and the field's own score there at the truth's rotation.
8959
+ Within 2.4 px of the truth that seed is a half turn off on 2 of the 238 found
8960
+ trials (a mouth at 48 px, both found anyway) and on none of the 6 misses, and
8961
+ every failed trial whose nearest seed is a quarter or half turn off but one is a
8962
+ flat part, none of them a miss. On the post itself, the upright rung scores 0.305
8963
+ at that cell against the half turn's 0.168, so a second rotation per cell would
8964
+ need a band 1.8 times wide to carry it. Re-gridding the rotation at the coarse level
8965
+ instead of the next one took the grid to 236 found (7 gained, 9 lost), and
8966
+ suppressing minima only within a rotation family took it to 231 (2 gained, 10
8967
+ lost). Both lose trials far from the truth, so neither shipped and the field is
8968
+ unchanged.
8969
+
8939
8970
  ⚠️ **What this is and is not.** The grid is not a reliability line: six native cells
8940
8971
  place 14 or 15 of 15 and the trials at `detail` 3 and over place 65 of 66, but the
8941
8972
  goggles still place 13 of 42, and a threshold read off two dozen parts of one figure
@@ -8962,6 +8993,8 @@ no miss at all, and `PO23` places the 48 px fist #865 was made for; `PO28` and
8962
8993
  cutoff, and `PO26` holds every polish to strictly descending residuals, read through
8963
8994
  the same trace. `PO31` places §6's post at the old placement from the page's own
8964
8995
  input bytes, and `PO32` holds it to taking the quarter-rung escape and reading the same twice.
8996
+ `PO33` holds the coarse row the tool reads rotations from to the field each seed was
8997
+ picked from.
8965
8998
 
8966
8999
  ## 12. Reading the half of that picture `pose` refuses — `rigc chainfit`
8967
9000
 
package/docs/MOTION.md CHANGED
@@ -614,7 +614,7 @@ quietly, and every in-between hung off it swings about the wrong centre.
614
614
  further than you would accept as a bone position, treat it as the next case.
615
615
  - **Δ < 20°** — ⛔ **do not use the solve.** Take the default below and **say in your
616
616
  log that the pivot was defaulted and why** — the number, not the word: *"flag hinge
617
- defaulted; relative rotation changed 10.8° between the two poses, amplification 5.3×"*.
617
+ defaulted; relative rotation changed 10.5° between the two poses, amplification 5.5×"*.
618
618
  - **Δ = 0** (a part that only translates, or a rigid pair) — the pivot is not a
619
619
  quantity the pictures contain at all. Default it.
620
620
 
@@ -632,10 +632,13 @@ actually see, which is why they beat an ill-conditioned solve:
632
632
  exactly why it gets said out loud.
633
633
 
634
634
  📌 **Then check the default the cheap way: it should agree with itself across the
635
- poses.** Take your chosen pivot point into the parent's local frame once per pose. A
636
- real hinge is *fixed* there, so the readings should differ by about your placement
637
- noise; if they differ by several pixels, the point you picked is not the hinge. §6
638
- runs this check on a real pair and gets 0.23 px.
635
+ poses.** Take your chosen pivot point into the parent **bone's** frame once per pose —
636
+ origin at the parent's own pivot, not at the centre of its image. A real hinge is
637
+ *fixed* there, so the readings should differ by about your placement noise; if they
638
+ differ by several pixels, the point you picked is not the hinge. §6 runs this check on
639
+ a real pair and gets 0.25 px. ⚠️ The check cannot see the origin: measured from the
640
+ wrong one, the readings agree exactly as well, because a constant offset agrees with
641
+ itself. §6 shows both, and what the wrong one draws.
639
642
 
640
643
  ### 3.10 📗 Secondary action, and 🧩 what it costs here
641
644
 
@@ -817,12 +820,15 @@ rigc pose
817
820
  ambiguous: part is 14x96 px (span 93.2 frame px, opaque 1) with texture 0.0146, detail 1.406; 43 candidate(s), best 0.0593, next 0.0687, spread 0.0095 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; either the frame holds more than one place this part fits, or every candidate missed the true one — a narrower --scale or --rotation window around what you know of the part tells the two apart
818
821
  ```
819
822
 
820
- ⚠️ **The post came back at scale 0.690 with two alternates trailing it down to 0.500 —
821
- the window's own floor.** That is §2.2's caveat in the open: the post is a long part with
822
- most of its area in one colour, so a shrunken copy sitting inside the real post explains
823
- those pixels nearly as well, and three near-equal optima marching toward the edge of the
824
- window is what that looks like. The arm and the flag agree on ≈0.96, which says the
825
- picture is at the art's own resolution. ⇒ Narrow, and say so:
823
+ ⚠️ **The post came back `AMBIG`: a best at scale 0.971 and an alternate at 0.691, 7.3 px
824
+ higher, trailing it by a spread of 0.0095.** That is §2.2's caveat in the open. The post
825
+ is a long part with most of its area in one colour, so a shrunken copy sitting inside the
826
+ real post explains those pixels nearly as well, and the report's own `ambiguous:` line
827
+ says size and texture do not account for how close the two come. The report cannot say
828
+ which one is true, and it says so: it names both and asks for a narrower window. The
829
+ picture can. The arm and the flag read 0.970 and 0.955, so the picture is at the art's
830
+ own resolution, and a post at 0.69 would be drawn at about 0.7 of the size of everything
831
+ bolted to it. ⇒ Narrow, and say so:
826
832
 
827
833
  ```bash
828
834
  rigc pose --images parts --frame poseA.png --scale 0.85,1.2 --out poseA.json
@@ -859,18 +865,20 @@ rigc pose
859
865
 
860
866
  Three things to read out of that pair, none of which is a score:
861
867
 
862
- - **Scale.** All six readings sit in 0.955–0.995 — a spread of about 4 %, which is the
863
- method's own floor rather than six different scales. ⇒ Take the pictures as being at
864
- the art's own resolution and author the rig in **part pixels**, so no scaling appears
865
- in the spec at all. Say that this is what the spread was read as.
868
+ - **Scale.** All six readings sit in 0.956–0.995, a spread of about 4 %. The one part
869
+ read twice at one place, the post, reads 0.971 and 0.995 — 2.5 % apart on a part that
870
+ does not move — so a 4 % spread is the size of the method's own repeatability rather
871
+ than six different scales. ⇒ Take the pictures as being at the art's own resolution
872
+ and author the rig in **part pixels**, so no scaling appears in the spec at all. Say
873
+ that this is what the spread was read as.
866
874
  - **Draw order, from `unexplained`.** The post reads **16 %** unexplained in pose A and
867
875
  **9 %** in pose B, at placements that barely move — §2.2's occlusion signature. The arm
868
876
  crosses more of the post in pose A, so the arm is **in front of** the post. The flag
869
- reads 0 % and 1 %: nothing covers it, so it is **in front of** the arm. ⇒ Slots in
877
+ reads 0 % in both: nothing covers it, so it is **in front of** the arm. ⇒ Slots in
870
878
  the order `post`, `arm`, `flag` (AUTHORING R4 — the slots array *is* the draw order,
871
879
  and there is nowhere else in the file to say it).
872
880
  - **The post does not move**, so its two readings are two measurements of one number:
873
- x 79.9/80.0 and y 148.6/148.0. ⇒ Use the mean, **(79.95, 148.3)**, and treat the 0.6 px
881
+ x 79.9/80.0 and y 148.5/148.0. ⇒ Use the mean, **(79.95, 148.25)**, and treat the 0.5 px
874
882
  disagreement as the noise floor for every other number on the page.
875
883
 
876
884
  ### 3. Convert, and derive the rig
@@ -880,33 +888,59 @@ Three things to read out of that pair, none of which is a score:
880
888
 
881
889
  | | pose A, Spine world | pose B, Spine world |
882
890
  | --- | --- | --- |
883
- | `post` | x 79.9 · y 51.4 · rot 0.1° | x 80.0 · y 52.0 · rot 0.0° |
884
- | `arm` | x 91.9 · y 77.2 · rot −61.9° | x 102.6 · y 105.2 · rot 18.2° |
885
- | `flag` | x 104.7 · y 43.9 · rot −84.3° | x 137.4 · y 114.0 · rot 6.6° |
891
+ | `post` | x 79.9 · y 51.5 · rot 0.1° | x 80.0 · y 52.0 · rot 0.0° |
892
+ | `arm` | x 91.9 · y 77.2 · rot −61.9° | x 102.5 · y 105.2 · rot 18.3° |
893
+ | `flag` | x 104.7 · y 43.9 · rot −84.1° | x 137.4 · y 114.0 · rot 6.6° |
886
894
 
887
895
  **The shoulder, by §3.9's solve.** The arm's screen rotation changes from 61.9° to
888
- −18.2°, so **Δ = 80.1°** — well inside the *solve it* band, `|det| = 4·sin²(40.05°) =
889
- 1.656`, amplification 0.78×. Solving the 2×2 puts the fixed point at frame **(80.59,
890
- 102.44)**, reconstructing identically from both poses, and the offset lands at arm-image
891
- pixel **(6.71, 7.38)** — inside the arm's own hub, which is where a hub is for. In Spine
892
- world that is (80.59, 97.56); in the post bone's local space, **(0.64, 45.86)**.
896
+ −18.3°, so **Δ = 80.2°** — well inside the *solve it* band, `|det| = 4·sin²(40.1°) =
897
+ 1.660`, amplification 0.78×. Solving the 2×2 puts the fixed point at frame **(80.57,
898
+ 102.51)**, reconstructing identically from both poses, and the offset lands at arm-image
899
+ pixel **(6.76, 7.43)** — inside the arm's own hub, which is where a hub is for. In Spine
900
+ world that is (80.57, 97.49); in the post bone's local space, **(0.62, 45.74)**. That
901
+ point is the `arm` bone's origin, and it is the origin of everything measured in the
902
+ arm's frame below.
893
903
 
894
904
  **The flag hinge, by §3.9's default — and this is the interesting one.** The relative
895
- angle across that joint is 22.4° in pose A and 11.6° in pose B, so **Δ = 10.8°**:
896
- `|det| = 0.0354`, amplification **5.3×**, comfortably inside the *do not use the solve*
897
- band. Run it anyway, to see what it would have cost — it returns arm-local **(25.64,
898
- 1.32)**, exactly as confidently as the shoulder did. The default instead: the flag's art
899
- draws its hinge as a dark hoist strip down one edge, whose centre is flag-image
900
- **(2.5, 10)**; carrying that point through each pose's placement into arm-local gives
901
- **(24.77, 0.01)** and **(24.54, 0.20)** — §3.9's self-agreement check, and the two poses
902
- agree to **0.23 px**. ⇒ Take the mean, **(24.66, 0)**, and write in the log that the
903
- pivot was **defaulted**, with the number: *relative rotation changed 10.8°,
904
- amplification 5.3×, ill-conditioned solve declined*.
905
+ angle across that joint is 22.2° in pose A and 11.7° in pose B, so **Δ = 10.5°**:
906
+ `|det| = 0.0335`, amplification **5.5×**, comfortably inside the *do not use the solve*
907
+ band. Run it anyway, to see what it would have cost — it puts the hinge at **(49.15,
908
+ −0.38)** in the arm bone's frame, exactly as confidently as the shoulder did. The default
909
+ instead: the flag's art draws its hinge as a dark hoist strip down one edge, whose centre
910
+ is flag-image **(2.5, 10)**. Carry that point through each pose's placement into the
911
+ **arm bone's frame** — origin at the shoulder pivot above, x along the arm, y up: take
912
+ the point's frame position, subtract the pivot's, turn it back through the arm's screen
913
+ rotation and flip y once. Carried unrounded, printed rounded:
914
+
915
+ | | pose A | pose B |
916
+ | --- | --- | --- |
917
+ | the hinge in the frame, through the flag's placement | (103.52, 144.66) | (125.98, 87.32) |
918
+ | minus the pivot, (80.57, 102.51) | (22.94, 42.15) | (45.40, −15.18) |
919
+ | turned by −61.9° and +18.3°, y flipped | **(47.99, 0.38)** | **(47.87, 0.16)** |
920
+
921
+ That is §3.9's self-agreement check, and the two poses agree to **0.25 px**. ⇒ Take the
922
+ mean, **(47.93, 0.27)**, write it as **(47.93, 0)** — the y is inside the 0.5 px noise
923
+ floor — and write in the log that the pivot was **defaulted**, with the number:
924
+ *relative rotation changed 10.5°, amplification 5.5×, ill-conditioned solve declined*.
925
+
926
+ ⚠️ **Which origin the arm's frame has is the whole of this step, and the check above
927
+ cannot tell you.** A placement reports where the part image's **centre** lands, so the
928
+ nearest frame to hand is the one centred on the arm image — and measured from there the
929
+ same hinge reads (24.76, −0.05) and (24.64, −0.27). Those agree to 0.25 px just as well,
930
+ because a constant offset agrees with itself. They are short by 23.24, the arm
931
+ attachment's own offset below, and a flag hung at x 24.70 draws over the middle of the
932
+ arm: built and rendered on the pictures' grid, its flag sits 23.5 px from where
933
+ `poseA.png` draws it in frame 0 and 23.2 px from `poseB.png` at the end. With x 47.93 it
934
+ sits 0.4 px and 0.2 px off, inside the noise floor. A bone sits at its **pivot**, and its
935
+ children's offsets are measured from there.
905
936
 
906
937
  ⭐ Worth pausing on, because it is what §3.9 is for: **the two solves are
907
938
  indistinguishable from the inside.** Both are exact, both reconstruct to a point that
908
- agrees between the poses, and no residual anywhere in either pose report moves. The only
909
- thing separating them is Δ, which is arithmetic you can do before you trust either.
939
+ agrees between the poses, and no residual anywhere in either pose report moves. Here
940
+ the declined one lands 1.4 px from the drawn hinge — the 5.5× amplification applied to
941
+ about a quarter-pixel of placement noise — and nothing in either answer says which of the
942
+ two is the one to trust. The only thing separating them is Δ, which is arithmetic you
943
+ can do before you trust either.
910
944
 
911
945
  `semaphore.rig.json` — complete, nothing trimmed:
912
946
 
@@ -918,9 +952,9 @@ thing separating them is Δ, which is arithmetic you can do before you trust eit
918
952
  "skeleton": { "width": 160, "height": 200 },
919
953
  "bones": [
920
954
  { "name": "root" },
921
- { "name": "post", "parent": "root", "x": 79.95, "y": 51.7 },
922
- { "name": "arm", "parent": "post", "x": 0.64, "y": 45.86 },
923
- { "name": "flag", "parent": "arm", "x": 24.66, "y": 0 }
955
+ { "name": "post", "parent": "root", "x": 79.95, "y": 51.75 },
956
+ { "name": "arm", "parent": "post", "x": 0.62, "y": 45.74 },
957
+ { "name": "flag", "parent": "arm", "x": 47.93, "y": 0 }
924
958
  ],
925
959
  "slots": [
926
960
  { "name": "post", "bone": "post", "attachment": "post" },
@@ -930,7 +964,7 @@ thing separating them is Δ, which is arithmetic you can do before you trust eit
930
964
  "skins": {
931
965
  "default": {
932
966
  "post": { "post": { "image": "post.png" } },
933
- "arm": { "arm": { "image": "arm.png", "x": 23.29 } },
967
+ "arm": { "arm": { "image": "arm.png", "x": 23.24 } },
934
968
  "flag": { "flag": { "image": "flag.png", "x": 11.5 } }
935
969
  }
936
970
  }
@@ -940,17 +974,19 @@ thing separating them is Δ, which is arithmetic you can do before you trust eit
940
974
  The two attachment offsets are the last of the arithmetic. A bone sits at its pivot and
941
975
  the placement told you where the image's **centre** goes, so the offset is the gap
942
976
  between them, in the bone's own axes with y flipped once: the arm's pivot is at image
943
- (6.71, 7.38) and its centre at (30, 7), giving **x 23.29** (the y term is 0.38, inside
944
- the 0.6 px noise floor, so it is not written); the flag's hinge is at image (2.5, 10) and
977
+ (6.76, 7.43) and its centre at (30, 7), giving **x 23.24** (the y term is 0.43, inside
978
+ the 0.5 px noise floor, so it is not written); the flag's hinge is at image (2.5, 10) and
945
979
  its centre at (14, 10), giving **x 11.5** exactly. Bone rotations are left off, which
946
980
  means *as drawn* — the arm plate is drawn horizontal and the post vertical, so the poses
947
981
  are entirely the motion spec's business.
948
982
 
949
983
  ### 4. In-between it
950
984
 
951
- Duration 0.55 s, proposed (§3.3). Three easings (§3.4). The arm is the driver; the flag
952
- is the next link out and it is light, so §3.7's table puts its extreme **+20–35 %** after
953
- the arm's and gives it an overshoot. *"Snap"* buys an anticipation (§3.6) and an
985
+ Duration 0.55 s, proposed (§3.3). Three easings (§3.4). The arm is the driver. The flag
986
+ is the last thing on it and it is cloth, which is the row §3.7's table gives to *a
987
+ pennant* — not the *next link out* row, because nothing hangs off the flag — so its
988
+ extreme lands **+20–35 %** after the arm's, it overshoots, and it settles last with one
989
+ crossing. *"Snap"* buys an anticipation (§3.6) and an
954
990
  overshoot (§3.8). The post is planted: ⛔ **no timeline**.
955
991
 
956
992
  `semaphore.motion.json` — complete, nothing trimmed:
@@ -976,19 +1012,19 @@ overshoot (§3.8). The post is planted: ⛔ **no timeline**.
976
1012
  "keys": [
977
1013
  { "t": 0, "v": [-61.9], "ease": "gather" },
978
1014
  { "t": 0.07, "v": [-66.4], "ease": "charge" },
979
- { "t": 0.32, "v": [24.6], "ease": "settle" },
980
- { "t": 0.55, "v": [18.2] }
1015
+ { "t": 0.32, "v": [24.7], "ease": "settle" },
1016
+ { "t": 0.55, "v": [18.3] }
981
1017
  ]
982
1018
  },
983
1019
  {
984
1020
  "bone": "flag",
985
1021
  "property": "rotate",
986
1022
  "keys": [
987
- { "t": 0, "v": [-22.4], "ease": "gather" },
1023
+ { "t": 0, "v": [-22.2], "ease": "gather" },
988
1024
  { "t": 0.09, "v": [-30.1], "ease": "charge" },
989
- { "t": 0.38, "v": [-3.8], "ease": "settle" },
990
- { "t": 0.48, "v": [-15.4], "ease": "settle" },
991
- { "t": 0.55, "v": [-11.6] }
1025
+ { "t": 0.47, "v": [-3.8], "ease": "settle" },
1026
+ { "t": 0.51, "v": [-15.4], "ease": "settle" },
1027
+ { "t": 0.55, "v": [-11.7] }
992
1028
  ]
993
1029
  }
994
1030
  ]
@@ -1002,12 +1038,12 @@ which is which is worth being able to point at:
1002
1038
 
1003
1039
  | Key | Where it came from |
1004
1040
  | --- | --- |
1005
- | arm `t: 0` = −61.9, flag `t: 0` = −22.4 | **given** — pose A, converted. Untouched, per §3.6 |
1006
- | arm `t: 0.55` = 18.2, flag `t: 0.55` = −11.6 | **given** — pose B. The flag's is `6.6 − 18.2`: both world rotations converted first, then differenced, because a child's track carries a **local** rotation under a rotated parent |
1041
+ | arm `t: 0` = −61.9, flag `t: 0` = −22.2 | **given** — pose A, converted. Untouched, per §3.6 |
1042
+ | arm `t: 0.55` = 18.3, flag `t: 0.55` = −11.7 | **given** — pose B. The flag's is `6.6 − 18.3`: both world rotations converted first, then differenced, because a child's track carries a **local** rotation under a rotated parent |
1007
1043
  | arm `t: 0.07` = −66.4 | §3.6 — 4.5° against an 80° excursion (5.6 %), at 13 % of the duration |
1008
- | arm `t: 0.32` = 24.6 | §3.8 — 6.4° past the end value (8 %), at 58 % of the duration |
1009
- | flag `t: 0.09`, `t: 0.38` | §3.7 — the flag's extreme lands at 69 % against the arm's 58 %, an offset of **+11 %**, and it drags the other way first |
1010
- | flag `t: 0.48` = −15.4 | §3.7's *one crossing* for a loose part: it comes back past its own end value before settling |
1044
+ | arm `t: 0.32` = 24.7 | §3.8 — 6.4° past the end value (8 %), at 58 % of the duration |
1045
+ | flag `t: 0.09`, `t: 0.47` | §3.7 — the flag's extreme lands at 85 % of the duration against the arm's 58 %, an offset of **+27 %**, the middle of its row's band, and it drags the other way first. Not the top of the band: at 35 % (`t: 0.51`) the crossing and the settle below would share 0.04 s, and the 24 fps render would still show the flag at its extreme one frame before the last |
1046
+ | flag `t: 0.51` = −15.4 | §3.7's *one crossing* for a loose part: it comes back past its own end value before settling, keyed halfway between its extreme and the end |
1011
1047
  | the three easings | §3.4 — one that gathers, one that arrives slowly, one symmetric. The **last key of each track carries no easing**, because there is nothing after it to ease towards (AUTHORING §4.5) |
1012
1048
  | the post's absent track | §3.7 — a planted part gets no timeline |
1013
1049
 
@@ -1029,20 +1065,26 @@ rigc render --candidate spine --fps 24 --max 200
1029
1065
  ```
1030
1066
 
1031
1067
  ```
1032
- .. 111x200px at 24 fps, 1 set(s) -> …/semaphore/render
1068
+ .. 134x200px at 24 fps, 1 set(s) -> …/semaphore/render
1033
1069
  .. raise 14 frame(s), 0.542s + contact.png -> …/semaphore/render/raise@24fps
1034
1070
  ```
1035
1071
 
1036
1072
  Open `render/raise@24fps/contact.png` **before anything else** — fourteen frames as one
1037
1073
  grid, and spacing is a comparison across frames rather than a property of any one of
1038
- them. What to check on it, and it is not a score: frame 0 is pose A, the last frame is
1039
- pose B, the anticipation dips *after* frame 0, and the flag's extreme is visibly later
1040
- than the arm's.
1074
+ them. What to check on it, and it is not a score: frame 0 is pose A — the flag hanging off
1075
+ the arm's collar, not over its middle — the last frame is pose B, the anticipation dips
1076
+ *after* frame 0, and the flag's extreme is visibly later than the arm's: the arm peaks at
1077
+ frame 8 while the flag is still closing on the arm's line, it keeps closing through
1078
+ frame 11 as the arm eases back, and at frame 12 it drops past its resting angle.
1041
1079
 
1042
1080
  🚫 **Do not run `rigc check` against `poseA.png` and `poseB.png`.** Two pictures are not
1043
- a frame set, and more to the point the ends are **given conditions** the spec states by
1044
- construction — measuring how near it got to them measures the pose estimator, not the
1045
- movement. §7.
1081
+ a frame set: `check` reads the second as the frame 1/12 s in, at its default rate. And
1082
+ the ends' **rotations** are given conditions the spec states by construction, so a
1083
+ number for how near the movement got to them is a number about the pose estimator. What
1084
+ the pictures can still catch is §3's arithmetic, because every **offset** in the rig was
1085
+ derived rather than given, and a wrong one moves an end pose off its picture while every
1086
+ key stays exact. That is what the frame-0 check above is for, and it is a look, not a
1087
+ score. §7.
1046
1088
 
1047
1089
  ### 6. Spread, and ask
1048
1090
 
@@ -1061,10 +1103,10 @@ with **these two fields replaced** and `spec`, `archetype` and `cut` unchanged:
1061
1103
  "tracks": [
1062
1104
  { "bone": "arm", "property": "rotate", "keys": [
1063
1105
  { "t": 0, "v": [-61.9], "ease": "drive" },
1064
- { "t": 0.55, "v": [18.2] } ] },
1106
+ { "t": 0.55, "v": [18.3] } ] },
1065
1107
  { "bone": "flag", "property": "rotate", "keys": [
1066
- { "t": 0, "v": [-22.4], "ease": "drive" },
1067
- { "t": 0.55, "v": [-11.6] } ] }
1108
+ { "t": 0, "v": [-22.2], "ease": "drive" },
1109
+ { "t": 0.55, "v": [-11.7] } ] }
1068
1110
  ]
1069
1111
  }
1070
1112
  }
@@ -1077,20 +1119,21 @@ rigc vote --candidate spine --candidate spine-b
1077
1119
 
1078
1120
  ```
1079
1121
  rigc vote
1080
- .. ballot 15b3f32bbbce77be
1122
+ .. ballot 862f444c58d10e3e
1081
1123
  .. animation raise
1082
- .. A sha256:2bc29990faf6… 3 page(s), 0.4 KiB <- …/semaphore/spine/skeleton.json
1083
- .. B sha256:1bec4ce801ad… 3 page(s), 0.4 KiB <- …/semaphore/spine-b/skeleton.json
1124
+ .. A sha256:c49e458df44e… 3 page(s), 0.4 KiB <- …/semaphore/spine/skeleton.json
1125
+ .. B sha256:d1bda24d7c36… 3 page(s), 0.4 KiB <- …/semaphore/spine-b/skeleton.json
1084
1126
  .. the page shows A/B and nothing else — the paths above are in its manifest, never on the screen
1085
- rigc: wrote …/semaphore/ballot.html (22.5 KiB — open it in a browser)
1086
- rigc: then record the saved vote with rigc vote --record vote-15b3f32bbbce77be.json --ballot …/semaphore/ballot.html
1127
+ .. embedded every candidate's skeleton, atlas and page(s) as data URIs; the player itself loads from unpkg (@4.3.*), so the first open needs a network
1128
+ rigc: wrote …/semaphore/ballot.html (22.3 KiB — open it in a browser)
1129
+ rigc: then record the saved vote with rigc vote --record vote-862f444c58d10e3e.json --ballot …/semaphore/ballot.html
1087
1130
  ```
1088
1131
 
1089
1132
  A person opens that page, watches two loops, picks one, and saves the small JSON it hands
1090
1133
  them. Then:
1091
1134
 
1092
1135
  ```bash
1093
- rigc vote --record vote-15b3f32bbbce77be.json --ballot ballot.html
1136
+ rigc vote --record vote-862f444c58d10e3e.json --ballot ballot.html
1094
1137
  ```
1095
1138
 
1096
1139
  ```
@@ -1101,8 +1144,8 @@ rigc vote --record vote-15b3f32bbbce77be.json --ballot ballot.html
1101
1144
  PASS V04_CHOICE_IS_ON_THE_BALLOT
1102
1145
  PASS V05_REASON_CODE_FITS_THE_CHOICE
1103
1146
  PASS V06_NOT_ALREADY_RECORDED
1104
- .. winner A = sha256:2bc29990faf6e24c953cabd09f87cdfc2edd3885a1486f3d3fdb4a88a81439c8, reason code preferred
1105
- .. coverage 2 candidate(s): A=sha256:2bc29990faf6… B=sha256:1bec4ce801ad…
1147
+ .. winner A = sha256:c49e458df44efd277aa6e50ff8fa1962c1211c7617079d63fbc4cf8008eaf0c5, reason code preferred
1148
+ .. coverage 2 candidate(s): A=sha256:c49e458df44e… B=sha256:d1bda24d7c36…
1106
1149
  rigc: appended line 1 to …/semaphore/votes.jsonl
1107
1150
  ```
1108
1151
 
@@ -1130,9 +1173,13 @@ that the ends are stateable by construction (`pose`), that the file is checkable
1130
1173
  (`vote`).
1131
1174
 
1132
1175
  🚫 **No scoring of end-pose reach, and nothing here to add one to.** At L1 and L2 the
1133
- end poses are given conditions; a number saying how near the animation got to them is a
1134
- number about the pose estimator. This is why §6 does not run `check` on the two pictures
1135
- and why no threshold, tolerance or pass bar appears anywhere in this document. The
1176
+ end poses are given conditions, and what a picture gives is a **pose**: the rotations are
1177
+ `pose`'s readings, which the spec states by construction, so a number for how near the
1178
+ animation got to *them* is a number about the pose estimator. The **offsets** are the
1179
+ other half — derived from the pictures rather than given — and a rig can miss its
1180
+ picture by a wrong one with every key exact. Catching that is a look, not a score (§6.5's
1181
+ frame-0 check). This is why §6 does not run `check` on the two pictures and why no
1182
+ threshold, tolerance or pass bar appears anywhere in this document. The
1136
1183
  residuals in a `pose` report are trust signals about *placements*, and AUTHORING §11.1
1137
1184
  says the same from the instrument's side.
1138
1185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, round-trips it through spine-core and named assertions, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/compile.ts CHANGED
@@ -38,7 +38,9 @@ import { parseJsonWithPosition } from './json-position.ts';
38
38
  // The "did you mean" list on a missing atlas region, from the one implementation
39
39
  // of it — the same search serves `refuseUnknownKeys`, and a second copy here with
40
40
  // a threshold edited is how such a pair drifts apart.
41
- import { dottedPath, nearMisses, refuseNumbersTheFileCannotCarry } from './keys.ts';
41
+ import { dottedPath, nearMisses, refuseNumbersTheFileCannotCarry, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
42
+ import type { ShapeVisit } from './keys.ts';
43
+ import { MANIFEST_ENUMS, MANIFEST_TYPES } from './types.ts';
42
44
  import { inEditorKeyOrder, withoutParserDefaults } from './keyorder.ts';
43
45
  import { EVERY_GLOBAL_PHYSICS, parseMotionSpec } from './motion.ts';
44
46
  import {
@@ -1996,6 +1998,30 @@ export function droppedStateReason(dropped: DroppedState): string {
1996
1998
  return dropped.why ?? `no PNG at ${dropped.path}`;
1997
1999
  }
1998
2000
 
2001
+ /**
2002
+ * Every node of a cut manifest that `MANIFEST_TYPES` has a row for, in document
2003
+ * order, named by its dotted path (`parts[2].mesh.center[1]`). A node that is
2004
+ * not an object is skipped: whether it should have been one is the reader's
2005
+ * refusal, and the rows below it have nothing to type.
2006
+ */
2007
+ function manifestShapeVisits(manifest: unknown): ShapeVisit[] {
2008
+ const visits: ShapeVisit[] = [];
2009
+ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
2010
+ const at = (node: unknown, shape: keyof typeof MANIFEST_TYPES, path: Array<string | number>): void => {
2011
+ if (isRecord(node)) visits.push({ node, shape, name: (tail) => dottedPath([...path, ...tail]) });
2012
+ };
2013
+ if (!isRecord(manifest)) return visits;
2014
+ at(manifest, 'FaceManifest', []);
2015
+ for (const field of ['crop', 'axis', 'stroke', 'roi'] as const) at(manifest[field], `FaceManifest.${field}`, [field]);
2016
+ for (const [i, part] of (Array.isArray(manifest.parts) ? manifest.parts : []).entries()) {
2017
+ at(part, 'FaceManifestPart', ['parts', i]);
2018
+ if (!isRecord(part)) continue;
2019
+ at(part.mesh, 'FaceManifestMesh', ['parts', i, 'mesh']);
2020
+ if (isRecord(part.mesh)) at(part.mesh.bias, 'FaceManifestMesh.bias', ['parts', i, 'mesh', 'bias']);
2021
+ }
2022
+ return visits;
2023
+ }
2024
+
1999
2025
  /**
2000
2026
  * Compile a rig spec and a motion spec into Spine 4.3 skeleton data.
2001
2027
  *
@@ -2034,6 +2060,15 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2034
2060
  // bone positions and the stage: a `crop.h` of 1e309 built as `"height": null`
2035
2061
  // before issue #881. The rig spec's walk is `parseRigSpec`'s; this is the
2036
2062
  // same rule over the other file whose numbers reach the skeleton.
2063
+ // Its values of the wrong type first (issue #890): `crop.h: "806"` built
2064
+ // green, the string coerced by whatever arithmetic read it.
2065
+ if (manifest !== null && manifestPath !== null) {
2066
+ const visits = manifestShapeVisits(manifest);
2067
+ refuseValuesOfTheWrongType(visits, MANIFEST_TYPES, manifestPath);
2068
+ // Then a name outside a stated set (issue #900): `mesh.kind: "foo"` was
2069
+ // read as a ribbon by one reader and a ring by two others.
2070
+ refuseValuesOutsideTheirSet(visits, MANIFEST_TYPES, MANIFEST_ENUMS, manifestPath);
2071
+ }
2037
2072
  if (manifest !== null && manifestPath !== null) refuseNumbersTheFileCannotCarry(manifest, manifestPath, dottedPath);
2038
2073
 
2039
2074
  // The rig spec names its own path in every message `parseRigSpec` throws (its