spine-rigc 1.4.0 → 1.5.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/rig.ts CHANGED
@@ -60,7 +60,7 @@
60
60
  * with no manifest at all declares them here.
61
61
  */
62
62
  import { CompileError, NotImplementedError } from './errors.ts';
63
- import { refuseUnknownKeys } from './keys.ts';
63
+ import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys } from './keys.ts';
64
64
 
65
65
  export { CompileError, NotImplementedError };
66
66
 
@@ -1925,6 +1925,10 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
1925
1925
  const whole = (field: string, min: number): void => {
1926
1926
  const value = seq[field];
1927
1927
  if (value === undefined) return;
1928
+ // A number the file cannot carry is `refuseNumbersTheFileCannotCarry`'s,
1929
+ // one sentence for the whole family. This check runs inside the key scan,
1930
+ // before that walk, and printed a stated 1e309 as `null` (issue #881).
1931
+ if (typeof value === 'number' && !Number.isFinite(Math.fround(value))) return;
1928
1932
  if (typeof value !== 'number' || !Number.isInteger(value) || value < min) {
1929
1933
  throw new CompileError(
1930
1934
  `${where}: ${at}.${field} is ${JSON.stringify(value) ?? String(value)}; it is a whole number` +
@@ -1946,7 +1950,7 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
1946
1950
  whole('digits', 0);
1947
1951
  whole('setup', 0);
1948
1952
  const count = seq.count as number;
1949
- if (typeof seq.setup === 'number' && seq.setup >= count) {
1953
+ if (typeof seq.setup === 'number' && Number.isFinite(Math.fround(seq.setup)) && Number.isFinite(Math.fround(count)) && seq.setup >= count) {
1950
1954
  throw new CompileError(
1951
1955
  `${where}: ${at}.setup is ${seq.setup}, and a ${count}-frame series has frames 0 to ${count - 1}. ` +
1952
1956
  '`Sequence.resolveIndex` clamps an index at or past the end to the LAST frame (measured: `setup: 7` on ' +
@@ -2100,6 +2104,42 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2100
2104
  }
2101
2105
  }
2102
2106
 
2107
+
2108
+ /**
2109
+ * A rig-spec path in the words the other refusals in this file use for it:
2110
+ * `bone "hip" x`, `constraint "aim" mixRotate`, `skin "default" slot "tail"
2111
+ * attachment "tail" weights[0][1].x`, `event "step" float`. Anything else is
2112
+ * the dotted path, which names every number exactly.
2113
+ */
2114
+ function rigPlace(raw: Record<string, unknown>): (path: ReadonlyArray<string | number>) => string {
2115
+ const named = (list: unknown, i: number, kind: string, plural: string): string => {
2116
+ const node = Array.isArray(list) ? list[i] : undefined;
2117
+ return isObj(node) && typeof node.name === 'string' ? `${kind} ${JSON.stringify(node.name)}` : `${plural}[${i}]`;
2118
+ };
2119
+ const rest = (path: ReadonlyArray<string | number>): string => {
2120
+ const tail = dottedPath(path);
2121
+ return tail === '' ? '' : ` ${tail}`;
2122
+ };
2123
+ return (path) => {
2124
+ const [head, second] = path;
2125
+ if (typeof second === 'number') {
2126
+ if (head === 'bones') return `${named(raw.bones, second, 'bone', 'bones')}${rest(path.slice(2))}`;
2127
+ if (head === 'slots') return `${named(raw.slots, second, 'slot', 'slots')}${rest(path.slice(2))}`;
2128
+ if (head === 'constraints') return `${named(raw.constraints, second, 'constraint', 'constraints')}${rest(path.slice(2))}`;
2129
+ }
2130
+ if (head === 'events' && typeof second === 'string') return `event ${JSON.stringify(second)}${rest(path.slice(2))}`;
2131
+ if (head === 'skins' && typeof second === 'string') {
2132
+ const inner = path[2] === 'attachments' ? path.slice(3) : path.slice(2);
2133
+ const [slot, attachment] = inner;
2134
+ if (typeof slot === 'string' && typeof attachment === 'string') {
2135
+ return `skin ${JSON.stringify(second)} slot ${JSON.stringify(slot)} attachment ${JSON.stringify(attachment)}${rest(inner.slice(2))}`;
2136
+ }
2137
+ return `skin ${JSON.stringify(second)}${rest(path.slice(2))}`;
2138
+ }
2139
+ return dottedPath(path);
2140
+ };
2141
+ }
2142
+
2103
2143
  /**
2104
2144
  * Parse and check the envelope, then hand back a typed spec.
2105
2145
  *
@@ -2137,6 +2177,10 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
2137
2177
  // throws directly below are the required-key checks, and each of them is the
2138
2178
  // consequence a typo at the root produces.
2139
2179
  checkRigSpecKeys(raw, where);
2180
+ // After the key check, so a misspelled key is named as a misspelling before
2181
+ // its value is judged, and before every reader below, so no range rule or
2182
+ // derived number ever meets a value the file cannot carry (issue #881).
2183
+ refuseNumbersTheFileCannotCarry(raw, where, rigPlace(raw));
2140
2184
 
2141
2185
  if (typeof raw.name !== 'string' || raw.name.length === 0) {
2142
2186
  throw new CompileError(`${where}: a rig spec needs a "name" — a motion spec names it to pick this rig`);
package/src/validate.ts CHANGED
@@ -82,6 +82,7 @@ import {
82
82
  type SpineGeneration,
83
83
  } from './generation.ts';
84
84
  import { colourTypeName, readPngHeader } from './png.ts';
85
+ import { nonFiniteOfPosed } from './render.ts';
85
86
  import { BONE_INHERIT_KNOWN } from './rig.ts';
86
87
  import {
87
88
  CHANNELS_BY_KIND,
@@ -4400,6 +4401,26 @@ export function validate(input: ValidateInput): ValidateReport {
4400
4401
  }
4401
4402
  if (unresolved > 0) return;
4402
4403
 
4404
+ // -- the whole world transform, at the setup pose and every stepped frame
4405
+ //
4406
+ // 🚨 Read through `nonFiniteOfPosed`, the scan `render` refuses on, and
4407
+ // not a second opinion of it (issue #882). This loop read `worldX` and
4408
+ // `worldY` alone until then, and a bone at `rotation: 1e309` — in its setup
4409
+ // pose or as a `rotate` key — poses a finite position over a NaN `a`, `b`,
4410
+ // `c` and `d` whenever it has no child offset from it: measured on every
4411
+ // leaf bone of the three generated probes, 20 of 20 green through the whole
4412
+ // gate, and `render` then refused the same file by the bone. So the gate
4413
+ // said the skeleton was fine and the renderer said it was not, which is two
4414
+ // definitions of one word. The scan reads the six terms of every bone, then
4415
+ // the vertices of every region and mesh shown — because a bone can be
4416
+ // finite over a vertex that is not (a `scaleX` chain whose product stays
4417
+ // under the largest double while every corner of the child's region
4418
+ // passes it) — and names the bone or the vertex, the term and the frame.
4419
+ //
4420
+ // ⚠️ The setup pose is its own surface and is read as such: every frame
4421
+ // below is posed AFTER `state.apply`, so a bone the animation keys from
4422
+ // t=0 never shows its setup value to the loop, and a runtime that shows
4423
+ // the rig at rest does show it.
4403
4424
  for (const anim of data.animations) {
4404
4425
  const skeleton = new Skeleton(data);
4405
4426
  const state = new AnimationState(new AnimationStateData(data));
@@ -4407,18 +4428,27 @@ export function validate(input: ValidateInput): ValidateReport {
4407
4428
  skeleton.setupPose();
4408
4429
  skeleton.update(0);
4409
4430
  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
+ }
4410
4436
  const step = Math.max(anim.duration, 1) / STEP_FRAMES;
4411
4437
  for (let i = 0; i < STEP_FRAMES; i++) {
4412
4438
  state.update(step);
4413
4439
  state.apply(skeleton);
4414
4440
  skeleton.update(step);
4415
4441
  skeleton.updateWorldTransform(Physics.update);
4442
+ const found = nonFiniteOfPosed(
4443
+ `animation ${JSON.stringify(anim.name)} frame ${i + 1} of ${STEP_FRAMES} (t=${((i + 1) * step).toFixed(4)}s)`,
4444
+ skeleton,
4445
+ );
4446
+ if (found !== null) {
4447
+ fail('A10_NO_NAN_AFTER_STEPPING', found);
4448
+ return;
4449
+ }
4416
4450
  for (const bone of skeleton.bones) {
4417
4451
  const pose = bone.appliedPose;
4418
- if (!Number.isFinite(pose.worldX) || !Number.isFinite(pose.worldY)) {
4419
- fail('A10_NO_NAN_AFTER_STEPPING', `${anim.name}: bone "${bone.data.name}" world is (${pose.worldX}, ${pose.worldY})`);
4420
- return;
4421
- }
4422
4452
  // The setup half of the clause above: a bone's own `inherit` goes
4423
4453
  // through the same lookup, and a miss there loads `undefined` into
4424
4454
  // the setup pose, which every frame copies. Only reachable on a