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,587 @@
1
+ /**
2
+ * The order the Spine editor writes an object's keys in, per kind of object —
3
+ * and the one pass that puts every emitted object into it (issue #716).
4
+ *
5
+ * ## Why the emitter has an opinion about something JSON does not mean
6
+ *
7
+ * spine-core reads every object below by name (`SkeletonJson` asks a bone map
8
+ * for `"rotation"`, never for its third key), so a key's position reaches no
9
+ * parsed value and no gate can see it. What it reaches is the text: a rebuild of
10
+ * an editor export that writes `length, x, y, rotation` where the export wrote
11
+ * `length, rotation, x, y` is not the export, and #716's pass line — the rebuild
12
+ * and the export identical in canonical form, `JSON.stringify(JSON.parse(text),
13
+ * null, 2)` — is decided on text. Measured at `af8b082` over the twelve editor
14
+ * exports under `examples/`: **280** objects of the rebuilds carried their shared
15
+ * keys in another order — 269 in seven kinds of field order (region attachments
16
+ * 125, bones 87, the three constraint types 46, slots 7, the top level 4) and 11
17
+ * skins whose `attachments` map is keyed by slot NAME, which is not a field
18
+ * order and is `compile.ts`'s `editorSlotKeyOrder`, not a row here.
19
+ *
20
+ * ## Where the rows come from
21
+ *
22
+ * Every row is **read off the twelve exports**, never off memory of the editor's
23
+ * source: for each kind, every export object's key sequence is a constraint
24
+ * (*these keys, in this order*), and the row is the one order that satisfies all
25
+ * of them at once. A writer with a fixed field order produces sequences that are
26
+ * all subsequences of one total order, and the twelve are exactly that — **no
27
+ * two exports contradict each other on any kind**, which is a measurement and
28
+ * not an assumption: an export that did would leave that kind with no row to
29
+ * derive. `IG77` in `selftest.ts` re-takes it from the files every run and prints
30
+ * the counts: every object of every row's kind is in its row's order, and every
31
+ * key an export writes for that kind is in the row.
32
+ *
33
+ * ⚠️ **Where the exports order a pair, the row is theirs; where they do not, it
34
+ * is rigc's, and that is a choice rather than a measurement.** Two keys no
35
+ * export object carries together have no measured order — a transform
36
+ * constraint's `rotation` and `x`, an ik key's `time` and `mix` — and the row
37
+ * places them the way `compile.ts` already emitted them at `af8b082`. So such a
38
+ * pair does not move, and nothing here claims the editor agrees. The pairs that
39
+ * are rigc's are: `animation` ik/transform/physics/attachments before
40
+ * `drawOrder`; `ik constraint` `mix` before `bendPositive`; `ik key` `time`
41
+ * before `mix`, `mix` before `softness`; `physics constraint` `x`, `y` before
42
+ * `rotate`; `transform constraint` `rotation` before `x`, `localSource` and
43
+ * `localTarget` before `mixRotate`, `mixRotate`, `mixX` before `mixY` before
44
+ * `mixScaleX`; `transform key` `time` before `mixRotate` before `mixX`.
45
+ *
46
+ * ## What a row does not list
47
+ *
48
+ * - **A kind with no row keeps rigc's order whole.** Those are the kinds no
49
+ * export writes with two keys or more: a linked mesh, a path or point
50
+ * attachment, a sequence, the path and slider constraints, an event
51
+ * definition, and the keys of the `scalex`/`scaley`/`shearx`/`sheary`/
52
+ * `inherit`, `rgb`/`alpha`/`rgba2`/`rgb2`, `sequence`, path, slider and
53
+ * physics `reset`/`gravity`/`strength` timelines. Unmeasured is not
54
+ * certified, so they are left exactly as the constructors build them.
55
+ * - **A key a row does not list keeps its own position.** A bone's `shearX`, a
56
+ * region's `name` and `path`, a header's `fps`: the row's keys are permuted
57
+ * among the places they occupy and every other key stays at the index the
58
+ * constructor gave it. ⚠️ The obvious alternative — unlisted keys after the
59
+ * listed ones — was rejected on a measured case: the editor wrote the 14
60
+ * physics `strength` keys of the corpus with `value` alone (every one at
61
+ * t=0), so that row would be `[value]` and `time`, which the editor writes
62
+ * first in every key kind the twelve carry it in, would be moved to the end.
63
+ * Rows of one key order nothing and are not rows.
64
+ *
65
+ * ## What is not a row, and why
66
+ *
67
+ * An object keyed by NAMES rather than fields — a bone's timelines, a slot's
68
+ * timelines, a transform constraint's `properties` and each `to` — is not
69
+ * reordered here, and for a reason the text cannot see: `SkeletonJson` builds an
70
+ * animation's timelines in the order it iterates those maps, and two timelines
71
+ * that write one property (`translate` and `translatex` on one bone) apply in
72
+ * that order. Their order is the runtime's, so it is not a key's position, and
73
+ * rigc already emits the export's own on all twelve (0 of 514 bone-timeline
74
+ * maps differ). `animations`, `skins` and a skin's slot keys are the three
75
+ * name-keyed collections the emitter sorts, and `compile.ts` owns all three.
76
+ *
77
+ * 🔒 Pure: no clock, no randomness, and the result depends only on each
78
+ * object's own key set and the order the constructor built it in — which is
79
+ * what `A18_DETERMINISTIC_EMIT` needs, and a second compile reproduces.
80
+ */
81
+
82
+ /** One order per kind of object: the keys the editor writes, in the order it writes them. */
83
+ export type KeyOrderTable = Readonly<Record<string, readonly string[]>>;
84
+
85
+ /**
86
+ * The editor's key order per kind, derived from the twelve exports (see the
87
+ * header). A kind is named by where the object sits: `bone`, `region
88
+ * attachment`, `ik constraint`, `bone rotate key` — `forEachKindedObject` is the
89
+ * one place that names them.
90
+ */
91
+ export const EDITOR_KEY_ORDER: KeyOrderTable = {
92
+ 'top level': ['skeleton', 'bones', 'slots', 'constraints', 'skins', 'events', 'animations'],
93
+ header: ['hash', 'spine', 'x', 'y', 'width', 'height', 'images', 'audio'],
94
+ bone: ['name', 'parent', 'length', 'rotation', 'x', 'y', 'scaleX', 'scaleY', 'inherit', 'color', 'icon'],
95
+ slot: ['name', 'bone', 'color', 'attachment', 'blend'],
96
+ 'ik constraint': ['type', 'name', 'target', 'bones', 'mix', 'bendPositive'],
97
+ 'transform constraint': [
98
+ 'type',
99
+ 'name',
100
+ 'source',
101
+ 'bones',
102
+ 'rotation',
103
+ 'x',
104
+ 'y',
105
+ 'properties',
106
+ 'localSource',
107
+ 'localTarget',
108
+ 'mixRotate',
109
+ 'mixX',
110
+ 'mixY',
111
+ 'mixScaleX',
112
+ 'mixShearY',
113
+ ],
114
+ 'physics constraint': ['type', 'name', 'bone', 'x', 'y', 'rotate', 'damping'],
115
+ skin: ['name', 'attachments'],
116
+ 'region attachment': ['x', 'y', 'scaleX', 'scaleY', 'rotation', 'width', 'height'],
117
+ 'mesh attachment': ['type', 'uvs', 'triangles', 'vertices', 'hull', 'edges', 'width', 'height'],
118
+ 'boundingbox attachment': ['type', 'vertexCount', 'vertices'],
119
+ 'clipping attachment': ['type', 'end', 'vertexCount', 'vertices', 'color'],
120
+ animation: ['slots', 'bones', 'ik', 'transform', 'physics', 'attachments', 'drawOrder', 'events'],
121
+ 'bone rotate key': ['time', 'value', 'curve'],
122
+ 'bone translate key': ['time', 'x', 'y', 'curve'],
123
+ 'bone translatex key': ['time', 'value', 'curve'],
124
+ 'bone translatey key': ['time', 'value', 'curve'],
125
+ 'bone scale key': ['time', 'x', 'y', 'curve'],
126
+ 'bone shear key': ['time', 'x', 'y', 'curve'],
127
+ 'slot attachment key': ['time', 'name'],
128
+ 'slot rgba key': ['time', 'color', 'curve'],
129
+ 'ik key': ['time', 'mix', 'softness', 'bendPositive', 'curve'],
130
+ 'transform key': ['time', 'mixRotate', 'mixX', 'mixY', 'curve'],
131
+ 'physics damping key': ['time', 'value'],
132
+ 'physics inertia key': ['time', 'value'],
133
+ 'physics mass key': ['time', 'value'],
134
+ 'physics mix key': ['time', 'value', 'curve'],
135
+ 'physics wind key': ['time', 'value'],
136
+ 'attachment deform key': ['time', 'offset', 'vertices', 'curve'],
137
+ 'drawOrder key': ['time', 'offsets'],
138
+ 'drawOrder offset': ['slot', 'offset'],
139
+ 'event key': ['time', 'name'],
140
+ };
141
+
142
+ type Rec = Record<string, unknown>;
143
+
144
+ function isRecord(value: unknown): value is Rec {
145
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
146
+ }
147
+
148
+ function records(value: unknown): Rec[] {
149
+ return Array.isArray(value) ? value.filter(isRecord) : [];
150
+ }
151
+
152
+ /** The animation groups that hold `<target>.<timeline> = keys[]`, and the word a key's kind is named by. */
153
+ const TIMELINE_FAMILIES: ReadonlyArray<readonly [group: string, family: string]> = [
154
+ ['bones', 'bone'],
155
+ ['slots', 'slot'],
156
+ ['path', 'path'],
157
+ ['physics', 'physics'],
158
+ ['slider', 'slider'],
159
+ ];
160
+
161
+ /** The groups where the constraint IS the timeline: `<group>.<constraint> = keys[]`. */
162
+ const CONSTRAINT_KEY_GROUPS: readonly string[] = ['ik', 'transform'];
163
+
164
+ /**
165
+ * Visit every object of a Spine 4.3 skeleton file whose keys are FIELDS, with
166
+ * the kind it is and where it sits — parents before their children.
167
+ *
168
+ * The kind is read off the object's position in the format, plus `type` where
169
+ * the format branches on it (a constraint, an attachment). Name-keyed maps are
170
+ * walked through and not visited: they are not a field order (see the header).
171
+ */
172
+ export function forEachKindedObject(skeleton: unknown, visit: (kind: string, object: Rec, path: string) => void): void {
173
+ if (!isRecord(skeleton)) return;
174
+ visit('top level', skeleton, '');
175
+ if (isRecord(skeleton.skeleton)) visit('header', skeleton.skeleton, '.skeleton');
176
+ records(skeleton.bones).forEach((bone, i) => visit('bone', bone, `.bones[${i}]`));
177
+ records(skeleton.slots).forEach((slot, i) => visit('slot', slot, `.slots[${i}]`));
178
+ records(skeleton.constraints).forEach((constraint, i) =>
179
+ visit(`${String(constraint.type)} constraint`, constraint, `.constraints[${i}]`),
180
+ );
181
+ records(skeleton.skins).forEach((skin, i) => {
182
+ const at = `.skins[${i}]`;
183
+ visit('skin', skin, at);
184
+ if (!isRecord(skin.attachments)) return;
185
+ for (const [slot, placeholders] of Object.entries(skin.attachments)) {
186
+ if (!isRecord(placeholders)) continue;
187
+ for (const [placeholder, attachment] of Object.entries(placeholders)) {
188
+ if (!isRecord(attachment)) continue;
189
+ const where = `${at}.attachments.${slot}.${placeholder}`;
190
+ visit(`${typeof attachment.type === 'string' ? attachment.type : 'region'} attachment`, attachment, where);
191
+ if (isRecord(attachment.sequence)) visit('sequence', attachment.sequence, `${where}.sequence`);
192
+ }
193
+ }
194
+ });
195
+ if (isRecord(skeleton.events)) {
196
+ for (const [name, event] of Object.entries(skeleton.events)) if (isRecord(event)) visit('event', event, `.events.${name}`);
197
+ }
198
+ if (!isRecord(skeleton.animations)) return;
199
+ for (const [name, animation] of Object.entries(skeleton.animations)) {
200
+ if (!isRecord(animation)) continue;
201
+ const at = `.animations.${name}`;
202
+ visit('animation', animation, at);
203
+ const timelines = (family: string, byTimeline: unknown, where: string): void => {
204
+ if (!isRecord(byTimeline)) return;
205
+ for (const [timeline, keys] of Object.entries(byTimeline)) {
206
+ records(keys).forEach((key, i) => visit(`${family} ${timeline} key`, key, `${where}.${timeline}[${i}]`));
207
+ }
208
+ };
209
+ for (const [group, family] of TIMELINE_FAMILIES) {
210
+ const byTarget = animation[group];
211
+ if (!isRecord(byTarget)) continue;
212
+ for (const [target, byTimeline] of Object.entries(byTarget)) timelines(family, byTimeline, `${at}.${group}.${target}`);
213
+ }
214
+ for (const group of CONSTRAINT_KEY_GROUPS) {
215
+ const byConstraint = animation[group];
216
+ if (!isRecord(byConstraint)) continue;
217
+ for (const [constraint, keys] of Object.entries(byConstraint)) {
218
+ records(keys).forEach((key, i) => visit(`${group} key`, key, `${at}.${group}.${constraint}[${i}]`));
219
+ }
220
+ }
221
+ if (isRecord(animation.attachments)) {
222
+ for (const [skin, bySlot] of Object.entries(animation.attachments)) {
223
+ if (!isRecord(bySlot)) continue;
224
+ for (const [slot, byAttachment] of Object.entries(bySlot)) {
225
+ if (!isRecord(byAttachment)) continue;
226
+ for (const [attachment, byTimeline] of Object.entries(byAttachment)) {
227
+ timelines('attachment', byTimeline, `${at}.attachments.${skin}.${slot}.${attachment}`);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ records(animation.drawOrder).forEach((key, i) => {
233
+ visit('drawOrder key', key, `${at}.drawOrder[${i}]`);
234
+ records(key.offsets).forEach((offset, j) => visit('drawOrder offset', offset, `${at}.drawOrder[${i}].offsets[${j}]`));
235
+ });
236
+ records(animation.events).forEach((key, i) => visit('event key', key, `${at}.events[${i}]`));
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Put one object's keys in its row's order, in place: the keys the row lists are
242
+ * permuted among the positions they hold, and every other key stays where it is.
243
+ */
244
+ function arrange(object: Rec, rank: ReadonlyMap<string, number>): void {
245
+ const keys = Object.keys(object);
246
+ const listed = keys.filter((key) => rank.has(key)).sort((a, b) => rank.get(a)! - rank.get(b)!);
247
+ let next = 0;
248
+ const order = keys.map((key) => (rank.has(key) ? listed[next++] : key));
249
+ if (order.every((key, i) => key === keys[i])) return;
250
+ const entries = order.map((key) => [key, object[key]] as const);
251
+ for (const key of keys) delete object[key];
252
+ for (const [key, value] of entries) object[key] = value;
253
+ }
254
+
255
+ /**
256
+ * Every object of `skeleton` whose kind has a row, put into that row's order —
257
+ * in place, and returned. `table` is a parameter so the selftest can hand it a
258
+ * row reversed and watch its own check go red; the emitter passes nothing.
259
+ */
260
+ export function inEditorKeyOrder<T extends object>(skeleton: T, table: KeyOrderTable = EDITOR_KEY_ORDER): T {
261
+ const ranks = new Map<string, ReadonlyMap<string, number>>();
262
+ forEachKindedObject(skeleton, (kind, object) => {
263
+ const row = table[kind];
264
+ if (row === undefined) return;
265
+ let rank = ranks.get(kind);
266
+ if (rank === undefined) {
267
+ rank = new Map(row.map((key, i) => [key, i]));
268
+ ranks.set(kind, rank);
269
+ }
270
+ arrange(object, rank);
271
+ });
272
+ return skeleton;
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // the keys the 4.3 parser reads back the same way without them (#716 tranche 3)
277
+ // ---------------------------------------------------------------------------
278
+ //
279
+ // The editor writes a key only when its value is not the one `SkeletonJson`
280
+ // reads in its absence, and rigc wrote every key it was given. Measured at
281
+ // `d48c505` over the twelve editor exports under `examples/`: **2,338** keys of
282
+ // the rebuilds stated a value the export leaves to the parser — `time: 0` on a
283
+ // first key, a rotate key's `value: 0`, a scale key's `x: 1`, an ik key's
284
+ // `mix: 1` — and **87** attachment keys wrote `"name": null`. Each one loads
285
+ // the same `SkeletonData` with the key and without it, which is why no parse
286
+ // and no gate could see any of them.
287
+ //
288
+ // ## Why this is a table and not a parse
289
+ //
290
+ // The parser is the only authority on what a default is, and this table is
291
+ // not a second one: it is a table of the parser's readings that the selftest
292
+ // holds to the parser row by row (`S103` on the in-tree builds, `IG82` on the corpus) by loading an
293
+ // object of each kind with the key at the row's value and without it, and
294
+ // comparing everything `SkeletonData` holds — and then one float32 step off,
295
+ // which must differ. It cannot BE a parse: `compile.ts` must not link the
296
+ // runtime (`CLAUDE.md` *Conventions*, held by `CUR07`), and an emitter that
297
+ // asked the parser what to write would be the gate answering its own question.
298
+ //
299
+ // ## What a row holds
300
+ //
301
+ // - a **constant**: the value `getValue(map, key, default)` falls back to. A
302
+ // colour row is `ffffffff` because the parser leaves the colour white when
303
+ // the key is absent; a mode is spelled the way rigc spells it (`normal`,
304
+ // `percent`), which `Utils.enumValue` reads alike with either first letter.
305
+ // - `{ field, only }`: another field of the same object, as the parser reads it
306
+ // — a transform key's `mixY` falls back to its own `mixX`, which falls back to
307
+ // 1 — and, with `only`, the one value at which the key is left out. `only` is
308
+ // the editor's, measured: `sack-pro` writes `mixX: 0, mixY: 0` on six
309
+ // transform keys where the parser would read an absent `mixY` as that same
310
+ // `0`, and omits both where both are `1`. So `mixY` is left out only where it
311
+ // is `1` AND `mixX` reads `1` — the two rules' intersection, which is always
312
+ // a key the parser reads back the same and never one the editor writes.
313
+ // - `{ previous, first }`: the same field of the timeline's previous key, and
314
+ // `first` on key 0 — a sequence key's `delay` is the parser's `lastDelay`.
315
+ //
316
+ // ⚠️ **A kind with no row keeps every key, and so does a key its row does not
317
+ // list.** A row is here only when an object of its kind is in a build the
318
+ // selftest loads, because a row nothing loads is a claim about the parser
319
+ // nobody has checked, and a wrong one changes a loaded value in silence — where
320
+ // a missing one costs a key the parser reads the same either way. `S101` is what
321
+ // finds a missing one: it deletes every key of every in-tree build in turn and
322
+ // names each the parser reads back unchanged.
323
+ //
324
+ // ⚠️ **Not rows, on purpose:** a transform CONSTRAINT's `mixY` and
325
+ // `mixScaleY`. Their fallback is `setup.mixX` / `setup.mixScaleX`, and the
326
+ // parser reads those only for a property the constraint drives — so on a
327
+ // constraint that drives `y` and not `x` the fallback is the pose's own initial
328
+ // `0` (`TransformConstraintPose.js:34`), not `mixX`'s `1`. A `{ field }` row
329
+ // there was measured wrong on the corpus: it dropped `8-follow-through-pro-
330
+ // ball`'s `mixY: 1` on two constraints that drive `y` alone, they loaded at
331
+ // `mixY: 0`, and `A48` refused the build. A transform KEY reads every mix
332
+ // unconditionally, so its `mixY` row stands. Also not rows: the header's `x`, `y` and `fps`
333
+ // (`SkeletonJson.js:70` assigns them raw, so an absent origin loads as
334
+ // `undefined` rather than `0` — there is no parser default for it to equal);
335
+ // a region's `path` and an attachment's `name` (their fallbacks are the
336
+ // attachment's own name and placeholder, which rigc writes only where they
337
+ // differ); an event key's payload (its fallback is its event definition's, a
338
+ // value held in another object). And two fallbacks the parser does have but
339
+ // nothing here can load: a slider's `time`, read only on a slider with no
340
+ // `bone` (no build or export carries one), and a sequence's `digits: 0`, which
341
+ // no atlas a build resolves against can be read with — the frame names change
342
+ // with it and the parse throws either way. Both are written as stated.
343
+ //
344
+ // 🔒 Pure, and a function of each object's keys and values alone, so
345
+ // `A18_DETERMINISTIC_EMIT` still compares two identical texts.
346
+
347
+ /** A field's value where it is absent, as the 4.3 parser reads it. */
348
+ export type ParserDefault =
349
+ | number
350
+ | boolean
351
+ | string
352
+ | null
353
+ | { readonly field: string; readonly only?: number }
354
+ | { readonly previous: string; readonly first: number };
355
+
356
+ /** One row per kind of object: each field whose absence the parser reads as a value, and that value. */
357
+ export type ParserDefaultTable = Readonly<Record<string, Readonly<Record<string, ParserDefault>>>>;
358
+
359
+ const TIME_ONLY: Readonly<Record<string, ParserDefault>> = { time: 0 };
360
+ const VALUE_AT_ZERO: Readonly<Record<string, ParserDefault>> = { time: 0, value: 0 };
361
+ const VALUE_AT_ONE: Readonly<Record<string, ParserDefault>> = { time: 0, value: 1 };
362
+
363
+ /**
364
+ * The 4.3 parser's defaults, per kind of object — see the header above. Each
365
+ * is a `getValue` fallback of `SkeletonJson.js` in the linked `spine-core`, and
366
+ * the selftest loads every one of them rather than believing this list.
367
+ */
368
+ export const PARSER_DEFAULTS: ParserDefaultTable = {
369
+ header: { referenceScale: 100 },
370
+ bone: {
371
+ length: 0,
372
+ rotation: 0,
373
+ x: 0,
374
+ y: 0,
375
+ scaleX: 1,
376
+ scaleY: 1,
377
+ shearX: 0,
378
+ shearY: 0,
379
+ inherit: 'normal',
380
+ skin: false,
381
+ iconSize: 1,
382
+ iconRotation: 0,
383
+ },
384
+ slot: { color: 'ffffffff', attachment: null, blend: 'normal', visible: true },
385
+ 'ik constraint': { skin: false, mix: 1, softness: 0, bendPositive: true, compress: false, stretch: false },
386
+ 'transform constraint': {
387
+ skin: false,
388
+ localSource: false,
389
+ localTarget: false,
390
+ additive: false,
391
+ clamp: false,
392
+ rotation: 0,
393
+ x: 0,
394
+ y: 0,
395
+ scaleX: 0,
396
+ scaleY: 0,
397
+ shearY: 0,
398
+ mixRotate: 1,
399
+ mixX: 1,
400
+ mixScaleX: 1,
401
+ mixShearY: 1,
402
+ },
403
+ 'path constraint': {
404
+ skin: false,
405
+ positionMode: 'percent',
406
+ spacingMode: 'length',
407
+ rotateMode: 'tangent',
408
+ rotation: 0,
409
+ position: 0,
410
+ spacing: 0,
411
+ mixRotate: 1,
412
+ mixX: 1,
413
+ mixY: { field: 'mixX', only: 1 },
414
+ },
415
+ 'physics constraint': {
416
+ skin: false,
417
+ x: 0,
418
+ y: 0,
419
+ rotate: 0,
420
+ scaleX: 0,
421
+ shearX: 0,
422
+ limit: 5000,
423
+ fps: 60,
424
+ inertia: 0.5,
425
+ strength: 100,
426
+ damping: 0.85,
427
+ mass: 1,
428
+ wind: 0,
429
+ gravity: 0,
430
+ mix: 1,
431
+ inertiaGlobal: false,
432
+ strengthGlobal: false,
433
+ dampingGlobal: false,
434
+ massGlobal: false,
435
+ windGlobal: false,
436
+ gravityGlobal: false,
437
+ mixGlobal: false,
438
+ },
439
+ 'slider constraint': { skin: false, additive: false, loop: false, mix: 1, from: 0, to: 0, scale: 1, max: 0, local: false },
440
+ 'region attachment': { x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0, color: 'ffffffff' },
441
+ 'mesh attachment': { color: 'ffffffff', hull: 0, width: 0, height: 0 },
442
+ 'path attachment': { closed: false, constantSpeed: true },
443
+ 'clipping attachment': { convex: false, inverse: false },
444
+ sequence: { start: 1, setup: 0 },
445
+ event: { int: 0, float: 0, string: '', audio: null },
446
+ 'bone rotate key': VALUE_AT_ZERO,
447
+ 'bone translate key': { time: 0, x: 0, y: 0 },
448
+ 'bone translatex key': VALUE_AT_ZERO,
449
+ 'bone translatey key': VALUE_AT_ZERO,
450
+ 'bone scale key': { time: 0, x: 1, y: 1 },
451
+ 'bone scalex key': VALUE_AT_ONE,
452
+ 'bone scaley key': VALUE_AT_ONE,
453
+ 'bone shear key': { time: 0, x: 0, y: 0 },
454
+ 'slot attachment key': { time: 0, name: null },
455
+ 'slot rgba key': TIME_ONLY,
456
+ 'ik key': { time: 0, mix: 1, softness: 0, bendPositive: true, compress: false, stretch: false },
457
+ 'transform key': { time: 0, mixRotate: 1, mixX: 1, mixY: { field: 'mixX', only: 1 }, mixScaleX: 1, mixScaleY: 1, mixShearY: 1 },
458
+ 'path position key': VALUE_AT_ZERO,
459
+ 'physics damping key': VALUE_AT_ZERO,
460
+ 'physics inertia key': VALUE_AT_ZERO,
461
+ 'physics mass key': VALUE_AT_ZERO,
462
+ 'physics strength key': VALUE_AT_ZERO,
463
+ 'physics wind key': VALUE_AT_ZERO,
464
+ 'physics mix key': VALUE_AT_ONE,
465
+ 'attachment deform key': { time: 0, offset: 0 },
466
+ 'attachment sequence key': { time: 0, index: 0, mode: 'hold', delay: { previous: 'delay', first: 0 } },
467
+ 'drawOrder key': TIME_ONLY,
468
+ 'event key': TIME_ONLY,
469
+ };
470
+
471
+ /**
472
+ * The `referenceScale` a header stating none is read as: the parser's
473
+ * `getValue(skeletonMap, "referenceScale", 100)`, taken from
474
+ * `PARSER_DEFAULTS.header` (which the selftest loads rather than believes) so
475
+ * the model and the omission pass cannot name two different numbers.
476
+ * `compile.ts` gives the model this value when the rig spec states none.
477
+ *
478
+ * Here rather than in `src/emit_spine.ts`, which defined it until issue
479
+ * #1026: the compiler reads it to fill the model, and the entry that compiles
480
+ * a model without calling the Spine emitter (`compileModel`) reads nothing
481
+ * from the emitter's module. `src/emit_spine.ts` re-exports it.
482
+ */
483
+ export const UNSTATED_REFERENCE_SCALE: number = ((): number => {
484
+ const v = PARSER_DEFAULTS.header?.referenceScale;
485
+ if (typeof v !== 'number') throw new Error(`internal: PARSER_DEFAULTS.header.referenceScale is ${JSON.stringify(v)}, not a number`);
486
+ return v;
487
+ })();
488
+
489
+ /**
490
+ * A physics constraint's parameters and their parser defaults
491
+ * (`SkeletonJson.js:295-319`), in the order the motion spec's physics table
492
+ * writes them. The same values are `PARSER_DEFAULTS['physics constraint']`'s;
493
+ * this list is also an ORDER, which that row is not: `compile.ts` reads it for
494
+ * the names the table copies, and `src/emit_spine.ts` for the order the table's
495
+ * constraint is written in and the defaults it leaves out. Here, beside the
496
+ * parser's other defaults, since issue #1026 — for the reason
497
+ * `UNSTATED_REFERENCE_SCALE` gives.
498
+ */
499
+ export const PHYSICS_PARAMS: ReadonlyArray<readonly [string, number]> = [
500
+ ['inertia', 0.5],
501
+ ['strength', 100],
502
+ ['damping', 0.85],
503
+ ['mass', 1],
504
+ ['wind', 0],
505
+ ['gravity', 0],
506
+ ['mix', 1],
507
+ ['fps', 60],
508
+ ['limit', 5000],
509
+ ];
510
+
511
+ /** `….rotate[3]` → its stem and 3; anything that does not end in an index → null. */
512
+ function trailingIndex(path: string): { stem: string; index: number } | null {
513
+ const m = /^(.*)\[(\d+)\]$/.exec(path);
514
+ return m === null ? null : { stem: m[1], index: Number(m[2]) };
515
+ }
516
+
517
+ /** An object a row is read on, and — for a timeline key — the key before it. */
518
+ export interface ParserReadingSite {
519
+ readonly object: Readonly<Record<string, unknown>>;
520
+ /** The timeline's previous key; null on key 0 and on anything that is not a timeline key. */
521
+ readonly previous: () => ParserReadingSite | null;
522
+ }
523
+
524
+ /**
525
+ * What the parser reads for `field` at `site`: the value written, or the row's
526
+ * where it is absent — `undefined` when the row does not list the field and it
527
+ * is absent.
528
+ */
529
+ export function parserReading(row: Readonly<Record<string, ParserDefault>>, site: ParserReadingSite, field: string): unknown {
530
+ if (field in site.object) return site.object[field];
531
+ const rule = row[field];
532
+ if (rule === undefined || rule === null || typeof rule !== 'object') return rule;
533
+ if ('field' in rule) return parserReading(row, site, rule.field);
534
+ const previous = site.previous();
535
+ return previous === null ? rule.first : parserReading(row, previous, rule.previous);
536
+ }
537
+
538
+ /**
539
+ * Whether the emitter leaves `field` out of the object at `site`: it is
540
+ * written, its row lists it, and the parser reads the same value without it —
541
+ * at the row's `only` value, where the row has one. The one decision, shared by
542
+ * the pass below and by `ingest`, which says a restated default out loud only
543
+ * where this would still write it.
544
+ */
545
+ export function parserOmits(row: Readonly<Record<string, ParserDefault>>, site: ParserReadingSite, field: string): boolean {
546
+ if (!(field in site.object)) return false;
547
+ const rule = row[field];
548
+ if (rule === undefined) return false;
549
+ const value = site.object[field];
550
+ if (rule !== null && typeof rule === 'object' && 'field' in rule && rule.only !== undefined && !Object.is(value, rule.only)) {
551
+ return false;
552
+ }
553
+ const without: Rec = { ...site.object };
554
+ delete without[field];
555
+ return Object.is(parserReading(row, { object: without, previous: site.previous }, field), value);
556
+ }
557
+
558
+ /**
559
+ * Every key of `skeleton` whose value is the one the 4.3 parser reads in its
560
+ * absence, removed — in place, and returned. Each is decided against the object
561
+ * as its constructor built it, before anything is removed, so a `{ field }` or
562
+ * `{ previous }` row reads exactly what the parser will. `table` is a parameter
563
+ * so the selftest can plant a wrong row; the emitter passes nothing.
564
+ */
565
+ export function withoutParserDefaults<T extends object>(skeleton: T, table: ParserDefaultTable = PARSER_DEFAULTS): T {
566
+ const byPath = new Map<string, Rec>();
567
+ const siteAt = (path: string, object: Rec): ParserReadingSite => ({
568
+ object,
569
+ previous: () => {
570
+ const at = trailingIndex(path);
571
+ if (at === null || at.index === 0) return null;
572
+ const stem = `${at.stem}[${at.index - 1}]`;
573
+ const before = byPath.get(stem);
574
+ return before === undefined ? null : siteAt(stem, before);
575
+ },
576
+ });
577
+ const removed: Array<{ object: Rec; field: string }> = [];
578
+ forEachKindedObject(skeleton, (kind, object, path) => {
579
+ byPath.set(path, object);
580
+ const row = table[kind];
581
+ if (row === undefined) return;
582
+ const site = siteAt(path, object);
583
+ for (const field of Object.keys(row)) if (parserOmits(row, site, field)) removed.push({ object, field });
584
+ });
585
+ for (const { object, field } of removed) delete object[field];
586
+ return skeleton;
587
+ }