spine-rigc 1.5.1 → 1.5.2

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
@@ -5569,7 +5581,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5569
5581
  | `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
5582
  | `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
5583
  | `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 |
5584
+ | `<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 (`blend`, `inherit`, a constraint's `type`, a generator's `kind`) keeps its own refusal, which lists the names that exist |
5585
+ | `<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
5586
  | `two bones are called "X"` | bone names are the join key; rename one |
5574
5587
  | `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
5588
  | `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 |
@@ -8936,6 +8949,21 @@ quarter alone found 234, losing the native arms #877 was made for. The one lost
8936
8949
  margin reading again: the blurred 32 px front shin (walk) now reports its truth as its
8937
8950
  best (0.07 px, 0.00968), with its old 0.51 px answer 0.0045 above it.
8938
8951
 
8952
+ Issue #892 asked whether the coarse field, which keeps **one rotation per anchor
8953
+ cell**, loses the truth more often than on that post, and the answer is no. The
8954
+ tool now records, per trial, the coarse seed nearest the truth, the rotation the
8955
+ field kept for its cell and the field's own score there at the truth's rotation.
8956
+ Within 2.4 px of the truth that seed is a half turn off on 2 of the 238 found
8957
+ trials (a mouth at 48 px, both found anyway) and on none of the 6 misses, and
8958
+ every failed trial whose nearest seed is a quarter or half turn off but one is a
8959
+ flat part, none of them a miss. On the post itself, the upright rung scores 0.305
8960
+ at that cell against the half turn's 0.168, so a second rotation per cell would
8961
+ need a band 1.8 times wide to carry it. Re-gridding the rotation at the coarse level
8962
+ instead of the next one took the grid to 236 found (7 gained, 9 lost), and
8963
+ suppressing minima only within a rotation family took it to 231 (2 gained, 10
8964
+ lost). Both lose trials far from the truth, so neither shipped and the field is
8965
+ unchanged.
8966
+
8939
8967
  ⚠️ **What this is and is not.** The grid is not a reliability line: six native cells
8940
8968
  place 14 or 15 of 15 and the trials at `detail` 3 and over place 65 of 66, but the
8941
8969
  goggles still place 13 of 42, and a threshold read off two dozen parts of one figure
@@ -8962,6 +8990,8 @@ no miss at all, and `PO23` places the 48 px fist #865 was made for; `PO28` and
8962
8990
  cutoff, and `PO26` holds every polish to strictly descending residuals, read through
8963
8991
  the same trace. `PO31` places §6's post at the old placement from the page's own
8964
8992
  input bytes, and `PO32` holds it to taking the quarter-rung escape and reading the same twice.
8993
+ `PO33` holds the coarse row the tool reads rotations from to the field each seed was
8994
+ picked from.
8965
8995
 
8966
8996
  ## 12. Reading the half of that picture `pose` refuses — `rigc chainfit`
8967
8997
 
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,8 +974,8 @@ 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.
@@ -976,19 +1010,19 @@ overshoot (§3.8). The post is planted: ⛔ **no timeline**.
976
1010
  "keys": [
977
1011
  { "t": 0, "v": [-61.9], "ease": "gather" },
978
1012
  { "t": 0.07, "v": [-66.4], "ease": "charge" },
979
- { "t": 0.32, "v": [24.6], "ease": "settle" },
980
- { "t": 0.55, "v": [18.2] }
1013
+ { "t": 0.32, "v": [24.7], "ease": "settle" },
1014
+ { "t": 0.55, "v": [18.3] }
981
1015
  ]
982
1016
  },
983
1017
  {
984
1018
  "bone": "flag",
985
1019
  "property": "rotate",
986
1020
  "keys": [
987
- { "t": 0, "v": [-22.4], "ease": "gather" },
1021
+ { "t": 0, "v": [-22.2], "ease": "gather" },
988
1022
  { "t": 0.09, "v": [-30.1], "ease": "charge" },
989
1023
  { "t": 0.38, "v": [-3.8], "ease": "settle" },
990
1024
  { "t": 0.48, "v": [-15.4], "ease": "settle" },
991
- { "t": 0.55, "v": [-11.6] }
1025
+ { "t": 0.55, "v": [-11.7] }
992
1026
  ]
993
1027
  }
994
1028
  ]
@@ -1002,10 +1036,10 @@ which is which is worth being able to point at:
1002
1036
 
1003
1037
  | Key | Where it came from |
1004
1038
  | --- | --- |
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 |
1039
+ | arm `t: 0` = −61.9, flag `t: 0` = −22.2 | **given** — pose A, converted. Untouched, per §3.6 |
1040
+ | 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
1041
  | 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 |
1042
+ | arm `t: 0.32` = 24.7 | §3.8 — 6.4° past the end value (8 %), at 58 % of the duration |
1009
1043
  | 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
1044
  | 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 |
1011
1045
  | 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) |
@@ -1029,20 +1063,24 @@ rigc render --candidate spine --fps 24 --max 200
1029
1063
  ```
1030
1064
 
