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,630 @@
1
+ /**
2
+ * A `deform` key's offsets, evaluated from a transform the spec states.
3
+ *
4
+ * ## Why this module exists
5
+ *
6
+ * A `deform` timeline was the one place in a rig spec where an author was still
7
+ * transcribing arithmetic. `gallery/portrait`'s held 12° head yaw is **160
8
+ * vertex offsets across 8 keys**, and not one of them is a judgement: every one
9
+ * is `x·(cos t − 1) − z·sin t` evaluated at a different column
10
+ * (docs/FACE.md §1). A second angle is a second full table, which is why that
11
+ * example's own angle sweep needed a throwaway script that was never in the
12
+ * repository — *the measurement existed and the reproduction did not*
13
+ * (issue #294).
14
+ *
15
+ * rigc already had this shape of answer for **geometry**:
16
+ * `generator: { kind: "contour" | "ring" | "ribbon" }` exists because "a table
17
+ * of numbers is the wrong way to say a deformation model" (AUTHORING §3.4).
18
+ * This is the same move on the animation half. **The spec states the model and
19
+ * the compiler states the numbers**, so `explain` can print what a key does, an
20
+ * author can sweep a parameter, and a reviewer can check a claim instead of a
21
+ * transcription.
22
+ *
23
+ * ## What this is NOT, and the three rules that keep it that way
24
+ *
25
+ * 1. **It never authors.** Every parameter arrives from the spec — the angle,
26
+ * the radius, the amplitude, the point a scale is about. Nothing here has a
27
+ * default measured off the art, and a missing number is a `CompileError`
28
+ * naming the field, exactly as everywhere else in the compiler.
29
+ * 2. **It generates no in-betweens.** MOTION §7 refuses a `rigc tween`, and
30
+ * this is not one: a transform is evaluated **at one key**, from parameters
31
+ * that key states, and the blend between two keys is still the deform
32
+ * timeline's own single 0..1 channel. Sweeping an angle is editing one number
33
+ * per key, never asking the compiler to interpolate a model.
34
+ * 3. **A parameter that cannot mean what it says is refused by name**, on the
35
+ * principle that a parameter changing nothing is a reader's false lead about
36
+ * which model produced the numbers: `wavelength: 0`, a determinant at or
37
+ * below 0, a radius that falls inside the part. Since issue #350 that extends
38
+ * one step later, to a model whose parameters are each legal and whose
39
+ * *evaluation* is an all-zero run — the refusal at the bottom of
40
+ * `evaluateDeformTransform`, whose whole difficulty is telling that apart
41
+ * from a key that means the identity and says so.
42
+ *
43
+ * ## Determinism
44
+ *
45
+ * Every closed form below is a fixed sequence of float64 operations over
46
+ * numbers read from the spec, in vertex order, with no iteration over an
47
+ * unordered set. The caller quantises with the compiler's `onModelGrid` — a
48
+ * model's own 1e-6 resolution, ending in the float32 name every emitted number
49
+ * takes — so the same spec emits the same bytes, which `A18_DETERMINISTIC_EMIT`
50
+ * proves on a second independent compile. The grid is absolute on purpose: a
51
+ * closed form's identities (a zero crossing, a whole revolution) are exact
52
+ * zeros float64 misses by ~1e-16, and a float32 grid alone has no zero to land
53
+ * on. The runtime then loads the numbers into a `Float32Array`, which is the
54
+ * reason the offsets are reported in the units they are emitted in rather than
55
+ * at full float64 width.
56
+ */
57
+
58
+ import { CompileError } from './errors.ts';
59
+
60
+ /** Which coordinate a bend or a wave reads, and which one it displaces. */
61
+ export type DeformAxis = 'x' | 'y';
62
+
63
+ /**
64
+ * A **2.5D turn**: the part is treated as painted on a cylinder standing on
65
+ * `about`, and the key is that rotation projected back onto the screen.
66
+ *
67
+ * `yaw` stands the cylinder vertically and moves vertices horizontally; `pitch`
68
+ * is the same expression with the axes swapped.
69
+ *
70
+ * ## Why there is no separate `parallax` kind
71
+ *
72
+ * A pure depth slide — `d = z · offset`, no angle — was written and then taken
73
+ * out again (2026-09-05), for two reasons and the second is the one that
74
+ * settles it.
75
+ *
76
+ * ⭐ **It is this form with a term dropped.** Subtract them and the whole
77
+ * remainder is `u · (cos t − 1)`, independent of the depth, so the slide is
78
+ * `yaw` at a small angle and nothing else: at 16° the gap is 2.32px, at 1°
79
+ * 0.0091px, quartering with each halving. A second kind for the same model
80
+ * evaluated less accurately is a second answer to one question.
81
+ *
82
+ * 🚨 **And what it was for is not rigc's to state.** A depth slide is a CAMERA
83
+ * move — the viewer shifts and the near parts lag — driven by a pointer, not by
84
+ * a clock. This spec is a timeline. Baking a pointer-driven parameter into time
85
+ * keys is a category error, and it bakes what a runtime should be evaluating.
86
+ *
87
+ * ⇒ What rigc states about a raised surface is the ANGLE it may turn through
88
+ * (here) and, for a soft one, how it answers an impact (a physics constraint on
89
+ * a bone the mesh is bound to). The camera belongs to whatever draws the
90
+ * result. docs/FACE.md §1 is the whole
91
+ * derivation, and §4.2 is the angle past which any given column pair folds —
92
+ * this evaluates the projection and says nothing about whether the angle is
93
+ * sane. `A39_DEFORM_KEEPS_TRIANGLE_WINDING` is what catches a fold.
94
+ */
95
+ export interface DeformTurn {
96
+ kind: 'yaw' | 'pitch';
97
+ /**
98
+ * The cylinder's radius, in the attachment's own units.
99
+ *
100
+ * ⚠️ **Not the plate's half-width.** For `gallery/portrait`'s head plate the
101
+ * two coincide (340/2 = 170); for its fringe they do not — 372 wide and
102
+ * `radius` 196, because 196 is where the fringe *sits*, 26 in front of the
103
+ * skull. Read it off the depth table, never off the PNG (FACE §4).
104
+ */
105
+ radius?: number;
106
+ /**
107
+ * Read `z` per vertex from the attachment's depth map instead of deriving it
108
+ * from a cylinder — the same closed form with a measured surface under it.
109
+ *
110
+ * ⭐ It replaces `radius`, and stating both is refused: they are two answers
111
+ * to "how far forward is this vertex", and a spec that carries both leaves a
112
+ * reader unable to tell which one the output came from. The map itself is
113
+ * named on the attachment's `generator` ([`src/rig.ts`](rig.ts)), because it
114
+ * is a property of the art rather than of this key — every key that turns
115
+ * this part reads the same surface.
116
+ */
117
+ depth?: boolean;
118
+ /** The turn, in degrees. Positive yaws toward −x, which is FACE §1's sign. */
119
+ degrees: number;
120
+ /** Where the axis crosses the driving coordinate. Default 0. */
121
+ about?: number;
122
+ }
123
+
124
+ /**
125
+ * A **scale about a point** — `gallery/squash`'s two shapes, which its README
126
+ * already writes out as `sx`/`sy` about a point.
127
+ *
128
+ * The point is fixed by construction, which is the property that makes the
129
+ * example's claim checkable: the ball's contact vertex sits at the contact
130
+ * point, so its offset is `(0, 0)` and the ball flattens against the ground
131
+ * rather than sinking through it — with no key anywhere that has to be tuned to
132
+ * make that true.
133
+ *
134
+ * ⭐ `det = sx · sy` is refused at or below 0, and above 0 it is a **proof**
135
+ * rather than a report: an affine map with a positive determinant preserves
136
+ * every triangle's winding, so a key of this kind cannot be the fold A39 hunts.
137
+ * A shear belongs to `bend` with `power: 1`.
138
+ */
139
+ export interface DeformAffine {
140
+ kind: 'affine';
141
+ /** `[sx, sy]`. 1 is unchanged; both are required, because a guessed one is a value the spec did not state. */
142
+ scale: [number, number];
143
+ /** The fixed point, in the attachment's own units. Default `[0, 0]`. */
144
+ about?: [number, number];
145
+ }
146
+
147
+ /**
148
+ * A sinusoid of one coordinate, displacing along another.
149
+ *
150
+ * ⚠️ **A wavelength is only as real as the geometry that samples it.** The mesh
151
+ * carries the wave at the coordinates it happens to have, so a period short
152
+ * against the spacing of those coordinates does not make a smaller ripple — it
153
+ * makes a different model. Against a spacing of `s`: `wavelength ≥ 4s` to read
154
+ * as a wave at all and `≥ 8s` to read as a curve; at `2s` every sample lands on
155
+ * the same pair of phases, which is a zigzag, and at that pair's zero crossings
156
+ * it is nothing at all. The last of those is refused (issue #350) because it
157
+ * emits an all-zero run while claiming an amplitude; the zigzag is not, because
158
+ * it is a bad wave rather than an absent one and that is authoring judgement.
159
+ */
160
+ export interface DeformWave {
161
+ kind: 'wave';
162
+ /** Peak displacement, in the attachment's own units. */
163
+ amplitude: number;
164
+ /** One period, in the same units as the coordinate `along` reads. Sampled by the geometry — see the note above. */
165
+ wavelength: number;
166
+ /** Phase at `along = 0`, in degrees. Default 0. */
167
+ phase?: number;
168
+ /** The coordinate the sinusoid reads. */
169
+ along: DeformAxis;
170
+ /** The coordinate it displaces. */
171
+ axis: DeformAxis;
172
+ }
173
+
174
+ /**
175
+ * A **polynomial bend**: displacement rising as a power of how far along the
176
+ * part a vertex is.
177
+ *
178
+ * `power: 1` is an affine shear, and every higher power is a curve no bone
179
+ * transform can produce — which is the whole reason a `deform` timeline is
180
+ * worth its numbers on a mesh whose vertices are pinned to one bone.
181
+ * `power: 2` is a cantilever: zero displacement **and zero slope** at `from`,
182
+ * so a part held at one end bends rather than tilting.
183
+ */
184
+ export interface DeformBend {
185
+ kind: 'bend';
186
+ /** Displacement at `to`, in the attachment's own units. */
187
+ amount: number;
188
+ /** Where the bend is anchored — displacement is 0 here. */
189
+ from: number;
190
+ /** Where it reaches `amount`. */
191
+ to: number;
192
+ /** The exponent. A whole number ≥ 1; default 2. */
193
+ power?: number;
194
+ /** The coordinate that measures how far along a vertex is. */
195
+ along: DeformAxis;
196
+ /** The coordinate it displaces. */
197
+ axis: DeformAxis;
198
+ }
199
+
200
+ export type DeformTransform = DeformTurn | DeformAffine | DeformWave | DeformBend;
201
+
202
+ /** The kinds this module evaluates, in the order the docs list them. */
203
+ export const DEFORM_TRANSFORM_KINDS = ['yaw', 'pitch', 'affine', 'wave', 'bend'] as const;
204
+
205
+ /**
206
+ * What one evaluation did, for `explain` to print and a reviewer to check.
207
+ *
208
+ * `stated` is the spec's own parameters and `derived` the scalars the closed
209
+ * form got out of them — the two lines that let somebody re-derive a column by
210
+ * hand. `offsets` is the emitted run, so the report and the artifact cannot
211
+ * disagree (`explain`'s `DEFORM` block, issue #316, quotes this rather than
212
+ * re-evaluating it — see `src/deformmeasure.ts`).
213
+ */
214
+ export interface DeformTransformReport {
215
+ kind: DeformTransform['kind'];
216
+ /** The transform as the spec states it. */
217
+ stated: string;
218
+ /** The scalars the closed form derived, e.g. `cos t − 1 = −0.021852`. */
219
+ derived: string[];
220
+ /** The closed form, written out. */
221
+ formula: string;
222
+ vertexCount: number;
223
+ /** Largest offset magnitude in the run, and the vertex carrying it. */
224
+ maxOffset: number;
225
+ maxOffsetVertex: number;
226
+ /**
227
+ * The displacement the model states: `x, y` per vertex, already quantised by
228
+ * the caller's rounder.
229
+ *
230
+ * On an attachment whose deform array is one pair per vertex this IS the
231
+ * emitted run, which is the case every report printed before issue #389. On a
232
+ * multi-influence one it is one WORLD displacement per vertex, and the array
233
+ * the caller wrote from it is `expanded` on the compile result — the report
234
+ * still quotes what was emitted rather than re-evaluating anything.
235
+ */
236
+ offsets: number[];
237
+ }
238
+
239
+ /** The compiler's `onModelGrid` (1e-6, then float32), passed in so the quantiser stays in one place. */
240
+ export type Rounder = (n: number) => number;
241
+
242
+ /**
243
+ * Evaluate one transform over an attachment's setup geometry.
244
+ *
245
+ * `setup` is `x, y` per vertex, and what comes back is **one displacement per
246
+ * vertex in the same space** — this function neither knows nor needs to know
247
+ * which space that is. The caller owns it, and since issue #389 there are two:
248
+ * the deform array's own space, where every vertex is one pair of one bone's
249
+ * bind space and the result IS the array; and setup **world**, where a vertex
250
+ * has several influences and the caller pushes each displacement into every one
251
+ * of them through that bone's inverse. Either way the array handed in means one
252
+ * thing all the way down it, which is the precondition the closed forms need.
253
+ *
254
+ * Returns a run as long as `setup`, so it always starts at deform index 0 and
255
+ * covers the whole attachment. That is deliberate and it is not a convenience:
256
+ * a transform is a model of the geometry rather than an edit of part of it, and
257
+ * a partially applied model leaves a **step discontinuity at the end of the
258
+ * run** — which is exactly one half of the defect issue #313 records, where a
259
+ * 20-vertex run of a hand-authored ripple began 6.3px away from the unkeyed
260
+ * vertex before it.
261
+ */
262
+ export function evaluateDeformTransform(
263
+ transform: DeformTransform,
264
+ setup: readonly number[],
265
+ round: Rounder,
266
+ where: string,
267
+ /**
268
+ * Per-vertex `z` for this attachment, when its generator named a depth map.
269
+ * Null when it named none — which is what makes `"depth": true` refusable by
270
+ * name rather than silently falling back to a cylinder.
271
+ */
272
+ depth: readonly number[] | null = null,
273
+ ): DeformTransformReport {
274
+ if (transform === null || typeof transform !== 'object' || Array.isArray(transform)) {
275
+ throw new CompileError(`${where}: "transform" is ${JSON.stringify(transform)}; it is an object with a "kind"`);
276
+ }
277
+ const kind = (transform as { kind?: unknown }).kind;
278
+ if (typeof kind !== 'string' || !(DEFORM_TRANSFORM_KINDS as readonly string[]).includes(kind)) {
279
+ throw new CompileError(
280
+ `${where}: "transform" has kind ${JSON.stringify(kind)}; this spec evaluates ${DEFORM_TRANSFORM_KINDS.join(', ')}. ` +
281
+ 'A model that is none of those is still authorable as a "vertices" run.',
282
+ );
283
+ }
284
+ const count = setup.length / 2;
285
+ const offsets = new Array<number>(setup.length);
286
+ let derived: string[];
287
+ let stated: string;
288
+ let formula: string;
289
+ // -- the three values the all-zero refusal at the bottom reads (issue #350) -
290
+ //
291
+ // `identity` is whether the transform's own scalars state the identity, and it
292
+ // is judged on the ROUNDED scalars rather than in float64 — a `degrees: 360`
293
+ // turn leaves `sin t` at −2.4e−16, which is 0 on the grid a model is evaluated
294
+ // on (`onModelGrid`), so a spec that states a whole revolution states the identity as
295
+ // surely as `degrees: 0` does. `band` is the largest magnitude the closed form
296
+ // reached *before* quantising, which is what separates a model that is
297
+ // arithmetically zero (a band of float noise, ~1e−15) from one that is real
298
+ // and smaller than that grid. `sampledTo` is the per-kind diagnosis.
299
+ let identity: boolean;
300
+ let identitySpelling: string;
301
+ let band = 0;
302
+ let sampledTo = '';
303
+ const widen = (d: number): number => {
304
+ const m = Math.abs(d);
305
+ if (m > band) band = m;
306
+ return d;
307
+ };
308
+
309
+ switch (kind) {
310
+ case 'yaw':
311
+ case 'pitch': {
312
+ const t = transform as DeformTurn;
313
+ const degrees = num(t.degrees, 'degrees', where);
314
+ const about = t.about === undefined ? 0 : num(t.about, 'about', where);
315
+ // -- which surface the turn is projected off (issue #382) --------------
316
+ //
317
+ // Two models, one closed form. A cylinder derives `z` from how far off
318
+ // the axis a vertex sits; a depth map states it per vertex. Everything
319
+ // below is shared, and the only difference is where `z` comes from — the
320
+ // reason this is a branch on the input and not a second transform kind.
321
+ const fromDepth = t.depth === true;
322
+ if (fromDepth && t.radius !== undefined) {
323
+ throw new CompileError(
324
+ `${where}: transform ${kind} states both "depth": true and a radius ${JSON.stringify(t.radius)}. They are ` +
325
+ 'two answers to how far forward each vertex sits — a cylinder derives it, a map states it — and a key ' +
326
+ 'carrying both leaves a reader unable to say which one the output came from. Drop one.',
327
+ );
328
+ }
329
+ if (fromDepth && depth === null) {
330
+ throw new CompileError(
331
+ `${where}: transform ${kind} says "depth": true and this attachment has no depth map. The map is named on ` +
332
+ 'the attachment\'s generator, as `"depth": { "image": …, "near": …, "zScale": … }`, because it describes ' +
333
+ 'the art rather than this key. Name it there, or give this key a radius.',
334
+ );
335
+ }
336
+ if (fromDepth && depth !== null && depth.length !== count) {
337
+ // Unreachable while the sampler walks the same vertex list the mesh
338
+ // emitted; stated because a silent mismatch here would turn into a turn
339
+ // evaluated against another vertex's depth.
340
+ throw new CompileError(
341
+ `${where}: transform ${kind} has ${depth.length} sampled depths for ${count} vertices`,
342
+ );
343
+ }
344
+ const radius = fromDepth ? 0 : num(t.radius, 'radius', where);
345
+ if (!fromDepth && radius <= 0) {
346
+ throw new CompileError(`${where}: transform ${kind} has radius ${radius}; it is the radius of the cylinder the part is painted on, so a positive number`);
347
+ }
348
+ const rad = (degrees * Math.PI) / 180;
349
+ const cosMinus1 = Math.cos(rad) - 1;
350
+ const sin = Math.sin(rad);
351
+ // The driving coordinate is the one the axis is perpendicular to: x for a
352
+ // yaw (a vertical axis), y for a pitch (a horizontal one). The other
353
+ // component of every pair is 0, because a turn about an axis moves
354
+ // nothing along it.
355
+ const along = kind === 'yaw' ? 0 : 1;
356
+ for (let v = 0; v < count; v++) {
357
+ const u = setup[2 * v + along] - about;
358
+ // A vertex outside the cylinder has no surface to be painted on, and
359
+ // clamping its depth to 0 would silently evaluate a DIFFERENT model
360
+ // there — a flat edge on a curved part. Refused by name instead: this is
361
+ // FACE §4's "read R off the depth table, never off the PNG" as a check.
362
+ if (!fromDepth && Math.abs(u) > radius) {
363
+ throw new CompileError(
364
+ `${where}: transform ${kind} has radius ${radius}, and vertex ${v} sits at ${kind === 'yaw' ? 'x' : 'y'}=` +
365
+ `${setup[2 * v + along]}, which is ${round(Math.abs(u) - radius)} past it (about=${about}). The cylinder has no ` +
366
+ 'surface there, so its depth would be 0 and the projection would be a different model at that vertex. ' +
367
+ 'Raise the radius to where the part actually sits, or move the vertex.',
368
+ );
369
+ }
370
+ // A depth map needs no such check: it states a surface everywhere it
371
+ // covers, and a vertex it does NOT cover was already refused when the
372
+ // mesh was built (`sampleContourDepth`) rather than here, where the
373
+ // sheet is long out of reach.
374
+ const z = fromDepth ? (depth as readonly number[])[v] : Math.sqrt(radius * radius - u * u);
375
+ const d = u * cosMinus1 - z * sin;
376
+ offsets[2 * v + along] = round(widen(d));
377
+ offsets[2 * v + (1 - along)] = 0;
378
+ }
379
+ identity = round(cosMinus1) === 0 && round(sin) === 0;
380
+ identitySpelling = 'degrees 0';
381
+ sampledTo = fromDepth
382
+ ? `The turn is ${degrees}°, and a projection of it can only vanish where every vertex shares one ` +
383
+ `${kind === 'yaw' ? 'x' : 'y'} AND one depth — check the depth map's sampled range in the mesh report, ` +
384
+ 'which is 0 wide when the sheet is flat where this part sits'
385
+ : `The turn is ${degrees}°, and a projection of it can only vanish where every vertex shares one ` +
386
+ `${kind === 'yaw' ? 'x' : 'y'} — check that this attachment's setup geometry is the shape the radius says it is`;
387
+ const c = kind === 'yaw' ? 'x' : 'y';
388
+ stated = fromDepth
389
+ ? `depth=true degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}`
390
+ : `radius=${radius} degrees=${degrees}${t.about === undefined ? '' : ` about=${about}`}`;
391
+ formula = fromDepth
392
+ ? `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = the vertex's sampled depth`
393
+ : `d${c} = (${c}−about)·(cos t − 1) − z·sin t, z = √(radius² − (${c}−about)²)`;
394
+ derived = [
395
+ `t = ${round(rad)} rad`,
396
+ `cos t − 1 = ${round(cosMinus1)}`,
397
+ `sin t = ${round(sin)}`,
398
+ ];
399
+ if (fromDepth) {
400
+ const zs = depth as readonly number[];
401
+ let zlo = Infinity;
402
+ let zhi = -Infinity;
403
+ for (const z of zs) {
404
+ if (z < zlo) zlo = z;
405
+ if (z > zhi) zhi = z;
406
+ }
407
+ // The depth range is what a reader checks the amplitude against: the
408
+ // deepest vertex moves `−zhi·sin t`, and that number is the one that
409
+ // either matches the art or does not.
410
+ derived.push(`z ∈ [${round(zlo)}, ${round(zhi)}] over ${zs.length} vertices`);
411
+ derived.push(`deepest shift = −z_max·sin t = ${round(-zhi * sin)}`);
412
+ } else {
413
+ derived.push(`centre shift = −radius·sin t = ${round(-radius * sin)}`);
414
+ }
415
+ break;
416
+ }
417
+ case 'affine': {
418
+ const a = transform as DeformAffine;
419
+ const scale = pair(a.scale, 'scale', where);
420
+ const about = a.about === undefined ? ([0, 0] as [number, number]) : pair(a.about, 'about', where);
421
+ const det = scale[0] * scale[1];
422
+ if (det <= 0) {
423
+ throw new CompileError(
424
+ `${where}: transform affine has scale [${scale[0]}, ${scale[1]}], whose determinant is ${round(det)}. ` +
425
+ 'At or below zero the map mirrors or collapses the geometry and EVERY triangle reverses its winding, ' +
426
+ 'which is the fold A39_DEFORM_KEEPS_TRIANGLE_WINDING refuses. A positive determinant is what makes this ' +
427
+ 'kind incapable of folding a mesh.',
428
+ );
429
+ }
430
+ for (let v = 0; v < count; v++) {
431
+ offsets[2 * v] = round(widen((scale[0] - 1) * (setup[2 * v] - about[0])));
432
+ offsets[2 * v + 1] = round(widen((scale[1] - 1) * (setup[2 * v + 1] - about[1])));
433
+ }
434
+ identity = round(scale[0] - 1) === 0 && round(scale[1] - 1) === 0;
435
+ identitySpelling = 'scale [1, 1]';
436
+ sampledTo =
437
+ `A scale about a fixed point moves nothing that SITS on it, so every vertex this attachment has lies at ` +
438
+ `about=[${about[0]}, ${about[1]}] on the axis the scale changes — the geometry has collapsed onto the point ` +
439
+ 'the key holds still';
440
+ stated = `scale=[${scale[0]}, ${scale[1]}]${a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`}`;
441
+ formula = 'dx = (sx − 1)·(x − ax), dy = (sy − 1)·(y − ay)';
442
+ derived = [`sx − 1 = ${round(scale[0] - 1)}`, `sy − 1 = ${round(scale[1] - 1)}`, `det = sx·sy = ${round(det)} > 0, so no triangle can reverse`];
443
+ break;
444
+ }
445
+ case 'wave': {
446
+ const w = transform as DeformWave;
447
+ const amplitude = num(w.amplitude, 'amplitude', where);
448
+ const wavelength = num(w.wavelength, 'wavelength', where);
449
+ const phase = w.phase === undefined ? 0 : num(w.phase, 'phase', where);
450
+ if (wavelength === 0) {
451
+ throw new CompileError(`${where}: transform wave has wavelength 0; one period cannot be zero long`);
452
+ }
453
+ const [along, axis] = axes(w.along, w.axis, 'wave', where);
454
+ const phaseRad = (phase * Math.PI) / 180;
455
+ const k = (2 * Math.PI) / wavelength;
456
+ for (let v = 0; v < count; v++) {
457
+ const d = amplitude * Math.sin(k * setup[2 * v + along] + phaseRad);
458
+ offsets[2 * v + axis] = round(widen(d));
459
+ offsets[2 * v + (1 - axis)] = 0;
460
+ }
461
+ const an = along === 0 ? 'x' : 'y';
462
+ identity = round(amplitude) === 0;
463
+ identitySpelling = 'amplitude 0';
464
+ // The sampling fact, measured off the array this call was handed rather
465
+ // than inferred from a topology the compiler does not have: it knows which
466
+ // coordinates it read, not where anybody's rows are. The smallest gap
467
+ // between two distinct ones is the finest detail the geometry can carry,
468
+ // and the ratio to it is the rule `gallery/nod`'s README states.
469
+ const distinct = [...new Set(Array.from({ length: count }, (_, v) => setup[2 * v + along]))].sort((p, q) => p - q);
470
+ let gap = Infinity;
471
+ for (let i = 1; i < distinct.length; i++) gap = Math.min(gap, distinct[i] - distinct[i - 1]);
472
+ sampledTo =
473
+ distinct.length < 2
474
+ ? `Every vertex sits at ${an}=${distinct[0]}, so one value of the sinusoid covers the whole attachment and ` +
475
+ 'this phase is where that one value crosses zero. A wave needs the coordinate it reads to VARY across the ' +
476
+ 'geometry; a part that displaces as a whole is a bone, not a deform'
477
+ : `The closest two distinct ${an} coordinates in this attachment are ${round(gap)} apart, and a sinusoid has ` +
478
+ `to be sampled to exist: a wavelength of at least 4x that (${round(4 * gap)}) to read as a wave at all and ` +
479
+ `8x (${round(8 * gap)}) to read as a curve, where this key states ${round(wavelength / gap)}x. At 2x every ` +
480
+ 'sample lands on the same pair of phases, and at the zero crossings that pair is (0, 0). `gallery/nod`\'s ' +
481
+ 'README carries that rule and the measured table behind it';
482
+ stated = `amplitude=${amplitude} wavelength=${wavelength} phase=${phase} along=${an} axis=${axis === 0 ? 'x' : 'y'}`;
483
+ formula = `d${axis === 0 ? 'x' : 'y'} = amplitude · sin(2π·${an}/wavelength + phase)`;
484
+ derived = [`2π/wavelength = ${round(k)} rad per unit`, `phase = ${round(phaseRad)} rad`];
485
+ break;
486
+ }
487
+ default: {
488
+ const b = transform as DeformBend;
489
+ const amount = num(b.amount, 'amount', where);
490
+ const from = num(b.from, 'from', where);
491
+ const to = num(b.to, 'to', where);
492
+ const power = b.power === undefined ? 2 : num(b.power, 'power', where);
493
+ if (!Number.isInteger(power) || power < 1) {
494
+ throw new CompileError(
495
+ `${where}: transform bend has power ${power}; it is a whole number ≥ 1 (1 is an affine shear, 2 a cantilever ` +
496
+ 'that is flat at "from"). A fractional power is not evaluated because it has no value on the side of "from" ' +
497
+ 'the part does not reach.',
498
+ );
499
+ }
500
+ if (to === from) {
501
+ throw new CompileError(`${where}: transform bend has from ${from} and to ${to}; they are the two ends of the bend, so they differ`);
502
+ }
503
+ const [along, axis] = axes(b.along, b.axis, 'bend', where);
504
+ const span = to - from;
505
+ let reach = 0;
506
+ for (let v = 0; v < count; v++) {
507
+ const u = (setup[2 * v + along] - from) / span;
508
+ if (Math.abs(u) > reach) reach = Math.abs(u);
509
+ offsets[2 * v + axis] = round(widen(amount * u ** power));
510
+ offsets[2 * v + (1 - axis)] = 0;
511
+ }
512
+ const an = along === 0 ? 'x' : 'y';
513
+ identity = round(amount) === 0;
514
+ identitySpelling = 'amount 0';
515
+ // `u` is 0 at `from` and 1 at `to`, so the two ways a stated bend vanishes
516
+ // are both statements about where the geometry sits in that span — and
517
+ // both are measured here rather than guessed.
518
+ sampledTo =
519
+ reach === 0
520
+ ? `Every vertex sits at ${an}=${from}, which is "from" — the end the bend is anchored at, where the ` +
521
+ 'displacement is 0 by construction. The span the key names does not cross the part it is keyed on'
522
+ : `The furthest any vertex reaches into the span is u=${round(reach)} of 1, and u^${power} of that is ` +
523
+ `${(reach ** power).toExponential(3)} — so the part occupies only the flat end of the curve. Move "to" to ` +
524
+ 'where the geometry actually ends, or lower the power';
525
+ stated = `amount=${amount} from=${from} to=${to} power=${power} along=${an} axis=${axis === 0 ? 'x' : 'y'}`;
526
+ formula = `d${axis === 0 ? 'x' : 'y'} = amount · u^${power}, u = (${an} − from) / (to − from)`;
527
+ derived = [
528
+ `span = to − from = ${round(span)}`,
529
+ power === 1
530
+ ? 'power 1 is an affine shear (det = 1), so no triangle can reverse'
531
+ : `power ${power} is not affine — the gradient at u is ${power}·amount·u^${power - 1}/span, so it is 0 at "from"`,
532
+ ];
533
+ break;
534
+ }
535
+ }
536
+
537
+ // -- a model the geometry sampled to nothing (issue #350) ------------------
538
+ //
539
+ // The three refusals above catch a parameter that cannot mean what it says —
540
+ // `wavelength: 0`, a determinant at or below 0, a radius inside the part. This
541
+ // is the same principle one step later: every parameter is individually legal,
542
+ // and the model still evaluates to a run of zeros. The key then claims a
543
+ // deformation, emits nothing, and *gates green* — `A35` is right that the run
544
+ // fits and `A39` is right that no triangle moved, so neither can see it and
545
+ // this is the only place it can be said.
546
+ //
547
+ // ⭐ **The distinguishing condition is where the identity is stated.** A key
548
+ // that MEANS the setup pose says so in its own parameters — `degrees: 0` (or
549
+ // any whole revolution), `amplitude: 0`, `amount: 0`, `scale: [1, 1]` — or
550
+ // carries no run at all, which is the format's own `{ "t": … }`. Those pass.
551
+ // What is refused is the pair that cannot both be true: parameters that state
552
+ // a deformation, and an evaluation that is the identity. The two are not the
553
+ // same event, and before this they printed the same line.
554
+ //
555
+ // `count > 0` is not defensive noise: `every` on an empty array is `true`, so
556
+ // an attachment with no vertices would otherwise be refused with a message
557
+ // about arithmetic that never ran.
558
+ if (count > 0 && !identity && offsets.every((d) => d === 0)) {
559
+ throw new CompileError(
560
+ `${where}: transform ${kind} states ${stated}, and every one of this attachment's ${count} vertices evaluates ` +
561
+ `to an offset of 0 — the largest value the closed form reached at any of them is ${band.toExponential(3)}, ` +
562
+ 'which quantises to 0 on the 1e-6 grid a model is evaluated on. So the key states a deformation and ' +
563
+ `emits the identity, and nothing downstream can tell it apart from a key that meant the setup pose. ` +
564
+ `${sampledTo}. A key that MEANS the identity states it in its own parameters (${identitySpelling}) or carries ` +
565
+ 'no run at all.',
566
+ );
567
+ }
568
+
569
+ let maxOffset = 0;
570
+ let maxOffsetVertex = 0;
571
+ for (let v = 0; v < count; v++) {
572
+ const m = Math.hypot(offsets[2 * v], offsets[2 * v + 1]);
573
+ if (m > maxOffset) {
574
+ maxOffset = m;
575
+ maxOffsetVertex = v;
576
+ }
577
+ }
578
+ return {
579
+ kind: kind as DeformTransform['kind'],
580
+ stated,
581
+ derived,
582
+ formula,
583
+ vertexCount: count,
584
+ maxOffset: round(maxOffset),
585
+ maxOffsetVertex,
586
+ offsets,
587
+ };
588
+ }
589
+
590
+ /** One required finite number, refused by field name rather than compiled as a NaN. */
591
+ function num(value: unknown, field: string, where: string): number {
592
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
593
+ throw new CompileError(`${where}: transform field "${field}" is ${JSON.stringify(value)}; it is a finite number the spec has to state`);
594
+ }
595
+ return value;
596
+ }
597
+
598
+ /** One required `[a, b]`. */
599
+ function pair(value: unknown, field: string, where: string): [number, number] {
600
+ if (!Array.isArray(value) || value.length !== 2) {
601
+ throw new CompileError(`${where}: transform field "${field}" is ${JSON.stringify(value)}; it is a pair, [x, y]`);
602
+ }
603
+ return [num(value[0], `${field}[0]`, where), num(value[1], `${field}[1]`, where)];
604
+ }
605
+
606
+ /**
607
+ * The two axis fields, as indices into an `x, y` pair.
608
+ *
609
+ * `along === axis` is refused: a displacement of x driven by x is a stretch
610
+ * along one axis, which `affine` states as a scale — and states with a
611
+ * determinant, so it carries the proof that this kind cannot.
612
+ */
613
+ function axes(along: unknown, axis: unknown, kind: string, where: string): [0 | 1, 0 | 1] {
614
+ for (const [name, value] of [
615
+ ['along', along],
616
+ ['axis', axis],
617
+ ] as const) {
618
+ if (value !== 'x' && value !== 'y') {
619
+ throw new CompileError(`${where}: transform ${kind} has ${name}=${JSON.stringify(value)}; it is "x" or "y"`);
620
+ }
621
+ }
622
+ if (along === axis) {
623
+ throw new CompileError(
624
+ `${where}: transform ${kind} reads ${String(along)} and displaces ${String(axis)} — the same coordinate, which is a ` +
625
+ 'stretch rather than a bend or a wave. An affine scale states that, and states the determinant that proves it ' +
626
+ 'keeps every triangle\'s winding.',
627
+ );
628
+ }
629
+ return [along === 'x' ? 0 : 1, axis === 'x' ? 0 : 1];
630
+ }