rig-c 0.0.0-stage → 2.20.4
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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -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/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
package/src/motion.ts
ADDED
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a motion spec — the parse the motion spec did not have.
|
|
3
|
+
*
|
|
4
|
+
* The rig spec has had `parseRigSpec` since it stopped being three hard-coded
|
|
5
|
+
* tables; the motion spec reached `compile` as `readJson<MotionSpec>(path)`, a
|
|
6
|
+
* CAST, so its declared type said what a correct file holds and not what the one
|
|
7
|
+
* on disk does. Issue #307. The compiler's own comment named the consequence out
|
|
8
|
+
* loud, at the one field that had since grown a guard of its own (#293/#303):
|
|
9
|
+
* `setup: { "lid_l": "plate" }` — the attachment name written where its wrapper
|
|
10
|
+
* belongs — compiled **green** and hid the slot, which is the opposite of what
|
|
11
|
+
* was asked and is stated nowhere. That guard is now here, and it now covers
|
|
12
|
+
* every key in the table rather than the ones the emit loop happened to reach.
|
|
13
|
+
*
|
|
14
|
+
* ## What lives here, and what stays in `compile`
|
|
15
|
+
*
|
|
16
|
+
* The split is **shape versus meaning**, and it is a split about what each layer
|
|
17
|
+
* can see rather than a ranking of the checks:
|
|
18
|
+
*
|
|
19
|
+
* - **here** — is this a number, a string, an array, an object; is a required
|
|
20
|
+
* field present; is a structure the structure the format describes. Answerable
|
|
21
|
+
* from the motion file ALONE, which is why it can run at load and why the
|
|
22
|
+
* rest of the compiler is allowed to assume its inputs from then on.
|
|
23
|
+
* - **in `compile`** — does this name resolve against the rig spec, is this bone
|
|
24
|
+
* in that group, does this key's value have the right number of channels for
|
|
25
|
+
* its property, does this `derive` have a projection onto this axis, is the
|
|
26
|
+
* last key allowed to carry an easing. Every one of those needs something the
|
|
27
|
+
* motion file does not contain: the rig, the property table, or the key's
|
|
28
|
+
* position in its own track.
|
|
29
|
+
*
|
|
30
|
+
* 🚨 **The failure this split is drawn to avoid is two layers refusing one thing
|
|
31
|
+
* under two names**, which makes the error output contradict itself. So a guard
|
|
32
|
+
* the parser makes unreachable was DELETED from `compile` rather than left as a
|
|
33
|
+
* second opinion — the version tag, the `setup` entry shape, the three
|
|
34
|
+
* `non-finite time` guards, the `ik`/`transform`/`deform` array-and-name guards.
|
|
35
|
+
* Where a compile guard is still reachable it stays: `checkKeyTime` still catches
|
|
36
|
+
* a key genuinely past its duration, `rgbaHex` still counts the channels of an
|
|
37
|
+
* `rgba` track key, and the group `v`-map / `derive` refusals (#320,
|
|
38
|
+
* `src/trackgen.ts`) are untouched — every one of them reads the group's member
|
|
39
|
+
* list or the property's projection table, neither of which is in this file.
|
|
40
|
+
*
|
|
41
|
+
* ## Unknown keys ARE refused — issue #545
|
|
42
|
+
*
|
|
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.
|
|
55
|
+
*/
|
|
56
|
+
import { CompileError } from './errors.ts';
|
|
57
|
+
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
58
|
+
import type { ShapeVisit, SpecEnumTable, SpecValueType } from './keys.ts';
|
|
59
|
+
import { SEQUENCE_MODES } from './timelines.ts';
|
|
60
|
+
import type { MotionSpec } from './types.ts';
|
|
61
|
+
|
|
62
|
+
export const MOTION_SPEC_VERSION = 'rigc-motion/1';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A track's `physics` target for the timeline that names NO constraint (issue
|
|
66
|
+
* #726) — the one the runtime applies to every physics constraint whose own data
|
|
67
|
+
* declares the keyed property global (`"strengthGlobal": true` for `strength`,
|
|
68
|
+
* and so on; `reset` resets every physics constraint and asks no flag).
|
|
69
|
+
* `compile` emits it under the empty name, which is the skeleton file's own
|
|
70
|
+
* spelling of it (`SkeletonJson.js:1048-1054`).
|
|
71
|
+
*
|
|
72
|
+
* 🔑 Not the empty string itself, and that is the choice rather than a detail:
|
|
73
|
+
* `""` is the likeliest shape of a value somebody forgot to fill in, and a
|
|
74
|
+
* forgotten target that silently became "every global constraint" is the exact
|
|
75
|
+
* silence this format exists to name. So `"physics": ""` is refused by name and
|
|
76
|
+
* points here, and `"*"` is reserved the other way round: `compile` refuses a
|
|
77
|
+
* physics constraint that is CALLED `"*"`, because a track naming it could not
|
|
78
|
+
* say which of the two it meant.
|
|
79
|
+
*/
|
|
80
|
+
export const EVERY_GLOBAL_PHYSICS = '*';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The six fields that pick a track's target family. Listed here as well as in
|
|
84
|
+
* `compile`'s `resolveTargets` because the two ask different questions of it:
|
|
85
|
+
* this one asks whether each is a string, that one asks whether exactly one is
|
|
86
|
+
* present and what it resolves to.
|
|
87
|
+
*/
|
|
88
|
+
const TARGET_FIELDS = ['slot', 'group', 'bone', 'physics', 'path', 'slider'] as const;
|
|
89
|
+
|
|
90
|
+
/** Every numeric field of a `physics` table entry — `bone` and `note` are not numbers. */
|
|
91
|
+
const PHYSICS_NUMBERS = [
|
|
92
|
+
'x',
|
|
93
|
+
'y',
|
|
94
|
+
'rotate',
|
|
95
|
+
'scaleX',
|
|
96
|
+
'shearX',
|
|
97
|
+
'inertia',
|
|
98
|
+
'strength',
|
|
99
|
+
'damping',
|
|
100
|
+
'mass',
|
|
101
|
+
'wind',
|
|
102
|
+
'gravity',
|
|
103
|
+
'mix',
|
|
104
|
+
'fps',
|
|
105
|
+
'limit',
|
|
106
|
+
] as const;
|
|
107
|
+
|
|
108
|
+
/** The two animation-level constraint families, which share one entry shape. */
|
|
109
|
+
const CONSTRAINT_GROUPS = ['ik', 'transform'] as const;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Every key each shape of this format owns, keyed by the interface that declares
|
|
113
|
+
* it — the runtime shadow of types TypeScript erases, and the other half of
|
|
114
|
+
* `RIG_KEYS` in [`rig.ts`](rig.ts).
|
|
115
|
+
*
|
|
116
|
+
* 🔒 Held to those interfaces by `CUR17` in `selftest.ts`, which reads the
|
|
117
|
+
* declaring source and compares. The interfaces are spread over three modules —
|
|
118
|
+
* `types.ts` for the format, [`trackgen.ts`](trackgen.ts) for a track's `derive`
|
|
119
|
+
* and [`deformgen.ts`](deformgen.ts) for a deform key's `transform` — and the
|
|
120
|
+
* table is one table anyway, because what it describes is one file.
|
|
121
|
+
*
|
|
122
|
+
* ⚠️ `MotionKey.v` is deliberately not a shape here. On a group track it is a
|
|
123
|
+
* `MotionMemberValues` map keyed by **member name**, so every key of it is a
|
|
124
|
+
* name from the rig rather than a field of this format; `resolveMemberTrack`
|
|
125
|
+
* refuses a member the group does not have, which is the check that fits.
|
|
126
|
+
*/
|
|
127
|
+
export const MOTION_KEYS = {
|
|
128
|
+
MotionSpec: ['spec', 'archetype', 'cut', 'note', 'easings', 'groups', 'setup', 'physics', 'animations', 'mix'],
|
|
129
|
+
MotionMix: ['default', 'pairs'],
|
|
130
|
+
MotionSetupSlot: ['attachment', 'color'],
|
|
131
|
+
MotionPhysics: [
|
|
132
|
+
'bone', 'x', 'y', 'rotate', 'scaleX', 'shearX', 'inertia', 'strength', 'damping', 'mass', 'wind', 'gravity',
|
|
133
|
+
'mix', 'fps', 'limit', 'note',
|
|
134
|
+
],
|
|
135
|
+
MotionAnimation: ['duration', 'loop', 'note', 'tracks', 'ik', 'transform', 'deform', 'sequence', 'drawOrder', 'events'],
|
|
136
|
+
MotionTrack: ['slot', 'group', 'bone', 'physics', 'path', 'slider', 'property', 'lag', 'stagger', 'keys'],
|
|
137
|
+
MotionKey: ['t', 'v', 'derive', 'ease', 'curve'],
|
|
138
|
+
MotionIkTrack: ['constraint', 'keys'],
|
|
139
|
+
MotionIkKey: ['t', 'mix', 'softness', 'bendPositive', 'compress', 'stretch', 'ease', 'curve'],
|
|
140
|
+
MotionTransformTrack: ['constraint', 'keys'],
|
|
141
|
+
MotionTransformKey: ['t', 'mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY', 'ease', 'curve'],
|
|
142
|
+
MotionDeformTrack: ['skin', 'slot', 'attachment', 'keys'],
|
|
143
|
+
MotionDeformKey: ['t', 'offset', 'fromVertex', 'vertices', 'transform', 'ease', 'curve'],
|
|
144
|
+
MotionSequenceTrack: ['skin', 'slot', 'attachment', 'keys'],
|
|
145
|
+
MotionSequenceKey: ['t', 'mode', 'index', 'delay'],
|
|
146
|
+
MotionDrawOrderKey: ['t', 'offsets'],
|
|
147
|
+
MotionDrawOrderOffset: ['slot', 'offset'],
|
|
148
|
+
MotionEventKey: ['t', 'name', 'int', 'float', 'string', 'volume', 'balance'],
|
|
149
|
+
TrackDeriveTurn: ['kind', 'degrees', 'depth', 'carried', 'about'],
|
|
150
|
+
DeformTurn: ['kind', 'radius', 'depth', 'degrees', 'about'],
|
|
151
|
+
DeformAffine: ['kind', 'scale', 'about'],
|
|
152
|
+
DeformWave: ['kind', 'amplitude', 'wavelength', 'phase', 'along', 'axis'],
|
|
153
|
+
DeformBend: ['kind', 'amount', 'from', 'to', 'power', 'along', 'axis'],
|
|
154
|
+
} as const satisfies Record<string, readonly string[]>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The type every key of `MOTION_KEYS` holds, shape by shape — what
|
|
158
|
+
* `refuseValuesOfTheWrongType` refuses a value against (issue #890), held to
|
|
159
|
+
* the key table by `satisfies MotionTypeTable` and by the selftest, exactly as
|
|
160
|
+
* `RIG_TYPES` is.
|
|
161
|
+
*
|
|
162
|
+
* ⚠️ The walk runs LAST in `parseMotionSpec`, after every field check above,
|
|
163
|
+
* so a field this file already checks keeps its own sentence (a physics tuning
|
|
164
|
+
* number, a key's `t`, an easing's handles) and the walk is what covers the
|
|
165
|
+
* fields nothing here reads — an ik key's `mix`, a deform key's `offset`, an
|
|
166
|
+
* event key's `float`, a derive's `degrees`.
|
|
167
|
+
*
|
|
168
|
+
* Where an unchecked type is refused: an `enum` row — the entry `MOTION_ENUMS`
|
|
169
|
+
* has for it, below, which is a table a control reads rather than a list here;
|
|
170
|
+
* `MotionKey.v`, a key's `curve`, a derive's `depth` and `mix.pairs` (`mixed`)
|
|
171
|
+
* — the track compiler by property, the curve reader, the derive evaluator and
|
|
172
|
+
* `parseMix`. A sequence key's `mode` is a closed set too, but its interface
|
|
173
|
+
* declares it `string` and so does this row: `parseSequence` refuses a name
|
|
174
|
+
* outside `SEQUENCE_MODES` before the walk runs, and a non-string with it.
|
|
175
|
+
*/
|
|
176
|
+
type MotionTypeTable = {
|
|
177
|
+
readonly [S in keyof typeof MOTION_KEYS]: { readonly [K in (typeof MOTION_KEYS)[S][number]]: SpecValueType };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const MOTION_TYPES = {
|
|
181
|
+
MotionSpec: {
|
|
182
|
+
spec: 'enum', archetype: 'string', cut: 'string', note: 'string', easings: 'map of number[]', groups: 'map of string[]',
|
|
183
|
+
setup: 'map of object', physics: 'map of object', animations: 'map of object', mix: 'object',
|
|
184
|
+
},
|
|
185
|
+
MotionMix: { default: 'number', pairs: 'mixed' },
|
|
186
|
+
MotionSetupSlot: { attachment: 'string | null', color: 'number[]' },
|
|
187
|
+
MotionPhysics: {
|
|
188
|
+
bone: 'string', x: 'number', y: 'number', rotate: 'number', scaleX: 'number', shearX: 'number', inertia: 'number',
|
|
189
|
+
strength: 'number', damping: 'number', mass: 'number', wind: 'number', gravity: 'number', mix: 'number',
|
|
190
|
+
fps: 'number', limit: 'number', note: 'string',
|
|
191
|
+
},
|
|
192
|
+
MotionAnimation: {
|
|
193
|
+
duration: 'number', loop: 'boolean', note: 'string', tracks: 'object[]', ik: 'object[]', transform: 'object[]',
|
|
194
|
+
deform: 'object[]', sequence: 'object[]', drawOrder: 'object[]', events: 'object[]',
|
|
195
|
+
},
|
|
196
|
+
MotionTrack: {
|
|
197
|
+
slot: 'string', group: 'string', bone: 'string', physics: 'string', path: 'string', slider: 'string',
|
|
198
|
+
property: 'enum', lag: 'number', stagger: 'number', keys: 'object[]',
|
|
199
|
+
},
|
|
200
|
+
MotionKey: { t: 'number', v: 'mixed', derive: 'object', ease: 'string', curve: 'mixed' },
|
|
201
|
+
MotionIkTrack: { constraint: 'string', keys: 'object[]' },
|
|
202
|
+
MotionIkKey: {
|
|
203
|
+
t: 'number', mix: 'number', softness: 'number', bendPositive: 'boolean', compress: 'boolean', stretch: 'boolean',
|
|
204
|
+
ease: 'string', curve: 'mixed',
|
|
205
|
+
},
|
|
206
|
+
MotionTransformTrack: { constraint: 'string', keys: 'object[]' },
|
|
207
|
+
MotionTransformKey: {
|
|
208
|
+
t: 'number', mixRotate: 'number', mixX: 'number', mixY: 'number', mixScaleX: 'number', mixScaleY: 'number',
|
|
209
|
+
mixShearY: 'number', ease: 'string', curve: 'mixed',
|
|
210
|
+
},
|
|
211
|
+
MotionDeformTrack: { skin: 'string', slot: 'string', attachment: 'string', keys: 'object[]' },
|
|
212
|
+
MotionDeformKey: {
|
|
213
|
+
t: 'number', offset: 'number', fromVertex: 'number', vertices: 'number[] | null', transform: 'object', ease: 'string', curve: 'mixed',
|
|
214
|
+
},
|
|
215
|
+
MotionSequenceTrack: { skin: 'string', slot: 'string', attachment: 'string', keys: 'object[]' },
|
|
216
|
+
MotionSequenceKey: { t: 'number', mode: 'string', index: 'number', delay: 'number' },
|
|
217
|
+
MotionDrawOrderKey: { t: 'number', offsets: 'object[]' },
|
|
218
|
+
MotionDrawOrderOffset: { slot: 'string', offset: 'number' },
|
|
219
|
+
MotionEventKey: {
|
|
220
|
+
t: 'number', name: 'string', int: 'number', float: 'number', string: 'string', volume: 'number', balance: 'number',
|
|
221
|
+
},
|
|
222
|
+
TrackDeriveTurn: { kind: 'enum', degrees: 'number', depth: 'mixed', carried: 'number', about: 'number' },
|
|
223
|
+
DeformTurn: { kind: 'enum', radius: 'number', depth: 'boolean', degrees: 'number', about: 'number' },
|
|
224
|
+
DeformAffine: { kind: 'enum', scale: 'number[]', about: 'number[]' },
|
|
225
|
+
DeformWave: { kind: 'enum', amplitude: 'number', wavelength: 'number', phase: 'number', along: 'enum', axis: 'enum' },
|
|
226
|
+
DeformBend: { kind: 'enum', amount: 'number', from: 'number', to: 'number', power: 'number', along: 'enum', axis: 'enum' },
|
|
227
|
+
} as const satisfies MotionTypeTable;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Who refuses each `enum` row of `MOTION_TYPES` outside its set (issue #900);
|
|
231
|
+
* `SpecEnumTable` in [`keys.ts`](keys.ts) says what an entry means. Every one
|
|
232
|
+
* is an owner: measured when this table was written, a planted `5` and `"foo"`
|
|
233
|
+
* on each row was refused by name by the reader named here, listing the names
|
|
234
|
+
* that exist, so none of them states a set. A deform transform's and a derive's
|
|
235
|
+
* `kind` choose the row they are checked against, as a generator's does.
|
|
236
|
+
*/
|
|
237
|
+
export const MOTION_ENUMS = {
|
|
238
|
+
MotionSpec: { spec: { owner: 'parseMotionSpecInto' } },
|
|
239
|
+
MotionTrack: { property: { owner: 'compileTrack' } },
|
|
240
|
+
TrackDeriveTurn: { kind: { owner: 'evaluateTrackDerive' } },
|
|
241
|
+
DeformTurn: { kind: { owner: 'evaluateDeformTransform' } },
|
|
242
|
+
DeformAffine: { kind: { owner: 'evaluateDeformTransform' } },
|
|
243
|
+
DeformWave: { kind: { owner: 'evaluateDeformTransform' }, along: { owner: 'evaluateDeformTransform' }, axis: { owner: 'evaluateDeformTransform' } },
|
|
244
|
+
DeformBend: { kind: { owner: 'evaluateDeformTransform' }, along: { owner: 'evaluateDeformTransform' }, axis: { owner: 'evaluateDeformTransform' } },
|
|
245
|
+
} as const satisfies SpecEnumTable<typeof MOTION_TYPES>;
|
|
246
|
+
|
|
247
|
+
/** A deform key's `transform` kinds, and the shape each one's keys come from. */
|
|
248
|
+
const DEFORM_TRANSFORM_SHAPE: Record<string, keyof typeof MOTION_KEYS> = {
|
|
249
|
+
yaw: 'DeformTurn',
|
|
250
|
+
pitch: 'DeformTurn',
|
|
251
|
+
affine: 'DeformAffine',
|
|
252
|
+
wave: 'DeformWave',
|
|
253
|
+
bend: 'DeformBend',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
257
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* What a value actually IS, for a refusal to name.
|
|
262
|
+
*
|
|
263
|
+
* The generalisation of `describeSetupEntry`, whose wording it keeps verbatim for
|
|
264
|
+
* the three shapes that one covered — those exact strings are what issue #293's
|
|
265
|
+
* refusal reads like and what the selftest pins.
|
|
266
|
+
*/
|
|
267
|
+
function describe(v: unknown): string {
|
|
268
|
+
if (v === undefined) return 'absent';
|
|
269
|
+
if (v === null) return 'null';
|
|
270
|
+
if (Array.isArray(v)) return `an array of ${v.length}`;
|
|
271
|
+
if (typeof v === 'string') return `the string ${JSON.stringify(v)}`;
|
|
272
|
+
if (typeof v === 'number' || typeof v === 'boolean') return `${String(v)}`;
|
|
273
|
+
// "a object" is what `describeSetupEntry` printed; the article is worth a line
|
|
274
|
+
// because these messages are read far more often than they are written.
|
|
275
|
+
return `${typeof v === 'object' ? 'an' : 'a'} ${typeof v}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The one refusal shape in this file: **file, key, what it actually is, and the
|
|
280
|
+
* spelling that works.** Every message below is built from it, so a reader who
|
|
281
|
+
* has seen one has seen the format.
|
|
282
|
+
*/
|
|
283
|
+
function refuse(where: string, key: string, is: unknown, hint: string): never {
|
|
284
|
+
throw new CompileError(`${where}: \`${key}\` is ${describe(is)}; ${hint}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The second refusal shape: **a key this format does not have**, from the one
|
|
289
|
+
* implementation of it. `at` is the path the messages above already print, so a
|
|
290
|
+
* reader meets `setup."lid_l"` whether the entry was the wrong type or carried
|
|
291
|
+
* the wrong field.
|
|
292
|
+
*/
|
|
293
|
+
function known(node: unknown, shape: keyof typeof MOTION_KEYS, where: string, at: string): void {
|
|
294
|
+
if (!isObj(node)) return;
|
|
295
|
+
refuseUnknownKeys(node, MOTION_KEYS[shape], where, `\`${at}\``);
|
|
296
|
+
// The root is named by its keys alone — `archetype`, not `this motion spec.archetype`.
|
|
297
|
+
const prefix = shape === 'MotionSpec' ? '' : at;
|
|
298
|
+
visiting?.push({
|
|
299
|
+
node,
|
|
300
|
+
shape,
|
|
301
|
+
name: (tail) => `\`${prefix}${tail.map((step, i) => (typeof step === 'number' ? `[${step}]` : prefix === '' && i === 0 ? step : `.${step}`)).join('')}\``,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The nodes `known` admitted during the parse in progress, in the order it
|
|
307
|
+
* admitted them — what `refuseValuesOfTheWrongType` walks at the end of
|
|
308
|
+
* `parseMotionSpec`. Module state rather than a parameter because `known` is
|
|
309
|
+
* called from a dozen readers that would otherwise each carry it; it is set and
|
|
310
|
+
* cleared by `parseMotionSpec` alone, around a parse that is synchronous and
|
|
311
|
+
* does not re-enter, so no two parses ever share it.
|
|
312
|
+
*/
|
|
313
|
+
let visiting: ShapeVisit[] | null = null;
|
|
314
|
+
|
|
315
|
+
// --- the leaf checks, each returning the value it just proved ---------------
|
|
316
|
+
|
|
317
|
+
function needObj(v: unknown, where: string, key: string, hint: string): Record<string, unknown> {
|
|
318
|
+
if (!isObj(v)) refuse(where, key, v, hint);
|
|
319
|
+
return v;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function needArray(v: unknown, where: string, key: string, hint: string): unknown[] {
|
|
323
|
+
if (!Array.isArray(v)) refuse(where, key, v, hint);
|
|
324
|
+
return v;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function needString(v: unknown, where: string, key: string, hint: string): string {
|
|
328
|
+
if (typeof v !== 'string' || v.length === 0) refuse(where, key, v, hint);
|
|
329
|
+
return v;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function optString(v: unknown, where: string, key: string, hint: string): void {
|
|
333
|
+
if (v !== undefined && typeof v !== 'string') refuse(where, key, v, hint);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function needFinite(v: unknown, where: string, key: string, hint: string): number {
|
|
337
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) refuse(where, key, v, hint);
|
|
338
|
+
return v;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function optFinite(v: unknown, where: string, key: string, hint: string): void {
|
|
342
|
+
if (v !== undefined && (typeof v !== 'number' || !Number.isFinite(v))) refuse(where, key, v, hint);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* A named easing's handles: four finite numbers, and nothing else.
|
|
347
|
+
*
|
|
348
|
+
* ⭐ The most silent field in the format before this parse existed. `easings` is
|
|
349
|
+
* read only through `bezierForChannel`, which destructures four values with no
|
|
350
|
+
* guard at all — so `[0.42, 0, 0.58]` emitted `"curve": [0.42, 0, 0.58, null]`
|
|
351
|
+
* into the artifact, and a `"x"` in one slot emitted a `NaN` the round trip
|
|
352
|
+
* turns into `null` too. Neither is a curve, both loaded, and nothing said so.
|
|
353
|
+
*/
|
|
354
|
+
function parseEasings(raw: unknown, where: string): void {
|
|
355
|
+
const easings = needObj(raw, where, 'easings', 'it is a table of named handles, `{ "<name>": [hx1, hy1, hx2, hy2] }` (write `{}` if this spec names none)');
|
|
356
|
+
for (const [name, handles] of Object.entries(easings)) {
|
|
357
|
+
const key = `easings."${name}"`;
|
|
358
|
+
const hint =
|
|
359
|
+
'a named easing is FOUR finite numbers — the graph-view handles [hx1, hy1, hx2, hy2]. ' +
|
|
360
|
+
'Nothing downstream counts them, so a short or non-numeric array reaches the artifact as a curve with a `null` in it';
|
|
361
|
+
const arr = needArray(handles, where, key, hint);
|
|
362
|
+
if (arr.length !== 4) refuse(where, key, arr, hint);
|
|
363
|
+
for (const [i, n] of arr.entries()) needFinite(n, where, `${key}[${i}]`, hint);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* `setup` — the entry-shape guard of #293/#303, moved here and widened.
|
|
369
|
+
*
|
|
370
|
+
* 🚨 The guard used to live in the emit loop, which walks the RIG's slots and
|
|
371
|
+
* `continue`s past a slot with no attachments before it ever reads `setup`. So
|
|
372
|
+
* two corners of the very shape it was written for stayed green: a `setup` entry
|
|
373
|
+
* for a slot the rig declares without attachments, and one for a slot the rig
|
|
374
|
+
* does not declare at all. Both are the reader's most likely spelling of the
|
|
375
|
+
* mistake — you write the entry, and the slot it names is exactly the one you
|
|
376
|
+
* have not finished wiring up. Parsing the table on its own terms has no such
|
|
377
|
+
* blind spot: every key is checked, and whether the rig knows the slot is a
|
|
378
|
+
* separate question `compile` still asks.
|
|
379
|
+
*/
|
|
380
|
+
function parseSetup(raw: unknown, where: string): void {
|
|
381
|
+
if (raw === undefined) return;
|
|
382
|
+
const setup = needObj(raw, where, 'setup', 'it is a table keyed by slot name, `{ "<slot>": { "attachment": … } }`');
|
|
383
|
+
for (const [slot, entry] of Object.entries(setup)) {
|
|
384
|
+
const key = `setup."${slot}"`;
|
|
385
|
+
if (!isObj(entry)) {
|
|
386
|
+
refuse(
|
|
387
|
+
where,
|
|
388
|
+
key,
|
|
389
|
+
entry,
|
|
390
|
+
'a setup entry is an object of `{ attachment?: string | null, color?: [r, g, b, a] }` — to show nothing ' +
|
|
391
|
+
`there write \`"${slot}": { "attachment": null }\`, and to show an attachment write ` +
|
|
392
|
+
`\`"${slot}": { "attachment": "<name>" }\``,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (entry.attachment !== undefined && entry.attachment !== null && typeof entry.attachment !== 'string') {
|
|
396
|
+
refuse(where, `${key}.attachment`, entry.attachment, 'it is an attachment name, or null for "show nothing"');
|
|
397
|
+
}
|
|
398
|
+
known(entry, 'MotionSetupSlot', where, key);
|
|
399
|
+
if (entry.color !== undefined) {
|
|
400
|
+
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"';
|
|
401
|
+
const color = needArray(entry.color, where, `${key}.color`, hint);
|
|
402
|
+
if (color.length !== 4) refuse(where, `${key}.color`, color, hint);
|
|
403
|
+
for (const [i, n] of color.entries()) {
|
|
404
|
+
const at = `${key}.color[${i}]`;
|
|
405
|
+
needFinite(n, where, at, hint);
|
|
406
|
+
if ((n as number) < 0 || (n as number) > 1) refuse(where, at, n, hint);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* `physics` — the tuning table.
|
|
414
|
+
*
|
|
415
|
+
* Every field but `bone` and `note` goes straight into `r6`, which is NaN in and
|
|
416
|
+
* NaN out, and the emitter writes that NaN as `null`: `"mass": "heavy"` shipped
|
|
417
|
+
* `"mass": null` in the constraint, which the runtime reads as zero mass. A
|
|
418
|
+
* constraint with zero mass is assertion A23's own example of one that never
|
|
419
|
+
* settles, and it arrived without a word from either layer.
|
|
420
|
+
*/
|
|
421
|
+
function parsePhysics(raw: unknown, where: string): void {
|
|
422
|
+
if (raw === undefined) return;
|
|
423
|
+
const table = needObj(raw, where, 'physics', 'it is a table keyed by constraint name, `{ "<name>": { "bone": … } }`');
|
|
424
|
+
for (const [name, entry] of Object.entries(table)) {
|
|
425
|
+
const key = `physics."${name}"`;
|
|
426
|
+
if (name === EVERY_GLOBAL_PHYSICS) {
|
|
427
|
+
throw new CompileError(
|
|
428
|
+
`${where}: \`${key}\` names a physics constraint "${EVERY_GLOBAL_PHYSICS}", and that name is reserved: a ` +
|
|
429
|
+
`track's \`"physics": "${EVERY_GLOBAL_PHYSICS}"\` is the timeline that names no constraint and drives ` +
|
|
430
|
+
'every one declaring the keyed property global, so a constraint called ' +
|
|
431
|
+
`"${EVERY_GLOBAL_PHYSICS}" could not be keyed by name. Give it another name`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const spec = needObj(entry, where, key,'a physics constraint is an object naming the bone it drives and the components it drives it in');
|
|
435
|
+
known(spec, 'MotionPhysics', where, key);
|
|
436
|
+
needString(spec.bone, where, `${key}.bone`, 'a physics constraint drives one bone, named here');
|
|
437
|
+
for (const field of PHYSICS_NUMBERS) {
|
|
438
|
+
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');
|
|
439
|
+
}
|
|
440
|
+
optString(spec.note, where, `${key}.note`, 'it is prose for a reader');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* `mix` — the player-side `AnimationStateData` config.
|
|
446
|
+
*
|
|
447
|
+
* Not emitted into skeleton JSON, which is why nothing had ever looked at it:
|
|
448
|
+
* `{ "default": "fast" }` passed the compiler, the gate and the round trip, and
|
|
449
|
+
* became a `NaN` mix duration in whatever player read the spec.
|
|
450
|
+
*/
|
|
451
|
+
function parseMix(raw: unknown, where: string): void {
|
|
452
|
+
if (raw === undefined) return;
|
|
453
|
+
const mix = needObj(raw, where, 'mix', 'it is `{ "default": <seconds>, "pairs"?: [["<from>", "<to>", <seconds>], …] }`');
|
|
454
|
+
known(mix, 'MotionMix', where, 'mix');
|
|
455
|
+
needFinite(mix.default, where, 'mix.default', 'the default mix duration is a finite number of seconds');
|
|
456
|
+
if (mix.pairs === undefined) return;
|
|
457
|
+
const pairs = needArray(mix.pairs, where, 'mix.pairs', 'it is an array of `["<from>", "<to>", <seconds>]` triples');
|
|
458
|
+
for (const [i, pair] of pairs.entries()) {
|
|
459
|
+
const key = `mix.pairs[${i}]`;
|
|
460
|
+
const hint = 'a mix pair is `["<from animation>", "<to animation>", <seconds>]` — three entries, two names and a duration';
|
|
461
|
+
const triple = needArray(pair, where, key, hint);
|
|
462
|
+
if (triple.length !== 3) refuse(where, key, triple, hint);
|
|
463
|
+
needString(triple[0], where, `${key}[0]`, hint);
|
|
464
|
+
needString(triple[1], where, `${key}[1]`, hint);
|
|
465
|
+
needFinite(triple[2], where, `${key}[2]`, hint);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* A key's `t`, for every key family there is.
|
|
471
|
+
*
|
|
472
|
+
* ⭐ One owner for one question. `events`, `ik`/`transform` and `deform` each
|
|
473
|
+
* grew their own `has a non-finite time` guard as they were added and the three
|
|
474
|
+
* families that came first — value tracks, slot tracks, `drawOrder` — never got
|
|
475
|
+
* one, so `{ "t": "0" }` on a `rotate` track reached the emitted JSON as a `NaN`
|
|
476
|
+
* time. The three guards in `compile` are gone: they can no longer fire.
|
|
477
|
+
*/
|
|
478
|
+
function parseKeyTime(key: Record<string, unknown>, where: string, at: string): void {
|
|
479
|
+
needFinite(key.t, where, `${at}.t`, 'a key states its time in seconds, as a finite number');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function parseKeyEasing(key: Record<string, unknown>, where: string, at: string): void {
|
|
483
|
+
optString(key.ease, where, `${at}.ease`, 'it names an entry of this spec\'s `easings` table, or is "stepped"');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* One `{ t, … }` key of any family: an object, with a finite time and a string
|
|
488
|
+
* `ease`.
|
|
489
|
+
*
|
|
490
|
+
* `shape` is per caller because the five families' key shapes are five
|
|
491
|
+
* different sets — an `rgba` key's `v` is not a thing an ik key may carry, and
|
|
492
|
+
* an ik key's `softness` is not a thing a value track may. One shared shape here
|
|
493
|
+
* would accept every field of every family on all of them, which is a key set
|
|
494
|
+
* nothing in the format actually has.
|
|
495
|
+
*/
|
|
496
|
+
function parseKey(
|
|
497
|
+
raw: unknown,
|
|
498
|
+
where: string,
|
|
499
|
+
at: string,
|
|
500
|
+
hint: string,
|
|
501
|
+
shape: keyof typeof MOTION_KEYS,
|
|
502
|
+
): Record<string, unknown> {
|
|
503
|
+
const key = needObj(raw, where, at, hint);
|
|
504
|
+
known(key, shape, where, at);
|
|
505
|
+
parseKeyTime(key, where, at);
|
|
506
|
+
parseKeyEasing(key, where, at);
|
|
507
|
+
return key;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function parseTracks(raw: unknown, where: string, at: string): void {
|
|
511
|
+
const tracks = needArray(raw, where, `${at}.tracks`, 'it is an array of `{ <target>, property, keys }` tracks (write `[]` for an animation whose timelines are all in the families beside it)');
|
|
512
|
+
for (const [i, entry] of tracks.entries()) {
|
|
513
|
+
const key = `${at}.tracks[${i}]`;
|
|
514
|
+
const track = needObj(entry, where, key, 'a track is an object naming one target, one property and its keys');
|
|
515
|
+
known(track, 'MotionTrack', where, key);
|
|
516
|
+
needString(track.property, where, `${key}.property`, 'a track states the property it keys — the table is AUTHORING §4.4');
|
|
517
|
+
for (const field of TARGET_FIELDS) {
|
|
518
|
+
optString(track[field], where, `${key}.${field}`, `a track's "${field}" is the name of the ${field === 'slot' || field === 'bone' ? field : `${field} it targets`}`);
|
|
519
|
+
}
|
|
520
|
+
if (track.physics === '') {
|
|
521
|
+
refuse(
|
|
522
|
+
where,
|
|
523
|
+
`${key}.physics`,
|
|
524
|
+
track.physics,
|
|
525
|
+
'the empty name is how a skeleton file spells a physics timeline that names no constraint, and a motion ' +
|
|
526
|
+
`spec spells that "${EVERY_GLOBAL_PHYSICS}" — it drives every physics constraint that declares the keyed ` +
|
|
527
|
+
'property global (`"strengthGlobal": true` for `strength`). Name one constraint, or write ' +
|
|
528
|
+
`"${EVERY_GLOBAL_PHYSICS}"`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
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');
|
|
532
|
+
optFinite(track.stagger, where, `${key}.stagger`, '"stagger" is the extra per-member delay inside a group, in seconds, so a finite number');
|
|
533
|
+
const keys = needArray(track.keys, where, `${key}.keys`, 'it is an array of `{ t, v }` keys');
|
|
534
|
+
for (const [j, k] of keys.entries()) {
|
|
535
|
+
const at = `${key}.keys[${j}]`;
|
|
536
|
+
const parsed = parseKey(k, where, at, 'a key is an object of `{ t, v, … }`', 'MotionKey');
|
|
537
|
+
// `derive` states a generator's parameters rather than a value, and its
|
|
538
|
+
// shape lives with the evaluator (`src/trackgen.ts`). `yaw` and `pitch`
|
|
539
|
+
// are one interface — they differ in which coordinate they read, not in
|
|
540
|
+
// what they carry — so there is no dispatch to do here, and whether the
|
|
541
|
+
// kind is one of the two stays `evaluateTrackDerive`'s refusal.
|
|
542
|
+
known(parsed.derive, 'TrackDeriveTurn', where, `${at}.derive`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function parseConstraintTracks(raw: unknown, where: string, at: string, group: (typeof CONSTRAINT_GROUPS)[number]): void {
|
|
548
|
+
if (raw === undefined) return;
|
|
549
|
+
const entries = needArray(raw, where, `${at}.${group}`, `it is an array of \`{ "constraint": "<name>", "keys": [...] }\` entries — one per ${group} constraint`);
|
|
550
|
+
for (const [i, entry] of entries.entries()) {
|
|
551
|
+
const key = `${at}.${group}[${i}]`;
|
|
552
|
+
const track = needObj(entry, where, key, `${group === 'ik' ? 'an ik' : 'a transform'} timeline is an object of \`{ constraint, keys }\``);
|
|
553
|
+
known(track, group === 'ik' ? 'MotionIkTrack' : 'MotionTransformTrack', where, key);
|
|
554
|
+
needString(track.constraint, where, `${key}.constraint`, `4.3 writes this group as \`${group}.<constraint>\`, so the constraint name is the only target there is`);
|
|
555
|
+
const keys = needArray(track.keys, where, `${key}.keys`, 'it is an array of keys, each naming the same set of mix fields');
|
|
556
|
+
for (const [j, k] of keys.entries()) {
|
|
557
|
+
parseKey(
|
|
558
|
+
k,
|
|
559
|
+
where,
|
|
560
|
+
`${key}.keys[${j}]`,
|
|
561
|
+
`a ${group} key is an object of \`{ t, … }\``,
|
|
562
|
+
group === 'ik' ? 'MotionIkKey' : 'MotionTransformKey',
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function parseDeform(raw: unknown, where: string, at: string): void {
|
|
569
|
+
if (raw === undefined) return;
|
|
570
|
+
const entries = needArray(raw, where, `${at}.deform`, 'it is an array of `{ slot, attachment, keys }` entries — one per skin/slot/attachment triple');
|
|
571
|
+
for (const [i, entry] of entries.entries()) {
|
|
572
|
+
const key = `${at}.deform[${i}]`;
|
|
573
|
+
const track = needObj(entry, where, key, 'a deform timeline is an object of `{ skin?, slot, attachment, keys }`');
|
|
574
|
+
known(track, 'MotionDeformTrack', where, key);
|
|
575
|
+
optString(track.skin, where, `${key}.skin`, 'it names the skin the attachment lives in; absent means "default"');
|
|
576
|
+
needString(track.slot, where, `${key}.slot`, 'a deform timeline keys one attachment of one slot, named here');
|
|
577
|
+
needString(track.attachment, where, `${key}.attachment`, "it is the attachment's placeholder name inside that skin and slot");
|
|
578
|
+
const keys = needArray(track.keys, where, `${key}.keys`, 'it is an array of keys, each a sparse edit of the setup geometry');
|
|
579
|
+
for (const [j, k] of keys.entries()) {
|
|
580
|
+
const at = `${key}.keys[${j}]`;
|
|
581
|
+
const parsed = parseKey(k, where, at, 'a deform key is an object of `{ t, vertices? | transform? }`', 'MotionDeformKey');
|
|
582
|
+
// `transform` is five kinds sharing one field name, and they share almost
|
|
583
|
+
// nothing else: `wave` carries `wavelength` and `bend` carries `power`, so
|
|
584
|
+
// checking either against the union's flattened keys would accept both on
|
|
585
|
+
// both. An unrecognised `kind` is `evaluateDeformTransform`'s refusal,
|
|
586
|
+
// which names the five.
|
|
587
|
+
if (isObj(parsed.transform)) {
|
|
588
|
+
const shape = DEFORM_TRANSFORM_SHAPE[String(parsed.transform.kind)];
|
|
589
|
+
if (shape !== undefined) known(parsed.transform, shape, where, `${at}.transform`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* `sequence` — which frame of an attachment's numbered series shows.
|
|
597
|
+
*
|
|
598
|
+
* Everything decidable from the key alone is decided here; what needs the rig
|
|
599
|
+
* (does the attachment carry a `sequence` block, is `index` inside its `count`)
|
|
600
|
+
* is `compile`'s. Each refusal is a key the parser loads without a word and
|
|
601
|
+
* plays as something else — measured on spine-core 4.3.13 (issue #729):
|
|
602
|
+
*
|
|
603
|
+
* - a `mode` outside the seven — `SequenceMode[name]` is `undefined`, the mode
|
|
604
|
+
* bits store 0, and the key plays as `hold`;
|
|
605
|
+
* - an `index` that is not a whole number of at least 0 — it is stored as
|
|
606
|
+
* `index << 4`, so `1.5` shows frame 1 and a negative one indexes before the
|
|
607
|
+
* series;
|
|
608
|
+
* - a `delay` that is not a number of at least 0 — and, under a mode that
|
|
609
|
+
* advances, an EFFECTIVE delay of 0: the parser carries a key's delay from
|
|
610
|
+
* the key before (`lastDelay`, 0 on the first), `(time - keyTime) / 0` is
|
|
611
|
+
* `Infinity` and `Infinity | 0` is 0, so a `loop` at delay 0 shows its
|
|
612
|
+
* first frame for the whole key.
|
|
613
|
+
*/
|
|
614
|
+
function parseSequence(raw: unknown, where: string, at: string): void {
|
|
615
|
+
if (raw === undefined) return;
|
|
616
|
+
const entries = needArray(raw, where, `${at}.sequence`, 'it is an array of `{ slot, attachment, keys }` entries — one per skin/slot/attachment triple');
|
|
617
|
+
for (const [i, entry] of entries.entries()) {
|
|
618
|
+
const key = `${at}.sequence[${i}]`;
|
|
619
|
+
const track = needObj(entry, where, key, 'a sequence timeline is an object of `{ skin?, slot, attachment, keys }`');
|
|
620
|
+
known(track, 'MotionSequenceTrack', where, key);
|
|
621
|
+
optString(track.skin, where, `${key}.skin`, 'it names the skin the attachment lives in; absent means "default"');
|
|
622
|
+
needString(track.slot, where, `${key}.slot`, 'a sequence timeline steps one attachment of one slot, named here');
|
|
623
|
+
needString(track.attachment, where, `${key}.attachment`, "it is the attachment's placeholder name inside that skin and slot");
|
|
624
|
+
const keys = needArray(track.keys, where, `${key}.keys`, 'it is an array of `{ t, mode?, index?, delay? }` keys');
|
|
625
|
+
// The delay the PARSER will read at each key: the stated one, else the
|
|
626
|
+
// previous key's, else 0 (`lastDelay` in `readAnimation`).
|
|
627
|
+
let carried = 0;
|
|
628
|
+
for (const [j, k] of keys.entries()) {
|
|
629
|
+
const kat = `${key}.keys[${j}]`;
|
|
630
|
+
const parsed = parseKey(k, where, kat, 'a sequence key is an object of `{ t, mode?, index?, delay? }`', 'MotionSequenceKey');
|
|
631
|
+
if (parsed.mode !== undefined && !(SEQUENCE_MODES as readonly unknown[]).includes(parsed.mode)) {
|
|
632
|
+
refuse(
|
|
633
|
+
where,
|
|
634
|
+
`${kat}.mode`,
|
|
635
|
+
parsed.mode,
|
|
636
|
+
`a sequence mode is one of the ${SEQUENCE_MODES.length} the format has — ${SEQUENCE_MODES.join(', ')} ` +
|
|
637
|
+
'(absent means "hold"). The parser reads `SequenceMode[mode]`, which is undefined for anything else, and ' +
|
|
638
|
+
'stores mode bits 0: the key would load without a word and play as "hold"',
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const index = parsed.index;
|
|
642
|
+
if (index !== undefined && (typeof index !== 'number' || !Number.isInteger(index) || index < 0)) {
|
|
643
|
+
refuse(
|
|
644
|
+
where,
|
|
645
|
+
`${kat}.index`,
|
|
646
|
+
index,
|
|
647
|
+
'it is the 0-based frame this key starts on, so a whole number of at least 0 — the runtime stores it as ' +
|
|
648
|
+
'`index << 4`, which truncates a fraction (1.5 showed frame 1) and puts a negative one before the series',
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const delay = parsed.delay;
|
|
652
|
+
if (delay !== undefined && (typeof delay !== 'number' || !Number.isFinite(delay) || delay < 0)) {
|
|
653
|
+
refuse(where, `${kat}.delay`, delay, 'it is the seconds each frame shows for, so a finite number of at least 0');
|
|
654
|
+
}
|
|
655
|
+
if (typeof delay === 'number') carried = delay;
|
|
656
|
+
const mode = typeof parsed.mode === 'string' ? parsed.mode : 'hold';
|
|
657
|
+
if (mode !== 'hold' && carried === 0) {
|
|
658
|
+
throw new CompileError(
|
|
659
|
+
`${where}: \`${kat}\` plays "${mode}" at a delay of 0` +
|
|
660
|
+
(delay === undefined
|
|
661
|
+
? ` — it states none, and the parser carries the previous key's (${j === 0 ? 'there is none, so 0' : '0'})`
|
|
662
|
+
: '') +
|
|
663
|
+
'. The runtime advances the frame by `(time - keyTime) / delay`, which is Infinity at 0, and ' +
|
|
664
|
+
'`Infinity | 0` is 0 — so the key shows its first frame for as long as it lasts, which is "hold" spelt ' +
|
|
665
|
+
`as "${mode}". State the seconds per frame, or write "hold"`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function parseEvents(raw: unknown, where: string, at: string): void {
|
|
673
|
+
if (raw === undefined) return;
|
|
674
|
+
const keys = needArray(raw, where, `${at}.events`, 'it is an array of `{ t, name }` firings — one timeline per animation, naming no target');
|
|
675
|
+
for (const [i, k] of keys.entries()) {
|
|
676
|
+
parseKey(k, where, `${at}.events[${i}]`, 'an event key is an object of `{ t, name, … }`', 'MotionEventKey');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* `drawOrder`.
|
|
682
|
+
*
|
|
683
|
+
* ⚠️ The quiet one is `offsets`: `readDrawOrder` treats a key with no offsets as
|
|
684
|
+
* "restore the setup order", and `compile` tested that with `!key.offsets?.length`
|
|
685
|
+
* — which is true for `{}` and for a string, so a malformed `offsets` silently
|
|
686
|
+
* became a restore key. That is a complete statement of the draw order made by
|
|
687
|
+
* accident.
|
|
688
|
+
*/
|
|
689
|
+
function parseDrawOrder(raw: unknown, where: string, at: string): void {
|
|
690
|
+
if (raw === undefined) return;
|
|
691
|
+
const keys = needArray(raw, where, `${at}.drawOrder`, 'it is an array of `{ t, offsets? }` keys — one timeline per animation, naming no target');
|
|
692
|
+
for (const [i, entry] of keys.entries()) {
|
|
693
|
+
const key = `${at}.drawOrder[${i}]`;
|
|
694
|
+
const dk = parseKey(entry, where, key, 'a draw-order key is an object of `{ t, offsets? }`', 'MotionDrawOrderKey');
|
|
695
|
+
if (dk.offsets === undefined) continue;
|
|
696
|
+
const offsets = needArray(
|
|
697
|
+
dk.offsets,
|
|
698
|
+
where,
|
|
699
|
+
`${key}.offsets`,
|
|
700
|
+
'it is an array of `{ slot, offset }` moves. Omit the field entirely to restore the setup draw order — a malformed one used to BE that restore key, silently',
|
|
701
|
+
);
|
|
702
|
+
for (const [j, o] of offsets.entries()) {
|
|
703
|
+
const oat = `${key}.offsets[${j}]`;
|
|
704
|
+
const off = needObj(o, where, oat, 'one moved slot is `{ "slot": "<name>", "offset": <places later> }`');
|
|
705
|
+
known(off, 'MotionDrawOrderOffset', where, oat);
|
|
706
|
+
needString(off.slot, where, `${oat}.slot`, 'it names the slot this key moves');
|
|
707
|
+
// The TYPE only. Whether it is a whole number, and whether it lands inside
|
|
708
|
+
// the emitted slots array, are `compile`'s — both need the slot table.
|
|
709
|
+
needFinite(off.offset, where, `${oat}.offset`, 'it is how many places later the slot is drawn, so a number (negative moves it earlier)');
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function parseAnimation(raw: unknown, where: string, name: string): void {
|
|
715
|
+
const at = `animations."${name}"`;
|
|
716
|
+
const anim = needObj(raw, where, at, 'an animation is an object of `{ duration, tracks, … }`');
|
|
717
|
+
known(anim, 'MotionAnimation', where, at);
|
|
718
|
+
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');
|
|
719
|
+
if (duration < 0) {
|
|
720
|
+
refuse(where, `${at}.duration`, duration, 'a duration is a length of time, so it is not negative');
|
|
721
|
+
}
|
|
722
|
+
// ⚠️ Optional, and the type used to say otherwise: 20 of the 37 motion specs
|
|
723
|
+
// in this repository declare no `loop` at all. It is a player hint that is not
|
|
724
|
+
// expressible in skeleton JSON, so an absent one costs the artifact nothing —
|
|
725
|
+
// requiring it here would have refused most of the benchmark corpus.
|
|
726
|
+
if (anim.loop !== undefined && typeof anim.loop !== 'boolean') {
|
|
727
|
+
refuse(where, `${at}.loop`, anim.loop, 'it is a player hint, so true or false (absent means the player decides)');
|
|
728
|
+
}
|
|
729
|
+
optString(anim.note, where, `${at}.note`, 'it is prose for a reader');
|
|
730
|
+
parseTracks(anim.tracks, where, at);
|
|
731
|
+
for (const group of CONSTRAINT_GROUPS) parseConstraintTracks(anim[group], where, at, group);
|
|
732
|
+
parseDeform(anim.deform, where, at);
|
|
733
|
+
parseSequence(anim.sequence, where, at);
|
|
734
|
+
parseDrawOrder(anim.drawOrder, where, at);
|
|
735
|
+
parseEvents(anim.events, where, at);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Parse and check a motion spec, then hand back a typed one.
|
|
740
|
+
*
|
|
741
|
+
* `where` is the file's own path and every message begins with it, for the reason
|
|
742
|
+
* `parseRigSpec` does the same: a reader with two input files and one error has
|
|
743
|
+
* otherwise no way to tell which of them is at fault (issue #227).
|
|
744
|
+
*/
|
|
745
|
+
export function parseMotionSpec(raw: unknown, where: string): MotionSpec {
|
|
746
|
+
const visits: ShapeVisit[] = [];
|
|
747
|
+
visiting = visits;
|
|
748
|
+
try {
|
|
749
|
+
return parseMotionSpecInto(raw, where, visits);
|
|
750
|
+
} finally {
|
|
751
|
+
visiting = null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function parseMotionSpecInto(raw: unknown, where: string, visits: readonly ShapeVisit[]): MotionSpec {
|
|
756
|
+
if (!isObj(raw)) {
|
|
757
|
+
throw new CompileError(`${where}: a motion spec must be a JSON object, and this file holds ${describe(raw)}`);
|
|
758
|
+
}
|
|
759
|
+
if (raw.spec !== MOTION_SPEC_VERSION) {
|
|
760
|
+
throw new CompileError(`${where}: unknown motion spec version: ${String(raw.spec)}, expected "${MOTION_SPEC_VERSION}"`);
|
|
761
|
+
}
|
|
762
|
+
// Before the field checks, for the reason `parseRigSpec` puts its own first: a
|
|
763
|
+
// key nothing reads is often the CAUSE of the field that is missing, and
|
|
764
|
+
// `"animation"` for `"animations"` should be named as the typo it is rather
|
|
765
|
+
// than as an absent table.
|
|
766
|
+
known(raw, 'MotionSpec', where, 'this motion spec');
|
|
767
|
+
needString(raw.archetype, where, 'archetype', "it names the rig this spec was authored against, and must equal that rig spec's own `name`");
|
|
768
|
+
needString(raw.cut, where, 'cut', 'it names the cut these keys were authored for');
|
|
769
|
+
optString(raw.note, where, 'note', 'it is prose for a reader');
|
|
770
|
+
|
|
771
|
+
parseEasings(raw.easings, where);
|
|
772
|
+
// `groups` — the parser proves the TABLE is a table; `checkMotionGroups` owns
|
|
773
|
+
// each entry's member list, because what it refuses (an empty group, a repeated
|
|
774
|
+
// member) is about what a track naming it would compile, not about JSON shape.
|
|
775
|
+
if (raw.groups !== undefined) {
|
|
776
|
+
needObj(raw.groups, where, 'groups', 'it is a table keyed by group name, `{ "<group>": ["<member>", …] }`');
|
|
777
|
+
}
|
|
778
|
+
parseSetup(raw.setup, where);
|
|
779
|
+
parsePhysics(raw.physics, where);
|
|
780
|
+
parseMix(raw.mix, where);
|
|
781
|
+
|
|
782
|
+
const animations = needObj(raw.animations, where, 'animations', 'it is a table keyed by animation name, `{ "<name>": { "duration": …, "tracks": [...] } }` (write `{}` for a static rig)');
|
|
783
|
+
for (const [name, anim] of Object.entries(animations)) {
|
|
784
|
+
if (name.length === 0) throw new CompileError(`${where}: an animation has an empty name`);
|
|
785
|
+
parseAnimation(anim, where, name);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// After every field check above, so a field this file checks keeps its own
|
|
789
|
+
// sentence, and before the finite walk, so a value that is not a number is
|
|
790
|
+
// named as that rather than skipped (issue #890). What reaches this line is a
|
|
791
|
+
// field nothing above reads. Measured before the walk, every one of 303
|
|
792
|
+
// wrong-typed plants over 18 motion shapes was refused somewhere — but later,
|
|
793
|
+
// by the timeline compiler, and 7 of those sentences named the wrong fault:
|
|
794
|
+
// an event key's `"float": "1"` as *float 1 is not finite*, a `"volume": "1"`
|
|
795
|
+
// as an event with no audio, a yaw transform's `"depth": "true"` as a radius
|
|
796
|
+
// that is undefined.
|
|
797
|
+
refuseValuesOfTheWrongType(visits, MOTION_TYPES, where);
|
|
798
|
+
// Every `enum` row here names an owner, so this refuses nothing today; it is
|
|
799
|
+
// called so that a row given a set is refused from the day it is given one.
|
|
800
|
+
refuseValuesOutsideTheirSet(visits, MOTION_TYPES, MOTION_ENUMS, where);
|
|
801
|
+
|
|
802
|
+
// Last, so every field check above keeps its own sentence for a number that
|
|
803
|
+
// is not finite; what reaches this line is a finite double the float32 file
|
|
804
|
+
// cannot carry — a key at 1e308 built green with a `null` in it before issue
|
|
805
|
+
// #881 — and any number in a field no check above reads.
|
|
806
|
+
refuseNumbersTheFileCannotCarry(raw, where, (path) => `\`${dottedPath(path)}\``);
|
|
807
|
+
|
|
808
|
+
return raw as unknown as MotionSpec;
|
|
809
|
+
}
|