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/README.md +2 -1
- package/cli.ts +131 -403
- package/docs/AUTHORING.md +63 -14
- package/docs/INGEST.md +1 -0
- package/docs/MOTION.md +1 -0
- package/package.json +1 -1
- package/src/atlas.ts +25 -0
- package/src/check.ts +113 -24
- package/src/checkpics.ts +2 -0
- package/src/compile.ts +641 -506
- package/src/core/animation.ts +982 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1721 -0
- package/src/core/constraints_path.ts +942 -0
- package/src/core/constraints_physics.ts +830 -0
- package/src/core/constraints_slider.ts +505 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1103 -0
- package/src/core/raw.ts +347 -0
- package/src/core/skins.ts +104 -0
- package/src/core/uvs.ts +448 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/world.ts +233 -0
- package/src/deformmeasure.ts +337 -73
- package/src/deformreport.ts +370 -0
- package/src/depth.ts +101 -2
- package/src/emit_spine.ts +860 -0
- package/src/mesh.ts +24 -15
- package/src/model.ts +958 -0
- package/src/render.ts +649 -143
- package/src/render_core.ts +582 -0
- package/src/rig.ts +34 -6
- package/src/transform.ts +26 -4
- package/src/types.ts +18 -0
- package/src/validate.ts +30 -2
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 ---
|