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,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct 5 of the core, third and fourth cuts (issue #938, step 2e-iii,
|
|
3
|
+
* and issue #956, step 2e-iv, of issue #380): the physics constraint — under
|
|
4
|
+
* `Physics.none`, the oracle's default phase, where it applies nothing, and
|
|
5
|
+
* stepped, `Physics.reset` then `Physics.update` at a fixed `dt`, where it
|
|
6
|
+
* integrates.
|
|
7
|
+
*
|
|
8
|
+
* ## What an unstepped physics constraint contributes: nothing
|
|
9
|
+
*
|
|
10
|
+
* Measured by posing hand-written skeletons through `tools/pose_oracle.ts
|
|
11
|
+
* dump` (spine-core 4.3.13, `--skin all`, `--physics none`) twice — once as
|
|
12
|
+
* written, once with every physics constraint and every physics timeline
|
|
13
|
+
* taken out — and comparing the two dumps at tolerance 0:
|
|
14
|
+
*
|
|
15
|
+
* - 400 one-constraint skeletons: the constrained bone in all five inherit
|
|
16
|
+
* modes under a reflecting, sheared, scaled parent, with amplifier bones ten
|
|
17
|
+
* thousand units out along its axes; each of the five components (`x`, `y`,
|
|
18
|
+
* `rotate`, `scaleX`, `shearX`) at 1, between 0 and 2 or negative; `limit`,
|
|
19
|
+
* `fps`, `inertia`, `strength`, `damping`, `mass`, `wind`, `gravity` and
|
|
20
|
+
* `mix` at random, each `…Global` flag at random; half of them with
|
|
21
|
+
* physics timelines keying `mix`, `wind` and `reset` in an animation that
|
|
22
|
+
* also swings the parent — sampled at seven `grid` or `irr` times, every
|
|
23
|
+
* sample posed on the ONE skeleton the dump reuses: **400 of 400
|
|
24
|
+
* identical**.
|
|
25
|
+
* - 462 nine-bone rigs whose two to six constraints mix physics constraints
|
|
26
|
+
* with ik and world- and local-space transform constraints in random order
|
|
27
|
+
* (so a physics constraint sits between a world-space edit and a constraint
|
|
28
|
+
* that re-poses it), physics timelines keying `mix` and `inertia`, five
|
|
29
|
+
* `irr` samples: **462 of 462 identical**, 41,577 bone-samples.
|
|
30
|
+
*
|
|
31
|
+
* So under `Physics.none` the physics constraint neither moves its bone nor
|
|
32
|
+
* re-poses anything below it, whatever its parameters, its `mix` or its
|
|
33
|
+
* timelines, and wherever it stands in the update order: `applyConstraints`
|
|
34
|
+
* is given no step context and applies nothing.
|
|
35
|
+
*
|
|
36
|
+
* ## The stepped phase, measured (issue #956)
|
|
37
|
+
*
|
|
38
|
+
* Every rule below was fixed the same way: hand-written skeletons dumped by
|
|
39
|
+
* `tools/pose_oracle.ts dump --physics step --dt <s>` (spine-core 4.3.13) and
|
|
40
|
+
* by the core, compared at tolerance 0, most with amplifier bones ten
|
|
41
|
+
* thousand units out so a difference of 1e-10 in a matrix reads as a grid
|
|
42
|
+
* step. spine-core's source was not read; each rule is the reading the dump
|
|
43
|
+
* left exact, and each is stated with the population that fixed it and the
|
|
44
|
+
* readings it rejected. The figures `n of 400` below are one population
|
|
45
|
+
* (seed 81 of the lab): 400 rigs of six bones under one to five constraints —
|
|
46
|
+
* physics with every component, setting, `…Global` flag and timeline at
|
|
47
|
+
* random, ik and transform among them, skin-required constraints, two
|
|
48
|
+
* animations, `dt` among 1/60, 1/30, 1/120 and a random 0.005–0.05 — exact
|
|
49
|
+
* on 400 of 400 (63,982 bone-samples) under the rules as written, and the
|
|
50
|
+
* count is how many stayed exact with ONE rule replaced by the reading named.
|
|
51
|
+
* The core suite's `CK` controls hold the probes.
|
|
52
|
+
*
|
|
53
|
+
* **Defaults.** A field the record leaves out reads the parser's value, read
|
|
54
|
+
* off the oracle's `physics` block for a constraint stating nothing: every
|
|
55
|
+
* component 0, `limit` 5000, `fps` 60 (`step` = 1 / fps: 45 read 0.022222),
|
|
56
|
+
* `inertia` 0.5, `strength` 100, `damping` 0.85, `mass` 1 (the block holds
|
|
57
|
+
* `massInverse`: 3 read 0.333333), `wind`, `gravity` 0, `mix` 1, every flag
|
|
58
|
+
* false, `scaleY` `None`. The core writes that block from its records
|
|
59
|
+
* (`physicsRows`), under both phases.
|
|
60
|
+
*
|
|
61
|
+
* **The schedule** (`stepSchedule`) is the oracle's, from its header: a
|
|
62
|
+
* fresh state per animation, the animation applied at 0 and the skeleton
|
|
63
|
+
* reset; then from each sample's predecessor `p`, a step to `p + k·dt` while
|
|
64
|
+
* that is before `t`, then to `t`. The skeleton's clock is the SUM of the
|
|
65
|
+
* steps' deltas (`s − previous s`), not `s`: the constraint reads its delta
|
|
66
|
+
* off that clock. The setup pose under the step is the reset pose. Each step
|
|
67
|
+
* applies the animation from the time the walk last applied it at
|
|
68
|
+
* (`PhysicsStepContext.last`, −1 before the first), as a player does — which
|
|
69
|
+
* decides only when a `reset` key fires (*reset*, below).
|
|
70
|
+
*
|
|
71
|
+
* **Which constraints step.** A constraint on an inactive bone does not. A
|
|
72
|
+
* constraint with `skin: true` steps exactly when an applied skin's
|
|
73
|
+
* `constraints.physics` list names it — under `--skin all` every skin is
|
|
74
|
+
* applied at once, under `--skin <name>` the named skin alone
|
|
75
|
+
* (`listedBySkin`, set per skin view by `underSkin` in `./index.ts`;
|
|
76
|
+
* `physicsActive`). Measured on the
|
|
77
|
+
* commander's private-corpus finding (two rows DIFF, the core leaving such
|
|
78
|
+
* constraints inert): hand-written documents dumped under `--physics step`
|
|
79
|
+
* with `--skin all`, `--skin s1` and `--skin default` — named by no skin:
|
|
80
|
+
* not stepped under any; named by `s1`: stepped under `all` and `s1`, not
|
|
81
|
+
* under `default`; named by the default skin: stepped (under `all`, and —
|
|
82
|
+
* issue #932 — under `default` but not under `s1`); a skin naming the
|
|
83
|
+
* constrained bone (skin-required or not), another physics constraint or an
|
|
84
|
+
* ik constraint, but not this one: not stepped; no skins at all: not
|
|
85
|
+
* stepped. The reading inherited from issue #938 — `skin: true` is never
|
|
86
|
+
* applied under `--skin all` — was measured under `Physics.none`, where a
|
|
87
|
+
* physics constraint applies nothing either way; it is rejected (`CK13`
|
|
88
|
+
* plants it and turns the listed probe red). A
|
|
89
|
+
* constraint whose `mix` is 0 does nothing at all at that update — not even
|
|
90
|
+
* read its clock: a constraint muted from 0 to 0.3 s and then keyed to 1
|
|
91
|
+
* integrates the whole 0.3 s on its first live update after the pending one
|
|
92
|
+
* (sack-pro's `fall-in`; advancing the clock at `mix` 0 kept 394 of 400). A
|
|
93
|
+
* constraint never updated is pending, like a reset one: its first update
|
|
94
|
+
* takes the bone where it is and integrates nothing (not pending kept 399 of
|
|
95
|
+
* 400 and turned sack-pro red).
|
|
96
|
+
*
|
|
97
|
+
* **The components.** Each is on when it is ABOVE 0: `x`, `y`, `scaleX`, and
|
|
98
|
+
* the rotation when `rotate` or `shearX` is (reading any non-zero as on kept
|
|
99
|
+
* 279 of 400). A negative `rotate` or `shearX` still weighs the rotation: its
|
|
100
|
+
* weight is `(rotate + shearX)·mix` (dropping the negative kept 369 of 400).
|
|
101
|
+
*
|
|
102
|
+
* **The step** (`stepPhysics`), in the order measured. `reset` clears every
|
|
103
|
+
* offset, velocity and lag, zeroes what remains and sets the constraint's
|
|
104
|
+
* clock to now; it is also what a `reset` key does, while the animation is
|
|
105
|
+
* applied — before the step's delta is added. Then:
|
|
106
|
+
*
|
|
107
|
+
* - `delta` is the clock since the constraint last read it (never below 0),
|
|
108
|
+
* added to what remains. A pending constraint takes the bone's origin as
|
|
109
|
+
* the one it follows and integrates nothing; what remains is kept.
|
|
110
|
+
* - `x`, `y`: the offset gains `(previous origin − origin)·inertia`, held to
|
|
111
|
+
* `±limit·delta`. Then while at least one fixed `step` remains: `velocity
|
|
112
|
+
* += (wind·R − offset·strength)·massInverse·step` (`y`: `−= (gravity·R +
|
|
113
|
+
* offset·strength)·…`), `offset += velocity·step`, `velocity *=
|
|
114
|
+
* damping^(60·step)` (`damping` itself kept 268 of 400; exact at 60 fps,
|
|
115
|
+
* where the exponent is 1), and one step is taken off. What remains is
|
|
116
|
+
* carried to the next update (dropping it kept 116 of 400; integrating once
|
|
117
|
+
* per update at `dt` in place of `step` kept 116).
|
|
118
|
+
* - The offset shown is `offset − lag·(1 − remaining/step)` (at least 0),
|
|
119
|
+
* `lag` the change the integration just made: the pose between two fixed
|
|
120
|
+
* steps (showing the integrated offset kept 116 of 400). The bone's origin
|
|
121
|
+
* moves by it times `mix` times the component.
|
|
122
|
+
* - Rotation and shear follow the bone's tip: with `ca` the bone's x-axis
|
|
123
|
+
* angle and `(dx, dy)` how far its origin moved since the last update (each
|
|
124
|
+
* held to `±limit·delta`), the offset gains `atan2(dy + ty, dx + tx) − ca −
|
|
125
|
+
* (offset − lag·z₀)·w`, brought into (−π, π] with the runtime's π
|
|
126
|
+
* (3.1415927: `Math.PI` kept 369 of 400), times `inertia` — `(tx, ty)` the
|
|
127
|
+
* tip `length·(a, c)` the last update left, `w` the weight above, and `z₀`
|
|
128
|
+
* the SHOWN weight of the previous update: `1 − remaining/step` before
|
|
129
|
+
* this update's delta is added (after it kept 177 of 400; no lag at all
|
|
130
|
+
* kept 176). The spring then runs as for `x` with `−((wind·sin + gravity·
|
|
131
|
+
* cos)·length/R + offset·strength)·massInverse` — `sin`, `cos` of the
|
|
132
|
+
* shown angle, re-read after every step.
|
|
133
|
+
* - `scaleX` follows the tip along the bone: the offset gains `(dx·cos +
|
|
134
|
+
* dy·sin)·inertia / r`, `r = length·|x column|`, and springs with `(wind·cos
|
|
135
|
+
* − gravity·sin − offset·strength)·massInverse`. ⚠️ With no rotation on,
|
|
136
|
+
* `r` is `length·|x column| − lag·z₀` — a dimensionless lag taken from a
|
|
137
|
+
* length. It is the measured reading, not a derived one: the plain `r`
|
|
138
|
+
* left scale-only probes off by a relative `(inertia step)·lag/length`
|
|
139
|
+
* (`1 − 0.00020008` on a 50-unit bone moved one unit a step, `5e-5` at 100
|
|
140
|
+
* units, `8e-4` at 25), and the subtraction made them exact (the plain `r`
|
|
141
|
+
* kept 357 of 400; with rotation on the plain `r` is the exact one).
|
|
142
|
+
* - Applied last: the rotation `o = (offset − lag·z)·mix` turns both columns
|
|
143
|
+
* by `o·rotate`; with `shearX` on, the y column turns by `o·rotate` and
|
|
144
|
+
* the x column by `o·(rotate + shearX)` (the first reading turned the y
|
|
145
|
+
* column for the shear and read 0.049 off). The x column is scaled by `1 +
|
|
146
|
+
* (offset − lag·z)·mix·scaleX`. The tip `length·(a, c)` is kept for the next
|
|
147
|
+
* update.
|
|
148
|
+
* - After it the bone is a world edit, as a world-space transform constraint
|
|
149
|
+
* leaves one: its local values read back (`localFromWorld`) and every bone
|
|
150
|
+
* below it posed again — so a constraint whose components are all 0 but
|
|
151
|
+
* whose `mix` is not still re-derives its bone.
|
|
152
|
+
*
|
|
153
|
+
* **referenceScale.** `R` above is the skeleton's `referenceScale`. A Spine
|
|
154
|
+
* header stating 50 moved 30 of 50 wind-and-gravity probes, and moved none
|
|
155
|
+
* without them. The model document states it (`referenceScale`, issue
|
|
156
|
+
* #958): `readModel` refuses a document without it by name and stamps it on
|
|
157
|
+
* every physics record (`CorePhysicsRecord.referenceScale`), which is what
|
|
158
|
+
* the step reads. Until #958 the core read the parser's 100 as a constant.
|
|
159
|
+
*
|
|
160
|
+
* ## The timelines
|
|
161
|
+
*
|
|
162
|
+
* `inertia`, `strength`, `damping`, `mass`, `wind`, `gravity` and `mix`
|
|
163
|
+
* keys, one channel each, construct 4's key search and curves (key times and
|
|
164
|
+
* values float32), applied in the animation's order. Before the first key
|
|
165
|
+
* the setup value; after it `setup + (value − setup)` — the blend at alpha 1,
|
|
166
|
+
* not the value itself: the two differ by a unit in the last place, and a
|
|
167
|
+
* mass probe (a parent thrown 3e11 units) and a two-constraint wind-and-mix
|
|
168
|
+
* probe both read it (the value itself kept 400 of 400 in the population and
|
|
169
|
+
* turned both probes red). A `mass` key states a mass: the pose holds
|
|
170
|
+
* `1 / (m₀ + (value − m₀))`, `m₀ = 1 / massInverse`. A key omitting its
|
|
171
|
+
* value reads 0, a `mix` key 1 (`docs/AUTHORING.md` §4.4; a first key with
|
|
172
|
+
* no value, on each of the seven, read exact — the `mass` one NaN in both
|
|
173
|
+
* dumps). The timeline that
|
|
174
|
+
* names no constraint (`*`) writes every active constraint whose `…Global`
|
|
175
|
+
* flag for that value is on, and resets every active constraint. A
|
|
176
|
+
* slider's animation keys them too, at the slider's place in the update
|
|
177
|
+
* order (issue #1049, `./constraints_slider.ts` *Its physics timelines*).
|
|
178
|
+
*
|
|
179
|
+
* **reset.** A `reset` key resets its constraints on the step whose apply
|
|
180
|
+
* crosses it — a key in `(last, t]`, `last` the time the walk last applied
|
|
181
|
+
* the animation at, −1 before the first (`resetCrossed`) — so once, as a
|
|
182
|
+
* player crosses it (issue #960, measured against `Animation.apply(last,
|
|
183
|
+
* t)` and against `AnimationState` with one track, which agreed to the bit:
|
|
184
|
+
* `tools/pose_oracle.ts` §`--physics step`). Until issue #960 the oracle
|
|
185
|
+
* applied the animation from 0 at every step, and this walk mirrored it: a
|
|
186
|
+
* key reset its constraints at EVERY step from it on, holding the rig still
|
|
187
|
+
* (ignoring the key, or firing it once, kept 278 of 400 under that
|
|
188
|
+
* oracle). That
|
|
189
|
+
* reading is rejected as a schedule no player runs; the core suite's `SC`
|
|
190
|
+
* controls plant it and turn the probe red on the steps after the key.
|
|
191
|
+
*
|
|
192
|
+
* ## What is not exact, and why
|
|
193
|
+
*
|
|
194
|
+
* Over 3,000 random rigs of three shapes (all five inherit modes; ik and
|
|
195
|
+
* transform among them; skin-required constraints; the timeline naming no
|
|
196
|
+
* constraint) the step read exact on 2,986. The fourteen: three are wrong
|
|
197
|
+
* under `--physics none` too (a transform collapsing its bone's scale, and
|
|
198
|
+
* no step involved); one is a bone under an inactive parent, whose zero
|
|
199
|
+
* matrix both dumpers take to NaN by different routes; the other ten are
|
|
200
|
+
* last-bit differences upstream of the step, amplified by it — seven on a
|
|
201
|
+
* bone in `noScale`, `noScaleOrReflection` or `noRotationOrReflection`,
|
|
202
|
+
* whose world transform `./world.ts` reproduces to the grid but not to the
|
|
203
|
+
* bit (a probe amplified a billion units read one of those modes a few
|
|
204
|
+
* units in the last place on 61 of 61 samples, and the four other modes on
|
|
205
|
+
* none), two after `localFromWorld` re-derived a bone the step then moves
|
|
206
|
+
* again, and one under a constraint whose rotation weight is negative, where
|
|
207
|
+
* the inertia pushes away and any difference grows. Each is exact with the
|
|
208
|
+
* bones normal, or with the weight positive. The step itself holds no
|
|
209
|
+
* residual found.
|
|
210
|
+
*
|
|
211
|
+
* ## Purity
|
|
212
|
+
*
|
|
213
|
+
* As the rest of the core: nothing from the Spine runtime package, nothing
|
|
214
|
+
* from `src/transform.ts`, no clock, no randomness, no I/O.
|
|
215
|
+
*/
|
|
216
|
+
import { channelAt, keyIndexAt, poseAnimations, sampleTime, type CoreAnimationPose, type CoreCurve, type CoreKey, type SamplePhase, type TimelinePlant } from './animation.ts';
|
|
217
|
+
import { RUNTIME_PI, type CoreWorld } from './world.ts';
|
|
218
|
+
import { previousPassSlotBones, type CoreConstraintRecord } from './constraints.ts';
|
|
219
|
+
import { activeBones, type CompiledDocument } from './index.ts';
|
|
220
|
+
|
|
221
|
+
/** The fields a physics constraint's record may carry after `kind`, `name`, `declaredIn` — the rig branch of `buildRigConstraint` and the motion spec's physics table (`src/compile.ts`). */
|
|
222
|
+
export const PHYSICS_FIELDS = [
|
|
223
|
+
'bone', 'scaleY', 'x', 'y', 'rotate', 'scaleX', 'shearX', 'limit', 'fps', 'inertia', 'strength', 'damping', 'mass', 'wind', 'gravity', 'mix',
|
|
224
|
+
'inertiaGlobal', 'strengthGlobal', 'dampingGlobal', 'massGlobal', 'windGlobal', 'gravityGlobal', 'mixGlobal', 'skin',
|
|
225
|
+
] as const;
|
|
226
|
+
const PHYSICS_NUMBERS = ['x', 'y', 'rotate', 'scaleX', 'shearX', 'limit', 'fps', 'inertia', 'strength', 'damping', 'mass', 'wind', 'gravity', 'mix'] as const;
|
|
227
|
+
const PHYSICS_FLAGS = ['inertiaGlobal', 'strengthGlobal', 'dampingGlobal', 'massGlobal', 'windGlobal', 'gravityGlobal', 'mixGlobal', 'skin'] as const;
|
|
228
|
+
/** The `scaleY` modes, first letter in either case (the ik constraint's enum, which a physics constraint shares). */
|
|
229
|
+
const SCALE_Y_MODES = ['none', 'uniform', 'volume'] as const;
|
|
230
|
+
|
|
231
|
+
/** The five components a physics constraint drives, in the row's order. */
|
|
232
|
+
export const PHYSICS_COMPONENTS = ['x', 'y', 'rotate', 'scaleX', 'shearX'] as const;
|
|
233
|
+
/** The seven values its pose holds and its timelines key, in the row's order (`mass` as the key states it; the pose holds its inverse). */
|
|
234
|
+
export const PHYSICS_PARAMETERS = ['inertia', 'strength', 'damping', 'mass', 'wind', 'gravity', 'mix'] as const;
|
|
235
|
+
export type PhysicsParameter = (typeof PHYSICS_PARAMETERS)[number];
|
|
236
|
+
/** The timelines a physics constraint is keyed by: the seven values and `reset`. */
|
|
237
|
+
export const PHYSICS_TIMELINE_KINDS = [...PHYSICS_PARAMETERS, 'reset'] as const;
|
|
238
|
+
export type PhysicsTimelineKind = (typeof PHYSICS_TIMELINE_KINDS)[number];
|
|
239
|
+
/** The model's name for the timeline that names no constraint (`EVERY_GLOBAL_PHYSICS` in `src/motion.ts`). */
|
|
240
|
+
export const EVERY_GLOBAL_PHYSICS = '*';
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The parser's value for a field the record leaves out, measured off the
|
|
244
|
+
* oracle's `physics` block for a constraint stating nothing (the header's
|
|
245
|
+
* *Defaults*): every component 0, `limit` 5000, `fps` 60, `inertia` 0.5,
|
|
246
|
+
* `strength` 100, `damping` 0.85, `mass` 1, `wind` and `gravity` 0, `mix` 1.
|
|
247
|
+
*/
|
|
248
|
+
export const PHYSICS_DEFAULTS = { x: 0, y: 0, rotate: 0, scaleX: 0, shearX: 0, limit: 5000, fps: 60, inertia: 0.5, strength: 100, damping: 0.85, mass: 1, wind: 0, gravity: 0, mix: 1 } as const;
|
|
249
|
+
|
|
250
|
+
/** The values a physics constraint's pose holds — what its timelines set and its step reads. */
|
|
251
|
+
export interface PhysicsPose {
|
|
252
|
+
inertia: number;
|
|
253
|
+
strength: number;
|
|
254
|
+
damping: number;
|
|
255
|
+
massInverse: number;
|
|
256
|
+
wind: number;
|
|
257
|
+
gravity: number;
|
|
258
|
+
mix: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A physics constraint, read field by field: its bone, the components it drives, its settings and its setup pose. */
|
|
262
|
+
export interface CorePhysicsRecord extends PhysicsPose {
|
|
263
|
+
kind: 'physics';
|
|
264
|
+
name: string;
|
|
265
|
+
bone: string;
|
|
266
|
+
skin: boolean;
|
|
267
|
+
/** An applied skin's `constraints.physics` list names it — what makes a skin-required constraint step (the header's *Which constraints step*); set per skin view by `underSkin` in `./index.ts`. */
|
|
268
|
+
listedBySkin: boolean;
|
|
269
|
+
x: number;
|
|
270
|
+
y: number;
|
|
271
|
+
rotate: number;
|
|
272
|
+
scaleX: number;
|
|
273
|
+
shearX: number;
|
|
274
|
+
limit: number;
|
|
275
|
+
/** `1 / fps`, the integrator's fixed step in seconds. */
|
|
276
|
+
step: number;
|
|
277
|
+
/** The skeleton's reference scale — the document's `referenceScale` (issue #958), which wind and gravity act over. */
|
|
278
|
+
referenceScale: number;
|
|
279
|
+
/** Each `…Global` flag, by the value it opts in to being driven by the timeline that names no constraint. */
|
|
280
|
+
global: Record<PhysicsParameter, boolean>;
|
|
281
|
+
scaleYMode: 'None' | 'Uniform' | 'Volume';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A physics constraint's record, read field by field — every field the writer
|
|
286
|
+
* can write for the kind and no other, each of its type, its bone a bone of
|
|
287
|
+
* the document — or `undefined` with the problems named. A field left out
|
|
288
|
+
* reads the parser's value (`PHYSICS_DEFAULTS`). `referenceScale` is the
|
|
289
|
+
* document's, read by `readModel` (issue #958), and stamped on the record.
|
|
290
|
+
*/
|
|
291
|
+
export function readPhysicsRecord(raw: Record<string, unknown>, name: string, where: string, bones: ReadonlySet<string>, referenceScale: number, problems: string[]): CorePhysicsRecord | undefined {
|
|
292
|
+
const before = problems.length;
|
|
293
|
+
for (const key of Object.keys(raw)) {
|
|
294
|
+
if (key === 'kind' || key === 'name' || key === 'declaredIn') continue;
|
|
295
|
+
if (!(PHYSICS_FIELDS as readonly string[]).includes(key)) problems.push(`${where}: field "${key}" is not one this reader knows; it reads [${PHYSICS_FIELDS.join(', ')}]`);
|
|
296
|
+
}
|
|
297
|
+
if (typeof raw.bone !== 'string' || !bones.has(raw.bone)) problems.push(`${where}: bone is ${JSON.stringify(raw.bone)}, not a bone of this document`);
|
|
298
|
+
for (const key of PHYSICS_NUMBERS) {
|
|
299
|
+
const v = raw[key];
|
|
300
|
+
if (v !== undefined && (typeof v !== 'number' || !Number.isFinite(v))) problems.push(`${where}: ${key} is ${JSON.stringify(v)}, not a finite number`);
|
|
301
|
+
}
|
|
302
|
+
for (const key of PHYSICS_FLAGS) {
|
|
303
|
+
const v = raw[key];
|
|
304
|
+
if (v !== undefined && typeof v !== 'boolean') problems.push(`${where}: ${key} is ${JSON.stringify(v)}, not a boolean`);
|
|
305
|
+
}
|
|
306
|
+
let scaleYMode: CorePhysicsRecord['scaleYMode'] = 'None';
|
|
307
|
+
if (raw.scaleY !== undefined) {
|
|
308
|
+
const folded = typeof raw.scaleY === 'string' && raw.scaleY.length > 0 ? raw.scaleY[0].toLowerCase() + raw.scaleY.slice(1) : '';
|
|
309
|
+
if (!(SCALE_Y_MODES as readonly string[]).includes(folded)) problems.push(`${where}: scaleY is ${JSON.stringify(raw.scaleY)}, none of ${SCALE_Y_MODES.join(', ')}`);
|
|
310
|
+
else scaleYMode = folded === 'uniform' ? 'Uniform' : folded === 'volume' ? 'Volume' : 'None';
|
|
311
|
+
}
|
|
312
|
+
if (problems.length !== before) return undefined;
|
|
313
|
+
const n = (key: keyof typeof PHYSICS_DEFAULTS): number => (typeof raw[key] === 'number' ? (raw[key] as number) : PHYSICS_DEFAULTS[key]);
|
|
314
|
+
const global = {} as Record<PhysicsParameter, boolean>;
|
|
315
|
+
for (const p of PHYSICS_PARAMETERS) global[p] = raw[`${p}Global`] === true;
|
|
316
|
+
return {
|
|
317
|
+
kind: 'physics', name, bone: raw.bone as string, skin: raw.skin === true, listedBySkin: false,
|
|
318
|
+
x: n('x'), y: n('y'), rotate: n('rotate'), scaleX: n('scaleX'), shearX: n('shearX'), limit: n('limit'), step: 1 / n('fps'), referenceScale,
|
|
319
|
+
inertia: n('inertia'), strength: n('strength'), damping: n('damping'), massInverse: 1 / n('mass'), wind: n('wind'), gravity: n('gravity'), mix: n('mix'),
|
|
320
|
+
global, scaleYMode,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Whether a physics record steps under the skin view posed: its bone active, and not skin-required unless an applied skin lists it — measured, the header's *Which constraints step*. */
|
|
325
|
+
export function physicsActive(r: CorePhysicsRecord, active: ReadonlySet<string>): boolean {
|
|
326
|
+
return (!r.skin || r.listedBySkin) && active.has(r.bone);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** One physics timeline: the constraint it names (`EVERY_GLOBAL_PHYSICS` for none), what it keys, its keys (a `reset` key's `values` empty). */
|
|
330
|
+
export interface CorePhysicsTimeline {
|
|
331
|
+
name: string;
|
|
332
|
+
kind: PhysicsTimelineKind;
|
|
333
|
+
keys: CoreKey[];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* How many physics timelines one animation record's `constraints.physics`
|
|
340
|
+
* holds — the census's count. Their key times also set the animation's
|
|
341
|
+
* duration, which `./animation.ts` reads.
|
|
342
|
+
*/
|
|
343
|
+
export function physicsTimelineCount(value: unknown): number {
|
|
344
|
+
if (!Array.isArray(value)) return 0;
|
|
345
|
+
let n = 0;
|
|
346
|
+
for (const entry of value) {
|
|
347
|
+
if (typeof entry === 'object' && entry !== null && Array.isArray((entry as { timelines?: unknown }).timelines)) n += (entry as { timelines: unknown[] }).timelines.length;
|
|
348
|
+
}
|
|
349
|
+
return n;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* One animation record's physics timelines, read in the record's order —
|
|
354
|
+
* which is the order the runtime applies them in — each naming a declared
|
|
355
|
+
* physics constraint or `EVERY_GLOBAL_PHYSICS`, each kind once per name, keys
|
|
356
|
+
* strictly increasing in time with a finite `value` (absent: 1 for `mix`, 0
|
|
357
|
+
* for the rest — the parser's reading) and a `reset` key a time alone.
|
|
358
|
+
*/
|
|
359
|
+
export function readPhysicsTimelines(value: unknown, label: string, declared: ReadonlySet<string>, problems: string[]): CorePhysicsTimeline[] {
|
|
360
|
+
const out: CorePhysicsTimeline[] = [];
|
|
361
|
+
if (!Array.isArray(value)) return out;
|
|
362
|
+
value.forEach((entry, i) => {
|
|
363
|
+
const at = `${label}.constraints.physics[${i}]`;
|
|
364
|
+
if (!isObject(entry) || typeof entry.name !== 'string') {
|
|
365
|
+
problems.push(`${at} names no physics constraint`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const name = entry.name;
|
|
369
|
+
if (name !== EVERY_GLOBAL_PHYSICS && !declared.has(name)) problems.push(`${at}: "${name}" is not a physics constraint of this document (nor "${EVERY_GLOBAL_PHYSICS}", the timeline naming none)`);
|
|
370
|
+
if (!Array.isArray(entry.timelines)) {
|
|
371
|
+
problems.push(`${at}: timelines is not a list`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const seen = new Set<string>();
|
|
375
|
+
entry.timelines.forEach((raw: unknown, j: number) => {
|
|
376
|
+
const tat = `${at}.timelines[${j}]`;
|
|
377
|
+
const kind = isObject(raw) ? PHYSICS_TIMELINE_KINDS.find((k) => k === raw.name) : undefined;
|
|
378
|
+
if (!isObject(raw) || kind === undefined) {
|
|
379
|
+
problems.push(`${tat}: ${JSON.stringify(isObject(raw) ? raw.name : raw)} is not a physics timeline; one keys ${PHYSICS_TIMELINE_KINDS.join(', ')}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (seen.has(kind)) problems.push(`${tat}: "${kind}" is keyed twice on "${name}"`);
|
|
383
|
+
seen.add(kind);
|
|
384
|
+
const keys: CoreKey[] = [];
|
|
385
|
+
const rawKeys: unknown = raw.keys;
|
|
386
|
+
if (!Array.isArray(rawKeys) || rawKeys.length === 0) {
|
|
387
|
+
problems.push(`${tat} "${kind}": keys is not a non-empty list`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
let last = -Infinity;
|
|
391
|
+
rawKeys.forEach((k: unknown, q: number) => {
|
|
392
|
+
const kat = `${tat} "${kind}".keys[${q}]`;
|
|
393
|
+
if (!isObject(k)) {
|
|
394
|
+
problems.push(`${kat} is not an object`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const known = kind === 'reset' ? ['time'] : ['time', 'value', 'curve'];
|
|
398
|
+
for (const f of Object.keys(k)) if (!known.includes(f)) problems.push(`${kat}: field "${f}" is not one this reader knows; it reads [${known.join(', ')}]`);
|
|
399
|
+
const time = k.time;
|
|
400
|
+
if (typeof time !== 'number' || !Number.isFinite(time) || time < 0) {
|
|
401
|
+
problems.push(`${kat}: time is ${JSON.stringify(time)}, not a finite time at or after 0`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
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`);
|
|
405
|
+
last = Math.max(last, time);
|
|
406
|
+
if (kind === 'reset') {
|
|
407
|
+
keys.push({ time: Math.fround(time), values: [], stated: { time, values: [] }, curve: 'stepped' });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
let v = kind === 'mix' ? 1 : 0;
|
|
411
|
+
if (k.value !== undefined) {
|
|
412
|
+
if (typeof k.value !== 'number' || !Number.isFinite(k.value)) problems.push(`${kat}: value is ${JSON.stringify(k.value)}, not a finite number`);
|
|
413
|
+
else v = k.value;
|
|
414
|
+
}
|
|
415
|
+
let curve: CoreCurve = 'linear';
|
|
416
|
+
if (k.curve !== undefined) {
|
|
417
|
+
if (q === rawKeys.length - 1) problems.push(`${kat}: the last key carries a curve, which eases to no key — the writer refuses it`);
|
|
418
|
+
else if (k.curve === 'stepped') curve = 'stepped';
|
|
419
|
+
else if (Array.isArray(k.curve) && k.curve.length === 4 && k.curve.every((c) => typeof c === 'number' && Number.isFinite(c))) curve = k.curve as number[];
|
|
420
|
+
else problems.push(`${kat}: curve is ${JSON.stringify(k.curve)}, not "stepped" nor four finite numbers`);
|
|
421
|
+
}
|
|
422
|
+
keys.push({ time: Math.fround(time), values: [Math.fround(v)], stated: { time, values: [v] }, curve });
|
|
423
|
+
});
|
|
424
|
+
out.push({ name, kind, keys });
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** What evaluates one channel of a key and finds the key — construct 4's unless a plant passes others. */
|
|
431
|
+
export interface PhysicsTimelinePlant {
|
|
432
|
+
channel?: (keys: readonly CoreKey[], index: number, channel: number, t: number) => number;
|
|
433
|
+
search?: (keys: readonly CoreKey[], t: number) => number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The records a physics timeline that names no constraint (`*`) writes: every
|
|
438
|
+
* active one whose `…Global` flag for `kind` is on, and for `reset` every
|
|
439
|
+
* active one (the header's *The timelines*), in the order given. `posedPhysics`
|
|
440
|
+
* asks it, and so does the additive probe (`./additive.ts`, issue #1025).
|
|
441
|
+
*/
|
|
442
|
+
export function unnamedPhysicsTargets<R extends CorePhysicsRecord>(records: readonly R[], kind: PhysicsTimelineKind, active: (r: CorePhysicsRecord) => boolean): R[] {
|
|
443
|
+
return records.filter((r) => active(r) && (kind === 'reset' || r.global[kind]));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Every physics record posed by the animation's physics timelines at `t`, in
|
|
448
|
+
* the timelines' order (the header's *The timelines*), and the names of the
|
|
449
|
+
* constraints a `reset` key resets while the animation is applied from
|
|
450
|
+
* `last` to `t` (`resetCrossed`). `active`
|
|
451
|
+
* says whether a constraint is applied at all (its skin and its bone).
|
|
452
|
+
*/
|
|
453
|
+
export function posedPhysics(records: readonly CorePhysicsRecord[], timelines: readonly CorePhysicsTimeline[], t: number, last: number, active: (r: CorePhysicsRecord) => boolean, plant: PhysicsTimelinePlant = {}, wrapped = false): { records: Map<string, CorePhysicsRecord>; reset: Set<string> } {
|
|
454
|
+
const search = plant.search ?? keyIndexAt;
|
|
455
|
+
const channel = plant.channel ?? channelAt;
|
|
456
|
+
const setupOf = new Map(records.map((r) => [r.name, r]));
|
|
457
|
+
const posed = new Map(records.map((r) => [r.name, { ...r }]));
|
|
458
|
+
const reset = new Set<string>();
|
|
459
|
+
for (const tl of timelines) {
|
|
460
|
+
const targets = tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets([...posed.values()], tl.kind, active) : [posed.get(tl.name) as CorePhysicsRecord].filter((r) => active(r));
|
|
461
|
+
if (tl.kind === 'reset') {
|
|
462
|
+
if (resetCrossed(tl.keys, last, t, wrapped)) for (const r of targets) reset.add(r.name);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const i = search(tl.keys, t);
|
|
466
|
+
for (const r of targets) {
|
|
467
|
+
const setup = setupOf.get(r.name) as CorePhysicsRecord;
|
|
468
|
+
if (tl.kind === 'mass') {
|
|
469
|
+
// The key states a mass; the pose holds its inverse, and the setup the blend starts from is the inverse inverted.
|
|
470
|
+
const from = 1 / setup.massInverse;
|
|
471
|
+
r.massInverse = i < 0 ? setup.massInverse : 1 / (from + (channel(tl.keys, i, 0, t) - from));
|
|
472
|
+
} else {
|
|
473
|
+
r[tl.kind] = i < 0 ? setup[tl.kind] : setup[tl.kind] + (channel(tl.keys, i, 0, t) - setup[tl.kind]);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return { records: posed, reset };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Whether a `reset` timeline resets its constraints when the animation is
|
|
482
|
+
* applied from `last` to `t`, as a player applies it (the header's *reset*,
|
|
483
|
+
* issue #960): exactly when a key lies in `(last, t]` — so a key is crossed
|
|
484
|
+
* once, by the step that reaches it, and `last` is −1 before the walk's
|
|
485
|
+
* first apply, where a key at 0 is crossed.
|
|
486
|
+
*
|
|
487
|
+
* `wrapped` is a looping track's step whose animation time went DOWN (issue
|
|
488
|
+
* #1025, `./walk.ts`): the key crossed is then one after `last` or one at or
|
|
489
|
+
* before `t` — `(last, ∞)` and `(−1, t]` — measured against spine-core's
|
|
490
|
+
* `PhysicsConstraint.reset` calls on A10's walk. A step that wraps the track
|
|
491
|
+
* one or more times while its animation time still rises is not `wrapped`:
|
|
492
|
+
* the runtime fired only `(last, t]` there (a key at 0.25 of a 0.3 s
|
|
493
|
+
* animation stepped by 0.7 s, crossed by the track twice between two applies
|
|
494
|
+
* at 0.1 and 0.2, fired on neither).
|
|
495
|
+
*/
|
|
496
|
+
export function resetCrossed(keys: readonly CoreKey[], last: number, t: number, wrapped = false): boolean {
|
|
497
|
+
return wrapped ? keys.some((k) => k.time > last || k.time <= t) : keys.some((k) => last < k.time && k.time <= t);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// the step
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
/** One physics constraint's state between two updates (the header's *The step*). */
|
|
505
|
+
export interface PhysicsState {
|
|
506
|
+
/** The next update takes the bone where it is and integrates nothing: set by a reset and on a constraint never updated. */
|
|
507
|
+
pending: boolean;
|
|
508
|
+
lastTime: number;
|
|
509
|
+
remaining: number;
|
|
510
|
+
ux: number;
|
|
511
|
+
uy: number;
|
|
512
|
+
cx: number;
|
|
513
|
+
cy: number;
|
|
514
|
+
tx: number;
|
|
515
|
+
ty: number;
|
|
516
|
+
xOffset: number;
|
|
517
|
+
xLag: number;
|
|
518
|
+
xVelocity: number;
|
|
519
|
+
yOffset: number;
|
|
520
|
+
yLag: number;
|
|
521
|
+
yVelocity: number;
|
|
522
|
+
rotateOffset: number;
|
|
523
|
+
rotateLag: number;
|
|
524
|
+
rotateVelocity: number;
|
|
525
|
+
scaleOffset: number;
|
|
526
|
+
scaleLag: number;
|
|
527
|
+
scaleVelocity: number;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** A constraint's state before its first update: pending, its clock at 0. */
|
|
531
|
+
export function freshPhysicsState(): PhysicsState {
|
|
532
|
+
return { pending: true, lastTime: 0, remaining: 0, ux: 0, uy: 0, cx: 0, cy: 0, tx: 0, ty: 0, xOffset: 0, xLag: 0, xVelocity: 0, yOffset: 0, yLag: 0, yVelocity: 0, rotateOffset: 0, rotateLag: 0, rotateVelocity: 0, scaleOffset: 0, scaleLag: 0, scaleVelocity: 0 };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** A reset: offsets, velocities and lags cleared, nothing remaining, the clock read at `time`, pending. */
|
|
536
|
+
export function resetPhysicsState(s: PhysicsState, time: number): void {
|
|
537
|
+
s.remaining = 0;
|
|
538
|
+
s.lastTime = time;
|
|
539
|
+
s.pending = true;
|
|
540
|
+
s.xOffset = s.xLag = s.xVelocity = 0;
|
|
541
|
+
s.yOffset = s.yLag = s.yVelocity = 0;
|
|
542
|
+
s.rotateOffset = s.rotateLag = s.rotateVelocity = 0;
|
|
543
|
+
s.scaleOffset = s.scaleLag = s.scaleVelocity = 0;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The runtime's two phases the oracle steps with: `reset` resets the state first. */
|
|
547
|
+
export type PhysicsPhase = 'reset' | 'update';
|
|
548
|
+
|
|
549
|
+
/** What steps one constraint — `stepPhysics` unless a plant passes another. */
|
|
550
|
+
export type PhysicsStepper = (r: CorePhysicsRecord, w: CoreWorld, length: number, ctx: PhysicsStepContext) => boolean;
|
|
551
|
+
|
|
552
|
+
/** What a stepped pose carries into every physics constraint it applies. */
|
|
553
|
+
export interface PhysicsStepContext {
|
|
554
|
+
phase: PhysicsPhase;
|
|
555
|
+
/** The skeleton's clock: the sum of every step's delta so far. */
|
|
556
|
+
time: number;
|
|
557
|
+
/** Each constraint's state, by name, carried from update to update. */
|
|
558
|
+
states: Map<string, PhysicsState>;
|
|
559
|
+
/** The step itself; a control passes a planted copy. */
|
|
560
|
+
step?: PhysicsStepper;
|
|
561
|
+
/**
|
|
562
|
+
* The animation time the walk last applied the animation at, −1 before its
|
|
563
|
+
* first apply — a fresh track's `TrackEntry.animationLast`, read −1 on
|
|
564
|
+
* spine-core 4.3.13 (issue #960). A `reset` key fires on an apply from it
|
|
565
|
+
* to the step's time (`resetCrossed`).
|
|
566
|
+
*/
|
|
567
|
+
last: number;
|
|
568
|
+
/**
|
|
569
|
+
* The walk's track loops (`./walk.ts`, issue #1025): a step whose animation
|
|
570
|
+
* time went down from `last` wrapped, and crosses the keys `resetCrossed`
|
|
571
|
+
* names for that. Absent on every walk but the looping one.
|
|
572
|
+
*/
|
|
573
|
+
loop?: boolean;
|
|
574
|
+
/** A plant only (issue #1049, `sliderPhysicsLastsOnePass: false`): what the sliders wrote on the step before, by constraint and field. */
|
|
575
|
+
carried?: Map<string, Partial<Record<'inertia' | 'strength' | 'damping' | 'massInverse' | 'wind' | 'gravity' | 'mix', number>>>;
|
|
576
|
+
/** Each `constraint/field` the step's own animation keys — what the planted carried write gives way to. */
|
|
577
|
+
keyedNow?: Set<string>;
|
|
578
|
+
/** A plant only (issue #1049, `sliderPhysicsFromCurrent: false`): each physics record's setup values, before the step's own animation. */
|
|
579
|
+
setupRecords?: Map<string, CorePhysicsRecord>;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** A constraint's state in `ctx`, created fresh on its first update. */
|
|
583
|
+
export function physicsState(ctx: PhysicsStepContext, name: string): PhysicsState {
|
|
584
|
+
let s = ctx.states.get(name);
|
|
585
|
+
if (s === undefined) {
|
|
586
|
+
s = freshPhysicsState();
|
|
587
|
+
ctx.states.set(name, s);
|
|
588
|
+
}
|
|
589
|
+
return s;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const PI2 = RUNTIME_PI * 2;
|
|
593
|
+
const INV_PI2 = 1 / PI2;
|
|
594
|
+
|
|
595
|
+
/** What a physics constraint's step moves, by its five component fields. */
|
|
596
|
+
export interface PhysicsDrives {
|
|
597
|
+
x: boolean;
|
|
598
|
+
y: boolean;
|
|
599
|
+
/** `rotate` or `shearX` above 0: the two share the angular part of the step. */
|
|
600
|
+
rotateOrShearX: boolean;
|
|
601
|
+
scaleX: boolean;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Which parts of the step a constraint drives: a component above 0 drives its
|
|
606
|
+
* part and nothing else does — the test `stepPhysics` takes before each part
|
|
607
|
+
* of *The step* above, posed against the runtime with the rest of it. Exported
|
|
608
|
+
* because `src/ingest.ts` asks the same question of a raw file (a
|
|
609
|
+
* constraint that drives none of the four moves no bone, issue #731) and
|
|
610
|
+
* answers it through this function (issue #1015).
|
|
611
|
+
*/
|
|
612
|
+
export function physicsDrives(r: { x: number; y: number; rotate: number; shearX: number; scaleX: number }): PhysicsDrives {
|
|
613
|
+
return { x: r.x > 0, y: r.y > 0, rotateOrShearX: r.rotate > 0 || r.shearX > 0, scaleX: r.scaleX > 0 };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* One physics constraint updated on its bone's world transform `w`
|
|
618
|
+
* (mutated), `length` the bone's length — the header's *The step*, every
|
|
619
|
+
* expression in the order measured. Returns false when its `mix` is 0: the
|
|
620
|
+
* constraint then does nothing at all, its clock included, and the bone is
|
|
621
|
+
* not re-posed.
|
|
622
|
+
*/
|
|
623
|
+
export function stepPhysics(r: CorePhysicsRecord, w: CoreWorld, length: number, ctx: PhysicsStepContext): boolean {
|
|
624
|
+
const mix = r.mix;
|
|
625
|
+
if (mix === 0) return false;
|
|
626
|
+
const s = physicsState(ctx, r.name);
|
|
627
|
+
const { x, y, rotateOrShearX, scaleX } = physicsDrives(r);
|
|
628
|
+
const l = length;
|
|
629
|
+
if (ctx.phase === 'reset') resetPhysicsState(s, ctx.time);
|
|
630
|
+
const delta = Math.max(ctx.time - s.lastTime, 0);
|
|
631
|
+
// The interpolation weight of the previous update, before this one's time is added: the rotate and the scale-only inertia read the pose as it was shown.
|
|
632
|
+
const shown = Math.max(0, 1 - s.remaining / r.step);
|
|
633
|
+
s.remaining += delta;
|
|
634
|
+
s.lastTime = ctx.time;
|
|
635
|
+
const bx = w.worldX;
|
|
636
|
+
const by = w.worldY;
|
|
637
|
+
let z = 0;
|
|
638
|
+
if (s.pending) {
|
|
639
|
+
s.pending = false;
|
|
640
|
+
s.ux = bx;
|
|
641
|
+
s.uy = by;
|
|
642
|
+
} else {
|
|
643
|
+
let a = s.remaining;
|
|
644
|
+
const i = r.inertia;
|
|
645
|
+
const t = r.step;
|
|
646
|
+
const f = r.referenceScale;
|
|
647
|
+
let d = -1;
|
|
648
|
+
const qx = r.limit * delta;
|
|
649
|
+
const qy = qx;
|
|
650
|
+
if (x || y) {
|
|
651
|
+
if (x) {
|
|
652
|
+
const u = (s.ux - bx) * i;
|
|
653
|
+
s.xOffset += u > qx ? qx : u < -qx ? -qx : u;
|
|
654
|
+
s.ux = bx;
|
|
655
|
+
}
|
|
656
|
+
if (y) {
|
|
657
|
+
const u = (s.uy - by) * i;
|
|
658
|
+
s.yOffset += u > qy ? qy : u < -qy ? -qy : u;
|
|
659
|
+
s.uy = by;
|
|
660
|
+
}
|
|
661
|
+
if (a >= t) {
|
|
662
|
+
d = Math.pow(r.damping, 60 * t);
|
|
663
|
+
const m = r.massInverse * t;
|
|
664
|
+
const e = r.strength;
|
|
665
|
+
const wind = r.wind * f;
|
|
666
|
+
const g = r.gravity * f;
|
|
667
|
+
const xs = s.xOffset;
|
|
668
|
+
const ys = s.yOffset;
|
|
669
|
+
do {
|
|
670
|
+
if (x) {
|
|
671
|
+
s.xVelocity += (wind - s.xOffset * e) * m;
|
|
672
|
+
s.xOffset += s.xVelocity * t;
|
|
673
|
+
s.xVelocity *= d;
|
|
674
|
+
}
|
|
675
|
+
if (y) {
|
|
676
|
+
s.yVelocity -= (g + s.yOffset * e) * m;
|
|
677
|
+
s.yOffset += s.yVelocity * t;
|
|
678
|
+
s.yVelocity *= d;
|
|
679
|
+
}
|
|
680
|
+
a -= t;
|
|
681
|
+
} while (a >= t);
|
|
682
|
+
s.xLag = s.xOffset - xs;
|
|
683
|
+
s.yLag = s.yOffset - ys;
|
|
684
|
+
}
|
|
685
|
+
z = Math.max(0, 1 - a / t);
|
|
686
|
+
if (x) w.worldX += (s.xOffset - s.xLag * z) * mix * r.x;
|
|
687
|
+
if (y) w.worldY += (s.yOffset - s.yLag * z) * mix * r.y;
|
|
688
|
+
}
|
|
689
|
+
if (rotateOrShearX || scaleX) {
|
|
690
|
+
const ca = Math.atan2(w.c, w.a);
|
|
691
|
+
let c = 0;
|
|
692
|
+
let sn = 0;
|
|
693
|
+
let mr = 0;
|
|
694
|
+
let dx = s.cx - w.worldX;
|
|
695
|
+
let dy = s.cy - w.worldY;
|
|
696
|
+
if (dx > qx) dx = qx;
|
|
697
|
+
else if (dx < -qx) dx = -qx;
|
|
698
|
+
if (dy > qy) dy = qy;
|
|
699
|
+
else if (dy < -qy) dy = -qy;
|
|
700
|
+
if (rotateOrShearX) {
|
|
701
|
+
mr = (r.rotate + r.shearX) * mix;
|
|
702
|
+
const lagged = s.rotateLag * shown;
|
|
703
|
+
let rr = Math.atan2(dy + s.ty, dx + s.tx) - ca - (s.rotateOffset - lagged) * mr;
|
|
704
|
+
s.rotateOffset += (rr - Math.ceil(rr * INV_PI2 - 0.5) * PI2) * i;
|
|
705
|
+
rr = (s.rotateOffset - lagged) * mr + ca;
|
|
706
|
+
c = Math.cos(rr);
|
|
707
|
+
sn = Math.sin(rr);
|
|
708
|
+
if (scaleX) {
|
|
709
|
+
rr = l * Math.sqrt(w.a * w.a + w.c * w.c);
|
|
710
|
+
if (rr > 0) s.scaleOffset += ((dx * c + dy * sn) * i) / rr;
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
c = Math.cos(ca);
|
|
714
|
+
sn = Math.sin(ca);
|
|
715
|
+
const rr = l * Math.sqrt(w.a * w.a + w.c * w.c) - s.scaleLag * shown;
|
|
716
|
+
if (rr > 0) s.scaleOffset += ((dx * c + dy * sn) * i) / rr;
|
|
717
|
+
}
|
|
718
|
+
a = s.remaining;
|
|
719
|
+
if (a >= t) {
|
|
720
|
+
if (d === -1) d = Math.pow(r.damping, 60 * t);
|
|
721
|
+
const m = r.massInverse * t;
|
|
722
|
+
const e = r.strength;
|
|
723
|
+
const wind = r.wind;
|
|
724
|
+
const g = r.gravity;
|
|
725
|
+
const h = l / f;
|
|
726
|
+
const rs = s.rotateOffset;
|
|
727
|
+
const ss = s.scaleOffset;
|
|
728
|
+
for (;;) {
|
|
729
|
+
a -= t;
|
|
730
|
+
if (scaleX) {
|
|
731
|
+
s.scaleVelocity += (wind * c - g * sn - s.scaleOffset * e) * m;
|
|
732
|
+
s.scaleOffset += s.scaleVelocity * t;
|
|
733
|
+
s.scaleVelocity *= d;
|
|
734
|
+
}
|
|
735
|
+
if (rotateOrShearX) {
|
|
736
|
+
s.rotateVelocity -= ((wind * sn + g * c) * h + s.rotateOffset * e) * m;
|
|
737
|
+
s.rotateOffset += s.rotateVelocity * t;
|
|
738
|
+
s.rotateVelocity *= d;
|
|
739
|
+
if (a < t) break;
|
|
740
|
+
const rr = s.rotateOffset * mr + ca;
|
|
741
|
+
c = Math.cos(rr);
|
|
742
|
+
sn = Math.sin(rr);
|
|
743
|
+
} else if (a < t) break;
|
|
744
|
+
}
|
|
745
|
+
s.rotateLag = s.rotateOffset - rs;
|
|
746
|
+
s.scaleLag = s.scaleOffset - ss;
|
|
747
|
+
}
|
|
748
|
+
z = Math.max(0, 1 - a / t);
|
|
749
|
+
}
|
|
750
|
+
s.remaining = a;
|
|
751
|
+
}
|
|
752
|
+
s.cx = w.worldX;
|
|
753
|
+
s.cy = w.worldY;
|
|
754
|
+
if (rotateOrShearX) {
|
|
755
|
+
let o = (s.rotateOffset - s.rotateLag * z) * mix;
|
|
756
|
+
let sn = 0;
|
|
757
|
+
let c = 0;
|
|
758
|
+
let a = 0;
|
|
759
|
+
if (r.shearX > 0) {
|
|
760
|
+
let rr = 0;
|
|
761
|
+
if (r.rotate > 0) {
|
|
762
|
+
// The rotation's share turns the y column here, and the x column with the shear below.
|
|
763
|
+
rr = o * r.rotate;
|
|
764
|
+
sn = Math.sin(rr);
|
|
765
|
+
c = Math.cos(rr);
|
|
766
|
+
a = w.b;
|
|
767
|
+
w.b = c * a - sn * w.d;
|
|
768
|
+
w.d = sn * a + c * w.d;
|
|
769
|
+
}
|
|
770
|
+
rr += o * r.shearX;
|
|
771
|
+
sn = Math.sin(rr);
|
|
772
|
+
c = Math.cos(rr);
|
|
773
|
+
a = w.a;
|
|
774
|
+
w.a = c * a - sn * w.c;
|
|
775
|
+
w.c = sn * a + c * w.c;
|
|
776
|
+
} else {
|
|
777
|
+
o *= r.rotate;
|
|
778
|
+
sn = Math.sin(o);
|
|
779
|
+
c = Math.cos(o);
|
|
780
|
+
a = w.a;
|
|
781
|
+
w.a = c * a - sn * w.c;
|
|
782
|
+
w.c = sn * a + c * w.c;
|
|
783
|
+
a = w.b;
|
|
784
|
+
w.b = c * a - sn * w.d;
|
|
785
|
+
w.d = sn * a + c * w.d;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (scaleX) {
|
|
789
|
+
const k = 1 + (s.scaleOffset - s.scaleLag * z) * mix * r.scaleX;
|
|
790
|
+
w.a *= k;
|
|
791
|
+
w.c *= k;
|
|
792
|
+
}
|
|
793
|
+
s.tx = l * w.a;
|
|
794
|
+
s.ty = l * w.c;
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** The oracle's `physics` block written from the records, in constraint order, rounded as the oracle rounds. */
|
|
799
|
+
export function physicsRows(records: readonly CorePhysicsRecord[], round: (v: number) => number | null): Array<Record<string, unknown>> {
|
|
800
|
+
return records.map((r) => ({
|
|
801
|
+
name: r.name, bone: r.bone,
|
|
802
|
+
x: round(r.x), y: round(r.y), rotate: round(r.rotate), scaleX: round(r.scaleX), shearX: round(r.shearX), limit: round(r.limit), step: round(r.step),
|
|
803
|
+
inertia: round(r.inertia), strength: round(r.strength), damping: round(r.damping), massInverse: round(r.massInverse), wind: round(r.wind), gravity: round(r.gravity), mix: round(r.mix),
|
|
804
|
+
inertiaGlobal: r.global.inertia, strengthGlobal: r.global.strength, dampingGlobal: r.global.damping, massGlobal: r.global.mass, windGlobal: r.global.wind, gravityGlobal: r.global.gravity, mixGlobal: r.global.mix,
|
|
805
|
+
skinRequired: r.skin, scaleYMode: r.scaleYMode,
|
|
806
|
+
}));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ---------------------------------------------------------------------------
|
|
810
|
+
// the walk
|
|
811
|
+
// ---------------------------------------------------------------------------
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* One step's constraint records: every physics record posed by the
|
|
815
|
+
* animation's physics timelines at `t` (`posedPhysics`), and each constraint
|
|
816
|
+
* a `reset` key resets reset now — while the animation is applied, before
|
|
817
|
+
* the skeleton's clock moves to this step (`before` is the clock it reads;
|
|
818
|
+
* the header's *reset*). `./animation.ts` calls it for every step of the walk.
|
|
819
|
+
*/
|
|
820
|
+
export function stepPhysicsRecords(records: readonly CoreConstraintRecord[], keyed: readonly CorePhysicsTimeline[], t: number, active: ReadonlySet<string>, ctx: PhysicsStepContext, before: number): CoreConstraintRecord[] {
|
|
821
|
+
const physics = records.filter((r): r is CorePhysicsRecord => r.kind === 'physics');
|
|
822
|
+
const posed = posedPhysics(physics, keyed, t, ctx.last, (r) => physicsActive(r, active), {}, ctx.loop === true && t < ctx.last);
|
|
823
|
+
ctx.last = t;
|
|
824
|
+
ctx.setupRecords = new Map(physics.map((r) => [r.name, r]));
|
|
825
|
+
ctx.keyedNow = new Set(keyed.flatMap((tl) => (tl.kind === 'reset' ? [] : (tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets(physics, tl.kind, (r) => physicsActive(r, active)).map((r) => r.name) : [tl.name]).map((n) => `${n}/${tl.kind === 'mass' ? 'massInverse' : tl.kind}`))));
|
|
826
|
+
for (const name of posed.reset) resetPhysicsState(physicsState(ctx, name), before);
|
|
827
|
+
return records.map((r) => (r.kind === 'physics' ? (posed.records.get(r.name) as CorePhysicsRecord) : r));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** A fresh step context for one animation's walk (or the setup's reset), the plant's step in place of `stepPhysics` when a control passes one. */
|
|
831
|
+
export function freshStepContext(step?: PhysicsStepper): PhysicsStepContext {
|
|
832
|
+
return { phase: 'reset', time: 0, states: new Map(), last: -1, ...(step ? { step } : {}) };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Why the samples' bones are left out under the step, or null: a path
|
|
837
|
+
* constraint whose offset reads its slot bone from the previous pose, which
|
|
838
|
+
* is then the previous step's — a pose the walk does not carry.
|
|
839
|
+
*/
|
|
840
|
+
export function steppedPreviousPassWhy(doc: CompiledDocument): string | null {
|
|
841
|
+
const reads = previousPassSlotBones(doc, activeBones(doc));
|
|
842
|
+
return reads.length === 0 ? null : `${reads.map((r) => `path constraint "${r.constraint}" reads slot bone "${r.bone}" from the previous pose`).join('; ')} — under the step the previous pose is the previous step's, which the stepped walk does not carry`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* The oracle's schedule (the header's *The schedule*): for each of the `n`
|
|
847
|
+
* samples of an animation of duration `d` under `phase`, the times the walk
|
|
848
|
+
* steps to after the sample before it — from the previous sample's time `p`
|
|
849
|
+
* (0 before the first), `p + k·dt` for `k = 1, 2, …` while that is before
|
|
850
|
+
* the sample's `t`, then `t` itself unless the walk is already there. The
|
|
851
|
+
* walk starts at 0 with the reset, so a sample at 0 takes no step.
|
|
852
|
+
*/
|
|
853
|
+
export function stepSchedule(phase: SamplePhase, d: number, n: number, dt: number): number[][] {
|
|
854
|
+
const out: number[][] = [];
|
|
855
|
+
let now = 0;
|
|
856
|
+
for (let i = 0; i < n; i++) {
|
|
857
|
+
const t = sampleTime(phase, d, i, n);
|
|
858
|
+
const from = now;
|
|
859
|
+
const steps: number[] = [];
|
|
860
|
+
for (let k = 1; from + k * dt < t; k++) steps.push(from + k * dt);
|
|
861
|
+
if (steps.length > 0) now = steps[steps.length - 1];
|
|
862
|
+
if (t > now) {
|
|
863
|
+
steps.push(t);
|
|
864
|
+
now = t;
|
|
865
|
+
}
|
|
866
|
+
out.push(steps);
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Every animation of the document walked as the oracle's `--physics step
|
|
873
|
+
* --dt <dt>` walks it (the header's *The schedule*) — `poseAnimations` in
|
|
874
|
+
* `./animation.ts` given `dt`, which poses every block of each sample off
|
|
875
|
+
* the last step's pose — and the steps the schedule takes to reach each
|
|
876
|
+
* sample, per animation.
|
|
877
|
+
*/
|
|
878
|
+
export function poseSteppedAnimations(doc: CompiledDocument, phase: SamplePhase, n: number, dt: number, plant: TimelinePlant = {}): { animations: CoreAnimationPose[]; absent: Array<[string, string]>; steps: number[][] } {
|
|
879
|
+
const posed = poseAnimations(doc, phase, n, plant, dt);
|
|
880
|
+
return { ...posed, steps: doc.animations.map((a) => stepSchedule(phase, a.timelines.duration, n, dt).map((x) => x.length)) };
|
|
881
|
+
}
|