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
package/src/types.ts ADDED
@@ -0,0 +1,1797 @@
1
+ /**
2
+ * Input and output shapes for rigc.
3
+ *
4
+ * Three inputs, one domain each:
5
+ * - the cut manifest, which owns measured geometry: crop, part offsets, part sizes, mask
6
+ * polygons, the state machine, anchors, the axis and the measured ceilings.
7
+ * Optional — a foreign skeleton has none;
8
+ * - the **rig spec** ([`src/rig.ts`](rig.ts), `spec: "rigc-rig/1"`), which owns
9
+ * skeleton structure: bones, slots, skins, constraints, invariants. Required.
10
+ * It replaced the three hard-coded archetype tables that used to be code;
11
+ * - the motion spec, which owns time: keys, named
12
+ * easings, groups, declared durations.
13
+ *
14
+ * Nothing else is an input, and the compiler never invents a value that is in
15
+ * none of them.
16
+ */
17
+ import type { AtlasRegion } from './atlas.ts';
18
+ import type { DeformTransform, DeformTransformReport } from './deformgen.ts';
19
+ import type { TurnCeiling } from './depth.ts';
20
+ import type { SpecEnumTable, SpecTypeRow } from './keys.ts';
21
+ import type { MeshKind } from './mesh.ts';
22
+ import type { CompiledModel } from './model.ts';
23
+ import type { TrackDerive, TrackDeriveReport } from './trackgen.ts';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Cut manifest (face class)
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Mesh declaration for a part — geometry, so it belongs to the manifest and not
31
+ * to the motion spec. It says WHERE the deformable ring
32
+ * is; the motion spec says WHEN it moves, by keying the control bone.
33
+ */
34
+ export interface FaceManifestMesh {
35
+ /**
36
+ * Which generator builds this mesh. Absent means `ring`, so every manifest
37
+ * written before the joint archetype keeps its meaning.
38
+ *
39
+ * ring — three concentric rings + a hub. The outer two are pinned (region
40
+ * border, then mask contour) and only the aperture ring moves.
41
+ * ribbon — a two-wide strip along a bone chain. Length changes, width does
42
+ * not, because paired vertices carry identical weights.
43
+ */
44
+ kind?: 'ring' | 'ribbon';
45
+ /** Ring only. The part's own mask polygon, which is the seam. */
46
+ hull?: 'polygon';
47
+ /** Ring only. Aperture centre in CROP pixels (y down) — measured, not guessed. */
48
+ center?: [number, number];
49
+ /** Ring only. Inner ring position between centre (0) and hull (1). */
50
+ inner?: number;
51
+ /**
52
+ * Ring only, legacy form: ONE control bone which the compiler CREATES as a
53
+ * child of the slot bone. Used by archetypes that have no explicit bone tree.
54
+ */
55
+ control_bone?: string;
56
+ /**
57
+ * Control bones that already exist in the archetype's bone tree — the ring's
58
+ * authority is split between them by angular position, so a four-grip ring
59
+ * can expand asymmetrically without a key per vertex.
60
+ */
61
+ control_bones?: string[];
62
+ /** Ribbon only. Number of cross rows; triangles = 2 * (rows - 1). */
63
+ rows?: number;
64
+ /** Ribbon only. The bone chain the strip rides, root first. */
65
+ chain?: string[];
66
+ /**
67
+ * Directional weighting. Without it the ring deforms symmetrically about the
68
+ * centre, which moves the upper lip and the upper teeth along with the jaw —
69
+ * anatomically wrong, and the owner spotted it on the first review.
70
+ *
71
+ * `axis_deg` is the mouth line measured on the art (screen degrees, y down).
72
+ * `ramp` is the signed distance across that axis, in part pixels, over which
73
+ * control authority goes 0 -> 1; positive is the jaw side.
74
+ */
75
+ bias?: { axis_deg: number; ramp: [number, number]; note?: string };
76
+ }
77
+
78
+ export interface FaceManifestPart {
79
+ slot: string;
80
+ /**
81
+ * The archetype slot this part joins on, when it differs from `slot`.
82
+ *
83
+ * ⚠️ A cut manifest is often ALSO the record of the pipeline that generated
84
+ * the art, and that pipeline names parts after what they depict while a rig
85
+ * names them after the role they play. The rig's slot table has to stay
86
+ * single-valued — the runtime, the tooling and A26 all join on the emitted slot
87
+ * name, and a second alias for one slot is how a slot vanishes with no error.
88
+ * So the manifest carries the mapping and `slot` keeps meaning what its author
89
+ * meant. Absent = the two are the same name.
90
+ */
91
+ rig_slot?: string;
92
+ draw_order: number;
93
+ /**
94
+ * Base plate only: the unmodified crop. Explicit `null` (with no `states`)
95
+ * means the manifest is recording a part this cut does NOT carry, and the
96
+ * compiler skips it and reports the absence.
97
+ */
98
+ image?: string | null;
99
+ /** Top-left of the part window in crop pixels, y down. */
100
+ offset: [number, number];
101
+ /** Part window size in pixels. Absent on the base plate (= the crop size). */
102
+ size?: [number, number];
103
+ /** Which key of `state_machine` drives this slot. */
104
+ state_key?: string;
105
+ /** state name -> PNG path relative to the manifest, or null for "base pixels". */
106
+ states?: Record<string, string | null>;
107
+ /** Mask polygon in CROP pixels (y down). Required when `mesh` is present. */
108
+ polygon?: Array<[number, number]>;
109
+ /** Promote this part's attachments from regions to ring meshes. */
110
+ mesh?: FaceManifestMesh;
111
+ }
112
+
113
+ export interface FaceManifest {
114
+ schema: string;
115
+ crop: { x: number; y: number; w: number; h: number; resample: string };
116
+ base: string;
117
+ /** Overlay archetypes only; the joint archetype has no per-slot state list. */
118
+ state_machine?: Record<string, string[]>;
119
+ parts: FaceManifestPart[];
120
+
121
+ // -- articulated-cut fields ------------------------------------------------
122
+ /**
123
+ * Entry point in crop pixels, y down — the origin of the cut's axis frame.
124
+ */
125
+ insertion?: [number, number];
126
+ /**
127
+ * ⭐ The one value a new cut of this archetype changes.
128
+ * `deg` is SCREEN degrees, y down, the same convention as `mesh.bias.axis_deg`;
129
+ * the compiler negates it into Spine's y-up CCW rotation. `unit` is the same
130
+ * direction as a vector and is cross-checked against `deg`, because a manifest
131
+ * that disagrees with itself is the cheapest bug to catch and the worst to
132
+ * debug later.
133
+ */
134
+ axis?: { deg: number; unit: [number, number] };
135
+ /**
136
+ * One-way stroke amplitude in axis pixels, the extension the plate covers, and
137
+ * the DERIVED ceiling on inward travel.
138
+ *
139
+ * 🎯 `contact_depth` is the owner's rule of 2026-08-22 made mechanical: the
140
+ * swallow goes at most until the inserting mass touches the occluder. It is a
141
+ * MEASURED fact about two plates (rigc/tools/contact.ts), so it belongs in the
142
+ * manifest for exactly the reason `mesh.center` does — the compiler never
143
+ * re-measures art. Assertion A29 holds every animation to it.
144
+ */
145
+ stroke?: {
146
+ amplitude?: number;
147
+ extension?: number;
148
+ contact_depth?: number | null;
149
+ /**
150
+ * 🎯 The second, independent ceiling on inward travel: the deepest insert at
151
+ * which the moving part's cap contour is still entirely inside the occluder's
152
+ * opaque footprint. Past it the cap is DRAWN where it should be swallowed.
153
+ *
154
+ * It is not a restatement of `contact_depth`. Contact asks when two masses
155
+ * collide; containment asks when the drawn flesh runs out of patch to hide
156
+ * behind — and a cut can have one without the other. The real tier-2 cut has
157
+ * exactly that shape: no contact ceiling at all, and a containment ceiling of
158
+ * 118px. Measured, like every other art fact in this file. Assertion A30.
159
+ */
160
+ cap_containment_ceiling?: number | null;
161
+ };
162
+ /** ROI box, recorded for provenance; the compiler does not read it. */
163
+ roi?: { x: number; y: number; w: number; h: number };
164
+ /**
165
+ * Bone positions in crop pixels, y down: `[x, y]`, or `[x, y, facing_deg]`
166
+ * where the third element is a SCREEN-space facing angle that becomes the
167
+ * bone's setup rotation. A grip whose local +X points radially outward turns
168
+ * "expand the ring" into one shared translate key, which is the same trick the
169
+ * `axis` bone plays for the stroke.
170
+ */
171
+ anchors?: Record<string, number[]>;
172
+ }
173
+
174
+ /**
175
+ * The type every field of the cut manifest holds, shape by shape — what
176
+ * `refuseValuesOfTheWrongType` refuses a manifest value against (issue #890).
177
+ * The three interfaces above name three rows; the inline object types they
178
+ * declare (`crop`, `axis`, `stroke`, `roi`, `mesh.bias`) are rows named
179
+ * `<interface>.<field>`, and the selftest derives every row's keys and checked
180
+ * types from the declarations here.
181
+ *
182
+ * ⚠️ A manifest has no key scan, and this table does not add one: a manifest is
183
+ * often also the record of the pipeline that made the art and carries fields
184
+ * rigc does not read. A key outside a row is left alone, exactly as before; a
185
+ * key inside one must hold its type. Measured before the walk existed, 62 of 84
186
+ * wrong-typed plants on the three fixture manifests built green —
187
+ * `crop.h: "256"` among them — and 4 threw a TypeError from `node:path`.
188
+ *
189
+ * Unchecked by this walk: `mesh.kind` and `mesh.hull` (`enum`) — see
190
+ * `MANIFEST_ENUMS`, below; `crop` and the other nested shapes, `parts` and
191
+ * `mesh` (`object`) — the readers that walk them.
192
+ */
193
+ export const MANIFEST_TYPES = {
194
+ FaceManifest: {
195
+ schema: 'string', crop: 'object', base: 'string', state_machine: 'map of string[]', parts: 'object[]',
196
+ insertion: 'number[]', axis: 'object', stroke: 'object', roi: 'object', anchors: 'map of number[]',
197
+ },
198
+ 'FaceManifest.crop': { x: 'number', y: 'number', w: 'number', h: 'number', resample: 'string' },
199
+ 'FaceManifest.axis': { deg: 'number', unit: 'number[]' },
200
+ 'FaceManifest.stroke': {
201
+ amplitude: 'number', extension: 'number', contact_depth: 'number | null', cap_containment_ceiling: 'number | null',
202
+ },
203
+ 'FaceManifest.roi': { x: 'number', y: 'number', w: 'number', h: 'number' },
204
+ FaceManifestPart: {
205
+ slot: 'string', rig_slot: 'string', draw_order: 'number', image: 'string | null', offset: 'number[]', size: 'number[]',
206
+ state_key: 'string', states: 'map of string | null', polygon: 'number[][]', mesh: 'object',
207
+ },
208
+ FaceManifestMesh: {
209
+ kind: 'enum', hull: 'enum', center: 'number[]', inner: 'number', control_bone: 'string', control_bones: 'string[]',
210
+ rows: 'number', chain: 'string[]', bias: 'object',
211
+ },
212
+ 'FaceManifestMesh.bias': { axis_deg: 'number', ramp: 'number[]', note: 'string' },
213
+ } as const satisfies Record<string, SpecTypeRow>;
214
+
215
+ /** The two mesh generators a manifest part may name (`FaceManifestMesh.kind`); absent means `ring`. */
216
+ export const MANIFEST_MESH_KINDS = ['ring', 'ribbon'] as const satisfies ReadonlyArray<NonNullable<FaceManifestMesh['kind']>>;
217
+
218
+ /**
219
+ * Who refuses each `enum` row of `MANIFEST_TYPES` outside its set (issue #900);
220
+ * `SpecEnumTable` in [`keys.ts`](keys.ts) says what an entry means.
221
+ *
222
+ * 🚨 `mesh.kind` was listed as held by "the manifest mesh reader", and it was
223
+ * not held at all: that reader has three places that ask the kind, and they
224
+ * disagreed about a value outside the two. The mesh checks read `"foo"` as a
225
+ * ribbon, while the control-bone reader and the geometry builder read it as a
226
+ * ring — so a ring part was refused as *a ribbon mesh needs mesh.rows and a
227
+ * mesh.chain*, and a ribbon part as *a mesh with no control bone deforms
228
+ * nothing*, both naming a fault the part did not have. `hull` is held: the ring
229
+ * check refuses anything but `"polygon"` by name, and keeps that sentence.
230
+ */
231
+ export const MANIFEST_ENUMS = {
232
+ FaceManifestMesh: {
233
+ kind: {
234
+ set: MANIFEST_MESH_KINDS,
235
+ readAs:
236
+ 'anything else was read as a ribbon by the mesh checks and as a ring by the control-bone reader and the ' +
237
+ 'geometry builder, so the refusal it got named a fault the part did not have',
238
+ },
239
+ hull: { owner: 'compileInto' },
240
+ },
241
+ } as const satisfies SpecEnumTable<typeof MANIFEST_TYPES>;
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Motion spec (spec: "rigc-motion/1")
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /** Graph-view style normalised handles [hx1, hy1, hx2, hy2]. */
248
+ export type EasingHandles = [number, number, number, number];
249
+
250
+ /**
251
+ * One value per group member, keyed by member name — the map form of `MotionKey.v`.
252
+ *
253
+ * Each entry is exactly what `v` would be for that one member: `[x, y]` on a
254
+ * paired property, `[value]` on a single-axis one, `[r, g, b, a]` on `rgba`, an
255
+ * attachment name on `attachment`. The times, the easings and the key count stay
256
+ * shared, because those being shared is what makes a group a group.
257
+ */
258
+ export type MotionMemberValues = Record<string, number[] | string | null>;
259
+
260
+ export interface MotionKey {
261
+ /** Time in seconds. */
262
+ t: number;
263
+ /**
264
+ * Value. Meaning depends on the track property:
265
+ * rgba -> [r, g, b, a] in 0..1
266
+ * attachment -> attachment name, or null for "show nothing"
267
+ * translate -> [x, y] in pixels, relative to the bone's setup position
268
+ * scale -> [x, y] as multipliers (1 = setup)
269
+ * rotate -> [degrees]
270
+ * inherit -> a mode name (`normal`, `onlyTranslation`,
271
+ * `noRotationOrReflection`, `noScale`, `noScaleOrReflection`);
272
+ * stepped by the format, so no `ease` and no `curve`
273
+ * mix -> [0..1] physics authority
274
+ * inertia / strength / damping / mass / wind / gravity
275
+ * -> [value]; the physics constraint's own setting, over time
276
+ * reset -> null; the key is the event
277
+ *
278
+ * ⭐ On a track that names a `group`, this may instead be a **map keyed by
279
+ * member name** (`MotionMemberValues`) — the six numbers of a head turn side
280
+ * by side rather than six tracks apart, which is the only arrangement in which
281
+ * a reader notices that one of them has the wrong sign (issue #295). A
282
+ * non-map `v` keeps meaning exactly what it means today: every member gets it.
283
+ *
284
+ * ⚠️ Absent only when the key states a `derive` instead. Every other timeline
285
+ * still refuses a key with no value, by name.
286
+ */
287
+ v?: number[] | string | null | MotionMemberValues;
288
+ /**
289
+ * The model form of `v`: a named kind whose per-member values the compiler
290
+ * evaluates from stated parameters (`src/trackgen.ts`, AUTHORING §4.5.1).
291
+ *
292
+ * ⭐ A `v` map states six numbers; this states the one line of arithmetic that
293
+ * produced them plus the six **depths** that are the actual decisions. A key
294
+ * carries one or the other and never both — two answers to one question, the
295
+ * same refusal a `deform` key's `transform` has against a `vertices` run.
296
+ */
297
+ derive?: TrackDerive;
298
+ /** Named easing from `easings`, or "stepped". Absent = linear. */
299
+ ease?: string;
300
+ /**
301
+ * Escape hatch: this key's bezier written out, as ABSOLUTE (time, value)
302
+ * control points — four numbers per value channel, in field order, which is
303
+ * exactly what the emitted JSON holds.
304
+ *
305
+ * ⭐ `ease` stays the recommended path and a key may carry one or the other,
306
+ * never both. A named easing says "this shape, wherever it is used", which is
307
+ * what makes a motion spec readable as intent; this says "these numbers", which
308
+ * is what a transcription of an editor export needs, because an export has a
309
+ * different shape per key per channel.
310
+ *
311
+ * ⚠️ Not the normalised graph-view handles `easings` takes. Those go through
312
+ * `bezierForChannel`; writing them here loads clean and plays a different
313
+ * curve.
314
+ */
315
+ curve?: number[] | 'stepped';
316
+ }
317
+
318
+ /**
319
+ * Bone timelines the compiler emits. Channel counts live in the validator.
320
+ *
321
+ * The single-axis forms are not sugar for the paired ones: Spine keys them as
322
+ * separate timelines, and an export that used `translatex` alone is not
323
+ * reproduced by a `translate` whose y channel happens to be flat — the key
324
+ * counts differ, and so does what a runtime blends against.
325
+ */
326
+ export type BoneProperty =
327
+ | 'translate'
328
+ | 'translatex'
329
+ | 'translatey'
330
+ | 'scale'
331
+ | 'scalex'
332
+ | 'scaley'
333
+ | 'shear'
334
+ | 'shearx'
335
+ | 'sheary'
336
+ | 'rotate'
337
+ | 'inherit';
338
+
339
+ /**
340
+ * Physics timelines the compiler emits — all eight `SkeletonJson`'s physics
341
+ * branch reads, in the order it reads them (`SkeletonJson.js:1063-1094`).
342
+ *
343
+ * Six of them are one number that overrides the constraint's own setting for
344
+ * the length of an animation: `[inertia]`, `[strength]`, `[damping]`, `[mass]`,
345
+ * `[wind]`, `[gravity]`. `mix` is the constraint's authority and `reset`
346
+ * carries no value at all.
347
+ *
348
+ * ⚠️ A key that omits its value reads **0** on all six, and 1 on `mix` — the
349
+ * per-key default, which is NOT the constraint default (`inertia` 0.5,
350
+ * `strength` 100, `damping` 0.85, `mass` 1). rigc never omits a channel, so the
351
+ * distinction only bites a reader comparing an emitted file with an editor
352
+ * export; `PHYSICS_TRACKS` in `compile.ts` carries the argument.
353
+ *
354
+ * 🔒 **Four of them have a compile-time range** (issue #610): `mass` must be
355
+ * `> 0`, `damping` inside the closed `[0, 1]`, and `mix` and `strength` `0` or
356
+ * more. The bounds are `PHYSICS_POSE_RULES` in `src/timelines.ts`, and each row's
357
+ * `basis` says, per way out, whose they are (issue #798). Two are the
358
+ * runtime's arithmetic: a `mass` of 0 is an infinite `massInverse` and NaN from
359
+ * the first step, and a `damping` below 0 is a negative base under a fractional
360
+ * exponent at any `fps` where `60 / fps` is not whole. The other six are rigc's
361
+ * call — the rig runs, finitely, and runs wrongly: `mass` below 0 and
362
+ * `strength` below 0 run away, `damping` above 1 diverges until it overflows,
363
+ * `mix` below 0 is the jiggle inverted, and a setup `mix` or `strength` of 0 is
364
+ * a constraint muted or pulled back by nothing. `inertia`, `wind`, `gravity`
365
+ * and the top of `mix` are bounded nowhere, because the runtime documents
366
+ * nothing for the first three and documents `mix` as "a percentage (0+)" —
367
+ * which is also the text the bottom of `mix` rests on. The same four rows are what
368
+ * `A23_PHYSICS_CONSTRAINT_EFFECTIVE` judges a setup pose and a foreign file's
369
+ * timeline keys with, which is why they are not stated here as numbers.
370
+ *
371
+ * ⚠️ **Two of the four are wider on a KEY than at rest**, and `A23` holds a
372
+ * constraint's own tuning to the narrower one: a setup `mix` or `strength` of 0
373
+ * is a constraint that does nothing, while a key of 0 is an animation muting or
374
+ * releasing it for a span and the next key restores it (issues #610, #727).
375
+ */
376
+ export type PhysicsProperty =
377
+ | 'inertia'
378
+ | 'strength'
379
+ | 'damping'
380
+ | 'mass'
381
+ | 'wind'
382
+ | 'gravity'
383
+ | 'mix'
384
+ | 'reset';
385
+
386
+ /**
387
+ * Path constraint timelines. `mix` is three values in one key —
388
+ * `[mixRotate, mixX, mixY]` — because the format writes them as one timeline
389
+ * with three curve channels (`SkeletonJson.ts:1025-1056`).
390
+ */
391
+ export type PathProperty = 'position' | 'spacing' | 'mix';
392
+
393
+ /** Slider timelines. `time` is the animation time the slider applies. */
394
+ export type SliderProperty = 'time' | 'mix';
395
+
396
+ /**
397
+ * One physics constraint.
398
+ *
399
+ * Structure, so it could argue for the manifest — but every field here is a
400
+ * tuning number for motion over time, so the starting-parameter table goes into
401
+ * the motion spec. It lives with the keys it competes against.
402
+ *
403
+ * ⚠️ The component fields (`x`/`y`/`rotate`/`scaleX`/`shearX`) all default to 0,
404
+ * which means a constraint that names none of them parses cleanly and does
405
+ * absolutely nothing. That is assertion A23.
406
+ */
407
+ export interface MotionPhysics {
408
+ bone: string;
409
+ x?: number;
410
+ y?: number;
411
+ rotate?: number;
412
+ scaleX?: number;
413
+ shearX?: number;
414
+ inertia?: number;
415
+ strength?: number;
416
+ damping?: number;
417
+ mass?: number;
418
+ wind?: number;
419
+ gravity?: number;
420
+ mix?: number;
421
+ fps?: number;
422
+ limit?: number;
423
+ note?: string;
424
+ }
425
+
426
+ /**
427
+ * One target, one property, a list of keys.
428
+ *
429
+ * ⭐ **The target field picks the family, not the property.** Three constraint
430
+ * families spell a timeline `group.<constraint>.<timeline>` and all three of them
431
+ * have a timeline called `mix`, so `property` alone cannot say which one a track
432
+ * means — `physics`, `path` and `slider` each name their own constraint, and a
433
+ * track that names none of them is a slot or bone track as before.
434
+ */
435
+ export interface MotionTrack {
436
+ /** Target one slot... */
437
+ slot?: string;
438
+ /** ...or a named group of slots. */
439
+ group?: string;
440
+ /** ...or one bone, for the mesh tier: the control bone carries every key. */
441
+ bone?: string;
442
+ /** ...or one physics constraint, by name. */
443
+ physics?: string;
444
+ /** ...or one path constraint, by name. */
445
+ path?: string;
446
+ /** ...or one slider, by name. */
447
+ slider?: string;
448
+ property: 'rgba' | 'rgb' | 'alpha' | 'rgba2' | 'rgb2' | 'attachment' | BoneProperty | PhysicsProperty | PathProperty | SliderProperty;
449
+ /** Seconds added to every key time of this track. */
450
+ lag?: number;
451
+ /** Extra per-member delay inside a group, in member order. */
452
+ stagger?: number;
453
+ keys: MotionKey[];
454
+ }
455
+
456
+ /**
457
+ * A track after its per-member values have been resolved **for one target**.
458
+ *
459
+ * ⭐ The type exists to make the resolution order an invariant rather than a
460
+ * convention: a `v` that is a map and a `v` that is a value mean different
461
+ * things, so nothing downstream of `resolveMemberTrack` is allowed to see the
462
+ * first. By the time a key reaches `compileValueTrack`, `v` means one thing —
463
+ * which is the same guarantee `evaluateDeformTransform`'s `setup` array carries,
464
+ * stated in the type system instead of in a comment.
465
+ */
466
+ export interface MotionValueKey extends Omit<MotionKey, 'v' | 'derive'> {
467
+ v?: number[] | string | null;
468
+ }
469
+
470
+ export interface MotionValueTrack extends Omit<MotionTrack, 'keys'> {
471
+ keys: MotionValueKey[];
472
+ }
473
+
474
+ /**
475
+ * One slot moved, at one draw-order key: `offset` positions later in the array.
476
+ *
477
+ * ⚠️ The offset is counted against the SETUP order, not against wherever the
478
+ * slot ended up at the previous key — `readDrawOrder` rebuilds the whole
479
+ * permutation from the setup array every time (SkeletonJson.ts:1336-1374). A key
480
+ * is a complete statement of the change, not an edit to the one before it.
481
+ */
482
+ export interface MotionDrawOrderOffset {
483
+ slot: string;
484
+ /** How many places later this slot is drawn. Negative moves it earlier. */
485
+ offset: number;
486
+ }
487
+
488
+ /**
489
+ * One key of the whole-animation draw-order timeline.
490
+ *
491
+ * A key with **no** `offsets` restores the setup draw order — that is the
492
+ * parser's own encoding (`readDrawOrder` returns null, and the timeline sets the
493
+ * setup array), and it is how an animation that has swapped two slots puts them
494
+ * back.
495
+ */
496
+ export interface MotionDrawOrderKey {
497
+ /** Time in seconds. */
498
+ t: number;
499
+ offsets?: MotionDrawOrderOffset[];
500
+ }
501
+
502
+ /**
503
+ * One firing of a declared event, at one time.
504
+ *
505
+ * ⚠️ Like `drawOrder` and unlike a `track`, this timeline names **no target**:
506
+ * 4.3 writes it as `animations.<a>.events` beside `bones` and `slots`
507
+ * (SPEC_COVERAGE part 1-8), and there is one per animation. The `name` picks
508
+ * an entry out of the rig spec's `events` table; the optional payload fields
509
+ * override that entry's defaults for this firing only.
510
+ *
511
+ * A key with no `int`/`float`/`string` inherits the event's setup payload
512
+ * (`:1250-1252`) — which is what the editor writes, and why `{ "t": 0.5,
513
+ * "name": "footstep" }` is the common shape.
514
+ */
515
+ export interface MotionEventKey {
516
+ /** Time in seconds. */
517
+ t: number;
518
+ /** An event the rig spec declares. A miss throws in the parser; rigc refuses it. */
519
+ name: string;
520
+ /** Payload overrides for this firing. Omit to inherit the event's defaults. */
521
+ int?: number;
522
+ float?: number;
523
+ string?: string;
524
+ /** Read only when the declared event carries an `audio` path — see `RigEvent`. */
525
+ volume?: number;
526
+ balance?: number;
527
+ }
528
+
529
+ /**
530
+ * One key of an IK constraint's timeline (`animations.<a>.ik.<constraint>`).
531
+ *
532
+ * ⚠️ Every field is **optional and absolute**, and that pairing is the trap. The
533
+ * parser reads each one with its own default per key
534
+ * (`SkeletonJson.ts`: `mix` 1, `softness` 0, `bendPositive` true, `compress`
535
+ * false, `stretch` false) — so a key that omits `softness` does not hold the
536
+ * previous key's softness, it snaps to 0. rigc therefore refuses a track whose
537
+ * keys do not all name the SAME set of fields: state the value on every key, or
538
+ * on none of them.
539
+ *
540
+ * `mix` and `softness` are the timeline's two curve channels, in that order.
541
+ * The three booleans are stepped by nature — nothing interpolates them.
542
+ */
543
+ export interface MotionIkKey {
544
+ /** Time in seconds. */
545
+ t: number;
546
+ /** 0..1: how much of the constrained rotation is applied. Parser default 1. */
547
+ mix?: number;
548
+ /** Distance from full reach at which the bones stop straightening. Default 0. */
549
+ softness?: number;
550
+ /** Two-bone IK bend direction. Default true. */
551
+ bendPositive?: boolean;
552
+ /** One-bone IK: scale the bone down to reach a close target. Default false. */
553
+ compress?: boolean;
554
+ /** Scale the bone up to reach a far target. Default false. */
555
+ stretch?: boolean;
556
+ /** Named easing from `easings`, or "stepped". Absent = linear. */
557
+ ease?: string;
558
+ /** The raw form: 4 absolute (time, value) numbers per channel — 8 here. */
559
+ curve?: number[] | 'stepped';
560
+ }
561
+
562
+ /**
563
+ * One IK constraint keyed over time.
564
+ *
565
+ * 4.3 writes this as `animations.<a>.ik.<constraint>` — **one unnamed timeline
566
+ * per constraint**, so the constraint name is the only target there is and the
567
+ * group carries no timeline name at all.
568
+ */
569
+ export interface MotionIkTrack {
570
+ /** An `ik` constraint the rig spec declares. */
571
+ constraint: string;
572
+ keys: MotionIkKey[];
573
+ }
574
+
575
+ /**
576
+ * One key of a transform constraint's timeline
577
+ * (`animations.<a>.transform.<constraint>`).
578
+ *
579
+ * Six mixes, six curve channels, in the order written here — which is the order
580
+ * the parser reads them and therefore the order a curve array concatenates.
581
+ * The same absent-means-default rule as `MotionIkKey` applies, with one extra
582
+ * quirk: `mixY` defaults to **this key's own `mixX`**, not to 1.
583
+ */
584
+ export interface MotionTransformKey {
585
+ /** Time in seconds. */
586
+ t: number;
587
+ /** Parser default 1. */
588
+ mixRotate?: number;
589
+ /** Parser default 1. */
590
+ mixX?: number;
591
+ /** Parser default: the same key's `mixX`. */
592
+ mixY?: number;
593
+ /** Parser default 1. */
594
+ mixScaleX?: number;
595
+ /** Parser default 1. */
596
+ mixScaleY?: number;
597
+ /** Parser default 1. */
598
+ mixShearY?: number;
599
+ ease?: string;
600
+ /** 4 absolute (time, value) numbers per channel — 24 here. */
601
+ curve?: number[] | 'stepped';
602
+ }
603
+
604
+ /** One transform constraint keyed over time. Same shape rule as `MotionIkTrack`. */
605
+ export interface MotionTransformTrack {
606
+ /** A `transform` constraint the rig spec declares. */
607
+ constraint: string;
608
+ keys: MotionTransformKey[];
609
+ }
610
+
611
+ /**
612
+ * One key of a deform timeline
613
+ * (`animations.<a>.attachments.<skin>.<slot>.<attachment>.deform`).
614
+ *
615
+ * 🚨 This is the only key in the format whose meaning depends on the object it
616
+ * is attached to. The parser builds a zero-filled array as long as the
617
+ * attachment's own deform array, copies this key's `vertices` into it starting at
618
+ * `offset`, and leaves the rest alone — so a key is a **sparse edit of the setup
619
+ * geometry**, and both the length of that array and the meaning of an index into
620
+ * it come from the attachment (`SkeletonJson.ts`, the `deform` branch):
621
+ *
622
+ * - **unweighted** attachment — the array is one `x, y` pair per VERTEX, and
623
+ * the parser adds the setup position back on load. The numbers here are
624
+ * therefore offsets from setup, in the slot bone's space.
625
+ * - **weighted** attachment — the array is one `x, y` pair per BONE INFLUENCE
626
+ * (`vertices.length / 3 * 2`), each in the bind space of that influence's
627
+ * bone. A vertex with three bones on it occupies three pairs.
628
+ *
629
+ * `offset` is an index into that array and works for both. `fromVertex` is
630
+ * rigc's ergonomic form — a VERTEX index, which rigc translates — and it is
631
+ * accepted only where the translation is honest: always on an unweighted
632
+ * attachment, and on a weighted one only when every vertex the run covers has
633
+ * exactly one bone influence. Anything else is refused by name rather than
634
+ * emitted as a plausible-looking lie.
635
+ */
636
+ export interface MotionDeformKey {
637
+ /** Time in seconds. */
638
+ t: number;
639
+ /**
640
+ * Where the run starts in the attachment's own deform array. Default 0.
641
+ *
642
+ * Any index the array holds, **odd ones included** (issue #576): the parser
643
+ * copies at this index and does no pair arithmetic, so an odd start is what an
644
+ * editor writes when it trims the leading numbers off a delta run.
645
+ */
646
+ offset?: number;
647
+ /** The same start, given as a vertex index. Never together with `offset`. */
648
+ fromVertex?: number;
649
+ /**
650
+ * The run: consecutive numbers written into the deform array from `offset` on,
651
+ * `x, y` per array slot. Absent or `null` is the parser's own encoding for
652
+ * "back to the setup pose" — the key with no edit.
653
+ *
654
+ * ⚠️ **An ODD count is legal and means something** (issue #576). Both readers
655
+ * copy the run verbatim — `Utils.arrayCopy(vertices, 0, deform, offset,
656
+ * vertices.length)` in `SkeletonJson`, a `for (let v = start; v < end; v++)`
657
+ * fill in `SkeletonBinary` — so a run of three numbers moves one vertex in x
658
+ * and y and the next in x alone, leaving that y at its setup value. Padding a
659
+ * `0` to even it out is a different animation whenever that setup y is
660
+ * non-zero, which is why the even-length rule that stood here could not be kept
661
+ * as a convenience: it left one production export with no spelling in this
662
+ * spec at all.
663
+ */
664
+ vertices?: number[] | null;
665
+ /**
666
+ * The run stated as a **model** instead, which the compiler evaluates over the
667
+ * attachment's own setup geometry (`src/deformgen.ts`, issue #294).
668
+ *
669
+ * ⭐ The same move `generator` already made for geometry: a table of numbers is
670
+ * the wrong way to say a deformation model. `gallery/portrait`'s held 12° yaw
671
+ * is 160 hand-transcribed floats of one closed form, and none of them is a
672
+ * judgement — so the spec states the model and the compiler states the
673
+ * numbers.
674
+ *
675
+ * Never together with `vertices`, for the same reason `generator` and authored
676
+ * geometry cannot sit on one attachment, and never with `offset` or
677
+ * `fromVertex`: a transform covers **every** vertex, because a model applied
678
+ * to part of an attachment leaves a step at the end of its run.
679
+ */
680
+ transform?: DeformTransform;
681
+ ease?: string;
682
+ /**
683
+ * One channel, and it interpolates the deform FRACTION from 0 to 1 rather than
684
+ * any value in `vertices` (`readCurve(..., 0, 1, 1)`). So a raw curve is 4
685
+ * numbers whose value axis runs 0..1.
686
+ */
687
+ curve?: number[] | 'stepped';
688
+ }
689
+
690
+ /** One attachment's geometry keyed over time. */
691
+ export interface MotionDeformTrack {
692
+ /** The skin the attachment lives in. Absent = `"default"`. */
693
+ skin?: string;
694
+ slot: string;
695
+ /** The attachment's placeholder name inside that skin and slot. */
696
+ attachment: string;
697
+ keys: MotionDeformKey[];
698
+ }
699
+
700
+ /**
701
+ * One key of a sequence timeline
702
+ * (`animations.<a>.attachments.<skin>.<slot>.<attachment>.sequence`), which
703
+ * picks the frame of an attachment's `sequence` block (see `RigSequence`).
704
+ *
705
+ * From the key's time on, the frame shown is `index` advanced by one every
706
+ * `delay` seconds and folded back into the series by `mode` —
707
+ * `SequenceTimeline.applyToSlot` (`Animation.js`):
708
+ * `index + floor((time - keyTime) / delay + 0.00001)`, then per mode (`hold`
709
+ * never advances; `once` stops on the last frame; `loop` wraps; `pingpong`
710
+ * bounces; the three `Reverse` modes run from the last frame down). Before the
711
+ * first key the frame is the attachment's `setup`.
712
+ *
713
+ * ⚠️ Three silences the compiler refuses, every one measured on spine-core
714
+ * 4.3.13: a `mode` outside the seven loads as `hold`; a `delay` of 0 under an
715
+ * advancing mode divides by zero and `Infinity | 0` is 0, so the frame never
716
+ * moves; an `index` at or past the series' `count` is clamped to the last frame
717
+ * and a fractional one is truncated (`1.5` showed frame 1).
718
+ *
719
+ * 🔑 Each field is optional in the FORMAT and none is invented here: `mode`
720
+ * defaults to `"hold"` and `index` to 0 in the parser, and `delay` defaults to
721
+ * the PREVIOUS key's delay (0 on the first) — so a key that omits it keeps its
722
+ * neighbour's rate, and the compiler emits exactly the fields the spec states.
723
+ */
724
+ export interface MotionSequenceKey {
725
+ /** Time in seconds. */
726
+ t: number;
727
+ /** One of the seven `SEQUENCE_MODES`. Parser default `"hold"`. */
728
+ mode?: string;
729
+ /** The frame this key starts on, 0-based. Parser default 0. */
730
+ index?: number;
731
+ /** Seconds per frame. Parser default: the previous key's, 0 on the first. */
732
+ delay?: number;
733
+ }
734
+
735
+ /** One attachment's frame keyed over time — the `deform` family's triple, a sequence's keys. */
736
+ export interface MotionSequenceTrack {
737
+ /** The skin the attachment lives in. Absent = `"default"`. */
738
+ skin?: string;
739
+ slot: string;
740
+ /** The attachment's placeholder name inside that skin and slot. */
741
+ attachment: string;
742
+ keys: MotionSequenceKey[];
743
+ }
744
+
745
+ export interface MotionAnimation {
746
+ /** Declared, then verified against the compiled result (rule 4). */
747
+ duration: number;
748
+ /**
749
+ * Player hint only; not expressible in skeleton JSON, and therefore optional.
750
+ *
751
+ * ⚠️ It was declared required until issue #307 put a parser in front of this
752
+ * type and the corpus disagreed: 20 of the 37 motion specs in the repository
753
+ * name no `loop` at all. Nothing in the emitted artifact depends on it, so a
754
+ * required-field refusal here would have refused most of the benchmark corpus
755
+ * over a field the compiler never reads.
756
+ */
757
+ loop?: boolean;
758
+ note?: string;
759
+ tracks: MotionTrack[];
760
+ /**
761
+ * IK constraint timelines, one entry per constraint.
762
+ *
763
+ * ⭐ Not a `track`, and the reason is the key rather than the target. A track's
764
+ * key is one `v` — an array, a name, or nothing — and these three families each
765
+ * carry a shape of their own: five named fields for IK, six for a transform
766
+ * constraint, and for a deform a sparse run whose meaning depends on the
767
+ * attachment. Folding them into `MotionKey` would make `v` mean four different
768
+ * things depending on `property`, and the type would stop documenting any of
769
+ * them. So they sit beside `tracks`, where 4.3 also writes them
770
+ * (`animations.<a>.ik`, `.transform`, `.attachments`).
771
+ */
772
+ ik?: MotionIkTrack[];
773
+ /** Transform constraint timelines, one entry per constraint. */
774
+ transform?: MotionTransformTrack[];
775
+ /** Deform timelines, one entry per skin/slot/attachment triple. */
776
+ deform?: MotionDeformTrack[];
777
+ /**
778
+ * Sequence timelines, one entry per skin/slot/attachment triple — the other
779
+ * of the two timelines an attachment carries, beside `deform` for the same
780
+ * reason: a key of three named fields aimed at an attachment rather than at a
781
+ * slot.
782
+ */
783
+ sequence?: MotionSequenceTrack[];
784
+ /**
785
+ * The draw-order timeline. **One per animation, and it names no target** —
786
+ * which is why it is not a `track`: 4.3 writes it as `animations.<a>.drawOrder`
787
+ * beside `bones` and `slots`, not inside either (SPEC_COVERAGE part 1-8).
788
+ *
789
+ * Draw order is the one thing about a slot that the slots array already
790
+ * states (rule R4), so this timeline is the only way to say it changes over
791
+ * time. First needed at ladder rung 5.
792
+ */
793
+ drawOrder?: MotionDrawOrderKey[];
794
+ /**
795
+ * The event timeline. One per animation, names no target, and for the same
796
+ * reason `drawOrder` is not a `track`. First needed at the spineboy rung.
797
+ */
798
+ events?: MotionEventKey[];
799
+ }
800
+
801
+ /**
802
+ * Setup pose per slot. Declared, never inferred — rule 5. It decides which of
803
+ * the two overlay mechanisms a slot uses:
804
+ * - an attachment + alpha 0 => the lid tier, driven by rgba timelines;
805
+ * - attachment null => the swap tier, driven by attachment timelines
806
+ * (null = the untouched base pixels show).
807
+ */
808
+ export interface MotionSetupSlot {
809
+ attachment?: string | null;
810
+ /** [r, g, b, a] in 0..1. Omit for opaque white. */
811
+ color?: [number, number, number, number];
812
+ }
813
+
814
+ export interface MotionSpec {
815
+ spec: 'rigc-motion/1';
816
+ /**
817
+ * The rig this spec was authored against — it must equal the rig spec's
818
+ * `name`, and a mismatch is a compile error rather than a silent pairing.
819
+ *
820
+ * It named a hard-coded table until 2026-08-22; now it names a file's own
821
+ * name, and the file's path comes from the cuts table. The check is kept
822
+ * because the keys in here are aimed at bones by NAME: pair the spec with
823
+ * another rig whose names happen to overlap and every one of them lands on
824
+ * something that means something else.
825
+ */
826
+ archetype: string;
827
+ cut: string;
828
+ note?: string;
829
+ easings: Record<string, EasingHandles>;
830
+ groups?: Record<string, string[]>;
831
+ setup?: Record<string, MotionSetupSlot>;
832
+ /** Physics constraints by name. Emitted into the 4.3 `constraints` array. */
833
+ physics?: Record<string, MotionPhysics>;
834
+ animations: Record<string, MotionAnimation>;
835
+ /** Player-side AnimationStateData config; not emitted into skeleton JSON. */
836
+ mix?: MotionMix;
837
+ }
838
+
839
+ /**
840
+ * The player-side mix table — a default crossfade and the pairs that override
841
+ * it. Never emitted, which is why nothing had ever looked at it before issue
842
+ * #307's parse.
843
+ *
844
+ * ⭐ Named rather than inline so that `MOTION_KEYS` can pair a key set with it:
845
+ * `CUR17` resolves each set against an interface of the same name, and an
846
+ * anonymous shape is one the pairing cannot reach.
847
+ */
848
+ export interface MotionMix {
849
+ default: number;
850
+ pairs?: Array<[string, string, number]>;
851
+ }
852
+
853
+ // ---------------------------------------------------------------------------
854
+ // Emitted Spine 4.3 skeleton JSON
855
+ // ---------------------------------------------------------------------------
856
+
857
+ /**
858
+ * Field order here is the EDITOR's, for every field its exports write — the
859
+ * order `src/keyorder.ts`'s `EDITOR_KEY_ORDER` states per kind and the emitter
860
+ * writes since issue #716 (`length, rotation, x, y` on a bone, `x, y, …, width,
861
+ * height` on a region, `type` before `name` on a constraint). `CUR84` in
862
+ * `selftest.ts` holds each interface below to its row, so this is a checked
863
+ * claim rather than a description. A field no export writes (`shearX`, a
864
+ * region's `name` and `path`) sits where it reads best: the table leaves such a
865
+ * key at the position its constructor gives it.
866
+ *
867
+ * ⚠️ This comment said the opposite until #716 — *"rigc's, not the editor's …
868
+ * changing it would be a byte-level diff that says nothing"*. It says something:
869
+ * a rebuild of an editor export is the export only if it is the same text, and
870
+ * 269 objects over the twelve exports under `examples/` were not.
871
+ *
872
+ * A field is present exactly when the rig spec declared it; see `src/rig.ts`.
873
+ */
874
+ export interface SpineBone {
875
+ name: string;
876
+ parent?: string;
877
+ length?: number;
878
+ /** Spine degrees, CCW in a y-up world. */
879
+ rotation?: number;
880
+ x?: number;
881
+ y?: number;
882
+ scaleX?: number;
883
+ scaleY?: number;
884
+ shearX?: number;
885
+ shearY?: number;
886
+ /** 4.2+ name. 4.0/4.1's `transform` still loads and is silently ignored — A02. */
887
+ inherit?: string;
888
+ skin?: boolean;
889
+ color?: string;
890
+ /** Editor-only affordance, read at `SkeletonJson.ts:121-126`. */
891
+ icon?: string;
892
+ }
893
+
894
+ export interface SpineSlot {
895
+ name: string;
896
+ bone: string;
897
+ color?: string;
898
+ attachment?: string;
899
+ dark?: string;
900
+ blend?: string;
901
+ }
902
+
903
+ /**
904
+ * The attachment's own name, as distinct from the placeholder it is filed under.
905
+ *
906
+ * `readAttachment` reads `const name = getValue(map, "name", placeholder)`
907
+ * (`SkeletonJson.ts:526`), so an absent field means "the placeholder is also the
908
+ * name". Nothing in the format resolves by it — skins, setup attachments,
909
+ * attachment and deform timelines and a linked mesh's `source` are keyed by the
910
+ * placeholder — so several skins' entries under one placeholder may carry one
911
+ * name, which is what the editor itself exports (issue #796).
912
+ *
913
+ * 🚨 It moves a second field's default with it. For the three types that carry
914
+ * texture art, `path` defaults to **`name`**, not to the placeholder
915
+ * (`:529`, `:559`), so an attachment given a name and no path resolves the region
916
+ * that name spells. rigc emits it exactly when the rig spec states it
917
+ * (`RigAttachmentName` in `rig.ts`); from #541 to #796 it composed
918
+ * `<skin>/<placeholder>` for a placeholder several skins fill, and that is gone.
919
+ *
920
+ * ⚠️ And the **`default` skin may never be one of the skins sharing a
921
+ * placeholder** — a fact about the editor rather than about the format (issue
922
+ * #567, Spine 4.3.26, round trips 7 and 8), and a `CompileError` rather than a
923
+ * spelling. The editor's named skins hold *skin placeholders*, a key holding a
924
+ * named attachment, and come back untouched. Its default skin holds no
925
+ * placeholders: an attachment there hangs on the slot and is known by its name
926
+ * alone. So writing a name there gets it re-keyed by that name on export and
927
+ * the slot's setup `attachment` stops resolving (trip 7), and NOT writing one
928
+ * makes that attachment's name collide with the named skins' placeholder of the
929
+ * same name, which the editor refuses at import (trip 8). Both spellings are
930
+ * measured, so there is no third; `refuseDefaultSkinContest` in `compile.ts` is
931
+ * where that lives.
932
+ */
933
+ export interface SpineRegionAttachment {
934
+ name?: string;
935
+ path?: string;
936
+ x?: number;
937
+ y?: number;
938
+ scaleX?: number;
939
+ scaleY?: number;
940
+ /**
941
+ * Cancels the bone's world rotation so a plate authored in screen space stays
942
+ * screen-upright under a rotated bone. Without it every slot hanging off the
943
+ * `axis` bone would render tilted by the axis angle.
944
+ */
945
+ rotation?: number;
946
+ /** Required. Omitting these yields NaN with no error. */
947
+ width: number;
948
+ height: number;
949
+ color?: string;
950
+ sequence?: SpineSequence;
951
+ }
952
+
953
+ /** `readSequence`'s four fields, emitted as the spec stated them (`RigSequence`). */
954
+ export interface SpineSequence {
955
+ count: number;
956
+ start?: number;
957
+ digits?: number;
958
+ setup?: number;
959
+ }
960
+
961
+ /**
962
+ * Weighted mesh. `triangles` and `uvs` are not optional in practice: a missing
963
+ * `triangles` loads as `undefined` and `uvs` is
964
+ * what decides `worldVerticesLength`.
965
+ */
966
+ export interface SpineMeshAttachment {
967
+ type: 'mesh';
968
+ /** See `SpineRegionAttachment.name` — and it takes `path` with it. */
969
+ name?: string;
970
+ /** `path` and `color` are written right after `type` — `compile.ts`'s `meshTextureKeys` says where that is measured. */
971
+ path?: string;
972
+ color?: string;
973
+ uvs: number[];
974
+ triangles: number[];
975
+ /** Weighted encoding: boneCount, (boneIndex, bindX, bindY, weight)*n, repeated. */
976
+ vertices: number[];
977
+ /**
978
+ * Hull vertex count — the outline polygon is the first `hull` vertices, in
979
+ * order. The loader stores this doubled, and the binary reader derives the
980
+ * triangle count from it, so it is always the count the triangles state
981
+ * (`traceOutline` in mesh.ts) and never 0.
982
+ */
983
+ hull: number;
984
+ /**
985
+ * Nonessential edge list the editor draws: vertex index pairs, each index
986
+ * TIMES TWO (`meshEdges` in mesh.ts). Always written — authored edges are
987
+ * carried through, every other mesh gets its triangle edges — because a mesh
988
+ * without one imports with its interior edges reported lost.
989
+ */
990
+ edges: number[];
991
+ /** Nonessential, but they make the mesh budget assertions readable. */
992
+ width: number;
993
+ height: number;
994
+ sequence?: SpineSequence;
995
+ }
996
+
997
+ /**
998
+ * A mesh that borrows another mesh's geometry (`RigLinkedMeshAttachment`).
999
+ *
1000
+ * Only the keys the parser reads, and only where they differ from its defaults:
1001
+ * `slot` defaults to the link's own slot, `skin` to the default skin and
1002
+ * `timelines` to true (`SkeletonJson.js:571-581`), so writing one at its default
1003
+ * would be a byte the editor's own export does not carry. `width`/`height` are
1004
+ * emitted for the editor and overwritten by the source's at load
1005
+ * (`MeshAttachment.setSourceMesh`), which is why nothing reads them back.
1006
+ */
1007
+ export interface SpineLinkedMeshAttachment {
1008
+ type: 'linkedmesh';
1009
+ /** See `SpineRegionAttachment.name` — and it takes `path` with it. */
1010
+ name?: string;
1011
+ path?: string;
1012
+ source: string;
1013
+ slot?: string;
1014
+ skin?: string;
1015
+ timelines?: boolean;
1016
+ width: number;
1017
+ height: number;
1018
+ color?: string;
1019
+ sequence?: SpineSequence;
1020
+ }
1021
+
1022
+ /**
1023
+ * The two vertex-only attachments: a polygon and nothing else.
1024
+ *
1025
+ * `vertexCount` is not optional the way a mesh's is absent-by-design: the parser
1026
+ * reads `map.vertexCount << 1`, so an omission is `0` and `readVertices` decodes
1027
+ * the coordinate array as a weight run and stores nothing.
1028
+ */
1029
+ export interface SpineBoundingBoxAttachment {
1030
+ type: 'boundingbox';
1031
+ /** See `SpineRegionAttachment.name`. No `path`: this type reads none. */
1032
+ name?: string;
1033
+ vertexCount: number;
1034
+ /** Unweighted x/y pairs, or the weighted run — same encoding as a mesh's. */
1035
+ vertices: number[];
1036
+ color?: string;
1037
+ }
1038
+
1039
+ export interface SpineClippingAttachment {
1040
+ type: 'clipping';
1041
+ /** See `SpineRegionAttachment.name`. No `path`: this type reads none. */
1042
+ name?: string;
1043
+ /** The last slot the clip applies to. Absent = to the bottom of the order. */
1044
+ end?: string;
1045
+ convex?: boolean;
1046
+ inverse?: boolean;
1047
+ vertexCount: number;
1048
+ vertices: number[];
1049
+ color?: string;
1050
+ }
1051
+
1052
+ /**
1053
+ * A composite cubic Bezier, for a path constraint to slide bones along.
1054
+ *
1055
+ * `lengths` is the cumulative length at the end of each curve in the setup pose,
1056
+ * measured **the way `PathConstraint` measures it** — a four-sample forward
1057
+ * difference per curve (`PathConstraint.js:301-320`), which is also what the
1058
+ * Spine editor exports and which reads about **0.5 % below the true arc**.
1059
+ * `vertexCount / 3` entries on both shapes — one per curve on a closed path, and
1060
+ * one more than the curves on an open one, the wrap-around curve's cumulative,
1061
+ * which nothing reads (below). It has no parser default and the parser
1062
+ * dereferences `map.lengths.length` unconditionally, so an absent array is one of
1063
+ * the format's few loud failures. Since issue #804 rigc emits a stated array as
1064
+ * stated and measures only an omitted one — see `buildRigPath`.
1065
+ *
1066
+ * ⚠️ That sentence read *"the cumulative **arc** length"* until issue #560, and
1067
+ * the word was load-bearing in the wrong direction: this is not an arc length,
1068
+ * and no refinement of the integral converges on it. It is the number the
1069
+ * field's own consumer computes when it is not given one. ⇒ Do not derive a
1070
+ * physical quantity from it — how far a wheel rolls, how long a ribbon is.
1071
+ * `position` is stated against it; arc length is not it.
1072
+ *
1073
+ * ⚠️ **The EDITOR writes `vertexCount / 3` entries on BOTH — measured**
1074
+ * (round trip 6, 2026-09-16, Spine 4.3.26). It computes the wrap-around curve
1075
+ * even for an open path: `gallery/ride`'s 12-vertex open path came back with
1076
+ * **four** entries on 2026-09-04 (4.3.23) and the fourth, `2136.228`, is the
1077
+ * *closed*-chain cumulative. ⚠️ Neither array is wrong, and this is not a case
1078
+ * of the editor knowing something rigc does not. `SkeletonJson.js:601` allocates
1079
+ * `Utils.newArray(vertexCount / 3, 0)` and copies whatever is there, while
1080
+ * `PathConstraint` reads at most `lengths[curveCount]` with
1081
+ * `curveCount = verticesLength / 6 − (closed ? 1 : 2)` — index 2 on that path.
1082
+ * The trailing entry the editor adds to an open path is never read by anything.
1083
+ * Since issue #804 rigc writes it too, over the closed chain, and the `ride`
1084
+ * build ends on the editor's `2136.228`: a rebuild is the file the editor
1085
+ * writes rather than one entry short of it.
1086
+ *
1087
+ * 🚨 **What the editor writes INTO those entries is its own measurement, and it
1088
+ * is the runtime's, not calculus'** (issue #560, measured on the same trip).
1089
+ * `PathConstraint`'s `constantSpeed` re-measure is a four-sample forward
1090
+ * difference per curve — its constants are `0.1875 = 3t²`, `0.09375 = 6t³` and
1091
+ * `(cx1 − x1) · 0.75 = 3t` at **t = 1/4**, accumulating four `Math.sqrt` terms —
1092
+ * and the editor's stored `lengths` are that same computation. A 4-sample chord
1093
+ * sum over the same control points reproduces the editor to every digit it
1094
+ * prints, on two rigs and two editor builds: `pathmodes` (closed, 4.3.26) came
1095
+ * back `[152.7006, 305.4012, 458.1019, 610.8025]` and `ride` (open, 4.3.23)
1096
+ * `[430.8389, 838.0142, 1127.736, …]`. It is a recomputation at **export** — the
1097
+ * inflated `.spine` project holds the imported numbers verbatim — so a path rig
1098
+ * is re-parameterised by the trip rather than corrupted by it.
1099
+ *
1100
+ * ⇒ **rigc emits that computation, not a sampler aimed at it** (issue #560).
1101
+ * `pathCurveLengths` in [`compile.ts`](compile.ts) measures with the core's
1102
+ * curve table (`curveLengthTable` in `src/core/constraints_path.ts`, issue
1103
+ * #1015), down to `Math.sqrt(dx * dx + dy * dy)` rather than `Math.hypot` and
1104
+ * `0.16666667` rather than `1 / 6`; `PS67`, `PS68` and `PS186` in `selftest.ts`
1105
+ * hold it there by requiring it to reproduce a real `PathConstraint.curves`
1106
+ * array **bit for bit** on the runtime's own posed chain. Measured after the change, all
1107
+ * seven entries of both editor exports above come back at the precision the
1108
+ * editor prints them.
1109
+ *
1110
+ * ⚠️ This paragraph used to point at a constant — `PATH_LENGTH_SAMPLES` — and ask
1111
+ * *how finely rigc should sample*. There is no such constant now, and the
1112
+ * question was the wrong one. The chord-sum reading above is true and it is not
1113
+ * sufficient: a 4-sample chord sum agrees with the forward difference to about
1114
+ * **nine significant digits**, which is *below* what float32 can hold — so no
1115
+ * editor export can tell the two apart, and that reading can only settle the
1116
+ * MODEL — and *above* rigc's six-decimal rounding, so the emitted file can. On
1117
+ * both rigs above the two spellings round apart on the **last** curve, where the
1118
+ * running total has accumulated most: `610.802519` against `610.802520`, and
1119
+ * `1127.735817` against `1127.735818`. So the editor is the evidence for what is
1120
+ * being computed and only the runtime is evidence for how.
1121
+ *
1122
+ * 📌 For the record of what the disagreement cost when it was found: the build
1123
+ * rigc **0.21.0** emitted for `pathmodes` sat a uniform **0.70 %** above the
1124
+ * editor's four numbers, and `check` read **4.9612 mean MAE** against 0.0000 on
1125
+ * the seven rigs of that run without a path. ⚠️ What decides whether that moves a
1126
+ * pixel is the POSITION mode, not the spacing mode: `pathmodes` is
1127
+ * `positionMode: fixed`, where an absolute `position` is compared against a total
1128
+ * that scaled, so the bone slides. Under `positionMode: percent` a uniform scale
1129
+ * cancels out of both the position and the spacing — `gallery/ride` is
1130
+ * percent/percent and every one of its 74 rendered frames came back **byte
1131
+ * identical** across this change, on an emitted array all three of whose numbers
1132
+ * moved. `A33_VERTEX_ATTACHMENT_GEOMETRY` asks only that the array strictly
1133
+ * increase, which both arrays do, and `diff` does not compare it at all.
1134
+ */
1135
+ export interface SpinePathAttachment {
1136
+ type: 'path';
1137
+ /** See `SpineRegionAttachment.name`. No texture `path`: this type reads none. */
1138
+ name?: string;
1139
+ closed?: boolean;
1140
+ constantSpeed?: boolean;
1141
+ vertexCount: number;
1142
+ /** Unweighted x/y pairs, or the weighted run — same encoding as a mesh's. */
1143
+ vertices: number[];
1144
+ lengths: number[];
1145
+ color?: string;
1146
+ }
1147
+
1148
+ export type SpineAttachment =
1149
+ | SpineRegionAttachment
1150
+ | SpineMeshAttachment
1151
+ | SpineLinkedMeshAttachment
1152
+ | SpineBoundingBoxAttachment
1153
+ | SpineClippingAttachment
1154
+ | SpinePathAttachment;
1155
+
1156
+ export type SpineTimelineKey = Record<string, unknown>;
1157
+
1158
+ /**
1159
+ * 4.3 puts every constraint type in ONE top-level `constraints` array and
1160
+ * branches on `type` (SkeletonJson.js:129-350). The 4.1-era per-type arrays
1161
+ * (`physics: [...]`, `ik: [...]`) are not read at all — the constraint vanishes
1162
+ * with no error, which is assertion A01.
1163
+ */
1164
+ export type SpineConstraint = { type: string; name: string } & Record<string, unknown>;
1165
+
1166
+ export interface SpineSkeletonJson {
1167
+ skeleton: {
1168
+ spine: string;
1169
+ /**
1170
+ * The setup-pose bounding box — and since issue #907 that is what `build`
1171
+ * writes here, computed by rigc's core (`headerBoundsOf` in `compile.ts`);
1172
+ * it used to copy the stage. All four together or none of them: a rig spec
1173
+ * that declares no stage (`skeleton.width`/`height` stated `null` — see
1174
+ * `RigSkeletonHeader`) emits a header without any of them, which is what an
1175
+ * export of a skeleton whose bounds were never set carries (issue #578).
1176
+ *
1177
+ * ⚠️ Optional here because the *runtime* leaves them `undefined` when they
1178
+ * are absent, not 0. `SkeletonData` declares `x = 0 … height = 0`
1179
+ * (`SkeletonData.js:55-61`), and `SkeletonJson` then overwrites all four
1180
+ * unconditionally — `skeletonData.x = skeletonMap.x` (`SkeletonJson.js:70-73`,
1181
+ * no `getValue` default) — so an absent field lands as `undefined` on a
1182
+ * `SkeletonData` whose own `.d.ts` types it `number`. Anything reading these
1183
+ * back off a parsed skeleton guards for it; `validate.ts`'s `data.width || 0`
1184
+ * is why A14 and A19 were already right about a stage-less file.
1185
+ */
1186
+ x?: number;
1187
+ y?: number;
1188
+ width?: number;
1189
+ height?: number;
1190
+ fps?: number;
1191
+ referenceScale?: number;
1192
+ images?: string;
1193
+ /** Nonessential, carried from `RigSkeletonHeader.audio` as stated — `null` included. */
1194
+ audio?: string | null;
1195
+ };
1196
+ bones: SpineBone[];
1197
+ slots: SpineSlot[];
1198
+ constraints?: SpineConstraint[];
1199
+ /**
1200
+ * Field order inside a skin entry is `readSkeletonData`'s reading order —
1201
+ * `bones`, then the five constraint lists, then `attachments` — and every one
1202
+ * of them but `name` and `attachments` is emitted only when the rig declared
1203
+ * it, so a spec that names no per-skin member emits exactly what it always did.
1204
+ */
1205
+ skins: Array<{
1206
+ name: string;
1207
+ /** Bone names this skin activates. Each one carries `skin: true`. */
1208
+ bones?: string[];
1209
+ ik?: string[];
1210
+ transform?: string[];
1211
+ path?: string[];
1212
+ physics?: string[];
1213
+ slider?: string[];
1214
+ attachments: Record<string, Record<string, SpineAttachment>>;
1215
+ }>;
1216
+ /**
1217
+ * Event definitions, keyed by name (`SkeletonJson.ts:451-464`). An object, not
1218
+ * an array — one of the **two** top-level collections in the format that are,
1219
+ * `animations` being the other.
1220
+ *
1221
+ * ⚠️ This sentence said "the one" until issue #535, and the collection it was
1222
+ * overlooking is where the defect that card is about lived. The distinction is
1223
+ * not cosmetic: the binary format addresses both of these by ORDINAL
1224
+ * (`SkeletonBinary`: `animations[readInt()]` for a slider's animation,
1225
+ * `events[readInt()]` for an event key), and an editor round trip was measured
1226
+ * to re-key every name-keyed object while returning the arrays it was taken
1227
+ * over in the order they were given. So a reference into either of these two
1228
+ * is a reference whose ordinal an editor can move.
1229
+ *
1230
+ * ✅ **`events` does not need what `animations` needed, and that is measured
1231
+ * rather than owed.** This comment said *unmeasured* until issue #539 carried
1232
+ * three of them through the editor on the same session's discriminator rigs:
1233
+ * `zebra, mike, alpha` came back keyed `alpha, mike, zebra`, and every firing
1234
+ * still resolved **by name** — `0.3 -> mike`, `0.6 -> alpha`, payloads intact.
1235
+ * So the editor re-keys `events` and repoints nothing, while the same re-key
1236
+ * of `animations` repoints every slider (#535). `animations` is emitted in the
1237
+ * editor's order for that reason (`compile.ts`'s `editorAnimationOrder`);
1238
+ * `events` is emitted in the order the rig spec declares them.
1239
+ *
1240
+ * ⚠️ Two more of this paragraph's claims were falsified by the same session,
1241
+ * and both stood here for a release because nothing re-read them (#544):
1242
+ *
1243
+ * - **The re-key is not in codepoint order.** It is natural and
1244
+ * case-insensitive: `Turn, sweep, wave` came back `sweep, Turn, wave` and
1245
+ * `turn10, turn2, zoom` came back `turn2, turn10, zoom` (#539). rigc emits
1246
+ * `animations` in **that** comparator's order since issue #543, and since
1247
+ * #728 the comparator itself is measured rather than quantified over —
1248
+ * five stored round trips, folders and all — so only what those files leave
1249
+ * open is refused by name. See `compile.ts`'s `editorNameOrder` and
1250
+ * `refuseNamesTheEditorCouldKeyDifferently`. It emitted codepoint until
1251
+ * #543, with the refusal widened to cover every pair codepoint could order
1252
+ * differently; that refused both rigs above, which are the only two anybody
1253
+ * had measured then, and it moved no byte to stop doing so.
1254
+ * - 🚨 **`skins` is NOT an array the editor leaves alone. It is the first one
1255
+ * measured moved** (#541). A four-skin rig built `default, zulu, mike,
1256
+ * alpha` came back `default, alpha, mike, zulu`: `default` is pinned first
1257
+ * and the rest are re-sorted, and the deform timelines came back keyed
1258
+ * `mike, zulu` rather than `zulu, mike` with it. `SkeletonBinary` addresses
1259
+ * skins by ORDINAL — `skins[readInt()]` for an attachment timeline,
1260
+ * `skins[skinIndex]` for a linked mesh — so this is #535 in the collection
1261
+ * nobody had checked. rigc emits `default` first and the rest in that
1262
+ * order since #541; see `compile.ts`'s `editorSkinOrder`.
1263
+ *
1264
+ * ⚠️ **The two readings this replaces, kept because the second is the one
1265
+ * that cost something.** #537's pull request called `skins` "measured
1266
+ * preserved"; #544 corrected that to *unmeasured*, on the grounds that the
1267
+ * arrays the round trip actually returned element for element were `bones`
1268
+ * (30), `slots` (24) and `constraints` (3), that every rig in this tree
1269
+ * declares exactly ONE skin, and that a one-element array comes back in
1270
+ * order whatever the editor does with it. Both readings were reached by
1271
+ * generalising from the three arrays that *were* measured — "an editor does
1272
+ * not move arrays" — and the generalisation is what was false. #544 also
1273
+ * said the measurement could not be taken, because the editor refused a
1274
+ * four-skin rig on import without a word: that refusal was rigc's own
1275
+ * harness discarding the editor's stderr, and the editor had named the
1276
+ * cause all along.
1277
+ *
1278
+ * - ✅ **What round trip 6 added to the `constraints` line is TYPES, not
1279
+ * order** (2026-09-16, Spine 4.3.26, eight rigs). The three that stood here
1280
+ * were `gallery/look`'s, and they are two types: `yaw` and `tilt`
1281
+ * (**slider**) and `whip` (**physics**). The trip carried a `transform`
1282
+ * with its whole 4.3 `source` + `properties` map, a `path` with three
1283
+ * non-default modes, another `physics` and another `slider`, and every one
1284
+ * came back field for field — so the array is now measured over **four**
1285
+ * of the format's types. `ik` is in none of the rigs anybody has
1286
+ * round-tripped, which is why the count is four and not five.
1287
+ *
1288
+ * ⚠️ **None of round 6's own constraint arrays can tell order preserved
1289
+ * from a name sort, and reading them as if they could would be #537's
1290
+ * mistake in a second collection.** The three rigs that carry constraints
1291
+ * hold `aim, hold` (xform), `hold, knob` (physlider) and `ride` alone
1292
+ * (pathmodes). All three came back in the order they were given — and all
1293
+ * three were *already* in name order, so a re-sort and a preservation are
1294
+ * the same picture there, exactly as a one-element array is for `skins`
1295
+ * above. ⇒ The order claim still rests entirely on `gallery/look`, whose
1296
+ * build order `yaw, tilt, whip` is **not** name order (`tilt < whip < yaw`)
1297
+ * and which #539 read back unchanged. That one rig is load-bearing and
1298
+ * nothing in this tree re-takes it.
1299
+ */
1300
+ events?: Record<string, SpineEvent>;
1301
+ animations: Record<
1302
+ string,
1303
+ {
1304
+ slots?: Record<string, Record<string, SpineTimelineKey[]>>;
1305
+ bones?: Record<string, Record<string, SpineTimelineKey[]>>;
1306
+ /**
1307
+ * `ik.<constraint> = keys[]` — the constraint IS the timeline, so there is
1308
+ * no timeline name between the two. Same for `transform`.
1309
+ */
1310
+ ik?: Record<string, SpineTimelineKey[]>;
1311
+ transform?: Record<string, SpineTimelineKey[]>;
1312
+ /** `path.<constraint>.<position|spacing|mix> = keys[]` — the physics shape. */
1313
+ path?: Record<string, Record<string, SpineTimelineKey[]>>;
1314
+ physics?: Record<string, Record<string, SpineTimelineKey[]>>;
1315
+ /** `slider.<constraint>.<time|mix> = keys[]`. */
1316
+ slider?: Record<string, Record<string, SpineTimelineKey[]>>;
1317
+ /** `attachments.<skin>.<slot>.<attachment>.<timeline> = keys[]` — four deep. */
1318
+ attachments?: Record<string, Record<string, Record<string, Record<string, SpineTimelineKey[]>>>>;
1319
+ /** Whole-animation timeline: no target name, one array per animation. */
1320
+ drawOrder?: SpineTimelineKey[];
1321
+ /** The other whole-animation timeline; same shape, same reason. */
1322
+ events?: SpineTimelineKey[];
1323
+ }
1324
+ >;
1325
+ }
1326
+
1327
+ /** One entry of the emitted `skins` array — what `emitSkins` in `src/emit_spine.ts` writes per skin. */
1328
+ export type SpineSkin = SpineSkeletonJson['skins'][number];
1329
+
1330
+ /** One animation of the emitted `animations` object — what `emitAnimations` in `src/emit_spine.ts` writes per animation. */
1331
+ export type SpineAnimation = SpineSkeletonJson['animations'][string];
1332
+
1333
+ /** One entry of the emitted `events` map: the payload a firing inherits. */
1334
+ export interface SpineEvent {
1335
+ int?: number;
1336
+ float?: number;
1337
+ string?: string;
1338
+ audio?: string;
1339
+ volume?: number;
1340
+ balance?: number;
1341
+ }
1342
+
1343
+ // ---------------------------------------------------------------------------
1344
+ // Compiler result
1345
+ // ---------------------------------------------------------------------------
1346
+
1347
+ export interface CompiledImage {
1348
+ /** Region name = attachment name = PNG basename. */
1349
+ region: string;
1350
+ /** Atlas page name: the PNG path relative to the atlas file. */
1351
+ page: string;
1352
+ /** Absolute path on disk, for the size assertions. */
1353
+ absPath: string;
1354
+ width: number;
1355
+ height: number;
1356
+ /**
1357
+ * A per-pixel alpha channel, and only that — colour types 4 and 6.
1358
+ *
1359
+ * ⚠️ Not "this part can be transparent": indexed and greyscale art keeps its
1360
+ * transparency in a `tRNS` chunk and reads `false` here. Anything asking
1361
+ * whether the art can draw a transparent pixel wants `PngInfo.hasTransparency`
1362
+ * ([`src/png.ts`](png.ts)), which is the distinction A19 got wrong (#215).
1363
+ */
1364
+ hasAlpha: boolean;
1365
+ isBase: boolean;
1366
+ /**
1367
+ * The atlas region this part was resolved FROM, when it came out of a
1368
+ * pre-packed atlas (`build --atlas-in`). Absent for the ordinary case, where
1369
+ * the part is a loose PNG and its region covers its page exactly.
1370
+ *
1371
+ * Carried rather than flattened because a packed region says things a loose
1372
+ * PNG cannot: where on the page it sits, how much border the packer trimmed,
1373
+ * whether it is turned. `width`/`height` above are the untrimmed DRAWING's
1374
+ * size — the region's `originalWidth`/`originalHeight` divided by the page's
1375
+ * `scale:` — so every existing reader of this interface keeps the meaning it
1376
+ * had; this field is for the two that need the rectangle itself, in the page's
1377
+ * own texels (lifting the drawing back off the page, and reporting the pack).
1378
+ *
1379
+ * ⚠️ So these two are in DIFFERENT units whenever the page declares a `scale:`
1380
+ * other than 1: `width` is world/art size, `atlas.width` is texels. See
1381
+ * `atlasScale`.
1382
+ */
1383
+ atlas?: AtlasRegion;
1384
+ /**
1385
+ * The `scale:` of the page the region above sits on, when it declares one
1386
+ * other than 1. Absent otherwise, and absent for a loose PNG.
1387
+ *
1388
+ * Present so a message can show its work: `width`/`height` are already
1389
+ * descaled, and a refusal that says "region X is 746" without saying it read
1390
+ * 373 texels at `scale: 0.5` names a number that is in neither file.
1391
+ *
1392
+ * And so a figure taken off the lifted texels can be stated in the drawing's
1393
+ * pixels, which is what `width`/`height` are in (issue #762): a distance
1394
+ * measured on this page's grid is `texels / atlasScale` pixels of the drawing,
1395
+ * and a sheet made at the drawing's size is read at the same ratio. It is the
1396
+ * value the atlas states, never one rigc measured.
1397
+ */
1398
+ atlasScale?: number;
1399
+ /**
1400
+ * The page this region sits on, when that page's file is not the size the
1401
+ * atlas declares for it (issue #750): `said` is `pageGridSaid`'s clause and
1402
+ * `sentence` is `A06`'s whole sentence (`pageGridSentence`). Absent for a
1403
+ * page whose file agrees, and for a loose PNG, which is its own page.
1404
+ *
1405
+ * Carried because the region lift (`partPlate`) addresses the page at the
1406
+ * coordinates the atlas states, and on such a file those are not where the
1407
+ * part's texels are: every reader of the lift has to know that before it
1408
+ * takes a figure off it.
1409
+ */
1410
+ pageGrid?: { said: string; sentence: string };
1411
+ }
1412
+
1413
+ /**
1414
+ * Structural expectations the validator cannot read out of skeleton JSON.
1415
+ *
1416
+ * Some invariants of a rig are simply not written down in the artifact:
1417
+ * nothing in the file says "this mesh is a ribbon" or "this emitter must not
1418
+ * hang off the part that released it". The compiler knows, because the rig
1419
+ * spec's `invariants` block says so, and it hands the knowledge over rather than
1420
+ * letting the validator guess. Mutants stay honest because a mutant edits the
1421
+ * ARTIFACT while this block keeps saying what the rig was supposed to be.
1422
+ */
1423
+ export interface RigInfo {
1424
+ /** The rig spec's `name`. Reported by the validator so a green names its rig. */
1425
+ archetype: string;
1426
+ /** The bone whose setup rotation carries the cut's axis, if the rig has one. */
1427
+ axisBone: string | null;
1428
+ /** Bones under the axis bone, whose translate keys must stay on the axis. */
1429
+ axisSubtree: string[];
1430
+ /** [bone, ancestor it must never have] — see `invariants.detached`. */
1431
+ detached: Array<[string, string]>;
1432
+ /** Canonical draw order (the rig's slot array), or null if it declares none. */
1433
+ slotOrder: string[] | null;
1434
+ /**
1435
+ * slot -> what built this mesh, for the kind-aware mesh assertions.
1436
+ *
1437
+ * `ring`, `ribbon` and `contour` are rigc's own generators, whose topology it
1438
+ * therefore knows: where the rim is, which edge is the entry row, that the
1439
+ * rows pair up, that a contour's hull IS every vertex it has.
1440
+ * **`authored`** is geometry that came in through the rig spec — drawn by an
1441
+ * animator, transcribed from an export — and rigc knows nothing about its
1442
+ * topology at all. An assertion that measures generator topology has nothing
1443
+ * to say about one, so it SKIPs with that as the reason rather than checking
1444
+ * a ring the mesh was never supposed to be.
1445
+ */
1446
+ meshKinds: Record<string, MeshKind | 'authored'>;
1447
+ /**
1448
+ * slot -> every bone the compiler bound this mesh to, in the order the `MESH`
1449
+ * report line prints them: the slot bone first, then the control bones the
1450
+ * generator named.
1451
+ *
1452
+ * ⭐ A20 reads it to ask the one question a weighted run cannot answer about
1453
+ * itself — whether a bone the mesh DECLARES is bound by any vertex at all. A
1454
+ * ring that named two grips and bound one was a green build on every
1455
+ * per-vertex rule there is, because each of those rules reads a vertex and the
1456
+ * missing bone is in none of them (issue #684).
1457
+ *
1458
+ * ⚠️ On an `authored` mesh this is the set the weights themselves name, so it
1459
+ * is a tautology there and A20's clause skips it with the rest of the
1460
+ * generator policy — rigc did not choose those bindings.
1461
+ */
1462
+ meshDeclaredBones: Record<string, string[]>;
1463
+ /**
1464
+ * Slots whose mesh carries a SOFT region on a second bone, and which bone —
1465
+ * from a generator's `soft` block.
1466
+ *
1467
+ * ⭐ A21 reads it. A rim vertex the mask CARRIED is supposed to move — that
1468
+ * is what a soft region is — so the rule splits by declaration rather than
1469
+ * being relaxed, exactly as it already splits for a ribbon's entry row: on
1470
+ * such a mesh the invariant is that a vertex is either pinned to the slot
1471
+ * bone or shared between it and the declared bone, and never anything else.
1472
+ */
1473
+ meshSoftBones: Record<string, string>;
1474
+ /**
1475
+ * Slots whose deform timelines may turn a triangle inside out, from
1476
+ * `invariants.deformMayFold`. Empty is the ordinary case, and A39 gates every
1477
+ * mesh not named here — see `RigInvariants.deformMayFold` for why the default
1478
+ * is on.
1479
+ */
1480
+ deformMayFold: string[];
1481
+ /**
1482
+ * Ik and transform constraints whose mix the consumer sets, from
1483
+ * `invariants.consumerDrivenMix`, in the rig spec's order. `A47` / `A48` do
1484
+ * not measure these: a declared constraint is named on the stats line, and it
1485
+ * is the SKIP's subject when nothing else of its kind is left to measure. A
1486
+ * bare `validate <dir>` has no rig and so no declaration, and refuses every
1487
+ * muted constraint as before (issue #784).
1488
+ */
1489
+ consumerDrivenMix: Array<{ type: 'ik' | 'transform'; constraint: string; why: string }>;
1490
+ /**
1491
+ * The `why` of `invariants.idleDrivesMeshes`, or null when the rig does not
1492
+ * declare that its `idle` deforms meshes on purpose. Declared, `A15` SKIPs
1493
+ * with the renderer cost it measured, or FAILs when the `idle` keys no
1494
+ * mesh-driving bone and the declaration switches off nothing. A bare
1495
+ * `validate <dir>` has no rig, so null, and A15 refuses per bone as before.
1496
+ */
1497
+ idleDrivesMeshes: string | null;
1498
+ /**
1499
+ * The atlas region(s) the build names as its base plate — the one image
1500
+ * `A19_OVERLAY_PNGS_HAVE_ALPHA` lets be opaque — in compile order.
1501
+ *
1502
+ * A cut manifest states it: the part whose window IS the crop
1503
+ * (`CompiledImage.isBase`), with no stage box involved. A rig spec has no way
1504
+ * to state it, so a build from one names none and this is empty. `A19` reads
1505
+ * this first and falls back to "an attachment at least the stage's size" only
1506
+ * when it is empty (issue #770) — two readings that can disagree need an
1507
+ * order, and the statement outranks the measurement.
1508
+ */
1509
+ basePlates: string[];
1510
+ /** Mesh slots this rig budgets for, or null when it declares no budget. */
1511
+ meshSlotBudget: number | null;
1512
+ /** Triangles one mesh may carry, or null when the rig declares no budget. */
1513
+ meshTriangleBudget: number | null;
1514
+ /** Deepest inward advance the two masses allow, from the manifest. */
1515
+ contactDepth: number | null;
1516
+ /**
1517
+ * Deepest inward advance at which the cap contour is still covered, from the
1518
+ * manifest. Null when the cut has not measured one — A30 then says nothing
1519
+ * rather than inventing a wall.
1520
+ */
1521
+ capContainmentCeiling: number | null;
1522
+ /**
1523
+ * The bone the inserting mass hangs on. Its own inward keys spend the same
1524
+ * clearance the stroke does: if both move in, both close the gap.
1525
+ */
1526
+ massBone: string | null;
1527
+ /** Inward unit vector in SPINE world (y up), for projecting off-axis keys. */
1528
+ inwardUnit: [number, number] | null;
1529
+ }
1530
+
1531
+ /**
1532
+ * A state the manifest lists whose art was not where the manifest said.
1533
+ *
1534
+ * Named rather than written inline in `CompileResult` because it outlives the
1535
+ * result: a compile that REFUSES returns nothing, and the drops it recorded on
1536
+ * the way to that refusal are facts about the inputs the caller still has to be
1537
+ * told (issue #671). `CompileError.droppedStates` carries them, and it can only
1538
+ * do that if the shape has a name.
1539
+ */
1540
+ export interface DroppedState {
1541
+ slot: string;
1542
+ state: string;
1543
+ path: string;
1544
+ /**
1545
+ * What was consulted and came up empty, when it was not a file on disk.
1546
+ *
1547
+ * Absent on the ordinary path, where "no PNG at <path>" says everything. An
1548
+ * `--atlas-in` build opened no such file — it looked for a REGION — so
1549
+ * reporting the path would send the reader to a directory instead of to the
1550
+ * pack that is missing it.
1551
+ */
1552
+ why?: string;
1553
+ }
1554
+
1555
+ export interface CompileResult {
1556
+ /**
1557
+ * The compiled model (`src/model.ts`): what the skeleton below was emitted
1558
+ * from. This cut fills its `bones` and `setupWorld`; every other field is
1559
+ * this result's own, carried by reference.
1560
+ */
1561
+ model: CompiledModel;
1562
+ /**
1563
+ * The Spine 4.3 skeleton: `emitSkeleton`'s value over `model` (issue #922),
1564
+ * the one call the assembly makes. `build` writes its text, and beside it
1565
+ * `modelDocument(model)` as `skeleton.model.json`.
1566
+ */
1567
+ skeleton: SpineSkeletonJson;
1568
+ skeletonText: string;
1569
+ atlasText: string;
1570
+ images: CompiledImage[];
1571
+ /**
1572
+ * Every page of an `--atlas-in` pack whose file is not the size the atlas
1573
+ * declares, in the pack's page order, each with `pageGridSaid`'s clause.
1574
+ * Empty on the loose and packing routes, and on a pack whose pages agree.
1575
+ */
1576
+ pageGrids: Array<{ page: string; said: string }>;
1577
+ /** States listed in the manifest whose PNG is not on disk. */
1578
+ droppedStates: DroppedState[];
1579
+ /**
1580
+ * Parts the manifest declares and the cut does not carry (`image: null`, no
1581
+ * states). Reported rather than swallowed: "the optional slots are optional" is
1582
+ * a claim about the emit path, so the emit path says out loud which ones it
1583
+ * left out.
1584
+ */
1585
+ absentParts: Array<{ slot: string; why: string }>;
1586
+ /** Declared durations, carried into the validator (rule 4). */
1587
+ declaredDurations: Record<string, number>;
1588
+ /** Bones that drive a mesh attachment: the slot bone plus its control bone. */
1589
+ meshBones: string[];
1590
+ /** Mesh slots emitted, with triangle counts — reported by `build`. */
1591
+ meshes: Array<{
1592
+ slot: string;
1593
+ kind: MeshKind | 'authored';
1594
+ attachments: string[];
1595
+ vertices: number;
1596
+ triangles: number;
1597
+ bones: string[];
1598
+ /**
1599
+ * Share of the part's own art the triangles cover, 0..1.
1600
+ *
1601
+ * Measured for every mesh that names an `image`, generated or authored: it is
1602
+ * a number between two things the compiler has in front of it — the emitted
1603
+ * triangles and the PNG — and it assumes nothing about how the vertices are
1604
+ * arranged (issue #277). Absent on a mesh with no `image`, which has nothing
1605
+ * to be measured against, and on a `ring` or `ribbon`, whose window size
1606
+ * comes from the spec rather than from art.
1607
+ */
1608
+ coverage?: number;
1609
+ /**
1610
+ * How far past the silhouette that mesh reaches, in the DRAWING's pixels —
1611
+ * the unit `CompiledImage.width` is in, and the one an author draws in.
1612
+ *
1613
+ * The distance is measured on the grid the part's alpha is read off, and on
1614
+ * a page that declares a `scale:` that grid is the page's texels, so it is
1615
+ * divided by the stated scale here (issue #762). It printed 8.00px on a
1616
+ * `scale: 0.5` page for a mesh that reaches 16.00px past the same drawing
1617
+ * on the page it was packed from. `pageScale` says when that happened.
1618
+ */
1619
+ overshoot?: number;
1620
+ /**
1621
+ * The `scale:` of the page the fit above was measured on, when it is not 1
1622
+ * (`CompiledImage.atlasScale`). Absent on a loose part and on a page at
1623
+ * scale 1, where the texel grid is the drawing's. Carried so the report can
1624
+ * say the figure was taken on a grid whose step is `1 / pageScale` pixels
1625
+ * of the drawing, which is the precision it has.
1626
+ */
1627
+ pageScale?: number;
1628
+ /**
1629
+ * Why `coverage` and `overshoot` are absent on a mesh that names an image:
1630
+ * its part sits on a packed page whose file is not the size the atlas
1631
+ * declares, and this is `pageGridSaid`'s clause for that page (issue #750).
1632
+ * The fit is a measurement between the triangles and the part's texels,
1633
+ * and the coordinates the atlas states do not locate those texels on such
1634
+ * a file, so the figure is withheld rather than taken off the wrong ones.
1635
+ */
1636
+ fitWithheld?: string;
1637
+ /**
1638
+ * Transparent pixels the traced outline encloses — inside the mesh, drawing
1639
+ * nothing. Only a `contour` has one: it is a property of the trace, and an
1640
+ * authored mesh was not traced.
1641
+ */
1642
+ holePixels?: number;
1643
+ /**
1644
+ * The soft region a `soft` block carried to its own bone, when one was
1645
+ * named — the mask, its digest, and how many vertices it reached.
1646
+ *
1647
+ * ⚠️ Separate from `depth` on purpose. It WAS a depth threshold and that
1648
+ * conflated two properties: the most prominent thing on a face is the nose,
1649
+ * and a nose does not wobble.
1650
+ */
1651
+ soft?: { mask: string; digest: string; bone: string; carried: number; ramped: number };
1652
+ /**
1653
+ * How a `segments` mesh's bones share its vertices — the figures `build`
1654
+ * and `explain` print under its line. Only `segments` has one: it is the
1655
+ * generator whose weights are the whole point, and every other generator's
1656
+ * weighting is fixed by its kind.
1657
+ */
1658
+ influence?: {
1659
+ /** Most bones any one vertex binds, and the mean over the mesh. */
1660
+ maxBones: number;
1661
+ meanBones: number;
1662
+ /** Vertices bound to exactly one bone. */
1663
+ singleBone: number;
1664
+ /** The bones some vertex binds, in the order `bones` names them. */
1665
+ bound: string[];
1666
+ /** The lattice: its cell, cells across and down, cells with art, cells kept, islands joined. */
1667
+ cell: number;
1668
+ cols: number;
1669
+ rows: number;
1670
+ artCells: number;
1671
+ keptCells: number;
1672
+ islands: number;
1673
+ };
1674
+ /**
1675
+ * What a depth map put on this mesh's vertices, when one was named.
1676
+ *
1677
+ * The digest is over the levels rather than the file, so a re-encode of the
1678
+ * same sheet reports the same provenance; `range` is what was actually
1679
+ * sampled. Absent when no map was named — never zeroes, which would read as
1680
+ * "sampled and found flat".
1681
+ *
1682
+ * ⚠️ This comment sat above `soft` rather than above the field it describes
1683
+ * until issue #449 came to add to it, which is the same drift `CUR07` was
1684
+ * built for one file over — nothing derives a doc comment's neighbour.
1685
+ */
1686
+ depth?: {
1687
+ /** The sheet, as written in the spec. */
1688
+ image: string;
1689
+ /** First 16 hex of a sha256 over width, height and levels. */
1690
+ digest: string;
1691
+ near: 'white' | 'black';
1692
+ zScale: number;
1693
+ /** The stated curve, in full — `1 / 1 / 0` is the straight line. */
1694
+ tone: { gamma: number; contrast: number; bias: number };
1695
+ /** Least and greatest `z` over the mesh's vertices, in attachment units. */
1696
+ range: [number, number];
1697
+ /**
1698
+ * How many of the mesh's vertices took their depth from a texel **the
1699
+ * part image does not draw** (issue #449).
1700
+ *
1701
+ * ⚠️ Not what `range` says, and this is the field that exists because
1702
+ * `range` was claimed to say it. A map that is half background has
1703
+ * exactly as full a range as one that is all subject, because a
1704
+ * background level is a legitimate depth — so a full-frame sheet over a
1705
+ * cut-out part reports a healthy `[0, 223.97]` of 224 with 54 % of the
1706
+ * mesh reading background.
1707
+ *
1708
+ * A count and never a refusal: the same defect is already a named refusal
1709
+ * when the sheet's alpha is cut to the art, and a sheet that is opaque
1710
+ * everywhere is a statement rigc has no authority to guess away. Zero is
1711
+ * a real answer here rather than an absence — every mesh that names a
1712
+ * depth map also names an image, so the measurement is taken whenever
1713
+ * the image's texels can be located.
1714
+ *
1715
+ * `null` is the one case they cannot (issue #750): a part lifted off a
1716
+ * packed page whose file is not the size its atlas declares, where the
1717
+ * coordinates the atlas states are not where the part's texels are. The
1718
+ * count is withheld rather than taken off whatever sits there — a
1719
+ * number measured over the wrong pixels reads exactly like a right one.
1720
+ */
1721
+ undrawn: number | null;
1722
+ /** `pageGridSaid`'s clause for the page, exactly when `undrawn` is `null`. */
1723
+ unlocated?: string;
1724
+ /**
1725
+ * The turn this geometry takes on this sheet before a triangle reverses,
1726
+ * per axis and per direction — `src/depth.ts`'s `turnCeiling`.
1727
+ *
1728
+ * ⭐ It is what an author needs BEFORE writing a key, and the loop it
1729
+ * replaces is "pick an angle, build, read `A39`'s refusal, guess again".
1730
+ * A report and never a refusal: `A39` owns the refusal, from the artifact.
1731
+ */
1732
+ ceiling: TurnCeiling;
1733
+ };
1734
+ }>;
1735
+ /** Structural expectations handed to the validator. */
1736
+ rig: RigInfo;
1737
+ /** Physics constraints emitted, with the bone each one drives. */
1738
+ physics: Array<{ name: string; bone: string; components: string[]; mix: number; drivesMesh: boolean }>;
1739
+ /**
1740
+ * Deform keys that stated a `transform` instead of a run, one entry per key in
1741
+ * emit order — reported by `explain` (issue #294).
1742
+ *
1743
+ * ⭐ The report carries the offsets it **emitted**, not a second evaluation of
1744
+ * the same model, so the printed audit and the artifact cannot disagree — and
1745
+ * where the two are not one array, `expanded` carries the second (issue #389).
1746
+ * An empty array is the ordinary case: a spec whose deform keys are all
1747
+ * authored runs generated nothing to report.
1748
+ */
1749
+ deformTransforms: Array<
1750
+ DeformTransformReport & {
1751
+ animation: string;
1752
+ skin: string;
1753
+ slot: string;
1754
+ attachment: string;
1755
+ /** The key's own time, as emitted. */
1756
+ time: number;
1757
+ /**
1758
+ * The deform array actually written, when it is not `offsets` itself
1759
+ * (issue #389).
1760
+ *
1761
+ * Present only on a multi-influence attachment, where the model is
1762
+ * evaluated at setup **world** positions and `offsets` is therefore one
1763
+ * world displacement per vertex, while the array holds one `Mᵢ⁻¹ · D` pair
1764
+ * per bone INFLUENCE. Absent everywhere else, because there the two are
1765
+ * the same numbers and a second copy of them could only ever drift.
1766
+ */
1767
+ expanded?: number[];
1768
+ }
1769
+ >;
1770
+ /**
1771
+ * Group-track keys whose per-member values were **stated as a map** or
1772
+ * **derived from a model**, one entry per key in emit order — reported by
1773
+ * `explain` (issue #295).
1774
+ *
1775
+ * ⭐ Both spellings are here, and that is the point of the report rather than
1776
+ * an accident of it: what an author needs to see is the members' values *side
1777
+ * by side*, and whether they were transcribed or derived is one column of that
1778
+ * table. The values carried are the **emitted** ones, so the printed audit and
1779
+ * the artifact cannot disagree.
1780
+ */
1781
+ trackDerivations: Array<{
1782
+ animation: string;
1783
+ /** The group the track named, or the bone if a bone track stated a model. */
1784
+ target: string;
1785
+ /** `group` or `bone` — which field carried the target. */
1786
+ targetKind: 'group' | 'bone';
1787
+ property: string;
1788
+ /** The key's own time, as emitted for the FIRST member (before any `stagger`). */
1789
+ time: number;
1790
+ /** The authored key time, which is what the spec names. */
1791
+ authoredTime: number;
1792
+ /** Absent when the key stated a `v` map rather than a model. */
1793
+ model: TrackDeriveReport | null;
1794
+ /** Every member's emitted value, in member order. */
1795
+ members: Array<{ member: string; value: number[] | string | null }>;
1796
+ }>;
1797
+ }