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