rig-c 0.0.0-stage → 2.20.4

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.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
package/docs/FACE.md ADDED
@@ -0,0 +1,1948 @@
1
+ # Authoring a face — a turn, a gaze and a blink on plain Spine data
2
+
3
+ **Read this when the request is a head rather than a body.** It is written for an
4
+ agent that has been handed a drawn face — or has to draw one — and asked for the
5
+ moves a portrait makes: it breathes, it blinks, its eyes move, and it **turns a
6
+ few degrees off axis**. The last one is the reason this page exists. It is the
7
+ move a second format is usually bought for, and it is authorable here, out of an
8
+ ordinary `deform` timeline plus per-part parallax, at a cost this page prices
9
+ before you spend it.
10
+
11
+ [AUTHORING.md](AUTHORING.md) is the format — read it first and keep it open; this
12
+ page never restates a field it documents, and
13
+ [AUTHORING §4.11](AUTHORING.md) is the `deform` timeline field by field.
14
+ [MOTION.md](MOTION.md) is the recipe for
15
+ *movement* — timing, easing, anticipation, follow-through, the offset table — and
16
+ everything in it applies to a face unchanged. This page is the part neither has:
17
+ **a face's own geometry**, and what a projection costs when the spec can only
18
+ hold its results.
19
+
20
+ - The `deform` timeline, field by field, and the six things rigc refuses in it:
21
+ **AUTHORING §4.11**
22
+ - Named failures, and the file each one points at: **AUTHORING §5–§6** — the
23
+ refusals this page names are read there, and §4.2 quotes the one that points
24
+ back at this page
25
+ - Timing, easing, arcs, anticipation, follow-through, the per-bone offset table:
26
+ **MOTION §3** — a blink and a gaze are ordinary MOTION.md work
27
+ - Candidate spreading and the ballot: **MOTION §4–§5**. Nothing on this page
28
+ replaces a person's eye
29
+ - A skeleton somebody else authored, and moving a pivot inside it:
30
+ [INGEST.md](INGEST.md)
31
+ - The hierarchy underneath a face, as a general rule rather than this closed form:
32
+ [RIGGING.md](RIGGING.md) — §5 is why §3's `faceshift` is a bone at all and why a
33
+ breath's `chest` is a **sibling** of the plate it must not scale, and §4.4 is the
34
+ artless-parent pattern the shared-shift split is one instance of
35
+ - The worked example every number below comes from:
36
+ [`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait),
37
+ and its measurement half,
38
+ [`FINDINGS.md`](https://github.com/firejune/rigc/tree/main/gallery/portrait/FINDINGS.md)
39
+ - **The same closed forms on the other axis**:
40
+ [`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod) is a
41
+ worked `pitch`, and this page stays written for a **yaw**. It re-derives §4.2's
42
+ fold angle on uneven *rows* and brackets it against `A39` at 33°/34°, and it
43
+ measures §5's foreshortening at **0.863–1.176** against the yaw's 0.892–1.064
44
+ below — a wider span at the same 12°, because a face is taller than it is deep.
45
+ Read it after this page, not instead of it
46
+ - **The turn driven by a value instead of played as a time** — the same keys, on
47
+ an axis: §8's *The turn as a value rather than a time* derives the range,
48
+ **AUTHORING §3.5.2** is the `slider` constraint that does it, and
49
+ [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) is
50
+ the worked case. Read it when the angle has to follow something outside the
51
+ animation — a pointer, a gaze target, a game value
52
+
53
+ 🚨 **A `deform` key's winding is gated; how far it moved the geometry is not, and
54
+ that half is the one you have to author around.**
55
+ `A39_DEFORM_KEEPS_TRIANGLE_WINDING` refuses a key that turns the mesh inside out —
56
+ by name, by key and by triangle — and the build writes nothing. What no assertion
57
+ has an opinion about is **magnitude**: a band that stretches where the projection
58
+ says it should compress keeps every triangle's winding, so it gates green, beside
59
+ the same `MESH` coverage line as the good build, because coverage reports the
60
+ *setup* pose. §9.2 is that pair as three builds of one rig — the good one, one
61
+ wrong and green, one refused. For the half still ungated, `explain`'s `DEFORM`
62
+ block prints each key's area and stretch ratios per triangle with no reference
63
+ render (§9.2, AUTHORING §4.11.2), and §9.3 is the differential audit, the three
64
+ things it cannot do, and the procedure that survives them.
65
+
66
+ 📐 **Where the numbers on this page come from.** Every figure marked **derived**
67
+ is re-computed from the closed form in §1 and reproduces to the digits printed.
68
+ Every figure marked **measured** was read off the shipped artifact by the
69
+ `gallery/portrait` record and carries its scope in the line. Nothing here is a
70
+ prediction, and nothing here is a pass bar.
71
+
72
+ ---
73
+
74
+ ## 0. The normal form
75
+
76
+ **A face request normalises to a depth list plus MOTION.md.** That is the whole
77
+ internal shape:
78
+
79
+ | What arrived | What it becomes |
80
+ | --- | --- |
81
+ | a face that **blinks**, **breathes**, **looks around** | ordinary MOTION.md tracks. A lid, a chest, an iris. Nothing on this page is needed |
82
+ | 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 |
83
+ | 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 |
84
+ | 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 |
85
+
86
+ ⭐ **The turn is the only part of a face that is not already MOTION.md's job**,
87
+ and it is 90% of this page. A blink is a translating plate (§6); a gaze is
88
+ MOTION §3.7's offset table applied to an eye (§7). If the request does not
89
+ contain a turn, read §6 and §7 and stop.
90
+
91
+ The one thing that is genuinely new: a turn is **not a pose you can key**. It is
92
+ a projection, so its values are not chosen, they are *evaluated* — and the
93
+ authoring is transcription rather than judgement. Which means the failure mode is
94
+ not "it reads badly", it is **"one number is wrong and nothing can see it"**.
95
+ That is what §3 and §9 are for.
96
+
97
+ ---
98
+
99
+ ## 1. The one line, and that it **is** one line
100
+
101
+ Treat the face as painted on a cylinder standing on the skull's vertical axis. A
102
+ yaw of `t` about that axis carries a point at `(x, z)` — `x` across the screen,
103
+ `z` toward the viewer — to `x·cos t − z·sin t`, so the shift a part takes is
104
+
105
+ ```
106
+ dx = x·(cos t − 1) − z·sin t
107
+ \____________/ \______/
108
+ the head depth times sin t:
109
+ narrowing to the WHOLE of the move
110
+ cos t of its
111
+ width (2.2% at 12°)
112
+ ```
113
+
114
+ **That is the entire model.** Everything else on this page is that expression
115
+ evaluated somewhere, and the two terms are worth reading separately:
116
+
117
+ - **`−z·sin t` is proportional to depth.** A part further forward travels
118
+ further. That *is* parallax, and it is why a fringe moves more than a face and
119
+ why hair behind the axis moves the **other way** (§2).
120
+ - **`x·(cos t − 1)` pulls both edges inward by the same amount** — the head
121
+ narrowing. It is second order and it is small: at 12° it is 2.2%, which is 3.5
122
+ units at the silhouette against a 35-unit centre shift.
123
+
124
+ At the 12° the worked example ships (**derived**):
125
+
126
+ ```
127
+ t = 0.209440 rad cos t − 1 = −0.021852 sin t = 0.207912
128
+ ```
129
+
130
+ ⇒ **A reader who has this line derives every number in a turn and never reaches
131
+ for a hand-tuned table.** That matters more than it sounds: a hand-tuned face
132
+ table has no property you can check, and a derived one has exactly one — it
133
+ agrees with the line, or it does not.
134
+
135
+ ### 1.1 ⭐ And now the spec holds the line, not its results
136
+
137
+ **A `deform` key states this expression** (AUTHORING §4.11.1). The worked
138
+ example's held 12° yaw is four lines of which two are the angle:
139
+
140
+ ```json
141
+ { "t": 0.62, "transform": { "kind": "yaw", "radius": 170, "degrees": 12 }, "ease": "swell" },
142
+ { "t": 1.5, "transform": { "kind": "yaw", "radius": 170, "degrees": 12 }, "ease": "settle" }
143
+ ```
144
+
145
+ ⇒ **`radius` is `R` off the depth table (§2) and `degrees` is the angle.** The
146
+ compiler evaluates `dx = x·(cos t − 1) − z·sin t` at every vertex with
147
+ `z = √(R² − x²)`, writes the millimetre-level results into the artifact, and
148
+ `explain` prints both the model and the offsets it produced. What the key
149
+ states is a measurement of the shape the drawing implies, and nothing else.
150
+
151
+ Three things follow, and they are the reasons the construct exists rather than
152
+ side effects:
153
+
154
+ - **A second angle is a second number.** Each angle of §8's cliff sweep — 8, 12,
155
+ 16, 20, 24, 28 and 32 degrees — is `"degrees": <n>` and a rebuild.
156
+ - **A small yaw is one key.** The anticipation MOTION §3.6 asks for and the
157
+ head-follow §7 prices are each one key with a smaller `degrees`.
158
+ - **The audit is two parameters.** A transcription can only be checked against the
159
+ line by hand, which is §9.3's gap; a stated model is checked by reading two
160
+ parameters. What is still unmeasured is the *consequence* — whether that angle
161
+ folds the mesh — and §4.2 and A39 are that half.
162
+
163
+ ⚠️ **What it does not do.** It evaluates; it never chooses. The radius, the
164
+ angle, the depth table and whether 12° reads are all yours, and a wrong `radius`
165
+ produces 160 consistent wrong numbers at once — see §4's warning, which the
166
+ construct makes cheaper to get wrong rather than harder.
167
+
168
+ 📌 **Author `t` in degrees, in the spec.** Radians appear nowhere: the key holds
169
+ the angle and the compiler holds the conversion.
170
+
171
+ ---
172
+
173
+ ## 2. Depth is the parameter you are actually authoring
174
+
175
+ 🚨 **A part list for a face is not a list of drawings. It is a list of
176
+ `(x, z)`.** The bones carry `x` (`eye_l` at `−62`, `nose` at `0`) and the rig
177
+ carries the mesh columns' `x`; the `z` is in no drawing.
178
+
179
+ ⭐ **Every depth goes in the file, and each half has its own construct.** A `yaw`
180
+ deform key states its `radius` (§1.1), which is the `R` of the cylinder that plate
181
+ is painted on — so the head's 170 and the fringe's 196 are in `motion.json`. A
182
+ **bone** track's key can state a `derive` model over a group, whose `depth` is one
183
+ number **per member** — so the feature depths, the hair depths and the sign flip
184
+ below are in the file too (§3, AUTHORING §4.5.1). Neither is spelled `"z"`: the
185
+ field is `radius` on a mesh key and `depth` on a bone track.
186
+
187
+ ⇒ **Write the depth table down somewhere a reader will find it.** The specs hold
188
+ the depths the turn *uses*; a project's own reasoning about them — why the fringe
189
+ stands off 26 and not 15 — belongs beside the rig, and in the worked example that
190
+ is a table in its README.
191
+
192
+ ⚠️ **A depth is a decision.** The table below is the thing to argue about before
193
+ authoring anything, and the point of writing it into the spec is that a reader of
194
+ the numbers can see the table that produced them:
195
+
196
+ ```bash
197
+ grep -c '"depth"' gallery/portrait/motion.json # 8, one per derive key
198
+ ```
199
+
200
+ **The depths in the worked example**, and what each one is doing (**derived**
201
+ column: `dx` at 12°):
202
+
203
+ | Part | `x` | `z` | `dx` | What the depth is |
204
+ | --- | --- | --- | --- | --- |
205
+ | face centre (skull surface) | 0 | **170** | −35.345 | `R`, the cylinder radius. Every other number is relative to this |
206
+ | fringe centre | 0 | **196** | −40.751 | 26 **in front** of the skull. The one number in the rig that exists only to make parallax |
207
+ | `nose` | 0 | **192** | −39.919 | protrudes 22. The only feature in front of the surface |
208
+ | `eye_l` / `eye_r` | ∓62 | 150 | −29.832 / −32.542 | 20 **below** the surface — a socket is a hollow |
209
+ | `brow_l` / `brow_r` | ∓62 | 158 | −31.495 / −34.205 | 12 below. A brow sits proud of its socket |
210
+ | `mouth` | 0 | 166 | −34.513 | 4 below. Almost on the surface |
211
+ | `hairmass` (back hair) | 0 | **−55** | **+11.435** | its centroid is 55 **behind** the axis |
212
+ | `lock_l` / `lock_r` | ∓150 | 100 | −17.513 / −24.069 | off-axis and shallow, so they travel about half as far as the centre |
213
+ | `ahoge` (cowlick) | −23 | 20 | −3.656 | almost on the axis, so it barely moves |
214
+
215
+ ⭐ **The sign flip is the strongest single depth cue you can buy.** `hairmass` at
216
+ `z = −55` makes `−z·sin t` positive: as the face swings left, the back of the
217
+ head swings **right**. It is one track, one number, and it is what separates a
218
+ turn from a slide more than any mesh work does.
219
+
220
+ ⚠️ **Get the depths right and the parallax is free; get one wrong and nothing
221
+ complains.** A depth is not measurable from the art — it is a decision about a
222
+ shape the drawing only implies. Two readings that help:
223
+
224
+ 1. **Depth is what the drawing overlaps for.** A part drawn *over* another is
225
+ usually in front of it, and the slot order already records that (AUTHORING R4).
226
+ Depth is the same ordering with a magnitude attached, so **derive the sign from
227
+ the draw order and argue only about the size.**
228
+ 2. **The stand-off is the number to state out loud.** The fringe's 26 is not a
229
+ measurement of anything; it is how much parallax the shot wanted. At 12° it
230
+ buys `26·sin 12° = 5.406` units of extra travel — **derived**, and the record
231
+ measured 5.406 on the artifact. If the fringe does not read as separate,
232
+ this is the one number to move, and moving it moves nothing else.
233
+
234
+ ### 2.1 One depth per part, and the limit of that
235
+
236
+ Everything above states **one depth per part** — a stand-off the whole plate
237
+ shares. That is the right resolution for a part list: the fringe is 26 in front
238
+ of the skull, and arguing about which *pixel* of the fringe is 26 would be
239
+ arguing past what the drawing says.
240
+
241
+ It stops being the right resolution the moment a part's own surface is the
242
+ subject. §4 meshes the face plate into columns precisely because the plate is not
243
+ flat, and §4.2 then finds that refining those columns makes the fold **worse**,
244
+ not better — the columns are sampling a cylinder that was never the shape of a
245
+ face. A cylinder is one number pretending to be a surface, and past a certain
246
+ density the pretence is what fails.
247
+
248
+ ⭐ **Two things arrive together, and both are generated rather than written.**
249
+ [AUTHORING §3.4](AUTHORING.md#grid--a-lattice-over-the-part-window)'s `grid`
250
+ generator builds the lattice from the column positions alone, and reproduces this
251
+ example's hand-numbered 25 vertex pairs, 32 triangles and hull walk exactly. That
252
+ is the prerequisite: a turn needs interior vertices to move, and
253
+ §4.3's contour has none.
254
+
255
+ ⭐ **And a depth map is the number above becoming a surface.** A greyscale sheet in the
256
+ part's own pixel grid gives every mesh vertex its own `z`, sampled where the
257
+ vertex actually is, and `yaw`/`pitch` project off it with the same closed form as
258
+ §1 — only the source of `z` changes. What it buys is stated where it is authored:
259
+ [AUTHORING §3.4](AUTHORING.md#depth--give-every-vertex-its-own-z-instead-of-one-cylinder-radius),
260
+ with the sampling order, the refusals and the one that matters most (a sheet cut
261
+ to the art covers **none** of a contour mesh's vertices, because they all sit on
262
+ the silhouette and outside it).
263
+
264
+ ⭐ **And the mesh really is evaluating the pixels' model — measured, not
265
+ asserted.** A consumer that renders the same sheet in a shader displaces every
266
+ PIXEL by its own depth; a mesh displaces vertices and interpolates across
267
+ triangles. On a dome (a ramp would prove nothing — linear interpolation is exact
268
+ on a linear field) at 18°, the two evaluations converge as the lattice refines:
269
+
270
+ **No run reproduces this:** the 2026-09-05 density study's figures, printed by `bench/studies/2026-09-05-density/tools/densprobe.ts` and kept in its `evidence/`; no command this page states re-takes them
271
+
272
+ | lattice | 3×3 | 5×5 | 9×9 | 17×17 | 33×33 |
273
+ | --- | --- | --- | --- | --- | --- |
274
+ | mean disagreement, px | 6.4141 | 1.8452 | 0.5887 | 0.2276 | **0.0926** |
275
+ | the same lattice reading a **cylinder** instead | 3.8972 | 3.1628 | 3.2558 | 3.3429 | **3.3777** |
276
+
277
+ 🚨 **Read the second row before the first.** A mesh evaluating the wrong surface
278
+ does not converge — it settles at ~3.4px however dense it gets — and at 3×3 it
279
+ reads *better* than the right one. So one measurement at one density cannot tell
280
+ the two models apart, and would have picked the wrong one. The claim is the
281
+ convergence, never a single number. (The worst
282
+ case falls more slowly than the mean and is expected to, because the dome's rim
283
+ has an unbounded depth gradient.)
284
+
285
+ ⚠️ **This is not the same quantity as §4.2's fold angle, which refining makes
286
+ WORSE.** Both are true: a finer lattice buys fidelity to the model and costs the
287
+ angle at which a column pair inverts. This measures the first; `A39` refuses the
288
+ second.
289
+
290
+ ⚠️ **It does not make depth measurable.** The map is relative — 8 bits of level
291
+ say nothing about world units — so `zScale` is authored exactly as the fringe's
292
+ 26 was, and the two warnings above survive intact: get it right and the parallax
293
+ is free, get it wrong and nothing complains. What the map removes is the
294
+ *resolution* limit, not the judgement.
295
+
296
+ ### 2.2 The angle belongs to the map, not to the mesh
297
+
298
+ §2.1 leaves an open question, and it is measured here. §4.2 finds that
299
+ refining the lattice makes the fold **worse**, and read that as the cylinder's
300
+ error surfacing — so per-vertex depth ought to have bought the angle back. ⛔ **It
301
+ does not, and it never could have.**
302
+
303
+ Two neighbouring vertices swap places when the turn tips one past the other,
304
+ which is `tan t ≥ Δu/Δz` — so the largest turn a part supports is
305
+
306
+ tan t_max = 1 / max |dz/du|
307
+
308
+ **the reciprocal of the steepest slope anywhere in its depth map**, and there is
309
+ no mesh in that formula at all. Refining the lattice does not change the angle;
310
+ it changes which slopes the lattice is close enough to *find*. A map with a
311
+ vertical edge has an infinite slope there, so a fine enough mesh folds at any
312
+ angle you name.
313
+
314
+ ⭐ **Which is exactly what a dome is.** `z = Z√(1 − r²)` is vertical at its rim,
315
+ and an even lattice over it folds at `tan t = √h·√(R/2)/Z` for spacing
316
+ `h = W/(side−1)` — a **√h** that goes to zero. Measured against that closed form
317
+ over a 1,300× range of vertex counts, agreeing to ≤ 1.2°:
318
+
319
+ **No run reproduces this:** the same 2026-09-05 density study, its `evidence/` and its harness; the closed-form rows are arithmetic and the measured rows are that run's
320
+
321
+ | lattice | 5×5 | 17×17 | 33×33 | 65×65 | 129×129 | 181×181 |
322
+ | --- | --- | --- | --- | --- | --- | --- |
323
+ | **dome**, largest turn admitted | 62° | 41° | 31° | 23° | 17° | **14°** |
324
+ | the closed form above | 59.0° | 39.8° | 30.5° | 22.6° | 16.4° | **14.0°** |
325
+ | **raised cosine**, slope bounded | 73° | 65° | 64° | 64° | 64° | **63°** |
326
+ | its closed form, `atan(2R/Zπ)` | 64.8° | 64.8° | 64.8° | 64.8° | 64.8° | **64.8°** |
327
+
328
+ ⇒ **Author the map so its slope is bounded, and the angle stops depending on the
329
+ mesh.** A raised cosine — flat at the centre, flat again at the rim — holds
330
+ 63–64° from 289 vertices to 32,761. The dome loses three quarters of its angle
331
+ over the same refinement. Both are "correct" depth; only one of them is a
332
+ *surface a turn can be built on*, and the difference is entirely in the input.
333
+
334
+ 🚨 **So a map traced straight off a rendered normal or a photogrammetry pass is
335
+ the dome case, not the cosine case.** Where the part curves away to its
336
+ silhouette is where every such map goes vertical. Flattening it there — letting
337
+ z reach its floor *before* the outline rather than at it — is the edit that buys
338
+ the angle, and it is an edit to the sheet. rigc will not do it for you: the
339
+ compiler never invents a value that is not in the spec, and a depth map is a
340
+ measurement.
341
+
342
+ ⚠️ **This does not touch §2.1's convergence.** A finer lattice still evaluates
343
+ the map's own surface more faithfully; it also finds steeper slopes in it. Those
344
+ are two different quantities and both are true — §2.1 measures the first, `A39`
345
+ refuses the second. What is new here is that the second is a fact about the
346
+ sheet, and can be fixed there.
347
+
348
+ ⭐ **And you do not have to find the ceiling by building into it.** `build`
349
+ and `explain` print it for any mesh with a depth map — per axis, per direction,
350
+ naming the triangle that goes first — from `tan t = A₀/A_axis` on the mesh's own
351
+ geometry ([AUTHORING §3.4](AUTHORING.md)):
352
+
353
+ ```
354
+ turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
355
+ 1st pct yaw +31.55° x1.004 of 1004 / -32.10° x1.003 of 1044 pitch +32.10° x1.003 of 1044 / -31.55° x1.004 of 1004
356
+ first to fold: yaw + at 31.41°, triangle 960 [113,112,593], the sheet steps 12.52 level(s) across it, which is 0.049 of the range this mesh sampled
357
+ ```
358
+
359
+ Read it as a fact about **the sheet**. If the number is too small, the fix is in
360
+ the map — flatten it where the part curves away — and not in the lattice.
361
+
362
+ #### ⚠️ That rule presumes the map is continuous
363
+
364
+ "Flatten it where the part curves away" is an edit to a **surface**, and it
365
+ assumes there is one under the whole mesh. A depth sheet estimated from a
366
+ picture of cut-out art is not a surface: it is piecewise, with a **cliff at every
367
+ occlusion boundary** — figure against background at the silhouette, and one part
368
+ of the figure over another wherever they overlap. There is nothing to flatten
369
+ across a cliff, because the two sides are not two ends of a slope. They are two
370
+ different things at two different depths, and a 2.5D turn does not model
371
+ occlusion at all.
372
+
373
+ The ceiling reads the cliff, correctly, and the number it reports is real:
374
+ walked one degree at a time through the survey `A39` refuses from, a reported
375
+ 1.936° admits +1° and reverses 8 triangles at +2°. **The rig genuinely folds at
376
+ two degrees.** What is wrong is not the instrument and not the mesh — it is that
377
+ the question "how far can this turn" has no answer for an input with a
378
+ discontinuity in it, and the ceiling proves it by halving with every doubling of
379
+ the lattice: measured tangent ratios 2.02 / 1.95 / 2.02, `tan t ∝ h` exactly,
380
+ with no limit to converge to.
381
+
382
+ 🚨 **And the two figures beside the ceiling both call it healthy.** The 1st
383
+ percentile reads 1.02–2.17, which is a band reaching the limit together — and it
384
+ *is* a band, because an outline is long. The depth step reads 148–252 levels of
385
+ 255, far above the quantisation floor — and the sheet really did say that much,
386
+ in one step. Divided by the range, the same number says the opposite: the step
387
+ share pins at **0.92–0.99** where rigc's own gallery reads 0.112 and 0.468.
388
+
389
+ ⇒ Two ways out, and **neither of them is flattening**:
390
+
391
+ - **Mesh only what is continuous.** One face, one lock, one sleeve — a region
392
+ the sheet describes without a jump in it — rather than a lattice over a whole
393
+ figure. Whether a mask that tight gives a usable angle is not yet measured;
394
+ the mask has to be painted rather than thresholded, for the reason `soft`
395
+ is painted (§3.4's `soft` block).
396
+ - **State a sheet that was authored rather than estimated.** §2.2's raised
397
+ cosine holds 63–64° from 289 vertices to 32,761 because somebody drew its
398
+ slope. That is the input this whole section is about.
399
+
400
+ ⛔ rigc will not decide that your sheet is the wrong kind of thing. It has every
401
+ authority to say what it measured, and the step share is that.
402
+
403
+ 📐 Method, harness and the full ladders live in the repository rather than in
404
+ this package, as
405
+ [`bench/studies/2026-09-05-density`](https://github.com/firejune/rigc/tree/main/bench/studies/2026-09-05-density) —
406
+ the same study also measures why `contour` is the wrong generator for a turn: it
407
+ saturates at 868 vertices however fine the tolerance, its vertices all sit on the
408
+ silhouette so it samples **2 %** of the depth range, and its ear-clipped interior
409
+ holds triangles three orders of magnitude apart in area, the smallest of which
410
+ reverse under a fraction of a pixel.
411
+
412
+ **No run reproduces this:** the ladder in the paragraph below is the 2026-09-05 noise study's, taken with `bench/studies/2026-09-05-noise/tools/noiseprobe.ts`, one evidence file per experiment; no command this page states re-takes it
413
+
414
+ 🚨 **The sheet's grain is a slope too, and the ceiling reads it.** Because
415
+ `max|dz/du|` is a maximum over sampled gradients, there is no averaging anywhere
416
+ in it. On a sheet whose true ceiling is 64.77°, measured at 4,225 vertices:
417
+ **±1 level** of noise reports 61.37°, **±8 levels** reports 45.34°, and **one
418
+ stray pixel out of 160,000** reports 6.08° — and `A39` refuses at each of those
419
+ angles, so the rig is as damaged as the report says. Refining the lattice makes
420
+ every one of them worse. Two hard bounds follow, both independent of the form:
421
+ a sheet can never report above `atan(255·h / zScale)` for cell size `h`, and
422
+ below one texel per cell the answer saturates at the steepest adjacent-texel
423
+ step. ⇒ **Author the sheet so it changes by at least ~8 levels across one mesh
424
+ cell**, and spend its whole 0–255 range on the part — a map using an eighth of
425
+ the range describes the same surface and reports 5.8° less of it. Method and
426
+ ladders:
427
+ [`bench/studies/2026-09-05-noise`](https://github.com/firejune/rigc/tree/main/bench/studies/2026-09-05-noise).
428
+
429
+ ⭐ **And you do not have to guess which of the three you are looking at.** The
430
+ lines under the ceiling say it: the **1st percentile over
431
+ the ceiling** is near 1 when a band of the mesh reaches the limit together and
432
+ near 10 when one triangle does, which is a texel — 1.003 against 10.652 for the
433
+ two sheets above. The **depth step across the triangle that folds first**, in
434
+ levels, is the second: below about 3 the ceiling is quantisation, and at 1 it is
435
+ `atan(255·h / zScale)` and carries nothing about the form at all. The **same step
436
+ over the range the mesh sampled** is the third, and it is the one that separates
437
+ a steep surface from a cliff — a form's halves with every doubling of the lattice
438
+ while its angle settles, a discontinuity's does not move while its angle halves.
439
+ All three are reports and none of them moves the ceiling — ⛔ rigc will not filter
440
+ a depth map, because a smoothed measurement would describe a surface the deform
441
+ key is not built from and `A39` would go on refusing at the raw angle.
442
+ [AUTHORING §3.4](AUTHORING.md) has the reading table.
443
+
444
+ ---
445
+
446
+ ## 3. One shared shift, then residuals
447
+
448
+ ⛔ **Do not key each feature's whole `dx`.** Put a bone at the face plate's own
449
+ origin, key the part every feature shares onto that one bone, and let each
450
+ feature key only what is left:
451
+
452
+ ```
453
+ faceshift.translatex = −R·sin t ← −35.345 at 12°, R = 170
454
+
455
+ residual(x, z) = dx(x, z) − (−R·sin t)
456
+ = x·(cos t − 1) + (R − z)·sin t
457
+ \_______/
458
+ the part's depth BELOW
459
+ the skull surface
460
+ ```
461
+
462
+ ⭐ **So a feature's own track is nothing but its depth below the surface, times
463
+ `sin t`.** That is the sentence this page exists to produce.
464
+
465
+ **The features of the worked example** (**derived**, and every value matches the
466
+ shipped `motion.json` to the last digit printed there):
467
+
468
+ | Bone | `x` | `z` | `dx` | residual | `scalex` |
469
+ | --- | --- | --- | --- | --- | --- |
470
+ | `eye_l` (far) | −62 | 150 | −29.832 | **5.513** | 0.8922 |
471
+ | `eye_r` (near) | 62 | 150 | −32.542 | **2.803** | 1.0641 |
472
+ | `brow_l` | −62 | 158 | −31.495 | 3.850 | 0.8966 |
473
+ | `brow_r` | 62 | 158 | −34.205 | 1.140 | 1.0597 |
474
+ | `nose` | 0 | 192 | −39.919 | **−4.574** | 0.9781 |
475
+ | `mouth` | 0 | 166 | −34.513 | 0.832 | 0.9781 |
476
+
477
+ 🚨 **The reason to do it this way is that it makes a wrong number visible.** A
478
+ residual is **1–6 units**; a total is **30–40**. Nobody can eyeball an error in
479
+ the second, and everybody can eyeball one in the first — a residual with the
480
+ wrong sign, or one an order of magnitude off its neighbours, is obvious in a
481
+ column of six. ⇒ **The shared-shift split is an auditing decision before it is a
482
+ rigging one**, and given §9 that is the whole argument for it.
483
+
484
+ ### 3.1 ⭐ And now the spec holds this split, not its results
485
+
486
+ The pattern above is a
487
+ named construct rather than a page of advice: **one group track whose key states
488
+ the model, with a depth per member** (AUTHORING §4.5.1). The worked example's six
489
+ residuals and six scale factors are two tracks:
490
+
491
+ ```json
492
+ { "group": "features", "property": "translatex", "keys": [
493
+ { "t": 0, "v": [0], "ease": "rise" },
494
+ { "t": 0.62, "derive": { "kind": "yaw", "degrees": 12, "carried": 170,
495
+ "depth": { "eye_l": 150, "eye_r": 150, "brow_l": 158,
496
+ "brow_r": 158, "nose": 192, "mouth": 166 } },
497
+ "ease": "swell" },
498
+ { "t": 2.2, "v": [0] } ] }
499
+ ```
500
+
501
+ `carried` **is** the shared shift, stated: it is the depth whose `−R·sin t` the
502
+ `faceshift` bone already applies, so what each member keys is exactly the residual
503
+ this section derives. Drop it — write `carried: 0` or leave it out — and the same
504
+ kind emits the **full** `dx` instead, which is what the parts hanging off `head`
505
+ rather than off `faceshift` need. ⇒ **The split is the one parameter, and the seam falls where the parent chain already
506
+ put it** (AUTHORING §4.5.1 refuses a model over members under different parents,
507
+ for exactly that reason).
508
+
509
+ ⭐ **`explain` then prints the column of six, which is what the argument above
510
+ asked for.** The `MEMBER` block (AUTHORING §4.5.2) is a row per member — the
511
+ emitted value, the `x` it read off the rig, and the depth the spec stated — so the
512
+ nose diagnostic below is a line you read rather than arithmetic you redo:
513
+
514
+ **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
515
+ ```
516
+ MEMBER turn group "features".translatex t=0.620000 6 member(s) derive yaw degrees=12 carried=170 -> the displacement
517
+ eye_l 5.513083 <- -62 at depth 150
518
+ eye_r 2.803385 <- 62 at depth 150
519
+ brow_l 3.849789 <- -62 at depth 158
520
+ brow_r 1.140092 <- 62 at depth 158
521
+ nose -4.574057 <- 0 at depth 192
522
+ mouth 0.831647 <- 0 at depth 166
523
+ ```
524
+
525
+ ⭐ **The nose is the diagnostic.** It is the only **negative** residual on the
526
+ face, because it is the only feature in front of the surface: it protrudes 22, and
527
+ `22·sin 12° = 4.57` is exactly how much further left it goes than the cheek it
528
+ sits on (**derived**: −4.574). ⇒ **If the nose's residual is not negative, the
529
+ depths are wrong.** It is the cheapest check on this page and it is arithmetic,
530
+ not a render.
531
+
532
+ 📌 **`faceshift` has to be its own bone, and not the head bone.** The head bone
533
+ is the head mesh's own slot bone, and the mesh deform already carries that
534
+ plate's motion; a translate there would move the plate **twice**. Make
535
+ `faceshift` a child of `head` that carries only the features — the worked
536
+ example's chain is `headroll → head → faceshift → {eyes, brows, nose, mouth}`,
537
+ which `rigc explain` prints as a parent column (§9.2).
538
+
539
+ ⭐ **The same reasoning puts the head's pivot one link above the mesh.** A head
540
+ rotates about the top of the neck, not about the middle of its own face, so the
541
+ roll belongs on a `headroll` bone above `head`. There is a rule that enforces it
542
+ under `--profile spine-html`: `A15_IDLE_NO_MESH_BONE_KEYS` refuses an `idle` that
543
+ keys a bone driving a mesh — its own slot bone or its control bone — because that
544
+ renderer must never idle-skip a mesh. Keying the pivot one link up satisfies it,
545
+ and **the rig that satisfies the assertion is the better rig anyway.**
546
+
547
+ ⚠️ **A15 assumes the meshes are mostly static, and that assumption is the rule's
548
+ whole premise.** The `spine-html` renderer skips redrawing a mesh nothing moved, so a
549
+ face whose `idle` breathes through one pivot keeps every mesh under it cheap only if
550
+ nothing keys the meshes' own bones. The rule reads bone **names**: keying `headroll`
551
+ passes it, and `head`'s mesh still moves, because a child's world transform is
552
+ composed from its parent's — so pivoting is right here for the anatomical reason
553
+ above, not because it stops a redraw. A **painting rig** is the genre where the
554
+ premise is false by design: one illustration in layers, most of them weighted
555
+ meshes, with an `idle` whose job is to move them. Pivoting every keyed bone one link
556
+ up there satisfies the wording and not the purpose (issue #855: 42 extra bones, the
557
+ same pose, the meshes still moving). That rig **declares** instead —
558
+ `invariants.idleDrivesMeshes: { "why": … }` ([AUTHORING](AUTHORING.md) §3.7) — and A15
559
+ reports the bones, meshes and vertices the `idle` moves as a SKIP rather than a
560
+ refusal per bone.
561
+
562
+ ---
563
+
564
+ ## 4. The mesh — where the columns go is the whole decision
565
+
566
+ Two meshes and **40 vertices** carried a head turn in the worked example, which
567
+ cuts against the expectation that a face mesh needs hundreds. The reason is
568
+ structural: **a yaw moves nothing vertically**, so the rows are along for the
569
+ ride and only the column count buys anything.
570
+
571
+ ```
572
+ head 340 × 380 plate, R = 170 hair_bang 372 × 168 plate, R = 196
573
+ 5 columns × 5 rows = 25 vertices 5 columns × 3 rows = 15 vertices
574
+ 32 triangles 16 triangles
575
+ columns x = −162, −120, 0, 120, 162 columns x = −170, −120, 0, 120, 170
576
+ rows y = 180, 90, 0, −90, −180 rows y = 80, 0, −80
577
+ z = √(R² − x²) z = √(R² − x²)
578
+ ```
579
+
580
+ Each column's offset is `dx` at its own `(x, z)`, and **every row gets the same
581
+ value**, so the run the compiler writes is one row of five repeated down the grid
582
+ with a `0` for every `y`:
583
+
584
+ ```
585
+ -7.175, 0, -22.414, 0, -35.345, 0, -27.658, 0, -14.255, 0, <- ×5 rows
586
+ ```
587
+
588
+ ⭐ **The spec states the model and the compiler writes that** (§1.1): the key is
589
+ `{ "kind": "yaw", "radius": 170, "degrees": 12 }`, and the 50 numbers above are
590
+ what `explain` prints and what lands in the artifact. The offsets are still worth
591
+ reading, because the *shape* of that row is §4.1's whole argument.
592
+
593
+ ⚠️ **`R` is the radius of the cylinder a part is painted on, and it is not always
594
+ the plate's half-width.** For the head plate the two coincide (`340/2 = 170`). For
595
+ the fringe they do **not**: its plate is 372 wide (half-width 186) but its `R` is
596
+ **196**, because 196 is where the fringe *sits* — 26 in front of the skull. ⇒
597
+ Read `R` off the depth table, never off the PNG.
598
+
599
+ ### 4.1 Uneven columns, and it costs nothing
600
+
601
+ 🚨 **The columns are not evenly spaced, and that is the trick.** `−162, −120, 0,
602
+ 120, 162` puts them **dense near the silhouette and sparse in the middle**, which
603
+ is the sampling a cosine needs: the centre of the face travels `R·sin t` and the
604
+ edges barely travel at all, so all the *variation* is at the edges. Five evenly
605
+ spaced columns spend their resolution where nothing happens.
606
+
607
+ What the grid then does to the drawing is a **non-uniform horizontal
608
+ redistribution** (**derived** at 12°, widest row):
609
+
610
+ | Band, from the far edge | Rest width | At 12° | Ratio |
611
+ | --- | --- | --- | --- |
612
+ | −162 → −120 | 42.0 | 26.76 | **0.637** |
613
+ | −120 → 0 | 120.0 | 107.07 | 0.892 |
614
+ | 0 → 120 | 120.0 | 127.69 | 1.064 |
615
+ | 120 → 162 | 42.0 | 55.40 | **1.319** |
616
+
617
+ The far side compresses to 64%, the near side stretches to 132%, and the ink
618
+ inside each band compresses and stretches with it. **That gradient is what makes
619
+ it read as a turn instead of a slide.** The whole-head narrowing, by contrast, is
620
+ the cheap part: ink edge to ink edge, `309 · cos 12° = 302.2` (**derived**; the
621
+ record measured 309.0 → 302.2 on the artifact).
622
+
623
+ ⭐ **It is one decision, made once, in the setup geometry, and every deform key
624
+ after it is better for free.** There is no cost side to this trade.
625
+
626
+ ### 4.2 The silhouette is a tangent, not a mark — and refining makes it worse
627
+
628
+ This is the most misleading thing about a face plate, and the counter-intuitive
629
+ result of the whole experiment.
630
+
631
+ A column at `x` sits at depth `z = √(R² − x²)`. Project two adjacent columns and
632
+ ask when they **swap order** — when the mesh turns inside out:
633
+
634
+ ```
635
+ x₁·cos t − z₁·sin t = x₂·cos t − z₂·sin t
636
+
637
+ Δx x₁ − x₂
638
+ ⇒ tan θ_fold = ── = ───────── over ADJACENT columns
639
+ Δz z₁ − z₂
640
+ ```
641
+
642
+ and a grid folds at the **minimum** of that over its pairs. As the gap closes,
643
+ `Δx/Δz → z/|x|`, so the limit for a column is `atan(z / |x|)` — a property of the
644
+ **continuous surface**, not of the mesh.
645
+
646
+ **Re-derived from that formula** (and each agrees with a bisection search on the
647
+ shipped column table to 0.01°):
648
+
649
+ | Columns | Folds at | The pair that folds |
650
+ | --- | --- | --- |
651
+ | 5 — `±162, ±120, 0` (shipped) | **31.37°** | `−162, −120` |
652
+ | 7 — `±145` added | **24.56°** | `−162, −145` |
653
+ | 7 — `±155` added | **20.95°** | `−162, −155` |
654
+ | 9 — `±155, ±145` added | **20.95°** | `−162, −155` |
655
+ | 13 — uniform, every 27 units | 27.54° | `−162, −135` |
656
+ | continuous limit at `x = −162` | **17.65°** | — |
657
+
658
+ 🚨 **A denser face mesh is not a safer face mesh.** Refining near the silhouette
659
+ drives `Δx/Δz` toward the tangent limit *from above*, so every column you add out
660
+ there **lowers** the angle at which the mesh inverts. A coarse grid survives past
661
+ 17.65° only because it does not *sample* there — it crushes instead of folding.
662
+
663
+ ⚠️ **And the fold angle is set by the outermost gap, not by the column count.**
664
+ The 5-, 7- and 9-column rows above make that concrete: the 9-column grid folds at
665
+ **exactly** the same 20.95° as the 7-column one, because both contain the pair
666
+ `(−162, −155)` and the `±145` column changes nothing. ⇒ Counting vertices tells
667
+ you nothing about this failure; **look at the outermost two columns.**
668
+
669
+ ⭐ **The rule, and it is an identity rather than a rule of thumb.** Put the
670
+ outermost column at
671
+
672
+ ```
673
+ |x|outer = R · cos θmax ⇒ its tangent limit is EXACTLY θmax
674
+ ```
675
+
676
+ because `z = R·sin θ` there and `atan(z/|x|) = θ`. So you do not tune this: you
677
+ **pick the ceiling first and the column position falls out.** For `R = 170`
678
+ (**derived**):
679
+
680
+ | θmax you want | Outermost column at |
681
+ | --- | --- |
682
+ | 12° | 166.3 |
683
+ | 16° | 163.4 |
684
+ | 20° | 159.7 |
685
+ | 26° | 152.8 |
686
+
687
+ The shipped grid's 162 gives 17.65°, comfortably above the 12° it ships and just
688
+ above the 16° §8 calls the instrument's ceiling. ⇒ **Then let the last band be a
689
+ single wide one** — that is the direction that buys safety, and it is the
690
+ opposite of refining.
691
+
692
+ 🔁 **And this is the section read from the other end: the refusal you get for
693
+ picking the ceiling wrong sends you back here by name.** A build whose key
694
+ evaluates past the fold angle above is refused by
695
+ `A39_DEFORM_KEEPS_TRIANGLE_WINDING` — AUTHORING §5–§6 is the failure map and its
696
+ `A39` row reads this message field by field — and the message carries the
697
+ triangles, the signed areas, the fix and this section:
698
+
699
+ **No run reproduces this:** abridged and re-wrapped — the three further triangles it names and its `and 4 more` are cut at the ellipsis, and the run prints the whole refusal on one line; §9.2's build (b) is what prints it
700
+
701
+ ```
702
+ FAIL A39_DEFORM_KEEPS_TRIANGLE_WINDING: animation "turn" deform head/head key 1
703
+ (t=0.6200000047683716s): 8 of 32 triangle(s) reverse winding — triangle 0
704
+ [0,15,16] 1890.000 -> -544.548px²; … The mesh has turned inside out there
705
+ and draws its texture backwards. Fix the key's offsets in the motion
706
+ spec's deform timeline (a projection past its fold angle is the usual
707
+ cause — docs/FACE.md §4.2 has the closed form), or, if this slot folds on
708
+ purpose, declare it in the rig spec as invariants.deformMayFold:
709
+ [{ "slot": "head", "why": … }]
710
+ ```
711
+
712
+ ⇒ The table above is what *"a projection past its fold angle"* means, and the
713
+ outermost-column identity is how you stop meeting the message at all.
714
+
715
+ 🎭 **The fourth way out, and the one a Live2D-style face actually takes: stop
716
+ drawing the part.** The ceiling only binds while the part is on screen, so a turn
717
+ that has to go past it fades the far cheek or ear out — `rgba` to alpha 0 — or
718
+ swaps the attachment away, and another part takes over. `A39` measures that: a
719
+ deform key whose slot draws **no pixels at that key's own time** is passed over by
720
+ name rather than refused, with the reason on the stats line and beside the key's
721
+ own figures in the `DEFORM` block. Two things it is not — the
722
+ bar is alpha **exactly 0**, so a part faded halfway is still refused with the
723
+ alpha in the message; and it is per key and per time, so the same slot folding at
724
+ full alpha anywhere else is refused. ⛔ It is also not
725
+ `invariants.deformMayFold`: that field turns the check off for the slot at every
726
+ angle, including the ones where the part is fully visible.
727
+
728
+ 🚨 **Fade out *up to* the angle you cannot take, never *at* it — and the gate
729
+ keeps that rule rather than asking you to.** The runtime interpolates between
730
+ keys, so an alpha-0 key landing exactly on the folding key leaves the frames just
731
+ before it drawn and nearly folded (§9.2 measures it: 8 reversed triangles at
732
+ alpha 0.20). `A39` scans the spans between consecutive keys as well and refuses
733
+ one by name, at a time solved for in closed form and then posed and measured
734
+ (AUTHORING §4.11.3). ⇒ The
735
+ practical shape is checkable: **key the fade to 0 at or before
736
+ the last angle that gates green, and let the fold happen after it.**
737
+
738
+ 📘 **The identity above, used forwards on a `pitch`.**
739
+ [`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod)
740
+ (repository material) picks its ceiling first and solves the *rows* out of it —
741
+ `|y|outer = R·cos θmax` with θmax chosen at 21° giving 140.037 — and then
742
+ brackets the fold this section predicts against `A39` itself: **33° gates green
743
+ and 34° does not**, with the refusal naming the row pair the closed form names.
744
+ That is this table checked from the other end, on the other axis.
745
+
746
+ ### 4.3 The perimeter comes first, and `hull` is read off the triangles
747
+
748
+ ⚠️ **A grid's perimeter is 16 of its 25 vertices, and in row-major order they are
749
+ interleaved with the interior.** Spine's `hull` is the first `hull` vertices of
750
+ the list, in order — the editor draws the outline by joining them in sequence — so
751
+ a row-major grid cannot declare one. And an undeclared hull is not neutral: the
752
+ editor's import repairs a `hull: 0` by making
753
+ **every** vertex a hull vertex in list order, which is a self-intersecting outline
754
+ on the mesh the person refining the draft sees. So the list is written the way the editor writes one: **the perimeter
755
+ first, walked around — top row, right column, bottom row, left column — then the
756
+ interior, row-major.** rigc derives `hull` from the triangles (AUTHORING §3.4) and
757
+ refuses any other order with the walk to renumber along, so getting this wrong
758
+ costs one loop rather than a silent file.
759
+
760
+ **What it costs the reader.** `explain` prints one offset per vertex in list
761
+ order, so the run does not read as one row of five values repeated down the
762
+ grid: entries 0–15 walk the perimeter and 16–24 are the interior. The column
763
+ table is still what to check it against — a vertex's offset depends on its column
764
+ alone — and the right column's five entries (4–8) carrying one value is the
765
+ quickest check that the walk and the table agree. `gallery/portrait`'s README
766
+ shows the listing beside the order.
767
+
768
+ ⚠️ **A large `MESH` overshoot on a face is not a defect.** A rectangular grid
769
+ over an oval face has transparent corners, and the line says so:
770
+
771
+ ```
772
+ MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
773
+ ```
774
+
775
+ **`covers 100.00%` is what matters** — nothing of the drawing is outside the
776
+ triangles. The 95.90px is the corner. A mesh that hugged the silhouette instead
777
+ would have to be a `contour`, which cannot be a grid and has **no interior
778
+ vertices to redistribute** — so it cannot carry a turn at all.
779
+
780
+ ---
781
+
782
+ ## 5. What foreshortens, and what does not
783
+
784
+ A feature is a rigid drawing sitting on a curved surface, so it also has to
785
+ **narrow** as its patch of surface turns away. That is a bone `scalex`:
786
+
787
+ ```
788
+ scaleX = cos(α − t) / cos α where α = atan2(x, z)
789
+ ```
790
+
791
+ The far eye narrows to 89%, the near eye widens to 106% (**derived**: 0.8922 and
792
+ 1.0641). Parts on the axis get `cos t = 0.9781`.
793
+
794
+ 📘 **How much this is worth depends on the axis, and there is a worked case for
795
+ the other one.** The same closed form on the `pitch` of
796
+ [`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod)
797
+ (repository material) spans **0.863 … 1.176** across its five features, against
798
+ the **0.892 … 1.064** above — a wider span at the *identical* angle, because
799
+ `α = atan2(coordinate, depth)` grows with the coordinate and a face is taller
800
+ than it is deep. ⇒ **The foreshortening buys more on a nod than on a turn**, and
801
+ that is arithmetic rather than a judgement about the art.
802
+
803
+ 📌 **The on-axis pair needs no `groups` entry.** `scalex` is the
804
+ foreshortening projection of the same `derive` kind §3.1 uses (AUTHORING §4.5.1),
805
+ so all six features are one track and the on-axis pair's shared value **falls out
806
+ of the arithmetic** — `α = atan2(0, z)` is 0, so `cos(α − t)/cos α` is `cos t`.
807
+ A coincidence between two members is not something an author has to notice and
808
+ spend a group on.
809
+
810
+ ### 🚨 The iris does not foreshorten, and this is the finding
811
+
812
+ `iris` and `spark` are children of `eye`, so they inherit the socket's `scalex` —
813
+ and **a circular iris under `scalex 0.89` is an ellipse.** That reads as *a
814
+ drawing squashed sideways*, not as a head turned, and it is the first thing to
815
+ break as the angle grows.
816
+
817
+ It is also wrong on the physics. If the character keeps looking at the camera
818
+ through the turn, her eyeball counter-rotates by the same angle the head yawed,
819
+ so the iris stays square-on and stays **circular**. ⇒ **The socket foreshortens;
820
+ the pupil does not.**
821
+
822
+ The fix is one reciprocal per side, on two groups (**derived**):
823
+
824
+ ```
825
+ look_l: [iris_l, spark_l] scalex = 1 / 0.8922 = 1.1208
826
+ look_r: [iris_r, spark_r] scalex = 1 / 1.0641 = 0.9398
827
+ ```
828
+
829
+ ⭐ **These two stay a shared value on a group, and they are the case where the
830
+ per-member construct is the wrong tool** — stated here because "there is a
831
+ model" is exactly the reasoning that would spoil them. A counter-scale belongs to
832
+ the **socket**, not to the part: `spark_l` sits at local `x = −11` and takes the
833
+ same `1.1208` as `iris_l` at `0`, because what is being cancelled is the socket's
834
+ foreshortening and not the highlight's own. ⇒ **The value is precisely NOT a
835
+ function of the member's position**, so a `groups` entry with one number is the
836
+ true statement and a `derive` over `iris_l`/`spark_l` would be a fiction that
837
+ happened to use the `derive` field.
838
+
839
+ ⭐ **Their positions still ride the socket, and that is the part that makes it
840
+ correct rather than a hack.** A bone's scale moves its children's local
841
+ translation, so the highlight at local `(−11, +11)` under the far socket's 0.8922
842
+ lands at `−9.81` — it slides 1.19 units inward, toward the surface it reflects
843
+ off (**derived**). Only the *shape* is held.
844
+
845
+ **No run reproduces this:** a person's judgement over seven renders, kept in the `gallery/portrait` record (`FINDINGS.md`, *The measured sweep*); no instrument in this repository grades a turn, so nothing re-takes it and nothing can
846
+
847
+ 📊 **Measured effect on the cliff, by the worked example's own sweep — a
848
+ looked-at judgement over seven renders, not a computed figure:** without the
849
+ counter-scale the turn stops reading at about **18°**; with it, about **26°**.
850
+ Eight degrees of usable range for two tracks, and the record reports nothing else
851
+ in the experiment came close to that ratio.
852
+
853
+ ⚠️ **A `clipping` attachment would lift the iris's travel ceiling and you cannot
854
+ have one.** Spine has them (AUTHORING §3.4) but `A11_NO_CLIPPING_ATTACHMENTS`
855
+ refuses one under `--profile spine-html`, so a rig carrying one builds on only one
856
+ of the two profiles. The ceiling is then geometric: in the worked example the
857
+ iris's ink ring has radius 28.5 against a socket opening 36 half-wide, so
858
+ `36 − 28.5 = 7.5` units is as far as it can travel before it crosses its own lash.
859
+ ⇒ **Compute that ceiling from the art before keying a gaze**; the first draft of
860
+ the worked example keyed 9 and had to come back to 7.
861
+
862
+ ---
863
+
864
+ ## 6. A blink is a continuous channel, not a swap
865
+
866
+ **Two ways to blink, and both are right somewhere:**
867
+
868
+ | | An `attachment` swap | A translating lid plate |
869
+ | --- | --- | --- |
870
+ | what it is | two drawings, `eyes` and `eyes_shut`, stepped | one plate, one `translatey`, 65 units down its own bone |
871
+ | costs | 1 extra PNG | 2 PNGs, 2 bones, 2 slots |
872
+ | where it is right | a mascot, a stylised blink, anything whose shut eye is a **different drawing** rather than a covered one | a portrait |
873
+
874
+ ⭐ **Choose the swap for a mascot and the channel for a face**, and the reasons
875
+ are all timing:
876
+
877
+ - **A swap has no shape.** A real blink is fast shut and slow open. The worked
878
+ example is `0.07 s` down and `0.16 s` open — a **1 : 2.3** asymmetry, easing
879
+ into the close and out of the open. A stepped timeline has one frame of
880
+ transition and no curve to put an asymmetry in.
881
+ - **A swap has no partial.** A half-blink, a sleepy lid, a lid that rides the
882
+ gaze — every one of them is a *fraction of the same channel*, and none of them
883
+ is a third drawing.
884
+ - **A swap has to dodge the frame grid.** A stepped key exactly on a sample time
885
+ can be missed by a player accumulating `1/fps`, which is why
886
+ [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash)'s
887
+ blink key sits at `0.399999` (AUTHORING §4.5). **A continuous channel does not
888
+ care where the samples land.**
889
+
890
+ ⚠️ **One constraint on the lid's own drawing, and it is an art decision the rig
891
+ cannot express.**
892
+
893
+ **Fade its top edge out, and size the fade in pixels.** A flat plate of skin
894
+ translating down a forehead has a visible edge; fading its top 30 pixels
895
+ removes it. ⛔ **Do not write that fade proportionally.** In the worked example
896
+ it was first authored as `offset 0.34` in `objectBoundingBox` units — 34% of
897
+ *whatever height the plate happened to be* — and when the plate grew from 88 to
898
+ 112 to cover the eye, the faded band grew with it and stopped covering the top
899
+ of the shut eye. In `userSpaceOnUse` with `y2="30"` the height becomes a free
900
+ variable and the coverage becomes a stated one: `106 − 30 = 76` opaque pixels
901
+ above the lash, against a 70-unit eye.
902
+
903
+ 📌 **Blink both lids on one `groups` track.** An L/R offset of one frame was
904
+ tried in the worked example and rejected: at 25 fps it does not read as a soft
905
+ blink, it reads as a **wink**. ⇒ MOTION §3.7's offset table is about a chain
906
+ hanging off a driver, and two lids are not that — they are one event.
907
+
908
+ ---
909
+
910
+ ## 7. Channel allocation, before the first key
911
+
912
+ 🚨 **Whoever composes with this face will layer — an idle that keeps running
913
+ under a triggered gaze, under a turn — and two animations keying the same bone
914
+ property are BLENDED, not summed.** The rig cannot decide when those play, and it
915
+ is the only thing that can decide whether they collide when they do. So the
916
+ animations have to divide the rig up front. The
917
+ worked example's table, which is the shape of thing to write before authoring
918
+ anything:
919
+
920
+ | Channel | `idle` | `gaze` | `turn` |
921
+ | --- | --- | --- | --- |
922
+ | `torso` scale, `chest` translatey | ✔ | | |
923
+ | `lids` translatey | ✔ | | |
924
+ | `brows` translatey | ✔ | ✔ | |
925
+ | `irises` / `sparks` translate | | ✔ | |
926
+ | `head` + `hair_bang` mesh deform | | | ✔ |
927
+ | `faceshift`, feature translatex / scalex | | | ✔ |
928
+ | `hairmass` translatex | | | ✔ |
929
+ | `lock_l` / `lock_r` / `ahoge` | rotate | rotate | translatex |
930
+ | `headroll` rotate | ✔ | ✔ | ✔ |
931
+ | `neck` | rotate | | translatex |
932
+
933
+ Three collisions survive there: `headroll` rotate in all three, `brows`
934
+ translatey in two, and the locks' rotate in two. ⚠️ **On plain Spine the fixes
935
+ are ordinary — `MixBlend.add` on the layered track, or splitting a bone into a
936
+ stack (`headroll_idle` under `headroll_layer`) — but both are runtime or rig
937
+ decisions the motion spec cannot express, so nothing warns an author that two of
938
+ their animations will fight.**
939
+
940
+ 🚨 **A `slider` is a third way for two animations to meet on one property, and it
941
+ is an overwrite rather than a blend — so allocate it in this table too.** An
942
+ animation a slider applies (AUTHORING §3.5.2) is never on a track: the constraint
943
+ applies it every frame, at whatever time its own bone currently points at. At
944
+ `mix: 1` with `additive` left at its default that apply **writes the property
945
+ outright**, which erases both any earlier slider on the same property and the
946
+ **playing** animation on the bones its animation keys — including at its own
947
+ neutral, where it looks switched off. ⇒ Every face axis that shares a target
948
+ declares `"additive": true`, and unlike the two collisions above this one **is**
949
+ gated: `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` names the bone, the property,
950
+ every slider keying it in array order and which one wins today. ⛔ The one case
951
+ `additive` cannot rescue is a **slot colour, an attachment swap, a draw order or
952
+ a sequence**: those timelines ignore the flag entirely, so a fade — §8's way of
953
+ taking a part off the screen before its own ceiling — belongs *inside* the single
954
+ animation one slider applies, never in a second slider beside it.
955
+
956
+ ⛔ **And the cost is real, so name it rather than discovering it by shipping.** In
957
+ the worked example `idle` keys **nothing** on the iris, on purpose, even though a
958
+ completely still eye reads as a mannequin. The iris is `gaze`'s channel; an
959
+ `idle` drift plus a `gaze` on a second track would be two animations holding two
960
+ opinions about where she is looking. **That is a loss, it was chosen, and it is
961
+ written down.**
962
+
963
+ ⇒ **The gaze itself is then pure MOTION §3.7** — a chain of offsets, and the only
964
+ face-specific part is which bone leads. The worked example's ordering
965
+ (**measured** off its own key times):
966
+
967
+ | Track | Extreme at | What it is |
968
+ | --- | --- | --- |
969
+ | `irises` translate | **0.22 s** | the eyes lead. `(7, −3)` |
970
+ | `sparks` translate | 0.22 s | `(2.8, −1.2)` — **40% of the iris distance**, arriving at the *same time*. A specular highlight is fixed to the light, not to the eyeball, so it lags in **distance** and not in time |
971
+ | `brows` translatey | 0.34 s | +1.8, which turns a flick of the eyes into interest |
972
+ | `headroll` translatex + rotate | **0.40 s** | the head follows **+12% of the duration after the eyes**. A rigid 3.4-unit slide and a −1.2° roll |
973
+ | `lock_l` / `lock_r` / `ahoge` rotate | 0.72 / 0.76 / 0.82 s | **+21% / +24% / +28% after the head**, each with one overshoot crossing of opposite sign |
974
+
975
+ 🩹 **The head's follow there is a rigid slide plus a roll, not a small yaw.** A
976
+ head following a gaze really does yaw a few degrees, and that is one `transform`
977
+ key with a smaller `degrees` (§1.1). ⇒ What there is to weigh is §7's
978
+ table — a yaw on the `gaze` channel is a second animation keying the head mesh's
979
+ deform, which `turn` already owns, and that collision is the thing the format
980
+ cannot express. Same trade in the turn's anticipation: MOTION §3.6 asks
981
+ for a counter-move, and the worked example's is a **−0.9° counter-roll on the
982
+ neck bone rather than a counter-yaw**, which would be one more `transform` key.
983
+
984
+ ⭐ **A roll channel is worth having for a second reason: it is where a turn's arc
985
+ comes from.** A yaw shift is a straight horizontal line and `translatex` draws
986
+ exactly that (MOTION §3.5). A 1.6° roll on a bone at the top of the neck bends
987
+ every feature's path into an arc, because a rotation carries its descendants on a
988
+ circle for free.
989
+
990
+ ---
991
+
992
+ ## 8. The three cliffs, and picking a construction from the turn you need
993
+
994
+ Three separate failures at three different angles. ⇒ **Read this before the art
995
+ is drawn**, because two of the three are answered by *parts*, and parts are the
996
+ expensive thing to change.
997
+
998
+ **The sweep** — the worked example's `turn` re-derived at seven angles, built,
999
+ rendered and looked at. The geometry columns are **derived**; the last column is
1000
+ the record's looked-at judgement:
1001
+
1002
+ **No run reproduces this:** the geometry columns are §1's line evaluated by hand and the last column is a person's, from the sweep in the `gallery/portrait` record; each of the seven builds is `"degrees": <n>` and a rebuild, but no command this page states takes them
1003
+
1004
+ | yaw | centre shift | far band 42 → | ratio | 9-unit ink → | reads as a turn? |
1005
+ | --- | --- | --- | --- | --- | --- |
1006
+ | 8° | 23.7 | 32.0 | 0.762 | 6.9 | yes, gently |
1007
+ | **12° (shipped)** | **35.3** | **26.8** | **0.637** | **5.7** | **yes** |
1008
+ | 16° | 46.9 | 21.4 | 0.509 | 4.6 | yes |
1009
+ | 20° | 58.1 | 15.9 | 0.379 | 3.4 | marginal |
1010
+ | 24° | 69.1 | 10.4 | 0.247 | 2.2 | no — the eyes have stopped being eyes |
1011
+ | 28° | 79.8 | 4.7 | 0.113 | 1.0 | no — the far outline is gone |
1012
+ | 32° | 90.1 | −0.9 | −0.021 | — | **the mesh has folded** |
1013
+
1014
+ **Failure 1 — the iris goes elliptical, at ~18°, and it is fixable.** §5. Two
1015
+ reciprocal `scalex` tracks buy 8° of range. Do this one always; it is the best
1016
+ ratio in the experiment.
1017
+
1018
+ **Failure 2 — a `scalex` cannot rotate an almond, at ~26°, and it is not
1019
+ fixable.** The eye *socket* goes next. A real eye at 26° does not narrow
1020
+ uniformly: its far corner disappears behind the nose bridge, its lash line
1021
+ rotates, its lid wraps. `scalex` does exactly one of those things, so the far eye
1022
+ becomes *a thin version of a front-facing eye* and the near eye's lash stretches
1023
+ into a wide flat slab (**derived** socket scales, from §1's line at the sweep's
1024
+ own angles and re-derivable from it alone: 0.892/1.064 at 12°, 0.745/1.082
1025
+ at 24°, 0.629/1.067 at 32° — note the near side barely moves past 20°, which is
1026
+ why the stretch stops looking like foreshortening). ⇒ **Past roughly 26° the eyes
1027
+ need their own deform meshes** — socket, lash and lid as a 3–4 column grid each —
1028
+ and that is where the vertex count stops being 40. The fringe tips (a rigid plate
1029
+ that should be splaying) and the neck go in the same band for the same reason.
1030
+
1031
+ **Failure 3 — the mesh folds, and refining it makes this worse.** §4.2, and it is
1032
+ the one to design around rather than discover.
1033
+
1034
+ 🩹 **The neck is the honest fudge, and label yours the same way.** A neck twists:
1035
+ its top follows the head almost entirely and its base hardly at all. A single
1036
+ rigid plate can only take an average — the worked example takes **28%** of the
1037
+ head's shift (`−10` against `−35.345`), which is **the one number in its turn that
1038
+ is not derived**, chosen as the value at which the chin stopped hanging off the
1039
+ throat at 12°. A turn that had to read at 20° would need the neck to be its own
1040
+ mesh with its own column table.
1041
+
1042
+ ### The verdict on angle
1043
+
1044
+ ⭐ **A 5-column grid with bone-scaled features is a 0–16° instrument, comfortable
1045
+ at 12°.** For a standing portrait that is enough: an idle turn, a glance away, a
1046
+ lean into frame. **A 30–45° three-quarter turn is a different rig** — per-eye
1047
+ meshes, a meshed neck, probably a second art layer for the far cheek — and it is
1048
+ not a format problem, it is a parts-and-labour problem.
1049
+
1050
+ 📊 **What one held yaw actually costs**, from the shipped `motion.json`
1051
+ (**derived** by counting it):
1052
+
1053
+ | Animation | Duration | Tracks | Track keys | Deform entries | Deform keys | Hand-written deform floats |
1054
+ | --- | --- | --- | --- | --- | --- | --- |
1055
+ | `idle` | 3.2 s | 9 | 37 | 0 | 0 | 0 |
1056
+ | `gaze` | 1.5 s | 8 | 32 | 0 | 0 | 0 |
1057
+ | **`turn`** | 2.2 s | **8** | **33** | 2 | 8 | **0** |
1058
+
1059
+ ⇒ **`idle` and `gaze` cost what an ordinary MOTION.md shot costs, and the turn
1060
+ has no transcription in it.** The deform is four `transform` keys (§1.1) and the
1061
+ compiler writes the run: **five distinct values** per head key (one per column,
1062
+ repeated down five rows), with **25 of its 50 slots structurally `0`** because a
1063
+ yaw has no vertical component. The **tracks** column is 8 because the features are
1064
+ two group tracks whose keys state a model (§3, AUTHORING §4.5.1) and the four hair
1065
+ parts are one.
1066
+
1067
+ 🚨 **And here is the number that matters more than either count: what the
1068
+ hand-written figures ARE.** The turn states **34 depths** — of which **11 are
1069
+ distinct**, the rest being the same table repeated on the second held key and on
1070
+ the `scalex` track. ⇒ The point is **auditability**: a residual of `1.14` is a
1071
+ number a reader can only take on trust, and `brow_r at depth 158` is a claim they
1072
+ can argue with. §3 is why that is the whole point, and §2 is why the depths had
1073
+ nowhere else to live.
1074
+
1075
+ ⚠️ **A plain `groups` entry still buys almost nothing on a face, and that is
1076
+ structural rather than an oversight.** Keying several bones **identically** is
1077
+ right for a wheel pair, and **every part needing a different number is what
1078
+ parallax means.** The on-axis pair's shared `cos t` falls out of the same closed
1079
+ form as everybody else's value, so it needs no group, while `look_l`/`look_r` stay
1080
+ one — because those two really are one shared number (§5).
1081
+
1082
+ ### The turn as a value rather than a time
1083
+
1084
+ Everything above prices a turn as an animation somebody plays. The same geometry
1085
+ also runs on an **axis**: a `slider` constraint reads a driving bone, maps that
1086
+ bone's rotation to a time inside the turn animation, and applies the animation
1087
+ there (AUTHORING §3.5.2). Nothing in §1–§5 changes — the keys are still §1's line
1088
+ evaluated at each angle — but what selects among them is a **value** rather than a
1089
+ playhead, so the face follows a number somebody else is holding: a pointer, a gaze
1090
+ target, a game state.
1091
+
1092
+ ⭐ **The object offers a dial and does not decide when it turns.** That is the
1093
+ whole of the claim and it is deliberately not a larger one: what the rig
1094
+ guarantees is the axis — its range, its arithmetic, and that every angle on it is
1095
+ sound — and what moves the dial belongs to whoever is using the face. The
1096
+ paragraphs below are the part that is ours.
1097
+
1098
+ #### The range stops being a choice and becomes a measurement
1099
+
1100
+ This is the half an author has no other way to get right. §4.2's fold angle is not
1101
+ a rule of thumb once the angle is a dial position: it is the **top of the dial**,
1102
+ because past it a triangle turns inside out and `A39` refuses the build by name.
1103
+ `build` prints that angle for every depth mesh it compiles (AUTHORING §3.4), so
1104
+ the whole mapping falls out of one reading:
1105
+
1106
+ ```
1107
+ ceiling the largest turn this depth mesh admits — printed by `build`, not guessed
1108
+ range the largest whole degree strictly INSIDE the ceiling
1109
+ from -range max +range local true (AUTHORING §3.5.2's circle)
1110
+ scale seconds per degree — the one number here you choose
1111
+ duration 2 x range x scale
1112
+ time to + (degrees - from) x scale the slider's own mapping
1113
+ ```
1114
+
1115
+ 📐 **Worked, on [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look)**,
1116
+ whose face is a 21 × 9 `grid` over one depth sheet:
1117
+
1118
+ ```bash
1119
+ bun cli.ts build --rig gallery/look/rig.json \
1120
+ --motion gallery/look/motion.json \
1121
+ --out gallery/look/build
1122
+ ```
1123
+
1124
+ ```
1125
+ MESH head grid 189 vertices / 320 triangles (budget 320) bones=[head] attachments=[head]
1126
+ depth "face_depth.png" bf156ea0cfc970a3 near=white zScale=194 z=[0, 194]
1127
+ 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
1128
+ turn ceiling yaw +19.32° / -19.32° pitch +22.92° / -26.94°
1129
+ 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
1130
+ 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
1131
+ ```
1132
+
1133
+ ⇒ the ceiling is **±19.32°**, so the range is **19** — `floor(19.32)`, and
1134
+ *strictly inside* is the whole of the rule. At the **0.05 s per degree** that rig
1135
+ chooses, `turn` runs `2 × 19 × 0.05` = **1.9 s** and the map is
1136
+ `time = 0 + (degrees + 19) × 0.05`, which is the constraint as its rig spec
1137
+ declares it: `"from": -19, "to": 0, "scale": 0.05, "max": 19, "local": true,
1138
+ "additive": true`. ⭐ **The only two numbers there that anybody chose are `scale`
1139
+ and `to`** — and `to: 0` says nothing more than *the bottom of the range is the
1140
+ animation's first frame*. `from`, `max` and the duration are all the ceiling.
1141
+
1142
+ 🔸 **And `scale` is chosen for the endpoint.** rigc emits every number as its
1143
+ float32's shortest name, so a `scale` the float cannot hold moves the top of the
1144
+ dial: `1/60` ships as `0.016666668`, and a 60° turn then applies at 1.00000008 s
1145
+ rather than 1 s — the last frame under `loop: false`, the *first* under
1146
+ `loop: true` (AUTHORING §3.5.2). `0.05` is its own float's name, so the file states
1147
+ exactly `0.05`, which is the only reason the example can put its endpoint exactly
1148
+ on the duration. Pick a `scale` that is not, and land the endpoint inside the
1149
+ duration instead.
1150
+
1151
+ ⚠️ **The ceiling is per mesh, and the face's is not the smallest one on the
1152
+ face.** The same run prints one for every depth mesh, and in this example each
1153
+ sidelock reads `yaw +17.04° / -45.80°`: it folds at **17.04°** on one side, which
1154
+ is *inside* the ±19° the face itself admits, and not until 45.80° on the other.
1155
+ The asymmetry is the sheet's, not a coincidence — each of those sheets is
1156
+ steepest at the edge where the strand curves away, and a yaw folds a pair of
1157
+ vertices only in the direction their depth is rising.
1158
+
1159
+ ⇒ **A part whose ceiling is lower than the range has to be gone before the turn
1160
+ reaches it.** That is AUTHORING §3.4's third way to live with a ceiling: fade the
1161
+ slot to alpha 0 **inside the animation the slider applies**, landing the alpha-0
1162
+ key *before* the key that folds rather than on it. §7's paragraph on sliders is
1163
+ why that fade cannot be a second slider.
1164
+
1165
+ ⭐ **A lookup table wants linear keys, and that is not a style note.** The slider
1166
+ makes the pose a function of the dial, so an easing curve between two keys makes
1167
+ it a **non-linear** function of a number the consumer may be holding perfectly
1168
+ still — the face would drift and settle while the value sits where it was put.
1169
+ Anticipation and follow-through fail for the same reason, not a weaker one
1170
+ (MOTION §3.6, §3.7): both are functions of time, and there is no time on this
1171
+ axis. [MOTION §0.1](MOTION.md) is that split written out, and it is also where the
1172
+ shaping *does* belong — in whatever animation moves the dial.
1173
+
1174
+ ⚠️ **Two axes on one face need `"additive": true` on both.** A `pitch` dial
1175
+ beside the `yaw` is the ordinary case and it is the one the format's default
1176
+ breaks the moment the two share a target — in the worked example both `turn` and
1177
+ `tilt` key `headroll`. §7's paragraph on sliders is the mechanism and
1178
+ `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` is the refusal.
1179
+
1180
+ ✅ **And the space between the two dials is measured rather than inferred.**
1181
+ Two additive sliders posed
1182
+ at a grid of *both* values — and over a rotation and a translation, so the claim
1183
+ is not one property's — are the closed-form sum at every cell of it, to float64:
1184
+ each slider maps its own reading to a time by §3.5.2's rule, its animation is read
1185
+ there, and the two contributions add. ⭐ That is a property of the **interior**
1186
+ and not of the corners, which is the whole reason it needed a grid: with an `ease` on one of the two
1187
+ animations, every corner of the space still reads as correct while most of the
1188
+ inside has moved. ⇒ **a
1189
+ lookup table wants linear keys for a second reason** — not only that the face
1190
+ would drift while the value sat still, but that a curve is invisible to any check
1191
+ that reads an axis at its ends.
1192
+
1193
+ ⚠️ **And `mix` below 1 is not what it looks like when the later slider is not
1194
+ additive.** An additive slider scales its whole contribution by its own `mix`, so
1195
+ two of them at any pair of mixes are still the sum — which is why `A40` skipping
1196
+ below full authority is right: what happens there is a weighting, not the erasure
1197
+ it refuses. A **non-additive** slider at `mix` α applies
1198
+ `current + (value + setup − current) × α`, a lerp *from the pose it found*, so the
1199
+ earlier slider is not erased — it is attenuated by `1 − α`. A pitch dial turned
1200
+ halfway down takes that share of the yaw with it, on every frame, with the gate
1201
+ green. ⇒ write `"additive": true` on **every** slider that shares a target and
1202
+ not only on the later one, which is what the paragraph above already asks for and
1203
+ this is the second reason for.
1204
+
1205
+ 🚨 **A second dial on a slot colour, an attachment swap or a draw order is not a
1206
+ second dial at all.** Those timelines ignore `additive`, so the flag is not the
1207
+ repair and writing it on both changes nothing: the slider **later in the
1208
+ `constraints` array** owns that property outright and the earlier one contributes
1209
+ nothing, at every position of its dial. An **ik constraint's mix** behaves the
1210
+ same way. What does compose is the rest of what a face keys — a bone transform, a
1211
+ mesh deform, a transform constraint's mix, a physics `wind`, a path constraint's
1212
+ `mix`, and another slider's own `mix` or `time` — and those are the same sum as
1213
+ the two axes above, over each target's own setup value. ⇒ if a blink fades a slot
1214
+ and the yaw dial also fades it, one of the two has to stop: key that property
1215
+ from **one** slider, or move both edits into the animation a single slider
1216
+ applies. `A40` refuses the rest by name, and AUTHORING §3.5.2 is the mechanism.
1217
+
1218
+ ✅ **And which of the two a timeline is, the gate poses rather than asks.**
1219
+ `A40` applies the shared timeline twice with `add` set and reads whether the second
1220
+ application accumulated, rather than reading `Timeline.additive`, the runtime's own
1221
+ declaration, which two classes state falsely about themselves — a path
1222
+ constraint's `mix` and a slider's `time`, both in the composing list above. Two
1223
+ dials whose animations both fire **events** are not refused either: a slider fires
1224
+ no event at all — it applies its animation with `firedEvents` null.
1225
+
1226
+ ⭐ **Three dials are the same sum as two — as long as every one of them is
1227
+ additive.** The case worth knowing is a non-additive dial in the *middle* of
1228
+ three, because it is neither of the two failures you would expect: it erases every
1229
+ dial **before** it and is then added to by every dial **after** it, so the face is
1230
+ neither the sum nor the last dial alone, and no reading of a two-dial rig has that
1231
+ shape.
1232
+
1233
+ ⚠️ **`"loop": true` puts the animation's FIRST frame at the top of the dial.** A
1234
+ looping slider wraps its time as a positive modulo rather than holding the last
1235
+ frame, so the axis is a sawtooth: the two ends of the range are the same pose and
1236
+ every position past the top repeats the range from its bottom. That is right for a
1237
+ parameter that genuinely cycles — a wheel, a breath — and wrong for a yaw, where
1238
+ the range's top has to *stay* at the extreme of the turn. Leave `loop` off for a
1239
+ face axis; the default is the one you want.
1240
+
1241
+ 🔸 **And a `local: false` dial never reads back the number you set.** The world
1242
+ reader goes through the bone's matrix, where the reference runtime's float32 π
1243
+ leaves a fraction of a degree behind, and it has a period: a bone one whole turn
1244
+ from its position reads *identically*, so the dial cannot tell the two apart. The
1245
+ composition is unchanged — two world dials add exactly as two local ones do — but
1246
+ `local: true` is what makes the number on the dial the number the rig reads, which
1247
+ is the same repair §3.5.2's circle already asks for. Every one of the six
1248
+ `property` readings composes by that one arithmetic under `local: true`, and so
1249
+ does every one of them read through the world — the composition is the same sum
1250
+ on both sides of the flag, and what the flag changes is what the dial can say.
1251
+
1252
+ 🚨 **A `local: false` scale axis folds at zero: the negative half is the positive
1253
+ half again.** A world scale reading is a square root, so a dial at −2 and a dial
1254
+ at +2 are not two positions — they read the same, select the same frame and pose
1255
+ the same face. Nothing at compile or at runtime says so, and a squash axis
1256
+ authored through negative scale therefore gets the mirror of the dial you wrote,
1257
+ symmetric about the point where it should have passed through. A world `shearY`
1258
+ axis folds the same way at a whole turn, and its seam is not at a fixed value —
1259
+ it moves with wherever the bone is pointing. ⇒ for a scale or a shear axis, write
1260
+ `local: true`; for a scale axis you cannot, keep the whole range on one side of
1261
+ zero, because the reader has no other half to give you.
1262
+
1263
+ ⭐ **A dial can drive another dial's authority, and that composes as a product.**
1264
+ A slider's own `mix` is a keyable property, so one axis can scale another axis'
1265
+ whole contribution — a "strength" dial over an expression, which is the one shape
1266
+ on this page that multiplies rather than adds. ⚠️ **It only works downward through
1267
+ the `constraints` array.** A slider reads its own authority when its turn comes
1268
+ and the array is the update order, so a dial that keys the `mix` of a slider
1269
+ *earlier* than itself writes a number that slider has already read past: the
1270
+ driven axis is dead at every position, every frame. ✅ **The gate names that
1271
+ pair** — `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` refuses it with both
1272
+ sliders, the property and both array indices, and says which way to move them.
1273
+ ⇒ put the driving slider **first**, which is what the refusal tells you to do.
1274
+
1275
+ 🚨 **And a dial cannot turn itself on.** `Slider.update` reads its own `mix` as
1276
+ the alpha it applies the animation with, before that animation runs, so a slider
1277
+ whose *own* animation keys its `mix` writes after the only read of it — the same
1278
+ refusal with the two array indices equal. The shape to watch for is an axis
1279
+ muted at setup that means to raise itself: it never applies anything at all,
1280
+ because `update` returns on `mix` 0 before reaching the key that would raise it,
1281
+ and `A37` reports green because it asks whether *an* animation keys the mix and
1282
+ never which one.
1283
+
1284
+ 🚨 **And it is not only another dial: a face axis that drives a jiggle or a
1285
+ path is the same rule.** `physics.<name>.wind`, `path.<name>.position`, an ik or
1286
+ transform mix — every property a dial can key belongs to a constraint that reads
1287
+ it when its own turn comes, and that turn is its place in `constraints`. A
1288
+ "wind strength" dial declared after the physics constraint it drives poses the
1289
+ number in that constraint's pose and moves nothing at all: [measured] the bone a
1290
+ physics constraint drives travels `0.000e+0` across the dial with the constraint
1291
+ declared first and `4.256e+2` with it declared last, and a path constraint's
1292
+ rider `0.000e+0` against `1.620e+2`. ✅ `A42` names those pairs
1293
+ too, with the constraint's kind and both array indices. ⇒ **every constraint a
1294
+ dial drives goes after that dial in `constraints`** — which, for a face, means
1295
+ the dials come first and the jiggles, paths and aim constraints they scale come
1296
+ after. 🔸 One key is outside the rule because no order repairs it: a `physics`
1297
+ `reset` from a dial fires on a crossed frame time and a slider applies its
1298
+ animation at a single instant, so it never fires at all — the gate says that in
1299
+ its SKIP rather than asking you to move anything.
1300
+
1301
+ 🔸 **The bone-less slider is the same story one field over.** A slider with no
1302
+ `bone` takes its time from `slider.<name>.time`, which any animation can key — and
1303
+ two dials keying it *add*, so a time-driven axis composes like everything else
1304
+ here, and the gate agrees. The same array rule applies, for the
1305
+ same reason — and `A42` refuses it there too, naming `time` instead of `mix`.
1306
+ ⚠️ Two things the bone
1307
+ form does and this one does not: there is no `Math.max(0, time)` and no wrap, so a
1308
+ driven time below zero does not pose the first frame — it leaves the pose exactly
1309
+ as it found it, which is a different picture whenever the animation's first frame
1310
+ is not the rest pose.
1311
+
1312
+ ⚠️ **Two `skinRequired` sliders are three states, not two.** Under a skin that
1313
+ lists one of them the face is that dial alone; under a skin that lists the other
1314
+ it is the other alone; and under a skin that lists **neither** — the default skin
1315
+ is usually one — every dial is dead and the face holds its rest pose with both
1316
+ dials turned to their extremes. That last state is indistinguishable, from the
1317
+ outside, from a rig whose sliders do not work, and the gate is right to be silent
1318
+ about it because the pair genuinely never meets.
1319
+
1320
+ ⭐ **Four dials are the same sum as three.** Nothing new arrives with the fourth
1321
+ axis: the arithmetic, the flag, and the non-additive-in-the-middle case all read
1322
+ exactly as they do above. Write `"additive": true` on all of them.
1323
+
1324
+ ✅ **The editor half, measured.** The round trip was taken with
1325
+ `tools/editor_roundtrip.ts` on a licensed editor (data version 4.3.26) against a 4.3.13 build of
1326
+ [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) — this
1327
+ subsection's worked case, not the `gallery/portrait` this page names at the top,
1328
+ which declares no constraints at all and so can carry no slider. What it found:
1329
+
1330
+ - **Both sliders come back, and the parameter axis survives.** `additive`,
1331
+ `local`, `bone`, `property`, `from`, `max` and `scale` are identical field for
1332
+ field, and the two keep their places in the `constraints` array. `mix: 1` and
1333
+ `to: 0` are dropped, and those are the format's own defaults (`SkeletonJson`
1334
+ reads `mix` as 1 and `to` as 0 when absent) — an elision, not a loss.
1335
+ - ⚠️ **The animation each slider *names* comes back because rigc emits
1336
+ animations in the editor's own order** — natural and case-insensitive. The
1337
+ editor re-sorts the `animations` object and a slider's animation is an ordinal
1338
+ in the format, so out of that order `yaw -> "turn"` returns as
1339
+ `yaw -> "sweep"` — the first animation of the sorted list. In order, on the
1340
+ same rig through the same editor, `yaw -> "turn"` comes back and the
1341
+ re-rendered mean absolute error is 0.3035 / 0.0769 / 0.0588
1342
+ (`sweep` / `tilt` / `turn`) against 10.4655 / 8.4961 / 8.7140 out of order,
1343
+ worst drift 3.947 px against 16.535 px.
1344
+ - 🚨 **The physics constraint on the cowlick comes back driving nothing.**
1345
+ `rotate: 1` is absent from the export, and an absent `rotate` parses as **0**
1346
+ (`SkeletonJson`), so the returned file states *drives nothing* rather than
1347
+ omitting a default — which is why `A23_PHYSICS_CONSTRAINT_EFFECTIVE` refuses it
1348
+ by name. Independent of the ordering.
1349
+
1350
+ ✅ **Why, measured** — and corrected. It is not elision and not a defect in
1351
+ one field. Issue #540 measured three rigs and twelve constraints, predictions
1352
+ written before the round trip: a lone `y` came back, `x` and `y` together came
1353
+ back, and a lone `rotate`, a lone `scaleX` and a lone `shearX` each came back as
1354
+ **no components at all**, with every constraint's fixed-point `strength`
1355
+ returning exactly. It read that as *"the editor's physics model holds `x` and
1356
+ `y` and nothing else"*, and that reading was wrong.
1357
+
1358
+ 🔁 **Corrected 2026-10-06 (issue #1196), on Spine 4.3.23 and 4.3.26:** what
1359
+ decides the loss is the **bone's length**, not the component. The editor's own
1360
+ example export `sack-pro` keeps 18 of 18 `rotate` constraints through the same
1361
+ import and export, and deleting the `length` of one of its bones loses `rotate`
1362
+ on exactly that one constraint. `look` with a 40-unit `ahoge_whip` keeps
1363
+ `rotate`, `scaleX` and `shearX` each; at 0.01 and 1 `rotate` is kept too; with no
1364
+ length all three are lost, on both versions. `look`'s `ahoge_whip` was emitted
1365
+ with no length; #540's rigs were not at hand to read, but every row it reported
1366
+ is reproduced here at length 0. ⇒
1367
+ **A rotation-driven jiggle survives the editor on a bone that has a length**,
1368
+ and `A23_PHYSICS_CONSTRAINT_EFFECTIVE` refuses one on a bone that has none
1369
+ (issue #1195).
1370
+
1371
+ ✅ **And the locus is measured: the loss is at EXPORT** — against Spine
1372
+ 4.3.26 and without decoding the project format. `gallery/look`'s build and three variants of its
1373
+ `skeleton.json` were each imported with the documented CLI, the project files
1374
+ inflated, and compared byte by byte. **Two imports of the same file differ
1375
+ only at bytes 13–28** — a timestamp or a hash — so that is the noise floor,
1376
+ and reading the bytes works because everything below byte 1914 is stable
1377
+ across imports. Against that floor the **only** stable difference between the
1378
+ `rotate`-driving file and the `rotate`-less one is the float32 at byte 248:
1379
+ `3f 80 00 00` = **1.0** where the source said `rotate: 1`, `7f c0 00 00` =
1380
+ **NaN** — the editor's own unset — where it did not, while an `x: 1` variant
1381
+ leaves 248 at NaN and moves a slot 66 bytes on, which is the component that
1382
+ survives. ⇒ The importer read `rotate` and kept it as a **non-default** value;
1383
+ the exporter wrote nothing for it. It is the editor's **writer**, not its
1384
+ reader, and not rigc's emitter.
1385
+
1386
+ The importer stores the value on a zero-length bone too, at 4.3.23 as well as
1387
+ 4.3.26 (issue #1196: `rotate: 0.234375` lands as `3e 70 00 00` in the project,
1388
+ and the JSON and binary exports both omit it — the binary export of a project
1389
+ holding `rotate: 1` is byte-identical to one holding NaN).
1390
+
1391
+ 🗑️ **A41, the rule that gated the editor round trip, and its declaration
1392
+ `invariants.editorRoundTrip` were retired in issue #1196.** They gated the
1393
+ components rather than the bone, so they refused rotation physics the editor
1394
+ keeps, and once A23 refuses those components on a zero-length bone a rig that
1395
+ passes it has nothing the editor's export drops — the declaration could no
1396
+ longer move a verdict.
1397
+ - 🔸 Unexplained, and **unreproduced by anything in this tree**: `diff` reports
1398
+ `animations.curve_kinds` moved on **196 of 200** keys in every round trip taken,
1399
+ the clean one included. Visually small once the ordering is fixed — but it is
1400
+ 98% of the keys, and *small* is not *explained*. ⚠️ **[measured] by
1401
+ `tools/editor_roundtrip.ts` on the trips this subsection records, and no
1402
+ command this page states re-takes it** — so read the figure as those runs' and
1403
+ not as a property the gate holds.
1404
+
1405
+ ✅ **Two more things are measured, and both came back safe:**
1406
+
1407
+ - **More than one event is safe.** The editor re-keys `events` the way it re-keys
1408
+ `animations` — `zebra, mike, alpha` came back `alpha, mike, zebra` — but every
1409
+ firing resolved **by name**, `0.3 -> mike` and `0.6 -> alpha`, payloads intact. The ordinal shape does
1410
+ *not* bite here, and rigc emits events in the order you declare them.
1411
+ - **More than one skin imports.** A four-skin rig imports in **both build
1412
+ modes** — default and `--copy-images` — **exit 0, project written**. What binds
1413
+ is a `CompileError` when the default skin shares a placeholder with a named
1414
+ one. ⇒ Skins are not the reason to stay at one, and every figure on this page
1415
+ was taken on a rig carrying exactly one (AUTHORING §10.1).
1416
+
1417
+ Every figure above was read back through `spine-core`.
1418
+
1419
+ ---
1420
+
1421
+ ## 9. Looking at it, and the audit gap
1422
+
1423
+ ### 9.1 The looking protocol is three scales, not one
1424
+
1425
+ | Scale | What it is for |
1426
+ | --- | --- |
1427
+ | **the contact sheet** (~0.45×) | whether the **motion** reads. Spacing is a comparison *across* frames, so the grid is the only place to see it |
1428
+ | **1:1** | whether the **drawing arrived** |
1429
+ | **3–4× on the eyes** | because that is where a reader will look, and a face has no other equivalent |
1430
+
1431
+ 📊 **Four of the five art defects in the worked example were invisible at contact
1432
+ sheet scale** and all four were found at 1:1 or better: the dark seam, the lid's fade letting a
1433
+ shut eye's lash show through as a grey smudge, the iris crossing its own lash at
1434
+ the gaze extreme, and a forehead highlight turning the lid's soft edge into a
1435
+ tonal step.
1436
+
1437
+ ⚠️ **A portrait's plates are *supposed* to overlap invisibly**, and that is
1438
+ where a renderer's edge defect shows: a part that carries an ink outline at its
1439
+ edge hides one, because a dark rim on a dark line cannot be seen. ⇒ **Scene work exercises a renderer where game-part work does
1440
+ not.** Expect to find renderer defects on your first face, and check a suspicious
1441
+ edge against a **region build** before reading a single vertex — which is the
1442
+ next item.
1443
+
1444
+ ⭐ **When a mesh is the new thing in a shot, build the region version and diff it
1445
+ before suspecting the mesh.** The worked example's first seam suspect was the
1446
+ grid: faint vertical lines down the forehead, at what looked like column
1447
+ positions. Building the same rig with both meshes replaced by plain regions and
1448
+ diffing the rest frame settled it in one command — worst channel difference 1,
1449
+ zero pixels differing — so the mesh was innocent and the lines were art
1450
+ compositing. **Two minutes, and it halves the search space.**
1451
+
1452
+ 📌 **Draw the face before you rig it.** The worked example took **seven art
1453
+ passes before a `rig.json` existed** — brows twice, hair silhouette, fringe
1454
+ depth, neck length, garment mass, choker, proportions. That order is not
1455
+ fastidiousness: **the head plate's half-width *is* `R`, and `R` is in every one of
1456
+ the thirty derived numbers.** Rigging first means re-deriving all of them every
1457
+ time the plate changes width.
1458
+
1459
+ ⚠️ **And expression lives in ink weight before it lives in shape.** The worked
1460
+ example's brows read as a scowl for two iterations and the brows were not the
1461
+ problem — two eyes whose upper lash is heaviest at the **inner** corner read as
1462
+ angry whatever the brow above them does. Worth knowing before spending a pass on
1463
+ the wrong part.
1464
+
1465
+ ### 9.2 🚨 The half nothing measures — three builds, one of them refused
1466
+
1467
+ **The setup geometry is measured; one half of the deformed geometry is not.**
1468
+ Here is that claim as three builds of the same rig, each command runnable verbatim from a
1469
+ clean checkout. Start from the good one:
1470
+
1471
+ ```bash
1472
+ bun install # once
1473
+
1474
+ bun cli.ts build --rig gallery/portrait/rig.json \
1475
+ --motion gallery/portrait/motion.json \
1476
+ --out gallery/portrait/build --profile spine-html
1477
+ bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
1478
+ --out gallery/portrait/render
1479
+ ```
1480
+
1481
+ ```
1482
+ MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
1483
+ MESH hair_bang authored 15 vertices / 16 triangles (budget 32) bones=[bang] attachments=[hair_bang] covers 100.00% of the art, reaching 55.22px past it
1484
+ .. validate (spine-core round trip + machine assertions, profile spine-html)
1485
+ .. profile spine-html — every assertion applies
1486
+ ```
1487
+
1488
+ Green — the profile line above says so in the tool's own words.
1489
+
1490
+ Now break the projection two ways. Both scripts write a variant motion spec
1491
+ beside the originals and touch nothing in the repository:
1492
+
1493
+ ```bash
1494
+ # (a) INVERT ONE BAND: give the two far columns each other's shift.
1495
+ # The far side now STRETCHES 1.363 where it should compress to 0.637 —
1496
+ # the head reads as turning the other way at its own edge. This one has to
1497
+ # REPLACE the transform with a table: an inverted band is not the closed
1498
+ # form at any angle, and §1.1 refuses a `transform` beside a `vertices` run.
1499
+ # Each vertex takes the shift for THE COLUMN IT IS IN, looked up in the rig:
1500
+ # a `vertices` run is positional, so a script that assumes the list's order
1501
+ # rather than reading it breaks in silence the day the list is renumbered.
1502
+ bun -e '
1503
+ const r = await Bun.file("gallery/portrait/rig.json").json();
1504
+ const m = await Bun.file("gallery/portrait/motion.json").json();
1505
+ const v = r.skins.default.head.head.vertices;
1506
+ const d = m.animations.turn.deform.find(x => x.slot === "head");
1507
+ const shift = {"-162": -22.414, "-120": -7.175, "0": -35.345, "120": -27.658, "162": -14.255};
1508
+ const run = [];
1509
+ for (let i = 0; i < v.length / 2; i++) run.push(shift[v[i * 2]], 0);
1510
+ for (const k of d.keys) if (k.transform) {
1511
+ delete k.transform;
1512
+ k.fromVertex = 0;
1513
+ k.vertices = run;
1514
+ }
1515
+ await Bun.write("/tmp/swapped.motion.json", JSON.stringify(m, null, 2));
1516
+ '
1517
+ bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/swapped.motion.json \
1518
+ --out /tmp/swapped --profile spine-html
1519
+
1520
+ # (b) FOLD IT: evaluate the same closed form at 40°, past the 31.37° of §4.2,
1521
+ # so the two far columns swap order and the mesh turns inside out. Since
1522
+ # §1.1 the whole of it is one number.
1523
+ bun -e '
1524
+ const m = await Bun.file("gallery/portrait/motion.json").json();
1525
+ const d = m.animations.turn.deform.find(x => x.slot === "head");
1526
+ for (const k of d.keys) if (k.transform) k.transform.degrees = 40;
1527
+ await Bun.write("/tmp/folded.motion.json", JSON.stringify(m, null, 2));
1528
+ '
1529
+ bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/folded.motion.json \
1530
+ --out /tmp/folded --profile spine-html
1531
+ ```
1532
+
1533
+ **What comes back from both:**
1534
+
1535
+ | | good | (a) one band inverted | (b) mesh folded |
1536
+ | --- | --- | --- | --- |
1537
+ | `--profile spine-html`, **without `A39`** | green | **green, and the same counts** | **green, and the same counts** |
1538
+ | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | PASS | **PASS** | **PASS** |
1539
+ | the `MESH` coverage line | 100.00%, 95.90px past | **byte-identical** | **byte-identical** |
1540
+ | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | PASS | PASS | **FAIL, both keys, 8 of 32 triangles** |
1541
+ | `--profile spine-html`, **with `A39`** | green | green | **refused, and nothing written** |
1542
+ | the `DEFORM` block, `head` key 1 `area` | x0.637174 … x1.319122 | **x0.765250 … x1.362834** | **x−0.288121 … x1.820211** |
1543
+ | … and its `winding` | 32 of 32 kept | 32 of 32 kept | **24 of 32 kept** |
1544
+
1545
+ 🚨 **All three were green, and the coverage line is the same string in all three,
1546
+ because it reports the SETUP pose.** The `--profile spine-html` row with `A39` is
1547
+ the only verdict that separates them, and the two rows above it measure only the
1548
+ setup geometry: `A35` is silent about build (b). `A35` checks that a deform run *fits* its
1549
+ attachment — an honest and useful check, and orthogonal to whether the numbers in
1550
+ it mean anything.
1551
+
1552
+ ⭐ **The two `DEFORM` rows are the ones that separate (a) from the good build**,
1553
+ and nothing else in the toolchain does that without a reference render. `A39` is
1554
+ right to pass (a) — no triangle reverses — so the whole of the difference is a
1555
+ pair of ratios, and the way to read them is to ask the tool rather than to copy
1556
+ them down:
1557
+
1558
+ ```bash
1559
+ bun cli.ts explain --rig gallery/portrait/rig.json \
1560
+ --motion /tmp/swapped.motion.json --out /tmp/explain-swapped
1561
+ ```
1562
+
1563
+ ```
1564
+ DEFORM turn default/head/head key 1 t=0.620000 authored table
1565
+ frame played on a track
1566
+ moved 25 of 25 vertices, worst 35.3450px at v10
1567
+ area min x0.765250 tri 19 max x1.362834 tri 1 (32 triangles, 0 with no area at the cleared pose, band 0.146694px²)
1568
+ stretch max x1.362834 tri 1 min x0.765250 tri 26
1569
+ winding 32 of 32 kept, 0 collapsed
1570
+ ```
1571
+
1572
+ 🚨 **Read it band by band, because the extremes have swapped ends and comparing
1573
+ worst to worst hides that.** The block names the triangle beside every ratio, and
1574
+ §4.1's table says what each band should be. `tri 1` spans **−162 → −120**, the
1575
+ band §4.1 puts at **0.637** and the good build's own block reports as
1576
+ `x0.637174 tri 17` — the same band, and here it comes back at **x1.362834**. That
1577
+ is this section's prose, *"stretches 1.363 where it should compress to 0.637"*, as
1578
+ a figure the tool produces, and it is the far edge turning the wrong way. `tri 19`
1579
+ spans **−120 → 0**, tabled at 0.892 and measured at **x0.765250**: the swapped
1580
+ column is the boundary between those two bands, so the compression the far band
1581
+ gave up lands next door. ⚠️ Read as min-against-min and max-against-max instead,
1582
+ the very same four figures say 1.319 → 1.363 and 0.637 → 0.765 — two comparisons
1583
+ across *different* bands, both of them mild, neither of them what happened. Only
1584
+ the two bands the swapped columns bound move at all: the 0 → 120 and 120 → 162
1585
+ 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:
1586
+ the model is gone, so nothing is left to check the ratios against but the ratios.
1587
+
1588
+ `rigc explain` is the instrument that prints every timeline's actual values, and
1589
+ on a deform that is the model and every offset it produced (AUTHORING §4.11.1)
1590
+ and the `DEFORM` block's geometry (AUTHORING §4.11.2) —
1591
+
1592
+ ```bash
1593
+ bun cli.ts explain --rig gallery/portrait/rig.json \
1594
+ --motion gallery/portrait/motion.json --out /tmp/explain
1595
+ ```
1596
+
1597
+ ```
1598
+ DEFORM turn default/head/head key 1 t=0.620000 transform yaw radius=170 degrees=12
1599
+ frame played on a track
1600
+ moved 25 of 25 vertices, worst 35.3450px at v2
1601
+ area min x0.637174 tri 17 max x1.319122 tri 31 (32 triangles, 0 with no area at the cleared pose, band 0.146694px²)
1602
+ stretch max x1.319121 tri 22 min x0.637175 tri 8
1603
+ winding 32 of 32 kept, 0 collapsed
1604
+ ```
1605
+
1606
+ ⇒ **`0.637` is a figure the tool prints.** It is a *report* and not a bar —
1607
+ `explain` takes no `--profile` and gates nothing, because a deliberate
1608
+ 3× stretch is a real thing to author, and the one deformed-geometry fault with no
1609
+ legitimate counter-example is the fold, which is `A39`'s.
1610
+
1611
+ **`A39_DEFORM_KEEPS_TRIANGLE_WINDING` closes the half of that gap the fold
1612
+ lives in** — `--profile spine-html`. Build (b) above is refused
1613
+ by name, on both its keys, with the triangles listed:
1614
+
1615
+ ```
1616
+ FAIL A39_DEFORM_KEEPS_TRIANGLE_WINDING: animation "turn" deform head/head key 1
1617
+ (t=0.6200000047683716s): 8 of 32 triangle(s) reverse winding — triangle 0
1618
+ [0,15,16] 1890.000 -> -544.548px²; …
1619
+ ```
1620
+
1621
+ and builds (a) and the good one both still PASS it, because **an inverted band is
1622
+ not a fold**: its winding survives. The angle A39 first fires at agrees with
1623
+ §4.2's `tan θ = Δx/Δz` to **0.0001°**, so the formula above is checkable by
1624
+ running the gate instead of by rendering seven variants.
1625
+
1626
+ **And one thing it does not refuse.** Build (b) is refused
1627
+ because the head is *drawn* while it folds. Fade that slot to alpha exactly 0 over
1628
+ the same keys — §4.2's fourth way out, and what a face past its ceiling actually
1629
+ does — and the same build is green, with the key still measured and the reason
1630
+ printed rather than passed over in silence:
1631
+
1632
+ ```
1633
+ DEFORM turn default/head/head key 1 t=0.500000 transform yaw radius=170 degrees=40
1634
+ skipped A39 reads no winding off this key: the slot's alpha is exactly 0 at this
1635
+ time (slot 0.0000 x attachment 1.0000), so this key draws no pixels — a
1636
+ triangle that draws no pixels cannot draw them backwards
1637
+ winding 24 of 32 kept, 0 collapsed <- a fold, and nothing gates it: this key draws no pixels
1638
+ ```
1639
+
1640
+ `A39`'s message says the mesh "draws its texture backwards there", and that
1641
+ sentence is false when the slot draws nothing. The bar is **alpha exactly
1642
+ 0**; at 0.5 build (b) is refused, with the alpha in the message. The
1643
+ same fold at full alpha in another animation is refused, because the
1644
+ measurement is of one key at one time.
1645
+
1646
+ 🚨 **And a key is not the whole of it: the geometry between two keys is
1647
+ interpolated.** Put the alpha-0 key exactly on
1648
+ the 40° key and 8 triangles are already reversed at `t=0.4`, where the slot is
1649
+ still drawing at **alpha 0.20**:
1650
+
1651
+ | t | slot alpha | reversed triangles |
1652
+ | --- | ---: | ---: |
1653
+ | 0.30 | 0.40 | 0 |
1654
+ | 0.40 | **0.20** | **8** |
1655
+ | 0.45 | 0.10 | 8 |
1656
+ | 0.49 | 0.02 | 8 |
1657
+ | 0.50 | 0.00 | 8 |
1658
+
1659
+ 📌 The table and the refusal below are on the **turn probe** — this head's own
1660
+ five columns and 32 triangles on a one-second timeline, which is why the times
1661
+ are not build (b)'s.
1662
+
1663
+ **Every interval between two consecutive deform keys is scanned.** A deform interpolated between two
1664
+ keys travels a **straight line through offset space**, so a triangle's signed
1665
+ area is a *quadratic in the interpolation fraction* — the fold is a root of it,
1666
+ solved for rather than searched, with no sample spacing anybody would have to
1667
+ defend. What that arithmetic names is then posed and measured by the same code
1668
+ that measures a key, **alpha read at that same instant**, so the fade a correct
1669
+ rig relies on is not refused and the frames it does not cover are. The sentence names
1670
+ the two keys the fold lies between rather than one key index, the time it solved for
1671
+ and how far along the segment that is, the reversed triangles with their signed areas,
1672
+ `NO KEY LANDS THERE` in those words, and the alpha read at that same instant — so what
1673
+ it refuses is legible as a frame rather than as a key. AUTHORING §4.11.3 reads it field
1674
+ by field and [`src/validate.ts`](../src/validate.ts) builds it; no spec this repository
1675
+ ships produces one.
1676
+
1677
+ ⇒ The rule — *fade out over the run up to the angle you cannot
1678
+ take, so that every key past the ceiling is one that draws nothing* — is a
1679
+ **measurement**: land the alpha-0 key
1680
+ on the fold and the build is refused, with the frame it is refused for.
1681
+
1682
+ ⚠️ **What that scan cannot see**: the closed form holds the BONES still across the span. On an
1683
+ unweighted attachment that is exact — one matrix multiplies every vertex and its
1684
+ determinant cancels out of the sign comparison — but on a weighted mesh whose
1685
+ bones move across the span it is an approximation, and a prediction no
1686
+ measurement reproduced is reported (`deformSpansUnconfirmed`) rather than
1687
+ refused. A fold caused by the bones alone is not this rule's subject at all, and
1688
+ `check` against a trusted render (§9.3) is what sees it.
1689
+
1690
+ ⚠️ **One thing it deliberately does not do.** It is an **archetype** rule, so a
1691
+ `--profile spine` build reads `PROF` — the premise "a fold has no legitimate
1692
+ counter-example" is false: an official
1693
+ `spineboy-pro` export reverses one of `hoverboard-board`'s 101 triangles, and a
1694
+ `validity` rule would have told its author to change correct data. ⇒ `explain`'s
1695
+ `DEFORM` block is the surface with **no profile at all**, so the winding count is
1696
+ readable on a `--profile spine` build the gate will not mention it to.
1697
+
1698
+ `check` against a trusted render, below, stays the deeper instrument for the
1699
+ reasons §9.3 gives, and these two are the cheap always-on layer above it.
1700
+
1701
+ ### 9.3 The audit that works today, and exactly what it cannot do
1702
+
1703
+ `rigc check` renders a candidate onto reference frames' own pixel grid and
1704
+ compares (INGEST §1.4). Point it at a render of a build you already trust and it
1705
+ **does** see a wrong deform:
1706
+
1707
+ ```bash
1708
+ bun cli.ts check --candidate gallery/portrait/build --frames gallery/portrait/render/turn@25fps
1709
+ bun cli.ts check --candidate /tmp/swapped --frames gallery/portrait/render/turn@25fps
1710
+ bun cli.ts check --candidate /tmp/folded --frames gallery/portrait/render/turn@25fps
1711
+ ```
1712
+
1713
+ ⚠️ **The third of those needs a build §9.2's own command will not write.** Build (b)
1714
+ under `--profile spine-html` is refused and nothing
1715
+ lands in `/tmp/folded` — which is the row above it in §9.2's table, working. Take
1716
+ that candidate from the **default** profile, where `A39` reads `PROF` and the
1717
+ artifact is written — a profile selects which assertions apply and not what is
1718
+ emitted, and on this rig the two profiles write a `skeleton.json` and a
1719
+ `skeleton.atlas` that are identical byte for byte.
1720
+
1721
+ **No run reproduces this:** the three commands above read a build directory and two `/tmp` paths this repository does not track, so no gate reaches them; re-taken by hand against a render of the good build
1722
+
1723
+ | Candidate | MAE mean | worst | at |
1724
+ | --- | --- | --- | --- |
1725
+ | the build the frames came from | **0.00** | 0.00 | — |
1726
+ | (a) one band inverted | **0.20** | 0.38 | **f0016** |
1727
+ | (b) mesh folded | **2.07** | 3.66 | **f0016** |
1728
+
1729
+ ⭐ **Both defects land on `f0016`, which is the frame the turn arrives on** — the
1730
+ `worst at` column points straight at the moment, which is what makes this worth
1731
+ running at all.
1732
+
1733
+ 🚨 **And now the three limits, because this is the instrument you will be tempted
1734
+ to call an audit:**
1735
+
1736
+ 1. **It is differential.** It measures a candidate against **a render of another
1737
+ build**, so it catches a *regression* and cannot validate a *first authoring*.
1738
+ There is no reference for a face nobody has drawn yet.
1739
+ 2. **A wrong projection is a whisper in the aggregate.** Inverting a whole band —
1740
+ the far edge stretching to **1.363** where it should compress to **0.637**, so
1741
+ the head's own edge turns the wrong way — moves the mean MAE by **0.20 of
1742
+ 255**. Nothing about that number says *"the winding"*; you have to already
1743
+ suspect it.
1744
+ 3. **The `slot drift` column cannot see it at all.** It was `1.2 px "lid_r"` in
1745
+ **all three** runs above, unchanged, because drift is attributed per **slot**
1746
+ and a folded head mesh is entirely inside one slot.
1747
+
1748
+ 📌 **`explain`'s `DEFORM` block (§9.2, AUTHORING §4.11.2) takes half of limits 1
1749
+ and 2 away, and none of limit 3.** It is reference-free, so it says something
1750
+ about a first authoring; and it is *per key and per triangle*, so the band
1751
+ inversion above reads as `x1.362834` on `tri 1` where the same band is `x0.637174`
1752
+ 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
1753
+ wanted** — that needs the picture, which is why the procedure below survives the
1754
+ block as it survived `A39`.
1755
+
1756
+ 📌 **`explain`'s `MEMBER` block (§3.1, AUTHORING §4.5.2) does the same for the
1757
+ bone half, and it takes the **nose test** off the procedure below.** §3 makes the
1758
+ nose the diagnostic — *if the nose's residual is not negative, the depths are
1759
+ wrong* — and the block prints the six residuals in a column with the depth that
1760
+ produced each one, so the check is reading one sign. ⚠️ It still cannot say
1761
+ whether the **depth** was right: `nose at depth 192` evaluates as consistently
1762
+ wrong as it does right, and no reference frame separates a plausible depth table
1763
+ from the intended one.
1764
+
1765
+ ⇒ **So the honest procedure — thinner for `A39` and the two report blocks, and
1766
+ still a procedure, because none of the three limits above is one they
1767
+ lift:** state the model on the key rather than deriving a table (§1.1 for the
1768
+ mesh, §3.1 for the bones), so what a reviewer reads is a radius, an angle and a
1769
+ depth per part; read the **nose's sign** off the `MEMBER` block and check the
1770
+ **fold angle** (§4.2's formula) arithmetically before you build — the second is
1771
+ still yours, and a stated model evaluates a wrong radius as consistently as a
1772
+ right one — then render, **look at three scales**, and keep a render of the last
1773
+ build you trusted so `check` has something to be differential against.
1774
+
1775
+ 📌 **What §1.1 and §9.2's block make readable, and what neither does.**
1776
+ `explain` prints the model beside the offsets it produced, so *what a key claims* is readable; the `DEFORM` block prints what the
1777
+ key **did** — the area and stretch extremes, the displacement and the winding —
1778
+ so *what the claim came to* is readable too, per key and with no reference
1779
+ (`A39` catches the fold inside it). *Whether the claim is right* is not: **nothing
1780
+ measures whether 12° was the angle the shot wanted**, and nothing above is a
1781
+ substitute for looking at three scales.
1782
+
1783
+ ---
1784
+
1785
+ ## 10. The worked example
1786
+
1787
+ [`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait)
1788
+ is this page on real art — **22 parts, 27 bones, 22 slots, 2 meshes, 40
1789
+ vertices**, three animations, and a README that derives every number rather than
1790
+ listing it. Its
1791
+ [`FINDINGS.md`](https://github.com/firejune/rigc/tree/main/gallery/portrait/FINDINGS.md)
1792
+ is the measurement half: what it cost, the seven-angle sweep, the five tool gaps
1793
+ it filed.
1794
+
1795
+ 📘 **The `pitch` has its own worked case**, and this page deliberately stays a
1796
+ yaw: [`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod) is
1797
+ the same two closed forms on the other axis, plus a travelling `wave` (AUTHORING
1798
+ §4.11.1). §4.2 and §5 above point at the two places its figures are worth
1799
+ reading beside these — the fold angle bracketed against `A39`, and a
1800
+ foreshortening span that is wider at the same angle.
1801
+
1802
+ ```bash
1803
+ bun install # once
1804
+
1805
+ bun cli.ts build --rig gallery/portrait/rig.json \
1806
+ --motion gallery/portrait/motion.json \
1807
+ --out gallery/portrait/build
1808
+ bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
1809
+ --out gallery/portrait/render
1810
+ bun cli.ts preview --candidate gallery/portrait/build \
1811
+ --out gallery/portrait/preview.html
1812
+
1813
+ # do the cycles close on the poses they opened with?
1814
+ bun gallery/loop_seam.ts gallery/portrait/render/idle@25fps
1815
+ bun gallery/loop_seam.ts gallery/portrait/render/gaze@25fps
1816
+ bun gallery/loop_seam.ts gallery/portrait/render/turn@25fps
1817
+ ```
1818
+
1819
+ `build`, `render` and `preview` are not committed; the specs and the 22 part PNGs
1820
+ are, and those commands regenerate the rest. **The frames to look at:**
1821
+
1822
+ | Frame | What it is |
1823
+ | --- | --- |
1824
+ | `render/turn@25fps/f0016.png` | the yaw arrives. **Look at this one at 1:1** — a contact sheet cannot show you whether the face turned or merely slid |
1825
+ | `render/turn@25fps/f0000.png` | rest, for the comparison. The pair is the whole example |
1826
+ | `render/idle@25fps/f0028.png` | the blink, shut |
1827
+ | `render/gaze@25fps/f0015.png` | the gaze, held |
1828
+
1829
+ **What those commands print** — re-run verbatim for this page, from a checkout
1830
+ with no `build/`, `render/` or `preview.html` in that directory. The `build`
1831
+ says it in its own words, and these are the four lines this page's claims about
1832
+ it come off:
1833
+
1834
+ ```
1835
+ MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
1836
+ MESH hair_bang authored 15 vertices / 16 triangles (budget 32) bones=[bang] attachments=[hair_bang] covers 100.00% of the art, reaching 55.22px past it
1837
+ .. validate (spine-core round trip + machine assertions, profile spine)
1838
+ .. profile spine — 8 renderer-policy and 8 archetype assertion(s) do not apply
1839
+ ```
1840
+
1841
+ `spine` is the default, so that is the `build` above with no `--profile` on it —
1842
+ the report names what the profile leaves out rather than this page counting it.
1843
+ §9.2 runs the same rig under `--profile spine-html` and quotes what comes back
1844
+ there; `A13_MESH_BUDGET` and `A15_IDLE_NO_MESH_BONE_KEYS` are among the rules
1845
+ the default profile excludes.
1846
+
1847
+ **No run reproduces this:** the three rows below come from commands that read the build directory this repository does not track, so no gate reaches them; re-taken by hand from a clean `gallery/portrait`
1848
+
1849
+ | Command | What came back |
1850
+ | --- | --- |
1851
+ | `render --fps 25 --max 640` | **81 + 39 + 56 frames**, 478×640, three contact sheets |
1852
+ | `loop_seam.ts` ×3 | **0 / 255**, **0 of 305 920 pixels** differing, for all three |
1853
+ | `preview` | one **414.5 KiB** HTML file, 22 pages embedded as data URIs — the figure the tool prints |
1854
+
1855
+ 📊 **Figures this page took from the record rather than re-deriving**, because
1856
+ they need the artifact's own pixels: the blink's occlusion (hiding the whole eye
1857
+ assembly at the shut hold changes **0 of 305 920** pixels; positive control at
1858
+ rest moves **8 183**), the per-edge narrowing displacements, the `spine-core`
1859
+ agreement of every posed column and scale with §1's line to **under 0.001 px**,
1860
+ and the Web Player interop pass (**0 console errors, 0 page exceptions**).
1861
+ `setup: { "slot": null }`, the obvious way to hide one slot, is a named
1862
+ `rigc compile error` that gives the spelling. Measured on a copy of this
1863
+ example's `motion.json` carrying one such entry, with the path the run echoes
1864
+ shortened and the line wrapped:
1865
+
1866
+ ```
1867
+ rigc compile error: motion.json: `setup."eye_l"` is null; a setup entry is an
1868
+ object of `{ attachment?: string | null, color?: [r, g, b, a] }` — to show
1869
+ nothing there write `"eye_l": { "attachment": null }`, and to show an attachment
1870
+ write `"eye_l": { "attachment": "<name>" }`
1871
+ ```
1872
+
1873
+ ⚠️ **And the second refusal says where that edit goes.** Writing the spelling that message names on a slot this rig
1874
+ gives an attachment to is refused in turn — `slot "eye_l" has a setup attachment
1875
+ in the rig spec AND in the motion spec; the setup pose has one author` — so on
1876
+ the worked example hiding one slot is an edit to the **rig** spec rather
1877
+ than a line in the motion spec.
1878
+
1879
+ ⭐ **Vela is a second cast member and that was deliberate**, against the gallery's
1880
+ own rule that its examples share one drawing. A 2.5D turn reads off four things: a
1881
+ brow that frames an eye, an **iris and a highlight as separate parts**, hair in
1882
+ **layers** that can lag the skull, and a cheek-to-jaw silhouette with a landmark
1883
+ in it to foreshorten. The gallery's mascot has a muzzle, and a muzzle points
1884
+ wherever the head points — so a mascot's turn is a bone rotation and nothing
1885
+ else, which is precisely the move this page is not about. ⇒ **If the art you were
1886
+ handed has no landmark to foreshorten, a turn will not read no matter how the
1887
+ mesh is built**, and that is worth saying to the user before you build it.
1888
+
1889
+ ---
1890
+
1891
+ ## 11. Non-goals — stated, so nobody proposes them as gaps
1892
+
1893
+ 🚫 **No command generates a turn, and neither model construct is one.**
1894
+ §1 is one line of arithmetic; a `rigc yaw --degrees 12` would be guessing at
1895
+ every depth in §2 on the user's behalf, and depth is the parameter the *author*
1896
+ is choosing. Both constructs are the other thing — **a way to say the model in
1897
+ the spec** (§1.1 for the mesh, §3.1 for the bones) — so the radius, the angle and
1898
+ **every depth** arrive from the author and the compiler only evaluates. Neither one generates an in-between
1899
+ either: a model is evaluated at one key, and sweeping an angle is editing one
1900
+ number per key. What the toolchain owes is that the file is checkable,
1901
+ that you can look, and that a person can choose.
1902
+
1903
+ 🚫 **No pass bar for a face, and nothing here to hang one on.** MOTION.md's
1904
+ banner applies unchanged: `build` says a file is valid, `render` and `preview`
1905
+ let you look, `vote` lets a person choose. The angles in §8 are where a
1906
+ construction **stopped reading for one viewer looking at one drawing**, not
1907
+ thresholds.
1908
+
1909
+ 🚫 **No claim that 12° is the right angle for any request.** It is the angle the
1910
+ worked example ships, chosen to sit comfortably inside a 5-column grid's
1911
+ 17.65° tangent limit. §4.2 is how to pick your own, and picking it **first** is
1912
+ the entire point of that section.
1913
+
1914
+ ⚠️ **Not a Live2D comparison, and not a recommendation between formats.** What
1915
+ the worked example measured is that a portrait turn is authorable on plain Spine
1916
+ 4.3 at draft quality — nothing outside the format, no plugin, no runtime patch —
1917
+ and that the **split is authoring cost rather than runtime capability**. Neither
1918
+ the deform table nor the track table is transcribed (§1.1, §3.1), so the
1919
+ remaining cost is not on the keyboard but on the **parts**: per-eye meshes, a meshed neck, a
1920
+ second art layer for the far cheek (§8). Whether to pay *that* is a project's
1921
+ decision and this page does not make it.
1922
+
1923
+ 🚫 **No Live2D file is read or written, and none ever will be — a boundary
1924
+ rather than an unbuilt feature, and it runs in both directions.** rigc's inputs
1925
+ are a rig spec and a motion spec; its outputs are Spine 4.3 skeleton data and an
1926
+ atlas. There is no importer, no exporter and no converter for `.moc3`, `.cmo3`,
1927
+ `.model3.json` or anything else in that family, and nothing in this repository
1928
+ claims compatibility with that format in either direction.
1929
+ ⭐ **What is in scope is an authoring idea, stated on its own terms rather than
1930
+ as anybody's feature: that a face angle can be a value rather than a time.**
1931
+ §8's *The turn as a value rather than a time* is that idea on Spine's own
1932
+ `slider` constraint, and every mechanism under it is Spine's — the arithmetic,
1933
+ the flags, the readers and the failure modes are all in AUTHORING §3.5.2 and all
1934
+ measured against `spine-core`. ⚠️ Nothing on this page is a statement about how
1935
+ any other tool works inside, and nothing above implies one: what this repository
1936
+ has measured is its own format.
1937
+
1938
+ 🚫 **No per-eye mesh recipe.** §8 says the eyes need their own deform meshes past
1939
+ about 26°, and nobody has built that here. The column-placement arithmetic in
1940
+ §4.2 applies to any grid over any curved patch, so the tangent limit is the part
1941
+ that carries over; **what a lash line and a wrapping lid need is unmeasured, and
1942
+ this page does not guess.**
1943
+
1944
+ 🚫 **No expression system, no visemes, no phoneme mapping.** A face that *acts* is
1945
+ a different document and a different measurement. Everything here is one head
1946
+ turning, blinking and looking — and the one general lesson that might carry into
1947
+ that work is §7's: **allocate the channels before the first key, because Spine
1948
+ blends and does not add.**