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/cli.ts +8 -5
- package/docs/AUTHORING.md +155 -27
- package/docs/INGEST.md +20 -13
- package/docs/MOTION.md +31 -17
- package/package.json +1 -1
- package/src/atlas.ts +145 -24
- package/src/check.ts +22 -0
- package/src/compile.ts +6 -1
- package/src/keys.ts +87 -0
- package/src/motion.ts +7 -1
- package/src/pose.ts +260 -13
- package/src/render.ts +192 -22
- package/src/rig.ts +46 -2
- package/src/validate.ts +34 -4
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
|