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
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct 5 of the core, third cut (issue #938, step 2e-iii of issue
|
|
3
|
+
* #380): the slider constraint at the setup pose and at a sample time, with
|
|
4
|
+
* its timelines. A slider applies an animation — the one constraint kind
|
|
5
|
+
* that writes bones' LOCAL values and slots rather than a world transform.
|
|
6
|
+
*
|
|
7
|
+
* Every rule below was measured by posing hand-written skeletons through
|
|
8
|
+
* `tools/pose_oracle.ts dump` (spine-core 4.3.13, `--skin all`,
|
|
9
|
+
* `--physics none`) and reading the rows back — a bone's local values through
|
|
10
|
+
* `localSource` transform constraints written onto spare bones, a slot's row
|
|
11
|
+
* as the dump prints it — then held at tolerance 0 by the core suite's `CQ`
|
|
12
|
+
* controls. Nothing here was written from the runtime's source.
|
|
13
|
+
*
|
|
14
|
+
* ## Where it stands: the update order
|
|
15
|
+
*
|
|
16
|
+
* A slider is one more constraint in the document's order (`./constraints.ts`,
|
|
17
|
+
* *The update order*): it reads its dial bone as the constraints before it
|
|
18
|
+
* left it, writes the local values of the bones its animation keys, and those
|
|
19
|
+
* bones and everything below them are posed again before the next
|
|
20
|
+
* constraint. **Every bone its animation keys is posed again, whether or not
|
|
21
|
+
* a timeline wrote it** (issue #989): a slider before a timeline's first key
|
|
22
|
+
* writes nothing (below), and the bone is still posed again from its local
|
|
23
|
+
* values — which is observable only on a bone an earlier constraint moved in
|
|
24
|
+
* world space, whose world transform the runtime then rebuilds from the
|
|
25
|
+
* local values read back from it (`localFromWorld` in `./constraints.ts`,
|
|
26
|
+
* last-bit lossy against the world it came from). Measured on the three rigs
|
|
27
|
+
* of `CQ06`'s population the lcg never drew (each a world-space transform on
|
|
28
|
+
* `b2`, then a slider before its first key keying `b2` or its parent): `b2`'s
|
|
29
|
+
* y column read 1e-7 relative off with the world kept, exact with it posed
|
|
30
|
+
* again; so did every bone below. A slider at `mix` 0 poses nothing again
|
|
31
|
+
* (the same three at mix 0 read exact only without it), nor does a bone
|
|
32
|
+
* the animation names with no timeline (a `"b2": {}` entry). `CZ01` holds
|
|
33
|
+
* the three, and plants the reading before.
|
|
34
|
+
*
|
|
35
|
+
* The runtime's setup pose and every sample apply it: at setup
|
|
36
|
+
* the sliders compose on the setup pose; at a sample on the pose the sample's
|
|
37
|
+
* own animation left (applied at alpha 1 from the setup pose, construct 4),
|
|
38
|
+
* so **the sample's animation first, then each slider in constraint order**.
|
|
39
|
+
*
|
|
40
|
+
* ## The time it applies its animation at
|
|
41
|
+
*
|
|
42
|
+
* - **Bone-less**: the slider's `time` (a `slider` timeline's `time` key at a
|
|
43
|
+
* sample), as it is: a negative time is a time before the animation's keys.
|
|
44
|
+
* - **With a `bone`**: `to + (value − from) · scale`, `value` the dial's
|
|
45
|
+
* `property` read exactly as a transform constraint reads its source with
|
|
46
|
+
* no offset (`sourceValue` in `./constraints.ts`): `local` the local field
|
|
47
|
+
* as earlier constraints left it, unwrapped; world `rotate` the x column's
|
|
48
|
+
* angle brought into [0, 360); world `x`, `y` the origin; world `scaleX`,
|
|
49
|
+
* `scaleY` the column lengths; world `shearY` the angle between the columns
|
|
50
|
+
* less 90. Measured on a dial under a parent turned 30° and scaled 2 in x:
|
|
51
|
+
* a local 10° at `scale` 0.01 read time 0.1, a world 10° read 0.350384 (the
|
|
52
|
+
* x column at 35.0384°), a world −50° read 359.21° (0.35921 at 0.001).
|
|
53
|
+
* Then `max(0, time)` — a dial at −50° applied the first frame — or, with
|
|
54
|
+
* `loop`, `duration + (time mod duration)`: 3 over a 2 s animation read 1,
|
|
55
|
+
* −0.5 read 1.5. A stated `time` beside a `bone` is not read (the bone won).
|
|
56
|
+
* - The animation is then applied at that time, wrapped `time mod duration`
|
|
57
|
+
* when `loop` (a bone-less 3.25 over 1 s read 0.25, which is before the
|
|
58
|
+
* first key at 0.5 and so applied nothing), `duration` being the runtime's
|
|
59
|
+
* (`./animation.ts`, the last key time as float32). ⚠️ A dial looping over
|
|
60
|
+
* an animation of duration 0 computes `0 + (time mod 0)`, NaN — and the
|
|
61
|
+
* runtime still applied the animation's one key (a rotate keyed 45 on a
|
|
62
|
+
* bone at 20 read 65, as the same slider without `loop` and the bone-less
|
|
63
|
+
* form looping at 0 and at 0.5 did): every key of such an animation sits
|
|
64
|
+
* at 0, so the core applies it at 0. The compiler refuses the shape (A37);
|
|
65
|
+
* a foreign file can still carry it.
|
|
66
|
+
*
|
|
67
|
+
* ## How it composes: the current pose, the mix, `additive`
|
|
68
|
+
*
|
|
69
|
+
* Each timeline of the slider's animation, in the animation's order, blends
|
|
70
|
+
* from the CURRENT pose — whatever the setup, the sample's animation and the
|
|
71
|
+
* constraints before it left — with `alpha` the slider's `mix`:
|
|
72
|
+
*
|
|
73
|
+
* - **Before the timeline's first key it writes nothing** (the current pose
|
|
74
|
+
* stands: a slider at 0.2 over a rotate keyed from 0.5 left 20° at setup
|
|
75
|
+
* and the sample's 50° alike). Not the setup value, as a sample's own
|
|
76
|
+
* animation writes (construct 4).
|
|
77
|
+
* - **`mix` 0 applies nothing at all** — not even an attachment key.
|
|
78
|
+
* - `rotate`, `translate`/`x`/`y`, `shear`/`x`/`y`: `current + (setup + v −
|
|
79
|
+
* current) · mix`; **`additive`**: `current + v · mix`. Measured on setup
|
|
80
|
+
* rotation 20 and a sample at 50, the slider's value 150: 170 at mix 1,
|
|
81
|
+
* 110 at 0.5, −10 at −0.5 (no clamp); additive 200 and 125. No wrap to the
|
|
82
|
+
* short way round: 50 toward 290 at 0.3 read 122.
|
|
83
|
+
* - `scale`/`x`/`y`: the target is `setup · v`; `current' + (target −
|
|
84
|
+
* current') · mix` where `current'` is |current| with the TARGET's sign — a
|
|
85
|
+
* setup `scaleY` 0.5 toward −0.5 at mix 0.5 read −0.5, not 0 — and ⚠️ at
|
|
86
|
+
* mix exactly 1 the target itself (issue #989: `current' + (target −
|
|
87
|
+
* current')·1` reads 11 of `CZ02`'s 50 absolute bones at mix 1 last-bit
|
|
88
|
+
* off under `--raw`, the target none); **additive**: `current + (v · setup
|
|
89
|
+
* − setup) · mix` (a current 6 on setup 2, v 3 read 10; issue #989:
|
|
90
|
+
* `(v − 1) · setup · mix`, the reading before, reads 65 of `CZ02`'s 250
|
|
91
|
+
* additive bones last-bit off, this none). The other kinds read 1,122 of
|
|
92
|
+
* 1,122 probes of issue #989's scratch population bit-exact at every mix,
|
|
93
|
+
* over the setup and over a current an earlier slider moved, and were left
|
|
94
|
+
* as they are.
|
|
95
|
+
* - `inherit`: the key's mode, from its first key on (`CQ06` holds it).
|
|
96
|
+
* - Slot colours (`rgba`, `rgb`, `alpha`, `rgba2`, `rgb2`): each channel the
|
|
97
|
+
* timeline names moves from the current value toward the key's by the mix
|
|
98
|
+
* and is then clamped to [0, 1] — ⚠️ at mix exactly 1 it IS the key's value:
|
|
99
|
+
* `current + (v − current)·1` read 1 ulp off spine-core on 30 of 13,950
|
|
100
|
+
* slot readings of issue #969's hand-written population (`DM01`), the key's
|
|
101
|
+
* value on none (a mix of −1 read alpha 0, of 2 read
|
|
102
|
+
* 0.87451 from 0.12549 toward 0.5); `additive` changes nothing for them.
|
|
103
|
+
* - `attachment`: the key's placeholder from its first key on, whatever the
|
|
104
|
+
* mix above 0 (0.01 switched it); `null` shows nothing.
|
|
105
|
+
*
|
|
106
|
+
* ## Its timelines
|
|
107
|
+
*
|
|
108
|
+
* A sample's animation may key a slider's `time` and `mix` (one channel
|
|
109
|
+
* each, construct 4's key search and curves, float32 key values). Before the
|
|
110
|
+
* first key the slider's own value; a key omitting `value` reads 1 (the
|
|
111
|
+
* compiler's note on the parser, measured again by `CQ07`). A bone-driven
|
|
112
|
+
* slider ignores a `time` key: its time is its dial's.
|
|
113
|
+
*
|
|
114
|
+
* The keyed value reaches the slider through the setup blend at alpha 1,
|
|
115
|
+
* `own + (value − own)·1` with `own` the slider's own `time` or `mix` —
|
|
116
|
+
* the rule the ik, transform and path timelines follow (`./constraints.ts`,
|
|
117
|
+
* *The timelines*). The two agree on the grid and part in the last bit: a
|
|
118
|
+
* mix keyed linearly from 0.462 to 0.152 read 0.3521456338160804 as keyed
|
|
119
|
+
* and 0.35214563381608044 through the blend at one sample, and the
|
|
120
|
+
* partial-mix blend of a shear key then carried that ulp into the pose (issue
|
|
121
|
+
* #991, which found it on a looped dial; the loop, the dial and the
|
|
122
|
+
* translate key it was reduced with each turned out incidental — the same
|
|
123
|
+
* rig without them misses on 44 to 71 of 200 draws of its two mix values as
|
|
124
|
+
* keyed and on none through the blend). Measured on the issue's scratch
|
|
125
|
+
* population — 500 rigs of one or two sliders, looped and unlooped dials and
|
|
126
|
+
* bone-less ones, over own mixes in [−1, 2], mix keys and time keys in
|
|
127
|
+
* linear, stepped and Bézier segments, under `--raw` at tolerance 0 — the
|
|
128
|
+
* value as keyed read 351 of 500 exact, the blend on `mix` alone 465, on
|
|
129
|
+
* `time` alone 375, on both 500. `CZ03` holds a reduced rig and plants the
|
|
130
|
+
* value as keyed.
|
|
131
|
+
*
|
|
132
|
+
* ## What is left out, by name
|
|
133
|
+
*
|
|
134
|
+
* A slider whose animation keys a constraint timeline (ik, transform, path,
|
|
135
|
+
* slider — writing a later constraint's pose, issue #665's case) is not
|
|
136
|
+
* posed: the document's bones are absent, naming it; so is one whose
|
|
137
|
+
* animation deforms a curve a path constraint walks (unmeasured). A slider's
|
|
138
|
+
* deform, sequence and draw-order keys are posed since issue #955, after the
|
|
139
|
+
* sample's own (`./deform.ts`, `./draw_order.ts`; they were `sliderAttachmentsWhy`'s
|
|
140
|
+
* absence before). Its physics timelines are posed since issue #1049 (below),
|
|
141
|
+
* and events a slider does not fire, so neither leaves anything out.
|
|
142
|
+
*
|
|
143
|
+
* ## Its physics timelines (issue #1049)
|
|
144
|
+
*
|
|
145
|
+
* Under `Physics.none` a physics constraint applies nothing
|
|
146
|
+
* (`./constraints_physics.ts`), so neither do a slider's physics keys. Under
|
|
147
|
+
* the step they do, and until issue #1049 the core applied none of them: on
|
|
148
|
+
* the selftest's own builds where a dial's animation keys `wind` or
|
|
149
|
+
* `gravity` of a constraint declared after it, every bone the constraint
|
|
150
|
+
* drives left spine-core from the third step (`tip` worldX
|
|
151
|
+
* 14.87037037037037 against 12.092592592592593 at 0.025 s), and with a slot
|
|
152
|
+
* on that bone `render` drew wrong frames with no refusal. Measured against
|
|
153
|
+
* spine-core 4.3.13's output only — A10's looping walk, `render`'s recipe at
|
|
154
|
+
* 60 and 12 fps and the oracle's stepped grid under `--raw`, on 400 compiled
|
|
155
|
+
* rigs of one or two sliders and one or two physics constraints in random
|
|
156
|
+
* order, every kind keyed, at tolerance 0 — the rule is:
|
|
157
|
+
*
|
|
158
|
+
* - **A slider writes its physics keys into the pass's physics records, at
|
|
159
|
+
* its place in the update order**, at the time it applies its animation at
|
|
160
|
+
* (`at`), after the step's own animation posed them; a physics constraint
|
|
161
|
+
* after it steps with what it wrote, one before it has already stepped.
|
|
162
|
+
* Applying the writes to every constraint of the pass whatever its place
|
|
163
|
+
* read 239 of the 400 rigs off; not applying them, 205.
|
|
164
|
+
* - **A write lasts one pass.** The next step's records start again from the
|
|
165
|
+
* step's own animation: a write that stood where the step's animation does
|
|
166
|
+
* not key read 323 of 400 off.
|
|
167
|
+
* - **Before the timeline's first key it writes nothing**, as for bones.
|
|
168
|
+
* - **`wind`, `gravity` under `additive`: `current + v·mix`.** Every other
|
|
169
|
+
* value (`inertia`, `strength`, `damping`, `mass`, `mix`), and every value
|
|
170
|
+
* of a slider that is not additive: `current + (v − current)·mix`, with
|
|
171
|
+
* `current` the value as the pass holds it (the step's own animation and
|
|
172
|
+
* any slider before). Adding every kind read 104 of 400 off; blending from
|
|
173
|
+
* the setup value (before the step's own animation), 80; writing the key
|
|
174
|
+
* itself at mix exactly 1, 20 — in the last bit.
|
|
175
|
+
* - **A `mass` key blends the mass**, `1 / (m + (v − m)·mix)` with `m =
|
|
176
|
+
* 1 / massInverse`; blending the inverse read 28 of 400 off.
|
|
177
|
+
* - **A `reset` key fires nothing** — the slider applies its animation at one
|
|
178
|
+
* time, from that time, so no key is crossed (the path-slider suite's
|
|
179
|
+
* `PS159` measured the same on the pose); firing a key at or before `at` on
|
|
180
|
+
* every pass read 112 of 400 off.
|
|
181
|
+
* - The timeline naming no constraint (`*`) writes every active constraint
|
|
182
|
+
* whose `…Global` flag for that value is on, as a step's own does.
|
|
183
|
+
*
|
|
184
|
+
* `CO31` holds the rule on the `core_gate` probe rows and a seeded population
|
|
185
|
+
* on all three entries; `CO32` plants each rejected reading (`SolverRules`).
|
|
186
|
+
*
|
|
187
|
+
* ## Purity
|
|
188
|
+
*
|
|
189
|
+
* As the rest of the core: nothing from the Spine runtime package, nothing
|
|
190
|
+
* from `src/transform.ts`, no clock, no randomness, no I/O.
|
|
191
|
+
*/
|
|
192
|
+
import type { ModelBone } from '../model.ts';
|
|
193
|
+
import { channelAt, keyIndexAt, type CoreAnimationTimelines, type CoreCurve, type CoreKey } from './animation.ts';
|
|
194
|
+
import { sourceValue, TRANSFORM_PROPERTIES, type CoreConstraintRecord, type SolverRules, type SolverState, type TransformProperty } from './constraints.ts';
|
|
195
|
+
import type { CompiledDocument, CoreAnimation } from './index.ts';
|
|
196
|
+
import { EVERY_GLOBAL_PHYSICS, physicsActive, physicsState, resetPhysicsState, unnamedPhysicsTargets, type CorePhysicsRecord, type CorePhysicsTimeline, type PhysicsStepContext } from './constraints_physics.ts';
|
|
197
|
+
|
|
198
|
+
/** The fields a slider's record may carry after `kind`, `name`, `declaredIn` (`buildRigConstraint` in `src/compile.ts`). */
|
|
199
|
+
export const SLIDER_FIELDS = ['animation', 'additive', 'loop', 'mix', 'bone', 'property', 'from', 'to', 'scale', 'max', 'local', 'time', 'skin'] as const;
|
|
200
|
+
|
|
201
|
+
/** A slider, read: its animation's timelines and every setup value of the bones they key, so it can be applied with no other part of the document. */
|
|
202
|
+
export interface CoreSliderRecord {
|
|
203
|
+
kind: 'slider';
|
|
204
|
+
name: string;
|
|
205
|
+
animation: string;
|
|
206
|
+
timelines: CoreAnimationTimelines;
|
|
207
|
+
/** Its animation's physics timelines, in the animation's order (issue #1049) — set by `readModel` once the animations' constraint timelines are read, which is after the records. */
|
|
208
|
+
physics: readonly CorePhysicsTimeline[];
|
|
209
|
+
/** The setup bones its animation keys, by name — what a non-additive key's value is added to. */
|
|
210
|
+
setup: ReadonlyMap<string, ModelBone>;
|
|
211
|
+
additive: boolean;
|
|
212
|
+
loop: boolean;
|
|
213
|
+
mix: number;
|
|
214
|
+
time: number;
|
|
215
|
+
/** The dial, or `null` for the bone-less form. */
|
|
216
|
+
bone: string | null;
|
|
217
|
+
property: TransformProperty;
|
|
218
|
+
from: number;
|
|
219
|
+
to: number;
|
|
220
|
+
scale: number;
|
|
221
|
+
local: boolean;
|
|
222
|
+
skin: boolean;
|
|
223
|
+
/** An applied skin's `slider` list names it — what applies a skin-required one (`./constraints.ts`'s header); set per skin view by `underSkin` in `./index.ts`. */
|
|
224
|
+
listedBySkin: boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** One slider's timelines in one animation: the `time` and `mix` keys, `null` where not keyed. */
|
|
228
|
+
export interface CoreSliderTimeline {
|
|
229
|
+
name: string;
|
|
230
|
+
time: CoreKey[] | null;
|
|
231
|
+
mix: CoreKey[] | null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A slider as it was applied: its animation, at which time, with which alpha — what the slots are posed from after the bones. */
|
|
235
|
+
export interface SliderApplication {
|
|
236
|
+
name: string;
|
|
237
|
+
timelines: CoreAnimationTimelines;
|
|
238
|
+
at: number;
|
|
239
|
+
alpha: number;
|
|
240
|
+
/** The slider's `additive` — how its deform timelines blend (`./deform.ts`). */
|
|
241
|
+
additive: boolean;
|
|
242
|
+
/** #969: the time it stored — `sliderTime`, before the animation's loop wrap — what the runtime's `SliderPose.time` reads (`./hooks.ts`). */
|
|
243
|
+
time: number;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A slider's record, read field by field — every field the writer can write
|
|
250
|
+
* for the kind and no other, each of its type, the animation one of the
|
|
251
|
+
* document's, the dial a bone of it — or `undefined` with the problems named.
|
|
252
|
+
* An absent field reads the parser's value (the header): `mix` 1, `time`,
|
|
253
|
+
* `from`, `to` 0, `scale` 1, the flags false.
|
|
254
|
+
*/
|
|
255
|
+
export function readSliderRecord(raw: Record<string, unknown>, name: string, where: string, bones: readonly ModelBone[], animations: readonly CoreAnimation[], problems: string[]): CoreSliderRecord | undefined {
|
|
256
|
+
const before = problems.length;
|
|
257
|
+
for (const key of Object.keys(raw)) {
|
|
258
|
+
if (key === 'kind' || key === 'name' || key === 'declaredIn') continue;
|
|
259
|
+
if (!(SLIDER_FIELDS as readonly string[]).includes(key)) problems.push(`${where}: field "${key}" is not one this reader knows; it reads [${SLIDER_FIELDS.join(', ')}]`);
|
|
260
|
+
}
|
|
261
|
+
const num = (key: string, dflt: number): number => {
|
|
262
|
+
const v = raw[key];
|
|
263
|
+
if (v === undefined) return dflt;
|
|
264
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
265
|
+
problems.push(`${where}: ${key} is ${JSON.stringify(v)}, not a finite number`);
|
|
266
|
+
return dflt;
|
|
267
|
+
}
|
|
268
|
+
return v;
|
|
269
|
+
};
|
|
270
|
+
const flag = (key: string): boolean => {
|
|
271
|
+
const v = raw[key];
|
|
272
|
+
if (v === undefined) return false;
|
|
273
|
+
if (typeof v !== 'boolean') {
|
|
274
|
+
problems.push(`${where}: ${key} is ${JSON.stringify(v)}, not a boolean`);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return v;
|
|
278
|
+
};
|
|
279
|
+
const anim = animations.find((a) => a.name === raw.animation);
|
|
280
|
+
if (anim === undefined) problems.push(`${where}: a slider's animation is ${JSON.stringify(raw.animation)}, not an animation of this document`);
|
|
281
|
+
const byName = new Map(bones.map((b) => [b.name, b]));
|
|
282
|
+
let bone: string | null = null;
|
|
283
|
+
if (raw.bone !== undefined) {
|
|
284
|
+
if (typeof raw.bone !== 'string' || !byName.has(raw.bone)) problems.push(`${where}: bone is ${JSON.stringify(raw.bone)}, not a bone of this document`);
|
|
285
|
+
else bone = raw.bone;
|
|
286
|
+
}
|
|
287
|
+
let property: TransformProperty = 'rotate';
|
|
288
|
+
if (raw.property !== undefined) {
|
|
289
|
+
const p = TRANSFORM_PROPERTIES.find((x) => x === raw.property);
|
|
290
|
+
if (p === undefined) problems.push(`${where}: property is ${JSON.stringify(raw.property)}, none of ${TRANSFORM_PROPERTIES.join(', ')}`);
|
|
291
|
+
else property = p;
|
|
292
|
+
}
|
|
293
|
+
num('max', 0);
|
|
294
|
+
const loop = flag('loop');
|
|
295
|
+
const setup = new Map<string, ModelBone>();
|
|
296
|
+
for (const target of anim?.timelines.bones ?? []) {
|
|
297
|
+
const b = byName.get(target.name);
|
|
298
|
+
if (b !== undefined) setup.set(b.name, b);
|
|
299
|
+
}
|
|
300
|
+
const record: CoreSliderRecord = {
|
|
301
|
+
kind: 'slider', name, animation: anim?.name ?? '', timelines: anim?.timelines ?? { declared: 0, duration: 0, bones: [], slots: [], later: [], attachments: [], drawOrder: [], events: [] }, physics: [], setup,
|
|
302
|
+
additive: flag('additive'), loop, mix: num('mix', 1), time: num('time', 0), bone, property,
|
|
303
|
+
from: num('from', 0), to: num('to', 0), scale: num('scale', 1), local: flag('local'), skin: flag('skin'), listedBySkin: false,
|
|
304
|
+
};
|
|
305
|
+
return problems.length === before ? record : undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The slider timelines of one animation record's `constraints.slider`, read:
|
|
310
|
+
* each names a declared slider, each of its timelines is `time` or `mix` and
|
|
311
|
+
* appears once, its keys strictly increase in time and carry `time`, `value`
|
|
312
|
+
* and `curve` only.
|
|
313
|
+
*/
|
|
314
|
+
export function readSliderTimelines(value: unknown, label: string, declared: ReadonlyArray<{ kind: string; name: string }>, problems: string[]): CoreSliderTimeline[] {
|
|
315
|
+
const out: CoreSliderTimeline[] = [];
|
|
316
|
+
if (!Array.isArray(value)) return out;
|
|
317
|
+
value.forEach((entry, i) => {
|
|
318
|
+
const at = `${label}.constraints.slider[${i}]`;
|
|
319
|
+
if (!isRecord(entry) || typeof entry.name !== 'string') {
|
|
320
|
+
problems.push(`${at} names no slider`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!declared.some((c) => c.kind === 'slider' && c.name === entry.name)) problems.push(`${at}: "${entry.name}" is not a slider of this document`);
|
|
324
|
+
const tl: CoreSliderTimeline = { name: entry.name, time: null, mix: null };
|
|
325
|
+
if (!Array.isArray(entry.timelines)) {
|
|
326
|
+
problems.push(`${at}: timelines is not a list`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
entry.timelines.forEach((raw: unknown, j: number) => {
|
|
330
|
+
const tat = `${at}.timelines[${j}]`;
|
|
331
|
+
if (!isRecord(raw) || (raw.name !== 'time' && raw.name !== 'mix')) {
|
|
332
|
+
problems.push(`${tat}: ${JSON.stringify(isRecord(raw) ? raw.name : raw)} is not a slider timeline; a slider keys time and mix`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const which = raw.name;
|
|
336
|
+
if (tl[which] !== null) problems.push(`${tat}: "${which}" is keyed twice on slider "${entry.name}"`);
|
|
337
|
+
tl[which] = readKeys(raw.keys, `${tat} "${which}"`, problems);
|
|
338
|
+
});
|
|
339
|
+
out.push(tl);
|
|
340
|
+
});
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function readKeys(value: unknown, at: string, problems: string[]): CoreKey[] {
|
|
345
|
+
const keys: CoreKey[] = [];
|
|
346
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
347
|
+
problems.push(`${at}: keys is not a non-empty list`);
|
|
348
|
+
return keys;
|
|
349
|
+
}
|
|
350
|
+
let last = -Infinity;
|
|
351
|
+
value.forEach((k: unknown, j: number) => {
|
|
352
|
+
const kat = `${at}.keys[${j}]`;
|
|
353
|
+
if (!isRecord(k)) {
|
|
354
|
+
problems.push(`${kat} is not an object`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
for (const key of Object.keys(k)) if (key !== 'time' && key !== 'value' && key !== 'curve') problems.push(`${kat}: field "${key}" is not one this reader knows; it reads [time, value, curve]`);
|
|
358
|
+
const time = k.time;
|
|
359
|
+
if (typeof time !== 'number' || !Number.isFinite(time) || time < 0) {
|
|
360
|
+
problems.push(`${kat}: time is ${JSON.stringify(time)}, not a finite time at or after 0`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (time <= last) problems.push(`${kat}: time ${time} is not after the key before it — the writer refuses key times that do not strictly increase`);
|
|
364
|
+
last = Math.max(last, time);
|
|
365
|
+
let v = 1;
|
|
366
|
+
if (k.value !== undefined) {
|
|
367
|
+
if (typeof k.value !== 'number' || !Number.isFinite(k.value)) problems.push(`${kat}: value is ${JSON.stringify(k.value)}, not a finite number`);
|
|
368
|
+
else v = k.value;
|
|
369
|
+
}
|
|
370
|
+
let curve: CoreCurve = 'linear';
|
|
371
|
+
if (k.curve !== undefined) {
|
|
372
|
+
if (j === value.length - 1) problems.push(`${kat}: the last key carries a curve, which eases to no key — the writer refuses it`);
|
|
373
|
+
else if (k.curve === 'stepped') curve = 'stepped';
|
|
374
|
+
else if (Array.isArray(k.curve) && k.curve.length === 4 && k.curve.every((n) => typeof n === 'number' && Number.isFinite(n))) curve = k.curve as number[];
|
|
375
|
+
else problems.push(`${kat}: curve is ${JSON.stringify(k.curve)}, not "stepped" nor 4 finite numbers`);
|
|
376
|
+
}
|
|
377
|
+
keys.push({ time: Math.fround(time), values: [Math.fround(v)], stated: { time, values: [v] }, curve });
|
|
378
|
+
});
|
|
379
|
+
return keys;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** What evaluates one channel of a key and finds the key — construct 4's unless a plant passes others. */
|
|
383
|
+
export interface SliderTimelinePlant {
|
|
384
|
+
channel?: (keys: readonly CoreKey[], index: number, channel: number, t: number) => number;
|
|
385
|
+
search?: (keys: readonly CoreKey[], t: number) => number;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** A slider record with its timelines' `time` and `mix` at `t` in place of its own (the header's *Its timelines*). */
|
|
389
|
+
export function posedSlider(r: CoreSliderRecord, timelines: readonly CoreSliderTimeline[], t: number, plant: SliderTimelinePlant = {}): CoreSliderRecord {
|
|
390
|
+
const tl = timelines.find((x) => x.name === r.name);
|
|
391
|
+
if (tl === undefined) return r;
|
|
392
|
+
const search = plant.search ?? keyIndexAt;
|
|
393
|
+
const channel = plant.channel ?? channelAt;
|
|
394
|
+
// Through the setup blend at alpha 1, `own + (value − own)·1` (issue #991), as the ik, transform and path timelines are: the value as keyed read 351 of 500 of the issue's scratch population bit-exact under --raw, the blend 500 (CZ03).
|
|
395
|
+
const at = (keys: CoreKey[] | null, own: number): number => {
|
|
396
|
+
if (keys === null) return own;
|
|
397
|
+
const i = search(keys, t);
|
|
398
|
+
return i < 0 ? own : own + (channel(keys, i, 0, t) - own) * 1;
|
|
399
|
+
};
|
|
400
|
+
return { ...r, time: at(tl.time, r.time), mix: at(tl.mix, r.mix) };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** The time a slider applies its animation at (the header's rule), before the animation's own loop wrap. */
|
|
404
|
+
export function sliderTime(state: SolverState, r: CoreSliderRecord): number {
|
|
405
|
+
if (r.bone === null) return r.time;
|
|
406
|
+
const value = sourceValue(state, { source: r.bone, localSource: r.local, offsets: ZERO_OFFSETS }, r.property);
|
|
407
|
+
const time = r.to + (value - r.from) * r.scale;
|
|
408
|
+
const d = r.timelines.duration;
|
|
409
|
+
// Looping over a duration of 0 is NaN in the runtime, and it applied the keys: every key of such an animation sits at 0.
|
|
410
|
+
if (r.loop && d === 0) return 0;
|
|
411
|
+
return r.loop ? d + (time % d) : Math.max(0, time);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const ZERO_OFFSETS: Record<TransformProperty, number> = { rotate: 0, x: 0, y: 0, scaleX: 0, scaleY: 0, shearY: 0 };
|
|
415
|
+
|
|
416
|
+
/** The channels of a timeline at `t`, or `null` before its first key — when a slider writes nothing. */
|
|
417
|
+
function valuesAt(keys: readonly CoreKey[], t: number): number[] | null {
|
|
418
|
+
const i = keyIndexAt(keys, t);
|
|
419
|
+
if (i < 0) return null;
|
|
420
|
+
return keys[i].values.map((_v, c) => channelAt(keys, i, c, t));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Apply one slider to the solver's bones (the header's composition rules):
|
|
425
|
+
* its local values move, and the bones written are returned for the update
|
|
426
|
+
* loop to pose again with everything below them. The application is pushed
|
|
427
|
+
* onto `applied` for the slots.
|
|
428
|
+
*/
|
|
429
|
+
export function applySlider(state: SolverState, r: CoreSliderRecord, applied?: SliderApplication[], physics?: SliderPhysicsTarget): string[] {
|
|
430
|
+
if (r.mix === 0) return [];
|
|
431
|
+
const time = sliderTime(state, r);
|
|
432
|
+
const d = r.timelines.duration;
|
|
433
|
+
const at = r.loop && d !== 0 ? time % d : time;
|
|
434
|
+
applied?.push({ name: r.name, timelines: r.timelines, at, alpha: r.mix, additive: r.additive, time });
|
|
435
|
+
const alpha = r.mix;
|
|
436
|
+
if (physics !== undefined && state.rules.sliderWritesPhysics) applySliderPhysics(r, at, alpha, physics);
|
|
437
|
+
const changed: string[] = [];
|
|
438
|
+
for (const target of r.timelines.bones) {
|
|
439
|
+
const index = state.index.get(target.name);
|
|
440
|
+
const setup = r.setup.get(target.name);
|
|
441
|
+
if (index === undefined || setup === undefined) continue;
|
|
442
|
+
const b = state.bones[index];
|
|
443
|
+
let wrote = false;
|
|
444
|
+
for (const tl of target.timelines) {
|
|
445
|
+
if (tl.kind === 'inherit') {
|
|
446
|
+
const i = keyIndexAt(tl.keys, at);
|
|
447
|
+
if (i < 0) continue;
|
|
448
|
+
b.inheritMode = tl.keys[i].mode;
|
|
449
|
+
wrote = true;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const v = valuesAt(tl.keys, at);
|
|
453
|
+
if (v === null) continue;
|
|
454
|
+
wrote = true;
|
|
455
|
+
const add = (field: 'x' | 'y' | 'rotation' | 'shearX' | 'shearY', i: number): void => {
|
|
456
|
+
const current = b[field] ?? 0;
|
|
457
|
+
b[field] = r.additive ? current + v[i] * alpha : current + ((setup[field] ?? 0) + v[i] - current) * alpha;
|
|
458
|
+
};
|
|
459
|
+
const times = (field: 'scaleX' | 'scaleY', i: number): void => {
|
|
460
|
+
const current = b[field] ?? 1;
|
|
461
|
+
const s = setup[field] ?? 1;
|
|
462
|
+
if (r.additive) {
|
|
463
|
+
// #989: the key's product with the setup less the setup, then the mix — `(v − 1)·setup·mix` reads last-bit off (CZ02).
|
|
464
|
+
b[field] = state.rules.sliderAdditiveScaleProduct ? current + (v[i] * s - s) * alpha : current + (v[i] - 1) * s * alpha;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const target = s * v[i];
|
|
468
|
+
// #989: at mix exactly 1 the target itself — `from + (target − from)·1` reads last-bit off (CZ02).
|
|
469
|
+
if (alpha === 1 && state.rules.sliderScaleMixOneIsTarget) {
|
|
470
|
+
b[field] = target;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const from = Math.abs(current) * Math.sign(target);
|
|
474
|
+
b[field] = from + (target - from) * alpha;
|
|
475
|
+
};
|
|
476
|
+
switch (tl.kind) {
|
|
477
|
+
case 'rotate': add('rotation', 0); break;
|
|
478
|
+
case 'translate': add('x', 0); add('y', 1); break;
|
|
479
|
+
case 'translatex': add('x', 0); break;
|
|
480
|
+
case 'translatey': add('y', 0); break;
|
|
481
|
+
case 'shear': add('shearX', 0); add('shearY', 1); break;
|
|
482
|
+
case 'shearx': add('shearX', 0); break;
|
|
483
|
+
case 'sheary': add('shearY', 0); break;
|
|
484
|
+
case 'scale': times('scaleX', 0); times('scaleY', 1); break;
|
|
485
|
+
case 'scalex': times('scaleX', 0); break;
|
|
486
|
+
case 'scaley': times('scaleY', 0); break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// #989: a bone the animation keys is posed again whether or not a timeline wrote it (the header's update order).
|
|
490
|
+
if (wrote || (state.rules.sliderReposesKeyedBones && target.timelines.length > 0)) changed.push(target.name);
|
|
491
|
+
}
|
|
492
|
+
return changed;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The physics records a stepped pass carries, by name — what a slider's physics timelines write and a later physics constraint steps with (issue #1049, the header's *Its physics timelines*). */
|
|
496
|
+
export interface SliderPhysicsTarget {
|
|
497
|
+
records: Map<string, CorePhysicsRecord>;
|
|
498
|
+
/** Each record's setup values, before the step's own animation — what a planted blend from the setup reads. */
|
|
499
|
+
setup: ReadonlyMap<string, CorePhysicsRecord>;
|
|
500
|
+
active: (r: CorePhysicsRecord) => boolean;
|
|
501
|
+
rules: Readonly<SolverRules>;
|
|
502
|
+
ctx: PhysicsStepContext;
|
|
503
|
+
/** What the sliders wrote this pass, by constraint and field — kept only under the planted reading that a write outlasts its pass. */
|
|
504
|
+
written: Map<string, Partial<Record<PhysicsField, number>>>;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** The fields of a physics record a timeline writes. */
|
|
508
|
+
type PhysicsField = 'inertia' | 'strength' | 'damping' | 'massInverse' | 'wind' | 'gravity' | 'mix';
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The physics records of one stepped pass, ready for the sliders to write
|
|
512
|
+
* (issue #1049): every physics record as the step's own animation posed it.
|
|
513
|
+
* Under the planted reading that a slider's write outlasts its pass, what
|
|
514
|
+
* the sliders wrote on the step before stands wherever the step's own
|
|
515
|
+
* animation does not key it (`PhysicsStepContext.keyedNow`).
|
|
516
|
+
*/
|
|
517
|
+
export function sliderPhysicsTarget(records: readonly CoreConstraintRecord[], active: ReadonlySet<string>, ctx: PhysicsStepContext, rules: Readonly<SolverRules>): SliderPhysicsTarget {
|
|
518
|
+
const posed = new Map(records.flatMap((r): Array<[string, CorePhysicsRecord]> => (r.kind === 'physics' ? [[r.name, r]] : [])));
|
|
519
|
+
const out = new Map(posed);
|
|
520
|
+
const written = new Map<string, Partial<Record<PhysicsField, number>>>();
|
|
521
|
+
if (!rules.sliderPhysicsLastsOnePass) {
|
|
522
|
+
for (const [name, fields] of ctx.carried ?? new Map<string, Partial<Record<PhysicsField, number>>>()) {
|
|
523
|
+
const r = out.get(name);
|
|
524
|
+
if (r === undefined) continue;
|
|
525
|
+
const next = { ...r };
|
|
526
|
+
const kept: Partial<Record<PhysicsField, number>> = {};
|
|
527
|
+
for (const [f, v] of Object.entries(fields) as Array<[PhysicsField, number]>) {
|
|
528
|
+
if (ctx.keyedNow?.has(`${name}/${f}`)) continue;
|
|
529
|
+
next[f] = v;
|
|
530
|
+
kept[f] = v;
|
|
531
|
+
}
|
|
532
|
+
out.set(name, next);
|
|
533
|
+
written.set(name, kept);
|
|
534
|
+
}
|
|
535
|
+
ctx.carried = written;
|
|
536
|
+
}
|
|
537
|
+
return { records: out, setup: ctx.setupRecords ?? posed, active: (r) => physicsActive(r, active), rules, ctx, written };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** The value kinds a slider's physics timeline adds under `additive` (`./additive.ts`'s table); the rest write. */
|
|
541
|
+
const PHYSICS_ADDS: ReadonlySet<string> = new Set(['wind', 'gravity']);
|
|
542
|
+
|
|
543
|
+
/** A slider's physics timelines applied at `at` with alpha `alpha` onto the pass's physics records (the header's *Its physics timelines*). */
|
|
544
|
+
function applySliderPhysics(r: CoreSliderRecord, at: number, alpha: number, target: SliderPhysicsTarget): void {
|
|
545
|
+
const rules = target.rules;
|
|
546
|
+
for (const tl of r.physics) {
|
|
547
|
+
const named = target.records.get(tl.name);
|
|
548
|
+
const targets = tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets([...target.records.values()], tl.kind, target.active) : named !== undefined && target.active(named) ? [named] : [];
|
|
549
|
+
if (tl.kind === 'reset') {
|
|
550
|
+
// A slider applies its animation at one time, from that time: no key is crossed, so a reset key fires nothing (PS159).
|
|
551
|
+
if (!rules.sliderPhysicsResetIsDead && tl.keys.some((k) => k.time <= at)) for (const p of targets) resetPhysicsState(physicsState(target.ctx, p.name), target.ctx.time);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const i = keyIndexAt(tl.keys, at);
|
|
555
|
+
if (i < 0) continue;
|
|
556
|
+
const v = channelAt(tl.keys, i, 0, at);
|
|
557
|
+
const adds = r.additive && (PHYSICS_ADDS.has(tl.kind) || !rules.sliderPhysicsAddsWindGravityOnly);
|
|
558
|
+
for (const p of targets) {
|
|
559
|
+
const from = rules.sliderPhysicsFromCurrent ? p : (target.setup.get(p.name) as CorePhysicsRecord);
|
|
560
|
+
const next = { ...p };
|
|
561
|
+
let field: PhysicsField;
|
|
562
|
+
if (tl.kind === 'mass') {
|
|
563
|
+
field = 'massInverse';
|
|
564
|
+
if (rules.sliderPhysicsBlendsMass) {
|
|
565
|
+
const m = 1 / from.massInverse;
|
|
566
|
+
next.massInverse = 1 / (adds ? 1 / p.massInverse + v * alpha : m + (v - m) * alpha);
|
|
567
|
+
} else next.massInverse = adds ? p.massInverse + (1 / v) * alpha : from.massInverse + (1 / v - from.massInverse) * alpha;
|
|
568
|
+
} else {
|
|
569
|
+
field = tl.kind;
|
|
570
|
+
next[field] = adds ? p[field] + v * alpha : from[field] + (v - from[field]) * alpha;
|
|
571
|
+
}
|
|
572
|
+
target.records.set(p.name, next);
|
|
573
|
+
if (!rules.sliderPhysicsLastsOnePass) target.written.set(p.name, { ...target.written.get(p.name), [field]: next[field] });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** A slot's pose as the sliders move it: what it shows, its light colour and its dark colour (`null` when it states none). */
|
|
579
|
+
export interface SlotPoseState {
|
|
580
|
+
placeholder: string | null;
|
|
581
|
+
light: number[];
|
|
582
|
+
dark: number[] | null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
586
|
+
/** Each colour timeline's channels, as indices into `light` (0–3) and `dark` (4–6). */
|
|
587
|
+
const COLOUR_CHANNELS: Record<string, readonly number[]> = { rgba: [0, 1, 2, 3], rgb: [0, 1, 2], alpha: [3], rgba2: [0, 1, 2, 3, 4, 5, 6], rgb2: [0, 1, 2, 4, 5, 6] };
|
|
588
|
+
|
|
589
|
+
/** Every slider application's slot timelines on one slot, in order (the header's slot rules). */
|
|
590
|
+
export function applySliderSlots(slot: string, pose: SlotPoseState, applications: readonly SliderApplication[]): void {
|
|
591
|
+
for (const app of applications) {
|
|
592
|
+
for (const target of app.timelines.slots) {
|
|
593
|
+
if (target.name !== slot) continue;
|
|
594
|
+
for (const tl of target.timelines) {
|
|
595
|
+
if (tl.kind === 'attachment') {
|
|
596
|
+
const i = keyIndexAt(tl.keys, app.at);
|
|
597
|
+
if (i >= 0) pose.placeholder = tl.keys[i].name ?? null;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const v = valuesAt(tl.keys, app.at);
|
|
601
|
+
if (v === null) continue;
|
|
602
|
+
COLOUR_CHANNELS[tl.kind].forEach((at, i) => {
|
|
603
|
+
// #969: at mix 1 the key's value itself, not `current + (v − current)·1`, which is last-bit off it (measured, below).
|
|
604
|
+
if (at < 4) pose.light[at] = app.alpha === 1 ? clamp01(v[i]) : clamp01(pose.light[at] + (v[i] - pose.light[at]) * app.alpha);
|
|
605
|
+
else if (pose.dark !== null) pose.dark[at - 4] = app.alpha === 1 ? clamp01(v[i]) : clamp01(pose.dark[at - 4] + (v[i] - pose.dark[at - 4]) * app.alpha);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Why the document's bones cannot be posed because of a slider, or null: a
|
|
614
|
+
* slider whose animation keys a constraint timeline other than physics, or
|
|
615
|
+
* deforms a curve a path constraint walks (the header's *What is left out*),
|
|
616
|
+
* named with the animation and the kinds or the attachments.
|
|
617
|
+
*/
|
|
618
|
+
export function sliderBonesWhy(doc: CompiledDocument): string | null {
|
|
619
|
+
const found: string[] = [];
|
|
620
|
+
for (const c of doc.constraints) {
|
|
621
|
+
if (c.kind !== 'slider') continue;
|
|
622
|
+
const anim = doc.animations.find((a) => a.name === c.animation);
|
|
623
|
+
if (anim === undefined) continue;
|
|
624
|
+
const k = anim.constraints;
|
|
625
|
+
const sliderTimelines = k.slider.reduce((n, s) => n + (s.time === null ? 0 : 1) + (s.mix === null ? 0 : 1), 0);
|
|
626
|
+
const paths = k.path.reduce((n, p) => n + [p.position, p.spacing, p.mix].filter((x) => x !== undefined).length, 0);
|
|
627
|
+
const kinds = [k.ik.length > 0 ? 'ik' : null, k.transform.length > 0 ? 'transform' : null, paths > 0 ? 'path' : null, sliderTimelines > 0 ? 'slider' : null].filter((x): x is string => x !== null);
|
|
628
|
+
if (kinds.length > 0) found.push(`slider "${c.name}" applies animation "${c.animation}", which keys ${kinds.join(', ')} constraint timelines — a slider writing a later constraint's pose is not posed by this cut`);
|
|
629
|
+
// A path constraint walks its slot's curve as the constraints before it left it; a slider's deform of that curve is not measured (issue #955).
|
|
630
|
+
const walked = doc.constraints.flatMap((p) => (p.record?.kind === 'path' ? [p.record.slot] : []));
|
|
631
|
+
const deformsPath = anim.timelines.attachments.filter((a) => a.deform !== null && walked.includes(a.slot) && doc.skins.find((s) => s.name === a.skin)?.attachments[a.slot]?.[a.attachment]?.geometry?.kind === 'path');
|
|
632
|
+
if (deformsPath.length > 0) found.push(`slider "${c.name}" applies animation "${c.animation}", which deforms walked path attachment(s) ${deformsPath.map((a) => `"${a.skin}/${a.slot}/${a.attachment}"`).join(', ')} — a slider's deform of a curve a path constraint walks is not posed by this cut`);
|
|
633
|
+
}
|
|
634
|
+
return found.length === 0 ? null : found.join('; ');
|
|
635
|
+
}
|