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,129 @@
1
+ /**
2
+ * The skin a document is posed under (issue #932): every skin merged — the
3
+ * oracle's `--skin all` — or one skin by name, `--skin <name>`. Each rule
4
+ * below was measured by posing hand-written skeletons through
5
+ * `tools/pose_oracle.ts dump --skin <name>` (spine-core 4.3.13) and reading
6
+ * the rows it printed; the core suite's `CN` controls hold the same
7
+ * skeletons against the core at tolerance 0.
8
+ *
9
+ * - **What a slot shows under a named skin**: the named skin's record for
10
+ * the placeholder, else the default skin's, else nothing. Three skins
11
+ * `default`, `s1`, `s2` over five slots — a placeholder the default and
12
+ * `s1` and `s2` fill, one the default alone fills, one `s1` alone, one no
13
+ * skin, one `s1` and the default — dumped under `default`, `s1` and `s2`
14
+ * in two file orders (`default, s1, s2` and `s2, s1, default`): `s1`
15
+ * showed `s1`'s record where it filled the placeholder and the default's
16
+ * where only the default did; `s2` showed `s2`'s, then the default's, and
17
+ * nothing for the placeholder only `s1` fills; a placeholder no skin fills
18
+ * showed nothing under every skin; the two file orders read the same row
19
+ * under every named skin. An attachment key switching the slot at a
20
+ * sample resolves the same way. So a named skin has no order question:
21
+ * nothing a placeholder shows depends on the file's skin order, which the
22
+ * model does not hold (the slots' ⚠️ in `./index.ts`).
23
+ * - **The bones a named skin activates**: a skin-required bone is active
24
+ * exactly when the NAMED skin names it or a bone below it — the default
25
+ * skin's `bones` list does not count. Measured on the same probe: a
26
+ * skin-required bone only the default names read active under `default`
27
+ * and inactive under `s1` and `s2`; one `s1` names, and its skin-required
28
+ * parent, active under `s1` only; a bone that is not skin-required under
29
+ * an inactive one stayed active under every skin.
30
+ * - **The constraints a named skin applies**: a skin-required constraint of
31
+ * any kind is applied exactly when the named skin's list for its kind names
32
+ * it — the default skin's list does not count either (`./constraints.ts`'s
33
+ * header, the nine readings per kind).
34
+ *
35
+ * - **A slot on an inactive bone is not animated** (the commander's private
36
+ * finding on this issue: a 19-skin rig read DIFF on 18 of its 19 skin
37
+ * runs). A slot whose bone the applied skin leaves inactive keeps its
38
+ * setup colour, dark colour and attachment under every slot timeline —
39
+ * `rgba`, `rgb`, `alpha`, `rgba2`, `rgb2` and `attachment` — whether an
40
+ * animation plays it or a slider applies it, and a deform timeline on its
41
+ * attachment does not apply either (a mesh weighted to an ACTIVE bone, so
42
+ * the deform would show: moved by 5 under `all` and `s1`, unmoved under
43
+ * `default`, which leaves the slot's bone inactive). Measured on
44
+ * hand-written skeletons under `--skin all`, `s1` (naming the bone) and
45
+ * `default` (not): each timeline moved the slot under the first two and
46
+ * not under the third, and the setup row read the setup under all three
47
+ * (a slider's key included). A slot on an ACTIVE bone that shows nothing
48
+ * (no skin holds its placeholder) IS animated: every colour key applied,
49
+ * and an attachment key switched it. A draw-order key moves a slot on an
50
+ * inactive bone like any other. A sequence timeline is held by the same
51
+ * gate; its effect on such a slot is not observable in the dump — a
52
+ * region's corners on an inactive bone are all zeros whatever the frame.
53
+ * So the predicate is the slot bone's activity (`slotTimelinesApply`),
54
+ * not what the slot shows; under `all` every bone some skin names is
55
+ * active, so a one-skin rig is unchanged.
56
+ *
57
+ * - **No skin set** (issue #1051): a fresh skeleton whose `setSkin` was never
58
+ * called — `underNoSkin`, the state `render` without `--skin`, A10's walk
59
+ * and `validate()` pose in. No skin's `bones` or constraint lists are
60
+ * applied, the default skin's included, and a slot shows the default
61
+ * skin's record for its placeholder, else nothing — so over a document
62
+ * with skins and no `default` one every slot shows nothing. Measured with
63
+ * `tools/pose_oracle.ts dump --skin none` on 93 skeletons rebuilt through
64
+ * `ingest` and `compile` (31 with no default skin, 31 whose default names
65
+ * skin-required members, 31 with a plain default): `Skeleton.skin` read
66
+ * `null` on all 93; all 311 skin-required bones read inactive — the 51 a
67
+ * default skin names among them, and the 66 a constraint writes — and the
68
+ * 125 bones that are not skin-required under one read active and unposed;
69
+ * all 73 skin-required constraints read inactive and moved nothing when
70
+ * removed — the 12 a default skin lists among them — and the 81 that are
71
+ * not read active; 199 slots whose placeholder the default skin fills
72
+ * showed it and 359 only named skins fill showed nothing; a colour key on
73
+ * a slot whose bone is inactive held its setup colour (50 of 50) and moved
74
+ * it on an active one (46 of 46), and a draw-order key applied either way
75
+ * (93 of 93). The core suite's `CO29` holds the same population against
76
+ * the core under `--raw` at tolerance 0, and `CO30` plants each rejected
77
+ * reading (the old one among them: no skin set read as the default skin,
78
+ * which differs exactly where the default skin names a skin-required
79
+ * member).
80
+ *
81
+ * Under `all` every skin is applied at once: `bones` and the constraint
82
+ * lists of every skin count, and a placeholder several skins fill shows the
83
+ * LAST of them in the Spine file's order — the case `./index.ts` leaves out
84
+ * by name, and the one the per-skin dumps exist to judge.
85
+ */
86
+ import type { ModelSlot } from '../model.ts';
87
+ import type { CompiledDocument, CoreSkin } from './index.ts';
88
+
89
+ /** The skin option that merges every skin — the oracle's `--skin all`. */
90
+ export const CORE_ALL_SKINS = 'all';
91
+
92
+ /** The skin a document names its default skin with — Spine's `SkeletonData.defaultSkin` is the skin of this name. */
93
+ export const CORE_DEFAULT_SKIN = 'default';
94
+
95
+ /** The skins whose `bones` and constraint lists are applied: every skin under `all`, none with no skin set, the named skin alone otherwise (the header's measurements). */
96
+ export function appliedSkins(doc: CompiledDocument): CoreSkin[] {
97
+ if (doc.skin === null) return [];
98
+ return doc.skin === CORE_ALL_SKINS ? doc.skins : doc.skins.filter((k) => k.name === doc.skin);
99
+ }
100
+
101
+ /** Under a named skin or none, the skins a placeholder is looked up in, in precedence order: the named skin, then the default skin (the header's measurements). */
102
+ export function lookupSkins(doc: CompiledDocument): CoreSkin[] {
103
+ if (doc.skin === CORE_ALL_SKINS) return doc.skins;
104
+ const named = doc.skin === null ? undefined : doc.skins.find((k) => k.name === doc.skin);
105
+ const fallback = doc.skin === CORE_DEFAULT_SKIN ? undefined : doc.skins.find((k) => k.name === CORE_DEFAULT_SKIN);
106
+ return [named, fallback].filter((k): k is CoreSkin => k !== undefined);
107
+ }
108
+
109
+ /**
110
+ * The skins that could show a slot's placeholder: under `all` every skin
111
+ * filling it — several is the file-order question — and under a named skin
112
+ * the one it resolves to, or none.
113
+ */
114
+ export function fillingSkins(doc: CompiledDocument, slot: string, placeholder: string): string[] {
115
+ if (doc.skin === CORE_ALL_SKINS) return doc.skins.filter((k) => k.attachments[slot]?.[placeholder] !== undefined).map((k) => k.name);
116
+ const first = lookupSkins(doc).find((k) => k.attachments[slot]?.[placeholder] !== undefined);
117
+ return first === undefined ? [] : [first.name];
118
+ }
119
+
120
+ /** Whether a slot's timelines — its colour and attachment timelines, and the deform and sequence timelines on what it shows — apply: `slotTimelinesApply` unless a plant passes another. */
121
+ export type SlotTimelineGate = (doc: CompiledDocument, slot: ModelSlot, active: ReadonlySet<string>) => boolean;
122
+
123
+ /** A slot's timelines apply exactly when its bone is active under the skin posed (the header's measurement). */
124
+ export const slotTimelinesApply: SlotTimelineGate = (_doc, slot, active) => active.has(slot.bone);
125
+
126
+ /** Whether an applied skin's list for `kind` names the constraint — what applies a skin-required one (`./constraints.ts`'s header). */
127
+ export function listedByAppliedSkin(doc: CompiledDocument, kind: keyof CoreSkin['constraints'], name: string): boolean {
128
+ return appliedSkins(doc).some((k) => k.constraints[kind].includes(name));
129
+ }
@@ -0,0 +1,469 @@
1
+ /**
2
+ * The draw's last input the core lacked (issue #967, step 3c of issue #380):
3
+ * each drawn attachment's atlas PAGE and its PAGE UVs — the oracle's `uvs`
4
+ * block, what `src/render.ts`'s `pieceOf` reads off `sequence.regions[index]`
5
+ * and `sequence.getUVs(index)` — and `MeshAttachment.computeUVs`'s job, the
6
+ * mapping texture substitution calls, as pure functions.
7
+ *
8
+ * The page layout is not a model record (`ModelAtlasRect`'s 🔸 in
9
+ * `src/model.ts`: the page, `x`, `y` and `rotate` are the packer's
10
+ * arrangement, not the drawing). It reaches these rules as a lookup
11
+ * (`UvLookup`), so nothing here links the runtime or opens a file: since
12
+ * issue #1016 from the document's own `pages` section, which `build` spells
13
+ * from the atlas it writes (`documentPageLookup` below); for a
14
+ * `rigc-compiled/1` document, which has none, from the atlas beside it, read
15
+ * by rigc's own reader (`parseAtlasText` and `atlasRegionLookup` in
16
+ * `src/atlas.ts`).
17
+ *
18
+ * ## How every rule below was fixed
19
+ *
20
+ * By loading hand-written atlases and skeletons through spine-core 4.3.13
21
+ * (`TextureAtlas`, `AtlasAttachmentLoader`, `SkeletonJson`) and reading what
22
+ * the loaded attachments hold — `sequence.getUVs(i)`, `sequence.regions[i]`,
23
+ * `region.page.name` — and what `MeshAttachment.computeUVs` writes, then
24
+ * comparing a formula over the atlas's own numbers, bit for bit (not on the
25
+ * oracle's grid). The runtime's source was not read. Regions were random:
26
+ * pages of power-of-two and other sizes, one to three pages, `bounds` anywhere
27
+ * on the page, 70% trimmed (`offsets` other than `0, 0, width, height`), and
28
+ * `rotate` spelled `0`, `90`, `180`, `270`, `true`, `false` and, for the
29
+ * branch rules, `45`, `-90`, `360` and `450`; mesh and art UVs spelled with
30
+ * five non-float32 decimals, some outside `[0, 1]`.
31
+ *
32
+ * ### The region's own numbers
33
+ *
34
+ * With `W, H` the page's `size`, the region's `u = x/W`, `v = y/H`, and its far
35
+ * corner `u2 = (x + width)/W`, `v2 = (y + height)/H` — with `width` and
36
+ * `height` exchanged at `rotate: 90` (and `true`) ONLY. 1,500 of 1,500
37
+ * regions, as doubles; exchanged at 270 as well missed 262, never exchanged
38
+ * 484. (That 270 is not exchanged is the runtime's, and the reason
39
+ * `pageFootprint` in `src/atlas.ts` derives the page rectangle rather than
40
+ * reading these two numbers.)
41
+ *
42
+ * ### A region attachment's four UVs
43
+ *
44
+ * In the corner order of its world vertices (bottom-left, upper-left,
45
+ * upper-right, bottom-right — `./vertices.ts`): at `degrees === 90`
46
+ * `(u2, v2) (u, v2) (u, v) (u2, v)`, at every other value — 0, 180, 270 and
47
+ * the odd spellings alike — `(u, v2) (u, v) (u2, v) (u2, v2)`; each held as
48
+ * float32. 1,500 of 1,500 through `Math.fround`; as doubles, 575 missed. The
49
+ * readings rejected, on the same 1,500:
50
+ *
51
+ * | reading | misses |
52
+ * | --- | ---: |
53
+ * | 270 turned as 90 over a transposed rectangle | 262 |
54
+ * | 90's corners the other way round, `(u, v) (u2, v) (u2, v2) (u, v2)` | 487 |
55
+ * | 180 turned, `(u2, v) (u2, v2) (u, v2) (u, v)` | 260 |
56
+ * | the trim inset into the UVs | 724 |
57
+ *
58
+ * The trim does not enter a region's UVs: it enters its corners
59
+ * (`regionCorners` in `./vertices.ts`, through `ModelAtlasRect`).
60
+ *
61
+ * ### A mesh's page UVs — `MeshAttachment.computeUVs`
62
+ *
63
+ * From the attachment's own UVs `(s, t)` — art space, over the untrimmed
64
+ * drawing — per `degrees`, in doubles, starting from `u = x/W`, `v = y/H`:
65
+ *
66
+ * | degrees | `u −=` | `v −=` | span `w, h` | `(u′, v′)` |
67
+ * | --- | --- | --- | --- | --- |
68
+ * | 90 | `(oh − oy − height)/W` | `(ow − ox − width)/H` | `oh/W, ow/H` | `u + t·w, v + (1 − s)·h` |
69
+ * | 180 | `(ow − ox − width)/W` | `oy/H` | `ow/W, oh/H` | `u + (1 − s)·w, v + (1 − t)·h` |
70
+ * | 270 | `oy/W` | `ox/H` | `oh/W, ow/H` | `u + (1 − t)·w, v + s·h` |
71
+ * | any other | `ox/W` | `(oh − oy − height)/H` | `ow/W, oh/H` | `u + s·w, v + t·h` |
72
+ *
73
+ * (`ox, oy, ow, oh` the region's `offsetX, offsetY, originalWidth,
74
+ * originalHeight`; `width, height` its `bounds` size.) The subtraction is its
75
+ * own step: called into a plain array, the rule read 6,000 of 6,000 calls bit
76
+ * for bit in doubles, while one fraction `(x − ox)/W` missed 936 and texels
77
+ * first, `(x − ox + s·ow)/W`, 2,227. Into a plain array the function writes
78
+ * these doubles — 4,000 of 4,000 calls wrote a value that is not a float32 —
79
+ * and what an attachment HOLDS is each through `Math.fround`, because its
80
+ * array is a `Float32Array`: 1,500 of 1,500 meshes, and `computeUVs` over the
81
+ * attachment's region and own UVs reproduced the held array on all 1,500. The
82
+ * rule through `Math.fround` held on 4,000 direct calls into a `Float32Array`,
83
+ * 400 per spelling, the odd spellings taking the last row.
84
+ *
85
+ * ⚠️ **The mesh's own UVs are read as the doubles the text spells, not as
86
+ * float32.** Reading them through `Math.fround` first missed 311 of 1,500
87
+ * meshes (worst 4.8e-7, one float32 step of a UV near 1); every intermediate
88
+ * in float32 missed 844. So `computeUvs` takes the model's `uvs` as they are.
89
+ *
90
+ * ### Which region, on which page
91
+ *
92
+ * - A region or mesh draws the atlas region named by its `path`, else its
93
+ * name — the oracle's `path` cell (`shownRow` in `./index.ts`). The first
94
+ * region of that name in the file wins (two regions named alike, the first
95
+ * drawn), and the name is compared as the atlas line spells it: a region
96
+ * line `art ` is not found as `art`, while CRLF line ends are not part of a
97
+ * name (`parseAtlasText` splits on them).
98
+ * - A **sequence**'s frame `i` draws `path + (start + i)`, the number padded
99
+ * with zeros to `digits` (`padStart`): `start` 1 and `digits` 0 when
100
+ * unstated — a three-frame `x` drew `x1, x2, x3`; `start 8, digits 0` drew
101
+ * `x8, x9, x10`; `start 99, digits 1` `x99, x100`; `start 5, digits 4`
102
+ * `x0005, x0006`. The atlas's own `index:` field is not how a frame is
103
+ * found: regions named `seq` with `index: 1` and `2` loaded as `Region not
104
+ * found in atlas: seq1`. Each frame draws on its own region's page: a
105
+ * four-frame series over two pages drew frames 0–1 on the first and 2–3 on
106
+ * the second, a mesh's frames alike.
107
+ * - The frame drawn is the one the pose shows (`./deform.ts`'s sequence rule,
108
+ * measured there through a region's corners), or the sequence's `setup` (0
109
+ * unstated) where no timeline set one.
110
+ * - A **linked mesh** draws its OWN `path`'s region (and its own sequence's
111
+ * frames — a link stating one drew `l1, l2, l3` from setup frame 2 whatever
112
+ * its source stated; a link stating none drew its own path) over its
113
+ * SOURCE's UVs: a link on page `a.png` whose source sat on `b.png` drew
114
+ * `a.png`, and its uvs were `computeUVs` of its own region over the
115
+ * source's `regionUVs`.
116
+ * - The page cell is the page's name as `parseAtlasText` trims it — the
117
+ * runtime's `page.name` for a page line ` p.png ` read `p.png`.
118
+ *
119
+ * ## What is posed, and what is left out
120
+ *
121
+ * Rows in the pose's draw order, one per slot showing a region, a mesh or a
122
+ * linked mesh — the slots the `attachments` block lists, whatever their bone
123
+ * or alpha. Which record a slot shows and the frame its series is at are the
124
+ * ones `./deform.ts`'s `attachmentStates` computes for the same pose, from the
125
+ * placeholders the sample's own timelines leave (`posedSlots`) and the
126
+ * sliders' applications (`posedBoneWorld`); at setup, from the setup
127
+ * placeholders and the sliders applied to the setup pose. That repeats the
128
+ * walk `poseSetup` and `poseAnimations` make, because neither returns the
129
+ * frame (issue #966 retains it in the raw entry); the gate holds the two
130
+ * walks to one answer by comparing both against the runtime.
131
+ *
132
+ * A region the atlas does not have is refused by name (`CoreInputError`):
133
+ * spine-core refuses to load the same pair. The block is left out, with the
134
+ * reason, where what a slot shows is not posed — the slots or the draw order
135
+ * absent — and where a linked mesh stating its own sequence is stepped by a
136
+ * sequence timeline counted over its source's frames, a case never measured.
137
+ *
138
+ * ## Purity
139
+ *
140
+ * As the rest of the core: nothing from the Spine runtime package, nothing
141
+ * from `src/transform.ts`, no clock, no randomness, no I/O.
142
+ */
143
+ import { activeBones, constraintRecords, CoreInputError, shownAttachment, type CompiledDocument, type CoreAnimation } from './index.ts';
144
+ import { posedBoneWorld, posedSlots } from './animation.ts';
145
+ import { applyConstraints } from './constraints.ts';
146
+ import type { SliderApplication } from './constraints_slider.ts';
147
+ import { attachmentStates } from './deform.ts';
148
+ import type { ShownGeometry } from './vertices.ts';
149
+ import type { ModelPage, ModelPageRegion } from '../model.ts';
150
+ import { worldTransforms } from './world.ts';
151
+
152
+ /** A region as these rules read it — `AtlasRegion` in `src/atlas.ts` is one, and so is spine-core's `TextureAtlasRegion`. */
153
+ export interface UvRegion {
154
+ x: number;
155
+ y: number;
156
+ width: number;
157
+ height: number;
158
+ offsetX: number;
159
+ offsetY: number;
160
+ originalWidth: number;
161
+ originalHeight: number;
162
+ degrees: number;
163
+ }
164
+
165
+ /** A page as these rules read it: its trimmed name and its `size`. */
166
+ export interface UvPage {
167
+ name: string;
168
+ width: number;
169
+ height: number;
170
+ }
171
+
172
+ /** The region an atlas draws under a name, and its page — the first of that name, as the header states; `null` when there is none. */
173
+ export type UvLookup = (name: string) => { page: UvPage; region: UvRegion } | null;
174
+
175
+ /**
176
+ * The lookup over a document's `pages` section (issue #1016): the first region
177
+ * of a name in file order, pages in order and regions in each page's order —
178
+ * the order `pagesOfAtlas` in `src/model.ts` copies from the atlas, so this is
179
+ * `atlasRegionLookup` over the same text, with no atlas read. The name is
180
+ * compared as stored, untrimmed, as the header's *Which region* states.
181
+ */
182
+ export function documentPageLookup(pages: readonly ModelPage[]): (name: string) => { page: UvPage; region: ModelPageRegion } | null {
183
+ const first = new Map<string, { page: UvPage; region: ModelPageRegion }>();
184
+ for (const page of pages) {
185
+ const at: UvPage = { name: page.name, width: page.width, height: page.height };
186
+ for (const region of page.regions) if (!first.has(region.name)) first.set(region.name, { page: at, region });
187
+ }
188
+ return (name) => first.get(name) ?? null;
189
+ }
190
+
191
+ /**
192
+ * The readings the header's tables rejected, each a switch — what the core
193
+ * suite's `CU` plants pass to show the gate names it. The functions below with
194
+ * none set are the rules measured; nothing but a plant sets one.
195
+ */
196
+ export interface UvReading {
197
+ /** A region at 90 in the other corner order, `(u, v) (u2, v) (u2, v2) (u, v2)` — the rotation flipped. */
198
+ rotationFlipped?: boolean;
199
+ /** A region at 270 turned as one at 90, over a transposed rectangle; a mesh at 270 mapped by 90's `(u + t·w, v + (1 − s)·h)`. */
200
+ turned270?: boolean;
201
+ /** A mesh's page UVs with the trim offsets dropped (`ox = oy = 0`, the original size the kept one). */
202
+ trimDropped?: boolean;
203
+ /** A mesh's own UVs read through `Math.fround` first. */
204
+ artAsFloat32?: boolean;
205
+ }
206
+
207
+ /** A region attachment's four page UVs as the runtime holds them — the header's rule, in the world corners' order. */
208
+ export function regionPageUvs(region: UvRegion, page: UvPage, reading: UvReading = {}): number[] {
209
+ const turned = region.degrees === 90 || (reading.turned270 === true && region.degrees === 270);
210
+ const u = region.x / page.width;
211
+ const v = region.y / page.height;
212
+ const u2 = (region.x + (turned ? region.height : region.width)) / page.width;
213
+ const v2 = (region.y + (turned ? region.width : region.height)) / page.height;
214
+ let out: number[];
215
+ if (turned) out = reading.rotationFlipped === true ? [u, v, u2, v, u2, v2, u, v2] : [u2, v2, u, v2, u, v, u2, v];
216
+ else out = [u, v2, u, v, u2, v, u2, v2];
217
+ return out.map(Math.fround);
218
+ }
219
+
220
+ /**
221
+ * `MeshAttachment.computeUVs`: art-space UVs over the untrimmed drawing mapped
222
+ * onto `region` of `page`, in doubles — the header's table. What texture
223
+ * substitution writes into its plain array; an attachment holds each value
224
+ * through `Math.fround` (`meshPageUvs`).
225
+ */
226
+ export function computeUvs(region: UvRegion, page: UvPage, art: readonly number[], reading: UvReading = {}): number[] {
227
+ const dropped = reading.trimDropped === true;
228
+ const ox = dropped ? 0 : region.offsetX;
229
+ const oy = dropped ? 0 : region.offsetY;
230
+ const ow = dropped ? region.width : region.originalWidth;
231
+ const oh = dropped ? region.height : region.originalHeight;
232
+ const W = page.width;
233
+ const H = page.height;
234
+ const s = reading.artAsFloat32 === true ? art.map(Math.fround) : art;
235
+ let u = region.x / W;
236
+ let v = region.y / H;
237
+ const out: number[] = [];
238
+ switch (region.degrees) {
239
+ case 90: {
240
+ u -= (oh - oy - region.height) / W;
241
+ v -= (ow - ox - region.width) / H;
242
+ const w = oh / W;
243
+ const h = ow / H;
244
+ for (let i = 0; i + 1 < s.length; i += 2) out.push(u + s[i + 1] * w, v + (1 - s[i]) * h);
245
+ return out;
246
+ }
247
+ case 180: {
248
+ u -= (ow - ox - region.width) / W;
249
+ v -= oy / H;
250
+ const w = ow / W;
251
+ const h = oh / H;
252
+ for (let i = 0; i + 1 < s.length; i += 2) out.push(u + (1 - s[i]) * w, v + (1 - s[i + 1]) * h);
253
+ return out;
254
+ }
255
+ case 270: {
256
+ u -= oy / W;
257
+ v -= ox / H;
258
+ const w = oh / W;
259
+ const h = ow / H;
260
+ if (reading.turned270 === true) for (let i = 0; i + 1 < s.length; i += 2) out.push(u + s[i + 1] * w, v + (1 - s[i]) * h);
261
+ else for (let i = 0; i + 1 < s.length; i += 2) out.push(u + (1 - s[i + 1]) * w, v + s[i] * h);
262
+ return out;
263
+ }
264
+ default: {
265
+ u -= ox / W;
266
+ v -= (oh - oy - region.height) / H;
267
+ const w = ow / W;
268
+ const h = oh / H;
269
+ for (let i = 0; i + 1 < s.length; i += 2) out.push(u + s[i] * w, v + s[i + 1] * h);
270
+ return out;
271
+ }
272
+ }
273
+ }
274
+
275
+ /** A mesh's page UVs as the attachment holds them: `computeUvs`, each through `Math.fround`. */
276
+ export function meshPageUvs(region: UvRegion, page: UvPage, art: readonly number[], reading: UvReading = {}): number[] {
277
+ return computeUvs(region, page, art, reading).map(Math.fround);
278
+ }
279
+
280
+ /** A record's series, as the parser reads the four fields: `start` 1, `digits` 0 and `setup` 0 when unstated. */
281
+ export interface UvSequence {
282
+ count: number;
283
+ start: number;
284
+ digits: number;
285
+ setup: number;
286
+ }
287
+
288
+ /** Every record's series, by `skin/slot/placeholder` — `readUvSequences`. */
289
+ export type UvSequences = ReadonlyMap<string, UvSequence>;
290
+
291
+ /** The region a series' frame draws: `path + (start + frame)`, padded with zeros to `digits` — the header's rule. */
292
+ export function frameRegionName(path: string, sequence: UvSequence, frame: number): string {
293
+ return path + String(sequence.start + frame).padStart(sequence.digits, '0');
294
+ }
295
+
296
+ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
297
+ const whole = (v: unknown, min: number): v is number => typeof v === 'number' && Number.isInteger(v) && v >= min;
298
+
299
+ /**
300
+ * Every record's `sequence` in a `rigc-compiled/1` document, as the parser
301
+ * reads it — `readModel` keeps a series' count and nothing else of it, and a
302
+ * frame's region name needs `start` and `digits`. Refuses, naming the path, a
303
+ * field that is not a whole number (`count` of at least 1, the others of at
304
+ * least 0) or a `setup` that is not one of the frames.
305
+ */
306
+ export function readUvSequences(document: unknown): UvSequences {
307
+ const out = new Map<string, UvSequence>();
308
+ const problems: string[] = [];
309
+ const skins = isRecord(document) && Array.isArray(document.skins) ? document.skins : [];
310
+ skins.forEach((skin: unknown, k: number) => {
311
+ if (!isRecord(skin) || typeof skin.name !== 'string' || !isRecord(skin.attachments)) return;
312
+ for (const [slot, table] of Object.entries(skin.attachments)) {
313
+ if (!isRecord(table)) continue;
314
+ for (const [placeholder, record] of Object.entries(table)) {
315
+ if (!isRecord(record) || record.sequence === undefined) continue;
316
+ const where = `skins[${k}] "${skin.name}".attachments["${slot}"]["${placeholder}"].sequence`;
317
+ const seq = record.sequence;
318
+ if (!isRecord(seq)) {
319
+ problems.push(`${where} is not an object`);
320
+ continue;
321
+ }
322
+ const before = problems.length;
323
+ if (!whole(seq.count, 1)) problems.push(`${where}: count is ${JSON.stringify(seq.count) ?? 'absent'}, not a whole number of at least 1`);
324
+ for (const key of ['start', 'digits', 'setup'] as const) if (seq[key] !== undefined && !whole(seq[key], 0)) problems.push(`${where}: ${key} is ${JSON.stringify(seq[key])}, not a whole number of at least 0`);
325
+ if (problems.length !== before) continue;
326
+ const count = seq.count as number;
327
+ const setup = (seq.setup as number | undefined) ?? 0;
328
+ if (setup >= count) {
329
+ problems.push(`${where}: setup is ${setup}, and a ${count}-frame series has frames 0 to ${count - 1}`);
330
+ continue;
331
+ }
332
+ out.set(`${skin.name}/${slot}/${placeholder}`, { count, start: (seq.start as number | undefined) ?? 1, digits: (seq.digits as number | undefined) ?? 0, setup });
333
+ }
334
+ }
335
+ });
336
+ if (problems.length > 0) throw new CoreInputError(`the model document's sequences: ${problems.join('; ')}`);
337
+ return out;
338
+ }
339
+
340
+ /** What `poseUvs` reads besides the pose: the atlas lookup, the document's series, and a plant's reading. */
341
+ export interface UvSource {
342
+ lookup: UvLookup;
343
+ sequences: UvSequences;
344
+ reading?: UvReading;
345
+ }
346
+
347
+ /** One row of `uvs`: `[slot, attachment, page, uvs]`, the oracle's row. */
348
+ export type CoreUvRow = [string, string, string, Array<number | null>];
349
+
350
+ /**
351
+ * Each slot's shown record and its series' frame at the setup pose — what
352
+ * `poseSetup` hands `poseGeometry`: the setup placeholders, and the sliders
353
+ * applied to the setup bones (`applyConstraints`), each slider's attachment,
354
+ * deform and sequence keys applied in order by `attachmentStates`.
355
+ */
356
+ export function shownAtSetup(doc: CompiledDocument): { shown: ShownGeometry[]; why: string[] } {
357
+ const active = activeBones(doc);
358
+ const applied: SliderApplication[] = [];
359
+ applyConstraints(doc.bones, worldTransforms(doc.bones, active), active, constraintRecords(doc), null, applied);
360
+ const placeholders = new Map(doc.slots.map((s) => [s.name, s.setup]));
361
+ return attachmentStates(doc, shownAttachment, placeholders, null, applied);
362
+ }
363
+
364
+ /**
365
+ * Each slot's shown record and its series' frame at time `t` of `anim` — what
366
+ * `poseAnimations` hands `poseGeometry` under `--physics none`: the sliders
367
+ * `posedBoneWorld` applies at `t`, the placeholders the sample's own
368
+ * timelines leave (`posedSlots`), then `attachmentStates`.
369
+ */
370
+ export function shownAtSample(doc: CompiledDocument, anim: CoreAnimation, t: number): { shown: ShownGeometry[]; why: string[] } {
371
+ const sliders: SliderApplication[] = [];
372
+ // The bones are posed for what the sliders apply, and only a slider constraint applies one (`applySlider`): with none, the list stays empty and the pose would be read for nothing (issue #1134).
373
+ if (doc.constraints.some((c) => c.kind === 'slider')) posedBoneWorld(doc, anim.timelines, t, {}, anim.constraints, sliders);
374
+ const placeholders = new Map<string, string | null>();
375
+ posedSlots(doc, anim.timelines, t, {}, sliders, placeholders);
376
+ return attachmentStates(doc, shownAttachment, placeholders, { timelines: anim.timelines, t }, sliders);
377
+ }
378
+
379
+ /** One drawn attachment resolved to its atlas region: what `poseUvs` maps, and what a census reads. */
380
+ export interface DrawnRegion {
381
+ slot: string;
382
+ /** The attachment's name — the row's second cell. */
383
+ name: string;
384
+ kind: 'region' | 'mesh' | 'linkedmesh';
385
+ /** The region name drawn: the path, with the frame's number for a series. */
386
+ region: string;
387
+ /** The series' frame drawn, or `null` for a record with no series. */
388
+ frame: number | null;
389
+ found: { page: UvPage; region: UvRegion };
390
+ /** The attachment's own UVs for a mesh — a linked mesh's source's — or `null` for a region. */
391
+ art: readonly number[] | null;
392
+ }
393
+
394
+ /**
395
+ * Every slot of `order` showing a region, a mesh or a linked mesh, resolved to
396
+ * the atlas region it draws — the header's *Which region, on which page* — or
397
+ * why the pose is not posed. A region the atlas lacks is refused by name.
398
+ */
399
+ export function drawnRegions(doc: CompiledDocument, shown: readonly ShownGeometry[], order: readonly string[], source: UvSource): { drawn: DrawnRegion[] | null; why: string | null } {
400
+ const bySlot = new Map(shown.map((s) => [s.slot, s]));
401
+ const drawn: DrawnRegion[] = [];
402
+ const unmeasured: string[] = [];
403
+ for (const slot of order) {
404
+ const s = bySlot.get(slot);
405
+ if (s === undefined) continue;
406
+ const g = s.geometry;
407
+ if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') continue;
408
+ const record = doc.skins.find((k) => k.name === s.skin)?.attachments[s.slot]?.[s.placeholder];
409
+ if (record === undefined) throw new CoreInputError(`slot "${s.slot}": skin "${s.skin}" files no record under placeholder "${s.placeholder}" (readModel refuses it first)`);
410
+ const path = record.path ?? s.name;
411
+ const sequence = source.sequences.get(`${s.skin}/${s.slot}/${s.placeholder}`);
412
+ if (g.kind === 'linkedmesh' && sequence !== undefined && s.frame !== undefined) {
413
+ const sourceCount = doc.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.sequenceCount;
414
+ if (sourceCount !== sequence.count) {
415
+ unmeasured.push(`slot "${s.slot}" shows linked mesh "${s.name}", whose own ${sequence.count}-frame series a sequence timeline steps over its source's ${sourceCount ?? 'unstated'} frame(s)`);
416
+ continue;
417
+ }
418
+ }
419
+ const frame = sequence === undefined ? null : (s.frame ?? sequence.setup);
420
+ const name = sequence === undefined || frame === null ? path : frameRegionName(path, sequence, frame);
421
+ const found = source.lookup(name);
422
+ if (found === null) {
423
+ throw new CoreInputError(
424
+ `slot "${s.slot}" shows "${s.name}", which draws atlas region "${name}"${frame === null ? '' : ` (frame ${frame} of its series over "${path}")`}, and the atlas has no region of that name — spine-core refuses to load the pair ("Region not found in atlas")`,
425
+ );
426
+ }
427
+ let art: readonly number[] | null = null;
428
+ if (g.kind === 'mesh') art = g.uvs;
429
+ else if (g.kind === 'linkedmesh') {
430
+ // A linked mesh draws its source's UVs over its own region (the header's *Which region*).
431
+ const from = doc.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
432
+ if (from?.kind !== 'mesh') throw new CoreInputError(`slot "${s.slot}": the linked mesh's source "${g.source}" carries no uvs (readModel refuses it first)`);
433
+ art = from.uvs;
434
+ }
435
+ drawn.push({ slot: s.slot, name: s.name, kind: g.kind, region: name, frame, found, art });
436
+ }
437
+ if (unmeasured.length > 0) return { drawn: null, why: `${unmeasured.join('; ')} — which of the two counts the runtime steps it over was not measured` };
438
+ return { drawn, why: null };
439
+ }
440
+
441
+ /**
442
+ * The `uvs` rows of one pose: `shown` (from `shownAtSetup`/`shownAtSample`) in
443
+ * `order`, the pose's draw order, every number through `round` (the oracle's,
444
+ * `gridRound`) — or why the block is left out. A region the atlas lacks is
445
+ * refused by name.
446
+ */
447
+ export function poseUvs(doc: CompiledDocument, shown: readonly ShownGeometry[], order: readonly string[], source: UvSource, round: (v: number) => number | null): { rows: CoreUvRow[] | null; why: string | null } {
448
+ const reading = source.reading ?? {};
449
+ const { drawn, why } = drawnRegions(doc, shown, order, source);
450
+ if (drawn === null) return { rows: null, why };
451
+ const rows = drawn.map((d): CoreUvRow => {
452
+ const uvs = d.art === null ? regionPageUvs(d.found.region, d.found.page, reading) : meshPageUvs(d.found.region, d.found.page, d.art, reading);
453
+ return [d.slot, d.name, d.found.page.name, uvs.map(round)];
454
+ });
455
+ return { rows, why: null };
456
+ }
457
+
458
+ /** Why a document's `uvs` blocks are not posed under the stepped phase, or null: a slider keying a slot's attachment or a series — its time is read off bones the step moves, and this walk poses them unstepped. */
459
+ export function steppedUvsWhy(doc: CompiledDocument): string | null {
460
+ const keyed = doc.constraints.flatMap((c) => {
461
+ if (c.kind !== 'slider') return [];
462
+ const anim = doc.animations.find((a) => a.name === c.animation);
463
+ if (anim === undefined) return [];
464
+ const switches = anim.timelines.slots.some((sl) => sl.timelines.some((tl) => tl.kind === 'attachment'));
465
+ const frames = anim.timelines.attachments.some((a) => a.sequence !== null);
466
+ return switches || frames ? [`slider "${c.name}" applies animation "${c.animation}", which keys ${switches ? 'a slot\'s attachment' : 'a sequence'}`] : [];
467
+ });
468
+ return keyed.length === 0 ? null : `${keyed.join('; ')} — under --physics step its time is read off stepped bones, and the uvs walk poses the sliders unstepped`;
469
+ }