spine-rigc 0.20.3 → 0.22.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 +4 -4
- package/cli.ts +7 -1
- package/docs/AUTHORING.md +275 -43
- package/docs/FACE.md +53 -7
- package/docs/SPEC_COVERAGE.md +9 -0
- package/package.json +2 -1
- package/src/compile.ts +618 -171
- package/src/keys.ts +117 -0
- package/src/motion.ts +143 -15
- package/src/rig.ts +445 -117
- package/src/types.ts +97 -8
- package/src/validate.ts +106 -1
- package/tools/editor_roundtrip.ts +99 -12
package/src/keys.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The other half of "the compiler never invents a value that is not in the
|
|
3
|
+
* spec": **the compiler never discards a value that is in the spec.**
|
|
4
|
+
*
|
|
5
|
+
* `src/compile.ts` reads an input object by naming the fields it knows — a
|
|
6
|
+
* literal list walked by a `copy` helper, or a run of `if (spec.x !== undefined)`
|
|
7
|
+
* lines. Either spelling asks the same question, *what does the emitter want?*,
|
|
8
|
+
* and neither ever asks the opposite one, *what does this file actually say?* So
|
|
9
|
+
* a key outside the known set was never looked at, never mentioned and never
|
|
10
|
+
* emitted, and the build exited 0 with every assertion green (issue #545).
|
|
11
|
+
*
|
|
12
|
+
* That is the input-side twin of the silence the whole tool exists to remove.
|
|
13
|
+
* A missing number is already a `CompileError` naming the field; an extra one
|
|
14
|
+
* was nothing at all — and the extra one is the worse of the two, because an
|
|
15
|
+
* invented value at least appears in the output where somebody can see it.
|
|
16
|
+
*
|
|
17
|
+
* ## What this module is, and what it is not
|
|
18
|
+
*
|
|
19
|
+
* It is one refusal and the near-miss search that makes the refusal a repair. It
|
|
20
|
+
* is **not** a schema: the known-key sets live beside the shapes they describe
|
|
21
|
+
* (`RIG_KEYS` in [`rig.ts`](rig.ts), `MOTION_KEYS` in [`motion.ts`](motion.ts)),
|
|
22
|
+
* because a set of key names two files away from its interface is a set that
|
|
23
|
+
* drifts from it. `KEY01` in `selftest.ts` derives each set from the interface's
|
|
24
|
+
* own source text and compares, so the pair cannot drift in silence, and `KEY02`
|
|
25
|
+
* refuses a declared key that occurs nowhere else in the tree — which is the
|
|
26
|
+
* exact shape `scaleYMode` had.
|
|
27
|
+
*
|
|
28
|
+
* ## Why the refusal is at parse time rather than at emit time
|
|
29
|
+
*
|
|
30
|
+
* The alternative considered was to record the keys the emitter actually
|
|
31
|
+
* touches and subtract them afterwards — no table at all, and therefore no
|
|
32
|
+
* drift. It is rejected for two measured reasons:
|
|
33
|
+
*
|
|
34
|
+
* - **It answers a different question.** `buildRigMesh` returns at
|
|
35
|
+
* `if (att.generator)` before reading `width`, `hull` or `edges`, so a
|
|
36
|
+
* recorder would refuse `"width"` on a generated mesh and accept it on an
|
|
37
|
+
* authored one. That makes the accepted key set a property of the file's own
|
|
38
|
+
* values rather than of the format, and the format's key set is what an
|
|
39
|
+
* agent authoring against it has to be told.
|
|
40
|
+
* - **It cannot reach what the emitter never visits.** The emit loop walks the
|
|
41
|
+
* rig's slots and skips one with no attachments before it reads `setup` —
|
|
42
|
+
* the blind spot issue #293 was lost in for three weeks and `parseMotionSpec`
|
|
43
|
+
* was written to remove. A recorder rebuilds it.
|
|
44
|
+
*/
|
|
45
|
+
import { CompileError } from './errors.ts';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Levenshtein distance. Only ever called on the losing side of a refusal, so the
|
|
49
|
+
* O(n*m) table is free and a cheaper heuristic (shared prefix, substring) would
|
|
50
|
+
* miss the commonest real case — a transposition or one wrong character in a
|
|
51
|
+
* hand-typed name.
|
|
52
|
+
*/
|
|
53
|
+
export function nameDistance(a: string, b: string): number {
|
|
54
|
+
const rows = a.length + 1;
|
|
55
|
+
const cols = b.length + 1;
|
|
56
|
+
let previous = new Array<number>(cols);
|
|
57
|
+
for (let j = 0; j < cols; j++) previous[j] = j;
|
|
58
|
+
for (let i = 1; i < rows; i++) {
|
|
59
|
+
const current = new Array<number>(cols);
|
|
60
|
+
current[0] = i;
|
|
61
|
+
for (let j = 1; j < cols; j++) {
|
|
62
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
63
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
|
|
64
|
+
}
|
|
65
|
+
previous = current;
|
|
66
|
+
}
|
|
67
|
+
return previous[cols - 1];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Up to five known names closest to the one that was not found. */
|
|
71
|
+
export function nearMisses(wanted: string, known: Iterable<string>): string[] {
|
|
72
|
+
const scored: Array<{ name: string; d: number }> = [];
|
|
73
|
+
for (const name of known) {
|
|
74
|
+
const d = nameDistance(wanted.toLowerCase(), name.toLowerCase());
|
|
75
|
+
// Half the name's length, floored at 2: "leg" must not suggest "arm", and a
|
|
76
|
+
// long name may still be recognisable through several typos.
|
|
77
|
+
if (d <= Math.max(2, Math.floor(wanted.length / 2))) scored.push({ name, d });
|
|
78
|
+
}
|
|
79
|
+
scored.sort((x, y) => x.d - y.d || (x.name < y.name ? -1 : 1));
|
|
80
|
+
return scored.slice(0, 5).map((s) => s.name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Refuse every key of `node` that `known` does not carry.
|
|
85
|
+
*
|
|
86
|
+
* ⭐ **Every** one of them, in one message, rather than the first. The four keys
|
|
87
|
+
* issue #545 planted into a single constraint were four separate mistakes an
|
|
88
|
+
* author had made in one place, and a refusal that names one of them buys three
|
|
89
|
+
* more round trips through a compile that is not cheap.
|
|
90
|
+
*
|
|
91
|
+
* ⭐ The near miss is what makes this a repair rather than a lecture. Both sides
|
|
92
|
+
* are lower-cased before the distance is taken, so `ROTATE` is distance 0 from
|
|
93
|
+
* `rotate` and comes back first — a real key in the wrong case is the commonest
|
|
94
|
+
* of these and the one an author is least likely to spot by re-reading.
|
|
95
|
+
*
|
|
96
|
+
* `known` may be empty (a shape with no fields at all); the message then says so
|
|
97
|
+
* rather than printing `known: ` with nothing after it.
|
|
98
|
+
*/
|
|
99
|
+
export function refuseUnknownKeys(
|
|
100
|
+
node: Record<string, unknown>,
|
|
101
|
+
known: readonly string[],
|
|
102
|
+
where: string,
|
|
103
|
+
what: string,
|
|
104
|
+
): void {
|
|
105
|
+
const strays = Object.keys(node).filter((key) => !known.includes(key));
|
|
106
|
+
if (strays.length === 0) return;
|
|
107
|
+
const named = strays.map((key) => {
|
|
108
|
+
const near = nearMisses(key, known);
|
|
109
|
+
return near.length === 0 ? `"${key}"` : `"${key}" (did you mean ${near.map((n) => `"${n}"`).join(', ')}?)`;
|
|
110
|
+
});
|
|
111
|
+
throw new CompileError(
|
|
112
|
+
`${where}: ${what} has ${strays.length === 1 ? 'a key' : `${strays.length} keys`} this compiler does not read: ` +
|
|
113
|
+
`${named.join(', ')}. Nothing reads such a key, so it would be dropped from the emitted skeleton in silence — ` +
|
|
114
|
+
'fix the spelling or remove it. ' +
|
|
115
|
+
(known.length === 0 ? 'This shape has no fields at all.' : `Known here: ${[...known].sort().join(', ')}.`),
|
|
116
|
+
);
|
|
117
|
+
}
|
package/src/motion.ts
CHANGED
|
@@ -38,17 +38,23 @@
|
|
|
38
38
|
* `src/trackgen.ts`) are untouched — every one of them reads the group's member
|
|
39
39
|
* list or the property's projection table, neither of which is in this file.
|
|
40
40
|
*
|
|
41
|
-
* ## Unknown keys
|
|
41
|
+
* ## Unknown keys ARE refused — issue #545
|
|
42
42
|
*
|
|
43
|
-
* ⚠️
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
43
|
+
* ⚠️ This section said the opposite from 2026-09-03 (#321) until #545: *"a
|
|
44
|
+
* misspelled optional field is therefore still silent — `easing` for `ease`
|
|
45
|
+
* plays linear and says nothing"*, declined because `parseRigSpec` did not refuse one
|
|
46
|
+
* either and one format shrugging while the other refuses is a worse surprise
|
|
47
|
+
* than the stray key. That argument was sound and its premise is now false —
|
|
48
|
+
* `parseRigSpec` refuses by name, so the consistent behaviour is this one. The
|
|
49
|
+
* note's own parenthesis is what made it cheap: the motion specs in this
|
|
50
|
+
* repository carried no undeclared key at any level then and carry none now, so
|
|
51
|
+
* the migration cost, measured over all 39, is zero.
|
|
52
|
+
*
|
|
53
|
+
* `MOTION_KEYS` below is the key set, and the refusal itself is one helper in
|
|
54
|
+
* [`keys.ts`](keys.ts) shared with the rig parser.
|
|
50
55
|
*/
|
|
51
56
|
import { CompileError } from './errors.ts';
|
|
57
|
+
import { refuseUnknownKeys } from './keys.ts';
|
|
52
58
|
import type { MotionSpec } from './types.ts';
|
|
53
59
|
|
|
54
60
|
export const MOTION_SPEC_VERSION = 'rigc-motion/1';
|
|
@@ -82,6 +88,58 @@ const PHYSICS_NUMBERS = [
|
|
|
82
88
|
/** The two animation-level constraint families, which share one entry shape. */
|
|
83
89
|
const CONSTRAINT_GROUPS = ['ik', 'transform'] as const;
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Every key each shape of this format owns, keyed by the interface that declares
|
|
93
|
+
* it — the runtime shadow of types TypeScript erases, and the other half of
|
|
94
|
+
* `RIG_KEYS` in [`rig.ts`](rig.ts).
|
|
95
|
+
*
|
|
96
|
+
* 🔒 Held to those interfaces by `KEY01` in `selftest.ts`, which reads the
|
|
97
|
+
* declaring source and compares. The interfaces are spread over three modules —
|
|
98
|
+
* `types.ts` for the format, [`trackgen.ts`](trackgen.ts) for a track's `derive`
|
|
99
|
+
* and [`deformgen.ts`](deformgen.ts) for a deform key's `transform` — and the
|
|
100
|
+
* table is one table anyway, because what it describes is one file.
|
|
101
|
+
*
|
|
102
|
+
* ⚠️ `MotionKey.v` is deliberately not a shape here. On a group track it is a
|
|
103
|
+
* `MotionMemberValues` map keyed by **member name**, so every key of it is a
|
|
104
|
+
* name from the rig rather than a field of this format; `resolveMemberTrack`
|
|
105
|
+
* refuses a member the group does not have, which is the check that fits.
|
|
106
|
+
*/
|
|
107
|
+
export const MOTION_KEYS = {
|
|
108
|
+
MotionSpec: ['spec', 'archetype', 'cut', 'note', 'easings', 'groups', 'setup', 'physics', 'animations', 'mix'],
|
|
109
|
+
MotionMix: ['default', 'pairs'],
|
|
110
|
+
MotionSetupSlot: ['attachment', 'color'],
|
|
111
|
+
MotionPhysics: [
|
|
112
|
+
'bone', 'x', 'y', 'rotate', 'scaleX', 'shearX', 'inertia', 'strength', 'damping', 'mass', 'wind', 'gravity',
|
|
113
|
+
'mix', 'fps', 'limit', 'note',
|
|
114
|
+
],
|
|
115
|
+
MotionAnimation: ['duration', 'loop', 'note', 'tracks', 'ik', 'transform', 'deform', 'drawOrder', 'events'],
|
|
116
|
+
MotionTrack: ['slot', 'group', 'bone', 'physics', 'path', 'slider', 'property', 'lag', 'stagger', 'keys'],
|
|
117
|
+
MotionKey: ['t', 'v', 'derive', 'ease', 'curve'],
|
|
118
|
+
MotionIkTrack: ['constraint', 'keys'],
|
|
119
|
+
MotionIkKey: ['t', 'mix', 'softness', 'bendPositive', 'compress', 'stretch', 'ease', 'curve'],
|
|
120
|
+
MotionTransformTrack: ['constraint', 'keys'],
|
|
121
|
+
MotionTransformKey: ['t', 'mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY', 'ease', 'curve'],
|
|
122
|
+
MotionDeformTrack: ['skin', 'slot', 'attachment', 'keys'],
|
|
123
|
+
MotionDeformKey: ['t', 'offset', 'fromVertex', 'vertices', 'transform', 'ease', 'curve'],
|
|
124
|
+
MotionDrawOrderKey: ['t', 'offsets'],
|
|
125
|
+
MotionDrawOrderOffset: ['slot', 'offset'],
|
|
126
|
+
MotionEventKey: ['t', 'name', 'int', 'float', 'string', 'volume', 'balance'],
|
|
127
|
+
TrackDeriveTurn: ['kind', 'degrees', 'depth', 'carried', 'about'],
|
|
128
|
+
DeformTurn: ['kind', 'radius', 'depth', 'degrees', 'about'],
|
|
129
|
+
DeformAffine: ['kind', 'scale', 'about'],
|
|
130
|
+
DeformWave: ['kind', 'amplitude', 'wavelength', 'phase', 'along', 'axis'],
|
|
131
|
+
DeformBend: ['kind', 'amount', 'from', 'to', 'power', 'along', 'axis'],
|
|
132
|
+
} as const satisfies Record<string, readonly string[]>;
|
|
133
|
+
|
|
134
|
+
/** A deform key's `transform` kinds, and the shape each one's keys come from. */
|
|
135
|
+
const DEFORM_TRANSFORM_SHAPE: Record<string, keyof typeof MOTION_KEYS> = {
|
|
136
|
+
yaw: 'DeformTurn',
|
|
137
|
+
pitch: 'DeformTurn',
|
|
138
|
+
affine: 'DeformAffine',
|
|
139
|
+
wave: 'DeformWave',
|
|
140
|
+
bend: 'DeformBend',
|
|
141
|
+
};
|
|
142
|
+
|
|
85
143
|
function isObj(v: unknown): v is Record<string, unknown> {
|
|
86
144
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
87
145
|
}
|
|
@@ -113,6 +171,16 @@ function refuse(where: string, key: string, is: unknown, hint: string): never {
|
|
|
113
171
|
throw new CompileError(`${where}: \`${key}\` is ${describe(is)}; ${hint}`);
|
|
114
172
|
}
|
|
115
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The second refusal shape: **a key this format does not have**, from the one
|
|
176
|
+
* implementation of it. `at` is the path the messages above already print, so a
|
|
177
|
+
* reader meets `setup."lid_l"` whether the entry was the wrong type or carried
|
|
178
|
+
* the wrong field.
|
|
179
|
+
*/
|
|
180
|
+
function known(node: unknown, shape: keyof typeof MOTION_KEYS, where: string, at: string): void {
|
|
181
|
+
if (isObj(node)) refuseUnknownKeys(node, MOTION_KEYS[shape], where, `\`${at}\``);
|
|
182
|
+
}
|
|
183
|
+
|
|
116
184
|
// --- the leaf checks, each returning the value it just proved ---------------
|
|
117
185
|
|
|
118
186
|
function needObj(v: unknown, where: string, key: string, hint: string): Record<string, unknown> {
|
|
@@ -196,6 +264,7 @@ function parseSetup(raw: unknown, where: string): void {
|
|
|
196
264
|
if (entry.attachment !== undefined && entry.attachment !== null && typeof entry.attachment !== 'string') {
|
|
197
265
|
refuse(where, `${key}.attachment`, entry.attachment, 'it is an attachment name, or null for "show nothing"');
|
|
198
266
|
}
|
|
267
|
+
known(entry, 'MotionSetupSlot', where, key);
|
|
199
268
|
if (entry.color !== undefined) {
|
|
200
269
|
const hint = 'a setup colour is [r, g, b, a], four finite numbers in 0..1 — a channel that is not one is clamped to `NaN` and written into the slot as the text "NaN"';
|
|
201
270
|
const color = needArray(entry.color, where, `${key}.color`, hint);
|
|
@@ -224,6 +293,7 @@ function parsePhysics(raw: unknown, where: string): void {
|
|
|
224
293
|
for (const [name, entry] of Object.entries(table)) {
|
|
225
294
|
const key = `physics."${name}"`;
|
|
226
295
|
const spec = needObj(entry, where, key, 'a physics constraint is an object naming the bone it drives and the components it drives it in');
|
|
296
|
+
known(spec, 'MotionPhysics', where, key);
|
|
227
297
|
needString(spec.bone, where, `${key}.bone`, 'a physics constraint drives one bone, named here');
|
|
228
298
|
for (const field of PHYSICS_NUMBERS) {
|
|
229
299
|
optFinite(spec[field], where, `${key}.${field}`, 'every tuning field of a physics constraint is a finite number — a non-number is rounded to `NaN` and emitted as `null`, which the runtime reads as zero');
|
|
@@ -242,6 +312,7 @@ function parsePhysics(raw: unknown, where: string): void {
|
|
|
242
312
|
function parseMix(raw: unknown, where: string): void {
|
|
243
313
|
if (raw === undefined) return;
|
|
244
314
|
const mix = needObj(raw, where, 'mix', 'it is `{ "default": <seconds>, "pairs"?: [["<from>", "<to>", <seconds>], …] }`');
|
|
315
|
+
known(mix, 'MotionMix', where, 'mix');
|
|
245
316
|
needFinite(mix.default, where, 'mix.default', 'the default mix duration is a finite number of seconds');
|
|
246
317
|
if (mix.pairs === undefined) return;
|
|
247
318
|
const pairs = needArray(mix.pairs, where, 'mix.pairs', 'it is an array of `["<from>", "<to>", <seconds>]` triples');
|
|
@@ -273,9 +344,25 @@ function parseKeyEasing(key: Record<string, unknown>, where: string, at: string)
|
|
|
273
344
|
optString(key.ease, where, `${at}.ease`, 'it names an entry of this spec\'s `easings` table, or is "stepped"');
|
|
274
345
|
}
|
|
275
346
|
|
|
276
|
-
/**
|
|
277
|
-
|
|
347
|
+
/**
|
|
348
|
+
* One `{ t, … }` key of any family: an object, with a finite time and a string
|
|
349
|
+
* `ease`.
|
|
350
|
+
*
|
|
351
|
+
* `shape` is per caller because the five families' key shapes are five
|
|
352
|
+
* different sets — an `rgba` key's `v` is not a thing an ik key may carry, and
|
|
353
|
+
* an ik key's `softness` is not a thing a value track may. One shared shape here
|
|
354
|
+
* would accept every field of every family on all of them, which is a key set
|
|
355
|
+
* nothing in the format actually has.
|
|
356
|
+
*/
|
|
357
|
+
function parseKey(
|
|
358
|
+
raw: unknown,
|
|
359
|
+
where: string,
|
|
360
|
+
at: string,
|
|
361
|
+
hint: string,
|
|
362
|
+
shape: keyof typeof MOTION_KEYS,
|
|
363
|
+
): Record<string, unknown> {
|
|
278
364
|
const key = needObj(raw, where, at, hint);
|
|
365
|
+
known(key, shape, where, at);
|
|
279
366
|
parseKeyTime(key, where, at);
|
|
280
367
|
parseKeyEasing(key, where, at);
|
|
281
368
|
return key;
|
|
@@ -286,6 +373,7 @@ function parseTracks(raw: unknown, where: string, at: string): void {
|
|
|
286
373
|
for (const [i, entry] of tracks.entries()) {
|
|
287
374
|
const key = `${at}.tracks[${i}]`;
|
|
288
375
|
const track = needObj(entry, where, key, 'a track is an object naming one target, one property and its keys');
|
|
376
|
+
known(track, 'MotionTrack', where, key);
|
|
289
377
|
needString(track.property, where, `${key}.property`, 'a track states the property it keys — the table is AUTHORING §4.4');
|
|
290
378
|
for (const field of TARGET_FIELDS) {
|
|
291
379
|
optString(track[field], where, `${key}.${field}`, `a track's "${field}" is the name of the ${field === 'slot' || field === 'bone' ? field : `${field} it targets`}`);
|
|
@@ -293,7 +381,16 @@ function parseTracks(raw: unknown, where: string, at: string): void {
|
|
|
293
381
|
optFinite(track.lag, where, `${key}.lag`, '"lag" is seconds added to every key time of this track, so a finite number — a string is CONCATENATED onto each time and a boolean adds 1');
|
|
294
382
|
optFinite(track.stagger, where, `${key}.stagger`, '"stagger" is the extra per-member delay inside a group, in seconds, so a finite number');
|
|
295
383
|
const keys = needArray(track.keys, where, `${key}.keys`, 'it is an array of `{ t, v }` keys');
|
|
296
|
-
for (const [j, k] of keys.entries())
|
|
384
|
+
for (const [j, k] of keys.entries()) {
|
|
385
|
+
const at = `${key}.keys[${j}]`;
|
|
386
|
+
const parsed = parseKey(k, where, at, 'a key is an object of `{ t, v, … }`', 'MotionKey');
|
|
387
|
+
// `derive` states a generator's parameters rather than a value, and its
|
|
388
|
+
// shape lives with the evaluator (`src/trackgen.ts`). `yaw` and `pitch`
|
|
389
|
+
// are one interface — they differ in which coordinate they read, not in
|
|
390
|
+
// what they carry — so there is no dispatch to do here, and whether the
|
|
391
|
+
// kind is one of the two stays `evaluateTrackDerive`'s refusal.
|
|
392
|
+
known(parsed.derive, 'TrackDeriveTurn', where, `${at}.derive`);
|
|
393
|
+
}
|
|
297
394
|
}
|
|
298
395
|
}
|
|
299
396
|
|
|
@@ -303,9 +400,18 @@ function parseConstraintTracks(raw: unknown, where: string, at: string, group: (
|
|
|
303
400
|
for (const [i, entry] of entries.entries()) {
|
|
304
401
|
const key = `${at}.${group}[${i}]`;
|
|
305
402
|
const track = needObj(entry, where, key, `${group === 'ik' ? 'an ik' : 'a transform'} timeline is an object of \`{ constraint, keys }\``);
|
|
403
|
+
known(track, group === 'ik' ? 'MotionIkTrack' : 'MotionTransformTrack', where, key);
|
|
306
404
|
needString(track.constraint, where, `${key}.constraint`, `4.3 writes this group as \`${group}.<constraint>\`, so the constraint name is the only target there is`);
|
|
307
405
|
const keys = needArray(track.keys, where, `${key}.keys`, 'it is an array of keys, each naming the same set of mix fields');
|
|
308
|
-
for (const [j, k] of keys.entries())
|
|
406
|
+
for (const [j, k] of keys.entries()) {
|
|
407
|
+
parseKey(
|
|
408
|
+
k,
|
|
409
|
+
where,
|
|
410
|
+
`${key}.keys[${j}]`,
|
|
411
|
+
`a ${group} key is an object of \`{ t, … }\``,
|
|
412
|
+
group === 'ik' ? 'MotionIkKey' : 'MotionTransformKey',
|
|
413
|
+
);
|
|
414
|
+
}
|
|
309
415
|
}
|
|
310
416
|
}
|
|
311
417
|
|
|
@@ -315,18 +421,33 @@ function parseDeform(raw: unknown, where: string, at: string): void {
|
|
|
315
421
|
for (const [i, entry] of entries.entries()) {
|
|
316
422
|
const key = `${at}.deform[${i}]`;
|
|
317
423
|
const track = needObj(entry, where, key, 'a deform timeline is an object of `{ skin?, slot, attachment, keys }`');
|
|
424
|
+
known(track, 'MotionDeformTrack', where, key);
|
|
318
425
|
optString(track.skin, where, `${key}.skin`, 'it names the skin the attachment lives in; absent means "default"');
|
|
319
426
|
needString(track.slot, where, `${key}.slot`, 'a deform timeline keys one attachment of one slot, named here');
|
|
320
427
|
needString(track.attachment, where, `${key}.attachment`, "it is the attachment's placeholder name inside that skin and slot");
|
|
321
428
|
const keys = needArray(track.keys, where, `${key}.keys`, 'it is an array of keys, each a sparse edit of the setup geometry');
|
|
322
|
-
for (const [j, k] of keys.entries())
|
|
429
|
+
for (const [j, k] of keys.entries()) {
|
|
430
|
+
const at = `${key}.keys[${j}]`;
|
|
431
|
+
const parsed = parseKey(k, where, at, 'a deform key is an object of `{ t, vertices? | transform? }`', 'MotionDeformKey');
|
|
432
|
+
// `transform` is five kinds sharing one field name, and they share almost
|
|
433
|
+
// nothing else: `wave` carries `wavelength` and `bend` carries `power`, so
|
|
434
|
+
// checking either against the union's flattened keys would accept both on
|
|
435
|
+
// both. An unrecognised `kind` is `evaluateDeformTransform`'s refusal,
|
|
436
|
+
// which names the five.
|
|
437
|
+
if (isObj(parsed.transform)) {
|
|
438
|
+
const shape = DEFORM_TRANSFORM_SHAPE[String(parsed.transform.kind)];
|
|
439
|
+
if (shape !== undefined) known(parsed.transform, shape, where, `${at}.transform`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
323
442
|
}
|
|
324
443
|
}
|
|
325
444
|
|
|
326
445
|
function parseEvents(raw: unknown, where: string, at: string): void {
|
|
327
446
|
if (raw === undefined) return;
|
|
328
447
|
const keys = needArray(raw, where, `${at}.events`, 'it is an array of `{ t, name }` firings — one timeline per animation, naming no target');
|
|
329
|
-
for (const [i, k] of keys.entries())
|
|
448
|
+
for (const [i, k] of keys.entries()) {
|
|
449
|
+
parseKey(k, where, `${at}.events[${i}]`, 'an event key is an object of `{ t, name, … }`', 'MotionEventKey');
|
|
450
|
+
}
|
|
330
451
|
}
|
|
331
452
|
|
|
332
453
|
/**
|
|
@@ -343,7 +464,7 @@ function parseDrawOrder(raw: unknown, where: string, at: string): void {
|
|
|
343
464
|
const keys = needArray(raw, where, `${at}.drawOrder`, 'it is an array of `{ t, offsets? }` keys — one timeline per animation, naming no target');
|
|
344
465
|
for (const [i, entry] of keys.entries()) {
|
|
345
466
|
const key = `${at}.drawOrder[${i}]`;
|
|
346
|
-
const dk = parseKey(entry, where, key, 'a draw-order key is an object of `{ t, offsets? }`');
|
|
467
|
+
const dk = parseKey(entry, where, key, 'a draw-order key is an object of `{ t, offsets? }`', 'MotionDrawOrderKey');
|
|
347
468
|
if (dk.offsets === undefined) continue;
|
|
348
469
|
const offsets = needArray(
|
|
349
470
|
dk.offsets,
|
|
@@ -354,6 +475,7 @@ function parseDrawOrder(raw: unknown, where: string, at: string): void {
|
|
|
354
475
|
for (const [j, o] of offsets.entries()) {
|
|
355
476
|
const oat = `${key}.offsets[${j}]`;
|
|
356
477
|
const off = needObj(o, where, oat, 'one moved slot is `{ "slot": "<name>", "offset": <places later> }`');
|
|
478
|
+
known(off, 'MotionDrawOrderOffset', where, oat);
|
|
357
479
|
needString(off.slot, where, `${oat}.slot`, 'it names the slot this key moves');
|
|
358
480
|
// The TYPE only. Whether it is a whole number, and whether it lands inside
|
|
359
481
|
// the emitted slots array, are `compile`'s — both need the slot table.
|
|
@@ -365,6 +487,7 @@ function parseDrawOrder(raw: unknown, where: string, at: string): void {
|
|
|
365
487
|
function parseAnimation(raw: unknown, where: string, name: string): void {
|
|
366
488
|
const at = `animations."${name}"`;
|
|
367
489
|
const anim = needObj(raw, where, at, 'an animation is an object of `{ duration, tracks, … }`');
|
|
490
|
+
known(anim, 'MotionAnimation', where, at);
|
|
368
491
|
const duration = needFinite(anim.duration, where, `${at}.duration`, 'an animation declares its duration in seconds, as a finite number — it is checked against the compiled last key (rule R7), and a comparison against a non-number is silently false');
|
|
369
492
|
if (duration < 0) {
|
|
370
493
|
refuse(where, `${at}.duration`, duration, 'a duration is a length of time, so it is not negative');
|
|
@@ -398,6 +521,11 @@ export function parseMotionSpec(raw: unknown, where: string): MotionSpec {
|
|
|
398
521
|
if (raw.spec !== MOTION_SPEC_VERSION) {
|
|
399
522
|
throw new CompileError(`${where}: unknown motion spec version: ${String(raw.spec)}, expected "${MOTION_SPEC_VERSION}"`);
|
|
400
523
|
}
|
|
524
|
+
// Before the field checks, for the reason `parseRigSpec` puts its own first: a
|
|
525
|
+
// key nothing reads is often the CAUSE of the field that is missing, and
|
|
526
|
+
// `"animation"` for `"animations"` should be named as the typo it is rather
|
|
527
|
+
// than as an absent table.
|
|
528
|
+
known(raw, 'MotionSpec', where, 'this motion spec');
|
|
401
529
|
needString(raw.archetype, where, 'archetype', "it names the rig this spec was authored against, and must equal that rig spec's own `name`");
|
|
402
530
|
needString(raw.cut, where, 'cut', 'it names the cut these keys were authored for');
|
|
403
531
|
optString(raw.note, where, 'note', 'it is prose for a reader');
|