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
package/src/render.ts ADDED
@@ -0,0 +1,1013 @@
1
+ /**
2
+ * The rasteriser — one code path for reference frames and for candidates.
3
+ *
4
+ * ⭐ Why this is a module and not a script. `bench/render_reference.ts` renders
5
+ * the official export to the PNG frames an authoring agent is allowed to see;
6
+ * `rigc check` renders the agent's own candidate and compares it against those
7
+ * frames. If those two drew pixels differently, every number `check` reports
8
+ * would carry the difference between two renderers on top of the difference
9
+ * between two rigs — and the second is the only one anybody wants to read. So
10
+ * there is exactly one rasteriser, and both callers are thin.
11
+ *
12
+ * ## What it draws
13
+ *
14
+ * Region and mesh attachments, in draw order, tinted by slot colour x attachment
15
+ * colour. Both are **affine** texture maps and neither divides by w:
16
+ *
17
+ * - a **region** is one quad. `spine-core` computes its four world vertices on
18
+ * the CPU, a destination pixel maps back into the region's rectangle by
19
+ * inverting one 2x2, and there is no triangle split at all;
20
+ * - a **mesh** is a triangle list. `MeshAttachment.computeWorldVertices` does the
21
+ * work — it is the runtime's own routine, so weighted vertices resolve through
22
+ * their bones and a `deform` timeline's offsets are applied, exactly the way a
23
+ * real runtime would. Each triangle is then filled with barycentric UV
24
+ * interpolation.
25
+ *
26
+ * A **clipping attachment** draws no pixel of its own and removes the pixels of
27
+ * every slot from the one carrying it through its `end` slot. `piecesOf` runs
28
+ * spine-core's own `SkeletonClipping` beside the draw-order walk, in the call
29
+ * sequence spine-webgl's `SkeletonRenderer.draw` runs, so a slot inside a clip
30
+ * reaches the rasteriser as the geometry the runtime draws rather than as the
31
+ * attachment's whole (issue #844).
32
+ *
33
+ * ⭐ **Sampling is bilinear on both paths, and the source is straight alpha —
34
+ * so the interpolation is premultiplied.** One filter rather than two is not a
35
+ * detail: `check` measures a candidate against reference frames, and a mesh
36
+ * triangle sampled nearest against a reference sampled bilinear would put a
37
+ * filter difference into the residual where only a rig difference belongs.
38
+ * Bilinear rather than nearest because the region path was already bilinear and
39
+ * the five committed rungs are rendered with it.
40
+ *
41
+ * Straight alpha is a property of the SOURCE, not a licence to average it
42
+ * channel by channel: a transparent texel's `(0, 0, 0, 0)` is the absence of a
43
+ * colour, and giving it a vote drew a dark rim along every region edge — over
44
+ * the top of whatever was behind the part, and into `check`'s residual on the
45
+ * candidate side. `bilinear` weights each colour by its own alpha and divides
46
+ * back out; see it for what that does and does not move (issue #292).
47
+ *
48
+ * ⚠️ **Region rasterising is untouched by the mesh path**, deliberately. A region
49
+ * could be drawn as two triangles and very nearly the same pixels would come out;
50
+ * "very nearly" would have silently rewritten five rungs of committed reference
51
+ * frames. `rasteriseQuad` still owns regions, `rasteriseMesh` owns meshes, and
52
+ * `rasterisePiece` picks.
53
+ *
54
+ * ## The fill rule, and why a mesh needs one
55
+ *
56
+ * Two triangles that share an edge must cover the pixels along it exactly once.
57
+ * Include the boundary in both and every interior edge of a mesh blends twice —
58
+ * a visible lattice of seams wherever the art is not opaque. Exclude it in both
59
+ * and the seams become holes.
60
+ *
61
+ * So `rasteriseMesh` normalises each triangle's winding and applies the standard
62
+ * **top-left rule**: a pixel centre exactly on an edge belongs to the triangle
63
+ * only when that edge is a top or a left one. The two triangles sharing an edge
64
+ * traverse it in opposite directions, so exactly one of them calls it top-left —
65
+ * which is the property that makes the rule watertight without an epsilon.
66
+ *
67
+ * ## Two conventions this file owns
68
+ *
69
+ * - **Spine world is y up; an image is y down.** The projection from world to
70
+ * frame pixels lives in `projector` and nowhere else.
71
+ * - **The framing box is measured at `FRAMING_FPS`, whatever rate frames are
72
+ * written at.** The union of the posed vertices depends on WHICH TIMES you
73
+ * sample, so taking it at the output rate made the viewport a property of the
74
+ * rate: rung 1's `balls` framed to 256x240 at 12 fps and 256x239 at 24 fps.
75
+ * One pixel is enough to be a trap — the two sets look comparable, an author
76
+ * measures a distance in one and a time in the other, and the scale between
77
+ * them is silently off.
78
+ *
79
+ * ⚠️ Two notes on where this sits. It imports `spine-core`, which `src/` is
80
+ * otherwise careful about: posing a skeleton *is* running the runtime, and there
81
+ * is no honest way to render one without it. (Since issue #968 that holds for a
82
+ * Spine export: a rigc build is posed by rigc's own core through
83
+ * `./render_core.ts`, and this file keeps the runtime for the export's poser,
84
+ * the export's atlas pages and texture substitution — see *which poser* below.
85
+ * Since issue #1014 a rigc build the core poses touches none of it: what a
86
+ * render reads off the candidate besides the pose comes from its model
87
+ * document and the skeleton's own JSON (`loadCandidate`), and since issue
88
+ * #1020 its pages and a `--texture-from` atlas through rigc's own reader, so
89
+ * spine-core is reached only where it poses an export or a fallback.) The rule that matters is unchanged
90
+ * — `src/compile.ts` must stay independent of the runtime so the compiler and
91
+ * the gate are not checking each other's assumptions — and this file is neither.
92
+ * It also imports `tools/plate.ts` for the PNG codec, which is dependency-free.
93
+ *
94
+ * ## What is here, and what is not (issue #1052)
95
+ *
96
+ * Only what names the runtime: spine-core's implementation of the posing seam
97
+ * (`spinePoser`), loading a Spine export (`posableFromText`, `loadPosedSkeleton`,
98
+ * and the half of `loadCandidate` an export takes), the atlas class's readings
99
+ * (`atlasPageNames`, the `spine` reader of a texture substitution), and what
100
+ * reads a posed spine-core skeleton (`piecesOf`, `boneSnapshots`,
101
+ * `posedNumbersOf`, the rest table). Everything else — the frame-set contract,
102
+ * the samplers, the candidate and its poser choice, the geometry export, the
103
+ * framing and the rasteriser — is `./render_shared.ts`, which links nothing of
104
+ * the runtime, and every name it holds that this file exported is re-exported
105
+ * from here. That module reaches this one's half through the seam
106
+ * (`./spine_side.ts`): loading this file registers it (`registerSpinePosing`,
107
+ * at the bottom), so every program that imports it renders exactly as before,
108
+ * and one that does not refuses an export by name.
109
+ */
110
+ import {
111
+ AnimationState,
112
+ AnimationStateData,
113
+ AtlasAttachmentLoader,
114
+ ClippingAttachment,
115
+ MeshAttachment,
116
+ Physics,
117
+ RegionAttachment,
118
+ Skeleton,
119
+ SkeletonClipping,
120
+ SkeletonJson,
121
+ TextureAtlas,
122
+ TextureAtlasRegion,
123
+ SkeletonData,
124
+ type Slot,
125
+ } from '@esotericsoftware/spine-core';
126
+ import { readFileSync } from 'node:fs';
127
+ // `dirname` is read by no body here today: RC23's `bonedist-pages` plant puts a `loadPosable(…, dirname(atlasPath))` back into
128
+ // `loadPosedSkeleton`, and kept, the plant reads red for its own reason rather than for a missing name.
129
+ import { dirname, join } from 'node:path';
130
+ import { Plate, readPlate } from '../tools/plate.ts';
131
+ import { clipSourceOf, corePoser, inactiveBoneSnapshot, subsetOver, unposedBones } from './render_core.ts';
132
+ import { type PosedVertices, type WorldTransform } from './nonfinite.ts';
133
+ import {
134
+ atlasScales,
135
+ choosePosers,
136
+ pairRefusal,
137
+ refuseUnchosen,
138
+ regionKey,
139
+ type AttachmentPose,
140
+ type AttachmentRest,
141
+ type BoneSnapshot,
142
+ type DrawOptions,
143
+ type MakeCorePoser,
144
+ type Piece,
145
+ type PieceTexture,
146
+ type PoseOptions,
147
+ type Posed,
148
+ type Poser,
149
+ type PoserChoice,
150
+ type PoserName,
151
+ type SkeletonFacts,
152
+ type SkinRoster,
153
+ type SlotSubset,
154
+ type SubstituteRegion,
155
+ } from './render_shared.ts';
156
+ import { POSING_RUNTIME_TAIL, registerSpinePosing, SpineRuntimeError, spineRuntimeSentence } from './spine_side.ts';
157
+
158
+ // Every name `./render_shared.ts` holds that this file exported before issue #1052, re-exported so a dependant's
159
+ // import resolves where it always did.
160
+ export {
161
+ BACKGROUND,
162
+ CandidateAtlasError,
163
+ CandidatePairError,
164
+ EMPTY_FOOTPRINT,
165
+ FRAMES_SIDECAR,
166
+ FRAMES_SPEC,
167
+ FRAMING_FPS,
168
+ GEOMETRY_COORDINATES,
169
+ GEOMETRY_FILE,
170
+ GEOMETRY_SPEC,
171
+ GeometryError,
172
+ PAD,
173
+ POSER_NAMES,
174
+ PROTOCOL_FPS,
175
+ PoserChoiceError,
176
+ SETUP_POSE_DIR,
177
+ SHEET_COLUMNS,
178
+ SHEET_FILE,
179
+ SHEET_GAP,
180
+ SHEET_LABEL,
181
+ SHEET_RULE,
182
+ SHEET_TILE,
183
+ SUBSTITUTE_PAGE,
184
+ SlotSubsetError,
185
+ UnframeablePoseError,
186
+ atlasScales,
187
+ bilinear,
188
+ bilinearChannels,
189
+ blitPiece,
190
+ contactSheet,
191
+ fill,
192
+ frameGeometry,
193
+ framingViewport,
194
+ geometryFileOf,
195
+ geometryText,
196
+ loadCandidate,
197
+ nonFinitePoseOf,
198
+ pageFor,
199
+ projector,
200
+ rasteriseMesh,
201
+ rasterisePiece,
202
+ rasteriseQuad,
203
+ refuseUnchosen,
204
+ regionTrim,
205
+ renderFrame,
206
+ sampleAll,
207
+ sampleAnimation,
208
+ sampleSetupPose,
209
+ sidecarViewport,
210
+ skeletonFacts,
211
+ slotsOnUnposedBones,
212
+ substituteTexture,
213
+ textureSubstitutionFromText,
214
+ throughPoser,
215
+ trimmedUnionBounds,
216
+ unframeableSentence,
217
+ unionBounds,
218
+ viewportFor,
219
+ viewportOfSize,
220
+ } from './render_shared.ts';
221
+ export type {
222
+ AttachmentPose,
223
+ AttachmentRest,
224
+ BoneSnapshot,
225
+ Candidate,
226
+ ClipSource,
227
+ DrawOptions,
228
+ EmitPixel,
229
+ Footprint,
230
+ Frame,
231
+ FrameGeometry,
232
+ FrameSet,
233
+ FramesSidecar,
234
+ GeometryBone,
235
+ GeometryFile,
236
+ GeometryFrame,
237
+ MakeCorePoser,
238
+ Mesh,
239
+ Piece,
240
+ PieceCommon,
241
+ PieceTexture,
242
+ PoseOptions,
243
+ Posed,
244
+ Poser,
245
+ PoserChoice,
246
+ PoserName,
247
+ Quad,
248
+ RegionTrim,
249
+ SkeletonFacts,
250
+ SkinRoster,
251
+ SlotSubset,
252
+ SubstituteRegion,
253
+ SubstitutionReader,
254
+ TextureSubstitution,
255
+ UvWindow,
256
+ Viewport,
257
+ } from './render_shared.ts';
258
+ // The refusal of a run that needs the runtime and cannot use it — `./spine_side.ts`'s since issue #1052, where an
259
+ // entry that registered no Spine side is refused in the same words.
260
+ export { SpineRuntimeError };
261
+
262
+ /**
263
+ * Resolve `slots` / `hidden` against `data` as posed under `skin`, or refuse by name.
264
+ *
265
+ * `undefined` when neither is set — the whole rig, which is the ordinary case and
266
+ * costs nothing. Refused, each naming what would have worked:
267
+ *
268
+ * - **both at once** — one statement two ways, as `--rig` with `--cut` is;
269
+ * - **a name the skeleton does not declare** — with every slot it does declare,
270
+ * in draw order, and how many;
271
+ * - **a slot whose attachments live only under skins this pose does not
272
+ * resolve through.** Every slot is declared at the skeleton's top level, so
273
+ * "a slot only a named skin declares" is not a shape the format has — what a
274
+ * skin declares is the slot's ART. A pose resolves an attachment through the
275
+ * skin it was set to and then the default skin (`Skeleton.getAttachment`), so
276
+ * a slot none of whose attachments is in either of those draws nothing in
277
+ * every frame, and `--slot` on it would be a blank picture that looks like an
278
+ * answer. The refusal names the skin(s) that do carry it.
279
+ *
280
+ * ⚠️ A declared slot with no attachment in ANY skin is accepted: it draws
281
+ * nothing under every skin, so there is no skin to name and no picture of it
282
+ * that a different invocation would produce.
283
+ */
284
+ export function slotSubsetOf(
285
+ data: SkeletonData,
286
+ opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined,
287
+ skin: string | undefined,
288
+ ): SlotSubset | undefined {
289
+ // The rule is `subsetOver`'s (`./render_core.ts`), shared with the core
290
+ // poser; this is the roster a parsed Spine skeleton gives it.
291
+ return subsetOver(
292
+ {
293
+ declared: data.slots.map((slot) => slot.name),
294
+ carriers: (name) => {
295
+ const index = data.findSlot(name)?.index ?? -1;
296
+ return data.skins.filter((s) => s.getAttachments().some((entry) => entry.slotIndex === index)).map((s) => s.name);
297
+ },
298
+ defaultSkin: data.defaultSkin?.name ?? null,
299
+ },
300
+ opts,
301
+ skin,
302
+ );
303
+ }
304
+
305
+ /**
306
+ * A fresh skeleton with `skin` applied, or refused by name.
307
+ *
308
+ * ## Why the skin goes on before `setupPose`, and why nothing else is needed
309
+ *
310
+ * `Skeleton.setSkin` (spine-core 4.3.13 `Skeleton.js:279-311`) attaches the new
311
+ * skin's art into each slot's pose and calls `updateCache`, which is what turns
312
+ * on a `skinRequired` bone or constraint the skin names. Every sampler below
313
+ * then calls `skeleton.setupPose()`, and `setupPose` → `setupPoseSlots`
314
+ * (`Skeleton.js:231-249`) re-resolves each slot's setup attachment through
315
+ * `Slot.setupPose` → `Skeleton.getAttachment`, which checks `this.skin` first
316
+ * and `SkeletonData.defaultSkin` second (`Skeleton.js:335-346`). So the setup
317
+ * pose of a skinned skeleton is already the skin's, and the extra
318
+ * `setSlotsToSetupPose()` a 4.1-era recipe prescribes has no 4.3 spelling to
319
+ * call: the method is named `setupPoseSlots` here, and `setupPose()` runs it.
320
+ *
321
+ * `setSkin(string)` exists too, but its by-name half throws
322
+ * `Skin not found: <name>` (`Skeleton.js:286-291`) — a message that names the
323
+ * miss and not the alternatives. Everything in a rig resolves by name and a miss
324
+ * is refused **by name, with the names that would have worked**, so the lookup
325
+ * happens here and the runtime is handed a `Skin` it cannot fail on.
326
+ */
327
+ function skeletonUnderSkin(data: SkeletonData, skin: string | undefined): Skeleton {
328
+ const skeleton = new Skeleton(data);
329
+ if (skin === undefined) return skeleton;
330
+ const found = data.findSkin(skin);
331
+ if (!found) {
332
+ throw new Error(
333
+ `no skin ${JSON.stringify(skin)} in this skeleton; it declares [${
334
+ data.skins.map((s) => s.name).join(', ') || 'none'
335
+ }]`,
336
+ );
337
+ }
338
+ skeleton.setSkin(found);
339
+ return skeleton;
340
+ }
341
+
342
+ /**
343
+ * Every bone's world transform in the skeleton's own declaration order — a bone
344
+ * the posed skin leaves unposed (inactive, or below an inactive bone) written as the zero transform
345
+ * (`inactiveBoneSnapshot` in `./render_core.ts`: it is not posed, and what a
346
+ * constraint left in its matrix is not a pose — issue #968).
347
+ */
348
+ export function boneSnapshots(skeleton: Skeleton): BoneSnapshot[] {
349
+ const unposed = unposedBones(skeleton.bones.map((bone) => ({ name: bone.data.name, parent: bone.parent?.data.name ?? null, active: bone.active })));
350
+ return skeleton.bones.map((bone) => {
351
+ // Not posed under this skin: the seam's zero snapshot (`inactiveBoneSnapshot`, issue #968).
352
+ if (unposed.has(bone.data.name)) return inactiveBoneSnapshot(bone.data.name);
353
+ const pose = bone.appliedPose;
354
+ return {
355
+ name: bone.data.name,
356
+ worldX: pose.worldX,
357
+ worldY: pose.worldY,
358
+ a: pose.a,
359
+ b: pose.b,
360
+ c: pose.c,
361
+ d: pose.d,
362
+ rotationX: pose.getWorldRotationX(),
363
+ rotationY: pose.getWorldRotationY(),
364
+ scaleX: pose.getWorldScaleX(),
365
+ scaleY: pose.getWorldScaleY(),
366
+ };
367
+ });
368
+ }
369
+
370
+ /** A loaded skeleton and every atlas page it can sample, keyed by page name. */
371
+ export interface Posable {
372
+ data: SkeletonData;
373
+ pages: Map<string, Plate>;
374
+ }
375
+
376
+ /**
377
+ * Load a skeleton, its atlas and every page the atlas declares.
378
+ *
379
+ * Every page, not the first: rigc emits **one part per page**, so a rigc
380
+ * candidate for rung 1 has eight of them. `render_reference.ts` used to insist
381
+ * on exactly one because an editor export packs into one — that assumption is
382
+ * true of the reference and false of every candidate, and a renderer both sides
383
+ * share cannot hold it.
384
+ */
385
+ export function loadPosable(skeletonPath: string, atlasPath: string, atlasDir: string): Posable {
386
+ return posableFromText(readFileSync(skeletonPath, 'utf8'), readFileSync(atlasPath, 'utf8'), atlasDir);
387
+ }
388
+
389
+ /**
390
+ * The page names an atlas declares, in the order it declares them.
391
+ *
392
+ * Through `TextureAtlas` rather than by reading the lines: a page name and a
393
+ * region name are both unindented in the atlas format, so anything that told them
394
+ * apart here would be a second opinion about the file's syntax — and the one
395
+ * caller that needs this list (`rigc preview`, embedding each page) has to agree
396
+ * exactly with the player that will ask for them by name.
397
+ */
398
+ export function atlasPageNames(atlasText: string): string[] {
399
+ return new TextureAtlas(atlasText).pages.map((page) => page.name);
400
+ }
401
+
402
+ /** Same, for artifacts held in memory rather than on disk. */
403
+ export function posableFromText(skeletonText: string, atlasText: string, atlasDir: string): Posable {
404
+ const atlas = new TextureAtlas(atlasText);
405
+ const pages = new Map<string, Plate>();
406
+ for (const page of atlas.pages) pages.set(page.name, readPlate(join(atlasDir, page.name)));
407
+ const data = new SkeletonJson(new AtlasAttachmentLoader(atlas)).readSkeletonData(JSON.parse(skeletonText));
408
+ return { data, pages };
409
+ }
410
+
411
+ /** The facts as spine-core loaded them — an export's reading, and every candidate's before issue #1014; `atlasText` the atlas it was loaded through. */
412
+ export function spineFacts(data: SkeletonData, atlasText: string | null): SkeletonFacts {
413
+ return {
414
+ atlasScales: atlasText === null ? null : atlasScales(atlasText),
415
+ animations: data.animations.map((a) => a.name),
416
+ skins: data.skins.map((s) => s.name),
417
+ // `SkeletonJson` copies both header fields across unconditionally, so an omitted extent is `undefined` here, not 0.
418
+ declaresStage: typeof data.width === 'number' && typeof data.height === 'number',
419
+ bones: data.bones.map((bone) => ({ name: bone.name, parent: bone.parent === null ? null : bone.parent.name })),
420
+ slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
421
+ subset: (opts, skin) => slotSubsetOf(data, opts, skin),
422
+ };
423
+ }
424
+
425
+ /** Touch the runtime once, before anything is parsed through it, and refuse by name when it cannot be used. */
426
+ function requireSpineRuntime(label: string, why: string): void {
427
+ try {
428
+ // A property read on the class the load starts from: no runtime code runs, and a runtime that cannot be used throws here.
429
+ void TextureAtlas.prototype;
430
+ } catch (err) {
431
+ // 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.
432
+ throw new SpineRuntimeError(spineRuntimeSentence(label, why, (err as Error).message, POSING_RUNTIME_TAIL));
433
+ }
434
+ }
435
+
436
+ /**
437
+ * The one place a candidate's skeleton is handed to the runtime's parser
438
+ * against its atlas (issues #1033, #1042): a pair it cannot load is `refuse`'s
439
+ * refusal, given the runtime's own message, rather than the runtime's throw
440
+ * and a stack. The JSON is parsed outside the catch: a file that is not JSON
441
+ * is not something the runtime said, and the CLI refuses it by name before it
442
+ * gets here (`readSkeletonText`).
443
+ */
444
+ function spineSkeletonData(skeletonText: string, atlasText: string, refuse: (runtime: string) => Error): SkeletonData {
445
+ const json = JSON.parse(skeletonText);
446
+ const reader = new SkeletonJson(new AtlasAttachmentLoader(new TextureAtlas(atlasText)));
447
+ try {
448
+ return reader.readSkeletonData(json);
449
+ } catch (err) {
450
+ throw refuse(err instanceof Error ? err.message : String(err));
451
+ }
452
+ }
453
+
454
+ /**
455
+ * A skeleton a user named, posed through spine-core for what its bones do —
456
+ * `bonedist`'s two sides (issue #1042). A pair that does not load is refused
457
+ * by name (`CandidatePairError`, `pairRefusal` with `poses`), `why` saying
458
+ * which side and what the pose is read for.
459
+ *
460
+ * ⭐ The skeleton data alone, and no page: `bonedist` reads bone world
461
+ * transforms and samples no pixel, and the atlas's regions — which the
462
+ * runtime resolves every attachment against — are in the atlas text. Reading
463
+ * the page PNGs as `loadPosable` does made a pair whose pages are elsewhere
464
+ * die on an ENOENT for images the command never looks at; the figures are the
465
+ * same with or without them (the PR of #1042 measures it).
466
+ *
467
+ * `loadPosable` and `posableFromText` stay the unguarded loaders, for pairs
468
+ * the caller wrote itself (the tools and the selftest), where the runtime's
469
+ * own message is what the caller reads.
470
+ */
471
+ export function loadPosedSkeleton(skeletonPath: string, atlasPath: string, why: string): SkeletonData {
472
+ const paths = { skeleton: skeletonPath, atlas: atlasPath };
473
+ return spineSkeletonData(readFileSync(skeletonPath, 'utf8'), readFileSync(atlasPath, 'utf8'), (runtime) => pairRefusal(skeletonPath, paths, why, runtime, 'poses'));
474
+ }
475
+
476
+ /** What every sampler takes: a poser, or spine-core's parsed skeleton, which is posed through `spinePoser`. */
477
+ export type PoseSource = Poser | SkeletonData;
478
+
479
+ // ---------------------------------------------------------------------------
480
+ // the spine-core implementation of the seam
481
+ // ---------------------------------------------------------------------------
482
+
483
+ /**
484
+ * `Poser` over spine-core: the runtime's own `Skeleton`, `AnimationState` and
485
+ * `SkeletonClipping`, stepped the way a runtime steps them.
486
+ *
487
+ * The pose is driven through `AnimationState` rather than `Animation.apply`
488
+ * because that is the path a runtime actually takes, and 4.3's
489
+ * `Animation.apply` takes a `MixFrom` that only the state machine has any
490
+ * business choosing. Frame 0 applies, updates by 0 and resets physics; every
491
+ * later frame is one `state.update(1/fps)`, apply, `skeleton.update(1/fps)` and
492
+ * `Physics.update` — one continuous trajectory, so the track time of frame `i`
493
+ * is the sum of `i` steps.
494
+ */
495
+ export function spinePoser(data: SkeletonData): Poser {
496
+ return {
497
+ animations: data.animations.map((a) => ({ name: a.name, duration: a.duration })),
498
+ bones: data.bones.map((bone) => ({ name: bone.name, parent: bone.parent?.name ?? null })),
499
+ slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
500
+ subset: (opts, skin) => slotSubsetOf(data, opts, skin),
501
+ setup: (skin) => spinePosed(setupPosed(data, skin)),
502
+ animation: (name, skin, fps, count, visit) => {
503
+ const skeleton = skeletonUnderSkin(data, skin);
504
+ const state = new AnimationState(new AnimationStateData(data));
505
+ // Not looping: the last frame sits at the animation's duration, and a looping
506
+ // entry would wrap it back onto the first pose.
507
+ state.setAnimation(0, name, false);
508
+ skeleton.setupPose();
509
+ const step = 1 / fps;
510
+ const posed = spinePosed(skeleton);
511
+ for (let i = 0; i <= count; i++) {
512
+ if (i > 0) {
513
+ state.update(step);
514
+ state.apply(skeleton);
515
+ skeleton.update(step);
516
+ skeleton.updateWorldTransform(Physics.update);
517
+ } else {
518
+ state.apply(skeleton);
519
+ skeleton.update(0);
520
+ skeleton.updateWorldTransform(Physics.reset);
521
+ }
522
+ visit(i, posed);
523
+ }
524
+ },
525
+ rest: (skin, shown) => restOf(data, skin, shown),
526
+ };
527
+ }
528
+
529
+ /** A spine-core skeleton as it stands posed, read through the seam's `Posed`. */
530
+ function spinePosed(skeleton: Skeleton): Posed {
531
+ return {
532
+ pieces: (draw) => drawPieces(skeleton, draw),
533
+ bones: () => boneSnapshots(skeleton),
534
+ attachments: () => attachmentsOf(skeleton),
535
+ };
536
+ }
537
+
538
+ /**
539
+ * A fresh skeleton under `skin`, stepped into its setup pose — the one recipe
540
+ * `sampleSetupPose` and the geometry export's `rest` table both pose from, so
541
+ * the two cannot come to describe different rest poses.
542
+ */
543
+ function setupPosed(data: SkeletonData, skin: string | undefined): Skeleton {
544
+ const skeleton = skeletonUnderSkin(data, skin);
545
+ skeleton.setupPose();
546
+ skeleton.update(0);
547
+ skeleton.updateWorldTransform(Physics.reset);
548
+ return skeleton;
549
+ }
550
+
551
+ /**
552
+ * Both posers for the skeleton at `skeletonPath` drawn through the atlas at
553
+ * `atlasPath`, with spine-core's parse of it already in hand (`data`) — the
554
+ * choice `loadCandidate` makes, for a caller that loaded the pair itself.
555
+ * `forced` is `--poser`; `core` on an input that cannot carry it is refused by
556
+ * name (`PoserChoiceError`).
557
+ */
558
+ export function candidatePosers(
559
+ data: SkeletonData,
560
+ skeletonPath: string,
561
+ atlasPath: string,
562
+ forced: PoserName | undefined,
563
+ /** What builds the core poser: `corePoser` — the suite's `RC02`, `RC43` and `RC44` pass planted or counted copies, and nothing else passes any. */
564
+ make: MakeCorePoser = corePoser,
565
+ ): PoserChoice {
566
+ return refuseUnchosen(choosePosers(skeletonPath, atlasPath, readFileSync(atlasPath, 'utf8'), forced, () => data, make).choice);
567
+ }
568
+
569
+ /**
570
+ * The posed drawables of one frame, in draw order.
571
+ *
572
+ * Regions and meshes take the same three steps — resolve the sequence index,
573
+ * ask `spine-core` for the world vertices, read the page UVs back off the same
574
+ * sequence — and differ only in which runtime call does step two. An attachment
575
+ * type that is neither is skipped rather than refused: a bounding box, a point
576
+ * and a clipping attachment are all things a rig legitimately carries and none
577
+ * of them draws a pixel.
578
+ *
579
+ * ## A clipping attachment is skipped as a piece and applied as a mask
580
+ *
581
+ * It draws nothing, and it removes what every slot from its own through its
582
+ * `end` slot draws outside its polygon. Skipping it outright drew those pixels —
583
+ * measured on a port whose eye masks clip the irises: the blink frame read MAE
584
+ * 1.07 against 0.53 at an open eye, with both irises drawn over closed lids,
585
+ * where spine-webgl reads 0.16 (issue #844). So spine-core's own
586
+ * `SkeletonClipping` runs beside the walk, and the call sequence is
587
+ * spine-webgl's `SkeletonRenderer.draw` (branch `4.3`) step for step: at a
588
+ * clipping attachment `clipEnd(slot)` then `clipStart(skeleton, slot, clip)` and
589
+ * nothing drawn; at every other slot, drawn or not, `clipEnd(slot)` after it,
590
+ * which is what ends a clip AT its end slot rather than before it; `clipEnd()`
591
+ * after the walk. The polygon, its convex decomposition, `inverse` and `convex`
592
+ * are the clipper's, so there is no second opinion about any of them here.
593
+ *
594
+ * A slot inside a clip hands its world vertices, its triangles — a region's are
595
+ * the runtime's own `0 1 2 2 3 0` — and its UVs to `clipTrianglesUnpacked`, and
596
+ * the piece carries what comes back. ⚠️ **Only when the clipper says it clipped**,
597
+ * exactly as spine-webgl uses the result only when `clipTriangles` returns
598
+ * true: an attachment wholly inside the polygon draws its own geometry, so a
599
+ * region there is still a `Quad` and its pixels are the unclipped ones to the
600
+ * bit. One that is cut becomes a `Mesh` — the clipper's output is a triangle
601
+ * list — and one wholly outside becomes a mesh with no triangle, which keeps the
602
+ * slot in the frame, undrawn, rather than absent.
603
+ *
604
+ * ⭐ A cut piece's pixels are sampled at each drawn triangle's SOURCE triangle's
605
+ * affine UV (`Mesh.source`), not at its own float32 corner UVs (issue #964):
606
+ * which convex pieces a clipper cuts a concave polygon into is the clipper's —
607
+ * spine-core's, the core's, or spine-core's under another spelling of the same
608
+ * polygon — and only this keeps the picture from depending on it. Measured: the
609
+ * same notched square spelled from four start vertices moved up to 18 pixels one
610
+ * level against itself through spine-core before, 0 after; the 19 tree rows'
611
+ * renders did not move (spineboy-pro's portal, the one clipped row, included).
612
+ * An unclipped piece carries no `source` and is rasterised as before, byte for
613
+ * byte.
614
+ *
615
+ * The clip is applied whatever `slots`/`hidden` draw: a hidden clip still masks
616
+ * what is shown, so a subset frame is the whole frame's pixels for those slots.
617
+ * `unclipped` turns it off for the framing box alone — see its note.
618
+ */
619
+ export function piecesOf(skeleton: Skeleton, opts?: PoseOptions): Piece[] {
620
+ // A skin is chosen before a skeleton is posed, and this one is already posed —
621
+ // so there is nothing honest to do with the name except say so. Silently
622
+ // ignoring it is the shape of defect #571 itself: a skin asked for, no skin
623
+ // applied, and a picture that looks like an answer.
624
+ if (opts?.skin !== undefined) {
625
+ throw new Error(
626
+ `piecesOf was asked for skin ${JSON.stringify(opts.skin)}, and it reads a skeleton that is already posed. ` +
627
+ 'Ask a sampler for it — sampleSetupPose/sampleAnimation/sampleAll take { skin } — or call ' +
628
+ 'skeleton.setSkin(...) and skeleton.setupPose() before this.',
629
+ );
630
+ }
631
+ // Resolved against the skeleton as it was posed — its own slots and the skin
632
+ // it was set to — so a name that draws nothing is refused here, where the one
633
+ // application point is, rather than matching no piece in silence.
634
+ const subset = slotSubsetOf(skeleton.data, opts, skeleton.skin?.name);
635
+ return drawPieces(skeleton, { subset, unclipped: opts?.unclipped === true, texture: opts?.texture === true });
636
+ }
637
+
638
+ /** `piecesOf`'s walk over a posed spine-core skeleton, the subset already resolved — the spine poser's `Posed.pieces`. */
639
+ function drawPieces(skeleton: Skeleton, draw: DrawOptions): Piece[] {
640
+ const { subset } = draw;
641
+ const named = subset === undefined ? undefined : new Set(subset.names);
642
+ const clipper = draw.unclipped ? null : new SkeletonClipping();
643
+ const pieces: Piece[] = [];
644
+ for (const slot of skeleton.drawOrder.appliedPose) {
645
+ const attachment = slot.appliedPose.attachment;
646
+ if (attachment instanceof ClippingAttachment) {
647
+ if (clipper !== null) {
648
+ clipper.clipEnd(slot);
649
+ // spine-webgl ends the clip at a slot whose bone is inactive and starts
650
+ // none there; the order of the two calls is the same either way.
651
+ if (slot.bone.active) clipper.clipStart(skeleton, slot, attachment);
652
+ }
653
+ continue;
654
+ }
655
+ const drawn = subset === undefined || named === undefined || named.has(slot.data.name) === (subset.mode === 'slots');
656
+ const piece = drawn ? pieceOf(skeleton, slot, draw.texture, clipper) : null;
657
+ if (piece !== null) pieces.push(piece);
658
+ clipper?.clipEnd(slot);
659
+ }
660
+ clipper?.clipEnd();
661
+ return pieces;
662
+ }
663
+
664
+ /** The runtime's own triangulation of a region's quad — spine-webgl's `QUAD_TRIANGLES`. */
665
+ const QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
666
+
667
+ /**
668
+ * One slot's posed drawable, or `null` for an attachment that draws nothing —
669
+ * clipped by `clipper` when a clip is active over it (see `piecesOf`).
670
+ */
671
+ function pieceOf(
672
+ skeleton: Skeleton,
673
+ slot: Slot,
674
+ withTexture: boolean,
675
+ clipper: SkeletonClipping | null,
676
+ ): Piece | null {
677
+ const pose = slot.appliedPose;
678
+ const attachment = pose.attachment;
679
+ if (!attachment) return null;
680
+ const isMesh = attachment instanceof MeshAttachment;
681
+ if (!isMesh && !(attachment instanceof RegionAttachment)) return null;
682
+
683
+ const index = attachment.sequence.resolveIndex(pose);
684
+ const region = attachment.sequence.regions[index];
685
+ if (!(region instanceof TextureAtlasRegion)) {
686
+ throw new Error(
687
+ `slot "${slot.data.name}" attachment "${attachment.name}" resolved to no atlas region; ` +
688
+ 'the attachment names a region the atlas does not have',
689
+ );
690
+ }
691
+ const tint = tintOf(slot, attachment);
692
+ // The dark colour is the SLOT's alone — an attachment has a `color` and no
693
+ // dark one, so there is nothing to multiply it by. Read off `appliedPose`
694
+ // like the light colour, so an `rgba2` timeline reaches the picture.
695
+ const darkPose = pose.darkColor;
696
+ const dark: [number, number, number] | undefined =
697
+ darkPose === null ? undefined : [darkPose.r, darkPose.g, darkPose.b];
698
+ const common = { tint, dark, slot: slot.data.name, page: region.page.name };
699
+ const texture = withTexture ? artUvsOf(attachment, region) : undefined;
700
+ const uvs = attachment.sequence.getUVs(index);
701
+
702
+ let piece: Piece;
703
+ let triangles: number[];
704
+ const world = worldVerticesOf(skeleton, slot, attachment);
705
+ if (isMesh) {
706
+ triangles = attachment.triangles;
707
+ piece = { kind: 'mesh', ...common, texture, world, uvs, triangles };
708
+ } else {
709
+ triangles = QUAD_TRIANGLES;
710
+ piece = { kind: 'region', ...common, texture, world, uvs };
711
+ }
712
+ if (clipper === null || !clipper.isClipping()) return piece;
713
+ return clippedPiece(piece, triangles, uvs, clipper);
714
+ }
715
+
716
+ /** Slot colour x attachment colour, straight alpha — what a piece is tinted by and a geometry entry records. */
717
+ function tintOf(slot: Slot, attachment: MeshAttachment | RegionAttachment): [number, number, number, number] {
718
+ const colour = slot.appliedPose.color;
719
+ const own = attachment.color;
720
+ return [colour.r * own.r, colour.g * own.g, colour.b * own.b, colour.a * own.a];
721
+ }
722
+
723
+ /**
724
+ * One attachment's world vertices on `slot`, whole, by the runtime's own routine.
725
+ *
726
+ * The one place either kind is asked for them: `pieceOf` draws what this returns
727
+ * and `attachmentsOf` records it, so a drawn frame and its geometry export
728
+ * cannot disagree about where a vertex is.
729
+ *
730
+ * `worldVerticesLength` is 2 per vertex whether or not the mesh is weighted —
731
+ * the weight runs live in `vertices`, not here — so this is the full output
732
+ * length and the whole mesh is computed in one call. Deform offsets, if the
733
+ * slot's pose carries any, are applied inside it; a region's offsets are read
734
+ * for the sequence frame the slot's pose resolves.
735
+ */
736
+ function worldVerticesOf(skeleton: Skeleton, slot: Slot, attachment: MeshAttachment | RegionAttachment): number[] {
737
+ if (attachment instanceof MeshAttachment) {
738
+ const world = new Array<number>(attachment.worldVerticesLength).fill(0);
739
+ attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, world, 0, 2);
740
+ return world;
741
+ }
742
+ const world = new Array<number>(8).fill(0);
743
+ attachment.computeWorldVertices(slot, attachment.getOffsets(slot.appliedPose), world, 0, 2);
744
+ return world;
745
+ }
746
+
747
+ /**
748
+ * Every slot's region or mesh attachment, whole, in the posed draw order — see
749
+ * `Frame.attachments`. No subset and no clip: those are `piecesOf`'s business.
750
+ */
751
+ function attachmentsOf(skeleton: Skeleton): AttachmentPose[] {
752
+ const out: AttachmentPose[] = [];
753
+ for (const slot of skeleton.drawOrder.appliedPose) {
754
+ const attachment = slot.appliedPose.attachment;
755
+ if (!(attachment instanceof MeshAttachment) && !(attachment instanceof RegionAttachment)) continue;
756
+ out.push({
757
+ slot: slot.data.name,
758
+ attachment: attachment.name,
759
+ vertices: worldVerticesOf(skeleton, slot, attachment),
760
+ color: tintOf(slot, attachment),
761
+ });
762
+ }
763
+ return out;
764
+ }
765
+
766
+ /**
767
+ * `piece` as the active clip leaves it, or `piece` itself when the clipper cut
768
+ * nothing — see `piecesOf`.
769
+ *
770
+ * The page UVs and the original-art UVs (`PieceTexture`, when the piece carries
771
+ * them) each go through their own `clipTrianglesUnpacked` call over the same
772
+ * vertices and triangles. The clipper's geometry depends on the positions alone
773
+ * and it interpolates a UV set barycentrically inside each source triangle, so
774
+ * the two calls cut the same polygons and each UV set lands on them — which is
775
+ * what lets `substituteTexture` re-seat a clipped piece exactly as it re-seats a
776
+ * whole one, through the drawing's own coordinates. The vertex count is compared
777
+ * all the same, because a clipped piece whose two UV sets disagreed on it would
778
+ * sample the wrong texels and say nothing.
779
+ */
780
+ function clippedPiece(piece: Piece, triangles: number[], uvs: Float32Array, clipper: SkeletonClipping): Piece {
781
+ if (!clipper.clipTrianglesUnpacked(piece.world, 0, triangles, triangles.length, uvs, 2)) return piece;
782
+ const world = Array.from(clipper.clippedVerticesTyped);
783
+ const clippedUvs = Array.from(clipper.clippedUVsTyped);
784
+ const clipped = Array.from(clipper.clippedTrianglesTyped);
785
+ // Which source triangle each drawn triangle was cut from (`Mesh.source`): the clipper cuts triangle by triangle, so each source
786
+ // triangle is clipped alone and its drawn triangles counted; the concatenation is held to the whole call's output, vertex for vertex.
787
+ const sources: number[] = [];
788
+ const again: number[] = [];
789
+ for (let t = 0; t + 2 < triangles.length; t += 3) {
790
+ clipper.clipTrianglesUnpacked(piece.world, 0, triangles.slice(t, t + 3), 3, uvs, 2);
791
+ for (let k = 0; k < clipper.clippedTrianglesTyped.length; k += 3) sources.push(t / 3);
792
+ again.push(...clipper.clippedVerticesTyped);
793
+ }
794
+ if (sources.length !== clipped.length / 3 || again.length !== world.length || again.some((v, i) => v !== world[i])) {
795
+ throw new Error(`slot "${piece.slot}": the clipper cut ${clipped.length / 3} triangle(s) in one call and ${sources.length} triangle by triangle, over other vertices — the source of each drawn triangle cannot be named`);
796
+ }
797
+ let texture = piece.texture;
798
+ const source = clipSourceOf(piece.world, Array.from(uvs), triangles, sources, texture?.artUvs);
799
+ if (texture !== undefined) {
800
+ clipper.clipTrianglesUnpacked(piece.world, 0, triangles, triangles.length, texture.artUvs, 2);
801
+ const artUvs = Array.from(clipper.clippedUVsTyped);
802
+ if (artUvs.length !== clippedUvs.length) {
803
+ throw new Error(
804
+ `slot "${piece.slot}": the clip cut ${clippedUvs.length / 2} vertices for the page UVs and ` +
805
+ `${artUvs.length / 2} for the original-art UVs over the same geometry`,
806
+ );
807
+ }
808
+ texture = { region: texture.region, artUvs, sourceArtUvs: source.artUvs };
809
+ }
810
+ const { tint, dark, slot, page } = piece;
811
+ return { kind: 'mesh', tint, dark, slot, page, texture, world, uvs: clippedUvs, triangles: clipped, source: { world: source.world, uvs: source.uvs } };
812
+ }
813
+
814
+ /**
815
+ * The rest table: every (slot, attachment) the given frames show, in order of
816
+ * first appearance, posed on one setup skeleton with the slot's deform empty.
817
+ *
818
+ * The attachment is resolved the way the pose resolved it — `Skeleton.getAttachment`,
819
+ * the skin first and the default skin second — so a name means the object the
820
+ * frames drew. A region's rest corners are read for the sequence frame the setup
821
+ * pose resolves, which is the frame its setup pose draws.
822
+ */
823
+ function restOf(data: SkeletonData, skin: string | undefined, shown: readonly AttachmentPose[][]): AttachmentRest[] {
824
+ const skeleton = setupPosed(data, skin);
825
+ // Slot, then attachment: two maps rather than one joined key, so no pair of
826
+ // names can fold into another's entry whatever characters they carry.
827
+ const seen = new Map<string, Set<string>>();
828
+ const out: AttachmentRest[] = [];
829
+ for (const entries of shown) {
830
+ for (const entry of entries) {
831
+ const names = seen.get(entry.slot) ?? new Set<string>();
832
+ if (names.has(entry.attachment)) continue;
833
+ names.add(entry.attachment);
834
+ seen.set(entry.slot, names);
835
+ const slotIndex = data.findSlot(entry.slot)?.index ?? -1;
836
+ const slot = skeleton.slots[slotIndex];
837
+ const attachment = slot === undefined ? null : skeleton.getAttachment(slotIndex, entry.attachment);
838
+ if (slot === undefined || !(attachment instanceof MeshAttachment || attachment instanceof RegionAttachment)) {
839
+ throw new Error(
840
+ `slot ${JSON.stringify(entry.slot)} showed attachment ${JSON.stringify(entry.attachment)} in a frame, and ` +
841
+ 'the setup skeleton resolves no region or mesh of that name there',
842
+ );
843
+ }
844
+ // A mesh reads the slot's deform array; the setup pose leaves it empty,
845
+ // and emptying it here says so rather than trusting that it is.
846
+ slot.appliedPose.deform.length = 0;
847
+ const vertices = worldVerticesOf(skeleton, slot, attachment);
848
+ out.push(
849
+ attachment instanceof MeshAttachment
850
+ ? {
851
+ slot: entry.slot,
852
+ attachment: entry.attachment,
853
+ kind: 'mesh',
854
+ vertices,
855
+ triangles: Array.from(attachment.triangles),
856
+ hull: attachment.hullLength / 2,
857
+ uvs: Array.from(attachment.regionUVs),
858
+ }
859
+ : { slot: entry.slot, attachment: entry.attachment, kind: 'region', vertices, triangles: [...QUAD_TRIANGLES] },
860
+ );
861
+ }
862
+ }
863
+ return out;
864
+ }
865
+
866
+ /**
867
+ * One skeleton as it stands posed now, as the numbers `firstNonFinite`
868
+ * (`./nonfinite.ts`) reads: every bone's six world-transform terms, in skeleton
869
+ * order, then the world vertices of every region and mesh its slots show, in
870
+ * draw order — the runtime's reading of each (`worldVerticesOf`).
871
+ *
872
+ * ⭐ This is how `A10_NO_NAN_AFTER_STEPPING` reads each pose spine-core steps
873
+ * (issue #882; its runtime supplier in `validate.ts` since issue #1025), so the
874
+ * gate and the renderer hold one definition of "not finite": the six terms
875
+ * `firstNonFinite` reads off a bone, and the vertices the runtime computes from
876
+ * them. Before #882 A10 read the world POSITION alone, and a bone at `rotation:
877
+ * 1e309` — finite position, NaN `a`, `b`, `c`, `d` — was gated green and then
878
+ * refused here. The vertices are read too because a bone can be finite and
879
+ * still carry one that is not: a two-bone `scaleX` chain of 1e154 × 1e154
880
+ * leaves the child's `a` at 1e308, finite, and every vertex of its attachment
881
+ * past the largest double. `firstNonFinite` reads the bones first, so a broken
882
+ * bone is named as the cause rather than through its vertices.
883
+ */
884
+ export function posedNumbersOf(skeleton: Skeleton): { bones: WorldTransform[]; drawn: PosedVertices[] } {
885
+ const bones: WorldTransform[] = skeleton.bones.map((bone) => {
886
+ const { a, b, c, d, worldX, worldY } = bone.appliedPose;
887
+ return { name: bone.data.name, a, b, c, d, worldX, worldY };
888
+ });
889
+ const drawn: PosedVertices[] = [];
890
+ for (const slot of skeleton.drawOrder.appliedPose) {
891
+ const attachment = slot.appliedPose.attachment;
892
+ if (!(attachment instanceof MeshAttachment) && !(attachment instanceof RegionAttachment)) continue;
893
+ drawn.push({ slot: slot.data.name, attachment: attachment.name, vertices: worldVerticesOf(skeleton, slot, attachment) });
894
+ }
895
+ return { bones, drawn };
896
+ }
897
+
898
+ // ---------------------------------------------------------------------------
899
+ // texture-only substitution — see `substituteTexture`
900
+ // ---------------------------------------------------------------------------
901
+
902
+ /**
903
+ * One piece's UVs in the drawing's own space, for the region it resolved to.
904
+ *
905
+ * ## The two shapes, and why only one needs arithmetic
906
+ *
907
+ * A **mesh** already carries them. `MeshAttachment.regionUVs` are read by
908
+ * `spine-core` as coordinates over the *untrimmed* drawing — that is what its own
909
+ * `u -= region.offsetX / textureWidth` and `width = region.originalWidth /
910
+ * textureWidth` mean — so a mesh's authored UVs are atlas-independent by
911
+ * construction, and so is its geometry: `MeshAttachment.computeWorldVertices`
912
+ * reads `vertices` and bones and never touches the region at all. A mesh
913
+ * therefore has nothing an atlas swap could move except its texels.
914
+ *
915
+ * A **region** is the case issue #199 is about. Its quad is derived from the
916
+ * region rectangle — `RegionAttachment.computeUVs` insets it by `offsetX/offsetY`
917
+ * and sizes it by `region.width/height` over `originalWidth/originalHeight` — so
918
+ * swapping the atlas re-seats the quad as well as the texels. Its four corners
919
+ * span the sub-rectangle of the drawing its own atlas kept, in `spine-core`'s
920
+ * corner order (left-bottom, left-top, right-top, right-bottom, read straight off
921
+ * that function's `uvs` assignments).
922
+ *
923
+ * ⚠️ `null` for a region whose own atlas packs it **rotated**: the corner order
924
+ * above is the unrotated one, and `RegionAttachment.computeUVs` assigns a
925
+ * different one at 90°. rigc emits one unrotated part per page and never packs, so
926
+ * no candidate this ships for reaches that branch; a refusal that names itself is
927
+ * better than a fourth opinion about a mapping only three callers have.
928
+ */
929
+ function artUvsOf(attachment: MeshAttachment | RegionAttachment, region: TextureAtlasRegion): PieceTexture | undefined {
930
+ if (attachment instanceof MeshAttachment) {
931
+ return { region: regionKey(region), artUvs: Array.from(attachment.regionUVs) };
932
+ }
933
+ if (region.degrees !== 0) return undefined;
934
+ const ow = region.originalWidth;
935
+ const oh = region.originalHeight;
936
+ if (!(ow > 0) || !(oh > 0)) return undefined;
937
+ const s0 = region.offsetX / ow;
938
+ const s1 = (region.offsetX + region.width) / ow;
939
+ // `offsetY` is the trim measured from the drawing's BOTTOM and art space runs
940
+ // downwards, so the region's bottom edge is the larger of the two.
941
+ const tBottom = 1 - region.offsetY / oh;
942
+ const tTop = 1 - (region.offsetY + region.height) / oh;
943
+ return { region: regionKey(region), artUvs: [s0, tBottom, s0, tTop, s1, tTop, s1, tBottom] };
944
+ }
945
+
946
+ /**
947
+ * The skin roster of a parsed Spine file, as the runtime flags it —
948
+ * `unposedBones` over a fresh skeleton's `active` under each skin, so Spine's
949
+ * activation rule is the runtime's and is not restated here. Nothing is posed
950
+ * until a question is asked: the framing asks one (the skin it frames under,
951
+ * `slotsOnUnposedBones`) and the refusal asks one per skin.
952
+ */
953
+ export function skinRosterOf(data: SkeletonData): SkinRoster {
954
+ return {
955
+ skins: data.skins.map((k) => k.name),
956
+ unposedUnder: (skin) => {
957
+ const skeleton = skeletonUnderSkin(data, skin);
958
+ return unposedBones(skeleton.bones.map((bone) => ({ name: bone.data.name, parent: bone.parent?.data.name ?? null, active: bone.active })));
959
+ },
960
+ };
961
+ }
962
+
963
+ /**
964
+ * An atlas's pages and regions as spine-core's `TextureAtlas` reads them, each
965
+ * region mapping the drawing's own coordinates onto its page through the
966
+ * runtime's `MeshAttachment.computeUVs` — the `spine` reader of
967
+ * `textureSubstitutionFromText` (`SubstitutionReader`), which reaches it
968
+ * through the seam. Moved here unchanged from that function's branch (issue
969
+ * #1052): it names the runtime, and that function does not.
970
+ */
971
+ function spineSubstitution(atlasText: string): { pages: string[]; regions: Map<string, SubstituteRegion> } {
972
+ const regions = new Map<string, SubstituteRegion>();
973
+ const names: string[] = [];
974
+ const atlas = new TextureAtlas(atlasText);
975
+ for (const page of atlas.pages) names.push(page.name);
976
+ for (const region of atlas.regions) {
977
+ const page = { name: region.page.name, width: region.page.width, height: region.page.height };
978
+ regions.set(regionKey(region), {
979
+ page,
980
+ x: region.x,
981
+ y: region.y,
982
+ width: region.width,
983
+ height: region.height,
984
+ degrees: region.degrees,
985
+ pageUvs: (art) => {
986
+ const uvs = new Array<number>(art.length).fill(0);
987
+ MeshAttachment.computeUVs(region, [...art], uvs);
988
+ return uvs;
989
+ },
990
+ });
991
+ }
992
+ return { pages: names, regions };
993
+ }
994
+
995
+ // ---------------------------------------------------------------------------
996
+ // the Spine side, registered (issue #1052)
997
+ // ---------------------------------------------------------------------------
998
+ //
999
+ // ⭐ Loading this file is what fills the seam `./render_shared.ts` reads: every
1000
+ // program that imports it — `cli.ts`, the tools, the selftest — poses an export,
1001
+ // `--poser spine` and a fallback through spine-core exactly as before, and one
1002
+ // that never loads it links nothing of the runtime. Functions only: nothing of
1003
+ // the runtime is touched here, so a runtime that cannot be used is still met
1004
+ // where a run first needs it (`requireSpineRuntime`).
1005
+ registerSpinePosing({
1006
+ requireRuntime: requireSpineRuntime,
1007
+ skeletonData: (skeletonText, atlasText, refuse) => spineSkeletonData(skeletonText, atlasText, refuse),
1008
+ facts: (data, atlasText) => spineFacts(data as SkeletonData, atlasText),
1009
+ poser: (data) => spinePoser(data as SkeletonData),
1010
+ skinRoster: (data) => skinRosterOf(data as SkeletonData),
1011
+ atlasPageNames,
1012
+ substitution: spineSubstitution,
1013
+ });