rig-c 0.0.0-stage → 2.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
@@ -0,0 +1,854 @@
1
+ /**
2
+ * The Spine emitter — the compiled model (`src/model.ts`) written as Spine 4.3
3
+ * skeleton objects (issue #915, step 1b of #380; every structural record since
4
+ * #919, cut 1d; the animations since #921, cut 1e).
5
+ *
6
+ * The model holds values; this file owns the bytes. So every Spine 4.3
7
+ * spelling of a model field lives here and nowhere in the model: a bone's
8
+ * inherit mode is written under `inherit` (4.0/4.1 wrote `transform`, which 4.3
9
+ * loads silently as Normal — `A02`), and its skin-required flag under `skin`; a
10
+ * constraint's and an attachment's kind under `type` (a region's left out); a
11
+ * slot's setup attachment under `attachment`; the physics timeline that names
12
+ * no constraint under the empty name.
13
+ *
14
+ * 🔒 **Key insertion order is part of the byte contract.** `inEditorKeyOrder`
15
+ * (`src/keyorder.ts`) permutes only the keys its row lists; a key the row does
16
+ * not list — a bone's `shearX`, `shearY`, `skin` — keeps the position the
17
+ * constructor gave it, and a kind with no row (a linked mesh, a path
18
+ * attachment, a sequence, the path and slider constraints, an event) keeps the
19
+ * constructor's order whole. So each emitter here inserts keys in exactly the
20
+ * order `compile.ts` inserted them before the model existed, written down in
21
+ * its doc comment, and a reorder here is a byte change on every build that
22
+ * carries the key, which the byte-identity gate (`tools/emit_hashes.ts`) names.
23
+ *
24
+ * ✂️ **Omissions.** The parser defaults `PARSER_DEFAULTS` lists are dropped by
25
+ * `withoutParserDefaults` on the finished skeleton, once, as before. The
26
+ * omissions the constructors used to make INLINE are made here, because the
27
+ * model now holds the value: a slot's `null` setup attachment, a region's `x`,
28
+ * `y`, `rotation` at 0, a physics component at 0 and a parameter at its
29
+ * default (`PHYSICS_PARAMS`), and a linked mesh's own slot, `default` skin and
30
+ * `timelines: true`. Each restates what the parser reads in the key's absence.
31
+ * One omission is not here: a `path` equal to the name, which `attachmentPath`
32
+ * in `compile.ts` still decides (`src/model.ts`, the header's ⚠️).
33
+ *
34
+ * ⭐ **A weighted vertex's bone index is written here and nowhere else** (issue
35
+ * #917). The model binds by name; `emitVertices` writes Spine's run —
36
+ * `boneCount, (boneIndex, x, y, weight) × n` per vertex — with each index the
37
+ * bone's position in the model's bone array, which `emitBones` keeps in order.
38
+ *
39
+ * 🔸 **The editor's name comparator stays in `compile.ts`** and is handed to
40
+ * `emitSkins` (`EditorOrder`) and to `emitAnimations` (`AnimationOrder`): the
41
+ * selftest's scans of the fold read it in that file, and importing it from
42
+ * here would make the two modules import each other. The ORDER is applied
43
+ * here, and so is the refusal that comes with it — a set of animation names
44
+ * the editor could key two ways is refused inside `emitAnimations`, in the
45
+ * words `compile.ts` gives it; the comparator is passed in, not restated.
46
+ *
47
+ * 🧪 Every object this file returns is a fresh one. The parser-default and
48
+ * key-order passes run in place on the finished skeleton, and before issue
49
+ * #919 they reached the region and linked-mesh objects the skin tables held;
50
+ * no object of the model that those passes visit is handed to them — a
51
+ * timeline key included (issue #921): the passes delete a first key's
52
+ * `time: 0` and re-key an ik key's fields in place, and a key handed over by
53
+ * reference would come back out of the model that way.
54
+ */
55
+ import { CompileError } from './errors.ts';
56
+ import { inEditorKeyOrder, PARSER_DEFAULTS, PHYSICS_PARAMS, UNSTATED_REFERENCE_SCALE, withoutParserDefaults } from './keyorder.ts';
57
+ import type {
58
+ CompiledAnimation,
59
+ CompiledModel,
60
+ ModelAttachmentTimelines,
61
+ ModelBone,
62
+ ModelBoundingBoxAttachment,
63
+ ModelClippingAttachment,
64
+ ModelConstraint,
65
+ ModelConstraintKind,
66
+ ModelEvent,
67
+ ModelKey,
68
+ ModelLinkedMeshAttachment,
69
+ ModelMeshAttachment,
70
+ ModelPathAttachment,
71
+ ModelRegionAttachment,
72
+ ModelSequence,
73
+ ModelSkin,
74
+ ModelSlot,
75
+ ModelTimelines,
76
+ ModelVertexAttachment,
77
+ ModelVertices,
78
+ SkinTable,
79
+ SkinTableEntry,
80
+ } from './model.ts';
81
+ import { EVERY_GLOBAL_PHYSICS } from './motion.ts';
82
+ import { RIG_SKIN_CONSTRAINT_KEYS, type RigSkinConstraintKey } from './rig.ts';
83
+ import type {
84
+ SpineAnimation,
85
+ SpineAttachment,
86
+ SpineBone,
87
+ SpineBoundingBoxAttachment,
88
+ SpineClippingAttachment,
89
+ SpineConstraint,
90
+ SpineEvent,
91
+ SpineLinkedMeshAttachment,
92
+ SpineMeshAttachment,
93
+ SpinePathAttachment,
94
+ SpineRegionAttachment,
95
+ SpineSequence,
96
+ SpineSkeletonJson,
97
+ SpineSkin,
98
+ SpineSlot,
99
+ SpineTimelineKey,
100
+ } from './types.ts';
101
+
102
+ /**
103
+ * One `SpineBone` per model bone, in the model's order — which is the order a
104
+ * weighted vertex's bone index counts in, so it is never re-sorted.
105
+ *
106
+ * Keys, each only when the model bone carries it: `name, parent, length, x, y,
107
+ * rotation, scaleX, scaleY, shearX, shearY, inherit, skin, color, icon`.
108
+ */
109
+ export function emitBones(bones: readonly ModelBone[]): SpineBone[] {
110
+ return bones.map((model) => {
111
+ const bone: SpineBone = { name: model.name };
112
+ if (model.parent !== undefined) bone.parent = model.parent;
113
+ if (model.length !== undefined) bone.length = model.length;
114
+ if (model.x !== undefined) bone.x = model.x;
115
+ if (model.y !== undefined) bone.y = model.y;
116
+ if (model.rotation !== undefined) bone.rotation = model.rotation;
117
+ if (model.scaleX !== undefined) bone.scaleX = model.scaleX;
118
+ if (model.scaleY !== undefined) bone.scaleY = model.scaleY;
119
+ if (model.shearX !== undefined) bone.shearX = model.shearX;
120
+ if (model.shearY !== undefined) bone.shearY = model.shearY;
121
+ if (model.inheritMode !== undefined) bone.inherit = model.inheritMode;
122
+ if (model.skinRequired !== undefined) bone.skin = model.skinRequired;
123
+ if (model.editor?.color !== undefined) bone.color = model.editor.color;
124
+ if (model.editor?.icon !== undefined) bone.icon = model.editor.icon;
125
+ return bone;
126
+ });
127
+ }
128
+
129
+ /**
130
+ * The bone index a weighted run writes for each name: its position in the model's
131
+ * bone array, the array `emitBones` writes in the same order. A name that is not
132
+ * a bone is refused by name — the builders resolve every binding before it
133
+ * reaches the model, so this is the emitter declining to write an index for
134
+ * something it cannot find rather than a check an author can reach.
135
+ */
136
+ export function boneIndexOf(bones: readonly ModelBone[]): (bone: string) => number {
137
+ const index = new Map(bones.map((bone, i) => [bone.name, i] as const));
138
+ return (bone) => {
139
+ const at = index.get(bone);
140
+ if (at === undefined) throw new CompileError(`internal: a weighted vertex binds bone "${bone}", which is not in the model's bone list`);
141
+ return at;
142
+ };
143
+ }
144
+
145
+ /**
146
+ * A vertex attachment's `vertices` array in Spine 4.3's encoding.
147
+ *
148
+ * Unweighted: `xy` verbatim. Weighted: per vertex its binding count, then per
149
+ * binding `indexOf(bone), x, y, weight` — every number but the index copied from
150
+ * the model untouched, since the model already holds the float32 values the
151
+ * file carries. The reader tells the two apart by length alone (`readVertices`),
152
+ * which is why the model says `weighted` out loud and this is the one place it
153
+ * becomes a length.
154
+ */
155
+ export function emitVertices(vertices: ModelVertices, indexOf: (bone: string) => number): number[] {
156
+ if (!vertices.weighted) return vertices.xy.slice();
157
+ const out: number[] = [];
158
+ for (const vertex of vertices.bindings) {
159
+ out.push(vertex.length);
160
+ for (const binding of vertex) out.push(indexOf(binding.bone), binding.x, binding.y, binding.weight);
161
+ }
162
+ return out;
163
+ }
164
+
165
+ /**
166
+ * A mesh, keys in the order every mesh builder in `compile.ts` inserted them
167
+ * before the model existed — `buildRigMesh`, `buildGeneratedMesh`,
168
+ * `buildGridAttachment`, `buildSegmentsAttachment`, `buildContourAttachment`
169
+ * and the manifest's `buildMesh` all wrote one order, with `withStatedName`
170
+ * putting `name` ahead of everything:
171
+ *
172
+ * `name, type, path, color, uvs, triangles, vertices, hull, edges, width, height, sequence`
173
+ *
174
+ * each optional key only when the record carries it. The mesh row of the key
175
+ * order lists `type` … `height`; `name`, `path`, `color` and `sequence` it does
176
+ * not list, so their positions are this constructor's and are bytes.
177
+ */
178
+ export function emitMesh(mesh: ModelMeshAttachment, indexOf: (bone: string) => number): SpineMeshAttachment {
179
+ return {
180
+ ...(mesh.name !== undefined ? { name: mesh.name } : {}),
181
+ type: 'mesh',
182
+ ...(mesh.path !== undefined ? { path: mesh.path } : {}),
183
+ ...(mesh.color !== undefined ? { color: mesh.color } : {}),
184
+ uvs: mesh.uvs,
185
+ triangles: mesh.triangles,
186
+ vertices: emitVertices(mesh.vertices, indexOf),
187
+ hull: mesh.hull,
188
+ edges: mesh.edges,
189
+ width: mesh.width,
190
+ height: mesh.height,
191
+ ...(mesh.sequence !== undefined ? { sequence: emitSequenceBlock(mesh.sequence) } : {}),
192
+ };
193
+ }
194
+
195
+ /**
196
+ * A bounding box, keys in `buildRigBoundingBox`'s order after `withStatedName`:
197
+ * `name, type, vertexCount, vertices, color`. The row lists `type`,
198
+ * `vertexCount`, `vertices`; `name` and `color` hold the constructor's places.
199
+ */
200
+ export function emitBoundingBox(box: ModelBoundingBoxAttachment, indexOf: (bone: string) => number): SpineBoundingBoxAttachment {
201
+ return {
202
+ ...(box.name !== undefined ? { name: box.name } : {}),
203
+ type: 'boundingbox',
204
+ vertexCount: box.vertexCount,
205
+ vertices: emitVertices(box.vertices, indexOf),
206
+ ...(box.editorColor !== undefined ? { color: box.editorColor } : {}),
207
+ };
208
+ }
209
+
210
+ /**
211
+ * A clipping polygon, keys in `buildRigClipping`'s order after `withStatedName`:
212
+ * `name, type, end, convex, inverse, vertexCount, vertices, color`. The row
213
+ * lists `type`, `end`, `vertexCount`, `vertices`, `color`; `name`, `convex` and
214
+ * `inverse` hold the constructor's places.
215
+ */
216
+ export function emitClipping(clip: ModelClippingAttachment, indexOf: (bone: string) => number): SpineClippingAttachment {
217
+ return {
218
+ ...(clip.name !== undefined ? { name: clip.name } : {}),
219
+ type: 'clipping',
220
+ ...(clip.end !== undefined ? { end: clip.end } : {}),
221
+ ...(clip.convex !== undefined ? { convex: clip.convex } : {}),
222
+ ...(clip.inverse !== undefined ? { inverse: clip.inverse } : {}),
223
+ vertexCount: clip.vertexCount,
224
+ vertices: emitVertices(clip.vertices, indexOf),
225
+ ...(clip.editorColor !== undefined ? { color: clip.editorColor } : {}),
226
+ };
227
+ }
228
+
229
+ /**
230
+ * A path, keys in `buildRigPath`'s order after `withStatedName`:
231
+ * `name, type, closed, constantSpeed, vertexCount, vertices, lengths, color`.
232
+ * The key order has no row for a path attachment, so every position here is
233
+ * this constructor's.
234
+ */
235
+ export function emitPath(path: ModelPathAttachment, indexOf: (bone: string) => number): SpinePathAttachment {
236
+ return {
237
+ ...(path.name !== undefined ? { name: path.name } : {}),
238
+ type: 'path',
239
+ ...(path.closed !== undefined ? { closed: path.closed } : {}),
240
+ ...(path.constantSpeed !== undefined ? { constantSpeed: path.constantSpeed } : {}),
241
+ vertexCount: path.vertexCount,
242
+ vertices: emitVertices(path.vertices, indexOf),
243
+ lengths: path.lengths,
244
+ ...(path.editorColor !== undefined ? { color: path.editorColor } : {}),
245
+ };
246
+ }
247
+
248
+ /** One model vertex attachment as its Spine 4.3 object. */
249
+ export function emitVertexAttachment(att: ModelVertexAttachment, indexOf: (bone: string) => number): SpineAttachment {
250
+ switch (att.kind) {
251
+ case 'mesh':
252
+ return emitMesh(att, indexOf);
253
+ case 'boundingbox':
254
+ return emitBoundingBox(att, indexOf);
255
+ case 'clipping':
256
+ return emitClipping(att, indexOf);
257
+ case 'path':
258
+ return emitPath(att, indexOf);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * A `sequence` block, keys in `buildSequence`'s order in `compile.ts`:
264
+ * `count, start, digits, setup`, each optional key only when stated. The key
265
+ * order has no row for a sequence, so the order is this one's. A fresh object
266
+ * every time: the parser-default pass removes `start: 1` and `setup: 0` in
267
+ * place, and it must not reach into the model.
268
+ */
269
+ export function emitSequenceBlock(seq: ModelSequence): SpineSequence {
270
+ const out: SpineSequence = { count: seq.count };
271
+ if (seq.start !== undefined) out.start = seq.start;
272
+ if (seq.digits !== undefined) out.digits = seq.digits;
273
+ if (seq.setup !== undefined) out.setup = seq.setup;
274
+ return out;
275
+ }
276
+
277
+ /**
278
+ * A region, keys in the order `buildRigRegion` and `placeRegion` inserted them
279
+ * before the model existed (the two wrote one order; `withStatedName` put
280
+ * `name` ahead of everything):
281
+ *
282
+ * `name, width, height, path, x, y, rotation, scaleX, scaleY, color, sequence`
283
+ *
284
+ * The region row lists `x, y, scaleX, scaleY, rotation, width, height`; `name`,
285
+ * `path`, `color` and `sequence` hold this constructor's places.
286
+ *
287
+ * ✂️ `x`, `y` and `rotation` are left out at 0 — `placeRegion`'s inline omission,
288
+ * made here for every region. On a rig spec's region that is the parser-default
289
+ * pass's own decision one step early (the region row reads each at 0, and
290
+ * every number here is `f32`'d, which never leaves a `-0`), so no byte moves.
291
+ */
292
+ export function emitRegion(region: ModelRegionAttachment): SpineRegionAttachment {
293
+ const out: SpineRegionAttachment = {
294
+ ...(region.name !== undefined ? { name: region.name } : {}),
295
+ width: region.width,
296
+ height: region.height,
297
+ };
298
+ if (region.path !== undefined) out.path = region.path;
299
+ if (region.x !== undefined && region.x !== 0) out.x = region.x;
300
+ if (region.y !== undefined && region.y !== 0) out.y = region.y;
301
+ if (region.rotation !== undefined && region.rotation !== 0) out.rotation = region.rotation;
302
+ if (region.scaleX !== undefined) out.scaleX = region.scaleX;
303
+ if (region.scaleY !== undefined) out.scaleY = region.scaleY;
304
+ if (region.color !== undefined) out.color = region.color;
305
+ if (region.sequence !== undefined) out.sequence = emitSequenceBlock(region.sequence);
306
+ return out;
307
+ }
308
+
309
+ /**
310
+ * A linked mesh, keys in `buildRigLinkedMesh`'s order after `withStatedName`:
311
+ *
312
+ * `name, type, source, width, height, path, slot, skin, timelines, color, sequence`
313
+ *
314
+ * The key order has no row for a linked mesh, so every position is this one's.
315
+ *
316
+ * ✂️ The link is written only where it differs from the parser's fallback
317
+ * (`SkeletonJson` reads `skin` as the default skin, `slot` as the attachment's
318
+ * own, `timelines` as true): `slot` when it is not `ownSlot`, `skin` when it is
319
+ * not `default`, `timelines` only when false.
320
+ */
321
+ export function emitLinkedMesh(link: ModelLinkedMeshAttachment, ownSlot: string): SpineLinkedMeshAttachment {
322
+ const out: SpineLinkedMeshAttachment = {
323
+ ...(link.name !== undefined ? { name: link.name } : {}),
324
+ type: 'linkedmesh',
325
+ source: link.source,
326
+ width: link.width,
327
+ height: link.height,
328
+ };
329
+ if (link.path !== undefined) out.path = link.path;
330
+ if (link.slot !== ownSlot) out.slot = link.slot;
331
+ if (link.skin !== 'default') out.skin = link.skin;
332
+ if (!link.timelines) out.timelines = false;
333
+ if (link.color !== undefined) out.color = link.color;
334
+ if (link.sequence !== undefined) out.sequence = emitSequenceBlock(link.sequence);
335
+ return out;
336
+ }
337
+
338
+ /** One skin-table record as its Spine 4.3 object; `ownSlot` is the slot it is filed under. */
339
+ export function emitAttachment(att: SkinTableEntry, ownSlot: string, indexOf: (bone: string) => number): SpineAttachment {
340
+ switch (att.kind) {
341
+ case 'region':
342
+ return emitRegion(att);
343
+ case 'linkedmesh':
344
+ return emitLinkedMesh(att, ownSlot);
345
+ default:
346
+ return emitVertexAttachment(att, indexOf);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * One skin's slot -> placeholder table as the skeleton file carries it: every
352
+ * record emitted, slots and placeholders in the table's own order. The
353
+ * editor's slot-key order is applied by `emitSkins`.
354
+ */
355
+ export function emitSkinAttachments(table: SkinTable, indexOf: (bone: string) => number): Record<string, Record<string, SpineAttachment>> {
356
+ const out: Record<string, Record<string, SpineAttachment>> = {};
357
+ for (const [slot, perSlot] of Object.entries(table)) {
358
+ const emitted: Record<string, SpineAttachment> = {};
359
+ for (const [placeholder, entry] of Object.entries(perSlot)) emitted[placeholder] = emitAttachment(entry, slot, indexOf);
360
+ out[slot] = emitted;
361
+ }
362
+ return out;
363
+ }
364
+
365
+ /**
366
+ * The editor's orders for the two name-keyed collections a skin array carries:
367
+ * the skins themselves (`default` pinned first, the rest by the editor's
368
+ * comparator, refusing a pair it could key differently) and a skin's slot keys.
369
+ * `compile.ts`'s `editorSkinOrder` and `editorSlotKeyOrder` — see the header's
370
+ * 🔸 for why they are passed rather than defined here.
371
+ */
372
+ export interface EditorOrder {
373
+ skins: <T extends { name: string }>(skins: readonly T[]) => T[];
374
+ slotKeys: <T>(attachments: Record<string, T>) => Record<string, T>;
375
+ }
376
+
377
+ /**
378
+ * The `skins` array, in the assembly's order before the model existed: each
379
+ * entry built in the model's (the spec's) order, then `order.skins` over the
380
+ * whole — `default` first, the rest in the editor's order. An entry's keys:
381
+ *
382
+ * `name, bones, ik, transform, path, physics, slider, attachments`
383
+ *
384
+ * — `readSkeletonData`'s own order; each member list only when non-empty, so a
385
+ * skin that activates nothing is the two-key entry it always was. `attachments`
386
+ * is `emitSkinAttachments` keyed by `order.slotKeys`.
387
+ */
388
+ export function emitSkins(skins: readonly ModelSkin[], indexOf: (bone: string) => number, order: EditorOrder): SpineSkin[] {
389
+ return order.skins(
390
+ skins.map((skin): SpineSkin => {
391
+ const members: Partial<Record<'bones' | RigSkinConstraintKey, string[]>> = {};
392
+ if (skin.bones.length) members.bones = skin.bones;
393
+ for (const key of RIG_SKIN_CONSTRAINT_KEYS) if (skin.constraints[key].length) members[key] = skin.constraints[key];
394
+ return { name: skin.name, ...members, attachments: order.slotKeys(emitSkinAttachments(skin.attachments, indexOf)) };
395
+ }),
396
+ );
397
+ }
398
+
399
+ /**
400
+ * The `slots` array, in the model's order — the draw order, which a draw-order
401
+ * key's offsets count in, so it is never re-sorted. Keys in the slot
402
+ * constructor's order: `name, bone, attachment, color, dark, blend`, each only
403
+ * when the model carries it.
404
+ *
405
+ * ✂️ `attachment` is left out when the setup is `null`: `SkeletonJson` reads
406
+ * the key with a `null` default, so a slot with no `attachment` shows nothing,
407
+ * which is the shape the editor exports (34 of the 52 slots of `spineboy-pro`).
408
+ */
409
+ export function emitSlots(slots: readonly ModelSlot[]): SpineSlot[] {
410
+ return slots.map((model) => {
411
+ const slot: SpineSlot = { name: model.name, bone: model.bone };
412
+ if (model.setup !== null) slot.attachment = model.setup;
413
+ if (model.color !== undefined) slot.color = model.color;
414
+ if (model.dark !== undefined) slot.dark = model.dark;
415
+ if (model.blend !== undefined) slot.blend = model.blend;
416
+ return slot;
417
+ });
418
+ }
419
+
420
+ /**
421
+ * The `referenceScale` a header stating none is read as, and a physics
422
+ * constraint's parameters with their parser defaults, in the physics table's
423
+ * order. Defined in `src/keyorder.ts` beside the parser's other defaults since
424
+ * issue #1026 — the compiler reads both, and the entry that compiles a model
425
+ * without the emitter reads nothing from this module — and re-exported here
426
+ * under the names they always had.
427
+ */
428
+ export { PHYSICS_PARAMS, UNSTATED_REFERENCE_SCALE };
429
+
430
+ /** The five components a physics constraint drives; each reads 0 in its absence. */
431
+ const PHYSICS_COMPONENT_FIELDS = ['x', 'y', 'rotate', 'scaleX', 'shearX'] as const;
432
+
433
+ /**
434
+ * Each constraint's fields after `name, type`, in the order its builder
435
+ * inserted them before the model existed — read off `buildRigConstraint`'s
436
+ * branch per kind (its `copy` lists and assignments, in call order) and, for a
437
+ * physics constraint the motion spec's table declares, off that loop:
438
+ *
439
+ * - `ik`: `bones, target, scaleY, mix, softness, bendPositive, compress, stretch, skin`
440
+ * - `transform`: `bones, source, properties, localSource, localTarget, additive, clamp,
441
+ * rotation, x, y, scaleX, scaleY, shearY, mixRotate, mixX, mixY, mixScaleX, mixScaleY,
442
+ * mixShearY, skin`
443
+ * - `path`: `bones, slot, positionMode, spacingMode, rotateMode, rotation, position, spacing,
444
+ * mixRotate, mixX, mixY, skin`
445
+ * - `slider`: `animation, additive, loop, mix, bone, property, from, to, scale, max, local,
446
+ * time, skin` — `bone` … `local` and `time` are exclusive (the bone-less form), so one
447
+ * list holds both branches
448
+ * - `physics` (rig): `bone, scaleY, x, y, rotate, scaleX, shearX, limit, fps, inertia,
449
+ * strength, damping, mass, wind, gravity, mix, inertiaGlobal, strengthGlobal,
450
+ * dampingGlobal, massGlobal, windGlobal, gravityGlobal, mixGlobal, skin`
451
+ * - `physics` (motion table): `bone, x, y, rotate, scaleX, shearX`, then `PHYSICS_PARAMS`'
452
+ * order — `inertia` … `mix, fps, limit`
453
+ *
454
+ * The rows of the key-order table list `type, name` and a few fields of ik,
455
+ * transform and physics; path and slider have no row, so there every position
456
+ * here is a byte.
457
+ */
458
+ const CONSTRAINT_FIELD_ORDER: Readonly<Record<ModelConstraintKind | 'physics table', readonly string[]>> = {
459
+ ik: ['bones', 'target', 'scaleY', 'mix', 'softness', 'bendPositive', 'compress', 'stretch', 'skin'],
460
+ transform: [
461
+ 'bones', 'source', 'properties', 'localSource', 'localTarget', 'additive', 'clamp', 'rotation', 'x', 'y', 'scaleX',
462
+ 'scaleY', 'shearY', 'mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY', 'skin',
463
+ ],
464
+ path: [
465
+ 'bones', 'slot', 'positionMode', 'spacingMode', 'rotateMode', 'rotation', 'position', 'spacing', 'mixRotate', 'mixX',
466
+ 'mixY', 'skin',
467
+ ],
468
+ slider: ['animation', 'additive', 'loop', 'mix', 'bone', 'property', 'from', 'to', 'scale', 'max', 'local', 'time', 'skin'],
469
+ physics: [
470
+ 'bone', 'scaleY', 'x', 'y', 'rotate', 'scaleX', 'shearX', 'limit', 'fps', 'inertia', 'strength', 'damping', 'mass', 'wind',
471
+ 'gravity', 'mix', 'inertiaGlobal', 'strengthGlobal', 'dampingGlobal', 'massGlobal', 'windGlobal', 'gravityGlobal',
472
+ 'mixGlobal', 'skin',
473
+ ],
474
+ 'physics table': ['bone', ...PHYSICS_COMPONENT_FIELDS, ...PHYSICS_PARAMS.map(([param]) => param)],
475
+ };
476
+
477
+ /**
478
+ * One constraint as the `constraints[]` entry 4.3 reads: `name, type`, then
479
+ * the kind's fields in `CONSTRAINT_FIELD_ORDER`, each only when the model
480
+ * carries it. A field the list has no place for is refused by name rather than
481
+ * dropped — the model's fields are the builders', so this is the emitter
482
+ * declining to guess a position, not a check an author can reach.
483
+ *
484
+ * ✂️ On a physics constraint a component at 0 and a parameter at its
485
+ * `PHYSICS_PARAMS` default are left out — the table loop's inline omission,
486
+ * made here for both routes. On the rig's route it is the parser-default
487
+ * pass's own decision one step early (`PARSER_DEFAULTS['physics constraint']`
488
+ * holds the same values, and `f32` never leaves a `-0`), so no byte moves.
489
+ */
490
+ export function emitConstraint(model: ModelConstraint): SpineConstraint {
491
+ const order = CONSTRAINT_FIELD_ORDER[model.kind === 'physics' && model.declaredIn === 'motion' ? 'physics table' : model.kind];
492
+ const out: SpineConstraint = { name: model.name, type: model.kind };
493
+ const stray = Object.keys(model).filter((key) => key !== 'kind' && key !== 'name' && key !== 'declaredIn' && !order.includes(key));
494
+ if (stray.length > 0) {
495
+ throw new CompileError(`internal: ${model.kind} constraint "${model.name}" carries ${stray.join(', ')}, which the emitter has no place for`);
496
+ }
497
+ for (const field of order) {
498
+ const value = model[field];
499
+ if (value === undefined) continue;
500
+ if (model.kind === 'physics') {
501
+ if ((PHYSICS_COMPONENT_FIELDS as readonly string[]).includes(field) && value === 0) continue;
502
+ if (PHYSICS_PARAMS.some(([param, dflt]) => param === field && value === dflt)) continue;
503
+ }
504
+ out[field] = value;
505
+ }
506
+ return out;
507
+ }
508
+
509
+ /** The `constraints[]` array, in the model's order: the rig's in declaration order, then the physics table's. */
510
+ export function emitConstraints(constraints: readonly ModelConstraint[]): SpineConstraint[] {
511
+ return constraints.map(emitConstraint);
512
+ }
513
+
514
+ /**
515
+ * The `events` map, in the model's (the rig's declared) order, each definition's
516
+ * keys in the order the assembly wrote them: `int, float, string, audio,
517
+ * volume, balance`, each only when declared. The key order has no row for an
518
+ * event, so every position is this one's. The caller writes the map only when
519
+ * it is non-empty.
520
+ */
521
+ export function emitEvents(events: ReadonlyMap<string, ModelEvent>): Record<string, SpineEvent> {
522
+ const out: Record<string, SpineEvent> = {};
523
+ for (const [name, def] of events) {
524
+ const entry: SpineEvent = {};
525
+ if (def.int !== undefined) entry.int = def.int;
526
+ if (def.float !== undefined) entry.float = def.float;
527
+ if (def.string !== undefined) entry.string = def.string;
528
+ if (def.audio !== undefined) entry.audio = def.audio;
529
+ if (def.volume !== undefined) entry.volume = def.volume;
530
+ if (def.balance !== undefined) entry.balance = def.balance;
531
+ out[name] = entry;
532
+ }
533
+ return out;
534
+ }
535
+
536
+ // ---------------------------------------------------------------------------
537
+ // animations (issue #921, cut 1e)
538
+ // ---------------------------------------------------------------------------
539
+
540
+ /**
541
+ * The order `animations` is keyed in, over the model's names — `compile.ts`'s
542
+ * `editorAnimationOrder`, which refuses by name every pair the editor could key
543
+ * two ways (`refuseNamesTheEditorCouldKeyDifferently`). Passed in, for the
544
+ * reason the header's 🔸 gives.
545
+ */
546
+ export type AnimationOrder = (names: readonly string[]) => string[];
547
+
548
+ /**
549
+ * One key as the file carries it: a fresh object with the model key's fields,
550
+ * in the model key's order — the order its compiler inserted them, which is a
551
+ * byte wherever the key-order table has no row (the `scalex`, `inherit`, `rgb`,
552
+ * `sequence`, path, slider and several physics keys) or lists only some of the
553
+ * fields. Arrays inside (`curve`, `vertices`) are shared: neither pass reaches
554
+ * into an array of numbers.
555
+ */
556
+ function emitKey(key: ModelKey): SpineTimelineKey {
557
+ return { ...key };
558
+ }
559
+
560
+ function emitKeys(keys: readonly ModelKey[]): SpineTimelineKey[] {
561
+ return keys.map(emitKey);
562
+ }
563
+
564
+ /** One target's timelines, in the model's order. */
565
+ function emitTimelines(timelines: ModelTimelines): Record<string, SpineTimelineKey[]> {
566
+ const out: Record<string, SpineTimelineKey[]> = {};
567
+ for (const [name, keys] of timelines) out[name] = emitKeys(keys);
568
+ return out;
569
+ }
570
+
571
+ /** target -> timelines, in the model's order; `rename` spells a target the format names otherwise. */
572
+ function emitTargets(
573
+ targets: ReadonlyMap<string, ModelTimelines>,
574
+ rename: (target: string) => string = (target) => target,
575
+ ): Record<string, Record<string, SpineTimelineKey[]>> {
576
+ const out: Record<string, Record<string, SpineTimelineKey[]>> = {};
577
+ for (const [target, timelines] of targets) out[rename(target)] = emitTimelines(timelines);
578
+ return out;
579
+ }
580
+
581
+ /**
582
+ * The three ik-key flags, and the value `SkeletonJson` reads for each on a key
583
+ * that omits it (`PARSER_DEFAULTS['ik key']`, which the selftest holds to the
584
+ * parser row by row).
585
+ */
586
+ const IK_KEY_FLAGS: ReadonlyArray<readonly [string, unknown]> = (['bendPositive', 'compress', 'stretch'] as const).map(
587
+ (flag) => [flag, PARSER_DEFAULTS['ik key'][flag]] as const,
588
+ );
589
+
590
+ /**
591
+ * An ik key. The model holds the flags IN EFFECT on every key (`ModelKey`); the
592
+ * file carries a flag only where it is not the parser's per-key default, which
593
+ * is where the restatement of issue #273 used to stop — the constraint's flag
594
+ * was carried onto a key only where it differed from that default. A key that
595
+ * STATED a flag at its default was written and then left out by the
596
+ * parser-default pass (its `ik key` row holds the same three values), so
597
+ * leaving it out here moves no byte. Every other field is copied in the key's
598
+ * own order.
599
+ */
600
+ function emitIkKey(key: ModelKey): SpineTimelineKey {
601
+ const out: SpineTimelineKey = {};
602
+ for (const [field, value] of Object.entries(key)) {
603
+ if (IK_KEY_FLAGS.some(([flag, dflt]) => flag === field && value === dflt)) continue;
604
+ out[field] = value;
605
+ }
606
+ return out;
607
+ }
608
+
609
+ /** One draw-order move as the model holds it. */
610
+ interface DrawOrderMove {
611
+ slot: string;
612
+ offset: number;
613
+ }
614
+
615
+ function isDrawOrderMoves(value: unknown): value is DrawOrderMove[] {
616
+ return (
617
+ Array.isArray(value) &&
618
+ value.every(
619
+ (move: unknown) =>
620
+ typeof move === 'object' &&
621
+ move !== null &&
622
+ typeof (move as { slot?: unknown }).slot === 'string' &&
623
+ typeof (move as { offset?: unknown }).offset === 'number',
624
+ )
625
+ );
626
+ }
627
+
628
+ /**
629
+ * A draw-order key. Its `offsets` are written in SETUP order — each move's
630
+ * slot's index in the model's slot array, which is the emitted draw order:
631
+ * `readDrawOrder` walks the offsets with a forward-only cursor over the setup
632
+ * order, so an entry whose slot sits before the previous entry's never lets the
633
+ * cursor meet it and the loader runs away (`compileDrawOrder`'s first refusal
634
+ * note). The model holds the moves as stated; the sort is the format's
635
+ * requirement and so the emitter's. Each move is a fresh `slot, offset` object.
636
+ * A slot the model does not have is refused by name — the compiler resolved
637
+ * every move against the same slots, so this is the emitter declining to guess
638
+ * a position, not a check an author can reach.
639
+ */
640
+ function emitDrawOrderKey(key: ModelKey, slotIndex: ReadonlyMap<string, number>): SpineTimelineKey {
641
+ const out = emitKey(key);
642
+ if (key.offsets === undefined) return out;
643
+ if (!isDrawOrderMoves(key.offsets)) {
644
+ throw new CompileError(`internal: a draw-order key at t=${key.time} carries offsets that are not slot/offset moves`);
645
+ }
646
+ const indexOf = (slot: string): number => {
647
+ const at = slotIndex.get(slot);
648
+ if (at === undefined) throw new CompileError(`internal: a draw-order key at t=${key.time} moves slot "${slot}", which is not in the model's slot list`);
649
+ return at;
650
+ };
651
+ out.offsets = key.offsets
652
+ .map((move) => ({ at: indexOf(move.slot), move: { slot: move.slot, offset: move.offset } }))
653
+ .sort((a, b) => a.at - b.at)
654
+ .map(({ move }) => move);
655
+ return out;
656
+ }
657
+
658
+ /** An attachment's timelines: `deform` then `sequence`, each only when the model holds it. */
659
+ function emitAttachmentTimelines(timelines: ModelAttachmentTimelines): Record<string, SpineTimelineKey[]> {
660
+ const out: Record<string, SpineTimelineKey[]> = {};
661
+ if (timelines.deform !== undefined) out.deform = emitKeys(timelines.deform);
662
+ if (timelines.sequence !== undefined) out.sequence = emitKeys(timelines.sequence);
663
+ return out;
664
+ }
665
+
666
+ /**
667
+ * One animation as Spine 4.3's `animations.<name>` object.
668
+ *
669
+ * The groups in `readAnimation`'s own reading order —
670
+ *
671
+ * `slots, bones, ik, transform, path, physics, slider, attachments, drawOrder, events`
672
+ *
673
+ * — each only when the model holds something for it, so an animation that keys
674
+ * one kind of thing is an object of one group, as it always was. (The
675
+ * `animation` row of the key-order table lists eight of the ten; `path` and
676
+ * `slider` hold the positions written here.)
677
+ *
678
+ * ✂️ Spellings and omissions that are Spine's: the physics timeline that names
679
+ * no constraint (the model's `EVERY_GLOBAL_PHYSICS` target) is written under the
680
+ * empty name, which is what `readAnimation` resolves as "every global
681
+ * constraint", in the position the model holds it; an ik key's flags at their
682
+ * per-key default are left out (`emitIkKey`); draw-order moves are sorted into
683
+ * setup order (`emitDrawOrderKey`). Every key is a fresh object.
684
+ */
685
+ export function emitAnimation(animation: CompiledAnimation, slotIndex: ReadonlyMap<string, number>): SpineAnimation {
686
+ const out: SpineAnimation = {};
687
+ if (animation.slots.size) out.slots = emitTargets(animation.slots);
688
+ if (animation.bones.size) out.bones = emitTargets(animation.bones);
689
+ const { ik, transform, path, physics, slider } = animation.constraints;
690
+ if (ik.size) {
691
+ const byConstraint: Record<string, SpineTimelineKey[]> = {};
692
+ for (const [name, keys] of ik) byConstraint[name] = keys.map(emitIkKey);
693
+ out.ik = byConstraint;
694
+ }
695
+ if (transform.size) {
696
+ const byConstraint: Record<string, SpineTimelineKey[]> = {};
697
+ for (const [name, keys] of transform) byConstraint[name] = emitKeys(keys);
698
+ out.transform = byConstraint;
699
+ }
700
+ if (path.size) out.path = emitTargets(path);
701
+ if (physics.size) out.physics = emitTargets(physics, (target) => (target === EVERY_GLOBAL_PHYSICS ? '' : target));
702
+ if (slider.size) out.slider = emitTargets(slider);
703
+ if (animation.attachments.size) {
704
+ const bySkin: NonNullable<SpineAnimation['attachments']> = {};
705
+ for (const [skin, bySlot] of animation.attachments) {
706
+ const slots: Record<string, Record<string, Record<string, SpineTimelineKey[]>>> = {};
707
+ for (const [slot, byAttachment] of bySlot) {
708
+ const attachments: Record<string, Record<string, SpineTimelineKey[]>> = {};
709
+ for (const [attachment, timelines] of byAttachment) attachments[attachment] = emitAttachmentTimelines(timelines);
710
+ slots[slot] = attachments;
711
+ }
712
+ bySkin[skin] = slots;
713
+ }
714
+ out.attachments = bySkin;
715
+ }
716
+ if (animation.drawOrder.length) out.drawOrder = animation.drawOrder.map((key) => emitDrawOrderKey(key, slotIndex));
717
+ if (animation.events.length) out.events = emitKeys(animation.events);
718
+ return out;
719
+ }
720
+
721
+ /**
722
+ * The `animations` object: the model's names keyed in `order`'s order — the
723
+ * editor's, which raises the refusal for a pair it could key two ways before
724
+ * anything is written — each written by `emitAnimation`. `slots` is the model's
725
+ * slot array, the draw order a draw-order move's index counts in.
726
+ */
727
+ export function emitAnimations(
728
+ animations: ReadonlyMap<string, CompiledAnimation>,
729
+ slots: readonly ModelSlot[],
730
+ order: AnimationOrder,
731
+ ): Record<string, SpineAnimation> {
732
+ const names = order([...animations.keys()]);
733
+ const slotIndex = new Map(slots.map((slot, i) => [slot.name, i] as const));
734
+ const out: Record<string, SpineAnimation> = {};
735
+ for (const name of names) {
736
+ const animation = animations.get(name);
737
+ if (animation === undefined) throw new CompileError(`internal: the animation order named "${name}", which is not in the model`);
738
+ out[name] = emitAnimation(animation, slotIndex);
739
+ }
740
+ return out;
741
+ }
742
+
743
+ // ---------------------------------------------------------------------------
744
+ // the skeleton: the emitter's one entry (issue #922, cut 1f)
745
+ // ---------------------------------------------------------------------------
746
+
747
+ /**
748
+ * What the emitter adds that the model does not hold: the skeleton header.
749
+ * Each field is the assembly's, passed by name — none is a value a posing core
750
+ * reads off the rig.
751
+ *
752
+ * - `spine` — the spine-core line the file is written for (`SPINE_VERSION`
753
+ * in `compile.ts`).
754
+ * - `fps`, `images`, `audio` — the rig spec's header bookkeeping as
755
+ * stated, `images` spelled relative to `--out` (`skeletonImagesPath`);
756
+ * each only when present.
757
+ *
758
+ * - `bounds` — the setup-pose bounding box, `x`, `y`, `width`, `height`,
759
+ * which is what the format says the header's four are (issue #907):
760
+ * computed by `compile` with rigc's core over the model
761
+ * (`headerBoundsOf`), each on the 1e-6 grid at float32 (`headerBoxNumber`), or `null` for no box — a rig that
762
+ * declares no stage (issue #578), a setup pose that draws nothing, a
763
+ * region with no atlas rectangle, or a setup pose the core leaves out
764
+ * (that function's header). Four fields or none.
765
+ *
766
+ * `referenceScale` is not here: wind and gravity act over it, so a posing
767
+ * core reads it, and the model holds it (`CompiledModel.referenceScale`,
768
+ * issue #958). The emitter writes the model's value into the header, where
769
+ * `withoutParserDefaults` drops it at the parser's 100.
770
+ *
771
+ * ⚠️ Nor is the stage (`CompiledModel.stage`, issue #1026) — and since issue
772
+ * #907 the header does not carry it at all. Until then the emitter copied the
773
+ * stage into the four box fields, so a reader that took the header for what
774
+ * the format says it is — the box around the figure at rest, for scaling and
775
+ * layout — got the crop the art was painted in. The stage stays the model's:
776
+ * the document states it, and everything that measures against it reads it
777
+ * there.
778
+ */
779
+ export interface SkeletonHeader {
780
+ spine: string;
781
+ fps?: number;
782
+ images?: string;
783
+ audio?: string | null;
784
+ bounds: { x: number; y: number; width: number; height: number } | null;
785
+ }
786
+
787
+ /** The editor's orders the emitter applies, passed for the reason the header's 🔸 gives. */
788
+ export interface SkeletonOrder extends EditorOrder {
789
+ animations: AnimationOrder;
790
+ }
791
+
792
+ /** The model fields a skeleton is written from. */
793
+ export type SkeletonSource = Pick<CompiledModel, 'referenceScale' | 'bones' | 'slots' | 'skins' | 'constraints' | 'events' | 'animations'>;
794
+
795
+ /**
796
+ * The Spine 4.3 skeleton of a compiled model: the emitter's one entry, and
797
+ * the one object `compile` assembles — `CompileResult.skeleton` is its value.
798
+ *
799
+ * Top-level keys in the order the assembly wrote them before the model
800
+ * existed, each section by its own emitter:
801
+ *
802
+ * `skeleton, bones, slots, skins, events, animations, constraints`
803
+ *
804
+ * — `events` only when the rig declares one (a conditional spread, so it lands
805
+ * between `skins` and `animations`, where the editor writes it), `constraints`
806
+ * only when the model holds one (assigned after, so it lands last until the
807
+ * key-order pass moves it). The header's keys: `spine, x, y, width, height,
808
+ * fps, referenceScale, images, audio`, the box's four only together.
809
+ *
810
+ * Then, on the finished object and once: `withoutParserDefaults` drops every
811
+ * key at the value the 4.3 parser reads in its absence, and `inEditorKeyOrder`
812
+ * puts every kind's keys in the editor's order (`src/keyorder.ts`). Neither
813
+ * adds, drops or re-values anything the two tables do not list, and neither
814
+ * throws, so a refusal raised here is raised by a section emitter, in the
815
+ * order above.
816
+ *
817
+ * The box (`header.bounds`, issue #907) is written as `compile` computed it,
818
+ * its four fields together or none. `referenceScale` is the model's, written
819
+ * always and dropped by the parser-default pass at 100 — so a rig stating none and a rig stating 100
820
+ * write the same bytes, as they did when the header carried the stated value.
821
+ *
822
+ * What the emitter adds that the model does not hold is `header`
823
+ * (`SkeletonHeader`); what it restates in Spine's words is every section
824
+ * emitter's own doc comment.
825
+ */
826
+ export function emitSkeleton(model: SkeletonSource, header: SkeletonHeader, order: SkeletonOrder): SpineSkeletonJson {
827
+ const head: SpineSkeletonJson['skeleton'] = { spine: header.spine };
828
+ if (header.bounds !== null) {
829
+ head.x = header.bounds.x;
830
+ head.y = header.bounds.y;
831
+ head.width = header.bounds.width;
832
+ head.height = header.bounds.height;
833
+ }
834
+ if (header.fps !== undefined) head.fps = header.fps;
835
+ head.referenceScale = model.referenceScale;
836
+ if (header.images !== undefined) head.images = header.images;
837
+ if (header.audio !== undefined) head.audio = header.audio;
838
+
839
+ const events = emitEvents(model.events);
840
+ // A weighted vertex's bone index is its bone's position in the array
841
+ // `emitBones` writes; the skin tables bind by name and are encoded here.
842
+ const indexOf = boneIndexOf(model.bones);
843
+ const skeleton: SpineSkeletonJson = {
844
+ skeleton: head,
845
+ bones: emitBones(model.bones),
846
+ slots: emitSlots(model.slots),
847
+ skins: emitSkins(model.skins, indexOf, { skins: order.skins, slotKeys: order.slotKeys }),
848
+ ...(Object.keys(events).length ? { events } : {}),
849
+ animations: emitAnimations(model.animations, model.slots, order.animations),
850
+ };
851
+ if (model.constraints.length) skeleton.constraints = emitConstraints(model.constraints);
852
+ inEditorKeyOrder(withoutParserDefaults(skeleton));
853
+ return skeleton;
854
+ }