spine-rigc 1.5.0 → 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/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
 
@@ -767,7 +770,7 @@ late, §3.8's overshoot is live; animation name → **`raise`**.
767
770
 
768
771
  ### 1. The art, and the two pictures
769
772
 
770
- ```bash
773
+ ```bash setup
771
774
  mkdir -p semaphore/parts && cd semaphore
772
775
  bun -e '
773
776
  const files = {
@@ -806,23 +809,26 @@ rigc pose
806
809
  .. frame …/semaphore/poseA.png (160x200)
807
810
  .. ground rgb(238, 238, 234) over 100% of the border ring
808
811
  .. parts …/semaphore/parts (3 png)
809
- .. search scale 0.5–2 in 7 step(s) · rotation -180°–180° step 15° · refuse above residual 0.25
810
- PLACE arm.png x= 91.9 y= 122.8 rot= 61.9° scale=0.968 residual=0.0320 unexplained= 4%
811
- found on a 10x13 anchor grid, step 4 at 4x reduction
812
- PLACE flag.png x= 104.7 y= 156.1 rot= 84.3° scale=0.955 residual=0.0135 unexplained= 0%
812
+ .. search scale 0.5–2 in 7 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
813
+ PLACE arm.png x= 92.0 y= 122.8 rot= 61.9° scale=0.970 residual=0.0320 unexplained= 4%
813
814
  found on a 20x25 anchor grid, step 4 at 2x reduction
814
- AMBIG post.png x= 79.0 y= 141.7 rot= -0.4° scale=0.690 residual=0.0687 unexplained= 25%
815
- found on a 7x9 anchor grid, step 3 at 8x reduction
816
- alt 2: x= 78.9 y= 140.7 rot= -0.4° scale=0.650 residual=0.0692 unexplained= 26%
817
- alt 3: x= 78.4 y= 135.5 rot= -0.5° scale=0.500 residual=0.0702 unexplained= 23%
815
+ PLACE flag.png x= 104.7 y= 156.1 rot= 84.3° scale=0.955 residual=0.0135 unexplained= 0%
816
+ found on a 40x50 anchor grid, step 2 at 2x reduction
817
+ AMBIG post.png x= 79.9 y= 149.0 rot= -0.1° scale=0.971 residual=0.0593 unexplained= 16%
818
+ found on a 14x17 anchor grid, step 6 at 2x reduction
819
+ alt 2: x= 79.0 y= 141.7 rot= -0.5° scale=0.691 residual=0.0687 unexplained= 25%
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
@@ -830,33 +836,49 @@ rigc pose --images parts --frame poseB.png --scale 0.85,1.2 --out poseB.json
830
836
  ```
831
837
 
832
838
  ```
833
- .. search scale 0.85–1.2 in 2 step(s) · rotation -180°–180° step 15° · refuse above residual 0.25
834
- PLACE arm.png x= 91.9 y= 122.8 rot= 61.9° scale=0.968 residual=0.0320 unexplained= 4%
835
- PLACE flag.png x= 104.7 y= 156.1 rot= 84.3° scale=0.955 residual=0.0135 unexplained= 0%
836
- PLACE post.png x= 79.9 y= 148.6 rot= -0.1° scale=0.975 residual=0.0589 unexplained= 16%
839
+ rigc pose
840
+ .. frame …/semaphore/poseA.png (160x200)
841
+ .. ground rgb(238, 238, 234) over 100% of the border ring
842
+ .. parts …/semaphore/parts (3 png)
843
+ .. search scale 0.85–1.2 in 2 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
844
+ PLACE arm.png x= 91.9 y= 122.8 rot= 61.9° scale=0.970 residual=0.0320 unexplained= 4%
845
+ found on a 20x25 anchor grid, step 4 at 2x reduction
846
+ PLACE flag.png x= 104.7 y= 156.1 rot= 84.1° scale=0.956 residual=0.0135 unexplained= 0%
847
+ found on a 40x50 anchor grid, step 2 at 2x reduction
848
+ PLACE post.png x= 79.9 y= 148.5 rot= -0.1° scale=0.971 residual=0.0593 unexplained= 16%
849
+ found on a 14x17 anchor grid, step 6 at 2x reduction
837
850
  ```
838
851
 
839
852
  ```
840
- .. search scale 0.85–1.2 in 2 step(s) · rotation -180°–180° step 15° · refuse above residual 0.25
841
- PLACE arm.png x= 102.6 y= 94.8 rot= -18.2° scale=0.973 residual=0.0305 unexplained= 4%
842
- PLACE flag.png x= 137.4 y= 86.0 rot= -6.6° scale=0.956 residual=0.0134 unexplained= 1%
853
+ rigc pose
854
+ .. frame …/semaphore/poseB.png (160x200)
855
+ .. ground rgb(238, 238, 234) over 100% of the border ring
856
+ .. parts …/semaphore/parts (3 png)
857
+ .. search scale 0.85–1.2 in 2 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
858
+ PLACE arm.png x= 102.5 y= 94.8 rot= -18.3° scale=0.974 residual=0.0305 unexplained= 4%
859
+ found on a 20x25 anchor grid, step 4 at 2x reduction
860
+ PLACE flag.png x= 137.4 y= 86.0 rot= -6.6° scale=0.956 residual=0.0134 unexplained= 0%
861
+ found on a 40x50 anchor grid, step 2 at 2x reduction
843
862
  PLACE post.png x= 80.0 y= 148.0 rot= 0.0° scale=0.995 residual=0.0394 unexplained= 9%
863
+ found on a 14x17 anchor grid, step 6 at 2x reduction
844
864
  ```
845
865
 
846
866
  Three things to read out of that pair, none of which is a score:
847
867
 
848
- - **Scale.** All six readings sit in 0.955–0.995 — a spread of about 4 %, which is the
849
- method's own floor rather than six different scales. ⇒ Take the pictures as being at
850
- the art's own resolution and author the rig in **part pixels**, so no scaling appears
851
- 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.
852
874
  - **Draw order, from `unexplained`.** The post reads **16 %** unexplained in pose A and
853
875
  **9 %** in pose B, at placements that barely move — §2.2's occlusion signature. The arm
854
876
  crosses more of the post in pose A, so the arm is **in front of** the post. The flag
855
- 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
856
878
  the order `post`, `arm`, `flag` (AUTHORING R4 — the slots array *is* the draw order,
857
879
  and there is nowhere else in the file to say it).
858
880
  - **The post does not move**, so its two readings are two measurements of one number:
859
- 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
860
882
  disagreement as the noise floor for every other number on the page.
861
883
 
862
884
  ### 3. Convert, and derive the rig
@@ -866,33 +888,59 @@ Three things to read out of that pair, none of which is a score:
866
888
 
867
889
  | | pose A, Spine world | pose B, Spine world |
868
890
  | --- | --- | --- |
869
- | `post` | x 79.9 · y 51.4 · rot 0.1° | x 80.0 · y 52.0 · rot 0.0° |
870
- | `arm` | x 91.9 · y 77.2 · rot −61.9° | x 102.6 · y 105.2 · rot 18.2° |
871
- | `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° |
872
894
 
873
895
  **The shoulder, by §3.9's solve.** The arm's screen rotation changes from 61.9° to
874
- −18.2°, so **Δ = 80.1°** — well inside the *solve it* band, `|det| = 4·sin²(40.05°) =
875
- 1.656`, amplification 0.78×. Solving the 2×2 puts the fixed point at frame **(80.59,
876
- 102.44)**, reconstructing identically from both poses, and the offset lands at arm-image
877
- pixel **(6.71, 7.38)** — inside the arm's own hub, which is where a hub is for. In Spine
878
- 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.
879
903
 
880
904
  **The flag hinge, by §3.9's default — and this is the interesting one.** The relative
881
- angle across that joint is 22.4° in pose A and 11.6° in pose B, so **Δ = 10.8°**:
882
- `|det| = 0.0354`, amplification **5.3×**, comfortably inside the *do not use the solve*
883
- band. Run it anyway, to see what it would have cost — it returns arm-local **(25.64,
884
- 1.32)**, exactly as confidently as the shoulder did. The default instead: the flag's art
885
- draws its hinge as a dark hoist strip down one edge, whose centre is flag-image
886
- **(2.5, 10)**; carrying that point through each pose's placement into arm-local gives
887
- **(24.77, 0.01)** and **(24.54, 0.20)** — §3.9's self-agreement check, and the two poses
888
- agree to **0.23 px**. ⇒ Take the mean, **(24.66, 0)**, and write in the log that the
889
- pivot was **defaulted**, with the number: *relative rotation changed 10.8°,
890
- 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.
891
936
 
892
937
  ⭐ Worth pausing on, because it is what §3.9 is for: **the two solves are
893
938
  indistinguishable from the inside.** Both are exact, both reconstruct to a point that
894
- agrees between the poses, and no residual anywhere in either pose report moves. The only
895
- 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.
896
944
 
897
945
  `semaphore.rig.json` — complete, nothing trimmed:
898
946
 
@@ -904,9 +952,9 @@ thing separating them is Δ, which is arithmetic you can do before you trust eit
904
952
  "skeleton": { "width": 160, "height": 200 },
905
953
  "bones": [
906
954
  { "name": "root" },
907
- { "name": "post", "parent": "root", "x": 79.95, "y": 51.7 },
908
- { "name": "arm", "parent": "post", "x": 0.64, "y": 45.86 },
909
- { "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 }
910
958
  ],
911
959
  "slots": [
912
960
  { "name": "post", "bone": "post", "attachment": "post" },
@@ -916,7 +964,7 @@ thing separating them is Δ, which is arithmetic you can do before you trust eit
916
964
  "skins": {
917
965
  "default": {
918
966
  "post": { "post": { "image": "post.png" } },
919
- "arm": { "arm": { "image": "arm.png", "x": 23.29 } },
967
+ "arm": { "arm": { "image": "arm.png", "x": 23.24 } },
920
968
  "flag": { "flag": { "image": "flag.png", "x": 11.5 } }
921
969
  }
922
970
  }
@@ -926,8 +974,8 @@ thing separating them is Δ, which is arithmetic you can do before you trust eit
926
974
  The two attachment offsets are the last of the arithmetic. A bone sits at its pivot and
927
975
  the placement told you where the image's **centre** goes, so the offset is the gap
928
976
  between them, in the bone's own axes with y flipped once: the arm's pivot is at image
929
- (6.71, 7.38) and its centre at (30, 7), giving **x 23.29** (the y term is 0.38, inside
930
- 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
931
979
  its centre at (14, 10), giving **x 11.5** exactly. Bone rotations are left off, which
932
980
  means *as drawn* — the arm plate is drawn horizontal and the post vertical, so the poses
933
981
  are entirely the motion spec's business.
@@ -962,19 +1010,19 @@ overshoot (§3.8). The post is planted: ⛔ **no timeline**.
962
1010
  "keys": [
963
1011
  { "t": 0, "v": [-61.9], "ease": "gather" },
964
1012
  { "t": 0.07, "v": [-66.4], "ease": "charge" },
965
- { "t": 0.32, "v": [24.6], "ease": "settle" },
966
- { "t": 0.55, "v": [18.2] }
1013
+ { "t": 0.32, "v": [24.7], "ease": "settle" },
1014
+ { "t": 0.55, "v": [18.3] }
967
1015
  ]
968
1016
  },
969
1017
  {
970
1018
  "bone": "flag",
971
1019
  "property": "rotate",
972
1020
  "keys": [
973
- { "t": 0, "v": [-22.4], "ease": "gather" },
1021
+ { "t": 0, "v": [-22.2], "ease": "gather" },
974
1022
  { "t": 0.09, "v": [-30.1], "ease": "charge" },
975
1023
  { "t": 0.38, "v": [-3.8], "ease": "settle" },
976
1024
  { "t": 0.48, "v": [-15.4], "ease": "settle" },
977
- { "t": 0.55, "v": [-11.6] }
1025
+ { "t": 0.55, "v": [-11.7] }
978
1026
  ]
979
1027
  }
980
1028
  ]
@@ -988,10 +1036,10 @@ which is which is worth being able to point at:
988
1036
 
989
1037
  | Key | Where it came from |
990
1038
  | --- | --- |
991
- | arm `t: 0` = −61.9, flag `t: 0` = −22.4 | **given** — pose A, converted. Untouched, per §3.6 |
992
- | 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 |
993
1041
  | arm `t: 0.07` = −66.4 | §3.6 — 4.5° against an 80° excursion (5.6 %), at 13 % of the duration |
994
- | 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 |
995
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 |
996
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 |
997
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) |
@@ -1015,20 +1063,24 @@ rigc render --candidate spine --fps 24 --max 200
1015
1063
  ```
1016
1064
 
1017
1065
  ```
1018
- .. 111x200px at 24 fps, 1 set(s) -> …/semaphore/render
1066
+ .. 134x200px at 24 fps, 1 set(s) -> …/semaphore/render
1019
1067
  .. raise 14 frame(s), 0.542s + contact.png -> …/semaphore/render/raise@24fps
1020
1068
  ```
1021
1069
 
1022
1070
  Open `render/raise@24fps/contact.png` **before anything else** — fourteen frames as one
1023
1071
  grid, and spacing is a comparison across frames rather than a property of any one of
1024
- them. What to check on it, and it is not a score: frame 0 is pose A, the last frame is
1025
- pose B, the anticipation dips *after* frame 0, and the flag's extreme is visibly later
1026
- 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.
1027
1075
 
1028
1076
  🚫 **Do not run `rigc check` against `poseA.png` and `poseB.png`.** Two pictures are not
1029
- a frame set, and more to the point the ends are **given conditions** the spec states by
1030
- construction — measuring how near it got to them measures the pose estimator, not the
1031
- 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.
1032
1084
 
1033
1085
  ### 6. Spread, and ask
1034
1086
 
@@ -1047,10 +1099,10 @@ with **these two fields replaced** and `spec`, `archetype` and `cut` unchanged:
1047
1099
  "tracks": [
1048
1100
  { "bone": "arm", "property": "rotate", "keys": [
1049
1101
  { "t": 0, "v": [-61.9], "ease": "drive" },
1050
- { "t": 0.55, "v": [18.2] } ] },
1102
+ { "t": 0.55, "v": [18.3] } ] },
1051
1103
  { "bone": "flag", "property": "rotate", "keys": [
1052
- { "t": 0, "v": [-22.4], "ease": "drive" },
1053
- { "t": 0.55, "v": [-11.6] } ] }
1104
+ { "t": 0, "v": [-22.2], "ease": "drive" },
1105
+ { "t": 0.55, "v": [-11.7] } ] }
1054
1106
  ]
1055
1107
  }
1056
1108
  }
@@ -1063,20 +1115,21 @@ rigc vote --candidate spine --candidate spine-b
1063
1115
 
1064
1116
  ```
1065
1117
  rigc vote
1066
- .. ballot 15b3f32bbbce77be
1118
+ .. ballot bce70ad82fe90941
1067
1119
  .. animation raise
1068
- .. A sha256:2bc29990faf6… 3 page(s), 0.4 KiB <- …/semaphore/spine/skeleton.json
1069
- .. 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
1070
1122
  .. the page shows A/B and nothing else — the paths above are in its manifest, never on the screen
1071
- rigc: wrote …/semaphore/ballot.html (22.5 KiB — open it in a browser)
1072
- 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
1073
1126
  ```
1074
1127
 
1075
1128
  A person opens that page, watches two loops, picks one, and saves the small JSON it hands
1076
1129
  them. Then:
1077
1130
 
1078
1131
  ```bash
1079
- rigc vote --record vote-15b3f32bbbce77be.json --ballot ballot.html
1132
+ rigc vote --record vote-bce70ad82fe90941.json --ballot ballot.html
1080
1133
  ```
1081
1134
 
1082
1135
  ```
@@ -1087,8 +1140,8 @@ rigc vote --record vote-15b3f32bbbce77be.json --ballot ballot.html
1087
1140
  PASS V04_CHOICE_IS_ON_THE_BALLOT
1088
1141
  PASS V05_REASON_CODE_FITS_THE_CHOICE
1089
1142
  PASS V06_NOT_ALREADY_RECORDED
1090
- .. winner A = sha256:2bc29990faf6e24c953cabd09f87cdfc2edd3885a1486f3d3fdb4a88a81439c8, reason code preferred
1091
- .. 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…
1092
1145
  rigc: appended line 1 to …/semaphore/votes.jsonl
1093
1146
  ```
1094
1147
 
@@ -1168,7 +1221,7 @@ skeleton — AUTHORING §10.3 and PROMPTING clause 4 both price it. Keys are str
1168
1221
  The bytes of `poseA.png` and `poseB.png`, so §6 runs end to end. Both are 160×200 on a
1169
1222
  flat ground; both are invented.
1170
1223
 
1171
- ```bash
1224
+ ```bash setup
1172
1225
  bun -e '
1173
1226
  const frames = {
1174
1227
  "poseA.png": "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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "1.5.0",
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 { nearMisses } 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
  *
@@ -2030,6 +2056,14 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2030
2056
  // and which refusals it deliberately leaves in this file.
2031
2057
  const motion = parseMotionSpec(readJson<unknown>(motionPath), motionPath);
2032
2058
  const manifest = manifestPath === null ? null : readJson<FaceManifest>(manifestPath);
2059
+ // The manifest is the record of what was measured, and its numbers become
2060
+ // bone positions and the stage: a `crop.h` of 1e309 built as `"height": null`
2061
+ // before issue #881. The rig spec's walk is `parseRigSpec`'s; this is the
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);
2066
+ if (manifest !== null && manifestPath !== null) refuseNumbersTheFileCannotCarry(manifest, manifestPath, dottedPath);
2033
2067
 
2034
2068
  // The rig spec names its own path in every message `parseRigSpec` throws (its
2035
2069
  // `where` argument, above). The motion spec gets no such treatment below —