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/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
+ }