rig-c 0.0.0-stage → 2.21.0

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 +1191 -0
  185. package/src/meshquality.ts +2051 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1444 -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,871 @@
1
+ /**
2
+ * The render's second poser (issue #968, step 3d of issue #380): the posing
3
+ * seam of `src/render.ts` (`Poser` / `Posed`) implemented over rigc's own
4
+ * core — the compiled model document (`skeleton.model.json`) posed by
5
+ * `src/core/`, each drawn region placed on its page by the document's own
6
+ * `pages` section (issue #1016; a `rigc-compiled/1` document, which has none,
7
+ * by the atlas beside it, read by rigc's own reader `src/atlas.ts`). Nothing
8
+ * here links spine-core, and nothing here is reached
9
+ * from `src/core/`: the core stays pure, and this module is the adapter from
10
+ * its raw entry to the renderer's shapes.
11
+ *
12
+ * ⭐ Why a module of its own rather than a second block in `src/render.ts`.
13
+ * `render.ts` links spine-core (an export's atlas pages and texture
14
+ * substitution, and `spinePoser`), so a core poser written there would share an import list
15
+ * with the runtime it replaces — and "the core poser imports nothing from
16
+ * spine-core" could then only be said, not read off the file. Here it can be:
17
+ * the value imports below are the core, the atlas reader and nothing else, and
18
+ * the one thing taken from `render.ts` is its types (erased at run time). The
19
+ * modules that link spine-core stay three (`CUR07`).
20
+ *
21
+ * ## What each part of a pose is read from
22
+ *
23
+ * - **The walk** is `poseRawAnimation` (`src/core/raw.ts`, issue #966) with
24
+ * `count` steps of `1/fps` and the reset taken at the animation
25
+ * (`'animation'`): pose `i` is the running sum of `i` steps, applied at
26
+ * `min(sum, duration)`, physics reset at pose 0 and stepped by `1/fps` after
27
+ * — the recipe `spinePoser` runs, measured bit-exact against it on every
28
+ * frame of the nineteen tree rows by the core suite's `CR03`. The setup pose
29
+ * is `poseRawSetup`. Since issue #1180 the walk is `poseRawAnimationEach`,
30
+ * the same walk handing each pose over as it is posed (`CORE_RAW_WALK`), so
31
+ * one pose is held at a time.
32
+ * - **Vertices, triangles, bones, slot colours, draw order** are the raw
33
+ * pose's, unchanged: the same doubles `computeWorldVertices`,
34
+ * `getWorldRotationX` and the slot pose hold (`CR03`).
35
+ * - **The page and page UVs** are `./core/uvs.ts`'s (issue #967): which
36
+ * region a drawn attachment samples (`drawnRegions` over the pose's `shown`
37
+ * records and draw order), a region's four UVs (`regionPageUvs`) and a
38
+ * mesh's (`meshPageUvs`), each held to `sequence.getUVs(index)` at
39
+ * tolerance 0 on every corpus by `core_gate`'s `uvs` blocks. Where each
40
+ * region sits — its page, `x`, `y`, turn and the page's size — is the
41
+ * document's `pages` section (issue #1016, `placementOf`), the atlas `build`
42
+ * wrote spelled into the document, so a build draws with its `.atlas`
43
+ * removed.
44
+ * - **The tint** is the slot's light colour times the attachment's colour,
45
+ * channel by channel, as `src/render.ts`'s `tintOf` forms it; the dark
46
+ * colour is the slot's, absent where the slot carries none.
47
+ * - **The clip.** Which slot a clip covers is the core's walk
48
+ * (`./core/clipping.ts`, issue #964), whose rows the raw pose carries with
49
+ * the attachment's LOCAL UVs. The render samples PAGE UVs, so each clipped
50
+ * row is cut again with `clipThrough` over the page UVs — the UV rule is
51
+ * linear in the corner UVs, and the vertices and triangles depend on
52
+ * positions alone, so the second cut must return the core's vertices,
53
+ * triangles and verdict to the bit, and a disagreement is thrown as a
54
+ * defect here rather than drawn. The polygon is the clip slot's world
55
+ * polygon from the pose's `clips` rows, and the walk that pairs a slot with
56
+ * the clip over it is the core's rule restated: a clip whose bone is active
57
+ * starts when none is active, and ends after its end slot is walked.
58
+ * - **A concave or inverse clip** is cut through the core's own convex
59
+ * decomposition (`clipThrough`, issue #964), and each drawn triangle carries
60
+ * its source triangle (`clipSourceOf`, `Mesh.source`): the rasteriser
61
+ * samples it at the source triangle's affine UV, so the pixels are the
62
+ * spine-core render's whatever pieces either clipper cut (`RC08`). A clip
63
+ * that is not simple is refused by the raw entry, naming the slot
64
+ * (`CoreInputError`); the render then falls back to `spinePoser` for that
65
+ * input and says so (`throughPoser` in `src/render.ts`).
66
+ *
67
+ * ## The skin
68
+ *
69
+ * `--skin <name>` poses `underSkin(doc, name)` — the named skin's record,
70
+ * else the default skin's; the named skin's bones and constraints —
71
+ * measured against spine-core's `setSkin(name)` by the per-skin gate
72
+ * (issue #932). No `--skin` is spine-core's "no skin set" — a fresh skeleton
73
+ * whose `setSkin` was never called — posed as `underNoSkin` (`noSkinView`,
74
+ * issue #1051): no skin's bones or constraints applied, the default skin's
75
+ * included, and every slot resolved through the default skin alone, or
76
+ * through nothing where the document declares skins and no `default` one
77
+ * (`./core/skins.ts`, *No skin set*, for the measurement). Until #1051 it was
78
+ * posed as `underSkin(doc, 'default')`, and the two documents that reading
79
+ * did not cover — a default skin naming a skin-required member, skins with
80
+ * no default one — were refused here and rendered through spine-core.
81
+ */
82
+ import type {
83
+ AttachmentPose,
84
+ AttachmentRest,
85
+ BoneSnapshot,
86
+ DrawOptions,
87
+ Piece,
88
+ PieceTexture,
89
+ PoseOptions,
90
+ Posed,
91
+ Poser,
92
+ SkinRoster,
93
+ SlotSubset,
94
+ } from './render.ts';
95
+ import { atlasRegionLookup, parseAtlasText } from './atlas.ts';
96
+ import { pagesOfAtlas, spineFileSha256, type ModelPage } from './model.ts';
97
+ import { clipThrough, type ClipShape, type ShapeClipper } from './core/clipping.ts';
98
+ import { activeBones, CoreInputError, readModel, sourceOfDoc, underNoSkin, underSkin, type CompiledDocument, type CoreSlotRow } from './core/index.ts';
99
+ import { poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
100
+ import type { TimelinePlant } from './core/animation.ts';
101
+ import { CORE_DEFAULT_SKIN, lookupSkins } from './core/skins.ts';
102
+ import { documentPageLookup, drawnRegions, meshPageUvs, readUvSequences, regionPageUvs, type DrawnRegion, type UvRegion, type UvSource } from './core/uvs.ts';
103
+ import { regionCorners, worldVertices } from './core/vertices.ts';
104
+ import type { CoreWorld } from './core/world.ts';
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // the slot subset, spelled once for both posers
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /**
111
+ * Why a slot subset cannot be drawn — a `--slot`/`--hide` naming no slot, a slot
112
+ * whose art only another skin carries, or both flags at once (issue #835).
113
+ *
114
+ * A class of its own so `cli.ts` can turn it into a usage refusal (exit 2,
115
+ * nothing written) without reading a message to decide what kind it is. It
116
+ * lives here, and `src/render.ts` re-exports it, so both posers throw the one
117
+ * class.
118
+ */
119
+ export class SlotSubsetError extends Error {}
120
+
121
+ /** The flag spelling each half of a subset is refused under — the UI's, since that is who reads it. */
122
+ const SUBSET_FLAG = { slots: '--slot', hidden: '--hide' } as const;
123
+
124
+ /** What `subsetOver` reads off a skeleton: its slots in draw order, which skins carry a slot's art, and its default skin. */
125
+ export interface SubsetRoster {
126
+ /** Every declared slot, in the setup draw order. */
127
+ declared: readonly string[];
128
+ /** The skins, in the skeleton's order, holding at least one attachment for `slot`. */
129
+ carriers(slot: string): string[];
130
+ /** The default skin's name, or `null` when there is none. */
131
+ defaultSkin: string | null;
132
+ }
133
+
134
+ /**
135
+ * `slotSubsetOf`'s rule over a roster rather than a parsed Spine skeleton, so
136
+ * the spine-core poser and the core poser refuse a subset in the same words —
137
+ * see `slotSubsetOf` in `src/render.ts` for the rule and its three refusals.
138
+ */
139
+ export function subsetOver(
140
+ roster: SubsetRoster,
141
+ opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined,
142
+ skin: string | undefined,
143
+ ): SlotSubset | undefined {
144
+ if (opts?.slots !== undefined && opts.hidden !== undefined) {
145
+ throw new SlotSubsetError(
146
+ '--slot and --hide are one statement two ways; name the slots to draw or the slots to hide, not both',
147
+ );
148
+ }
149
+ const mode = opts?.slots !== undefined ? 'slots' : opts?.hidden !== undefined ? 'hidden' : undefined;
150
+ if (mode === undefined) return undefined;
151
+ const asked = (mode === 'slots' ? opts?.slots : opts?.hidden) ?? [];
152
+ const flag = SUBSET_FLAG[mode];
153
+ const declared = [...roster.declared];
154
+
155
+ const unknown = asked.filter((name) => !declared.includes(name));
156
+ if (unknown.length > 0 || asked.length === 0) {
157
+ const named =
158
+ unknown.length === 0
159
+ ? 'was given no slot name'
160
+ : `${unknown.map((name) => JSON.stringify(name)).join(', ')} ${unknown.length === 1 ? 'names' : 'name'} no slot`;
161
+ throw new SlotSubsetError(
162
+ `${flag} ${named}; this skeleton declares, in draw order: ${declared.join(', ') || 'none'} (${declared.length})`,
163
+ );
164
+ }
165
+
166
+ const resolving = new Set([skin ?? null, roster.defaultSkin]);
167
+ const underThisPose =
168
+ skin === undefined ? 'under no skin (the default skin alone)' : `under skin ${JSON.stringify(skin)}`;
169
+ for (const name of asked) {
170
+ const carriers = roster.carriers(name);
171
+ if (carriers.length === 0 || carriers.some((s) => resolving.has(s))) continue;
172
+ const skins = carriers.map((s) => JSON.stringify(s));
173
+ throw new SlotSubsetError(
174
+ `${flag} ${JSON.stringify(name)} draws nothing ${underThisPose}: its attachments are declared only under ` +
175
+ `${skins.length === 1 ? 'skin' : 'skins'} ${skins.join(', ')} — pass --skin ${
176
+ skins.length === 1 ? skins[0] : 'with one of them'
177
+ }`,
178
+ );
179
+ }
180
+ const chosen = new Set(asked);
181
+ return { mode, names: declared.filter((name) => chosen.has(name)) };
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // a bone the posed skin leaves inactive, spelled once for both posers
186
+ // ---------------------------------------------------------------------------
187
+
188
+ /**
189
+ * The snapshot of a bone the posed skin leaves UNPOSED — inactive itself (a
190
+ * skin-required bone the skin does not name) or below an inactive bone: the
191
+ * zero transform, every number `0` (issue #968).
192
+ *
193
+ * ⚠️ "Below an inactive bone" is the half the runtime's flag does not say. A
194
+ * bone that is not skin-required reads `active` true under an inactive parent
195
+ * (measured: `arm` skin-required and unnamed, its child `hand` not
196
+ * skin-required — spine-core reads `arm=false hand=true`), yet its matrix is
197
+ * never computed: it stays the zero matrix over every frame unless a
198
+ * constraint writes into it. So the predicate is the bone's own flag and every
199
+ * ancestor's (`unposedBones`).
200
+ *
201
+ * ⭐ Why the seam defines it rather than relaying either runtime. An inactive
202
+ * bone is not posed: neither poser computes its world transform, so its
203
+ * matrix holds whatever a constraint listing it happened to write into a zero
204
+ * matrix. Measured on hand-written rigs under no skin (a skin-required `arm`
205
+ * and its child `hand`, both inactive): a one-bone ik on `hand` left
206
+ * spine-core's matrix at zeros and the core's at NaN; a two-bone ik on `arm,
207
+ * hand` left spine-core's `b` and `d` at `-0` — so `getWorldRotationY` read
208
+ * −179.99999734 (`atan2(−0, …)` at the runtime's pi) — and the core's at `+0`,
209
+ * reading 0; a world transform constraint wrote `worldX` 19.99999979 and a
210
+ * `-0` into both alike. The private corpus met the second case on two rigs:
211
+ * 508 rotation readings of 179.99999734 against 0, every pixel identical. None
212
+ * of those numbers is a pose — a rotation of ±180 is the sign of a zero, and
213
+ * the core's NaN the same degenerate arithmetic taken another way — so the
214
+ * snapshot says what is true of the bone, that it is not posed, in both
215
+ * posers alike. The drawn pieces are untouched: they are posed vertices, not
216
+ * snapshots.
217
+ */
218
+ export function inactiveBoneSnapshot(name: string): BoneSnapshot {
219
+ return { name, worldX: 0, worldY: 0, a: 0, b: 0, c: 0, d: 0, rotationX: 0, rotationY: 0, scaleX: 0, scaleY: 0 };
220
+ }
221
+
222
+ /** The bones the posed skin leaves unposed — inactive, or below an inactive bone — from each bone's parent and flag, parents first. */
223
+ export function unposedBones(bones: ReadonlyArray<{ name: string; parent: string | null; active: boolean }>): Set<string> {
224
+ const out = new Set<string>();
225
+ for (const b of bones) if (!b.active || (b.parent !== null && out.has(b.parent))) out.add(b.name);
226
+ return out;
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // a clipped piece's source triangles, spelled once for both posers
231
+ // ---------------------------------------------------------------------------
232
+
233
+ /**
234
+ * `Mesh.source` of a clipped piece (issue #964): for each drawn triangle, in
235
+ * order, its source triangle's world corners and page UVs — and, with
236
+ * `artUvs`, their original-art UVs — six numbers each, read off the UNCLIPPED
237
+ * piece's own arrays. `sources[i]` is the source triangle of drawn triangle `i`
238
+ * (an index into `triangles` by threes). The rasteriser samples a drawn
239
+ * triangle at that source triangle's affine map, so the pixels do not depend on
240
+ * which convex pieces a clipper cut.
241
+ */
242
+ export function clipSourceOf(
243
+ world: ArrayLike<number>,
244
+ uvs: ArrayLike<number>,
245
+ triangles: ArrayLike<number>,
246
+ sources: readonly number[],
247
+ artUvs?: ArrayLike<number>,
248
+ ): { world: number[]; uvs: number[]; artUvs: number[] | undefined } {
249
+ const w: number[] = [];
250
+ const u: number[] = [];
251
+ const a: number[] | undefined = artUvs === undefined ? undefined : [];
252
+ for (const t of sources) {
253
+ for (let k = 0; k < 3; k++) {
254
+ const i = triangles[3 * t + k];
255
+ w.push(world[2 * i], world[2 * i + 1]);
256
+ u.push(uvs[2 * i], uvs[2 * i + 1]);
257
+ if (a !== undefined && artUvs !== undefined) a.push(artUvs[2 * i], artUvs[2 * i + 1]);
258
+ }
259
+ }
260
+ return { world: w, uvs: u, artUvs: a };
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // the core poser
265
+ // ---------------------------------------------------------------------------
266
+
267
+ /** The runtime's own triangulation of a region's quad — `src/render.ts`'s `QUAD_TRIANGLES`. */
268
+ const QUAD_TRIANGLES: readonly number[] = [0, 1, 2, 2, 3, 0];
269
+
270
+ /** A region as texture substitution reads it: the page-UV rules' numbers, and the name and `index:` its key is made of (`regionKey`). */
271
+ type TextureRegion = UvRegion & { name: string; index: number };
272
+
273
+ /** Everything a core pose reads besides the pose: the document under one skin, and where each region sits on its page. */
274
+ interface CoreInput {
275
+ doc: CompiledDocument;
276
+ source: UvSource;
277
+ /** The region a name draws — the document's `pages`, or for a `/1` document the atlas's — what an original-art UV reads its trim from. */
278
+ region: (name: string) => TextureRegion | null;
279
+ /** The page UVs and triangle lists the pieces hold (`PieceArrays`), one table for every pose of this poser. */
280
+ arrays: PieceArrays;
281
+ }
282
+
283
+ /**
284
+ * The two arrays a drawn piece holds that are not a function of the pose
285
+ * (issue #1180): its page UVs — a function of the region it samples and,
286
+ * for a mesh, the attachment's own UVs — and a mesh's triangle list.
287
+ *
288
+ * ⭐ Why a table rather than an array per piece. A render holds its frames
289
+ * before it writes one (every animation at `PROTOCOL_FPS`), and the framing
290
+ * reads every animation at `FRAMING_FPS`; a fresh copy of both arrays in every
291
+ * piece of every frame was what made the core poser's render peak at 3.3x
292
+ * spine-core's resident size on a production rig (4,015 against 1,214 MiB,
293
+ * n = 5), where spine-core's pieces hold the attachment's own `uvs` and
294
+ * `triangles` and only the world vertices are per frame. Held here, the count
295
+ * of these arrays is bounded by what the rig draws — one per region and
296
+ * attachment UVs, one per distinct triangle list of a slot's attachment — and
297
+ * not by how many frames are posed (`RC43`).
298
+ *
299
+ * The values are the same doubles either way: the UVs are computed once by
300
+ * the same call from the same inputs, and a triangle list is reused only where
301
+ * it is equal, index for index, to the one the pose carries. No reader writes
302
+ * into a piece's arrays — spine-core's pieces have always shared theirs.
303
+ */
304
+ export interface PieceArrays {
305
+ /** The page UVs `found` is drawn with: `regionPageUvs` for a region, `meshPageUvs` over the attachment's UVs for a mesh. */
306
+ uvs(found: DrawnRegion): number[];
307
+ /** A triangle list equal, index for index, to `drawn.triangles`. */
308
+ triangles(drawn: RawDrawn): number[];
309
+ }
310
+
311
+ /** `PieceArrays` kept for the poser's lifetime — what `corePoser` builds unless a plant passes another. */
312
+ export function keptPieceArrays(): PieceArrays {
313
+ // Keyed on the placement's own object (one per region name, `documentPageLookup`/`atlasRegionLookup`) and the
314
+ // document's UV array (or `null` for a region): both live as long as the poser, so a key is never a copy.
315
+ const uvs = new WeakMap<object, Map<readonly number[] | null, number[]>>();
316
+ // Keyed by slot and attachment, and each list compared index for index before it is reused: a key names where
317
+ // a list was drawn, the comparison is what makes reusing it exact.
318
+ const triangles = new Map<string, number[][]>();
319
+ return {
320
+ uvs: (found) => {
321
+ let byArt = uvs.get(found.found);
322
+ if (byArt === undefined) {
323
+ byArt = new Map();
324
+ uvs.set(found.found, byArt);
325
+ }
326
+ let kept = byArt.get(found.art);
327
+ if (kept === undefined) {
328
+ kept = found.art === null ? regionPageUvs(found.found.region, found.found.page) : meshPageUvs(found.found.region, found.found.page, found.art);
329
+ byArt.set(found.art, kept);
330
+ }
331
+ return kept;
332
+ },
333
+ triangles: (drawn) => {
334
+ const key = `${drawn.slot}\u0000${drawn.attachment}`;
335
+ let lists = triangles.get(key);
336
+ if (lists === undefined) {
337
+ lists = [];
338
+ triangles.set(key, lists);
339
+ }
340
+ const wanted = drawn.triangles;
341
+ const equal = (list: readonly number[]): boolean => {
342
+ if (list.length !== wanted.length) return false;
343
+ for (let i = 0; i < list.length; i++) if (list[i] !== wanted[i]) return false;
344
+ return true;
345
+ };
346
+ let kept = lists.find(equal);
347
+ if (kept === undefined) {
348
+ kept = [...wanted];
349
+ lists.push(kept);
350
+ }
351
+ return kept;
352
+ },
353
+ };
354
+ }
355
+
356
+ /**
357
+ * The raw walk the core poser steps an animation with (issue #1180):
358
+ * `poseRawAnimationEach`, which hands each pose to `visit` as it is posed, so
359
+ * one pose is held at a time rather than the animation's whole series. A plant
360
+ * passes another (`RC44`).
361
+ */
362
+ export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void) => void;
363
+
364
+ /** The walk `corePoser` steps with unless a plant passes another: one pose posed, drawn and released before the next. */
365
+ export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit) => poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit);
366
+
367
+ /** What a suite passes `corePoser` in place of the poser's own parts — `PieceArrays` (`RC43`) and the raw walk (`RC44`). */
368
+ export interface CorePoserPlants {
369
+ arrays?: () => PieceArrays;
370
+ walk?: CoreRawWalk;
371
+ }
372
+
373
+ /**
374
+ * The document posed with no skin set — spine-core's initial state, a fresh
375
+ * skeleton whose `setSkin` was never called: `underNoSkin` (issue #1051), no
376
+ * skin's `bones` or constraint lists applied, the default skin's included,
377
+ * and every slot resolved through the default skin alone — or through
378
+ * nothing, where the document declares skins and none of them `default`
379
+ * (`./core/skins.ts`, *No skin set*, for the measurement).
380
+ *
381
+ * Exported for the model side of the validator (issue #1025), which poses a
382
+ * slot the way `validate()` does — a fresh skeleton, no skin set — and must
383
+ * resolve that state by this rule rather than by a copy of it.
384
+ */
385
+ export function noSkinView(doc: CompiledDocument): CompiledDocument {
386
+ return underNoSkin(doc);
387
+ }
388
+
389
+ /** A slot row's number, which the raw entry writes as a double — `null` only for a value that is not finite. */
390
+ function channel(value: number | null, slot: string): number {
391
+ if (value === null) throw new CoreInputError(`slot "${slot}": the raw pose computed a colour channel that is not finite`);
392
+ return value;
393
+ }
394
+
395
+ /** The world transforms of a raw pose's bones, by name — what the rest table poses through. */
396
+ function worldOf(pose: RawPose): Map<string, CoreWorld> {
397
+ return new Map(pose.bones.map((b) => [b.name, { a: b.a, b: b.b, c: b.c, d: b.d, worldX: b.worldX, worldY: b.worldY }]));
398
+ }
399
+
400
+ /** `regionKey` in `src/render.ts`: the trimmed name and the sequence index — the key a substitution matches on. */
401
+ function regionKey(region: TextureRegion): string {
402
+ return `${region.name.trim()}#${region.index}`;
403
+ }
404
+
405
+ /** `artUvsOf` in `src/render.ts`, over rigc's atlas reader: a mesh's own UVs, a region's kept rectangle in the drawing's space. */
406
+ function artUvsOf(drawn: RawDrawn, region: TextureRegion): PieceTexture | undefined {
407
+ if (drawn.kind === 'mesh') return { region: regionKey(region), artUvs: [...drawn.uvs] };
408
+ if (region.degrees !== 0) return undefined;
409
+ const ow = region.originalWidth;
410
+ const oh = region.originalHeight;
411
+ if (!(ow > 0) || !(oh > 0)) return undefined;
412
+ const s0 = region.offsetX / ow;
413
+ const s1 = (region.offsetX + region.width) / ow;
414
+ const tBottom = 1 - region.offsetY / oh;
415
+ const tTop = 1 - (region.offsetY + region.height) / oh;
416
+ return { region: regionKey(region), artUvs: [s0, tBottom, s0, tTop, s1, tTop, s1, tBottom] };
417
+ }
418
+
419
+ /**
420
+ * The clip polygon over each slot of the draw order, where a clip covers it —
421
+ * the core's walk (`poseClipped` in `./core/clipping.ts`) restated to pair a
422
+ * slot with its polygon; `clippedPieces` holds its answer to the core's own
423
+ * rows.
424
+ */
425
+ function clipCover(pose: RawPose, active: ReadonlySet<string>): Map<string, ClipShape> {
426
+ const polygons = new Map(pose.clips.map((c) => [c[0], { end: c[2], polygon: c[3] }]));
427
+ const kinds = new Map(pose.shown.map((s) => [s.slot, s]));
428
+ const cover = new Map<string, ClipShape>();
429
+ let current: { end: string | null; shape: ClipShape } | null = null;
430
+ for (const slot of pose.drawOrder) {
431
+ const shown = kinds.get(slot);
432
+ const g = shown?.geometry;
433
+ const clip = g?.kind === 'clipping' ? polygons.get(slot) : undefined;
434
+ if (shown !== undefined && g?.kind === 'clipping' && clip !== undefined) {
435
+ if (current !== null && current.end === slot) current = null;
436
+ if (active.has(shown.bone) && current === null) current = { end: clip.end, shape: { polygon: clip.polygon, inverse: g.inverse, convex: g.convex } };
437
+ continue;
438
+ }
439
+ if (current !== null) cover.set(slot, current.shape);
440
+ if (current !== null && current.end === slot) current = null;
441
+ }
442
+ return cover;
443
+ }
444
+
445
+ /** One posed moment of the core, read through the seam's `Posed`. */
446
+ function corePosed(input: CoreInput, pose: RawPose, through: ShapeClipper): Posed {
447
+ const slotRows = new Map<string, CoreSlotRow>(pose.slots.map((row) => [row[0], row]));
448
+ let regions: Map<string, DrawnRegion> | null = null;
449
+ const regionsOf = (): Map<string, DrawnRegion> => {
450
+ if (regions !== null) return regions;
451
+ const { drawn, why } = drawnRegions(input.doc, pose.shown, pose.drawOrder, input.source);
452
+ if (drawn === null) throw new CoreInputError(`the page UVs are not posed: ${why}`);
453
+ regions = new Map(drawn.map((d) => [d.slot, d]));
454
+ return regions;
455
+ };
456
+ const tintOf = (d: RawDrawn): [number, number, number, number] => {
457
+ const row = slotRows.get(d.slot);
458
+ if (row === undefined) throw new CoreInputError(`slot "${d.slot}" drew and has no slot row`);
459
+ return [
460
+ channel(row[2], d.slot) * d.colour[0],
461
+ channel(row[3], d.slot) * d.colour[1],
462
+ channel(row[4], d.slot) * d.colour[2],
463
+ channel(row[5], d.slot) * d.colour[3],
464
+ ];
465
+ };
466
+ const darkOf = (d: RawDrawn): [number, number, number] | undefined => {
467
+ const dark = slotRows.get(d.slot)?.[6] ?? null;
468
+ return dark === null ? undefined : [channel(dark[0], d.slot), channel(dark[1], d.slot), channel(dark[2], d.slot)];
469
+ };
470
+ return {
471
+ pieces: (draw: DrawOptions): Piece[] => pieces(input, pose, draw, regionsOf(), tintOf, darkOf, through),
472
+ bones: (): BoneSnapshot[] => {
473
+ const unposed = unposedBones(pose.bones);
474
+ return pose.bones.map((b) => !unposed.has(b.name) ? ({
475
+ name: b.name,
476
+ worldX: b.worldX,
477
+ worldY: b.worldY,
478
+ a: b.a,
479
+ b: b.b,
480
+ c: b.c,
481
+ d: b.d,
482
+ rotationX: b.rotationX,
483
+ rotationY: b.rotationY,
484
+ scaleX: b.scaleX,
485
+ scaleY: b.scaleY,
486
+ }) : inactiveBoneSnapshot(b.name));
487
+ },
488
+ attachments: (): AttachmentPose[] =>
489
+ pose.drawn.map((d) => ({ slot: d.slot, attachment: d.attachment, vertices: [...d.vertices], color: tintOf(d) })),
490
+ };
491
+ }
492
+
493
+ /** The drawables of one core pose, in draw order — `drawPieces` in `src/render.ts`, read off the raw pose. */
494
+ function pieces(
495
+ input: CoreInput,
496
+ pose: RawPose,
497
+ draw: DrawOptions,
498
+ regions: Map<string, DrawnRegion>,
499
+ tintOf: (d: RawDrawn) => [number, number, number, number],
500
+ darkOf: (d: RawDrawn) => [number, number, number] | undefined,
501
+ through: ShapeClipper,
502
+ ): Piece[] {
503
+ const named = draw.subset === undefined ? undefined : new Set(draw.subset.names);
504
+ const cover = draw.unclipped ? new Map<string, ClipShape>() : clipCover(pose, new Set(pose.bones.filter((b) => b.active).map((b) => b.name)));
505
+ const rows = new Map(pose.clipped.map((row) => [row[0], row]));
506
+ if (!draw.unclipped) {
507
+ const walked = [...cover.keys()].filter((slot) => pose.drawn.some((d) => d.slot === slot)).join();
508
+ const cored = pose.clipped.map((row) => row[0]).join();
509
+ if (walked !== cored) throw new Error(`the clip cover [${walked}] is not the core's clipped roster [${cored}] — the two walks disagree`);
510
+ }
511
+ const out: Piece[] = [];
512
+ for (const d of pose.drawn) {
513
+ const drawn = draw.subset === undefined || named === undefined || named.has(d.slot) === (draw.subset.mode === 'slots');
514
+ if (!drawn) continue;
515
+ const found = regions.get(d.slot);
516
+ if (found === undefined) throw new CoreInputError(`slot "${d.slot}" drew "${d.attachment}", and no atlas region was resolved for it`);
517
+ const uvs = input.arrays.uvs(found);
518
+ const atlasRegion = draw.texture ? input.region(found.region) : null;
519
+ if (draw.texture && atlasRegion === null) throw new CoreInputError(`slot "${d.slot}": atlas region "${found.region}" is not in the atlas`);
520
+ const texture = atlasRegion === null ? undefined : artUvsOf(d, atlasRegion);
521
+ const common = { tint: tintOf(d), dark: darkOf(d), slot: d.slot, page: found.found.page.name };
522
+ const world = [...d.vertices];
523
+ const piece: Piece =
524
+ d.kind === 'mesh'
525
+ ? { kind: 'mesh', ...common, texture, world, uvs, triangles: input.arrays.triangles(d) }
526
+ : { kind: 'region', ...common, texture, world, uvs };
527
+ const shape = cover.get(d.slot);
528
+ out.push(shape === undefined ? piece : clippedPiece(piece, d, shape, uvs, rows.get(d.slot), through));
529
+ }
530
+ return out;
531
+ }
532
+
533
+ /**
534
+ * `piece` cut by the clip over it, over the page UVs (the header's *The clip*),
535
+ * or `piece` itself when the clipper cut nothing — `clippedPiece` in
536
+ * `src/render.ts`. The cut's vertices, triangles and verdict are held to the
537
+ * core's own row for the slot.
538
+ */
539
+ function clippedPiece(piece: Piece, d: RawDrawn, shape: ClipShape, uvs: number[], row: RawPose['clipped'][number] | undefined, through: ShapeClipper): Piece {
540
+ const cut = through(shape, d.vertices, d.triangles, uvs);
541
+ if (cut === null) throw new Error(`slot "${d.slot}": the clip over it is one the core does not draw, and the raw pose carried it — a defect in src/render_core.ts`);
542
+ if (row === undefined || (row[2] === 1) !== cut.clipped || row[3].join() !== cut.vertices.join() || row[5].join() !== cut.triangles.join()) {
543
+ throw new Error(`slot "${d.slot}": the clip over the page UVs cut other geometry than the core's clipped row — a defect in src/render_core.ts`);
544
+ }
545
+ if (!cut.clipped) return piece;
546
+ let texture = piece.texture;
547
+ const source = clipSourceOf(d.vertices, uvs, d.triangles, cut.sources, texture?.artUvs);
548
+ if (texture !== undefined) {
549
+ const art = through(shape, d.vertices, d.triangles, texture.artUvs);
550
+ if (art === null || art.uvs.length !== cut.uvs.length) {
551
+ throw new Error(
552
+ `slot "${piece.slot}": the clip cut ${cut.uvs.length / 2} vertices for the page UVs and ` +
553
+ `${(art?.uvs.length ?? 0) / 2} for the original-art UVs over the same geometry`,
554
+ );
555
+ }
556
+ texture = { region: texture.region, artUvs: art.uvs, sourceArtUvs: source.artUvs };
557
+ }
558
+ const { tint, dark, slot, page } = piece;
559
+ return { kind: 'mesh', tint, dark, slot, page, texture, world: cut.vertices, uvs: cut.uvs, triangles: cut.triangles, source: { world: source.world, uvs: source.uvs } };
560
+ }
561
+
562
+ /**
563
+ * The rest table (`restOf` in `src/render.ts`): every (slot, attachment) the
564
+ * frames show, in order of first appearance, posed on the setup bones with no
565
+ * deform — the attachment looked up by that name as a placeholder in the
566
+ * posed skin, then the default skin, as `Skeleton.getAttachment` does.
567
+ */
568
+ function restOf(view: CompiledDocument, setup: RawPose, shown: readonly AttachmentPose[][]): AttachmentRest[] {
569
+ const world = worldOf(setup);
570
+ const bones = new Map(view.slots.map((s) => [s.name, s.bone]));
571
+ const sourceOf = sourceOfDoc(view);
572
+ const seen = new Map<string, Set<string>>();
573
+ const out: AttachmentRest[] = [];
574
+ for (const entries of shown) {
575
+ for (const entry of entries) {
576
+ const names = seen.get(entry.slot) ?? new Set<string>();
577
+ if (names.has(entry.attachment)) continue;
578
+ names.add(entry.attachment);
579
+ seen.set(entry.slot, names);
580
+ const skin = lookupSkins(view).find((k) => k.attachments[entry.slot]?.[entry.attachment] !== undefined);
581
+ const record = skin?.attachments[entry.slot]?.[entry.attachment];
582
+ const g = record?.geometry;
583
+ const boneName = bones.get(entry.slot);
584
+ const bone = boneName === undefined ? undefined : world.get(boneName);
585
+ if (skin === undefined || g === undefined || bone === undefined || (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh')) {
586
+ throw new Error(
587
+ `slot ${JSON.stringify(entry.slot)} showed attachment ${JSON.stringify(entry.attachment)} in a frame, and ` +
588
+ 'the setup skeleton resolves no region or mesh of that name there',
589
+ );
590
+ }
591
+ if (g.kind === 'region') {
592
+ const rect = g.region.atlas;
593
+ if (rect === null) throw new CoreInputError(`slot "${entry.slot}" region "${entry.attachment}": the model states the build had no atlas rectangle for it`);
594
+ out.push({ slot: entry.slot, attachment: entry.attachment, kind: 'region', vertices: regionCorners({ ...g.region, atlas: rect }, bone), triangles: [...QUAD_TRIANGLES] });
595
+ continue;
596
+ }
597
+ const mesh = g.kind === 'mesh' ? g : view.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
598
+ const vertices = g.kind === 'mesh' ? g.vertices : sourceOf(g.skin, g.slot, g.source);
599
+ if (mesh === undefined || mesh.kind !== 'mesh' || typeof vertices === 'string') throw new CoreInputError(`slot "${entry.slot}": the linked mesh "${entry.attachment}" resolves no source mesh`);
600
+ if (mesh.hull === undefined) throw new CoreInputError(`slot "${entry.slot}" mesh "${entry.attachment}": the model states no hull`);
601
+ out.push({
602
+ slot: entry.slot,
603
+ attachment: entry.attachment,
604
+ kind: 'mesh',
605
+ vertices: worldVertices(vertices, bone, world),
606
+ triangles: [...mesh.triangles],
607
+ hull: mesh.hull,
608
+ uvs: [...mesh.uvs],
609
+ });
610
+ }
611
+ }
612
+ return out;
613
+ }
614
+
615
+ /**
616
+ * The first place two `pages` sections differ, by path, or `null` when they
617
+ * are the same — the page count, a page's name or size, its `pma` or `scale`
618
+ * where `stated` carries them (a `rigc-compiled/3` document's, issue #1026), a
619
+ * region count, or a region's name or one of its numbers.
620
+ */
621
+ export function firstPageDifference(stated: readonly ModelPage[], found: readonly ModelPage[]): string | null {
622
+ if (stated.length !== found.length) return `the document states ${stated.length} page(s), the atlas has ${found.length}`;
623
+ for (let i = 0; i < stated.length; i++) {
624
+ const a = stated[i];
625
+ const b = found[i];
626
+ for (const key of ['name', 'width', 'height'] as const) {
627
+ if (a[key] !== b[key]) return `pages[${i}].${key} is ${JSON.stringify(a[key])} in the document and ${JSON.stringify(b[key])} in the atlas`;
628
+ }
629
+ // A rigc-compiled/3 page states its `pma` and `scale` too (issue #1026), and an atlas edited in either after the build is not the one it was written beside; a /2 page states neither, and is held to its placement alone.
630
+ for (const key of ['pma', 'scale'] as const) {
631
+ if (a[key] !== undefined && a[key] !== b[key]) return `pages[${i}] "${a.name}": ${key} is ${JSON.stringify(a[key])} in the document and ${JSON.stringify(b[key])} in the atlas`;
632
+ }
633
+ if (a.regions.length !== b.regions.length) return `pages[${i}] "${a.name}" holds ${a.regions.length} region(s) in the document and ${b.regions.length} in the atlas`;
634
+ for (let j = 0; j < a.regions.length; j++) {
635
+ const r = a.regions[j];
636
+ const q = b.regions[j];
637
+ for (const key of Object.keys(r) as Array<keyof typeof r>) {
638
+ if (r[key] !== q[key]) return `pages[${i}] "${a.name}" region ${JSON.stringify(r.name)}: ${key} is ${JSON.stringify(r[key])} in the document and ${JSON.stringify(q[key])} in the atlas`;
639
+ }
640
+ }
641
+ }
642
+ return null;
643
+ }
644
+
645
+ /**
646
+ * Where each region a core pose draws sits on its page, read from the
647
+ * document's `pages` section (issue #1016) — or, for a `rigc-compiled/1`
648
+ * document, which has none, from `atlasText` as before. With both, the atlas
649
+ * must be the one the document was written beside: a page or region that
650
+ * differs is refused naming the first difference, so the core never draws the
651
+ * build's placement over another atlas's pages while spine-core, reading that
652
+ * atlas, would draw another picture.
653
+ */
654
+ function placementOf(doc: CompiledDocument, atlasText: string, where: string): { lookup: UvSource['lookup']; region: CoreInput['region'] } {
655
+ if (doc.pages !== null) {
656
+ if (atlasText !== '') {
657
+ const differs = firstPageDifference(doc.pages, pagesOfAtlas(atlasText));
658
+ if (differs !== null) {
659
+ throw new CoreInputError(
660
+ `the atlas beside ${where} is not the one it was written beside: ${differs} — ` +
661
+ 'the core would draw the build\'s placement over pages the atlas has rearranged',
662
+ );
663
+ }
664
+ }
665
+ const lookup = documentPageLookup(doc.pages);
666
+ return { lookup, region: (name) => lookup(name)?.region ?? null };
667
+ }
668
+ if (atlasText === '') {
669
+ throw new CoreInputError(
670
+ `${where} is a ${doc.spec} document, which does not state where each region sits on its page (the pages section, issue #1016), ` +
671
+ 'and no atlas was given to read it from — rebuild it to carry them, or pose it beside the atlas it was built with',
672
+ );
673
+ }
674
+ const lookup = atlasRegionLookup(parseAtlasText(atlasText));
675
+ return { lookup, region: (name) => lookup(name)?.region ?? null };
676
+ }
677
+
678
+ /**
679
+ * `Poser` over rigc's own core: `modelText` a `rigc-compiled/3` or `/2` document
680
+ * (`skeleton.model.json`), which states where each region sits on its page
681
+ * (`pages`, issue #1016), so `atlasText` may be `''`; given, it is held to
682
+ * the document's `pages` (`placementOf`). A `rigc-compiled/1` document states
683
+ * no placement and is drawn through `atlasText`, the atlas `build` wrote
684
+ * beside it, as before; with none given it is refused by name. With `skeleton`, the Spine file beside the document
685
+ * is held to the digest the document records (`spine.sha256`, issue #968) and
686
+ * refused, naming both digests, when it is not that build's. Refused by
687
+ * `CoreInputError`, naming why, where the document or the atlas cannot be read; a pose the core leaves a block of out
688
+ * is refused the same way when it is asked for (the header).
689
+ */
690
+ export function corePoser(
691
+ modelText: string,
692
+ atlasText: string,
693
+ where = 'skeleton.model.json',
694
+ skeleton?: { path: string; bytes: Uint8Array },
695
+ through: ShapeClipper = clipThrough,
696
+ plants: CorePoserPlants = {},
697
+ ): Poser {
698
+ const doc = readModel(modelText, where);
699
+ // The document poses the rig it was built with; the Spine file beside it must be that build's (issue #968).
700
+ if (skeleton !== undefined) {
701
+ const found = spineFileSha256(skeleton.bytes);
702
+ if (found !== doc.spine.sha256) {
703
+ throw new CoreInputError(
704
+ `${skeleton.path} is not the skeleton.json ${where} was written beside: its sha256 is ${found}, the document records ${doc.spine.sha256} — ` +
705
+ 'the Spine file was edited or replaced after the build, and the core would draw the build\'s rig instead of it',
706
+ );
707
+ }
708
+ }
709
+ let parsed: unknown;
710
+ try {
711
+ parsed = JSON.parse(modelText);
712
+ } catch (err) {
713
+ throw new CoreInputError(`${where}: not JSON — ${(err as Error).message}`);
714
+ }
715
+ const sequences = readUvSequences(parsed);
716
+ const { lookup, region } = placementOf(doc, atlasText, where);
717
+ const views = new Map<string, CompiledDocument>();
718
+ const arrays = (plants.arrays ?? keptPieceArrays)();
719
+ const walk = plants.walk ?? CORE_RAW_WALK;
720
+ const inputOf = (skin: string | undefined): CoreInput => {
721
+ const key = skin === undefined ? '' : `=${skin}`;
722
+ let view = views.get(key);
723
+ if (view === undefined) {
724
+ view = skin === undefined ? noSkinView(doc) : underSkin(doc, skin);
725
+ views.set(key, view);
726
+ }
727
+ return { doc: view, source: { lookup, sequences }, region, arrays };
728
+ };
729
+ const roster: SubsetRoster = {
730
+ declared: doc.slots.map((s) => s.name),
731
+ carriers: (slot) => doc.skins.filter((k) => Object.keys(k.attachments[slot] ?? {}).length > 0).map((k) => k.name),
732
+ defaultSkin: doc.skins.some((k) => k.name === CORE_DEFAULT_SKIN) ? CORE_DEFAULT_SKIN : null,
733
+ };
734
+ return {
735
+ animations: doc.animations.map((a) => ({ name: a.name, duration: a.timelines.duration })),
736
+ bones: doc.bones.map((b) => ({ name: b.name, parent: b.parent ?? null })),
737
+ slots: doc.slots.map((s) => ({ name: s.name, bone: s.bone })),
738
+ subset: (opts, skin) => subsetOver(roster, opts, skin),
739
+ setup: (skin) => {
740
+ const input = inputOf(skin);
741
+ return corePosed(input, poseRawSetup(input.doc, { through }), through);
742
+ },
743
+ animation: (name, skin, fps, count, visit) => {
744
+ const input = inputOf(skin);
745
+ const step = 1 / fps;
746
+ // Each pose is drawn as it is posed and released before the next (issue #1180), so the walk holds one pose and not
747
+ // the animation's series. What is thrown is what was thrown when the series was posed first and drawn after: a
748
+ // walk that refuses a later pose still refuses the call, and a draw that throws is re-thrown once the walk has
749
+ // finished without refusing — the first such throw, by frame — and no frame after it is drawn.
750
+ const drawFailed: unknown[] = [];
751
+ walk(input.doc, name, new Array<number>(count).fill(step), { through }, (pose, i) => {
752
+ if (drawFailed.length > 0) return;
753
+ try {
754
+ visit(i, corePosed(input, pose, through));
755
+ } catch (thrown) {
756
+ drawFailed.push(thrown);
757
+ }
758
+ });
759
+ if (drawFailed.length > 0) throw drawFailed[0];
760
+ },
761
+ rest: (skin, shown) => {
762
+ const input = inputOf(skin);
763
+ return restOf(input.doc, poseRawSetup(input.doc), shown);
764
+ },
765
+ };
766
+ }
767
+
768
+ /**
769
+ * The skin roster behind the core poser (`SkinRoster` in `src/render.ts`,
770
+ * issue #1014): the bones each skin leaves unposed, read off the model
771
+ * document — `unposedBones` over `activeBones` of the skin's view, the
772
+ * predicate the raw pose flags its bones with, and under no skin the view the
773
+ * core poses with no skin set (`noSkinView`). `skins` is the skin list in the Spine file's order:
774
+ * a `rigc-compiled/3` document's `editorOrder` (issue #1026), else the file's
775
+ * own list, which a `/2` or `/1` document does not hold (`resolveSkinView`'s
776
+ * note in `./core/index.ts`).
777
+ *
778
+ * Measured against the runtime's reading (`skinRosterOf`, a fresh skeleton's
779
+ * `active` under each skin and under none) on every rigc build the tree
780
+ * carries: the same bones, under every skin. Since issue #1020 it is handed the
781
+ * document `coreDocumentFacts` read for the run's other facts, so the roster
782
+ * costs no reading of its own; a skin's view is still built only when a
783
+ * question is asked of it.
784
+ */
785
+ function coreSkinRoster(doc: CompiledDocument, skins: readonly string[]): SkinRoster {
786
+ return {
787
+ skins,
788
+ unposedUnder: (skin) => {
789
+ const view = skin === undefined ? noSkinView(doc) : underSkin(doc, skin);
790
+ const active = activeBones(view);
791
+ return unposedBones(view.bones.map((b) => ({ name: b.name, parent: b.parent ?? null, active: active.has(b.name) })));
792
+ },
793
+ };
794
+ }
795
+
796
+ /**
797
+ * What `render` and `check` read off a rigc build's model document besides the
798
+ * pose (issue #1020) — so a build the core poses draws with its
799
+ * `skeleton.atlas` gone, and reads off `skeleton.json` only what the document
800
+ * does not state.
801
+ */
802
+ export interface CoreDocumentFacts {
803
+ /** The document's spec: `rigc-compiled/3` and `/2` state where each region sits on its page, `rigc-compiled/1` does not; `/3` alone states the orders, the stage and the pages' `scale:` lines (issue #1026). */
804
+ spec: string;
805
+ /**
806
+ * Every page the `pages` section states, by name, in file order — the
807
+ * images the draw samples, read by these names; `null` for a
808
+ * `rigc-compiled/1` document, whose pages only its atlas names.
809
+ */
810
+ pageNames: readonly string[] | null;
811
+ /** The skin roster behind the core poser (`coreSkinRoster`), over this one reading. */
812
+ roster: SkinRoster;
813
+ /**
814
+ * What a `rigc-compiled/3` document states that `skeleton.json` and the
815
+ * atlas were the only place of before issue #1026 — the order the file lists
816
+ * animations and skins in, whether a stage is declared, and the `scale:`
817
+ * lines its pages state, in page order — or `null` for a `/2` or `/1`
818
+ * document, whose reader takes them off the files beside it and says so.
819
+ */
820
+ stated: { animations: readonly string[]; skins: readonly string[]; declaresStage: boolean; scales: readonly number[] } | null;
821
+ /**
822
+ * The slot subset's roster (`subsetOver`): the slots in the document's draw
823
+ * order, its default skin, and the skins the document files a slot's
824
+ * attachments under. A refusal lists those skins, and the order it lists
825
+ * them in is the Spine file's (`./core/index.ts`, *Several skins filling one
826
+ * placeholder*) — so they are put in the skin order a `rigc-compiled/3`
827
+ * document states (`editorOrder`, issue #1026), or, for a `/2` or `/1`
828
+ * document, which does not hold it, the Spine file's own list.
829
+ */
830
+ subset: SubsetRoster;
831
+ }
832
+
833
+ /**
834
+ * The document's facts for `render` and `check` (`CoreDocumentFacts`), read
835
+ * once. `fileSkins` is the Spine file's skin list, in its order — what a
836
+ * `rigc-compiled/2` or `/1` document does not state; a `/3` document's own
837
+ * `editorOrder` is read instead (issue #1026). Refused by `CoreInputError`
838
+ * where `readModel` refuses the document.
839
+ */
840
+ export function coreDocumentFacts(modelText: string, where: string, fileSkins: readonly string[]): CoreDocumentFacts {
841
+ const doc = readModel(modelText, where);
842
+ const stated = doc.stated;
843
+ const skins = stated === null ? fileSkins : stated.editorOrder.skins.map((k) => k.name);
844
+ const rank = (name: string): number => {
845
+ const at = skins.indexOf(name);
846
+ return at < 0 ? skins.length : at;
847
+ };
848
+ return {
849
+ spec: doc.spec,
850
+ pageNames: doc.pages === null ? null : doc.pages.map((page) => page.name),
851
+ roster: coreSkinRoster(doc, skins),
852
+ stated:
853
+ stated === null
854
+ ? null
855
+ : {
856
+ animations: stated.editorOrder.animations,
857
+ skins,
858
+ declaresStage: stated.stage !== null,
859
+ scales: (doc.pages ?? []).flatMap((page) => (typeof page.scale === 'number' ? [page.scale] : [])),
860
+ },
861
+ subset: {
862
+ declared: doc.slots.map((s) => s.name),
863
+ carriers: (slot) =>
864
+ doc.skins
865
+ .filter((k) => Object.keys(k.attachments[slot] ?? {}).length > 0)
866
+ .map((k) => k.name)
867
+ .sort((a, b) => rank(a) - rank(b)),
868
+ defaultSkin: doc.skins.some((k) => k.name === CORE_DEFAULT_SKIN) ? CORE_DEFAULT_SKIN : null,
869
+ },
870
+ };
871
+ }