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,732 @@
1
+ /**
2
+ * What a `deform` key does to the geometry, measured once.
3
+ *
4
+ * ⭐ **One survey, two consumers.** `A39_DEFORM_KEEPS_TRIANGLE_WINDING` reads the
5
+ * reversals out of it and refuses a build; `explain`'s `DEFORM` block prints the
6
+ * whole of it and refuses nothing. That split is issue
7
+ * [#296](https://github.com/firejune/rigc/issues/296)'s two halves —
8
+ * [#314](https://github.com/firejune/rigc/pull/314) landed the assertion and
9
+ * [#316](https://github.com/firejune/rigc/issues/316) the report — and the reason
10
+ * they share this file rather than each posing the skeleton themselves is that
11
+ * the report's `winding 32 of 32 kept` and A39's `8 of 32 reverse` are **the same
12
+ * count**. Two derivations of one number drift, and the one that drifts silently
13
+ * is the one nobody exits non-zero on.
14
+ *
15
+ * ## The frame, and why it is the posed one
16
+ *
17
+ * Both sides of every comparison here are taken at the key's OWN time with the
18
+ * animation applied: the deformed mesh against **the same posed bones with the
19
+ * deform cleared**. Holding the bones at setup instead was tried in #314 and is
20
+ * wrong in principle — a weighted mesh's offsets are authored in bone space
21
+ * against the pose they land in, so setup bones measure a pose that never occurs.
22
+ * Sharing the bones between the two sides is also what makes a MIRRORED slot bone
23
+ * a non-event: a negative determinant flips every triangle on both sides and
24
+ * cancels.
25
+ *
26
+ * ⇒ So every ratio on a key is *the deform's own contribution*, and the
27
+ * denominator is 1.000 by construction rather than by measurement. A block that
28
+ * printed `(setup 1.000)` beside it would be printing the definition.
29
+ *
30
+ * ## What is deliberately NOT here
31
+ *
32
+ * **Deformed coverage.** #296 asked for it and it does not exist: `coverage` is
33
+ * rasterised from the attachment's **uvs** against the part's alpha
34
+ * (`measureAuthoredMeshFit`, called from `src/compile.ts`), and a deform moves
35
+ * positions and never uvs. The figure is therefore identical at every key of
36
+ * every timeline, so a `coverage 100.00% (setup 100.00%)` line would be a
37
+ * tautology dressed as a measurement. What actually moves — how much art each
38
+ * drawn pixel now carries — is the stretch below, and `DR04` in `selftest.ts` is
39
+ * the control that says the coverage figure cannot move.
40
+ *
41
+ * ## What the geometry is not enough to say (issue #401)
42
+ *
43
+ * A winding is a claim about **drawn** pixels: A39's own message says the mesh
44
+ * "draws its texture backwards there", and that sentence is false when the slot
45
+ * draws nothing at that time. So each key also carries `draw` — the attachment
46
+ * the slot actually shows and the alpha it shows it at, both read off the same
47
+ * posed skeleton the geometry came from. A key whose `draw.blank` is set is
48
+ * measured and then **passed over by name**: a triangle that draws no pixels
49
+ * cannot draw them backwards. It is per key and per time, never per slot —
50
+ * `invariants.deformMayFold` is the per-slot instrument and it is a declaration,
51
+ * not a measurement.
52
+ *
53
+ * ## And what a key is not enough to say either (issue #403)
54
+ *
55
+ * The keys are where the data is; they are not where the runtime is. Between two
56
+ * of them it interpolates, so a deform inside its fold angle at every key can be
57
+ * past it in between and no key-time measurement looks there. `scanDeformSpan`
58
+ * closes that, and the derivation is in its own comment: the reversal condition
59
+ * over a span has a **closed form** — a quadratic in the interpolation fraction —
60
+ * so the time is solved for rather than searched, and the measurement taken there
61
+ * is this file's ordinary one, at a time no key lands on.
62
+ *
63
+ * ## And WHICH frame, when the animation is never on a track (issue #407)
64
+ *
65
+ * Everything above says *at the key's own time*, and until #407 that meant one
66
+ * thing: the animation played on track 0. An animation a **slider** applies is
67
+ * never played that way — spine-core says so itself, in
68
+ * `SkeletonData.findSliderAnimations`: *"Slider animations are designed to be
69
+ * applied by slider constraints rather than on their own."* The slider picks the
70
+ * time out of a bone property, so **the key's time and the applied time are the
71
+ * same number by construction**; posing the animation on a track while its own
72
+ * slider applies it at the neutral is a frame no playthrough contains, and it
73
+ * reported a fold on a rig that is correct.
74
+ *
75
+ * ⇒ So a deform key is posed at the **reach** its animation actually has
76
+ * (`DeformReach`): on a track when nothing applies it, and otherwise once per
77
+ * slider, with that slider's own mapping inverted and its driving bone moved
78
+ * until the runtime selects this key's time. Inverting the constraint away
79
+ * instead was considered and refused for A39's own reason — a slider's animation
80
+ * may carry bone tracks that move the very bones the offsets are authored
81
+ * against, so dropping it reintroduces "setup bones measure a pose that never
82
+ * occurs" one level up.
83
+ *
84
+ * ⚠️ **What the artifact cannot say, and this therefore does not:** whether a
85
+ * slider's animation is ALSO played on a track somewhere. Nothing in skeleton
86
+ * data records that, so a slider-applied animation is measured in its slider
87
+ * frames only. Two sliders on one animation are two frames and both are measured
88
+ * — one frame's pass never hides another's fold — but a consumer that plays a
89
+ * slider animation on a track as well is outside what this can see.
90
+ *
91
+ * ## Where the halves live (issue #1025, cut 4c-3)
92
+ *
93
+ * Everything above that names no runtime class — the measurements, the
94
+ * survey's interface to its posers, the core's poser and the survey off a
95
+ * model document — is `./deformsurvey.ts`, moved there unchanged so that
96
+ * A39's model-side supplier can reach the survey without reaching spine-core.
97
+ * This file is spine-core's reader and poser and the entries that choose
98
+ * between the two, and re-exports every name it exported before.
99
+ */
100
+ import {
101
+ AnimationState,
102
+ AnimationStateData,
103
+ type Attachment,
104
+ AtlasAttachmentLoader,
105
+ type Bone,
106
+ type CurveTimeline,
107
+ DeformTimeline,
108
+ FromProperty,
109
+ FromRotate,
110
+ FromScaleX,
111
+ FromScaleY,
112
+ FromShearY,
113
+ FromX,
114
+ FromY,
115
+ MeshAttachment,
116
+ Physics,
117
+ Skeleton,
118
+ type SkeletonData,
119
+ SkeletonJson,
120
+ type Skin,
121
+ Slider,
122
+ SliderData,
123
+ TextureAtlas,
124
+ type Timeline,
125
+ } from '@esotericsoftware/spine-core';
126
+ // #969: the core's side of the seam — the model document read.
127
+ import { readModel } from './core/index.ts';
128
+ // #1019: what the survey reads of the skeleton's structure, and the model document's reading of it.
129
+ import {
130
+ modelStructure,
131
+ type SurveyAnimation,
132
+ type SurveyCurve,
133
+ type SurveyDeformTimeline,
134
+ type SurveyMesh,
135
+ type SurveySkin,
136
+ type SurveySlider,
137
+ type SurveyStructure,
138
+ } from './deformstructure.ts';
139
+ // Issue #1025 (cut 4c-3): the half of the survey that names no runtime class.
140
+ import { BEZIER_POINTS, corePoser, surveyWith, type DeformSurvey, type DialField, type ShownReading, type SurveyPose, type SurveyPoser } from './deformsurvey.ts';
141
+ // Issue #1052: the choice of reader and poser over a build is `./deformbuild.ts`'s, where an entry that links nothing of
142
+ // the runtime can load it; this file's half of it — the survey through spine-core — is registered into the seam below.
143
+ import type { DeformSurveyInput } from './deformbuild.ts';
144
+ import { registerSpineSurvey, SpineRuntimeError, spineRuntimeSentence, SURVEY_RUNTIME_TAIL } from './spine_side.ts';
145
+
146
+ export { BEZIER_POINTS, DEFORM_AREA_EPSILON, float32AreaNoise, stretchSingularValues, surveyOfModel, triangleAreas, unreachableWhy } from './deformsurvey.ts';
147
+ export { surveyOfBuild } from './deformbuild.ts';
148
+ export type { DeformSurveyInput } from './deformbuild.ts';
149
+ export type {
150
+ DeformDial,
151
+ DeformDialDispute,
152
+ DeformDialTie,
153
+ DeformExtreme,
154
+ DeformFrameMeasure,
155
+ DeformKeyDraw,
156
+ DeformKeyMeasure,
157
+ DeformReach,
158
+ DeformReversal,
159
+ DeformSpan,
160
+ DeformSpanCurve,
161
+ DeformSurvey,
162
+ DeformSurveyRecord,
163
+ DeformSurveySource,
164
+ DialSession,
165
+ DialSpan,
166
+ ShownReading,
167
+ SurveyPose,
168
+ SurveyPoser,
169
+ } from './deformsurvey.ts';
170
+
171
+
172
+ /**
173
+ * Load an emitted pair through the real spine-core, without touching the pages.
174
+ *
175
+ * `TextureAtlas` needs only the atlas TEXT — the page sizes and region rectangles
176
+ * are in it — so this runs on a build whose `--out` was never written, which is
177
+ * what `explain` is. Reading the PNGs is `posableFromText`'s job and it needs
178
+ * them because it rasterises.
179
+ */
180
+ export function skeletonDataFromText(skeletonText: string, atlasText: string): SkeletonData {
181
+ const atlas = new TextureAtlas(atlasText);
182
+ return new SkeletonJson(new AtlasAttachmentLoader(atlas)).readSkeletonData(JSON.parse(skeletonText));
183
+ }
184
+
185
+ /**
186
+ * Measure every deform key of every animation.
187
+ *
188
+ * `exempt` holds slot names to pass over — A39 hands it `invariants.deformMayFold`
189
+ * so a declared fold is not measured at all. ⚠️ The **report** hands it an empty
190
+ * set on purpose: an exempted slot is the one an author most wants figures for,
191
+ * and a report that went quiet where the gate does would leave the only surface
192
+ * that can say anything about a declared fold saying nothing.
193
+ *
194
+ * ⭐ Not the same thing as `draw.blank` on a key, and the difference is the whole
195
+ * of issue #401: `exempt` is a **declaration** about a slot for all time and is
196
+ * not measured, `draw.blank` is a **measurement** of one key at one time and
197
+ * cannot be declared. A key that draws nothing is still surveyed and still
198
+ * printed; what it is not is gated.
199
+ */
200
+ export function surveyDeformKeys(data: SkeletonData, exempt: ReadonlySet<string> = new Set()): DeformSurvey {
201
+ const side = runtimeSide(data);
202
+ return { ...surveyWith(side.structure, exempt, side.poser), source: { used: 'spine-core', why: null } };
203
+ }
204
+
205
+ /**
206
+ * Which `BonePose` field spine-core's own reader is named for — off the ARTIFACT,
207
+ * which is the second, independent answer the probe below is checked against
208
+ * (issue #419).
209
+ *
210
+ * ⭐ This is an identity, not a dispatch table. `SliderData.property` is one of
211
+ * six classes the parser built out of the rig spec's `property` field, so asking
212
+ * which class it is asks the file what the author wrote; it says nothing about
213
+ * *how* the value is computed, which is the part #407 was careful never to
214
+ * transcribe and which the probe still measures.
215
+ *
216
+ * `null` for a reader this file does not know — unreachable against spine-core
217
+ * 4.3, which has exactly these six, and deliberately not folded into a default:
218
+ * a seventh reader in some later runtime must be **named** in the report, not
219
+ * silently mapped to whatever the probe happened to find.
220
+ */
221
+ function readerField(property: FromProperty): DialField | null {
222
+ if (property instanceof FromRotate) return 'rotation';
223
+ if (property instanceof FromX) return 'x';
224
+ if (property instanceof FromY) return 'y';
225
+ if (property instanceof FromScaleX) return 'scaleX';
226
+ if (property instanceof FromScaleY) return 'scaleY';
227
+ if (property instanceof FromShearY) return 'shearY';
228
+ return null;
229
+ }
230
+
231
+ /**
232
+ * A fresh skeleton with `skin` worn, which is what every pose below starts from.
233
+ *
234
+ * ## Why a skin, and why this one
235
+ *
236
+ * A deform timeline is keyed on a `skin / slot / attachment` triple, so the mesh
237
+ * it deforms belongs to exactly one skin — and a skeleton nobody dressed shows
238
+ * only what `SkeletonData.defaultSkin` holds. Posing with no skin therefore read
239
+ * a slot that showed **nothing** for every mesh an author had moved into a named
240
+ * skin, and the whole survey then reported the rig as undrawn: `A39` went from
241
+ * PASS to SKIP with a sentence that blamed the rig for what the measurement was
242
+ * doing (issue #583). `placementOf` already recovers which skin holds a
243
+ * timeline's attachment, so the pose can wear it.
244
+ *
245
+ * ## What `setSkin` changes, off the runtime rather than from memory
246
+ *
247
+ * `Skeleton.setSkinBySkin` (spine-core 4.3.13 `Skeleton.js:292-313`) puts the
248
+ * skin's art into each slot's pose and calls `updateCache`, and `updateCache`
249
+ * (`Skeleton.js:142-187`) is where the other two thirds live: a `skinRequired`
250
+ * bone is `active` only if the worn skin lists it, and a `skinRequired`
251
+ * constraint only if `skin.constraints` includes it. Both were measured on this
252
+ * fixture before the repair and both were silent in their own way —
253
+ *
254
+ * - a slider whose driving bone is skin-required reads a world property that
255
+ * **nothing moves** while the bone is inactive, so `planDial` found no
256
+ * responding field, returned `null`, and the animation was reported as
257
+ * *"played on a track"* — an animation only a slider ever applies;
258
+ * - a skin-required slider **constraint** is left out of the update cache, so
259
+ * `SliderPose.time` never leaves its setup value and every key came back as
260
+ * *"at a time no dial selects"*.
261
+ *
262
+ * Each pose below then calls `setupPose()`, which re-resolves every slot's setup
263
+ * attachment through `Skeleton.getAttachment` — the worn skin first, then
264
+ * `defaultSkin` (`Skeleton.js:335-346`) — so wearing the skin before the pose is
265
+ * the whole of what is needed and nothing has to be re-attached afterwards.
266
+ *
267
+ * ⭐ It takes the `Skin` **object**, not its name. `placementOf` found it by
268
+ * identity, and `findSkin` resolves a name to the FIRST skin that carries it —
269
+ * so a round trip through the name would hand the runtime a different skin on a
270
+ * skeleton that declares two of one name, and there would be nothing in the
271
+ * output to say so. `src/render.ts`'s `skeletonUnderSkin` takes a name because
272
+ * its name came from `--skin` on the command line and refusing an unknown one
273
+ * **by name, with the names that would have worked** is the whole of its job;
274
+ * here there is no name to refuse and no lookup that can fail.
275
+ */
276
+ function skeletonUnderSkin(data: SkeletonData, skin: Skin | null): Skeleton {
277
+ const skeleton = new Skeleton(data);
278
+ if (skin !== null) skeleton.setSkin(skin);
279
+ return skeleton;
280
+ }
281
+
282
+ /** The `Slider` on `skeleton` that `data` describes, or `null`. */
283
+ function sliderOn(skeleton: Skeleton, data: SliderData): Slider | null {
284
+ for (const constraint of skeleton.constraints) {
285
+ if (constraint instanceof Slider && constraint.data === data) return constraint;
286
+ }
287
+ return null;
288
+ }
289
+
290
+ /**
291
+ * The `offsets` argument every `FromProperty.value` takes.
292
+ *
293
+ * `Slider.offsets` is a private all-zero array — a slider has no per-property
294
+ * offset the way a transform constraint does — so this is that constant, spelled
295
+ * out because it cannot be imported.
296
+ */
297
+ const DIAL_ZERO_OFFSETS = [0, 0, 0, 0, 0, 0];
298
+
299
+ /**
300
+ * The skeleton of `data`, posed by `animation` at `time`.
301
+ *
302
+ * By the same route A10 steps an animation, and with a fresh state every call:
303
+ * that is what lands the sample exactly ON `time` rather than one update short of
304
+ * it, and it is why a probe between two keys is as trustworthy as a key.
305
+ */
306
+ function poseAt(data: SkeletonData, skin: Skin | null, animation: string, time: number): Skeleton {
307
+ const posed = skeletonUnderSkin(data, skin);
308
+ const state = new AnimationState(new AnimationStateData(data));
309
+ state.setAnimation(0, animation, false);
310
+ posed.setupPose();
311
+ posed.update(0);
312
+ posed.updateWorldTransform(Physics.reset);
313
+ state.update(time);
314
+ state.apply(posed);
315
+ posed.update(time);
316
+ posed.updateWorldTransform(Physics.update);
317
+ return posed;
318
+ }
319
+
320
+ /**
321
+ * `CurveTimeline.curves`, which is `protected` and is read anyway.
322
+ *
323
+ * ⚠️ A deliberate reach into the runtime's own storage, in the one file whose
324
+ * whole job is reading what the runtime will do. The public surface is
325
+ * `getCurvePercent(time, frame)` — a fraction at a time — and the scan needs the
326
+ * inverse and the reachable range, which no sequence of forward evaluations
327
+ * gives exactly: the breakpoints of the polyline it interpolates over are
328
+ * precisely what this array holds, and any other route to them would be sampling
329
+ * with a spacing to defend. The alternative considered and rejected was
330
+ * re-deriving the sampling from the four bezier handles in the emitted JSON,
331
+ * which would be a **second** derivation of the runtime's own arithmetic — the
332
+ * thing this file's opening paragraph forbids — and would then be checking
333
+ * rigc's copy of spine-core's maths rather than spine-core's.
334
+ *
335
+ * ⚠️ That alternative is what the model document's reader does since issue
336
+ * #1019, and the objection does not reach it, because it is not a copy written
337
+ * here: it is the core's own deform curve (`bezierPolyline` with
338
+ * `DEFORM_CURVE_END`, `./core/deform.ts`), the one the core POSES with, measured
339
+ * against the runtime there. On a build the core poses, the pose and the span
340
+ * scan read one derivation rather than two, and `DM12` (the gallery) and
341
+ * `tools/survey_hashes.ts structure` (every corpus row) hold its points to this
342
+ * array's. What spine-core's reader of the survey reads
343
+ * is still this array.
344
+ *
345
+ * `A05` already gates the emitted curve arrays, and `DW18` is the control that
346
+ * the reading here matches what the runtime does with them.
347
+ *
348
+ * Exported for `validate.ts`'s `curveChannelValues` (issue #752), which asks the
349
+ * same array what values a constraint's `mix` timeline poses between its keys —
350
+ * so the reach into protected storage stays one reach, in this file.
351
+ */
352
+ export function curveStorage(timeline: CurveTimeline): ArrayLike<number> {
353
+ return (timeline as unknown as { curves: ArrayLike<number> }).curves;
354
+ }
355
+
356
+ /**
357
+ * The mesh's world vertices with one deform array written into the slot.
358
+ *
359
+ * The array is copied rather than aliased: for an unweighted attachment with no
360
+ * `vertices` on its key, `SkeletonJson` stores the ATTACHMENT'S OWN setup array
361
+ * as that key's deform, and handing the runtime a live reference to it would let
362
+ * a later write edit the mesh itself.
363
+ */
364
+ function worldWithDeform(
365
+ posed: Skeleton,
366
+ slotIndex: number,
367
+ attachment: MeshAttachment,
368
+ deform: ArrayLike<number>,
369
+ ): Float32Array {
370
+ const count = attachment.worldVerticesLength;
371
+ const slot = posed.slots[slotIndex];
372
+ const array = slot.appliedPose.deform;
373
+ array.length = deform.length;
374
+ for (let i = 0; i < deform.length; i++) array[i] = deform[i];
375
+ const world = new Float32Array(count);
376
+ attachment.computeWorldVertices(posed, slot, 0, count, world, 0, 2);
377
+ array.length = 0;
378
+ return world;
379
+ }
380
+
381
+ /**
382
+ * What one slot shows of one mesh at the pose it is currently in, and at what
383
+ * alpha — with no opinion about whether that is a reason for anything.
384
+ *
385
+ * `alpha` is `slot.color.a × attachment.color.a`, which is the product
386
+ * `src/render.ts` builds a piece's tint from; it is 0 when the slot shows some
387
+ * other attachment, because then none of this mesh is on screen. The skeleton's
388
+ * own colour is deliberately not a factor: it is runtime state a consumer sets,
389
+ * not something the skeleton data can say, and `src/render.ts` does not read it
390
+ * either.
391
+ */
392
+ function shownAt(posed: Skeleton, slotIndex: number, attachment: MeshAttachment): ShownReading {
393
+ const pose = posed.slots[slotIndex]?.appliedPose;
394
+ const shown: Attachment | null = pose?.attachment ?? null;
395
+ // The same comparison `DeformTimeline.applyToSlot` makes before it writes
396
+ // anything, so "shown" here means exactly "the runtime deforms it here".
397
+ const showsThisMesh = shown !== null && shown.timelineAttachment === attachment;
398
+ const slotAlpha = pose?.color.a ?? 0;
399
+ const attachmentAlpha = shown instanceof MeshAttachment ? shown.color.a : 1;
400
+ return {
401
+ shown: shown?.name ?? null,
402
+ showsThisMesh,
403
+ slotAlpha,
404
+ attachmentAlpha,
405
+ alpha: showsThisMesh ? slotAlpha * attachmentAlpha : 0,
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Which skin holds this attachment, and under which placeholder — the other two
411
+ * thirds of the `skin/slot/attachment` triple the format keys a deform timeline
412
+ * on, and the triple `explain` already prints for the timeline itself.
413
+ *
414
+ * ⚠️ Recovered by identity rather than by name. A `DeformTimeline` carries the
415
+ * attachment it RESOLVED to and neither the skin it came out of nor the
416
+ * placeholder it was written as, and those two are not the same string in
417
+ * general: a skin puts its own attachment behind a shared placeholder, which is
418
+ * the whole point of skins. So the scan compares the attachment object, and the
419
+ * placeholder printed is the one the spec wrote.
420
+ *
421
+ * ⭐ The `Skin` it found comes back with the name, because that object is what
422
+ * the pose wears (`skeletonUnderSkin`, issue #583). Handing the pose the name
423
+ * instead would resolve it again through `SkeletonData.findSkin`, which returns
424
+ * the FIRST skin of that name — a second resolution that can land somewhere else
425
+ * on a skeleton declaring two, and land there silently.
426
+ *
427
+ * `holder` is `null` only when no skin carries this attachment at all. Nothing
428
+ * reaches that through `SkeletonJson`, which resolves a deform timeline's
429
+ * attachment out of a skin before it can build the timeline, so it is the
430
+ * defensive branch and not a case: the pose then wears nothing and behaves
431
+ * exactly as every pose here did before #583.
432
+ */
433
+ function placementOf(
434
+ data: SkeletonData,
435
+ slotIndex: number,
436
+ attachment: Attachment,
437
+ ): { skin: string; holder: Skin | null; placeholder: string } {
438
+ for (const skin of [data.defaultSkin, ...data.skins]) {
439
+ if (!skin) continue;
440
+ const entries: Array<{ placeholder: string; attachment: unknown }> = [];
441
+ skin.getAttachmentsForSlot(slotIndex, entries as Parameters<typeof skin.getAttachmentsForSlot>[1]);
442
+ for (const entry of entries) {
443
+ if (entry.attachment === attachment) return { skin: skin.name, holder: skin, placeholder: entry.placeholder };
444
+ }
445
+ }
446
+ return { skin: 'default', holder: null, placeholder: attachment.name };
447
+ }
448
+
449
+ // --- #1019 the runtime's reading of the structure: begin ---
450
+ //
451
+ // `src/deformstructure.ts` states what the survey reads of a skeleton's
452
+ // structure and gives the model document's reading of it. This is the other
453
+ // one — the survey's reads as they always were, off spine-core's parsed
454
+ // `SkeletonData`, moved behind the interface unchanged — and the spine-core
455
+ // poser over the same objects. The two are made together because the poser
456
+ // needs the parsed objects the handles stand for: a handle the model
457
+ // document's reader made carries none, and is refused here by name.
458
+
459
+ /** The parsed objects behind the runtime reader's handles — what spine-core's poser is handed. */
460
+ interface RuntimeLinks {
461
+ skin(handle: SurveySkin | null): Skin | null;
462
+ mesh(handle: SurveyMesh): MeshAttachment;
463
+ slider(handle: SurveySlider): SliderData;
464
+ }
465
+
466
+ /**
467
+ * The slot timelines' key times on `slots` — every timeline carrying a
468
+ * `slotIndex`, duck-typed (see `scanDeformSpan`'s visibility split).
469
+ *
470
+ * ⭐ Duck-typed rather than matched against a list of classes, because a list
471
+ * is a thing that goes stale when the format grows a timeline and the failure
472
+ * would be silent.
473
+ */
474
+ function visibilityKeyTimes(timelines: readonly Timeline[], slots: ReadonlySet<number>): number[] {
475
+ const times: number[] = [];
476
+ for (const timeline of timelines) {
477
+ const carrier = timeline as unknown as { slotIndex?: unknown };
478
+ if (typeof carrier.slotIndex !== 'number' || !slots.has(carrier.slotIndex)) continue;
479
+ const entries = timeline.getFrameEntries();
480
+ for (let i = 0; i < timeline.getFrameCount(); i++) times.push(timeline.frames[i * entries]);
481
+ }
482
+ return times;
483
+ }
484
+
485
+ /**
486
+ * spine-core's parsed skeleton read as the survey's structure, and posed by
487
+ * spine-core — the survey as it always was (the section's note).
488
+ */
489
+ function runtimeSide(data: SkeletonData): { structure: SurveyStructure; poser: SurveyPoser; links: RuntimeLinks } {
490
+ const skins = new Map<Skin, SurveySkin>();
491
+ const skinBack = new Map<SurveySkin, Skin>();
492
+ const skinOf = (skin: Skin | null): SurveySkin | null => {
493
+ if (skin === null) return null;
494
+ let handle = skins.get(skin);
495
+ if (handle === undefined) {
496
+ handle = { name: skin.name };
497
+ skins.set(skin, handle);
498
+ skinBack.set(handle, skin);
499
+ }
500
+ return handle;
501
+ };
502
+ const meshes = new Map<MeshAttachment, SurveyMesh>();
503
+ const meshBack = new Map<SurveyMesh, MeshAttachment>();
504
+ const meshOf = (attachment: MeshAttachment, slotIndex: number): SurveyMesh => {
505
+ const already = meshes.get(attachment);
506
+ if (already !== undefined) return already;
507
+ const placed = placementOf(data, slotIndex, attachment);
508
+ const handle: SurveyMesh = {
509
+ name: attachment.name,
510
+ triangles: attachment.triangles,
511
+ worldVerticesLength: attachment.worldVerticesLength,
512
+ weighted: attachment.bones !== null,
513
+ timelineSlots: attachment.timelineSlots,
514
+ record: placed.holder === null ? null : { skin: placed.skin, slot: data.slots[slotIndex]?.name ?? `#${slotIndex}`, placeholder: placed.placeholder },
515
+ };
516
+ meshes.set(attachment, handle);
517
+ meshBack.set(handle, attachment);
518
+ return handle;
519
+ };
520
+ const deformOf = (timeline: DeformTimeline): SurveyDeformTimeline => {
521
+ const attachment = timeline.attachment;
522
+ const mesh = attachment instanceof MeshAttachment ? meshOf(attachment, timeline.slotIndex) : null;
523
+ return {
524
+ slotIndex: timeline.slotIndex,
525
+ mesh,
526
+ frames: timeline.frames,
527
+ vertices: (frame) => timeline.vertices[frame],
528
+ curve: (frame): SurveyCurve => {
529
+ const curves = curveStorage(timeline);
530
+ const code = curves[frame];
531
+ // 1 is STEPPED and 0 LINEAR; 2 + i is BEZIER, its sampled points starting at `i`.
532
+ if (code === 1) return { kind: 'stepped' };
533
+ if (code === 0) return { kind: 'linear' };
534
+ const points: number[] = [];
535
+ for (let i = code - 2, n = code - 2 + BEZIER_POINTS * 2; i < n; i++) points.push(curves[i]);
536
+ return { kind: 'bezier', points };
537
+ },
538
+ placement: () => {
539
+ const placed = placementOf(data, timeline.slotIndex, attachment);
540
+ return { skin: placed.skin, holder: skinOf(placed.holder), placeholder: placed.placeholder };
541
+ },
542
+ };
543
+ };
544
+ const animations: SurveyAnimation[] = data.animations.map((anim) => ({
545
+ name: anim.name,
546
+ deforms: anim.timelines.flatMap((timeline) => (timeline instanceof DeformTimeline ? [deformOf(timeline)] : [])),
547
+ slotKeyTimes: (slots) => visibilityKeyTimes(anim.timelines, slots),
548
+ }));
549
+ const sliderBack = new Map<SurveySlider, SliderData>();
550
+ const sliders: SurveySlider[] = [];
551
+ for (const constraint of data.constraints) {
552
+ if (!(constraint instanceof SliderData)) continue;
553
+ // The animation and the property are read when they are used, as they always were: only a slider with an
554
+ // animation is planned, and only one with a bone has a property — a bone-less one's is not set at all.
555
+ const handle: SurveySlider = {
556
+ name: constraint.name,
557
+ mix: constraint.setupPose.mix,
558
+ animation: constraint.animation?.name ?? '',
559
+ get duration() {
560
+ return constraint.animation.duration;
561
+ },
562
+ bone: constraint.bone?.name ?? null,
563
+ local: constraint.local,
564
+ get stated() {
565
+ return readerField(constraint.property);
566
+ },
567
+ get reader() {
568
+ return constraint.property.constructor.name;
569
+ },
570
+ get from() {
571
+ return constraint.property.offset;
572
+ },
573
+ to: constraint.offset,
574
+ scale: constraint.scale,
575
+ loop: constraint.loop,
576
+ };
577
+ sliders.push(handle);
578
+ sliderBack.set(handle, constraint);
579
+ }
580
+ const structure: SurveyStructure = {
581
+ skins: [data.defaultSkin, ...data.skins].flatMap((k, i, all) => (k !== null && all.indexOf(k) === i ? [skinOf(k) as SurveySkin] : [])),
582
+ animations,
583
+ sliders,
584
+ slotName: (index) => data.slots[index]?.name ?? `#${index}`,
585
+ skinsNamed: (name) => data.skins.filter((k) => k.name === name).length,
586
+ };
587
+ const foreign = (what: string): Error => new Error(`internal: spine-core's poser was handed a ${what} another reader made — it poses the parsed skeleton's own objects`);
588
+ const links: RuntimeLinks = {
589
+ skin: (handle) => {
590
+ if (handle === null) return null;
591
+ const skin = skinBack.get(handle);
592
+ if (skin === undefined) throw foreign(`skin "${handle.name}"`);
593
+ return skin;
594
+ },
595
+ mesh: (handle) => {
596
+ const mesh = meshBack.get(handle);
597
+ if (mesh === undefined) throw foreign(`mesh "${handle.name}"`);
598
+ return mesh;
599
+ },
600
+ slider: (handle) => {
601
+ const slider = sliderBack.get(handle);
602
+ if (slider === undefined) throw foreign(`slider "${handle.name}"`);
603
+ return slider;
604
+ },
605
+ };
606
+ return { structure, poser: spinePoser(data, links), links };
607
+ }
608
+ // --- #1019 the runtime's reading of the structure: end ---
609
+
610
+ /** A live spine-core skeleton, read the way the survey always read it. */
611
+ function spinePose(skeleton: Skeleton, links: RuntimeLinks): SurveyPose {
612
+ return {
613
+ under: () => skeleton.skin?.name ?? null,
614
+ shownAt: (slotIndex, handle) => shownAt(skeleton, slotIndex, links.mesh(handle)),
615
+ deformed: (slotIndex, handle) => {
616
+ const attachment = links.mesh(handle);
617
+ const count = attachment.worldVerticesLength;
618
+ const world = new Float32Array(count);
619
+ attachment.computeWorldVertices(skeleton, skeleton.slots[slotIndex], 0, count, world, 0, 2);
620
+ return world;
621
+ },
622
+ plain: (slotIndex, handle) => {
623
+ const attachment = links.mesh(handle);
624
+ const count = attachment.worldVerticesLength;
625
+ const slot = skeleton.slots[slotIndex];
626
+ slot.appliedPose.deform.length = 0;
627
+ const world = new Float32Array(count);
628
+ attachment.computeWorldVertices(skeleton, slot, 0, count, world, 0, 2);
629
+ return world;
630
+ },
631
+ withDeform: (slotIndex, handle, deform) => worldWithDeform(skeleton, slotIndex, links.mesh(handle), deform),
632
+ rows: (slotIndex, handle, deform) => {
633
+ const attachment = links.mesh(handle);
634
+ const slot = skeleton.slots[slotIndex];
635
+ const array = slot.appliedPose.deform;
636
+ const kept = [...array];
637
+ if (deform === 'cleared') array.length = 0;
638
+ else if (deform !== 'posed') {
639
+ array.length = deform.length;
640
+ for (let i = 0; i < deform.length; i++) array[i] = deform[i];
641
+ }
642
+ const out = new Array<number>(attachment.worldVerticesLength);
643
+ attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, out, 0, 2);
644
+ array.length = kept.length;
645
+ for (let i = 0; i < kept.length; i++) array[i] = kept[i];
646
+ return out;
647
+ },
648
+ };
649
+ }
650
+
651
+ /** The survey's poses through spine-core — the recipes the survey has always taken (`poseAt`; the dial below). */
652
+ function spinePoser(data: SkeletonData, links: RuntimeLinks): SurveyPoser {
653
+ return {
654
+ track: (skin, animation, time) => spinePose(poseAt(data, links.skin(skin), animation, time), links),
655
+ dial: (skin, handle) => {
656
+ const slider = links.slider(handle);
657
+ const skeleton = skeletonUnderSkin(data, links.skin(skin));
658
+ const instance = sliderOn(skeleton, slider);
659
+ if (instance === null) return null;
660
+ const bone = instance.bone;
661
+ return {
662
+ hasBone: bone !== null,
663
+ base: (field) => {
664
+ skeleton.setupPose();
665
+ return (bone as Bone).pose[field];
666
+ },
667
+ // `Slider.update`'s own reading, at its own point in the update: the
668
+ // setup pose, `update(0)`, the field (or the time) written, `Physics.reset`.
669
+ at: (field, candidate) => {
670
+ skeleton.setupPose();
671
+ skeleton.update(0);
672
+ if (field !== null && bone !== null) bone.pose[field] = candidate;
673
+ else instance.pose.time = candidate;
674
+ skeleton.updateWorldTransform(Physics.reset);
675
+ if (field === null || bone === null) return { read: candidate, applied: instance.appliedPose.time };
676
+ if (slider.local) bone.appliedPose.validateLocalTransform(skeleton);
677
+ return { read: slider.property.value(skeleton, bone.appliedPose, slider.local, DIAL_ZERO_OFFSETS), applied: instance.appliedPose.time };
678
+ },
679
+ pose: () => spinePose(skeleton, links),
680
+ };
681
+ },
682
+ };
683
+ }
684
+
685
+ /**
686
+ * Both readers and both posers over one build, for measurement:
687
+ * `tools/survey_hashes.ts` holds every hook of the core's poser to
688
+ * spine-core's at tolerance 0 over the runtime's structure (`hooks`), and the
689
+ * model document's structure to the runtime's (`structure`); the selftest's
690
+ * `DM` controls hold the same. `spine` and `core` both pose `structure`'s
691
+ * handles; `model` is the model document's reading, which only `core` and the
692
+ * model's own poser can pose.
693
+ */
694
+ export function deformPosers(input: DeformSurveyInput & { modelText: string }): { data: SkeletonData; structure: SurveyStructure; model: SurveyStructure; spine: SurveyPoser; core: SurveyPoser } {
695
+ const data = skeletonDataFromText(input.skeletonText, input.atlasText);
696
+ const side = runtimeSide(data);
697
+ const doc = readModel(input.modelText);
698
+ return { data, structure: side.structure, model: modelStructure(doc), spine: side.poser, core: corePoser(side.structure, doc) };
699
+ }
700
+
701
+ /**
702
+ * Touch the runtime once, before the skeleton is parsed through it, and refuse
703
+ * by name when it cannot be used (issue #1019) — the sentence `render` and
704
+ * `check` give a run that needs the runtime and cannot use it
705
+ * (`SpineRuntimeError`, issue #1014). A survey whose input the core poses
706
+ * never comes here.
707
+ */
708
+ function requireSpineRuntime(label: string, why: string): void {
709
+ try {
710
+ // A property read on the class the parse starts from: no runtime code runs, and a runtime that cannot be used throws here.
711
+ void TextureAtlas.prototype;
712
+ } catch (err) {
713
+ // The sentence an entry that registered no Spine side is refused in too (`./spine_side.ts`), with the runtime's own words as the reason.
714
+ throw new SpineRuntimeError(spineRuntimeSentence(label, why, (err as Error).message, SURVEY_RUNTIME_TAIL));
715
+ }
716
+ }
717
+
718
+ /**
719
+ * The survey through spine-core — the half of `surveyOfBuild`
720
+ * (`./deformbuild.ts`, moved there unchanged in issue #1052) that names the
721
+ * runtime, registered into the seam when this file is loaded: it touches the
722
+ * runtime once (`requireSpineRuntime`), then reads and poses the Spine
723
+ * skeleton and names `why` as the survey's source. Every program that imports
724
+ * this file surveys as before; one that does not refuses this half by name.
725
+ */
726
+ registerSpineSurvey({
727
+ throughSpine: (label, why, input, exempt) => {
728
+ requireSpineRuntime(label, why ?? '--poser spine');
729
+ const side = runtimeSide(skeletonDataFromText(input.skeletonText, input.atlasText));
730
+ return { ...surveyWith(side.structure, exempt, side.poser), source: { used: 'spine-core', why } };
731
+ },
732
+ });