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,364 @@
1
+ /**
2
+ * A group track's **per-member** values, evaluated from a model the spec states.
3
+ *
4
+ * ## Why this module exists
5
+ *
6
+ * `groups` keys several bones **identically**, which is the right tool for a
7
+ * wheel pair and the wrong one for a face: on a face the whole content of the
8
+ * motion is that each part gets a *different* number. `gallery/portrait`'s held
9
+ * 12° yaw was **20 tracks**, sixteen of them the same two properties on six
10
+ * sibling bones with identical times, identical easings and six different
11
+ * values — so `groups` bought exactly one of the sixteen, the pair that happened
12
+ * to share `cos t` (issue #295).
13
+ *
14
+ * And not one of those numbers was a judgement either. Each feature's own
15
+ * `translatex` is `x·(cos t − 1) − (z − R)·sin t` and its `scalex` is
16
+ * `cos(α − t)/cos α` — docs/FACE.md §3 and §5 derive both, and the *only* free
17
+ * parameter per member is its **depth**. So this is `src/deformgen.ts`'s move on
18
+ * the bone half of the same turn: **the spec states the model and the compiler
19
+ * states the numbers.**
20
+ *
21
+ * ⭐ **The depth table is the point, not the line count.** FACE §2's sharp edge
22
+ * is that `x` is in the file and `z` is not — `grep -c '"z"'` over the worked
23
+ * example's two specs returned `0` and `0`, and every depth that produced every
24
+ * number lived only in a README beside them. A `derive` key states each
25
+ * member's depth, so the table a reader needs in order to check a sign is in the
26
+ * file that uses it.
27
+ *
28
+ * ## What this is NOT, and the rules that keep it that way
29
+ *
30
+ * 1. **It never authors.** Every parameter arrives from the spec — the angle,
31
+ * each member's depth, what the parent already carries. The one number read
32
+ * off the rig is the member bone's own **setup position**, which is the same
33
+ * licence `evaluateDeformTransform` has to read an attachment's setup
34
+ * vertices: it is a coordinate the spec already states, resolved by name.
35
+ * 2. **It generates no in-betweens.** MOTION §7 refuses a `rigc tween`. A model
36
+ * is evaluated **at one key**, from parameters that key states, and what
37
+ * happens between two keys is still the timeline's own curve. Sweeping an
38
+ * angle is editing one number per key.
39
+ * 3. **It does not do timing.** `stagger` already shifts a member's keys in
40
+ * member order (AUTHORING §4.3), so nothing here takes a phase, an index or
41
+ * a delay. Two mechanisms for one lag would mean two places to look for it.
42
+ * 4. **It is one closed form per kind, named and refusable.** There is no
43
+ * expression language here and no `v` that is a formula string: a kind the
44
+ * compiler does not know is refused *by name*, which is the whole reason a
45
+ * reviewer can check a claim.
46
+ *
47
+ * ## Determinism
48
+ *
49
+ * Each closed form is a fixed sequence of float64 operations over numbers read
50
+ * from the spec, evaluated in **member order** — the group's own array order,
51
+ * never an iteration over an unordered set. The caller quantises with the
52
+ * compiler's `onModelGrid` — the model's 1e-6 resolution, then the float32 name
53
+ * every emitted number takes — so the same spec emits the same bytes and
54
+ * `A18_DETERMINISTIC_EMIT` proves it on a second independent compile.
55
+ */
56
+
57
+ import { CompileError } from './errors.ts';
58
+
59
+ /** The kinds this module evaluates, in the order the docs list them. */
60
+ export const TRACK_DERIVE_KINDS = ['yaw', 'pitch'] as const;
61
+
62
+ export type TrackDeriveKind = (typeof TRACK_DERIVE_KINDS)[number];
63
+
64
+ /**
65
+ * A **2.5D turn**, read per member instead of per vertex.
66
+ *
67
+ * The members are treated as rigid parts sitting on a surface that turns about
68
+ * one axis, and the key is that rotation projected back onto the screen. `yaw`
69
+ * stands the axis vertically and reads each member's setup `x`; `pitch` lays it
70
+ * horizontally and reads `y`. docs/FACE.md §3 is the displacement's derivation
71
+ * and §5 the foreshortening's.
72
+ *
73
+ * ⚠️ **`depth` is a decision, not a measurement.** FACE §2: a depth is not
74
+ * readable off the art — it is a statement about a shape the drawing only
75
+ * implies. Derive its *sign* from the draw order and argue only about the size.
76
+ */
77
+ export interface TrackDeriveTurn {
78
+ kind: TrackDeriveKind;
79
+ /** The turn, in degrees. Positive yaws toward −x, which is FACE §1's sign. */
80
+ degrees: number;
81
+ /**
82
+ * Each member's depth: `{ member: z }` on a group track, one number on a bone
83
+ * track. `z` runs **toward the viewer** (FACE §1), so a **larger** depth is
84
+ * **nearer** — a nose in front of the skull surface takes a bigger number than
85
+ * the socket beside it — and a **negative** depth is behind the axis, which is
86
+ * what makes the back of a head swing the other way (FACE §2).
87
+ *
88
+ * ⚠️ That sign is the one thing here no assertion can check, and this comment
89
+ * had it backwards until issue #350's neighbour
90
+ * ([#351](https://github.com/firejune/rigc/issues/351)). The closed form is
91
+ * the arbiter: `d = (x−about)·(cos t − 1) − (depth − carried)·sin t` gives a
92
+ * part with `depth > carried` a **negative** residual, and FACE §3 makes
93
+ * exactly that the nose diagnostic — *if the nose's residual is not negative,
94
+ * the depths are wrong*. That only holds if a larger depth means nearer.
95
+ */
96
+ depth: number | Record<string, number>;
97
+ /**
98
+ * The depth whose shift a **parent bone already applies**. Default 0.
99
+ *
100
+ * ⭐ This is FACE §3's shared-shift split, stated. Put a bone at the plate's
101
+ * own origin, key `−carried·sin t` there, and each member then keys only what
102
+ * is left: `x·(cos t − 1) + (carried − z)·sin t`. A residual is 1–6 units
103
+ * where a total is 30–40, and **that is an auditing decision before it is a
104
+ * rigging one** — nobody can eyeball a wrong sign in the second and everybody
105
+ * can in the first.
106
+ *
107
+ * 0 means the parent carries nothing, which is the honest reading for a part
108
+ * hanging off the head itself rather than off the shared-shift bone. It is
109
+ * deliberately not named for the surface it usually is: `surface: 0` would
110
+ * claim a skull surface at depth 0, and what the number means is *what has
111
+ * already been applied*.
112
+ */
113
+ carried?: number;
114
+ /** Where the axis crosses the driving coordinate, in the members' shared parent's space. Default 0. */
115
+ about?: number;
116
+ }
117
+
118
+ export type TrackDerive = TrackDeriveTurn;
119
+
120
+ /**
121
+ * Which bone property each kind projects onto, and which setup coordinate it
122
+ * reads.
123
+ *
124
+ * ⭐ **The property picks the projection.** A turn does two things to a part
125
+ * sitting on a curved surface — it moves it, and it narrows it — and those are
126
+ * two Spine timelines rather than two models. So the track's `property`, which
127
+ * the author has already stated, says which half of the same turn this key is;
128
+ * a property the kind has no projection onto is refused by name rather than
129
+ * quietly driven by the wrong half.
130
+ */
131
+ export const TRACK_DERIVE_PROJECTIONS: Record<
132
+ TrackDeriveKind,
133
+ { coordinate: 'x' | 'y'; shift: string; foreshorten: string }
134
+ > = {
135
+ yaw: { coordinate: 'x', shift: 'translatex', foreshorten: 'scalex' },
136
+ pitch: { coordinate: 'y', shift: 'translatey', foreshorten: 'scaley' },
137
+ };
138
+
139
+ /** One member, as the compiler resolved it against the rig. */
140
+ export interface TrackDeriveMember {
141
+ /** The bone's name. */
142
+ name: string;
143
+ /** Its setup coordinate on the kind's driving axis, in its parent's space. */
144
+ at: number;
145
+ }
146
+
147
+ /** One evaluated member row, for `explain` to print and a reviewer to check. */
148
+ export interface TrackDeriveMemberValue {
149
+ member: string;
150
+ /** The setup coordinate read off the rig, already relative to `about`. */
151
+ at: number;
152
+ /** The depth the spec stated for this member. */
153
+ depth: number;
154
+ /** The emitted value, quantised by the caller's rounder. */
155
+ value: number;
156
+ }
157
+
158
+ /**
159
+ * What one evaluation did.
160
+ *
161
+ * `stated` is the spec's own parameters, `derived` the scalars the closed form
162
+ * got out of them, and `members` the per-member rows in member order — the
163
+ * three things that let somebody re-derive a row by hand. The values are the
164
+ * **emitted** ones, so the report and the artifact cannot disagree (the same
165
+ * rule `DeformTransformReport` holds to, and issue #319's `DEFORM` block reads
166
+ * that report rather than recomputing it).
167
+ */
168
+ export interface TrackDeriveReport {
169
+ kind: TrackDeriveKind;
170
+ /** Which half of the turn this key is: the displacement or the foreshortening. */
171
+ projection: 'shift' | 'foreshorten';
172
+ /** The model as the spec states it. */
173
+ stated: string;
174
+ /** The closed form, written out. */
175
+ formula: string;
176
+ /** The scalars the closed form derived, e.g. `sin t = 0.207912`. */
177
+ derived: string[];
178
+ members: TrackDeriveMemberValue[];
179
+ }
180
+
181
+ /** The compiler's `onModelGrid` (1e-6, then float32), passed in so the quantiser stays in one place. */
182
+ export type Rounder = (n: number) => number;
183
+
184
+ /**
185
+ * Evaluate one `derive` over a track's members.
186
+ *
187
+ * `members` arrive in the group's own declared order, each with the setup
188
+ * coordinate the kind reads. The caller owns resolving those against the rig and
189
+ * refusing the cases where they do not mean one thing — by the time execution
190
+ * reaches here every `at` is measured from the same origin.
191
+ *
192
+ * Returns one value per member, in the same order, plus the report.
193
+ */
194
+ export function evaluateTrackDerive(
195
+ derive: TrackDerive,
196
+ property: string,
197
+ members: readonly TrackDeriveMember[],
198
+ round: Rounder,
199
+ where: string,
200
+ ): TrackDeriveReport {
201
+ if (derive === null || typeof derive !== 'object' || Array.isArray(derive)) {
202
+ throw new CompileError(`${where}: "derive" is ${JSON.stringify(derive)}; it is an object with a "kind"`);
203
+ }
204
+ const kind = (derive as { kind?: unknown }).kind;
205
+ if (typeof kind !== 'string' || !(TRACK_DERIVE_KINDS as readonly string[]).includes(kind)) {
206
+ throw new CompileError(
207
+ `${where}: "derive" has kind ${JSON.stringify(kind)}; this spec evaluates ${TRACK_DERIVE_KINDS.join(', ')}. ` +
208
+ 'A model that is none of those is still authorable as a per-member "v" map, or as one track per member.',
209
+ );
210
+ }
211
+ const turn = derive as TrackDeriveTurn;
212
+ const projections = TRACK_DERIVE_PROJECTIONS[kind as TrackDeriveKind];
213
+ const projection: 'shift' | 'foreshorten' =
214
+ property === projections.shift ? 'shift' : property === projections.foreshorten ? 'foreshorten' : refuseProperty(kind, property, where);
215
+
216
+ const degrees = num(turn.degrees, 'degrees', where);
217
+ const about = turn.about === undefined ? 0 : num(turn.about, 'about', where);
218
+ // `carried` is subtracted from a depth, and the foreshortening does not read a
219
+ // depth difference at all — it reads the member's own angle off the axis. So a
220
+ // `carried` here changes nothing, and a parameter that changes nothing is a
221
+ // reader's false lead about which model produced the numbers.
222
+ if (projection === 'foreshorten' && turn.carried !== undefined) {
223
+ throw new CompileError(
224
+ `${where}: derive ${kind} states carried=${JSON.stringify(turn.carried)} on a "${property}" track, and the ` +
225
+ 'foreshortening reads no depth difference — it is cos(α − t)/cos α, where α is the member\'s own angle off the ' +
226
+ `axis. "carried" belongs on the "${projections.shift}" track, whose shared shift is the thing it names.`,
227
+ );
228
+ }
229
+ const carried = turn.carried === undefined ? 0 : num(turn.carried, 'carried', where);
230
+
231
+ const rad = (degrees * Math.PI) / 180;
232
+ const cosMinus1 = Math.cos(rad) - 1;
233
+ const sin = Math.sin(rad);
234
+
235
+ const values: TrackDeriveMemberValue[] = [];
236
+ for (const member of members) {
237
+ const depth = depthOf(turn.depth, member.name, members, where, kind);
238
+ const u = member.at - about;
239
+ if (projection === 'shift') {
240
+ values.push({ member: member.name, at: u, depth, value: round(u * cosMinus1 - (depth - carried) * sin) });
241
+ continue;
242
+ }
243
+ // A part behind the axis has no patch of front surface to foreshorten, and
244
+ // `atan2(u, z)` there is an angle measured round the back — so the closed
245
+ // form would silently evaluate a different model. Refused by name, which is
246
+ // also FACE §2's "derive the sign from the draw order" as a check.
247
+ if (depth <= 0) {
248
+ throw new CompileError(
249
+ `${where}: derive ${kind} projects onto "${property}" and member "${member.name}" states depth ${depth}. ` +
250
+ 'Foreshortening is cos(α − t)/cos α with α = atan2(coordinate, depth), which needs the part to be IN FRONT ' +
251
+ 'of the axis; at or behind it there is no front surface to narrow. A part behind the axis still takes the ' +
252
+ `"${projections.shift}" projection, where a negative depth is exactly what swings it the other way.`,
253
+ );
254
+ }
255
+ const alpha = Math.atan2(u, depth);
256
+ const scale = Math.cos(alpha - rad) / Math.cos(alpha);
257
+ if (scale <= 0) {
258
+ throw new CompileError(
259
+ `${where}: derive ${kind} turns member "${member.name}" (${projections.coordinate}=${u}, depth=${depth}) past ` +
260
+ `its own edge: α = ${round(alpha)} rad and the turn is ${round(rad)} rad, so cos(α − t) is ` +
261
+ `${round(Math.cos(alpha - rad))} and the scale would be ${round(scale)}. A scale at or below 0 mirrors the ` +
262
+ 'drawing rather than narrowing it. The part has turned edge-on — the turn is past what this construction ' +
263
+ 'carries, and FACE §8 is the page that picks a construction from the angle.',
264
+ );
265
+ }
266
+ values.push({ member: member.name, at: u, depth, value: round(scale) });
267
+ }
268
+
269
+ const c = projections.coordinate;
270
+ const stated =
271
+ `degrees=${degrees}` +
272
+ (projection === 'shift' && turn.carried !== undefined ? ` carried=${carried}` : '') +
273
+ (turn.about === undefined ? '' : ` about=${about}`);
274
+ const formula =
275
+ projection === 'shift'
276
+ ? `d${c} = (${c}−about)·(cos t − 1) − (depth − carried)·sin t`
277
+ : `scale${c.toUpperCase()} = cos(α − t) / cos α, α = atan2(${c}−about, depth)`;
278
+ const derived =
279
+ projection === 'shift'
280
+ ? [
281
+ `t = ${round(rad)} rad`,
282
+ `cos t − 1 = ${round(cosMinus1)}`,
283
+ `sin t = ${round(sin)}`,
284
+ `shift the parent carries = −carried·sin t = ${round(-carried * sin)}`,
285
+ ]
286
+ : [`t = ${round(rad)} rad`, `cos t = ${round(Math.cos(rad))}, which is the value on the axis (α = 0)`];
287
+
288
+ return { kind: kind as TrackDeriveKind, projection, stated, formula, derived, members: values };
289
+ }
290
+
291
+ /**
292
+ * The property refusal, as its own function so the message can list both
293
+ * projections rather than only the one that missed.
294
+ */
295
+ function refuseProperty(kind: string, property: string, where: string): never {
296
+ const p = TRACK_DERIVE_PROJECTIONS[kind as TrackDeriveKind];
297
+ throw new CompileError(
298
+ `${where}: derive ${kind} has no projection onto "${property}". It projects onto "${p.shift}" (the displacement, ` +
299
+ `FACE §3) and "${p.foreshorten}" (the narrowing, FACE §5), and onto nothing else — a turn about one axis moves ` +
300
+ `nothing along it, so a paired "translate"/"scale" would need a second channel this model does not state. ` +
301
+ 'State the axis, or write the values as a per-member "v" map.',
302
+ );
303
+ }
304
+
305
+ /**
306
+ * One member's depth, from either shape of the field.
307
+ *
308
+ * A group track's `depth` is a map and a bone track's is one number, and each
309
+ * refuses the other's shape: a map on a bone track names members that track has
310
+ * no way to reach, and a number on a group track is the shared value `groups`
311
+ * already keys without any of this (issue #295's own starting point).
312
+ */
313
+ function depthOf(
314
+ depth: unknown,
315
+ member: string,
316
+ members: readonly TrackDeriveMember[],
317
+ where: string,
318
+ kind: string,
319
+ ): number {
320
+ if (typeof depth === 'number') {
321
+ if (members.length !== 1) {
322
+ throw new CompileError(
323
+ `${where}: derive ${kind} states one depth ${depth} for ${members.length} members ` +
324
+ `(${members.map((m) => m.name).join(', ')}). One number for every member is the shared value a plain "v" ` +
325
+ 'already keys — state a depth per member, as { "member": z, … }.',
326
+ );
327
+ }
328
+ return num(depth, 'depth', where);
329
+ }
330
+ if (depth === null || typeof depth !== 'object' || Array.isArray(depth)) {
331
+ throw new CompileError(
332
+ `${where}: derive ${kind} has depth ${JSON.stringify(depth)}; it is a number on a bone track and ` +
333
+ '{ "member": z, … } on a group track',
334
+ );
335
+ }
336
+ const table = depth as Record<string, unknown>;
337
+ const stated = Object.keys(table);
338
+ const known = new Set(members.map((m) => m.name));
339
+ for (const name of stated) {
340
+ if (!known.has(name)) {
341
+ throw new CompileError(
342
+ `${where}: derive ${kind} states a depth for "${name}", which this track does not key ` +
343
+ `(its members are: ${members.map((m) => m.name).join(', ')})`,
344
+ );
345
+ }
346
+ }
347
+ if (!(member in table)) {
348
+ throw new CompileError(
349
+ `${where}: derive ${kind} states no depth for member "${member}" ` +
350
+ `(it states: ${stated.length ? stated.join(', ') : 'nothing'}). ` +
351
+ 'A depth is refused rather than defaulted: a member silently at depth 0 would be keyed with a DIFFERENT ' +
352
+ 'model from the ones beside it, and that is exactly the error a table of six is written to make visible.',
353
+ );
354
+ }
355
+ return num(table[member], `depth.${member}`, where);
356
+ }
357
+
358
+ /** One required finite number, refused by field name rather than compiled as a NaN. */
359
+ function num(value: unknown, field: string, where: string): number {
360
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
361
+ throw new CompileError(`${where}: derive field "${field}" is ${JSON.stringify(value)}; it is a finite number the spec has to state`);
362
+ }
363
+ return value;
364
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Setup-pose world transforms, and their inverses.
3
+ *
4
+ * The mesh tier used to get away with "bind = world - bone origin", and the
5
+ * compiler asserted the precondition it needed: every bone translation-only. The
6
+ * joint archetype breaks that on purpose — `axis` carries the cut's angle and the
7
+ * grips carry their radial facing — so the shortcut has to become a real inverse.
8
+ *
9
+ * ⚠️ A rotated parent would NOT have failed loudly under the old shortcut. It
10
+ * would have sheared every weighted vertex at setup and looked like a badly
11
+ * measured polygon. That is why the old code refused rotation instead of ignoring
12
+ * it, and why this file exists rather than a relaxed assertion.
13
+ *
14
+ * The composition, for a bone with a parent in the `normal` mode:
15
+ *
16
+ * la = cos(rot) lb = cos(rot + 90) = -sin(rot)
17
+ * lc = sin(rot) ld = sin(rot + 90) = cos(rot)
18
+ * a = pa*la + pb*lc b = pa*lb + pb*ld
19
+ * c = pc*la + pd*lc d = pc*lb + pd*ld
20
+ * worldX = pa*x + pb*y + parent.worldX
21
+ *
22
+ * ⚠️ **Scale, shear and `inherit` are honoured, and that is a correction** (issue
23
+ * #804). This header said *"scale and shear are not emitted by rigc, so they are
24
+ * fixed at 1 and 0 here"* long after the rig spec gained `scaleX`/`scaleY`/
25
+ * `shearX`/`shearY`/`inherit` and `ingest` began carrying them from every export
26
+ * that states them — so every setup measurement over a scaled bone was taken on
27
+ * a skeleton the runtime never builds. A 50/50-weighted probe path over one bone
28
+ * at scale 2 measured **0.752×** the runtime's own `PathConstraint.curves` on the
29
+ * build's posed geometry, and a production rig's weighted path 2.35×. The
30
+ * weights were blended all along; the matrices they were blended through were
31
+ * wrong.
32
+ *
33
+ * ⭐ **`computeWorldTransforms` is the core's evaluator** (`worldTransforms` in
34
+ * [`src/core/world.ts`](core/world.ts), issue #1015), whose every rule — the five
35
+ * inherit modes, the collapsed parent axis, the operation orders — was measured
36
+ * against the runtime's pose dump and is held there by the core suite. Since
37
+ * issue #1021 it runs under the runtime's own arithmetic (`RUNTIME_ARITHMETIC`,
38
+ * the evaluator's default), so the matrices every point is bound through are the
39
+ * ones the runtime poses the build with, to the bit. This file keeps what is the
40
+ * compiler's own: the coordinate contract, the refusals by name, the world
41
+ * rotation a region cancels, and the inverses.
42
+ *
43
+ * ⚠️ **Why the runtime's arithmetic and not the textbook's** (issue #1021). A
44
+ * point is bound by inverting a setup matrix; the runtime then poses it through
45
+ * its own. The two used to differ in the last bits — `Math.PI` against the
46
+ * runtime's 3.1415927, `[cos, −sin, sin, cos]` against the y column taken at
47
+ * `rotation + 90`, radians to degrees by division against multiplication — and
48
+ * that is not noise below the file's precision: an unrotated root reads
49
+ * `b = −2.3e-8` in the runtime, so a point 256 units above its bone was bound
50
+ * 5.9e-6 off before the float32 spelling was even applied. Measured from inside
51
+ * `compile` (every authored world position `toBoneLocal` was handed) against
52
+ * spine-core's pose of the emitted build, the old arithmetic put `gallery/look`'s
53
+ * 312 bound points up to 8.6e-5 from where they were authored (RMS 5.1e-5) and
54
+ * `gallery/flex`'s 258 up to 2.3e-5; the runtime's puts every one of them at the
55
+ * float32 floor — the distance the float32 spelling of the exact inverse alone
56
+ * accounts for (look 8.1e-6, flex 4.1e-6). `CO24` holds it.
57
+ *
58
+ * 🔸 **The rule has a second half: what is MEASURED reads the exact frame.**
59
+ * The runtime's arithmetic is for what is bound — the matrices a point is
60
+ * inverted through. Anything the compiler measures of the authored rig and
61
+ * reports, compares against a threshold, sorts by or refuses on is a statement
62
+ * about the spec, and reads `computeExactFrameTransforms` (below): a depth turn
63
+ * ceiling, a ring's control angles, a `segments` mesh's segments and falloff.
64
+ * An angle the author stated as 90 has to print as 90.
65
+ */
66
+ import type { ModelBone } from './model.ts';
67
+ import { modeMatrix, RUNTIME_ARITHMETIC, worldTransforms, type CoreWorld, type WorldArithmetic } from './core/world.ts';
68
+
69
+ /**
70
+ * What `computeWorldTransforms` reads off a bone — a structural type, so both a
71
+ * Spine bone (`SpineBone`, what spine-parts and every skeleton reader hold) and
72
+ * a compiled-model bone (`ModelBone`, what `compile` holds) satisfy it as they
73
+ * are, without a cast.
74
+ *
75
+ * The two differ in one key, the inherit mode: Spine 4.3 spells it `inherit`
76
+ * and the model `inheritMode`. The union below admits exactly one of the two on
77
+ * any one bone (the other is `never`), so a bone carrying both is a type error
78
+ * rather than a question of which one wins, and the mode is read as
79
+ * `inheritMode ?? inherit` — whichever the bone has.
80
+ */
81
+ export type PosableBone = {
82
+ name: string;
83
+ parent?: string;
84
+ x?: number;
85
+ y?: number;
86
+ rotation?: number;
87
+ scaleX?: number;
88
+ scaleY?: number;
89
+ shearX?: number;
90
+ shearY?: number;
91
+ } & ({ inherit?: string; inheritMode?: never } | { inheritMode?: string; inherit?: never });
92
+
93
+ export interface BoneTransform {
94
+ a: number;
95
+ b: number;
96
+ c: number;
97
+ d: number;
98
+ worldX: number;
99
+ worldY: number;
100
+ /** World rotation in degrees, CCW, y up. What a region attachment cancels. */
101
+ worldRotation: number;
102
+ }
103
+
104
+ export class TransformError extends Error {}
105
+
106
+ /**
107
+ * Crop pixels (y down, origin top-left) -> Spine world (y up, origin at the
108
+ * bottom-left of the crop).
109
+ *
110
+ * This is the whole coordinate contract, and it is also the viewer's: the
111
+ * renderer's root element sits at the BOTTOM-left of the stage because
112
+ * spine-html negates world Y (Spine is Y-up, CSS is Y-down).
113
+ *
114
+ * It lived in `src/archetype.ts` until the archetype tables left the code, which
115
+ * was the wrong home for it anyway — it is not a property of any formation.
116
+ */
117
+ export function cropToSpineY(cropY: number, cropHeight: number): number {
118
+ return cropHeight - cropY;
119
+ }
120
+
121
+ /**
122
+ * The textbook's arithmetic — `Math.PI`, a bone with the default scale and no
123
+ * shear framed as `[cos, −sin, sin, cos]`, radians to degrees by dividing by
124
+ * the degree factor — in which an unrotated bone's frame is the identity
125
+ * exactly. Every point the compiler binds goes through the runtime's instead
126
+ * (the header's ⚠️); this is what `computeExactFrameTransforms` evaluates
127
+ * under, and nothing else.
128
+ */
129
+ const EXACT_FRAME_ARITHMETIC: WorldArithmetic = {
130
+ radiansPerDegree: Math.PI / 180,
131
+ degreesOf: (radians) => radians / (Math.PI / 180),
132
+ rotationOnlyFrame: true,
133
+ };
134
+
135
+ /**
136
+ * World transform of every bone, in declaration order.
137
+ *
138
+ * Bones must be declared parents-first, which is also what `SkeletonJson`
139
+ * requires (it resolves `parent` by name against the bones already read), so a
140
+ * violation here is a violation there.
141
+ *
142
+ * The matrices and origins are the core's (`worldTransforms`, under the
143
+ * runtime's arithmetic — the header's ⚠️). What is decided here is what the core does not say:
144
+ * an undeclared parent and an `inherit` no mode answers to are refused by name
145
+ * as `TransformError`, a bone whose `parent` is empty is a root as it always was
146
+ * here, and `worldRotation` is read off the matrix — a root with the default
147
+ * scale and no shear keeps its stated rotation, unwrapped. The angle is turned
148
+ * into degrees as the runtime turns one (`RUNTIME_DEG`, by multiplying), so
149
+ * the rotation a region writes to cancel it is read back by the runtime as
150
+ * that angle.
151
+ */
152
+ export function computeWorldTransforms(bones: readonly PosableBone[]): Map<string, BoneTransform> {
153
+ return evaluate(bones, RUNTIME_ARITHMETIC);
154
+ }
155
+
156
+ /**
157
+ * The same bones under `EXACT_FRAME_ARITHMETIC`: the frame the compiler
158
+ * MEASURES the authored rig in, and binds nothing through (issue #1021) — a
159
+ * mesh's depth turn ceiling (`sampleMeshDepth` in `compile.ts`), a ring's
160
+ * control angles (`ringControlAngles`, which orders the split and refuses a
161
+ * tie by printing the angle), and a `segments` mesh's segments, the distances
162
+ * its falloff and `minWeight` read and its shared-origin refusal. It differs from
163
+ * `computeWorldTransforms` only in the last bits, and that is exactly why it is
164
+ * kept (issue #1021): the ceiling is a statement about the authored mesh, and
165
+ * its reader treats an axis whose area term is exactly zero as one that cannot
166
+ * fold. The runtime frames an unrotated bone with `b = −2.3e-8`, so in its
167
+ * bind space a sheet whose depth rises linearly in x — which cannot fold a
168
+ * pitch at any angle — reads a pitch ceiling of 89.99997°, and `gallery/look`'s
169
+ * two hair locks gained four such ceilings with their counts (`TC03`, `TC06`
170
+ * went red on it). A control angle read through the runtime's frame printed
171
+ * an authored 90 as 89.99999734089101 in its tie refusal (`RF46`). On the 19
172
+ * recipes the only bytes this keeps from moving are look's ceilings; no
173
+ * emitted position goes through it.
174
+ */
175
+ export function computeExactFrameTransforms(bones: readonly PosableBone[]): Map<string, BoneTransform> {
176
+ return evaluate(bones, EXACT_FRAME_ARITHMETIC);
177
+ }
178
+
179
+ function evaluate(bones: readonly PosableBone[], arithmetic: WorldArithmetic): Map<string, BoneTransform> {
180
+ const model: ModelBone[] = [];
181
+ const declared = new Set<string>();
182
+ for (const bone of bones) {
183
+ const parent = bone.parent ? bone.parent : undefined;
184
+ if (parent !== undefined && !declared.has(parent)) {
185
+ throw new TransformError(`bone "${bone.name}" names parent "${parent}", which is not declared before it`);
186
+ }
187
+ model.push({
188
+ name: bone.name,
189
+ parent,
190
+ x: bone.x,
191
+ y: bone.y,
192
+ rotation: bone.rotation,
193
+ scaleX: bone.scaleX,
194
+ scaleY: bone.scaleY,
195
+ shearX: bone.shearX,
196
+ shearY: bone.shearY,
197
+ inheritMode: parent === undefined ? undefined : inheritMode(bone),
198
+ });
199
+ declared.add(bone.name);
200
+ }
201
+ const worlds = worldTransforms(model, null, modeMatrix, arithmetic);
202
+ const out = new Map<string, BoneTransform>();
203
+ for (const bone of model) {
204
+ const w = worlds.get(bone.name) as CoreWorld;
205
+ const plain = (bone.scaleX ?? 1) === 1 && (bone.scaleY ?? 1) === 1 && (bone.shearX ?? 0) === 0 && (bone.shearY ?? 0) === 0;
206
+ const worldRotation = bone.parent === undefined && plain ? (bone.rotation ?? 0) : (arithmetic.degreesOf(Math.atan2(w.c, w.a)) + 360) % 360;
207
+ out.set(bone.name, { a: w.a, b: w.b, c: w.c, d: w.d, worldX: w.worldX, worldY: w.worldY, worldRotation });
208
+ }
209
+ return out;
210
+ }
211
+
212
+ type InheritMode = 'normal' | 'onlyTranslation' | 'noRotationOrReflection' | 'noScale' | 'noScaleOrReflection';
213
+
214
+ const INHERIT_MODES: readonly InheritMode[] = ['normal', 'onlyTranslation', 'noRotationOrReflection', 'noScale', 'noScaleOrReflection'];
215
+
216
+ /**
217
+ * A bone's `inherit`, folded the way `Utils.enumValue` folds it — the first
218
+ * letter and nothing else. A spelling the runtime cannot resolve is refused by
219
+ * name rather than read as normal: the runtime's switch matches no case and
220
+ * leaves the matrix where it was, and the rig spec's parse refuses that
221
+ * spelling first (issue #733), so reaching this throw is rigc's own defect.
222
+ */
223
+ function inheritMode(bone: PosableBone): InheritMode {
224
+ const value = bone.inheritMode ?? bone.inherit;
225
+ if (value === undefined) return 'normal';
226
+ const folded = value.length === 0 ? value : value[0].toLowerCase() + value.slice(1);
227
+ const mode = INHERIT_MODES.find((m) => m === folded);
228
+ if (!mode) {
229
+ throw new TransformError(`bone "${bone.name}" inherit is ${JSON.stringify(value)}, which the runtime resolves to no mode`);
230
+ }
231
+ return mode;
232
+ }
233
+
234
+ /**
235
+ * World point -> the bone's local space. Used for bind coordinates and offsets.
236
+ *
237
+ * ⚠️ **Not rounded**, since issue #716. It used to round to six decimals here,
238
+ * which made this file a second emitter: `buildBone` and the region placement
239
+ * wrote its result straight into the skeleton. The emitted number's precision
240
+ * is one decision, `f32` in `compile.ts`, so the callers quantise what they
241
+ * emit and this returns the double.
242
+ */
243
+ export function toBoneLocal(m: BoneTransform, worldX: number, worldY: number): [number, number] {
244
+ const det = m.a * m.d - m.b * m.c;
245
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-9) {
246
+ throw new TransformError(`bone transform is singular (det ${det}); a zero-scale bone cannot hold bind coordinates`);
247
+ }
248
+ const px = worldX - m.worldX;
249
+ const py = worldY - m.worldY;
250
+ return [(px * m.d - py * m.b) / det, (py * m.a - px * m.c) / det];
251
+ }
252
+
253
+ /**
254
+ * A world **displacement** -> the bone's local space: rotation and scale only,
255
+ * with the translation left out.
256
+ *
257
+ * ⭐ Not a variant of `toBoneLocal` for convenience — it is the other half of a
258
+ * different identity. `computeWorldVertices` composes a weighted vertex as
259
+ * `Σ wᵢ · (Rᵢ · (bindᵢ + deformᵢ) + tᵢ)`, so the translation `tᵢ` lands on the
260
+ * vertex once and never on the offset. Writing `Rᵢ⁻¹ · D` into every influence
261
+ * therefore moves the vertex by exactly `D · Σ wᵢ`, which is `D` because the
262
+ * weights close at 1 (issue #389). Running a displacement through `toBoneLocal`
263
+ * instead would subtract the bone's origin from it and land the vertex
264
+ * somewhere nobody asked for.
265
+ *
266
+ * ⚠️ **Not rounded**, for `toWorld`'s reason: the caller quantises the one
267
+ * number it emits, and rounding an intermediate would bias it.
268
+ */
269
+ export function toBoneLocalVector(m: BoneTransform, worldDX: number, worldDY: number): [number, number] {
270
+ const det = m.a * m.d - m.b * m.c;
271
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-9) {
272
+ throw new TransformError(`bone transform is singular (det ${det}); a zero-scale bone cannot carry a displacement`);
273
+ }
274
+ return [(worldDX * m.d - worldDY * m.b) / det, (worldDY * m.a - worldDX * m.c) / det];
275
+ }
276
+
277
+ /**
278
+ * A bone-local point -> world. The inverse of `toBoneLocal`, and the direction
279
+ * `VertexAttachment.computeWorldVertices` takes at setup.
280
+ *
281
+ * ⚠️ **Not rounded**, and that is the difference from every other function here.
282
+ * Its callers measure with the result — a path's arc lengths are a sum over many
283
+ * of these — so quantising each point first would bias the sum by up to half a
284
+ * step per sample. Round the answer, not the samples.
285
+ */
286
+ export function toWorld(m: BoneTransform, localX: number, localY: number): [number, number] {
287
+ return [m.a * localX + m.b * localY + m.worldX, m.c * localX + m.d * localY + m.worldY];
288
+ }
289
+
290
+ /**
291
+ * Normalise degrees into (-180, 180], which is how an editor shows a rotation.
292
+ *
293
+ * Not rounded, for `toBoneLocal`'s reason: a caller that emits the angle
294
+ * quantises it with `f32`, and one that compares or measures with it wants the
295
+ * double.
296
+ */
297
+ export function normaliseDegrees(deg: number): number {
298
+ let v = ((deg % 360) + 360) % 360;
299
+ if (v > 180) v -= 360;
300
+ return v;
301
+ }
302
+
303
+ /**
304
+ * Screen degrees (y down, the convention the manifest and the art share) ->
305
+ * Spine degrees (y up, CCW). One negation, stated once, so no other file has to
306
+ * remember which way the y flip goes.
307
+ */
308
+ export function screenToSpineDegrees(screenDeg: number): number {
309
+ return normaliseDegrees(-screenDeg);
310
+ }