spine-rigc 1.5.1 → 1.5.3

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/validate.ts CHANGED
@@ -289,16 +289,17 @@ export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure
289
289
  * `A15_IDLE_NO_MESH_BONE_KEYS` is the pattern and already reads this way: no
290
290
  * `idle` animation, or an `idle` with no bone timeline, is the named subject
291
291
  * absent and SKIPs; no mesh-driving bone among the bones `idle` does key is
292
- * the construct absent and passes. `A10_NO_NAN_AFTER_STEPPING` is the same
293
- * shape — the stepping is per animation, so a skeleton with none has not
294
- * been stepped.
292
+ * the construct absent and passes. `A10_NO_NAN_AFTER_STEPPING` was the same
293
+ * shape until issue #902, and is the next bullet's now: #882 gave it a
294
+ * setup-pose clause whose subject is the bones, beside the stepping clause
295
+ * whose subject is the animations.
295
296
  * * An assertion with more than one clause SKIPs only when EVERY clause had
296
297
  * nothing to measure, which is the shape `A09`, `A33` and `A38` already
297
- * carry (`polygons.length === 0 && endsChecked === 0`). `A13_MESH_BUDGET` is
298
- * why the clause is stated: a declared slot budget is measured against a
299
- * count of mesh slots, and zero is a count, so that half passes on a rig
300
- * with no mesh — while a rig that declares only a TRIANGLE budget has
301
- * nothing left to measure and skips.
298
+ * carry (`polygons.length === 0 && endsChecked === 0`), and `A10` since #902.
299
+ * `A13_MESH_BUDGET` is why the clause is stated: a declared slot budget is
300
+ * measured against a count of mesh slots, and zero is a count, so that half
301
+ * passes on a rig with no mesh — while a rig that declares only a TRIANGLE
302
+ * budget has nothing left to measure and skips.
302
303
  *
303
304
  * ⚠️ What the criterion is not allowed to become is a table of assertions with
304
305
  * their verdicts written beside them. Every row above is decided by reading the
@@ -318,6 +319,13 @@ export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure
318
319
  export const SKIP_NO_REGION_ATTACHMENT = 'the skeleton carries no region attachment';
319
320
  export const SKIP_NO_MESH_ATTACHMENT = 'the skeleton carries no mesh attachment';
320
321
  export const SKIP_NO_ANIMATION = 'the skeleton carries no animation';
322
+ /**
323
+ * A10's (issue #902): its setup clause reads the bones and its stepping clause
324
+ * the animations, so it skips only when the skeleton carries neither — and the
325
+ * sentence is `SKIP_NO_ANIMATION`'s with the second subject added, so the two
326
+ * cannot drift into different words for the same absence.
327
+ */
328
+ export const SKIP_NO_POSE = `${SKIP_NO_ANIMATION} and no bone, so there is no setup pose to read and nothing to step`;
321
329
  export const SKIP_NO_TIMELINE = 'no animation here carries a timeline';
322
330
  export const SKIP_NO_PHYSICS_CONSTRAINT = 'the skeleton declares no physics constraint';
323
331
  export const SKIP_NO_ATLAS_PAGE = 'the atlas declares no page';
