rig-c 0.0.0-stage → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +1191 -0
- package/src/meshquality.ts +2051 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1444 -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/timelines.ts
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 4.3 animation timeline catalogue, as data plus one walker.
|
|
3
|
+
*
|
|
4
|
+
* This lives on its own because two very different consumers need the same
|
|
5
|
+
* enumeration and neither may drift from the other: `validate.ts` walks it to
|
|
6
|
+
* check curve arrays (A05) and two-colour timelines (A12), and `diff.ts` walks
|
|
7
|
+
* it to count what a rig actually keys. When it was inlined in the validator,
|
|
8
|
+
* "which groups exist" was stated in one place and the comparison tool would
|
|
9
|
+
* have had to restate it — and a second copy of a catalogue is a second copy
|
|
10
|
+
* that goes stale silently.
|
|
11
|
+
*
|
|
12
|
+
* `PHYSICS_POSE_RULES` at the bottom is here for that reason and no other: the
|
|
13
|
+
* compiler refuses an out-of-range physics value a spec states, `A23` names one
|
|
14
|
+
* in a file rigc did not write, and the two have to be the same criterion rather
|
|
15
|
+
* than two readings of one (issue #610).
|
|
16
|
+
*
|
|
17
|
+
* Pure JSON reading. No spine-core, no filesystem. The line numbers cited are
|
|
18
|
+
* into `SkeletonJson.ts` on branch 4.3; the field-by-field survey is in
|
|
19
|
+
* `docs/SPEC_COVERAGE.md` part 1-8.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
type Json = Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
function isObj(v: unknown): v is Json {
|
|
25
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* How many value channels each timeline carries. A curve array holds exactly
|
|
30
|
+
* four numbers PER channel; anything shorter
|
|
31
|
+
* multiplies `undefined` into the cubic and produces a NaN curve with no error
|
|
32
|
+
* (case 6g). `null` means "this timeline takes no curve at all".
|
|
33
|
+
*/
|
|
34
|
+
const BONE_CHANNELS: Record<string, number | null> = {
|
|
35
|
+
rotate: 1,
|
|
36
|
+
translate: 2,
|
|
37
|
+
translatex: 1,
|
|
38
|
+
translatey: 1,
|
|
39
|
+
scale: 2,
|
|
40
|
+
scalex: 1,
|
|
41
|
+
scaley: 1,
|
|
42
|
+
shear: 2,
|
|
43
|
+
shearx: 1,
|
|
44
|
+
sheary: 1,
|
|
45
|
+
inherit: null,
|
|
46
|
+
};
|
|
47
|
+
const SLOT_CHANNELS: Record<string, number | null> = {
|
|
48
|
+
attachment: null,
|
|
49
|
+
rgba: 4,
|
|
50
|
+
rgb: 3,
|
|
51
|
+
alpha: 1,
|
|
52
|
+
rgba2: 7,
|
|
53
|
+
rgb2: 6,
|
|
54
|
+
};
|
|
55
|
+
const ATTACHMENT_CHANNELS: Record<string, number | null> = {
|
|
56
|
+
deform: 1,
|
|
57
|
+
sequence: null,
|
|
58
|
+
};
|
|
59
|
+
const PHYSICS_CHANNELS: Record<string, number | null> = {
|
|
60
|
+
inertia: 1,
|
|
61
|
+
strength: 1,
|
|
62
|
+
damping: 1,
|
|
63
|
+
mass: 1,
|
|
64
|
+
wind: 1,
|
|
65
|
+
gravity: 1,
|
|
66
|
+
mix: 1,
|
|
67
|
+
reset: null,
|
|
68
|
+
};
|
|
69
|
+
const PATH_CHANNELS: Record<string, number | null> = {
|
|
70
|
+
position: 1,
|
|
71
|
+
spacing: 1,
|
|
72
|
+
mix: 3,
|
|
73
|
+
};
|
|
74
|
+
const SLIDER_CHANNELS: Record<string, number | null> = {
|
|
75
|
+
time: 1,
|
|
76
|
+
mix: 1,
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* `ik` and `transform` are ONE timeline per constraint with no sub-name — the
|
|
80
|
+
* group maps a constraint name straight to a key array. There is no name in the
|
|
81
|
+
* file to look up, so the walker passes the group's own name and these tables
|
|
82
|
+
* hold that single entry.
|
|
83
|
+
*/
|
|
84
|
+
const IK_CHANNELS: Record<string, number | null> = { ik: 2 };
|
|
85
|
+
const TRANSFORM_CHANNELS: Record<string, number | null> = { transform: 6 };
|
|
86
|
+
/** Whole-animation timelines. None of the three can carry a curve at all. */
|
|
87
|
+
const DRAW_ORDER_CHANNELS: Record<string, number | null> = { drawOrder: null };
|
|
88
|
+
const DRAW_ORDER_FOLDER_CHANNELS: Record<string, number | null> = { drawOrderFolder: null };
|
|
89
|
+
const EVENT_CHANNELS: Record<string, number | null> = { events: null };
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* How far past an animation's declared `duration` a key time may land: one step
|
|
93
|
+
* of the **float32** grid at that duration, which is the grid a key time is
|
|
94
|
+
* both emitted on and stored on.
|
|
95
|
+
*
|
|
96
|
+
* `spine-core` reads every timeline's frames into a `Float32Array`, and since
|
|
97
|
+
* issue #716 the compiler writes every number as its float32's shortest name,
|
|
98
|
+
* so the two grids are one. A key placed exactly ON a duration the float cannot
|
|
99
|
+
* hold is stored at most half a step from it — later, for a time like `0.2`
|
|
100
|
+
* whose own text names a float; never later for a time off that grid, which
|
|
101
|
+
* `keyTime` steps down (issue #99). What still needs the tolerance is the first
|
|
102
|
+
* case, and every artifact no rigc compile touched. Anything a whole step past a
|
|
103
|
+
* declared duration was authored there, not rounded onto it.
|
|
104
|
+
*
|
|
105
|
+
* ⭐ **A function and not a constant, because the grid is relative.** The fixed
|
|
106
|
+
* `KEY_TIME_EPSILON = 1e-6` that stood here was one step of `r6`'s six-decimal
|
|
107
|
+
* grid, and `A09` added this function to it for the float the file is read back
|
|
108
|
+
* into. With `r6` retired the first term had nothing left to be a step of, so it
|
|
109
|
+
* retired with it: a float32 step is 4.8e-7 s at 5 s and 3.8e-6 s at 32 s, and a
|
|
110
|
+
* flat epsilon would fail correct data for being long — 972 frames at 30 fps
|
|
111
|
+
* keyed exactly on their own declared duration are stored 1.5e-6 s late.
|
|
112
|
+
*
|
|
113
|
+
* ⚠️ `FRAME` (1/60 s) is the wrong tolerance for this, which is why the function
|
|
114
|
+
* is separate rather than reused: 1/60 s answers "is the DECLARED DURATION
|
|
115
|
+
* wrong?", it is some 35,000 times wider than this at 5 s, and it hid the
|
|
116
|
+
* defect that put this here. Rung 6 rounded key times to 4 dp in its authoring
|
|
117
|
+
* tooling, so a one-frame attachment reveal landed 3.3e-5 s past a 68/12 s
|
|
118
|
+
* duration — about 70 steps past this line but 1/500 of FRAME, with another
|
|
119
|
+
* track already sitting on the declared duration, so the compiler's Rule 4 and
|
|
120
|
+
* the validator's A09 both compared the animation's max key time and agreed.
|
|
121
|
+
* The reveal never fired (issue #54).
|
|
122
|
+
*
|
|
123
|
+
* The spacing of a normal float is 2^(exponent − 23), and `Math.log2` recovers
|
|
124
|
+
* the exponent. Zero takes the guard — a named empty animation declares
|
|
125
|
+
* `duration: 0` and A09 does compare it — and the magnitude is taken first, so a
|
|
126
|
+
* sign never reaches `log2`.
|
|
127
|
+
*
|
|
128
|
+
* It lives here, beside the timeline catalogue, for the reason the catalogue
|
|
129
|
+
* does: `compile.ts` refuses on it and `validate.ts` re-checks the emitted file
|
|
130
|
+
* against it, and a second copy is a second copy that drifts.
|
|
131
|
+
*/
|
|
132
|
+
export function float32Step(t: number): number {
|
|
133
|
+
const magnitude = Math.abs(t);
|
|
134
|
+
if (!Number.isFinite(magnitude) || magnitude === 0) return 0;
|
|
135
|
+
return 2 ** (Math.floor(Math.log2(magnitude)) - 23);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Every timeline group `readAnimation` reads (SPEC_COVERAGE part 1-8), keyed by
|
|
140
|
+
* the walker's `kind`. A05 selects its channel table from here, so a group that
|
|
141
|
+
* is missing from this map is a group whose curves nobody checks.
|
|
142
|
+
*/
|
|
143
|
+
export const CHANNELS_BY_KIND: Record<TimelineKind, Record<string, number | null>> = {
|
|
144
|
+
bone: BONE_CHANNELS,
|
|
145
|
+
slot: SLOT_CHANNELS,
|
|
146
|
+
ik: IK_CHANNELS,
|
|
147
|
+
transform: TRANSFORM_CHANNELS,
|
|
148
|
+
path: PATH_CHANNELS,
|
|
149
|
+
physics: PHYSICS_CHANNELS,
|
|
150
|
+
slider: SLIDER_CHANNELS,
|
|
151
|
+
attachment: ATTACHMENT_CHANNELS,
|
|
152
|
+
drawOrder: DRAW_ORDER_CHANNELS,
|
|
153
|
+
drawOrderFolder: DRAW_ORDER_FOLDER_CHANNELS,
|
|
154
|
+
event: EVENT_CHANNELS,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** The eleven timeline groups one animation can hold in 4.3. */
|
|
158
|
+
export type TimelineKind =
|
|
159
|
+
| 'bone'
|
|
160
|
+
| 'slot'
|
|
161
|
+
| 'ik'
|
|
162
|
+
| 'transform'
|
|
163
|
+
| 'path'
|
|
164
|
+
| 'physics'
|
|
165
|
+
| 'slider'
|
|
166
|
+
| 'attachment'
|
|
167
|
+
| 'drawOrder'
|
|
168
|
+
| 'drawOrderFolder'
|
|
169
|
+
| 'event';
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Walks every timeline group `readAnimation` reads and hands each timeline to
|
|
174
|
+
* the visitor.
|
|
175
|
+
*
|
|
176
|
+
* ⚠️ This function is the reach of A05 and A12, so a group it does not descend
|
|
177
|
+
* is a group whose curve arrays nobody checks — and a short curve array is the
|
|
178
|
+
* format's nastiest silent failure (`curve[i+3]` is
|
|
179
|
+
* `undefined`, the cubic yields NaN, nothing throws). It used to descend
|
|
180
|
+
* `bones`, `slots`, `physics` and `attachments` only, which left `ik`,
|
|
181
|
+
* `transform`, `path`, `slider`, `drawOrder`, `drawOrderFolder` and `events`
|
|
182
|
+
* completely unexamined. The comment that stood here claimed drawOrder and
|
|
183
|
+
* events were "skipped by design" because they carry no curves — but "carries
|
|
184
|
+
* no curve" is precisely a rule that has to be CHECKED, and the parser ignores
|
|
185
|
+
* a stray `curve` key on those timelines rather than rejecting it.
|
|
186
|
+
*
|
|
187
|
+
* The groups come in four shapes, and the shape is the whole reason this is not
|
|
188
|
+
* one loop (SPEC_COVERAGE part 1-8):
|
|
189
|
+
*
|
|
190
|
+
* group.<target>.<timeline> = keys[] bones, slots, path, physics, slider
|
|
191
|
+
* group.<target> = keys[] ik, transform — one unnamed timeline
|
|
192
|
+
* group = keys[] drawOrder, events — one per animation
|
|
193
|
+
* group = folders[] with .keys[] drawOrderFolder
|
|
194
|
+
*
|
|
195
|
+
* plus `attachments.<skin>.<slot>.<attachment>.<timeline>`. For the shapes with
|
|
196
|
+
* no timeline name in the file, the walker passes the group's own name so that
|
|
197
|
+
* A05's table lookup and its "unchecked timeline" fail-closed branch both keep
|
|
198
|
+
* working unchanged.
|
|
199
|
+
*/
|
|
200
|
+
export function walkTimelines(
|
|
201
|
+
raw: Json | null,
|
|
202
|
+
visit: (path: string, kind: TimelineKind, name: string, keys: unknown[]) => void,
|
|
203
|
+
): void {
|
|
204
|
+
if (!raw || !isObj(raw.animations)) return;
|
|
205
|
+
for (const [animName, anim] of Object.entries(raw.animations as Json)) {
|
|
206
|
+
if (!isObj(anim)) continue;
|
|
207
|
+
|
|
208
|
+
// group.<target>.<timeline> = keys[]
|
|
209
|
+
for (const [group, kind] of [
|
|
210
|
+
['bones', 'bone'],
|
|
211
|
+
['slots', 'slot'],
|
|
212
|
+
['path', 'path'],
|
|
213
|
+
['physics', 'physics'],
|
|
214
|
+
['slider', 'slider'],
|
|
215
|
+
] as const) {
|
|
216
|
+
if (!isObj(anim[group])) continue;
|
|
217
|
+
for (const [targetName, timelines] of Object.entries(anim[group] as Json)) {
|
|
218
|
+
if (!isObj(timelines)) continue;
|
|
219
|
+
for (const [timelineName, keys] of Object.entries(timelines)) {
|
|
220
|
+
if (!Array.isArray(keys)) continue;
|
|
221
|
+
visit(`${animName}.${group}.${targetName}.${timelineName}`, kind, timelineName, keys);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// group.<constraint> = keys[] — the constraint IS the timeline
|
|
227
|
+
for (const [group, kind] of [
|
|
228
|
+
['ik', 'ik'],
|
|
229
|
+
['transform', 'transform'],
|
|
230
|
+
] as const) {
|
|
231
|
+
if (!isObj(anim[group])) continue;
|
|
232
|
+
for (const [targetName, keys] of Object.entries(anim[group] as Json)) {
|
|
233
|
+
if (!Array.isArray(keys)) continue;
|
|
234
|
+
visit(`${animName}.${group}.${targetName}`, kind, group, keys);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// group = keys[] — one timeline for the whole animation
|
|
239
|
+
for (const [group, kind] of [
|
|
240
|
+
['drawOrder', 'drawOrder'],
|
|
241
|
+
['events', 'event'],
|
|
242
|
+
] as const) {
|
|
243
|
+
const keys = anim[group];
|
|
244
|
+
if (!Array.isArray(keys)) continue;
|
|
245
|
+
visit(`${animName}.${group}`, kind, group, keys);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// drawOrderFolder = [ { slots: [...], keys: [...] } ] — one timeline per folder
|
|
249
|
+
if (Array.isArray(anim.drawOrderFolder)) {
|
|
250
|
+
(anim.drawOrderFolder as unknown[]).forEach((folder, i) => {
|
|
251
|
+
if (!isObj(folder) || !Array.isArray(folder.keys)) return;
|
|
252
|
+
visit(`${animName}.drawOrderFolder[${i}]`, 'drawOrderFolder', 'drawOrderFolder', folder.keys);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// attachments.<skin>.<slot>.<attachment>.<timeline>
|
|
257
|
+
if (isObj(anim.attachments)) {
|
|
258
|
+
for (const [skinName, skinMap] of Object.entries(anim.attachments as Json)) {
|
|
259
|
+
if (!isObj(skinMap)) continue;
|
|
260
|
+
for (const [slotName, slotMap] of Object.entries(skinMap)) {
|
|
261
|
+
if (!isObj(slotMap)) continue;
|
|
262
|
+
for (const [attName, attMap] of Object.entries(slotMap)) {
|
|
263
|
+
if (!isObj(attMap)) continue;
|
|
264
|
+
for (const [timelineName, keys] of Object.entries(attMap)) {
|
|
265
|
+
if (!Array.isArray(keys)) continue;
|
|
266
|
+
visit(
|
|
267
|
+
`${animName}.attachments.${skinName}.${slotName}.${attName}.${timelineName}`,
|
|
268
|
+
'attachment',
|
|
269
|
+
timelineName,
|
|
270
|
+
keys,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* A physics constraint's pose, as the four fields `A23` judges.
|
|
283
|
+
*
|
|
284
|
+
* Structural rather than spine-core's `PhysicsConstraintPose`, because this
|
|
285
|
+
* module links no runtime (see the header) — the runtime's class satisfies it,
|
|
286
|
+
* and so does the probe `validate.ts` hands the runtime's own timeline `set` to
|
|
287
|
+
* fill.
|
|
288
|
+
*/
|
|
289
|
+
export interface PhysicsJudgedPose {
|
|
290
|
+
mix: number;
|
|
291
|
+
massInverse: number;
|
|
292
|
+
strength: number;
|
|
293
|
+
damping: number;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** One way out of a physics bound, and what the runtime does with a value that takes it. */
|
|
297
|
+
export interface PhysicsOutsideArm {
|
|
298
|
+
/** True for the pose values this arm is about — every one of them outside the bound. */
|
|
299
|
+
when: (poseValue: number) => boolean;
|
|
300
|
+
/** What the runtime does with such a value, in the words both of `A23`'s arms print. */
|
|
301
|
+
says: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* A value a basis arm is measured at: a stated number (a key's or the tuning
|
|
306
|
+
* table's, before `toPose`) and the constraint `fps` it is stepped at, where the
|
|
307
|
+
* arm's claim depends on the rate. Absent `fps` is the constraint's own rate.
|
|
308
|
+
*/
|
|
309
|
+
export interface PhysicsBasisWitness {
|
|
310
|
+
value: number;
|
|
311
|
+
fps?: number;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Why one way out of a physics bound is refused, in the one of two kinds it is
|
|
316
|
+
* (issue #798).
|
|
317
|
+
*
|
|
318
|
+
* - **arithmetic** — the runtime cannot compute the value: an expression in the
|
|
319
|
+
* integrator is non-finite at it, whatever the rest of the rig does.
|
|
320
|
+
* `expression` says which and `lines` cites where.
|
|
321
|
+
* - **behavioural** — the runtime computes it, finitely, and the rig runs
|
|
322
|
+
* wrongly: a run-away, an inverted jiggle, a constraint doing nothing.
|
|
323
|
+
* Refusing it is rigc's call, and the sentence says so.
|
|
324
|
+
*
|
|
325
|
+
* ⚠️ The line between them is **non-finite within the walk that measures it**,
|
|
326
|
+
* never "non-finite eventually". [measured] through spine-core on the generated
|
|
327
|
+
* physics fixture, stepped from `Physics.reset` at 60 fps under a displacing
|
|
328
|
+
* animation: a setup `damping` of 2 is finite for 1,064 steps and its velocity
|
|
329
|
+
* is Infinity at step 1,065, and 1.0001 is finite over 4,000; a `mass` of −1
|
|
330
|
+
* takes the offset to 2.7e6 in 120 steps and is finite on every one. Every
|
|
331
|
+
* run-away overflows at SOME horizon, so "eventually" would call each of them
|
|
332
|
+
* arithmetic and the distinction would say nothing. The arithmetic arms are
|
|
333
|
+
* non-finite from the first or second step: `mass` 0 at step 1, a `damping` of
|
|
334
|
+
* −0.5 at 45 fps at step 2.
|
|
335
|
+
*
|
|
336
|
+
* `witness` is the value `T113` steps through the runtime to hold `kind`
|
|
337
|
+
* against it: an arithmetic arm has to go non-finite within its 120 steps, a
|
|
338
|
+
* behavioural one has to stay finite for all of them.
|
|
339
|
+
*/
|
|
340
|
+
export type PhysicsBoundBasis =
|
|
341
|
+
| {
|
|
342
|
+
kind: 'arithmetic';
|
|
343
|
+
/** True for the pose values this arm is about — every one of them outside the bound. */
|
|
344
|
+
when: (poseValue: number) => boolean;
|
|
345
|
+
witness: PhysicsBasisWitness;
|
|
346
|
+
/** The expression that is non-finite at the value, and what that does to the pose. */
|
|
347
|
+
expression: string;
|
|
348
|
+
/** Where the runtime computes it, in spine-core 4.3.13's `dist`. */
|
|
349
|
+
lines: string;
|
|
350
|
+
}
|
|
351
|
+
| {
|
|
352
|
+
kind: 'behavioural';
|
|
353
|
+
/** True for the pose values this arm is about — every one of them outside the bound. */
|
|
354
|
+
when: (poseValue: number) => boolean;
|
|
355
|
+
witness: PhysicsBasisWitness;
|
|
356
|
+
/** What the runtime does with the value, finitely — the reason the rig is wrong. */
|
|
357
|
+
does: string;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* One arm's reason, in the words every sentence that refuses a value by it
|
|
362
|
+
* prints: an arithmetic arm names its expression and lines, a behavioural one
|
|
363
|
+
* says that refusing it is rigc's call and then what the value does.
|
|
364
|
+
*
|
|
365
|
+
* ⚠️ The behavioural reason ENDS on `does`, so a sentence built on it ends on
|
|
366
|
+
* what the value does — `T100` holds `strength`'s setup sentences to ending on
|
|
367
|
+
* the row's own `outside` arm, and this order is what keeps that true.
|
|
368
|
+
*/
|
|
369
|
+
export function physicsBasisSays(arm: PhysicsBoundBasis): string {
|
|
370
|
+
return arm.kind === 'arithmetic'
|
|
371
|
+
? `${arm.expression} (\`${arm.lines}\`)`
|
|
372
|
+
: `the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: ${arm.does}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** The arm of a row's `basis` that holds for a pose value, or `undefined` where none does. */
|
|
376
|
+
export function physicsBasisFor(rule: PhysicsPoseRule, poseValue: number): PhysicsBoundBasis | undefined {
|
|
377
|
+
return rule.basis.find((arm) => arm.when(poseValue));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* One physics property `A23` has an opinion about, stated once for the two
|
|
382
|
+
* layers that hold it.
|
|
383
|
+
*
|
|
384
|
+
* ⚠️ **The keyed number and the pose field are not always the same number.**
|
|
385
|
+
* `mass` is the one: `PhysicsConstraintMassTimeline.set` is
|
|
386
|
+
* `pose.massInverse = 1 / value` (`Animation.js:2132-2145`) and the parser does
|
|
387
|
+
* the same to a constraint's own `mass` (`SkeletonJson.js:309`), so a key states
|
|
388
|
+
* a mass and the integrator reads its reciprocal. `toPose` IS that transform, and
|
|
389
|
+
* every predicate here is written against the pose field rather than against the
|
|
390
|
+
* keyed number — which is what makes "the compiler and the assertion apply the
|
|
391
|
+
* same criterion" a property of the code and not a claim about it.
|
|
392
|
+
*/
|
|
393
|
+
export interface PhysicsPoseRule {
|
|
394
|
+
/** The timeline name in skeleton JSON, and the motion spec's `property`. */
|
|
395
|
+
timeline: string;
|
|
396
|
+
/** The pose field the integrator reads. */
|
|
397
|
+
field: keyof PhysicsJudgedPose;
|
|
398
|
+
/** The pose field, from the number a key or the rig's tuning table states. */
|
|
399
|
+
toPose: (value: number) => number;
|
|
400
|
+
/** True when the integrator can use that pose field. */
|
|
401
|
+
poseOk: (poseValue: number) => boolean;
|
|
402
|
+
/**
|
|
403
|
+
* The same question asked of a KEY, where it differs — `null` means it does
|
|
404
|
+
* not. **Two rows have one, and in both the widening is to 0 exactly**, for
|
|
405
|
+
* the same reason: a setup pose states what a constraint IS and a key states
|
|
406
|
+
* what it is doing for a stretch, so a value that makes a constraint
|
|
407
|
+
* permanently useless can be a deliberate span inside an animation.
|
|
408
|
+
*
|
|
409
|
+
* - `mix`: `update` opens with `if (mix === 0) return;`
|
|
410
|
+
* (`PhysicsConstraint.js:109-111`) and `PhysicsConstraintPose` documents the
|
|
411
|
+
* field as "a percentage (0+)", so a mix of exactly 0 is a state the runtime
|
|
412
|
+
* has a branch for. Measured, not assumed: the editor's own `sack-pro`
|
|
413
|
+
* example keys mix to 0 on 24 of its 36 mix keys, and applying the setup
|
|
414
|
+
* rule to keys would refuse all 24 (issue #610).
|
|
415
|
+
* - `strength`: 0 takes the restoring term out of the velocity update and
|
|
416
|
+
* leaves `damping` and `inertia` applied, which is a released span rather
|
|
417
|
+
* than a broken constraint. Measured through spine-core on the generated
|
|
418
|
+
* overlay fixture, keying 0 for a span and restoring it (issue #727): no
|
|
419
|
+
* NaN; with no wind or gravity the offset coasts to a LIMIT rather than
|
|
420
|
+
* running away — 0.5 s of it and 2.0 s of it end 0.95 % apart — and the
|
|
421
|
+
* restoring key takes the offset from 5.5063 back under 0.01 in 54 steps at
|
|
422
|
+
* 60 fps. With `gravity -40` acting, the offset travels at
|
|
423
|
+
* terminal velocity while the key holds (178 units over 0.5 s, 843 over
|
|
424
|
+
* 2.0 s) and the restoring key still pulls it back to the never-keyed run's
|
|
425
|
+
* own equilibrium — 39.999969 against 40.000000 — in 13 steps. What that
|
|
426
|
+
* measurement rules out is the thing a key cannot undo, and the neighbour
|
|
427
|
+
* that HAS one is the contrast: a keyed `mass` of 0 is NaN from the first
|
|
428
|
+
* sub-step and still NaN after the restoring key, so it stays refused.
|
|
429
|
+
*/
|
|
430
|
+
keyOk: ((poseValue: number) => boolean) | null;
|
|
431
|
+
/**
|
|
432
|
+
* True where a SETUP value this bound refuses leaves the constraint doing
|
|
433
|
+
* **nothing**, rather than doing something wrong — so an animation keying a
|
|
434
|
+
* value the bound accepts makes it effective, and a rig resting outside the
|
|
435
|
+
* bound is off rather than broken.
|
|
436
|
+
*
|
|
437
|
+
* Only `mix` is, and it is measured rather than argued (issue #743). At rest —
|
|
438
|
+
* setup pose, no animation applied, 36 steps at 60 fps — a constraint resting
|
|
439
|
+
* at `mix` 0 leaves its bone at worldX 12.0000 on every frame, while one
|
|
440
|
+
* resting at `mass` 0 reads NaN on every frame *although an animation keys
|
|
441
|
+
* `mass` to 1*, because `massInverse` is already Infinity before anything
|
|
442
|
+
* plays. Measured with the fixture's gravity at 0 and again at −40: the second
|
|
443
|
+
* rig is NaN either way, since `m = t * massInverse` is Infinity and the force
|
|
444
|
+
* it multiplies need not be non-zero for the product to be NaN.
|
|
445
|
+
*
|
|
446
|
+
* The same asymmetry is in the runtime's own text: `update` opens with
|
|
447
|
+
* `if (mix === 0) return;` (`PhysicsConstraint.js:109-111`) and has no such
|
|
448
|
+
* branch for the other three.
|
|
449
|
+
*
|
|
450
|
+
* ⚠️ This is NOT `keyOk !== null`, although today both are `mix` alone. That
|
|
451
|
+
* one says what a KEY may hold; this says whether a key can rescue the SETUP
|
|
452
|
+
* value — two questions with one answer here and no reason to share a field.
|
|
453
|
+
*/
|
|
454
|
+
inertAtSetup: boolean;
|
|
455
|
+
/**
|
|
456
|
+
* What the runtime does with a value outside the bound, one arm per way out,
|
|
457
|
+
* where the ways out do different things — or `null` where one sentence covers
|
|
458
|
+
* them all. `A23`'s SETUP sentence reads the arm that holds for the value, and
|
|
459
|
+
* the row's `why` — the KEY's sentence — is written from the same arms, so the
|
|
460
|
+
* two cannot say different things about one number (issue #748).
|
|
461
|
+
*
|
|
462
|
+
* Only `strength` has two, and they are measured rather than argued: resting
|
|
463
|
+
* at 0 the offset is only the bone's own lag, since nothing restores it,
|
|
464
|
+
* while resting below 0 the restoring term is added instead of taken out and
|
|
465
|
+
* the offset runs away — on the generated physics fixture, stepped at 60 fps
|
|
466
|
+
* from `Physics.reset`, a setup `strength` of −100 grew the offset 28.35× over
|
|
467
|
+
* 0.5 s with no sign change. The single sentence this replaced said "nothing
|
|
468
|
+
* pulls it back" of both, which sends an author reading the negative one to
|
|
469
|
+
* the wrong fix.
|
|
470
|
+
*/
|
|
471
|
+
outside: readonly PhysicsOutsideArm[] | null;
|
|
472
|
+
/**
|
|
473
|
+
* Why each way out of the bound is refused — one arm per way out, arithmetic
|
|
474
|
+
* or behavioural, disjoint and together covering every pose value `poseOk`
|
|
475
|
+
* refuses (issue #798). `A23`'s setup sentence prints the arm that holds for
|
|
476
|
+
* the value, and the row's `why` — the key's sentence — opens with the arms a
|
|
477
|
+
* key can still take. Until this field existed `src/types.ts` said of all four
|
|
478
|
+
* rows that "the bounds are the runtime's, not a policy", which is true of two
|
|
479
|
+
* of the eight ways out.
|
|
480
|
+
*/
|
|
481
|
+
basis: readonly PhysicsBoundBasis[];
|
|
482
|
+
/** The bound in words, for a message: what the value has to be. */
|
|
483
|
+
states: string;
|
|
484
|
+
/** The bound a KEY is held to, where `keyOk` widens it. */
|
|
485
|
+
statesKeyed: string;
|
|
486
|
+
/**
|
|
487
|
+
* What the runtime does outside the bound, with the lines that say so. It
|
|
488
|
+
* OPENS with the basis of the ways out a key can take — the arithmetic
|
|
489
|
+
* arm's expression, or the behavioural arm's "refusing it is rigc's call"
|
|
490
|
+
* and what the value does (issue #798) — quoted off the same `basis` objects
|
|
491
|
+
* the setup sentence prints, so the two cannot say different things.
|
|
492
|
+
*
|
|
493
|
+
* ⚠️ It is the sentence a **key** is refused with — `physicsKeyRefusal` is
|
|
494
|
+
* this field's only reader, and the setup pose's own wording lives beside
|
|
495
|
+
* `A23` in `validate.ts`. So a row whose `keyOk` widens the bound states here
|
|
496
|
+
* what is wrong with the values a key can still be refused for, not what is
|
|
497
|
+
* wrong with the value the widening admitted (issue #727).
|
|
498
|
+
*/
|
|
499
|
+
why: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* `strength`'s two ways out of its bound — two different rigs, so two sentences
|
|
504
|
+
* (issue #748). Resting at 0 there is no restoring force and the offset is only
|
|
505
|
+
* the bone's own lag; below 0 the restoring term has the wrong sign, so the
|
|
506
|
+
* offset feeds its own velocity and runs away. The row's `outside` is this array
|
|
507
|
+
* and its `why` quotes the first arm, which is the one a key can still take.
|
|
508
|
+
*/
|
|
509
|
+
const STRENGTH_OUTSIDE: readonly PhysicsOutsideArm[] = [
|
|
510
|
+
{
|
|
511
|
+
when: (v) => v < 0,
|
|
512
|
+
says:
|
|
513
|
+
'below 0 the restoring term is ADDED to the offset instead of taken out of it, so the offset is pushed ' +
|
|
514
|
+
'away and grows with every step',
|
|
515
|
+
},
|
|
516
|
+
{ when: (v) => v === 0, says: 'nothing pulls it back' },
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The eight ways out of the four bounds, each with its basis (issue #798). They
|
|
521
|
+
* are named constants rather than literals inside the rows because each row's
|
|
522
|
+
* `why` quotes its own — the key's sentence and `A23`'s setup sentence are then
|
|
523
|
+
* one text about one number, as `STRENGTH_OUTSIDE` already made them for two.
|
|
524
|
+
*
|
|
525
|
+
* 📏 Every witness below was stepped through spine-core 4.3.13 on the generated
|
|
526
|
+
* physics fixture — `x` and `y` driven, 120 steps from `Physics.reset` at 60 fps
|
|
527
|
+
* under an animation that swings the constraint's bone and brings it back — as
|
|
528
|
+
* the constraint's SETUP value: `mass` 0 is NaN at step 1 (`xVelocity`, and the
|
|
529
|
+
* bone's `worldX` with it); `mass` −1 is finite on every step and runs the
|
|
530
|
+
* offset away to 2.7e6; `strength` 0 and −5 are finite; `mix` 0 poses the bone
|
|
531
|
+
* exactly where the rig with no constraint does, on every step; `mix` −0.5 is
|
|
532
|
+
* finite and moves the bone by exactly −1× what +0.5 moves it by; `damping` 2 is
|
|
533
|
+
* finite over the walk (Infinity only at step 1,065); `damping` −0.5 at 45 fps is
|
|
534
|
+
* NaN at step 2. `T113` re-takes all eight on every run.
|
|
535
|
+
*/
|
|
536
|
+
const MIX_BELOW_0: PhysicsBoundBasis = {
|
|
537
|
+
kind: 'behavioural',
|
|
538
|
+
when: (v) => v < 0,
|
|
539
|
+
witness: { value: -0.5 },
|
|
540
|
+
does:
|
|
541
|
+
'below 0 the jiggle is applied inverted, on `x` and `y` exactly the offset a positive mix of the same size ' +
|
|
542
|
+
'applies, mirrored (`PhysicsConstraint.js:172,174`), and `PhysicsConstraintPose` documents mix as ' +
|
|
543
|
+
'"a percentage (0+)" where a transform constraint documents its own as "unbounded"',
|
|
544
|
+
};
|
|
545
|
+
const MIX_AT_0: PhysicsBoundBasis = {
|
|
546
|
+
kind: 'behavioural',
|
|
547
|
+
when: (v) => v === 0,
|
|
548
|
+
witness: { value: 0 },
|
|
549
|
+
does: 'at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted',
|
|
550
|
+
};
|
|
551
|
+
const MASS_AT_0: PhysicsBoundBasis = {
|
|
552
|
+
kind: 'arithmetic',
|
|
553
|
+
// The pose holds 1 / mass, so a mass of 0 arrives as Infinity (−0 as −Infinity).
|
|
554
|
+
when: (v) => !Number.isFinite(v),
|
|
555
|
+
witness: { value: 0 },
|
|
556
|
+
expression:
|
|
557
|
+
'at 0 `massInverse = 1 / mass` is Infinity and `m = t * massInverse` multiplies every velocity update, so the ' +
|
|
558
|
+
'first step takes every velocity and offset to NaN',
|
|
559
|
+
lines: 'SkeletonJson.js:309, Animation.js:2140, PhysicsConstraint.js:149,156,211',
|
|
560
|
+
};
|
|
561
|
+
const MASS_BELOW_0: PhysicsBoundBasis = {
|
|
562
|
+
kind: 'behavioural',
|
|
563
|
+
when: (v) => Number.isFinite(v) && v < 0,
|
|
564
|
+
witness: { value: -1 },
|
|
565
|
+
does:
|
|
566
|
+
'below 0 `massInverse` is a finite negative, which flips the sign of every force the velocity update applies, ' +
|
|
567
|
+
'so the restoring force pushes the offset away and it grows with every step',
|
|
568
|
+
};
|
|
569
|
+
const STRENGTH_BELOW_0: PhysicsBoundBasis = {
|
|
570
|
+
kind: 'behavioural',
|
|
571
|
+
when: STRENGTH_OUTSIDE[0].when,
|
|
572
|
+
witness: { value: -5 },
|
|
573
|
+
does: STRENGTH_OUTSIDE[0].says,
|
|
574
|
+
};
|
|
575
|
+
const STRENGTH_AT_0: PhysicsBoundBasis = {
|
|
576
|
+
kind: 'behavioural',
|
|
577
|
+
when: STRENGTH_OUTSIDE[1].when,
|
|
578
|
+
witness: { value: 0 },
|
|
579
|
+
does: STRENGTH_OUTSIDE[1].says,
|
|
580
|
+
};
|
|
581
|
+
const DAMPING_ABOVE_1: PhysicsBoundBasis = {
|
|
582
|
+
kind: 'behavioural',
|
|
583
|
+
when: (v) => v > 1,
|
|
584
|
+
witness: { value: 2 },
|
|
585
|
+
does: 'above 1 every velocity grows on every step and the offset diverges',
|
|
586
|
+
};
|
|
587
|
+
const DAMPING_BELOW_0: PhysicsBoundBasis = {
|
|
588
|
+
kind: 'arithmetic',
|
|
589
|
+
when: (v) => v < 0,
|
|
590
|
+
// 45 rather than 60: at 60 the exponent is exactly 1 and a negative base is
|
|
591
|
+
// finite, which is the whole of #748.
|
|
592
|
+
witness: { value: -0.5, fps: 45 },
|
|
593
|
+
expression: 'at any fps where `60 / fps` is not whole it is a negative number raised to a fractional power, which is NaN',
|
|
594
|
+
lines: 'PhysicsConstraint.js:148,210',
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Every physics property with a bound, and **only** those — each bound either
|
|
599
|
+
* where the runtime's arithmetic fails or where the value runs and runs wrongly,
|
|
600
|
+
* and each row's `basis` says which.
|
|
601
|
+
*
|
|
602
|
+
* 🚫 `inertia`, `wind` and `gravity` are absent on purpose. The runtime
|
|
603
|
+
* documents no range for any of them and the integrator diverges on none:
|
|
604
|
+
* `inertia` scales how much bone movement is converted (`PhysicsConstraint.js:137,143`)
|
|
605
|
+
* so 0 is an inert frame and nothing worse, and `wind`/`gravity` are forces along
|
|
606
|
+
* the skeleton's own vectors (`:151-153, :214-215`) where a negative number is the
|
|
607
|
+
* other direction — the corpus keys `wind` at −27.4 through −12.6 on all 48 of its
|
|
608
|
+
* wind keys. Inventing a bound for them would refuse correct data, which is the
|
|
609
|
+
* failure this repository has already paid for twice (issues #44, #262).
|
|
610
|
+
*
|
|
611
|
+
* 🚫 There is no UPPER bound on `mix` either, for the same reason and a stronger
|
|
612
|
+
* one: `PhysicsConstraintPose` documents it as "a percentage (0+)", and on `x`
|
|
613
|
+
* and `y` a keyed mix of 1.5 changes nothing inside the integration at all — it
|
|
614
|
+
* multiplies the finished offset onto the bone (`:172,174,251,253`), so it is an
|
|
615
|
+
* over-mix and an over-mix is a real idiom (the same argument
|
|
616
|
+
* `CONSTRAINT_TIMELINES` makes for a transform mix).
|
|
617
|
+
*
|
|
618
|
+
* ⚠️ On `rotate` and `shearX` that is not the whole of it, and the sentence this
|
|
619
|
+
* replaced said it was (issue #798): `mr = (rotate + shearX) * mix` (`:188`) is
|
|
620
|
+
* read INSIDE the rotation solve (`:190,192,230`), so there mix changes what is
|
|
621
|
+
* integrated as well as how much of it is applied. [measured] on the generated
|
|
622
|
+
* physics fixture with `rotate: 1` added, 120 steps from `Physics.reset` at
|
|
623
|
+
* 60 fps: a setup mix of −0.5 leaves `xOffset` equal to the mix-1 run's while
|
|
624
|
+
* `rotateOffset` peaks at 1.4151 against the mix-1 run's 1.7994. Finite either
|
|
625
|
+
* way, which is all the `mix` row's basis claims.
|
|
626
|
+
*
|
|
627
|
+
* 🔎 Each row's `basis` says, per way out, whether the runtime's arithmetic
|
|
628
|
+
* fails there or the value runs and rigc refuses what it does (issue #798). Of
|
|
629
|
+
* the eight ways out, two are arithmetic — `mass` at 0 and `damping` below 0 —
|
|
630
|
+
* and six are rigc's call.
|
|
631
|
+
*/
|
|
632
|
+
export const PHYSICS_POSE_RULES: PhysicsPoseRule[] = [
|
|
633
|
+
{
|
|
634
|
+
timeline: 'mix',
|
|
635
|
+
field: 'mix',
|
|
636
|
+
toPose: (v) => v,
|
|
637
|
+
poseOk: (v) => v > 0,
|
|
638
|
+
keyOk: (v) => v >= 0,
|
|
639
|
+
inertAtSetup: true,
|
|
640
|
+
outside: null,
|
|
641
|
+
// 🔑 Both ways out are BEHAVIOURAL (issue #798), and the negative one is the
|
|
642
|
+
// question #798 asked: a transform constraint resting at a negative mix is
|
|
643
|
+
// accepted (`T105`) and a physics one is refused. [measured] the arithmetic
|
|
644
|
+
// is the same on both sides, a signed scale of what the constraint applies
|
|
645
|
+
// and finite on every step: on the generated physics fixture a setup mix of
|
|
646
|
+
// −0.5 moves the bone by exactly −1× what +0.5 moves it by, step for step,
|
|
647
|
+
// and a transform constraint at `mixRotate` −0.5 rotates its bone by exactly
|
|
648
|
+
// −1× what +0.5 does. So neither the refusal nor the acceptance is the
|
|
649
|
+
// runtime's arithmetic. What differs is the runtime's own DOCUMENTED range,
|
|
650
|
+
// and each rule follows its own: `PhysicsConstraintPose.mix` is "a
|
|
651
|
+
// percentage (0+)" and `TransformConstraintPose.mixRotate` is "a percentage
|
|
652
|
+
// (unbounded)". Neither is wrong; both are rigc's call, made on the
|
|
653
|
+
// runtime's text rather than on its arithmetic, and `T114` holds the
|
|
654
|
+
// mirror, the finiteness and both documented ranges against the runtime.
|
|
655
|
+
basis: [MIX_BELOW_0, MIX_AT_0],
|
|
656
|
+
states: '> 0',
|
|
657
|
+
statesKeyed: '>= 0',
|
|
658
|
+
// A key is refused below 0 only, so the key's sentence is that arm's.
|
|
659
|
+
why:
|
|
660
|
+
`${physicsBasisSays(MIX_BELOW_0)}. 0 is a key the runtime has a branch for: \`update\` returns immediately ` +
|
|
661
|
+
'(`PhysicsConstraint.js:109-111`), which mutes the constraint for the span',
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
timeline: 'mass',
|
|
665
|
+
field: 'massInverse',
|
|
666
|
+
toPose: (v) => 1 / v,
|
|
667
|
+
poseOk: (v) => Number.isFinite(v) && v > 0,
|
|
668
|
+
keyOk: null,
|
|
669
|
+
inertAtSetup: false,
|
|
670
|
+
outside: null,
|
|
671
|
+
// The one row with a way out of each kind: 0 is the runtime's arithmetic
|
|
672
|
+
// and below 0 is rigc's call, so the key's sentence says both, in that order.
|
|
673
|
+
basis: [MASS_AT_0, MASS_BELOW_0],
|
|
674
|
+
states: '> 0',
|
|
675
|
+
statesKeyed: '> 0',
|
|
676
|
+
why: `${physicsBasisSays(MASS_AT_0)}; ${physicsBasisSays(MASS_BELOW_0)}`,
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
timeline: 'strength',
|
|
680
|
+
field: 'strength',
|
|
681
|
+
toPose: (v) => v,
|
|
682
|
+
poseOk: (v) => v > 0,
|
|
683
|
+
keyOk: (v) => v >= 0,
|
|
684
|
+
inertAtSetup: false,
|
|
685
|
+
outside: STRENGTH_OUTSIDE,
|
|
686
|
+
basis: [STRENGTH_BELOW_0, STRENGTH_AT_0],
|
|
687
|
+
states: '> 0',
|
|
688
|
+
statesKeyed: '>= 0',
|
|
689
|
+
// The key's sentence names the arm a key can still take — below 0 — off the
|
|
690
|
+
// same object the setup sentence reads, so the two say one thing about it.
|
|
691
|
+
why:
|
|
692
|
+
`${physicsBasisSays(STRENGTH_BELOW_0)}. It is the restoring force, \`velocity += (a - offset * strength) * m\` ` +
|
|
693
|
+
'(`PhysicsConstraint.js:150,156,212,220`), and a multiplicand everywhere it is read. 0 is a key the runtime plays: ' +
|
|
694
|
+
'it releases the constraint for the span, damping and inertia still apply, and the next key pulls the offset back',
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
timeline: 'damping',
|
|
698
|
+
field: 'damping',
|
|
699
|
+
toPose: (v) => v,
|
|
700
|
+
poseOk: (v) => v >= 0 && v <= 1,
|
|
701
|
+
keyOk: null,
|
|
702
|
+
inertAtSetup: false,
|
|
703
|
+
outside: null,
|
|
704
|
+
// Below 0 is arithmetic at every rate where the exponent is fractional;
|
|
705
|
+
// above 1 is a run-away, finite until the velocity overflows (step 1,065
|
|
706
|
+
// for a setup of 2 at 60 fps), so it is behavioural by the line
|
|
707
|
+
// `PhysicsBoundBasis` draws — the card that asked for the bases (#798)
|
|
708
|
+
// counted it arithmetic, and the walk says otherwise.
|
|
709
|
+
basis: [DAMPING_ABOVE_1, DAMPING_BELOW_0],
|
|
710
|
+
states: 'inside [0, 1]',
|
|
711
|
+
statesKeyed: 'inside [0, 1]',
|
|
712
|
+
// 🔑 The interval is CLOSED (issue #794). `1 ** x` is 1 and `0 ** x` is 0
|
|
713
|
+
// for every positive exponent, so both ends are finite at every `fps` and
|
|
714
|
+
// both are rigs somebody can mean: 1 holds the jiggle, 0 follows the bone
|
|
715
|
+
// with no overshoot. [measured] through spine-core on the generated physics
|
|
716
|
+
// fixture, 120 steps from `Physics.reset` at 60, 45 and 30 fps under a
|
|
717
|
+
// displacing animation, as a setup value and as a key: every pose value
|
|
718
|
+
// finite, the velocity never 0 at 1 and 0 after every step at 0. And 1 is
|
|
719
|
+
// not only a choice: it is 4.2's own parser default for an omitted
|
|
720
|
+
// `damping` (`SkeletonJson.js:242` in 4.2.120,
|
|
721
|
+
// `getValue(constraintMap, "damping", 1)`; 4.3.13 reads 0.85 at `:308`), so a
|
|
722
|
+
// rig migrated from 4.2 that keys "the default" writes 1 and the editor
|
|
723
|
+
// exports it as it is. The open interval this replaced refused that rig.
|
|
724
|
+
//
|
|
725
|
+
// ⚠️ The frame-rate half is why the bound cannot be checked by playing a rig
|
|
726
|
+
// at 60 fps (issue #748): `step` is `1 / fps`, the constraint's own rate, so
|
|
727
|
+
// the exponent is exactly 1 there and a negative damping only flips the
|
|
728
|
+
// velocity's sign. [measured] on the generated physics fixture, a keyed −0.5
|
|
729
|
+
// stays finite at 60 fps and at 30 (exponent 2), and is NaN within three
|
|
730
|
+
// steps of the key at 45 and at 120 (exponents 1.3333 and 0.5).
|
|
731
|
+
why:
|
|
732
|
+
'the per-step decay is `damping ** (60 * step)`, with `step` = 1 / the constraint\'s `fps`, and every velocity ' +
|
|
733
|
+
`is multiplied by it (\`PhysicsConstraint.js:114,148,158,163,210,222,227\`), so ${DAMPING_ABOVE_1.does}, at ` +
|
|
734
|
+
"every rate — finite on every step until the velocity overflows, so refusing it is rigc's call rather than the " +
|
|
735
|
+
"runtime's. Below 0 the result depends on `fps`: where `60 / fps` is a " +
|
|
736
|
+
'whole number a negative base stays finite — at 60 fps it is the velocity\'s sign flipped each step, which can ' +
|
|
737
|
+
`look like a jiggle settling — and ${DAMPING_BELOW_0.expression} (\`(-0.5) ** (60 / 45)\`), so a rig tried only at 60 fps never shows the failure. The two ends are ` +
|
|
738
|
+
'values the runtime plays at every rate: at 1 the velocity never decays, so the jiggle holds for as long as it ' +
|
|
739
|
+
'runs, and at 0 every velocity is zeroed on every step, so the offset follows the bone with no overshoot',
|
|
740
|
+
},
|
|
741
|
+
];
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* What `A23`'s setup arm says about a pose value its rule refuses: the arm of
|
|
745
|
+
* `outside` that holds for it, or the bound itself where the row has no arms or
|
|
746
|
+
* none holds (a non-number the parser handed over, say).
|
|
747
|
+
*/
|
|
748
|
+
export function physicsOutsideSays(rule: PhysicsPoseRule, poseValue: number): string {
|
|
749
|
+
return rule.outside?.find((arm) => arm.when(poseValue))?.says ?? `must be ${rule.states}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** The rule for one timeline name, or `undefined` where the runtime bounds nothing. */
|
|
753
|
+
export function physicsRuleFor(timeline: string): PhysicsPoseRule | undefined {
|
|
754
|
+
return PHYSICS_POSE_RULES.find((rule) => rule.timeline === timeline);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Whether the number a KEY states is one the runtime can use, judged on the pose
|
|
759
|
+
* field it becomes rather than on itself.
|
|
760
|
+
*
|
|
761
|
+
* `null` when it is. The string is the tail of a message and names the bound and
|
|
762
|
+
* the reason, never just "invalid".
|
|
763
|
+
*/
|
|
764
|
+
export function physicsKeyRefusal(rule: PhysicsPoseRule, value: number, posedBy?: number): string | null {
|
|
765
|
+
// ⚠️ `posedBy` exists so the VALIDATOR can hand over the number the runtime's
|
|
766
|
+
// own `PhysicsConstraint*Timeline.set` wrote, rather than rigc's reading of
|
|
767
|
+
// what that call does. The compiler cannot: it links no runtime, by the rule in
|
|
768
|
+
// CLAUDE.md, so it passes nothing and `toPose` answers. Those are two paths to
|
|
769
|
+
// one number and a selftest control measures that they agree — without it this
|
|
770
|
+
// parameter would be exactly the silent second opinion this table exists to
|
|
771
|
+
// remove.
|
|
772
|
+
const posed = posedBy ?? rule.toPose(value);
|
|
773
|
+
const ok = rule.keyOk ?? rule.poseOk;
|
|
774
|
+
if (ok(posed)) return null;
|
|
775
|
+
// The pose field only when it is a different number from the keyed one, which
|
|
776
|
+
// is derived rather than declared: it is exactly `mass`, and only where the
|
|
777
|
+
// reciprocal has moved.
|
|
778
|
+
const shown = posed === value ? '' : ` (${rule.field} ${posed})`;
|
|
779
|
+
return `${value}${shown}; must be ${rule.statesKeyed} — ${rule.why}`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** One of the three colours a slot poses: the light colour's rgb, its alpha, and the dark colour. */
|
|
783
|
+
export type SlotColorChannel = 'rgb' | 'alpha' | 'dark';
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Which of a slot's colour channels each of the format's five colour timelines
|
|
787
|
+
* poses — the `propertyIds` each class registers in `Animation.js`
|
|
788
|
+
* (`Property.rgb`, `Property.alpha`, `Property.rgb2`), under the names an author
|
|
789
|
+
* reads them by.
|
|
790
|
+
*
|
|
791
|
+
* ⭐ **It is the whole of what makes `rgb` + `alpha` a different thing from
|
|
792
|
+
* `rgba`**, and the reason it is a table rather than a fact every reader knows:
|
|
793
|
+
* `RGBTimeline.apply1` writes `color.r/g/b` and never touches `color.a`,
|
|
794
|
+
* `AlphaTimeline.apply` writes `color.a` alone, and `RGB2Timeline` writes the
|
|
795
|
+
* light rgb and the dark colour and leaves the light alpha where it was. A
|
|
796
|
+
* timeline that poses a channel poses it at EVERY time — before its first key
|
|
797
|
+
* it writes the setup value (`MixFrom.setup`) — so two timelines of one slot
|
|
798
|
+
* that share a channel are not two layers of one colour: the one applied later,
|
|
799
|
+
* which is the one the file states later, overwrites the other everywhere, and
|
|
800
|
+
* the earlier one's keys on that channel are read by nothing.
|
|
801
|
+
*
|
|
802
|
+
* It lives here, beside the catalogue, for `PHYSICS_POSE_RULES`' reason:
|
|
803
|
+
* `compile.ts` refuses two tracks that share a channel, `A45` names the same
|
|
804
|
+
* pair on a file rigc did not write, and the two have to be one criterion. The
|
|
805
|
+
* compiler links no runtime, so it cannot ask a timeline for its ids; a selftest
|
|
806
|
+
* control asks the linked runtime instead and holds this table to what it says.
|
|
807
|
+
*/
|
|
808
|
+
export const SLOT_COLOR_CHANNELS: Record<string, readonly SlotColorChannel[]> = {
|
|
809
|
+
rgba: ['rgb', 'alpha'],
|
|
810
|
+
rgb: ['rgb'],
|
|
811
|
+
alpha: ['alpha'],
|
|
812
|
+
rgba2: ['rgb', 'alpha', 'dark'],
|
|
813
|
+
rgb2: ['rgb', 'dark'],
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* The seven modes a `sequence` key may state, in the runtime's own enum order
|
|
818
|
+
* (`SequenceMode` in `attachments/Sequence.js`: `hold` 0 … `pingpongReverse` 6).
|
|
819
|
+
*
|
|
820
|
+
* 🚨 **The parser does not refuse a mode it does not know.** `readAnimation`
|
|
821
|
+
* reads `SequenceMode[getValue(keyMap, "mode", "hold")]`, which is `undefined`
|
|
822
|
+
* for a spelling outside the seven (and for a NUMBER, because the enum's reverse
|
|
823
|
+
* mapping turns `3` into the string `"pingpong"`), and `setFrame` then stores
|
|
824
|
+
* `undefined | (index << 4)` — mode bits 0, which is `hold`. Measured: a key
|
|
825
|
+
* spelled `"pingPong"` loads without a word and shows its `index` frame for
|
|
826
|
+
* the whole key. So every reader of a mode refuses anything outside this list,
|
|
827
|
+
* by this list.
|
|
828
|
+
*
|
|
829
|
+
* It lives here for `SLOT_COLOR_CHANNELS`' reason: the motion parser refuses an
|
|
830
|
+
* unknown mode in a spec, `A46` names one in a file rigc did not write, `ingest`
|
|
831
|
+
* carries one, and the four have to be one list. The compiler links no runtime,
|
|
832
|
+
* so a selftest control asks the linked one for its enum and holds this to it.
|
|
833
|
+
*/
|
|
834
|
+
export const SEQUENCE_MODES = ['hold', 'once', 'loop', 'pingpong', 'onceReverse', 'loopReverse', 'pingpongReverse'] as const;
|
|
835
|
+
|
|
836
|
+
/** One of `SEQUENCE_MODES`. */
|
|
837
|
+
export type SequenceModeName = (typeof SEQUENCE_MODES)[number];
|