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,490 @@
1
+ /**
2
+ * The core's third construct: every drawn attachment's world vertices at the
3
+ * setup pose (issue #931, step 2c of issue #380) — the oracle's
4
+ * `setup.attachments` block, a region's four corners and a mesh's vertices,
5
+ * and its `setup.clips` block, a clipping polygon and the slot its clip ends
6
+ * at. It is the first construct where the core's arithmetic meets geometry the
7
+ * compiler measured, so exact equivalence here is also a check on the model.
8
+ *
9
+ * ## How every rule below was fixed
10
+ *
11
+ * By posing hand-written skeletons through `tools/pose_oracle.ts dump`
12
+ * (spine-core 4.3.13, `--skin all`, `--physics none`) and comparing a formula
13
+ * over the core's own world transforms (`./world.ts`) with the rows the dump
14
+ * printed, at tolerance 0 on the oracle's six-decimal grid. The runtime's
15
+ * source was not read. The bone rows read exact in every probe, so every
16
+ * residual below belongs to the attachment rule. The core suite's `CO17`
17
+ * holds a hand-written skeleton of every case against the core at tolerance 0.
18
+ *
19
+ * ## A region's four corners
20
+ *
21
+ * Let `W, H` be the record's `width, height`, `sx, sy` its `scaleX, scaleY`
22
+ * (1 unstated), and `ox, oy, w, h, ow, oh` its atlas rectangle's `offsetX,
23
+ * offsetY, width, height, originalWidth, originalHeight` (`ModelAtlasRect`,
24
+ * issue #935). With `kx = W/ow·sx` and `ky = H/oh·sy`:
25
+ *
26
+ * x1 = −W/2·sx + ox·kx, y1 = −H/2·sy + oy·ky, x2 = x1 + w·kx, y2 = y1 + h·ky
27
+ *
28
+ * and the corners, in this order, are `(x1,y1) (x1,y2) (x2,y2) (x2,y1)` —
29
+ * bottom-left, upper-left, upper-right, bottom-right, as the oracle's header
30
+ * words them. Each is turned by the record's `rotation` (degrees, pi written
31
+ * as `./world.ts`'s `RUNTIME_PI`), moved by its `x, y` (0 unstated), and
32
+ * carried through the slot bone's world matrix: `a·x + b·y + worldX`,
33
+ * `c·x + d·y + worldY`. Every record field is read as the double the document
34
+ * spells.
35
+ *
36
+ * Measured on 6000 hand-written regions — 70% of them trimmed, atlas `rotate`
37
+ * 0, 90, 180 or 270, placement in ±50, rotation in ±180 and scale in ±2 —
38
+ * each on one of ten bones rotated, scaled, sheared and reflecting, posed
39
+ * four times: field values float32-exact and spelled with five non-float32
40
+ * decimals, each on a page of `scale:` 1 and 0.37 (1500 each):
41
+ *
42
+ * | reading | exact |
43
+ * | --- | ---: |
44
+ * | the rule above, fields as doubles | 6000 of 6000 |
45
+ * | the trim ignored (`w, h` for `ow, oh`, offsets 0) | 1796 of 6000, worst 335.43 units |
46
+ * | the fields read through `Math.fround`, on the non-float32 spellings | 13 of 3000, worst 41 millionths |
47
+ * | the atlas `bounds` taken as transposed at 90 and 270 | 3007 of 6000, worst 1634.26 units |
48
+ *
49
+ * So the page's `scale:` and the atlas's `rotate` do not enter the corners;
50
+ * the rectangle's `width` and `height` are in the drawing's orientation, as
51
+ * `AtlasRegion` in `src/atlas.ts` states.
52
+ *
53
+ * - **A region with a `sequence` draws its setup frame's rectangle.** Four
54
+ * probes of one three-frame sequence whose frames differ in their trims:
55
+ * `setup` unstated and `setup: 0` matched frame 0's corners, `setup: 1`
56
+ * frame 1's and `setup: 2` frame 2's, and no other frame's. The frame is
57
+ * the model's `sequence.atlas[setup ?? 0]`.
58
+ * - ⛔ **A region whose rectangle is `null` leaves the whole block out, by
59
+ * name.** `null` is the model's statement that the build had no source for
60
+ * that region (`ModelRegionAttachment.atlas`); posing it at a trim of 0
61
+ * would be inventing the four numbers the corners read — the build pair of
62
+ * issue #931 posed one such document 11.925 world units apart. A block
63
+ * with one row missing would read as a roster difference, not an absence,
64
+ * so the block is absent and every such slot is named.
65
+ *
66
+ * ## A mesh's vertices, and a clipping polygon's
67
+ *
68
+ * - Unweighted: each local `x, y` through the slot bone's world matrix plus
69
+ * its origin.
70
+ * - Weighted: per vertex, the sum over its bindings in order of
71
+ * `(x·a + y·b + worldX)·weight` and `(x·c + y·d + worldY)·weight`, each
72
+ * binding's bone looked up BY NAME (`ModelBinding`) — its world, not the
73
+ * slot bone's.
74
+ * - ⚠️ **Every vertex coordinate, bind coordinate and weight is read through
75
+ * `Math.fround` first**: the runtime stores a vertex attachment's array as
76
+ * float32, while it reads a bone's and a region's numbers as the doubles
77
+ * the text spells (`src/model.ts`'s 🔸). Measured on 1200 meshes over the
78
+ * ten bones (60% weighted over one to four of them): float32-exact inputs
79
+ * 600 of 600 either way; inputs with five non-float32 decimals 600 of 600
80
+ * through `Math.fround`, 0 of 600 as doubles (worst 11 millionths). On the
81
+ * corpus, `gallery/flex`'s weighted meshes and `gallery/squash`'s ball read
82
+ * 1 to 3 millionths off as doubles and exact through `Math.fround`.
83
+ * - A linked mesh is written by the oracle as kind `mesh` under its own name,
84
+ * and its vertices are its SOURCE mesh's (`skin`, `slot`, `source` in the
85
+ * model) through the linked mesh's own slot bone: a linked mesh on a
86
+ * rotated, reflected and sheared bone, sourced from a mesh in another slot
87
+ * of another skin, read exact.
88
+ * - The atlas does not enter a mesh's vertices, and a mesh's `sequence`
89
+ * changes none of them.
90
+ * - A clipping polygon follows the mesh rule, and its row's `end` is the
91
+ * model's `end` slot or `null`: 1200 polygons drawn as the meshes above,
92
+ * half of them ending at a slot, read exact and with the right end through
93
+ * `Math.fround` (600 of 600 on each spelling), and 0 of 600 as doubles on
94
+ * the non-float32 spelling (worst 11 millionths).
95
+ *
96
+ * ## Which slots, and in what order
97
+ *
98
+ * A slot is listed when what it shows at setup (`shownAttachment`, issue
99
+ * #928) is a region, a mesh or a linked mesh (`setup.attachments`) or a
100
+ * clipping polygon (`setup.clips`), whether or not its bone is active — an
101
+ * inactive bone's world is all zeros, and the oracle's ill-conditioned rule
102
+ * excludes it. Rows are in the model's slot order, which is the setup draw
103
+ * order (`ModelSlot`): measured the same order as spine-core's on 19 of 19
104
+ * recipes. A bounding box and a path attachment have no row: the oracle's
105
+ * dump writes no vertices for either, so neither can be posed against it.
106
+ *
107
+ * ## At a sample, and under a deform or a sequence (issue #955)
108
+ *
109
+ * `poseGeometry` poses a sample's attachments the same way, through the
110
+ * sample's bones and in its draw order. A slot a deform timeline moved
111
+ * carries its deform array (`ShownGeometry.deform`): the vertices are the
112
+ * array's (`deformedVertices` in `./deform.ts`) and their coordinates are read
113
+ * AS THEY ARE, not through `Math.fround` again — a lerped deform is a double
114
+ * the runtime does not round (`./deform.ts`'s header: the lerped positions
115
+ * rounded to float32 missed 791 of 800 samples). A region a sequence
116
+ * timeline stepped carries its frame (`ShownGeometry.frame`), and its corners
117
+ * read that frame's rectangle (`CoreRegionGeometry.frames`).
118
+ */
119
+ import type { ModelAtlasRect, ModelBinding, ModelVertices } from '../model.ts';
120
+ import { RUNTIME_PI, type CoreWorld } from './world.ts';
121
+ import { deformedVertices } from './deform.ts';
122
+ import { poseClipped, REGION_TRIANGLES, REGION_UVS, type CoreClippedRow, type DrawStep, type ShapeClipper, type TriangleClipper } from './clipping.ts';
123
+
124
+ const RAD = RUNTIME_PI / 180;
125
+
126
+ /** What a region record carries for its corners — `atlas` already the rectangle of the frame shown at setup. */
127
+ export interface CoreRegionGeometry {
128
+ x?: number;
129
+ y?: number;
130
+ rotation?: number;
131
+ scaleX?: number;
132
+ scaleY?: number;
133
+ width: number;
134
+ height: number;
135
+ /** The rectangle drawn at setup, or `null` when the model states the build had none. */
136
+ atlas: ModelAtlasRect | null;
137
+ /** A sequence's rectangles, one per frame (`sequence.atlas`) — what a sequence timeline's frame draws (`./deform.ts`). */
138
+ frames?: ModelAtlasRect[];
139
+ }
140
+
141
+ /** The geometry a record carries, by kind, as far as the construct reads it. */
142
+ export type CoreGeometry =
143
+ | { kind: 'region'; region: CoreRegionGeometry }
144
+ | { kind: 'mesh'; vertices: ModelVertices; uvs: number[]; triangles: number[]; hull?: number }
145
+ | { kind: 'linkedmesh'; skin: string; slot: string; source: string }
146
+ | { kind: 'clipping'; end: string | null; vertices: ModelVertices; inverse: boolean; convex: boolean }
147
+ | { kind: 'boundingbox'; vertices: ModelVertices }
148
+ | { kind: 'path'; vertices: ModelVertices; closed: boolean; constantSpeed: boolean; lengths: number[] };
149
+
150
+ /** One row of `setup.attachments`: `[slot, attachment, kind, vertices]`, the oracle's row. */
151
+ export type CoreAttachmentRow = [string, string, 'region' | 'mesh', Array<number | null>];
152
+ /** One row of `setup.clips`: `[slot, attachment, end, polygon]`, the oracle's row. */
153
+ export type CoreClipRow = [string, string, string | null, Array<number | null>];
154
+
155
+ /** What computes a region's corners in the slot bone's world: `regionCorners` unless a caller passes another. */
156
+ export type RegionPoser = (region: CoreRegionGeometry & { atlas: ModelAtlasRect }, bone: CoreWorld) => number[];
157
+ /** What computes a vertex array's world positions: `worldVertices` unless a caller passes another. */
158
+ export type VertexPoser = (vertices: ModelVertices, bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>, coords?: (v: number) => number) => number[];
159
+
160
+ /** A deformed array's coordinates are read as they are (`./deform.ts`, `deformedVertices`): they are not the stored float32s. */
161
+ export const EXACT_COORDS = (v: number): number => v;
162
+
163
+ /** A region's four corners in world units — the header's rule. */
164
+ export function regionCorners(region: CoreRegionGeometry & { atlas: ModelAtlasRect }, bone: CoreWorld): number[] {
165
+ const { width: W, height: H } = region;
166
+ const sx = region.scaleX ?? 1;
167
+ const sy = region.scaleY ?? 1;
168
+ const r = region.atlas;
169
+ const kx = (W / r.originalWidth) * sx;
170
+ const ky = (H / r.originalHeight) * sy;
171
+ const x1 = (-W / 2) * sx + r.offsetX * kx;
172
+ const y1 = (-H / 2) * sy + r.offsetY * ky;
173
+ const x2 = x1 + r.width * kx;
174
+ const y2 = y1 + r.height * ky;
175
+ const angle = (region.rotation ?? 0) * RAD;
176
+ const cos = Math.cos(angle);
177
+ const sin = Math.sin(angle);
178
+ const x = region.x ?? 0;
179
+ const y = region.y ?? 0;
180
+ const out: number[] = [];
181
+ for (const [px, py] of [[x1, y1], [x1, y2], [x2, y2], [x2, y1]]) {
182
+ // The operation order is measured to the bit (issue #966): the placement is added to the cosine term before the sine term is taken off — `px·cos − py·sin + x` reads 1 ulp off on 748 attachment-samples of six corpus rows.
183
+ const lx = px * cos + x - py * sin;
184
+ const ly = py * cos + y + px * sin;
185
+ out.push(bone.a * lx + bone.b * ly + bone.worldX, bone.c * lx + bone.d * ly + bone.worldY);
186
+ }
187
+ return out;
188
+ }
189
+
190
+ /**
191
+ * A vertex array's world positions — the header's mesh rule, every stored
192
+ * number through `Math.fround`; with `coords`, the coordinates through it
193
+ * instead (a deformed array's are read as they are, `EXACT_COORDS`), the
194
+ * weights through `Math.fround` still.
195
+ */
196
+ export function worldVertices(vertices: ModelVertices, bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>, coords: (v: number) => number = Math.fround): number[] {
197
+ const out: number[] = [];
198
+ if (!vertices.weighted) {
199
+ for (let i = 0; i + 1 < vertices.xy.length; i += 2) {
200
+ const x = coords(vertices.xy[i]);
201
+ const y = coords(vertices.xy[i + 1]);
202
+ out.push(x * bone.a + y * bone.b + bone.worldX, x * bone.c + y * bone.d + bone.worldY);
203
+ }
204
+ return out;
205
+ }
206
+ for (const influences of vertices.bindings) {
207
+ let wx = 0;
208
+ let wy = 0;
209
+ for (const binding of influences) {
210
+ const t = world.get(binding.bone);
211
+ if (t === undefined) throw new Error(`a binding names bone "${binding.bone}", which has no world transform (readModel refuses it first)`);
212
+ const x = coords(binding.x);
213
+ const y = coords(binding.y);
214
+ const weight = Math.fround(binding.weight);
215
+ wx += (x * t.a + y * t.b + t.worldX) * weight;
216
+ wy += (x * t.c + y * t.d + t.worldY) * weight;
217
+ }
218
+ out.push(wx, wy);
219
+ }
220
+ return out;
221
+ }
222
+
223
+ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
224
+ const finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);
225
+ const RECT_FIELDS = ['width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight'] as const;
226
+
227
+ function readRect(value: unknown, where: string, problems: string[]): ModelAtlasRect | undefined {
228
+ if (!isRecord(value)) {
229
+ problems.push(`${where} is ${JSON.stringify(value)}, not an atlas rectangle`);
230
+ return undefined;
231
+ }
232
+ const before = problems.length;
233
+ for (const key of Object.keys(value)) if (!(RECT_FIELDS as readonly string[]).includes(key)) problems.push(`${where}: field "${key}" is not one this reader knows; it reads [${RECT_FIELDS.join(', ')}]`);
234
+ for (const key of RECT_FIELDS) if (!finite(value[key])) problems.push(`${where}: ${key} is ${JSON.stringify(value[key]) ?? 'absent'}, not a finite number`);
235
+ return problems.length === before ? (value as unknown as ModelAtlasRect) : undefined;
236
+ }
237
+
238
+ /** A `ModelVertices`, checked: an even run of finite numbers, or per vertex a non-empty list of bindings each naming a bone of the document. */
239
+ function readVertices(value: unknown, where: string, bones: ReadonlySet<string>, problems: string[]): ModelVertices | undefined {
240
+ if (!isRecord(value) || typeof value.weighted !== 'boolean') {
241
+ problems.push(`${where} is not { weighted: false, xy } or { weighted: true, bindings }`);
242
+ return undefined;
243
+ }
244
+ const before = problems.length;
245
+ if (!value.weighted) {
246
+ const xy = value.xy;
247
+ if (!Array.isArray(xy) || xy.length % 2 !== 0 || !xy.every(finite)) problems.push(`${where}.xy is not an even-length list of finite numbers`);
248
+ return problems.length === before ? { weighted: false, xy: xy as number[] } : undefined;
249
+ }
250
+ const bindings = value.bindings;
251
+ if (!Array.isArray(bindings)) {
252
+ problems.push(`${where}.bindings is not a list`);
253
+ return undefined;
254
+ }
255
+ bindings.forEach((influences, i) => {
256
+ if (!Array.isArray(influences) || influences.length === 0) {
257
+ problems.push(`${where}.bindings[${i}] is not a non-empty list of bindings`);
258
+ return;
259
+ }
260
+ influences.forEach((b: unknown, j) => {
261
+ const at = `${where}.bindings[${i}][${j}]`;
262
+ if (!isRecord(b)) {
263
+ problems.push(`${at} is not an object`);
264
+ return;
265
+ }
266
+ if (typeof b.bone !== 'string' || !bones.has(b.bone)) problems.push(`${at}: bone ${JSON.stringify(b.bone)} is not a bone of this document`);
267
+ for (const key of ['x', 'y', 'weight'] as const) if (!finite(b[key])) problems.push(`${at}: ${key} is ${JSON.stringify(b[key]) ?? 'absent'}, not a finite number`);
268
+ });
269
+ });
270
+ return problems.length === before ? { weighted: true, bindings: bindings as ModelBinding[][] } : undefined;
271
+ }
272
+
273
+ /**
274
+ * The geometry of one attachment record, read and checked field by field —
275
+ * called by `readModel` for every record of every skin. Returns `undefined`
276
+ * when something was refused (the problem is pushed, naming the path). What is
277
+ * refused mirrors the writer (`attachmentOf` in `src/model.ts`): a region
278
+ * carrying neither a rectangle nor a sequence, or both; a sequence whose
279
+ * `atlas` is not one rectangle per frame, or whose setup frame is not one of
280
+ * them; and a binding, a linked mesh's link or a clip's `end` naming nothing.
281
+ * A linked mesh's source is resolved later, once every skin is read.
282
+ */
283
+ export function readGeometry(raw: Record<string, unknown>, kind: CoreGeometry['kind'], where: string, bones: ReadonlySet<string>, slots: ReadonlySet<string>, problems: string[]): CoreGeometry | undefined {
284
+ const before = problems.length;
285
+ switch (kind) {
286
+ case 'region': {
287
+ for (const key of ['x', 'y', 'rotation', 'scaleX', 'scaleY'] as const) if (raw[key] !== undefined && !finite(raw[key])) problems.push(`${where}: ${key} is ${JSON.stringify(raw[key])}, not a finite number`);
288
+ for (const key of ['width', 'height'] as const) if (!finite(raw[key])) problems.push(`${where}: ${key} is ${JSON.stringify(raw[key]) ?? 'absent'}, not a finite number`);
289
+ const hasRect = raw.atlas !== undefined;
290
+ const hasSequence = raw.sequence !== undefined;
291
+ let atlas: ModelAtlasRect | null | undefined;
292
+ let frames: ModelAtlasRect[] | undefined;
293
+ if (hasRect === hasSequence) {
294
+ problems.push(`${where}: the region carries ${hasRect ? 'both an atlas rectangle and a sequence' : 'neither an atlas rectangle nor a sequence'}; the writer states exactly one of the two, and a sequence holds a rectangle per frame (issue #935)`);
295
+ } else if (hasRect) {
296
+ // `readModel` has read the rectangle itself (`readAtlasRect`); here only its value is taken.
297
+ atlas = raw.atlas === null ? null : isRecord(raw.atlas) ? (raw.atlas as unknown as ModelAtlasRect) : undefined;
298
+ } else {
299
+ const seq = raw.sequence;
300
+ if (!isRecord(seq)) problems.push(`${where}.sequence is not an object`);
301
+ else {
302
+ const count = seq.count;
303
+ const setup = seq.setup ?? 0;
304
+ if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) problems.push(`${where}.sequence: count is ${JSON.stringify(count)}, not a whole number of at least 1`);
305
+ else if (typeof setup !== 'number' || !Number.isInteger(setup) || setup < 0 || setup >= count) problems.push(`${where}.sequence: setup is ${JSON.stringify(seq.setup)}, not a frame of the ${count}`);
306
+ if (!Array.isArray(seq.atlas) || seq.atlas.length !== count) problems.push(`${where}.sequence: atlas is not a list of ${JSON.stringify(count)} rectangle(s), one per frame`);
307
+ else {
308
+ const rects = seq.atlas.map((r, i) => readRect(r, `${where}.sequence.atlas[${i}]`, problems));
309
+ if (typeof setup === 'number' && Number.isInteger(setup) && setup >= 0 && setup < rects.length) atlas = rects[setup];
310
+ if (rects.every((r) => r !== undefined)) frames = rects as ModelAtlasRect[];
311
+ }
312
+ }
313
+ }
314
+ if (problems.length !== before || atlas === undefined) return undefined;
315
+ const pick = (key: 'x' | 'y' | 'rotation' | 'scaleX' | 'scaleY'): { [k: string]: number } => (raw[key] === undefined ? {} : { [key]: raw[key] as number });
316
+ return {
317
+ kind: 'region',
318
+ region: { ...pick('x'), ...pick('y'), ...pick('rotation'), ...pick('scaleX'), ...pick('scaleY'), width: raw.width as number, height: raw.height as number, atlas, ...(frames === undefined ? {} : { frames }) },
319
+ };
320
+ }
321
+ case 'linkedmesh': {
322
+ for (const key of ['skin', 'slot', 'source'] as const) if (typeof raw[key] !== 'string' || raw[key] === '') problems.push(`${where}: ${key} is ${JSON.stringify(raw[key]) ?? 'absent'}, not a non-empty string`);
323
+ if (typeof raw.slot === 'string' && !slots.has(raw.slot)) problems.push(`${where}: slot "${raw.slot}" is not a slot of this document`);
324
+ if (problems.length !== before) return undefined;
325
+ return { kind: 'linkedmesh', skin: raw.skin as string, slot: raw.slot as string, source: raw.source as string };
326
+ }
327
+ case 'mesh':
328
+ case 'clipping':
329
+ case 'boundingbox':
330
+ case 'path': {
331
+ const vertices = readVertices(raw.vertices, `${where}.vertices`, bones, problems);
332
+ if (kind === 'clipping' && raw.end !== undefined && (typeof raw.end !== 'string' || !slots.has(raw.end))) problems.push(`${where}: end is ${JSON.stringify(raw.end)}, not a slot of this document`);
333
+ if (kind === 'clipping') for (const key of ['inverse', 'convex'] as const) if (raw[key] !== undefined && typeof raw[key] !== 'boolean') problems.push(`${where}: ${key} is ${JSON.stringify(raw[key])}, not a boolean`);
334
+ const mesh = kind === 'mesh' && vertices !== undefined ? readMeshTriangles(raw, vertices, where, problems) : undefined;
335
+ if (vertices === undefined || problems.length !== before) return undefined;
336
+ if (kind === 'mesh') return mesh === undefined ? undefined : { kind, vertices, ...mesh };
337
+ if (kind === 'clipping') return { kind, end: typeof raw.end === 'string' ? raw.end : null, vertices, inverse: raw.inverse === true, convex: raw.convex === true };
338
+ if (kind === 'boundingbox') return { kind, vertices };
339
+ return readPathGeometry(raw, vertices, where, problems);
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * A mesh's `uvs` and `triangles`, what the clipper reads besides its vertices
346
+ * (issue #964, `./clipping.ts`): one finite pair per vertex, and whole
347
+ * indices into the vertices in threes — as the writer writes them.
348
+ */
349
+ function readMeshTriangles(raw: Record<string, unknown>, vertices: ModelVertices, where: string, problems: string[]): { uvs: number[]; triangles: number[]; hull?: number } | undefined {
350
+ const before = problems.length;
351
+ const count = vertices.weighted ? vertices.bindings.length : vertices.xy.length / 2;
352
+ const uvs = raw.uvs;
353
+ const triangles = raw.triangles;
354
+ if (!Array.isArray(uvs) || !uvs.every(finite) || uvs.length !== 2 * count) problems.push(`${where}.uvs is not ${2 * count} finite numbers, one pair per vertex`);
355
+ if (!Array.isArray(triangles) || triangles.length % 3 !== 0 || !triangles.every((i) => Number.isInteger(i) && i >= 0 && i < count)) problems.push(`${where}.triangles is not a list of whole indices below ${count}, in threes`);
356
+ // The hull's vertex count (issue #966, retained for the raw entry): a whole number no larger than the vertices.
357
+ const hull = raw.hull;
358
+ if (hull !== undefined && !(typeof hull === 'number' && Number.isInteger(hull) && hull >= 0 && hull <= count)) problems.push(`${where}.hull is ${JSON.stringify(hull)}, not a whole number of vertices from 0 to ${count}`);
359
+ return problems.length === before ? { uvs: uvs as number[], triangles: triangles as number[], ...(hull === undefined ? {} : { hull: hull as number }) } : undefined;
360
+ }
361
+
362
+ /**
363
+ * A path attachment's own fields past its vertices (construct 5's second cut,
364
+ * `./constraints_path.ts`): `closed` and `constantSpeed` as the writer states
365
+ * them — absent reads the parser's `false` and `true` (the `pathAttachments`
366
+ * block of a dump of a record stating neither) — and `lengths`, one finite
367
+ * entry per three points, as the writer always writes it (`buildRigPath` in
368
+ * `src/compile.ts`). `vertexCount` must be the vertices' own count and a
369
+ * multiple of 3: the writer refuses any other, and the curve walk reads
370
+ * groups of three.
371
+ */
372
+ function readPathGeometry(raw: Record<string, unknown>, vertices: ModelVertices, where: string, problems: string[]): CoreGeometry | undefined {
373
+ const before = problems.length;
374
+ for (const key of ['closed', 'constantSpeed'] as const) if (raw[key] !== undefined && typeof raw[key] !== 'boolean') problems.push(`${where}: ${key} is ${JSON.stringify(raw[key])}, not a boolean`);
375
+ const count = vertices.weighted ? vertices.bindings.length : vertices.xy.length / 2;
376
+ if (raw.vertexCount !== count) problems.push(`${where}: vertexCount is ${JSON.stringify(raw.vertexCount) ?? 'absent'}, and the vertices hold ${count} point(s)`);
377
+ else if (count % 3 !== 0 || count < (raw.closed === true ? 3 : 6)) problems.push(`${where}: vertexCount is ${count}, not a multiple of 3 of at least ${raw.closed === true ? 3 : 6} — the curve walk reads knots and handles in threes`);
378
+ const lengths = raw.lengths;
379
+ if (!Array.isArray(lengths) || !lengths.every(finite)) problems.push(`${where}: lengths is not a list of finite numbers`);
380
+ else if (lengths.length !== count / 3) problems.push(`${where}: lengths has ${lengths.length} entry(ies) where the vertices make ${count / 3}`);
381
+ if (problems.length !== before) return undefined;
382
+ return { kind: 'path', vertices, closed: raw.closed === true, constantSpeed: raw.constantSpeed !== false, lengths: lengths as number[] };
383
+ }
384
+
385
+ /** One slot as the construct reads it: its name and bone, and what it shows — the record's name, kind and geometry — or nothing. */
386
+ export interface ShownGeometry {
387
+ slot: string;
388
+ bone: string;
389
+ /** The name the oracle's row carries (`shownRow`). */
390
+ name: string;
391
+ placeholder: string;
392
+ skin: string;
393
+ geometry: CoreGeometry;
394
+ /** The deform array a deform timeline set on the slot (`./deform.ts`), or none. */
395
+ deform?: readonly number[];
396
+ /** The frame a sequence timeline set on a region's series (`./deform.ts`), or none: the setup frame. */
397
+ frame?: number;
398
+ }
399
+
400
+ /** Resolves a linked mesh's source to its vertex array, or names why not. */
401
+ export type SourceOf = (skin: string, slot: string, source: string) => ModelVertices | string;
402
+
403
+ /**
404
+ * What the draw walk of `./clipping.ts` needs past the shown records: the
405
+ * draw order (every slot, showing something or not — a clip ends at a slot
406
+ * whatever it shows), the active bones, and a mesh's `uvs` and `triangles`
407
+ * by where a linked mesh's source is filed.
408
+ */
409
+ export interface DrawWalk {
410
+ order: readonly string[];
411
+ active: ReadonlySet<string>;
412
+ meshOf: (skin: string, slot: string, source: string) => { uvs: number[]; triangles: number[] } | undefined;
413
+ clip?: TriangleClipper;
414
+ /** What the drawn rows are cut through (`clipThrough`), replaceable by the render suite's plants. */
415
+ through?: ShapeClipper;
416
+ }
417
+
418
+ /**
419
+ * The two blocks from the slots' shown records, in slot order, every number
420
+ * through `round` (the oracle's, `gridRound` in `./index.ts`), or — for
421
+ * `attachments` — the reason it is absent: a shown region whose rectangle is
422
+ * `null` (the header's ⛔), every one named. With `draw`, also the `clipped`
423
+ * block (issue #964, `./clipping.ts`) from the unrounded geometry, or why it
424
+ * is absent.
425
+ */
426
+ export function poseGeometry(
427
+ shown: readonly ShownGeometry[],
428
+ world: ReadonlyMap<string, CoreWorld>,
429
+ sourceOf: SourceOf,
430
+ round: (v: number) => number | null,
431
+ plant: { region?: RegionPoser; vertices?: VertexPoser } = {},
432
+ draw?: DrawWalk,
433
+ ): { attachments: CoreAttachmentRow[] | null; attachmentsWhy: string | null; clips: CoreClipRow[]; clipped: CoreClippedRow[] | null; clippedWhy: string | null; drawnClipped: CoreClippedRow[] | null; drawnClippedWhy: string | null } {
434
+ const region = plant.region ?? regionCorners;
435
+ const vertices = plant.vertices ?? worldVertices;
436
+ const attachments: CoreAttachmentRow[] = [];
437
+ const clips: CoreClipRow[] = [];
438
+ const nulls: string[] = [];
439
+ const steps = new Map<string, DrawStep>();
440
+ for (const s of shown) {
441
+ const bone = world.get(s.bone);
442
+ if (bone === undefined) throw new Error(`slot "${s.slot}": bone "${s.bone}" has no world transform`);
443
+ const g = s.geometry;
444
+ // A deform array replaces the stored vertices, its coordinates read as they are (`./deform.ts`).
445
+ const drawn = (v: ModelVertices): number[] => (s.deform === undefined ? vertices(v, bone, world) : vertices(deformedVertices(v, s.deform), bone, world, EXACT_COORDS));
446
+ if (g.kind === 'region') {
447
+ const rect = s.frame === undefined ? g.region.atlas : (g.region.frames?.[s.frame] ?? g.region.atlas);
448
+ if (rect === null) {
449
+ nulls.push(`slot "${s.slot}" shows region "${s.name}" (skin "${s.skin}", placeholder "${s.placeholder}")`);
450
+ continue;
451
+ }
452
+ const corners = region({ ...g.region, atlas: rect }, bone);
453
+ attachments.push([s.slot, s.name, 'region', corners.map(round)]);
454
+ steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: corners, triangles: [...REGION_TRIANGLES], uvs: [...REGION_UVS] });
455
+ } else if (g.kind === 'mesh') {
456
+ const world = drawn(g.vertices);
457
+ attachments.push([s.slot, s.name, 'mesh', world.map(round)]);
458
+ steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: world, triangles: g.triangles, uvs: g.uvs });
459
+ } else if (g.kind === 'linkedmesh') {
460
+ const source = sourceOf(g.skin, g.slot, g.source);
461
+ if (typeof source === 'string') throw new Error(`slot "${s.slot}": ${source} (readModel refuses it first)`);
462
+ const world = drawn(source);
463
+ attachments.push([s.slot, s.name, 'mesh', world.map(round)]);
464
+ // A linked mesh draws its source's triangles over its source's UVs.
465
+ const mesh = draw?.meshOf(g.skin, g.slot, g.source);
466
+ if (draw !== undefined && mesh === undefined) throw new Error(`slot "${s.slot}": the linked mesh's source "${g.source}" carries no triangles (readModel refuses it first)`);
467
+ if (mesh !== undefined) steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: world, triangles: mesh.triangles, uvs: mesh.uvs });
468
+ } else if (g.kind === 'clipping') {
469
+ const polygon = drawn(g.vertices);
470
+ clips.push([s.slot, s.name, g.end, polygon.map(round)]);
471
+ steps.set(s.slot, { slot: s.slot, kind: 'clip', attachment: s.name, active: draw?.active.has(s.bone) ?? false, end: g.end, polygon, inverse: g.inverse, convex: g.convex });
472
+ }
473
+ }
474
+ const attachmentsWhy = nulls.length === 0
475
+ ? null
476
+ : `${nulls.join('; ')} — the model states the build had no atlas rectangle for it (atlas: null), and its corners read the trim and original size; a trim of 0 is not assumed`;
477
+ let clipped: CoreClippedRow[] | null = null;
478
+ let clippedWhy: string | null = draw === undefined ? 'the draw order was not given' : attachmentsWhy;
479
+ let drawnClipped: CoreClippedRow[] | null = null;
480
+ let drawnClippedWhy: string | null = clippedWhy;
481
+ if (draw !== undefined && attachmentsWhy === null) {
482
+ const walk = draw.order.map((slot): DrawStep => steps.get(slot) ?? { slot, kind: 'none' });
483
+ const posed = poseClipped(walk, round, draw.clip, draw.through);
484
+ clipped = posed.rows;
485
+ clippedWhy = posed.why;
486
+ drawnClipped = posed.drawn;
487
+ drawnClippedWhy = posed.drawnWhy;
488
+ }
489
+ return { attachments: attachmentsWhy === null ? attachments : null, attachmentsWhy, clips, clipped, clippedWhy, drawnClipped, drawnClippedWhy };
490
+ }