rig-c 0.0.0-stage → 2.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
package/src/model.ts ADDED
@@ -0,0 +1,1245 @@
1
+ /**
2
+ * The compiled model — what `compile` knows about a rig once every name is
3
+ * resolved and every number is on the grid the runtime will read, held apart
4
+ * from the shape any one output format gives it (issue #915, step 1b of #380).
5
+ *
6
+ * ## What it is
7
+ *
8
+ * The record a posing core of rigc's own would read: bones with their inherit
9
+ * modes, slots, every attachment kind rigc builds, skins, constraints, events
10
+ * and animations. It is the neutral side of the census in `docs/COMPILED_MODEL.md`:
11
+ * a value belongs here when any backend posing this rig would need it, and a
12
+ * spelling, a key order or an omission at a parser default belongs to the
13
+ * emitter that writes one format. The Spine emitter (`src/emit_spine.ts`) is its
14
+ * first consumer, and the one that owns every Spine 4.3 byte; the model owns
15
+ * none.
16
+ *
17
+ * ⚠️ **The numbers are exactly the numbers the file holds**, and the emitter
18
+ * copies them without touching one. Which decimal a number is spelled with is
19
+ * model content and not formatting: it is the double spine-core's JSON reader
20
+ * keeps, so it moves the pose (census §4, 19 of 19 builds).
21
+ *
22
+ * 🔸 **"On the float32 grid" holds for the values `f32` produced, not for
23
+ * every number here** (issue #931's measurement, corrected by #935). A bone's
24
+ * transform, a region's placement and size and a key's value are `f32`'d. A
25
+ * generated weight, and the bind and vertex coordinates an ingested rig
26
+ * states, are `r6` or the source's own decimals: on the nineteen recipes
27
+ * `tools/emit_hashes.ts` generates, 503 of 516 bind coordinates of
28
+ * `gallery/flex` and 767 of 792 of `spineboy-pro` are not float32 values. The
29
+ * runtime reads a bone's and a region's numbers as the doubles the text
30
+ * spells, and a vertex attachment's `vertices` (weights and bind coordinates
31
+ * included) into a float32 array; so a reader reproducing the runtime's pose
32
+ * reads vertex arrays and weights through `Math.fround` and every other number
33
+ * as written. Read as doubles, the corpus's weighted meshes pose 1 to 3
34
+ * millionths away from spine-core; read through `Math.fround`, exact.
35
+ *
36
+ * ## What it holds
37
+ *
38
+ * `bones` and `setupWorld` (issue #915, cut 1b); the four vertex-attachment
39
+ * kinds — mesh, path, bounding box, clipping — as records whose weighted
40
+ * vertices name their bone (issue #917, cut 1c); and the remaining structural
41
+ * records (issue #919, cut 1d): `slots`, the region and linked-mesh
42
+ * attachments, `skins` holding every attachment as a model record,
43
+ * `constraints` and `events`; the skeleton's `referenceScale` (issue #958),
44
+ * which wind and gravity act over; and `animations` (issue #921, cut 1e), each a
45
+ * `CompiledAnimation` whose timelines hold the keys the timeline compilers
46
+ * build; and every region's atlas rectangle (issue #935, `ModelAtlasRect`). Every other field is `CompileResult`'s own, carried by reference under
47
+ * the same name (`CarriedFromCompileResult`) until its own cut gives it a
48
+ * model-side form; the skeleton object, its text and the atlas text are emitted
49
+ * artifacts and are not part of the model at all.
50
+ *
51
+ * ⭐ **A weighted vertex names its bone.** Spine's run binds a vertex to a bone by
52
+ * its POSITION in the emitted bone array, and until cut 1c every vertex
53
+ * attachment carried that run from the moment it was built, so five later
54
+ * stages decoded the index back into a bone. The model keeps the binding by
55
+ * name (`ModelBinding`); the Spine emitter turns names into positions once, at
56
+ * emission, against `bones`' order (`emitVertices`). A bone inserted ahead of a
57
+ * mesh then moves the emitted indexes and no binding.
58
+ *
59
+ * 🔸 **Every omission at a parser fallback the constructors made inline is the
60
+ * emitter's now, and the model holds the value.** A slot's setup attachment is
61
+ * `null` rather than absent; a manifest region carries its `x`, `y` and
62
+ * `rotation` at 0; a physics constraint carries every component and parameter
63
+ * the spec stated, at 0 and at the parser's default included; a linked mesh
64
+ * carries the skin, slot and `timelines` flag it resolves through, its own
65
+ * slot, `default` and `true` included. The Spine emitter leaves each of those
66
+ * out where the constructor used to (`src/emit_spine.ts`).
67
+ *
68
+ * ⚠️ **One omission is still the builder's: `path`** on a region, a mesh and a
69
+ * linked mesh. It is present exactly when `attachmentPath` in `compile.ts`
70
+ * returns one — a stated `path` always, one derived from `image` only where the
71
+ * basename differs from the name the attachment carries. That is the value
72
+ * semantics cut 1c kept for a mesh, and it is not a rule the emitter could run
73
+ * on the model: a STATED `path` equal to the name is written today, a DERIVED
74
+ * one equal to it is not, and the record cannot tell the two apart. Holding
75
+ * the region an attachment resolves through always, and moving that omission
76
+ * to the emitter with a flag for which was stated, is a later decision.
77
+ *
78
+ * 🔸 **The atlas's two constants are not model content.** `filter: Linear,
79
+ * Linear` and `pma: false` (`writeAtlasText` in `src/atlas.ts`) are a sampling
80
+ * hint and an alpha convention the Spine backend chooses and no input states —
81
+ * the census's two `open` rows (docs/COMPILED_MODEL.md §5), decided at cut 1d:
82
+ * they are the atlas emitter's constants, and the model does not carry them.
83
+ *
84
+ * 📄 **It is written as a document** (issue #922, cut 1f): `modelDocument` below
85
+ * spells it as `rigc-compiled/2` (`/1` until issue #1016 added `pages`), and
86
+ * `build` writes that text into `--out` as `skeleton.model.json` beside the
87
+ * Spine files, after the gate, like them.
88
+ */
89
+ import { createHash } from 'node:crypto';
90
+ import { parseAtlasText } from './atlas.ts';
91
+ import { CompileError } from './errors.ts';
92
+ import type { BoneTransform } from './transform.ts';
93
+ import type { RigSkinConstraintKey } from './rig.ts';
94
+ import type { CompileResult } from './types.ts';
95
+
96
+ /**
97
+ * One bone, as `buildBone` computes it — a field is present exactly when the rig
98
+ * spec declared it (or, for `x`/`y`/`rotation`, when `from` supplied it), and
99
+ * every number is already `f32`'d.
100
+ */
101
+ export interface ModelBone {
102
+ name: string;
103
+ parent?: string;
104
+ length?: number;
105
+ /** Local to the parent, y up. Solved from the manifest when the spec says `from`. */
106
+ x?: number;
107
+ y?: number;
108
+ /** Degrees, CCW, y up. */
109
+ rotation?: number;
110
+ scaleX?: number;
111
+ scaleY?: number;
112
+ shearX?: number;
113
+ shearY?: number;
114
+ /**
115
+ * The inherit MODE, as the spec states it (the rig parse admits the runtime's
116
+ * first-letter fold, so `noScale` and `NoScale` both reach here). What key the
117
+ * mode is written under is the emitter's: Spine 4.3 spells it `inherit`.
118
+ */
119
+ inheritMode?: string;
120
+ /** The bone is inactive unless the applied skin names it. Spine 4.3 spells it `skin`. */
121
+ skinRequired?: boolean;
122
+ /** Editor affordances: no pose reads them. */
123
+ editor?: { color?: string; icon?: string };
124
+ }
125
+
126
+ /**
127
+ * The fields of `CompileResult` the model carries as they are today, by
128
+ * reference. A later cut replaces one of these with a model-side form by moving
129
+ * it out of this list.
130
+ */
131
+ export type CarriedFromCompileResult = Pick<
132
+ CompileResult,
133
+ | 'images'
134
+ | 'pageGrids'
135
+ | 'droppedStates'
136
+ | 'absentParts'
137
+ | 'meshBones'
138
+ | 'meshes'
139
+ | 'physics'
140
+ | 'deformTransforms'
141
+ | 'trackDerivations'
142
+ | 'rig'
143
+ >;
144
+
145
+ /**
146
+ * One influence of a weighted vertex: the bone by NAME, the vertex in that bone's
147
+ * local setup space, and the share it carries. Every number is exactly what the
148
+ * emitted run holds — `f32` (and, for a generated mesh, the generator's `r6` on
149
+ * the weight) already applied where the builder applies it.
150
+ */
151
+ export interface ModelBinding {
152
+ bone: string;
153
+ x: number;
154
+ y: number;
155
+ weight: number;
156
+ }
157
+
158
+ /**
159
+ * A vertex attachment's geometry, in one of the format's two encodings.
160
+ *
161
+ * `weighted` is said outright. Spine's reader chooses the encoding by comparing
162
+ * the run's length with the vertex count and nothing else, so a run of the
163
+ * wrong length is read as the other encoding without a word; the model does not
164
+ * inherit that, and the Spine emitter is the one place the choice becomes a
165
+ * length again.
166
+ *
167
+ * - unweighted: `xy`, one `x, y` per vertex in the attachment's own space (the
168
+ * slot bone's), the numbers exactly as the emitted array carries them;
169
+ * - weighted: `bindings`, per vertex its influences in order, each naming its bone.
170
+ */
171
+ export type ModelVertices = { weighted: false; xy: number[] } | { weighted: true; bindings: ModelBinding[][] };
172
+
173
+ /**
174
+ * A numbered series of atlas regions an attachment draws in turn: the four
175
+ * fields exactly as the spec stated them, each optional one only when stated.
176
+ * Which of them Spine leaves out at the parser's default is the emitter's
177
+ * (`emitSequenceBlock`).
178
+ */
179
+ export interface ModelSequence {
180
+ count: number;
181
+ start?: number;
182
+ digits?: number;
183
+ setup?: number;
184
+ }
185
+
186
+ /**
187
+ * The rectangle a region draws through, in the atlas's own numbers and under
188
+ * the names the atlas's `bounds:` and `offsets:` lines carry (issue #935): the
189
+ * kept rectangle's `width` and `height`, in the drawing's orientation; the
190
+ * trim's `offsetX` (from the drawing's left) and `offsetY` (from its bottom);
191
+ * and the untrimmed drawing's `originalWidth` and `originalHeight`. All six are
192
+ * in the page's texels, exactly as the atlas states them (`AtlasRegion` in
193
+ * `src/atlas.ts`) — NOT divided by a page's `scale:`, because the pose reads
194
+ * only their ratios to the record's `width` and `height`.
195
+ *
196
+ * ⭐ **Why the model holds it.** A region's four corners are
197
+ * `x1 = -W/2·sx + offsetX·W/originalWidth·sx`, `x2 = x1 + width·W/originalWidth·sx`
198
+ * (and the same in y) before the record's placement and the bone — measured
199
+ * exact on 3000 of 3000 probes against spine-core 4.3.13, and 976 of 3000 with
200
+ * the trim ignored (issue #931). The trim and the original size live only in
201
+ * the atlas, so until this record held them one model document posed two ways:
202
+ * `examples/3-timing-and-spacing` built against two atlases differing only in
203
+ * `square`'s trim wrote byte-identical `skeleton.model.json` and
204
+ * `skeleton.json`, and spine-core moved that region's corners by 11.925 world
205
+ * units. A value any backend posing the rig needs belongs in the model.
206
+ *
207
+ * 🔸 **What it leaves out, and why.** The page, the rectangle's `x`/`y` on it
208
+ * and its `rotate` are WHERE the drawing sits in one arrangement of the pixels,
209
+ * not what the drawing is: `build --pack` repacks every part onto shared pages
210
+ * after this document is spelled, and `--copy-images` renames every page, so
211
+ * those four would state a place the written atlas does not have on two of
212
+ * the three routes that write one. None of them enters a region's corners
213
+ * (#931: the atlas's `rotate` transposed into the corners was exact on 2018 of
214
+ * 3000 probes, ignored on 3000 of 3000). The trim and the original size do not
215
+ * move under either: rigc's packer never trims or rotates (`src/atlas.ts`).
216
+ * The draw, which does need the four — a region's page and page UVs — read
217
+ * them off the atlas written beside this document, as a second input
218
+ * (`src/core/uvs.ts`, issue #967), until issue #1016 gave the document a
219
+ * `pages` section spelled from the atlas text `build` writes (`pagesOfAtlas`
220
+ * below): the four are still not this record's, because they are the written
221
+ * arrangement's, and the section moves with it.
222
+ */
223
+ export interface ModelAtlasRect {
224
+ width: number;
225
+ height: number;
226
+ offsetX: number;
227
+ offsetY: number;
228
+ originalWidth: number;
229
+ originalHeight: number;
230
+ }
231
+
232
+ /**
233
+ * A region's sequence: the four stated fields, and `atlas`, one rectangle per
234
+ * frame in frame order — frame `i` is the region `<path><start + i>`, padded
235
+ * to `digits`, and the runtime draws each frame through its own rectangle
236
+ * (`Sequence.apply` sets the region the corners are computed from).
237
+ *
238
+ * A parallel array rather than a `frames` list of objects: a frame's region
239
+ * NAME is derived from the four fields, so the rectangle is the one thing per
240
+ * frame the record does not already state. Never `null`: the gather pass
241
+ * atlases every frame of every sequence or refuses the missing one by name
242
+ * before any attachment is built. A mesh's or a linked mesh's sequence does not
243
+ * carry it — a mesh's vertices read no atlas (#931, 400 of 400 meshes exact
244
+ * with none), and neither kind's record carries a rectangle.
245
+ */
246
+ export interface ModelRegionSequence extends ModelSequence {
247
+ atlas: ModelAtlasRect[];
248
+ }
249
+
250
+ /**
251
+ * A mesh: `buildRigMesh`'s authored geometry, the five generators' output, and a
252
+ * manifest part's ring or ribbon. Fields as the builders compute them today.
253
+ */
254
+ export interface ModelMeshAttachment {
255
+ kind: 'mesh';
256
+ /** The spec's own `name`, present exactly when stated (issue #796). */
257
+ name?: string;
258
+ /**
259
+ * The atlas region, present exactly when `attachmentPath` in `compile.ts`
260
+ * returns one: stated, or derived from `image` where the basename differs from
261
+ * the name the attachment carries. See the header's ⚠️ — resolving it to the
262
+ * region always is a later decision, and so is moving that omission here.
263
+ */
264
+ path?: string;
265
+ color?: string;
266
+ uvs: number[];
267
+ triangles: number[];
268
+ vertices: ModelVertices;
269
+ hull: number;
270
+ edges: number[];
271
+ width: number;
272
+ height: number;
273
+ sequence?: ModelSequence;
274
+ }
275
+
276
+ /** A bounding box: a polygon and nothing else. */
277
+ export interface ModelBoundingBoxAttachment {
278
+ kind: 'boundingbox';
279
+ name?: string;
280
+ vertexCount: number;
281
+ vertices: ModelVertices;
282
+ /** The editor's display colour; no pose reads it. Spine writes it as `color`. */
283
+ editorColor?: string;
284
+ }
285
+
286
+ /** A clipping polygon, and the slot its clip ends at. */
287
+ export interface ModelClippingAttachment {
288
+ kind: 'clipping';
289
+ name?: string;
290
+ end?: string;
291
+ convex?: boolean;
292
+ inverse?: boolean;
293
+ vertexCount: number;
294
+ vertices: ModelVertices;
295
+ editorColor?: string;
296
+ }
297
+
298
+ /** A path: knots and their handles, and the cumulative curve lengths stated or measured on the setup pose. */
299
+ export interface ModelPathAttachment {
300
+ kind: 'path';
301
+ name?: string;
302
+ closed?: boolean;
303
+ constantSpeed?: boolean;
304
+ vertexCount: number;
305
+ vertices: ModelVertices;
306
+ lengths: number[];
307
+ editorColor?: string;
308
+ }
309
+
310
+ /** The four attachment kinds that carry a vertex array — the ones a deform timeline can key. */
311
+ export type ModelVertexAttachment =
312
+ | ModelMeshAttachment
313
+ | ModelBoundingBoxAttachment
314
+ | ModelClippingAttachment
315
+ | ModelPathAttachment;
316
+
317
+ /**
318
+ * A region: one quad of one atlas region (or of a numbered series), placed on
319
+ * the slot's bone. `buildRigRegion` for a rig spec's, `placeRegion` for a
320
+ * manifest part's.
321
+ *
322
+ * `x`, `y`, `rotation`, `scaleX`, `scaleY` are present exactly when the spec
323
+ * stated them — and, for a manifest part, `x`, `y` and `rotation` ALWAYS, since
324
+ * `placeRegion` computes all three and 0 is a placement like any other. The
325
+ * Spine emitter leaves a 0 out (`emitRegion`).
326
+ */
327
+ export interface ModelRegionAttachment {
328
+ kind: 'region';
329
+ name?: string;
330
+ /** As on a mesh: present exactly when `attachmentPath` returns one (see the header's ⚠️). */
331
+ path?: string;
332
+ x?: number;
333
+ y?: number;
334
+ rotation?: number;
335
+ scaleX?: number;
336
+ scaleY?: number;
337
+ width: number;
338
+ height: number;
339
+ color?: string;
340
+ sequence?: ModelRegionSequence;
341
+ /**
342
+ * The rectangle this region draws through (`ModelAtlasRect`), present exactly
343
+ * when the record has no `sequence` — a sequence's frames carry theirs. It is
344
+ * looked up under the region NAME the runtime asks for — `path`, else the
345
+ * stated `name`, else the placeholder — in the build's one atlas source:
346
+ *
347
+ * - `--atlas-in`: the pack's region of that name, first match (as
348
+ * `TextureAtlas.findRegion` resolves it), its numbers as the pack states
349
+ * them;
350
+ * - otherwise: the part PNG atlased under that name, which is its own page —
351
+ * `width`/`height` and `originalWidth`/`originalHeight` the PNG's size,
352
+ * offsets 0. `build --pack` keeps all six (it never trims);
353
+ * - `null` when the source has no region of that name — an ingested region
354
+ * built without `--atlas-in`, or one naming a region the pack lacks. That
355
+ * is a statement that the build has no source, never a trim of 0, and a
356
+ * green build never carries one: the emitted atlas has no such region
357
+ * either, and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refuses the build.
358
+ */
359
+ atlas?: ModelAtlasRect | null;
360
+ }
361
+
362
+ /**
363
+ * A linked mesh: another mesh's geometry under a region of its own.
364
+ *
365
+ * The link is held IN FULL — `skin`, `slot` and `timelines` as the parser
366
+ * resolves them, the defaults applied: `skin` "default" where none was stated,
367
+ * `slot` the attachment's own slot, `timelines` true unless stated false. The
368
+ * Spine emitter leaves out each one that equals the parser's fallback
369
+ * (`emitLinkedMesh`). Whether the author wrote a key or took its default is a
370
+ * fact only the refusals need, and it stays on `compile.ts`'s `PendingLink`.
371
+ */
372
+ export interface ModelLinkedMeshAttachment {
373
+ kind: 'linkedmesh';
374
+ name?: string;
375
+ /** As on a mesh: present exactly when `attachmentPath` returns one (see the header's ⚠️). */
376
+ path?: string;
377
+ /** The PLACEHOLDER of the source mesh in `skin`/`slot`. */
378
+ source: string;
379
+ skin: string;
380
+ slot: string;
381
+ /** Whether the link plays its source's deform and sequence timelines. */
382
+ timelines: boolean;
383
+ width: number;
384
+ height: number;
385
+ color?: string;
386
+ sequence?: ModelSequence;
387
+ }
388
+
389
+ /**
390
+ * What a skin table holds per placeholder: a model record of one of the six
391
+ * attachment kinds rigc builds, told apart by `kind`, whose words are Spine's
392
+ * `type` words (a region's `type` is the one the emitter leaves out). rigc
393
+ * emits no point attachment (`DEFERRED_ATTACHMENTS` in `compile.ts`).
394
+ */
395
+ export type SkinTableEntry = ModelVertexAttachment | ModelRegionAttachment | ModelLinkedMeshAttachment;
396
+
397
+ /** One skin's attachments: slot -> placeholder -> record, in the spec's order. */
398
+ export type SkinTable = Record<string, Record<string, SkinTableEntry>>;
399
+
400
+ /** Whether a skin-table entry is one of the four vertex kinds — the ones a deform timeline can key. */
401
+ export function isModelVertexAttachment(entry: SkinTableEntry): entry is ModelVertexAttachment {
402
+ return entry.kind === 'mesh' || entry.kind === 'boundingbox' || entry.kind === 'clipping' || entry.kind === 'path';
403
+ }
404
+
405
+ /**
406
+ * One slot, as the slot loop computes it. The slot array IS the draw order, so
407
+ * `CompiledModel.slots` is in the rig's declaration order and a draw-order
408
+ * offset counts in it.
409
+ */
410
+ export interface ModelSlot {
411
+ name: string;
412
+ bone: string;
413
+ /** The setup attachment's placeholder; `null` is "shows nothing", which Spine spells by leaving the key out. */
414
+ setup: string | null;
415
+ /**
416
+ * The setup tint as 8-bit hex channels: `rgbaHex` of the motion spec's setup
417
+ * colour (the rounding to the 8-bit grid is value), or the rig's as stated.
418
+ */
419
+ color?: string;
420
+ /** The two-colour tint's dark colour, as stated; a slot with none takes no `rgba2`/`rgb2` timeline. */
421
+ dark?: string;
422
+ /**
423
+ * The blend as stated, which `parseRigSpec` has held to a spelling the
424
+ * runtime resolves (`RigSlotBlend`: only the first letter's case is free).
425
+ */
426
+ blend?: string;
427
+ }
428
+
429
+ /**
430
+ * One skin. `bones` and `constraints` are what it activates (`splitRigSkin`'s
431
+ * member lists, empty for a skin the spec gives none — the manifest's `default`
432
+ * among them); `attachments` is its table, in the spec's order.
433
+ */
434
+ export interface ModelSkin {
435
+ name: string;
436
+ bones: string[];
437
+ constraints: Record<RigSkinConstraintKey, string[]>;
438
+ attachments: SkinTable;
439
+ }
440
+
441
+ /** The five constraint kinds Spine 4.3 has. */
442
+ export type ModelConstraintKind = 'ik' | 'transform' | 'path' | 'physics' | 'slider';
443
+
444
+ /**
445
+ * One constraint: its `kind` and `name`, and every field `buildRigConstraint`
446
+ * (for a rig spec's) or the motion spec's physics table computes, under the
447
+ * name it has in the spec, numbers already `f32`'d.
448
+ *
449
+ * `declaredIn` says which of the two built it, and it is here because the
450
+ * bytes differ: the physics table writes a physics constraint's fields in
451
+ * another order than `buildRigConstraint`'s physics branch — `inertia` …
452
+ * `mix`, then `fps`, `limit`, where the builder writes `limit`, `fps` first —
453
+ * and the key-order table lists neither `fps` nor `limit`, so the order
454
+ * survives into the file (`MS05` plants it). A table constraint holds every
455
+ * component and parameter the spec stated, 0 and the parser's default
456
+ * included; which of them Spine leaves out is the emitter's.
457
+ */
458
+ export type ModelConstraint = {
459
+ kind: ModelConstraintKind;
460
+ name: string;
461
+ declaredIn: 'rig' | 'motion';
462
+ } & Record<string, unknown>;
463
+
464
+ /** An event's payload, as the rig declares it; `float`, `volume`, `balance` `f32`'d. */
465
+ export interface ModelEvent {
466
+ int?: number;
467
+ float?: number;
468
+ string?: string;
469
+ audio?: string;
470
+ volume?: number;
471
+ balance?: number;
472
+ }
473
+
474
+ /**
475
+ * One timeline key, as the timeline compilers in `compile.ts` build it: `time`,
476
+ * then the channels its timeline defines, then `curve` — each in the order the
477
+ * compiler inserted it, which the Spine emitter keeps (a key's field order is a
478
+ * byte wherever the key-order table has no row for its kind).
479
+ *
480
+ * Every number is already on the float32 grid (`keyTime` for `time`, `f32` for
481
+ * the rest), for the reason the header gives.
482
+ *
483
+ * 🔸 **The channel names are Spine's** — `value`, `x`/`y`, `mix`, `mixRotate`,
484
+ * `color`, `offset`/`vertices`, `name`, `mode`/`index`/`delay` — because the
485
+ * motion spec's vocabulary is Spine's (docs/COMPILED_MODEL.md §1.1, *The input
486
+ * vocabulary is already Spine's*; docs/AUTHORING.md, *The vocabulary is
487
+ * Spine's*): a channel is named once, by the format the spec was written
488
+ * against, and a second backend maps from it. A later cut may give the model
489
+ * names of its own; this one does not.
490
+ *
491
+ * 🔸 **`curve` is absolute control points, four per channel, or `'stepped'`.**
492
+ * The points are what `bezierForChannel` computes from an easing's handles (the
493
+ * curve the runtime samples, not the handles an editor shows) or a raw curve's
494
+ * own numbers. `'stepped'` is the format's word for a hold, and the model keeps
495
+ * that word because its one consumer reads the same word: a named easing over a
496
+ * segment whose values do not move is held as `'stepped'` (`easingCurve`,
497
+ * issue #369) — a curve over a flat segment draws nothing, so the two encodings
498
+ * play one animation, and `'stepped'` is the one the editor writes. A later cut
499
+ * may spell the hold another way; this one does not.
500
+ *
501
+ * On an ik key the three flags `bendPositive`, `compress` and `stretch` are
502
+ * the flags IN EFFECT on that key: the key's own where the motion states one,
503
+ * else the constraint's, else the constraint's parser default (issue #273 — the
504
+ * 4.3 parser reads the flags per key without inheriting the constraint's, so
505
+ * which flag a key carries is a value). Which of them Spine writes is the
506
+ * emitter's (`emitAnimations`).
507
+ */
508
+ export type ModelKey = { time: number; curve?: number[] | 'stepped' } & Record<string, unknown>;
509
+
510
+ /** One target's timelines: timeline name (`rotate`, `rgba`, `mix` …) -> its keys, in the spec's order. */
511
+ export type ModelTimelines = Map<string, ModelKey[]>;
512
+
513
+ /** The two timelines an attachment can carry, `deform` compiled before `sequence`. */
514
+ export interface ModelAttachmentTimelines {
515
+ deform?: ModelKey[];
516
+ sequence?: ModelKey[];
517
+ }
518
+
519
+ /**
520
+ * One animation: every timeline the motion spec's animation compiles to, each
521
+ * collection in the order the spec states its tracks (the order the timelines
522
+ * are built in), and the declared duration the compiler verified against the
523
+ * last key.
524
+ *
525
+ * A collection is empty when the animation keys nothing of its kind — `drawOrder`
526
+ * and `events` included, since the compiler refuses an empty key list for both.
527
+ * Which of them a format writes, in what order, and under what spelling is the
528
+ * emitter's.
529
+ *
530
+ * ⭐ **The physics timeline that names no constraint is held under
531
+ * `EVERY_GLOBAL_PHYSICS` (`'*'`, `src/motion.ts`)** — the motion spec's own
532
+ * name for the target that drives every physics constraint declaring the keyed
533
+ * property global (issue #726). It is a key of `constraints.physics` and not a
534
+ * collection of its own because its POSITION among the named physics
535
+ * constraints is a value: `readAnimation` builds the physics timelines in the
536
+ * order it reads them and applies them in that order, and the file interleaves
537
+ * it — a motion keying `wob_b`, then `*`, then `wob` writes `wob_b, "", wob`.
538
+ * `'*'` cannot name a constraint (the rig parse refuses a physics constraint of
539
+ * that name), so the key is unambiguous. Spine spells it as the empty name;
540
+ * that spelling is the emitter's.
541
+ */
542
+ export interface CompiledAnimation {
543
+ /** The motion spec's declared duration, verified by the compiler to be the last key's time within a frame. */
544
+ duration: number;
545
+ /** bone -> its timelines. */
546
+ bones: Map<string, ModelTimelines>;
547
+ /** slot -> its timelines. */
548
+ slots: Map<string, ModelTimelines>;
549
+ /**
550
+ * By constraint kind: `ik` and `transform` hold one unnamed timeline per
551
+ * constraint, so a constraint maps straight to its keys; `path`, `physics`
552
+ * and `slider` hold timelines by name, like a bone.
553
+ */
554
+ constraints: {
555
+ ik: Map<string, ModelKey[]>;
556
+ transform: Map<string, ModelKey[]>;
557
+ path: Map<string, ModelTimelines>;
558
+ physics: Map<string, ModelTimelines>;
559
+ slider: Map<string, ModelTimelines>;
560
+ };
561
+ /** skin -> slot -> attachment placeholder -> its deform and/or sequence keys. */
562
+ attachments: Map<string, Map<string, Map<string, ModelAttachmentTimelines>>>;
563
+ /**
564
+ * Draw-order keys. A key's `offsets` are the moves as the motion spec states
565
+ * them, each resolved and checked; the order the format needs them in (by
566
+ * setup index) is the emitter's. A key with no `offsets` is "back to the setup
567
+ * order".
568
+ */
569
+ drawOrder: ModelKey[];
570
+ /** Event firings, in the spec's (non-decreasing time) order, each naming a declared event. */
571
+ events: ModelKey[];
572
+ }
573
+
574
+ /**
575
+ * The stage the skeleton declares (issue #1026): its origin `x`, `y` and its
576
+ * extent `width`, `height` — the rig spec's `skeleton.width`/`height` (or the
577
+ * manifest's crop), and its `x`/`y` or 0 beside them (issue #578: four fields
578
+ * or none). `null` where the rig declares no stage.
579
+ *
580
+ * ⭐ **Why the model holds it.** `render` and `check` say whether a candidate
581
+ * declares a stage (issue #714), and `A14`/`A19` read its box; until this field
582
+ * the header of `skeleton.json` was the only place the value was written, so a
583
+ * reader of the document had to open the Spine file beside it.
584
+ *
585
+ * 🔁 **And since issue #907 this is the only place it is written.** The Spine
586
+ * emitter used to copy it into the header's `x`, `y`, `width`, `height`,
587
+ * which the format defines as the setup-pose bounding box; the header now
588
+ * carries that box (`headerBoundsOf` in `src/compile.ts`), and every reader of
589
+ * the stage — `A14`, `A19`, `explain` — reads it here.
590
+ */
591
+ export interface ModelStage {
592
+ x: number;
593
+ y: number;
594
+ width: number;
595
+ height: number;
596
+ /**
597
+ * Where the stage also travels in the Spine files, when the rig asked for it
598
+ * (`skeleton.stageBox`, issue #1168): the slot and the attachment name of the
599
+ * bounding box `compile` wrote from these four numbers. Absent where the rig
600
+ * did not ask, so no document written before the field existed moves a byte;
601
+ * a `/3` reader that predates it refuses the field by name rather than
602
+ * reading the document without it. `A50_STAGE_BOX_IS_THE_STAGE` reads it on
603
+ * both suppliers.
604
+ */
605
+ box?: ModelStageBox;
606
+ }
607
+
608
+ /** The stage box a rig asked for (`ModelStage.box`): the slot it is in and its attachment name. */
609
+ export interface ModelStageBox {
610
+ slot: string;
611
+ attachment: string;
612
+ }
613
+
614
+ /**
615
+ * The orders the Spine file lists two collections in, which are not the
616
+ * model's (issue #1026): `skins` — each skin's name and its slot keys — and
617
+ * `animations`, both in the EDITOR's order. `compile` computes it from the
618
+ * model with the same three functions it hands the Spine emitter
619
+ * (`editorSkinOrder`, `editorSlotKeyOrder`, `editorAnimationOrder` in
620
+ * `src/compile.ts`), so the order stated here and the order `skeleton.json`
621
+ * keys are one computation over one input.
622
+ *
623
+ * ⭐ **Why it is a statement of its own rather than the model's arrays
624
+ * re-sorted.** The model's `skins`, a skin's table and `animations` are in the
625
+ * spec's order on purpose (each field's own doc), and everything that reads
626
+ * them reads that order: a draw-order offset, a binding and a constraint
627
+ * count in the model's arrays; `A18` is held, by `MD04`, to see an animations
628
+ * map inserted in another order; and the model-side suppliers of #1025 put the
629
+ * spec's order through the emitter's rules themselves. Measured on the 19
630
+ * recipes `tools/emit_hashes.ts` generates, writing the arrays in the editor's
631
+ * order would have rewritten 18 of the 19 documents (a skin's slot keys on
632
+ * 18, the animations on 5, where 6,308 leaves move to another index) and told
633
+ * every one of those readers something else; stating the order beside them
634
+ * moves no leaf (`docs/COMPILED_MODEL.md` §9 has both counts).
635
+ *
636
+ * A placeholder's position inside one slot is not here: the emitter keeps a
637
+ * slot's own map as built, which is the model's order already.
638
+ */
639
+ export interface ModelEditorOrder {
640
+ /** Every skin, `default` first and the rest by the editor's comparator, each with its table's slot keys in the order the file keys them. */
641
+ skins: Array<{ name: string; slots: string[] }>;
642
+ /** Every animation's name, in the order the file keys `animations`. */
643
+ animations: string[];
644
+ }
645
+
646
+ export interface CompiledModel extends CarriedFromCompileResult {
647
+ /**
648
+ * The stage the skeleton declares, or `null` (issue #1026, `ModelStage`).
649
+ * Not in the Spine header since issue #907, which carries the setup-pose
650
+ * bounding box there.
651
+ */
652
+ stage: ModelStage | null;
653
+ /**
654
+ * The editor's orders the Spine file lists skins, their slot keys and
655
+ * animations in (issue #1026, `ModelEditorOrder`) — computed from this
656
+ * model's own `skins` and `animations`, never read back from the file.
657
+ */
658
+ editorOrder: ModelEditorOrder;
659
+ /**
660
+ * The skeleton's reference scale, which a physics constraint's `wind` and
661
+ * `gravity` act over (issue #958): the rig spec's `skeleton.referenceScale`
662
+ * exactly as stated, or — stating none — the 100 the parser reads a header
663
+ * without one as (`UNSTATED_REFERENCE_SCALE` in `src/emit_spine.ts`). It is
664
+ * the number the Spine file is read as either way: the emitter writes this
665
+ * value into the header and the parser-default pass drops it at 100.
666
+ *
667
+ * ⭐ **Why the model holds it.** A Spine header stating 50 moved 30 of 50
668
+ * wind-and-gravity probes under the stepped oracle, and none without wind
669
+ * or gravity (issue #956); the core read the parser's 100 as a constant
670
+ * until this field. A value any backend posing the rig needs belongs here.
671
+ *
672
+ * 🔸 Not `f32`'d: the runtime reads it as the double the header spells
673
+ * (`SkeletonJson` multiplies it by the loader's `scale` and stores it), so
674
+ * it is the rig's number unchanged, and on the grids `modelDocument`
675
+ * states exactly when the rig spec wrote it on one. It is not a libm
676
+ * result, so no platform moves it; none of the nineteen recipes states one,
677
+ * and all nineteen documents carry 100.
678
+ */
679
+ referenceScale: number;
680
+ /** Every bone, in the rig's declaration order — parents first, as the runtime requires. */
681
+ bones: ModelBone[];
682
+ /** The setup world transform of every bone, computed from `bones`. Never emitted. */
683
+ setupWorld: Map<string, BoneTransform>;
684
+ /** Every slot, in draw order — the rig's declaration order. */
685
+ slots: ModelSlot[];
686
+ /**
687
+ * Every skin, in the order the spec declares them (`default` first when there
688
+ * is one), each with its attachments as model records in the spec's order —
689
+ * NOT the editor's order the emitter sorts skins and slot keys into.
690
+ */
691
+ skins: ModelSkin[];
692
+ /** The rig's constraints in declaration order, then the motion spec's physics table's. */
693
+ constraints: ModelConstraint[];
694
+ /** Event definitions, in the rig's declared order. */
695
+ events: Map<string, ModelEvent>;
696
+ /**
697
+ * Every animation, in the motion spec's order — NOT the editor's order the
698
+ * Spine emitter keys them in (`emitAnimations`).
699
+ */
700
+ animations: Map<string, CompiledAnimation>;
701
+ }
702
+
703
+ // ---------------------------------------------------------------------------
704
+ // the document: `rigc-compiled/3` (issue #922, cut 1f; `pages` and `/2`,
705
+ // issue #1016; `stage`, `editorOrder` and each page's `pma` and `scale`, `/3`,
706
+ // issue #1026)
707
+ // ---------------------------------------------------------------------------
708
+
709
+ /**
710
+ * The document's `spec` value. `/2` since issue #1016 added the `pages`
711
+ * section: a `/1` reader (`readModel` of rigc 1.6) refuses a section it does
712
+ * not know by name, so the same spec over a new section would be refused by
713
+ * every reader already installed rather than read wrongly — a new spec says so
714
+ * before the first section is opened. `/3` since issue #1026 added `stage`,
715
+ * `editorOrder` and two fields on every page, for the same reason: a `/2`
716
+ * reader refuses each of them by name. `readModel` reads all three.
717
+ */
718
+ export const MODEL_DOCUMENT_SPEC = 'rigc-compiled/3';
719
+
720
+ /** The file `build` writes the document to, in `--out` beside `skeleton.json` and `skeleton.atlas`. */
721
+ export const MODEL_DOCUMENT_FILE = 'skeleton.model.json';
722
+
723
+ /** A value the document writes: what `JSON.stringify` reproduces exactly. */
724
+ type DocValue = string | number | boolean | null | DocValue[] | { [key: string]: DocValue };
725
+
726
+ /**
727
+ * `record`'s fields in `keys`' order, each only when present (`undefined` is
728
+ * absent, as `JSON.stringify` reads it). A field `keys` does not list is
729
+ * refused by name rather than dropped: a field added to a model record without
730
+ * a place in the document would otherwise vanish from it in silence.
731
+ */
732
+ function ordered(record: object, keys: readonly string[], where: string, value: (key: string, v: unknown) => DocValue = (_k, v) => plain(v, `${where}.${_k}`)): { [key: string]: DocValue } {
733
+ const own = record as Record<string, unknown>;
734
+ for (const key of Object.keys(own)) {
735
+ if (!keys.includes(key)) {
736
+ throw new CompileError(`internal: the model document has no place for field "${key}" of ${where}; it writes [${keys.join(', ')}]`);
737
+ }
738
+ }
739
+ const out: { [key: string]: DocValue } = {};
740
+ for (const key of keys) if (own[key] !== undefined) out[key] = value(key, own[key]);
741
+ return out;
742
+ }
743
+
744
+ /**
745
+ * A value the model holds, as the document writes it: objects in their own
746
+ * key order, arrays in theirs. A number JSON cannot carry exactly is refused
747
+ * by its path — `-0` (written `0`), `NaN` and the infinities (written `null`)
748
+ * — and so are `undefined` inside an array (written `null`), a `Map` or `Set`
749
+ * (written `{}`), and anything that is not plain data. Each of those would
750
+ * make the document state a value the model does not hold.
751
+ */
752
+ function plain(value: unknown, where: string): DocValue {
753
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
754
+ if (typeof value === 'number') {
755
+ if (!Number.isFinite(value)) throw new CompileError(`internal: the model document cannot carry ${value} at ${where}; JSON writes it as null`);
756
+ if (Object.is(value, -0)) throw new CompileError(`internal: the model document cannot carry -0 at ${where}; JSON writes it as 0`);
757
+ return value;
758
+ }
759
+ if (Array.isArray(value)) {
760
+ return value.map((item, i) => {
761
+ if (item === undefined) throw new CompileError(`internal: the model document cannot carry undefined at ${where}[${i}]; JSON writes it as null`);
762
+ return plain(item, `${where}[${i}]`);
763
+ });
764
+ }
765
+ if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
766
+ const out: { [key: string]: DocValue } = {};
767
+ for (const [key, item] of Object.entries(value)) if (item !== undefined) out[key] = plain(item, `${where}.${key}`);
768
+ return out;
769
+ }
770
+ throw new CompileError(`internal: the model document cannot carry ${Object.prototype.toString.call(value)} at ${where}; it writes plain data only`);
771
+ }
772
+
773
+ /** A `Map` as the array of its entries in the map's order, each `{ name, … }`. */
774
+ function named<V>(map: ReadonlyMap<string, V>, where: string, entry: (value: V, at: string) => { [key: string]: DocValue }): DocValue[] {
775
+ return [...map].map(([name, value]) => ({ name, ...entry(value, `${where}["${name}"]`) }));
776
+ }
777
+
778
+ /** Keys, as the model holds them: each key's own field order is the compiler's, and a byte for the emitter. */
779
+ function keysOf(keys: readonly ModelKey[], where: string): DocValue {
780
+ return plain(keys, where);
781
+ }
782
+
783
+ /** One target's timelines: `[{ name, keys }]`, in the model's order. */
784
+ function timelinesOf(timelines: ModelTimelines, where: string): DocValue[] {
785
+ return named(timelines, where, (keys, at) => ({ keys: keysOf(keys, at) }));
786
+ }
787
+
788
+ /** target -> timelines: `[{ name, timelines }]`, in the model's order. */
789
+ function targetsOf(targets: ReadonlyMap<string, ModelTimelines>, where: string): DocValue[] {
790
+ return named(targets, where, (timelines, at) => ({ timelines: timelinesOf(timelines, at) }));
791
+ }
792
+
793
+ const BONE_FIELDS = ['name', 'parent', 'length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inheritMode', 'skinRequired', 'editor'] as const;
794
+ const SLOT_FIELDS = ['name', 'bone', 'setup', 'color', 'dark', 'blend'] as const;
795
+ const SEQUENCE_FIELDS = ['count', 'start', 'digits', 'setup', 'atlas'] as const;
796
+ const ATLAS_RECT_FIELDS = ['width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight'] as const;
797
+ const BINDING_FIELDS = ['bone', 'x', 'y', 'weight'] as const;
798
+ const EVENT_FIELDS = ['int', 'float', 'string', 'audio', 'volume', 'balance'] as const;
799
+ const CONSTRAINT_KINDS = ['ik', 'transform', 'path', 'physics', 'slider'] as const;
800
+ const ATTACHMENT_FIELDS: Readonly<Record<SkinTableEntry['kind'], readonly string[]>> = {
801
+ mesh: ['kind', 'name', 'path', 'color', 'uvs', 'triangles', 'vertices', 'hull', 'edges', 'width', 'height', 'sequence'],
802
+ boundingbox: ['kind', 'name', 'vertexCount', 'vertices', 'editorColor'],
803
+ clipping: ['kind', 'name', 'end', 'convex', 'inverse', 'vertexCount', 'vertices', 'editorColor'],
804
+ path: ['kind', 'name', 'closed', 'constantSpeed', 'vertexCount', 'vertices', 'lengths', 'editorColor'],
805
+ region: ['kind', 'name', 'path', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color', 'sequence', 'atlas'],
806
+ linkedmesh: ['kind', 'name', 'path', 'source', 'skin', 'slot', 'timelines', 'width', 'height', 'color', 'sequence'],
807
+ };
808
+
809
+ function verticesOf(vertices: ModelVertices, where: string): DocValue {
810
+ return vertices.weighted
811
+ ? ordered(vertices, ['weighted', 'bindings'], where, (key, v) =>
812
+ key === 'bindings'
813
+ ? vertices.bindings.map((influences, i) => influences.map((b, j) => ordered(b, BINDING_FIELDS, `${where}.bindings[${i}][${j}]`)))
814
+ : plain(v, `${where}.${key}`),
815
+ )
816
+ : ordered(vertices, ['weighted', 'xy'], where);
817
+ }
818
+
819
+ function attachmentOf(entry: SkinTableEntry, where: string): DocValue {
820
+ const fields = ATTACHMENT_FIELDS[entry.kind];
821
+ if (fields === undefined) throw new CompileError(`internal: the model document knows no attachment kind "${String(entry.kind)}" at ${where}`);
822
+ if (entry.kind === 'region' && (entry.atlas === undefined) === (entry.sequence === undefined)) {
823
+ throw new CompileError(
824
+ `internal: the region at ${where} carries ${entry.atlas === undefined ? 'neither an atlas rectangle nor a sequence' : 'both an atlas rectangle and a sequence'}; ` +
825
+ 'a region states exactly one of the two, and a sequence holds a rectangle per frame (issue #935)',
826
+ );
827
+ }
828
+ return ordered(entry, fields, where, (key, v) => {
829
+ if (key === 'vertices') return verticesOf(v as ModelVertices, `${where}.vertices`);
830
+ if (key === 'sequence') {
831
+ return ordered(v as object, SEQUENCE_FIELDS, `${where}.sequence`, (k, item) =>
832
+ k === 'atlas' ? (item as ModelAtlasRect[]).map((rect, i) => atlasRectOf(rect, `${where}.sequence.atlas[${i}]`)) : plain(item, `${where}.sequence.${k}`),
833
+ );
834
+ }
835
+ if (key === 'atlas') return v === null ? null : atlasRectOf(v as ModelAtlasRect, `${where}.atlas`);
836
+ return plain(v, `${where}.${key}`);
837
+ });
838
+ }
839
+
840
+ /** One rectangle, in `ModelAtlasRect`'s order, every field required. */
841
+ function atlasRectOf(rect: ModelAtlasRect, where: string): DocValue {
842
+ for (const key of ATLAS_RECT_FIELDS) {
843
+ if (rect[key] === undefined) throw new CompileError(`internal: the atlas rectangle at ${where} has no ${key}; it states all of [${ATLAS_RECT_FIELDS.join(', ')}]`);
844
+ }
845
+ return ordered(rect, ATLAS_RECT_FIELDS, where);
846
+ }
847
+
848
+ function skinOf(skin: ModelSkin, where: string): DocValue {
849
+ return ordered(skin, ['name', 'bones', 'constraints', 'attachments'], where, (key, v) => {
850
+ if (key === 'constraints') return ordered(skin.constraints, CONSTRAINT_KINDS, `${where}.constraints`);
851
+ if (key !== 'attachments') return plain(v, `${where}.${key}`);
852
+ const bySlot: { [slot: string]: DocValue } = {};
853
+ for (const [slot, byPlaceholder] of Object.entries(skin.attachments)) {
854
+ const entries: { [placeholder: string]: DocValue } = {};
855
+ for (const [placeholder, entry] of Object.entries(byPlaceholder)) entries[placeholder] = attachmentOf(entry, `${where}.attachments["${slot}"]["${placeholder}"]`);
856
+ bySlot[slot] = entries;
857
+ }
858
+ return bySlot;
859
+ });
860
+ }
861
+
862
+ /** A constraint: `kind`, `name`, `declaredIn`, then every other field in the builder's order, which is a byte for the emitter. */
863
+ function constraintOf(constraint: ModelConstraint, where: string): DocValue {
864
+ const { kind, name, declaredIn, ...rest } = constraint;
865
+ return { kind, name, declaredIn, ...(plain(rest, where) as { [key: string]: DocValue }) };
866
+ }
867
+
868
+ function animationOf(animation: CompiledAnimation, where: string): { [key: string]: DocValue } {
869
+ return ordered(animation, ['duration', 'bones', 'slots', 'constraints', 'attachments', 'drawOrder', 'events'], where, (key, v) => {
870
+ const at = `${where}.${key}`;
871
+ switch (key) {
872
+ case 'bones':
873
+ case 'slots':
874
+ return targetsOf(v as ReadonlyMap<string, ModelTimelines>, at);
875
+ case 'constraints': {
876
+ const c = animation.constraints;
877
+ return ordered(c, CONSTRAINT_KINDS, at, (kind, byName) =>
878
+ kind === 'ik' || kind === 'transform'
879
+ ? named(byName as ReadonlyMap<string, ModelKey[]>, `${at}.${kind}`, (keys, k) => ({ keys: keysOf(keys, k) }))
880
+ : targetsOf(byName as ReadonlyMap<string, ModelTimelines>, `${at}.${kind}`),
881
+ );
882
+ }
883
+ case 'attachments':
884
+ return named(animation.attachments, at, (bySlot, s) => ({
885
+ slots: named(bySlot, s, (byAttachment, a) => ({
886
+ attachments: named(byAttachment, a, (timelines, t) =>
887
+ ordered(timelines, ['deform', 'sequence'], t, (k, keys) => keysOf(keys as ModelKey[], `${t}.${k}`)),
888
+ ),
889
+ })),
890
+ }));
891
+ case 'drawOrder':
892
+ case 'events':
893
+ return keysOf(v as ModelKey[], at);
894
+ default:
895
+ return plain(v, at);
896
+ }
897
+ });
898
+ }
899
+
900
+ /** The stage's fields, in the order the Spine header writes them. */
901
+ export const MODEL_STAGE_FIELDS = ['x', 'y', 'width', 'height'] as const;
902
+ /** The stage box's fields (issue #1168), in `ModelStageBox`'s order. */
903
+ export const MODEL_STAGE_BOX_FIELDS = ['slot', 'attachment'] as const;
904
+ /** What the `stage` section writes: the four numbers, then the box where the rig asked for one. */
905
+ const STAGE_FIELDS: readonly string[] = [...MODEL_STAGE_FIELDS, 'box'];
906
+
907
+ /**
908
+ * The `editorOrder` section: `{ skins: [{ name, slots }], animations }`,
909
+ * refused by name where it is not a permutation of what the model holds — an
910
+ * order naming a skin, a slot key or an animation the model does not have, or
911
+ * leaving one out, would state an order for another rig.
912
+ */
913
+ function editorOrderOf(model: CompiledModel): DocValue {
914
+ const order = model.editorOrder;
915
+ const problems: string[] = [];
916
+ const same = (what: string, stated: readonly string[], held: readonly string[]): void => {
917
+ if (JSON.stringify([...stated].sort()) !== JSON.stringify([...held].sort())) problems.push(`${what} lists [${stated.join(', ')}], the model holds [${held.join(', ')}]`);
918
+ };
919
+ same('editorOrder.skins', order.skins.map((s) => s.name), model.skins.map((s) => s.name));
920
+ for (const [i, entry] of order.skins.entries()) {
921
+ const skin = model.skins.find((s) => s.name === entry.name);
922
+ if (skin !== undefined) same(`editorOrder.skins[${i}] "${entry.name}".slots`, entry.slots, Object.keys(skin.attachments));
923
+ }
924
+ same('editorOrder.animations', order.animations, [...model.animations.keys()]);
925
+ if (problems.length > 0) throw new CompileError(`internal: the model's editor order is not its own: ${problems.join('; ')}`);
926
+ return {
927
+ skins: order.skins.map((entry, i) => ordered(entry, ['name', 'slots'], `editorOrder.skins[${i}]`)),
928
+ animations: plain(order.animations, 'editorOrder.animations'),
929
+ };
930
+ }
931
+
932
+ /** The model's fields the document writes, after `spec`, in its key order. */
933
+ const MODEL_DOCUMENT_FIELDS: readonly string[] = [
934
+ 'referenceScale', 'stage', 'bones', 'slots', 'skins', 'constraints', 'events', 'animations', 'editorOrder',
935
+ 'images', 'pageGrids', 'droppedStates', 'absentParts', 'meshBones', 'meshes', 'physics', 'deformTransforms', 'trackDerivations', 'rig',
936
+ ];
937
+
938
+ /** The model's fields the document leaves out — see `modelDocument`. */
939
+ const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
940
+
941
+ /**
942
+ * The compiled model as a document: `rigc-compiled/3`, `JSON.stringify(doc,
943
+ * null, 2)` and a newline — the text `build` writes to `skeleton.model.json`,
944
+ * and the record rigc's own posing core reads (`readModel` in
945
+ * `src/core/index.ts`, issue #380, step 2). Its cost in a build is measured in
946
+ * docs/COMPILED_MODEL.md §6 (issue #926).
947
+ *
948
+ * **Key order.** `spec`, then the model's fields in the order this file
949
+ * declares them — `referenceScale` (issue #958), `stage` (issue #1026, the
950
+ * header's four fields or `null`), `bones`, `slots`, `skins`, `constraints`,
951
+ * `events`, `animations`, `editorOrder` (issue #1026, `{ skins: [{ name,
952
+ * slots }], animations }`) — then the fields carried from `CompileResult` in
953
+ * `CarriedFromCompileResult`'s order: `images`, `pageGrids`, `droppedStates`,
954
+ * `absentParts`, `meshBones`, `meshes`, `physics`, `deformTransforms`,
955
+ * `trackDerivations`, `rig`; then `pages`, where each region sits on its page
956
+ * in the atlas written beside it, and each page's `pma` and `scale` (issue
957
+ * #1026) (`pagesOfAtlas`, issue #1016), which is why
958
+ * the atlas text is the third argument; and last `spine`, the digest of the
959
+ * `skeleton.json` written beside it (`spineFileSha256`, issue #968), which is
960
+ * why the Spine text is the second argument. Inside a model record, its interface's field
961
+ * order (`ModelBone`, `ModelSlot`, `ModelSkin`, each attachment kind,
962
+ * `ModelBinding`, `ModelSequence`, `ModelAtlasRect`, `ModelEvent`, `CompiledAnimation`), each
963
+ * field only when the record carries it, and a field the interface does not
964
+ * list refused by name. Three kinds of record keep their own order, because
965
+ * there the order is the value: a constraint's fields after `kind`, `name`,
966
+ * `declaredIn` (the builder's order, which the emitter keeps and which reaches
967
+ * the file where the key-order table has no row), a timeline key's fields
968
+ * (`time`, its channels, `curve`, as the compiler inserted them), and the
969
+ * carried reports, written as `compile` builds them.
970
+ *
971
+ * **Collections.** Every `Map` is an array of `{ name, … }` in the map's
972
+ * order, and that order is the model's own: bones parents first (a weighted
973
+ * binding's index counts in it), slots the draw order (a draw-order offset
974
+ * counts in it), skins, constraints, events and animations in the spec's
975
+ * declared order, timelines and keys in the order they are applied. A skin's
976
+ * attachment table stays an object keyed slot -> placeholder, as the model
977
+ * holds it. An animation writes `bones` and `slots` as `[{ name, timelines:
978
+ * [{ name, keys }] }]`, its `ik` and `transform` constraints as `[{ name, keys
979
+ * }]` and the other three kinds like a bone, and `attachments` as `[{ name:
980
+ * skin, slots: [{ name, attachments: [{ name, deform?, sequence? }] }] }]`;
981
+ * the physics timeline that names no constraint keeps the model's name for it,
982
+ * `*` (`EVERY_GLOBAL_PHYSICS`), not the empty name Spine spells it with.
983
+ *
984
+ * **Numbers** are written exactly as the model holds them, so nothing is
985
+ * re-rounded here; which of them are float32 values, and how a reader reads
986
+ * the rest, is the header's 🔸. A number JSON cannot carry exactly — `-0`,
987
+ * `NaN`, an infinity — is refused by its path (`plain`).
988
+ *
989
+ * 🔒 **Every number the document spells is on one of two grids**: a fixed
990
+ * point of the compiler's float32 spelling (`f32(x) === x` — the shortest
991
+ * decimal naming a float, which is not the float's own double) or of the
992
+ * six-decimal grid (`Math.round(x·1e6)/1e6 === x`). `MX01` in `selftest.ts`
993
+ * holds it over every document the tree's recipes build, with a full double
994
+ * planted to turn it red by its path. One field is the rig spec's number
995
+ * carried unchanged rather than computed, `referenceScale` (issue #958): the
996
+ * runtime reads the header's double, so rounding it here would pose another
997
+ * skeleton; it is on a grid when the spec wrote it on one. A number on neither grid is a full
998
+ * double whose last digits are the platform libm's, which is how the rule was
999
+ * found (issue #942): `meshes[].depth.ceiling` carried `turnCeiling`'s fold
1000
+ * figures (`src/depth.ts`) as full doubles from `Math.atan` and the depth
1001
+ * tone, and `gallery/look`'s document differed between macOS and the Linux
1002
+ * runner by exactly `/meshes/0/depth/ceiling/pitch/negative/{degrees,p1}`,
1003
+ * `26.935130523311` against `26.935130523311003`, while its Spine files were
1004
+ * identical. Those figures are reported on the six-decimal grid now, and a
1005
+ * one-ulp `Math.atan` perturbation either way moves no byte of that document.
1006
+ *
1007
+ * ⚠️ A grid absorbs a difference in a value, not in a choice. Perturbing
1008
+ * `Math.pow` by one ulp moved `gallery/look`'s document until issue #949: the
1009
+ * depth tone reaches every `z`, two triangles whose fold angles round to the
1010
+ * same six-decimal value traded places as the minimum, and the fold named the
1011
+ * other triangle, with its own `depthStep` and `stepShare`. The minimum is
1012
+ * now chosen on the six-decimal grid with the lowest triangle ordinal taking
1013
+ * a tie (`src/depth.ts`'s `foldPrecedes`), and a one-ulp perturbation of
1014
+ * sixteen libm functions, either way, moves no leaf of any gallery document
1015
+ * (`TB02`). What is left is a value within one ulp of a rounding boundary.
1016
+ *
1017
+ * **Left out, and why.** Each is something the model holds that is not a
1018
+ * statement about the rig:
1019
+ *
1020
+ * - `setupWorld` — computed from `bones` by `computeWorldTransforms`
1021
+ * (`src/transform.ts`), so it states nothing `bones` does not, and a core
1022
+ * is to compute it rather than read it; and it is the one field that can
1023
+ * hold a `-0` (a bone's `c` is `sin 0 · scaleX`, which is `-0` at a
1024
+ * `scaleX` of −1), which JSON cannot spell. (A root's `b` was `-sin 0`
1025
+ * until issue #1021; under the runtime's arithmetic it is `cos 90°` at
1026
+ * pi = 3.1415927, −2.3e-8.)
1027
+ * - `images[].absPath` — where the part was on this machine's disk, for the
1028
+ * size assertions.
1029
+ * - `droppedStates[].why` — the sentence names the `--atlas-in` file by its
1030
+ * absolute path.
1031
+ *
1032
+ * What the Spine emitter adds (`emitSkeleton`'s header, its spellings and
1033
+ * omissions) is not in the model and so not here — except the two things the
1034
+ * Spine files were the only place of until issue #1026, which the model now
1035
+ * holds and the emitter reads from it: the stage, and the orders the editor
1036
+ * lists skins, slot keys and animations in (`editorOrder`, computed by the
1037
+ * functions the emitter is handed). The header's `spine`, `fps`, `images` and
1038
+ * `audio` are still the emitter's alone.
1039
+ */
1040
+ export function modelDocument(model: CompiledModel, skeletonText: string, atlasText: string): string {
1041
+ for (const key of Object.keys(model)) {
1042
+ if (!MODEL_DOCUMENT_FIELDS.includes(key) && !MODEL_DOCUMENT_LEFT_OUT.includes(key)) {
1043
+ throw new CompileError(`internal: the model document has no place for the model's field "${key}"; it writes [${MODEL_DOCUMENT_FIELDS.join(', ')}] and leaves out [${MODEL_DOCUMENT_LEFT_OUT.join(', ')}]`);
1044
+ }
1045
+ }
1046
+ const doc: { [key: string]: DocValue } = {
1047
+ spec: MODEL_DOCUMENT_SPEC,
1048
+ referenceScale: plain(model.referenceScale, 'referenceScale'),
1049
+ stage: model.stage === null ? null : ordered(model.stage, STAGE_FIELDS, 'stage', (key, v) => (key === 'box' ? ordered(v as object, MODEL_STAGE_BOX_FIELDS, 'stage.box') : plain(v, `stage.${key}`))),
1050
+ bones: model.bones.map((bone, i) =>
1051
+ ordered(bone, BONE_FIELDS, `bones[${i}]`, (key, v) => (key === 'editor' ? ordered(v as object, ['color', 'icon'], `bones[${i}].editor`) : plain(v, `bones[${i}].${key}`))),
1052
+ ),
1053
+ slots: model.slots.map((slot, i) => ordered(slot, SLOT_FIELDS, `slots[${i}]`)),
1054
+ skins: model.skins.map((skin, i) => skinOf(skin, `skins[${i}]`)),
1055
+ constraints: model.constraints.map((constraint, i) => constraintOf(constraint, `constraints[${i}]`)),
1056
+ events: named(model.events, 'events', (event, at) => ordered(event, EVENT_FIELDS, at)),
1057
+ animations: named(model.animations, 'animations', (animation, at) => animationOf(animation, at)),
1058
+ editorOrder: editorOrderOf(model),
1059
+ images: plain(model.images.map(({ absPath: _absPath, ...image }) => image), 'images'),
1060
+ pageGrids: plain(model.pageGrids, 'pageGrids'),
1061
+ droppedStates: plain(model.droppedStates.map(({ why: _why, ...state }) => state), 'droppedStates'),
1062
+ absentParts: plain(model.absentParts, 'absentParts'),
1063
+ meshBones: plain(model.meshBones, 'meshBones'),
1064
+ meshes: plain(model.meshes, 'meshes'),
1065
+ physics: plain(model.physics, 'physics'),
1066
+ deformTransforms: plain(model.deformTransforms, 'deformTransforms'),
1067
+ trackDerivations: plain(model.trackDerivations, 'trackDerivations'),
1068
+ rig: plain(model.rig, 'rig'),
1069
+ pages: plain(pagesOfAtlas(atlasText), 'pages'),
1070
+ spine: { sha256: spineFileSha256(skeletonText) },
1071
+ };
1072
+ return `${JSON.stringify(doc, null, 2)}\n`;
1073
+ }
1074
+
1075
+ // --- #1016 where each region sits on its page: begin ---
1076
+ /**
1077
+ * One region of a page, as the document's `pages` section states it: the
1078
+ * region's name exactly as the atlas line spells it (untrimmed — the core finds
1079
+ * a region by that spelling, `./core/uvs.ts`), its rectangle's top-left `x`,
1080
+ * `y` on the page (y down) and `width`, `height` in the drawing's orientation,
1081
+ * the trim (`offsetX` from the drawing's left, `offsetY` from its bottom) and
1082
+ * the untrimmed size, its turn in `degrees`, and its `index:` field — every
1083
+ * number in the page's texels, exactly as `parseAtlasText` reads it.
1084
+ */
1085
+ export interface ModelPageRegion {
1086
+ name: string;
1087
+ x: number;
1088
+ y: number;
1089
+ width: number;
1090
+ height: number;
1091
+ offsetX: number;
1092
+ offsetY: number;
1093
+ originalWidth: number;
1094
+ originalHeight: number;
1095
+ degrees: number;
1096
+ index: number;
1097
+ }
1098
+
1099
+ /**
1100
+ * One page: its name (a path from the directory `build` writes into, trimmed),
1101
+ * its `size`, its `pma` and `scale` (issue #1026), and its regions in file
1102
+ * order.
1103
+ *
1104
+ * `pma` and `scale` are present on every page `pagesOfAtlas` spells and on
1105
+ * every page a `rigc-compiled/3` document states; a `rigc-compiled/2`
1106
+ * document's pages carry neither, which is why they are optional here.
1107
+ */
1108
+ export interface ModelPage {
1109
+ name: string;
1110
+ width: number;
1111
+ height: number;
1112
+ /** Whether the page's texels are premultiplied, as the runtime reads the page's `pma:` line (absent: false). */
1113
+ pma?: boolean;
1114
+ /**
1115
+ * The number the page's own `scale:` line states, or `null` where the page
1116
+ * has none. Not defaulted to the 1 a page without the line is read as: the
1117
+ * line is an importer's instruction ("these texels are this much smaller than
1118
+ * the drawings"), `check` reports a declared one (issue #171), and a default
1119
+ * would state a line the atlas does not have.
1120
+ */
1121
+ scale?: number | null;
1122
+ regions: ModelPageRegion[];
1123
+ }
1124
+
1125
+ /** The fields of a page and of a region, in the order the document writes them — `readModel` mirrors both lists. */
1126
+ export const MODEL_PAGE_FIELDS = ['name', 'width', 'height', 'pma', 'scale', 'regions'] as const;
1127
+ export const MODEL_PAGE_REGION_FIELDS = ['name', 'x', 'y', 'width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight', 'degrees', 'index'] as const;
1128
+
1129
+ /**
1130
+ * The document's `pages` section (issue #1016): every page of `atlasText` and
1131
+ * every region on it, in file order, with the numbers the draw reads — where
1132
+ * each drawing sits, which `./core/uvs.ts` turns into page UVs. `atlasText`
1133
+ * is the atlas `build` writes beside the document, the one that run's gate
1134
+ * last read: after `--pack` the packed text, after `--copy-images` the text
1135
+ * with the copies' page names.
1136
+ *
1137
+ * ⭐ **Why it is a function of the written atlas rather than a model field.**
1138
+ * Issue #939 left the page, `x`, `y` and `rotate` out of `ModelAtlasRect`
1139
+ * because `--pack` moves them after `compile` returns and `--copy-images`
1140
+ * renames every page — a value the model held from `compile` would state a
1141
+ * place the written atlas does not have. The section is spelled from the
1142
+ * atlas text instead, so it moves exactly when that text does, and `build`
1143
+ * spells the document from the text it writes (`cli.ts`): the document a pack
1144
+ * writes is spelled from the packed text and gated by the packed pass, where
1145
+ * `A18` compares it with a second compile's document spelled from a second,
1146
+ * independent pack.
1147
+ *
1148
+ * 🔸 **What it leaves out.** The page's `format`, `filter` and `repeat`
1149
+ * lines: nothing that draws or checks reads them (the rasteriser samples one
1150
+ * way, `src/render.ts`'s header; the pose reads only the ratios the trimmed
1151
+ * and original sizes make, #939's decision 2). `pma` and `scale` were left out
1152
+ * with them until issue #1026: they are not placement either, but `A06` reads
1153
+ * `pma` and `check`'s texture note reads `scale:`, and the atlas was the only
1154
+ * place either was written. Every region of every page is written, drawn or
1155
+ * not: which regions a rig draws is the core's lookup rule (`./core/uvs.ts`,
1156
+ * *Which region, on which page*), and a writer choosing a subset would
1157
+ * restate it.
1158
+ */
1159
+ export function pagesOfAtlas(atlasText: string): ModelPage[] {
1160
+ const parsed = parseAtlasText(atlasText);
1161
+ return parsed.pages.map((page) => ({
1162
+ name: page.name,
1163
+ width: page.width,
1164
+ height: page.height,
1165
+ pma: page.pma,
1166
+ scale: statedPageScale(parsed.lines, page.nameLine),
1167
+ regions: page.regions.map((r) => ({
1168
+ name: r.name,
1169
+ x: r.x,
1170
+ y: r.y,
1171
+ width: r.width,
1172
+ height: r.height,
1173
+ offsetX: r.offsetX,
1174
+ offsetY: r.offsetY,
1175
+ originalWidth: r.originalWidth,
1176
+ originalHeight: r.originalHeight,
1177
+ degrees: r.degrees,
1178
+ index: r.index,
1179
+ })),
1180
+ }));
1181
+ }
1182
+
1183
+ /**
1184
+ * A `scale:` line, as `check` has always read one (`atlasScales` in
1185
+ * `src/render.ts` reads every such line in a text with this pattern): an
1186
+ * indented `scale:` entry and one number. Shared so the two readings — every
1187
+ * line of a text, and the lines of one page — cannot drift into two patterns.
1188
+ */
1189
+ export const ATLAS_SCALE_LINE = /^[ \t]+scale:[ \t]*([0-9.eE+-]+)[ \t]*$/;
1190
+
1191
+ /**
1192
+ * The number a page's own `scale:` line states, or `null` (issue #1026). The
1193
+ * page's entries are the lines after its name up to the first line that is
1194
+ * blank or carries no colon — where `TextureAtlasReader` stops reading a page's
1195
+ * fields, and where `parseAtlasText` stops too. A line `ATLAS_SCALE_LINE`
1196
+ * matches and whose number is finite is the statement; the last one wins, as a
1197
+ * repeated entry does in `parseAtlasText`. Unlike `AtlasPage.scale`, nothing is
1198
+ * defaulted and a non-positive number is stated as written, because this is
1199
+ * the line `check` reports, not the ratio an importer divides by.
1200
+ */
1201
+ function statedPageScale(lines: readonly string[], nameLine: number): number | null {
1202
+ let stated: number | null = null;
1203
+ for (let at = nameLine + 1; at < lines.length; at++) {
1204
+ const line = lines[at];
1205
+ const trimmed = line.trim();
1206
+ if (trimmed.length === 0 || !trimmed.includes(':')) break;
1207
+ const m = ATLAS_SCALE_LINE.exec(line);
1208
+ if (m === null) continue;
1209
+ const value = Number(m[1]);
1210
+ if (Number.isFinite(value)) stated = value;
1211
+ }
1212
+ return stated;
1213
+ }
1214
+ // --- #1016 where each region sits on its page: end ---
1215
+
1216
+ // --- #968 the Spine file the document was written beside: begin ---
1217
+ /**
1218
+ * The document's last section, `spine`: `{ "sha256": "<64 lowercase hex>" }`,
1219
+ * the SHA-256 of the exact bytes `build` writes to `skeleton.json` in the same
1220
+ * run (the UTF-8 of `skeletonText`, as `writeFileSync` writes it).
1221
+ *
1222
+ * ⭐ Why it exists (issue #968). `build` writes the Spine pair and this document
1223
+ * as one output, and `rigc render` poses the document through rigc's own core
1224
+ * when it finds one beside the skeleton. Nothing else ties the two files: a
1225
+ * `skeleton.json` edited by hand after the build, beside the document it was
1226
+ * built with, was drawn from the document — the build's rig, not the file the
1227
+ * render was pointed at (the `RF89`/`RF91`/`RF93` plants, exit 0 where the
1228
+ * Spine file refuses). The render takes the core only when the file beside the
1229
+ * document hashes to this value, and names the mismatch otherwise.
1230
+ *
1231
+ * Named `spine.sha256` rather than a top-level `skeletonSha256`: the section
1232
+ * is the document's statement about the Spine output it was written with, a
1233
+ * digest of it and nothing about the rig, so it sits apart from the model's
1234
+ * fields and after them; and an object leaves the atlas's digest a field to
1235
+ * add, not a second section. The file's NAME is not recorded — `build` always
1236
+ * writes `skeleton.json` beside this document, and a copied pair keeps both.
1237
+ *
1238
+ * Deterministic because the Spine file is (`A18` compares a second compile's
1239
+ * bytes), so two builds and two platforms write the same value wherever their
1240
+ * Spine files agree.
1241
+ */
1242
+ export function spineFileSha256(skeletonText: string | Uint8Array): string {
1243
+ return createHash('sha256').update(skeletonText).digest('hex');
1244
+ }
1245
+ // --- #968 the Spine file the document was written beside: end ---