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/types.ts
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input and output shapes for rigc.
|
|
3
|
+
*
|
|
4
|
+
* Three inputs, one domain each:
|
|
5
|
+
* - the cut manifest, which owns measured geometry: crop, part offsets, part sizes, mask
|
|
6
|
+
* polygons, the state machine, anchors, the axis and the measured ceilings.
|
|
7
|
+
* Optional — a foreign skeleton has none;
|
|
8
|
+
* - the **rig spec** ([`src/rig.ts`](rig.ts), `spec: "rigc-rig/1"`), which owns
|
|
9
|
+
* skeleton structure: bones, slots, skins, constraints, invariants. Required.
|
|
10
|
+
* It replaced the three hard-coded archetype tables that used to be code;
|
|
11
|
+
* - the motion spec, which owns time: keys, named
|
|
12
|
+
* easings, groups, declared durations.
|
|
13
|
+
*
|
|
14
|
+
* Nothing else is an input, and the compiler never invents a value that is in
|
|
15
|
+
* none of them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Cut manifest (face class)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mesh declaration for a part — geometry, so it belongs to the manifest and not
|
|
24
|
+
* to the motion spec. It says WHERE the deformable ring
|
|
25
|
+
* is; the motion spec says WHEN it moves, by keying the control bone.
|
|
26
|
+
*/
|
|
27
|
+
export interface FaceManifestMesh {
|
|
28
|
+
/**
|
|
29
|
+
* Which generator builds this mesh. Absent means `ring`, so every manifest
|
|
30
|
+
* written before the joint archetype keeps its meaning.
|
|
31
|
+
*
|
|
32
|
+
* ring — three concentric rings + a hub. The outer two are pinned (region
|
|
33
|
+
* border, then mask contour) and only the aperture ring moves.
|
|
34
|
+
* ribbon — a two-wide strip along a bone chain. Length changes, width does
|
|
35
|
+
* not, because paired vertices carry identical weights.
|
|
36
|
+
*/
|
|
37
|
+
kind?: 'ring' | 'ribbon';
|
|
38
|
+
/** Ring only. The part's own mask polygon, which is the seam. */
|
|
39
|
+
hull?: 'polygon';
|
|
40
|
+
/** Ring only. Aperture centre in CROP pixels (y down) — measured, not guessed. */
|
|
41
|
+
center?: [number, number];
|
|
42
|
+
/** Ring only. Inner ring position between centre (0) and hull (1). */
|
|
43
|
+
inner?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Ring only, legacy form: ONE control bone which the compiler CREATES as a
|
|
46
|
+
* child of the slot bone. Used by archetypes that have no explicit bone tree.
|
|
47
|
+
*/
|
|
48
|
+
control_bone?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Control bones that already exist in the archetype's bone tree — the ring's
|
|
51
|
+
* authority is split between them by angular position, so a four-grip ring
|
|
52
|
+
* can expand asymmetrically without a key per vertex.
|
|
53
|
+
*/
|
|
54
|
+
control_bones?: string[];
|
|
55
|
+
/** Ribbon only. Number of cross rows; triangles = 2 * (rows - 1). */
|
|
56
|
+
rows?: number;
|
|
57
|
+
/** Ribbon only. The bone chain the strip rides, root first. */
|
|
58
|
+
chain?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Directional weighting. Without it the ring deforms symmetrically about the
|
|
61
|
+
* centre, which moves the upper lip and the upper teeth along with the jaw —
|
|
62
|
+
* anatomically wrong, and the owner spotted it on the first review.
|
|
63
|
+
*
|
|
64
|
+
* `axis_deg` is the mouth line measured on the art (screen degrees, y down).
|
|
65
|
+
* `ramp` is the signed distance across that axis, in part pixels, over which
|
|
66
|
+
* control authority goes 0 -> 1; positive is the jaw side.
|
|
67
|
+
*/
|
|
68
|
+
bias?: { axis_deg: number; ramp: [number, number]; note?: string };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FaceManifestPart {
|
|
72
|
+
slot: string;
|
|
73
|
+
/**
|
|
74
|
+
* The archetype slot this part joins on, when it differs from `slot`.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ A cut manifest is often ALSO the record of the pipeline that generated
|
|
77
|
+
* the art, and that pipeline names parts after what they depict while a rig
|
|
78
|
+
* names them after the role they play. The rig's slot table has to stay
|
|
79
|
+
* single-valued — the runtime, the tooling and A26 all join on the emitted slot
|
|
80
|
+
* name, and a second alias for one slot is how a slot vanishes with no error.
|
|
81
|
+
* So the manifest carries the mapping and `slot` keeps meaning what its author
|
|
82
|
+
* meant. Absent = the two are the same name.
|
|
83
|
+
*/
|
|
84
|
+
rig_slot?: string;
|
|
85
|
+
draw_order: number;
|
|
86
|
+
/**
|
|
87
|
+
* Base plate only: the unmodified crop. Explicit `null` (with no `states`)
|
|
88
|
+
* means the manifest is recording a part this cut does NOT carry, and the
|
|
89
|
+
* compiler skips it and reports the absence.
|
|
90
|
+
*/
|
|
91
|
+
image?: string | null;
|
|
92
|
+
/** Top-left of the part window in crop pixels, y down. */
|
|
93
|
+
offset: [number, number];
|
|
94
|
+
/** Part window size in pixels. Absent on the base plate (= the crop size). */
|
|
95
|
+
size?: [number, number];
|
|
96
|
+
/** Which key of `state_machine` drives this slot. */
|
|
97
|
+
state_key?: string;
|
|
98
|
+
/** state name -> PNG path relative to the manifest, or null for "base pixels". */
|
|
99
|
+
states?: Record<string, string | null>;
|
|
100
|
+
/** Mask polygon in CROP pixels (y down). Required when `mesh` is present. */
|
|
101
|
+
polygon?: Array<[number, number]>;
|
|
102
|
+
/** Promote this part's attachments from regions to ring meshes. */
|
|
103
|
+
mesh?: FaceManifestMesh;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface FaceManifest {
|
|
107
|
+
schema: string;
|
|
108
|
+
crop: { x: number; y: number; w: number; h: number; resample: string };
|
|
109
|
+
base: string;
|
|
110
|
+
/** Overlay archetypes only; the joint archetype has no per-slot state list. */
|
|
111
|
+
state_machine?: Record<string, string[]>;
|
|
112
|
+
parts: FaceManifestPart[];
|
|
113
|
+
|
|
114
|
+
// -- articulated-cut fields ------------------------------------------------
|
|
115
|
+
/**
|
|
116
|
+
* Entry point in crop pixels, y down — the origin of the cut's axis frame.
|
|
117
|
+
*/
|
|
118
|
+
insertion?: [number, number];
|
|
119
|
+
/**
|
|
120
|
+
* ⭐ The one value a new cut of this archetype changes.
|
|
121
|
+
* `deg` is SCREEN degrees, y down, the same convention as `mesh.bias.axis_deg`;
|
|
122
|
+
* the compiler negates it into Spine's y-up CCW rotation. `unit` is the same
|
|
123
|
+
* direction as a vector and is cross-checked against `deg`, because a manifest
|
|
124
|
+
* that disagrees with itself is the cheapest bug to catch and the worst to
|
|
125
|
+
* debug later.
|
|
126
|
+
*/
|
|
127
|
+
axis?: { deg: number; unit: [number, number] };
|
|
128
|
+
/**
|
|
129
|
+
* One-way stroke amplitude in axis pixels, the extension the plate covers, and
|
|
130
|
+
* the DERIVED ceiling on inward travel.
|
|
131
|
+
*
|
|
132
|
+
* 🎯 `contact_depth` is the owner's rule of 2026-08-22 made mechanical: the
|
|
133
|
+
* swallow goes at most until the inserting mass touches the occluder. It is a
|
|
134
|
+
* MEASURED fact about two plates (rigc/tools/contact.ts), so it belongs in the
|
|
135
|
+
* manifest for exactly the reason `mesh.center` does — the compiler never
|
|
136
|
+
* re-measures art. Assertion A29 holds every animation to it.
|
|
137
|
+
*/
|
|
138
|
+
stroke?: {
|
|
139
|
+
amplitude?: number;
|
|
140
|
+
extension?: number;
|
|
141
|
+
contact_depth?: number | null;
|
|
142
|
+
/**
|
|
143
|
+
* 🎯 The second, independent ceiling on inward travel: the deepest insert at
|
|
144
|
+
* which the moving part's cap contour is still entirely inside the occluder's
|
|
145
|
+
* opaque footprint. Past it the cap is DRAWN where it should be swallowed.
|
|
146
|
+
*
|
|
147
|
+
* It is not a restatement of `contact_depth`. Contact asks when two masses
|
|
148
|
+
* collide; containment asks when the drawn flesh runs out of patch to hide
|
|
149
|
+
* behind — and a cut can have one without the other. The real tier-2 cut has
|
|
150
|
+
* exactly that shape: no contact ceiling at all, and a containment ceiling of
|
|
151
|
+
* 118px. Measured, like every other art fact in this file. Assertion A30.
|
|
152
|
+
*/
|
|
153
|
+
cap_containment_ceiling?: number | null;
|
|
154
|
+
};
|
|
155
|
+
/** ROI box, recorded for provenance; the compiler does not read it. */
|
|
156
|
+
roi?: { x: number; y: number; w: number; h: number };
|
|
157
|
+
/**
|
|
158
|
+
* Bone positions in crop pixels, y down: `[x, y]`, or `[x, y, facing_deg]`
|
|
159
|
+
* where the third element is a SCREEN-space facing angle that becomes the
|
|
160
|
+
* bone's setup rotation. A grip whose local +X points radially outward turns
|
|
161
|
+
* "expand the ring" into one shared translate key, which is the same trick the
|
|
162
|
+
* `axis` bone plays for the stroke.
|
|
163
|
+
*/
|
|
164
|
+
anchors?: Record<string, number[]>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Motion spec (spec: "rigc-motion/1")
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/** Graph-view style normalised handles [hx1, hy1, hx2, hy2]. */
|
|
172
|
+
export type EasingHandles = [number, number, number, number];
|
|
173
|
+
|
|
174
|
+
export interface MotionKey {
|
|
175
|
+
/** Time in seconds. */
|
|
176
|
+
t: number;
|
|
177
|
+
/**
|
|
178
|
+
* Value. Meaning depends on the track property:
|
|
179
|
+
* rgba -> [r, g, b, a] in 0..1
|
|
180
|
+
* attachment -> attachment name, or null for "show nothing"
|
|
181
|
+
* translate -> [x, y] in pixels, relative to the bone's setup position
|
|
182
|
+
* scale -> [x, y] as multipliers (1 = setup)
|
|
183
|
+
* rotate -> [degrees]
|
|
184
|
+
* mix -> [0..1] physics authority
|
|
185
|
+
* reset -> null; the key is the event
|
|
186
|
+
*/
|
|
187
|
+
v: number[] | string | null;
|
|
188
|
+
/** Named easing from `easings`, or "stepped". Absent = linear. */
|
|
189
|
+
ease?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Escape hatch: this key's bezier written out, as ABSOLUTE (time, value)
|
|
192
|
+
* control points — four numbers per value channel, in field order, which is
|
|
193
|
+
* exactly what the emitted JSON holds.
|
|
194
|
+
*
|
|
195
|
+
* ⭐ `ease` stays the recommended path and a key may carry one or the other,
|
|
196
|
+
* never both. A named easing says "this shape, wherever it is used", which is
|
|
197
|
+
* what makes a motion spec readable as intent; this says "these numbers", which
|
|
198
|
+
* is what a transcription of an editor export needs, because an export has a
|
|
199
|
+
* different shape per key per channel.
|
|
200
|
+
*
|
|
201
|
+
* ⚠️ Not the normalised graph-view handles `easings` takes. Those go through
|
|
202
|
+
* `bezierForChannel`; writing them here loads clean and plays a different
|
|
203
|
+
* curve.
|
|
204
|
+
*/
|
|
205
|
+
curve?: number[] | 'stepped';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Bone timelines the compiler emits. Channel counts live in the validator.
|
|
210
|
+
*
|
|
211
|
+
* The single-axis forms are not sugar for the paired ones: Spine keys them as
|
|
212
|
+
* separate timelines, and an export that used `translatex` alone is not
|
|
213
|
+
* reproduced by a `translate` whose y channel happens to be flat — the key
|
|
214
|
+
* counts differ, and so does what a runtime blends against.
|
|
215
|
+
*/
|
|
216
|
+
export type BoneProperty =
|
|
217
|
+
| 'translate'
|
|
218
|
+
| 'translatex'
|
|
219
|
+
| 'translatey'
|
|
220
|
+
| 'scale'
|
|
221
|
+
| 'scalex'
|
|
222
|
+
| 'scaley'
|
|
223
|
+
| 'shear'
|
|
224
|
+
| 'shearx'
|
|
225
|
+
| 'sheary'
|
|
226
|
+
| 'rotate';
|
|
227
|
+
|
|
228
|
+
/** Physics timelines the compiler emits. `reset` carries no value at all. */
|
|
229
|
+
export type PhysicsProperty = 'mix' | 'reset';
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* One physics constraint.
|
|
233
|
+
*
|
|
234
|
+
* Structure, so it could argue for the manifest — but every field here is a
|
|
235
|
+
* tuning number for motion over time, so the starting-parameter table goes into
|
|
236
|
+
* the motion spec. It lives with the keys it competes against.
|
|
237
|
+
*
|
|
238
|
+
* ⚠️ The component fields (`x`/`y`/`rotate`/`scaleX`/`shearX`) all default to 0,
|
|
239
|
+
* which means a constraint that names none of them parses cleanly and does
|
|
240
|
+
* absolutely nothing. That is assertion A23.
|
|
241
|
+
*/
|
|
242
|
+
export interface MotionPhysics {
|
|
243
|
+
bone: string;
|
|
244
|
+
x?: number;
|
|
245
|
+
y?: number;
|
|
246
|
+
rotate?: number;
|
|
247
|
+
scaleX?: number;
|
|
248
|
+
shearX?: number;
|
|
249
|
+
inertia?: number;
|
|
250
|
+
strength?: number;
|
|
251
|
+
damping?: number;
|
|
252
|
+
mass?: number;
|
|
253
|
+
wind?: number;
|
|
254
|
+
gravity?: number;
|
|
255
|
+
mix?: number;
|
|
256
|
+
fps?: number;
|
|
257
|
+
limit?: number;
|
|
258
|
+
note?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface MotionTrack {
|
|
262
|
+
/** Target one slot... */
|
|
263
|
+
slot?: string;
|
|
264
|
+
/** ...or a named group of slots. */
|
|
265
|
+
group?: string;
|
|
266
|
+
/** ...or one bone, for the mesh tier: the control bone carries every key. */
|
|
267
|
+
bone?: string;
|
|
268
|
+
/** ...or one physics constraint, by name. */
|
|
269
|
+
physics?: string;
|
|
270
|
+
property: 'rgba' | 'attachment' | BoneProperty | PhysicsProperty;
|
|
271
|
+
/** Seconds added to every key time of this track. */
|
|
272
|
+
lag?: number;
|
|
273
|
+
/** Extra per-member delay inside a group, in member order. */
|
|
274
|
+
stagger?: number;
|
|
275
|
+
keys: MotionKey[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* One slot moved, at one draw-order key: `offset` positions later in the array.
|
|
280
|
+
*
|
|
281
|
+
* ⚠️ The offset is counted against the SETUP order, not against wherever the
|
|
282
|
+
* slot ended up at the previous key — `readDrawOrder` rebuilds the whole
|
|
283
|
+
* permutation from the setup array every time (SkeletonJson.ts:1336-1374). A key
|
|
284
|
+
* is a complete statement of the change, not an edit to the one before it.
|
|
285
|
+
*/
|
|
286
|
+
export interface MotionDrawOrderOffset {
|
|
287
|
+
slot: string;
|
|
288
|
+
/** How many places later this slot is drawn. Negative moves it earlier. */
|
|
289
|
+
offset: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* One key of the whole-animation draw-order timeline.
|
|
294
|
+
*
|
|
295
|
+
* A key with **no** `offsets` restores the setup draw order — that is the
|
|
296
|
+
* parser's own encoding (`readDrawOrder` returns null, and the timeline sets the
|
|
297
|
+
* setup array), and it is how an animation that has swapped two slots puts them
|
|
298
|
+
* back.
|
|
299
|
+
*/
|
|
300
|
+
export interface MotionDrawOrderKey {
|
|
301
|
+
/** Time in seconds. */
|
|
302
|
+
t: number;
|
|
303
|
+
offsets?: MotionDrawOrderOffset[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface MotionAnimation {
|
|
307
|
+
/** Declared, then verified against the compiled result (rule 4). */
|
|
308
|
+
duration: number;
|
|
309
|
+
/** Player hint only; not expressible in skeleton JSON. */
|
|
310
|
+
loop: boolean;
|
|
311
|
+
note?: string;
|
|
312
|
+
tracks: MotionTrack[];
|
|
313
|
+
/**
|
|
314
|
+
* The draw-order timeline. **One per animation, and it names no target** —
|
|
315
|
+
* which is why it is not a `track`: 4.3 writes it as `animations.<a>.drawOrder`
|
|
316
|
+
* beside `bones` and `slots`, not inside either (SPEC_COVERAGE part 1-8).
|
|
317
|
+
*
|
|
318
|
+
* Draw order is the one thing about a slot that the slots array already
|
|
319
|
+
* states (rule R4), so this timeline is the only way to say it changes over
|
|
320
|
+
* time. First needed at ladder rung 5.
|
|
321
|
+
*/
|
|
322
|
+
drawOrder?: MotionDrawOrderKey[];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Setup pose per slot. Declared, never inferred — rule 5. It decides which of
|
|
327
|
+
* the two overlay mechanisms a slot uses:
|
|
328
|
+
* - an attachment + alpha 0 => the lid tier, driven by rgba timelines;
|
|
329
|
+
* - attachment null => the swap tier, driven by attachment timelines
|
|
330
|
+
* (null = the untouched base pixels show).
|
|
331
|
+
*/
|
|
332
|
+
export interface MotionSetupSlot {
|
|
333
|
+
attachment?: string | null;
|
|
334
|
+
/** [r, g, b, a] in 0..1. Omit for opaque white. */
|
|
335
|
+
color?: [number, number, number, number];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface MotionSpec {
|
|
339
|
+
spec: 'rigc-motion/1';
|
|
340
|
+
/**
|
|
341
|
+
* The rig this spec was authored against — it must equal the rig spec's
|
|
342
|
+
* `name`, and a mismatch is a compile error rather than a silent pairing.
|
|
343
|
+
*
|
|
344
|
+
* It named a hard-coded table until 2026-08-22; now it names a file's own
|
|
345
|
+
* name, and the file's path comes from the cuts table. The check is kept
|
|
346
|
+
* because the keys in here are aimed at bones by NAME: pair the spec with
|
|
347
|
+
* another rig whose names happen to overlap and every one of them lands on
|
|
348
|
+
* something that means something else.
|
|
349
|
+
*/
|
|
350
|
+
archetype: string;
|
|
351
|
+
cut: string;
|
|
352
|
+
note?: string;
|
|
353
|
+
easings: Record<string, EasingHandles>;
|
|
354
|
+
groups?: Record<string, string[]>;
|
|
355
|
+
setup?: Record<string, MotionSetupSlot>;
|
|
356
|
+
/** Physics constraints by name. Emitted into the 4.3 `constraints` array. */
|
|
357
|
+
physics?: Record<string, MotionPhysics>;
|
|
358
|
+
animations: Record<string, MotionAnimation>;
|
|
359
|
+
/** Player-side AnimationStateData config; not emitted into skeleton JSON. */
|
|
360
|
+
mix?: { default: number; pairs?: Array<[string, string, number]> };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Emitted Spine 4.3 skeleton JSON
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Field order here is the order the emitter writes them, and it is rigc's, not
|
|
369
|
+
* the editor's: Spine writes `length, rotation, x, y` and rigc writes
|
|
370
|
+
* `length, x, y, rotation`. Both load identically — key order carries no meaning
|
|
371
|
+
* in JSON — and rigc's order is the one every artifact on disk already has, so
|
|
372
|
+
* changing it would be a byte-level diff that says nothing.
|
|
373
|
+
*
|
|
374
|
+
* A field is present exactly when the rig spec declared it; see `src/rig.ts`.
|
|
375
|
+
*/
|
|
376
|
+
export interface SpineBone {
|
|
377
|
+
name: string;
|
|
378
|
+
parent?: string;
|
|
379
|
+
length?: number;
|
|
380
|
+
x?: number;
|
|
381
|
+
y?: number;
|
|
382
|
+
/** Spine degrees, CCW in a y-up world. */
|
|
383
|
+
rotation?: number;
|
|
384
|
+
scaleX?: number;
|
|
385
|
+
scaleY?: number;
|
|
386
|
+
shearX?: number;
|
|
387
|
+
shearY?: number;
|
|
388
|
+
/** 4.2+ name. 4.0/4.1's `transform` still loads and is silently ignored — A02. */
|
|
389
|
+
inherit?: string;
|
|
390
|
+
skin?: boolean;
|
|
391
|
+
color?: string;
|
|
392
|
+
/** Editor-only affordance, read at `SkeletonJson.ts:121-126`. */
|
|
393
|
+
icon?: string;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface SpineSlot {
|
|
397
|
+
name: string;
|
|
398
|
+
bone: string;
|
|
399
|
+
attachment?: string;
|
|
400
|
+
color?: string;
|
|
401
|
+
dark?: string;
|
|
402
|
+
blend?: string;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export interface SpineRegionAttachment {
|
|
406
|
+
path?: string;
|
|
407
|
+
/** Required. Omitting these yields NaN with no error. */
|
|
408
|
+
width: number;
|
|
409
|
+
height: number;
|
|
410
|
+
x?: number;
|
|
411
|
+
y?: number;
|
|
412
|
+
/**
|
|
413
|
+
* Cancels the bone's world rotation so a plate authored in screen space stays
|
|
414
|
+
* screen-upright under a rotated bone. Without it every slot hanging off the
|
|
415
|
+
* `axis` bone would render tilted by the axis angle.
|
|
416
|
+
*/
|
|
417
|
+
rotation?: number;
|
|
418
|
+
scaleX?: number;
|
|
419
|
+
scaleY?: number;
|
|
420
|
+
color?: string;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Weighted mesh. `triangles` and `uvs` are not optional in practice: a missing
|
|
425
|
+
* `triangles` loads as `undefined` and `uvs` is
|
|
426
|
+
* what decides `worldVerticesLength`.
|
|
427
|
+
*/
|
|
428
|
+
export interface SpineMeshAttachment {
|
|
429
|
+
type: 'mesh';
|
|
430
|
+
path?: string;
|
|
431
|
+
uvs: number[];
|
|
432
|
+
triangles: number[];
|
|
433
|
+
/** Weighted encoding: boneCount, (boneIndex, bindX, bindY, weight)*n, repeated. */
|
|
434
|
+
vertices: number[];
|
|
435
|
+
/** Hull vertex count. The loader stores this doubled. */
|
|
436
|
+
hull: number;
|
|
437
|
+
/** Nonessential, but they make the mesh budget assertions readable. */
|
|
438
|
+
width: number;
|
|
439
|
+
height: number;
|
|
440
|
+
/** Nonessential index pairs the editor draws; carried through when authored. */
|
|
441
|
+
edges?: number[];
|
|
442
|
+
color?: string;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export type SpineAttachment = SpineRegionAttachment | SpineMeshAttachment;
|
|
446
|
+
|
|
447
|
+
export type SpineTimelineKey = Record<string, unknown>;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 4.3 puts every constraint type in ONE top-level `constraints` array and
|
|
451
|
+
* branches on `type` (SkeletonJson.js:129-350). The 4.1-era per-type arrays
|
|
452
|
+
* (`physics: [...]`, `ik: [...]`) are not read at all — the constraint vanishes
|
|
453
|
+
* with no error, which is assertion A01.
|
|
454
|
+
*/
|
|
455
|
+
export type SpineConstraint = { name: string; type: string } & Record<string, unknown>;
|
|
456
|
+
|
|
457
|
+
export interface SpineSkeletonJson {
|
|
458
|
+
skeleton: {
|
|
459
|
+
spine: string;
|
|
460
|
+
x: number;
|
|
461
|
+
y: number;
|
|
462
|
+
width: number;
|
|
463
|
+
height: number;
|
|
464
|
+
fps?: number;
|
|
465
|
+
referenceScale?: number;
|
|
466
|
+
images?: string;
|
|
467
|
+
};
|
|
468
|
+
bones: SpineBone[];
|
|
469
|
+
slots: SpineSlot[];
|
|
470
|
+
constraints?: SpineConstraint[];
|
|
471
|
+
skins: Array<{ name: string; attachments: Record<string, Record<string, SpineAttachment>> }>;
|
|
472
|
+
animations: Record<
|
|
473
|
+
string,
|
|
474
|
+
{
|
|
475
|
+
slots?: Record<string, Record<string, SpineTimelineKey[]>>;
|
|
476
|
+
bones?: Record<string, Record<string, SpineTimelineKey[]>>;
|
|
477
|
+
physics?: Record<string, Record<string, SpineTimelineKey[]>>;
|
|
478
|
+
/** Whole-animation timeline: no target name, one array per animation. */
|
|
479
|
+
drawOrder?: SpineTimelineKey[];
|
|
480
|
+
}
|
|
481
|
+
>;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// Compiler result
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
export interface CompiledImage {
|
|
489
|
+
/** Region name = attachment name = PNG basename. */
|
|
490
|
+
region: string;
|
|
491
|
+
/** Atlas page name: the PNG path relative to the atlas file. */
|
|
492
|
+
page: string;
|
|
493
|
+
/** Absolute path on disk, for the size assertions. */
|
|
494
|
+
absPath: string;
|
|
495
|
+
width: number;
|
|
496
|
+
height: number;
|
|
497
|
+
hasAlpha: boolean;
|
|
498
|
+
isBase: boolean;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Structural expectations the validator cannot read out of skeleton JSON.
|
|
503
|
+
*
|
|
504
|
+
* Some invariants of a rig are simply not written down in the artifact:
|
|
505
|
+
* nothing in the file says "this mesh is a ribbon" or "this emitter must not
|
|
506
|
+
* hang off the part that released it". The compiler knows, because the rig
|
|
507
|
+
* spec's `invariants` block says so, and it hands the knowledge over rather than
|
|
508
|
+
* letting the validator guess. Mutants stay honest because a mutant edits the
|
|
509
|
+
* ARTIFACT while this block keeps saying what the rig was supposed to be.
|
|
510
|
+
*/
|
|
511
|
+
export interface RigInfo {
|
|
512
|
+
/** The rig spec's `name`. Reported by the validator so a green names its rig. */
|
|
513
|
+
archetype: string;
|
|
514
|
+
/** The bone whose setup rotation carries the cut's axis, if the rig has one. */
|
|
515
|
+
axisBone: string | null;
|
|
516
|
+
/** Bones under the axis bone, whose translate keys must stay on the axis. */
|
|
517
|
+
axisSubtree: string[];
|
|
518
|
+
/** [bone, ancestor it must never have] — see `invariants.detached`. */
|
|
519
|
+
detached: Array<[string, string]>;
|
|
520
|
+
/** Canonical draw order (the rig's slot array), or null if it declares none. */
|
|
521
|
+
slotOrder: string[] | null;
|
|
522
|
+
/**
|
|
523
|
+
* slot -> what built this mesh, for the kind-aware mesh assertions.
|
|
524
|
+
*
|
|
525
|
+
* `ring` and `ribbon` are rigc's own generators, whose topology it therefore
|
|
526
|
+
* knows: where the rim is, which edge is the entry row, that the rows pair up.
|
|
527
|
+
* **`authored`** is geometry that came in through the rig spec — drawn by an
|
|
528
|
+
* animator, transcribed from an export — and rigc knows nothing about its
|
|
529
|
+
* topology at all. An assertion that measures generator topology has nothing
|
|
530
|
+
* to say about one, so it SKIPs with that as the reason rather than checking
|
|
531
|
+
* a ring the mesh was never supposed to be.
|
|
532
|
+
*/
|
|
533
|
+
meshKinds: Record<string, 'ring' | 'ribbon' | 'authored'>;
|
|
534
|
+
/** Mesh slots this rig budgets for, or null when it declares no budget. */
|
|
535
|
+
meshSlotBudget: number | null;
|
|
536
|
+
/** Triangles one mesh may carry, or null when the rig declares no budget. */
|
|
537
|
+
meshTriangleBudget: number | null;
|
|
538
|
+
/** Deepest inward advance the two masses allow, from the manifest. */
|
|
539
|
+
contactDepth: number | null;
|
|
540
|
+
/**
|
|
541
|
+
* Deepest inward advance at which the cap contour is still covered, from the
|
|
542
|
+
* manifest. Null when the cut has not measured one — A30 then says nothing
|
|
543
|
+
* rather than inventing a wall.
|
|
544
|
+
*/
|
|
545
|
+
capContainmentCeiling: number | null;
|
|
546
|
+
/**
|
|
547
|
+
* The bone the inserting mass hangs on. Its own inward keys spend the same
|
|
548
|
+
* clearance the stroke does: if both move in, both close the gap.
|
|
549
|
+
*/
|
|
550
|
+
massBone: string | null;
|
|
551
|
+
/** Inward unit vector in SPINE world (y up), for projecting off-axis keys. */
|
|
552
|
+
inwardUnit: [number, number] | null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export interface CompileResult {
|
|
556
|
+
skeleton: SpineSkeletonJson;
|
|
557
|
+
skeletonText: string;
|
|
558
|
+
atlasText: string;
|
|
559
|
+
images: CompiledImage[];
|
|
560
|
+
/** States listed in the manifest whose PNG is not on disk. */
|
|
561
|
+
droppedStates: Array<{ slot: string; state: string; path: string }>;
|
|
562
|
+
/**
|
|
563
|
+
* Parts the manifest declares and the cut does not carry (`image: null`, no
|
|
564
|
+
* states). Reported rather than swallowed: "the optional slots are optional" is
|
|
565
|
+
* a claim about the emit path, so the emit path says out loud which ones it
|
|
566
|
+
* left out.
|
|
567
|
+
*/
|
|
568
|
+
absentParts: Array<{ slot: string; why: string }>;
|
|
569
|
+
/** Declared durations, carried into the validator (rule 4). */
|
|
570
|
+
declaredDurations: Record<string, number>;
|
|
571
|
+
/** Bones that drive a mesh attachment: the slot bone plus its control bone. */
|
|
572
|
+
meshBones: string[];
|
|
573
|
+
/** Mesh slots emitted, with triangle counts — reported by `build`. */
|
|
574
|
+
meshes: Array<{
|
|
575
|
+
slot: string;
|
|
576
|
+
kind: 'ring' | 'ribbon' | 'authored';
|
|
577
|
+
attachments: string[];
|
|
578
|
+
vertices: number;
|
|
579
|
+
triangles: number;
|
|
580
|
+
bones: string[];
|
|
581
|
+
}>;
|
|
582
|
+
/** Structural expectations handed to the validator. */
|
|
583
|
+
rig: RigInfo;
|
|
584
|
+
/** Physics constraints emitted, with the bone each one drives. */
|
|
585
|
+
physics: Array<{ name: string; bone: string; components: string[]; mix: number; drivesMesh: boolean }>;
|
|
586
|
+
}
|