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,1050 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct 4 of the core (issue #936, step 2d of issue #380): an animation's
|
|
3
|
+
* bone and slot timelines at a sample time — the key search, the curve as the
|
|
4
|
+
* runtime evaluates it, and every bone and slot timeline kind — posed on the
|
|
5
|
+
* setup pose with the animation alone at full weight, the way the oracle
|
|
6
|
+
* samples one (`tools/pose_oracle.ts`, *How a pose is posed*: the setup pose,
|
|
7
|
+
* then the animation applied at `t` with alpha 1 from the setup pose, then the
|
|
8
|
+
* world transforms).
|
|
9
|
+
*
|
|
10
|
+
* Every rule below was measured by posing a hand-written skeleton through
|
|
11
|
+
* `tools/pose_oracle.ts dump` (spine-core 4.3.13) and reading the rows it
|
|
12
|
+
* printed; nothing here was written from the runtime's source. The core
|
|
13
|
+
* suite's `CA` controls hold the same skeletons against the core at tolerance
|
|
14
|
+
* 0, and `tools/core_gate.ts` holds every recipe of the tree's corpus.
|
|
15
|
+
*
|
|
16
|
+
* ## The key search
|
|
17
|
+
*
|
|
18
|
+
* A timeline's value at `t` comes from the LAST key whose time is at or
|
|
19
|
+
* before `t`. Measured on a `translatex` keyed at 0.5, 1 and 1.5 on a bone at
|
|
20
|
+
* setup `x` 5, sampled on a grid of quarter seconds over 2 s:
|
|
21
|
+
*
|
|
22
|
+
* - **Before the first key the channel is at its setup value** — `x` read 5
|
|
23
|
+
* at 0 and 0.25. ⚠️ Not "untouched": the timeline POSES the setup value. A
|
|
24
|
+
* bone keyed by `translate` (0 → 10, 2 → 20) and by a `translatex` whose
|
|
25
|
+
* first key is at 1 read `x` = setup (1, not 11) before 1 when `translatex`
|
|
26
|
+
* is listed after `translate`, and 11 when it is listed before — each
|
|
27
|
+
* timeline, in the animation's order, writes its channels at every time.
|
|
28
|
+
* - **On a key exactly, that key** — a stepped key at 0.5 (value 10) followed
|
|
29
|
+
* by a key at 1 (value 30) read 30 at t = 1, not 10.
|
|
30
|
+
* - **At and after the last key, the last key's value** — 45 (setup 5 + 40)
|
|
31
|
+
* at 1.5, 1.75 and 2.
|
|
32
|
+
* - Key times are compared as the runtime stores them, float32
|
|
33
|
+
* (`Math.fround`), against the sample time as a double; key values are read
|
|
34
|
+
* as float32 too. A linear segment from (0, 0) to (0.4666667, 100000)
|
|
35
|
+
* sampled at `off` read 12500, 37500, 62500, 87500 exactly — the key time
|
|
36
|
+
* and the duration both the float32 0.46666669845581055; the double
|
|
37
|
+
* 0.4666667 would have read 4.1e-5 to 2.9e-4 higher.
|
|
38
|
+
*
|
|
39
|
+
* ## The three curves, per segment
|
|
40
|
+
*
|
|
41
|
+
* A key's `curve` shapes the segment from it to the next key.
|
|
42
|
+
*
|
|
43
|
+
* - **None — linear**: `v0 + (t − t0) / (t1 − t0) · (v1 − v0)`, per channel.
|
|
44
|
+
* - **`stepped`**: the key's own value until the next key's time.
|
|
45
|
+
* - **Bézier** — four absolute numbers per channel, `cx1, cy1, cx2, cy2`, the
|
|
46
|
+
* two handles of the cubic from `(t0, v0)` to `(t1, v1)` (the format's
|
|
47
|
+
* documentation: a curve's control points are in the timeline's own time and
|
|
48
|
+
* value). ⭐ **The runtime does not evaluate the cubic.** Posed on one bone,
|
|
49
|
+
* one `translatex` from (0, 0) to (1, 1000) with handles (0.1, 800) and
|
|
50
|
+
* (0.3, 1000), 2,000 `dense` samples read BELOW the exact cubic solved for
|
|
51
|
+
* `t` at every interior sample, by up to 10.34 — the residual of chords
|
|
52
|
+
* under a concave curve — and the slopes between consecutive samples fall
|
|
53
|
+
* into exactly ten runs whose nine intersections are the cubic's points at
|
|
54
|
+
* the parameter `u = 0.1, 0.2, …, 0.9` (to the fourth decimal of `t`). So
|
|
55
|
+
* the curve is the POLYLINE through the key, the cubic's nine points at
|
|
56
|
+
* tenths of its parameter, and the next key, and a sample is interpolated
|
|
57
|
+
* linearly on the piece whose end is the first point at or after `t`.
|
|
58
|
+
*
|
|
59
|
+
* **The nine points, to the last float32 step.** They are stored float32,
|
|
60
|
+
* and which float32 each lands on was fixed by two measurements, both with
|
|
61
|
+
* the samples of spine-core's dump as the judge at the oracle's rounding:
|
|
62
|
+
*
|
|
63
|
+
* 1. *The recurrence.* Over 60 random segments whose every number was
|
|
64
|
+
* already a float32 (82,731 samples), the cubic's tenths evaluated
|
|
65
|
+
* directly missed 866 samples: each miss a point one float32 step off,
|
|
66
|
+
* where the exact double lay within a few hundredths of a step of a
|
|
67
|
+
* rounding midpoint — so the runtime's points carry a relative error
|
|
68
|
+
* near 1e-9 before the rounding. Forward differencing the cubic at the
|
|
69
|
+
* step h = 1/10 (the textbook recurrence: each point the previous plus a
|
|
70
|
+
* first difference, which grows by a second, which grows by a third)
|
|
71
|
+
* reproduces that error exactly when the third difference's one-sixth
|
|
72
|
+
* is the eight-digit decimal `0.16666667`: 0 misses. With 1/6 exact it
|
|
73
|
+
* missed 867, with 1/6 as a float32 1,694; a recurrence whose running
|
|
74
|
+
* sums are float32 missed more than any.
|
|
75
|
+
* 2. *The numbers it runs on.* Over 300 of the 8,145 Bézier channel
|
|
76
|
+
* segments in the 19 recipes' bone timelines, each re-posed alone
|
|
77
|
+
* (18,430 samples), the recurrence above missed 4,145 samples when run
|
|
78
|
+
* on the key times, values and handles as float32 — and 0 when run on
|
|
79
|
+
* the numbers AS THE FILE STATES THEM (the doubles its text parses to),
|
|
80
|
+
* with only the polyline's two ends, the keys themselves, float32. The
|
|
81
|
+
* mixed readings missed 2,479 (handles stated, keys float32), 2,587
|
|
82
|
+
* (keys stated, handles float32) and 1,054 (the ends stated too). The
|
|
83
|
+
* first population could not tell these apart because its numbers were
|
|
84
|
+
* float32 to begin with; the model's numbers are float32 SHORTEST NAMES
|
|
85
|
+
* (`f32` in `src/compile.ts`), whose doubles are not the float32 itself.
|
|
86
|
+
*
|
|
87
|
+
* So a key keeps both (`CoreKey.stated` and its float32 `time`/`values`),
|
|
88
|
+
* and `bezierPolyline` is the recurrence over the stated numbers. The core
|
|
89
|
+
* suite's `CA02` holds the measurement and every rejected reading missing.
|
|
90
|
+
*
|
|
91
|
+
* ## The bone timelines — alpha 1 from the setup pose
|
|
92
|
+
*
|
|
93
|
+
* Measured on bones with non-trivial setup values:
|
|
94
|
+
*
|
|
95
|
+
* - `rotate` ADDS to the setup rotation: setup 20, keys 350 → 10 read a world
|
|
96
|
+
* x axis at 10° at t = 0 and at 200° (−160°) half way — the value is
|
|
97
|
+
* interpolated as a number, 350 → 10 passing 180, and never wrapped to the
|
|
98
|
+
* short way round.
|
|
99
|
+
* - `translate`, `translatex`, `translatey` ADD to the setup position.
|
|
100
|
+
* - `scale`, `scalex`, `scaley` MULTIPLY the setup scale: setup (2, 0.5) and a
|
|
101
|
+
* key (3, 1) read (6, 0.5); a negative key or setup multiplies through
|
|
102
|
+
* (setup −2, keys −1 → 3 read 2 → −6 through 0).
|
|
103
|
+
* - `shear`, `shearx`, `sheary` ADD to the setup shear.
|
|
104
|
+
* - `inherit` sets the bone's inherit mode, stepped by the format: before its
|
|
105
|
+
* first key the setup mode, from each key that key's mode (measured
|
|
106
|
+
* `normal → onlyTranslation → normal` under a rotated, scaled parent).
|
|
107
|
+
*
|
|
108
|
+
* The posed values replace the setup values in a copy of the bone and the
|
|
109
|
+
* core's own evaluator (`worldTransforms`, `./world.ts`) poses the copies.
|
|
110
|
+
*
|
|
111
|
+
* ## The slot timelines
|
|
112
|
+
*
|
|
113
|
+
* - `attachment`: stepped by nature; before its first key the setup
|
|
114
|
+
* attachment, from a key its placeholder, resolved exactly as the setup
|
|
115
|
+
* placeholder is (`shownAttachment`), a placeholder no skin fills and a
|
|
116
|
+
* `null` name both showing nothing.
|
|
117
|
+
* - `rgba`, `rgb`, `alpha`, `rgba2`, `rgb2` SET the channels they name (they do
|
|
118
|
+
* not add to or scale the setup colour) and leave every other channel at
|
|
119
|
+
* setup: `rgb` keeps the setup alpha, `alpha` the setup rgb; `rgba2` and
|
|
120
|
+
* `rgb2` set the dark colour too. A key's hex pair reads as its byte over 255
|
|
121
|
+
* and is stored float32, as every key value is.
|
|
122
|
+
* - **Every colour channel is clamped to [0, 1] after the curve**: an `rgba`
|
|
123
|
+
* whose green curve dips below 0 and whose red curve rises above 1 read 0
|
|
124
|
+
* and 1 there.
|
|
125
|
+
* - 🚫 **`rgba2` or `rgb2` on a slot with no dark colour is refused.** Posed
|
|
126
|
+
* through the runtime, such a timeline throws while the animation is
|
|
127
|
+
* applied (`TypeError`), so no dump exists to hold a core to; the compiler
|
|
128
|
+
* refuses the same timeline before any file is written (`compileTrack`).
|
|
129
|
+
*
|
|
130
|
+
* ## The duration and the sample times
|
|
131
|
+
*
|
|
132
|
+
* The oracle's sample times are the tool's formula (`sampleTime`, which the
|
|
133
|
+
* tool calls here, as it rounds with `gridRound`), over the runtime's
|
|
134
|
+
* duration of the animation: the LAST key time of every timeline in it — the
|
|
135
|
+
* later constructs' (constraints, deform, sequence, draw order, events)
|
|
136
|
+
* included — as float32, not the model's declared `duration`. Measured: a
|
|
137
|
+
* rotate ending at 1 beside an event at 2.5, the model stating 0, sampled over
|
|
138
|
+
* 2.5 in spine-core's dump (`CA09`); over the 54 animations of the 19 recipes,
|
|
139
|
+
* every duration agreed.
|
|
140
|
+
*
|
|
141
|
+
* ## What is left out, by name
|
|
142
|
+
*
|
|
143
|
+
* The constraints are applied after the timelines, posed by their own
|
|
144
|
+
* timelines at `t` (issue #938, `./constraints.ts`), and each slider's slot
|
|
145
|
+
* timelines after the sample's own (`./constraints_slider.ts`); a document
|
|
146
|
+
* whose setup bones are absent has no animation bones from the core, for the
|
|
147
|
+
* same reason, and neither has one whose animation keys the attachment of a
|
|
148
|
+
* walked path's slot, deforms a walked path whose placeholder several skins
|
|
149
|
+
* fill, or whose path offset reads a slot bone from the previous pose that
|
|
150
|
+
* changes its reflection (`./constraints_path.ts`); a slider whose animation
|
|
151
|
+
* keys a slot on such a document, or a placeholder skins fill differently,
|
|
152
|
+
* leaves the animation slots out as they leave the setup slots out.
|
|
153
|
+
*
|
|
154
|
+
* ## The rest of a sample (issue #955)
|
|
155
|
+
*
|
|
156
|
+
* The oracle's sample carries `drawOrder`, `attachments`, `clips` and
|
|
157
|
+
* `events` too, and since issue #955 the core poses each: the draw order
|
|
158
|
+
* (`./draw_order.ts`), every drawn attachment's and clipping polygon's world
|
|
159
|
+
* vertices through the sample's bones, in that order, with what the deform
|
|
160
|
+
* and sequence timelines set on them (`./deform.ts`), and the events fired
|
|
161
|
+
* since the sample before (`./events.ts`); each module's header states its
|
|
162
|
+
* rules with the measurements. A path walks the curve the sample's deform
|
|
163
|
+
* timelines left on its slot (`pathDeformed`). The attachments and clips are
|
|
164
|
+
* absent where the bones or the slots are, and the attachments where a shown
|
|
165
|
+
* region's rectangle is `null` or a timeline moves a record several skins
|
|
166
|
+
* fill; the draw order where a slider keys it on a document whose bones are
|
|
167
|
+
* absent.
|
|
168
|
+
*/
|
|
169
|
+
import type { ModelBone, ModelSlot } from '../model.ts';
|
|
170
|
+
import { readAttachmentTimelines, type CoreAttachmentTimeline } from './deform.ts';
|
|
171
|
+
import { readDrawOrderKeys, type CoreDrawOrderKey } from './draw_order.ts';
|
|
172
|
+
import { readEventKeys, type CoreEventDef, type CoreEventKey } from './events.ts';
|
|
173
|
+
import { worldTransforms } from './world.ts';
|
|
174
|
+
import { applyConstraints, constraintsAbsentWhy, pathAnimationsWhy, posedRecords, previousPassSlotBones, solverRules, type CoreConstraintRecord, type CoreConstraintTimelines } from './constraints.ts';
|
|
175
|
+
import { attachmentStates, deformAt, deformedVertices, timelineIdentity } from './deform.ts';
|
|
176
|
+
import { freshStepContext, stepPhysicsRecords, stepSchedule, steppedPreviousPassWhy, type PhysicsStepContext } from './constraints_physics.ts';
|
|
177
|
+
import { drawOrderAt } from './draw_order.ts';
|
|
178
|
+
import { eventsFired, type CoreEventRow } from './events.ts';
|
|
179
|
+
import { poseGeometry, type CoreAttachmentRow, type CoreClipRow } from './vertices.ts';
|
|
180
|
+
import type { CoreClippedRow } from './clipping.ts';
|
|
181
|
+
import type { CoreWorld } from './world.ts';
|
|
182
|
+
import { applySliderSlots, type SliderApplication } from './constraints_slider.ts';
|
|
183
|
+
import { slotTimelinesApply } from './skins.ts';
|
|
184
|
+
import {
|
|
185
|
+
activeBones,
|
|
186
|
+
constraintRecords,
|
|
187
|
+
CoreInputError,
|
|
188
|
+
drawWalkOf,
|
|
189
|
+
foldInheritMode,
|
|
190
|
+
gridRound,
|
|
191
|
+
readBlend,
|
|
192
|
+
readColour,
|
|
193
|
+
shownAttachment,
|
|
194
|
+
shownRow,
|
|
195
|
+
slidersKeyingWhy,
|
|
196
|
+
sourceOfDoc,
|
|
197
|
+
type CompiledDocument,
|
|
198
|
+
type CoreAnimation,
|
|
199
|
+
type CoreBoneRow,
|
|
200
|
+
type CorePlant,
|
|
201
|
+
type CoreSkin,
|
|
202
|
+
type CoreSlotRow,
|
|
203
|
+
} from './index.ts';
|
|
204
|
+
|
|
205
|
+
/** The bone timelines that key numbers, and the channels each key carries, in the order a curve indexes them. */
|
|
206
|
+
export const BONE_TIMELINE_CHANNELS = {
|
|
207
|
+
rotate: ['value'],
|
|
208
|
+
translate: ['x', 'y'],
|
|
209
|
+
translatex: ['value'],
|
|
210
|
+
translatey: ['value'],
|
|
211
|
+
scale: ['x', 'y'],
|
|
212
|
+
scalex: ['value'],
|
|
213
|
+
scaley: ['value'],
|
|
214
|
+
shear: ['x', 'y'],
|
|
215
|
+
shearx: ['value'],
|
|
216
|
+
sheary: ['value'],
|
|
217
|
+
} as const satisfies Record<string, readonly string[]>;
|
|
218
|
+
export type BoneNumberKind = keyof typeof BONE_TIMELINE_CHANNELS;
|
|
219
|
+
|
|
220
|
+
/** Every bone timeline this construct poses: the ten that key numbers, and `inherit`. */
|
|
221
|
+
export const BONE_TIMELINE_KINDS = [...(Object.keys(BONE_TIMELINE_CHANNELS) as BoneNumberKind[]), 'inherit'] as const;
|
|
222
|
+
export type BoneTimelineKind = (typeof BONE_TIMELINE_KINDS)[number];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The slot timelines that key a colour: the key fields the writer spells each
|
|
226
|
+
* with (`COLOUR_KEYS` in `src/compile.ts`), each field's hex length, and the
|
|
227
|
+
* channels they carry, in the order a curve indexes them.
|
|
228
|
+
*/
|
|
229
|
+
export const SLOT_COLOUR_TIMELINES = {
|
|
230
|
+
rgba: { fields: [['color', 8]], channels: ['r', 'g', 'b', 'a'] },
|
|
231
|
+
rgb: { fields: [['color', 6]], channels: ['r', 'g', 'b'] },
|
|
232
|
+
alpha: { fields: [], channels: ['a'] },
|
|
233
|
+
rgba2: { fields: [['light', 8], ['dark', 6]], channels: ['r', 'g', 'b', 'a', 'r2', 'g2', 'b2'] },
|
|
234
|
+
rgb2: { fields: [['light', 6], ['dark', 6]], channels: ['r', 'g', 'b', 'r2', 'g2', 'b2'] },
|
|
235
|
+
} as const satisfies Record<string, { fields: ReadonlyArray<readonly [string, number]>; channels: readonly string[] }>;
|
|
236
|
+
export type SlotColourKind = keyof typeof SLOT_COLOUR_TIMELINES;
|
|
237
|
+
|
|
238
|
+
/** Every slot timeline this construct poses. */
|
|
239
|
+
export const SLOT_TIMELINE_KINDS = ['attachment', ...(Object.keys(SLOT_COLOUR_TIMELINES) as SlotColourKind[])] as const;
|
|
240
|
+
export type SlotTimelineKind = (typeof SLOT_TIMELINE_KINDS)[number];
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The groups of an animation after its bones and slots, in the document's
|
|
244
|
+
* order: the constraint timelines (construct 5, `./constraints.ts`) and this
|
|
245
|
+
* construct's remainder — attachment timelines (`./deform.ts`), the draw
|
|
246
|
+
* order (`./draw_order.ts`) and events (`./events.ts`). Every key time in
|
|
247
|
+
* them sets the runtime's duration.
|
|
248
|
+
*/
|
|
249
|
+
export const LATER_GROUPS = ['constraints', 'attachments', 'drawOrder', 'events'] as const;
|
|
250
|
+
|
|
251
|
+
/** A key's curve as read: linear, a hold, or four float32 numbers per channel. */
|
|
252
|
+
export type CoreCurve = 'linear' | 'stepped' | number[];
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* One key, as read: its time and channels as the runtime stores them
|
|
256
|
+
* (float32), the same two as the document states them (the doubles the file's
|
|
257
|
+
* text parses to — what a Bézier's polyline is computed from, the header's
|
|
258
|
+
* measurement), its curve, and a name or mode where the kind keys one.
|
|
259
|
+
*/
|
|
260
|
+
export interface CoreKey {
|
|
261
|
+
time: number;
|
|
262
|
+
values: number[];
|
|
263
|
+
stated: { time: number; values: number[] };
|
|
264
|
+
curve: CoreCurve;
|
|
265
|
+
/** An attachment key's placeholder (`null` shows nothing). */
|
|
266
|
+
name?: string | null;
|
|
267
|
+
/** An inherit key's mode, folded. */
|
|
268
|
+
mode?: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface CoreTimeline<K extends string> {
|
|
272
|
+
kind: K;
|
|
273
|
+
keys: CoreKey[];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface CoreTarget<K extends string> {
|
|
277
|
+
name: string;
|
|
278
|
+
timelines: Array<CoreTimeline<K>>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* One animation's timelines, as far as this construct reads them: the bone
|
|
283
|
+
* and slot timelines in full, and of every later group only how many
|
|
284
|
+
* timelines it holds and its key times (which set the runtime's duration).
|
|
285
|
+
*/
|
|
286
|
+
export interface CoreAnimationTimelines {
|
|
287
|
+
/** The model's declared duration. */
|
|
288
|
+
declared: number;
|
|
289
|
+
/** The runtime's duration: the last key time of every timeline, as float32 — the header's rule. */
|
|
290
|
+
duration: number;
|
|
291
|
+
bones: Array<CoreTarget<BoneTimelineKind>>;
|
|
292
|
+
slots: Array<CoreTarget<SlotTimelineKind>>;
|
|
293
|
+
/** Timelines per later group, in `LATER_GROUPS` order; a group holding none is not listed. */
|
|
294
|
+
later: Array<[(typeof LATER_GROUPS)[number], number]>;
|
|
295
|
+
/** The deform and sequence timelines, each attachment's (`./deform.ts`). */
|
|
296
|
+
attachments: CoreAttachmentTimeline[];
|
|
297
|
+
/** The draw-order keys (`./draw_order.ts`); empty where the animation keys none. */
|
|
298
|
+
drawOrder: CoreDrawOrderKey[];
|
|
299
|
+
/** The event keys (`./events.ts`); empty where the animation fires none. */
|
|
300
|
+
events: CoreEventKey[];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
304
|
+
const isHex = (v: unknown, digits: number): v is string => typeof v === 'string' && v.length === digits && /^[0-9a-fA-F]+$/.test(v);
|
|
305
|
+
|
|
306
|
+
/** A key's time: finite, not negative, float32 as the runtime stores it — or a problem. */
|
|
307
|
+
function keyTime(raw: Record<string, unknown>, where: string, problems: string[]): number | null {
|
|
308
|
+
const time = raw.time;
|
|
309
|
+
if (typeof time !== 'number' || !Number.isFinite(time) || time < 0) {
|
|
310
|
+
problems.push(`${where}: time is ${JSON.stringify(time)}, not a finite time at or after 0`);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
return time;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** A list of keys, each an object with a strictly later time than the one before — the writer's rule. */
|
|
317
|
+
function keyList(value: unknown, where: string, problems: string[]): Array<{ raw: Record<string, unknown>; at: string; time: number }> {
|
|
318
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
319
|
+
problems.push(`${where}: keys is not a non-empty list`);
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
const out: Array<{ raw: Record<string, unknown>; at: string; time: number }> = [];
|
|
323
|
+
let last = -Infinity;
|
|
324
|
+
value.forEach((raw, i) => {
|
|
325
|
+
const at = `${where}.keys[${i}]`;
|
|
326
|
+
if (!isRecord(raw)) {
|
|
327
|
+
problems.push(`${at} is not an object`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const time = keyTime(raw, at, problems);
|
|
331
|
+
if (time === null) return;
|
|
332
|
+
if (time <= last) problems.push(`${at}: time ${raw.time} is not after the key before it — the writer refuses key times that do not strictly increase`);
|
|
333
|
+
last = Math.max(last, time);
|
|
334
|
+
out.push({ raw, at, time });
|
|
335
|
+
});
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function unknownKeyFields(raw: Record<string, unknown>, known: readonly string[], at: string, problems: string[]): void {
|
|
340
|
+
for (const key of Object.keys(raw)) if (!known.includes(key)) problems.push(`${at}: field "${key}" is not one this timeline's keys carry; they carry [${known.join(', ')}]`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** A key's curve: absent (linear), `stepped`, or 4 finite numbers per channel; never on the last key. */
|
|
344
|
+
function readCurve(raw: Record<string, unknown>, channels: number, last: boolean, at: string, problems: string[]): CoreCurve {
|
|
345
|
+
const curve = raw.curve;
|
|
346
|
+
if (curve === undefined) return 'linear';
|
|
347
|
+
if (last) {
|
|
348
|
+
problems.push(`${at}: the last key carries a curve, which eases to no key — the writer refuses it`);
|
|
349
|
+
return 'linear';
|
|
350
|
+
}
|
|
351
|
+
if (curve === 'stepped') return 'stepped';
|
|
352
|
+
if (!Array.isArray(curve) || curve.length !== channels * 4 || !curve.every((n) => typeof n === 'number' && Number.isFinite(n))) {
|
|
353
|
+
problems.push(`${at}: curve is ${JSON.stringify(curve)}, not "stepped" nor ${channels * 4} finite numbers (four per channel)`);
|
|
354
|
+
return 'linear';
|
|
355
|
+
}
|
|
356
|
+
return curve as number[];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function readBoneTimeline(kind: BoneTimelineKind, keysValue: unknown, where: string, problems: string[]): CoreKey[] {
|
|
360
|
+
const listed = keyList(keysValue, where, problems);
|
|
361
|
+
return listed.map(({ raw, at, time }, i): CoreKey => {
|
|
362
|
+
if (kind === 'inherit') {
|
|
363
|
+
unknownKeyFields(raw, ['time', 'inherit'], at, problems);
|
|
364
|
+
const mode = typeof raw.inherit === 'string' ? foldInheritMode(raw.inherit) : null;
|
|
365
|
+
if (mode === null) problems.push(`${at}: inherit is ${JSON.stringify(raw.inherit)}, which folds to no inherit mode`);
|
|
366
|
+
return { time: Math.fround(time), values: [], stated: { time, values: [] }, curve: 'stepped', mode: mode ?? 'normal' };
|
|
367
|
+
}
|
|
368
|
+
const channels: readonly string[] = BONE_TIMELINE_CHANNELS[kind];
|
|
369
|
+
unknownKeyFields(raw, ['time', ...channels, 'curve'], at, problems);
|
|
370
|
+
const values = channels.map((c) => {
|
|
371
|
+
const v = raw[c];
|
|
372
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
373
|
+
problems.push(`${at}: ${c} is ${JSON.stringify(v)}, not a finite number — the writer states every channel on every key`);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
return v;
|
|
377
|
+
});
|
|
378
|
+
return { time: Math.fround(time), values: values.map(Math.fround), stated: { time, values }, curve: readCurve(raw, channels.length, i === listed.length - 1, at, problems) };
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** A hex spelling's channels, each byte over 255 (the stated values; the runtime stores each float32). */
|
|
383
|
+
function hexChannels(hex: string, count: number): number[] {
|
|
384
|
+
const out: number[] = [];
|
|
385
|
+
for (let i = 0; i < count; i++) out.push(Number.parseInt(hex.slice(2 * i, 2 * i + 2), 16) / 255);
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function readSlotTimeline(kind: SlotTimelineKind, keysValue: unknown, where: string, problems: string[]): CoreKey[] {
|
|
390
|
+
const listed = keyList(keysValue, where, problems);
|
|
391
|
+
return listed.map(({ raw, at, time }, i): CoreKey => {
|
|
392
|
+
if (kind === 'attachment') {
|
|
393
|
+
unknownKeyFields(raw, ['time', 'name'], at, problems);
|
|
394
|
+
if (raw.name !== null && (typeof raw.name !== 'string' || raw.name === '')) problems.push(`${at}: name is ${JSON.stringify(raw.name)}, not a placeholder name or null`);
|
|
395
|
+
return { time: Math.fround(time), values: [], stated: { time, values: [] }, curve: 'stepped', name: typeof raw.name === 'string' ? raw.name : null };
|
|
396
|
+
}
|
|
397
|
+
const shape: { fields: ReadonlyArray<readonly [string, number]>; channels: readonly string[] } = SLOT_COLOUR_TIMELINES[kind];
|
|
398
|
+
let values: number[] = [];
|
|
399
|
+
if (kind === 'alpha') {
|
|
400
|
+
unknownKeyFields(raw, ['time', 'value', 'curve'], at, problems);
|
|
401
|
+
const v = raw.value;
|
|
402
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) problems.push(`${at}: value is ${JSON.stringify(v)}, not a finite number`);
|
|
403
|
+
else values = [v];
|
|
404
|
+
} else {
|
|
405
|
+
unknownKeyFields(raw, ['time', ...shape.fields.map(([f]) => f), 'curve'], at, problems);
|
|
406
|
+
for (const [field, digits] of shape.fields) {
|
|
407
|
+
const v = raw[field];
|
|
408
|
+
if (!isHex(v, digits)) problems.push(`${at}: ${field} is ${JSON.stringify(v)}, not ${digits} hex digits — the one spelling the writer gives a ${kind} key's ${field}`);
|
|
409
|
+
else values.push(...hexChannels(v, field === 'dark' || digits === 6 ? 3 : 4));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (values.length !== shape.channels.length) values = shape.channels.map(() => 0);
|
|
413
|
+
return { time: Math.fround(time), values: values.map(Math.fround), stated: { time, values }, curve: readCurve(raw, shape.channels.length, i === listed.length - 1, at, problems) };
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Every key time under an animation's `constraints` group, for the runtime's duration; a time that is not a finite number is a problem. */
|
|
418
|
+
function constraintKeyTimes(value: unknown, where: string, problems: string[]): { timelines: number; times: number[] } {
|
|
419
|
+
const times: number[] = [];
|
|
420
|
+
let timelines = 0;
|
|
421
|
+
const keys = (list: unknown, at: string): void => {
|
|
422
|
+
timelines++;
|
|
423
|
+
if (!Array.isArray(list)) {
|
|
424
|
+
problems.push(`${at} is not a list of keys`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
list.forEach((k, i) => {
|
|
428
|
+
if (!isRecord(k)) problems.push(`${at}[${i}] is not an object`);
|
|
429
|
+
else {
|
|
430
|
+
const t = keyTime(k, `${at}[${i}]`, problems);
|
|
431
|
+
if (t !== null) times.push(Math.fround(t));
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
const named = (list: unknown, at: string, each: (entry: Record<string, unknown>, at: string) => void): void => {
|
|
436
|
+
if (!Array.isArray(list)) {
|
|
437
|
+
problems.push(`${at} is not a list`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
list.forEach((entry, i) => (isRecord(entry) ? each(entry, `${at}[${i}]`) : problems.push(`${at}[${i}] is not an object`)));
|
|
441
|
+
};
|
|
442
|
+
if (!isRecord(value)) problems.push(`${where} is not an object`);
|
|
443
|
+
else {
|
|
444
|
+
for (const kind of ['ik', 'transform'] as const) named(value[kind], `${where}.${kind}`, (e, at) => keys(e.keys, `${at}.keys`));
|
|
445
|
+
for (const kind of ['path', 'physics', 'slider'] as const) named(value[kind], `${where}.${kind}`, (e, at) => named(e.timelines, `${at}.timelines`, (tl, at2) => keys(tl.keys, `${at2}.keys`)));
|
|
446
|
+
}
|
|
447
|
+
return { timelines, times };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* One animation record's timelines, read and checked: every bone and slot
|
|
452
|
+
* timeline in full — a target that is not a bone or slot of the document, a
|
|
453
|
+
* kind that is not one of this construct's, a kind keyed twice on one target,
|
|
454
|
+
* a key missing a channel or spelling one otherwise than the writer does, a
|
|
455
|
+
* curve of the wrong length or on the last key, times that do not increase,
|
|
456
|
+
* and `rgba2`/`rgb2` on a slot with no dark colour are each a problem, named —
|
|
457
|
+
* and of the later groups only their key times.
|
|
458
|
+
*/
|
|
459
|
+
export function readAnimationTimelines(
|
|
460
|
+
raw: Record<string, unknown>,
|
|
461
|
+
label: string,
|
|
462
|
+
bones: ReadonlySet<string>,
|
|
463
|
+
slots: readonly ModelSlot[],
|
|
464
|
+
problems: string[],
|
|
465
|
+
context: { skins: readonly CoreSkin[]; events: ReadonlyMap<string, CoreEventDef> },
|
|
466
|
+
): CoreAnimationTimelines {
|
|
467
|
+
const declared = typeof raw.duration === 'number' && Number.isFinite(raw.duration) ? raw.duration : 0;
|
|
468
|
+
if (typeof raw.duration !== 'number' || !Number.isFinite(raw.duration)) problems.push(`${label}: duration is ${JSON.stringify(raw.duration)}, not a finite number`);
|
|
469
|
+
const slotByName = new Map(slots.map((s) => [s.name, s]));
|
|
470
|
+
const times: number[] = [];
|
|
471
|
+
const targets = <K extends string>(group: 'bones' | 'slots', kinds: readonly K[], known: (name: string) => string | null, read: (kind: K, keys: unknown, at: string, target: string) => CoreKey[]): Array<CoreTarget<K>> => {
|
|
472
|
+
const value = raw[group];
|
|
473
|
+
const out: Array<CoreTarget<K>> = [];
|
|
474
|
+
if (!Array.isArray(value)) {
|
|
475
|
+
problems.push(`${label}: ${group} is not a list`);
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
value.forEach((entry, i) => {
|
|
479
|
+
const at = `${label}.${group}[${i}]`;
|
|
480
|
+
if (!isRecord(entry) || typeof entry.name !== 'string') {
|
|
481
|
+
problems.push(`${at} names no target`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const where = `${at} "${entry.name}"`;
|
|
485
|
+
const miss = known(entry.name);
|
|
486
|
+
if (miss !== null) problems.push(`${where}: ${miss}`);
|
|
487
|
+
if (!Array.isArray(entry.timelines)) {
|
|
488
|
+
problems.push(`${where}: timelines is not a list`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const name = entry.name;
|
|
492
|
+
const target: CoreTarget<K> = { name, timelines: [] };
|
|
493
|
+
entry.timelines.forEach((tl, j) => {
|
|
494
|
+
const tat = `${where}.timelines[${j}]`;
|
|
495
|
+
if (!isRecord(tl) || typeof tl.name !== 'string') {
|
|
496
|
+
problems.push(`${tat} names no timeline`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const kind = kinds.find((k) => k === tl.name);
|
|
500
|
+
if (kind === undefined) {
|
|
501
|
+
problems.push(`${tat}: "${tl.name}" is not a ${group === 'bones' ? 'bone' : 'slot'} timeline this core reads; it reads [${kinds.join(', ')}]`);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (target.timelines.some((x) => x.kind === kind)) problems.push(`${tat}: "${kind}" is keyed twice on ${name} — the writer merges one property's tracks into one`);
|
|
505
|
+
const keys = read(kind, tl.keys, `${tat} "${kind}"`, name);
|
|
506
|
+
for (const k of keys) times.push(k.time);
|
|
507
|
+
target.timelines.push({ kind, keys });
|
|
508
|
+
});
|
|
509
|
+
out.push(target);
|
|
510
|
+
});
|
|
511
|
+
return out;
|
|
512
|
+
};
|
|
513
|
+
const boneTargets = targets('bones', BONE_TIMELINE_KINDS, (n) => (bones.has(n) ? null : `"${n}" is not a bone of this document`), (kind, keys, at) => readBoneTimeline(kind, keys, at, problems));
|
|
514
|
+
const slotTargets = targets('slots', SLOT_TIMELINE_KINDS, (n) => (slotByName.has(n) ? null : `"${n}" is not a slot of this document`), (kind, keys, at, slot) => {
|
|
515
|
+
if ((kind === 'rgba2' || kind === 'rgb2') && slotByName.get(slot)?.dark === undefined) {
|
|
516
|
+
problems.push(`${at}: slot "${slot}" states no dark colour, and a ${kind} timeline on such a slot makes the runtime throw while it is applied — the writer refuses it`);
|
|
517
|
+
}
|
|
518
|
+
return readSlotTimeline(kind, keys, at, problems);
|
|
519
|
+
});
|
|
520
|
+
const later: CoreAnimationTimelines['later'] = [];
|
|
521
|
+
// The constraint timelines' key times only (construct 5 reads them, `./constraints.ts`); the other three groups are read in full here.
|
|
522
|
+
const constraintTimes = constraintKeyTimes(raw.constraints, `${label}.constraints`, problems);
|
|
523
|
+
times.push(...constraintTimes.times);
|
|
524
|
+
if (constraintTimes.timelines > 0) later.push(['constraints', constraintTimes.timelines]);
|
|
525
|
+
const attachments = readAttachmentTimelines(raw.attachments, label, context.skins, problems);
|
|
526
|
+
const drawOrder = readDrawOrderKeys(raw.drawOrder, label, slots, problems);
|
|
527
|
+
const events = readEventKeys(raw.events, label, context.events, problems);
|
|
528
|
+
for (const a of attachments) for (const k of [...(a.deform ?? []), ...(a.sequence ?? [])]) times.push(k.time);
|
|
529
|
+
for (const k of [...drawOrder, ...events]) times.push(k.time);
|
|
530
|
+
const attachmentTimelines = attachments.reduce((n, a) => n + (a.deform === null ? 0 : 1) + (a.sequence === null ? 0 : 1), 0);
|
|
531
|
+
if (attachmentTimelines > 0) later.push(['attachments', attachmentTimelines]);
|
|
532
|
+
if (drawOrder.length > 0) later.push(['drawOrder', 1]);
|
|
533
|
+
if (events.length > 0) later.push(['events', 1]);
|
|
534
|
+
return { declared, duration: times.length === 0 ? 0 : Math.max(...times), bones: boneTargets, slots: slotTargets, later, attachments, drawOrder, events };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// evaluation
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The one-sixth of the third forward difference, as the header measured it:
|
|
543
|
+
* the eight-digit decimal, which alone of the spellings tried reproduced
|
|
544
|
+
* 82,731 samples of 60 random Bézier segments and 18,430 of 300 corpus
|
|
545
|
+
* segments at the oracle's rounding.
|
|
546
|
+
*/
|
|
547
|
+
export const BEZIER_SIXTH = 0.16666667;
|
|
548
|
+
/** Pieces of the runtime's polyline per Bézier segment — the header's ten slope runs. */
|
|
549
|
+
export const BEZIER_PIECES = 10;
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The nine interior points of one channel's Bézier segment, `[t1, v1, …, t9,
|
|
553
|
+
* v9]`, each float32: the cubic from `(t0, v0)` through the handles to `(t3,
|
|
554
|
+
* v3)` — every one of the eight the number the document states, not its
|
|
555
|
+
* float32 (the header's second measurement) — at its parameter `u = 0.1 …
|
|
556
|
+
* 0.9`, by forward differences at the step
|
|
557
|
+
* h = 1/10. With `p` the four control numbers of one coordinate, the second
|
|
558
|
+
* difference is `3h²(p0 − 2p1 + p2)` and the third `6h³(3(p1 − p2) − p0 + p3)`;
|
|
559
|
+
* the first starts at `3h(p1 − p0)` plus the second plus the third times
|
|
560
|
+
* `BEZIER_SIXTH`; each step adds the first to the point, the second to the
|
|
561
|
+
* first and the third to the second (the header's measurement fixes the
|
|
562
|
+
* order of those additions and the constant).
|
|
563
|
+
*/
|
|
564
|
+
export function bezierPolyline(t0: number, v0: number, cx1: number, cy1: number, cx2: number, cy2: number, t3: number, v3: number): number[] {
|
|
565
|
+
const walk = (p0: number, p1: number, p2: number, p3: number): number[] => {
|
|
566
|
+
const second = (p0 - 2 * p1 + p2) * 0.03;
|
|
567
|
+
const third = ((p1 - p2) * 3 - p0 + p3) * 0.006;
|
|
568
|
+
let first = (p1 - p0) * 0.3 + second + third * BEZIER_SIXTH;
|
|
569
|
+
let d2 = second * 2 + third;
|
|
570
|
+
let at = p0;
|
|
571
|
+
const out: number[] = [];
|
|
572
|
+
for (let i = 1; i < BEZIER_PIECES; i++) {
|
|
573
|
+
at += first;
|
|
574
|
+
first += d2;
|
|
575
|
+
d2 += third;
|
|
576
|
+
out.push(Math.fround(at));
|
|
577
|
+
}
|
|
578
|
+
return out;
|
|
579
|
+
};
|
|
580
|
+
const ts = walk(t0, cx1, cx2, t3);
|
|
581
|
+
const vs = walk(v0, cy1, cy2, v3);
|
|
582
|
+
const out: number[] = [];
|
|
583
|
+
for (let i = 0; i < ts.length; i++) out.push(ts[i], vs[i]);
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** What evaluates one channel of a segment: `channelAt` unless a plant passes another. */
|
|
588
|
+
export type ChannelEvaluator = (keys: readonly CoreKey[], index: number, channel: number, t: number) => number;
|
|
589
|
+
|
|
590
|
+
/** What finds the key a time falls on: `keyIndexAt` unless a plant passes another. */
|
|
591
|
+
export type KeySearch = (keys: readonly CoreKey[], t: number) => number;
|
|
592
|
+
|
|
593
|
+
/** The last key at or before `t`, or −1 before the first — the header's key search. */
|
|
594
|
+
export function keyIndexAt(keys: ReadonlyArray<{ time: number }>, t: number): number {
|
|
595
|
+
let found = -1;
|
|
596
|
+
for (let i = 0; i < keys.length; i++) {
|
|
597
|
+
if (keys[i].time <= t) found = i;
|
|
598
|
+
else break;
|
|
599
|
+
}
|
|
600
|
+
return found;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Each Bézier segment's polylines, one per channel, kept with the key that
|
|
605
|
+
* starts the segment and the key that ends it (issue #1134): a polyline is a
|
|
606
|
+
* function of those two keys' stated numbers and handles only, and a walk
|
|
607
|
+
* evaluated the same segment's on every step. A key read is never written; a
|
|
608
|
+
* plant that changes a key passes a copy, which keeps polylines of its own.
|
|
609
|
+
*/
|
|
610
|
+
const segmentPolylines = new WeakMap<CoreKey, { end: CoreKey; channels: Array<number[] | undefined> }>();
|
|
611
|
+
|
|
612
|
+
/** Channel `channel` of the segment starting at key `index` (not the last), at `t` — the header's three curves. */
|
|
613
|
+
export function channelAt(keys: readonly CoreKey[], index: number, channel: number, t: number): number {
|
|
614
|
+
const a = keys[index];
|
|
615
|
+
const b = keys[index + 1];
|
|
616
|
+
if (b === undefined || a.curve === 'stepped') return a.values[channel];
|
|
617
|
+
if (a.curve === 'linear') return a.values[channel] + ((t - a.time) / (b.time - a.time)) * (b.values[channel] - a.values[channel]);
|
|
618
|
+
let kept = segmentPolylines.get(a);
|
|
619
|
+
if (kept === undefined || kept.end !== b) {
|
|
620
|
+
kept = { end: b, channels: [] };
|
|
621
|
+
segmentPolylines.set(a, kept);
|
|
622
|
+
}
|
|
623
|
+
let points = kept.channels[channel];
|
|
624
|
+
if (points === undefined) {
|
|
625
|
+
const c = a.curve.slice(channel * 4, channel * 4 + 4);
|
|
626
|
+
const inner = bezierPolyline(a.stated.time, a.stated.values[channel], c[0], c[1], c[2], c[3], b.stated.time, b.stated.values[channel]);
|
|
627
|
+
points = [a.time, a.values[channel], ...inner, b.time, b.values[channel]];
|
|
628
|
+
kept.channels[channel] = points;
|
|
629
|
+
}
|
|
630
|
+
let i = 2;
|
|
631
|
+
while (i < points.length - 2 && points[i] < t) i += 2;
|
|
632
|
+
const [x0, y0, x1, y1] = [points[i - 2], points[i - 1], points[i], points[i + 1]];
|
|
633
|
+
return y0 + ((t - x0) / (x1 - x0)) * (y1 - y0);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Every plant this construct takes, beside the setup constructs' own (`CorePlant`). */
|
|
637
|
+
export interface TimelinePlant extends CorePlant {
|
|
638
|
+
channel?: ChannelEvaluator;
|
|
639
|
+
search?: KeySearch;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** A timeline's channels at `t`, or `null` before its first key (the channels are then at setup). */
|
|
643
|
+
function valuesAt(keys: readonly CoreKey[], t: number, plant: TimelinePlant): number[] | null {
|
|
644
|
+
const index = (plant.search ?? keyIndexAt)(keys, t);
|
|
645
|
+
if (index < 0) return null;
|
|
646
|
+
const channel = plant.channel ?? channelAt;
|
|
647
|
+
return keys[index].values.map((_v, c) => channel(keys, index, c, t));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** The key a stepped-by-nature timeline (attachment, inherit) shows at `t`, or `null` before its first key. */
|
|
651
|
+
function keyAt(keys: readonly CoreKey[], t: number, plant: TimelinePlant): CoreKey | null {
|
|
652
|
+
const index = (plant.search ?? keyIndexAt)(keys, t);
|
|
653
|
+
return index < 0 ? null : keys[index];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Each animation's bone timelines by bone name, kept with the timelines they index (`posedBones`). */
|
|
657
|
+
const boneTimelinesByName = new WeakMap<CoreAnimationTimelines, ReadonlyMap<string, CoreTarget<BoneTimelineKind>>>();
|
|
658
|
+
|
|
659
|
+
/** The animation's bone timelines by bone name, built once per timelines object (issue #1134: a walk built it again on every step); timelines read are never written. */
|
|
660
|
+
function boneTimelinesOf(timelines: CoreAnimationTimelines): ReadonlyMap<string, CoreTarget<BoneTimelineKind>> {
|
|
661
|
+
let byName = boneTimelinesByName.get(timelines);
|
|
662
|
+
if (byName === undefined) {
|
|
663
|
+
byName = new Map(timelines.bones.map((b) => [b.name, b]));
|
|
664
|
+
boneTimelinesByName.set(timelines, byName);
|
|
665
|
+
}
|
|
666
|
+
return byName;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** The bones posed by the animation's bone timelines at `t`: copies of the setup bones with the posed fields — the header's rules. */
|
|
670
|
+
export function posedBones(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant = {}): ModelBone[] {
|
|
671
|
+
const byName = boneTimelinesOf(timelines);
|
|
672
|
+
return doc.bones.map((setup) => {
|
|
673
|
+
const target = byName.get(setup.name);
|
|
674
|
+
if (target === undefined) return setup;
|
|
675
|
+
const bone: ModelBone = { ...setup };
|
|
676
|
+
for (const tl of target.timelines) {
|
|
677
|
+
if (tl.kind === 'inherit') {
|
|
678
|
+
const key = keyAt(tl.keys, t, plant);
|
|
679
|
+
if (key === null) {
|
|
680
|
+
if (setup.inheritMode === undefined) delete bone.inheritMode;
|
|
681
|
+
else bone.inheritMode = setup.inheritMode;
|
|
682
|
+
} else bone.inheritMode = key.mode;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const v = valuesAt(tl.keys, t, plant);
|
|
686
|
+
const add = (field: 'x' | 'y' | 'rotation' | 'shearX' | 'shearY', i: number): void => {
|
|
687
|
+
bone[field] = (setup[field] ?? 0) + (v === null ? 0 : v[i]);
|
|
688
|
+
};
|
|
689
|
+
const times = (field: 'scaleX' | 'scaleY', i: number): void => {
|
|
690
|
+
bone[field] = (setup[field] ?? 1) * (v === null ? 1 : v[i]);
|
|
691
|
+
};
|
|
692
|
+
switch (tl.kind) {
|
|
693
|
+
case 'rotate': add('rotation', 0); break;
|
|
694
|
+
case 'translate': add('x', 0); add('y', 1); break;
|
|
695
|
+
case 'translatex': add('x', 0); break;
|
|
696
|
+
case 'translatey': add('y', 0); break;
|
|
697
|
+
case 'shear': add('shearX', 0); add('shearY', 1); break;
|
|
698
|
+
case 'shearx': add('shearX', 0); break;
|
|
699
|
+
case 'sheary': add('shearY', 0); break;
|
|
700
|
+
case 'scale': times('scaleX', 0); times('scaleY', 1); break;
|
|
701
|
+
case 'scalex': times('scaleX', 0); break;
|
|
702
|
+
case 'scaley': times('scaleY', 0); break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return bone;
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* The slot rows at `t`, in the oracle's shape and rounding, or the conflicts
|
|
713
|
+
* that leave them out: a slot showing a placeholder several skins fill
|
|
714
|
+
* differently (the setup slots' ⚠️, `shownAttachment`).
|
|
715
|
+
*/
|
|
716
|
+
export function posedSlots(
|
|
717
|
+
doc: CompiledDocument,
|
|
718
|
+
timelines: CoreAnimationTimelines,
|
|
719
|
+
t: number,
|
|
720
|
+
plant: TimelinePlant = {},
|
|
721
|
+
sliders: readonly SliderApplication[] = [],
|
|
722
|
+
/** Filled with each slot's placeholder once the sample's own timelines have moved it, before the sliders — what the attachments are posed from (`attachmentStates`). */
|
|
723
|
+
placeholders?: Map<string, string | null>,
|
|
724
|
+
): { rows: CoreSlotRow[]; conflicts: string[] } {
|
|
725
|
+
const resolve = plant.shown ?? shownAttachment;
|
|
726
|
+
const colour = plant.colour ?? readColour;
|
|
727
|
+
const blend = plant.blend ?? readBlend;
|
|
728
|
+
const round = plant.round ?? gridRound;
|
|
729
|
+
const byName = new Map(timelines.slots.map((s) => [s.name, s]));
|
|
730
|
+
const rows: CoreSlotRow[] = [];
|
|
731
|
+
const conflicts: string[] = [];
|
|
732
|
+
const active = activeBones(doc);
|
|
733
|
+
const gate = plant.slotTimelines ?? slotTimelinesApply;
|
|
734
|
+
for (const slot of doc.slots) {
|
|
735
|
+
let placeholder = slot.setup;
|
|
736
|
+
const light = slot.color === undefined ? [1, 1, 1, 1] : colour(slot.color);
|
|
737
|
+
const setupDark = slot.dark === undefined ? null : readColour(slot.dark);
|
|
738
|
+
const dark = setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]];
|
|
739
|
+
// A slot on a bone the skin leaves inactive is not animated — not by the sample's timelines, not by a slider (`./skins.ts`).
|
|
740
|
+
const live = gate(doc, slot, active);
|
|
741
|
+
for (const tl of live ? (byName.get(slot.name)?.timelines ?? []) : []) {
|
|
742
|
+
if (tl.kind === 'attachment') {
|
|
743
|
+
const key = keyAt(tl.keys, t, plant);
|
|
744
|
+
placeholder = key === null ? slot.setup : (key.name ?? null);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const v = valuesAt(tl.keys, t, plant);
|
|
748
|
+
const setupLight = slot.color === undefined ? [1, 1, 1, 1] : colour(slot.color);
|
|
749
|
+
const channels = SLOT_COLOUR_TIMELINES[tl.kind].channels;
|
|
750
|
+
channels.forEach((name, i) => {
|
|
751
|
+
const at = { r: 0, g: 1, b: 2, a: 3, r2: 4, g2: 5, b2: 6 }[name];
|
|
752
|
+
if (at < 4) light[at] = v === null ? setupLight[at] : clamp01(v[i]);
|
|
753
|
+
else if (dark !== null && setupDark !== null) dark[at - 4] = v === null ? setupDark[at - 4] : clamp01(v[i]);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
// The sliders, after the animation, in constraint order (`./constraints_slider.ts`).
|
|
757
|
+
// What the slot shows after the sample's own timelines: the attachments' deform and sequence timelines are matched against it (`./deform.ts`, *A switch*).
|
|
758
|
+
placeholders?.set(slot.name, placeholder);
|
|
759
|
+
const pose = { placeholder, light, dark };
|
|
760
|
+
if (live) applySliderSlots(slot.name, pose, sliders);
|
|
761
|
+
placeholder = pose.placeholder;
|
|
762
|
+
const shown = placeholder === null ? null : resolve(doc, { ...slot, setup: placeholder });
|
|
763
|
+
if (shown !== null && 'conflict' in shown) {
|
|
764
|
+
conflicts.push(`slot "${slot.name}" shows placeholder "${placeholder}", filled by skins ${shown.conflict.map((c) => `"${c.skin}" (shows ${JSON.stringify(c.shown)})`).join(', ')}`);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
const row = shown === null ? null : shownRow(shown);
|
|
768
|
+
rows.push([
|
|
769
|
+
slot.name,
|
|
770
|
+
row === null ? null : row.name,
|
|
771
|
+
round(light[0]), round(light[1]), round(light[2]), round(light[3]),
|
|
772
|
+
dark === null ? null : [round(dark[0]), round(dark[1]), round(dark[2])],
|
|
773
|
+
row === null ? null : row.path,
|
|
774
|
+
// The slot's data, not its pose: no timeline moves it (the index header's blend rule).
|
|
775
|
+
blend(slot),
|
|
776
|
+
]);
|
|
777
|
+
}
|
|
778
|
+
return { rows, conflicts };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* The bone rows at `t`, in the oracle's shape and rounding. With `constraints`
|
|
783
|
+
* — the animation's constraint timelines — the document's constraints are
|
|
784
|
+
* applied after the timelines, posed at `t` (`./constraints.ts`, construct
|
|
785
|
+
* 5; each slider's application recorded into `sliders` for its slot
|
|
786
|
+
* timelines), with the setup pose standing for the
|
|
787
|
+
* previous pose a path's offset may read (`previousPassWhy` holds that);
|
|
788
|
+
* without, the hierarchy and the timelines alone.
|
|
789
|
+
*/
|
|
790
|
+
export function posedBoneRows(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant = {}, constraints?: CoreConstraintTimelines, sliders?: SliderApplication[]): CoreBoneRow[] {
|
|
791
|
+
return posedBoneWorld(doc, timelines, t, plant, constraints, sliders).rows;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** `posedBoneRows`, with the world transforms the rows were read off — what the sample's attachments are posed through. */
|
|
795
|
+
export function posedBoneWorld(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant = {}, constraints?: CoreConstraintTimelines, sliders?: SliderApplication[], step?: { ctx: PhysicsStepContext; before: number }): { rows: CoreBoneRow[]; world: Map<string, CoreWorld> } {
|
|
796
|
+
const posed = posedBoneStep(doc, timelines, t, plant, constraints, sliders, step);
|
|
797
|
+
return { rows: stepRows(posed), world: posed.world };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* `posedBoneWorld`'s world transforms without its rows (issue #1179, the
|
|
802
|
+
* second part): the same pose by the same operations in the same order, since
|
|
803
|
+
* `stepRows` only reads the world once it is posed. A walk reads `world` and
|
|
804
|
+
* no row, so the scan's walk (`scanWalkIn` in `./raw.ts`, A10's) asks for
|
|
805
|
+
* this; the raw entry's walk still forms the rows it does not read, unchanged.
|
|
806
|
+
*/
|
|
807
|
+
export function posedBoneWorldAlone(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: { ctx: PhysicsStepContext; before: number } | undefined): Map<string, CoreWorld> {
|
|
808
|
+
return posedBoneStep(doc, timelines, t, plant, constraints, sliders, step).world;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* One pose of the bones before its rows are read: the world transforms, and
|
|
813
|
+
* what the rows are rounded from. A stepped walk reads rows only off the step
|
|
814
|
+
* that lands on a sample (issue #1134: of the stepped run's poses, nine in ten
|
|
815
|
+
* are steps between samples), so `stepRows` rounds them when they are read,
|
|
816
|
+
* once; every bone's transform is required here, at every step.
|
|
817
|
+
*/
|
|
818
|
+
interface PosedStep {
|
|
819
|
+
bones: ModelBone[];
|
|
820
|
+
world: Map<string, CoreWorld>;
|
|
821
|
+
active: ReadonlySet<string>;
|
|
822
|
+
round: (v: number) => number | null;
|
|
823
|
+
rows?: CoreBoneRow[];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** A step's bone rows, in the oracle's shape and rounding, rounded the first time they are read. */
|
|
827
|
+
function stepRows(posed: PosedStep): CoreBoneRow[] {
|
|
828
|
+
if (posed.rows !== undefined) return posed.rows;
|
|
829
|
+
const { world, active, round } = posed;
|
|
830
|
+
posed.rows = posed.bones.map((b): CoreBoneRow => {
|
|
831
|
+
const w = world.get(b.name) as CoreWorld;
|
|
832
|
+
return [b.name, round(w.worldX), round(w.worldY), round(w.a), round(w.b), round(w.c), round(w.d), active.has(b.name) ? 1 : 0, b.parent ?? null];
|
|
833
|
+
});
|
|
834
|
+
return posed.rows;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** `posedBoneWorld` up to its rows, refusing a bone the evaluator gave no transform (`PosedStep`). */
|
|
838
|
+
function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: { ctx: PhysicsStepContext; before: number } | undefined): PosedStep {
|
|
839
|
+
const active = activeBones(doc);
|
|
840
|
+
const bones = posedBones(doc, timelines, t, plant);
|
|
841
|
+
let world = (plant.evaluate ?? worldTransforms)(bones, active);
|
|
842
|
+
if (constraints !== undefined && step !== undefined) {
|
|
843
|
+
// One step of the stepped phase (issue #956, `./constraints_physics.ts`): the physics records posed by their timelines and stepped under the walk's context; the deformed curve a path walks as above. No previous pass: under the step it is the previous step's, and `poseAnimations` leaves such bones out.
|
|
844
|
+
const records = stepPhysicsRecords(pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, active, step.ctx, step.before);
|
|
845
|
+
world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(records) : records, null, sliders, step.ctx, undefined, solverRules(plant.solver));
|
|
846
|
+
} else if (constraints !== undefined) {
|
|
847
|
+
const setupRecords = constraintRecords(doc);
|
|
848
|
+
// A path walks the curve the sample's deform timelines left on its slot (`./deform.ts`); a slider's deform of a walked path leaves the bones out (`pathAnimationsWhy`).
|
|
849
|
+
const records = pathDeformed(doc, posedRecords(setupRecords, constraints, t), timelines, t, plant);
|
|
850
|
+
// A path constraint may read a slot bone the runtime has not yet brought up to date in this pass, as the previous pass left it (`./constraints_path.ts`, *Which slot bone*); `poseAnimations` holds that pass to the setup pose's reading.
|
|
851
|
+
const previous = setupRecords.some((r) => r.kind === 'path') ? applyConstraints(doc.bones, (plant.evaluate ?? worldTransforms)(doc.bones, active), active, plant.constraints ? plant.constraints(setupRecords) : setupRecords, null, undefined, undefined, undefined, solverRules(plant.solver)) : null;
|
|
852
|
+
world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(records) : records, previous, sliders, undefined, undefined, solverRules(plant.solver));
|
|
853
|
+
}
|
|
854
|
+
for (const b of bones) if (world.get(b.name) === undefined) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
|
|
855
|
+
return { bones, world, active, round: plant.round ?? gridRound };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Each path record with the curve its slot shows deformed by the sample's own deform timelines at `t` (`./deform.ts`), or as it was. */
|
|
859
|
+
function pathDeformed(doc: CompiledDocument, records: CoreConstraintRecord[], timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant): CoreConstraintRecord[] {
|
|
860
|
+
if (timelines.attachments.every((a) => a.deform === null)) return records;
|
|
861
|
+
return records.map((r) => {
|
|
862
|
+
if (r.kind !== 'path' || r.path === null) return r;
|
|
863
|
+
const slot = doc.slots.find((x) => x.name === r.slot);
|
|
864
|
+
const shown = slot === undefined ? null : (plant.shown ?? shownAttachment)(doc, slot);
|
|
865
|
+
if (shown === null || 'conflict' in shown) return r;
|
|
866
|
+
const identity = timelineIdentity(r.slot, shown);
|
|
867
|
+
const keyed = timelines.attachments.find((a) => a.deform !== null && `${a.skin}/${a.slot}/${a.attachment}` === identity);
|
|
868
|
+
if (keyed === undefined || keyed.deform === null) return r;
|
|
869
|
+
const d = (plant.deform ?? deformAt)(r.path.vertices, keyed.deform, t);
|
|
870
|
+
return d === null ? r : { ...r, path: { ...r.path, vertices: deformedVertices(r.path.vertices, d), exact: true } };
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ---------------------------------------------------------------------------
|
|
875
|
+
// the samples
|
|
876
|
+
// ---------------------------------------------------------------------------
|
|
877
|
+
|
|
878
|
+
/** The oracle's phases, as `tools/pose_oracle.ts` names them. */
|
|
879
|
+
export type SamplePhase = 'grid' | 'off' | 'irr' | 'dense';
|
|
880
|
+
|
|
881
|
+
/** `1 − 1/φ`, the tool's irrational offset, to the nine places it writes. */
|
|
882
|
+
export const IRR_OFFSET = 0.381966011;
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Sample `i` of `n` over duration `d` under `phase` — the tool's formula
|
|
886
|
+
* (`tools/pose_oracle.ts`, *Sample times*), held here so the two dumpers
|
|
887
|
+
* sample at one set of times: `grid` `d·i/(n−1)` (`0` when `n` is 1), `off`
|
|
888
|
+
* `d·(i+0.5)/n`, `irr` and `dense` `d·(i+IRR_OFFSET)/n`.
|
|
889
|
+
*/
|
|
890
|
+
export function sampleTime(phase: SamplePhase, d: number, i: number, n: number): number {
|
|
891
|
+
if (phase === 'off') return (d * (i + 0.5)) / n;
|
|
892
|
+
if (phase === 'irr' || phase === 'dense') return (d * (i + IRR_OFFSET)) / n;
|
|
893
|
+
return n === 1 ? 0 : (d * i) / (n - 1);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** One posed sample: the time and the blocks this construct produces (`null` where it leaves one out). */
|
|
897
|
+
export interface CoreSample {
|
|
898
|
+
t: number | null;
|
|
899
|
+
events: CoreEventRow[] | null;
|
|
900
|
+
bones: CoreBoneRow[] | null;
|
|
901
|
+
slots: CoreSlotRow[] | null;
|
|
902
|
+
drawOrder: string[] | null;
|
|
903
|
+
attachments: CoreAttachmentRow[] | null;
|
|
904
|
+
clips: CoreClipRow[] | null;
|
|
905
|
+
/** The triangles drawn under a clip (issue #964, `./clipping.ts`). */
|
|
906
|
+
clipped: CoreClippedRow[] | null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export interface CoreAnimationPose {
|
|
910
|
+
name: string;
|
|
911
|
+
duration: number | null;
|
|
912
|
+
samples: CoreSample[];
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
/** Why the animation slots are left out because a slider poses them and the bones are absent, or null. */
|
|
917
|
+
function slidersWhy(doc: CompiledDocument): string | null {
|
|
918
|
+
const keyed = doc.constraints.flatMap((c) => {
|
|
919
|
+
if (c.kind !== 'slider') return [];
|
|
920
|
+
const slots = doc.animations.find((a) => a.name === c.animation)?.slots ?? [];
|
|
921
|
+
return slots.length === 0 ? [] : [`slider "${c.name}" applies animation "${c.animation}", which keys slot(s) ${slots.map((x) => `"${x}"`).join(', ')}`];
|
|
922
|
+
});
|
|
923
|
+
return keyed.length === 0 ? null : `${keyed.join('; ')} — the oracle applies sliders after every animation, and a slider poses the slots its animation keys at a time read off the bones, which are absent`;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Why the samples' bones are left out although each was posed, or null: a
|
|
928
|
+
* path constraint whose offset reads its slot bone from the previous pass
|
|
929
|
+
* (`previousPassSlotBones`) is posed with the setup pose's reading of it,
|
|
930
|
+
* which is the runtime's exactly when that bone's world keeps the sign of
|
|
931
|
+
* its determinant — its reflection — at the setup pose and at every sample;
|
|
932
|
+
* the oracle's previous pass is the sample before, in its own order of
|
|
933
|
+
* animations, which the model does not hold.
|
|
934
|
+
*/
|
|
935
|
+
function previousPassWhy(doc: CompiledDocument, animations: readonly CoreAnimationPose[], plant: TimelinePlant): string | null {
|
|
936
|
+
const active = activeBones(doc);
|
|
937
|
+
const reads = previousPassSlotBones(doc, active);
|
|
938
|
+
if (reads.length === 0) return null;
|
|
939
|
+
const records = constraintRecords(doc);
|
|
940
|
+
const setup = applyConstraints(doc.bones, (plant.evaluate ?? worldTransforms)(doc.bones, active), active, plant.constraints ? plant.constraints(records) : records, null, undefined, undefined, undefined, solverRules(plant.solver));
|
|
941
|
+
const bad: string[] = [];
|
|
942
|
+
for (const { constraint, bone } of reads) {
|
|
943
|
+
const w = setup.get(bone);
|
|
944
|
+
const sign = w === undefined ? 0 : Math.sign(w.a * w.d - w.b * w.c);
|
|
945
|
+
const flips = animations.flatMap((a) => a.samples.filter((x) => {
|
|
946
|
+
const row = x.bones?.find((r) => r[0] === bone);
|
|
947
|
+
if (row === undefined || row[3] === null || row[4] === null || row[5] === null || row[6] === null) return true;
|
|
948
|
+
const det = row[3] * row[6] - row[4] * row[5];
|
|
949
|
+
return Math.abs(det) < 1e-6 || Math.sign(det) !== sign;
|
|
950
|
+
}).map((x) => `${a.name}@${x.t}`));
|
|
951
|
+
if (sign === 0 || flips.length > 0) bad.push(`path constraint "${constraint}" reads slot bone "${bone}" from the previous pose, and its reflection is not the setup's at ${flips.slice(0, 3).join(', ')}${flips.length > 3 ? ` and ${flips.length - 3} more` : ''}`);
|
|
952
|
+
}
|
|
953
|
+
return bad.length === 0 ? null : `${bad.join('; ')} — the runtime's previous pose is the sample before in the Spine file's order of animations, which the model does not hold`;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Every animation of the document sampled as the oracle samples it: `n`
|
|
958
|
+
* samples in `phase` over each animation's runtime duration, each the bone
|
|
959
|
+
* rows and the slot rows at `t`, in the model's order of animations. Returns
|
|
960
|
+
* the blocks left out with their reasons, `animations.bones` and
|
|
961
|
+
* `animations.slots` in document order.
|
|
962
|
+
*/
|
|
963
|
+
export function poseAnimations(doc: CompiledDocument, phase: SamplePhase, n: number, plant: TimelinePlant = {}, dt?: number): { animations: CoreAnimationPose[]; absent: Array<[string, string]> } {
|
|
964
|
+
let bonesReason = constraintsAbsentWhy(doc, solverRules(plant.solver)) ?? pathAnimationsWhy(doc) ?? (dt === undefined ? null : steppedPreviousPassWhy(doc));
|
|
965
|
+
const slotConflicts: string[] = [];
|
|
966
|
+
const attachmentWhy: string[] = [];
|
|
967
|
+
const clippedWhy: string[] = [];
|
|
968
|
+
const resolve = plant.shown ?? shownAttachment;
|
|
969
|
+
const round = plant.round ?? gridRound;
|
|
970
|
+
const animations = doc.animations.map((anim: CoreAnimation): CoreAnimationPose => {
|
|
971
|
+
const d = anim.timelines.duration;
|
|
972
|
+
const samples: CoreSample[] = [];
|
|
973
|
+
let last = -1;
|
|
974
|
+
// Under `--physics step` (issue #956): one walk per animation — reset at 0, then the oracle's schedule, each sample posed off its last step (`./constraints_physics.ts`).
|
|
975
|
+
const walk = dt === undefined || bonesReason !== null ? null : { ctx: freshStepContext(plant.physicsStep), now: 0, schedule: stepSchedule(phase, d, n, dt), sliders: [] as SliderApplication[], posed: null as PosedStep | null };
|
|
976
|
+
if (walk !== null) {
|
|
977
|
+
walk.posed = posedBoneStep(doc, anim.timelines, 0, plant, anim.constraints, walk.sliders, { ctx: walk.ctx, before: 0 });
|
|
978
|
+
walk.ctx.phase = 'update';
|
|
979
|
+
}
|
|
980
|
+
for (let i = 0; i < n; i++) {
|
|
981
|
+
const t = sampleTime(phase, d, i, n);
|
|
982
|
+
let sliders: SliderApplication[] = [];
|
|
983
|
+
let posed: { rows: CoreBoneRow[]; world: Map<string, CoreWorld> } | null = null;
|
|
984
|
+
if (walk !== null) {
|
|
985
|
+
for (const s of walk.schedule[i]) {
|
|
986
|
+
const before = walk.ctx.time;
|
|
987
|
+
walk.ctx.time += s - walk.now;
|
|
988
|
+
walk.sliders = [];
|
|
989
|
+
walk.posed = posedBoneStep(doc, anim.timelines, s, plant, anim.constraints, walk.sliders, { ctx: walk.ctx, before });
|
|
990
|
+
walk.now = s;
|
|
991
|
+
}
|
|
992
|
+
sliders = walk.sliders;
|
|
993
|
+
posed = walk.posed === null ? null : { rows: stepRows(walk.posed), world: walk.posed.world };
|
|
994
|
+
} else posed = bonesReason === null ? posedBoneWorld(doc, anim.timelines, t, plant, anim.constraints, sliders) : null;
|
|
995
|
+
const placeholders = new Map<string, string | null>();
|
|
996
|
+
const slots = posedSlots(doc, anim.timelines, t, plant, sliders, placeholders);
|
|
997
|
+
for (const c of slots.conflicts) if (!slotConflicts.includes(`${c} at animation "${anim.name}"`)) slotConflicts.push(`${c} at animation "${anim.name}"`);
|
|
998
|
+
// The draw order: the sample's key, or the setup order before it, then each slider's key (`./draw_order.ts`).
|
|
999
|
+
const evalOrder = plant.drawOrder ?? drawOrderAt;
|
|
1000
|
+
let order = evalOrder(doc.slots.length, anim.timelines.drawOrder, t) ?? doc.slots.map((_s, k) => k);
|
|
1001
|
+
for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
1002
|
+
const drawOrder = order.map((k) => doc.slots[k].name);
|
|
1003
|
+
// The attachments and clips, in the draw order, with the deform and sequence timelines' state (`./deform.ts`).
|
|
1004
|
+
let attachments: CoreAttachmentRow[] | null = null;
|
|
1005
|
+
let clips: CoreClipRow[] | null = null;
|
|
1006
|
+
let clipped: CoreClippedRow[] | null = null;
|
|
1007
|
+
if (posed !== null) {
|
|
1008
|
+
const states = attachmentStates(doc, resolve, placeholders, { timelines: anim.timelines, t }, sliders, plant);
|
|
1009
|
+
for (const w of states.why) if (!attachmentWhy.includes(w)) attachmentWhy.push(w);
|
|
1010
|
+
const rank = new Map(order.map((k, r) => [doc.slots[k].name, r]));
|
|
1011
|
+
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
1012
|
+
const geometry = poseGeometry(shown, posed.world, sourceOfDoc(doc), round, { region: plant.region, vertices: plant.vertices }, drawWalkOf(doc, drawOrder, plant));
|
|
1013
|
+
if (geometry.attachmentsWhy !== null && !attachmentWhy.includes(geometry.attachmentsWhy)) attachmentWhy.push(geometry.attachmentsWhy);
|
|
1014
|
+
attachments = geometry.attachments;
|
|
1015
|
+
clips = geometry.clips;
|
|
1016
|
+
clipped = geometry.clipped;
|
|
1017
|
+
if (geometry.clippedWhy !== null && geometry.attachmentsWhy === null && !clippedWhy.includes(geometry.clippedWhy)) clippedWhy.push(geometry.clippedWhy);
|
|
1018
|
+
}
|
|
1019
|
+
const events = (plant.events ?? eventsFired)(anim.timelines.events, last, t, round);
|
|
1020
|
+
last = t;
|
|
1021
|
+
samples.push({ t: round(t), events, bones: posed === null ? null : posed.rows, slots: slots.rows, drawOrder, attachments, clips, clipped });
|
|
1022
|
+
}
|
|
1023
|
+
return { name: anim.name, duration: round(d), samples };
|
|
1024
|
+
});
|
|
1025
|
+
if (bonesReason === null && dt === undefined) bonesReason = previousPassWhy(doc, animations, plant);
|
|
1026
|
+
if (bonesReason !== null) for (const a of animations) for (const s of a.samples) s.bones = null;
|
|
1027
|
+
const slotsReason = (bonesReason === null ? null : slidersWhy(doc)) ?? (slotConflicts.length === 0
|
|
1028
|
+
? null
|
|
1029
|
+
: `${slotConflicts.slice(0, 5).join('; ')}${slotConflicts.length > 5 ? `; and ${slotConflicts.length - 5} more` : ''} — under --skin all the LAST of them in the Spine file's skin order wins, and that order is the emitter's, not the model's`);
|
|
1030
|
+
if (slotsReason !== null) for (const a of animations) for (const s of a.samples) s.slots = null;
|
|
1031
|
+
const orderReason = bonesReason === null ? null : slidersKeyingWhy(doc, 'drawOrder');
|
|
1032
|
+
if (orderReason !== null) for (const a of animations) for (const s of a.samples) s.drawOrder = null;
|
|
1033
|
+
// Every vertex goes through a bone's world matrix and follows what the slot shows, so the attachments and clips need both blocks.
|
|
1034
|
+
const upstream = [bonesReason === null ? null : `animations.bones is absent (${bonesReason}), and every vertex goes through a bone's world matrix`, slotsReason === null ? null : 'animations.slots is absent, so what a slot shows is not posed'].filter((x): x is string => x !== null);
|
|
1035
|
+
const clipsReason = upstream.length > 0 ? upstream.join('; ') : attachmentWhy.some((w) => !w.includes('(atlas: null)')) ? attachmentWhy.filter((w) => !w.includes('(atlas: null)')).join('; ') : null;
|
|
1036
|
+
const attachmentsReason = clipsReason ?? (attachmentWhy.length === 0 ? null : attachmentWhy.join('; '));
|
|
1037
|
+
if (attachmentsReason !== null) for (const a of animations) for (const s of a.samples) s.attachments = null;
|
|
1038
|
+
if (clipsReason !== null) for (const a of animations) for (const s of a.samples) s.clips = null;
|
|
1039
|
+
// The clipped triangles follow the attachments and the clips, and are left out where a clip starts over a polygon the core does not clip against (`./clipping.ts`).
|
|
1040
|
+
const clippedReason = attachmentsReason ?? orderReason ?? (clippedWhy.length === 0 ? null : clippedWhy.join('; '));
|
|
1041
|
+
if (clippedReason !== null) for (const a of animations) for (const s of a.samples) s.clipped = null;
|
|
1042
|
+
const absent: Array<[string, string]> = [];
|
|
1043
|
+
if (bonesReason !== null) absent.push(['animations.bones', bonesReason]);
|
|
1044
|
+
if (slotsReason !== null) absent.push(['animations.slots', slotsReason]);
|
|
1045
|
+
if (orderReason !== null) absent.push(['animations.drawOrder', orderReason]);
|
|
1046
|
+
if (attachmentsReason !== null) absent.push(['animations.attachments', attachmentsReason]);
|
|
1047
|
+
if (clipsReason !== null) absent.push(['animations.clips', clipsReason]);
|
|
1048
|
+
if (clippedReason !== null) absent.push(['animations.clipped', clippedReason]);
|
|
1049
|
+
return { animations, absent };
|
|
1050
|
+
}
|