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,197 @@
1
+ /**
2
+ * The core's looping walk (issue #1025, cut 4c-5a of step 4c of #380): one
3
+ * animation on a LOOPING track, reset at the setup pose and then stepped at
4
+ * the caller's steps, every pose returned as the doubles the core computed —
5
+ * a value that is not finite included, in place, rather than refused.
6
+ *
7
+ * This is the walk `A10_NO_NAN_AFTER_STEPPING` takes, and the reason it is
8
+ * its own entry rather than the raw entry (`./raw.ts`) under another name is
9
+ * the two things the raw entry's contract excludes on purpose: its track
10
+ * holds at the duration, and it refuses a non-finite value by name
11
+ * (`CoreInputError`), because a renderer handed one would draw a different
12
+ * picture in silence. A10's subject is exactly that value, so here it is the
13
+ * answer, not a refusal. Everything else is the raw entry's walk, called
14
+ * rather than restated (`walkIn` in `./raw.ts`, which branches on the two).
15
+ *
16
+ * ## The walk, measured
17
+ *
18
+ * Written from spine-core 4.3.13 running A10's own recipe — `Skeleton`,
19
+ * `AnimationState` with `setAnimation(0, name, true)`; `setupPose()`,
20
+ * `update(0)`, `updateWorldTransform(Physics.reset)`; then per step
21
+ * `AnimationState.update(dt)`, `apply`, `Skeleton.update(dt)`,
22
+ * `updateWorldTransform(Physics.update)` — and read off `TrackEntry.trackTime`,
23
+ * `TrackEntry.getAnimationTime()`, every bone's `appliedPose`, every shown
24
+ * region's and mesh's world vertices, every slot's colours and the calls the
25
+ * runtime makes to `PhysicsConstraint.reset`. The PR of issue #1025's cut
26
+ * 4c-5a carries the tables; each rule names the count that held it.
27
+ *
28
+ * - **Pose 0 is the setup pose, reset** — the animation is set and not yet
29
+ * applied, so the reset is taken at the setup pose (`RawReset` `setup`).
30
+ * - **The track time is the running sum of the steps**, as on the raw
31
+ * entry's walk: `TrackEntry.trackTime` equalled it on every step of the
32
+ * nineteen tree rows' walks (6,480 of 6,480).
33
+ * - **The time** — the animation is applied at the track time wrapped by the
34
+ * duration (`loopedTime` in `./raw.ts`): `trackTime % duration` in double,
35
+ * so a track time of exactly the duration is 0, the first pose again, and
36
+ * over a duration of 0 the time is 0 whatever the track time (an animation
37
+ * keying nothing, or keying only at 0) — `getAnimationTime()` read so on
38
+ * 6,480 of 6,480 steps, 1,087 of them at or past the duration and 4 exactly
39
+ * on it; before the zero-duration reading it read 6,000.
40
+ * - **The physics clock moves by the step itself** (`Skeleton.update(dt)`),
41
+ * whatever the wrap does to the animation time: moved by the animation
42
+ * time's own change instead, 32 of 82 seeded and forged walks read off.
43
+ * - **A `reset` key across the wrap.** A key is crossed when it lies in
44
+ * `(previous animation time, animation time]`, as on the raw entry; on a
45
+ * step whose animation time went DOWN — the wrap — it is crossed when it
46
+ * lies after the previous time or at or before the new one, `(last, ∞)`
47
+ * and `(−1, t]` (`resetCrossed` in `./constraints_physics.ts`). A step
48
+ * that passes the duration once or more while its animation time still
49
+ * rises is not a wrap there: a key at 0.25 s of a 0.3 s animation stepped
50
+ * by 0.7 s fired on neither of the two steps that carried the track past
51
+ * it, and an equal time is not a wrap either (a 0.25 s animation stepped by
52
+ * 0.25 s fired its key at 0 once). With the wrap firing nothing, 957 of
53
+ * 14,460 poses of a seeded population read off.
54
+ * - **A value that is not finite** is the IEEE result of the same
55
+ * arithmetic, kept: a world matrix term, a world position, a vertex, a slot
56
+ * colour channel. Where the raw entry writes such a value `null` and
57
+ * refuses, this entry writes the double; on 4,333 poses holding one, every
58
+ * NaN and Infinity stood where spine-core's did.
59
+ *
60
+ * Events are not carried: A10 reads none, and what a looping track fires
61
+ * across the wrap was not measured. Nor are the clipped rows, and the walk
62
+ * does not cut them (issue #1179): every clip it starts is still planned, so
63
+ * a clip the core does not draw refuses the walk by name, as below.
64
+ *
65
+ * ⛔ **What is still refused** is the raw entry's other refusal: a construct
66
+ * the core would leave out (a block the oracle's core dump names absent) is a
67
+ * `CoreInputError` naming why, as there — a pose with a block missing is not
68
+ * a pose whose values can be judged finite.
69
+ *
70
+ * 🔁 **A slider whose animation keys a physics timeline** was refused here
71
+ * by name until issue #1049 (`sliderPhysicsWhy`): six of the path-slider
72
+ * suite's distinct builds walked off spine-core from the third step, and the
73
+ * raw entry read the same gap. The core had not applied a slider's physics keys at all;
74
+ * it now does, under the step, by the rule `./constraints_slider.ts` *Its
75
+ * physics timelines* states and the core suite's `CO31` holds — so this walk
76
+ * poses the class as the raw entry does, and refuses nothing the raw entry
77
+ * does not.
78
+ */
79
+ import { scanSetupIn, scanWalkIn, setupPoseIn, walkIn, type RawBone, type RawDrawn, type ScanPlant, type ScanPose, type WalkMode } from './raw.ts';
80
+ import { activeBones, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
81
+ import { historyTaint } from './constraints.ts';
82
+ import type { TimelinePlant } from './animation.ts';
83
+
84
+ /** One pose of the looping walk: the raw entry's pose less what this walk does not carry (events, the clip rows, the shown records). */
85
+ export interface WalkPose {
86
+ /** The running sum of the steps, 0 for pose 0. */
87
+ trackTime: number;
88
+ /** The time the animation was applied at: the track time wrapped by the duration (`loopedTime`). */
89
+ animationTime: number;
90
+ /** Every bone in document order, its world matrix and origin as computed, finite or not. */
91
+ bones: RawBone[];
92
+ /** Every slot row (`CoreSlotRow`) with its colour channels as computed, finite or not. */
93
+ slots: CoreSlotRow[];
94
+ /** The slot names in the posed draw order. */
95
+ drawOrder: string[];
96
+ /** Every region and mesh a slot shows, in draw order, its world vertices as computed, finite or not. */
97
+ drawn: RawDrawn[];
98
+ }
99
+
100
+ /**
101
+ * The bones and slots whose numbers the walk does not vouch for, because the
102
+ * runtime's own value there is HISTORY (issue #979): a bone the view leaves
103
+ * unposed — inactive, or below an inactive bone — that a constraint writing
104
+ * into an inactive bone reaches (`historyTaint` in `./constraints.ts`, the
105
+ * walk the core's leak refusal reads), each with that writer; and every slot
106
+ * on such a bone. spine-core never updates an inactive bone, so what a
107
+ * constraint wrote into it on the step before is what the constraint reads
108
+ * on the next one; the core poses every step from the setup pose. Measured on
109
+ * a public probe (a transform constraint writing four skin-required bones no
110
+ * skin activates): at mix 1 the two differ only in the sign of zero of the
111
+ * matrix cells (`atan2` of a zero matrix is 0° or 180° by those signs), and at
112
+ * a translate mix of 0.5 by value (worldX 30.000000348 in spine-core against
113
+ * 20.000000232 in the core at step 1), on the bones and on an active child of
114
+ * one. Where such a value reaches a posed bone the core already refuses the
115
+ * document (`unposedLeakWhy`); here nothing posed reads it, so a walk
116
+ * comparison counts these numbers apart by name and compares them with
117
+ * nothing, as `tools/pose_oracle.ts unposed` does under the step.
118
+ */
119
+ export function walkHistory(view: CompiledDocument): { bones: Map<string, string>; slots: Map<string, string> } {
120
+ const active = activeBones(view);
121
+ const unposed = new Set<string>();
122
+ for (const b of view.bones) if (!active.has(b.name) || (b.parent !== undefined && unposed.has(b.parent))) unposed.add(b.name);
123
+ const bones = new Map<string, string>();
124
+ for (const [bone, w] of historyTaint(view).tainted) if (unposed.has(bone)) bones.set(bone, `${w.kind}/${w.name} on inactive ${w.inactive}`);
125
+ return { bones, slots: new Map(view.slots.filter((s) => bones.has(s.bone)).map((s) => [s.name, s.bone])) };
126
+ }
127
+
128
+ /** The looping walk's mode: the track loops, and a non-finite value stays in the pose. */
129
+ const LOOPING: WalkMode = { loop: true, keep: true };
130
+
131
+ /**
132
+ * The looping walk's plants (the core suite's `CO26`), each the reading the
133
+ * walk was measured against and rejected, in a copy: the time a step applies
134
+ * at, what the physics clock moves by, and what a number is written as.
135
+ * Nothing but a control passes one.
136
+ */
137
+ export interface WalkPlant {
138
+ time?: WalkMode['time'];
139
+ clock?: WalkMode['clock'];
140
+ wrapResets?: WalkMode['wrapResets'];
141
+ /** Every number of a pose rewritten — a swallowed non-finite value is `(v) => (Number.isFinite(v) ? v : 0)`. */
142
+ number?: (v: number) => number;
143
+ }
144
+
145
+ /** A raw pose narrowed to what the looping walk carries, every number through `number`. */
146
+ function narrow(p: { trackTime: number; animationTime: number; bones: RawBone[]; slots: CoreSlotRow[]; drawOrder: string[]; drawn: RawDrawn[] }, number?: (v: number) => number): WalkPose {
147
+ if (number === undefined) return { trackTime: p.trackTime, animationTime: p.animationTime, bones: p.bones, slots: p.slots, drawOrder: p.drawOrder, drawn: p.drawn };
148
+ const n = (v: number | null): number | null => (v === null ? null : number(v));
149
+ return {
150
+ trackTime: p.trackTime,
151
+ animationTime: p.animationTime,
152
+ bones: p.bones.map((b) => ({ ...b, a: number(b.a), b: number(b.b), c: number(b.c), d: number(b.d), worldX: number(b.worldX), worldY: number(b.worldY) })),
153
+ slots: p.slots.map((r): CoreSlotRow => [r[0], r[1], n(r[2]), n(r[3]), n(r[4]), n(r[5]), r[6] === null ? null : [n(r[6][0]), n(r[6][1]), n(r[6][2])], r[7], r[8]]),
154
+ drawOrder: p.drawOrder,
155
+ drawn: p.drawn.map((d) => ({ ...d, vertices: d.vertices.map(number) })),
156
+ };
157
+ }
158
+
159
+ /**
160
+ * The setup pose, every physics state reset, every value kept whatever it is —
161
+ * what A10 reads before any animation is set.
162
+ */
163
+ export function poseWalkSetup(doc: CompiledDocument, plant: CorePlant = {}, walkPlant: WalkPlant = {}): WalkPose {
164
+ return narrow(setupPoseIn(doc, plant, LOOPING), walkPlant.number);
165
+ }
166
+
167
+ /**
168
+ * One animation on a looping track (the header's *The walk*): pose 0 the setup
169
+ * pose reset, then one pose per step of `steps` — `steps.length + 1` poses in
170
+ * all. A step must be a finite time at or above 0 and the animation the
171
+ * document's, as on the raw entry; a construct the core would leave out
172
+ * refuses the call by name; a value that is not finite does not.
173
+ */
174
+ export function poseLoopingWalk(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant = {}, walkPlant: WalkPlant = {}): WalkPose[] {
175
+ const mode: WalkMode = { ...LOOPING, ...(walkPlant.time === undefined ? {} : { time: walkPlant.time }), ...(walkPlant.clock === undefined ? {} : { clock: walkPlant.clock }), ...(walkPlant.wrapResets === undefined ? {} : { wrapResets: walkPlant.wrapResets }) };
176
+ return walkIn(doc, animation, steps, plant, 'setup', mode).map((p) => narrow(p, walkPlant.number));
177
+ }
178
+
179
+ /**
180
+ * The scan's walk (issue #1179, the second part): `poseLoopingWalk` and
181
+ * `poseWalkSetup` as A10 reads them, each pose a `ScanPose` — the bones' world
182
+ * matrix and origin, the slot rows and the drawn attachments, and nothing the
183
+ * walk forms beside them for other readers (the bones' oracle rows and getter
184
+ * readings). The same walk: `walkIn`'s body in `./raw.ts`, posing each step by
185
+ * the same operations in the same order and refusing at the same places, with
186
+ * another assembler (`PoseShape`). `src/assertions/model/stepped_poses.ts` is
187
+ * its one caller; the public entries above are unchanged, and the core suite
188
+ * holds the two to the same numbers (`CO43`, `CO44`, `CO45`).
189
+ */
190
+ export function scanLoopingWalk(doc: CompiledDocument, animation: string, steps: readonly number[], scanPlant: ScanPlant = {}): ScanPose[] {
191
+ return scanWalkIn(doc, animation, steps, {}, 'setup', LOOPING, scanPlant);
192
+ }
193
+
194
+ /** `poseWalkSetup` as the scan reads it (`scanLoopingWalk`). */
195
+ export function scanWalkSetup(doc: CompiledDocument): ScanPose {
196
+ return scanSetupIn(doc, {}, LOOPING);
197
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * The core's own setup evaluator: every bone's world matrix and origin from
3
+ * the compiled model's bones (issue #925). Written from what a bone's fields
4
+ * mean and from measurement against the runtime's pose dump
5
+ * (`tools/pose_oracle.ts dump`, spine-core 4.3.13), and held to it by the core
6
+ * suite's equivalence controls.
7
+ *
8
+ * ⭐ **The compiler's setup transforms are this evaluator too** (issue #1015),
9
+ * under the same arithmetic (issue #1021). `src/transform.ts` held a second
10
+ * evaluator of its own until #1015, and the two differed in exactly three
11
+ * places, none of them a different matrix: the degree factor (`Math.PI / 180`
12
+ * there, the runtime's 3.1415927 here), the frame of a bone with scale 1 and
13
+ * no shear (`[cos, −sin, sin, cos]` of the rotation there, the y column at
14
+ * `rotation + 90` here), and how a radian angle is turned into degrees
15
+ * (divided by the degree factor there, multiplied by `180 / pi` here). #1015
16
+ * made them a parameter (`WorldArithmetic`) so that no byte moved; #1021
17
+ * measured which binds closer — every point the compiler binds from a world
18
+ * position, posed by spine-core from the emitted build, against that position
19
+ * — and the runtime's three put every one at the float32 floor where the
20
+ * compiler's left `gallery/look`'s up to 8.6e-5 off. So the compiler passes
21
+ * none now, and `RUNTIME_ARITHMETIC`, the measured choices below, is the only
22
+ * arithmetic anything here runs under; the parameter stays for the plants
23
+ * that swap one choice back in to show a control fire.
24
+ *
25
+ * ## What each measured choice is
26
+ *
27
+ * - **Degrees to radians with pi written 3.1415927.** The runtime's root bone
28
+ * at rotation 0 reads `b = -2.3205103333142417e-8` and
29
+ * `d = 0.9999999999999998`: `cos` and `sin` of 90 degrees converted with
30
+ * pi = 3.1415927, since `(3.1415927 - pi) / 2 = 2.3205e-8`. It is that
31
+ * decimal in double arithmetic — not float32: pi rounded to float32 would
32
+ * give `4.37e-8`, and the bone fields read through `Math.fround` moved the
33
+ * dump further, not closer (measured on the nineteen recipes).
34
+ * - **The degree factor is taken once**, `pi / 180`, and every angle is
35
+ * multiplied by it.
36
+ * - **The y column is `cos(rotation + 90 + shearY)` and `sin(…)` on every
37
+ * bone**, never `-sin`/`cos` of the rotation. With the constant above but
38
+ * the `-sin` form, `2-the-12-principles` stays 61 millionths off.
39
+ * - **A root bone is its local matrix at its own `x`, `y`**: the skeleton is
40
+ * at the origin, unscaled, in every dump taken.
41
+ *
42
+ * Both halves together read IDENTICAL, worst delta exactly 0, on the 12
43
+ * recipes without constraints (`tools/core_gate.ts`); either half alone is
44
+ * exact on none of them.
45
+ *
46
+ * ## The five inherit modes
47
+ *
48
+ * What each mode takes from the parent, as the format's documentation words
49
+ * it, then how it is computed here. A child's ORIGIN is always the parent's
50
+ * matrix applied to the local `x`, `y` plus the parent's origin — the modes
51
+ * change only the matrix.
52
+ *
53
+ * - `normal` — everything: parent matrix times local matrix.
54
+ * - `onlyTranslation` — the parent's position only: the local matrix alone.
55
+ * - `noRotationOrReflection` — the parent's scale (and what shear leaves of
56
+ * it), not its rotation and not a reflection. The parent's matrix is made
57
+ * conformal on its x axis: that axis kept, the y axis turned to 90 degrees
58
+ * from it with length `|det| / |x axis|` (so the area, the scale the parent
59
+ * carries, is kept and its sign is not); the local matrix is built at the
60
+ * bone's rotation MINUS the parent's x-axis angle; the product is the world.
61
+ * ⚠️ **A parent x axis whose squared length is at most `1e-5` squared is
62
+ * collapsed** (issue #979, `COLLAPSED_X_AXIS_SQ`): the conformal frame is
63
+ * then the parent's y column with its x component negated, beside a zero x
64
+ * column, and the local matrix is built at the rotation less 90 plus the y
65
+ * column's angle, summed in that order. Measured on a child of such a
66
+ * parent: by bisection on the x axis's length, 1e-5 reads collapsed and the
67
+ * next double above does not, at parent rotations 0, 30 and 45 (whether the
68
+ * runtime compares the squared length with `1e-5 · 1e-5` or the length with
69
+ * `1e-5`, 200,000 parents searched separated no pair); 480 probes of x axes
70
+ * from 1e-4 down to 1e-20 and 0 under y axes up to 2e17, sheared, scaled and
71
+ * reflected, read exact, where the reading this replaced (collapsed only at
72
+ * length 0, the y column as it is, the angle less 90) read 71 of 96 off and
73
+ * the rotation-first sum `rotation − (90 − angle)` 10 of 480. The zero
74
+ * matrix of a bone below an inactive one is the collapsed case at length 0,
75
+ * and it fixes the signs of zero of every child in this mode there (`CC13`).
76
+ * - `noScale` — the parent's rotation (and reflection), not its scale: the
77
+ * bone's local rotation is carried through the parent's matrix as a
78
+ * direction, normalised to unit length; the y axis is that direction turned
79
+ * 90 degrees, turned the other way when the parent reflects (`det < 0`);
80
+ * the bone's own shear and scale are applied on that frame.
81
+ * - `noScaleOrReflection` — as `noScale`, never turned for a reflection.
82
+ *
83
+ * ## To the bit (issue #966)
84
+ *
85
+ * The rules above were measured on the oracle's six-decimal grid; three of
86
+ * the modes matched it and not the runtime's last bit (issue #959). Held
87
+ * against `pose_oracle.ts dump --raw` at tolerance 0 — a bone in the mode
88
+ * under a parent rotated, scaled, sheared and reflecting, two children 1e9
89
+ * units out (the core suite's `CR02`, 300 per mode) — three operation orders
90
+ * are the runtime's and the grid could not tell them:
91
+ *
92
+ * - a radian angle is turned into degrees by multiplying with `180 / pi`
93
+ * (`RUNTIME_DEG`), not by dividing by `pi / 180`: the division read 106
94
+ * of 300 `noRotationOrReflection` bones off;
95
+ * - `noRotationOrReflection`'s y angle is `r + shearY + 90`, the shear
96
+ * added before the right angle (`r + 90 + shearY` read 46 of 300 off);
97
+ * - `noScale`'s direction is normalised by multiplying with the reciprocal
98
+ * of its length (dividing read 138 of 300 off).
99
+ *
100
+ * Measured: `normal` on 12 recipes, `onlyTranslation` on 1 and
101
+ * `noRotationOrReflection` on 2 of the tree's corpus; all five, under rotated,
102
+ * scaled, sheared and reflecting parents, on the hand-written probe the core
103
+ * suite builds (`CO07`), because the corpus reaches `noScale` only on a rig
104
+ * with constraints and `noScaleOrReflection` on none.
105
+ */
106
+ import type { ModelBone } from '../model.ts';
107
+
108
+ /** Pi as the runtime converts degrees with it — see the header. */
109
+ export const RUNTIME_PI = 3.1415927;
110
+ const RAD = RUNTIME_PI / 180;
111
+ /** Radians to degrees as the runtime converts them (issue #966): multiplied by `180 / pi`, not divided by `pi / 180` — the two differ in the last bit. */
112
+ export const RUNTIME_DEG = 180 / RUNTIME_PI;
113
+
114
+ /** A parent x axis whose squared length is at most this is collapsed for `noRotationOrReflection` (issue #979): `1e-5` squared, as measured — see the header. */
115
+ export const COLLAPSED_X_AXIS_SQ = 0.00001 * 0.00001;
116
+
117
+ /**
118
+ * The three places an evaluator's arithmetic may differ in the last bit while
119
+ * computing the same matrix (issue #1015) — see the header. The core poses and
120
+ * the compiler binds with `RUNTIME_ARITHMETIC` (issue #1021); another value is
121
+ * a plant's.
122
+ */
123
+ export interface WorldArithmetic {
124
+ /** Degrees to radians: every angle is multiplied by it. */
125
+ radiansPerDegree: number;
126
+ /** Radians to degrees — the angle `noRotationOrReflection` takes off a parent's axis. */
127
+ degreesOf: (radians: number) => number;
128
+ /** A bone with scale 1 and no shear takes `[cos r, −sin r, sin r, cos r]` instead of the y column at `r + 90`; the general frame everywhere else. */
129
+ rotationOnlyFrame: boolean;
130
+ }
131
+
132
+ /** The runtime's arithmetic, as measured (the header's three choices): what the core poses with. */
133
+ export const RUNTIME_ARITHMETIC: WorldArithmetic = {
134
+ radiansPerDegree: RAD,
135
+ degreesOf: (radians) => radians * RUNTIME_DEG,
136
+ rotationOnlyFrame: false,
137
+ };
138
+
139
+ /** One bone's world transform: the matrix `[a b; c d]` and the origin, y up. */
140
+ export interface CoreWorld {
141
+ a: number;
142
+ b: number;
143
+ c: number;
144
+ d: number;
145
+ worldX: number;
146
+ worldY: number;
147
+ }
148
+
149
+ /** The five modes, after the first-letter fold the rig spec admits. */
150
+ export type CoreInheritMode = 'normal' | 'onlyTranslation' | 'noRotationOrReflection' | 'noScale' | 'noScaleOrReflection';
151
+
152
+ export type M2 = [number, number, number, number];
153
+
154
+ /** What computes a child's world matrix from its mode, its parent's world, its own fields and the arithmetic in force. */
155
+ export type InheritComputation = (mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone, arithmetic: WorldArithmetic) => M2;
156
+
157
+ /** The matrix of a rotation, shears and scales, in the column form the header states. */
158
+ function frame(rotation: number, shearX: number, shearY: number, scaleX: number, scaleY: number, rad: number = RAD): M2 {
159
+ const xAngle = (rotation + shearX) * rad;
160
+ const yAngle = (rotation + 90 + shearY) * rad;
161
+ return [Math.cos(xAngle) * scaleX, Math.cos(yAngle) * scaleY, Math.sin(xAngle) * scaleX, Math.sin(yAngle) * scaleY];
162
+ }
163
+
164
+ /** A bone's own frame — a root's matrix, and the local factor of `normal` and `onlyTranslation` — under `arithmetic`. */
165
+ function localFrame(arithmetic: WorldArithmetic, rotation: number, shearX: number, shearY: number, scaleX: number, scaleY: number): M2 {
166
+ const rad = arithmetic.radiansPerDegree;
167
+ if (arithmetic.rotationOnlyFrame && scaleX === 1 && scaleY === 1 && shearX === 0 && shearY === 0) {
168
+ const cos = Math.cos(rotation * rad);
169
+ const sin = Math.sin(rotation * rad);
170
+ return [cos, -sin, sin, cos];
171
+ }
172
+ return frame(rotation, shearX, shearY, scaleX, scaleY, rad);
173
+ }
174
+
175
+ function times(p: M2, q: M2): M2 {
176
+ return [p[0] * q[0] + p[1] * q[2], p[0] * q[1] + p[1] * q[3], p[2] * q[0] + p[3] * q[2], p[2] * q[1] + p[3] * q[3]];
177
+ }
178
+
179
+ /**
180
+ * The unit direction a `noScale` / `noScaleOrReflection` bone's x axis takes:
181
+ * its local rotation carried through the parent's matrix and normalised. Shared
182
+ * by the forward frame and `localFromWorld`'s read-back (`./constraints.ts`),
183
+ * which builds the frame to read the bone's columns in from the rotation it
184
+ * has just read, through this same arithmetic (issue #966).
185
+ */
186
+ export function noScaleDirection(p: M2, rotation: number, rad: number = RAD): [number, number] {
187
+ const r = rotation * rad;
188
+ const cos = Math.cos(r);
189
+ const sin = Math.sin(r);
190
+ const ux = p[0] * cos + p[1] * sin;
191
+ const uy = p[2] * cos + p[3] * sin;
192
+ // Normalised by multiplying with the reciprocal of the length, not by dividing by it (issue #966): the division reads 1 ulp off on 375 bone-samples of the corpus's `noScale` row.
193
+ const inverse = 1 / Math.sqrt(ux * ux + uy * uy);
194
+ return [ux * inverse, uy * inverse];
195
+ }
196
+
197
+ /** The world matrix of a child under `parent` in `mode`, under `arithmetic` (the runtime's unless a caller passes its own). */
198
+ function modeMatrix(mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone, arithmetic: WorldArithmetic = RUNTIME_ARITHMETIC): M2 {
199
+ const rotation = bone.rotation ?? 0;
200
+ const shearX = bone.shearX ?? 0;
201
+ const shearY = bone.shearY ?? 0;
202
+ const scaleX = bone.scaleX ?? 1;
203
+ const scaleY = bone.scaleY ?? 1;
204
+ const rad = arithmetic.radiansPerDegree;
205
+ const p: M2 = [parent.a, parent.b, parent.c, parent.d];
206
+ switch (mode) {
207
+ case 'normal':
208
+ return times(p, localFrame(arithmetic, rotation, shearX, shearY, scaleX, scaleY));
209
+ case 'onlyTranslation':
210
+ return localFrame(arithmetic, rotation, shearX, shearY, scaleX, scaleY);
211
+ case 'noRotationOrReflection': {
212
+ const xLengthSq = p[0] * p[0] + p[2] * p[2];
213
+ let conformal: M2;
214
+ let r: number;
215
+ if (xLengthSq > COLLAPSED_X_AXIS_SQ) {
216
+ const k = Math.abs(p[0] * p[3] - p[1] * p[2]) / xLengthSq;
217
+ conformal = [p[0], -p[2] * k, p[2], p[0] * k];
218
+ r = rotation - arithmetic.degreesOf(Math.atan2(p[2], p[0]));
219
+ } else {
220
+ // A collapsed x axis (issue #979): the parent's y column with its x component negated, and the rotation less 90 plus the y column's angle — see the header.
221
+ conformal = [0, -p[1], 0, p[3]];
222
+ r = rotation - 90 + arithmetic.degreesOf(Math.atan2(p[3], p[1]));
223
+ }
224
+ // The y angle adds the shear before the right angle here, unlike `frame` (issue #966): `(r + 90 + shearY)` reads last-bit off on 81 of 205 sheared or scaled bones at a 1e9 amplifier, `(r + shearY + 90)` on none; `r = rotation − parentAngle` taken first (adding the shear before the parent's angle is taken off reads 102 of 205 off).
225
+ const xAngle = (r + shearX) * rad;
226
+ const yAngle = (r + shearY + 90) * rad;
227
+ return times(conformal, [Math.cos(xAngle) * scaleX, Math.cos(yAngle) * scaleY, Math.sin(xAngle) * scaleX, Math.sin(yAngle) * scaleY]);
228
+ }
229
+ case 'noScale':
230
+ case 'noScaleOrReflection': {
231
+ const [ux, uy] = noScaleDirection(p, rotation, rad);
232
+ const flip = mode === 'noScale' && p[0] * p[3] - p[1] * p[2] < 0 ? -1 : 1;
233
+ const turned: M2 = [ux, -uy * flip, uy, ux * flip];
234
+ return times(turned, frame(0, shearX, shearY, scaleX, scaleY, rad));
235
+ }
236
+ }
237
+ }
238
+
239
+ /** The fold the rig spec admits: the first letter lower-cased; anything else is not a mode. */
240
+ function modeOf(bone: ModelBone): CoreInheritMode {
241
+ const stated = bone.inheritMode;
242
+ if (stated === undefined) return 'normal';
243
+ const folded = stated.length === 0 ? stated : stated[0].toLowerCase() + stated.slice(1);
244
+ if (folded === 'normal' || folded === 'onlyTranslation' || folded === 'noRotationOrReflection' || folded === 'noScale' || folded === 'noScaleOrReflection') return folded;
245
+ throw new Error(`bone "${bone.name}": inheritMode ${JSON.stringify(stated)} is no mode (readModel refuses it first)`);
246
+ }
247
+
248
+ /**
249
+ * Every bone's setup world transform, parents first as the model lists them.
250
+ *
251
+ * `active`, when given, is the set of bones the applied skins leave active; a
252
+ * bone outside it is not posed. Measured on a hand-written skeleton: the
253
+ * runtime's dump reads an inactive bone as all zeros — matrix and origin, its
254
+ * world as a skeleton is created — and a bone under it, active or not, is
255
+ * posed from those zeros like any child. `inherit` replaces the mode
256
+ * computation — the core suite's plant passes a copy with one mode's sign
257
+ * flipped, and nothing else does. `arithmetic` is the runtime's unless a
258
+ * caller states its own, and only a plant does: `src/transform.ts` takes the
259
+ * default since issue #1021, as every posing path does.
260
+ */
261
+ export function worldTransforms(
262
+ bones: readonly ModelBone[],
263
+ active: ReadonlySet<string> | null = null,
264
+ inherit: InheritComputation = modeMatrix,
265
+ arithmetic: WorldArithmetic = RUNTIME_ARITHMETIC,
266
+ ): Map<string, CoreWorld> {
267
+ const out = new Map<string, CoreWorld>();
268
+ for (const bone of bones) {
269
+ if (active !== null && !active.has(bone.name)) {
270
+ out.set(bone.name, { a: 0, b: 0, c: 0, d: 0, worldX: 0, worldY: 0 });
271
+ continue;
272
+ }
273
+ const x = bone.x ?? 0;
274
+ const y = bone.y ?? 0;
275
+ if (bone.parent === undefined) {
276
+ const [a, b, c, d] = localFrame(arithmetic, bone.rotation ?? 0, bone.shearX ?? 0, bone.shearY ?? 0, bone.scaleX ?? 1, bone.scaleY ?? 1);
277
+ out.set(bone.name, { a, b, c, d, worldX: x, worldY: y });
278
+ continue;
279
+ }
280
+ const parent = out.get(bone.parent);
281
+ if (parent === undefined) throw new Error(`bone "${bone.name}": parent "${bone.parent}" is not posed before it`);
282
+ const [a, b, c, d] = inherit(modeOf(bone), parent, bone, arithmetic);
283
+ out.set(bone.name, { a, b, c, d, worldX: parent.a * x + parent.b * y + parent.worldX, worldY: parent.c * x + parent.d * y + parent.worldY });
284
+ }
285
+ return out;
286
+ }
287
+
288
+ /** The mode computation `worldTransforms` uses, exported so a control can wrap it. */
289
+ export { modeMatrix };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The two spellings of a bone correspondence — what `rigc bonedist --bones`
3
+ * and `rigc bench --bones` take — moved here unchanged from `./bonedist.ts`
4
+ * (issue #1052), which re-exports both.
5
+ *
6
+ * Their own module because the CLI's help states them (`--bones`'s flag row)
7
+ * and an entry that links nothing of the runtime prints that help, while
8
+ * `./bonedist.ts` poses both skeletons through spine-core by design.
9
+ */
10
+
11
+ /** The sidecar spec a correspondence file declares, and the report's own. */
12
+ export const BONEDIST_SPEC = 'rigc-bonedist/1';
13
+
14
+ /** What `--bones identity` is spelled as, where a file path would go. */
15
+ export const IDENTITY_CORRESPONDENCE = 'identity';
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The deform survey of a build, through the reader and poser asked for —
3
+ * `explain`'s `DEFORM` block and `tools/survey_hashes.ts` (issues #969,
4
+ * #1019).
5
+ *
6
+ * Moved here unchanged from `./deformmeasure.ts` (issue #1052, step 4e of
7
+ * #380), which re-exports both names: the choice between the model document
8
+ * posed by the core and the Spine skeleton posed by spine-core names no
9
+ * runtime class, and `explain` makes it on a rigc build without touching the
10
+ * runtime — so an entry that links nothing of spine-core has to be able to
11
+ * load it. The spine-core half (`throughSpine`) is `./deformmeasure.ts`'s,
12
+ * reached through the seam (`./spine_side.ts`); where nothing registered it,
13
+ * a survey that needs it is refused by name (`SpineRuntimeError`).
14
+ */
15
+ import { CoreInputError, readModel } from './core/index.ts';
16
+ import { surveyOfModel, type DeformSurvey, type DeformSurveySource } from './deformsurvey.ts';
17
+ import { spineFileSha256 } from './model.ts';
18
+ import { spineSurveyFor } from './spine_side.ts';
19
+
20
+ /** A build's three texts, as `explain` and `tools/survey_hashes.ts` hold them. */
21
+ export interface DeformSurveyInput {
22
+ skeletonText: string;
23
+ atlasText: string;
24
+ /** The model document (`skeleton.model.json`), or `null` when the input carries none (a Spine export). */
25
+ modelText: string | null;
26
+ /** What a refusal names the survey's input as; `the deform survey` unstated. */
27
+ label?: string;
28
+ }
29
+
30
+ /**
31
+ * The survey of a build, through the reader and poser asked for (issues #969,
32
+ * #1019): `model` — the model document's structure posed by the core,
33
+ * refused when there is none or when the Spine file beside it is not the one
34
+ * it records; `spine-core` — the Spine skeleton read and posed by the runtime;
35
+ * `auto` — the model document when the input carries one and the core poses
36
+ * it, spine-core otherwise, the reason named in `source.why`. On the model
37
+ * path spine-core is not touched: the Spine text is hashed, never parsed.
38
+ */
39
+ export function surveyOfBuild(input: DeformSurveyInput, exempt: ReadonlySet<string>, asked: 'auto' | DeformSurveySource): DeformSurvey {
40
+ const label = input.label ?? 'the deform survey';
41
+ // `./deformmeasure.ts`'s reader and poser, through the seam: it touches the runtime first and refuses by name where it cannot be used.
42
+ const throughSpine = (why: string | null): DeformSurvey => spineSurveyFor(label, why ?? '--poser spine').throughSpine(label, why, input, exempt);
43
+ if (asked === 'spine-core') return throughSpine(null);
44
+ if (input.modelText === null) {
45
+ if (asked === 'model') throw new CoreInputError('the survey was asked to pose the model document, and the input carries none');
46
+ return throughSpine('the input carries no model document (skeleton.model.json), so the survey posed the Spine skeleton through spine-core');
47
+ }
48
+ try {
49
+ const doc = readModel(input.modelText);
50
+ // The document reads the rig it was built with; the Spine file beside it must be that build's (issue #968's rule).
51
+ const found = spineFileSha256(input.skeletonText);
52
+ if (found !== doc.spine.sha256) {
53
+ throw new CoreInputError(`the skeleton is not the one the model document was written beside: its sha256 is ${found}, the document records ${doc.spine.sha256 || 'none'}`);
54
+ }
55
+ return surveyOfModel(doc, exempt);
56
+ } catch (err) {
57
+ if (!(err instanceof CoreInputError) || asked === 'model') throw err;
58
+ return throughSpine(`the core refused to pose the model document — ${err.message}`);
59
+ }
60
+ }