spine-rigc 1.9.0 → 1.9.2
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 +48 -17
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/bone_timelines.ts +56 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +34 -16
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/index.ts +83 -2
- package/src/assertions/model/linked_meshes.ts +2 -2
- package/src/assertions/model/mesh_attachments.ts +1 -1
- package/src/assertions/model/region_joins.ts +20 -12
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +18 -8
- package/src/assertions/model/skin_members.ts +5 -3
- package/src/assertions/model/skin_meshes.ts +1 -1
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/slot_colour.ts +5 -4
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +9 -8
- package/src/bonedist.ts +18 -6
- package/src/compile.ts +114 -14
- package/src/core/additive.ts +468 -0
- package/src/core/constraints.ts +12 -2
- package/src/core/constraints_physics.ts +31 -8
- package/src/core/raw.ts +77 -16
- package/src/core/walk.ts +202 -0
- package/src/deformmeasure.ts +35 -2270
- package/src/deformstructure.ts +60 -18
- package/src/deformsurvey.ts +2295 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +200 -59
- package/src/render_core.ts +5 -0
- package/src/validate.ts +791 -1558
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A43, the body (issue #1025, cut 4c-3 of step 4c of #380): the two-colour
|
|
3
|
+
* tint loads and poses as written.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads, which is
|
|
6
|
+
* `TwoColourFacts` (`../facts/two_colour.ts`) where it read the skeleton JSON
|
|
7
|
+
* and the loaded skeleton: the dark colour each slot states and the one the
|
|
8
|
+
* runtime loaded, the file's slot timelines, whether an animation exists, and
|
|
9
|
+
* a slot's two colours posed at a key's time — which `validate()` takes from
|
|
10
|
+
* spine-core and the model side from rigc's core. The argument for the rule is
|
|
11
|
+
* above its `check` call, which stays in `validate()`.
|
|
12
|
+
*/
|
|
13
|
+
import type { Verdicts } from '../harness.ts';
|
|
14
|
+
import type { TwoColourFacts } from '../facts/two_colour.ts';
|
|
15
|
+
import { SKIP_NO_TWO_COLOR_TINT } from '../reasons.ts';
|
|
16
|
+
import { atStoredKey, isObj } from '../values.ts';
|
|
17
|
+
|
|
18
|
+
export function a43TwoColorTintLoadsAndPosesAsWritten({ fail, skip, stats }: Verdicts, facts: TwoColourFacts): void {
|
|
19
|
+
/** `rrggbb`, or `rrggbbaa` whose last pair the format drops. Null for anything else. */
|
|
20
|
+
const readDarkHex = (hex: string): [number, number, number] | null => {
|
|
21
|
+
const body = hex.startsWith('#') ? hex.slice(1) : hex;
|
|
22
|
+
if (!/^[\da-fA-F]{6}([\da-fA-F]{2})?$/.test(body)) return null;
|
|
23
|
+
return [0, 2, 4].map((i) => Number.parseInt(body.slice(i, i + 2), 16) / 255) as [number, number, number];
|
|
24
|
+
};
|
|
25
|
+
/** `rrggbbaa`, or `rrggbb` the runtime opens at alpha 1. Null for anything else. */
|
|
26
|
+
const readLightHex = (hex: string): [number, number, number, number] | null => {
|
|
27
|
+
const body = hex.startsWith('#') ? hex.slice(1) : hex;
|
|
28
|
+
if (!/^[\da-fA-F]{6}([\da-fA-F]{2})?$/.test(body)) return null;
|
|
29
|
+
const rgb = [0, 2, 4].map((i) => Number.parseInt(body.slice(i, i + 2), 16) / 255);
|
|
30
|
+
return [rgb[0], rgb[1], rgb[2], body.length === 8 ? Number.parseInt(body.slice(6, 8), 16) / 255 : 1];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Half a quantisation step. A channel is one byte in the file and a
|
|
34
|
+
* `Float32Array` entry in a timeline, so the widest honest gap between the
|
|
35
|
+
* number written and the number posed is well under `1/510`.
|
|
36
|
+
*/
|
|
37
|
+
const STEP = 1 / 510;
|
|
38
|
+
const off = (found: number, want: number): boolean => !Number.isFinite(found) || Math.abs(found - want) > STEP;
|
|
39
|
+
const show = (c: readonly number[]): string => c.map((n) => (Number.isFinite(n) ? n.toFixed(4) : 'NaN')).join(', ');
|
|
40
|
+
|
|
41
|
+
// -- the subjects, read off the FILE ---------------------------------
|
|
42
|
+
const declared = new Map<string, string>();
|
|
43
|
+
for (const { slot, dark } of facts.slotDarks) declared.set(slot, dark);
|
|
44
|
+
// `rgb2` joined in issue #730, and it is this rule's subject rather than a
|
|
45
|
+
// new one's because it is the same tint with the light alpha left out:
|
|
46
|
+
// `RGB2Timeline` writes the light rgb and the dark colour, and on a slot
|
|
47
|
+
// with no dark colour it throws in `apply1` exactly as `RGBA2Timeline`
|
|
48
|
+
// does (measured on a forged file: `TypeError: null is not an object
|
|
49
|
+
// (evaluating 'dark.r = …')`). The one thing it does differently is the
|
|
50
|
+
// alpha it does NOT pose, which is `A45`'s question, not this one's.
|
|
51
|
+
const keyed: Array<{ anim: string; slot: string; timeline: 'rgba2' | 'rgb2'; keys: unknown[] }> = [];
|
|
52
|
+
for (const { animation: animName, slot: slotName, timelines } of facts.slotTimelines) {
|
|
53
|
+
for (const timeline of ['rgba2', 'rgb2'] as const) {
|
|
54
|
+
const keys = timelines[timeline];
|
|
55
|
+
if (Array.isArray(keys)) keyed.push({ anim: animName, slot: slotName, timeline, keys });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (declared.size === 0 && keyed.length === 0) {
|
|
59
|
+
return skip('A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN', SKIP_NO_TWO_COLOR_TINT);
|
|
60
|
+
}
|
|
61
|
+
stats.darkSlots = declared.size;
|
|
62
|
+
stats.rgba2Timelines = keyed.filter((t) => t.timeline === 'rgba2').length;
|
|
63
|
+
stats.rgb2Timelines = keyed.filter((t) => t.timeline === 'rgb2').length;
|
|
64
|
+
|
|
65
|
+
// -- clause 1: the setup pose ----------------------------------------
|
|
66
|
+
for (const [name, hex] of declared) {
|
|
67
|
+
const want = readDarkHex(hex);
|
|
68
|
+
const loaded = facts.loadedDark(name);
|
|
69
|
+
// ⚠️ "The parser kept nothing" is asked FIRST, and the order is the
|
|
70
|
+
// finding rather than a style: an empty string is both unreadable as a
|
|
71
|
+
// colour and dropped outright, and only the second sentence is about
|
|
72
|
+
// what the loaded skeleton holds. Asked the other way round, `""` was
|
|
73
|
+
// reported as "not six hex digits" — true, and about the file, on the
|
|
74
|
+
// one input where the file is not what went wrong.
|
|
75
|
+
if (loaded === null) {
|
|
76
|
+
fail(
|
|
77
|
+
'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN',
|
|
78
|
+
`slot "${name}" states dark ${JSON.stringify(hex)} and the loaded skeleton holds no dark colour for ` +
|
|
79
|
+
'it at all — the slot reader takes `dark` through a truthiness test, so a falsy value is dropped ' +
|
|
80
|
+
'in silence and the slot is tinted with one colour',
|
|
81
|
+
);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (want === null) {
|
|
85
|
+
fail(
|
|
86
|
+
'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN',
|
|
87
|
+
`slot "${name}" states dark ${JSON.stringify(hex)}, which is not six hex digits — the parser reads ` +
|
|
88
|
+
'fixed two-character slices and stores whatever `parseInt` returns, so the loaded colour is ' +
|
|
89
|
+
`(${show([loaded.r, loaded.g, loaded.b])}) rather than a failure`,
|
|
90
|
+
);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const found: [number, number, number] = [loaded.r, loaded.g, loaded.b];
|
|
94
|
+
if (found.some((n, i) => off(n, want[i]))) {
|
|
95
|
+
fail(
|
|
96
|
+
'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN',
|
|
97
|
+
`slot "${name}" states dark ${JSON.stringify(hex)} and the runtime loaded (${show(found)}), wanted ` +
|
|
98
|
+
`(${show(want)})`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// -- clauses 2 and 3: every rgba2 and rgb2 timeline --------------------
|
|
104
|
+
//
|
|
105
|
+
// ⚠️ Clause 2 poisons its whole ANIMATION, not only its own timeline: the
|
|
106
|
+
// throw happens inside `state.apply`, which applies every timeline of the
|
|
107
|
+
// animation at once, so posing a second — correct — `rgba2` timeline in the
|
|
108
|
+
// same animation would take this assertion down with a `threw:` line
|
|
109
|
+
// instead of the two named failures it has already worked out.
|
|
110
|
+
const cannotPose = new Set(keyed.filter((t) => !declared.has(t.slot)).map((t) => t.anim));
|
|
111
|
+
for (const { anim: animName, slot: slotName, timeline, keys } of keyed) {
|
|
112
|
+
const where = `animation "${animName}" slot "${slotName}" ${timeline}`;
|
|
113
|
+
if (!declared.has(slotName)) {
|
|
114
|
+
fail(
|
|
115
|
+
'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN',
|
|
116
|
+
`${where}: slot "${slotName}" declares no setup "dark", so the runtime allocates no dark colour for ` +
|
|
117
|
+
`it and applying this animation throws instead of tinting — give the slot a \`dark\`, or key ` +
|
|
118
|
+
`"${timeline === 'rgba2' ? 'rgba' : 'rgb'}"`,
|
|
119
|
+
);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (cannotPose.has(animName)) continue;
|
|
123
|
+
if (!facts.hasAnimation(animName)) {
|
|
124
|
+
fail('A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN', `${where}: the loaded skeleton has no animation "${animName}"`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
for (const rawKey of keys) {
|
|
128
|
+
if (!isObj(rawKey)) continue;
|
|
129
|
+
const time = typeof rawKey.time === 'number' ? rawKey.time : 0;
|
|
130
|
+
const wantLight = typeof rawKey.light === 'string' ? readLightHex(rawKey.light) : null;
|
|
131
|
+
const wantDark = typeof rawKey.dark === 'string' ? readDarkHex(rawKey.dark) : null;
|
|
132
|
+
// ⚠️ Posed BEFORE the key's own spelling is judged, so that a key whose
|
|
133
|
+
// hex cannot be read is still reported with the colour the runtime
|
|
134
|
+
// actually holds. `Color.setFromString` slices fixed offsets and stores
|
|
135
|
+
// whatever `parseInt` gives back, so the value found is the product
|
|
136
|
+
// here — "not six hex digits" alone would be the value REQUIRED twice
|
|
137
|
+
// over and the found value nowhere.
|
|
138
|
+
// At the key as the runtime stores it — see `atStoredKey` (#771).
|
|
139
|
+
const posed = facts.posedTint(animName, slotName, atStoredKey(time));
|
|
140
|
+
const light = posed?.light;
|
|
141
|
+
const dark = posed?.dark ?? null;
|
|
142
|
+
if (!light || dark === null) {
|
|
143
|
+
fail(
|
|
144
|
+
'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN',
|
|
145
|
+
`${where} (t=${time}): the posed skeleton has no ${light ? 'dark colour' : 'slot'} to read`,
|
|
146
|
+
);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const foundLight: [number, number, number, number] = [light.r, light.g, light.b, light.a];
|
|
150
|
+
const foundDark: [number, number, number] = [dark.r, dark.g, dark.b];
|
|
151
|
+
if (wantLight === null || wantDark === null) {
|
|
152
|
+
fail(
|
|
153
|
+
'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN',
|
|
154
|
+
`${where} (t=${time}): the key states light ${JSON.stringify(rawKey.light)} and dark ` +
|
|
155
|
+
`${JSON.stringify(rawKey.dark)} — an ${timeline} key needs both, each six or eight hex digits — and the ` +
|
|
156
|
+
`runtime poses light (${show(foundLight)}), dark (${show(foundDark)})`,
|
|
157
|
+
);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// An `rgb2` key's light colour is three channels, and the posed alpha
|
|
161
|
+
// is not its to state — `RGB2Timeline` leaves it where it was — so the
|
|
162
|
+
// comparison is over the channels the timeline writes. For `rgba2`
|
|
163
|
+
// that is all four, and the line reads as it always did.
|
|
164
|
+
const lightChannels = timeline === 'rgba2' ? 4 : 3;
|
|
165
|
+
if (foundLight.slice(0, lightChannels).some((n, i) => off(n, wantLight[i]))) {
|
|
166
|
+
fail(
|
|
167
|
+
'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN',
|
|
168
|
+
`${where} (t=${time}): light posed (${show(foundLight.slice(0, lightChannels))}), the key states ` +
|
|
169
|
+
`${JSON.stringify(rawKey.light)} = (${show(wantLight.slice(0, lightChannels))})`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (foundDark.some((n, i) => off(n, wantDark[i]))) {
|
|
173
|
+
fail(
|
|
174
|
+
'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN',
|
|
175
|
+
`${where} (t=${time}): dark posed (${show(foundDark)}), the key states ${JSON.stringify(rawKey.dark)} ` +
|
|
176
|
+
`= (${show(wantDark)})`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A46, the body (issue #1025, cut 4c-3 of step 4c of #380): a numbered series
|
|
3
|
+
* shows the frame the file states.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads, which is
|
|
6
|
+
* `SequenceFacts` (`../facts/sequences.ts`) where it read the skeleton JSON and
|
|
7
|
+
* the loaded skeleton — and for one change of key: what it knew about an
|
|
8
|
+
* attachment it used to file under the loaded attachment OBJECT, and now files
|
|
9
|
+
* under the entry's address (skin, slot, placeholder), the one key both sides
|
|
10
|
+
* hold. The runtime's side loads one object per address, so every map below
|
|
11
|
+
* holds what it held. The argument for the rule is above its `check` call,
|
|
12
|
+
* which stays in `validate()`.
|
|
13
|
+
*/
|
|
14
|
+
import type { Verdicts } from '../harness.ts';
|
|
15
|
+
import { entryAddress, type EntryAddress, type SequenceFacts } from '../facts/sequences.ts';
|
|
16
|
+
import { attachmentRegionLookups } from '../region_lookups.ts';
|
|
17
|
+
import { SKIP_NO_SEQUENCE } from '../reasons.ts';
|
|
18
|
+
import { atStoredKey, isObj } from '../values.ts';
|
|
19
|
+
import { SEQUENCE_MODES } from '../../timelines.ts';
|
|
20
|
+
|
|
21
|
+
export function a46SequenceAttachmentsShowTheFrameTheFileStates({ fail, skip, stats }: Verdicts, facts: SequenceFacts): void {
|
|
22
|
+
const NAME = 'A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES';
|
|
23
|
+
type Series = { where: string; lookups: string[] | null; count: number; setup: number };
|
|
24
|
+
/** The loaded attachment's address -> what the FILE says its series is. */
|
|
25
|
+
const series = new Map<EntryAddress, Series>();
|
|
26
|
+
/** The addresses the runtime loaded an attachment at, for the timelines to find. */
|
|
27
|
+
const loadedAt = new Set<EntryAddress>();
|
|
28
|
+
/** Attachments whose block was already refused above — their timelines say nothing more. */
|
|
29
|
+
const refused = new Set<EntryAddress>();
|
|
30
|
+
const whole = (v: unknown, fallback: number): number | null =>
|
|
31
|
+
v === undefined ? fallback : typeof v === 'number' && Number.isInteger(v) ? v : null;
|
|
32
|
+
let blocks = 0;
|
|
33
|
+
for (const entry of facts.entries) {
|
|
34
|
+
const at = entryAddress(entry.skin, entry.slot, entry.placeholder);
|
|
35
|
+
const loaded = entry.loaded;
|
|
36
|
+
const where = `skin ${JSON.stringify(entry.skin)} slot ${JSON.stringify(entry.slot)} attachment ${JSON.stringify(entry.placeholder)}`;
|
|
37
|
+
if (loaded) loadedAt.add(at);
|
|
38
|
+
if (entry.sequence === undefined || entry.sequence === null) continue;
|
|
39
|
+
blocks++;
|
|
40
|
+
const seq = entry.sequence;
|
|
41
|
+
const name = typeof entry.name === 'string' ? entry.name : entry.placeholder;
|
|
42
|
+
const path = typeof entry.path === 'string' ? entry.path : name;
|
|
43
|
+
const count = isObj(seq) ? whole(seq.count, 0) : null;
|
|
44
|
+
const setup = isObj(seq) ? whole(seq.setup, 0) : null;
|
|
45
|
+
if ((count === null || setup === null || count < 1) && loaded) refused.add(at);
|
|
46
|
+
if (count === null || setup === null || count < 1) {
|
|
47
|
+
fail(
|
|
48
|
+
NAME,
|
|
49
|
+
`${where}: the sequence states ${JSON.stringify(seq)}` +
|
|
50
|
+
(isObj(seq) && seq.count === undefined
|
|
51
|
+
? ' and no "count" — `readSequence` reads 0 (`SkeletonJson.js:644`), so the attachment loads holding no region and draws nothing'
|
|
52
|
+
: ' — "count" is a whole number of at least 1 and "setup" a whole number, or the series the parser builds is not the one written'),
|
|
53
|
+
);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if ((setup < 0 || setup >= count) && loaded) refused.add(at);
|
|
57
|
+
if (setup < 0 || setup >= count) {
|
|
58
|
+
fail(
|
|
59
|
+
NAME,
|
|
60
|
+
`${where}: the sequence's setup frame is ${setup} of a ${count}-frame series (frames 0 to ${count - 1}); ` +
|
|
61
|
+
'`Sequence.resolveIndex` clamps it, so the setup pose shows ' +
|
|
62
|
+
`${setup < 0 ? 'no frame at all' : `frame ${count - 1}, which the file does not name`}`,
|
|
63
|
+
);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!loaded) continue; // A00/A08 own an attachment that did not load
|
|
67
|
+
series.set(at, { where, lookups: attachmentRegionLookups(seq, path), count, setup });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The frame the file's statement gives for one key at one elapsed time —
|
|
72
|
+
* A46's prediction, not a call into the runtime. What holds it is the
|
|
73
|
+
* comparison it feeds: every sample sets it against the frame the posed
|
|
74
|
+
* slot shows, so a reading that disagreed with the runtime would fail A46
|
|
75
|
+
* on a correct file. A46 passing with samples posed on the selftest's
|
|
76
|
+
* series probes is that measurement for the modes they key; `M73` is its
|
|
77
|
+
* red half.
|
|
78
|
+
*/
|
|
79
|
+
const frameOf = (mode: string, index: number, elapsed: number, delay: number, count: number): number => {
|
|
80
|
+
if (mode === 'hold') return index;
|
|
81
|
+
let i = index + Math.trunc(elapsed / delay + 0.00001);
|
|
82
|
+
const n = count * 2 - 2;
|
|
83
|
+
switch (mode) {
|
|
84
|
+
case 'once':
|
|
85
|
+
return Math.min(count - 1, i);
|
|
86
|
+
case 'loop':
|
|
87
|
+
return i % count;
|
|
88
|
+
case 'pingpong':
|
|
89
|
+
i = n === 0 ? 0 : i % n;
|
|
90
|
+
return i >= count ? n - i : i;
|
|
91
|
+
case 'onceReverse':
|
|
92
|
+
return Math.max(count - 1 - i, 0);
|
|
93
|
+
case 'loopReverse':
|
|
94
|
+
return count - 1 - (i % count);
|
|
95
|
+
default: // pingpongReverse
|
|
96
|
+
i = n === 0 ? 0 : (i + count - 1) % n;
|
|
97
|
+
return i >= count ? n - i : i;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let timelines = 0;
|
|
102
|
+
let compared = 0;
|
|
103
|
+
let unshown = 0;
|
|
104
|
+
for (const { animation: animName, skin: skinName, slot: slotName, placeholder, keys } of facts.timelines) {
|
|
105
|
+
if (keys.length === 0) continue; // `readAnimation` skips it; A34 owns an empty timeline
|
|
106
|
+
timelines++;
|
|
107
|
+
const where = `animation ${JSON.stringify(animName)} ${skinName}/${slotName}/${placeholder} sequence`;
|
|
108
|
+
const keyed = entryAddress(skinName, slotName, placeholder);
|
|
109
|
+
if (!loadedAt.has(keyed)) continue; // the parser throws on a missing target: A00's
|
|
110
|
+
if (refused.has(keyed)) continue; // its block is already named above
|
|
111
|
+
const own = series.get(keyed);
|
|
112
|
+
if (own === undefined) {
|
|
113
|
+
fail(
|
|
114
|
+
NAME,
|
|
115
|
+
`${where}: the timeline steps an attachment that carries no "sequence" block. The parser gives it a ` +
|
|
116
|
+
'series of ONE region (`readSequence(null)` is `new Sequence(1, false)`), so every mode shows that ' +
|
|
117
|
+
'region at every time — measured: a "loop" key on a plain region showed it throughout',
|
|
118
|
+
);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// -- the keys, as the file states them --------------------------
|
|
122
|
+
type Key = { time: number; mode: string; index: number; delay: number };
|
|
123
|
+
const read: Key[] = [];
|
|
124
|
+
let carried = 0;
|
|
125
|
+
let malformed = false;
|
|
126
|
+
keys.forEach((rawKey, k) => {
|
|
127
|
+
const key = isObj(rawKey) ? rawKey : {};
|
|
128
|
+
const at = `${where} key ${k}`;
|
|
129
|
+
const time = typeof key.time === 'number' ? key.time : 0;
|
|
130
|
+
const mode = key.mode === undefined ? 'hold' : key.mode;
|
|
131
|
+
const index = key.index === undefined ? 0 : key.index;
|
|
132
|
+
if (key.delay !== undefined) carried = typeof key.delay === 'number' ? key.delay : Number.NaN;
|
|
133
|
+
if (typeof mode !== 'string' || !(SEQUENCE_MODES as readonly string[]).includes(mode)) {
|
|
134
|
+
malformed = true;
|
|
135
|
+
fail(
|
|
136
|
+
NAME,
|
|
137
|
+
`${at} (t=${time}): mode ${JSON.stringify(key.mode)} is not one of the ${SEQUENCE_MODES.length} the ` +
|
|
138
|
+
`format has (${SEQUENCE_MODES.join(', ')}); the parser reads \`SequenceMode[mode]\`, which is ` +
|
|
139
|
+
'undefined, stores mode bits 0, and the key plays as "hold"',
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (typeof index !== 'number' || !Number.isInteger(index) || index < 0 || index >= own.count) {
|
|
144
|
+
malformed = true;
|
|
145
|
+
fail(
|
|
146
|
+
NAME,
|
|
147
|
+
`${at} (t=${time}): index ${JSON.stringify(key.index)} is not a frame of the ${own.count}-frame series ` +
|
|
148
|
+
`(0 to ${own.count - 1}); the runtime stores \`index << 4\`, truncating a fraction, and ` +
|
|
149
|
+
'`Sequence.resolveIndex` clamps a frame past the end to the last one',
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (mode !== 'hold' && !(carried > 0)) {
|
|
154
|
+
malformed = true;
|
|
155
|
+
fail(
|
|
156
|
+
NAME,
|
|
157
|
+
`${at} (t=${time}): "${mode}" at a delay of ${String(carried)}${key.delay === undefined ? ' (carried from the key before, 0 on the first)' : ''} — ` +
|
|
158
|
+
'the frame advances by `(time - keyTime) / delay`, and at 0 that is Infinity, `Infinity | 0` is 0, ' +
|
|
159
|
+
'and the key shows its first frame throughout: "hold" spelt as another mode',
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
read.push({ time, mode, index, delay: carried });
|
|
164
|
+
});
|
|
165
|
+
if (malformed) continue;
|
|
166
|
+
|
|
167
|
+
// -- the pose, sampled ------------------------------------------
|
|
168
|
+
const end = facts.duration(animName);
|
|
169
|
+
if (end === null || !facts.hasSlot(slotName)) continue; // A00's
|
|
170
|
+
/** `key` is the index into `read`, or -1 before the first key (the setup frame). */
|
|
171
|
+
const samples: Array<{ time: number; key: number; steps: number }> = [];
|
|
172
|
+
if (read[0].time > 0) samples.push({ time: read[0].time / 2, key: -1, steps: 0 });
|
|
173
|
+
read.forEach((key, k) => {
|
|
174
|
+
const until = k + 1 < read.length ? read[k + 1].time : end;
|
|
175
|
+
if (key.mode === 'hold') {
|
|
176
|
+
samples.push({ time: key.time, key: k, steps: 0 });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Mid-frame, and enough steps to wrap every mode at least once.
|
|
180
|
+
for (let step = 0; step < own.count * 2 + 2; step++) {
|
|
181
|
+
const time = key.time + (step + 0.5) * key.delay;
|
|
182
|
+
if (time >= until || time > end) break;
|
|
183
|
+
samples.push({ time, key: k, steps: step + 0.5 });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
let shownHere = 0;
|
|
187
|
+
for (const sample of samples) {
|
|
188
|
+
// A `hold` sample is AT its key, so it is posed at the key as
|
|
189
|
+
// the runtime stores it (`atStoredKey`); a mid-frame sample is
|
|
190
|
+
// half a delay from any key and is posed where it is.
|
|
191
|
+
const pose = facts.posedFrame(animName, slotName, sample.key >= 0 && sample.steps === 0 ? atStoredKey(sample.time) : sample.time);
|
|
192
|
+
// The slot must show the keyed attachment or one playing its
|
|
193
|
+
// timelines — the only case `applyToSlot` writes.
|
|
194
|
+
if (pose === null || (pose.shown !== keyed && pose.playsAs !== keyed)) continue;
|
|
195
|
+
const drawn = series.get(pose.shown);
|
|
196
|
+
if (drawn === undefined || drawn.lookups === null) continue;
|
|
197
|
+
shownHere++;
|
|
198
|
+
compared++;
|
|
199
|
+
// The frame count the runtime folds by is the SHOWN attachment's:
|
|
200
|
+
// a link with a series of its own steps it by its source's keys.
|
|
201
|
+
const key = sample.key < 0 ? null : read[sample.key];
|
|
202
|
+
const want = key === null ? drawn.setup : frameOf(key.mode, key.index, sample.time - key.time, key.delay, drawn.count);
|
|
203
|
+
const region = pose.region;
|
|
204
|
+
if (region !== drawn.lookups[want]) {
|
|
205
|
+
fail(
|
|
206
|
+
NAME,
|
|
207
|
+
`${where} (t=${Number(sample.time.toFixed(4))}): the slot shows region ${JSON.stringify(region)}, and ` +
|
|
208
|
+
`the file states frame ${want} of ${drawn.count} — ${JSON.stringify(drawn.lookups[want])} — ` +
|
|
209
|
+
(key === null
|
|
210
|
+
? 'the setup frame, before the first key'
|
|
211
|
+
: `key ${sample.key} plays "${key.mode}" from frame ${key.index} every ${key.delay}s, ` +
|
|
212
|
+
`${sample.steps} delay(s) in`),
|
|
213
|
+
);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (shownHere === 0) unshown++;
|
|
218
|
+
}
|
|
219
|
+
if (blocks === 0 && timelines === 0) return skip(NAME, SKIP_NO_SEQUENCE);
|
|
220
|
+
stats.sequenceBlocks = blocks;
|
|
221
|
+
stats.sequenceTimelines = timelines;
|
|
222
|
+
stats.sequenceSamples = compared;
|
|
223
|
+
if (unshown > 0) stats.sequenceSamplesUnshown = unshown;
|
|
224
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The animations and how long each one runs (issue #1025, cut 4c-3 of step
|
|
3
|
+
* 4c of #380) — the census's F18 and F19 as A09 reads them: the animation
|
|
4
|
+
* roster in the file's order, each animation's duration as the runtime holds
|
|
5
|
+
* it, and each of its timelines' own duration.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ **The runtime's durations, not the stated ones.** An animation's duration
|
|
8
|
+
* is the last key of every timeline it carries, as float32 — the number
|
|
9
|
+
* `SkeletonJson` computes and stores, which is what A09 holds against the
|
|
10
|
+
* declared duration — and a timeline's own duration is its last key, as
|
|
11
|
+
* float32 (`Timeline.getDuration`). A09 counts the timelines keyed past the
|
|
12
|
+
* declared duration in its message, so the list holds one entry per timeline
|
|
13
|
+
* the runtime builds, in no order a body may read: A09 counts them and
|
|
14
|
+
* nothing else.
|
|
15
|
+
*
|
|
16
|
+
* The order of the roster is the file's: A09's last clause prints one line per
|
|
17
|
+
* animation the spec does not declare, in it.
|
|
18
|
+
*
|
|
19
|
+
* Links nothing from the runtime: `validate()` reads it off the loaded
|
|
20
|
+
* skeleton (`spineAnimationDurations`), the model side off the document and
|
|
21
|
+
* the core (`../model/animation_durations.ts`).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** One animation, as A09 reads it. */
|
|
25
|
+
export interface AnimationDuration {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/** The runtime's duration: the last key of every timeline, float32. */
|
|
28
|
+
readonly duration: number;
|
|
29
|
+
/** Each timeline's own duration — its last key, float32 — one per timeline the runtime builds. */
|
|
30
|
+
readonly timelineDurations: readonly number[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What A09 reads. */
|
|
34
|
+
export interface AnimationDurationFacts {
|
|
35
|
+
/** Every animation the skeleton holds, in the file's order. */
|
|
36
|
+
readonly animations: readonly AnimationDuration[];
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every animation's bone timelines, in the file's order and spelling (issue
|
|
3
|
+
* #1025, cut 4c-4 of #380) — the half of the census's F18/F19 that the stroke
|
|
4
|
+
* rules read: which bones each animation keys, with which timelines, and each
|
|
5
|
+
* key as the file states it.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ **The file's order, and the file's spelling**, for the reason A45's slot
|
|
8
|
+
* timelines carry both (`./slot_colour.ts`): A24 and A30 print one line per
|
|
9
|
+
* bone and per timeline in the order the file keys them, and A24, A29 and A30
|
|
10
|
+
* print a key's `x`, `y` and `time` as the file states them. So the list holds,
|
|
11
|
+
* for every animation in the order the file keys them and every bone in the
|
|
12
|
+
* order the animation keys them, that bone's timelines — name to keys — as
|
|
13
|
+
* the file spells them. `validate()` reads it off the skeleton JSON
|
|
14
|
+
* (`rawBoneTimelines`); the model side produces it from the document by calling
|
|
15
|
+
* the emitter's own order and its parser-default and key-order passes
|
|
16
|
+
* (`../model/bone_timelines.ts`), so a key field the emitter would leave out at
|
|
17
|
+
* the parser's default is left out here too.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ `timelines` is `unknown` rather than an object because the raw JSON can
|
|
20
|
+
* hold anything there and A24 counts a keyed bone before it asks what the
|
|
21
|
+
* bone's timelines are; the document always holds an object.
|
|
22
|
+
*
|
|
23
|
+
* Links nothing from the runtime.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** One animation's timelines on one bone: timeline name -> keys, as the file states them. */
|
|
27
|
+
export interface BoneTimelines {
|
|
28
|
+
readonly animation: string;
|
|
29
|
+
readonly bone: string;
|
|
30
|
+
readonly timelines: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What A24, A29 and A30 read. */
|
|
34
|
+
export interface BoneTimelineFacts {
|
|
35
|
+
/** Every (animation, bone) pair the file keys, in the header's order. */
|
|
36
|
+
readonly boneTimelines: readonly BoneTimelines[];
|
|
37
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The constraint timelines of every animation, as A34 walks them (issue
|
|
3
|
+
* #1025, cut 4c-5 of step 4c of #380): the constraints the skeleton declares,
|
|
4
|
+
* and each animation's `ik`, `transform`, `path`, `physics` and `slider`
|
|
5
|
+
* groups as the Spine file states them — the constraint each entry names and
|
|
6
|
+
* the key arrays under it.
|
|
7
|
+
*
|
|
8
|
+
* ⭐ **The order is the file's.** The animations in the order the file keys
|
|
9
|
+
* them, in each the five groups in that fixed order, each group's entries in
|
|
10
|
+
* the order the file keys them, each entry's timelines likewise: A34 prints
|
|
11
|
+
* one line per finding, so the walk is a fact both suppliers state.
|
|
12
|
+
*
|
|
13
|
+
* 🔸 **Which constraints a physics timeline naming none reaches is a
|
|
14
|
+
* question, not a value** — the runtime answers it by building the timeline
|
|
15
|
+
* its parser builds for the name and asking each constraint's data whether it
|
|
16
|
+
* takes the key (`unnamedPhysicsReach`), the model side by the core's own
|
|
17
|
+
* reading (`posedPhysics`, through `unnamedReach`). A body asks for it
|
|
18
|
+
* (`reach`), and the selftest puts every question to both (`VF14`).
|
|
19
|
+
*
|
|
20
|
+
* Links nothing from the runtime.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One key array under an entry: the timeline's name (`''` for the `ik`/`transform` shape) and what it holds — how many keys, or the spelling of a value that is not a list. */
|
|
24
|
+
export interface TargetKeyArray {
|
|
25
|
+
readonly timeline: string;
|
|
26
|
+
readonly keys: { readonly count: number } | { readonly spelled: string };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One entry of a group: the constraint name the file keys it by, and either the spelling of a value that is not an object where named timelines go (`bare`) or its key arrays. */
|
|
30
|
+
export interface TargetEntry {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly bare: string | null;
|
|
33
|
+
readonly keyArrays: readonly TargetKeyArray[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One animation: its name and the groups it states, in the walk's order. */
|
|
37
|
+
export interface TargetAnimation {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly groups: ReadonlyArray<{ readonly group: 'ik' | 'transform' | 'path' | 'physics' | 'slider'; readonly entries: readonly TargetEntry[] }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What a physics timeline naming no constraint reaches: whether it is a `reset`, and the constraints that take its key. */
|
|
43
|
+
export interface UnnamedReach {
|
|
44
|
+
readonly resets: boolean;
|
|
45
|
+
readonly reached: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** What A34 reads. */
|
|
49
|
+
export interface ConstraintTargetFacts {
|
|
50
|
+
/** Every entry of the skeleton's `constraints` array that is an object: its name (null when it states none as a string), its type as spelled, and its name as the sentence spells it. */
|
|
51
|
+
readonly constraints: ReadonlyArray<{ readonly name: string | null; readonly type: string; readonly spelled: string }>;
|
|
52
|
+
/** Every animation, or `null` when the skeleton declares none (its `animations` is not an object). */
|
|
53
|
+
readonly groupsByAnimation: readonly TargetAnimation[] | null;
|
|
54
|
+
/** What a physics timeline named `timeline` that names no constraint reaches — or `null` for a name the parser builds no timeline for. */
|
|
55
|
+
reach(timeline: string): UnnamedReach | null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deform survey (issue #1025, cut 4c-3 of step 4c of #380) — the
|
|
3
|
+
* census's R6 and the structure under it, as A39 reads them: every deform key
|
|
4
|
+
* of every animation measured in the frame its animation is reached in, every
|
|
5
|
+
* span between two keys scanned, and what was passed over.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ **One fact, because it is one measurement.** The survey is what
|
|
8
|
+
* `explain`'s `DEFORM` block prints whole and A39 gates on, and the reason
|
|
9
|
+
* both read one survey rather than each posing for itself is that the
|
|
10
|
+
* report's reversal count and A39's are the same count
|
|
11
|
+
* (`src/deformmeasure.ts`'s header). So A39 is handed the survey, not the
|
|
12
|
+
* poses under it: `validate()` hands it spine-core's — the Spine skeleton read
|
|
13
|
+
* and posed by the runtime (`surveyDeformKeys`) — and the model side the core's
|
|
14
|
+
* — the model document's structure posed by rigc's core (`surveyOfModel`,
|
|
15
|
+
* `src/deformsurvey.ts`). `tools/survey_hashes.ts` holds the two to one survey
|
|
16
|
+
* on every corpus row, and the selftest on every call with a model in hand
|
|
17
|
+
* (`VF12`).
|
|
18
|
+
*
|
|
19
|
+
* Links nothing from the runtime.
|
|
20
|
+
*/
|
|
21
|
+
import type { DeformSurvey } from '../../deformsurvey.ts';
|
|
22
|
+
|
|
23
|
+
/** What A39 reads. */
|
|
24
|
+
export interface DeformSurveyFacts {
|
|
25
|
+
/** The survey, with the slots in `exempt` passed over unmeasured (the rig's `deformMayFold`). */
|
|
26
|
+
survey(exempt: ReadonlySet<string>): DeformSurvey;
|
|
27
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every event key, in the file's order, with the declaration it fires (issue
|
|
3
|
+
* #1025, cut 4c-4 of #380) — what A32 reads.
|
|
4
|
+
*
|
|
5
|
+
* ⭐ **Split per clause, and the split is in this interface.** A32 has five
|
|
6
|
+
* clauses. Four of them — a key with no string `name`, a key firing an event
|
|
7
|
+
* the skeleton does not declare, a key whose time is not a finite number, and
|
|
8
|
+
* a key earlier than the one before it — are states `readModel` refuses by
|
|
9
|
+
* name (`readEventKeys` in `src/core/events.ts`, measured on forged documents:
|
|
10
|
+
* each one is refused at the key's address), so no readable document reaches
|
|
11
|
+
* them and they stay with the round trip: `validate()` runs them over the
|
|
12
|
+
* skeleton JSON and hands their findings in as `kept`, in the order they
|
|
13
|
+
* print, with `stopped` where the clause ended the key's reading. The fifth —
|
|
14
|
+
* `volume` or `balance` on a key whose event declares no audio — is the rig's:
|
|
15
|
+
* the document holds the key's fields and the declaration's `audio`, and a
|
|
16
|
+
* readable document can carry it. That clause is the body's
|
|
17
|
+
* (`../bodies/a32.ts`), and the model side supplies `kept` empty.
|
|
18
|
+
*
|
|
19
|
+
* The order is the file's: animations in the order the file keys them (the
|
|
20
|
+
* emitter's `editorAnimationOrder`), and an animation's keys in the order the
|
|
21
|
+
* document lists them, which is the order the emitter writes them
|
|
22
|
+
* (`emitAnimation`'s `events`). `timelines` counts the animations carrying an
|
|
23
|
+
* event timeline at all, which is what A32 SKIPs on when it is zero.
|
|
24
|
+
*
|
|
25
|
+
* Links nothing from the runtime.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** One event key: where it is, what the kept clauses found, and what the moved clause reads. */
|
|
29
|
+
export interface EventKeyEntry {
|
|
30
|
+
readonly animation: string;
|
|
31
|
+
/** The key's index in its animation's event timeline. */
|
|
32
|
+
readonly index: number;
|
|
33
|
+
/** The findings of the clauses that stay with the round trip, in the order they print — always empty on the model side. */
|
|
34
|
+
readonly kept: readonly string[];
|
|
35
|
+
/** Whether a kept clause ended the key's reading, so the moved clause does not run on it. */
|
|
36
|
+
readonly stopped: boolean;
|
|
37
|
+
/** The event the key fires. */
|
|
38
|
+
readonly name: string;
|
|
39
|
+
/** Which of the two audio fields the key states. */
|
|
40
|
+
readonly sets: { readonly volume: boolean; readonly balance: boolean };
|
|
41
|
+
/** Whether the event the key fires declares an audio path (a string). */
|
|
42
|
+
readonly audio: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What A32 reads. */
|
|
46
|
+
export interface EventKeyFacts {
|
|
47
|
+
/** Whether the skeleton JSON parsed — always true on the model side, whose parse is the reader's. */
|
|
48
|
+
readonly parsed: boolean;
|
|
49
|
+
/** Whether the skeleton declares an `animations` object at all. */
|
|
50
|
+
readonly animations: boolean;
|
|
51
|
+
/** How many animations carry an event timeline. */
|
|
52
|
+
readonly timelines: number;
|
|
53
|
+
/** Every event key, in the file's order. */
|
|
54
|
+
readonly keys: readonly EventKeyEntry[];
|
|
55
|
+
}
|