spine-rigc 0.20.0 → 0.20.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/README.md CHANGED
@@ -443,7 +443,7 @@ limits: [AUTHORING.md §11](docs/AUTHORING.md). The parts it refuses because
443
443
  something is drawn over them are `rigc chainfit`'s, once a candidate exists —
444
444
  [§12](docs/AUTHORING.md).
445
445
 
446
- ## The gallery — six complete rigs over art that ships with them
446
+ ## The gallery — seven complete rigs over art that ships with them
447
447
 
448
448
  Each directory in [`gallery/`](https://github.com/firejune/rigc/tree/main/gallery) is
449
449
  one rig spec, one motion spec and the PNGs they name, small enough to read in one
@@ -460,6 +460,7 @@ was verified, and what writing it cost. Repository material: a clone and
460
460
  | [`gallery/ride`](https://github.com/firejune/rigc/tree/main/gallery/ride) | `path` attachments + **path constraints** | A trolley coasting down a drawn rail and rolling back, driven by a `position` timeline, with `groups` + `stagger` keying the wheels and the ears |
461
461
  | [`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait) | **deform `transform`** + `derive` group tracks | A 2.5D head turn: two meshes and six feature bones all keyed from one stated expression, `dx = x(cos t − 1) − z·sin t`, with the depths in the spec rather than a README |
462
462
  | [`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod) | the **`pitch`** and **`wave`** transform kinds | A head bowing and two lop ears rippling, on three meshes each laid out for the closed form that moves it — a fold angle solved for before authoring, and a shear whose winding no amplitude can reverse |
463
+ | [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) | **`slider` constraints** | A head that turns because a **value** says so: two dials drive two sliders, and the rendered animation moves the needles rather than the face — with a depth map under the face mesh, a soft mask on the cowlick, and a slider range derived from the turn ceiling `build` reports rather than picked by eye |
463
464
 
464
465
  <p align="center">
465
466
  <img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/rigc-scene.gif" alt="A portrait rig breathing, glancing aside, then turning its head in 2.5D — hair and features sliding at different depths" width="600" />
package/docs/AUTHORING.md CHANGED
@@ -773,8 +773,13 @@ between two things it has in front of it: the emitted triangles, and the PNG the
773
773
  attachment names with `image`. So any mesh that names one gets the figure on its
774
774
  `MESH` line, authored or generated:
775
775
 
776
+ ```bash
777
+ bun cli.ts build --rig gallery/squash/rig.json \
778
+ --motion gallery/squash/motion.json --out /tmp/squash
779
+ ```
780
+
776
781
  ```
777
- MESH ball authored 9 vertices / 8 triangles (budget 8) bones=[ball] attachments=[ball] covers 94.31% of the art, reaching 2.50px past it
782
+ MESH ball authored 9 vertices / 8 triangles (budget 8) bones=[ball] attachments=[ball] covers 100.00% of the art, reaching 15.00px past it
778
783
  ```
779
784
 
780
785
  **A number, not a bar.** A `contour` under 99.5% is *refused* because rigc
@@ -782,11 +787,17 @@ generated that geometry as a claim about the art; an authored mesh that sits ins
782
787
  its art is a legitimate thing to draw — a soft feather, a trimmed hull, a mesh
783
788
  meant to bend a core while its edges stretch — so the figure informs and the
784
789
  decision stays with the author. A mesh with no `image` reports nothing, because
785
- there is nothing to measure it against. The silence was worth closing: the line
786
- above is a round part meshed as a centre vertex plus 8 rim vertices placed on the
787
- silhouette, and an octagon's sides pass `R · cos(π/8)` from its centre, so 5.7% of
788
- the drawing — its whole ink outline, between the spokes — was not going to be
789
- drawn, and every assertion passed (issue #277).
790
+ there is nothing to measure it against.
791
+
792
+ **The silence was worth closing, and that example is where it was found.** The
793
+ ball is a centre vertex plus 8 rim vertices, and the first version placed them
794
+ *on* the silhouette — but an octagon's sides pass `R · cos(π/8)` from its centre,
795
+ so its whole ink outline between the spokes was not going to be drawn, and every
796
+ assertion passed (issue #277). That is why the command above prints 100.00%
797
+ rather than the figure it was filed over:
798
+ [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash)'s
799
+ README carries the inradius arithmetic, both coverage readings, and the rim move
800
+ that settled it.
790
801
 
791
802
  The generators are `ring`, `ribbon`, `contour` and `grid` (see
792
803
  [`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
@@ -1032,10 +1043,14 @@ triangle does not, and nothing on the first line says so.
1032
1043
  | `which is 0.049 of the range this mesh sampled` — the **same step, over the range this mesh sampled** | how much of everything the sheet said across the whole part it said across that one triangle. A form's slope is bounded, so this **halves every time you double the lattice** while the angle settles. Near 1 it is a **cliff**: a step with no slope in it, whose angle halves with the lattice instead and describes nothing at any density. Measured: `gallery/look` reads 0.112 and 0.468, a synthetic raised cosine 0.394 falling to 0.027 under refinement, the same cosine with one planted cliff a flat 0.50, and estimated depth sheets over cut-out art **0.92–0.99** |
1033
1044
  | `+none` | on the ceiling line, nothing folds on that side at all, at any angle. On the percentile line it is the same statement — there is no population, because there is nothing to take a percentile of |
1034
1045
 
1035
- A real one rather than the illustration above — `bun cli.ts build --rig
1036
- gallery/look/rig.json --motion gallery/look/motion.json --images
1037
- gallery/look/parts --out <dir>`, whose two meshes happen to print two of the
1038
- three spellings:
1046
+ A real one rather than the illustration above — `gallery/look`, whose two meshes
1047
+ happen to print two of the three spellings:
1048
+
1049
+ ```bash
1050
+ bun cli.ts build --rig gallery/look/rig.json \
1051
+ --motion gallery/look/motion.json \
1052
+ --images gallery/look/parts --out /tmp/look
1053
+ ```
1039
1054
 
1040
1055
  ```
1041
1056
  MESH head grid 189 vertices / 320 triangles (budget 320) bones=[head] attachments=[head]
@@ -1653,6 +1668,13 @@ rather than 1 s. With `loop: false` that is the last frame and harmless, with
1653
1668
  one — pick `to`/`scale` so the endpoint lands **inside** the duration rather than
1654
1669
  exactly on it.
1655
1670
 
1671
+ 🖼️ **Worked example: [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look)** — a face
1672
+ whose yaw and pitch are two sliders sharing one bone, both `local: true` and both
1673
+ `additive: true`, with each range derived from the turn ceiling `build` reports
1674
+ for that mesh (§3.4) rather than chosen. [`docs/FACE.md`](FACE.md) §8's *The turn
1675
+ as a value rather than a time* states that derivation as a rule, and its §7 is
1676
+ what a slider does to a face's channel allocation.
1677
+
1656
1678
  ### 3.6 `events` — names the animation can fire
1657
1679
 
1658
1680
  **When you need one:** something outside the skeleton has to happen on a
@@ -2082,6 +2104,7 @@ is not the arrangement the format has. Spine keys one bone per timeline, so the
2082
2104
  six numbers of a head turn are eighty lines apart in the artifact and nobody can
2083
2105
  see a wrong sign in them.
2084
2106
 
2107
+ **No run reproduces this:** the legend and one group's record lifted out of one `explain` run, which prints the two `bone "faceshift"` records between them
2085
2108
  ```
2086
2109
  group members (the per-member values of one track, side by side — issue #295)
2087
2110
  .. a row per member and a block per key, because a wrong sign is visible in a column of six and
@@ -2661,10 +2684,15 @@ makes against them.
2661
2684
 
2662
2685
  **It is auditable.** `explain` prints the model, the scalars the closed form
2663
2686
  derived from it, and every offset it produced — the emitted ones, not a second
2664
- evaluation:
2687
+ evaluation. This is the `t=0.62` key of the spec above, whole:
2665
2688
 
2689
+ ```bash
2690
+ bun cli.ts explain --rig gallery/portrait/rig.json \
2691
+ --motion gallery/portrait/motion.json --out /tmp/explain
2666
2692
  ```
2667
- t=0.62 deform[0..50] 25 pair(s) bezier[4]
2693
+
2694
+ ```
2695
+ t=0.62 deform[0..50] 25 pair(s) stepped
2668
2696
  transform yaw radius=170 degrees=12
2669
2697
  dx = (x−about)·(cos t − 1) − z·sin t, z = √(radius² − (x−about)²)
2670
2698
  t = 0.20944 rad
@@ -2673,9 +2701,19 @@ evaluation:
2673
2701
  centre shift = −radius·sin t = -35.344987
2674
2702
  25 vertices, largest offset 35.344987px at vertex 2
2675
2703
  v 0 (-7.17493, 0) v 1 (-22.413595, 0) v 2 (-35.344987, 0) v 3 (-27.658171, 0)
2676
- …five more lines
2704
+ v 4 (-14.255108, 0) v 5 (-14.255108, 0) v 6 (-14.255108, 0) v 7 (-14.255108, 0)
2705
+ v 8 (-14.255108, 0) v 9 (-27.658171, 0) v 10 (-35.344987, 0) v 11 (-22.413595, 0)
2706
+ v 12 (-7.17493, 0) v 13 (-7.17493, 0) v 14 (-7.17493, 0) v 15 (-7.17493, 0)
2707
+ v 16 (-22.413595, 0) v 17 (-35.344987, 0) v 18 (-27.658171, 0) v 19 (-22.413595, 0)
2708
+ v 20 (-35.344987, 0) v 21 (-27.658171, 0) v 22 (-22.413595, 0) v 23 (-35.344987, 0)
2709
+ v 24 (-27.658171, 0)
2677
2710
  ```
2678
2711
 
2712
+ The curve reads `stepped` where the spec says `"ease": "swell"`, and that is
2713
+ §4.5's hold rule rather than a discrepancy: the next key emits these same 25
2714
+ offsets, so the segment between them would draw nothing and is written the way
2715
+ the editor writes it.
2716
+
2679
2717
  📌 **Float behaviour, stated.** The closed forms are evaluated in float64 and
2680
2718
  quantised to six decimals like every other emitted number, so the same spec emits
2681
2719
  the same bytes and `A18_DETERMINISTIC_EMIT` proves it on a second compile. The
@@ -2701,6 +2739,7 @@ triangles. It is a report and it never gates: `explain` takes no `--profile` and
2701
2739
  exits 0 on a rig `build` would refuse, so the figures are readable on the build
2702
2740
  that is failing.
2703
2741
 
2742
+ **No run reproduces this:** abridged — the `WORST` rollup follows key 1 here, where the run prints `head/head`'s other two keys and all four of `hair_bang/hair_bang` between them
2704
2743
  ```
2705
2744
  deform (what each key does to the geometry — figures with names, never a bar; issue #316)
2706
2745
  .. every key measured at its OWN time against the same pose with the deform CLEARED, so the
@@ -2797,8 +2836,12 @@ clothes. The quantity that does move — how much art each drawn pixel now carri
2797
2836
  📘 **[FACE.md](FACE.md) §9.2** is this block on real art, as three builds of
2798
2837
  `gallery/portrait`: the good one, one with a band inverted, and one folded. The
2799
2838
  inverted build is the case worth reading — `A39` passes it (correctly: nothing
2800
- reverses), and the block is what says `x1.362834` where the model's own table
2801
- says `x1.319121`, with no reference render anywhere.
2839
+ reverses), and the block is what says `x1.362834` on the band the model's own key
2840
+ reports as `x0.637174`, with no reference render anywhere. ⭐ The comparison is
2841
+ per **triangle** and that is what makes it a reading: the block names one beside
2842
+ every ratio, so the two blocks can be lined up band against band instead of worst
2843
+ against worst — which on this build would have paired the inverted band with an
2844
+ untouched one and called the difference mild.
2802
2845
 
2803
2846
  📘 **[`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod)'s
2804
2847
  README is a second reading of the same block** (repository material, hence the
@@ -2823,15 +2866,23 @@ the frames just before it are drawn, nearly folded, and land on no key at all. O
2823
2866
  the turn probe that is **8 reversed triangles at alpha 0.20, gating green**.
2824
2867
 
2825
2868
  ⇒ `A39` now scans every interval between two consecutive deform keys as well, and
2826
- refuses one with its own sentence:
2827
-
2828
- ```
2829
- FAIL A39_DEFORM_KEEPS_TRIANGLE_WINDING: animation "turn" deform head/head BETWEEN key 0
2830
- (t=0s) and key 1 (t=0.5s), at t=0.444089s — 88.8% of the way from one to the other:
2831
- 8 of 32 triangle(s) reverse winding — triangle 0 [0,15,16] 1890.001 -> -272.314px²; …
2832
- NO KEY LANDS THERE: the runtime interpolates between the two keys, and the mesh is
2833
- inside out for part of the way, drawing its texture backwards at alpha 0.1118 …
2834
- ```
2869
+ refuses one with its own sentence. No spec this repository ships produces one — the
2870
+ rig it was written from is a probe `selftest.ts` generates and nothing else can
2871
+ invoke — so the sentence is described here rather than transcribed.
2872
+
2873
+ **What it carries**, in the order it says it: `BETWEEN key <i> (t=…s) and key <j>
2874
+ (t=…s)` where a key refusal puts one index; the time the closed form solved for, and
2875
+ how far along the segment that is — or `(a stepped segment)` instead, which
2876
+ interpolates nothing and holds the earlier key's geometry across the span; the
2877
+ reversed count out of the triangle total, with the first four named and each one's
2878
+ vertex ids and its signed area before and after; `NO KEY LANDS THERE` in those words,
2879
+ then whether the runtime interpolates across the span or holds it; the alpha read at
2880
+ that same instant, present only where it is not 1; and the ways out — for an
2881
+ interpolating span the four the table below gives, the fade one among them only
2882
+ where the alpha is not 1, and for a stepped one the key it holds instead, with
2883
+ `invariants.deformMayFold` the last resort either way.
2884
+ [`src/validate.ts`](../src/validate.ts) builds it, beside the key sentence §4.11.2
2885
+ quotes.
2835
2886
 
2836
2887
  **What to change when you see it**, in the order worth trying:
2837
2888
 
@@ -2899,7 +2950,13 @@ value = from + (time − to) / scale what A39 sets the dial to
2899
2950
 
2900
2951
  🔒 **The `DEFORM` block prints the frame on every key**, because the derivation
2901
2952
  changed and a block that went on printing the same figures under a changed meaning
2902
- would be worse than the red it replaced:
2953
+ would be worse than the red it replaced. `gallery/look`'s `turn` is the animation
2954
+ a slider applies, and this is one of its keys:
2955
+
2956
+ ```bash
2957
+ bun cli.ts explain --rig gallery/look/rig.json \
2958
+ --motion gallery/look/motion.json --out /tmp/explain-look
2959
+ ```
2903
2960
 
2904
2961
  ```
2905
2962
  DEFORM turn default/head/head key 6 t=1.900000 transform yaw depth=true degrees=19
@@ -3221,6 +3278,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3221
3278
 
3222
3279
  The report prints one line per assertion:
3223
3280
 
3281
+ **No run reproduces this:** assembled — one line of each verdict kind; the `PASS` and the `PROF` are verbatim from a `build` this page states, the `SKIP` is cut at the ellipsis and only `--profile spine-html` prints it, and the `FAIL` is invented, because a green build prints none
3224
3282
  ```
3225
3283
  PASS A08_REGION_NAMES_MATCH_ATTACHMENTS
3226
3284
  SKIP A21_MESH_RIM_PINNED: the skeleton has no weighted mesh attachment, …
package/docs/FACE.md CHANGED
@@ -40,16 +40,25 @@ hold its results.
40
40
  measures §5's foreshortening at **0.863–1.176** against the yaw's 0.892–1.064
41
41
  below — a wider span at the same 12°, because a face is taller than it is deep.
42
42
  Read it after this page, not instead of it
43
-
44
- 🚨 **Nothing in this toolchain measures what a `deform` key does, and that is the
45
- one gap you have to author around.** The setup geometry is measured and printed —
46
- coverage, overshoot, hole. The *deformed* geometry is not. A key that turns a
47
- mesh inside out gates green — **26 PASS on `--profile spine-html`, the same as
48
- the good build, and the same coverage line reporting the setup pose at
49
- 100.00%.** §9.2 demonstrates that with three builds and §9.3 gives you the
50
- differential audit that works today;
51
- [issue #296](https://github.com/firejune/rigc/issues/296) is the instrument that
52
- would close it. Reference it, do not wait for it.
43
+ - **The turn driven by a value instead of played as a time** — the same keys, on
44
+ an axis: §8's *The turn as a value rather than a time* derives the range,
45
+ **AUTHORING §3.5.2** is the `slider` constraint that does it, and
46
+ [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) is
47
+ the worked case. Read it when the angle has to follow something outside the
48
+ animation — a pointer, a gaze target, a game value
49
+
50
+ 🚨 **A `deform` key's winding is gated; how far it moved the geometry is not, and
51
+ that half is the one you have to author around.** Since 2026-09-03
52
+ `A39_DEFORM_KEEPS_TRIANGLE_WINDING` refuses a key that turns the mesh inside out —
53
+ by name, by key and by triangle — and the build writes nothing. What no assertion
54
+ has an opinion about is **magnitude**: a band that stretches where the projection
55
+ says it should compress keeps every triangle's winding, so it gates green, beside
56
+ the same `MESH` coverage line as the good build, because coverage reports the
57
+ *setup* pose. §9.2 is that pair as three builds of one rig — the good one, one
58
+ wrong and green, one refused. For the half still ungated, `explain`'s `DEFORM`
59
+ block prints each key's area and stretch ratios per triangle with no reference
60
+ render (§9.2, AUTHORING §4.11.2), and §9.3 is the differential audit, the three
61
+ things it cannot do, and the procedure that survives them.
53
62
 
54
63
  📐 **Where the numbers on this page come from.** Every figure marked **derived**
55
64
  is re-computed from the closed form in §1 and reproduces to the digits printed.
@@ -69,6 +78,7 @@ internal shape:
69
78
  | a face that **blinks**, **breathes**, **looks around** | ordinary MOTION.md tracks. A lid, a chest, an iris. Nothing on this page is needed |
70
79
  | a face that **turns** | a list of `(x, z)` — every part's position across the screen and its **depth** — plus one line of arithmetic evaluated at each of them |
71
80
  | a face that turns **far** (a three-quarter view) | ⛔ a different rig, and §8 says where the line is. Not a format problem: a parts-and-labour problem |
81
+ | a face that turns **by however much something outside it says** — a pointer, a gaze target, a game value | the same list and the same arithmetic, reached by a **value** instead of by a playhead: the turn animation becomes a `slider`'s lookup table (§8's last subsection, AUTHORING §3.5.2). What changes is not the geometry, it is the range — which stops being a choice and becomes a measurement |
72
82
 
73
83
  ⭐ **The turn is the only part of a face that is not already MOTION.md's job**,
74
84
  and it is 90% of this page. A blink is a translating plate (§6); a gaze is
@@ -524,6 +534,7 @@ asked for.** The `MEMBER` block (AUTHORING §4.5.2) is a row per member — the
524
534
  emitted value, the `x` it read off the rig, and the depth the spec stated — so the
525
535
  nose diagnostic below is now a line you read rather than arithmetic you redo:
526
536
 
537
+ **No run reproduces this:** abridged — the five derivation lines the run prints between this record's head and its rows are cut; AUTHORING §4.5.2 quotes them
527
538
  ```
528
539
  MEMBER turn group "features".translatex t=0.620000 6 member(s) derive yaw degrees=12 carried=170 -> the displacement
529
540
  eye_l 5.513083 <- -62 at depth 150
@@ -746,8 +757,7 @@ shows the listing beside the order.
746
757
  over an oval face has transparent corners, and the line says so:
747
758
 
748
759
  ```
749
- MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head]
750
- attachments=[head] covers 100.00% of the art, reaching 95.90px past it
760
+ MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
751
761
  ```
752
762
 
753
763
  **`covers 100.00%` is what matters** — nothing of the drawing is outside the
@@ -923,6 +933,22 @@ stack (`headroll_idle` under `headroll_layer`) — but both are runtime or rig
923
933
  decisions the motion spec cannot express, so nothing warns an author that two of
924
934
  their animations will fight.**
925
935
 
936
+ 🚨 **A `slider` is a third way for two animations to meet on one property, and it
937
+ is an overwrite rather than a blend — so allocate it in this table too.** An
938
+ animation a slider applies (AUTHORING §3.5.2) is never on a track: the constraint
939
+ applies it every frame, at whatever time its own bone currently points at. At
940
+ `mix: 1` with `additive` left at its default that apply **writes the property
941
+ outright**, which erases both any earlier slider on the same property and the
942
+ **playing** animation on the bones its animation keys — including at its own
943
+ neutral, where it looks switched off. ⇒ Every face axis that shares a target
944
+ declares `"additive": true`, and unlike the two collisions above this one **is**
945
+ gated: `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` names the bone, the property,
946
+ every slider keying it in array order and which one wins today. ⛔ The one case
947
+ `additive` cannot rescue is a **slot colour, an attachment swap, a draw order or
948
+ a sequence**: those timelines ignore the flag entirely, so a fade — §8's way of
949
+ taking a part off the screen before its own ceiling — belongs *inside* the single
950
+ animation one slider applies, never in a second slider beside it.
951
+
926
952
  ⛔ **And the cost is real, so name it rather than discovering it by shipping.** In
927
953
  the worked example `idle` keys **nothing** on the iris, on purpose, even though a
928
954
  completely still eye reads as a mannequin. The iris is `gaze`'s channel; an
@@ -1057,6 +1083,111 @@ authoring concept**: the on-axis pair's shared `cos t` now falls out of the same
1057
1083
  closed form as everybody else's value, so `axis` is gone from the spec while
1058
1084
  `look_l`/`look_r` stay — because those two really are one shared number (§5).
1059
1085
 
1086
+ ### The turn as a value rather than a time
1087
+
1088
+ Everything above prices a turn as an animation somebody plays. The same geometry
1089
+ also runs on an **axis**: a `slider` constraint reads a driving bone, maps that
1090
+ bone's rotation to a time inside the turn animation, and applies the animation
1091
+ there (AUTHORING §3.5.2). Nothing in §1–§5 changes — the keys are still §1's line
1092
+ evaluated at each angle — but what selects among them is a **value** rather than a
1093
+ playhead, so the face follows a number somebody else is holding: a pointer, a gaze
1094
+ target, a game state.
1095
+
1096
+ ⭐ **The object offers a dial and does not decide when it turns.** That is the
1097
+ whole of the claim and it is deliberately not a larger one: what the rig
1098
+ guarantees is the axis — its range, its arithmetic, and that every angle on it is
1099
+ sound — and what moves the dial belongs to whoever is using the face. The
1100
+ paragraphs below are the part that is ours.
1101
+
1102
+ #### The range stops being a choice and becomes a measurement
1103
+
1104
+ This is the half an author has no other way to get right. §4.2's fold angle is not
1105
+ a rule of thumb once the angle is a dial position: it is the **top of the dial**,
1106
+ because past it a triangle turns inside out and `A39` refuses the build by name.
1107
+ `build` prints that angle for every depth mesh it compiles (AUTHORING §3.4), so
1108
+ the whole mapping falls out of one reading:
1109
+
1110
+ ```
1111
+ ceiling the largest turn this depth mesh admits — printed by `build`, not guessed
1112
+ range the largest whole degree strictly INSIDE the ceiling
1113
+ from -range max +range local true (AUTHORING §3.5.2's circle)
1114
+ scale seconds per degree — the one number here you choose
1115
+ duration 2 x range x scale
1116
+ time to + (degrees - from) x scale the slider's own mapping
1117
+ ```
1118
+
1119
+ 📐 **Worked, on [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look)**,
1120
+ whose face is a 21 × 9 `grid` over one depth sheet:
1121
+
1122
+ ```bash
1123
+ bun cli.ts build --rig gallery/look/rig.json \
1124
+ --motion gallery/look/motion.json \
1125
+ --out gallery/look/build
1126
+ ```
1127
+
1128
+ ```
1129
+ MESH head grid 189 vertices / 320 triangles (budget 320) bones=[head] attachments=[head]
1130
+ depth "face_depth.png" bf156ea0cfc970a3 near=white zScale=194 z=[0, 194]
1131
+ 80 of 189 vertices sample a texel the part image does not draw — their z is the sheet's reading of somewhere the part is not
1132
+ turn ceiling yaw +19.32° / -19.32° pitch +22.92° / -26.94°
1133
+ 1st pct yaw +19.32° x1.000 of 80 / -19.32° x1.000 of 80 pitch +22.92° x1.000 of 102 / -26.94° x1.000 of 130
1134
+ first to fold: yaw + at 19.32°, triangle 174 [119,138,139], the sheet steps 28.50 level(s) across it, which is 0.112 of the range this mesh sampled
1135
+ ```
1136
+
1137
+ ⇒ the ceiling is **±19.32°**, so the range is **19** — `floor(19.32)`, and
1138
+ *strictly inside* is the whole of the rule. At the **0.05 s per degree** that rig
1139
+ chooses, `turn` runs `2 × 19 × 0.05` = **1.9 s** and the map is
1140
+ `time = 0 + (degrees + 19) × 0.05`, which is the constraint as its rig spec
1141
+ declares it: `"from": -19, "to": 0, "scale": 0.05, "max": 19, "local": true,
1142
+ "additive": true`. ⭐ **The only two numbers there that anybody chose are `scale`
1143
+ and `to`** — and `to: 0` says nothing more than *the bottom of the range is the
1144
+ animation's first frame*. `from`, `max` and the duration are all the ceiling.
1145
+
1146
+ 🔸 **And `scale` is chosen for the endpoint.** rigc rounds every number it emits
1147
+ to six decimals, so a `scale` that is not exact there moves the top of the dial:
1148
+ `1/60` ships as `0.016667`, and a 60° turn then applies at 1.00002 s rather than
1149
+ 1 s — the last frame under `loop: false`, the *first* under `loop: true`
1150
+ (AUTHORING §3.5.2). `0.05` is exact at six decimals, which is the only reason the
1151
+ example can put its endpoint exactly on the duration. Pick a `scale` that is not,
1152
+ and land the endpoint inside the duration instead.
1153
+
1154
+ ⚠️ **The ceiling is per mesh, and the face's is not the smallest one on the
1155
+ face.** The same run prints one for every depth mesh, and in this example each
1156
+ sidelock reads `yaw +17.04° / -45.80°`: it folds at **17.04°** on one side, which
1157
+ is *inside* the ±19° the face itself admits, and not until 45.80° on the other.
1158
+ The asymmetry is the sheet's, not a coincidence — each of those sheets is
1159
+ steepest at the edge where the strand curves away, and a yaw folds a pair of
1160
+ vertices only in the direction their depth is rising.
1161
+
1162
+ ⇒ **A part whose ceiling is lower than the range has to be gone before the turn
1163
+ reaches it.** That is AUTHORING §3.4's third way to live with a ceiling: fade the
1164
+ slot to alpha 0 **inside the animation the slider applies**, landing the alpha-0
1165
+ key *before* the key that folds rather than on it. §7's paragraph on sliders is
1166
+ why that fade cannot be a second slider.
1167
+
1168
+ ⭐ **A lookup table wants linear keys, and that is not a style note.** The slider
1169
+ makes the pose a function of the dial, so an easing curve between two keys makes
1170
+ it a **non-linear** function of a number the consumer may be holding perfectly
1171
+ still — the face would drift and settle while the value sits where it was put.
1172
+ Anticipation and follow-through fail for the same reason, not a weaker one
1173
+ (MOTION §3.6, §3.7): both are functions of time, and there is no time on this
1174
+ axis. [MOTION §0.1](MOTION.md) is that split written out, and it is also where the
1175
+ shaping *does* belong — in whatever animation moves the dial.
1176
+
1177
+ ⚠️ **Two axes on one face need `"additive": true` on both.** A `pitch` dial
1178
+ beside the `yaw` is the ordinary case and it is the one the format's default
1179
+ breaks the moment the two share a target — in the worked example both `turn` and
1180
+ `tilt` key `headroll`. §7's paragraph on sliders is the mechanism and
1181
+ `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` is the refusal.
1182
+
1183
+ ⚠️ **What none of this measures: the Spine editor.** No editor export in this
1184
+ repository carries a slider, so whether the editor preserves two of them, their
1185
+ `additive` and `local` flags, and their order in the constraints array is
1186
+ **unknown**. `tools/editor_roundtrip.ts` on a machine with a licensed editor is
1187
+ what would answer it, and until somebody runs it the editor half of a parameter
1188
+ axis is untested. The runtime half is not: every figure above came back through
1189
+ `spine-core`.
1190
+
1060
1191
  ---
1061
1192
 
1062
1193
  ## 9. Looking at it, and the audit gap
@@ -1105,7 +1236,7 @@ problem — two eyes whose upper lash is heaviest at the **inner** corner read a
1105
1236
  angry whatever the brow above them does. Worth knowing before spending a pass on
1106
1237
  the wrong part.
1107
1238
 
1108
- ### 9.2 🚨 What nothing measures — three builds, all green
1239
+ ### 9.2 🚨 The half nothing measures — three builds, one of them refused
1109
1240
 
1110
1241
  **The setup geometry is measured; the deformed geometry was not, and one half of
1111
1242
  it still is not.** Here is that
@@ -1123,11 +1254,15 @@ bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
1123
1254
  ```
1124
1255
 
1125
1256
  ```
1126
- MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head]
1127
- attachments=[head] covers 100.00% of the art, reaching 95.90px past it
1128
- … 26 PASS, 13 SKIP
1257
+ MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
1129
1258
  ```
1130
1259
 
1260
+ Green, and the tally is left to the tool: `bun run selftest` prints this build's
1261
+ live assertion and skip counts on its `GALLERY_EXAMPLE_IS_GREEN[portrait/spine-html]`
1262
+ line. One written here would be a figure nothing in the tree compares against a
1263
+ run, sitting inside a fence that reads as a transcript — and rigc prints no tally
1264
+ line, so it never was one.
1265
+
1131
1266
  Now break the projection two ways. Both scripts write a variant motion spec
1132
1267
  beside the originals and touch nothing in the repository:
1133
1268
 
@@ -1137,14 +1272,21 @@ beside the originals and touch nothing in the repository:
1137
1272
  # the head reads as turning the other way at its own edge. This one has to
1138
1273
  # REPLACE the transform with a table: an inverted band is not the closed
1139
1274
  # form at any angle, and §1.1 refuses a `transform` beside a `vertices` run.
1275
+ # Each vertex takes the shift for THE COLUMN IT IS IN, looked up in the rig:
1276
+ # a `vertices` run is positional, so a script that assumes the list's order
1277
+ # rather than reading it breaks in silence the day the list is renumbered.
1140
1278
  bun -e '
1279
+ const r = await Bun.file("gallery/portrait/rig.json").json();
1141
1280
  const m = await Bun.file("gallery/portrait/motion.json").json();
1281
+ const v = r.skins.default.head.head.vertices;
1142
1282
  const d = m.animations.turn.deform.find(x => x.slot === "head");
1143
- const row = [-22.414, 0, -7.175, 0, -35.345, 0, -27.658, 0, -14.255, 0];
1283
+ const shift = {"-162": -22.414, "-120": -7.175, "0": -35.345, "120": -27.658, "162": -14.255};
1284
+ const run = [];
1285
+ for (let i = 0; i < v.length / 2; i++) run.push(shift[v[i * 2]], 0);
1144
1286
  for (const k of d.keys) if (k.transform) {
1145
1287
  delete k.transform;
1146
1288
  k.fromVertex = 0;
1147
- k.vertices = [...row, ...row, ...row, ...row, ...row];
1289
+ k.vertices = run;
1148
1290
  }
1149
1291
  await Bun.write("/tmp/swapped.motion.json", JSON.stringify(m, null, 2));
1150
1292
  '
@@ -1172,15 +1314,33 @@ is a doc command silently passing rather than silently failing, which is the
1172
1314
  worse of the two: build (b) reported `A39 PASS` and the table below said `FAIL`.
1173
1315
  Both are re-run above.
1174
1316
 
1317
+ ⚠️ **And script (a) broke a second time, the same way, on 2026-09-04.** It wrote
1318
+ one row of five shifts and repeated it five times, which was the mesh's own
1319
+ column order while the vertex list was row-major.
1320
+ [#375](https://github.com/firejune/rigc/issues/375) renumbered both gallery
1321
+ grids along their outline walk — perimeter first, then the interior, the order
1322
+ Spine's `hull` needs — and the row went on landing at the same *positions* in a
1323
+ list that no longer meant columns. The build stayed green and stayed wrong, so
1324
+ nothing on the page moved; what it stopped being was **an inverted band**, which
1325
+ is the one thing the table below reads it as. The figures in that table were
1326
+ right the whole time and the command under them had stopped producing
1327
+ them — a stale figure shows up at one site, and a broken command shows up at
1328
+ every figure it feeds, which is how the two are told apart: this one also
1329
+ contradicted §9.3's `check` row, `0.33 / 0.61` against a table saying
1330
+ `0.20 / 0.38`. ⭐ **The repair is the doctrine's own**: the script resolves each
1331
+ shift through the vertex's coordinate instead of its index, so the next
1332
+ renumbering cannot move it. A `vertices` run is positional by format — that is
1333
+ `fromVertex`'s whole job — and a *generator* of one has no reason to be.
1334
+
1175
1335
  **What comes back from both:**
1176
1336
 
1177
1337
  | | good | (a) one band inverted | (b) mesh folded |
1178
1338
  | --- | --- | --- | --- |
1179
- | `--profile spine-html`, **before `A39`** | 26 PASS / 13 SKIP | **26 PASS / 13 SKIP** | **26 PASS / 13 SKIP** |
1339
+ | `--profile spine-html`, **before `A39`** | green | **green, and the same counts** | **green, and the same counts** |
1180
1340
  | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | PASS | **PASS** | **PASS** |
1181
1341
  | the `MESH` coverage line | 100.00%, 95.90px past | **byte-identical** | **byte-identical** |
1182
1342
  | 🆕 `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | PASS | PASS | **FAIL, both keys, 8 of 32 triangles** |
1183
- | `--profile spine-html`, **today** | 27 PASS / 13 SKIP | 27 PASS / 13 SKIP | **26 PASS / 13 SKIP / 2 FAIL** |
1343
+ | `--profile spine-html`, **today** | green | green | **refused, and nothing written** |
1184
1344
  | 🆕 the `DEFORM` block, `head` key 1 `area` | x0.637174 … x1.319122 | **x0.765250 … x1.362834** | **x−0.288121 … x1.820211** |
1185
1345
  | 🆕 … and its `winding` | 32 of 32 kept | 32 of 32 kept | **24 of 32 kept** |
1186
1346
 
@@ -1193,22 +1353,46 @@ it mean anything.
1193
1353
 
1194
1354
  ⭐ **The two `DEFORM` rows are the ones that separate (a) from the good build**,
1195
1355
  and nothing else in the toolchain does that without a reference render. `A39` is
1196
- right to pass (a) — no triangle reverses — and the block prints **x1.362834**
1197
- where the model's own table says x1.319121, and **x0.765250** where it says
1198
- x0.637174. That is this section's own prose, *"stretches 1.363 where it should
1199
- compress to 0.637"*, as a figure the tool produces.
1356
+ right to pass (a) — no triangle reverses — so the whole of the difference is a
1357
+ pair of ratios, and the way to read them is to ask the tool rather than to copy
1358
+ them down:
1200
1359
 
1201
- `rigc explain` is the instrument that prints every other timeline's actual values,
1202
- and on a deform it used to print the shape of the run rather than the run:
1360
+ ```bash
1361
+ bun cli.ts explain --rig gallery/portrait/rig.json \
1362
+ --motion /tmp/swapped.motion.json --out /tmp/explain-swapped
1363
+ ```
1203
1364
 
1204
1365
  ```
1205
- default/head/head.deform 4 key(s)
1206
- t=0 back to the setup pose bezier[4]
1207
- t=0.62 deform[0..50] 25 pair(s) bezier[4]
1208
- t=1.5 deform[0..50] 25 pair(s) bezier[4]
1209
- t=2.2 back to the setup pose linear
1366
+ DEFORM turn default/head/head key 1 t=0.620000 authored table
1367
+ frame played on a track
1368
+ moved 25 of 25 vertices, worst 35.3450px at v10
1369
+ area min x0.765250 tri 19 max x1.362834 tri 1 (32 triangles, 0 with no area at the cleared pose, band 0.146694px²)
1370
+ stretch max x1.362834 tri 1 min x0.765250 tri 26
1371
+ winding 32 of 32 kept, 0 collapsed
1210
1372
  ```
1211
1373
 
1374
+ 🚨 **Read it band by band, because the extremes have swapped ends and comparing
1375
+ worst to worst hides that.** The block names the triangle beside every ratio, and
1376
+ §4.1's table says what each band should be. `tri 1` spans **−162 → −120**, the
1377
+ band §4.1 puts at **0.637** and the good build's own block reports as
1378
+ `x0.637174 tri 17` — the same band, and here it comes back at **x1.362834**. That
1379
+ is this section's prose, *"stretches 1.363 where it should compress to 0.637"*, as
1380
+ a figure the tool produces, and it is the far edge turning the wrong way. `tri 19`
1381
+ spans **−120 → 0**, tabled at 0.892 and measured at **x0.765250**: the swapped
1382
+ column is the boundary between those two bands, so the compression the far band
1383
+ gave up lands next door. ⚠️ Read as min-against-min and max-against-max instead,
1384
+ the very same four figures say 1.319 → 1.363 and 0.637 → 0.765 — two comparisons
1385
+ across *different* bands, both of them mild, neither of them what happened. Only
1386
+ the two bands the swapped columns bound move at all: the 0 → 120 and 120 → 162
1387
+ bands still read **1.064** and **1.319**, exactly as §4.1 tables them. `authored table` on the key line is the other half of the diagnosis:
1388
+ the model is gone, so nothing is left to check the ratios against but the ratios.
1389
+
1390
+ `rigc explain` is the instrument that prints every other timeline's actual values,
1391
+ and on a deform it used to print the shape of the run rather than the run: the
1392
+ head's four keys came back as four lines, each giving a time, a curve kind, and
1393
+ either `back to the setup pose` or `deform[0..50] 25 pair(s)` — the extent of the
1394
+ run and how many pairs are in it, and not one of the numbers.
1395
+
1212
1396
  ⇒ **`25 pair(s)` was the whole of what `explain` would tell you about a face
1213
1397
  turn**, against a scalar track two lines up in the same report printing
1214
1398
  `value=-35.345`. That asymmetry is
@@ -1224,6 +1408,7 @@ bun cli.ts explain --rig gallery/portrait/rig.json \
1224
1408
 
1225
1409
  ```
1226
1410
  DEFORM turn default/head/head key 1 t=0.620000 transform yaw radius=170 degrees=12
1411
+ frame played on a track
1227
1412
  moved 25 of 25 vertices, worst 35.3450px at v2
1228
1413
  area min x0.637174 tri 17 max x1.319122 tri 31 (32 triangles, 0 with no area at the cleared pose, band 0.146694px²)
1229
1414
  stretch max x1.319121 tri 22 min x0.637175 tri 8
@@ -1299,13 +1484,13 @@ area is a *quadratic in the interpolation fraction* — the fold is a root of it
1299
1484
  solved for rather than searched, with no sample spacing anybody would have to
1300
1485
  defend. What that arithmetic names is then posed and measured by the same code
1301
1486
  that measures a key, **alpha read at that same instant**, so the fade a correct
1302
- rig relies on is not refused and the frames it does not cover are:
1303
-
1304
- ```
1305
- FAIL A39_DEFORM_KEEPS_TRIANGLE_WINDING: animation "turn" deform head/head BETWEEN key 0
1306
- (t=0s) and key 1 (t=0.5s), at t=0.444089s — 88.8% of the way from one to the other:
1307
- 8 of 32 triangle(s) reverse winding … NO KEY LANDS THERE … at alpha 0.1118
1308
- ```
1487
+ rig relies on is not refused and the frames it does not cover are. The sentence names
1488
+ the two keys the fold lies between rather than one key index, the time it solved for
1489
+ and how far along the segment that is, the reversed triangles with their signed areas,
1490
+ `NO KEY LANDS THERE` in those words, and the alpha read at that same instant — so what
1491
+ it refuses is legible as a frame rather than as a key. AUTHORING §4.11.3 reads it field
1492
+ by field and [`src/validate.ts`](../src/validate.ts) builds it; no spec this repository
1493
+ ships produces one, the turn probe being `selftest.ts`'s own.
1309
1494
 
1310
1495
  ⇒ The rule this section gave — *fade out over the run up to the angle you cannot
1311
1496
  take, so that every key past the ceiling is one that draws nothing* — is
@@ -1371,9 +1556,9 @@ to call an audit:**
1371
1556
 
1372
1557
  📌 **`explain`'s `DEFORM` block (§9.2, AUTHORING §4.11.2) takes half of limits 1
1373
1558
  and 2 away, and none of limit 3.** It is reference-free, so it says something
1374
- about a first authoring; and it is *per key*, so the band inversion above reads
1375
- as `x1.362834` beside a model stating x1.319121 rather than as 0.20 of 255 in an
1376
- aggregate. What it still cannot say is whether **12° was the angle the shot
1559
+ about a first authoring; and it is *per key and per triangle*, so the band
1560
+ inversion above reads as `x1.362834` on `tri 1` where the same band is `x0.637174`
1561
+ in the model, rather than as 0.20 of 255 in an aggregate. What it still cannot say is whether **12° was the angle the shot
1377
1562
  wanted** — that needs the picture, which is why the procedure below survives the
1378
1563
  block as it survived `A39`.
1379
1564
 
@@ -1458,8 +1643,8 @@ with no `build/`, `render/` or `preview.html` in that directory:
1458
1643
 
1459
1644
  | Command | What came back |
1460
1645
  | --- | --- |
1461
- | `build --profile spine` | green — **18 PASS, 7 SKIP**, 14 excluded by profile |
1462
- | `build --profile spine-html` | green — **26 PASS, 13 SKIP**, including `A13_MESH_BUDGET` and `A15_IDLE_NO_MESH_BONE_KEYS` |
1646
+ | `build --profile spine` | green, and the report names what the profile leaves out rather than this page counting it: `profile spine — 7 renderer-policy and 8 archetype assertion(s) do not apply` |
1647
+ | `build --profile spine-html` | green — `profile spine-html — every assertion applies`, `A13_MESH_BUDGET` and `A15_IDLE_NO_MESH_BONE_KEYS` among the ones the row above excludes |
1463
1648
  | both `MESH` lines | `head` **100.00%** covered, reaching 95.90px past the art; `hair_bang` 100.00%, 55.22px |
1464
1649
  | `render --fps 25 --max 640` | **81 + 39 + 56 frames**, 478×640, three contact sheets |
1465
1650
  | `loop_seam.ts` ×3 | **0 / 255**, **0 of 305 920 pixels** differing, for all three |
@@ -1523,6 +1708,22 @@ cost off the keyboard and onto the **parts**: per-eye meshes, a meshed neck, a
1523
1708
  second art layer for the far cheek (§8). Whether to pay *that* is a project's
1524
1709
  decision and this page does not make it.
1525
1710
 
1711
+ 🚫 **No Live2D file is read or written, and none ever will be — a boundary
1712
+ rather than an unbuilt feature, and it runs in both directions.** rigc's inputs
1713
+ are a rig spec and a motion spec; its outputs are Spine 4.3 skeleton data and an
1714
+ atlas. There is no importer, no exporter and no converter for `.moc3`, `.cmo3`,
1715
+ `.model3.json` or anything else in that family, and nothing in this repository
1716
+ claims compatibility with that format in either direction
1717
+ ([#399](https://github.com/firejune/rigc/issues/399) is where that was settled).
1718
+ ⭐ **What is in scope is an authoring idea, stated on its own terms rather than
1719
+ as anybody's feature: that a face angle can be a value rather than a time.**
1720
+ §8's *The turn as a value rather than a time* is that idea on Spine's own
1721
+ `slider` constraint, and every mechanism under it is Spine's — the arithmetic,
1722
+ the flags, the readers and the failure modes are all in AUTHORING §3.5.2 and all
1723
+ measured against `spine-core`. ⚠️ Nothing on this page is a statement about how
1724
+ any other tool works inside, and nothing above implies one: what this repository
1725
+ has measured is its own format.
1726
+
1526
1727
  🚫 **No per-eye mesh recipe.** §8 says the eyes need their own deform meshes past
1527
1728
  about 26°, and nobody has built that here. The column-placement arithmetic in
1528
1729
  §4.2 applies to any grid over any curved patch, so the tangent limit is the part
package/docs/INGEST.md CHANGED
@@ -607,14 +607,11 @@ rigc validate examples/spineboy/export/spineboy-pro.json \
607
607
  rigc: green
608
608
  ```
609
609
 
610
- **Why it is worth a section anyway.** That line used to be two `FAIL`s:
611
-
612
- ```
613
- FAIL A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: … key 1: offset 1 is odd, so the run's x values land on y slots and back again
614
- FAIL A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: … key 1: the run holds 147 numbers and the deform array is x, y pairs
615
- ```
616
-
617
- and nothing was wrong with the data. `hoverboard-board` is an unweighted mesh with 148
610
+ **Why it is worth a section anyway.** That line used to be two `FAIL`s on the same key —
611
+ one for an **odd `offset`**, on the reading that the run's x values would land on y slots
612
+ and back again, and one for an **odd-length run**, on the reading that the deform array is
613
+ x, y pairs — and nothing was wrong with the data, so neither sentence exists in the tool
614
+ any more. `hoverboard-board` is an unweighted mesh with 148
618
615
  floats; the key carries `offset: 1` and 147 values, covering `1..148` — the whole array
619
616
  minus a leading zero the editor trimmed. A trim can land on a y component, so an odd
620
617
  offset is what a trimmed run looks like, and Spine's own parser copies the run in at the
@@ -846,13 +843,12 @@ rig spec; the discipline is in what you check afterwards.
846
843
  `parent`, a slot's `bone`, a constraint's `bones` and `target`, a draw-order key's
847
844
  `slot`, an authored mesh's vertex `weights` — and, across the two files, the motion
848
845
  spec's `archetype` against the rig spec's `name`. That last one is the first refusal a
849
- rename produces, before anything else has a chance to go wrong:
850
-
851
- ```
852
- rigc compile error: …/work/renamed/pendulum-rig.motion.json: motion spec names
853
- archetype "3-timing-and-spacing-ess" but the rig spec at
854
- …/work/renamed/pendulum-rig.rig.json is called "pendulum-rig"
855
- ```
846
+ rename produces, before anything else has a chance to go wrong: a `rigc compile error`
847
+ naming the motion spec's path, the `archetype` that spec states, the path of the rig spec
848
+ it was handed, and the `name` that rig actually carries — both sides of the mismatch in
849
+ one sentence. [`src/compile.ts`](../src/compile.ts) builds it; the renamed copies this
850
+ section works on are not committed, for the reason the Appendix gives, so the figure is
851
+ described here rather than transcribed off one.
856
852
 
857
853
  ⭐ **A rename is therefore mostly safe by construction, and its failures arrive as
858
854
  sentences naming both sides.** That is the reason to do it in the specs rather than in
package/docs/MOTION.md CHANGED
@@ -80,6 +80,52 @@ and the last hop is the one that matters: a `both-unacceptable` tie means **prop
80
80
  again from a different axis** (§4), not *nudge the same candidate*. §5 has the
81
81
  detail.
82
82
 
83
+ ### 0.1 When the axis is not time — an animation a `slider` applies
84
+
85
+ ⛔ **One shape of request does not normalise to the table above, and carrying it
86
+ through §3 anyway produces a defect nothing on this page can measure.** A `slider`
87
+ constraint (AUTHORING §3.5.2) applies an animation as a function of a **value**: it
88
+ reads a driving bone's transform property, maps it with
89
+ `time = to + (value − from) × scale`, and applies the animation at that time on
90
+ every frame. What it applies is an ordinary animation in the motion spec — same
91
+ tracks, same keys, the same `duration` — but **nothing plays it**. It is a lookup
92
+ table, and `t` in it is a coordinate on the axis rather than a moment.
93
+
94
+ ⇒ **So the constructs in §3 that are functions of time are not available to it,
95
+ and each fails in its own way rather than merely reading oddly:**
96
+
97
+ | §3 construct | On an animation a slider applies |
98
+ | --- | --- |
99
+ | §3.4 slow in and slow out | an easing curve makes the pose a **non-linear** function of the dial. The consumer moves the value at one rate and the face moves at another, and at a value held still the pose is still whatever the curve says there |
100
+ | §3.6 anticipation | places a counter-pose at a dial *position*, so the pose runs backwards while the value runs forwards. Nothing anticipates a number |
101
+ | §3.8 overshoot and settle | there is no settle: at a held value the pose is what the table says at that value, indefinitely. An overshoot keyed past the extreme is just a wrong pose at the top of the dial |
102
+ | §3.7 follow-through and the offset table | wants parts to arrive at different **times**. Here they differ by **amount** at every value, which is §3.7.1's construct and not this one |
103
+ | §3.3 timing | the key times are the axis's own coordinates — one per angle, position or level the table states — so spacing them is choosing where to sample, not choosing a rhythm |
104
+
105
+ ⭐ **What is still this page's job is whatever moves the dial**, and that is an
106
+ ordinary animation with §3 applying to it unchanged. The split is visible in the
107
+ worked example: in
108
+ [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) the two
109
+ lookup tables `turn` and `tilt` carry **no easing at all**, and `sweep` — the one
110
+ animation there meant to be played — carries every `ease` in the file.
111
+
112
+ ```bash
113
+ bun -e 'const m = JSON.parse(await Bun.file("gallery/look/motion.json").text());
114
+ for (const [name, a] of Object.entries(m.animations))
115
+ console.log(name, (JSON.stringify(a).match(/"ease"/g) ?? []).length);'
116
+ ```
117
+
118
+ ⚠️ **And two of §4's candidate axes stop being axes.** *Anticipation* and
119
+ *Termination* are both readings of how a movement is placed in time, so a ballot
120
+ spread on either of them over a slider-applied animation is asking a person to
121
+ choose between two wrong answers. The axes that survive are the ones about
122
+ **amount** — *Part amount*, *Path*, *Key density* — because those are still
123
+ readings of the value.
124
+
125
+ 📘 The face case is worked end to end in [FACE.md](FACE.md): its §8 derives the
126
+ slider's range from the turn ceiling `build` reports, which is the one number on
127
+ that axis an author cannot guess.
128
+
83
129
  ---
84
130
 
85
131
  ## 1. Prompt grammar — what a request is made of
@@ -259,6 +305,12 @@ would not exist even if the user had more pictures of the same two poses. Everyt
259
305
  below is therefore authored knowledge, and it is sourced the way AUTHORING §10 sourced
260
306
  the editor's conventions.
261
307
 
308
+ ⚠️ **All of it assumes the axis is time.** If what you are authoring is an
309
+ animation a `slider` applies — a face angle, a dial, a suspension that compresses
310
+ as the wheel rises — §0.1 is the exception, and it is not a small one: the easing,
311
+ the anticipation and the overshoot below each produce a specific defect there
312
+ rather than merely reading oddly.
313
+
262
314
  ### 3.1 Where these come from, and how each line is marked
263
315
 
264
316
  Two public bodies of material, and nothing else: **the twelve basic principles of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/diff.ts CHANGED
@@ -685,10 +685,20 @@ function diffAttachments(c: Json, r: Json): DiffSection {
685
685
  // The issue's diagnosis was that Spine's exporter omits `width`/`height`
686
686
  // when they match the atlas region, so a rigc rig — which always states
687
687
  // them (AUTHORING R1/R5) — could never agree. That is not what the corpus
688
- // says: all twelve reference exports state a size on every one of their 168
689
- // regions, so there is nothing for an atlas lookup to resolve and the
690
- // `--atlas` plumbing the issue proposed would be dead code against every
691
- // rung on the ladder.
688
+ // says: all twelve reference exports state a size on every one of their
689
+ // regions, and no region omits either field — so there is nothing for an
690
+ // atlas lookup to resolve and the `--atlas` plumbing the issue proposed
691
+ // would be dead code against every rung on the ladder.
692
+ //
693
+ // ⛔ No count on that claim, deliberately, and this was the fourth copy of
694
+ // the one issue #490 struck off `docs/LADDER.md`: nothing derives the
695
+ // figure — not the plainest reading, every `region` attachment under
696
+ // `skins` in the twelve `export/*.json` skeletons, and not counting by
697
+ // skin, by slot, by attachment name, by atlas path or by atlas region. The
698
+ // claim carries the argument without one, because it is universal and a
699
+ // single counterexample refutes it: after `bun run fetch-examples`, read
700
+ // the region attachments of those twelve files and check that each states
701
+ // both `width` and `height`.
692
702
  //
693
703
  // What the measure actually reported was the naming gap, a third time. It
694
704
  // was keyed by `skin/slot/attachment`, so it could never exceed the name