1031
1065
  ```
1032
- .. 111x200px at 24 fps, 1 set(s) -> …/semaphore/render
1066
+ .. 134x200px at 24 fps, 1 set(s) -> …/semaphore/render
1033
1067
  .. raise 14 frame(s), 0.542s + contact.png -> …/semaphore/render/raise@24fps
1034
1068
  ```
1035
1069
 
1036
1070
  Open `render/raise@24fps/contact.png` **before anything else** — fourteen frames as one
1037
1071
  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.
1072
+ them. What to check on it, and it is not a score: frame 0 is pose A — the flag hanging off
1073
+ the arm's collar, not over its middle — the last frame is pose B, the anticipation dips
1074
+ *after* frame 0, and the flag's extreme is visibly later than the arm's.
1041
1075
 
1042
1076
  🚫 **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.
1077
+ a frame set: `check` reads the second as the frame 1/12 s in, at its default rate. And
1078
+ the ends' **rotations** are given conditions the spec states by construction, so a
1079
+ number for how near the movement got to them is a number about the pose estimator. What
1080
+ the pictures can still catch is §3's arithmetic, because every **offset** in the rig was
1081
+ derived rather than given, and a wrong one moves an end pose off its picture while every
1082
+ key stays exact. That is what the frame-0 check above is for, and it is a look, not a
1083
+ score. §7.
1046
1084
 
1047
1085
  ### 6. Spread, and ask
1048
1086
 
@@ -1061,10 +1099,10 @@ with **these two fields replaced** and `spec`, `archetype` and `cut` unchanged:
1061
1099
  "tracks": [
1062
1100
  { "bone": "arm", "property": "rotate", "keys": [
1063
1101
  { "t": 0, "v": [-61.9], "ease": "drive" },
1064
- { "t": 0.55, "v": [18.2] } ] },
1102
+ { "t": 0.55, "v": [18.3] } ] },
1065
1103
  { "bone": "flag", "property": "rotate", "keys": [
1066
- { "t": 0, "v": [-22.4], "ease": "drive" },
1067
- { "t": 0.55, "v": [-11.6] } ] }
1104
+ { "t": 0, "v": [-22.2], "ease": "drive" },
1105
+ { "t": 0.55, "v": [-11.7] } ] }
1068
1106
  ]
1069
1107
  }
1070
1108
  }
@@ -1077,20 +1115,21 @@ rigc vote --candidate spine --candidate spine-b
1077
1115
 
1078
1116
  ```
1079
1117
  rigc vote
1080
- .. ballot 15b3f32bbbce77be
1118
+ .. ballot bce70ad82fe90941
1081
1119
  .. 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
1120
+ .. A sha256:6740d3dbf304… 3 page(s), 0.4 KiB <- …/semaphore/spine/skeleton.json
1121
+ .. B sha256:d1bda24d7c36… 3 page(s), 0.4 KiB <- …/semaphore/spine-b/skeleton.json
1084
1122
  .. 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
1123
+ .. 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
1124
+ rigc: wrote …/semaphore/ballot.html (22.3 KiB — open it in a browser)
1125
+ rigc: then record the saved vote with rigc vote --record vote-bce70ad82fe90941.json --ballot …/semaphore/ballot.html
1087
1126
  ```
1088
1127
 
1089
1128
  A person opens that page, watches two loops, picks one, and saves the small JSON it hands
1090
1129
  them. Then:
1091
1130
 
1092
1131
  ```bash
1093
- rigc vote --record vote-15b3f32bbbce77be.json --ballot ballot.html
1132
+ rigc vote --record vote-bce70ad82fe90941.json --ballot ballot.html
1094
1133
  ```
1095
1134
 
1096
1135
  ```
@@ -1101,8 +1140,8 @@ rigc vote --record vote-15b3f32bbbce77be.json --ballot ballot.html
1101
1140
  PASS V04_CHOICE_IS_ON_THE_BALLOT
1102
1141
  PASS V05_REASON_CODE_FITS_THE_CHOICE
1103
1142
  PASS V06_NOT_ALREADY_RECORDED
1104
- .. winner A = sha256:2bc29990faf6e24c953cabd09f87cdfc2edd3885a1486f3d3fdb4a88a81439c8, reason code preferred
1105
- .. coverage 2 candidate(s): A=sha256:2bc29990faf6… B=sha256:1bec4ce801ad…
1143
+ .. winner A = sha256:6740d3dbf3043ff39c5f90577407c381f6841ff6af921517be7a0cfd3d7eb9d3, reason code preferred
1144
+ .. coverage 2 candidate(s): A=sha256:6740d3dbf304… B=sha256:d1bda24d7c36…
1106
1145
  rigc: appended line 1 to …/semaphore/votes.jsonl
1107
1146
  ```
1108
1147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
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 } from './keys.ts';
42
+ import type { ShapeVisit } from './keys.ts';
43
+ import { 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,9 @@ 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) refuseValuesOfTheWrongType(manifestShapeVisits(manifest), MANIFEST_TYPES, manifestPath);
2037
2066
  if (manifest !== null && manifestPath !== null) refuseNumbersTheFileCannotCarry(manifest, manifestPath, dottedPath);
2038
2067
 
2039
2068
  // The rig spec names its own path in every message `parseRigSpec` throws (its