rig-c 0.0.0-stage → 2.21.0

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 +1191 -0
  185. package/src/meshquality.ts +2051 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1444 -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
@@ -0,0 +1,837 @@
1
+ /**
2
+ * The 4.3 animation timeline catalogue, as data plus one walker.
3
+ *
4
+ * This lives on its own because two very different consumers need the same
5
+ * enumeration and neither may drift from the other: `validate.ts` walks it to
6
+ * check curve arrays (A05) and two-colour timelines (A12), and `diff.ts` walks
7
+ * it to count what a rig actually keys. When it was inlined in the validator,
8
+ * "which groups exist" was stated in one place and the comparison tool would
9
+ * have had to restate it — and a second copy of a catalogue is a second copy
10
+ * that goes stale silently.
11
+ *
12
+ * `PHYSICS_POSE_RULES` at the bottom is here for that reason and no other: the
13
+ * compiler refuses an out-of-range physics value a spec states, `A23` names one
14
+ * in a file rigc did not write, and the two have to be the same criterion rather
15
+ * than two readings of one (issue #610).
16
+ *
17
+ * Pure JSON reading. No spine-core, no filesystem. The line numbers cited are
18
+ * into `SkeletonJson.ts` on branch 4.3; the field-by-field survey is in
19
+ * `docs/SPEC_COVERAGE.md` part 1-8.
20
+ */
21
+
22
+ type Json = Record<string, unknown>;
23
+
24
+ function isObj(v: unknown): v is Json {
25
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
26
+ }
27
+
28
+ /**
29
+ * How many value channels each timeline carries. A curve array holds exactly
30
+ * four numbers PER channel; anything shorter
31
+ * multiplies `undefined` into the cubic and produces a NaN curve with no error
32
+ * (case 6g). `null` means "this timeline takes no curve at all".
33
+ */
34
+ const BONE_CHANNELS: Record<string, number | null> = {
35
+ rotate: 1,
36
+ translate: 2,
37
+ translatex: 1,
38
+ translatey: 1,
39
+ scale: 2,
40
+ scalex: 1,
41
+ scaley: 1,
42
+ shear: 2,
43
+ shearx: 1,
44
+ sheary: 1,
45
+ inherit: null,
46
+ };
47
+ const SLOT_CHANNELS: Record<string, number | null> = {
48
+ attachment: null,
49
+ rgba: 4,
50
+ rgb: 3,
51
+ alpha: 1,
52
+ rgba2: 7,
53
+ rgb2: 6,
54
+ };
55
+ const ATTACHMENT_CHANNELS: Record<string, number | null> = {
56
+ deform: 1,
57
+ sequence: null,
58
+ };
59
+ const PHYSICS_CHANNELS: Record<string, number | null> = {
60
+ inertia: 1,
61
+ strength: 1,
62
+ damping: 1,
63
+ mass: 1,
64
+ wind: 1,
65
+ gravity: 1,
66
+ mix: 1,
67
+ reset: null,
68
+ };
69
+ const PATH_CHANNELS: Record<string, number | null> = {
70
+ position: 1,
71
+ spacing: 1,
72
+ mix: 3,
73
+ };
74
+ const SLIDER_CHANNELS: Record<string, number | null> = {
75
+ time: 1,
76
+ mix: 1,
77
+ };
78
+ /**
79
+ * `ik` and `transform` are ONE timeline per constraint with no sub-name — the
80
+ * group maps a constraint name straight to a key array. There is no name in the
81
+ * file to look up, so the walker passes the group's own name and these tables
82
+ * hold that single entry.
83
+ */
84
+ const IK_CHANNELS: Record<string, number | null> = { ik: 2 };
85
+ const TRANSFORM_CHANNELS: Record<string, number | null> = { transform: 6 };
86
+ /** Whole-animation timelines. None of the three can carry a curve at all. */
87
+ const DRAW_ORDER_CHANNELS: Record<string, number | null> = { drawOrder: null };
88
+ const DRAW_ORDER_FOLDER_CHANNELS: Record<string, number | null> = { drawOrderFolder: null };
89
+ const EVENT_CHANNELS: Record<string, number | null> = { events: null };
90
+
91
+ /**
92
+ * How far past an animation's declared `duration` a key time may land: one step
93
+ * of the **float32** grid at that duration, which is the grid a key time is
94
+ * both emitted on and stored on.
95
+ *
96
+ * `spine-core` reads every timeline's frames into a `Float32Array`, and since
97
+ * issue #716 the compiler writes every number as its float32's shortest name,
98
+ * so the two grids are one. A key placed exactly ON a duration the float cannot
99
+ * hold is stored at most half a step from it — later, for a time like `0.2`
100
+ * whose own text names a float; never later for a time off that grid, which
101
+ * `keyTime` steps down (issue #99). What still needs the tolerance is the first
102
+ * case, and every artifact no rigc compile touched. Anything a whole step past a
103
+ * declared duration was authored there, not rounded onto it.
104
+ *
105
+ * ⭐ **A function and not a constant, because the grid is relative.** The fixed
106
+ * `KEY_TIME_EPSILON = 1e-6` that stood here was one step of `r6`'s six-decimal
107
+ * grid, and `A09` added this function to it for the float the file is read back
108
+ * into. With `r6` retired the first term had nothing left to be a step of, so it
109
+ * retired with it: a float32 step is 4.8e-7 s at 5 s and 3.8e-6 s at 32 s, and a
110
+ * flat epsilon would fail correct data for being long — 972 frames at 30 fps
111
+ * keyed exactly on their own declared duration are stored 1.5e-6 s late.
112
+ *
113
+ * ⚠️ `FRAME` (1/60 s) is the wrong tolerance for this, which is why the function
114
+ * is separate rather than reused: 1/60 s answers "is the DECLARED DURATION
115
+ * wrong?", it is some 35,000 times wider than this at 5 s, and it hid the
116
+ * defect that put this here. Rung 6 rounded key times to 4 dp in its authoring
117
+ * tooling, so a one-frame attachment reveal landed 3.3e-5 s past a 68/12 s
118
+ * duration — about 70 steps past this line but 1/500 of FRAME, with another
119
+ * track already sitting on the declared duration, so the compiler's Rule 4 and
120
+ * the validator's A09 both compared the animation's max key time and agreed.
121
+ * The reveal never fired (issue #54).
122
+ *
123
+ * The spacing of a normal float is 2^(exponent − 23), and `Math.log2` recovers
124
+ * the exponent. Zero takes the guard — a named empty animation declares
125
+ * `duration: 0` and A09 does compare it — and the magnitude is taken first, so a
126
+ * sign never reaches `log2`.
127
+ *
128
+ * It lives here, beside the timeline catalogue, for the reason the catalogue
129
+ * does: `compile.ts` refuses on it and `validate.ts` re-checks the emitted file
130
+ * against it, and a second copy is a second copy that drifts.
131
+ */
132
+ export function float32Step(t: number): number {
133
+ const magnitude = Math.abs(t);
134
+ if (!Number.isFinite(magnitude) || magnitude === 0) return 0;
135
+ return 2 ** (Math.floor(Math.log2(magnitude)) - 23);
136
+ }
137
+
138
+ /**
139
+ * Every timeline group `readAnimation` reads (SPEC_COVERAGE part 1-8), keyed by
140
+ * the walker's `kind`. A05 selects its channel table from here, so a group that
141
+ * is missing from this map is a group whose curves nobody checks.
142
+ */
143
+ export const CHANNELS_BY_KIND: Record<TimelineKind, Record<string, number | null>> = {
144
+ bone: BONE_CHANNELS,
145
+ slot: SLOT_CHANNELS,
146
+ ik: IK_CHANNELS,
147
+ transform: TRANSFORM_CHANNELS,
148
+ path: PATH_CHANNELS,
149
+ physics: PHYSICS_CHANNELS,
150
+ slider: SLIDER_CHANNELS,
151
+ attachment: ATTACHMENT_CHANNELS,
152
+ drawOrder: DRAW_ORDER_CHANNELS,
153
+ drawOrderFolder: DRAW_ORDER_FOLDER_CHANNELS,
154
+ event: EVENT_CHANNELS,
155
+ };
156
+
157
+ /** The eleven timeline groups one animation can hold in 4.3. */
158
+ export type TimelineKind =
159
+ | 'bone'
160
+ | 'slot'
161
+ | 'ik'
162
+ | 'transform'
163
+ | 'path'
164
+ | 'physics'
165
+ | 'slider'
166
+ | 'attachment'
167
+ | 'drawOrder'
168
+ | 'drawOrderFolder'
169
+ | 'event';
170
+
171
+
172
+ /**
173
+ * Walks every timeline group `readAnimation` reads and hands each timeline to
174
+ * the visitor.
175
+ *
176
+ * ⚠️ This function is the reach of A05 and A12, so a group it does not descend
177
+ * is a group whose curve arrays nobody checks — and a short curve array is the
178
+ * format's nastiest silent failure (`curve[i+3]` is
179
+ * `undefined`, the cubic yields NaN, nothing throws). It used to descend
180
+ * `bones`, `slots`, `physics` and `attachments` only, which left `ik`,
181
+ * `transform`, `path`, `slider`, `drawOrder`, `drawOrderFolder` and `events`
182
+ * completely unexamined. The comment that stood here claimed drawOrder and
183
+ * events were "skipped by design" because they carry no curves — but "carries
184
+ * no curve" is precisely a rule that has to be CHECKED, and the parser ignores
185
+ * a stray `curve` key on those timelines rather than rejecting it.
186
+ *
187
+ * The groups come in four shapes, and the shape is the whole reason this is not
188
+ * one loop (SPEC_COVERAGE part 1-8):
189
+ *
190
+ * group.<target>.<timeline> = keys[] bones, slots, path, physics, slider
191
+ * group.<target> = keys[] ik, transform — one unnamed timeline
192
+ * group = keys[] drawOrder, events — one per animation
193
+ * group = folders[] with .keys[] drawOrderFolder
194
+ *
195
+ * plus `attachments.<skin>.<slot>.<attachment>.<timeline>`. For the shapes with
196
+ * no timeline name in the file, the walker passes the group's own name so that
197
+ * A05's table lookup and its "unchecked timeline" fail-closed branch both keep
198
+ * working unchanged.
199
+ */
200
+ export function walkTimelines(
201
+ raw: Json | null,
202
+ visit: (path: string, kind: TimelineKind, name: string, keys: unknown[]) => void,
203
+ ): void {
204
+ if (!raw || !isObj(raw.animations)) return;
205
+ for (const [animName, anim] of Object.entries(raw.animations as Json)) {
206
+ if (!isObj(anim)) continue;
207
+
208
+ // group.<target>.<timeline> = keys[]
209
+ for (const [group, kind] of [
210
+ ['bones', 'bone'],
211
+ ['slots', 'slot'],
212
+ ['path', 'path'],
213
+ ['physics', 'physics'],
214
+ ['slider', 'slider'],
215
+ ] as const) {
216
+ if (!isObj(anim[group])) continue;
217
+ for (const [targetName, timelines] of Object.entries(anim[group] as Json)) {
218
+ if (!isObj(timelines)) continue;
219
+ for (const [timelineName, keys] of Object.entries(timelines)) {
220
+ if (!Array.isArray(keys)) continue;
221
+ visit(`${animName}.${group}.${targetName}.${timelineName}`, kind, timelineName, keys);
222
+ }
223
+ }
224
+ }
225
+
226
+ // group.<constraint> = keys[] — the constraint IS the timeline
227
+ for (const [group, kind] of [
228
+ ['ik', 'ik'],
229
+ ['transform', 'transform'],
230
+ ] as const) {
231
+ if (!isObj(anim[group])) continue;
232
+ for (const [targetName, keys] of Object.entries(anim[group] as Json)) {
233
+ if (!Array.isArray(keys)) continue;
234
+ visit(`${animName}.${group}.${targetName}`, kind, group, keys);
235
+ }
236
+ }
237
+
238
+ // group = keys[] — one timeline for the whole animation
239
+ for (const [group, kind] of [
240
+ ['drawOrder', 'drawOrder'],
241
+ ['events', 'event'],
242
+ ] as const) {
243
+ const keys = anim[group];
244
+ if (!Array.isArray(keys)) continue;
245
+ visit(`${animName}.${group}`, kind, group, keys);
246
+ }
247
+
248
+ // drawOrderFolder = [ { slots: [...], keys: [...] } ] — one timeline per folder
249
+ if (Array.isArray(anim.drawOrderFolder)) {
250
+ (anim.drawOrderFolder as unknown[]).forEach((folder, i) => {
251
+ if (!isObj(folder) || !Array.isArray(folder.keys)) return;
252
+ visit(`${animName}.drawOrderFolder[${i}]`, 'drawOrderFolder', 'drawOrderFolder', folder.keys);
253
+ });
254
+ }
255
+
256
+ // attachments.<skin>.<slot>.<attachment>.<timeline>
257
+ if (isObj(anim.attachments)) {
258
+ for (const [skinName, skinMap] of Object.entries(anim.attachments as Json)) {
259
+ if (!isObj(skinMap)) continue;
260
+ for (const [slotName, slotMap] of Object.entries(skinMap)) {
261
+ if (!isObj(slotMap)) continue;
262
+ for (const [attName, attMap] of Object.entries(slotMap)) {
263
+ if (!isObj(attMap)) continue;
264
+ for (const [timelineName, keys] of Object.entries(attMap)) {
265
+ if (!Array.isArray(keys)) continue;
266
+ visit(
267
+ `${animName}.attachments.${skinName}.${slotName}.${attName}.${timelineName}`,
268
+ 'attachment',
269
+ timelineName,
270
+ keys,
271
+ );
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+
281
+ /**
282
+ * A physics constraint's pose, as the four fields `A23` judges.
283
+ *
284
+ * Structural rather than spine-core's `PhysicsConstraintPose`, because this
285
+ * module links no runtime (see the header) — the runtime's class satisfies it,
286
+ * and so does the probe `validate.ts` hands the runtime's own timeline `set` to
287
+ * fill.
288
+ */
289
+ export interface PhysicsJudgedPose {
290
+ mix: number;
291
+ massInverse: number;
292
+ strength: number;
293
+ damping: number;
294
+ }
295
+
296
+ /** One way out of a physics bound, and what the runtime does with a value that takes it. */
297
+ export interface PhysicsOutsideArm {
298
+ /** True for the pose values this arm is about — every one of them outside the bound. */
299
+ when: (poseValue: number) => boolean;
300
+ /** What the runtime does with such a value, in the words both of `A23`'s arms print. */
301
+ says: string;
302
+ }
303
+
304
+ /**
305
+ * A value a basis arm is measured at: a stated number (a key's or the tuning
306
+ * table's, before `toPose`) and the constraint `fps` it is stepped at, where the
307
+ * arm's claim depends on the rate. Absent `fps` is the constraint's own rate.
308
+ */
309
+ export interface PhysicsBasisWitness {
310
+ value: number;
311
+ fps?: number;
312
+ }
313
+
314
+ /**
315
+ * Why one way out of a physics bound is refused, in the one of two kinds it is
316
+ * (issue #798).
317
+ *
318
+ * - **arithmetic** — the runtime cannot compute the value: an expression in the
319
+ * integrator is non-finite at it, whatever the rest of the rig does.
320
+ * `expression` says which and `lines` cites where.
321
+ * - **behavioural** — the runtime computes it, finitely, and the rig runs
322
+ * wrongly: a run-away, an inverted jiggle, a constraint doing nothing.
323
+ * Refusing it is rigc's call, and the sentence says so.
324
+ *
325
+ * ⚠️ The line between them is **non-finite within the walk that measures it**,
326
+ * never "non-finite eventually". [measured] through spine-core on the generated
327
+ * physics fixture, stepped from `Physics.reset` at 60 fps under a displacing
328
+ * animation: a setup `damping` of 2 is finite for 1,064 steps and its velocity
329
+ * is Infinity at step 1,065, and 1.0001 is finite over 4,000; a `mass` of −1
330
+ * takes the offset to 2.7e6 in 120 steps and is finite on every one. Every
331
+ * run-away overflows at SOME horizon, so "eventually" would call each of them
332
+ * arithmetic and the distinction would say nothing. The arithmetic arms are
333
+ * non-finite from the first or second step: `mass` 0 at step 1, a `damping` of
334
+ * −0.5 at 45 fps at step 2.
335
+ *
336
+ * `witness` is the value `T113` steps through the runtime to hold `kind`
337
+ * against it: an arithmetic arm has to go non-finite within its 120 steps, a
338
+ * behavioural one has to stay finite for all of them.
339
+ */
340
+ export type PhysicsBoundBasis =
341
+ | {
342
+ kind: 'arithmetic';
343
+ /** True for the pose values this arm is about — every one of them outside the bound. */
344
+ when: (poseValue: number) => boolean;
345
+ witness: PhysicsBasisWitness;
346
+ /** The expression that is non-finite at the value, and what that does to the pose. */
347
+ expression: string;
348
+ /** Where the runtime computes it, in spine-core 4.3.13's `dist`. */
349
+ lines: string;
350
+ }
351
+ | {
352
+ kind: 'behavioural';
353
+ /** True for the pose values this arm is about — every one of them outside the bound. */
354
+ when: (poseValue: number) => boolean;
355
+ witness: PhysicsBasisWitness;
356
+ /** What the runtime does with the value, finitely — the reason the rig is wrong. */
357
+ does: string;
358
+ };
359
+
360
+ /**
361
+ * One arm's reason, in the words every sentence that refuses a value by it
362
+ * prints: an arithmetic arm names its expression and lines, a behavioural one
363
+ * says that refusing it is rigc's call and then what the value does.
364
+ *
365
+ * ⚠️ The behavioural reason ENDS on `does`, so a sentence built on it ends on
366
+ * what the value does — `T100` holds `strength`'s setup sentences to ending on
367
+ * the row's own `outside` arm, and this order is what keeps that true.
368
+ */
369
+ export function physicsBasisSays(arm: PhysicsBoundBasis): string {
370
+ return arm.kind === 'arithmetic'
371
+ ? `${arm.expression} (\`${arm.lines}\`)`
372
+ : `the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: ${arm.does}`;
373
+ }
374
+
375
+ /** The arm of a row's `basis` that holds for a pose value, or `undefined` where none does. */
376
+ export function physicsBasisFor(rule: PhysicsPoseRule, poseValue: number): PhysicsBoundBasis | undefined {
377
+ return rule.basis.find((arm) => arm.when(poseValue));
378
+ }
379
+
380
+ /**
381
+ * One physics property `A23` has an opinion about, stated once for the two
382
+ * layers that hold it.
383
+ *
384
+ * ⚠️ **The keyed number and the pose field are not always the same number.**
385
+ * `mass` is the one: `PhysicsConstraintMassTimeline.set` is
386
+ * `pose.massInverse = 1 / value` (`Animation.js:2132-2145`) and the parser does
387
+ * the same to a constraint's own `mass` (`SkeletonJson.js:309`), so a key states
388
+ * a mass and the integrator reads its reciprocal. `toPose` IS that transform, and
389
+ * every predicate here is written against the pose field rather than against the
390
+ * keyed number — which is what makes "the compiler and the assertion apply the
391
+ * same criterion" a property of the code and not a claim about it.
392
+ */
393
+ export interface PhysicsPoseRule {
394
+ /** The timeline name in skeleton JSON, and the motion spec's `property`. */
395
+ timeline: string;
396
+ /** The pose field the integrator reads. */
397
+ field: keyof PhysicsJudgedPose;
398
+ /** The pose field, from the number a key or the rig's tuning table states. */
399
+ toPose: (value: number) => number;
400
+ /** True when the integrator can use that pose field. */
401
+ poseOk: (poseValue: number) => boolean;
402
+ /**
403
+ * The same question asked of a KEY, where it differs — `null` means it does
404
+ * not. **Two rows have one, and in both the widening is to 0 exactly**, for
405
+ * the same reason: a setup pose states what a constraint IS and a key states
406
+ * what it is doing for a stretch, so a value that makes a constraint
407
+ * permanently useless can be a deliberate span inside an animation.
408
+ *
409
+ * - `mix`: `update` opens with `if (mix === 0) return;`
410
+ * (`PhysicsConstraint.js:109-111`) and `PhysicsConstraintPose` documents the
411
+ * field as "a percentage (0+)", so a mix of exactly 0 is a state the runtime
412
+ * has a branch for. Measured, not assumed: the editor's own `sack-pro`
413
+ * example keys mix to 0 on 24 of its 36 mix keys, and applying the setup
414
+ * rule to keys would refuse all 24 (issue #610).
415
+ * - `strength`: 0 takes the restoring term out of the velocity update and
416
+ * leaves `damping` and `inertia` applied, which is a released span rather
417
+ * than a broken constraint. Measured through spine-core on the generated
418
+ * overlay fixture, keying 0 for a span and restoring it (issue #727): no
419
+ * NaN; with no wind or gravity the offset coasts to a LIMIT rather than
420
+ * running away — 0.5 s of it and 2.0 s of it end 0.95 % apart — and the
421
+ * restoring key takes the offset from 5.5063 back under 0.01 in 54 steps at
422
+ * 60 fps. With `gravity -40` acting, the offset travels at
423
+ * terminal velocity while the key holds (178 units over 0.5 s, 843 over
424
+ * 2.0 s) and the restoring key still pulls it back to the never-keyed run's
425
+ * own equilibrium — 39.999969 against 40.000000 — in 13 steps. What that
426
+ * measurement rules out is the thing a key cannot undo, and the neighbour
427
+ * that HAS one is the contrast: a keyed `mass` of 0 is NaN from the first
428
+ * sub-step and still NaN after the restoring key, so it stays refused.
429
+ */
430
+ keyOk: ((poseValue: number) => boolean) | null;
431
+ /**
432
+ * True where a SETUP value this bound refuses leaves the constraint doing
433
+ * **nothing**, rather than doing something wrong — so an animation keying a
434
+ * value the bound accepts makes it effective, and a rig resting outside the
435
+ * bound is off rather than broken.
436
+ *
437
+ * Only `mix` is, and it is measured rather than argued (issue #743). At rest —
438
+ * setup pose, no animation applied, 36 steps at 60 fps — a constraint resting
439
+ * at `mix` 0 leaves its bone at worldX 12.0000 on every frame, while one
440
+ * resting at `mass` 0 reads NaN on every frame *although an animation keys
441
+ * `mass` to 1*, because `massInverse` is already Infinity before anything
442
+ * plays. Measured with the fixture's gravity at 0 and again at −40: the second
443
+ * rig is NaN either way, since `m = t * massInverse` is Infinity and the force
444
+ * it multiplies need not be non-zero for the product to be NaN.
445
+ *
446
+ * The same asymmetry is in the runtime's own text: `update` opens with
447
+ * `if (mix === 0) return;` (`PhysicsConstraint.js:109-111`) and has no such
448
+ * branch for the other three.
449
+ *
450
+ * ⚠️ This is NOT `keyOk !== null`, although today both are `mix` alone. That
451
+ * one says what a KEY may hold; this says whether a key can rescue the SETUP
452
+ * value — two questions with one answer here and no reason to share a field.
453
+ */
454
+ inertAtSetup: boolean;
455
+ /**
456
+ * What the runtime does with a value outside the bound, one arm per way out,
457
+ * where the ways out do different things — or `null` where one sentence covers
458
+ * them all. `A23`'s SETUP sentence reads the arm that holds for the value, and
459
+ * the row's `why` — the KEY's sentence — is written from the same arms, so the
460
+ * two cannot say different things about one number (issue #748).
461
+ *
462
+ * Only `strength` has two, and they are measured rather than argued: resting
463
+ * at 0 the offset is only the bone's own lag, since nothing restores it,
464
+ * while resting below 0 the restoring term is added instead of taken out and
465
+ * the offset runs away — on the generated physics fixture, stepped at 60 fps
466
+ * from `Physics.reset`, a setup `strength` of −100 grew the offset 28.35× over
467
+ * 0.5 s with no sign change. The single sentence this replaced said "nothing
468
+ * pulls it back" of both, which sends an author reading the negative one to
469
+ * the wrong fix.
470
+ */
471
+ outside: readonly PhysicsOutsideArm[] | null;
472
+ /**
473
+ * Why each way out of the bound is refused — one arm per way out, arithmetic
474
+ * or behavioural, disjoint and together covering every pose value `poseOk`
475
+ * refuses (issue #798). `A23`'s setup sentence prints the arm that holds for
476
+ * the value, and the row's `why` — the key's sentence — opens with the arms a
477
+ * key can still take. Until this field existed `src/types.ts` said of all four
478
+ * rows that "the bounds are the runtime's, not a policy", which is true of two
479
+ * of the eight ways out.
480
+ */
481
+ basis: readonly PhysicsBoundBasis[];
482
+ /** The bound in words, for a message: what the value has to be. */
483
+ states: string;
484
+ /** The bound a KEY is held to, where `keyOk` widens it. */
485
+ statesKeyed: string;
486
+ /**
487
+ * What the runtime does outside the bound, with the lines that say so. It
488
+ * OPENS with the basis of the ways out a key can take — the arithmetic
489
+ * arm's expression, or the behavioural arm's "refusing it is rigc's call"
490
+ * and what the value does (issue #798) — quoted off the same `basis` objects
491
+ * the setup sentence prints, so the two cannot say different things.
492
+ *
493
+ * ⚠️ It is the sentence a **key** is refused with — `physicsKeyRefusal` is
494
+ * this field's only reader, and the setup pose's own wording lives beside
495
+ * `A23` in `validate.ts`. So a row whose `keyOk` widens the bound states here
496
+ * what is wrong with the values a key can still be refused for, not what is
497
+ * wrong with the value the widening admitted (issue #727).
498
+ */
499
+ why: string;
500
+ }
501
+
502
+ /**
503
+ * `strength`'s two ways out of its bound — two different rigs, so two sentences
504
+ * (issue #748). Resting at 0 there is no restoring force and the offset is only
505
+ * the bone's own lag; below 0 the restoring term has the wrong sign, so the
506
+ * offset feeds its own velocity and runs away. The row's `outside` is this array
507
+ * and its `why` quotes the first arm, which is the one a key can still take.
508
+ */
509
+ const STRENGTH_OUTSIDE: readonly PhysicsOutsideArm[] = [
510
+ {
511
+ when: (v) => v < 0,
512
+ says:
513
+ 'below 0 the restoring term is ADDED to the offset instead of taken out of it, so the offset is pushed ' +
514
+ 'away and grows with every step',
515
+ },
516
+ { when: (v) => v === 0, says: 'nothing pulls it back' },
517
+ ];
518
+
519
+ /**
520
+ * The eight ways out of the four bounds, each with its basis (issue #798). They
521
+ * are named constants rather than literals inside the rows because each row's
522
+ * `why` quotes its own — the key's sentence and `A23`'s setup sentence are then
523
+ * one text about one number, as `STRENGTH_OUTSIDE` already made them for two.
524
+ *
525
+ * 📏 Every witness below was stepped through spine-core 4.3.13 on the generated
526
+ * physics fixture — `x` and `y` driven, 120 steps from `Physics.reset` at 60 fps
527
+ * under an animation that swings the constraint's bone and brings it back — as
528
+ * the constraint's SETUP value: `mass` 0 is NaN at step 1 (`xVelocity`, and the
529
+ * bone's `worldX` with it); `mass` −1 is finite on every step and runs the
530
+ * offset away to 2.7e6; `strength` 0 and −5 are finite; `mix` 0 poses the bone
531
+ * exactly where the rig with no constraint does, on every step; `mix` −0.5 is
532
+ * finite and moves the bone by exactly −1× what +0.5 moves it by; `damping` 2 is
533
+ * finite over the walk (Infinity only at step 1,065); `damping` −0.5 at 45 fps is
534
+ * NaN at step 2. `T113` re-takes all eight on every run.
535
+ */
536
+ const MIX_BELOW_0: PhysicsBoundBasis = {
537
+ kind: 'behavioural',
538
+ when: (v) => v < 0,
539
+ witness: { value: -0.5 },
540
+ does:
541
+ 'below 0 the jiggle is applied inverted, on `x` and `y` exactly the offset a positive mix of the same size ' +
542
+ 'applies, mirrored (`PhysicsConstraint.js:172,174`), and `PhysicsConstraintPose` documents mix as ' +
543
+ '"a percentage (0+)" where a transform constraint documents its own as "unbounded"',
544
+ };
545
+ const MIX_AT_0: PhysicsBoundBasis = {
546
+ kind: 'behavioural',
547
+ when: (v) => v === 0,
548
+ witness: { value: 0 },
549
+ does: 'at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted',
550
+ };
551
+ const MASS_AT_0: PhysicsBoundBasis = {
552
+ kind: 'arithmetic',
553
+ // The pose holds 1 / mass, so a mass of 0 arrives as Infinity (−0 as −Infinity).
554
+ when: (v) => !Number.isFinite(v),
555
+ witness: { value: 0 },
556
+ expression:
557
+ 'at 0 `massInverse = 1 / mass` is Infinity and `m = t * massInverse` multiplies every velocity update, so the ' +
558
+ 'first step takes every velocity and offset to NaN',
559
+ lines: 'SkeletonJson.js:309, Animation.js:2140, PhysicsConstraint.js:149,156,211',
560
+ };
561
+ const MASS_BELOW_0: PhysicsBoundBasis = {
562
+ kind: 'behavioural',
563
+ when: (v) => Number.isFinite(v) && v < 0,
564
+ witness: { value: -1 },
565
+ does:
566
+ 'below 0 `massInverse` is a finite negative, which flips the sign of every force the velocity update applies, ' +
567
+ 'so the restoring force pushes the offset away and it grows with every step',
568
+ };
569
+ const STRENGTH_BELOW_0: PhysicsBoundBasis = {
570
+ kind: 'behavioural',
571
+ when: STRENGTH_OUTSIDE[0].when,
572
+ witness: { value: -5 },
573
+ does: STRENGTH_OUTSIDE[0].says,
574
+ };
575
+ const STRENGTH_AT_0: PhysicsBoundBasis = {
576
+ kind: 'behavioural',
577
+ when: STRENGTH_OUTSIDE[1].when,
578
+ witness: { value: 0 },
579
+ does: STRENGTH_OUTSIDE[1].says,
580
+ };
581
+ const DAMPING_ABOVE_1: PhysicsBoundBasis = {
582
+ kind: 'behavioural',
583
+ when: (v) => v > 1,
584
+ witness: { value: 2 },
585
+ does: 'above 1 every velocity grows on every step and the offset diverges',
586
+ };
587
+ const DAMPING_BELOW_0: PhysicsBoundBasis = {
588
+ kind: 'arithmetic',
589
+ when: (v) => v < 0,
590
+ // 45 rather than 60: at 60 the exponent is exactly 1 and a negative base is
591
+ // finite, which is the whole of #748.
592
+ witness: { value: -0.5, fps: 45 },
593
+ expression: 'at any fps where `60 / fps` is not whole it is a negative number raised to a fractional power, which is NaN',
594
+ lines: 'PhysicsConstraint.js:148,210',
595
+ };
596
+
597
+ /**
598
+ * Every physics property with a bound, and **only** those — each bound either
599
+ * where the runtime's arithmetic fails or where the value runs and runs wrongly,
600
+ * and each row's `basis` says which.
601
+ *
602
+ * 🚫 `inertia`, `wind` and `gravity` are absent on purpose. The runtime
603
+ * documents no range for any of them and the integrator diverges on none:
604
+ * `inertia` scales how much bone movement is converted (`PhysicsConstraint.js:137,143`)
605
+ * so 0 is an inert frame and nothing worse, and `wind`/`gravity` are forces along
606
+ * the skeleton's own vectors (`:151-153, :214-215`) where a negative number is the
607
+ * other direction — the corpus keys `wind` at −27.4 through −12.6 on all 48 of its
608
+ * wind keys. Inventing a bound for them would refuse correct data, which is the
609
+ * failure this repository has already paid for twice (issues #44, #262).
610
+ *
611
+ * 🚫 There is no UPPER bound on `mix` either, for the same reason and a stronger
612
+ * one: `PhysicsConstraintPose` documents it as "a percentage (0+)", and on `x`
613
+ * and `y` a keyed mix of 1.5 changes nothing inside the integration at all — it
614
+ * multiplies the finished offset onto the bone (`:172,174,251,253`), so it is an
615
+ * over-mix and an over-mix is a real idiom (the same argument
616
+ * `CONSTRAINT_TIMELINES` makes for a transform mix).
617
+ *
618
+ * ⚠️ On `rotate` and `shearX` that is not the whole of it, and the sentence this
619
+ * replaced said it was (issue #798): `mr = (rotate + shearX) * mix` (`:188`) is
620
+ * read INSIDE the rotation solve (`:190,192,230`), so there mix changes what is
621
+ * integrated as well as how much of it is applied. [measured] on the generated
622
+ * physics fixture with `rotate: 1` added, 120 steps from `Physics.reset` at
623
+ * 60 fps: a setup mix of −0.5 leaves `xOffset` equal to the mix-1 run's while
624
+ * `rotateOffset` peaks at 1.4151 against the mix-1 run's 1.7994. Finite either
625
+ * way, which is all the `mix` row's basis claims.
626
+ *
627
+ * 🔎 Each row's `basis` says, per way out, whether the runtime's arithmetic
628
+ * fails there or the value runs and rigc refuses what it does (issue #798). Of
629
+ * the eight ways out, two are arithmetic — `mass` at 0 and `damping` below 0 —
630
+ * and six are rigc's call.
631
+ */
632
+ export const PHYSICS_POSE_RULES: PhysicsPoseRule[] = [
633
+ {
634
+ timeline: 'mix',
635
+ field: 'mix',
636
+ toPose: (v) => v,
637
+ poseOk: (v) => v > 0,
638
+ keyOk: (v) => v >= 0,
639
+ inertAtSetup: true,
640
+ outside: null,
641
+ // 🔑 Both ways out are BEHAVIOURAL (issue #798), and the negative one is the
642
+ // question #798 asked: a transform constraint resting at a negative mix is
643
+ // accepted (`T105`) and a physics one is refused. [measured] the arithmetic
644
+ // is the same on both sides, a signed scale of what the constraint applies
645
+ // and finite on every step: on the generated physics fixture a setup mix of
646
+ // −0.5 moves the bone by exactly −1× what +0.5 moves it by, step for step,
647
+ // and a transform constraint at `mixRotate` −0.5 rotates its bone by exactly
648
+ // −1× what +0.5 does. So neither the refusal nor the acceptance is the
649
+ // runtime's arithmetic. What differs is the runtime's own DOCUMENTED range,
650
+ // and each rule follows its own: `PhysicsConstraintPose.mix` is "a
651
+ // percentage (0+)" and `TransformConstraintPose.mixRotate` is "a percentage
652
+ // (unbounded)". Neither is wrong; both are rigc's call, made on the
653
+ // runtime's text rather than on its arithmetic, and `T114` holds the
654
+ // mirror, the finiteness and both documented ranges against the runtime.
655
+ basis: [MIX_BELOW_0, MIX_AT_0],
656
+ states: '> 0',
657
+ statesKeyed: '>= 0',
658
+ // A key is refused below 0 only, so the key's sentence is that arm's.
659
+ why:
660
+ `${physicsBasisSays(MIX_BELOW_0)}. 0 is a key the runtime has a branch for: \`update\` returns immediately ` +
661
+ '(`PhysicsConstraint.js:109-111`), which mutes the constraint for the span',
662
+ },
663
+ {
664
+ timeline: 'mass',
665
+ field: 'massInverse',
666
+ toPose: (v) => 1 / v,
667
+ poseOk: (v) => Number.isFinite(v) && v > 0,
668
+ keyOk: null,
669
+ inertAtSetup: false,
670
+ outside: null,
671
+ // The one row with a way out of each kind: 0 is the runtime's arithmetic
672
+ // and below 0 is rigc's call, so the key's sentence says both, in that order.
673
+ basis: [MASS_AT_0, MASS_BELOW_0],
674
+ states: '> 0',
675
+ statesKeyed: '> 0',
676
+ why: `${physicsBasisSays(MASS_AT_0)}; ${physicsBasisSays(MASS_BELOW_0)}`,
677
+ },
678
+ {
679
+ timeline: 'strength',
680
+ field: 'strength',
681
+ toPose: (v) => v,
682
+ poseOk: (v) => v > 0,
683
+ keyOk: (v) => v >= 0,
684
+ inertAtSetup: false,
685
+ outside: STRENGTH_OUTSIDE,
686
+ basis: [STRENGTH_BELOW_0, STRENGTH_AT_0],
687
+ states: '> 0',
688
+ statesKeyed: '>= 0',
689
+ // The key's sentence names the arm a key can still take — below 0 — off the
690
+ // same object the setup sentence reads, so the two say one thing about it.
691
+ why:
692
+ `${physicsBasisSays(STRENGTH_BELOW_0)}. It is the restoring force, \`velocity += (a - offset * strength) * m\` ` +
693
+ '(`PhysicsConstraint.js:150,156,212,220`), and a multiplicand everywhere it is read. 0 is a key the runtime plays: ' +
694
+ 'it releases the constraint for the span, damping and inertia still apply, and the next key pulls the offset back',
695
+ },
696
+ {
697
+ timeline: 'damping',
698
+ field: 'damping',
699
+ toPose: (v) => v,
700
+ poseOk: (v) => v >= 0 && v <= 1,
701
+ keyOk: null,
702
+ inertAtSetup: false,
703
+ outside: null,
704
+ // Below 0 is arithmetic at every rate where the exponent is fractional;
705
+ // above 1 is a run-away, finite until the velocity overflows (step 1,065
706
+ // for a setup of 2 at 60 fps), so it is behavioural by the line
707
+ // `PhysicsBoundBasis` draws — the card that asked for the bases (#798)
708
+ // counted it arithmetic, and the walk says otherwise.
709
+ basis: [DAMPING_ABOVE_1, DAMPING_BELOW_0],
710
+ states: 'inside [0, 1]',
711
+ statesKeyed: 'inside [0, 1]',
712
+ // 🔑 The interval is CLOSED (issue #794). `1 ** x` is 1 and `0 ** x` is 0
713
+ // for every positive exponent, so both ends are finite at every `fps` and
714
+ // both are rigs somebody can mean: 1 holds the jiggle, 0 follows the bone
715
+ // with no overshoot. [measured] through spine-core on the generated physics
716
+ // fixture, 120 steps from `Physics.reset` at 60, 45 and 30 fps under a
717
+ // displacing animation, as a setup value and as a key: every pose value
718
+ // finite, the velocity never 0 at 1 and 0 after every step at 0. And 1 is
719
+ // not only a choice: it is 4.2's own parser default for an omitted
720
+ // `damping` (`SkeletonJson.js:242` in 4.2.120,
721
+ // `getValue(constraintMap, "damping", 1)`; 4.3.13 reads 0.85 at `:308`), so a
722
+ // rig migrated from 4.2 that keys "the default" writes 1 and the editor
723
+ // exports it as it is. The open interval this replaced refused that rig.
724
+ //
725
+ // ⚠️ The frame-rate half is why the bound cannot be checked by playing a rig
726
+ // at 60 fps (issue #748): `step` is `1 / fps`, the constraint's own rate, so
727
+ // the exponent is exactly 1 there and a negative damping only flips the
728
+ // velocity's sign. [measured] on the generated physics fixture, a keyed −0.5
729
+ // stays finite at 60 fps and at 30 (exponent 2), and is NaN within three
730
+ // steps of the key at 45 and at 120 (exponents 1.3333 and 0.5).
731
+ why:
732
+ 'the per-step decay is `damping ** (60 * step)`, with `step` = 1 / the constraint\'s `fps`, and every velocity ' +
733
+ `is multiplied by it (\`PhysicsConstraint.js:114,148,158,163,210,222,227\`), so ${DAMPING_ABOVE_1.does}, at ` +
734
+ "every rate — finite on every step until the velocity overflows, so refusing it is rigc's call rather than the " +
735
+ "runtime's. Below 0 the result depends on `fps`: where `60 / fps` is a " +
736
+ 'whole number a negative base stays finite — at 60 fps it is the velocity\'s sign flipped each step, which can ' +
737
+ `look like a jiggle settling — and ${DAMPING_BELOW_0.expression} (\`(-0.5) ** (60 / 45)\`), so a rig tried only at 60 fps never shows the failure. The two ends are ` +
738
+ 'values the runtime plays at every rate: at 1 the velocity never decays, so the jiggle holds for as long as it ' +
739
+ 'runs, and at 0 every velocity is zeroed on every step, so the offset follows the bone with no overshoot',
740
+ },
741
+ ];
742
+
743
+ /**
744
+ * What `A23`'s setup arm says about a pose value its rule refuses: the arm of
745
+ * `outside` that holds for it, or the bound itself where the row has no arms or
746
+ * none holds (a non-number the parser handed over, say).
747
+ */
748
+ export function physicsOutsideSays(rule: PhysicsPoseRule, poseValue: number): string {
749
+ return rule.outside?.find((arm) => arm.when(poseValue))?.says ?? `must be ${rule.states}`;
750
+ }
751
+
752
+ /** The rule for one timeline name, or `undefined` where the runtime bounds nothing. */
753
+ export function physicsRuleFor(timeline: string): PhysicsPoseRule | undefined {
754
+ return PHYSICS_POSE_RULES.find((rule) => rule.timeline === timeline);
755
+ }
756
+
757
+ /**
758
+ * Whether the number a KEY states is one the runtime can use, judged on the pose
759
+ * field it becomes rather than on itself.
760
+ *
761
+ * `null` when it is. The string is the tail of a message and names the bound and
762
+ * the reason, never just "invalid".
763
+ */
764
+ export function physicsKeyRefusal(rule: PhysicsPoseRule, value: number, posedBy?: number): string | null {
765
+ // ⚠️ `posedBy` exists so the VALIDATOR can hand over the number the runtime's
766
+ // own `PhysicsConstraint*Timeline.set` wrote, rather than rigc's reading of
767
+ // what that call does. The compiler cannot: it links no runtime, by the rule in
768
+ // CLAUDE.md, so it passes nothing and `toPose` answers. Those are two paths to
769
+ // one number and a selftest control measures that they agree — without it this
770
+ // parameter would be exactly the silent second opinion this table exists to
771
+ // remove.
772
+ const posed = posedBy ?? rule.toPose(value);
773
+ const ok = rule.keyOk ?? rule.poseOk;
774
+ if (ok(posed)) return null;
775
+ // The pose field only when it is a different number from the keyed one, which
776
+ // is derived rather than declared: it is exactly `mass`, and only where the
777
+ // reciprocal has moved.
778
+ const shown = posed === value ? '' : ` (${rule.field} ${posed})`;
779
+ return `${value}${shown}; must be ${rule.statesKeyed} — ${rule.why}`;
780
+ }
781
+
782
+ /** One of the three colours a slot poses: the light colour's rgb, its alpha, and the dark colour. */
783
+ export type SlotColorChannel = 'rgb' | 'alpha' | 'dark';
784
+
785
+ /**
786
+ * Which of a slot's colour channels each of the format's five colour timelines
787
+ * poses — the `propertyIds` each class registers in `Animation.js`
788
+ * (`Property.rgb`, `Property.alpha`, `Property.rgb2`), under the names an author
789
+ * reads them by.
790
+ *
791
+ * ⭐ **It is the whole of what makes `rgb` + `alpha` a different thing from
792
+ * `rgba`**, and the reason it is a table rather than a fact every reader knows:
793
+ * `RGBTimeline.apply1` writes `color.r/g/b` and never touches `color.a`,
794
+ * `AlphaTimeline.apply` writes `color.a` alone, and `RGB2Timeline` writes the
795
+ * light rgb and the dark colour and leaves the light alpha where it was. A
796
+ * timeline that poses a channel poses it at EVERY time — before its first key
797
+ * it writes the setup value (`MixFrom.setup`) — so two timelines of one slot
798
+ * that share a channel are not two layers of one colour: the one applied later,
799
+ * which is the one the file states later, overwrites the other everywhere, and
800
+ * the earlier one's keys on that channel are read by nothing.
801
+ *
802
+ * It lives here, beside the catalogue, for `PHYSICS_POSE_RULES`' reason:
803
+ * `compile.ts` refuses two tracks that share a channel, `A45` names the same
804
+ * pair on a file rigc did not write, and the two have to be one criterion. The
805
+ * compiler links no runtime, so it cannot ask a timeline for its ids; a selftest
806
+ * control asks the linked runtime instead and holds this table to what it says.
807
+ */
808
+ export const SLOT_COLOR_CHANNELS: Record<string, readonly SlotColorChannel[]> = {
809
+ rgba: ['rgb', 'alpha'],
810
+ rgb: ['rgb'],
811
+ alpha: ['alpha'],
812
+ rgba2: ['rgb', 'alpha', 'dark'],
813
+ rgb2: ['rgb', 'dark'],
814
+ };
815
+
816
+ /**
817
+ * The seven modes a `sequence` key may state, in the runtime's own enum order
818
+ * (`SequenceMode` in `attachments/Sequence.js`: `hold` 0 … `pingpongReverse` 6).
819
+ *
820
+ * 🚨 **The parser does not refuse a mode it does not know.** `readAnimation`
821
+ * reads `SequenceMode[getValue(keyMap, "mode", "hold")]`, which is `undefined`
822
+ * for a spelling outside the seven (and for a NUMBER, because the enum's reverse
823
+ * mapping turns `3` into the string `"pingpong"`), and `setFrame` then stores
824
+ * `undefined | (index << 4)` — mode bits 0, which is `hold`. Measured: a key
825
+ * spelled `"pingPong"` loads without a word and shows its `index` frame for
826
+ * the whole key. So every reader of a mode refuses anything outside this list,
827
+ * by this list.
828
+ *
829
+ * It lives here for `SLOT_COLOR_CHANNELS`' reason: the motion parser refuses an
830
+ * unknown mode in a spec, `A46` names one in a file rigc did not write, `ingest`
831
+ * carries one, and the four have to be one list. The compiler links no runtime,
832
+ * so a selftest control asks the linked one for its enum and holds this to it.
833
+ */
834
+ export const SEQUENCE_MODES = ['hold', 'once', 'loop', 'pingpong', 'onceReverse', 'loopReverse', 'pingpongReverse'] as const;
835
+
836
+ /** One of `SEQUENCE_MODES`. */
837
+ export type SequenceModeName = (typeof SEQUENCE_MODES)[number];