spine-rigc 0.6.0 → 0.7.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 +92 -11
- package/cli.ts +366 -15
- package/docs/AUTHORING.md +290 -9
- package/package.json +1 -1
- package/src/ballot.ts +869 -0
- package/src/compile.ts +612 -0
- package/src/preview.ts +2 -2
- package/src/types.ts +163 -0
- package/src/validate.ts +229 -1
package/src/types.ts
CHANGED
|
@@ -330,6 +330,144 @@ export interface MotionEventKey {
|
|
|
330
330
|
balance?: number;
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
/**
|
|
334
|
+
* One key of an IK constraint's timeline (`animations.<a>.ik.<constraint>`).
|
|
335
|
+
*
|
|
336
|
+
* ⚠️ Every field is **optional and absolute**, and that pairing is the trap. The
|
|
337
|
+
* parser reads each one with its own default per key
|
|
338
|
+
* (`SkeletonJson.ts`: `mix` 1, `softness` 0, `bendPositive` true, `compress`
|
|
339
|
+
* false, `stretch` false) — so a key that omits `softness` does not hold the
|
|
340
|
+
* previous key's softness, it snaps to 0. rigc therefore refuses a track whose
|
|
341
|
+
* keys do not all name the SAME set of fields: state the value on every key, or
|
|
342
|
+
* on none of them.
|
|
343
|
+
*
|
|
344
|
+
* `mix` and `softness` are the timeline's two curve channels, in that order.
|
|
345
|
+
* The three booleans are stepped by nature — nothing interpolates them.
|
|
346
|
+
*/
|
|
347
|
+
export interface MotionIkKey {
|
|
348
|
+
/** Time in seconds. */
|
|
349
|
+
t: number;
|
|
350
|
+
/** 0..1: how much of the constrained rotation is applied. Parser default 1. */
|
|
351
|
+
mix?: number;
|
|
352
|
+
/** Distance from full reach at which the bones stop straightening. Default 0. */
|
|
353
|
+
softness?: number;
|
|
354
|
+
/** Two-bone IK bend direction. Default true. */
|
|
355
|
+
bendPositive?: boolean;
|
|
356
|
+
/** One-bone IK: scale the bone down to reach a close target. Default false. */
|
|
357
|
+
compress?: boolean;
|
|
358
|
+
/** Scale the bone up to reach a far target. Default false. */
|
|
359
|
+
stretch?: boolean;
|
|
360
|
+
/** Named easing from `easings`, or "stepped". Absent = linear. */
|
|
361
|
+
ease?: string;
|
|
362
|
+
/** The raw form: 4 absolute (time, value) numbers per channel — 8 here. */
|
|
363
|
+
curve?: number[] | 'stepped';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* One IK constraint keyed over time.
|
|
368
|
+
*
|
|
369
|
+
* 4.3 writes this as `animations.<a>.ik.<constraint>` — **one unnamed timeline
|
|
370
|
+
* per constraint**, so the constraint name is the only target there is and the
|
|
371
|
+
* group carries no timeline name at all.
|
|
372
|
+
*/
|
|
373
|
+
export interface MotionIkTrack {
|
|
374
|
+
/** An `ik` constraint the rig spec declares. */
|
|
375
|
+
constraint: string;
|
|
376
|
+
keys: MotionIkKey[];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* One key of a transform constraint's timeline
|
|
381
|
+
* (`animations.<a>.transform.<constraint>`).
|
|
382
|
+
*
|
|
383
|
+
* Six mixes, six curve channels, in the order written here — which is the order
|
|
384
|
+
* the parser reads them and therefore the order a curve array concatenates.
|
|
385
|
+
* The same absent-means-default rule as `MotionIkKey` applies, with one extra
|
|
386
|
+
* quirk: `mixY` defaults to **this key's own `mixX`**, not to 1.
|
|
387
|
+
*/
|
|
388
|
+
export interface MotionTransformKey {
|
|
389
|
+
/** Time in seconds. */
|
|
390
|
+
t: number;
|
|
391
|
+
/** Parser default 1. */
|
|
392
|
+
mixRotate?: number;
|
|
393
|
+
/** Parser default 1. */
|
|
394
|
+
mixX?: number;
|
|
395
|
+
/** Parser default: the same key's `mixX`. */
|
|
396
|
+
mixY?: number;
|
|
397
|
+
/** Parser default 1. */
|
|
398
|
+
mixScaleX?: number;
|
|
399
|
+
/** Parser default 1. */
|
|
400
|
+
mixScaleY?: number;
|
|
401
|
+
/** Parser default 1. */
|
|
402
|
+
mixShearY?: number;
|
|
403
|
+
ease?: string;
|
|
404
|
+
/** 4 absolute (time, value) numbers per channel — 24 here. */
|
|
405
|
+
curve?: number[] | 'stepped';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** One transform constraint keyed over time. Same shape rule as `MotionIkTrack`. */
|
|
409
|
+
export interface MotionTransformTrack {
|
|
410
|
+
/** A `transform` constraint the rig spec declares. */
|
|
411
|
+
constraint: string;
|
|
412
|
+
keys: MotionTransformKey[];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* One key of a deform timeline
|
|
417
|
+
* (`animations.<a>.attachments.<skin>.<slot>.<attachment>.deform`).
|
|
418
|
+
*
|
|
419
|
+
* 🚨 This is the only key in the format whose meaning depends on the object it
|
|
420
|
+
* is attached to. The parser builds a zero-filled array as long as the
|
|
421
|
+
* attachment's own deform array, copies this key's `vertices` into it starting at
|
|
422
|
+
* `offset`, and leaves the rest alone — so a key is a **sparse edit of the setup
|
|
423
|
+
* geometry**, and both the length of that array and the meaning of an index into
|
|
424
|
+
* it come from the attachment (`SkeletonJson.ts`, the `deform` branch):
|
|
425
|
+
*
|
|
426
|
+
* - **unweighted** attachment — the array is one `x, y` pair per VERTEX, and
|
|
427
|
+
* the parser adds the setup position back on load. The numbers here are
|
|
428
|
+
* therefore offsets from setup, in the slot bone's space.
|
|
429
|
+
* - **weighted** attachment — the array is one `x, y` pair per BONE INFLUENCE
|
|
430
|
+
* (`vertices.length / 3 * 2`), each in the bind space of that influence's
|
|
431
|
+
* bone. A vertex with three bones on it occupies three pairs.
|
|
432
|
+
*
|
|
433
|
+
* `offset` is an index into that array and works for both. `fromVertex` is
|
|
434
|
+
* rigc's ergonomic form — a VERTEX index, which rigc translates — and it is
|
|
435
|
+
* accepted only where the translation is honest: always on an unweighted
|
|
436
|
+
* attachment, and on a weighted one only when every vertex the run covers has
|
|
437
|
+
* exactly one bone influence. Anything else is refused by name rather than
|
|
438
|
+
* emitted as a plausible-looking lie.
|
|
439
|
+
*/
|
|
440
|
+
export interface MotionDeformKey {
|
|
441
|
+
/** Time in seconds. */
|
|
442
|
+
t: number;
|
|
443
|
+
/** Where the run starts in the attachment's own deform array. Default 0. */
|
|
444
|
+
offset?: number;
|
|
445
|
+
/** The same start, given as a vertex index. Never together with `offset`. */
|
|
446
|
+
fromVertex?: number;
|
|
447
|
+
/**
|
|
448
|
+
* The run: `x, y` offsets, two numbers per array slot. Absent or `null` is the
|
|
449
|
+
* parser's own encoding for "back to the setup pose" — the key with no edit.
|
|
450
|
+
*/
|
|
451
|
+
vertices?: number[] | null;
|
|
452
|
+
ease?: string;
|
|
453
|
+
/**
|
|
454
|
+
* One channel, and it interpolates the deform FRACTION from 0 to 1 rather than
|
|
455
|
+
* any value in `vertices` (`readCurve(..., 0, 1, 1)`). So a raw curve is 4
|
|
456
|
+
* numbers whose value axis runs 0..1.
|
|
457
|
+
*/
|
|
458
|
+
curve?: number[] | 'stepped';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** One attachment's geometry keyed over time. */
|
|
462
|
+
export interface MotionDeformTrack {
|
|
463
|
+
/** The skin the attachment lives in. Absent = `"default"`. */
|
|
464
|
+
skin?: string;
|
|
465
|
+
slot: string;
|
|
466
|
+
/** The attachment's placeholder name inside that skin and slot. */
|
|
467
|
+
attachment: string;
|
|
468
|
+
keys: MotionDeformKey[];
|
|
469
|
+
}
|
|
470
|
+
|
|
333
471
|
export interface MotionAnimation {
|
|
334
472
|
/** Declared, then verified against the compiled result (rule 4). */
|
|
335
473
|
duration: number;
|
|
@@ -337,6 +475,23 @@ export interface MotionAnimation {
|
|
|
337
475
|
loop: boolean;
|
|
338
476
|
note?: string;
|
|
339
477
|
tracks: MotionTrack[];
|
|
478
|
+
/**
|
|
479
|
+
* IK constraint timelines, one entry per constraint.
|
|
480
|
+
*
|
|
481
|
+
* ⭐ Not a `track`, and the reason is the key rather than the target. A track's
|
|
482
|
+
* key is one `v` — an array, a name, or nothing — and these three families each
|
|
483
|
+
* carry a shape of their own: five named fields for IK, six for a transform
|
|
484
|
+
* constraint, and for a deform a sparse run whose meaning depends on the
|
|
485
|
+
* attachment. Folding them into `MotionKey` would make `v` mean four different
|
|
486
|
+
* things depending on `property`, and the type would stop documenting any of
|
|
487
|
+
* them. So they sit beside `tracks`, where 4.3 also writes them
|
|
488
|
+
* (`animations.<a>.ik`, `.transform`, `.attachments`).
|
|
489
|
+
*/
|
|
490
|
+
ik?: MotionIkTrack[];
|
|
491
|
+
/** Transform constraint timelines, one entry per constraint. */
|
|
492
|
+
transform?: MotionTransformTrack[];
|
|
493
|
+
/** Deform timelines, one entry per skin/slot/attachment triple. */
|
|
494
|
+
deform?: MotionDeformTrack[];
|
|
340
495
|
/**
|
|
341
496
|
* The draw-order timeline. **One per animation, and it names no target** —
|
|
342
497
|
* which is why it is not a `track`: 4.3 writes it as `animations.<a>.drawOrder`
|
|
@@ -541,7 +696,15 @@ export interface SpineSkeletonJson {
|
|
|
541
696
|
{
|
|
542
697
|
slots?: Record<string, Record<string, SpineTimelineKey[]>>;
|
|
543
698
|
bones?: Record<string, Record<string, SpineTimelineKey[]>>;
|
|
699
|
+
/**
|
|
700
|
+
* `ik.<constraint> = keys[]` — the constraint IS the timeline, so there is
|
|
701
|
+
* no timeline name between the two. Same for `transform`.
|
|
702
|
+
*/
|
|
703
|
+
ik?: Record<string, SpineTimelineKey[]>;
|
|
704
|
+
transform?: Record<string, SpineTimelineKey[]>;
|
|
544
705
|
physics?: Record<string, Record<string, SpineTimelineKey[]>>;
|
|
706
|
+
/** `attachments.<skin>.<slot>.<attachment>.<timeline> = keys[]` — four deep. */
|
|
707
|
+
attachments?: Record<string, Record<string, Record<string, Record<string, SpineTimelineKey[]>>>>;
|
|
545
708
|
/** Whole-animation timeline: no target name, one array per animation. */
|
|
546
709
|
drawOrder?: SpineTimelineKey[];
|
|
547
710
|
/** The other whole-animation timeline; same shape, same reason. */
|
package/src/validate.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface Failure {
|
|
|
43
43
|
*
|
|
44
44
|
* ⭐ The distinction this draws is the difference between "wrong" and "not how we
|
|
45
45
|
* do it here", and conflating the two is how a validator stops being usable on
|
|
46
|
-
* anybody else's data. Fourteen of the
|
|
46
|
+
* anybody else's data. Fourteen of the 36 assertions are policy — seven for one
|
|
47
47
|
* renderer (`spine-html`) and one project's canvas budget, seven for rigc's own
|
|
48
48
|
* formations — and every one of them fires
|
|
49
49
|
* on real, correct, editor-produced Spine data — the official example projects
|
|
@@ -133,6 +133,8 @@ const ASSERTION_KIND: Record<string, 'validity' | 'renderer' | 'archetype'> = {
|
|
|
133
133
|
A31_DRAW_ORDER_OFFSETS_RESOLVE: 'validity',
|
|
134
134
|
A32_EVENT_KEYS_RESOLVE: 'validity',
|
|
135
135
|
A33_VERTEX_ATTACHMENT_GEOMETRY: 'validity',
|
|
136
|
+
A34_CONSTRAINT_TIMELINE_TARGETS: 'validity',
|
|
137
|
+
A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: 'validity',
|
|
136
138
|
};
|
|
137
139
|
|
|
138
140
|
export interface ValidateInput {
|
|
@@ -228,6 +230,38 @@ function isObj(v: unknown): v is Json {
|
|
|
228
230
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
/**
|
|
234
|
+
* How long the array a deform key edits is, read off one raw attachment — or
|
|
235
|
+
* `null` when the file does not say.
|
|
236
|
+
*
|
|
237
|
+
* The rule is `readVertices`' own: the attachment's `vertices` is coordinates
|
|
238
|
+
* when its length equals `worldVerticesLength`, and a weight run otherwise. A
|
|
239
|
+
* deform array is therefore one `x, y` pair per **vertex** in the first case and
|
|
240
|
+
* one per **bone influence** (`vertices.length / 3`) in the second — the same
|
|
241
|
+
* count with two different meanings, which is exactly why this measures rather
|
|
242
|
+
* than assumes.
|
|
243
|
+
*
|
|
244
|
+
* `null` for the two shapes that cannot be measured from this object alone: a
|
|
245
|
+
* type with no vertices at all (nothing to deform, and the parser throws on it),
|
|
246
|
+
* and a `linkedmesh`, whose geometry belongs to another attachment.
|
|
247
|
+
*/
|
|
248
|
+
function deformArrayLength(att: Json): number | null {
|
|
249
|
+
const type = typeof att.type === 'string' ? att.type : 'region';
|
|
250
|
+
let worldVerticesLength: number;
|
|
251
|
+
if (type === 'mesh') {
|
|
252
|
+
if (!Array.isArray(att.uvs)) return null;
|
|
253
|
+
worldVerticesLength = att.uvs.length;
|
|
254
|
+
} else if (type === 'boundingbox' || type === 'clipping' || type === 'path') {
|
|
255
|
+
if (typeof att.vertexCount !== 'number') return null;
|
|
256
|
+
worldVerticesLength = att.vertexCount * 2;
|
|
257
|
+
} else {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
if (!Array.isArray(att.vertices)) return null;
|
|
261
|
+
const vertices = att.vertices as unknown[];
|
|
262
|
+
return vertices.length === worldVerticesLength ? worldVerticesLength : (vertices.length / 3) * 2;
|
|
263
|
+
}
|
|
264
|
+
|
|
231
265
|
export function validate(input: ValidateInput): ValidateReport {
|
|
232
266
|
const failures: Failure[] = [];
|
|
233
267
|
const passed: string[] = [];
|
|
@@ -468,6 +502,200 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
468
502
|
if (!sawATimeline) return skip('A32_EVENT_KEYS_RESOLVE', 'no animation carries an event timeline');
|
|
469
503
|
});
|
|
470
504
|
|
|
505
|
+
// --- A34: ik / transform timelines aim at a constraint of that type -------
|
|
506
|
+
//
|
|
507
|
+
// These two groups are one unnamed timeline per constraint
|
|
508
|
+
// (`animations.<a>.ik.<name>`), and the parser resolves the name AND the type:
|
|
509
|
+
// `findConstraint(name, IkConstraintData)` returns null for a transform
|
|
510
|
+
// constraint that happens to share the name, and `readAnimation` then throws
|
|
511
|
+
// `IK Constraint not found`. That one is loud — A00 reports it, as a parser
|
|
512
|
+
// message about a name with no context — so this assertion exists for the
|
|
513
|
+
// second failure, which is silent:
|
|
514
|
+
//
|
|
515
|
+
// **An empty key array.** `let keyMap = constraintMap[0]; if (!keyMap)
|
|
516
|
+
// continue;` — the group is read, the timeline is skipped, and nothing is
|
|
517
|
+
// said. `"ik": { "leg-ik": [] }` is a timeline that does not exist, written
|
|
518
|
+
// by a generator that thought it wrote one.
|
|
519
|
+
//
|
|
520
|
+
// Reporting both from here also means a candidate with a misspelled constraint
|
|
521
|
+
// gets told which constraints it does have, rather than being handed the
|
|
522
|
+
// loader's own sentence.
|
|
523
|
+
check('A34_CONSTRAINT_TIMELINE_TARGETS', () => {
|
|
524
|
+
if (!raw) return skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'the skeleton JSON did not parse (A00 owns that failure)');
|
|
525
|
+
if (!isObj(raw.animations)) return skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'the skeleton declares no animations');
|
|
526
|
+
const typeOf = new Map<string, string>();
|
|
527
|
+
for (const entry of Array.isArray(raw.constraints) ? (raw.constraints as unknown[]) : []) {
|
|
528
|
+
if (isObj(entry) && typeof entry.name === 'string') typeOf.set(entry.name, String(entry.type));
|
|
529
|
+
}
|
|
530
|
+
let sawATimeline = false;
|
|
531
|
+
for (const [animName, anim] of Object.entries(raw.animations as Json)) {
|
|
532
|
+
if (!isObj(anim)) continue;
|
|
533
|
+
for (const group of ['ik', 'transform'] as const) {
|
|
534
|
+
if (!isObj(anim[group])) continue;
|
|
535
|
+
for (const [name, keys] of Object.entries(anim[group] as Json)) {
|
|
536
|
+
sawATimeline = true;
|
|
537
|
+
const at = `animation "${animName}" ${group} timeline "${name}"`;
|
|
538
|
+
const declared = typeOf.get(name);
|
|
539
|
+
if (declared === undefined) {
|
|
540
|
+
const known = [...typeOf.entries()].filter(([, t]) => t === group).map(([n]) => n);
|
|
541
|
+
fail(
|
|
542
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
543
|
+
`${at}: the skeleton's constraints array has no "${name}"` +
|
|
544
|
+
(known.length ? ` (${group} constraints: ${known.join(', ')})` : `, and no ${group} constraint at all`),
|
|
545
|
+
);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (declared !== group) {
|
|
549
|
+
fail(
|
|
550
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
551
|
+
`${at}: "${name}" is declared as a "${declared}" constraint, so the ${group} lookup misses it and the loader throws`,
|
|
552
|
+
);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (!Array.isArray(keys) || keys.length === 0) {
|
|
556
|
+
fail(
|
|
557
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
558
|
+
`${at}: the key array is ${Array.isArray(keys) ? 'empty' : JSON.stringify(keys)}; the parser reads ` +
|
|
559
|
+
'key 0, finds nothing and skips the whole timeline without a word',
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (!sawATimeline) {
|
|
566
|
+
return skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'no animation carries an ik or transform constraint timeline');
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// --- A35: a deform key's run lands inside the attachment it edits ---------
|
|
571
|
+
//
|
|
572
|
+
// 🚨 The nastiest silent failure in the animation half of this format. A deform
|
|
573
|
+
// key is a sparse edit of a vertex array whose length comes from the attachment,
|
|
574
|
+
// and the parser applies it with
|
|
575
|
+
//
|
|
576
|
+
// Utils.arrayCopy(verticesValue, 0, deform, start, verticesValue.length)
|
|
577
|
+
//
|
|
578
|
+
// into a `Float32Array`. Writing past the end of a typed array in JavaScript is
|
|
579
|
+
// a **no-op** — no throw, no warning, no NaN — so a run one pair too long, or a
|
|
580
|
+
// run aimed at an attachment with fewer vertices than the author thought, loses
|
|
581
|
+
// its tail and deforms the rest correctly. The result looks nearly right, which
|
|
582
|
+
// is worse than looking wrong.
|
|
583
|
+
//
|
|
584
|
+
// Two more shapes, both of them equally quiet:
|
|
585
|
+
//
|
|
586
|
+
// * an **odd `offset`** puts every x of the run on a y and every y on the
|
|
587
|
+
// next x — the array is pairs, and nothing in the format says so;
|
|
588
|
+
// * a **non-finite value** in `vertices` reaches `computeWorldVertices` and
|
|
589
|
+
// turns the vertex into NaN, which A10 would only catch if the deformed
|
|
590
|
+
// slot's BONE went non-finite, and it does not.
|
|
591
|
+
//
|
|
592
|
+
// The length depends on the encoding and the two are the same split
|
|
593
|
+
// `readVertices` makes: unweighted is one pair per vertex, weighted is one pair
|
|
594
|
+
// per bone influence (`vertices.length / 3 * 2`). Deriving it here rather than
|
|
595
|
+
// assuming either is the whole point — assuming would produce a confident,
|
|
596
|
+
// wrong bound on half the meshes in the world.
|
|
597
|
+
check('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', () => {
|
|
598
|
+
if (!raw) return skip('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', 'the skeleton JSON did not parse (A00 owns that failure)');
|
|
599
|
+
if (!isObj(raw.animations)) return skip('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', 'the skeleton declares no animations');
|
|
600
|
+
/** skin name -> slot -> attachment, straight off the raw JSON. */
|
|
601
|
+
const skins = new Map<string, Json>();
|
|
602
|
+
for (const skin of Array.isArray(raw.skins) ? (raw.skins as unknown[]) : []) {
|
|
603
|
+
if (isObj(skin) && typeof skin.name === 'string' && isObj(skin.attachments)) {
|
|
604
|
+
skins.set(skin.name, skin.attachments as Json);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
let sawATimeline = false;
|
|
608
|
+
let measured = 0;
|
|
609
|
+
for (const [animName, anim] of Object.entries(raw.animations as Json)) {
|
|
610
|
+
if (!isObj(anim) || !isObj(anim.attachments)) continue;
|
|
611
|
+
for (const [skinName, slotMap] of Object.entries(anim.attachments as Json)) {
|
|
612
|
+
if (!isObj(slotMap)) continue;
|
|
613
|
+
for (const [slotName, attMap] of Object.entries(slotMap)) {
|
|
614
|
+
if (!isObj(attMap)) continue;
|
|
615
|
+
for (const [attName, timelines] of Object.entries(attMap)) {
|
|
616
|
+
if (!isObj(timelines) || !Array.isArray(timelines.deform)) continue;
|
|
617
|
+
sawATimeline = true;
|
|
618
|
+
const at = `animation "${animName}" deform ${skinName}/${slotName}/${attName}`;
|
|
619
|
+
const attachment = isObj(skins.get(skinName)?.[slotName])
|
|
620
|
+
? ((skins.get(skinName)![slotName] as Json)[attName] as unknown)
|
|
621
|
+
: undefined;
|
|
622
|
+
if (!isObj(attachment)) {
|
|
623
|
+
fail(
|
|
624
|
+
'A35_DEFORM_KEYS_FIT_THE_ATTACHMENT',
|
|
625
|
+
`${at}: skin "${skinName}" has no attachment "${attName}" on slot "${slotName}"; the parser throws ` +
|
|
626
|
+
'`Timeline attachment not found`',
|
|
627
|
+
);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const length = deformArrayLength(attachment);
|
|
631
|
+
if (length === null) {
|
|
632
|
+
// A linked mesh takes its geometry from another attachment, so the
|
|
633
|
+
// raw file cannot state this one's length. Saying nothing beats
|
|
634
|
+
// inventing a bound: an assertion with a default is how "nothing to
|
|
635
|
+
// measure" becomes a measurement of the wrong thing.
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
measured++;
|
|
639
|
+
const keys = timelines.deform as unknown[];
|
|
640
|
+
if (keys.length === 0) {
|
|
641
|
+
fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at}: the key array is empty; the parser skips the timeline in silence`);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
keys.forEach((key, k) => {
|
|
645
|
+
if (!isObj(key)) {
|
|
646
|
+
fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at} key ${k}: not an object`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const vertices = key.vertices;
|
|
650
|
+
if (vertices === undefined || vertices === null) return; // "back to setup" — nothing to fit
|
|
651
|
+
if (!Array.isArray(vertices)) {
|
|
652
|
+
fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at} key ${k}: vertices is ${JSON.stringify(vertices)}, not an array`);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const offset = key.offset === undefined ? 0 : key.offset;
|
|
656
|
+
if (typeof offset !== 'number' || !Number.isInteger(offset) || offset < 0) {
|
|
657
|
+
fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at} key ${k}: offset is ${JSON.stringify(key.offset)}`);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (offset % 2 !== 0) {
|
|
661
|
+
fail(
|
|
662
|
+
'A35_DEFORM_KEYS_FIT_THE_ATTACHMENT',
|
|
663
|
+
`${at} key ${k}: offset ${offset} is odd, so the run's x values land on y slots and back again`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
if (vertices.length % 2 !== 0) {
|
|
667
|
+
fail(
|
|
668
|
+
'A35_DEFORM_KEYS_FIT_THE_ATTACHMENT',
|
|
669
|
+
`${at} key ${k}: the run holds ${vertices.length} numbers and the deform array is x, y pairs`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
if (offset + vertices.length > length) {
|
|
673
|
+
fail(
|
|
674
|
+
'A35_DEFORM_KEYS_FIT_THE_ATTACHMENT',
|
|
675
|
+
`${at} key ${k}: the run covers ${offset}..${offset + vertices.length} of a ${length}-long deform ` +
|
|
676
|
+
'array; everything past the end is copied into a Float32Array and dropped without a word',
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
for (const n of vertices as unknown[]) {
|
|
680
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) {
|
|
681
|
+
fail('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', `${at} key ${k}: the run holds a non-finite value ${JSON.stringify(n)}`);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (!sawATimeline) return skip('A35_DEFORM_KEYS_FIT_THE_ATTACHMENT', 'no animation carries a deform timeline');
|
|
691
|
+
if (measured === 0) {
|
|
692
|
+
return skip(
|
|
693
|
+
'A35_DEFORM_KEYS_FIT_THE_ATTACHMENT',
|
|
694
|
+
'every deform timeline here keys an attachment whose vertex count the raw file does not state (a linked mesh)',
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
471
699
|
// --- A: the round trip ----------------------------------------------------
|
|
472
700
|
// The two loaded objects come back OUT of the assertion rather than being
|
|
473
701
|
// assigned into it. Everything below reads them, and a `let` written inside a
|