rig-c 0.0.0-stage → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1191 -0
- package/src/meshquality.ts +2051 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1444 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct 5 of the core, second cut (issue #938, step 2e-ii of issue #380):
|
|
3
|
+
* the path constraint, at the setup pose and at a sample time, with its
|
|
4
|
+
* timelines. `./constraints.ts` runs it in the update order the first cut
|
|
5
|
+
* measured; this module reads its record, walks its curve and moves its
|
|
6
|
+
* bones.
|
|
7
|
+
*
|
|
8
|
+
* Every rule below was measured by posing hand-written skeletons through
|
|
9
|
+
* `tools/pose_oracle.ts dump` (spine-core 4.3.13, `--skin all`,
|
|
10
|
+
* `--physics none`) and comparing the core's rows at tolerance 0, most with
|
|
11
|
+
* amplifier bones ten thousand units out along each axis of a constrained
|
|
12
|
+
* bone, so that 1e-10 in its matrix reads as a grid step. Nothing here was
|
|
13
|
+
* written from the runtime's source. Where a rule is not the obvious
|
|
14
|
+
* reading, the reading that missed is stated beside it with its count. The
|
|
15
|
+
* populations it names: the **grid** — one bone on two and three curves,
|
|
16
|
+
* open and closed, at constant speed and not, percent and fixed, 61
|
|
17
|
+
* positions each (976 probes); the **population** — 2,000 random probes of
|
|
18
|
+
* every mode, setting, mix and curve (the core suite's `CP03` draws 600
|
|
19
|
+
* like them); the **stated population** — 1,000 probes off stated lengths
|
|
20
|
+
* spelled with non-float32 decimals. The core suite's `CP` controls hold the
|
|
21
|
+
* probes; `tools/core_gate.ts` holds the corpus.
|
|
22
|
+
*
|
|
23
|
+
* ## The curve
|
|
24
|
+
*
|
|
25
|
+
* The constraint walks what its slot shows (`shownAttachment` at the setup
|
|
26
|
+
* pose; the slot's attachment and deform timelines are not admitted, and an
|
|
27
|
+
* animation keying either on a walked path leaves the samples' bones out by
|
|
28
|
+
* name). A slot showing no path makes the constraint do nothing. The path's
|
|
29
|
+
* world points are `./vertices.ts`'s `worldVertices`: every vertex number
|
|
30
|
+
* through `Math.fround`, an unweighted path through its slot bone, a weighted
|
|
31
|
+
* one through its bound bones — the doubles missed 1,946 of the population's
|
|
32
|
+
* 2,000. Its `lengths` are read as the doubles the text spells: through
|
|
33
|
+
* `Math.fround` they missed 697 of the stated population's 1,000. The points
|
|
34
|
+
* form the chain `src/compile.ts`'s `pathChain` states — an open path drops
|
|
35
|
+
* its first and last point (the end knots' outer handles), a closed one is
|
|
36
|
+
* turned by one and repeats its first knot and handle — knot, handle,
|
|
37
|
+
* handle, knot, …
|
|
38
|
+
*
|
|
39
|
+
* ## The walk: where a length along the curve is
|
|
40
|
+
*
|
|
41
|
+
* Every bone is placed at a length along the path: the `position`, then
|
|
42
|
+
* each space after it (below). **At constant speed** (the default) the
|
|
43
|
+
* runtime measures the curve itself, every pose:
|
|
44
|
+
*
|
|
45
|
+
* 1. **A table of the curves' lengths**, each curve four forward-difference
|
|
46
|
+
* steps (`0.1875`, `0.09375`, `0.75` and `0.16666667`, the running total
|
|
47
|
+
* carried across curves) — `curveLengthTable`, which `src/compile.ts`'s
|
|
48
|
+
* `pathCurveLengths` also calls (issue #1015). A ten-step table missed
|
|
49
|
+
* 460 of the grid's 976. The
|
|
50
|
+
* last entry is the path's length; `percent` multiplies the position by
|
|
51
|
+
* it. The stated `lengths` are not read: the stated total in their place
|
|
52
|
+
* missed 680 of 1,000.
|
|
53
|
+
* 2. **The curve** holding the length is searched for in the table from the
|
|
54
|
+
* curve the previous space ended on, forward — on an open path it is not
|
|
55
|
+
* restarted, so a space that walks backwards across a curve (a negative
|
|
56
|
+
* spacing) stays on the later curve with a negative fraction; restarting
|
|
57
|
+
* from the first curve missed 36 of the population's 2,000. A closed
|
|
58
|
+
* path takes the length modulo its total (plus the total below 0) and
|
|
59
|
+
* searches from the first curve every time. The fraction of the curve is
|
|
60
|
+
* `(p − previous) / (entry − previous)`.
|
|
61
|
+
* 3. **A table of ten segments** of that curve, ten forward-difference steps
|
|
62
|
+
* of 1/10 (`0.3`, `0.03`, `0.006`, `0.16666667`), built when the curve
|
|
63
|
+
* changes. The fraction times the ten-step total is searched for from the
|
|
64
|
+
* segment the previous space ended on while the curve is the same, and
|
|
65
|
+
* the curve's parameter is `(segment + fraction of the segment) / 10` —
|
|
66
|
+
* so the parameter is linear in length within a segment, not along the
|
|
67
|
+
* whole curve: the fraction of the curve used as the parameter missed 380
|
|
68
|
+
* of the grid, the parameter at the exact arc length (a 2,000-piece
|
|
69
|
+
* polyline) 380, and the point on the ten-segment polyline instead of the
|
|
70
|
+
* curve 392.
|
|
71
|
+
* 4. **The point is the cubic at that parameter**, evaluated for any
|
|
72
|
+
* parameter: a negative one extrapolates the curve backwards. Only a
|
|
73
|
+
* parameter that is not a number (a curve of length 0) takes the curve's
|
|
74
|
+
* first knot and the direction of its first handle. Clamping every
|
|
75
|
+
* parameter below 0.00001 to the start missed 11 of the population.
|
|
76
|
+
*
|
|
77
|
+
* **Off constant speed** (`constantSpeed: false`) the stated `lengths` are
|
|
78
|
+
* the table: its entry for the last curve is the total (`vertexCount / 3`
|
|
79
|
+
* less 2 on an open path, less 1 on a closed one), the curve is found as in
|
|
80
|
+
* 2, and the fraction of the curve IS the cubic's parameter — the stated
|
|
81
|
+
* numbers decide where bones stand even where they disagree with the
|
|
82
|
+
* geometry (issue #804): measured off the geometry instead, 711 of the
|
|
83
|
+
* stated population's 1,000 missed.
|
|
84
|
+
*
|
|
85
|
+
* **Past the ends** of an open path a bone stands on the straight line
|
|
86
|
+
* through the first knot and its handle (before the start) or through the
|
|
87
|
+
* last handle and the last knot (past the end), the overshoot along it.
|
|
88
|
+
*
|
|
89
|
+
* ## Spaces: `spacingMode`
|
|
90
|
+
*
|
|
91
|
+
* A `tangent` constraint places one point per bone; `chain` and
|
|
92
|
+
* `chainScale` one more, the last bone's tip. The first space is 0. Each
|
|
93
|
+
* next space, for the bone before it, with `L` its `length` and `W` the
|
|
94
|
+
* length of `L` times its world x column:
|
|
95
|
+
*
|
|
96
|
+
* - `length`: `max(0, L + spacing) · W / L` — the clamp at 0 is measured:
|
|
97
|
+
* without it 16 of the population missed;
|
|
98
|
+
* - `fixed`: `spacing · W / L`;
|
|
99
|
+
* - a bone whose `L` is below 0.00001, in either: `spacing` as stated;
|
|
100
|
+
* - `percent`: `spacing`, and every space is multiplied by the path's
|
|
101
|
+
* total;
|
|
102
|
+
* - `proportional`: `W` (or `spacing` for a bone below 0.00001), all of them
|
|
103
|
+
* then scaled by `count · spacing / their sum` when the sum is above 0,
|
|
104
|
+
* and multiplied by the total over the count of spaces — the whole total
|
|
105
|
+
* missed 260 of the population.
|
|
106
|
+
*
|
|
107
|
+
* ## Moving the bones: `rotateMode`, `offsetRotation`, the mixes
|
|
108
|
+
*
|
|
109
|
+
* Nothing moves when all three mixes are 0. Each bone in turn: its world
|
|
110
|
+
* origin moves toward its point by `mixX` and `mixY` (the two axes on their
|
|
111
|
+
* own); with `chainScale`, a bone longer than 0.00001 has its world x column
|
|
112
|
+
* scaled by `(distance to the next point / W − 1) · mixRotate + 1`. Then,
|
|
113
|
+
* when `mixRotate` is above 0 (a negative one turns nothing: reading it
|
|
114
|
+
* as "not 0" missed 58 of the population), the bone turns by
|
|
115
|
+
* `mixRotate` times the change from its x axis's angle to:
|
|
116
|
+
*
|
|
117
|
+
* - `tangent`: the curve's direction at its point — the derivative there,
|
|
118
|
+
* or below a parameter of 0.001 (negative ones included; 7 of 2,000
|
|
119
|
+
* tangent probes missed with 0.001 as a floor above 0) its first handle's
|
|
120
|
+
* direction;
|
|
121
|
+
* - `chain`, `chainScale`: the direction to the next point — ⚠️ but when the
|
|
122
|
+
* next space is exactly 0, the next point's tangent. That is exact
|
|
123
|
+
* equality: a space of ±1e-7 turns to the (rounding-level) direction,
|
|
124
|
+
* and an ε test missed 157 of the population.
|
|
125
|
+
* - `chain` with no offset: the next bone starts at this bone's tip, the
|
|
126
|
+
* bone's `length` along its turned axis (the runtime's "tip"); not
|
|
127
|
+
* carrying the tip missed 191 of the population.
|
|
128
|
+
* - Otherwise `offsetRotation` degrees (the model's `rotation`) are added,
|
|
129
|
+
* in the runtime's π, NEGATED when the slot bone's world matrix does not
|
|
130
|
+
* have a positive determinant — never flipping missed 241 of the
|
|
131
|
+
* population. Which slot bone's world that is, is the next section.
|
|
132
|
+
*
|
|
133
|
+
* The change is brought once into [−π, π] with the runtime's π
|
|
134
|
+
* (3.1415927), which `Math.PI` there missed 881 of 2,000 by. The bones are
|
|
135
|
+
* then moved in world space and read back into local values at once, as the
|
|
136
|
+
* first cut's world-space transform is (`./constraints.ts`, `localFromWorld`).
|
|
137
|
+
*
|
|
138
|
+
* ## Which slot bone the offset reads
|
|
139
|
+
*
|
|
140
|
+
* The runtime brings bones up to date in an order it builds before it
|
|
141
|
+
* poses: each constraint in turn brings up to date the bones it reads and
|
|
142
|
+
* the bones it moves (a bone after its parents, and once only until a
|
|
143
|
+
* constraint moves a bone above it), and every bone left over follows at
|
|
144
|
+
* the end. A path constraint reads its slot bone only for the offset's
|
|
145
|
+
* sign, and it asks the order for its slot bone only when the slot holds an
|
|
146
|
+
* UNWEIGHTED path in some skin — a weighted one asks for its bound bones
|
|
147
|
+
* instead. So a weighted path's slot bone can be read before the pose has
|
|
148
|
+
* set it (`slotBonePlan` emulates the order):
|
|
149
|
+
*
|
|
150
|
+
* - set by an earlier constraint's ordering — its world as it was brought
|
|
151
|
+
* up to date then, before that constraint ran; or as that constraint left
|
|
152
|
+
* it when it moved the bone itself;
|
|
153
|
+
* - set by nothing yet — the previous pose's world: on a new skeleton all
|
|
154
|
+
* zeros, which is not positive, so the oracle's setup pose turns the
|
|
155
|
+
* offset the other way; at a sample, the sample before's.
|
|
156
|
+
*
|
|
157
|
+
* Measured on eight hand-written cases, each exact at the setup pose and at
|
|
158
|
+
* nine samples, of which reading the slot bone fresh missed three, and on
|
|
159
|
+
* random update orders of path, ik and transform constraints, 783 of 783
|
|
160
|
+
* exact, fresh 774 (`CP06`, `CP07`).
|
|
161
|
+
*
|
|
162
|
+
* A physics constraint orders its one bone as a one-bone constraint does
|
|
163
|
+
* (its parents first, what hangs below it reset, the bone done), though
|
|
164
|
+
* under `Physics.none` it moves nothing. A slider sets no bone in the order —
|
|
165
|
+
* neither its dial nor the bones its animation keys — and resets each bone
|
|
166
|
+
* its animation keys and what hangs below it, so a later constraint that
|
|
167
|
+
* orders one of them sets it again. Measured on eighteen hand-written orders
|
|
168
|
+
* around a weighted path with an offset (`CP13`), each exact at the setup
|
|
169
|
+
* pose and three samples, where the readings rejected each miss one or two:
|
|
170
|
+
* physics ordering nothing, a slider ordering its dial, a slider ordering
|
|
171
|
+
* the bones it keys, a slider resetting nothing, a slider resetting only
|
|
172
|
+
* below the bones it keys; and on 400 random orders of one to five physics,
|
|
173
|
+
* slider, ik and transform constraints around such a path, 400 exact
|
|
174
|
+
* (physics ordering nothing 388, a slider ordering its dial and keyed bones
|
|
175
|
+
* 379). The previous sample is the oracle's
|
|
176
|
+
* order of animations, which the model does not hold: the core poses a
|
|
177
|
+
* sample with the setup pose's reading, which is the runtime's exactly
|
|
178
|
+
* when the slot bone's reflection is the setup's at every sample, and
|
|
179
|
+
* leaves the samples' bones out by name when it is not.
|
|
180
|
+
*
|
|
181
|
+
* ## Which constraints run
|
|
182
|
+
*
|
|
183
|
+
* One with `skin: true` runs exactly when an applied skin's `path` list
|
|
184
|
+
* names it (`./constraints.ts`'s header, issue #932); one whose slot bone is
|
|
185
|
+
* inactive does not. One whose constrained bone is inactive still runs
|
|
186
|
+
* (measured: its other bones move; the inactive one's matrix is collapsed
|
|
187
|
+
* and the oracle's ill-conditioned rule excludes it). A weight bound to an
|
|
188
|
+
* inactive bone reads its world of zeros.
|
|
189
|
+
*
|
|
190
|
+
* ## The record and its timelines
|
|
191
|
+
*
|
|
192
|
+
* The dump's `paths` block is the runtime's reading of each field, and the
|
|
193
|
+
* core writes the same block (`pathRows`): `positionMode` absent reads
|
|
194
|
+
* `percent`, `spacingMode` `length`, `rotateMode` `tangent`, `rotation`,
|
|
195
|
+
* `position` and `spacing` 0, `mixRotate` and `mixX` 1 and `mixY` the
|
|
196
|
+
* resolved `mixX`; a mode's first letter is read in either case. A path
|
|
197
|
+
* attachment's `closed` absent reads false and `constantSpeed` true (the
|
|
198
|
+
* `pathAttachments` block, `pathAttachmentRows`). The three timelines —
|
|
199
|
+
* `position` and `spacing` one channel each, `mix` three — are evaluated by
|
|
200
|
+
* construct 4's `keyIndexAt` and `channelAt`, float32 key times and values,
|
|
201
|
+
* before the first key the constraint's own; a key stating no value reads
|
|
202
|
+
* 0, one stating no mix 1 and no `mixY` its own `mixX` (`CP08`, 24 keyed
|
|
203
|
+
* skeletons at 200 dense samples). The value a key gives reaches the pose
|
|
204
|
+
* through the setup blend at alpha 1, `setup + (value − setup)·1`, as the ik
|
|
205
|
+
* and transform timelines' do (`./constraints.ts`, *The timelines*; issue
|
|
206
|
+
* #984, `CY01`: the value as keyed read 0 of 60 probes keying all three
|
|
207
|
+
* timelines bit-exact under `--raw`, the blend 60).
|
|
208
|
+
*
|
|
209
|
+
* ## Purity
|
|
210
|
+
*
|
|
211
|
+
* As the rest of the core: nothing from the Spine runtime package, nothing
|
|
212
|
+
* from `src/transform.ts`, no clock, no randomness, no I/O.
|
|
213
|
+
*/
|
|
214
|
+
import type { ModelBone, ModelVertices } from '../model.ts';
|
|
215
|
+
import { RUNTIME_PI, type CoreWorld } from './world.ts';
|
|
216
|
+
import { EXACT_COORDS, worldVertices } from './vertices.ts';
|
|
217
|
+
import type { CoreCurve, CoreKey } from './animation.ts';
|
|
218
|
+
import type { CoreConstraintRecord } from './constraints.ts';
|
|
219
|
+
|
|
220
|
+
const RAD = RUNTIME_PI / 180;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The constant-speed walk's table of the curves' lengths — the header's *The
|
|
224
|
+
* walk*, step 1: each curve four forward-difference steps (`0.1875`,
|
|
225
|
+
* `0.09375`, `0.75`, `0.16666667`), the running total carried across curves,
|
|
226
|
+
* one cumulative entry per curve. `chain` is flat, `x, y` per point, knot
|
|
227
|
+
* handle handle knot …; `curveCount` curves are read off it.
|
|
228
|
+
*
|
|
229
|
+
* Exported because it has a second caller: an omitted `lengths` on a path
|
|
230
|
+
* attachment is measured by `src/compile.ts` through this function (issue
|
|
231
|
+
* #1015), so the number rigc writes into a file and the number the core walks
|
|
232
|
+
* are one computation. Measured as the header states it on the grid (a
|
|
233
|
+
* ten-step table missed 460 of 976) and held there by the core suite's `CP`
|
|
234
|
+
* controls.
|
|
235
|
+
*/
|
|
236
|
+
export function curveLengthTable(chain: readonly number[], curveCount: number): number[] {
|
|
237
|
+
const curveEnds: number[] = [];
|
|
238
|
+
let total = 0;
|
|
239
|
+
let x1 = chain[0];
|
|
240
|
+
let y1 = chain[1];
|
|
241
|
+
for (let i = 0, w = 2; i < curveCount; i++, w += 6) {
|
|
242
|
+
const cx1 = chain[w];
|
|
243
|
+
const cy1 = chain[w + 1];
|
|
244
|
+
const cx2 = chain[w + 2];
|
|
245
|
+
const cy2 = chain[w + 3];
|
|
246
|
+
const x2 = chain[w + 4];
|
|
247
|
+
const y2 = chain[w + 5];
|
|
248
|
+
const h2x = (x1 - cx1 * 2 + cx2) * 0.1875;
|
|
249
|
+
const h2y = (y1 - cy1 * 2 + cy2) * 0.1875;
|
|
250
|
+
const d3x = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375;
|
|
251
|
+
const d3y = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375;
|
|
252
|
+
let d2x = h2x * 2 + d3x;
|
|
253
|
+
let d2y = h2y * 2 + d3y;
|
|
254
|
+
let d1x = (cx1 - x1) * 0.75 + h2x + d3x * 0.16666667;
|
|
255
|
+
let d1y = (cy1 - y1) * 0.75 + h2y + d3y * 0.16666667;
|
|
256
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
257
|
+
d1x += d2x;
|
|
258
|
+
d1y += d2y;
|
|
259
|
+
d2x += d3x;
|
|
260
|
+
d2y += d3y;
|
|
261
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
262
|
+
d1x += d2x;
|
|
263
|
+
d1y += d2y;
|
|
264
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
265
|
+
d1x += d2x + d3x;
|
|
266
|
+
d1y += d2y + d3y;
|
|
267
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
268
|
+
curveEnds.push(total);
|
|
269
|
+
x1 = x2;
|
|
270
|
+
y1 = y2;
|
|
271
|
+
}
|
|
272
|
+
return curveEnds;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const EPSILON = 0.00001;
|
|
276
|
+
|
|
277
|
+
/** The three modes a path constraint names, each as the model spells it once its first letter is lower-cased. */
|
|
278
|
+
export const PATH_POSITION_MODES = ['fixed', 'percent'] as const;
|
|
279
|
+
export const PATH_SPACING_MODES = ['length', 'fixed', 'percent', 'proportional'] as const;
|
|
280
|
+
export const PATH_ROTATE_MODES = ['tangent', 'chain', 'chainScale'] as const;
|
|
281
|
+
export type PathPositionMode = (typeof PATH_POSITION_MODES)[number];
|
|
282
|
+
export type PathSpacingMode = (typeof PATH_SPACING_MODES)[number];
|
|
283
|
+
export type PathRotateMode = (typeof PATH_ROTATE_MODES)[number];
|
|
284
|
+
|
|
285
|
+
/** The fields a path record may carry after `kind`, `name`, `declaredIn` (`buildRigConstraint` in `src/compile.ts`). */
|
|
286
|
+
export const PATH_FIELDS = ['bones', 'slot', 'positionMode', 'spacingMode', 'rotateMode', 'rotation', 'position', 'spacing', 'mixRotate', 'mixX', 'mixY', 'skin'] as const;
|
|
287
|
+
|
|
288
|
+
/** The curve a path constraint walks: the path attachment its slot shows, as the record states it. */
|
|
289
|
+
export interface CorePathGeometry {
|
|
290
|
+
vertices: ModelVertices;
|
|
291
|
+
closed: boolean;
|
|
292
|
+
constantSpeed: boolean;
|
|
293
|
+
lengths: number[];
|
|
294
|
+
/** Set when a deform timeline replaced the vertices at a sample (`./deform.ts`): their coordinates are read as they are, not through `Math.fround`. */
|
|
295
|
+
exact?: boolean;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The values a path constraint's timelines key. */
|
|
299
|
+
export interface PathPose {
|
|
300
|
+
position: number;
|
|
301
|
+
spacing: number;
|
|
302
|
+
mixRotate: number;
|
|
303
|
+
mixX: number;
|
|
304
|
+
mixY: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface CorePathRecord extends PathPose {
|
|
308
|
+
kind: 'path';
|
|
309
|
+
name: string;
|
|
310
|
+
bones: string[];
|
|
311
|
+
slot: string;
|
|
312
|
+
/** The slot's bone — the frame an unweighted path's vertices are in. */
|
|
313
|
+
slotBone: string;
|
|
314
|
+
positionMode: PathPositionMode;
|
|
315
|
+
spacingMode: PathSpacingMode;
|
|
316
|
+
rotateMode: PathRotateMode;
|
|
317
|
+
offsetRotation: number;
|
|
318
|
+
skin: boolean;
|
|
319
|
+
/** An applied skin's `path` list names it — what applies a skin-required one (`./constraints.ts`'s header); set per skin view by `underSkin` in `./index.ts`. */
|
|
320
|
+
listedBySkin: boolean;
|
|
321
|
+
/** What the slot shows at setup: its path, `null` when it shows no path (the constraint then does nothing). Set once the skins are read. */
|
|
322
|
+
path: CorePathGeometry | null;
|
|
323
|
+
/** Why what the slot shows cannot be told — skins that disagree over its placeholder — or null. */
|
|
324
|
+
unresolved: string | null;
|
|
325
|
+
/**
|
|
326
|
+
* The bones the runtime brings up to date for this constraint's slot before
|
|
327
|
+
* it runs, off every path attachment a skin the view can show files under
|
|
328
|
+
* the slot (every skin under `all`, the named and default skins under a
|
|
329
|
+
* named one, `lookupSkins` in `./skins.ts`): a
|
|
330
|
+
* weighted one's bound bones, an unweighted one's slot bone (the header's
|
|
331
|
+
* *Which slot bone*). Set once the skins are read.
|
|
332
|
+
*/
|
|
333
|
+
slotDeps: string[];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
337
|
+
|
|
338
|
+
function fold<T extends string>(raw: Record<string, unknown>, key: string, modes: readonly T[], dflt: T, where: string, problems: string[]): T {
|
|
339
|
+
const v = raw[key];
|
|
340
|
+
if (v === undefined) return dflt;
|
|
341
|
+
const folded = typeof v === 'string' && v.length > 0 ? v[0].toLowerCase() + v.slice(1) : '';
|
|
342
|
+
const mode = modes.find((m) => m === folded);
|
|
343
|
+
if (mode === undefined) {
|
|
344
|
+
problems.push(`${where}: ${key} is ${JSON.stringify(v)}, none of ${modes.join(', ')} (first letter in either case)`);
|
|
345
|
+
return dflt;
|
|
346
|
+
}
|
|
347
|
+
return mode;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function num(raw: Record<string, unknown>, key: string, dflt: number, where: string, problems: string[]): number {
|
|
351
|
+
const v = raw[key];
|
|
352
|
+
if (v === undefined) return dflt;
|
|
353
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
354
|
+
problems.push(`${where}: ${key} is ${JSON.stringify(v)}, not a finite number`);
|
|
355
|
+
return dflt;
|
|
356
|
+
}
|
|
357
|
+
return v;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* A path constraint's record, read field by field, or `undefined` with the
|
|
362
|
+
* problems named. `bones` and `slots` are the document's names and each
|
|
363
|
+
* slot's bone.
|
|
364
|
+
*/
|
|
365
|
+
export function readPathRecord(raw: Record<string, unknown>, name: string, where: string, bones: ReadonlySet<string>, slots: ReadonlyMap<string, string>, problems: string[]): CorePathRecord | undefined {
|
|
366
|
+
const before = problems.length;
|
|
367
|
+
for (const key of Object.keys(raw)) {
|
|
368
|
+
if (key === 'kind' || key === 'name' || key === 'declaredIn') continue;
|
|
369
|
+
if (!(PATH_FIELDS as readonly string[]).includes(key)) problems.push(`${where}: field "${key}" is not one this reader knows; it reads [${PATH_FIELDS.join(', ')}]`);
|
|
370
|
+
}
|
|
371
|
+
const list: string[] = [];
|
|
372
|
+
if (!Array.isArray(raw.bones) || raw.bones.length === 0) problems.push(`${where}: bones is ${JSON.stringify(raw.bones)}, not a non-empty list of bone names`);
|
|
373
|
+
else raw.bones.forEach((b, i) => (typeof b === 'string' && bones.has(b) ? list.push(b) : problems.push(`${where}: bones[${i}] ${JSON.stringify(b)} is not a bone of this document`)));
|
|
374
|
+
const slot = typeof raw.slot === 'string' ? raw.slot : '';
|
|
375
|
+
const slotBone = slots.get(slot);
|
|
376
|
+
if (slotBone === undefined) problems.push(`${where}: slot is ${JSON.stringify(raw.slot)}, not a slot of this document`);
|
|
377
|
+
if (raw.skin !== undefined && typeof raw.skin !== 'boolean') problems.push(`${where}: skin is ${JSON.stringify(raw.skin)}, not a boolean`);
|
|
378
|
+
const mixX = num(raw, 'mixX', 1, where, problems);
|
|
379
|
+
const record: CorePathRecord = {
|
|
380
|
+
kind: 'path', name, bones: list, slot, slotBone: slotBone ?? '',
|
|
381
|
+
positionMode: fold(raw, 'positionMode', PATH_POSITION_MODES, 'percent', where, problems),
|
|
382
|
+
spacingMode: fold(raw, 'spacingMode', PATH_SPACING_MODES, 'length', where, problems),
|
|
383
|
+
rotateMode: fold(raw, 'rotateMode', PATH_ROTATE_MODES, 'tangent', where, problems),
|
|
384
|
+
offsetRotation: num(raw, 'rotation', 0, where, problems),
|
|
385
|
+
position: num(raw, 'position', 0, where, problems),
|
|
386
|
+
spacing: num(raw, 'spacing', 0, where, problems),
|
|
387
|
+
mixRotate: num(raw, 'mixRotate', 1, where, problems),
|
|
388
|
+
mixX,
|
|
389
|
+
mixY: num(raw, 'mixY', mixX, where, problems),
|
|
390
|
+
skin: raw.skin === true,
|
|
391
|
+
listedBySkin: false,
|
|
392
|
+
path: null,
|
|
393
|
+
unresolved: null,
|
|
394
|
+
slotDeps: [],
|
|
395
|
+
};
|
|
396
|
+
return problems.length === before ? record : undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// timelines
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
/** A path constraint's three timelines, by the name the model gives them, and the channels each keys. */
|
|
404
|
+
export const PATH_TIMELINES = { position: ['value'], spacing: ['value'], mix: ['mixRotate', 'mixX', 'mixY'] } as const;
|
|
405
|
+
export type PathTimelineName = keyof typeof PATH_TIMELINES;
|
|
406
|
+
|
|
407
|
+
/** One animation's timelines of one path constraint. */
|
|
408
|
+
export interface CorePathTimelines {
|
|
409
|
+
name: string;
|
|
410
|
+
position?: CoreKey[];
|
|
411
|
+
spacing?: CoreKey[];
|
|
412
|
+
mix?: CoreKey[];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function keyCurve(raw: Record<string, unknown>, channels: number, last: boolean, at: string, problems: string[]): CoreCurve {
|
|
416
|
+
const curve = raw.curve;
|
|
417
|
+
if (curve === undefined) return 'linear';
|
|
418
|
+
if (last) {
|
|
419
|
+
problems.push(`${at}: the last key carries a curve, which eases to no key — the writer refuses it`);
|
|
420
|
+
return 'linear';
|
|
421
|
+
}
|
|
422
|
+
if (curve === 'stepped') return 'stepped';
|
|
423
|
+
if (!Array.isArray(curve) || curve.length !== channels * 4 || !curve.every((n) => typeof n === 'number' && Number.isFinite(n))) {
|
|
424
|
+
problems.push(`${at}: curve is ${JSON.stringify(curve)}, not "stepped" nor ${channels * 4} finite numbers (four per channel)`);
|
|
425
|
+
return 'linear';
|
|
426
|
+
}
|
|
427
|
+
return curve as number[];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The path timelines of one animation record's `constraints.path`, read: each
|
|
432
|
+
* names a declared path constraint, each timeline is `position`, `spacing` or
|
|
433
|
+
* `mix`, keys strictly increase in time, each key carries only its channels,
|
|
434
|
+
* `time` and `curve`. An absent channel reads the parser's value, measured:
|
|
435
|
+
* a position or spacing 0, a mix 1, a `mixY` the key's own `mixX` (the header).
|
|
436
|
+
*/
|
|
437
|
+
export function readPathTimelines(value: unknown, label: string, declared: ReadonlySet<string>, problems: string[]): CorePathTimelines[] {
|
|
438
|
+
const out: CorePathTimelines[] = [];
|
|
439
|
+
if (!Array.isArray(value)) return out;
|
|
440
|
+
value.forEach((entry, i) => {
|
|
441
|
+
const at = `${label}.constraints.path[${i}]`;
|
|
442
|
+
if (!isRecord(entry) || typeof entry.name !== 'string') {
|
|
443
|
+
problems.push(`${at} names no constraint`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (!declared.has(entry.name)) problems.push(`${at}: "${entry.name}" is not a path constraint of this document`);
|
|
447
|
+
const tls: CorePathTimelines = { name: entry.name };
|
|
448
|
+
if (!Array.isArray(entry.timelines)) {
|
|
449
|
+
problems.push(`${at}: timelines is not a list`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
entry.timelines.forEach((tl: unknown, j: number) => {
|
|
453
|
+
const tat = `${at}.timelines[${j}]`;
|
|
454
|
+
if (!isRecord(tl) || typeof tl.name !== 'string' || !(tl.name in PATH_TIMELINES)) {
|
|
455
|
+
problems.push(`${tat}: ${isRecord(tl) ? JSON.stringify(tl.name) : 'it'} is not a path timeline; they are ${Object.keys(PATH_TIMELINES).join(', ')}`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const kind = tl.name as PathTimelineName;
|
|
459
|
+
if (tls[kind] !== undefined) problems.push(`${tat}: "${kind}" is keyed twice on path constraint "${entry.name}"`);
|
|
460
|
+
const channels: readonly string[] = PATH_TIMELINES[kind];
|
|
461
|
+
const rawKeys = tl.keys;
|
|
462
|
+
if (!Array.isArray(rawKeys) || rawKeys.length === 0) {
|
|
463
|
+
problems.push(`${tat}: keys is not a non-empty list`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let last = -Infinity;
|
|
467
|
+
const keys: CoreKey[] = [];
|
|
468
|
+
rawKeys.forEach((k: unknown, n: number) => {
|
|
469
|
+
const kat = `${tat}.keys[${n}]`;
|
|
470
|
+
if (!isRecord(k)) {
|
|
471
|
+
problems.push(`${kat} is not an object`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
for (const f of Object.keys(k)) if (f !== 'time' && f !== 'curve' && !channels.includes(f)) problems.push(`${kat}: field "${f}" is not one this reader knows; it reads [time, ${channels.join(', ')}, curve]`);
|
|
475
|
+
const time = k.time;
|
|
476
|
+
if (typeof time !== 'number' || !Number.isFinite(time) || time < 0) {
|
|
477
|
+
problems.push(`${kat}: time is ${JSON.stringify(time)}, not a finite time at or after 0`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
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`);
|
|
481
|
+
last = Math.max(last, time);
|
|
482
|
+
const stated = channels.map((c) => {
|
|
483
|
+
if (c === 'mixY' && k.mixY === undefined) return num(k, 'mixX', 1, kat, problems);
|
|
484
|
+
return num(k, c, kind === 'mix' ? 1 : 0, kat, problems);
|
|
485
|
+
});
|
|
486
|
+
keys.push({ time: Math.fround(time), values: stated.map(Math.fround), stated: { time, values: stated }, curve: keyCurve(k, channels.length, n === rawKeys.length - 1, kat, problems) });
|
|
487
|
+
});
|
|
488
|
+
tls[kind] = keys;
|
|
489
|
+
});
|
|
490
|
+
out.push(tls);
|
|
491
|
+
});
|
|
492
|
+
return out;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// solving
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
/** What the path solver reads and writes: the bones' local values, their world transforms, which are active. */
|
|
500
|
+
export interface PathSolverState {
|
|
501
|
+
bones: ModelBone[];
|
|
502
|
+
index: Map<string, number>;
|
|
503
|
+
world: Map<string, CoreWorld>;
|
|
504
|
+
active: ReadonlySet<string>;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** The slot's path vertices in world units, as the constraint reads them at the moment it runs. */
|
|
508
|
+
function pathWorld(state: PathSolverState, c: CorePathRecord, g: CorePathGeometry): number[] {
|
|
509
|
+
return worldVertices(g.vertices, state.world.get(c.slotBone) as CoreWorld, state.world, g.exact === true ? EXACT_COORDS : Math.fround);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function onLineBefore(p: number, x1: number, y1: number, x2: number, y2: number, out: number[], o: number): void {
|
|
513
|
+
const r = Math.atan2(y2 - y1, x2 - x1);
|
|
514
|
+
out[o] = x1 + p * Math.cos(r);
|
|
515
|
+
out[o + 1] = y1 + p * Math.sin(r);
|
|
516
|
+
out[o + 2] = r;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function onLinePast(p: number, x0: number, y0: number, x1: number, y1: number, out: number[], o: number): void {
|
|
520
|
+
const r = Math.atan2(y1 - y0, x1 - x0);
|
|
521
|
+
out[o] = x1 + p * Math.cos(r);
|
|
522
|
+
out[o + 1] = y1 + p * Math.sin(r);
|
|
523
|
+
out[o + 2] = r;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function onCurve(p: number, x1: number, y1: number, cx1: number, cy1: number, cx2: number, cy2: number, x2: number, y2: number, out: number[], o: number, wantsTangent: boolean): void {
|
|
527
|
+
if (Number.isNaN(p)) {
|
|
528
|
+
out[o] = x1;
|
|
529
|
+
out[o + 1] = y1;
|
|
530
|
+
out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const t2 = p * p;
|
|
534
|
+
const t3 = t2 * p;
|
|
535
|
+
const u = 1 - p;
|
|
536
|
+
const u2 = u * u;
|
|
537
|
+
const u3 = u2 * u;
|
|
538
|
+
const ut = u * p;
|
|
539
|
+
const ut3x = ut * 3;
|
|
540
|
+
const w1 = u * ut3x;
|
|
541
|
+
const w2 = ut3x * p;
|
|
542
|
+
const x = x1 * u3 + cx1 * w1 + cx2 * w2 + x2 * t3;
|
|
543
|
+
const y = y1 * u3 + cy1 * w1 + cy2 * w2 + y2 * t3;
|
|
544
|
+
out[o] = x;
|
|
545
|
+
out[o + 1] = y;
|
|
546
|
+
if (wantsTangent) {
|
|
547
|
+
if (p < 0.001) out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1);
|
|
548
|
+
else out[o + 2] = Math.atan2(y - (y1 * u2 + cy1 * ut * 2 + cy2 * t2), x - (x1 * u2 + cx1 * ut * 2 + cx2 * t2));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** The world position and angle of each space along the path. */
|
|
553
|
+
function positionsOf(state: PathSolverState, c: CorePathRecord, g: CorePathGeometry, spaces: number[], wantsTangent: boolean): number[] {
|
|
554
|
+
const spaceCount = spaces.length;
|
|
555
|
+
const pts = pathWorld(state, c, g);
|
|
556
|
+
const n = pts.length / 2;
|
|
557
|
+
const P = (k: number): [number, number] => [pts[2 * k], pts[2 * k + 1]];
|
|
558
|
+
const out: number[] = new Array(spaceCount * 3 + 2).fill(0);
|
|
559
|
+
const closed = g.closed;
|
|
560
|
+
let position = c.position;
|
|
561
|
+
if (!g.constantSpeed) {
|
|
562
|
+
const lengths = g.lengths;
|
|
563
|
+
const curveCount = n / 3 - (closed ? 1 : 2);
|
|
564
|
+
const total = lengths[curveCount];
|
|
565
|
+
if (c.positionMode === 'percent') position *= total;
|
|
566
|
+
const spaceScale = c.spacingMode === 'percent' ? total : c.spacingMode === 'proportional' ? total / spaceCount : 1;
|
|
567
|
+
let curve = 0;
|
|
568
|
+
for (let i = 0, o = 0; i < spaceCount; i++, o += 3) {
|
|
569
|
+
const space = spaces[i] * spaceScale;
|
|
570
|
+
position += space;
|
|
571
|
+
let p = position;
|
|
572
|
+
if (closed) {
|
|
573
|
+
p %= total;
|
|
574
|
+
if (p < 0) p += total;
|
|
575
|
+
curve = 0;
|
|
576
|
+
} else if (p < 0) {
|
|
577
|
+
const [x1, y1] = P(1);
|
|
578
|
+
const [x2, y2] = P(2);
|
|
579
|
+
onLineBefore(p, x1, y1, x2, y2, out, o);
|
|
580
|
+
continue;
|
|
581
|
+
} else if (p > total) {
|
|
582
|
+
const [x0, y0] = P(n - 3);
|
|
583
|
+
const [x1, y1] = P(n - 2);
|
|
584
|
+
onLinePast(p - total, x0, y0, x1, y1, out, o);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
for (;; curve++) {
|
|
588
|
+
const length = lengths[curve];
|
|
589
|
+
if (p > length) continue;
|
|
590
|
+
if (curve === 0) p /= length;
|
|
591
|
+
else {
|
|
592
|
+
const prev = lengths[curve - 1];
|
|
593
|
+
p = (p - prev) / (length - prev);
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
const k = 3 * curve + 1;
|
|
598
|
+
const q = (m: number): [number, number] => P((k + m) % n);
|
|
599
|
+
const [x1, y1] = q(0);
|
|
600
|
+
const [cx1, cy1] = q(1);
|
|
601
|
+
const [cx2, cy2] = q(2);
|
|
602
|
+
const [x2, y2] = q(3);
|
|
603
|
+
onCurve(p, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, wantsTangent || (i > 0 && space === 0));
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
// constant speed: the chain, knot handle handle knot …
|
|
608
|
+
const chain: number[] = [];
|
|
609
|
+
if (closed) {
|
|
610
|
+
for (let k = 1; k < n; k++) chain.push(...P(k));
|
|
611
|
+
chain.push(...P(0), ...P(1));
|
|
612
|
+
} else for (let k = 1; k < n - 1; k++) chain.push(...P(k));
|
|
613
|
+
const curveCount = closed ? n / 3 : n / 3 - 1;
|
|
614
|
+
const curveEnds = curveLengthTable(chain, curveCount);
|
|
615
|
+
const total = curveEnds.length > 0 ? curveEnds[curveEnds.length - 1] : 0;
|
|
616
|
+
if (c.positionMode === 'percent') position *= total;
|
|
617
|
+
const spaceScale = c.spacingMode === 'percent' ? total : c.spacingMode === 'proportional' ? total / spaceCount : 1;
|
|
618
|
+
const segmentEnds: number[] = new Array(10).fill(0);
|
|
619
|
+
let curveTotal = 0;
|
|
620
|
+
let lastCurve = -1;
|
|
621
|
+
let x1 = 0, y1 = 0, cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0;
|
|
622
|
+
for (let i = 0, o = 0, curve = 0, segment = 0; i < spaceCount; i++, o += 3) {
|
|
623
|
+
const space = spaces[i] * spaceScale;
|
|
624
|
+
position += space;
|
|
625
|
+
let p = position;
|
|
626
|
+
if (closed) {
|
|
627
|
+
p %= total;
|
|
628
|
+
if (p < 0) p += total;
|
|
629
|
+
curve = 0;
|
|
630
|
+
segment = 0;
|
|
631
|
+
} else if (p < 0) {
|
|
632
|
+
onLineBefore(p, chain[0], chain[1], chain[2], chain[3], out, o);
|
|
633
|
+
continue;
|
|
634
|
+
} else if (p > total) {
|
|
635
|
+
const e = chain.length;
|
|
636
|
+
onLinePast(p - total, chain[e - 4], chain[e - 3], chain[e - 2], chain[e - 1], out, o);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
for (;; curve++) {
|
|
640
|
+
const length = curveEnds[curve];
|
|
641
|
+
if (p > length) continue;
|
|
642
|
+
if (curve === 0) p /= length;
|
|
643
|
+
else {
|
|
644
|
+
const prev = curveEnds[curve - 1];
|
|
645
|
+
p = (p - prev) / (length - prev);
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
if (curve !== lastCurve) {
|
|
650
|
+
lastCurve = curve;
|
|
651
|
+
const ii = curve * 6;
|
|
652
|
+
x1 = chain[ii];
|
|
653
|
+
y1 = chain[ii + 1];
|
|
654
|
+
cx1 = chain[ii + 2];
|
|
655
|
+
cy1 = chain[ii + 3];
|
|
656
|
+
cx2 = chain[ii + 4];
|
|
657
|
+
cy2 = chain[ii + 5];
|
|
658
|
+
x2 = chain[ii + 6];
|
|
659
|
+
y2 = chain[ii + 7];
|
|
660
|
+
const h2x = (x1 - cx1 * 2 + cx2) * 0.03;
|
|
661
|
+
const h2y = (y1 - cy1 * 2 + cy2) * 0.03;
|
|
662
|
+
const d3x = ((cx1 - cx2) * 3 - x1 + x2) * 0.006;
|
|
663
|
+
const d3y = ((cy1 - cy2) * 3 - y1 + y2) * 0.006;
|
|
664
|
+
let d2x = h2x * 2 + d3x;
|
|
665
|
+
let d2y = h2y * 2 + d3y;
|
|
666
|
+
let d1x = (cx1 - x1) * 0.3 + h2x + d3x * 0.16666667;
|
|
667
|
+
let d1y = (cy1 - y1) * 0.3 + h2y + d3y * 0.16666667;
|
|
668
|
+
curveTotal = Math.sqrt(d1x * d1x + d1y * d1y);
|
|
669
|
+
segmentEnds[0] = curveTotal;
|
|
670
|
+
for (let s = 1; s < 8; s++) {
|
|
671
|
+
d1x += d2x;
|
|
672
|
+
d1y += d2y;
|
|
673
|
+
d2x += d3x;
|
|
674
|
+
d2y += d3y;
|
|
675
|
+
curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
676
|
+
segmentEnds[s] = curveTotal;
|
|
677
|
+
}
|
|
678
|
+
d1x += d2x;
|
|
679
|
+
d1y += d2y;
|
|
680
|
+
curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
681
|
+
segmentEnds[8] = curveTotal;
|
|
682
|
+
d1x += d2x + d3x;
|
|
683
|
+
d1y += d2y + d3y;
|
|
684
|
+
curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
685
|
+
segmentEnds[9] = curveTotal;
|
|
686
|
+
segment = 0;
|
|
687
|
+
}
|
|
688
|
+
p *= curveTotal;
|
|
689
|
+
for (;; segment++) {
|
|
690
|
+
const length = segmentEnds[segment];
|
|
691
|
+
if (p > length) continue;
|
|
692
|
+
if (segment === 0) p /= length;
|
|
693
|
+
else {
|
|
694
|
+
const prev = segmentEnds[segment - 1];
|
|
695
|
+
p = segment + (p - prev) / (length - prev);
|
|
696
|
+
}
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
onCurve(p * 0.1, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, wantsTangent || (i > 0 && space === 0));
|
|
700
|
+
}
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** A path constraint (the header): returns the bones it moved, every one in world space. */
|
|
705
|
+
export function solvePath(state: PathSolverState, c: CorePathRecord, slotWorld: CoreWorld): { changed: string[]; inWorld: string[] } {
|
|
706
|
+
const g = c.path;
|
|
707
|
+
const none = { changed: [], inWorld: [] };
|
|
708
|
+
if (g === null) return none;
|
|
709
|
+
const { mixRotate, mixX, mixY } = c;
|
|
710
|
+
if (mixRotate === 0 && mixX === 0 && mixY === 0) return none;
|
|
711
|
+
const wantsTangent = c.rotateMode === 'tangent';
|
|
712
|
+
const scale = c.rotateMode === 'chainScale';
|
|
713
|
+
const bones = c.bones.map((n) => state.bones[state.index.get(n) as number]);
|
|
714
|
+
const boneCount = bones.length;
|
|
715
|
+
const spaceCount = wantsTangent ? boneCount : boneCount + 1;
|
|
716
|
+
const spaces: number[] = new Array(spaceCount).fill(0);
|
|
717
|
+
const lengths: number[] = scale ? new Array(boneCount).fill(0) : [];
|
|
718
|
+
const spacing = c.spacing;
|
|
719
|
+
const worldOf = (b: ModelBone): CoreWorld => state.world.get(b.name) as CoreWorld;
|
|
720
|
+
if (c.spacingMode === 'percent') {
|
|
721
|
+
if (scale) {
|
|
722
|
+
for (let i = 0; i < spaceCount - 1; i++) {
|
|
723
|
+
const b = bones[i];
|
|
724
|
+
const boneLength = b.length ?? 0;
|
|
725
|
+
const w = worldOf(b);
|
|
726
|
+
const x = boneLength * w.a;
|
|
727
|
+
const y = boneLength * w.c;
|
|
728
|
+
lengths[i] = Math.sqrt(x * x + y * y);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (let i = 1; i < spaceCount; i++) spaces[i] = spacing;
|
|
732
|
+
} else if (c.spacingMode === 'proportional') {
|
|
733
|
+
let sum = 0;
|
|
734
|
+
for (let i = 0, n = spaceCount - 1; i < n; ) {
|
|
735
|
+
const b = bones[i];
|
|
736
|
+
const boneLength = b.length ?? 0;
|
|
737
|
+
if (boneLength < EPSILON) {
|
|
738
|
+
if (scale) lengths[i] = 0;
|
|
739
|
+
spaces[++i] = spacing;
|
|
740
|
+
} else {
|
|
741
|
+
const w = worldOf(b);
|
|
742
|
+
const x = boneLength * w.a;
|
|
743
|
+
const y = boneLength * w.c;
|
|
744
|
+
const length = Math.sqrt(x * x + y * y);
|
|
745
|
+
if (scale) lengths[i] = length;
|
|
746
|
+
spaces[++i] = length;
|
|
747
|
+
sum += length;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (sum > 0) {
|
|
751
|
+
sum = (spaceCount / sum) * spacing;
|
|
752
|
+
for (let i = 1; i < spaceCount; i++) spaces[i] *= sum;
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
const addsLength = c.spacingMode === 'length';
|
|
756
|
+
for (let i = 0, n = spaceCount - 1; i < n; ) {
|
|
757
|
+
const b = bones[i];
|
|
758
|
+
const boneLength = b.length ?? 0;
|
|
759
|
+
if (boneLength < EPSILON) {
|
|
760
|
+
if (scale) lengths[i] = 0;
|
|
761
|
+
spaces[++i] = spacing;
|
|
762
|
+
} else {
|
|
763
|
+
const w = worldOf(b);
|
|
764
|
+
const x = boneLength * w.a;
|
|
765
|
+
const y = boneLength * w.c;
|
|
766
|
+
const length = Math.sqrt(x * x + y * y);
|
|
767
|
+
if (scale) lengths[i] = length;
|
|
768
|
+
spaces[++i] = ((addsLength ? Math.max(0, boneLength + spacing) : spacing) * length) / boneLength;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const positions = positionsOf(state, c, g, spaces, wantsTangent);
|
|
773
|
+
let atX = positions[0];
|
|
774
|
+
let atY = positions[1];
|
|
775
|
+
let offsetRotation = c.offsetRotation;
|
|
776
|
+
let fromTip: boolean;
|
|
777
|
+
if (offsetRotation === 0) fromTip = c.rotateMode === 'chain';
|
|
778
|
+
else {
|
|
779
|
+
fromTip = false;
|
|
780
|
+
const p = slotWorld;
|
|
781
|
+
offsetRotation *= p.a * p.d - p.b * p.c > 0 ? RAD : -RAD;
|
|
782
|
+
}
|
|
783
|
+
const changed: string[] = [];
|
|
784
|
+
for (let i = 0, p = 3; i < boneCount; i++, p += 3) {
|
|
785
|
+
const b = bones[i];
|
|
786
|
+
const w = { ...worldOf(b) };
|
|
787
|
+
w.worldX += (atX - w.worldX) * mixX;
|
|
788
|
+
w.worldY += (atY - w.worldY) * mixY;
|
|
789
|
+
const x = positions[p];
|
|
790
|
+
const y = positions[p + 1];
|
|
791
|
+
const dx = x - atX;
|
|
792
|
+
const dy = y - atY;
|
|
793
|
+
if (scale) {
|
|
794
|
+
const length = lengths[i];
|
|
795
|
+
if (length >= EPSILON) {
|
|
796
|
+
const s = (Math.sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
|
|
797
|
+
w.a *= s;
|
|
798
|
+
w.c *= s;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
atX = x;
|
|
802
|
+
atY = y;
|
|
803
|
+
if (mixRotate > 0) {
|
|
804
|
+
const { a, b: bb, c: cc, d } = w;
|
|
805
|
+
let r: number;
|
|
806
|
+
if (wantsTangent) r = positions[p - 1];
|
|
807
|
+
else if (spaces[i + 1] === 0) r = positions[p + 2];
|
|
808
|
+
else r = Math.atan2(dy, dx);
|
|
809
|
+
r -= Math.atan2(cc, a);
|
|
810
|
+
if (fromTip) {
|
|
811
|
+
const cos = Math.cos(r);
|
|
812
|
+
const sin = Math.sin(r);
|
|
813
|
+
const length = b.length ?? 0;
|
|
814
|
+
atX += (length * (cos * a - sin * cc) - dx) * mixRotate;
|
|
815
|
+
atY += (length * (sin * a + cos * cc) - dy) * mixRotate;
|
|
816
|
+
} else r += offsetRotation;
|
|
817
|
+
if (r > RUNTIME_PI) r -= RUNTIME_PI * 2;
|
|
818
|
+
else if (r < -RUNTIME_PI) r += RUNTIME_PI * 2;
|
|
819
|
+
r *= mixRotate;
|
|
820
|
+
const cos = Math.cos(r);
|
|
821
|
+
const sin = Math.sin(r);
|
|
822
|
+
w.a = cos * a - sin * cc;
|
|
823
|
+
w.b = cos * bb - sin * d;
|
|
824
|
+
w.c = sin * a + cos * cc;
|
|
825
|
+
w.d = sin * bb + cos * d;
|
|
826
|
+
}
|
|
827
|
+
state.world.set(b.name, w);
|
|
828
|
+
changed.push(b.name);
|
|
829
|
+
}
|
|
830
|
+
return { changed, inWorld: [...changed] };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
// the dump's two path blocks
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
|
|
837
|
+
/** A mode as the runtime's enum names it: the first letter upper-cased (`chainScale` → `ChainScale`). */
|
|
838
|
+
const enumName = (mode: string): string => mode[0].toUpperCase() + mode.slice(1);
|
|
839
|
+
|
|
840
|
+
/** One `paths` row, the oracle's shape (`OraclePathRow` in `tools/pose_oracle.ts`). */
|
|
841
|
+
export interface CorePathRow {
|
|
842
|
+
name: string;
|
|
843
|
+
slot: string;
|
|
844
|
+
bones: string[];
|
|
845
|
+
positionMode: string;
|
|
846
|
+
spacingMode: string;
|
|
847
|
+
rotateMode: string;
|
|
848
|
+
offsetRotation: number | null;
|
|
849
|
+
position: number | null;
|
|
850
|
+
spacing: number | null;
|
|
851
|
+
mixRotate: number | null;
|
|
852
|
+
mixX: number | null;
|
|
853
|
+
mixY: number | null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** One `pathAttachments` row, the oracle's shape. */
|
|
857
|
+
export interface CorePathAttachmentRow {
|
|
858
|
+
skin: string;
|
|
859
|
+
slot: string;
|
|
860
|
+
placeholder: string;
|
|
861
|
+
closed: boolean;
|
|
862
|
+
constantSpeed: boolean;
|
|
863
|
+
lengths: Array<number | null>;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** The `paths` block: every path record in the document's order, its modes as the runtime names them, its numbers through `round`. */
|
|
867
|
+
export function pathRows(records: readonly CorePathRecord[], round: (v: number) => number | null): CorePathRow[] {
|
|
868
|
+
return records.map((r) => ({
|
|
869
|
+
name: r.name, slot: r.slot, bones: [...r.bones],
|
|
870
|
+
positionMode: enumName(r.positionMode), spacingMode: enumName(r.spacingMode), rotateMode: enumName(r.rotateMode),
|
|
871
|
+
offsetRotation: round(r.offsetRotation), position: round(r.position), spacing: round(r.spacing), mixRotate: round(r.mixRotate), mixX: round(r.mixX), mixY: round(r.mixY),
|
|
872
|
+
}));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/** The `pathAttachments` block: every path attachment of every skin, its two flags as read and its `lengths` through `round`. */
|
|
876
|
+
export function pathAttachmentRows(skins: ReadonlyArray<{ name: string; attachments: Record<string, Record<string, { geometry?: { kind: string } & Partial<CorePathGeometry> }>> }>, round: (v: number) => number | null): CorePathAttachmentRow[] {
|
|
877
|
+
const out: CorePathAttachmentRow[] = [];
|
|
878
|
+
for (const skin of skins) {
|
|
879
|
+
for (const [slot, table] of Object.entries(skin.attachments)) {
|
|
880
|
+
for (const [placeholder, record] of Object.entries(table)) {
|
|
881
|
+
const g = record.geometry;
|
|
882
|
+
if (g?.kind !== 'path') continue;
|
|
883
|
+
out.push({ skin: skin.name, slot, placeholder, closed: g.closed === true, constantSpeed: g.constantSpeed !== false, lengths: (g.lengths ?? []).map(round) });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return out;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ---------------------------------------------------------------------------
|
|
891
|
+
// which slot bone the offset's sign reads
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* When the runtime last set a path constraint's slot bone's world before the
|
|
896
|
+
* constraint runs (the header's *Which slot bone*): during the ordering of
|
|
897
|
+
* constraint `at` — brought up to date from its parent just `before` that
|
|
898
|
+
* constraint runs, or moved by it and so as it left the bone, `after` it.
|
|
899
|
+
*/
|
|
900
|
+
export interface SlotBoneEvent {
|
|
901
|
+
at: number;
|
|
902
|
+
when: 'before' | 'after';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* For each path constraint, by its index in `records`, the last event that
|
|
907
|
+
* set its slot bone's world before it runs — `null` when nothing has in this
|
|
908
|
+
* pass, and the bone holds the previous pass's world. The runtime orders its
|
|
909
|
+
* update before it poses: each constraint in turn brings the bones it reads
|
|
910
|
+
* and moves up to date (a bone, its parents first, once until something
|
|
911
|
+
* below which it hangs moves), and every bone left over follows at the end.
|
|
912
|
+
* `skipped` names the constraints the runtime does not apply, which it does
|
|
913
|
+
* not order either.
|
|
914
|
+
*/
|
|
915
|
+
export function slotBonePlan(bones: readonly ModelBone[], active: ReadonlySet<string>, records: readonly CoreConstraintRecord[], skipped: ReadonlySet<number>): Map<number, SlotBoneEvent | null> {
|
|
916
|
+
const parent = new Map(bones.map((b) => [b.name, b.parent]));
|
|
917
|
+
const children = new Map<string, string[]>();
|
|
918
|
+
for (const b of bones) if (b.parent !== undefined) children.set(b.parent, [...(children.get(b.parent) ?? []), b.name]);
|
|
919
|
+
const ordered = new Map(bones.map((b) => [b.name, !active.has(b.name)]));
|
|
920
|
+
const last = new Map<string, SlotBoneEvent>();
|
|
921
|
+
const orderBone = (name: string, m: number): void => {
|
|
922
|
+
if (ordered.get(name)) return;
|
|
923
|
+
const p = parent.get(name);
|
|
924
|
+
if (p !== undefined) orderBone(p, m);
|
|
925
|
+
ordered.set(name, true);
|
|
926
|
+
last.set(name, { at: m, when: 'before' });
|
|
927
|
+
};
|
|
928
|
+
const unorder = (names: readonly string[]): void => {
|
|
929
|
+
for (const n of names) {
|
|
930
|
+
if (!active.has(n)) continue;
|
|
931
|
+
if (ordered.get(n)) unorder(children.get(n) ?? []);
|
|
932
|
+
ordered.set(n, false);
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
const plan = new Map<number, SlotBoneEvent | null>();
|
|
936
|
+
records.forEach((c, m) => {
|
|
937
|
+
if (skipped.has(m)) return;
|
|
938
|
+
let moved: readonly string[];
|
|
939
|
+
// A slider sets no bone in the order; it resets each bone its animation keys, and what hangs below it (the header's *Which slot bone*).
|
|
940
|
+
if (c.kind === 'slider') {
|
|
941
|
+
unorder(c.timelines.bones.map((t) => t.name));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (c.kind === 'ik') {
|
|
945
|
+
moved = c.bones;
|
|
946
|
+
orderBone(c.target, m);
|
|
947
|
+
orderBone(c.bones[0], m);
|
|
948
|
+
if (c.bones.length > 1) orderBone(c.bones[c.bones.length - 1], m);
|
|
949
|
+
unorder(children.get(c.bones[0]) ?? []);
|
|
950
|
+
if (c.bones.length > 1) ordered.set(c.bones[c.bones.length - 1], true);
|
|
951
|
+
} else {
|
|
952
|
+
// A physics constraint orders its one bone as a one-bone constraint does (the header's *Which slot bone*).
|
|
953
|
+
moved = c.kind === 'physics' ? [c.bone] : c.bones;
|
|
954
|
+
if (c.kind === 'transform') orderBone(c.source, m);
|
|
955
|
+
else if (c.kind === 'path') for (const d of c.slotDeps) orderBone(d, m);
|
|
956
|
+
for (const b of moved) orderBone(b, m);
|
|
957
|
+
if (c.kind === 'path') plan.set(m, last.get(c.slotBone) ?? null);
|
|
958
|
+
for (const b of moved) unorder(children.get(b) ?? []);
|
|
959
|
+
for (const b of moved) ordered.set(b, true);
|
|
960
|
+
}
|
|
961
|
+
for (const b of moved) last.set(b, { at: m, when: 'after' });
|
|
962
|
+
});
|
|
963
|
+
return plan;
|
|
964
|
+
}
|