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.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. 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
+ }