spine-rigc 0.2.1
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/LICENSE +21 -0
- package/NOTICE.md +76 -0
- package/README.md +558 -0
- package/cli.ts +739 -0
- package/docs/AUTHORING.md +1303 -0
- package/docs/SPEC_COVERAGE.md +1109 -0
- package/package.json +65 -0
- package/src/check.ts +1714 -0
- package/src/compile.ts +1861 -0
- package/src/diff.ts +847 -0
- package/src/errors.ts +22 -0
- package/src/framing.ts +539 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +433 -0
- package/src/png.ts +50 -0
- package/src/render.ts +974 -0
- package/src/rig.ts +731 -0
- package/src/slots.ts +603 -0
- package/src/timelines.ts +253 -0
- package/src/transform.ts +130 -0
- package/src/types.ts +586 -0
- package/src/validate.ts +1586 -0
- package/tools/font5x7.ts +101 -0
- package/tools/plate.ts +286 -0
package/src/rig.ts
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rig spec — `"spec": "rigc-rig/1"`. The skeleton as **data**.
|
|
3
|
+
*
|
|
4
|
+
* Until this file existed the bone tree and the slot table were code: three
|
|
5
|
+
* hard-coded formations in `src/archetype.ts`, a slot outside their tables a
|
|
6
|
+
* compile error, and therefore **no skeleton anybody else owns could be stated
|
|
7
|
+
* at all**. That was blocker B1 of [docs/LADDER.md](../docs/LADDER.md), and it
|
|
8
|
+
* gated every rung of the benchmark ladder.
|
|
9
|
+
*
|
|
10
|
+
* ## The vocabulary is Spine's
|
|
11
|
+
*
|
|
12
|
+
* ⭐ Wherever rigc has no better abstraction, this format uses **Spine 4.3's own
|
|
13
|
+
* concept and its own field name, with Spine's own default**, so that an agent
|
|
14
|
+
* that has read Spine's documentation can author a rig here without learning a
|
|
15
|
+
* second vocabulary. `bones[]` is Spine's bone list; `slots[]` is Spine's slot
|
|
16
|
+
* list and its array order is the draw order; `skins` holds Spine's placeholder
|
|
17
|
+
* → attachment maps; `constraints[]` is 4.3's single typed constraint array.
|
|
18
|
+
* Field lists below cite `SkeletonJson.ts` line numbers, which
|
|
19
|
+
* [docs/SPEC_COVERAGE.md](../docs/SPEC_COVERAGE.md) part 1 enumerates in full.
|
|
20
|
+
*
|
|
21
|
+
* Everything rigc adds sits **on top** of that vocabulary and is namespaced so a
|
|
22
|
+
* reader can see where Spine stops:
|
|
23
|
+
*
|
|
24
|
+
* - `from` on a bone — take this bone's setup position from the cut manifest
|
|
25
|
+
* (an anchor, a part window, a mesh centre) instead of writing a literal that
|
|
26
|
+
* would drift away from the measured art.
|
|
27
|
+
* - `generator` on a mesh attachment — build the geometry with one of the
|
|
28
|
+
* builders in `src/mesh.ts` instead of authoring vertex arrays by hand.
|
|
29
|
+
* - `image` on an attachment — name a PNG and let rigc **measure** it, rather
|
|
30
|
+
* than restating a `width`/`height` that can silently disagree with the file
|
|
31
|
+
* (SPEC_COVERAGE part 1-6: a missing `width` loads as `NaN`, with no error).
|
|
32
|
+
* - `invariants` — the structural facts skeleton JSON cannot state about itself,
|
|
33
|
+
* which the validator's archetype assertions read. Nothing in the file says
|
|
34
|
+
* "this bone carries the cut's axis" or "this parentage is forbidden".
|
|
35
|
+
*
|
|
36
|
+
* ## What a field's PRESENCE means
|
|
37
|
+
*
|
|
38
|
+
* 🔑 **A field is emitted exactly when the spec declares it.** Not "when it
|
|
39
|
+
* differs from the default" — Spine's own exporter omits defaults, but rigc
|
|
40
|
+
* cannot, because a rig may need to say `x: 0` out loud (the overlay formation's
|
|
41
|
+
* handle bone does) and because deciding emission from the *value* makes the
|
|
42
|
+
* emitted file depend on arithmetic rather than on what the author wrote. Omit a
|
|
43
|
+
* field and Spine's default stands; write it and it is in the file. A bone whose
|
|
44
|
+
* position comes `from` the manifest counts as declaring `x` and `y`, because
|
|
45
|
+
* the manifest declared them.
|
|
46
|
+
*
|
|
47
|
+
* ## What this format does NOT own
|
|
48
|
+
*
|
|
49
|
+
* rigc joins three files and each owns a domain:
|
|
50
|
+
*
|
|
51
|
+
* - the **cut manifest** owns measured geometry — crop, part offsets and sizes,
|
|
52
|
+
* mask polygons, the state machine, anchors, the axis, the measured ceilings;
|
|
53
|
+
* - the **rig spec** (this file) owns skeleton structure — bones, slots, skins,
|
|
54
|
+
* constraints, and the invariants;
|
|
55
|
+
* - the **motion spec** owns time — named easings, groups, setup pose, the
|
|
56
|
+
* physics tuning table, and the animations.
|
|
57
|
+
*
|
|
58
|
+
* A cut compiled from all three declares its attachments in the manifest (see
|
|
59
|
+
* `slots` below for the join rule) and leaves `skins` empty. A foreign skeleton
|
|
60
|
+
* with no manifest at all declares them here.
|
|
61
|
+
*/
|
|
62
|
+
import { CompileError, NotImplementedError } from './errors.ts';
|
|
63
|
+
|
|
64
|
+
export { CompileError, NotImplementedError };
|
|
65
|
+
|
|
66
|
+
/** The only version this compiler reads. */
|
|
67
|
+
export const RIG_SPEC_VERSION = 'rigc-rig/1';
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// skeleton header — `root.skeleton` (SkeletonJson.ts:75-87)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The setup-pose bounding box and the runtime hints, all optional.
|
|
75
|
+
*
|
|
76
|
+
* `x`/`y` default to 0 and `width`/`height` fall back to the cut manifest's crop
|
|
77
|
+
* when there is one. With neither a manifest nor a declaration here the compile
|
|
78
|
+
* fails by name: `width`/`height` are what `A14_NO_FULL_FRAME_MESH` and
|
|
79
|
+
* `A19_OVERLAY_PNGS_HAVE_ALPHA` measure against, and a guessed stage is a gate
|
|
80
|
+
* that measures against a number nobody wrote down.
|
|
81
|
+
*
|
|
82
|
+
* `spine` is not here: rigc emits its own version label and `A16` re-checks it.
|
|
83
|
+
* `hash` is not here either — it is the editor's change-detection token and
|
|
84
|
+
* inventing one would be claiming an export this file did not come from.
|
|
85
|
+
*/
|
|
86
|
+
export interface RigSkeletonHeader {
|
|
87
|
+
x?: number;
|
|
88
|
+
y?: number;
|
|
89
|
+
width?: number;
|
|
90
|
+
height?: number;
|
|
91
|
+
/** Nonessential; `SkeletonData.fps` stays 30 when absent. */
|
|
92
|
+
fps?: number;
|
|
93
|
+
/** 4.2+; the runtime's physics/scale reference. Parser default 100. */
|
|
94
|
+
referenceScale?: number;
|
|
95
|
+
/** Nonessential path hint the editor writes; carried through verbatim. */
|
|
96
|
+
images?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// bones — `root.bones[]` (SkeletonJson.ts:90-118)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `BoneData.ts:80`. Resolved by `Utils.enumValue`, which upper-cases the first
|
|
105
|
+
* letter, so `"noScale"` and `"NoScale"` both load; rigc accepts either and
|
|
106
|
+
* emits the lower-camel spelling the editor writes.
|
|
107
|
+
*
|
|
108
|
+
* ⚠️ 4.0/4.1 called this field `transform`. That name still *loads* in 4.3 and
|
|
109
|
+
* the inheritance silently falls back to Normal — assertion `A02`.
|
|
110
|
+
*/
|
|
111
|
+
export type RigBoneInherit = 'normal' | 'onlyTranslation' | 'noRotationOrReflection' | 'noScale' | 'noScaleOrReflection';
|
|
112
|
+
|
|
113
|
+
export const RIG_BONE_INHERIT: readonly RigBoneInherit[] = [
|
|
114
|
+
'normal',
|
|
115
|
+
'onlyTranslation',
|
|
116
|
+
'noRotationOrReflection',
|
|
117
|
+
'noScale',
|
|
118
|
+
'noScaleOrReflection',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Take a bone's setup transform from the cut manifest rather than from a literal.
|
|
123
|
+
*
|
|
124
|
+
* ⭐ This is the one place the rig spec deliberately does not mirror Spine, and
|
|
125
|
+
* the reason is the oldest rule in this project: **the compiler never re-measures
|
|
126
|
+
* art, and a measured number lives in exactly one file.** A rig that wrote
|
|
127
|
+
* `x: 456.5` would be a second copy of a part offset the manifest already holds,
|
|
128
|
+
* and the two would drift the first time the art moved — silently, because both
|
|
129
|
+
* files would still be valid.
|
|
130
|
+
*
|
|
131
|
+
* Exactly one of `anchor` / `slotWindow` / `meshCenter` may be given, and it
|
|
132
|
+
* supplies the bone's `x` and `y`. All three name a point in **crop pixels, y
|
|
133
|
+
* down**; the compiler converts it to Spine world (y up, origin at the crop's
|
|
134
|
+
* bottom-left) and then into the parent bone's local space, so a rotated parent
|
|
135
|
+
* is handled by the same inverse the mesh binder uses.
|
|
136
|
+
*/
|
|
137
|
+
export interface RigBoneFrom {
|
|
138
|
+
/** A key of the manifest's `anchors` block: `[x, y]` or `[x, y, facing_deg]`. */
|
|
139
|
+
anchor?: string;
|
|
140
|
+
/** The centre of a manifest part's window, named by the rig slot it fills. */
|
|
141
|
+
slotWindow?: string;
|
|
142
|
+
/** A manifest part's `mesh.center` — the aperture a ring deforms about. */
|
|
143
|
+
meshCenter?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Where the setup rotation comes from. Omit and no rotation is emitted.
|
|
146
|
+
*
|
|
147
|
+
* `axis` — the manifest's `axis.deg`, negated into Spine's y-up CCW. This
|
|
148
|
+
* is the keystone of an articulated cut: the stroke is a
|
|
149
|
+
* translateX along this bone, so a sibling cut at another camera
|
|
150
|
+
* angle changes one number instead of every key.
|
|
151
|
+
* `anchor` — the third element of the named anchor, a screen-space facing
|
|
152
|
+
* angle. A grip whose local +X points radially outward turns
|
|
153
|
+
* "expand the ring" into one shared translate key.
|
|
154
|
+
*/
|
|
155
|
+
rotation?: 'axis' | 'anchor';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* One bone. Spine's field set, Spine's defaults (`SkeletonJson.ts:90-118`).
|
|
160
|
+
*
|
|
161
|
+
* `parent` is resolved by name and **must be declared earlier in the array** —
|
|
162
|
+
* the parser resolves against the bones it has already read, so a forward
|
|
163
|
+
* reference is not a rigc restriction.
|
|
164
|
+
*/
|
|
165
|
+
export interface RigBone {
|
|
166
|
+
name: string;
|
|
167
|
+
/** Omitted only by the skeleton's single root bone. */
|
|
168
|
+
parent?: string;
|
|
169
|
+
/** Default 0. Cosmetic in a renderer; part of a faithful reproduction. */
|
|
170
|
+
length?: number;
|
|
171
|
+
/** Local to the parent. Default 0. Supplied by `from` when that is given. */
|
|
172
|
+
x?: number;
|
|
173
|
+
y?: number;
|
|
174
|
+
/** Degrees, CCW, y up. Default 0. Supplied by `from.rotation` when given. */
|
|
175
|
+
rotation?: number;
|
|
176
|
+
/** Default 1. */
|
|
177
|
+
scaleX?: number;
|
|
178
|
+
scaleY?: number;
|
|
179
|
+
/** Default 0. */
|
|
180
|
+
shearX?: number;
|
|
181
|
+
shearY?: number;
|
|
182
|
+
/** Default `normal`. 4.2+ name; 4.0/4.1 called it `transform` — see A02. */
|
|
183
|
+
inherit?: RigBoneInherit;
|
|
184
|
+
/** Default false → `BoneData.skinRequired`. */
|
|
185
|
+
skin?: boolean;
|
|
186
|
+
/** `rrggbbaa`. Editor affordance; no rendering effect. */
|
|
187
|
+
color?: string;
|
|
188
|
+
/**
|
|
189
|
+
* The editor's icon for this bone. Editor affordance; no rendering effect,
|
|
190
|
+
* and no assertion checks the name — the icon vocabulary belongs to the
|
|
191
|
+
* editor, so an unknown one is not rigc's error to raise.
|
|
192
|
+
*/
|
|
193
|
+
icon?: string;
|
|
194
|
+
/** rigc extension — see `RigBoneFrom`. */
|
|
195
|
+
from?: RigBoneFrom;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// slots — `root.slots[]` (SkeletonJson.ts:121-141)
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/** `SlotData.ts:64`. */
|
|
203
|
+
export type RigSlotBlend = 'normal' | 'additive' | 'multiply' | 'screen';
|
|
204
|
+
|
|
205
|
+
export const RIG_SLOT_BLEND: readonly RigSlotBlend[] = ['normal', 'additive', 'multiply', 'screen'];
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* One slot. **The array order IS the draw order** — there is no separate setup
|
|
209
|
+
* draw-order field anywhere in the format.
|
|
210
|
+
*
|
|
211
|
+
* The rig's slot list is the CANONICAL table, which is a slightly stronger claim
|
|
212
|
+
* than "the slots this cut emits". A cut whose manifest carries no part for a
|
|
213
|
+
* slot does not emit it, and the emitted array is then a *subsequence* of this
|
|
214
|
+
* one; that is what `A26_SLOT_DRAW_ORDER` checks. Declaring a slot no cut fills
|
|
215
|
+
* is therefore legitimate — it fixes where that slot will sit when a cut does
|
|
216
|
+
* fill it.
|
|
217
|
+
*/
|
|
218
|
+
export interface RigSlot {
|
|
219
|
+
name: string;
|
|
220
|
+
/** Required. A miss throws in the parser: `Couldn't find bone … for slot …`. */
|
|
221
|
+
bone: string;
|
|
222
|
+
/**
|
|
223
|
+
* The setup-pose attachment name, or `null` for "show nothing".
|
|
224
|
+
*
|
|
225
|
+
* ⚠️ For a cut compiled with a motion spec this is **not** where the setup pose
|
|
226
|
+
* comes from: `motion.setup` owns it, because which of the two overlay
|
|
227
|
+
* mechanisms a slot uses (attachment + alpha 0, or attachment swapping) is a
|
|
228
|
+
* decision about time. Declaring it in both is a compile error.
|
|
229
|
+
*/
|
|
230
|
+
attachment?: string | null;
|
|
231
|
+
/** `rrggbbaa`. Default opaque white. */
|
|
232
|
+
color?: string;
|
|
233
|
+
/** Two-colour tint, `rrggbb`. 🚫 `A12_NO_DARK_COLOR` under `spine-html`. */
|
|
234
|
+
dark?: string;
|
|
235
|
+
/** Default `normal`. */
|
|
236
|
+
blend?: RigSlotBlend;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// attachments — `readAttachment` (SkeletonJson.ts:535-654)
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Which builder in `src/mesh.ts` makes this mesh's geometry, and its parameters.
|
|
245
|
+
*
|
|
246
|
+
* The builders stay **code** and are invoked by **data**: they encode a
|
|
247
|
+
* deformation model (what is pinned, what may move, how authority falls off),
|
|
248
|
+
* and a model is not a table of numbers.
|
|
249
|
+
*
|
|
250
|
+
* ⚠️ A cut with a manifest does not use this. There the generator is invoked
|
|
251
|
+
* through the manifest's `mesh` block, because everything a generator needs —
|
|
252
|
+
* the mask contour, the aperture centre, the part window — is *measured art*,
|
|
253
|
+
* and measured art lives in the manifest. `generator` is for a skeleton with no
|
|
254
|
+
* manifest behind it.
|
|
255
|
+
*/
|
|
256
|
+
export type RigMeshGenerator =
|
|
257
|
+
| {
|
|
258
|
+
kind: 'ring';
|
|
259
|
+
/** The seam contour, in part-local pixels, y down. At least 6 points. */
|
|
260
|
+
hull: Array<[number, number]>;
|
|
261
|
+
/** Aperture centre, part-local pixels, y down. */
|
|
262
|
+
center: [number, number];
|
|
263
|
+
/** Inner ring position between the centre (0) and the hull (1). */
|
|
264
|
+
inner: number;
|
|
265
|
+
/** Part window size, for UVs. */
|
|
266
|
+
size: [number, number];
|
|
267
|
+
/** Directional authority across an axis — see `sideWeight` in mesh.ts. */
|
|
268
|
+
bias?: { axis_deg: number; ramp: [number, number] };
|
|
269
|
+
/** Control bones, by name. More than one splits the ring by angle. */
|
|
270
|
+
controls: string[];
|
|
271
|
+
}
|
|
272
|
+
| {
|
|
273
|
+
kind: 'ribbon';
|
|
274
|
+
/** Part window size in pixels. The strip spans it. */
|
|
275
|
+
size: [number, number];
|
|
276
|
+
/** Cross rows, entry first. Triangles = 2 * (rows - 1). */
|
|
277
|
+
rows: number;
|
|
278
|
+
/** The bone chain the strip rides, root first. */
|
|
279
|
+
chain: string[];
|
|
280
|
+
}
|
|
281
|
+
| {
|
|
282
|
+
/**
|
|
283
|
+
* 🚧 Not implemented. An alpha-contour triangulator (ear clipping over the
|
|
284
|
+
* part's own mask) is the obvious third builder and `src/mesh.ts` does not
|
|
285
|
+
* have one; `buildRingMesh` and `buildRibbonMesh` are the only two.
|
|
286
|
+
*/
|
|
287
|
+
kind: 'contour';
|
|
288
|
+
[param: string]: unknown;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/** `SkeletonJson.ts:540-559`. `type` defaults to `region` (`:539`). */
|
|
292
|
+
export interface RigRegionAttachment {
|
|
293
|
+
type?: 'region';
|
|
294
|
+
/** The atlas region to resolve. Defaults to the attachment's own name. */
|
|
295
|
+
path?: string;
|
|
296
|
+
/**
|
|
297
|
+
* rigc extension: a PNG, relative to the rig's `images` directory.
|
|
298
|
+
*
|
|
299
|
+
* ⭐ Naming a file instead of a size is the point. `width`/`height` have **no
|
|
300
|
+
* parser default** — an omission loads as `NaN` and every UV collapses with no
|
|
301
|
+
* error — so a spec that restates them by hand carries a number that can
|
|
302
|
+
* disagree with the pixels. Give an `image` and rigc reads the PNG header and
|
|
303
|
+
* fills both in; the atlas page it emits is that same file, so the size in the
|
|
304
|
+
* skeleton and the size in the atlas cannot drift apart.
|
|
305
|
+
*/
|
|
306
|
+
image?: string;
|
|
307
|
+
x?: number;
|
|
308
|
+
y?: number;
|
|
309
|
+
/** Degrees. Cancels a rotated bone for a plate authored in screen space. */
|
|
310
|
+
rotation?: number;
|
|
311
|
+
scaleX?: number;
|
|
312
|
+
scaleY?: number;
|
|
313
|
+
/** Required by the format; may be omitted here when `image` is given. */
|
|
314
|
+
width?: number;
|
|
315
|
+
height?: number;
|
|
316
|
+
/** `rrggbbaa`. */
|
|
317
|
+
color?: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* `SkeletonJson.ts:568-605`. Either authored geometry or a `generator`, never
|
|
322
|
+
* both.
|
|
323
|
+
*
|
|
324
|
+
* ⚠️ `vertices` has no encoding flag anywhere in the format. If its length
|
|
325
|
+
* equals `uvs.length` the parser reads unweighted x/y pairs; otherwise it reads
|
|
326
|
+
* the weighted run `boneCount, (boneIndex, bindX, bindY, weight) × n, …`. A
|
|
327
|
+
* coincidental length match reads weight data as coordinates, silently — which
|
|
328
|
+
* is `A04_MESH_TRIANGLES_AND_ENCODING`.
|
|
329
|
+
*/
|
|
330
|
+
/**
|
|
331
|
+
* One bone's pull on one vertex of an authored mesh, **named**.
|
|
332
|
+
*
|
|
333
|
+
* `x`/`y` are the vertex's position in that bone's own setup space — the same
|
|
334
|
+
* pair Spine's weighted run carries after the bone index. `weight` is its share;
|
|
335
|
+
* a vertex's weights sum to 1.
|
|
336
|
+
*/
|
|
337
|
+
export interface RigMeshBinding {
|
|
338
|
+
/** Resolved against the rig's bone list at emit. An unknown name is refused. */
|
|
339
|
+
bone: string;
|
|
340
|
+
x: number;
|
|
341
|
+
y: number;
|
|
342
|
+
weight: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface RigMeshAttachment {
|
|
346
|
+
type: 'mesh';
|
|
347
|
+
path?: string;
|
|
348
|
+
image?: string;
|
|
349
|
+
/** Its length defines `worldVerticesLength`; required with authored geometry. */
|
|
350
|
+
uvs?: number[];
|
|
351
|
+
triangles?: number[];
|
|
352
|
+
/**
|
|
353
|
+
* Geometry, in one of two forms.
|
|
354
|
+
*
|
|
355
|
+
* **Unweighted** — one `x, y` pair per uv pair, and `vertices.length` equals
|
|
356
|
+
* `uvs.length`. Nothing here names a bone, so nothing here can be rebound.
|
|
357
|
+
*
|
|
358
|
+
* **Weighted, raw** — Spine's own encoding,
|
|
359
|
+
* `boneCount, (boneIndex, bindX, bindY, weight) x n` per vertex, where
|
|
360
|
+
* `boneIndex` is a position in the EMITTED bone array. 🚨 That array is not
|
|
361
|
+
* something a rig spec writes or can see, so those indices shift under any
|
|
362
|
+
* edit to the bone list and every vertex silently rebinds — the mesh still
|
|
363
|
+
* loads, every weight still sums to 1, and nothing in the file objects. rigc
|
|
364
|
+
* therefore refuses this form unless the attachment says `boneIndexing: "raw"`
|
|
365
|
+
* out loud. Use `weights` instead.
|
|
366
|
+
*/
|
|
367
|
+
vertices?: number[];
|
|
368
|
+
/**
|
|
369
|
+
* Weighted geometry that binds **by name**: one entry per vertex, each a list
|
|
370
|
+
* of `{ bone, x, y, weight }`. This is the default form and the one everything
|
|
371
|
+
* else in a rig spec already uses — a bone's `parent`, a slot's `bone`, a
|
|
372
|
+
* constraint's `bones` and `target` all resolve by name and refuse a miss by
|
|
373
|
+
* name. The compiler resolves these to indices on emit, so inserting a bone
|
|
374
|
+
* moves the indices and changes nothing about what the mesh is bound to.
|
|
375
|
+
*
|
|
376
|
+
* Mutually exclusive with `vertices`.
|
|
377
|
+
*/
|
|
378
|
+
weights?: RigMeshBinding[][];
|
|
379
|
+
/**
|
|
380
|
+
* How a weighted `vertices` run names its bones. Default `"name"`, which means
|
|
381
|
+
* "there is no weighted run here — use `weights`". `"raw"` opts into the index
|
|
382
|
+
* encoding above, for a spec transcribed from an export that has not been
|
|
383
|
+
* migrated yet. It is an opt-in because the cost of it is silence.
|
|
384
|
+
*/
|
|
385
|
+
boneIndexing?: 'name' | 'raw';
|
|
386
|
+
/** Hull vertex count. The loader stores it doubled. */
|
|
387
|
+
hull?: number;
|
|
388
|
+
/** Edge index pairs; nonessential, editor-drawn. */
|
|
389
|
+
edges?: number[];
|
|
390
|
+
width?: number;
|
|
391
|
+
height?: number;
|
|
392
|
+
color?: string;
|
|
393
|
+
/** Build the geometry instead of authoring it — see `RigMeshGenerator`. */
|
|
394
|
+
generator?: RigMeshGenerator;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The four types the format holds and rigc's emitter does not cover yet. They
|
|
399
|
+
* are in the type so a spec can *say* them and get a named
|
|
400
|
+
* `NotImplementedError`; the alternative is the parser's own behaviour, which is
|
|
401
|
+
* to return `null` for an unknown `type` and drop the attachment without a word
|
|
402
|
+
* (`SkeletonJson.ts:653`).
|
|
403
|
+
*/
|
|
404
|
+
export interface RigUnimplementedAttachment {
|
|
405
|
+
type: 'boundingbox' | 'point' | 'clipping' | 'path' | 'linkedmesh';
|
|
406
|
+
[field: string]: unknown;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export type RigAttachment = RigRegionAttachment | RigMeshAttachment | RigUnimplementedAttachment;
|
|
410
|
+
|
|
411
|
+
/** `slotName -> placeholderName -> attachment` (`SkeletonJson.ts:431-439`). */
|
|
412
|
+
export type RigSkin = Record<string, Record<string, RigAttachment>>;
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// constraints — `root.constraints[]` (SkeletonJson.ts:144-369), the 4.3 shape
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* 4.3 folds every constraint into ONE array with a `type` discriminator. The
|
|
420
|
+
* 4.1/4.2 shape — top-level `ik`/`transform`/`path`/`physics` arrays — still
|
|
421
|
+
* loads clean and the constraints simply vanish, which is `A01`.
|
|
422
|
+
*
|
|
423
|
+
* 🚨 An entry whose `type` matches no case is dropped with no error and no
|
|
424
|
+
* `default:` branch (`:148-367`). rigc therefore refuses an unknown `type` by
|
|
425
|
+
* name rather than passing it through.
|
|
426
|
+
*/
|
|
427
|
+
export interface RigConstraintCommon {
|
|
428
|
+
name: string;
|
|
429
|
+
/** Default false → `skinRequired` (`:147`). */
|
|
430
|
+
skin?: boolean;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** `type: "ik"` (`:149-176`). `scaleY` is 4.3's replacement for 4.2's `uniform`. */
|
|
434
|
+
export interface RigIkConstraint extends RigConstraintCommon {
|
|
435
|
+
type: 'ik';
|
|
436
|
+
/** At least one, resolved by name; a miss throws in the parser. */
|
|
437
|
+
bones: string[];
|
|
438
|
+
target: string;
|
|
439
|
+
/** `ConstraintData.ts:50`. Absent → `None`. */
|
|
440
|
+
scaleY?: 'none' | 'uniform' | 'volume';
|
|
441
|
+
/** Default 1. */
|
|
442
|
+
mix?: number;
|
|
443
|
+
/** Default 0. */
|
|
444
|
+
softness?: number;
|
|
445
|
+
/** Default true → `bendDirection = ±1`. */
|
|
446
|
+
bendPositive?: boolean;
|
|
447
|
+
/** Default false. */
|
|
448
|
+
compress?: boolean;
|
|
449
|
+
stretch?: boolean;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* One entry of a transform constraint's `properties` map: which source property
|
|
454
|
+
* drives which target properties, and by how much (`:241`, `:521`).
|
|
455
|
+
*
|
|
456
|
+
* The `from` and `to` names are drawn from a fixed six — `rotate`, `x`, `y`,
|
|
457
|
+
* `scaleX`, `scaleY`, `shearY` — and **anything else throws in the parser**.
|
|
458
|
+
*/
|
|
459
|
+
export interface RigTransformProperty {
|
|
460
|
+
offset?: number;
|
|
461
|
+
to: Record<string, { offset?: number; max?: number; scale?: number }>;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** `type: "transform"` (`:177-268`) — rebuilt from scratch in 4.3. */
|
|
465
|
+
export interface RigTransformConstraint extends RigConstraintCommon {
|
|
466
|
+
type: 'transform';
|
|
467
|
+
bones: string[];
|
|
468
|
+
/** 4.2 called this `target`. */
|
|
469
|
+
source: string;
|
|
470
|
+
localSource?: boolean;
|
|
471
|
+
localTarget?: boolean;
|
|
472
|
+
additive?: boolean;
|
|
473
|
+
clamp?: boolean;
|
|
474
|
+
/** `fromName -> { offset, to: { toName -> { offset, max, scale } } }`. */
|
|
475
|
+
properties?: Record<string, RigTransformProperty>;
|
|
476
|
+
/** The offsets array. Default 0 each. */
|
|
477
|
+
rotation?: number;
|
|
478
|
+
x?: number;
|
|
479
|
+
y?: number;
|
|
480
|
+
scaleX?: number;
|
|
481
|
+
scaleY?: number;
|
|
482
|
+
shearY?: number;
|
|
483
|
+
/**
|
|
484
|
+
* Default 1. ⚠️ Each mix is read **only if the matching `to` property was
|
|
485
|
+
* declared** (`:259-264`), so a mix without its property is dead data.
|
|
486
|
+
*/
|
|
487
|
+
mixRotate?: number;
|
|
488
|
+
mixX?: number;
|
|
489
|
+
/** Defaults to `mixX`. */
|
|
490
|
+
mixY?: number;
|
|
491
|
+
mixScaleX?: number;
|
|
492
|
+
/** Defaults to `mixScaleX`. */
|
|
493
|
+
mixScaleY?: number;
|
|
494
|
+
mixShearY?: number;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* `type: "physics"` (`:301-339`), 4.2+.
|
|
499
|
+
*
|
|
500
|
+
* ⚠️ The five components all default to 0, so a constraint that names none of
|
|
501
|
+
* them parses cleanly and does absolutely nothing — `A23`.
|
|
502
|
+
*/
|
|
503
|
+
export interface RigPhysicsConstraint extends RigConstraintCommon {
|
|
504
|
+
type: 'physics';
|
|
505
|
+
bone: string;
|
|
506
|
+
/** The components. All zero = a constraint that parses and does nothing. */
|
|
507
|
+
x?: number;
|
|
508
|
+
y?: number;
|
|
509
|
+
rotate?: number;
|
|
510
|
+
scaleX?: number;
|
|
511
|
+
shearX?: number;
|
|
512
|
+
/** 4.3; absent → `ScaleYMode.None`. */
|
|
513
|
+
scaleYMode?: 'none' | 'uniform' | 'volume';
|
|
514
|
+
/** Default 5000. */
|
|
515
|
+
limit?: number;
|
|
516
|
+
/** Default 60 → `step = 1/fps`. */
|
|
517
|
+
fps?: number;
|
|
518
|
+
/** Defaults: 0.5 / 100 / 0.85 / 1 / 0 / 0 / 1. */
|
|
519
|
+
inertia?: number;
|
|
520
|
+
strength?: number;
|
|
521
|
+
damping?: number;
|
|
522
|
+
/** Stored as `massInverse = 1/mass`, so 0 becomes Infinity — `A23`. */
|
|
523
|
+
mass?: number;
|
|
524
|
+
wind?: number;
|
|
525
|
+
gravity?: number;
|
|
526
|
+
mix?: number;
|
|
527
|
+
inertiaGlobal?: boolean;
|
|
528
|
+
strengthGlobal?: boolean;
|
|
529
|
+
dampingGlobal?: boolean;
|
|
530
|
+
massGlobal?: boolean;
|
|
531
|
+
windGlobal?: boolean;
|
|
532
|
+
gravityGlobal?: boolean;
|
|
533
|
+
mixGlobal?: boolean;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 🚧 `path` (`:269-300`) and `slider` (`:340-366`) are in the type so a spec can
|
|
538
|
+
* say them and be refused by name. Neither appears anywhere in the benchmark
|
|
539
|
+
* corpus (SPEC_COVERAGE part 4-2), so neither is on the ladder's critical path.
|
|
540
|
+
*/
|
|
541
|
+
export interface RigUnimplementedConstraint extends RigConstraintCommon {
|
|
542
|
+
type: 'path' | 'slider';
|
|
543
|
+
[field: string]: unknown;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export type RigConstraint =
|
|
547
|
+
| RigIkConstraint
|
|
548
|
+
| RigTransformConstraint
|
|
549
|
+
| RigPhysicsConstraint
|
|
550
|
+
| RigUnimplementedConstraint;
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// invariants — what skeleton JSON cannot say about itself
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Structural facts the emitted artifact does not record, handed to the validator
|
|
558
|
+
* so its archetype assertions have something to check instead of a guess.
|
|
559
|
+
*
|
|
560
|
+
* These are the fields that used to be properties of a hard-coded formation.
|
|
561
|
+
* They are optional, and an assertion whose field is absent reports **SKIP** —
|
|
562
|
+
* never a pass, because an assertion with nothing to look at has not looked.
|
|
563
|
+
*/
|
|
564
|
+
export interface RigInvariants {
|
|
565
|
+
/**
|
|
566
|
+
* How many slots of this rig may carry a mesh. A budget, not a Spine rule:
|
|
567
|
+
* every mesh is a canvas that re-rasterises whenever a bone driving it moves.
|
|
568
|
+
*/
|
|
569
|
+
meshSlots?: number;
|
|
570
|
+
/**
|
|
571
|
+
* How many triangles one of those meshes may carry. Also a budget, and also
|
|
572
|
+
* not a Spine rule — the editor's own example projects ship meshes several
|
|
573
|
+
* times this size and they are perfectly valid.
|
|
574
|
+
*
|
|
575
|
+
* ⚠️ Declare it or `A13_MESH_BUDGET` has nothing to measure against and SKIPs.
|
|
576
|
+
* A number baked into the validator would be one project's frame time
|
|
577
|
+
* masquerading as a property of the format, and would fail every foreign
|
|
578
|
+
* skeleton that is merely denser than that project can afford.
|
|
579
|
+
*/
|
|
580
|
+
meshTriangles?: number;
|
|
581
|
+
/**
|
|
582
|
+
* The bone whose setup rotation carries the cut's insertion axis. Its subtree
|
|
583
|
+
* is authored in **axis space** — translateX only — which is what lets one set
|
|
584
|
+
* of keys move to a cut at another camera angle (`A24`).
|
|
585
|
+
*/
|
|
586
|
+
axisBone?: string;
|
|
587
|
+
/**
|
|
588
|
+
* The bone carrying the inserting mass. Its own inward keys spend the same
|
|
589
|
+
* clearance the stroke does, so `A29`/`A30` add them together.
|
|
590
|
+
*/
|
|
591
|
+
massBone?: string;
|
|
592
|
+
/** Parentage that must never happen, with the reason it is tempting (`A25`). */
|
|
593
|
+
detached?: Array<{ bone: string; notUnder: string; why?: string }>;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
// the file
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
export interface RigSpec {
|
|
601
|
+
spec: 'rigc-rig/1';
|
|
602
|
+
/**
|
|
603
|
+
* The rig's own name. A motion spec's `archetype` field must equal it: the
|
|
604
|
+
* spec was authored against one formation, and pairing it with a different rig
|
|
605
|
+
* silently produces keys aimed at bones that mean something else.
|
|
606
|
+
*/
|
|
607
|
+
name: string;
|
|
608
|
+
note?: string;
|
|
609
|
+
skeleton?: RigSkeletonHeader;
|
|
610
|
+
/**
|
|
611
|
+
* Base directory for every `image` in this file, relative to the rig file
|
|
612
|
+
* itself. The CLI's `--images <dir>` overrides it (and is then relative to the
|
|
613
|
+
* working directory), which is how a foreign corpus is compiled without
|
|
614
|
+
* editing its rig spec.
|
|
615
|
+
*/
|
|
616
|
+
images?: string;
|
|
617
|
+
bones: RigBone[];
|
|
618
|
+
slots: RigSlot[];
|
|
619
|
+
/** At least `default`, which becomes `skeletonData.defaultSkin` (`:441`). */
|
|
620
|
+
skins?: Record<string, RigSkin>;
|
|
621
|
+
constraints?: RigConstraint[];
|
|
622
|
+
invariants?: RigInvariants;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// reading
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
630
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Parse and check the envelope, then hand back a typed spec.
|
|
635
|
+
*
|
|
636
|
+
* What is checked here is what makes the REST of the compiler able to assume its
|
|
637
|
+
* inputs: the version tag, the two required arrays, name uniqueness, and that
|
|
638
|
+
* every parent and every slot bone resolves against a bone declared earlier.
|
|
639
|
+
* Deeper checks (does an attachment's image exist, does a constraint's target
|
|
640
|
+
* bone exist) belong where the data is used, so their message can name the
|
|
641
|
+
* consumer.
|
|
642
|
+
*/
|
|
643
|
+
export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
644
|
+
if (!isObj(raw)) throw new CompileError(`${where}: a rig spec must be a JSON object`);
|
|
645
|
+
if (raw.spec !== RIG_SPEC_VERSION) {
|
|
646
|
+
throw new CompileError(`${where}: unknown rig spec version ${JSON.stringify(raw.spec)}, expected "${RIG_SPEC_VERSION}"`);
|
|
647
|
+
}
|
|
648
|
+
if (typeof raw.name !== 'string' || raw.name.length === 0) {
|
|
649
|
+
throw new CompileError(`${where}: a rig spec needs a "name" — a motion spec names it to pick this rig`);
|
|
650
|
+
}
|
|
651
|
+
if (!Array.isArray(raw.bones) || raw.bones.length === 0) {
|
|
652
|
+
throw new CompileError(`${where}: a rig spec needs a non-empty "bones" array`);
|
|
653
|
+
}
|
|
654
|
+
if (!Array.isArray(raw.slots)) {
|
|
655
|
+
throw new CompileError(`${where}: a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)`);
|
|
656
|
+
}
|
|
657
|
+
const spec = raw as unknown as RigSpec;
|
|
658
|
+
|
|
659
|
+
const seen = new Set<string>();
|
|
660
|
+
for (const bone of spec.bones) {
|
|
661
|
+
if (!isObj(bone) || typeof bone.name !== 'string' || bone.name.length === 0) {
|
|
662
|
+
throw new CompileError(`${where}: every bone needs a "name"`);
|
|
663
|
+
}
|
|
664
|
+
if (seen.has(bone.name)) {
|
|
665
|
+
throw new CompileError(`${where}: two bones are called "${bone.name}"; bone names are the join key for slots, meshes and timelines`);
|
|
666
|
+
}
|
|
667
|
+
seen.add(bone.name);
|
|
668
|
+
if (bone.parent === undefined) continue;
|
|
669
|
+
if (typeof bone.parent !== 'string' || !seen.has(bone.parent)) {
|
|
670
|
+
// The parser resolves `parent` against the bones it has already read, so a
|
|
671
|
+
// forward reference is not a rigc restriction — it is a bone with no parent
|
|
672
|
+
// in the loaded skeleton, which loads as a second root.
|
|
673
|
+
throw new CompileError(
|
|
674
|
+
`${where}: bone "${bone.name}" names parent ${JSON.stringify(bone.parent)}, which is not declared before it`,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
if (bone.inherit !== undefined && !RIG_BONE_INHERIT.some((v) => v.toLowerCase() === String(bone.inherit).toLowerCase())) {
|
|
678
|
+
throw new CompileError(
|
|
679
|
+
`${where}: bone "${bone.name}" has inherit ${JSON.stringify(bone.inherit)}; known: ${RIG_BONE_INHERIT.join(', ')}`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
const from = bone.from;
|
|
683
|
+
if (from !== undefined) {
|
|
684
|
+
const sources = ['anchor', 'slotWindow', 'meshCenter'].filter((k) => from[k as keyof RigBoneFrom] !== undefined);
|
|
685
|
+
if (sources.length > 1) {
|
|
686
|
+
throw new CompileError(
|
|
687
|
+
`${where}: bone "${bone.name}" takes its position from more than one source (${sources.join(', ')}); name exactly one`,
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if ((bone.x !== undefined || bone.y !== undefined) && sources.length === 1) {
|
|
691
|
+
throw new CompileError(
|
|
692
|
+
`${where}: bone "${bone.name}" declares both a literal x/y and from.${sources[0]}; the two would disagree the first time the art moved`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (from.rotation !== undefined && bone.rotation !== undefined) {
|
|
696
|
+
throw new CompileError(`${where}: bone "${bone.name}" declares both a literal rotation and from.rotation`);
|
|
697
|
+
}
|
|
698
|
+
if (from.rotation === 'anchor' && from.anchor === undefined) {
|
|
699
|
+
throw new CompileError(`${where}: bone "${bone.name}" wants its rotation from an anchor but names no from.anchor`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const slotNames = new Set<string>();
|
|
705
|
+
for (const slot of spec.slots) {
|
|
706
|
+
if (!isObj(slot) || typeof slot.name !== 'string' || slot.name.length === 0) {
|
|
707
|
+
throw new CompileError(`${where}: every slot needs a "name"`);
|
|
708
|
+
}
|
|
709
|
+
if (slotNames.has(slot.name)) throw new CompileError(`${where}: two slots are called "${slot.name}"`);
|
|
710
|
+
slotNames.add(slot.name);
|
|
711
|
+
if (typeof slot.bone !== 'string' || !seen.has(slot.bone)) {
|
|
712
|
+
throw new CompileError(
|
|
713
|
+
`${where}: slot "${slot.name}" names bone ${JSON.stringify(slot.bone)}, which this rig does not declare`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
if (slot.blend !== undefined && !RIG_SLOT_BLEND.some((v) => v.toLowerCase() === String(slot.blend).toLowerCase())) {
|
|
717
|
+
throw new CompileError(`${where}: slot "${slot.name}" has blend ${JSON.stringify(slot.blend)}; known: ${RIG_SLOT_BLEND.join(', ')}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const constraintNames = new Set<string>();
|
|
722
|
+
for (const constraint of spec.constraints ?? []) {
|
|
723
|
+
if (!isObj(constraint) || typeof constraint.name !== 'string' || constraint.name.length === 0) {
|
|
724
|
+
throw new CompileError(`${where}: every constraint needs a "name"`);
|
|
725
|
+
}
|
|
726
|
+
if (constraintNames.has(constraint.name)) throw new CompileError(`${where}: two constraints are called "${constraint.name}"`);
|
|
727
|
+
constraintNames.add(constraint.name);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return spec;
|
|
731
|
+
}
|