spine-rigc 0.22.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -1
- package/cli.ts +205 -11
- package/docs/AUTHORING.md +357 -54
- package/docs/INGEST.md +185 -41
- package/docs/SPEC_COVERAGE.md +13 -1
- package/package.json +1 -1
- package/skills/ingest/SKILL.md +33 -11
- package/src/atlas.ts +57 -13
- package/src/check.ts +83 -1
- package/src/compile.ts +284 -58
- package/src/diff.ts +125 -2
- package/src/ingest.ts +1078 -0
- package/src/render.ts +104 -10
- package/src/rig.ts +92 -8
- package/src/types.ts +39 -7
- package/src/validate.ts +205 -27
- package/tools/editor_roundtrip.ts +172 -21
package/src/render.ts
CHANGED
|
@@ -184,6 +184,19 @@ export interface FramesSidecar {
|
|
|
184
184
|
example?: string;
|
|
185
185
|
rung?: string;
|
|
186
186
|
skeleton?: string;
|
|
187
|
+
/**
|
|
188
|
+
* The skin these frames were posed under, when one was asked for (issue #571).
|
|
189
|
+
*
|
|
190
|
+
* ⭐ **Absent is not `"default"`.** A render with no skin sets none — every
|
|
191
|
+
* slot resolves through `SkeletonData.defaultSkin` alone — and a frame set
|
|
192
|
+
* written before this field existed says nothing either, so the two are the
|
|
193
|
+
* same fact on disk and the field is omitted for both. That is what keeps
|
|
194
|
+
* every frame set in this repository byte-identical across this change, and it
|
|
195
|
+
* is why `check` can refuse a mismatch it can SEE (`skin` present and
|
|
196
|
+
* different, or present where the run asked for none) and can only NOTE the
|
|
197
|
+
* one it cannot (`skin` absent while the run asked for one).
|
|
198
|
+
*/
|
|
199
|
+
skin?: string;
|
|
187
200
|
/** The colour the frames were cleared to, straight RGBA 0..255. */
|
|
188
201
|
background: RGBA;
|
|
189
202
|
viewport: {
|
|
@@ -281,6 +294,71 @@ export interface PoseOptions {
|
|
|
281
294
|
* one instrument that does (`bonedist.ts`) needs it on every frame.
|
|
282
295
|
*/
|
|
283
296
|
bones?: boolean;
|
|
297
|
+
/**
|
|
298
|
+
* Pose under this skin, by the name the skeleton declares for it.
|
|
299
|
+
*
|
|
300
|
+
* ⭐ Absent means **no skin is set at all**, which is spine-core's own initial
|
|
301
|
+
* state (`Skeleton.skin` is null) and resolves every slot through
|
|
302
|
+
* `SkeletonData.defaultSkin` alone. That is not the same claim as "the default
|
|
303
|
+
* skin was chosen": it is the absence of a choice, and the two are spelled
|
|
304
|
+
* differently everywhere this travels — the frames sidecar omits the field
|
|
305
|
+
* rather than writing `"default"` into it (issue #571).
|
|
306
|
+
*
|
|
307
|
+
* ⚠️ Read by the SAMPLERS, never by `piecesOf`, which is handed a skeleton
|
|
308
|
+
* somebody else already posed; handing it one is refused by name rather than
|
|
309
|
+
* ignored, because a skin quietly dropped here is exactly the silence this
|
|
310
|
+
* whole flag exists to remove.
|
|
311
|
+
*/
|
|
312
|
+
skin?: string;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* A fresh skeleton with `skin` applied, or refused by name.
|
|
317
|
+
*
|
|
318
|
+
* ## Why the skin goes on before `setupPose`, and why nothing else is needed
|
|
319
|
+
*
|
|
320
|
+
* `Skeleton.setSkin` (spine-core 4.3.13 `Skeleton.js:279-311`) attaches the new
|
|
321
|
+
* skin's art into each slot's pose and calls `updateCache`, which is what turns
|
|
322
|
+
* on a `skinRequired` bone or constraint the skin names. Every sampler below
|
|
323
|
+
* then calls `skeleton.setupPose()`, and `setupPose` → `setupPoseSlots`
|
|
324
|
+
* (`Skeleton.js:231-249`) re-resolves each slot's setup attachment through
|
|
325
|
+
* `Slot.setupPose` → `Skeleton.getAttachment`, which checks `this.skin` first
|
|
326
|
+
* and `SkeletonData.defaultSkin` second (`Skeleton.js:335-346`). So the setup
|
|
327
|
+
* pose of a skinned skeleton is already the skin's, and the extra
|
|
328
|
+
* `setSlotsToSetupPose()` a 4.1-era recipe prescribes has no 4.3 spelling to
|
|
329
|
+
* call: the method is named `setupPoseSlots` here, and `setupPose()` runs it.
|
|
330
|
+
*
|
|
331
|
+
* `setSkin(string)` exists too, but its by-name half throws
|
|
332
|
+
* `Skin not found: <name>` (`Skeleton.js:286-291`) — a message that names the
|
|
333
|
+
* miss and not the alternatives. Everything in a rig resolves by name and a miss
|
|
334
|
+
* is refused **by name, with the names that would have worked**, so the lookup
|
|
335
|
+
* happens here and the runtime is handed a `Skin` it cannot fail on.
|
|
336
|
+
*/
|
|
337
|
+
function skeletonUnderSkin(data: SkeletonData, skin: string | undefined): Skeleton {
|
|
338
|
+
const skeleton = new Skeleton(data);
|
|
339
|
+
if (skin === undefined) return skeleton;
|
|
340
|
+
const found = data.findSkin(skin);
|
|
341
|
+
if (!found) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`no skin ${JSON.stringify(skin)} in this skeleton; it declares [${
|
|
344
|
+
data.skins.map((s) => s.name).join(', ') || 'none'
|
|
345
|
+
}]`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
skeleton.setSkin(found);
|
|
349
|
+
return skeleton;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The same options with the skin taken off — what the samplers hand `piecesOf`.
|
|
354
|
+
*
|
|
355
|
+
* Spelled once, so the one function that must not see a `skin` cannot come to
|
|
356
|
+
* see one because a second call site forgot.
|
|
357
|
+
*/
|
|
358
|
+
function piecesOptions(opts: PoseOptions | undefined): PoseOptions | undefined {
|
|
359
|
+
if (opts === undefined || opts.skin === undefined) return opts;
|
|
360
|
+
const { skin: _applied, ...rest } = opts;
|
|
361
|
+
return rest;
|
|
284
362
|
}
|
|
285
363
|
|
|
286
364
|
/**
|
|
@@ -460,7 +538,8 @@ export function sampleAnimation(data: SkeletonData, name: string, fps: number, o
|
|
|
460
538
|
`no animation "${name}" in this skeleton; it has [${data.animations.map((a) => a.name).join(', ') || 'none'}]`,
|
|
461
539
|
);
|
|
462
540
|
}
|
|
463
|
-
const skeleton =
|
|
541
|
+
const skeleton = skeletonUnderSkin(data, opts?.skin);
|
|
542
|
+
const pieceOpts = piecesOptions(opts);
|
|
464
543
|
const state = new AnimationState(new AnimationStateData(data));
|
|
465
544
|
// Not looping: the last frame sits at the animation's duration, and a looping
|
|
466
545
|
// entry would wrap it back onto the first pose.
|
|
@@ -484,7 +563,7 @@ export function sampleAnimation(data: SkeletonData, name: string, fps: number, o
|
|
|
484
563
|
frames.push({
|
|
485
564
|
index: i,
|
|
486
565
|
time: i * step,
|
|
487
|
-
pieces: piecesOf(skeleton,
|
|
566
|
+
pieces: piecesOf(skeleton, pieceOpts),
|
|
488
567
|
...(opts?.bones ? { bones: boneSnapshots(skeleton) } : {}),
|
|
489
568
|
});
|
|
490
569
|
}
|
|
@@ -500,7 +579,7 @@ export function sampleAnimation(data: SkeletonData, name: string, fps: number, o
|
|
|
500
579
|
* whole content is the setup pose.
|
|
501
580
|
*/
|
|
502
581
|
export function sampleSetupPose(data: SkeletonData, opts?: PoseOptions): Frame[] {
|
|
503
|
-
const skeleton =
|
|
582
|
+
const skeleton = skeletonUnderSkin(data, opts?.skin);
|
|
504
583
|
skeleton.setupPose();
|
|
505
584
|
skeleton.update(0);
|
|
506
585
|
skeleton.updateWorldTransform(Physics.reset);
|
|
@@ -508,7 +587,7 @@ export function sampleSetupPose(data: SkeletonData, opts?: PoseOptions): Frame[]
|
|
|
508
587
|
{
|
|
509
588
|
index: 0,
|
|
510
589
|
time: 0,
|
|
511
|
-
pieces: piecesOf(skeleton, opts),
|
|
590
|
+
pieces: piecesOf(skeleton, piecesOptions(opts)),
|
|
512
591
|
...(opts?.bones ? { bones: boneSnapshots(skeleton) } : {}),
|
|
513
592
|
},
|
|
514
593
|
];
|
|
@@ -519,10 +598,10 @@ export function sampleSetupPose(data: SkeletonData, opts?: PoseOptions): Frame[]
|
|
|
519
598
|
* filed under. A skeleton with no animation at all contributes its setup pose
|
|
520
599
|
* under `SETUP_POSE_DIR`.
|
|
521
600
|
*/
|
|
522
|
-
export function sampleAll(data: SkeletonData, fps: number): Map<string, Frame[]> {
|
|
601
|
+
export function sampleAll(data: SkeletonData, fps: number, opts?: PoseOptions): Map<string, Frame[]> {
|
|
523
602
|
const out = new Map<string, Frame[]>();
|
|
524
|
-
if (data.animations.length === 0) out.set(SETUP_POSE_DIR, sampleSetupPose(data));
|
|
525
|
-
else for (const animation of data.animations) out.set(animation.name, sampleAnimation(data, animation.name, fps));
|
|
603
|
+
if (data.animations.length === 0) out.set(SETUP_POSE_DIR, sampleSetupPose(data, opts));
|
|
604
|
+
else for (const animation of data.animations) out.set(animation.name, sampleAnimation(data, animation.name, fps, opts));
|
|
526
605
|
return out;
|
|
527
606
|
}
|
|
528
607
|
|
|
@@ -537,6 +616,17 @@ export function sampleAll(data: SkeletonData, fps: number): Map<string, Frame[]>
|
|
|
537
616
|
* of them draws a pixel.
|
|
538
617
|
*/
|
|
539
618
|
export function piecesOf(skeleton: Skeleton, opts?: PoseOptions): Piece[] {
|
|
619
|
+
// A skin is chosen before a skeleton is posed, and this one is already posed —
|
|
620
|
+
// so there is nothing honest to do with the name except say so. Silently
|
|
621
|
+
// ignoring it is the shape of defect #571 itself: a skin asked for, no skin
|
|
622
|
+
// applied, and a picture that looks like an answer.
|
|
623
|
+
if (opts?.skin !== undefined) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
`piecesOf was asked for skin ${JSON.stringify(opts.skin)}, and it reads a skeleton that is already posed. ` +
|
|
626
|
+
'Ask a sampler for it — sampleSetupPose/sampleAnimation/sampleAll take { skin } — or call ' +
|
|
627
|
+
'skeleton.setSkin(...) and skeleton.setupPose() before this.',
|
|
628
|
+
);
|
|
629
|
+
}
|
|
540
630
|
const pieces: Piece[] = [];
|
|
541
631
|
for (const slot of skeleton.drawOrder.appliedPose) {
|
|
542
632
|
const pose = slot.appliedPose;
|
|
@@ -970,11 +1060,15 @@ export function trimmedUnionBounds(
|
|
|
970
1060
|
* Measuring the box densely and once makes the framing a property of the SHOT,
|
|
971
1061
|
* so every rate of one skeleton lands on the same pixels.
|
|
972
1062
|
*/
|
|
973
|
-
export function framingViewport(data: SkeletonData, maxSide: number): Viewport | null {
|
|
1063
|
+
export function framingViewport(data: SkeletonData, maxSide: number, opts?: PoseOptions): Viewport | null {
|
|
1064
|
+
// The skin belongs here as much as in the frames: the union box is over the
|
|
1065
|
+
// attachments that POSE, and two skins fill a slot with art of different sizes
|
|
1066
|
+
// in different places. Framing one skin's shot with another skin's box would
|
|
1067
|
+
// put the difference between two skins into every measurement taken in it.
|
|
974
1068
|
const sets =
|
|
975
1069
|
data.animations.length === 0
|
|
976
|
-
? [sampleSetupPose(data)]
|
|
977
|
-
: data.animations.map((a) => sampleAnimation(data, a.name, FRAMING_FPS));
|
|
1070
|
+
? [sampleSetupPose(data, opts)]
|
|
1071
|
+
: data.animations.map((a) => sampleAnimation(data, a.name, FRAMING_FPS, opts));
|
|
978
1072
|
const box = unionBounds(sets);
|
|
979
1073
|
if (!Number.isFinite(box.minX)) return null;
|
|
980
1074
|
const pad = Math.max(box.maxX - box.minX, box.maxY - box.minY) * PAD;
|
package/src/rig.ts
CHANGED
|
@@ -80,6 +80,25 @@ export const RIG_SPEC_VERSION = 'rigc-rig/1';
|
|
|
80
80
|
* `A19_OVERLAY_PNGS_HAVE_ALPHA` measure against, and a guessed stage is a gate
|
|
81
81
|
* that measures against a number nobody wrote down.
|
|
82
82
|
*
|
|
83
|
+
* ⭐ **`width: null, height: null` is the third state: this skeleton declares no
|
|
84
|
+
* stage** (issue #578). Omitting them is silence and stays a refusal by name;
|
|
85
|
+
* stating them `null` is a claim, and the emitted header then carries none of
|
|
86
|
+
* `x`/`y`/`width`/`height` — which is what an editor export of a skeleton whose
|
|
87
|
+
* stage was never set looks like, and what a transcriber of one has to be able
|
|
88
|
+
* to write down. `null` is this spec's spelling for a stated absence everywhere
|
|
89
|
+
* else it has one (`RigSlot.attachment` = "show nothing", the cut manifest's
|
|
90
|
+
* `image` = "this cut does not carry the part"), so it is the spelling here too
|
|
91
|
+
* and no new key is introduced: the pair already exists, and only a third value
|
|
92
|
+
* of it is new.
|
|
93
|
+
*
|
|
94
|
+
* Two shapes are refused rather than interpreted, both in `parseRigSpec`:
|
|
95
|
+
* stating one of the pair `null` and the other a number (a stage with one
|
|
96
|
+
* extent is not a stage, and guessing which half was meant is inventing), and
|
|
97
|
+
* stating `x` or `y` alongside the absence (an origin for a box that is not
|
|
98
|
+
* there). ⚠️ A stated absence also beats a cut manifest's `crop`, for the reason
|
|
99
|
+
* a stated `width` already does: the rig spec is where a claim about the
|
|
100
|
+
* skeleton is made, and the manifest is a record of what the art measured.
|
|
101
|
+
*
|
|
83
102
|
* `spine` is not here: rigc emits its own version label and `A16` re-checks it.
|
|
84
103
|
* `hash` is not here either — it is the editor's change-detection token and
|
|
85
104
|
* inventing one would be claiming an export this file did not come from.
|
|
@@ -87,8 +106,10 @@ export const RIG_SPEC_VERSION = 'rigc-rig/1';
|
|
|
87
106
|
export interface RigSkeletonHeader {
|
|
88
107
|
x?: number;
|
|
89
108
|
y?: number;
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
/** A number, or `null` with `height` for "this skeleton declares no stage". */
|
|
110
|
+
width?: number | null;
|
|
111
|
+
/** A number, or `null` with `width` for "this skeleton declares no stage". */
|
|
112
|
+
height?: number | null;
|
|
92
113
|
/** Nonessential; `SkeletonData.fps` stays 30 when absent. */
|
|
93
114
|
fps?: number;
|
|
94
115
|
/** 4.2+; the runtime's physics/scale reference. Parser default 100. */
|
|
@@ -107,6 +128,18 @@ export interface RigSkeletonHeader {
|
|
|
107
128
|
images?: string;
|
|
108
129
|
}
|
|
109
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Does this header state that the skeleton has no stage?
|
|
133
|
+
*
|
|
134
|
+
* One reading of the spelling, exported so that the compiler, the emitter and
|
|
135
|
+
* anything that grows a third opinion later read it the same way. `parseRigSpec`
|
|
136
|
+
* has already refused the half-stated shapes by the time this is asked, so the
|
|
137
|
+
* two `null`s travel together.
|
|
138
|
+
*/
|
|
139
|
+
export function declaresNoStage(header: RigSkeletonHeader | undefined): boolean {
|
|
140
|
+
return header !== undefined && header.width === null && header.height === null;
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
// ---------------------------------------------------------------------------
|
|
111
144
|
// bones — `root.bones[]` (SkeletonJson.ts:90-118)
|
|
112
145
|
// ---------------------------------------------------------------------------
|
|
@@ -223,12 +256,22 @@ export const RIG_SLOT_BLEND: readonly RigSlotBlend[] = ['normal', 'additive', 'm
|
|
|
223
256
|
* One slot. **The array order IS the draw order** — there is no separate setup
|
|
224
257
|
* draw-order field anywhere in the format.
|
|
225
258
|
*
|
|
226
|
-
* The rig's slot list is the CANONICAL table
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
259
|
+
* The rig's slot list is the CANONICAL table and **every slot in it is emitted**,
|
|
260
|
+
* in this order, whether or not anything fills it. A slot no skin and no manifest
|
|
261
|
+
* part fills is emitted with no setup attachment — the shape an editor export
|
|
262
|
+
* carries for a slot that shows nothing (the slot reader above takes `attachment`
|
|
263
|
+
* with a `null` default) — so the emitted array and this one are the same array.
|
|
264
|
+
* `A26_SLOT_DRAW_ORDER` checks both halves of that: nothing out of order, and
|
|
265
|
+
* nothing missing. Declaring a slot no cut fills is therefore legitimate, and it
|
|
266
|
+
* fixes where that slot sits whether or not this cut has art for it.
|
|
267
|
+
*
|
|
268
|
+
* ⚠️ Until issue #575 such a slot was **dropped**, and the gate licensed it: the
|
|
269
|
+
* emitted array was allowed to be any *subsequence* of this one. What that
|
|
270
|
+
* bought was the format's own silence. Nothing said which slot had gone, and
|
|
271
|
+
* every slot below it moved up one index — the index a `drawOrder` key's offsets
|
|
272
|
+
* are counted against, and the one an index-keyed consumer splits on. Two
|
|
273
|
+
* production exports declaring 53 and 61 slots built green at 51 and 57 and read
|
|
274
|
+
* 0.962 and 0.934 under `diff` against the file they were transcribed from.
|
|
232
275
|
*/
|
|
233
276
|
export interface RigSlot {
|
|
234
277
|
name: string;
|
|
@@ -241,6 +284,12 @@ export interface RigSlot {
|
|
|
241
284
|
* comes from: `motion.setup` owns it, because which of the two overlay
|
|
242
285
|
* mechanisms a slot uses (attachment + alpha 0, or attachment swapping) is a
|
|
243
286
|
* decision about time. Declaring it in both is a compile error.
|
|
287
|
+
*
|
|
288
|
+
* Required for a slot something fills — the compiler will not guess which of
|
|
289
|
+
* the slot's attachments the setup pose shows — and **optional for a slot
|
|
290
|
+
* nothing fills**, where it can only be `null` and saying so changes no
|
|
291
|
+
* emitted byte. Naming an attachment on a slot nothing fills is refused: the
|
|
292
|
+
* name resolves to nothing, which is the shape of a half-finished wiring-up.
|
|
244
293
|
*/
|
|
245
294
|
attachment?: string | null;
|
|
246
295
|
/** `rrggbbaa`. Default opaque white. */
|
|
@@ -1520,6 +1569,15 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
|
|
|
1520
1569
|
const type = att.type === undefined ? 'region' : String(att.type);
|
|
1521
1570
|
const shape = ATTACHMENT_SHAPE[type];
|
|
1522
1571
|
if (shape === undefined) continue;
|
|
1572
|
+
// A mesh carrying `source` is a LINKED mesh — `type: "mesh"` and
|
|
1573
|
+
// `type: "linkedmesh"` share one parser branch and the `source` key is
|
|
1574
|
+
// what decides (`:568-569`, `:582`; SPEC_COVERAGE part 1-6). It has no
|
|
1575
|
+
// key set here because `RigUnimplementedAttachment` deliberately has
|
|
1576
|
+
// none, and checking it against a MESH's keys named the wrong fault:
|
|
1577
|
+
// *2 keys this compiler does not read: "source", "skin" … fix the
|
|
1578
|
+
// spelling or remove it*, where removing `source` is what unmakes the
|
|
1579
|
+
// linked mesh. `buildRigAttachment` refuses it as the construct it is.
|
|
1580
|
+
if (type === 'mesh' && att.source !== undefined) continue;
|
|
1523
1581
|
at(att, shape, `${who} (${type})`);
|
|
1524
1582
|
for (const [i, vertex] of (Array.isArray(att.weights) ? att.weights : []).entries()) {
|
|
1525
1583
|
for (const [j, binding] of (Array.isArray(vertex) ? vertex : []).entries()) {
|
|
@@ -1573,6 +1631,32 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
1573
1631
|
|
|
1574
1632
|
const spec = raw as unknown as RigSpec;
|
|
1575
1633
|
|
|
1634
|
+
// The stage, stated or stated absent. Half a statement is refused here rather
|
|
1635
|
+
// than resolved in `compile`, because which half was meant is not derivable
|
|
1636
|
+
// and a compiler that picks one is inventing a number (issue #578).
|
|
1637
|
+
const header = spec.skeleton;
|
|
1638
|
+
if (header !== undefined) {
|
|
1639
|
+
const noWidth = header.width === null;
|
|
1640
|
+
const noHeight = header.height === null;
|
|
1641
|
+
if (noWidth !== noHeight) {
|
|
1642
|
+
const stated = noWidth ? 'height' : 'width';
|
|
1643
|
+
const absent = noWidth ? 'width' : 'height';
|
|
1644
|
+
throw new CompileError(
|
|
1645
|
+
`${where}: "skeleton" states ${absent}: null and a ${stated} of ` +
|
|
1646
|
+
`${JSON.stringify(noWidth ? header.height : header.width)}. A stage has both extents or neither: ` +
|
|
1647
|
+
'write both as null for "this skeleton declares no stage", or give both a number',
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
if (noWidth && noHeight && (header.x !== undefined || header.y !== undefined)) {
|
|
1651
|
+
const origin = [header.x !== undefined ? 'x' : null, header.y !== undefined ? 'y' : null].filter((k) => k !== null);
|
|
1652
|
+
throw new CompileError(
|
|
1653
|
+
`${where}: "skeleton" declares no stage (width: null, height: null) and still states ${origin.join(' and ')}. ` +
|
|
1654
|
+
`${origin.length === 1 ? 'That is an origin' : 'Those are an origin'} for a box that is not there: ` +
|
|
1655
|
+
'drop them, or state a width and a height',
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1576
1660
|
const seen = new Set<string>();
|
|
1577
1661
|
for (const bone of spec.bones) {
|
|
1578
1662
|
if (!isObj(bone) || typeof bone.name !== 'string' || bone.name.length === 0) {
|
package/src/types.ts
CHANGED
|
@@ -515,13 +515,30 @@ export interface MotionTransformTrack {
|
|
|
515
515
|
export interface MotionDeformKey {
|
|
516
516
|
/** Time in seconds. */
|
|
517
517
|
t: number;
|
|
518
|
-
/**
|
|
518
|
+
/**
|
|
519
|
+
* Where the run starts in the attachment's own deform array. Default 0.
|
|
520
|
+
*
|
|
521
|
+
* Any index the array holds, **odd ones included** (issue #576): the parser
|
|
522
|
+
* copies at this index and does no pair arithmetic, so an odd start is what an
|
|
523
|
+
* editor writes when it trims the leading numbers off a delta run.
|
|
524
|
+
*/
|
|
519
525
|
offset?: number;
|
|
520
526
|
/** The same start, given as a vertex index. Never together with `offset`. */
|
|
521
527
|
fromVertex?: number;
|
|
522
528
|
/**
|
|
523
|
-
* The run:
|
|
524
|
-
*
|
|
529
|
+
* The run: consecutive numbers written into the deform array from `offset` on,
|
|
530
|
+
* `x, y` per array slot. Absent or `null` is the parser's own encoding for
|
|
531
|
+
* "back to the setup pose" — the key with no edit.
|
|
532
|
+
*
|
|
533
|
+
* ⚠️ **An ODD count is legal and means something** (issue #576). Both readers
|
|
534
|
+
* copy the run verbatim — `Utils.arrayCopy(vertices, 0, deform, offset,
|
|
535
|
+
* vertices.length)` in `SkeletonJson`, a `for (let v = start; v < end; v++)`
|
|
536
|
+
* fill in `SkeletonBinary` — so a run of three numbers moves one vertex in x
|
|
537
|
+
* and y and the next in x alone, leaving that y at its setup value. Padding a
|
|
538
|
+
* `0` to even it out is a different animation whenever that setup y is
|
|
539
|
+
* non-zero, which is why the even-length rule that stood here could not be kept
|
|
540
|
+
* as a convenience: it left one production export with no spelling in this
|
|
541
|
+
* spec at all.
|
|
525
542
|
*/
|
|
526
543
|
vertices?: number[] | null;
|
|
527
544
|
/**
|
|
@@ -926,10 +943,25 @@ export type SpineConstraint = { name: string; type: string } & Record<string, un
|
|
|
926
943
|
export interface SpineSkeletonJson {
|
|
927
944
|
skeleton: {
|
|
928
945
|
spine: string;
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
946
|
+
/**
|
|
947
|
+
* The setup-pose bounding box. All four together or none of them: a rig spec
|
|
948
|
+
* that declares no stage (`skeleton.width`/`height` stated `null` — see
|
|
949
|
+
* `RigSkeletonHeader`) emits a header without any of them, which is what an
|
|
950
|
+
* export of a skeleton whose stage was never set carries (issue #578).
|
|
951
|
+
*
|
|
952
|
+
* ⚠️ Optional here because the *runtime* leaves them `undefined` when they
|
|
953
|
+
* are absent, not 0. `SkeletonData` declares `x = 0 … height = 0`
|
|
954
|
+
* (`SkeletonData.js:55-61`), and `SkeletonJson` then overwrites all four
|
|
955
|
+
* unconditionally — `skeletonData.x = skeletonMap.x` (`SkeletonJson.js:70-73`,
|
|
956
|
+
* no `getValue` default) — so an absent field lands as `undefined` on a
|
|
957
|
+
* `SkeletonData` whose own `.d.ts` types it `number`. Anything reading these
|
|
958
|
+
* back off a parsed skeleton guards for it; `validate.ts`'s `data.width || 0`
|
|
959
|
+
* is why A14 and A19 were already right about a stage-less file.
|
|
960
|
+
*/
|
|
961
|
+
x?: number;
|
|
962
|
+
y?: number;
|
|
963
|
+
width?: number;
|
|
964
|
+
height?: number;
|
|
933
965
|
fps?: number;
|
|
934
966
|
referenceScale?: number;
|
|
935
967
|
images?: string;
|