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,1876 @@
1
+ /**
2
+ * Construct 5 of the core, first cut (issue #938, step 2e-i of issue #380):
3
+ * the update order, and the two constraint kinds that move bones only by
4
+ * what the bones and their target say — `ik` and `transform` — at the setup
5
+ * pose and at a sample time, with their timelines. The second cut (issue
6
+ * #938, step 2e-ii) adds `path`, whose record, walk and solver are
7
+ * `./constraints_path.ts`'s; the third cut (2e-iii) adds `physics` under
8
+ * `Physics.none` (`./constraints_physics.ts`: it applies nothing) and
9
+ * `slider` (`./constraints_slider.ts`); the fourth (issue #956, 2e-iv)
10
+ * steps physics when `applyConstraints` is given a step context — the
11
+ * constraint then moves its bone in world space and the bones below it are
12
+ * posed again, as after a world-space transform. Each is one more step of the update
13
+ * loop below, so every constraint kind is posed; what the core still cannot
14
+ * pose exactly leaves the bones out by name (`constraintsAbsentWhy`).
15
+ *
16
+ * Every rule below was measured by posing hand-written skeletons through
17
+ * `tools/pose_oracle.ts dump` (spine-core 4.3.13, `--skin all`,
18
+ * `--physics none`) and comparing the core's rows at tolerance 0, most of them
19
+ * with "amplifier" bones — a child a million units out along each axis of the
20
+ * bone under test — so that a difference of 1e-12 in a matrix reads as one
21
+ * grid step (the `CC` controls hold the populations at ten thousand units:
22
+ * at a million a skewed chain's amplifier reached 1.7e9, where a double no
23
+ * longer resolves the grid). Nothing here was written from the runtime's
24
+ * source. The public
25
+ * documentation fixes the order the constraints run in (the editor's list,
26
+ * top first) and the shape of each solver; every numeric choice beyond that —
27
+ * which frame, which wrap, which constant — is the measurement stated next to
28
+ * it. The core suite's `CC` controls hold the hand-written probes, and
29
+ * `tools/core_gate.ts` holds the corpus.
30
+ *
31
+ * ## The update order
32
+ *
33
+ * **Constraints run in the document's order** — the model lists them as the
34
+ * rig declares them and the Spine file carries that order, which is the
35
+ * runtime's constraint list (the oracle's `constraints` roster). After each
36
+ * one, every bone below a bone it changed is posed again from its local
37
+ * values under its parent's new world transform, so a later constraint reads
38
+ * the bones as the earlier ones left them. Measured: 500 random nine-bone
39
+ * rigs, each with two to six ik and transform constraints in random order,
40
+ * random targets and sources, reflecting, sheared, non-normal and softened —
41
+ * every one IDENTICAL at tolerance 0 (`CC05`). Swapping two constraints that
42
+ * touch one chain moves the pose (`CC10`).
43
+ *
44
+ * - A constraint whose mix for a property is 0 does not touch that property,
45
+ * and one that touches nothing leaves the bone and everything under it
46
+ * exactly as they were — not re-posed from its local values. A transform
47
+ * constraint driving `y` alone whose `mixY` is absent (the pose's initial
48
+ * 0) re-posed its bone's children from lossy local values in the first
49
+ * reading and was 4e-6 off; skipping it made the probe exact.
50
+ * - **A bone a transform constraint moved in world space keeps that world
51
+ * transform**, and its local values are re-derived from it at once
52
+ * (`localFromWorld`, below), so that when a later constraint moves one of
53
+ * its ancestors it follows from those. Measured: a transform setting a
54
+ * child's world rotation, then an ik turning its parent by 90°, turned the
55
+ * child with the parent (the child's world rotation 180°); in the other
56
+ * order, 90°.
57
+ * - **A slider poses again every bone its animation keys**, a timeline
58
+ * before its first key included (issue #989, `./constraints_slider.ts`,
59
+ * *Where it stands*): on a bone a transform moved in world space that
60
+ * rebuilds the world from the read-back local values, which the runtime
61
+ * does and keeping the world did not (`CZ01`).
62
+ * - A constraint with `skin: true` is applied exactly when an APPLIED skin's
63
+ * list for its kind names it (`listedBySkin`, set per skin view by
64
+ * `underSkin` in `./index.ts`): under `--skin all` any skin's, under
65
+ * `--skin <name>` that skin's alone — the default skin's list does not
66
+ * count there. Measured for all five kinds (issue #932, which carries card
67
+ * #961's fix): a skin-required ik, transform, path, physics and slider
68
+ * constraint on a probe where applying it moves a bone, dumped under
69
+ * `--skin all`, `default` and `s1` against the same constraint at mix 0 —
70
+ * named by no list: inert under all three; named by `s1`'s list: applied
71
+ * under `all` and `s1`, inert under `default`; named by the default skin's
72
+ * list: applied under `all` and `default`, inert under `s1` — the same
73
+ * nine readings for every kind, 45 in all (the core suite's `CN02`). A name filed under another kind's list
74
+ * is refused by the runtime's loader (`Couldn't find IK constraint k for
75
+ * skin s1.`), and by `readModel`. The rule this replaced — `skin: true`
76
+ * never applied (2e-i) — was measured only on unlisted constraints; issue
77
+ * #956 corrected it for physics first. An ik whose target is inactive is
78
+ * not applied (measured on an ik with its target skin-required and named by
79
+ * no skin: the bone did not turn), nor a transform whose source is; an ik
80
+ * and a transform are applied to every bone they name, inactive ones
81
+ * included (*Unposed bones and collapsed frames*, below); a path constraint is applied exactly when its
82
+ * slot's bone is active (`./constraints_path.ts`, *Which constraints run*).
83
+ * - A path constraint moves its bones in world space, as a world-space
84
+ * transform does, and reads the bones as the earlier constraints left
85
+ * them — except its slot bone's world, which the offset's sign reads as
86
+ * the runtime last set it: its update order is built before it poses, and
87
+ * a weighted path does not ask for its slot bone (`slotBonePlan`,
88
+ * *Which slot bone* there). This is the one place the order the runtime
89
+ * builds is observable: ik and transform ask for every bone they read.
90
+ *
91
+ * ## Unposed bones and collapsed frames (issue #979)
92
+ *
93
+ * A bone the posed skin leaves unposed — inactive, or below an inactive bone
94
+ * — holds the zero matrix, so a constraint over it works in a collapsed
95
+ * frame. The grid compare never reads such a bone (the oracle's
96
+ * ill-conditioned rule, and a document spells `-0` as `0`), so every rule here
97
+ * was measured through `tools/pose_oracle.ts unposed`, which compares those
98
+ * rows by `Object.is`, and on posed "reader" bones that carry a bone's local
99
+ * values through a local transform constraint. The rules, planted back one at
100
+ * a time by the core suite's `CC13` and `CC14` (`SolverRules`):
101
+ *
102
+ * - **An ik frame of determinant at most 0.00001 is collapsed** — exactly
103
+ * 0.00001 collapses, the next double above does not, on a posed parent
104
+ * (`CC14`). A one-bone ik then reads the target at the frame's origin (the
105
+ * offset `(0, 0)`, so `atan2` is 0): the bone turns to 0 less its `shearX`,
106
+ * plus 180 when it reflects, and `compress` scales it by `mix` toward 0.
107
+ * A two-bone ik reads its grandparent's inverse as zero, so the target and
108
+ * the child's origin are both the grandparent frame's origin. Before this
109
+ * the core divided by the determinant and wrote NaN into every field.
110
+ * - **A one-bone ik under a `noRotationOrReflection` parent floors the
111
+ * parent x axis's squared length at 0.00001** when it builds the conformal
112
+ * frame (`k = |det| / max(0.00001, a² + c²)`): solving for the floor on
113
+ * five parents whose x axes are under it read 1.0000000166e-5 on each, and
114
+ * with 0.00001 written 432 of 432 probes over three modes, eight parent x
115
+ * scales, three y scales, three rotations and with or without
116
+ * `compress`/`stretch` read exact, where no floor read 33 off. `./world.ts`'s frame for the same mode has no floor.
117
+ * - **`localFromWorld` under a collapsed parent** reads x and y as the
118
+ * division leaves them (NaN or infinite), `scaleX` 0, `shearY` 0, `scaleY`
119
+ * the y column's length (NaN) and `rotation` 0: a local x column that is not
120
+ * above 0.0001 — NaN included — is collapsed, and a y column no longer than
121
+ * 0.00001 reads `rotation` 0 when the x column is collapsed too, and
122
+ * `shearY` 0 when it is not (measured by bisection: 1e-5 reads 0, the next
123
+ * double above reads the column's angle).
124
+ * - **A world-space `rotate` source negates the constraint's `rotation`
125
+ * unless its determinant is above 0**: a source of determinant 0 negates it.
126
+ * - **An inactive bone is not posed again** when a constraint moves a bone
127
+ * above it — it keeps its zeros, or what a constraint wrote into them — and
128
+ * the bones below it are posed again only when a constraint moved it
129
+ * itself (a path on an inactive bone moves the bones below it; a transform
130
+ * on its parent does not).
131
+ * - **A transform constraint is applied when its source is active, to every
132
+ * bone it names**: one inactive bone among them did not stop it moving a
133
+ * posed one (the core skipped the whole constraint), and it wrote the
134
+ * source's position into the inactive one.
135
+ *
136
+ * Under these, transform (six flag sets, five modes), path, physics (under
137
+ * `none` and the step) and one- and two-bone ik over unposed bones, and iks
138
+ * aimed at them, read every bone row equal to spine-core's, the sign of every
139
+ * zero included (`CC13`).
140
+ *
141
+ * ⚠️ **What the runtime writes into an INACTIVE constrained bone depends on
142
+ * the pass before.** An ik naming an inactive bone is applied by the runtime
143
+ * reading that bone's ancestors before it brings them up to date in the pass
144
+ * — the previous pass's worlds, or zeros on a fresh skeleton — and a
145
+ * transform or path writing into an inactive bone starts from the world the
146
+ * previous pass left in it (the runtime does not reset it). Measured by
147
+ * posing one skeleton sample after sample, as the oracle's dump does, against
148
+ * a fresh skeleton per sample: an ik on `arm, hand` with `arm` inactive, a
149
+ * transform onto an inactive bone at mix 0.5, additive or at mix 1, and a
150
+ * path at mix 0.5 each read differently at 4 of 5 sample times (the mix-1
151
+ * transform at 1 of 5, by 2.4e-17 in a `worldX` of 5.8e-7). The runtime
152
+ * disagrees with itself there, so there is no rule of the format to hold, and
153
+ * the core poses as a function of the model, the skin and the time: it reads
154
+ * this class as a fresh skeleton does, which is the runtime's own setup pose.
155
+ * A transform or path writes into an inactive bone from zeros; an ik naming
156
+ * an inactive bone reads the frame above its first bone as zeros unless the
157
+ * pass has brought that bone up to date before the ik (`frameUpdatedBefore`,
158
+ * the order `slotBonePlan` walks), and as posed otherwise. Held against the
159
+ * fresh reading on every row, and against the sequential one wherever the
160
+ * two agree (`CC13`; `pose_oracle unposed` classes the rest HISTORY by
161
+ * measurement). An ik on `arm, hand` with its target under `arm`'s parent, or
162
+ * after a transform on that parent, read no history at all.
163
+ *
164
+ * The render never reads an unposed bone (the seam's zero snapshot), so the
165
+ * history reaches a picture only through a POSED bone that a constraint moves
166
+ * while reading a bone the history reached — and that pose is refused by
167
+ * name (`unposedLeakWhy`), which the render meets by falling back to
168
+ * spine-core. The taint is by bone and by what the reader reads, its world
169
+ * or its local values: measured on 48 writer-and-reader probes (`CC15`),
170
+ * every one whose posed rows spine-core's two readings disagree on (14) is
171
+ * refused, and 8 are refused where the reader reads a part of the bone the
172
+ * history does not move — a refusal costs a named fallback, a miss a picture
173
+ * that depends on which frame played before.
174
+ *
175
+ * ## Reading local values back from a world transform (`localFromWorld`)
176
+ *
177
+ * The runtime's re-derived values were read directly: a second transform
178
+ * constraint with `localSource` reading the bone's `rotate`, `x`, `y`,
179
+ * `scaleX`, `scaleY`, `shearY` onto an unrelated bone's `x` at scale 10000
180
+ * gave each to 1e-10, and they agree with these rules on every probe:
181
+ *
182
+ * - `normal`: the local matrix is the parent's inverse (1 over its
183
+ * determinant, multiplied in) times the world matrix, and the local
184
+ * position the same inverse applied to the world origin's offset. Then
185
+ * `scaleX` is the x column's length and `shearX` is 0; ⚠️ **a local x
186
+ * column shorter than 0.0001 is read as `scaleX` 0**, `shearY` 0, `scaleY`
187
+ * the y column's length and `rotation` the y column's angle less 90 —
188
+ * measured on a column collapsed to 1.8e-6 by two `scaleX` properties, where
189
+ * the runtime read `rotation` −248.958 (the unwrapped y angle less 90) and
190
+ * `scaleX` 0. Otherwise `rotation` is the x column's angle, `scaleY` the y
191
+ * column's length — NEGATIVE when the determinant is — and `shearY` the y
192
+ * column's angle less `rotation + 90`, or less `rotation − 90` when the
193
+ * determinant is negative, brought once into (−180, 180]. The negative
194
+ * `scaleY` and the right angle taken off the other way are what reproduce
195
+ * the runtime's ±2.66e-6° residual on a reflected bone: 138 of 400
196
+ * reflected and sheared probes missed with a positive `scaleY`; turning the
197
+ * y angle by 180 first agrees to 1e-10 and not to the bit (below).
198
+ * - `onlyTranslation`: the world matrix is the local matrix.
199
+ * - `noRotationOrReflection`: the local matrix is taken against the parent's
200
+ * conformal matrix (the frame `./world.ts` builds for that mode), and the
201
+ * parent's x-axis angle is added to the rotation.
202
+ * - `noScale`, `noScaleOrReflection`: `rotation` is the angle of the parent's
203
+ * inverse applied to the world x column; the frame the forward pose builds
204
+ * from that rotation (`noScaleDirection` in `./world.ts`) is formed, both
205
+ * world columns are read in it (flipped when the parent reflects, for
206
+ * `noScale`), and that local matrix is decomposed with its x column's
207
+ * angle — a residual of rounding — kept as `shearX`, and `shearY` measured
208
+ * from 0, as the forward frame `frame(0, shearX, shearY, …)` reads them.
209
+ *
210
+ * Each of the five held on 300 probes at the million-unit amplifier: a world
211
+ * edit on the bone, then a translation-only edit on its parent that makes the
212
+ * bone re-posed from its re-derived values (`CC06`).
213
+ *
214
+ * ## To the bit (issue #966)
215
+ *
216
+ * Held against `pose_oracle.ts dump --raw` at tolerance 0 (the core suite's
217
+ * `CR01`, the `CC` populations and seeds; the corpus through `core_gate
218
+ * --raw`), these orders are the runtime's where the grid could not tell:
219
+ *
220
+ * - a one-bone ik accumulates the change from the bone's own angles first,
221
+ * `−shearX − rotation + turn + atan2·DEG` (the target's direction first
222
+ * read 53 of 600 random iks off);
223
+ * - a two-bone ik takes the child's offset angle off in radians before
224
+ * turning the change into degrees, `(a1 − offset)·DEG` (`a1·DEG −
225
+ * offset·DEG` read 44 bone findings on `spineboy-pro` against 4);
226
+ * - an ik timeline's keyed mix and softness reach the pose through the
227
+ * setup blend, `setup + (value − setup)·1` (the value as keyed read a
228
+ * Bézier-eased mix 2 ulp off on `spineboy-pro`'s jump);
229
+ * - a world-space rotate turns the change in radians, wrapped at the
230
+ * runtime's pi (the degree form read 113 bone findings on `spineboy-pro`
231
+ * against 44);
232
+ * - `localFromWorld` forms the inverse's entries before applying them, and
233
+ * reads the shear as `yAngle − (rotation + 90)`;
234
+ * - and, in the second cut (the band `CW` controls; each rule planted back to
235
+ * the reading before it by a `SolverRules` flag), the three read-back forms
236
+ * above — a reflected bone's shear as the y angle less `rotation − 90`
237
+ * (`CW01`); `noRotationOrReflection`'s columns as `(pa·wa + pc·wc)·(1/(pa² +
238
+ * pc²))` and `(pa·wc − pc·wa)·(1/|det|)` (`CW02`); the `noScale` modes read
239
+ * in the frame of the rotation read first, the residual kept as `shearX`
240
+ * (`CW03`) — which bring the read-back population from 164 to 400 of 400,
241
+ * the update-order population from 389 to 400 and the stepped physics
242
+ * population (`CR07`) from 140 to 150 of 150; an additive world-space
243
+ * `shearY` as `(v + 90)·RAD − 90·RAD` (`CW04`: the transform population
244
+ * from 865 to 900 of 900); a transform timeline's mixes through the setup
245
+ * blend (`CW05`); and the two-bone ik over a parent whose scale is 0, below;
246
+ * - and a path timeline's position, spacing and mixes through the setup blend
247
+ * too (issue #984, `CY01`; *The timelines*, below), and a slider
248
+ * timeline's `time` and `mix` (issue #991, `CZ03`).
249
+ *
250
+ * The local values were read to the bit rather than through the re-posed
251
+ * world: a reader bone's `x` driven by a local transform from the bone's
252
+ * `rotate`, `x`, `y`, `scaleX`, `scaleY` or `shearY` at scale 1 under the
253
+ * root is the value itself in the raw dump. That separated a wrong local
254
+ * value from a right one the forward pose reads differently — the `noScale`
255
+ * modes' local values read bit-exact before their world did, which is how
256
+ * the kept `shearX` was found.
257
+ *
258
+ * ## ik — one bone
259
+ *
260
+ * The bone turns so its x axis points at the target. With `v` the target in
261
+ * the bone's parent frame:
262
+ *
263
+ * - `normal`, `noScale`, `noScaleOrReflection`: the parent's world matrix,
264
+ * inverted by dividing by its determinant, applied to the target's offset
265
+ * from the PARENT's world origin, less the bone's local `x`, `y`.
266
+ * `onlyTranslation`: the target's offset from the bone's own world origin,
267
+ * as it is. `noRotationOrReflection`: as `normal`, with the parent's
268
+ * conformal matrix (x axis kept, y axis at 90° with length |det| / |x|) for
269
+ * its matrix, and the parent's x-axis angle added to the angle. Each other
270
+ * reading of that mode missed 14 to 200 of 300 probes; the offset from the
271
+ * bone's own origin, for `normal`, is the same number up to rounding and
272
+ * was exact on every random probe — ⚠️ but a target AT the bone's origin
273
+ * has only rounding noise for a direction, and the runtime turned such a
274
+ * bone to 56.56° where that reading read 30° (0.3345 off in `b`); the
275
+ * parent-origin reading reproduces the noise exactly (`CC04`).
276
+ * - The rotation it needs is `atan2(v)` less `shearX` less the current
277
+ * rotation, plus 180 when `scaleX` is negative, brought once into
278
+ * (−180, 180], times `mix`, added to the rotation.
279
+ * - `compress` / `stretch`: with `len` the bone's `length` times `scaleX` and
280
+ * `d` the length of `v` — of the world offset for the two `noScale` modes —
281
+ * when `len` is above 0.00001 (issue #966, by bisection: 0.00001 exactly is
282
+ * left alone and the double above is scaled; 0.0001, the reading before,
283
+ * left every bone between the two unscaled) and the target is nearer (`compress`) or
284
+ * farther (`stretch`), `scaleX` is multiplied by `(d / len − 1) · mix + 1`.
285
+ * With `scaleY` `uniform` `scaleY` is multiplied by the same `s`; with
286
+ * `volume` it is divided by `s`, except below 0.7 where it is divided by
287
+ * `0.25 + s · 0.642857`. That constant is measured: a bone of `scaleY`
288
+ * 100000 compressed to seven lengths read `(1/scaleY − 0.25) / s` =
289
+ * 0.642857000 to nine places on every one, and the break is between 0.6999
290
+ * and 0.7001.
291
+ * - `mix` 0 leaves the bone alone.
292
+ *
293
+ * Measured: 1,000 random probes each over rotated, scaled, reflecting and
294
+ * sheared parents and bones, all five inherit modes, `compress`, `stretch`,
295
+ * the three `scaleY` modes and random `mix`, all exact (`CC02`).
296
+ *
297
+ * ## ik — two bones
298
+ *
299
+ * The public documentation: the second bone must be a child of the first;
300
+ * the child's tip reaches the target; `bendPositive` picks the side;
301
+ * `softness` slows the chain as it straightens; `stretch` scales it to reach.
302
+ * Measured, in the parent's parent frame (the "grandparent", its world
303
+ * matrix inverted by multiplying by 1 over its determinant):
304
+ *
305
+ * - **Either bone not `normal`: the constraint does nothing** (all four
306
+ * other modes on each bone, 200 probes each).
307
+ * - The parent keeps its shear. ⚠️ The public page says the parent's local
308
+ * shear is set to 0; spine-core 4.3.13 keeps it (100 sheared probes: 100
309
+ * off with the shear zeroed, 0 kept).
310
+ * - **Uniform** when the parent's |scaleX| and |scaleY| differ by at most
311
+ * 0.00001 — measured between 9.9e-6 (uniform) and 1.01e-5 (not), at scales
312
+ * 0.5, 1 and 2. The public page's "nonuniform" was read as 0.0001 at first,
313
+ * and a world-edited parent at 1.00005 went 2,261,052 units off at the
314
+ * amplifier.
315
+ * - The child's local `y` is set to 0 when the scale is not uniform or
316
+ * `stretch` is on; otherwise it is kept, and it offsets the angles by
317
+ * `atan2(y, x)` of the child's local position — unscaled.
318
+ * - `l1` is the distance, in the grandparent frame, from the parent's local
319
+ * origin to the child's world origin (that is, the parent's world matrix
320
+ * applied to the child's local `x`, `y`); `l2` is the child's `length`
321
+ * times |its scaleX|; the target is read in the same frame.
322
+ * - **softness**: `s` = `softness` · |parent scaleX| · (|child scaleX| + 1) ·
323
+ * 0.5; with `d` the target's distance and `sd = d − l1 − l2·|scaleX| + s`,
324
+ * when `sd > 0` the target is drawn in toward the parent by
325
+ * `(sd − s·(1 − q²)) / d` of itself, `q = min(1, sd / 2s) − 1`: the chain
326
+ * reaches `d − sd²/4s` until `sd` is `2s`, and full length after. Measured
327
+ * on a straight chain at eleven distances and two softnesses, then at the
328
+ * amplifier: the fully softened chain is ill-conditioned (its bend is the
329
+ * arc-cosine of 1 − a few ulp), and only this arithmetic, with the
330
+ * subtraction in this order and the square root (not `hypot`) for `l1`,
331
+ * read it exactly — 132, 102, 59 and 10 of 1,000 missed on the way.
332
+ * - **Uniform**: the law of cosines, `cos = (d² − l1² − l2²) / (2·l1·l2)`
333
+ * with `l2` scaled by |scaleX|; below −1 the bend is π·bend, above 1 it is
334
+ * 0 and `stretch` multiplies the parent's `scaleX` (and `scaleY` for
335
+ * `uniform`, divides it for `volume`) by `(d/(l1+l2) − 1)·mix + 1`. The
336
+ * parent's angle is one `atan2` of the target turned back by the bend's
337
+ * offset — the difference of two `atan2`s is off by 2π about 1 time in 30,
338
+ * and 2π in the runtime's degrees is not 2π.
339
+ * - **Not uniform**: the child's tip at `(X, Y)` in the parent's scaled frame
340
+ * solves `(b² − a²)X² − 2b²·l1·X + b²·l1² + a²·d² − a²b² = 0`
341
+ * (`a`, `b` = `l2` times |scaleX|, |scaleY|), solved the numerically stable
342
+ * way and taking the root of smaller magnitude; when it has none, the
343
+ * nearest reachable pose of the three candidates — folded, straight and the
344
+ * ellipse's vertex — on the side of the midpoint of their squared reaches
345
+ * the target is. The folded bend is π in the runtime's own π (3.1415927);
346
+ * the parent's angle is here the DIFFERENCE of two `atan2`s.
347
+ * - The parent's rotation is the angle less the offset, plus 180 when its
348
+ * `scaleX` is negative, and the child's is `(bend + offset)·deg − shearX`,
349
+ * times the product of the parent's scale signs, plus 180 when the child's
350
+ * `scaleX` is negative. Each change is brought once into (−180, 180] —
351
+ * **−180 goes to 180**: a folded chain under a reflecting parent read the
352
+ * same child angle for both bends, measured at `mix` 0.5 — and multiplied
353
+ * by `mix`.
354
+ *
355
+ * Measured: 2,000 random probes per combination — grandparents rotated,
356
+ * scaled, reflecting and sheared; parents uniform, reflecting and not
357
+ * uniform; children offset in `y`, scaled, reflecting and sheared; `stretch`,
358
+ * `softness`, the `scaleY` modes, both bends, random `mix` — exact at the
359
+ * amplifier (`CC03`). Degenerate cases (`CC04`): the child at the parent's
360
+ * origin, the target at the parent's origin, the chain exactly straight,
361
+ * exactly folded, the softness onset and its end, a child of length 0 — all
362
+ * exact. The runtime throws on an ik whose (first) bone is the root, so the
363
+ * reader refuses it. A collapsed grandparent (determinant 0) is left to the
364
+ * oracle's ill-conditioned rule, which excludes the bone and everything below
365
+ * it — the one case where the core's value is not the runtime's.
366
+ *
367
+ * **A parent whose scale is 0** (issue #966, reducing #959's third class, "a
368
+ * transform collapsing a bone's scale, with an ik below it", to a two-bone ik
369
+ * with no transform at all; `CW06`–`CW08`, compared by `Object.is` on every
370
+ * bone, the ones compare's ill-conditioned rule leaves out included):
371
+ *
372
+ * - a scale of 0 counts as POSITIVE in the product of the parent's scale
373
+ * signs that turns the child's offset and angle — `Math.sign` read it as 0
374
+ * and read 59 of 300 iks over a parent of `scaleY` 0 exact;
375
+ * - **a child whose origin is nearer the parent's than 0.00001** in the
376
+ * grandparent frame (`l1`) makes the constraint a one-bone ik on the parent
377
+ * — stretched as the constraint says, never compressed, its `scaleY` mode
378
+ * read as none — with the child posed at rotation 0 and the local `y` the
379
+ * solver set. A parent `scaleX` of 0 puts every child there (2 of 300
380
+ * exact before). By bisection on the parent's x scale at three child
381
+ * offsets and three rotations the edge is the distance, not the scale:
382
+ * 0.00001 exactly solves and the double below folds (`CW07`). The child at
383
+ * the parent's origin (`CC04`) is the same fold.
384
+ *
385
+ * ## transform
386
+ *
387
+ * 4.3's form: `properties` maps each source property (`rotate`, `x`, `y`,
388
+ * `scaleX`, `scaleY`, `shearY`) with its `offset` to one or more target
389
+ * properties, each with `offset`, `scale` and `max`. For each constrained
390
+ * bone, each source property in the document's order, each target property:
391
+ *
392
+ * - **The source's value.** World (`localSource` false): `rotate` is the x
393
+ * column's angle, plus the constraint's `rotation` — negated when the source
394
+ * reflects — and then brought into [0, 360) by adding 360 once when below 0
395
+ * (a scale of 0.5 read 95 for a source at −170); `x`, `y` are the source's
396
+ * world transform applied to the constraint's `x`, `y`; `scaleX`, `scaleY`
397
+ * the column lengths plus the constraint's offsets; `shearY` the angle
398
+ * between the columns less 90, plus `shearY`. Local: the source's local
399
+ * value (as constraints so far left it) plus the offset, unwrapped.
400
+ * - Less the source property's `offset`, times the target property's `scale`
401
+ * (absent 1), plus its `offset` (absent 0); with `clamp`, held between its
402
+ * `offset` and its `max` whichever is larger — `max` absent reads 1 (a
403
+ * clamped 30 read 1).
404
+ * - **The mix** of the target property: `mixRotate`, `mixX`, `mixScaleX`,
405
+ * `mixShearY` absent read 1 for a driven property; `mixY` absent reads the
406
+ * resolved `mixX` when `x` is driven and 0 when it is not, `mixScaleY` the
407
+ * same over `scaleX` (the parser's reading, `src/keyorder.ts`'s note on the
408
+ * transform constraint, measured again here). A mix of 0 applies nothing.
409
+ * - **World** (`localTarget` false): `rotate` turns both columns by the
410
+ * change to the value (or by the value, `additive`) brought once into
411
+ * (−180, 180] and times the mix; `x`, `y` move the world origin; `scaleX`,
412
+ * `scaleY` scale a column to the value (or by it, additive: `1 + (v−1)·mix`);
413
+ * `shearY` turns the y column to `v + 90` degrees from the x column, the
414
+ * change brought once into (−π, π] with the RUNTIME's π (78 of 300 missed
415
+ * with Math.PI), keeping its length — additive, it turns the y column by
416
+ * `((v + 90)·RAD − 90·RAD)·mix`, unwrapped (issue #966, `CW04`: `v·RAD`
417
+ * agrees on the grid and read 35 of 132 probes last-bit off). **Local**: the local field moves
418
+ * toward the value by the mix (additive: adds `v·mix`, or scales by
419
+ * `1 + (v−1)·mix`).
420
+ *
421
+ * Measured: 800 probes per property in world mode over reflecting, sheared,
422
+ * offset, scaled and clamped sources and targets; 300 per property and mode
423
+ * pair for local, additive and both; 1,000 with random cross-mappings (a
424
+ * property to several, several to one); the six identity mappings in both
425
+ * key orders — all exact, amplified (`CC07`). Every one on bones of all five
426
+ * inherit modes.
427
+ *
428
+ * ## The timelines
429
+ *
430
+ * An `ik` timeline keys `mix` and `softness` (two curve channels) and
431
+ * `bendPositive`, `compress`, `stretch` (stepped: the flags of the last key
432
+ * at or before `t`); a `transform` timeline keys the six mixes (six
433
+ * channels). The key search and the curves are construct 4's
434
+ * (`keyIndexAt`, `channelAt` in `./animation.ts`), key times and values
435
+ * float32, a Bézier from the stated numbers. Before the first key the
436
+ * constraint's own values; a key omitting a field reads the parser's value
437
+ * for it — `mix` 1, `softness` 0, `bendPositive` true, `compress` and
438
+ * `stretch` false; a transform key's mixes 1, `mixY` the key's `mixX`
439
+ * (`src/keyorder.ts`, `PARSER_DEFAULTS`). `CC08` holds each. The keyed values
440
+ * reach the pose through the setup blend at alpha 1, `setup + (value −
441
+ * setup)·1`, for both kinds (issue #966: the ik's mix on the corpus, the
442
+ * transform's six mixes on `CW05`'s probe, where the value as keyed read 0
443
+ * of 60 exact).
444
+ *
445
+ * A `path` timeline's values — `position`, `spacing` and the three mixes
446
+ * (`./constraints_path.ts`, *The record and its timelines*) — reach the pose
447
+ * through the same setup blend (issue #984). The reading before was the
448
+ * value as keyed; the corpus could not tell them apart, since its one row
449
+ * keying a path timeline keys `position` over a setup position of 0, where
450
+ * `0 + (value − 0)·1` is the value to the bit. Measured on `CY01`'s probes —
451
+ * 60 per timeline, keyed by Bézier segments over setup values that include 0
452
+ * and negatives, in every position, spacing and rotate mode, one to three
453
+ * bones, under `--raw` at 40 irrational samples, tolerance 0 — the value as
454
+ * keyed read position 33, spacing 40 and mix 1 of 60 bit-exact (0 of 60 with
455
+ * all three keyed), the blend 60 of 60 on each; each mix channel alone
456
+ * planted back to the value as keyed turns the mix population red.
457
+ *
458
+ * A `slider` timeline's `time` and `mix` go through the same setup blend
459
+ * (issue #991, `CZ03`; `./constraints_slider.ts`, *Its timelines*).
460
+ *
461
+ * ## Purity
462
+ *
463
+ * As the rest of the core: nothing from the Spine runtime package, nothing
464
+ * from `src/transform.ts`, no clock, no randomness, no I/O.
465
+ */
466
+ import type { ModelBone } from '../model.ts';
467
+ import { modeMatrix, noScaleDirection, RUNTIME_PI, worldTransforms, type CoreInheritMode, type CoreWorld } from './world.ts';
468
+ import { channelAt, keyIndexAt, type CoreCurve, type CoreKey } from './animation.ts';
469
+ import { activeBones, type CompiledDocument, type CoreConstraintKind } from './index.ts';
470
+ import { fillingSkins } from './skins.ts';
471
+ import { readPathTimelines, slotBonePlan, solvePath, type CorePathRecord, type CorePathTimelines, type SlotBoneEvent } from './constraints_path.ts';
472
+ import { physicsTimelineCount, readPhysicsTimelines, stepPhysics, type CorePhysicsRecord, type CorePhysicsTimeline, type PhysicsStepContext } from './constraints_physics.ts';
473
+ import { applySlider, posedSlider, sliderPhysicsTarget, readSliderTimelines, sliderBonesWhy, type CoreSliderRecord, type CoreSliderTimeline, type SliderApplication } from './constraints_slider.ts';
474
+
475
+ const DEG = 180 / RUNTIME_PI;
476
+ const RAD = RUNTIME_PI / 180;
477
+
478
+ /** The kinds this cut poses. */
479
+ export const ADMITTED_CONSTRAINT_KINDS: readonly CoreConstraintKind[] = ['ik', 'transform', 'path', 'physics', 'slider'];
480
+
481
+ /** A transform constraint's six properties, in the order its timeline's channels run. */
482
+ export const TRANSFORM_PROPERTIES = ['rotate', 'x', 'y', 'scaleX', 'scaleY', 'shearY'] as const;
483
+ export type TransformProperty = (typeof TRANSFORM_PROPERTIES)[number];
484
+ /** The mix field of each property, in the same order. */
485
+ export const TRANSFORM_MIXES: Record<TransformProperty, string> = { rotate: 'mixRotate', x: 'mixX', y: 'mixY', scaleX: 'mixScaleX', scaleY: 'mixScaleY', shearY: 'mixShearY' };
486
+ /** The constraint's offset field of each property. */
487
+ const TRANSFORM_OFFSETS: Record<TransformProperty, string> = { rotate: 'rotation', x: 'x', y: 'y', scaleX: 'scaleX', scaleY: 'scaleY', shearY: 'shearY' };
488
+ /** The three `scaleY` modes an ik constraint names. */
489
+ export const IK_SCALE_Y_MODES = ['none', 'uniform', 'volume'] as const;
490
+ export type IkScaleYMode = (typeof IK_SCALE_Y_MODES)[number];
491
+
492
+ /** The fields each admitted kind's record may carry after `kind`, `name`, `declaredIn` (`buildRigConstraint` in `src/compile.ts`). */
493
+ export const CONSTRAINT_FIELDS: Record<'ik' | 'transform', readonly string[]> = {
494
+ ik: ['bones', 'target', 'scaleY', 'mix', 'softness', 'bendPositive', 'compress', 'stretch', 'skin'],
495
+ transform: ['bones', 'source', 'properties', 'localSource', 'localTarget', 'additive', 'clamp', 'rotation', 'x', 'y', 'scaleX', 'scaleY', 'shearY', 'mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY', 'skin'],
496
+ };
497
+
498
+ /** The values an ik constraint's timeline keys, and a pose of them. */
499
+ export interface IkPose {
500
+ mix: number;
501
+ softness: number;
502
+ bendPositive: boolean;
503
+ compress: boolean;
504
+ stretch: boolean;
505
+ }
506
+
507
+ export interface CoreIkRecord extends IkPose {
508
+ kind: 'ik';
509
+ name: string;
510
+ bones: string[];
511
+ target: string;
512
+ scaleY: IkScaleYMode;
513
+ skin: boolean;
514
+ /** An applied skin's `ik` list names it — what applies a skin-required one (the header's rule); set per skin view by `underSkin` in `./index.ts`. */
515
+ listedBySkin: boolean;
516
+ }
517
+
518
+ export interface CoreTransformTo {
519
+ property: TransformProperty;
520
+ offset: number;
521
+ scale: number;
522
+ max: number;
523
+ }
524
+
525
+ export interface CoreTransformFrom {
526
+ property: TransformProperty;
527
+ offset: number;
528
+ to: CoreTransformTo[];
529
+ }
530
+
531
+ export interface CoreTransformRecord {
532
+ kind: 'transform';
533
+ name: string;
534
+ bones: string[];
535
+ source: string;
536
+ properties: CoreTransformFrom[];
537
+ localSource: boolean;
538
+ localTarget: boolean;
539
+ additive: boolean;
540
+ clamp: boolean;
541
+ /** The constraint's offsets: `rotation`, `x`, `y`, `scaleX`, `scaleY`, `shearY`, by the property they offset. */
542
+ offsets: Record<TransformProperty, number>;
543
+ /** The resolved mixes, by property — the header's reading of an absent one. */
544
+ mixes: Record<TransformProperty, number>;
545
+ skin: boolean;
546
+ /** An applied skin's `transform` list names it (the header's rule); set per skin view by `underSkin` in `./index.ts`. */
547
+ listedBySkin: boolean;
548
+ }
549
+
550
+ export type CoreConstraintRecord = CoreIkRecord | CoreTransformRecord | CorePathRecord | CorePhysicsRecord | CoreSliderRecord;
551
+
552
+ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
553
+
554
+ function unknownFields(record: Record<string, unknown>, known: readonly string[], where: string, problems: string[]): void {
555
+ for (const key of Object.keys(record)) if (!known.includes(key)) problems.push(`${where}: field "${key}" is not one this reader knows; it reads [${known.join(', ')}]`);
556
+ }
557
+
558
+ function num(raw: Record<string, unknown>, key: string, dflt: number, where: string, problems: string[]): number {
559
+ const v = raw[key];
560
+ if (v === undefined) return dflt;
561
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
562
+ problems.push(`${where}: ${key} is ${JSON.stringify(v)}, not a finite number`);
563
+ return dflt;
564
+ }
565
+ return v;
566
+ }
567
+
568
+ function flag(raw: Record<string, unknown>, key: string, dflt: boolean, where: string, problems: string[]): boolean {
569
+ const v = raw[key];
570
+ if (v === undefined) return dflt;
571
+ if (typeof v !== 'boolean') {
572
+ problems.push(`${where}: ${key} is ${JSON.stringify(v)}, not a boolean`);
573
+ return dflt;
574
+ }
575
+ return v;
576
+ }
577
+
578
+ function boneList(raw: Record<string, unknown>, bones: ReadonlySet<string>, where: string, problems: string[]): string[] {
579
+ if (!Array.isArray(raw.bones) || raw.bones.length === 0) {
580
+ problems.push(`${where}: bones is ${JSON.stringify(raw.bones)}, not a non-empty list of bone names`);
581
+ return [];
582
+ }
583
+ const out: string[] = [];
584
+ raw.bones.forEach((b, i) => {
585
+ if (typeof b !== 'string' || !bones.has(b)) problems.push(`${where}: bones[${i}] ${JSON.stringify(b)} is not a bone of this document`);
586
+ else out.push(b);
587
+ });
588
+ return out;
589
+ }
590
+
591
+ function boneRef(raw: Record<string, unknown>, key: string, bones: ReadonlySet<string>, where: string, problems: string[]): string {
592
+ const v = raw[key];
593
+ if (typeof v !== 'string' || !bones.has(v)) {
594
+ problems.push(`${where}: ${key} is ${JSON.stringify(v)}, not a bone of this document`);
595
+ return '';
596
+ }
597
+ return v;
598
+ }
599
+
600
+ /** The mix of `property` as the header reads an absent one. */
601
+ function resolvedMix(raw: Record<string, unknown>, property: TransformProperty, driven: ReadonlySet<TransformProperty>, where: string, problems: string[]): number {
602
+ const stated = raw[TRANSFORM_MIXES[property]];
603
+ if (stated !== undefined) return num(raw, TRANSFORM_MIXES[property], 0, where, problems);
604
+ if (property === 'y') return driven.has('x') ? resolvedMix(raw, 'x', driven, where, problems) : 0;
605
+ if (property === 'scaleY') return driven.has('scaleX') ? resolvedMix(raw, 'scaleX', driven, where, problems) : 0;
606
+ return driven.has(property) ? 1 : 0;
607
+ }
608
+
609
+ /**
610
+ * An ik or transform constraint's record, read field by field — every field
611
+ * the writer can write for its kind and no other, each of its type, every
612
+ * bone named a bone of the document — or `undefined` with the problems named.
613
+ * `bones` is the document's bone names; `parents` each bone's parent, for the
614
+ * two refusals the runtime would otherwise throw on while posing.
615
+ */
616
+ export function readConstraintRecord(raw: Record<string, unknown>, kind: 'ik' | 'transform', name: string, where: string, bones: ReadonlySet<string>, parents: ReadonlyMap<string, string | undefined>, problems: string[]): CoreConstraintRecord | undefined {
617
+ const before = problems.length;
618
+ const rest: Record<string, unknown> = { ...raw };
619
+ delete rest.kind;
620
+ delete rest.name;
621
+ delete rest.declaredIn;
622
+ unknownFields(rest, CONSTRAINT_FIELDS[kind], where, problems);
623
+ const list = boneList(raw, bones, where, problems);
624
+ const skin = flag(raw, 'skin', false, where, problems);
625
+ if (kind === 'ik') {
626
+ const target = boneRef(raw, 'target', bones, where, problems);
627
+ if (list.length > 2) problems.push(`${where}: an ik constraint names ${list.length} bones; the format takes one or two`);
628
+ if (list.length >= 1 && parents.get(list[0]) === undefined) problems.push(`${where}: its first bone "${list[0]}" is the root; spine-core throws posing an ik constraint on a bone with no parent`);
629
+ if (list.length === 2 && parents.get(list[1]) !== list[0]) problems.push(`${where}: "${list[1]}" is not a child of "${list[0]}"; a two-bone ik needs the second bone directly under the first`);
630
+ let scaleY: IkScaleYMode = 'none';
631
+ if (raw.scaleY !== undefined) {
632
+ const folded = typeof raw.scaleY === 'string' && raw.scaleY.length > 0 ? raw.scaleY[0].toLowerCase() + raw.scaleY.slice(1) : '';
633
+ const mode = IK_SCALE_Y_MODES.find((m) => m === folded);
634
+ if (mode === undefined) problems.push(`${where}: scaleY is ${JSON.stringify(raw.scaleY)}, none of ${IK_SCALE_Y_MODES.join(', ')}`);
635
+ else scaleY = mode;
636
+ }
637
+ const record: CoreIkRecord = {
638
+ kind, name, bones: list, target, scaleY, skin, listedBySkin: false,
639
+ mix: num(raw, 'mix', 1, where, problems),
640
+ softness: num(raw, 'softness', 0, where, problems),
641
+ bendPositive: flag(raw, 'bendPositive', true, where, problems),
642
+ compress: flag(raw, 'compress', false, where, problems),
643
+ stretch: flag(raw, 'stretch', false, where, problems),
644
+ };
645
+ return problems.length === before ? record : undefined;
646
+ }
647
+ const source = boneRef(raw, 'source', bones, where, problems);
648
+ const properties: CoreTransformFrom[] = [];
649
+ const driven = new Set<TransformProperty>();
650
+ if (raw.properties !== undefined && !isRecord(raw.properties)) problems.push(`${where}: properties is not an object`);
651
+ else if (isRecord(raw.properties)) {
652
+ for (const [fromName, fromRaw] of Object.entries(raw.properties)) {
653
+ const at = `${where}.properties.${fromName}`;
654
+ const from = TRANSFORM_PROPERTIES.find((p) => p === fromName);
655
+ if (from === undefined) {
656
+ problems.push(`${at}: "${fromName}" is none of ${TRANSFORM_PROPERTIES.join(', ')}`);
657
+ continue;
658
+ }
659
+ if (!isRecord(fromRaw)) {
660
+ problems.push(`${at} is not an object`);
661
+ continue;
662
+ }
663
+ unknownFields(fromRaw, ['offset', 'to'], at, problems);
664
+ const to: CoreTransformTo[] = [];
665
+ if (!isRecord(fromRaw.to)) problems.push(`${at}: to is not an object`);
666
+ else {
667
+ for (const [toName, toRaw] of Object.entries(fromRaw.to)) {
668
+ const at2 = `${at}.to.${toName}`;
669
+ const property = TRANSFORM_PROPERTIES.find((p) => p === toName);
670
+ if (property === undefined) {
671
+ problems.push(`${at2}: "${toName}" is none of ${TRANSFORM_PROPERTIES.join(', ')}`);
672
+ continue;
673
+ }
674
+ if (!isRecord(toRaw)) {
675
+ problems.push(`${at2} is not an object`);
676
+ continue;
677
+ }
678
+ unknownFields(toRaw, ['offset', 'scale', 'max'], at2, problems);
679
+ to.push({ property, offset: num(toRaw, 'offset', 0, at2, problems), scale: num(toRaw, 'scale', 1, at2, problems), max: num(toRaw, 'max', 1, at2, problems) });
680
+ driven.add(property);
681
+ }
682
+ }
683
+ properties.push({ property: from, offset: num(fromRaw, 'offset', 0, at, problems), to });
684
+ }
685
+ }
686
+ const offsets = {} as Record<TransformProperty, number>;
687
+ const mixes = {} as Record<TransformProperty, number>;
688
+ for (const p of TRANSFORM_PROPERTIES) {
689
+ offsets[p] = num(raw, TRANSFORM_OFFSETS[p], 0, where, problems);
690
+ mixes[p] = resolvedMix(raw, p, driven, where, problems);
691
+ }
692
+ const record: CoreTransformRecord = {
693
+ kind, name, bones: list, source, properties, skin, listedBySkin: false, offsets, mixes,
694
+ localSource: flag(raw, 'localSource', false, where, problems),
695
+ localTarget: flag(raw, 'localTarget', false, where, problems),
696
+ additive: flag(raw, 'additive', false, where, problems),
697
+ clamp: flag(raw, 'clamp', false, where, problems),
698
+ };
699
+ return problems.length === before ? record : undefined;
700
+ }
701
+
702
+ // ---------------------------------------------------------------------------
703
+ // timelines
704
+ // ---------------------------------------------------------------------------
705
+
706
+ /** An ik key: `mix`, `softness` as channels, the three flags as the key states them or the parser reads them. */
707
+ export interface CoreIkKey extends CoreKey {
708
+ flags: { bendPositive: boolean; compress: boolean; stretch: boolean };
709
+ }
710
+
711
+ /** One animation's ik, transform and path timelines, by constraint name, in the animation's order. */
712
+ export interface CoreConstraintTimelines {
713
+ ik: Array<{ name: string; keys: CoreIkKey[] }>;
714
+ transform: Array<{ name: string; keys: CoreKey[] }>;
715
+ /** The path constraints' timelines (`./constraints_path.ts`). */
716
+ path: CorePathTimelines[];
717
+ /** How many physics timelines the animation holds — they pose nothing under `Physics.none` (`./constraints_physics.ts`). */
718
+ physics: number;
719
+ /** The physics timelines themselves, in the animation's order — what the stepped phase poses (`./constraints_physics.ts`). */
720
+ physicsKeyed?: CorePhysicsTimeline[];
721
+ /** The slider timelines (`./constraints_slider.ts`). */
722
+ slider: CoreSliderTimeline[];
723
+ }
724
+
725
+ /** The ik key's curve channels, and the transform key's, in the order a curve indexes them. */
726
+ export const IK_KEY_CHANNELS = ['mix', 'softness'] as const;
727
+ export const TRANSFORM_KEY_CHANNELS = ['mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY'] as const;
728
+
729
+ function keyCurve(raw: Record<string, unknown>, channels: number, last: boolean, at: string, problems: string[]): CoreCurve {
730
+ const curve = raw.curve;
731
+ if (curve === undefined) return 'linear';
732
+ if (last) {
733
+ problems.push(`${at}: the last key carries a curve, which eases to no key — the writer refuses it`);
734
+ return 'linear';
735
+ }
736
+ if (curve === 'stepped') return 'stepped';
737
+ if (!Array.isArray(curve) || curve.length !== channels * 4 || !curve.every((n) => typeof n === 'number' && Number.isFinite(n))) {
738
+ problems.push(`${at}: curve is ${JSON.stringify(curve)}, not "stepped" nor ${channels * 4} finite numbers (four per channel)`);
739
+ return 'linear';
740
+ }
741
+ return curve as number[];
742
+ }
743
+
744
+ /**
745
+ * The ik, transform and path timelines of one animation record's
746
+ * `constraints`, read: each names a declared constraint of its kind, its
747
+ * keys strictly increase in time, each key carries only the fields the
748
+ * parser reads for its kind (the path's, `readPathTimelines` in
749
+ * `./constraints_path.ts`); the physics timelines are counted (they pose
750
+ * nothing under `Physics.none`) and the slider timelines read by
751
+ * `./constraints_slider.ts`.
752
+ */
753
+ export function readConstraintTimelines(value: unknown, label: string, declared: ReadonlyArray<{ kind: CoreConstraintKind; name: string }>, problems: string[]): CoreConstraintTimelines {
754
+ const out: CoreConstraintTimelines = { ik: [], transform: [], path: [], physics: 0, slider: [] };
755
+ if (!isRecord(value)) return out;
756
+ out.path = readPathTimelines(value.path, label, new Set(declared.filter((c) => c.kind === 'path').map((c) => c.name)), problems);
757
+ out.physics = physicsTimelineCount(value.physics);
758
+ out.physicsKeyed = readPhysicsTimelines(value.physics, label, new Set(declared.filter((c) => c.kind === 'physics').map((c) => c.name)), problems);
759
+ out.slider = readSliderTimelines(value.slider, label, declared, problems);
760
+ for (const kind of ['ik', 'transform'] as const) {
761
+ const list = value[kind];
762
+ if (!Array.isArray(list)) continue;
763
+ list.forEach((entry, i) => {
764
+ const at = `${label}.constraints.${kind}[${i}]`;
765
+ if (!isRecord(entry) || typeof entry.name !== 'string') {
766
+ problems.push(`${at} names no constraint`);
767
+ return;
768
+ }
769
+ if (!declared.some((c) => c.kind === kind && c.name === entry.name)) problems.push(`${at}: "${entry.name}" is not a ${kind} constraint of this document`);
770
+ const rawKeys: unknown = entry.keys;
771
+ if (!Array.isArray(rawKeys) || rawKeys.length === 0) {
772
+ problems.push(`${at}: keys is not a non-empty list`);
773
+ return;
774
+ }
775
+ const channels: readonly string[] = kind === 'ik' ? IK_KEY_CHANNELS : TRANSFORM_KEY_CHANNELS;
776
+ const known = kind === 'ik' ? ['time', ...IK_KEY_CHANNELS, 'bendPositive', 'compress', 'stretch', 'curve'] : ['time', ...TRANSFORM_KEY_CHANNELS, 'curve'];
777
+ let last = -Infinity;
778
+ const keys: CoreIkKey[] = [];
779
+ rawKeys.forEach((k: unknown, j: number) => {
780
+ const kat = `${at}.keys[${j}]`;
781
+ if (!isRecord(k)) {
782
+ problems.push(`${kat} is not an object`);
783
+ return;
784
+ }
785
+ unknownFields(k, known, kat, problems);
786
+ const time = k.time;
787
+ if (typeof time !== 'number' || !Number.isFinite(time) || time < 0) {
788
+ problems.push(`${kat}: time is ${JSON.stringify(time)}, not a finite time at or after 0`);
789
+ return;
790
+ }
791
+ 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`);
792
+ last = Math.max(last, time);
793
+ // The parser's reading of an absent field (`PARSER_DEFAULTS` in src/keyorder.ts): an ik key's mix 1, softness 0; a transform key's mixes 1 and its mixY its own mixX.
794
+ const stated = channels.map((c) => {
795
+ if (kind === 'transform' && c === 'mixY' && k.mixY === undefined) return num(k, 'mixX', 1, kat, problems);
796
+ return num(k, c, c === 'softness' ? 0 : 1, kat, problems);
797
+ });
798
+ keys.push({
799
+ time: Math.fround(time),
800
+ values: stated.map(Math.fround),
801
+ stated: { time, values: stated },
802
+ curve: keyCurve(k, channels.length, j === rawKeys.length - 1, kat, problems),
803
+ flags: { bendPositive: flag(k, 'bendPositive', true, kat, problems), compress: flag(k, 'compress', false, kat, problems), stretch: flag(k, 'stretch', false, kat, problems) },
804
+ });
805
+ });
806
+ if (kind === 'ik') out.ik.push({ name: entry.name, keys });
807
+ else out.transform.push({ name: entry.name, keys });
808
+ });
809
+ }
810
+ return out;
811
+ }
812
+
813
+ /** What evaluates one channel of a constraint key, and finds the key — construct 4's unless a plant passes others. */
814
+ export interface ConstraintTimelinePlant {
815
+ channel?: (keys: readonly CoreKey[], index: number, channel: number, t: number) => number;
816
+ search?: (keys: readonly CoreKey[], t: number) => number;
817
+ }
818
+
819
+ /** Every admitted constraint record with its timeline's values at `t` in place of its own (the header's *The timelines*). */
820
+ export function posedRecords(records: readonly CoreConstraintRecord[], timelines: CoreConstraintTimelines | null, t: number, plant: ConstraintTimelinePlant = {}): CoreConstraintRecord[] {
821
+ const search = plant.search ?? keyIndexAt;
822
+ const channel = plant.channel ?? channelAt;
823
+ return records.map((r): CoreConstraintRecord => {
824
+ if (timelines === null || r.kind === 'physics') return r;
825
+ if (r.kind === 'slider') return posedSlider(r, timelines.slider, t, plant);
826
+ if (r.kind === 'path') {
827
+ const tl = timelines.path.find((x) => x.name === r.name);
828
+ if (tl === undefined) return r;
829
+ const out = { ...r };
830
+ for (const [kind, fields] of [['position', ['position']], ['spacing', ['spacing']], ['mix', ['mixRotate', 'mixX', 'mixY']]] as const) {
831
+ const keys = tl[kind];
832
+ if (keys === undefined) continue;
833
+ const i = search(keys, t);
834
+ if (i < 0) continue;
835
+ // Through the setup blend at alpha 1 as well (issue #984): the value as keyed read position 33, spacing 40 and mix 1 of 60 Bézier-keyed probes bit-exact at 40 irrational samples, the blend 60 of each (CY01).
836
+ fields.forEach((f, c) => (out[f] = r[f] + (channel(keys, i, c, t) - r[f]) * 1));
837
+ }
838
+ return out;
839
+ }
840
+ if (r.kind === 'ik') {
841
+ const tl = timelines.ik.find((x) => x.name === r.name);
842
+ if (tl === undefined) return r;
843
+ const i = search(tl.keys, t);
844
+ if (i < 0) return r;
845
+ // The keyed values reach the pose through the setup blend at alpha 1, `setup + (value − setup)·1` (issue #966): the value as keyed agrees on the grid and reads off in the last bit — a Bézier-eased mix of 0.3899 on `spineboy-pro`'s jump moved its two-bone ik 2 ulp.
846
+ return { ...r, mix: r.mix + (channel(tl.keys, i, 0, t) - r.mix) * 1, softness: r.softness + (channel(tl.keys, i, 1, t) - r.softness) * 1, ...tl.keys[i].flags };
847
+ }
848
+ const tl = timelines.transform.find((x) => x.name === r.name);
849
+ if (tl === undefined) return r;
850
+ const i = search(tl.keys, t);
851
+ if (i < 0) return r;
852
+ const mixes = {} as Record<TransformProperty, number>;
853
+ // Through the setup blend at alpha 1 too (issue #966): on 60 probes keying all six mixes by Bézier segments over setup mixes in [−1, 2], sampled at 40 irrational times, the value as keyed read 0 bit-exact and the blend 60 (CW05).
854
+ TRANSFORM_PROPERTIES.forEach((p, c) => (mixes[p] = r.mixes[p] + (channel(tl.keys, i, c, t) - r.mixes[p]) * 1));
855
+ return { ...r, mixes };
856
+ });
857
+ }
858
+
859
+ // ---------------------------------------------------------------------------
860
+ // solving
861
+ // ---------------------------------------------------------------------------
862
+
863
+ /** A change brought once into (−180, 180] — the header's measured wrap (−180 goes to 180). */
864
+ function wrap180(r: number): number {
865
+ return r > 180 ? r - 360 : r <= -180 ? r + 360 : r;
866
+ }
867
+
868
+ const IDENTITY: CoreWorld = { a: 1, b: 0, c: 0, d: 1, worldX: 0, worldY: 0 };
869
+
870
+ function modeOf(bone: ModelBone): CoreInheritMode {
871
+ const m = bone.inheritMode;
872
+ if (m === undefined || m.length === 0) return 'normal';
873
+ const folded = m[0].toLowerCase() + m.slice(1);
874
+ return folded === 'onlyTranslation' || folded === 'noRotationOrReflection' || folded === 'noScale' || folded === 'noScaleOrReflection' ? folded : 'normal';
875
+ }
876
+
877
+ /**
878
+ * The rules issue #979 measured on collapsed frames and on bones the posed
879
+ * skin leaves unposed (the header's *Unposed bones and collapsed frames*),
880
+ * as one object so that a control can plant each one back to the reading
881
+ * before it (`CorePlant.solver`); nothing else passes another.
882
+ */
883
+ export interface SolverRules {
884
+ /** An ik frame whose determinant is at most this in magnitude is collapsed: a one-bone ik reads the target at the frame's origin, a two-bone ik reads its grandparent's inverse as zero. */
885
+ ikCollapsedDet: number;
886
+ /** A one-bone ik under a `noRotationOrReflection` parent: the floor under the parent x axis's squared length. */
887
+ ikXAxisFloor: number;
888
+ /** `localFromWorld`: a local x column that is not above 0.0001 — NaN included — reads as collapsed, and a y column no longer than 0.00001 reads rotation 0 (x collapsed too) or shearY 0. */
889
+ readBackCollapsed: boolean;
890
+ /** A world-space `rotate` source negates the constraint's `rotation` unless its determinant is above 0 — at 0 too. */
891
+ offsetNegatedAtZeroDet: boolean;
892
+ /** An inactive bone is never posed again, and the bones below it are posed again only when a constraint moved it. */
893
+ inactiveHoldsItsWorld: boolean;
894
+ /** A transform constraint is applied when its source is active, to every bone it names. */
895
+ transformIgnoresBoneActivity: boolean;
896
+ /** A pose in which a posed bone reads a bone whose value depends on the runtime's previous pass is refused by name (`unposedLeakWhy`). */
897
+ refuseHistoryLeak: boolean;
898
+ /** An ik naming an inactive bone is applied when its target is active, reading the frame above its first bone as a fresh skeleton holds it: zeros unless the pass brought that bone up to date before the ik (`frameUpdatedBefore`). */
899
+ ikOverInactiveFresh: boolean;
900
+ /** `localFromWorld`, a reflected local matrix: `shearY` is the y angle less `rotation − 90` (issue #966), not the y angle turned by 180 less `rotation + 90`. */
901
+ readBackReflectedShear: boolean;
902
+ /** `localFromWorld` in `noRotationOrReflection`: the conformal frame's inverse applied as the parent x axis over its squared length and its y axis over |det| (issue #966), not divided by the conformal determinant. */
903
+ readBackConformalInverse: boolean;
904
+ /** `localFromWorld` in the two `noScale` modes: the world columns read in the frame the forward pose builds from the rotation just read, the x column's residual angle kept as `shearX` (issue #966), not the x column's length with `shearX` 0. */
905
+ readBackNoScaleFrame: boolean;
906
+ /** An additive world-space `shearY`: `(v + 90)·RAD − 90·RAD` (issue #966), not `v·RAD`. */
907
+ additiveShearRightAngle: boolean;
908
+ /** A two-bone ik: a parent scale of 0 counts as positive in the product of the scale signs (issue #966), not as 0. */
909
+ ikZeroScaleSignPositive: boolean;
910
+ /** A one-bone ik compresses or stretches only a bone whose length times scaleX is above this (issue #966; the reading before, 0.0001, left bones between the two unscaled). */
911
+ ikStretchMinLength: number;
912
+ /** A two-bone ik whose child's origin is nearer the parent's than this, in the grandparent frame, is a one-bone ik on the parent with the child at rotation 0 (issue #966); negative, never. */
913
+ ikTwoBoneNearChild: number;
914
+ /** A slider poses again every bone its animation keys, whether or not a timeline wrote it — before its first key too (issue #989) — not only the bones a timeline wrote. */
915
+ sliderReposesKeyedBones: boolean;
916
+ /** A slider's additive scale key: `current + (v·setup − setup)·mix` (issue #989), not `current + (v − 1)·setup·mix`. */
917
+ sliderAdditiveScaleProduct: boolean;
918
+ /** A slider's scale key at mix exactly 1 writes `setup·v` itself (issue #989), not `from + (setup·v − from)·1`. */
919
+ sliderScaleMixOneIsTarget: boolean;
920
+ /**
921
+ * Issue #1049, each the rule `./constraints_slider.ts` *Physics timelines* states — `false` (or the named reading)
922
+ * plants the reading it rejected: a slider's physics keys write the pass's physics records (`false`: write nothing);
923
+ * a non-additive one blends from the CURRENT value (`false`: from the setup value); only `wind` and `gravity` add
924
+ * (`false`: every value kind adds); a `mass` key blends the mass (`false`: the inverse); the write lasts one pass
925
+ * (`false`: what a slider wrote stands on the next step wherever the step's own animation does not key it); a
926
+ * `reset` key fires nothing (`false`: a key at or before the slider's time resets its constraints on every pass).
927
+ */
928
+ sliderWritesPhysics: boolean;
929
+ sliderPhysicsFromCurrent: boolean;
930
+ sliderPhysicsAddsWindGravityOnly: boolean;
931
+ sliderPhysicsBlendsMass: boolean;
932
+ sliderPhysicsLastsOnePass: boolean;
933
+ sliderPhysicsResetIsDead: boolean;
934
+ }
935
+
936
+ /** The runtime's rules, as measured. */
937
+ export const RUNTIME_SOLVER_RULES: Readonly<SolverRules> = {
938
+ ikCollapsedDet: 0.00001,
939
+ ikXAxisFloor: 0.00001,
940
+ readBackCollapsed: true,
941
+ offsetNegatedAtZeroDet: true,
942
+ inactiveHoldsItsWorld: true,
943
+ transformIgnoresBoneActivity: true,
944
+ ikOverInactiveFresh: true,
945
+ refuseHistoryLeak: true,
946
+ readBackReflectedShear: true,
947
+ readBackConformalInverse: true,
948
+ readBackNoScaleFrame: true,
949
+ additiveShearRightAngle: true,
950
+ ikZeroScaleSignPositive: true,
951
+ ikStretchMinLength: 0.00001,
952
+ ikTwoBoneNearChild: 0.00001,
953
+ sliderReposesKeyedBones: true,
954
+ sliderAdditiveScaleProduct: true,
955
+ sliderScaleMixOneIsTarget: true,
956
+ sliderWritesPhysics: true,
957
+ sliderPhysicsFromCurrent: true,
958
+ sliderPhysicsAddsWindGravityOnly: true,
959
+ sliderPhysicsBlendsMass: true,
960
+ sliderPhysicsLastsOnePass: true,
961
+ sliderPhysicsResetIsDead: true,
962
+ };
963
+
964
+ /** The runtime's rules with a plant's over them. */
965
+ export function solverRules(plant: Partial<SolverRules> | undefined): Readonly<SolverRules> {
966
+ return plant === undefined ? RUNTIME_SOLVER_RULES : { ...RUNTIME_SOLVER_RULES, ...plant };
967
+ }
968
+
969
+ /** The runtime's `scaleY` for a bone an ik scaled by `s` along its length (the header's `volume` measurement). */
970
+ function volumeScaleY(scaleY: number, s: number): number {
971
+ return scaleY / (s < 0.7 ? 0.25 + s * 0.642857 : s);
972
+ }
973
+
974
+ /** The state the solvers work on: every bone's local values, its world transform, and which bones are active. */
975
+ export interface SolverState {
976
+ bones: ModelBone[];
977
+ index: Map<string, number>;
978
+ world: Map<string, CoreWorld>;
979
+ active: ReadonlySet<string>;
980
+ /** `RUNTIME_SOLVER_RULES` unless a plant passes others. */
981
+ rules: Readonly<SolverRules>;
982
+ }
983
+
984
+ function bone(state: SolverState, name: string): ModelBone {
985
+ return state.bones[state.index.get(name) as number];
986
+ }
987
+
988
+ function parentWorld(state: SolverState, b: ModelBone): CoreWorld {
989
+ return b.parent === undefined ? IDENTITY : (state.world.get(b.parent) as CoreWorld);
990
+ }
991
+
992
+ /** One bone's world transform from its local values under its parent's current world transform — `./world.ts`'s arithmetic. */
993
+ function poseBone(state: SolverState, b: ModelBone): void {
994
+ // An inactive bone is not posed again: it keeps its zeros, or what a constraint wrote into them (issue #979).
995
+ if (!state.active.has(b.name)) {
996
+ if (!state.rules.inactiveHoldsItsWorld) state.world.set(b.name, { a: 0, b: 0, c: 0, d: 0, worldX: 0, worldY: 0 });
997
+ return;
998
+ }
999
+ if (b.parent === undefined) {
1000
+ state.world.set(b.name, worldTransforms([b]).get(b.name) as CoreWorld);
1001
+ return;
1002
+ }
1003
+ const p = state.world.get(b.parent) as CoreWorld;
1004
+ const [a, bb, c, d] = modeMatrix(modeOf(b), p, b);
1005
+ const x = b.x ?? 0;
1006
+ const y = b.y ?? 0;
1007
+ state.world.set(b.name, { a, b: bb, c, d, worldX: p.a * x + p.b * y + p.worldX, worldY: p.c * x + p.d * y + p.worldY });
1008
+ }
1009
+
1010
+ /** The bone's local values read back from its world transform — the header's `localFromWorld`. */
1011
+ export function localFromWorld(b: ModelBone, p: CoreWorld, w: CoreWorld, rules: Readonly<SolverRules> = RUNTIME_SOLVER_RULES): void {
1012
+ const mode = b.parent === undefined ? 'normal' : modeOf(b);
1013
+ const pid = 1 / (p.a * p.d - p.b * p.c);
1014
+ const dx = w.worldX - p.worldX;
1015
+ const dy = w.worldY - p.worldY;
1016
+ // The inverse's entries are formed first, then applied (issue #966): `dx·d·pid` reads last-bit off where `dx·(d·pid)` does not.
1017
+ b.x = dx * (p.d * pid) - dy * (p.b * pid);
1018
+ b.y = dy * (p.a * pid) - dx * (p.c * pid);
1019
+ b.shearX = 0;
1020
+ if (mode === 'noScale' || mode === 'noScaleOrReflection') {
1021
+ const det = p.a * p.d - p.b * p.c;
1022
+ const flip = mode === 'noScale' && det < 0 ? -1 : 1;
1023
+ if (!rules.readBackNoScaleFrame) {
1024
+ // The reading before issue #966's second cut, kept for the control that plants it back (CW03).
1025
+ const length = Math.sqrt(w.a * w.a + w.c * w.c);
1026
+ const [ux, uy] = [w.a / length, w.c / length];
1027
+ decompose(b, length, 0, ux * w.b + uy * w.d, (ux * w.d - uy * w.b) * flip, rules);
1028
+ b.rotation = Math.atan2((uy * p.a - ux * p.c) / det, (ux * p.d - uy * p.b) / det) * DEG;
1029
+ return;
1030
+ }
1031
+ // The rotation first: the angle of the parent's adjugate applied to the world x column, both parts negated when the parent reflects (issue #966).
1032
+ const sign = det < 0 ? -1 : 1;
1033
+ const rotation = Math.atan2((p.a * w.c - p.c * w.a) * sign, (p.d * w.a - p.b * w.c) * sign) * DEG;
1034
+ // Then the frame the forward pose builds from that rotation, and the world columns read in it — its transpose, flipped for noScale under a reflecting parent.
1035
+ const [ux, uy] = noScaleDirection([p.a, p.b, p.c, p.d], rotation);
1036
+ decompose(b, ux * w.a + uy * w.c, (ux * w.c - uy * w.a) * flip, ux * w.b + uy * w.d, (ux * w.d - uy * w.b) * flip, rules, 'shearX');
1037
+ b.rotation = rotation;
1038
+ return;
1039
+ }
1040
+ let la: number;
1041
+ let lb: number;
1042
+ let lc: number;
1043
+ let ld: number;
1044
+ if (mode === 'onlyTranslation') {
1045
+ [la, lb, lc, ld] = [w.a, w.b, w.c, w.d];
1046
+ } else if (mode === 'noRotationOrReflection') {
1047
+ if (rules.readBackConformalInverse) {
1048
+ // The conformal frame's inverse as its axes over their lengths (issue #966): the x axis over its squared length, the y axis over |det|, each reciprocal multiplied in.
1049
+ const xLengthInverse = 1 / (p.a * p.a + p.c * p.c);
1050
+ const detInverse = 1 / Math.abs(p.a * p.d - p.b * p.c);
1051
+ [la, lb, lc, ld] = [(p.a * w.a + p.c * w.c) * xLengthInverse, (p.a * w.b + p.c * w.d) * xLengthInverse, (p.a * w.c - p.c * w.a) * detInverse, (p.a * w.d - p.c * w.b) * detInverse];
1052
+ } else {
1053
+ // The reading before issue #966's second cut, kept for the control that plants it back (CW02).
1054
+ const k = Math.abs(p.a * p.d - p.b * p.c) / (p.a * p.a + p.c * p.c);
1055
+ const [ca, cb, cc, cd] = [p.a, -p.c * k, p.c, p.a * k];
1056
+ const cdet = ca * cd - cb * cc;
1057
+ [la, lb, lc, ld] = [(cd * w.a - cb * w.c) / cdet, (cd * w.b - cb * w.d) / cdet, (ca * w.c - cc * w.a) / cdet, (ca * w.d - cc * w.b) / cdet];
1058
+ }
1059
+ decompose(b, la, lc, lb, ld, rules);
1060
+ b.rotation = (b.rotation ?? 0) + Math.atan2(p.c, p.a) * DEG;
1061
+ return;
1062
+ } else {
1063
+ const [ia, ib, ic, id] = [p.d * pid, p.b * pid, p.c * pid, p.a * pid];
1064
+ [la, lb, lc, ld] = [ia * w.a - ib * w.c, ia * w.b - ib * w.d, id * w.c - ic * w.a, id * w.d - ic * w.b];
1065
+ }
1066
+ decompose(b, la, lc, lb, ld, rules);
1067
+ }
1068
+
1069
+ /**
1070
+ * A local matrix `[la lb; lc ld]` (columns `(la, lc)` and `(lb, ld)`) as rotation, scales and shears — the header's rule.
1071
+ * `xAngleIn` says which field takes the x column's angle: `rotation` (every mode but the two `noScale` ones), or `shearX`
1072
+ * — the two `noScale` modes, whose rotation is read first (issue #966: the runtime keeps the residual angle of the x
1073
+ * column, in the frame built from that rotation, as `shearX`, and measures `shearY` from 0, as the forward frame
1074
+ * `frame(0, shearX, shearY, …)` reads them).
1075
+ */
1076
+ function decompose(b: ModelBone, la: number, lc: number, lb: number, ld: number, rules: Readonly<SolverRules>, xAngleIn: 'rotation' | 'shearX' = 'rotation'): void {
1077
+ const sx = Math.sqrt(la * la + lc * lc);
1078
+ const yLength = Math.sqrt(lb * lb + ld * ld);
1079
+ // Not above the bound, NaN included (issue #979: a collapsed parent's inverse is NaN, and the runtime reads scaleX 0 and shearY 0 from it); a y column no longer than 0.00001 reads rotation 0.
1080
+ if (rules.readBackCollapsed ? !(sx > 0.0001) : sx <= 0.0001) {
1081
+ b.scaleX = 0;
1082
+ b.scaleY = yLength;
1083
+ b.shearY = 0;
1084
+ b.rotation = !rules.readBackCollapsed || yLength > 0.00001 ? Math.atan2(ld, lb) * DEG - 90 : 0;
1085
+ return;
1086
+ }
1087
+ const xAngle = Math.atan2(lc, la) * DEG;
1088
+ const det = la * ld - lb * lc;
1089
+ let yAngle = Math.atan2(ld, lb) * DEG;
1090
+ if (xAngleIn === 'rotation') b.rotation = xAngle;
1091
+ else b.shearX = xAngle;
1092
+ b.scaleX = sx;
1093
+ b.scaleY = det < 0 ? -yLength : yLength;
1094
+ // The inverse of the forward frame's `rotation + 90 + shearY` (issue #966): the y angle less `rotation + 90`, or, when the determinant is negative (the
1095
+ // reflection carried by the negative scaleY), less `rotation − 90` — the reading before turned the y angle by 180 first and then took `rotation + 90` off.
1096
+ const from = xAngleIn === 'rotation' ? xAngle : 0;
1097
+ let right = 90;
1098
+ if (det < 0) {
1099
+ if (rules.readBackReflectedShear) right = -90;
1100
+ else yAngle += 180;
1101
+ }
1102
+ let shear = yAngle - (from + right);
1103
+ shear = shear > 180 ? shear - 360 : shear < -180 ? shear + 360 : shear;
1104
+ // A y column no longer than 0.00001 reads shearY 0 (issue #979), as it reads rotation 0 when the x column is collapsed too.
1105
+ b.shearY = !rules.readBackCollapsed || yLength > 0.00001 ? shear : 0;
1106
+ }
1107
+
1108
+ /** A one-bone ik (the header's *ik — one bone*): returns the bone changed, or none. */
1109
+ function solveOne(state: SolverState, c: CoreIkRecord, frame: CoreWorld | null = null): string[] {
1110
+ const b = bone(state, c.bones[0]);
1111
+ const p = frame ?? parentWorld(state, b);
1112
+ const bw = state.world.get(b.name) as CoreWorld;
1113
+ const t = state.world.get(c.target) as CoreWorld;
1114
+ const mode = modeOf(b);
1115
+ let [pa, pb, pc, pd] = [p.a, p.b, p.c, p.d];
1116
+ let turn = 0;
1117
+ let lx: number;
1118
+ let ly: number;
1119
+ if (mode === 'noRotationOrReflection') {
1120
+ // The x axis's squared length is floored here (issue #979, `ikXAxisFloor`) — not in `./world.ts`'s frame for the same mode.
1121
+ const k = Math.abs(pa * pd - pb * pc) / Math.max(state.rules.ikXAxisFloor, pa * pa + pc * pc);
1122
+ pb = -pc * k;
1123
+ pd = pa * k;
1124
+ turn = Math.atan2(pc, pa) * DEG;
1125
+ const det = pa * pd - pb * pc;
1126
+ const x = t.worldX - p.worldX;
1127
+ const y = t.worldY - p.worldY;
1128
+ [lx, ly] = Math.abs(det) <= state.rules.ikCollapsedDet ? [0, 0] : [(x * pd - y * pb) / det - (b.x ?? 0), (y * pa - x * pc) / det - (b.y ?? 0)];
1129
+ } else {
1130
+ if (mode === 'onlyTranslation') {
1131
+ lx = t.worldX - bw.worldX;
1132
+ ly = t.worldY - bw.worldY;
1133
+ } else {
1134
+ const det = pa * pd - pb * pc;
1135
+ const x = t.worldX - p.worldX;
1136
+ const y = t.worldY - p.worldY;
1137
+ [lx, ly] = Math.abs(det) <= state.rules.ikCollapsedDet ? [0, 0] : [(x * pd - y * pb) / det - (b.x ?? 0), (y * pa - x * pc) / det - (b.y ?? 0)];
1138
+ }
1139
+ }
1140
+ const rotation = b.rotation ?? 0;
1141
+ const scaleX = b.scaleX ?? 1;
1142
+ // Accumulated from the bone's own angles first and the target's direction last (issue #966): `atan2·DEG − shearX − rotation + turn` agrees on the grid and reads last-bit off on 53 of 600 random one-bone iks, this order on none.
1143
+ let r = -(b.shearX ?? 0) - rotation + turn + Math.atan2(ly, lx) * DEG;
1144
+ if (scaleX < 0) r += 180;
1145
+ b.rotation = rotation + wrap180(r) * c.mix;
1146
+ const len = (b.length ?? 0) * scaleX;
1147
+ if ((c.compress || c.stretch) && len > state.rules.ikStretchMinLength) {
1148
+ const wx = t.worldX - bw.worldX;
1149
+ const wy = t.worldY - bw.worldY;
1150
+ const d = mode === 'noScale' || mode === 'noScaleOrReflection' ? Math.sqrt(wx * wx + wy * wy) : Math.sqrt(lx * lx + ly * ly);
1151
+ if ((c.compress && d < len) || (c.stretch && d > len)) {
1152
+ const s = (d / len - 1) * c.mix + 1;
1153
+ b.scaleX = scaleX * s;
1154
+ if (c.scaleY === 'uniform') b.scaleY = (b.scaleY ?? 1) * s;
1155
+ else if (c.scaleY === 'volume') b.scaleY = volumeScaleY(b.scaleY ?? 1, s);
1156
+ }
1157
+ }
1158
+ return [b.name];
1159
+ }
1160
+
1161
+ /** A two-bone ik (the header's *ik — two bones*): returns the parent changed (the child is below it), or none. */
1162
+ function solveTwo(state: SolverState, c: CoreIkRecord, frame: CoreWorld | null = null): string[] {
1163
+ const parent = bone(state, c.bones[0]);
1164
+ const child = bone(state, c.bones[1]);
1165
+ if (modeOf(parent) !== 'normal' || modeOf(child) !== 'normal') return [];
1166
+ const g = frame ?? parentWorld(state, parent);
1167
+ const pw = state.world.get(parent.name) as CoreWorld;
1168
+ const t = state.world.get(c.target) as CoreWorld;
1169
+ const gdet = g.a * g.d - g.b * g.c;
1170
+ // A collapsed grandparent frame (issue #979): its inverse reads as zero, so every point in it is its origin.
1171
+ const gid = Math.abs(gdet) <= state.rules.ikCollapsedDet ? 0 : 1 / gdet;
1172
+ const inGrand = (wx: number, wy: number): [number, number] => {
1173
+ const x = wx - g.worldX;
1174
+ const y = wy - g.worldY;
1175
+ return [(x * g.d - y * g.b) * gid, (y * g.a - x * g.c) * gid];
1176
+ };
1177
+ const px = parent.x ?? 0;
1178
+ const py = parent.y ?? 0;
1179
+ const psx = parent.scaleX ?? 1;
1180
+ const psy = parent.scaleY ?? 1;
1181
+ const csx = child.scaleX ?? 1;
1182
+ let sx = psx;
1183
+ let sy = psy;
1184
+ const cx = child.x ?? 0;
1185
+ let cy = child.y ?? 0;
1186
+ const uniform = Math.abs(Math.abs(psx) - Math.abs(psy)) <= 0.00001;
1187
+ if (!uniform || c.stretch) cy = 0;
1188
+ const [ox, oy] = inGrand(pw.a * cx + pw.b * cy + pw.worldX, pw.c * cx + pw.d * cy + pw.worldY);
1189
+ const dx = ox - px;
1190
+ const dy = oy - py;
1191
+ const l1 = Math.sqrt(dx * dx + dy * dy);
1192
+ let l2 = (child.length ?? 0) * Math.abs(csx);
1193
+ // The child's origin nearer the parent's than 0.00001 in the grandparent frame (issue #966): the constraint turns the parent as a one-bone ik would,
1194
+ // stretching it but neither compressing it nor scaling its y (its `scaleY` mode read as none), and poses the child at rotation 0 with the local y the solver set. Measured on parents whose x scale is 0 (CW06), and by bisection
1195
+ // on the x scale at three child offsets and three rotations: an l1 of exactly 0.00001 solves, the double below folds.
1196
+ if (l1 < state.rules.ikTwoBoneNearChild) {
1197
+ solveOne(state, { ...c, bones: [parent.name], compress: false, scaleY: 'none' }, frame);
1198
+ child.rotation = 0;
1199
+ child.y = cy;
1200
+ return [parent.name];
1201
+ }
1202
+ let [tx, ty] = inGrand(t.worldX, t.worldY);
1203
+ tx -= px;
1204
+ ty -= py;
1205
+ let d2 = tx * tx + ty * ty;
1206
+ const bend = c.bendPositive ? 1 : -1;
1207
+ if (c.softness !== 0) {
1208
+ const soft = c.softness * (Math.abs(psx) * (Math.abs(csx) + 1) * 0.5);
1209
+ const d = Math.sqrt(d2);
1210
+ const sd = d - l1 - l2 * Math.abs(psx) + soft;
1211
+ if (sd > 0) {
1212
+ const q = Math.min(1, sd / (soft * 2)) - 1;
1213
+ const pull = (sd - soft * (1 - q * q)) / d;
1214
+ tx -= pull * tx;
1215
+ ty -= pull * ty;
1216
+ d2 = tx * tx + ty * ty;
1217
+ }
1218
+ }
1219
+ let a1: number;
1220
+ let a2: number;
1221
+ if (uniform) {
1222
+ l2 *= Math.abs(psx);
1223
+ let cos = (d2 - l1 * l1 - l2 * l2) / (2 * l1 * l2);
1224
+ if (cos < -1) {
1225
+ cos = -1;
1226
+ a2 = Math.PI * bend;
1227
+ } else if (cos > 1) {
1228
+ cos = 1;
1229
+ a2 = 0;
1230
+ if (c.stretch) {
1231
+ const f = (Math.sqrt(d2) / (l1 + l2) - 1) * c.mix + 1;
1232
+ sx *= f;
1233
+ if (c.scaleY === 'uniform') sy *= f;
1234
+ else if (c.scaleY === 'volume') sy = volumeScaleY(sy, f);
1235
+ }
1236
+ } else a2 = Math.acos(cos) * bend;
1237
+ const along = l1 + l2 * cos;
1238
+ const across = l2 * Math.sin(a2);
1239
+ a1 = Math.atan2(ty * along - tx * across, tx * along + ty * across);
1240
+ } else {
1241
+ const ea = Math.abs(psx) * l2;
1242
+ const eb = Math.abs(psy) * l2;
1243
+ const aa = ea * ea;
1244
+ const bb = eb * eb;
1245
+ const qa = bb - aa;
1246
+ const qb = -2 * bb * l1;
1247
+ const qc = bb * l1 * l1 + aa * d2 - aa * bb;
1248
+ const disc = qb * qb - 4 * qa * qc;
1249
+ let solved = false;
1250
+ a1 = 0;
1251
+ a2 = 0;
1252
+ if (disc >= 0) {
1253
+ let q = Math.sqrt(disc);
1254
+ if (qb < 0) q = -q;
1255
+ q = -(qb + q) * 0.5;
1256
+ const r0 = q / qa;
1257
+ const r1 = qc / q;
1258
+ const X = Math.abs(r0) < Math.abs(r1) ? r0 : r1;
1259
+ const y2 = d2 - X * X;
1260
+ if (y2 >= 0) {
1261
+ const Y = Math.sqrt(y2) * bend;
1262
+ a1 = Math.atan2(ty, tx) - Math.atan2(Y, X);
1263
+ a2 = Math.atan2(Y / Math.abs(psy), (X - l1) / Math.abs(psx));
1264
+ solved = true;
1265
+ }
1266
+ }
1267
+ if (!solved) {
1268
+ let near = { angle: RUNTIME_PI, x: l1 - ea, y: 0, dist: (l1 - ea) * (l1 - ea) };
1269
+ let far = { angle: 0, x: l1 + ea, y: 0, dist: (l1 + ea) * (l1 + ea) };
1270
+ const vertex = (-ea * l1) / (aa - bb);
1271
+ if (vertex >= -1 && vertex <= 1) {
1272
+ const angle = Math.acos(vertex);
1273
+ const x = ea * Math.cos(angle) + l1;
1274
+ const y = eb * Math.sin(angle);
1275
+ const dist = x * x + y * y;
1276
+ if (dist < near.dist) near = { angle, x, y, dist };
1277
+ if (dist > far.dist) far = { angle, x, y, dist };
1278
+ }
1279
+ const pick = d2 <= (near.dist + far.dist) * 0.5 ? near : far;
1280
+ a1 = Math.atan2(ty, tx) - Math.atan2(pick.y * bend, pick.x);
1281
+ a2 = pick.angle * bend;
1282
+ }
1283
+ }
1284
+ // A scale of 0 counts as positive (issue #966): `Math.sign` read 0 for it and zeroed both offsets and the child's angle — 59 of 300 two-bone iks over a parent of scaleY 0 exact, against 300 (CW06).
1285
+ const signs = state.rules.ikZeroScaleSignPositive ? (psx < 0 ? -1 : 1) * (psy < 0 ? -1 : 1) : Math.sign(psx) * Math.sign(psy);
1286
+ const offset = Math.atan2(cy, cx);
1287
+ // The child's offset angle is taken off in radians before the change is turned into degrees (issue #966): `a1·DEG − offset·DEG` agrees on the grid and reads 1–4 ulp off on `spineboy-pro`'s two-bone iks (44 bone findings of a raw compare against 4, the four the ik mix's setup blend below).
1288
+ const parentRotation = parent.rotation ?? 0;
1289
+ const parentChange = (a1 - signs * offset) * DEG + (psx < 0 ? 180 : 0) - parentRotation;
1290
+ parent.rotation = parentRotation + wrap180(parentChange) * c.mix;
1291
+ parent.scaleX = sx;
1292
+ parent.scaleY = sy;
1293
+ const childTarget = ((a2 + signs * offset) * DEG - (child.shearX ?? 0)) * signs + (csx < 0 ? 180 : 0);
1294
+ const childRotation = child.rotation ?? 0;
1295
+ child.y = cy;
1296
+ child.rotation = childRotation + wrap180(childTarget - childRotation) * c.mix;
1297
+ return [parent.name];
1298
+ }
1299
+
1300
+ /** The source's value of `property` (the header's *transform*); a slider reads its dial through it with no offset. */
1301
+ export function sourceValue(state: SolverState, c: Pick<CoreTransformRecord, 'source' | 'localSource' | 'offsets'>, property: TransformProperty): number {
1302
+ const s = bone(state, c.source);
1303
+ const o = c.offsets;
1304
+ if (c.localSource) {
1305
+ switch (property) {
1306
+ case 'rotate': return (s.rotation ?? 0) + o.rotate;
1307
+ case 'x': return (s.x ?? 0) + o.x;
1308
+ case 'y': return (s.y ?? 0) + o.y;
1309
+ case 'scaleX': return (s.scaleX ?? 1) + o.scaleX;
1310
+ case 'scaleY': return (s.scaleY ?? 1) + o.scaleY;
1311
+ case 'shearY': return (s.shearY ?? 0) + o.shearY;
1312
+ }
1313
+ }
1314
+ const w = state.world.get(c.source) as CoreWorld;
1315
+ switch (property) {
1316
+ case 'rotate': {
1317
+ // The offset is negated unless the determinant is above 0 — a collapsed source (determinant 0, issue #979) negates it too.
1318
+ const det = w.a * w.d - w.b * w.c;
1319
+ let r = Math.atan2(w.c, w.a) * DEG + ((state.rules.offsetNegatedAtZeroDet ? det > 0 : det >= 0) ? o.rotate : -o.rotate);
1320
+ if (r < 0) r += 360;
1321
+ return r;
1322
+ }
1323
+ case 'x': return w.a * o.x + w.b * o.y + w.worldX;
1324
+ case 'y': return w.c * o.x + w.d * o.y + w.worldY;
1325
+ case 'scaleX': return Math.sqrt(w.a * w.a + w.c * w.c) + o.scaleX;
1326
+ case 'scaleY': return Math.sqrt(w.b * w.b + w.d * w.d) + o.scaleY;
1327
+ case 'shearY': return (Math.atan2(w.d, w.b) - Math.atan2(w.c, w.a)) * DEG - 90 + o.shearY;
1328
+ }
1329
+ }
1330
+
1331
+ function applyWorld(w: CoreWorld, property: TransformProperty, v: number, mix: number, additive: boolean, rules: Readonly<SolverRules>): void {
1332
+ switch (property) {
1333
+ case 'rotate': {
1334
+ // In radians, the change wrapped at the runtime's pi (issue #966): the degree form `wrap180(v − atan2·DEG)·mix·RAD` agrees on the grid and reads 1–2 ulp off on the corpus's `spineboy-pro` (202 findings of a raw compare, 113 of them bones, against 96 with this reading, the rest the two-bone ik below); this reading is exact on the row once those are fixed too.
1335
+ let r = additive ? v * RAD : v * RAD - Math.atan2(w.c, w.a);
1336
+ if (r > RUNTIME_PI) r -= 2 * RUNTIME_PI;
1337
+ else if (r < -RUNTIME_PI) r += 2 * RUNTIME_PI;
1338
+ r *= mix;
1339
+ const cos = Math.cos(r);
1340
+ const sin = Math.sin(r);
1341
+ const [a, b] = [w.a, w.b];
1342
+ w.a = cos * a - sin * w.c;
1343
+ w.b = cos * b - sin * w.d;
1344
+ w.c = sin * a + cos * w.c;
1345
+ w.d = sin * b + cos * w.d;
1346
+ return;
1347
+ }
1348
+ case 'x':
1349
+ w.worldX = additive ? w.worldX + v * mix : w.worldX + (v - w.worldX) * mix;
1350
+ return;
1351
+ case 'y':
1352
+ w.worldY = additive ? w.worldY + v * mix : w.worldY + (v - w.worldY) * mix;
1353
+ return;
1354
+ case 'scaleX':
1355
+ case 'scaleY': {
1356
+ const [p, q] = property === 'scaleX' ? (['a', 'c'] as const) : (['b', 'd'] as const);
1357
+ const s = Math.sqrt(w[p] * w[p] + w[q] * w[q]);
1358
+ if (s === 0) return;
1359
+ const k = additive ? 1 + (v - 1) * mix : 1 + ((v - s) * mix) / s;
1360
+ w[p] *= k;
1361
+ w[q] *= k;
1362
+ return;
1363
+ }
1364
+ case 'shearY': {
1365
+ const yAngle = Math.atan2(w.d, w.b);
1366
+ const xAngle = Math.atan2(w.c, w.a);
1367
+ // Additive (issue #966): the value is turned into radians with the right angle on, and the right angle taken off after — `(v + 90)·RAD − 90·RAD`, unwrapped — as the absolute form takes the columns' angle off the same sum; `v·RAD` read 35 of 132 of the transform population's additive world-shear probes last-bit off, and 123 of 300 single-mapping probes (CW04).
1368
+ let r = additive && !rules.additiveShearRightAngle ? v * RAD : (v + 90) * RAD - (additive ? 90 * RAD : yAngle - xAngle);
1369
+ if (!additive) r = r > RUNTIME_PI ? r - 2 * RUNTIME_PI : r < -RUNTIME_PI ? r + 2 * RUNTIME_PI : r;
1370
+ const angle = yAngle + r * mix;
1371
+ const s = Math.sqrt(w.b * w.b + w.d * w.d);
1372
+ w.b = Math.cos(angle) * s;
1373
+ w.d = Math.sin(angle) * s;
1374
+ return;
1375
+ }
1376
+ }
1377
+ }
1378
+
1379
+ const LOCAL_FIELD: Record<TransformProperty, 'rotation' | 'x' | 'y' | 'scaleX' | 'scaleY' | 'shearY'> = { rotate: 'rotation', x: 'x', y: 'y', scaleX: 'scaleX', scaleY: 'scaleY', shearY: 'shearY' };
1380
+
1381
+ function applyLocal(b: ModelBone, property: TransformProperty, v: number, mix: number, additive: boolean): void {
1382
+ const field = LOCAL_FIELD[property];
1383
+ const scale = property === 'scaleX' || property === 'scaleY';
1384
+ const current = b[field] ?? (scale ? 1 : 0);
1385
+ if (!additive) b[field] = current + (v - current) * mix;
1386
+ else b[field] = scale ? current * (1 + (v - 1) * mix) : current + v * mix;
1387
+ }
1388
+
1389
+ /** A transform constraint (the header's *transform*): returns the bones it changed, and those it changed in world space. */
1390
+ function solveTransform(state: SolverState, c: CoreTransformRecord): { changed: string[]; inWorld: string[] } {
1391
+ const changed: string[] = [];
1392
+ const inWorld: string[] = [];
1393
+ for (const name of c.bones) {
1394
+ const b = bone(state, name);
1395
+ const w = { ...(state.world.get(name) as CoreWorld) };
1396
+ let applied = false;
1397
+ for (const from of c.properties) {
1398
+ const value = sourceValue(state, c, from.property) - from.offset;
1399
+ for (const to of from.to) {
1400
+ const mix = c.mixes[to.property];
1401
+ if (mix === 0) continue;
1402
+ let v = value * to.scale + to.offset;
1403
+ if (c.clamp) {
1404
+ const lo = Math.min(to.offset, to.max);
1405
+ const hi = Math.max(to.offset, to.max);
1406
+ v = v < lo ? lo : v > hi ? hi : v;
1407
+ }
1408
+ applied = true;
1409
+ if (c.localTarget) applyLocal(b, to.property, v, mix, c.additive);
1410
+ else applyWorld(w, to.property, v, mix, c.additive, state.rules);
1411
+ }
1412
+ }
1413
+ if (!applied) continue;
1414
+ changed.push(name);
1415
+ if (!c.localTarget) {
1416
+ state.world.set(name, w);
1417
+ inWorld.push(name);
1418
+ }
1419
+ }
1420
+ return { changed, inWorld };
1421
+ }
1422
+
1423
+ /** Why a constraint is not applied under the skin view posed (the header's measured rule), or null when it is. */
1424
+ function inactiveWhy(state: SolverState, c: CoreConstraintRecord): string | null {
1425
+ return constraintInactiveWhy(c, state.active, state.rules);
1426
+ }
1427
+
1428
+ /**
1429
+ * Why a constraint is not applied under a skin view whose active bones are
1430
+ * `active` (the header's measured rule), or null when it is — `inactiveWhy`'s
1431
+ * reading, exported so the additive probe (`./additive.ts`, issue #1025) asks
1432
+ * the rule the solver applies rather than a copy of it.
1433
+ */
1434
+ export function constraintInactiveWhy(c: CoreConstraintRecord, active: ReadonlySet<string>, rules: Readonly<SolverRules> = RUNTIME_SOLVER_RULES): string | null {
1435
+ // A skin-required constraint of any kind is applied when an applied skin's list for its kind names it (issue #932, card #961; physics first by issue #956).
1436
+ if (c.skin && !c.listedBySkin) return 'skin';
1437
+ // A path constraint is active when its slot's bone is (`./constraints_path.ts`, *Which constraints run*); every other kind when every bone it names is.
1438
+ // A transform constraint is applied when its source is, to every bone it names, inactive ones included (issue #979: an inactive bone among them did not stop it moving the others, and it wrote into the inactive one). An ik naming an inactive bone is left unapplied: the runtime applies it from ancestors it has not brought up to date (the header's *Unposed bones*).
1439
+ const named = c.kind === 'path' ? [c.slotBone] : c.kind === 'ik' ? (rules.ikOverInactiveFresh ? [c.target] : [...c.bones, c.target]) : c.kind === 'transform' ? (rules.transformIgnoresBoneActivity ? [c.source] : [...c.bones, c.source]) : c.kind === 'slider' ? (c.bone === null ? [] : [c.bone]) : [c.bone];
1440
+ return named.every((n) => active.has(n)) ? null : 'inactive bone';
1441
+ }
1442
+
1443
+ /**
1444
+ * For each ik naming an inactive bone, by its index in `records`: whether the
1445
+ * pass has brought the bone above its first bone up to date by the time the
1446
+ * ik runs (the header's *Unposed bones and collapsed frames*). The runtime
1447
+ * orders its update before it poses, each constraint bringing up to date the
1448
+ * bones it reads, parents first; an inactive bone counts as already ordered,
1449
+ * so ordering one does not reach its parent. A bone ordered once in the pass
1450
+ * holds a world from this pass; one never ordered holds what the skeleton
1451
+ * held before, which on a fresh skeleton is zeros. The same walk as
1452
+ * `slotBonePlan` in `./constraints_path.ts`.
1453
+ */
1454
+ export function frameUpdatedBefore(bones: readonly ModelBone[], active: ReadonlySet<string>, records: readonly CoreConstraintRecord[], skipped: ReadonlySet<number>): Map<number, boolean> {
1455
+ const parent = new Map(bones.map((b) => [b.name, b.parent]));
1456
+ const children = new Map<string, string[]>();
1457
+ for (const b of bones) if (b.parent !== undefined) children.set(b.parent, [...(children.get(b.parent) ?? []), b.name]);
1458
+ const ordered = new Map(bones.map((b) => [b.name, !active.has(b.name)]));
1459
+ const ever = new Set<string>();
1460
+ const orderBone = (name: string): void => {
1461
+ if (ordered.get(name)) return;
1462
+ const p = parent.get(name);
1463
+ if (p !== undefined) orderBone(p);
1464
+ ordered.set(name, true);
1465
+ ever.add(name);
1466
+ };
1467
+ const unorder = (names: readonly string[]): void => {
1468
+ for (const n of names) {
1469
+ if (!active.has(n)) continue;
1470
+ if (ordered.get(n)) unorder(children.get(n) ?? []);
1471
+ ordered.set(n, false);
1472
+ }
1473
+ };
1474
+ const out = new Map<number, boolean>();
1475
+ records.forEach((c, m) => {
1476
+ if (skipped.has(m)) return;
1477
+ if (c.kind === 'slider') {
1478
+ unorder(c.timelines.bones.map((t) => t.name));
1479
+ return;
1480
+ }
1481
+ if (c.kind === 'ik') {
1482
+ orderBone(c.target);
1483
+ orderBone(c.bones[0]);
1484
+ if (c.bones.length > 1) orderBone(c.bones[c.bones.length - 1]);
1485
+ if (c.bones.some((b) => !active.has(b))) {
1486
+ const f = parent.get(c.bones[0]);
1487
+ out.set(m, f === undefined || ever.has(f));
1488
+ }
1489
+ unorder(children.get(c.bones[0]) ?? []);
1490
+ if (c.bones.length > 1) ordered.set(c.bones[c.bones.length - 1], true);
1491
+ return;
1492
+ }
1493
+ const moved = c.kind === 'physics' ? [c.bone] : c.bones;
1494
+ if (c.kind === 'transform') orderBone(c.source);
1495
+ else if (c.kind === 'path') for (const d of c.slotDeps) orderBone(d);
1496
+ for (const b of moved) orderBone(b);
1497
+ for (const b of moved) unorder(children.get(b) ?? []);
1498
+ for (const b of moved) ordered.set(b, true);
1499
+ });
1500
+ return out;
1501
+ }
1502
+
1503
+ /** The frame an ik over an inactive bone reads (`frameUpdatedBefore`): zeros when the pass has not brought it up to date, null (the current one) otherwise. */
1504
+ function inactiveIkFrame(state: SolverState, c: CoreIkRecord, i: number, updated: ReadonlyMap<number, boolean>): CoreWorld | null {
1505
+ if (!state.rules.ikOverInactiveFresh || updated.get(i) !== false) return null;
1506
+ return { a: 0, b: 0, c: 0, d: 0, worldX: 0, worldY: 0 };
1507
+ }
1508
+
1509
+ /** A plant a control passes in place of the solving: the records as posed, rewritten (a mix scaled, a bend flipped, two swapped). */
1510
+ export type ConstraintPlant = (records: CoreConstraintRecord[]) => CoreConstraintRecord[];
1511
+
1512
+ /**
1513
+ * Every admitted constraint applied, in the document's order, to the bones'
1514
+ * local values (copied, never the caller's) and their world transforms as
1515
+ * `worldTransforms` posed them — the header's update order — and the world
1516
+ * transforms returned. With `physics`, the stepped phase's context, each
1517
+ * physics constraint is stepped on its bone (`stepPhysics` in
1518
+ * `./constraints_physics.ts`); without it, it applies nothing.
1519
+ */
1520
+ export function applyConstraints(bones: readonly ModelBone[], world: ReadonlyMap<string, CoreWorld>, active: ReadonlySet<string>, records: readonly CoreConstraintRecord[], previous: ReadonlyMap<string, CoreWorld> | null = null, applied?: SliderApplication[], physics?: PhysicsStepContext, settled?: (state: SolverState) => void, rules: Readonly<SolverRules> = RUNTIME_SOLVER_RULES): Map<string, CoreWorld> {
1521
+ // No record: nothing to solve, so the world as given, copied (issue #1134) — and what `sliderPhysicsTarget` leaves on the step's context with no record, an empty carry, under the plant that carries one.
1522
+ if (records.length === 0 && settled === undefined) {
1523
+ if (physics !== undefined && !rules.sliderPhysicsLastsOnePass) physics.carried = new Map();
1524
+ return new Map(world);
1525
+ }
1526
+ const state: SolverState = { bones: bones.map((b) => ({ ...b })), index: boneIndex(bones, active), world: new Map(world), active, rules };
1527
+ const skipped = new Set<number>();
1528
+ records.forEach((c, i) => {
1529
+ if (inactiveWhy(state, c) !== null) skipped.add(i);
1530
+ });
1531
+ // A path constraint's offset reads its slot bone's world as the runtime last brought it up to date (`./constraints_path.ts`, *Which slot bone*).
1532
+ const plan = records.some((c) => c.kind === 'path') ? keptPlan(slotBonePlans, state, records, skipped, () => slotBonePlan(bones, active, records, skipped)) : new Map<number, SlotBoneEvent | null>();
1533
+ // An ik over an inactive bone reads the frame above it as the pass left it (issue #979).
1534
+ const updated = records.some((c) => c.kind === 'ik' && c.bones.some((b) => !active.has(b))) ? keptPlan(frameUpdates, state, records, skipped, () => frameUpdatedBefore(bones, active, records, skipped)) : new Map<number, boolean>();
1535
+ const snapshots = new Map<number, CoreWorld>();
1536
+ // Issue #1049: under the step a slider's physics timelines write the pass's physics records, which a later physics constraint steps with.
1537
+ const physicsPose = physics === undefined ? undefined : sliderPhysicsTarget(records, active, physics, rules);
1538
+ const snap = (i: number, when: 'before' | 'after'): void => {
1539
+ for (const [k, e] of plan) if (e !== null && e.at === i && e.when === when && !(k === i && when === 'before')) snapshots.set(k, { ...(state.world.get((records[k] as CorePathRecord).slotBone) as CoreWorld) });
1540
+ };
1541
+ for (let i = 0; i < records.length; i++) {
1542
+ const c = records[i];
1543
+ snap(i, 'before');
1544
+ if (skipped.has(i)) continue;
1545
+ let changed: string[] = [];
1546
+ let inWorld: string[] = [];
1547
+ if (c.kind === 'physics') {
1548
+ // Under Physics.none a physics constraint applies nothing; stepped, it moves its bone in world space (`./constraints_physics.ts`).
1549
+ if (physics !== undefined) {
1550
+ const w = { ...(state.world.get(c.bone) as CoreWorld) };
1551
+ const posed = physicsPose?.records.get(c.name) ?? c;
1552
+ if ((physics.step ?? stepPhysics)(posed, w, bone(state, c.bone).length ?? 0, physics)) {
1553
+ state.world.set(c.bone, w);
1554
+ changed = [c.bone];
1555
+ inWorld = [c.bone];
1556
+ }
1557
+ }
1558
+ } else if (c.kind === 'slider') {
1559
+ changed = applySlider(state, c, applied, physicsPose);
1560
+ } else if (c.kind === 'ik') {
1561
+ if (c.mix !== 0) {
1562
+ const frame = inactiveIkFrame(state, c, i, updated);
1563
+ changed = c.bones.length === 1 ? solveOne(state, c, frame) : solveTwo(state, c, frame);
1564
+ }
1565
+ } else if (c.kind === 'path') {
1566
+ const e = plan.get(i) ?? null;
1567
+ const stale = previous === null ? { a: 0, b: 0, c: 0, d: 0, worldX: 0, worldY: 0 } : (previous.get(c.slotBone) as CoreWorld);
1568
+ const slotWorld = (e !== null && e.at === i) ? (state.world.get(c.slotBone) as CoreWorld) : e !== null ? (snapshots.get(i) as CoreWorld) : stale;
1569
+ ({ changed, inWorld } = solvePath(state, c, slotWorld));
1570
+ } else {
1571
+ ({ changed, inWorld } = solveTransform(state, c));
1572
+ }
1573
+ if (changed.length > 0) repose(state, changed, inWorld);
1574
+ snap(i, 'after');
1575
+ }
1576
+ // #969: the solver's last state — every bone's local values as the constraints left them — for a caller reading a dial off it (`./hooks.ts`).
1577
+ settled?.(state);
1578
+ return state.world;
1579
+ }
1580
+
1581
+ /**
1582
+ * What `slotBonePlan` (`./constraints_path.ts`) and `frameUpdatedBefore` read
1583
+ * of a record: its kind and the names it orders — a name by value, a list by
1584
+ * identity. Every pass of a walk poses its records afresh (`posedRecords`,
1585
+ * `pathDeformed`, `stepPhysicsRecords` spread each one), and a spread carries
1586
+ * these lists over as the same arrays.
1587
+ */
1588
+ function planShape(c: CoreConstraintRecord): readonly unknown[] {
1589
+ switch (c.kind) {
1590
+ case 'slider':
1591
+ return [c.kind, c.timelines.bones];
1592
+ case 'ik':
1593
+ return [c.kind, c.target, c.bones];
1594
+ case 'physics':
1595
+ return [c.kind, c.bone];
1596
+ case 'transform':
1597
+ return [c.kind, c.source, c.bones];
1598
+ case 'path':
1599
+ return [c.kind, c.bones, c.slotDeps, c.slotBone];
1600
+ }
1601
+ }
1602
+
1603
+ /** A plan kept per solver index, with the record shapes (`planShape`) and the skipped records it was built over. */
1604
+ interface KeptPlan<T> {
1605
+ shapes: ReadonlyArray<readonly unknown[]>;
1606
+ skipped: readonly number[];
1607
+ plan: T;
1608
+ }
1609
+ const slotBonePlans = new WeakMap<ReadonlyMap<string, number>, KeptPlan<Map<number, SlotBoneEvent | null>>>();
1610
+ const frameUpdates = new WeakMap<ReadonlyMap<string, number>, KeptPlan<Map<number, boolean>>>();
1611
+
1612
+ /**
1613
+ * A pass's ordering plan — `slotBonePlan` or `frameUpdatedBefore` — reused
1614
+ * from the pass before when nothing it reads has changed (issue #1179): the
1615
+ * bones' names and parents and the active set (one solver index stands for
1616
+ * those, `boneIndex`), each record's shape, and which records are skipped.
1617
+ * Otherwise built again. Both read names and nothing else, so a reused plan is
1618
+ * the plan the pass would have built; neither is written once built.
1619
+ */
1620
+ function keptPlan<T>(kept: WeakMap<ReadonlyMap<string, number>, KeptPlan<T>>, state: SolverState, records: readonly CoreConstraintRecord[], skipped: ReadonlySet<number>, build: () => T): T {
1621
+ const shapes = records.map(planShape);
1622
+ const skips = [...skipped];
1623
+ const last = kept.get(state.index);
1624
+ if (
1625
+ last !== undefined &&
1626
+ last.shapes.length === shapes.length &&
1627
+ shapes.every((sh, i) => sh.length === last.shapes[i].length && sh.every((v, j) => v === last.shapes[i][j])) &&
1628
+ last.skipped.length === skips.length &&
1629
+ skips.every((k, i) => k === last.skipped[i])
1630
+ ) {
1631
+ return last.plan;
1632
+ }
1633
+ const plan = build();
1634
+ kept.set(state.index, { shapes, skipped: skips, plan });
1635
+ return plan;
1636
+ }
1637
+
1638
+ /** The last bone index each view's active set was asked for, with the names and parents it was built over (`boneIndex`). */
1639
+ const indexOfView = new WeakMap<ReadonlySet<string>, { names: string[]; parents: Array<string | undefined>; index: Map<string, number> }>();
1640
+
1641
+ /**
1642
+ * Each bone's position in `bones`, by name — the solver's index. Every pose of
1643
+ * a view lists the same bones in the same order (issue #1134: a walk built it
1644
+ * anew on each of its passes), so the index last built under the view's
1645
+ * active set (`activeBones`, one object per view) is reused when the names
1646
+ * and the parents agree position by position, and built again otherwise —
1647
+ * the parents because `repose` keeps each bone's subtree beside the index
1648
+ * (`subtreesOf`), and a subtree is a reading of them. Read, never written.
1649
+ */
1650
+ function boneIndex(bones: readonly ModelBone[], active: ReadonlySet<string>): Map<string, number> {
1651
+ const kept = indexOfView.get(active);
1652
+ if (kept !== undefined && kept.names.length === bones.length && bones.every((b, i) => b.name === kept.names[i] && b.parent === kept.parents[i])) return kept.index;
1653
+ const index = new Map(bones.map((b, i) => [b.name, i]));
1654
+ indexOfView.set(active, { names: bones.map((b) => b.name), parents: bones.map((b) => b.parent), index });
1655
+ return index;
1656
+ }
1657
+
1658
+ /** Each bone's subtree as `subtreesOf` built it, kept per solver index (one object per view's bones, `boneIndex`). */
1659
+ const subtreesOfIndex = new WeakMap<ReadonlyMap<string, number>, ReadonlyArray<readonly number[]>>();
1660
+
1661
+ /**
1662
+ * For the bone at each position of `state.bones`, the positions of the bone
1663
+ * and of every bone below it, ascending (issue #1179). Bones are parents
1664
+ * first, so each position is appended to its own list and its ancestors' in
1665
+ * ascending order. Built from the bones' parents once per solver index —
1666
+ * `boneIndex` hands back one index only while the names and the parents are
1667
+ * the ones it was built over, and the other two solver states (`previousPassSlotBones`,
1668
+ * `historyTaint`) build an index of their own.
1669
+ */
1670
+ function subtreesOf(state: SolverState): ReadonlyArray<readonly number[]> {
1671
+ const kept = subtreesOfIndex.get(state.index);
1672
+ if (kept !== undefined) return kept;
1673
+ const lists: number[][] = state.bones.map(() => []);
1674
+ for (let k = 0; k < state.bones.length; k++) {
1675
+ let at: number | undefined = k;
1676
+ while (at !== undefined) {
1677
+ lists[at].push(k);
1678
+ const parent: string | undefined = state.bones[at].parent;
1679
+ at = parent === undefined ? undefined : state.index.get(parent);
1680
+ }
1681
+ }
1682
+ subtreesOfIndex.set(state.index, lists);
1683
+ return lists;
1684
+ }
1685
+
1686
+ /** After a constraint: the bones it moved in world space read back into local values, and every bone below one it moved posed again (the header's update order). */
1687
+ function repose(state: SolverState, changed: readonly string[], inWorld: readonly string[]): void {
1688
+ for (const name of inWorld) {
1689
+ const b = bone(state, name);
1690
+ localFromWorld(b, parentWorld(state, b), state.world.get(name) as CoreWorld, state.rules);
1691
+ }
1692
+ // `changed` and `inWorld` are a constraint's few bones: read as they are rather than copied into sets on each of a walk's passes (issue #1134).
1693
+ const moved = changed;
1694
+ const below = new Set(changed);
1695
+ const keep = inWorld;
1696
+ // A bone joins `below` only through its parent, so the bones that can are the moved bones' subtrees: the walk visits those, in bone order (issue
1697
+ // #1179), where it visited every bone from the first one moved (issue #1134) — the bones it no longer visits are the ones whose parent `below`
1698
+ // could never hold, so each bone it does visit reads the same `below`, and is posed again in the same order.
1699
+ const subtrees = subtreesOf(state);
1700
+ const lists: Array<readonly number[]> = [];
1701
+ for (const name of changed) {
1702
+ const at = state.index.get(name);
1703
+ if (at !== undefined) lists.push(subtrees[at]);
1704
+ }
1705
+ const visit = lists.length === 1 ? lists[0] : [...new Set(lists.flat())].sort((x, y) => x - y);
1706
+ for (const k of visit) {
1707
+ const b = state.bones[k];
1708
+ // Not through an inactive bone the constraint did not move itself (issue #979): the bones below it keep what the constraints wrote into them.
1709
+ if (b.parent !== undefined && below.has(b.parent) && (!state.rules.inactiveHoldsItsWorld || state.active.has(b.parent) || moved.includes(b.parent))) below.add(b.name);
1710
+ if (below.has(b.name) && !keep.includes(b.name)) poseBone(state, b);
1711
+ }
1712
+ }
1713
+
1714
+ /**
1715
+ * The slot bones a path constraint's offset reads from the PREVIOUS pass —
1716
+ * those the runtime has not brought up to date in this one by the time the
1717
+ * constraint runs (`./constraints_path.ts`, *Which slot bone*) — named with
1718
+ * their constraint. Only a constraint with an offset reads its slot bone.
1719
+ */
1720
+ export function previousPassSlotBones(doc: CompiledDocument, active: ReadonlySet<string>): Array<{ constraint: string; bone: string }> {
1721
+ const records = doc.constraints.flatMap((c) => (c.record === undefined ? [] : [c.record]));
1722
+ if (!records.some((r) => r.kind === 'path')) return [];
1723
+ const state: SolverState = { bones: [...doc.bones], index: new Map(doc.bones.map((b, i) => [b.name, i])), world: new Map(), active, rules: RUNTIME_SOLVER_RULES };
1724
+ const skipped = new Set<number>();
1725
+ records.forEach((c, i) => {
1726
+ if (inactiveWhy(state, c) !== null) skipped.add(i);
1727
+ });
1728
+ const out: Array<{ constraint: string; bone: string }> = [];
1729
+ for (const [k, e] of slotBonePlan(doc.bones, active, records, skipped)) {
1730
+ const r = records[k] as CorePathRecord;
1731
+ if (e === null && r.offsetRotation !== 0) out.push({ constraint: r.name, bone: r.slotBone });
1732
+ }
1733
+ return out;
1734
+ }
1735
+
1736
+ /**
1737
+ * Why an animation's bones cannot be posed by this cut though the setup's
1738
+ * can, or null: a path constraint's slot whose attachment an animation keys
1739
+ * — the curve would not be the one the setup reads, and a switch of the
1740
+ * walked curve is not measured — or a path attachment an animation deforms
1741
+ * whose placeholder several skins fill, so which record `--skin all` shows,
1742
+ * and whether the deform moves it, is the Spine file's skin order (under a
1743
+ * named skin one record resolves, `./skins.ts`). A deform
1744
+ * of a walked path is otherwise posed (`./deform.ts`, issue #955).
1745
+ */
1746
+ export function pathAnimationsWhy(doc: CompiledDocument): string | null {
1747
+ const found: string[] = [];
1748
+ const paths = doc.constraints.flatMap((c) => (c.record?.kind === 'path' ? [c.record] : []));
1749
+ for (const a of doc.animations) {
1750
+ for (const r of paths) {
1751
+ if (a.timelines.slots.some((s) => s.name === r.slot && s.timelines.some((t) => t.kind === 'attachment'))) found.push(`animation "${a.name}" keys the attachment of slot "${r.slot}", which path constraint "${r.name}" walks — a switch of the walked curve is not measured`);
1752
+ }
1753
+ for (const d of a.deforms) {
1754
+ const [skin, slot, att] = d.split('/');
1755
+ const g = doc.skins.find((k) => k.name === skin)?.attachments[slot]?.[att]?.geometry;
1756
+ const fillers = fillingSkins(doc, slot, att).map((k) => `"${k}"`);
1757
+ if (g?.kind === 'path' && fillers.length > 1) found.push(`animation "${a.name}" deforms path attachment "${att}" (skin "${skin}", slot "${slot}"), a placeholder skins ${fillers.join(', ')} fill — which of them --skin all walks, and so whether the deform moves it, is the Spine file's skin order, not the model's`);
1758
+ }
1759
+ }
1760
+ return found.length === 0 ? null : found.join('; ');
1761
+ }
1762
+
1763
+ /**
1764
+ * Why a document's bones cannot be posed by this cut, or null when they can:
1765
+ * a path constraint walks a slot whose setup placeholder skins fill with
1766
+ * different curves (`CorePathRecord.unresolved`), it declares a constraint
1767
+ * of a kind not admitted (`ADMITTED_CONSTRAINT_KINDS` — since the path cut,
1768
+ * none is left), named with the counts of every kind it declares, or a
1769
+ * slider whose animation keys a constraint timeline (`sliderBonesWhy`).
1770
+ */
1771
+ export function constraintsAbsentWhy(doc: CompiledDocument, rules: Readonly<SolverRules> = RUNTIME_SOLVER_RULES): string | null {
1772
+ const unresolved = doc.constraints.flatMap((c) => (c.record?.kind === 'path' && c.record.unresolved !== null ? [c.record.unresolved] : []));
1773
+ if (unresolved.length > 0) return unresolved.join('; ');
1774
+ const later = doc.constraints.filter((c) => !ADMITTED_CONSTRAINT_KINDS.includes(c.kind));
1775
+ if (later.length === 0) return sliderBonesWhy(doc) ?? (rules.refuseHistoryLeak ? unposedLeakWhy(doc) : null);
1776
+ const kinds = [...new Set(doc.constraints.map((c) => c.kind))];
1777
+ const laterKinds = [...new Set(later.map((c) => c.kind))];
1778
+ return `the document declares ${kinds.map((k) => `${k} ×${doc.constraints.filter((c) => c.kind === k).length}`).join(', ')}, and ${laterKinds.join(', ')} constraints are not admitted (item 5; ${ADMITTED_CONSTRAINT_KINDS.join(', ')} are): the oracle applies them`;
1779
+ }
1780
+
1781
+ /**
1782
+ * Why the core refuses a pose because a posed bone would read a value the
1783
+ * runtime makes depend on its previous pass, or null (issue #979, the
1784
+ * header's ⚠️ under *Unposed bones and collapsed frames*). A constraint that
1785
+ * writes into an inactive constrained bone — an ik reading a frame the pass
1786
+ * has not brought up to date (`frameUpdatedBefore`), a transform or a path —
1787
+ * taints that bone (and, for a world write or a two-bone ik's child, the
1788
+ * bones below it): the runtime's value there differs between a skeleton
1789
+ * posed sample after sample and a fresh one. A later constraint reading a
1790
+ * tainted bone — an ik's target, a transform's or a slider's source, a
1791
+ * path's slot bone or the bones weighting its curve — taints what it moves
1792
+ * when that is unposed too, and is refused by name when it moves a posed
1793
+ * bone: that is where the history would reach the picture. The rendered
1794
+ * value of an unposed bone is the seam's zero snapshot, so nothing else of
1795
+ * this class reaches a frame.
1796
+ */
1797
+ export function unposedLeakWhy(doc: CompiledDocument): string | null {
1798
+ return historyTaint(doc).refusal;
1799
+ }
1800
+
1801
+ /** A constraint writing into an inactive constrained bone — what a history-dependent value started from. */
1802
+ export interface HistoryWriter {
1803
+ kind: CoreConstraintKind;
1804
+ name: string;
1805
+ inactive: string;
1806
+ }
1807
+
1808
+ /**
1809
+ * The walk `unposedLeakWhy` reads: every bone a writer into an inactive bone
1810
+ * reaches, world or local, with its writer (the first, in constraint order),
1811
+ * and the refusal where a posed bone would read one — the walk stops there.
1812
+ * `tools/pose_oracle.ts unposed` classes a stepped bone-sample by it (no fresh
1813
+ * reading is taken under the step).
1814
+ */
1815
+ export function historyTaint(doc: CompiledDocument): { refusal: string | null; tainted: Map<string, HistoryWriter> } {
1816
+ const records = doc.constraints.flatMap((c) => (c.record === undefined ? [] : [c.record]));
1817
+ const active = activeBones(doc);
1818
+ if (doc.bones.every((b) => active.has(b.name))) return { refusal: null, tainted: new Map() };
1819
+ const parent = new Map(doc.bones.map((b) => [b.name, b.parent]));
1820
+ const unposed = new Set<string>();
1821
+ for (const b of doc.bones) if (!active.has(b.name) || (b.parent !== undefined && unposed.has(b.parent))) unposed.add(b.name);
1822
+ const below = (root: string): string[] => doc.bones.filter((b) => {
1823
+ for (let at: string | undefined = b.name; at !== undefined; at = parent.get(at)) if (at === root) return true;
1824
+ return false;
1825
+ }).map((b) => b.name);
1826
+ const state: SolverState = { bones: [...doc.bones], index: new Map(doc.bones.map((b, i) => [b.name, i])), world: new Map(), active, rules: RUNTIME_SOLVER_RULES };
1827
+ const skipped = new Set<number>();
1828
+ records.forEach((c, i) => {
1829
+ if (inactiveWhy(state, c) !== null) skipped.add(i);
1830
+ });
1831
+ const updated = frameUpdatedBefore(doc.bones, active, records, skipped);
1832
+ // What of a bone the previous pass reaches: its world (read by a world-space source, an ik's target, a path's slot) or its local values (read by a local source).
1833
+ const world = new Map<string, HistoryWriter>();
1834
+ const local = new Map<string, HistoryWriter>();
1835
+ const spell = (w: HistoryWriter): string => `${w.kind} constraint "${w.name}" writes into inactive bone "${w.inactive}"`;
1836
+ const union = (): Map<string, HistoryWriter> => {
1837
+ const out = new Map(world);
1838
+ for (const [k, v] of local) if (!out.has(k)) out.set(k, v);
1839
+ return out;
1840
+ };
1841
+ const taint = (into: Map<string, HistoryWriter>, names: readonly string[], origin: HistoryWriter): void => {
1842
+ for (const n of names) if (!into.has(n)) into.set(n, origin);
1843
+ };
1844
+ const label = (c: CoreConstraintRecord): string => `${c.kind} constraint "${c.name}"`;
1845
+ for (let i = 0; i < records.length; i++) {
1846
+ if (skipped.has(i)) continue;
1847
+ const c = records[i];
1848
+ const moved = c.kind === 'physics' ? [c.bone] : c.kind === 'slider' ? c.timelines.bones.map((t) => t.name) : c.bones;
1849
+ const readsLocal = (c.kind === 'transform' && c.localSource) || (c.kind === 'slider' && c.local);
1850
+ const operands = c.kind === 'ik' ? [c.target] : c.kind === 'transform' ? [c.source] : c.kind === 'path' ? [c.slotBone, ...c.slotDeps] : c.kind === 'slider' ? (c.bone === null ? [] : [c.bone]) : [];
1851
+ const from = readsLocal ? local : world;
1852
+ const read = operands.find((o) => from.has(o));
1853
+ if (read !== undefined) {
1854
+ const posed = moved.find((b) => !unposed.has(b));
1855
+ const origin = from.get(read) as HistoryWriter;
1856
+ if (posed !== undefined) return { refusal: `${label(c)} moves posed bone "${posed}" and reads ${readsLocal ? 'the local values' : 'the world transform'} of bone "${read}", a bone whose value depends on the runtime's previous pass (${spell(origin)}) — the runtime's own sample-after-sample and fresh-skeleton readings disagree there, so there is no one value to pose`, tainted: union() };
1857
+ taint(world, moved.flatMap(below), origin);
1858
+ taint(local, moved, origin);
1859
+ }
1860
+ const inactive = moved.find((b) => !active.has(b));
1861
+ if (inactive === undefined || c.kind === 'physics' || c.kind === 'slider') continue;
1862
+ const origin: HistoryWriter = { kind: c.kind, name: c.name, inactive };
1863
+ if (c.kind === 'ik') {
1864
+ // Its local values move; for a two-bone ik the child's, and with them the world of the child and the bones below it.
1865
+ if (updated.get(i) !== false) continue;
1866
+ taint(local, c.bones, origin);
1867
+ if (c.bones.length > 1) taint(world, below(c.bones[1]), origin);
1868
+ } else if (c.kind === 'transform' && c.localTarget) taint(local, moved, origin);
1869
+ else {
1870
+ // A world write: the bones moved and every bone below them, in world; their own local values read back from it.
1871
+ taint(world, moved.flatMap(below), origin);
1872
+ taint(local, moved, origin);
1873
+ }
1874
+ }
1875
+ return { refusal: null, tainted: union() };
1876
+ }