@@ -4336,13 +4344,31 @@ export function validate(input: ValidateInput): ValidateReport {
4336
4344
 
4337
4345
  // --- A10: step every animation and look for NaN ------------------------
4338
4346
  check('A10_NO_NAN_AFTER_STEPPING', () => {
4339
- // The name carries a subject as well as a construct, and A15 is the
4340
- // pattern (#580): the NaN is produced by STEPPING, which happens once per
4341
- // animation, so a skeleton with none has not been stepped and has no pose
4342
- // to find non-finite. A09 already reads the same subject this way — a
4343
- // static rig has no duration to compare — and the two must not print
4344
- // different verdicts over one skeleton's empty animation list.
4345
- if (data.animations.length === 0) return skip('A10_NO_NAN_AFTER_STEPPING', SKIP_NO_ANIMATION);
4347
+ // Two clauses, and since issue #902 each is read on its own subject. The
4348
+ // SETUP POSE is posed from the bones, so it has something to read on any
4349
+ // skeleton that carries one; the STEPPED FRAMES are posed once per
4350
+ // animation, so a skeleton with none has nothing to step. This used to
4351
+ // skip the whole rule on the second fact alone, which was A15's pattern
4352
+ // (#580) for a rule whose only clause was the stepping — and #882 gave it
4353
+ // the setup clause without moving the skip. Measured on the static probe
4354
+ // with a leaf bone at `rotation: 1e309` in its emitted file: `validate`
4355
+ // printed this rule as SKIP and the run green under both profiles, and
4356
+ // `render` refused the same file by the bone. A static rig's setup pose
4357
+ // is the whole of what it shows, so that was the one pose nothing read.
4358
+ //
4359
+ // ⇒ The multi-clause rule of #580 now governs, the shape A09, A13, A33
4360
+ // and A38 carry: the rule SKIPs only when neither clause has anything to
4361
+ // measure, the clause that ran decides PASS or FAIL, and the clause that
4362
+ // had nothing is named on the stats line (`nanStepping=skipped`, beside
4363
+ // `animations=0`) the way A47 names a constraint it did not measure. A
4364
+ // PASS row carries no detail, which is why the stats line is where.
4365
+ //
4366
+ // ⚠️ A09 does not follow, and that is its own name read at its word: a
4367
+ // declared duration is a fact about an animation and nothing else, so a
4368
+ // static rig still gives it nothing at all.
4369
+ if (data.animations.length === 0 && data.bones.length === 0) {
4370
+ return skip('A10_NO_NAN_AFTER_STEPPING', SKIP_NO_POSE);
4371
+ }
4346
4372
  /** Is this a mode `updateWorldTransform`'s switch has a case for? Read off the runtime's own enum. */
4347
4373
  const isMode = (inherit: unknown): boolean => typeof inherit === 'number' && Inherit[inherit] !== undefined;
4348
4374
 
@@ -4421,6 +4447,73 @@ export function validate(input: ValidateInput): ValidateReport {
4421
4447
  // below is posed AFTER `state.apply`, so a bone the animation keys from
4422
4448
  // t=0 never shows its setup value to the loop, and a runtime that shows
4423
4449
  // the rig at rest does show it.
4450
+ //
4451
+ // 🔸 Posed once, before any animation is set, rather than once per
4452
+ // animation as it was until #902: `setAnimation` does not touch the
4453
+ // skeleton, so every animation's copy of this pose was the same pose, and
4454
+ // the first of them was the only one ever read.
4455
+ if (data.animations.length === 0) stats.nanStepping = 'skipped';
4456
+ const atRest = new Skeleton(data);
4457
+ atRest.setupPose();
4458
+ atRest.update(0);
4459
+ atRest.updateWorldTransform(Physics.reset);
4460
+ const atSetup = nonFiniteOfPosed('the setup pose', atRest);
4461
+ if (atSetup !== null) {
4462
+ fail('A10_NO_NAN_AFTER_STEPPING', atSetup);
4463
+ return;
4464
+ }
4465
+
4466
+ /**
4467
+ * What a posed frame shows that the world-transform scan does not read: a
4468
+ * bone posing no inheritance mode, and a slot colour that is not finite.
4469
+ * `where` leads the sentence — the animation's name at a stepped frame,
4470
+ * as it always has, or `the setup pose` on a skeleton with no animation.
4471
+ */
4472
+ const poseDefect = (where: string, skeleton: Skeleton): string | null => {
4473
+ for (const bone of skeleton.bones) {
4474
+ const pose = bone.appliedPose;
4475
+ // The setup half of the inherit-key clause above: a bone's own
4476
+ // `inherit` goes through the same lookup, and a miss there loads
4477
+ // `undefined` into the setup pose, which every frame copies. Only
4478
+ // reachable on a file rigc did not write — `parseRigSpec` refuses the
4479
+ // spelling.
4480
+ if (!isMode(pose.inherit)) {
4481
+ const rawBone = Array.isArray(raw?.bones)
4482
+ ? (raw.bones as unknown[]).find((b) => isObj(b) && b.name === bone.data.name)
4483
+ : undefined;
4484
+ return (
4485
+ `${where}: bone "${bone.data.name}" poses inheritance mode ${String(pose.inherit)} — its setup ` +
4486
+ `spells inherit ${JSON.stringify(isObj(rawBone) ? rawBone.inherit : undefined)}, which the ` +
4487
+ `runtime's mode lookup does not resolve (${BONE_INHERIT_KNOWN}), so its world rotation, scale and shear ` +
4488
+ 'are never computed — they stay 0 and everything the bone carries collapses to a point'
4489
+ );
4490
+ }
4491
+ }
4492
+ for (const slot of skeleton.slots) {
4493
+ const c = slot.appliedPose.color;
4494
+ if (![c.r, c.g, c.b, c.a].every(Number.isFinite)) return `${where}: slot "${slot.data.name}" colour is non-finite`;
4495
+ // The other colour a slot poses, and it was outside this loop until
4496
+ // issue #690 for the reason every gap here has: nothing emitted one.
4497
+ // `null` is the ordinary case — a slot with no `dark` allocates no
4498
+ // dark colour at all — and is not a reading to make, so it is
4499
+ // skipped rather than treated as zero.
4500
+ const d = slot.appliedPose.darkColor;
4501
+ if (d !== null && ![d.r, d.g, d.b].every(Number.isFinite)) {
4502
+ return `${where}: slot "${slot.data.name}" dark colour is non-finite`;
4503
+ }
4504
+ }
4505
+ return null;
4506
+ };
4507
+
4508
+ // The stepping half, and on a static rig there is nothing to step: the
4509
+ // setup pose is then the only frame the skeleton has, and the two readings
4510
+ // a stepped frame gets are made on it instead, so a colour or an
4511
+ // inheritance mode the rig shows at rest is not left to `render`.
4512
+ if (data.animations.length === 0) {
4513
+ const atRestDefect = poseDefect('the setup pose', atRest);
4514
+ if (atRestDefect !== null) fail('A10_NO_NAN_AFTER_STEPPING', atRestDefect);
4515
+ return;
4516
+ }
4424
4517
  for (const anim of data.animations) {
4425
4518
  const skeleton = new Skeleton(data);
4426
4519
  const state = new AnimationState(new AnimationStateData(data));
@@ -4428,11 +4521,6 @@ export function validate(input: ValidateInput): ValidateReport {
4428
4521
  skeleton.setupPose();
4429
4522
  skeleton.update(0);
4430
4523
  skeleton.updateWorldTransform(Physics.reset);
4431
- const atSetup = nonFiniteOfPosed('the setup pose', skeleton);
4432
- if (atSetup !== null) {
4433
- fail('A10_NO_NAN_AFTER_STEPPING', atSetup);
4434
- return;
4435
- }
4436
4524
  const step = Math.max(anim.duration, 1) / STEP_FRAMES;
4437
4525
  for (let i = 0; i < STEP_FRAMES; i++) {
4438
4526
  state.update(step);
@@ -4447,42 +4535,10 @@ export function validate(input: ValidateInput): ValidateReport {
4447
4535
  fail('A10_NO_NAN_AFTER_STEPPING', found);
4448
4536
  return;
4449
4537
  }
4450
- for (const bone of skeleton.bones) {
4451
- const pose = bone.appliedPose;
4452
- // The setup half of the clause above: a bone's own `inherit` goes
4453
- // through the same lookup, and a miss there loads `undefined` into
4454
- // the setup pose, which every frame copies. Only reachable on a
4455
- // file rigc did not write — `parseRigSpec` refuses the spelling.
4456
- if (!isMode(pose.inherit)) {
4457
- const rawBone = Array.isArray(raw?.bones)
4458
- ? (raw.bones as unknown[]).find((b) => isObj(b) && b.name === bone.data.name)
4459
- : undefined;
4460
- fail(
4461
- 'A10_NO_NAN_AFTER_STEPPING',
4462
- `${anim.name}: bone "${bone.data.name}" poses inheritance mode ${String(pose.inherit)} — its setup ` +
4463
- `spells inherit ${JSON.stringify(isObj(rawBone) ? rawBone.inherit : undefined)}, which the ` +
4464
- `runtime's mode lookup does not resolve (${BONE_INHERIT_KNOWN}), so its world rotation, scale and shear ` +
4465
- 'are never computed — they stay 0 and everything the bone carries collapses to a point',
4466
- );
4467
- return;
4468
- }
4469
- }
4470
- for (const slot of skeleton.slots) {
4471
- const c = slot.appliedPose.color;
4472
- if (![c.r, c.g, c.b, c.a].every(Number.isFinite)) {
4473
- fail('A10_NO_NAN_AFTER_STEPPING', `${anim.name}: slot "${slot.data.name}" colour is non-finite`);
4474
- return;
4475
- }
4476
- // The other colour a slot poses, and it was outside this loop until
4477
- // issue #690 for the reason every gap here has: nothing emitted one.
4478
- // `null` is the ordinary case — a slot with no `dark` allocates no
4479
- // dark colour at all — and is not a reading to make, so it is
4480
- // skipped rather than treated as zero.
4481
- const d = slot.appliedPose.darkColor;
4482
- if (d !== null && ![d.r, d.g, d.b].every(Number.isFinite)) {
4483
- fail('A10_NO_NAN_AFTER_STEPPING', `${anim.name}: slot "${slot.data.name}" dark colour is non-finite`);
4484
- return;
4485
- }
4538
+ const defect = poseDefect(anim.name, skeleton);
4539
+ if (defect !== null) {
4540
+ fail('A10_NO_NAN_AFTER_STEPPING', defect);
4541
+ return;
4486
4542
  }
4487
4543
  }
4488
4544
  }