spine-rigc 1.5.3 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/model.ts ADDED
@@ -0,0 +1,958 @@
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/1`, and `build` writes that text into `--out` as
86
+ * `skeleton.model.json` beside the Spine files, after the gate, like them.
87
+ */
88
+ import { createHash } from 'node:crypto';
89
+ import { CompileError } from './errors.ts';
90
+ import type { BoneTransform } from './transform.ts';
91
+ import type { RigSkinConstraintKey } from './rig.ts';
92
+ import type { CompileResult } from './types.ts';
93
+
94
+ /**
95
+ * One bone, as `buildBone` computes it — a field is present exactly when the rig
96
+ * spec declared it (or, for `x`/`y`/`rotation`, when `from` supplied it), and
97
+ * every number is already `f32`'d.
98
+ */
99
+ export interface ModelBone {
100
+ name: string;
101
+ parent?: string;
102
+ length?: number;
103
+ /** Local to the parent, y up. Solved from the manifest when the spec says `from`. */
104
+ x?: number;
105
+ y?: number;
106
+ /** Degrees, CCW, y up. */
107
+ rotation?: number;
108
+ scaleX?: number;
109
+ scaleY?: number;
110
+ shearX?: number;
111
+ shearY?: number;
112
+ /**
113
+ * The inherit MODE, as the spec states it (the rig parse admits the runtime's
114
+ * first-letter fold, so `noScale` and `NoScale` both reach here). What key the
115
+ * mode is written under is the emitter's: Spine 4.3 spells it `inherit`.
116
+ */
117
+ inheritMode?: string;
118
+ /** The bone is inactive unless the applied skin names it. Spine 4.3 spells it `skin`. */
119
+ skinRequired?: boolean;
120
+ /** Editor affordances: no pose reads them. */
121
+ editor?: { color?: string; icon?: string };
122
+ }
123
+
124
+ /**
125
+ * The fields of `CompileResult` the model carries as they are today, by
126
+ * reference. A later cut replaces one of these with a model-side form by moving
127
+ * it out of this list.
128
+ */
129
+ export type CarriedFromCompileResult = Pick<
130
+ CompileResult,
131
+ | 'images'
132
+ | 'pageGrids'
133
+ | 'droppedStates'
134
+ | 'absentParts'
135
+ | 'meshBones'
136
+ | 'meshes'
137
+ | 'physics'
138
+ | 'deformTransforms'
139
+ | 'trackDerivations'
140
+ | 'rig'
141
+ >;
142
+
143
+ /**
144
+ * One influence of a weighted vertex: the bone by NAME, the vertex in that bone's
145
+ * local setup space, and the share it carries. Every number is exactly what the
146
+ * emitted run holds — `f32` (and, for a generated mesh, the generator's `r6` on
147
+ * the weight) already applied where the builder applies it.
148
+ */
149
+ export interface ModelBinding {
150
+ bone: string;
151
+ x: number;
152
+ y: number;
153
+ weight: number;
154
+ }
155
+
156
+ /**
157
+ * A vertex attachment's geometry, in one of the format's two encodings.
158
+ *
159
+ * `weighted` is said outright. Spine's reader chooses the encoding by comparing
160
+ * the run's length with the vertex count and nothing else, so a run of the
161
+ * wrong length is read as the other encoding without a word; the model does not
162
+ * inherit that, and the Spine emitter is the one place the choice becomes a
163
+ * length again.
164
+ *
165
+ * - unweighted: `xy`, one `x, y` per vertex in the attachment's own space (the
166
+ * slot bone's), the numbers exactly as the emitted array carries them;
167
+ * - weighted: `bindings`, per vertex its influences in order, each naming its bone.
168
+ */
169
+ export type ModelVertices = { weighted: false; xy: number[] } | { weighted: true; bindings: ModelBinding[][] };
170
+
171
+ /**
172
+ * A numbered series of atlas regions an attachment draws in turn: the four
173
+ * fields exactly as the spec stated them, each optional one only when stated.
174
+ * Which of them Spine leaves out at the parser's default is the emitter's
175
+ * (`emitSequenceBlock`).
176
+ */
177
+ export interface ModelSequence {
178
+ count: number;
179
+ start?: number;
180
+ digits?: number;
181
+ setup?: number;
182
+ }
183
+
184
+ /**
185
+ * The rectangle a region draws through, in the atlas's own numbers and under
186
+ * the names the atlas's `bounds:` and `offsets:` lines carry (issue #935): the
187
+ * kept rectangle's `width` and `height`, in the drawing's orientation; the
188
+ * trim's `offsetX` (from the drawing's left) and `offsetY` (from its bottom);
189
+ * and the untrimmed drawing's `originalWidth` and `originalHeight`. All six are
190
+ * in the page's texels, exactly as the atlas states them (`AtlasRegion` in
191
+ * `src/atlas.ts`) — NOT divided by a page's `scale:`, because the pose reads
192
+ * only their ratios to the record's `width` and `height`.
193
+ *
194
+ * ⭐ **Why the model holds it.** A region's four corners are
195
+ * `x1 = -W/2·sx + offsetX·W/originalWidth·sx`, `x2 = x1 + width·W/originalWidth·sx`
196
+ * (and the same in y) before the record's placement and the bone — measured
197
+ * exact on 3000 of 3000 probes against spine-core 4.3.13, and 976 of 3000 with
198
+ * the trim ignored (issue #931). The trim and the original size live only in
199
+ * the atlas, so until this record held them one model document posed two ways:
200
+ * `examples/3-timing-and-spacing` built against two atlases differing only in
201
+ * `square`'s trim wrote byte-identical `skeleton.model.json` and
202
+ * `skeleton.json`, and spine-core moved that region's corners by 11.925 world
203
+ * units. A value any backend posing the rig needs belongs in the model.
204
+ *
205
+ * 🔸 **What it leaves out, and why.** The page, the rectangle's `x`/`y` on it
206
+ * and its `rotate` are WHERE the drawing sits in one arrangement of the pixels,
207
+ * not what the drawing is: `build --pack` repacks every part onto shared pages
208
+ * after this document is spelled, and `--copy-images` renames every page, so
209
+ * those four would state a place the written atlas does not have on two of
210
+ * the three routes that write one. None of them enters a region's corners
211
+ * (#931: the atlas's `rotate` transposed into the corners was exact on 2018 of
212
+ * 3000 probes, ignored on 3000 of 3000). The trim and the original size do not
213
+ * move under either: rigc's packer never trims or rotates (`src/atlas.ts`).
214
+ * The draw, which does need the four — a region's page and page UVs — reads
215
+ * them off the atlas written beside this document, as a second input
216
+ * (`src/core/uvs.ts`, issue #967): measured at tolerance 0 against the
217
+ * runtime's own arrays on every corpus row with the model unchanged.
218
+ */
219
+ export interface ModelAtlasRect {
220
+ width: number;
221
+ height: number;
222
+ offsetX: number;
223
+ offsetY: number;
224
+ originalWidth: number;
225
+ originalHeight: number;
226
+ }
227
+
228
+ /**
229
+ * A region's sequence: the four stated fields, and `atlas`, one rectangle per
230
+ * frame in frame order — frame `i` is the region `<path><start + i>`, padded
231
+ * to `digits`, and the runtime draws each frame through its own rectangle
232
+ * (`Sequence.apply` sets the region the corners are computed from).
233
+ *
234
+ * A parallel array rather than a `frames` list of objects: a frame's region
235
+ * NAME is derived from the four fields, so the rectangle is the one thing per
236
+ * frame the record does not already state. Never `null`: the gather pass
237
+ * atlases every frame of every sequence or refuses the missing one by name
238
+ * before any attachment is built. A mesh's or a linked mesh's sequence does not
239
+ * carry it — a mesh's vertices read no atlas (#931, 400 of 400 meshes exact
240
+ * with none), and neither kind's record carries a rectangle.
241
+ */
242
+ export interface ModelRegionSequence extends ModelSequence {
243
+ atlas: ModelAtlasRect[];
244
+ }
245
+
246
+ /**
247
+ * A mesh: `buildRigMesh`'s authored geometry, the five generators' output, and a
248
+ * manifest part's ring or ribbon. Fields as the builders compute them today.
249
+ */
250
+ export interface ModelMeshAttachment {
251
+ kind: 'mesh';
252
+ /** The spec's own `name`, present exactly when stated (issue #796). */
253
+ name?: string;
254
+ /**
255
+ * The atlas region, present exactly when `attachmentPath` in `compile.ts`
256
+ * returns one: stated, or derived from `image` where the basename differs from
257
+ * the name the attachment carries. See the header's ⚠️ — resolving it to the
258
+ * region always is a later decision, and so is moving that omission here.
259
+ */
260
+ path?: string;
261
+ color?: string;
262
+ uvs: number[];
263
+ triangles: number[];
264
+ vertices: ModelVertices;
265
+ hull: number;
266
+ edges: number[];
267
+ width: number;
268
+ height: number;
269
+ sequence?: ModelSequence;
270
+ }
271
+
272
+ /** A bounding box: a polygon and nothing else. */
273
+ export interface ModelBoundingBoxAttachment {
274
+ kind: 'boundingbox';
275
+ name?: string;
276
+ vertexCount: number;
277
+ vertices: ModelVertices;
278
+ /** The editor's display colour; no pose reads it. Spine writes it as `color`. */
279
+ editorColor?: string;
280
+ }
281
+
282
+ /** A clipping polygon, and the slot its clip ends at. */
283
+ export interface ModelClippingAttachment {
284
+ kind: 'clipping';
285
+ name?: string;
286
+ end?: string;
287
+ convex?: boolean;
288
+ inverse?: boolean;
289
+ vertexCount: number;
290
+ vertices: ModelVertices;
291
+ editorColor?: string;
292
+ }
293
+
294
+ /** A path: knots and their handles, and the cumulative curve lengths stated or measured on the setup pose. */
295
+ export interface ModelPathAttachment {
296
+ kind: 'path';
297
+ name?: string;
298
+ closed?: boolean;
299
+ constantSpeed?: boolean;
300
+ vertexCount: number;
301
+ vertices: ModelVertices;
302
+ lengths: number[];
303
+ editorColor?: string;
304
+ }
305
+
306
+ /** The four attachment kinds that carry a vertex array — the ones a deform timeline can key. */
307
+ export type ModelVertexAttachment =
308
+ | ModelMeshAttachment
309
+ | ModelBoundingBoxAttachment
310
+ | ModelClippingAttachment
311
+ | ModelPathAttachment;
312
+
313
+ /**
314
+ * A region: one quad of one atlas region (or of a numbered series), placed on
315
+ * the slot's bone. `buildRigRegion` for a rig spec's, `placeRegion` for a
316
+ * manifest part's.
317
+ *
318
+ * `x`, `y`, `rotation`, `scaleX`, `scaleY` are present exactly when the spec
319
+ * stated them — and, for a manifest part, `x`, `y` and `rotation` ALWAYS, since
320
+ * `placeRegion` computes all three and 0 is a placement like any other. The
321
+ * Spine emitter leaves a 0 out (`emitRegion`).
322
+ */
323
+ export interface ModelRegionAttachment {
324
+ kind: 'region';
325
+ name?: string;
326
+ /** As on a mesh: present exactly when `attachmentPath` returns one (see the header's ⚠️). */
327
+ path?: string;
328
+ x?: number;
329
+ y?: number;
330
+ rotation?: number;
331
+ scaleX?: number;
332
+ scaleY?: number;
333
+ width: number;
334
+ height: number;
335
+ color?: string;
336
+ sequence?: ModelRegionSequence;
337
+ /**
338
+ * The rectangle this region draws through (`ModelAtlasRect`), present exactly
339
+ * when the record has no `sequence` — a sequence's frames carry theirs. It is
340
+ * looked up under the region NAME the runtime asks for — `path`, else the
341
+ * stated `name`, else the placeholder — in the build's one atlas source:
342
+ *
343
+ * - `--atlas-in`: the pack's region of that name, first match (as
344
+ * `TextureAtlas.findRegion` resolves it), its numbers as the pack states
345
+ * them;
346
+ * - otherwise: the part PNG atlased under that name, which is its own page —
347
+ * `width`/`height` and `originalWidth`/`originalHeight` the PNG's size,
348
+ * offsets 0. `build --pack` keeps all six (it never trims);
349
+ * - `null` when the source has no region of that name — an ingested region
350
+ * built without `--atlas-in`, or one naming a region the pack lacks. That
351
+ * is a statement that the build has no source, never a trim of 0, and a
352
+ * green build never carries one: the emitted atlas has no such region
353
+ * either, and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refuses the build.
354
+ */
355
+ atlas?: ModelAtlasRect | null;
356
+ }
357
+
358
+ /**
359
+ * A linked mesh: another mesh's geometry under a region of its own.
360
+ *
361
+ * The link is held IN FULL — `skin`, `slot` and `timelines` as the parser
362
+ * resolves them, the defaults applied: `skin` "default" where none was stated,
363
+ * `slot` the attachment's own slot, `timelines` true unless stated false. The
364
+ * Spine emitter leaves out each one that equals the parser's fallback
365
+ * (`emitLinkedMesh`). Whether the author wrote a key or took its default is a
366
+ * fact only the refusals need, and it stays on `compile.ts`'s `PendingLink`.
367
+ */
368
+ export interface ModelLinkedMeshAttachment {
369
+ kind: 'linkedmesh';
370
+ name?: string;
371
+ /** As on a mesh: present exactly when `attachmentPath` returns one (see the header's ⚠️). */
372
+ path?: string;
373
+ /** The PLACEHOLDER of the source mesh in `skin`/`slot`. */
374
+ source: string;
375
+ skin: string;
376
+ slot: string;
377
+ /** Whether the link plays its source's deform and sequence timelines. */
378
+ timelines: boolean;
379
+ width: number;
380
+ height: number;
381
+ color?: string;
382
+ sequence?: ModelSequence;
383
+ }
384
+
385
+ /**
386
+ * What a skin table holds per placeholder: a model record of one of the six
387
+ * attachment kinds rigc builds, told apart by `kind`, whose words are Spine's
388
+ * `type` words (a region's `type` is the one the emitter leaves out). rigc
389
+ * emits no point attachment (`DEFERRED_ATTACHMENTS` in `compile.ts`).
390
+ */
391
+ export type SkinTableEntry = ModelVertexAttachment | ModelRegionAttachment | ModelLinkedMeshAttachment;
392
+
393
+ /** One skin's attachments: slot -> placeholder -> record, in the spec's order. */
394
+ export type SkinTable = Record<string, Record<string, SkinTableEntry>>;
395
+
396
+ /** Whether a skin-table entry is one of the four vertex kinds — the ones a deform timeline can key. */
397
+ export function isModelVertexAttachment(entry: SkinTableEntry): entry is ModelVertexAttachment {
398
+ return entry.kind === 'mesh' || entry.kind === 'boundingbox' || entry.kind === 'clipping' || entry.kind === 'path';
399
+ }
400
+
401
+ /**
402
+ * One slot, as the slot loop computes it. The slot array IS the draw order, so
403
+ * `CompiledModel.slots` is in the rig's declaration order and a draw-order
404
+ * offset counts in it.
405
+ */
406
+ export interface ModelSlot {
407
+ name: string;
408
+ bone: string;
409
+ /** The setup attachment's placeholder; `null` is "shows nothing", which Spine spells by leaving the key out. */
410
+ setup: string | null;
411
+ /**
412
+ * The setup tint as 8-bit hex channels: `rgbaHex` of the motion spec's setup
413
+ * colour (the rounding to the 8-bit grid is value), or the rig's as stated.
414
+ */
415
+ color?: string;
416
+ /** The two-colour tint's dark colour, as stated; a slot with none takes no `rgba2`/`rgb2` timeline. */
417
+ dark?: string;
418
+ /**
419
+ * The blend as stated, which `parseRigSpec` has held to a spelling the
420
+ * runtime resolves (`RigSlotBlend`: only the first letter's case is free).
421
+ */
422
+ blend?: string;
423
+ }
424
+
425
+ /**
426
+ * One skin. `bones` and `constraints` are what it activates (`splitRigSkin`'s
427
+ * member lists, empty for a skin the spec gives none — the manifest's `default`
428
+ * among them); `attachments` is its table, in the spec's order.
429
+ */
430
+ export interface ModelSkin {
431
+ name: string;
432
+ bones: string[];
433
+ constraints: Record<RigSkinConstraintKey, string[]>;
434
+ attachments: SkinTable;
435
+ }
436
+
437
+ /** The five constraint kinds Spine 4.3 has. */
438
+ export type ModelConstraintKind = 'ik' | 'transform' | 'path' | 'physics' | 'slider';
439
+
440
+ /**
441
+ * One constraint: its `kind` and `name`, and every field `buildRigConstraint`
442
+ * (for a rig spec's) or the motion spec's physics table computes, under the
443
+ * name it has in the spec, numbers already `f32`'d.
444
+ *
445
+ * `declaredIn` says which of the two built it, and it is here because the
446
+ * bytes differ: the physics table writes a physics constraint's fields in
447
+ * another order than `buildRigConstraint`'s physics branch — `inertia` …
448
+ * `mix`, then `fps`, `limit`, where the builder writes `limit`, `fps` first —
449
+ * and the key-order table lists neither `fps` nor `limit`, so the order
450
+ * survives into the file (`MS05` plants it). A table constraint holds every
451
+ * component and parameter the spec stated, 0 and the parser's default
452
+ * included; which of them Spine leaves out is the emitter's.
453
+ */
454
+ export type ModelConstraint = {
455
+ kind: ModelConstraintKind;
456
+ name: string;
457
+ declaredIn: 'rig' | 'motion';
458
+ } & Record<string, unknown>;
459
+
460
+ /** An event's payload, as the rig declares it; `float`, `volume`, `balance` `f32`'d. */
461
+ export interface ModelEvent {
462
+ int?: number;
463
+ float?: number;
464
+ string?: string;
465
+ audio?: string;
466
+ volume?: number;
467
+ balance?: number;
468
+ }
469
+
470
+ /**
471
+ * One timeline key, as the timeline compilers in `compile.ts` build it: `time`,
472
+ * then the channels its timeline defines, then `curve` — each in the order the
473
+ * compiler inserted it, which the Spine emitter keeps (a key's field order is a
474
+ * byte wherever the key-order table has no row for its kind).
475
+ *
476
+ * Every number is already on the float32 grid (`keyTime` for `time`, `f32` for
477
+ * the rest), for the reason the header gives.
478
+ *
479
+ * 🔸 **The channel names are Spine's** — `value`, `x`/`y`, `mix`, `mixRotate`,
480
+ * `color`, `offset`/`vertices`, `name`, `mode`/`index`/`delay` — because the
481
+ * motion spec's vocabulary is Spine's (docs/COMPILED_MODEL.md §1.1, *The input
482
+ * vocabulary is already Spine's*; docs/AUTHORING.md, *The vocabulary is
483
+ * Spine's*): a channel is named once, by the format the spec was written
484
+ * against, and a second backend maps from it. A later cut may give the model
485
+ * names of its own; this one does not.
486
+ *
487
+ * 🔸 **`curve` is absolute control points, four per channel, or `'stepped'`.**
488
+ * The points are what `bezierForChannel` computes from an easing's handles (the
489
+ * curve the runtime samples, not the handles an editor shows) or a raw curve's
490
+ * own numbers. `'stepped'` is the format's word for a hold, and the model keeps
491
+ * that word because its one consumer reads the same word: a named easing over a
492
+ * segment whose values do not move is held as `'stepped'` (`easingCurve`,
493
+ * issue #369) — a curve over a flat segment draws nothing, so the two encodings
494
+ * play one animation, and `'stepped'` is the one the editor writes. A later cut
495
+ * may spell the hold another way; this one does not.
496
+ *
497
+ * On an ik key the three flags `bendPositive`, `compress` and `stretch` are
498
+ * the flags IN EFFECT on that key: the key's own where the motion states one,
499
+ * else the constraint's, else the constraint's parser default (issue #273 — the
500
+ * 4.3 parser reads the flags per key without inheriting the constraint's, so
501
+ * which flag a key carries is a value). Which of them Spine writes is the
502
+ * emitter's (`emitAnimations`).
503
+ */
504
+ export type ModelKey = { time: number; curve?: number[] | 'stepped' } & Record<string, unknown>;
505
+
506
+ /** One target's timelines: timeline name (`rotate`, `rgba`, `mix` …) -> its keys, in the spec's order. */
507
+ export type ModelTimelines = Map<string, ModelKey[]>;
508
+
509
+ /** The two timelines an attachment can carry, `deform` compiled before `sequence`. */
510
+ export interface ModelAttachmentTimelines {
511
+ deform?: ModelKey[];
512
+ sequence?: ModelKey[];
513
+ }
514
+
515
+ /**
516
+ * One animation: every timeline the motion spec's animation compiles to, each
517
+ * collection in the order the spec states its tracks (the order the timelines
518
+ * are built in), and the declared duration the compiler verified against the
519
+ * last key.
520
+ *
521
+ * A collection is empty when the animation keys nothing of its kind — `drawOrder`
522
+ * and `events` included, since the compiler refuses an empty key list for both.
523
+ * Which of them a format writes, in what order, and under what spelling is the
524
+ * emitter's.
525
+ *
526
+ * ⭐ **The physics timeline that names no constraint is held under
527
+ * `EVERY_GLOBAL_PHYSICS` (`'*'`, `src/motion.ts`)** — the motion spec's own
528
+ * name for the target that drives every physics constraint declaring the keyed
529
+ * property global (issue #726). It is a key of `constraints.physics` and not a
530
+ * collection of its own because its POSITION among the named physics
531
+ * constraints is a value: `readAnimation` builds the physics timelines in the
532
+ * order it reads them and applies them in that order, and the file interleaves
533
+ * it — a motion keying `wob_b`, then `*`, then `wob` writes `wob_b, "", wob`.
534
+ * `'*'` cannot name a constraint (the rig parse refuses a physics constraint of
535
+ * that name), so the key is unambiguous. Spine spells it as the empty name;
536
+ * that spelling is the emitter's.
537
+ */
538
+ export interface CompiledAnimation {
539
+ /** The motion spec's declared duration, verified by the compiler to be the last key's time within a frame. */
540
+ duration: number;
541
+ /** bone -> its timelines. */
542
+ bones: Map<string, ModelTimelines>;
543
+ /** slot -> its timelines. */
544
+ slots: Map<string, ModelTimelines>;
545
+ /**
546
+ * By constraint kind: `ik` and `transform` hold one unnamed timeline per
547
+ * constraint, so a constraint maps straight to its keys; `path`, `physics`
548
+ * and `slider` hold timelines by name, like a bone.
549
+ */
550
+ constraints: {
551
+ ik: Map<string, ModelKey[]>;
552
+ transform: Map<string, ModelKey[]>;
553
+ path: Map<string, ModelTimelines>;
554
+ physics: Map<string, ModelTimelines>;
555
+ slider: Map<string, ModelTimelines>;
556
+ };
557
+ /** skin -> slot -> attachment placeholder -> its deform and/or sequence keys. */
558
+ attachments: Map<string, Map<string, Map<string, ModelAttachmentTimelines>>>;
559
+ /**
560
+ * Draw-order keys. A key's `offsets` are the moves as the motion spec states
561
+ * them, each resolved and checked; the order the format needs them in (by
562
+ * setup index) is the emitter's. A key with no `offsets` is "back to the setup
563
+ * order".
564
+ */
565
+ drawOrder: ModelKey[];
566
+ /** Event firings, in the spec's (non-decreasing time) order, each naming a declared event. */
567
+ events: ModelKey[];
568
+ }
569
+
570
+ export interface CompiledModel extends CarriedFromCompileResult {
571
+ /**
572
+ * The skeleton's reference scale, which a physics constraint's `wind` and
573
+ * `gravity` act over (issue #958): the rig spec's `skeleton.referenceScale`
574
+ * exactly as stated, or — stating none — the 100 the parser reads a header
575
+ * without one as (`UNSTATED_REFERENCE_SCALE` in `src/emit_spine.ts`). It is
576
+ * the number the Spine file is read as either way: the emitter writes this
577
+ * value into the header and the parser-default pass drops it at 100.
578
+ *
579
+ * ⭐ **Why the model holds it.** A Spine header stating 50 moved 30 of 50
580
+ * wind-and-gravity probes under the stepped oracle, and none without wind
581
+ * or gravity (issue #956); the core read the parser's 100 as a constant
582
+ * until this field. A value any backend posing the rig needs belongs here.
583
+ *
584
+ * 🔸 Not `f32`'d: the runtime reads it as the double the header spells
585
+ * (`SkeletonJson` multiplies it by the loader's `scale` and stores it), so
586
+ * it is the rig's number unchanged, and on the grids `modelDocument`
587
+ * states exactly when the rig spec wrote it on one. It is not a libm
588
+ * result, so no platform moves it; none of the nineteen recipes states one,
589
+ * and all nineteen documents carry 100.
590
+ */
591
+ referenceScale: number;
592
+ /** Every bone, in the rig's declaration order — parents first, as the runtime requires. */
593
+ bones: ModelBone[];
594
+ /** The setup world transform of every bone, computed from `bones`. Never emitted. */
595
+ setupWorld: Map<string, BoneTransform>;
596
+ /** Every slot, in draw order — the rig's declaration order. */
597
+ slots: ModelSlot[];
598
+ /**
599
+ * Every skin, in the order the spec declares them (`default` first when there
600
+ * is one), each with its attachments as model records in the spec's order —
601
+ * NOT the editor's order the emitter sorts skins and slot keys into.
602
+ */
603
+ skins: ModelSkin[];
604
+ /** The rig's constraints in declaration order, then the motion spec's physics table's. */
605
+ constraints: ModelConstraint[];
606
+ /** Event definitions, in the rig's declared order. */
607
+ events: Map<string, ModelEvent>;
608
+ /**
609
+ * Every animation, in the motion spec's order — NOT the editor's order the
610
+ * Spine emitter keys them in (`emitAnimations`).
611
+ */
612
+ animations: Map<string, CompiledAnimation>;
613
+ }
614
+
615
+ // ---------------------------------------------------------------------------
616
+ // the document: `rigc-compiled/1` (issue #922, cut 1f)
617
+ // ---------------------------------------------------------------------------
618
+
619
+ /** The document's `spec` value. */
620
+ export const MODEL_DOCUMENT_SPEC = 'rigc-compiled/1';
621
+
622
+ /** The file `build` writes the document to, in `--out` beside `skeleton.json` and `skeleton.atlas`. */
623
+ export const MODEL_DOCUMENT_FILE = 'skeleton.model.json';
624
+
625
+ /** A value the document writes: what `JSON.stringify` reproduces exactly. */
626
+ type DocValue = string | number | boolean | null | DocValue[] | { [key: string]: DocValue };
627
+
628
+ /**
629
+ * `record`'s fields in `keys`' order, each only when present (`undefined` is
630
+ * absent, as `JSON.stringify` reads it). A field `keys` does not list is
631
+ * refused by name rather than dropped: a field added to a model record without
632
+ * a place in the document would otherwise vanish from it in silence.
633
+ */
634
+ function ordered(record: object, keys: readonly string[], where: string, value: (key: string, v: unknown) => DocValue = (_k, v) => plain(v, `${where}.${_k}`)): { [key: string]: DocValue } {
635
+ const own = record as Record<string, unknown>;
636
+ for (const key of Object.keys(own)) {
637
+ if (!keys.includes(key)) {
638
+ throw new CompileError(`internal: the model document has no place for field "${key}" of ${where}; it writes [${keys.join(', ')}]`);
639
+ }
640
+ }
641
+ const out: { [key: string]: DocValue } = {};
642
+ for (const key of keys) if (own[key] !== undefined) out[key] = value(key, own[key]);
643
+ return out;
644
+ }
645
+
646
+ /**
647
+ * A value the model holds, as the document writes it: objects in their own
648
+ * key order, arrays in theirs. A number JSON cannot carry exactly is refused
649
+ * by its path — `-0` (written `0`), `NaN` and the infinities (written `null`)
650
+ * — and so are `undefined` inside an array (written `null`), a `Map` or `Set`
651
+ * (written `{}`), and anything that is not plain data. Each of those would
652
+ * make the document state a value the model does not hold.
653
+ */
654
+ function plain(value: unknown, where: string): DocValue {
655
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
656
+ if (typeof value === 'number') {
657
+ if (!Number.isFinite(value)) throw new CompileError(`internal: the model document cannot carry ${value} at ${where}; JSON writes it as null`);
658
+ if (Object.is(value, -0)) throw new CompileError(`internal: the model document cannot carry -0 at ${where}; JSON writes it as 0`);
659
+ return value;
660
+ }
661
+ if (Array.isArray(value)) {
662
+ return value.map((item, i) => {
663
+ if (item === undefined) throw new CompileError(`internal: the model document cannot carry undefined at ${where}[${i}]; JSON writes it as null`);
664
+ return plain(item, `${where}[${i}]`);
665
+ });
666
+ }
667
+ if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
668
+ const out: { [key: string]: DocValue } = {};
669
+ for (const [key, item] of Object.entries(value)) if (item !== undefined) out[key] = plain(item, `${where}.${key}`);
670
+ return out;
671
+ }
672
+ throw new CompileError(`internal: the model document cannot carry ${Object.prototype.toString.call(value)} at ${where}; it writes plain data only`);
673
+ }
674
+
675
+ /** A `Map` as the array of its entries in the map's order, each `{ name, … }`. */
676
+ function named<V>(map: ReadonlyMap<string, V>, where: string, entry: (value: V, at: string) => { [key: string]: DocValue }): DocValue[] {
677
+ return [...map].map(([name, value]) => ({ name, ...entry(value, `${where}["${name}"]`) }));
678
+ }
679
+
680
+ /** Keys, as the model holds them: each key's own field order is the compiler's, and a byte for the emitter. */
681
+ function keysOf(keys: readonly ModelKey[], where: string): DocValue {
682
+ return plain(keys, where);
683
+ }
684
+
685
+ /** One target's timelines: `[{ name, keys }]`, in the model's order. */
686
+ function timelinesOf(timelines: ModelTimelines, where: string): DocValue[] {
687
+ return named(timelines, where, (keys, at) => ({ keys: keysOf(keys, at) }));
688
+ }
689
+
690
+ /** target -> timelines: `[{ name, timelines }]`, in the model's order. */
691
+ function targetsOf(targets: ReadonlyMap<string, ModelTimelines>, where: string): DocValue[] {
692
+ return named(targets, where, (timelines, at) => ({ timelines: timelinesOf(timelines, at) }));
693
+ }
694
+
695
+ const BONE_FIELDS = ['name', 'parent', 'length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inheritMode', 'skinRequired', 'editor'] as const;
696
+ const SLOT_FIELDS = ['name', 'bone', 'setup', 'color', 'dark', 'blend'] as const;
697
+ const SEQUENCE_FIELDS = ['count', 'start', 'digits', 'setup', 'atlas'] as const;
698
+ const ATLAS_RECT_FIELDS = ['width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight'] as const;
699
+ const BINDING_FIELDS = ['bone', 'x', 'y', 'weight'] as const;
700
+ const EVENT_FIELDS = ['int', 'float', 'string', 'audio', 'volume', 'balance'] as const;
701
+ const CONSTRAINT_KINDS = ['ik', 'transform', 'path', 'physics', 'slider'] as const;
702
+ const ATTACHMENT_FIELDS: Readonly<Record<SkinTableEntry['kind'], readonly string[]>> = {
703
+ mesh: ['kind', 'name', 'path', 'color', 'uvs', 'triangles', 'vertices', 'hull', 'edges', 'width', 'height', 'sequence'],
704
+ boundingbox: ['kind', 'name', 'vertexCount', 'vertices', 'editorColor'],
705
+ clipping: ['kind', 'name', 'end', 'convex', 'inverse', 'vertexCount', 'vertices', 'editorColor'],
706
+ path: ['kind', 'name', 'closed', 'constantSpeed', 'vertexCount', 'vertices', 'lengths', 'editorColor'],
707
+ region: ['kind', 'name', 'path', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color', 'sequence', 'atlas'],
708
+ linkedmesh: ['kind', 'name', 'path', 'source', 'skin', 'slot', 'timelines', 'width', 'height', 'color', 'sequence'],
709
+ };
710
+
711
+ function verticesOf(vertices: ModelVertices, where: string): DocValue {
712
+ return vertices.weighted
713
+ ? ordered(vertices, ['weighted', 'bindings'], where, (key, v) =>
714
+ key === 'bindings'
715
+ ? vertices.bindings.map((influences, i) => influences.map((b, j) => ordered(b, BINDING_FIELDS, `${where}.bindings[${i}][${j}]`)))
716
+ : plain(v, `${where}.${key}`),
717
+ )
718
+ : ordered(vertices, ['weighted', 'xy'], where);
719
+ }
720
+
721
+ function attachmentOf(entry: SkinTableEntry, where: string): DocValue {
722
+ const fields = ATTACHMENT_FIELDS[entry.kind];
723
+ if (fields === undefined) throw new CompileError(`internal: the model document knows no attachment kind "${String(entry.kind)}" at ${where}`);
724
+ if (entry.kind === 'region' && (entry.atlas === undefined) === (entry.sequence === undefined)) {
725
+ throw new CompileError(
726
+ `internal: the region at ${where} carries ${entry.atlas === undefined ? 'neither an atlas rectangle nor a sequence' : 'both an atlas rectangle and a sequence'}; ` +
727
+ 'a region states exactly one of the two, and a sequence holds a rectangle per frame (issue #935)',
728
+ );
729
+ }
730
+ return ordered(entry, fields, where, (key, v) => {
731
+ if (key === 'vertices') return verticesOf(v as ModelVertices, `${where}.vertices`);
732
+ if (key === 'sequence') {
733
+ return ordered(v as object, SEQUENCE_FIELDS, `${where}.sequence`, (k, item) =>
734
+ k === 'atlas' ? (item as ModelAtlasRect[]).map((rect, i) => atlasRectOf(rect, `${where}.sequence.atlas[${i}]`)) : plain(item, `${where}.sequence.${k}`),
735
+ );
736
+ }
737
+ if (key === 'atlas') return v === null ? null : atlasRectOf(v as ModelAtlasRect, `${where}.atlas`);
738
+ return plain(v, `${where}.${key}`);
739
+ });
740
+ }
741
+
742
+ /** One rectangle, in `ModelAtlasRect`'s order, every field required. */
743
+ function atlasRectOf(rect: ModelAtlasRect, where: string): DocValue {
744
+ for (const key of ATLAS_RECT_FIELDS) {
745
+ if (rect[key] === undefined) throw new CompileError(`internal: the atlas rectangle at ${where} has no ${key}; it states all of [${ATLAS_RECT_FIELDS.join(', ')}]`);
746
+ }
747
+ return ordered(rect, ATLAS_RECT_FIELDS, where);
748
+ }
749
+
750
+ function skinOf(skin: ModelSkin, where: string): DocValue {
751
+ return ordered(skin, ['name', 'bones', 'constraints', 'attachments'], where, (key, v) => {
752
+ if (key === 'constraints') return ordered(skin.constraints, CONSTRAINT_KINDS, `${where}.constraints`);
753
+ if (key !== 'attachments') return plain(v, `${where}.${key}`);
754
+ const bySlot: { [slot: string]: DocValue } = {};
755
+ for (const [slot, byPlaceholder] of Object.entries(skin.attachments)) {
756
+ const entries: { [placeholder: string]: DocValue } = {};
757
+ for (const [placeholder, entry] of Object.entries(byPlaceholder)) entries[placeholder] = attachmentOf(entry, `${where}.attachments["${slot}"]["${placeholder}"]`);
758
+ bySlot[slot] = entries;
759
+ }
760
+ return bySlot;
761
+ });
762
+ }
763
+
764
+ /** A constraint: `kind`, `name`, `declaredIn`, then every other field in the builder's order, which is a byte for the emitter. */
765
+ function constraintOf(constraint: ModelConstraint, where: string): DocValue {
766
+ const { kind, name, declaredIn, ...rest } = constraint;
767
+ return { kind, name, declaredIn, ...(plain(rest, where) as { [key: string]: DocValue }) };
768
+ }
769
+
770
+ function animationOf(animation: CompiledAnimation, where: string): { [key: string]: DocValue } {
771
+ return ordered(animation, ['duration', 'bones', 'slots', 'constraints', 'attachments', 'drawOrder', 'events'], where, (key, v) => {
772
+ const at = `${where}.${key}`;
773
+ switch (key) {
774
+ case 'bones':
775
+ case 'slots':
776
+ return targetsOf(v as ReadonlyMap<string, ModelTimelines>, at);
777
+ case 'constraints': {
778
+ const c = animation.constraints;
779
+ return ordered(c, CONSTRAINT_KINDS, at, (kind, byName) =>
780
+ kind === 'ik' || kind === 'transform'
781
+ ? named(byName as ReadonlyMap<string, ModelKey[]>, `${at}.${kind}`, (keys, k) => ({ keys: keysOf(keys, k) }))
782
+ : targetsOf(byName as ReadonlyMap<string, ModelTimelines>, `${at}.${kind}`),
783
+ );
784
+ }
785
+ case 'attachments':
786
+ return named(animation.attachments, at, (bySlot, s) => ({
787
+ slots: named(bySlot, s, (byAttachment, a) => ({
788
+ attachments: named(byAttachment, a, (timelines, t) =>
789
+ ordered(timelines, ['deform', 'sequence'], t, (k, keys) => keysOf(keys as ModelKey[], `${t}.${k}`)),
790
+ ),
791
+ })),
792
+ }));
793
+ case 'drawOrder':
794
+ case 'events':
795
+ return keysOf(v as ModelKey[], at);
796
+ default:
797
+ return plain(v, at);
798
+ }
799
+ });
800
+ }
801
+
802
+ /** The model's fields the document writes, after `spec`, in its key order. */
803
+ const MODEL_DOCUMENT_FIELDS: readonly string[] = [
804
+ 'referenceScale', 'bones', 'slots', 'skins', 'constraints', 'events', 'animations',
805
+ 'images', 'pageGrids', 'droppedStates', 'absentParts', 'meshBones', 'meshes', 'physics', 'deformTransforms', 'trackDerivations', 'rig',
806
+ ];
807
+
808
+ /** The model's fields the document leaves out — see `modelDocument`. */
809
+ const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
810
+
811
+ /**
812
+ * The compiled model as a document: `rigc-compiled/1`, `JSON.stringify(doc,
813
+ * null, 2)` and a newline — the text `build` writes to `skeleton.model.json`,
814
+ * and the record rigc's own posing core reads (`readModel` in
815
+ * `src/core/index.ts`, issue #380, step 2). Its cost in a build is measured in
816
+ * docs/COMPILED_MODEL.md §6 (issue #926).
817
+ *
818
+ * **Key order.** `spec`, then the model's fields in the order this file
819
+ * declares them — `referenceScale` (issue #958), `bones`, `slots`, `skins`,
820
+ * `constraints`, `events`, `animations` — then the fields carried from `CompileResult` in
821
+ * `CarriedFromCompileResult`'s order: `images`, `pageGrids`, `droppedStates`,
822
+ * `absentParts`, `meshBones`, `meshes`, `physics`, `deformTransforms`,
823
+ * `trackDerivations`, `rig`; and last `spine`, the digest of the
824
+ * `skeleton.json` written beside it (`spineFileSha256`, issue #968), which is
825
+ * why the Spine text is the second argument. Inside a model record, its interface's field
826
+ * order (`ModelBone`, `ModelSlot`, `ModelSkin`, each attachment kind,
827
+ * `ModelBinding`, `ModelSequence`, `ModelAtlasRect`, `ModelEvent`, `CompiledAnimation`), each
828
+ * field only when the record carries it, and a field the interface does not
829
+ * list refused by name. Three kinds of record keep their own order, because
830
+ * there the order is the value: a constraint's fields after `kind`, `name`,
831
+ * `declaredIn` (the builder's order, which the emitter keeps and which reaches
832
+ * the file where the key-order table has no row), a timeline key's fields
833
+ * (`time`, its channels, `curve`, as the compiler inserted them), and the
834
+ * carried reports, written as `compile` builds them.
835
+ *
836
+ * **Collections.** Every `Map` is an array of `{ name, … }` in the map's
837
+ * order, and that order is the model's own: bones parents first (a weighted
838
+ * binding's index counts in it), slots the draw order (a draw-order offset
839
+ * counts in it), skins, constraints, events and animations in the spec's
840
+ * declared order, timelines and keys in the order they are applied. A skin's
841
+ * attachment table stays an object keyed slot -> placeholder, as the model
842
+ * holds it. An animation writes `bones` and `slots` as `[{ name, timelines:
843
+ * [{ name, keys }] }]`, its `ik` and `transform` constraints as `[{ name, keys
844
+ * }]` and the other three kinds like a bone, and `attachments` as `[{ name:
845
+ * skin, slots: [{ name, attachments: [{ name, deform?, sequence? }] }] }]`;
846
+ * the physics timeline that names no constraint keeps the model's name for it,
847
+ * `*` (`EVERY_GLOBAL_PHYSICS`), not the empty name Spine spells it with.
848
+ *
849
+ * **Numbers** are written exactly as the model holds them, so nothing is
850
+ * re-rounded here; which of them are float32 values, and how a reader reads
851
+ * the rest, is the header's 🔸. A number JSON cannot carry exactly — `-0`,
852
+ * `NaN`, an infinity — is refused by its path (`plain`).
853
+ *
854
+ * 🔒 **Every number the document spells is on one of two grids**: a fixed
855
+ * point of the compiler's float32 spelling (`f32(x) === x` — the shortest
856
+ * decimal naming a float, which is not the float's own double) or of the
857
+ * six-decimal grid (`Math.round(x·1e6)/1e6 === x`). `MX01` in `selftest.ts`
858
+ * holds it over every document the tree's recipes build, with a full double
859
+ * planted to turn it red by its path. One field is the rig spec's number
860
+ * carried unchanged rather than computed, `referenceScale` (issue #958): the
861
+ * runtime reads the header's double, so rounding it here would pose another
862
+ * skeleton; it is on a grid when the spec wrote it on one. A number on neither grid is a full
863
+ * double whose last digits are the platform libm's, which is how the rule was
864
+ * found (issue #942): `meshes[].depth.ceiling` carried `turnCeiling`'s fold
865
+ * figures (`src/depth.ts`) as full doubles from `Math.atan` and the depth
866
+ * tone, and `gallery/look`'s document differed between macOS and the Linux
867
+ * runner by exactly `/meshes/0/depth/ceiling/pitch/negative/{degrees,p1}`,
868
+ * `26.935130523311` against `26.935130523311003`, while its Spine files were
869
+ * identical. Those figures are reported on the six-decimal grid now, and a
870
+ * one-ulp `Math.atan` perturbation either way moves no byte of that document.
871
+ *
872
+ * ⚠️ A grid absorbs a difference in a value, not in a choice. Perturbing
873
+ * `Math.pow` by one ulp moved `gallery/look`'s document until issue #949: the
874
+ * depth tone reaches every `z`, two triangles whose fold angles round to the
875
+ * same six-decimal value traded places as the minimum, and the fold named the
876
+ * other triangle, with its own `depthStep` and `stepShare`. The minimum is
877
+ * now chosen on the six-decimal grid with the lowest triangle ordinal taking
878
+ * a tie (`src/depth.ts`'s `foldPrecedes`), and a one-ulp perturbation of
879
+ * sixteen libm functions, either way, moves no leaf of any gallery document
880
+ * (`TB02`). What is left is a value within one ulp of a rounding boundary.
881
+ *
882
+ * **Left out, and why.** Each is something the model holds that is not a
883
+ * statement about the rig:
884
+ *
885
+ * - `setupWorld` — computed from `bones` by `computeWorldTransforms`
886
+ * (`src/transform.ts`), so it states nothing `bones` does not, and a core
887
+ * is to compute it rather than read it; and it is the one field holding a
888
+ * `-0` (a root bone's `b` is `-sin 0`), which JSON cannot spell.
889
+ * - `images[].absPath` — where the part was on this machine's disk, for the
890
+ * size assertions.
891
+ * - `droppedStates[].why` — the sentence names the `--atlas-in` file by its
892
+ * absolute path.
893
+ *
894
+ * What the Spine emitter adds (`emitSkeleton`'s header, its spellings, its
895
+ * orders and omissions) is not in the model and so not here.
896
+ */
897
+ export function modelDocument(model: CompiledModel, skeletonText: string): string {
898
+ for (const key of Object.keys(model)) {
899
+ if (!MODEL_DOCUMENT_FIELDS.includes(key) && !MODEL_DOCUMENT_LEFT_OUT.includes(key)) {
900
+ 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(', ')}]`);
901
+ }
902
+ }
903
+ const doc: { [key: string]: DocValue } = {
904
+ spec: MODEL_DOCUMENT_SPEC,
905
+ referenceScale: plain(model.referenceScale, 'referenceScale'),
906
+ bones: model.bones.map((bone, i) =>
907
+ ordered(bone, BONE_FIELDS, `bones[${i}]`, (key, v) => (key === 'editor' ? ordered(v as object, ['color', 'icon'], `bones[${i}].editor`) : plain(v, `bones[${i}].${key}`))),
908
+ ),
909
+ slots: model.slots.map((slot, i) => ordered(slot, SLOT_FIELDS, `slots[${i}]`)),
910
+ skins: model.skins.map((skin, i) => skinOf(skin, `skins[${i}]`)),
911
+ constraints: model.constraints.map((constraint, i) => constraintOf(constraint, `constraints[${i}]`)),
912
+ events: named(model.events, 'events', (event, at) => ordered(event, EVENT_FIELDS, at)),
913
+ animations: named(model.animations, 'animations', (animation, at) => animationOf(animation, at)),
914
+ images: plain(model.images.map(({ absPath: _absPath, ...image }) => image), 'images'),
915
+ pageGrids: plain(model.pageGrids, 'pageGrids'),
916
+ droppedStates: plain(model.droppedStates.map(({ why: _why, ...state }) => state), 'droppedStates'),
917
+ absentParts: plain(model.absentParts, 'absentParts'),
918
+ meshBones: plain(model.meshBones, 'meshBones'),
919
+ meshes: plain(model.meshes, 'meshes'),
920
+ physics: plain(model.physics, 'physics'),
921
+ deformTransforms: plain(model.deformTransforms, 'deformTransforms'),
922
+ trackDerivations: plain(model.trackDerivations, 'trackDerivations'),
923
+ rig: plain(model.rig, 'rig'),
924
+ spine: { sha256: spineFileSha256(skeletonText) },
925
+ };
926
+ return `${JSON.stringify(doc, null, 2)}\n`;
927
+ }
928
+
929
+ // --- #968 the Spine file the document was written beside: begin ---
930
+ /**
931
+ * The document's last section, `spine`: `{ "sha256": "<64 lowercase hex>" }`,
932
+ * the SHA-256 of the exact bytes `build` writes to `skeleton.json` in the same
933
+ * run (the UTF-8 of `skeletonText`, as `writeFileSync` writes it).
934
+ *
935
+ * ⭐ Why it exists (issue #968). `build` writes the Spine pair and this document
936
+ * as one output, and `rigc render` poses the document through rigc's own core
937
+ * when it finds one beside the skeleton. Nothing else ties the two files: a
938
+ * `skeleton.json` edited by hand after the build, beside the document it was
939
+ * built with, was drawn from the document — the build's rig, not the file the
940
+ * render was pointed at (the `RF89`/`RF91`/`RF93` plants, exit 0 where the
941
+ * Spine file refuses). The render takes the core only when the file beside the
942
+ * document hashes to this value, and names the mismatch otherwise.
943
+ *
944
+ * Named `spine.sha256` rather than a top-level `skeletonSha256`: the section
945
+ * is the document's statement about the Spine output it was written with, a
946
+ * digest of it and nothing about the rig, so it sits apart from the model's
947
+ * fields and after them; and an object leaves the atlas's digest a field to
948
+ * add, not a second section. The file's NAME is not recorded — `build` always
949
+ * writes `skeleton.json` beside this document, and a copied pair keeps both.
950
+ *
951
+ * Deterministic because the Spine file is (`A18` compares a second compile's
952
+ * bytes), so two builds and two platforms write the same value wherever their
953
+ * Spine files agree.
954
+ */
955
+ export function spineFileSha256(skeletonText: string | Uint8Array): string {
956
+ return createHash('sha256').update(skeletonText).digest('hex');
957
+ }
958
+ // --- #968 the Spine file the document was written beside: end ---