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,739 @@
1
+ /**
2
+ * The core's raw entry (issue #966, step 3b of issue #380): one pose, or one
3
+ * animation walked at the caller's own steps, returned as the doubles the
4
+ * core computed — no grid — with everything a renderer or a deform measurer
5
+ * reads past the oracle's rows: each bone's world rotation and scale
6
+ * readings, each drawn attachment's local UVs, triangles, hull, sequence
7
+ * frame and colour, the clip polygons, the triangles drawn under a clip, and
8
+ * the events fired.
9
+ *
10
+ * The oracle's document (`tools/pose_oracle.ts`) samples an animation on a
11
+ * phase grid, re-applying it from the setup pose at every sample; the two
12
+ * consumers step instead. `src/render.ts` resets a track at 0 and then takes
13
+ * exactly one step of `1/fps` per frame; `src/deformmeasure.ts` resets and
14
+ * takes one step of `time`. So the entry takes a list of steps rather than a
15
+ * list of times, and this header states what one step is, as measured.
16
+ *
17
+ * ## One step — the walk, measured
18
+ *
19
+ * Measured against spine-core 4.3.13 running `src/render.ts`'s recipe
20
+ * (`AnimationState` with one non-looping track: frame 0 `apply`,
21
+ * `Skeleton.update(0)`, `updateWorldTransform(Physics.reset)`; every later
22
+ * frame `AnimationState.update(dt)`, `apply`, `Skeleton.update(dt)`,
23
+ * `updateWorldTransform(Physics.update)`), read off `TrackEntry.trackTime`,
24
+ * `TrackEntry.getAnimationTime()` and every bone's `appliedPose` in full
25
+ * doubles (the PR of issue #966 carries the table):
26
+ *
27
+ * - **The track time is the running sum of the steps**, `T += dt` in double,
28
+ * not `i·dt`, and the animation is applied at the sum. Over every
29
+ * animation of the nineteen tree rows `TrackEntry.trackTime` equalled the
30
+ * running sum on 2,612 of 2,612 frames at 12 fps (12,850 of 12,850 at 60,
31
+ * 5,173 of 5,173 at 24) and `i·dt` on only 662 (1,168; 810): at 12 fps
32
+ * frame 6 reads `0.49999999999999994`, not `0.5`, and frame 10
33
+ * `0.8333333333333334`, not `0.8333333333333333`. `src/render.ts` files a
34
+ * frame under `i·dt`, which is its label and not the time posed.
35
+ * - **A non-looping track holds at its duration**: the animation time is
36
+ * `min(T, duration)`, where `duration` is the runtime's (the last key of
37
+ * every timeline, as float32 — `CoreAnimationTimelines.duration`). With
38
+ * `round(duration·fps)` frames the last one overshoots on 20 animation
39
+ * frames of the 2,612 at 12 fps (19 at 60, 25 at 24), and
40
+ * `getAnimationTime()` read the duration on every one of them.
41
+ * - **The physics clock moves by the step itself**, `time += dt` (the
42
+ * skeleton's `update`), and a physics constraint integrates the time it
43
+ * moved (`./constraints_physics.ts`). Pose 0 is the reset, every physics
44
+ * state reset there (where, below).
45
+ * - **Events fire over `(previous animation time, animation time]`**, the
46
+ * first step's interval opening at −1 — so held past the duration, a step
47
+ * fires nothing. Measured against `AnimationState`'s event listener on
48
+ * every frame above.
49
+ * - **Where the reset is taken is the caller's** (`RawReset`): render's
50
+ * frame 0 applies the animation at 0 and resets there (`animation`);
51
+ * deformmeasure's `poseAt` resets at the setup pose and then applies the
52
+ * animation in its one step (`setup`). On a physics rig the two differ by
53
+ * whole units, not bits: of the 162 one-jump poses (three per animation),
54
+ * 12 read off under `animation` — six on `7-anticipation`'s physics rig (a
55
+ * cape bone 1.7° off at 0.617 s) and six on `gallery/look`'s — and none
56
+ * under `setup`.
57
+ *
58
+ * Every step poses the animation from the setup pose at its animation time
59
+ * (the setup blend at alpha 1), as the oracle's stepped dump does, applied
60
+ * from the animation time of the step before (−1 before the first): a
61
+ * physics `reset` key fires on the step that crosses it, once. Until issue
62
+ * #960 this walk fired such a key at every step from it on (the oracle's
63
+ * reading then), and against the recipe above a probe with a key at 0.5
64
+ * read 32 of 61 frames bit-exact at 60 fps, the rest off by up to 53 units;
65
+ * with the key crossed once, 61 of 61 at 60 fps and 13 of 13 at 12. None of
66
+ * the nineteen tree rows keys a `reset`, which is why the raw gate never
67
+ * saw it. The one difference from `--physics step` is where the steps fall,
68
+ * which is the caller's.
69
+ *
70
+ * ## What a pose carries
71
+ *
72
+ * - `bones` — the world matrix and origin, and the four readings the
73
+ * runtime's getters give: `rotationX = atan2(c, a)·DEG`, `rotationY =
74
+ * atan2(d, b)·DEG`, `scaleX = √(a² + c²)`, `scaleY = √(b² + d²)`, with
75
+ * `DEG = 180 / π` at the runtime's π (`RUNTIME_DEG` in `./world.ts`) —
76
+ * measured bit-exact against `BonePose.getWorldRotationX/Y` and
77
+ * `getWorldScaleX/Y` on every bone of 2,793 poses of the nineteen tree
78
+ * rows (the setup, every 12 fps frame, three jumps per animation).
79
+ * - `slots` — the oracle's slot row as doubles (`posedSlots`).
80
+ * - `drawn` — every region and mesh a slot shows, in draw order: its world
81
+ * vertices (the oracle's `attachments` row as doubles), its LOCAL UVs as the
82
+ * runtime holds them (a region's unit corners `0 1, 0 0, 1 0, 1 1` in the
83
+ * corner order; a mesh's `uvs` — a linked mesh's source's — as the
84
+ * document spells them: `MeshAttachment.regionUVs` read the doubles, not
85
+ * their float32, on every mesh of the nineteen rows — `6-arcs`'
86
+ * `2.554152e-7` where `Math.fround` gives `2.5541518766658555e-7`), its
87
+ * triangles (a region's `0 1 2 2
88
+ * 3 0`), its hull (a mesh's; `null` for a region), its sequence frame (the
89
+ * frame a sequence timeline set, else the series' setup frame, else 0 —
90
+ * the runtime gives every region and mesh a series, one frame long when
91
+ * the record states none, and `Sequence.resolveIndex` read 0 on every such
92
+ * attachment of the nineteen rows) and its own colour (the record's `color`, white
93
+ * unstated) — so the caller forms the tint, slot colour times attachment
94
+ * colour, itself. Where a region sits on a page is not the model's
95
+ * (`ModelAtlasRect`'s 🔸); the page UVs are issue #967's.
96
+ * - `clips`, `clipped` — the oracle's rows as doubles; `clipped` is what the
97
+ * core DRAWS, which is the oracle's block wherever the core poses it and,
98
+ * under a clip that is not strictly convex or is inverse (a block the core
99
+ * leaves out), the core's own convex decomposition (`clipThrough` in
100
+ * `./clipping.ts`, issue #964), held to spine-core by the render's pixels.
101
+ * - `events` — the oracle's event rows as doubles.
102
+ *
103
+ * 🔁 **A second walk shares this one** (issue #1025, cut 4c-5a): A10's
104
+ * LOOPING walk, which keeps a non-finite value rather than refusing it, is
105
+ * `./walk.ts`; both are `walkIn` below in a `WalkMode`, and every difference
106
+ * is a branch on the mode — so this entry's walk is the one it was, and the
107
+ * raw gate's lines did not move.
108
+ *
109
+ * ⛔ **Nothing is posed that the core would leave out.** Where the oracle's
110
+ * core dump names a block absent — a constraint the core cannot pose
111
+ * exactly, skins disagreeing over a placeholder under the merged view, a
112
+ * region with no atlas rectangle — the raw entry refuses the whole call,
113
+ * naming why (`CoreInputError`): a renderer handed a pose with a block
114
+ * missing would draw a different picture in silence.
115
+ */
116
+ import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber, readColour, shownAttachment, sourceOfDoc, underNoSkin, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
117
+ import { posedBoneWorld, posedBoneWorldAlone, posedSlots, type TimelinePlant } from './animation.ts';
118
+ import { constraintsAbsentWhy, pathAnimationsWhy } from './constraints.ts';
119
+ import { freshStepContext, steppedPreviousPassWhy } from './constraints_physics.ts';
120
+ import { attachmentStates } from './deform.ts';
121
+ import { drawOrderAt } from './draw_order.ts';
122
+ import { eventsFired } from './events.ts';
123
+ import { REGION_TRIANGLES, REGION_UVS, type CoreClippedRow, type ShapeClipper } from './clipping.ts';
124
+ import { poseGeometry, type CoreAttachmentRow, type CoreClipRow, type ShownGeometry } from './vertices.ts';
125
+ import { RUNTIME_DEG, type CoreWorld } from './world.ts';
126
+ import type { SliderApplication } from './constraints_slider.ts';
127
+
128
+ /** One bone, posed: the world transform and the runtime getters' four readings (the header's `bones`). */
129
+ export interface RawBone {
130
+ name: string;
131
+ parent: string | null;
132
+ active: boolean;
133
+ a: number;
134
+ b: number;
135
+ c: number;
136
+ d: number;
137
+ worldX: number;
138
+ worldY: number;
139
+ rotationX: number;
140
+ rotationY: number;
141
+ scaleX: number;
142
+ scaleY: number;
143
+ }
144
+
145
+ /** One region or mesh drawn, in draw order (the header's `drawn`). */
146
+ export interface RawDrawn {
147
+ slot: string;
148
+ attachment: string;
149
+ kind: 'region' | 'mesh';
150
+ vertices: number[];
151
+ uvs: number[];
152
+ triangles: number[];
153
+ hull: number | null;
154
+ sequenceIndex: number;
155
+ colour: [number, number, number, number];
156
+ }
157
+
158
+ /** One clip polygon: `[slot, attachment, end, polygon]` as doubles. */
159
+ export type RawClip = [string, string, string | null, number[]];
160
+ /** One attachment drawn under a clip: `[slot, attachment, clipped, vertices, uvs, triangles]` as the clipper returns them. */
161
+ export type RawClipped = [string, string, 0 | 1, number[], number[], number[]];
162
+ /** One event fired: `[name, time, int, float, string]`. */
163
+ export type RawEvent = [string, number, number, number, string | null];
164
+
165
+ /**
166
+ * Where a walk's physics is reset (the header's *One step*): `animation` —
167
+ * the animation applied at 0 and the reset taken there (`src/render.ts`'s
168
+ * frame 0); `setup` — the reset taken at the setup pose before the animation
169
+ * is applied (`src/deformmeasure.ts`'s `poseAt`), pose 0 then the setup pose.
170
+ */
171
+ export type RawReset = 'animation' | 'setup';
172
+
173
+ /** One pose — the setup's, or one step of an animation's walk. */
174
+ export interface RawPose {
175
+ /** The track time (the running sum of the steps), 0 for the setup pose. */
176
+ trackTime: number;
177
+ /** The time the animation was applied at: the track time held at the duration. */
178
+ animationTime: number;
179
+ bones: RawBone[];
180
+ slots: CoreSlotRow[];
181
+ drawOrder: string[];
182
+ drawn: RawDrawn[];
183
+ clips: RawClip[];
184
+ clipped: RawClipped[];
185
+ events: RawEvent[];
186
+ // --- #968 render: begin ---
187
+ /**
188
+ * What each slot shows, in draw order — the records `drawn` was posed from
189
+ * (skin, placeholder, series frame, deform). `src/render_core.ts` resolves
190
+ * each drawn attachment's atlas region and page UVs through it, with
191
+ * `./uvs.ts`'s `drawnRegions` (issue #967's gated rule), rather than walking
192
+ * which record a slot shows a second time.
193
+ */
194
+ shown: ShownGeometry[];
195
+ // --- #968 render: end ---
196
+ }
197
+
198
+ /**
199
+ * One bone as the scan reads it (issue #1179, the second part): the world
200
+ * matrix and origin `RawBone` carries, and none of the readings it forms from
201
+ * them. `A10_NO_NAN_AFTER_STEPPING` reads these six numbers and the name off a
202
+ * bone, and nothing else (`firstNonFinite` in `src/nonfinite.ts`).
203
+ */
204
+ export interface ScanBone {
205
+ name: string;
206
+ a: number;
207
+ b: number;
208
+ c: number;
209
+ d: number;
210
+ worldX: number;
211
+ worldY: number;
212
+ }
213
+
214
+ /**
215
+ * One pose as the scan reads it (issue #1179, the second part) — the scan's
216
+ * walk (`scanWalkIn`, `scanSetupIn`): the time the animation was applied at,
217
+ * and the bones, slot rows and drawn attachments computed exactly as a
218
+ * `RawPose`'s are. Internal to A10's walk (`./walk.ts`); no exported pose type
219
+ * changed for it.
220
+ */
221
+ export interface ScanPose {
222
+ animationTime: number;
223
+ bones: ScanBone[];
224
+ slots: CoreSlotRow[];
225
+ drawn: ScanDrawn[];
226
+ }
227
+
228
+ /** One region or mesh drawn, as the scan reads it: its slot, its name and its world vertices, as `RawDrawn` carries them. */
229
+ export interface ScanDrawn {
230
+ slot: string;
231
+ attachment: string;
232
+ vertices: number[];
233
+ }
234
+
235
+ // --- #968 render: begin ---
236
+ /** The shown records in `order`, the pose's draw order (a pose's `shown`). */
237
+ function drawOrderOf(shown: readonly ShownGeometry[], order: readonly string[]): ShownGeometry[] {
238
+ const rank = new Map(order.map((n, r) => [n, r]));
239
+ return [...shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
240
+ }
241
+ // --- #968 render: end ---
242
+
243
+ /** A double the entry computed: never `null`, since a non-finite pose is refused rather than passed on. */
244
+ function finite(v: number | null, what: string): number {
245
+ if (v === null) throw new CoreInputError(`the raw pose computed a non-finite ${what}`);
246
+ return v;
247
+ }
248
+
249
+ /**
250
+ * How a walk treats the two things the raw entry and the looping walk
251
+ * (`./walk.ts`, issue #1025) read differently: whether the track loops, and
252
+ * whether a value that is not finite is kept in the pose or refuses the call.
253
+ * The raw entry is `{ loop: false, keep: false }`, and that is its contract.
254
+ */
255
+ export interface WalkMode {
256
+ loop: boolean;
257
+ keep: boolean;
258
+ /** A plant only (the core suite's `CO26`): the animation time a looping step applies at, from its track time, the duration and the track time before it — `loopedTime` unless given. */
259
+ time?: (trackTime: number, duration: number, before: number) => number;
260
+ /** A plant only (`CO26`): what the physics clock moves by, from the step and how far the animation time moved — the step itself unless given. */
261
+ clock?: (dt: number, moved: number) => number;
262
+ /** A plant only (`CO26`): `false` fires no `reset` key across the wrap — the reading the walk was measured against and rejected. */
263
+ wrapResets?: boolean;
264
+ }
265
+
266
+ /** The raw entry's mode: a track that holds at its duration, and a non-finite value refused. */
267
+ const RAW_MODE: WalkMode = { loop: false, keep: false };
268
+
269
+ /** What a kept walk writes a number as: the double itself, finite or not — so a NaN and an Infinity stay what they are. */
270
+ const keptNumber = (v: number): number => v;
271
+
272
+ /** The plant a walk in `mode` poses with: the raw entry's unrounded double, or under `keep` the number whatever it is. */
273
+ const roundPlant = (mode: WalkMode): CorePlant => ({ round: mode.keep ? keptNumber : rawNumber });
274
+
275
+ /** A double a kept walk computed, as it is; a raw walk's, refused when it is not finite (`finite`). */
276
+ function numberOf(v: number | null, what: string, mode: WalkMode): number {
277
+ return mode.keep ? (v === null ? Number.NaN : v) : finite(v, what);
278
+ }
279
+
280
+ /**
281
+ * What the looping walk cuts a drawn attachment through under a clip: nothing
282
+ * (issue #1179). Its one reader, A10, reads no clipped row — `./walk.ts`
283
+ * narrows them away with the events — so the walk does not cut them; what it
284
+ * still does is plan every clip it starts (`poseClipped`'s `clipShapeOf`), so
285
+ * a clip the core does not draw refuses the walk by name as before. Every
286
+ * number a looping pose carries is computed as it was.
287
+ */
288
+ const NO_CLIPPED_ROWS: ShapeClipper = () => null;
289
+
290
+ /** The bones of a pose from the world transforms (the header's `bones`). */
291
+ function rawBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>): RawBone[] {
292
+ const active = activeBones(doc);
293
+ return doc.bones.map((b): RawBone => {
294
+ const w = world.get(b.name);
295
+ if (w === undefined) throw new CoreInputError(`bone "${b.name}" has no world transform`);
296
+ return {
297
+ name: b.name,
298
+ parent: b.parent ?? null,
299
+ active: active.has(b.name),
300
+ a: w.a,
301
+ b: w.b,
302
+ c: w.c,
303
+ d: w.d,
304
+ worldX: w.worldX,
305
+ worldY: w.worldY,
306
+ rotationX: Math.atan2(w.c, w.a) * RUNTIME_DEG,
307
+ rotationY: Math.atan2(w.d, w.b) * RUNTIME_DEG,
308
+ scaleX: Math.sqrt(w.a * w.a + w.c * w.c),
309
+ scaleY: Math.sqrt(w.b * w.b + w.d * w.d),
310
+ };
311
+ });
312
+ }
313
+
314
+ /**
315
+ * The bones of a scan pose (`ScanBone`): `rawBones` less the parent, the
316
+ * active flag and the four getter readings, which nothing the scan reads is
317
+ * computed from — each reading is formed from the world after it is posed,
318
+ * and the matrix and origin are copied as they are.
319
+ */
320
+ function scanBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>): ScanBone[] {
321
+ return doc.bones.map((b): ScanBone => {
322
+ const w = world.get(b.name);
323
+ if (w === undefined) throw new CoreInputError(`bone "${b.name}" has no world transform`);
324
+ return { name: b.name, a: w.a, b: w.b, c: w.c, d: w.d, worldX: w.worldX, worldY: w.worldY };
325
+ });
326
+ }
327
+
328
+ /**
329
+ * What a pose draws, posed and refused where the core leaves it out: the
330
+ * attachment rows, the clip rows and the drawn clipped rows — what `rawDrawn`
331
+ * and `scanDrawn` both assemble from.
332
+ */
333
+ function drawnGeometry(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode): { rows: CoreAttachmentRow[]; clips: CoreClipRow[]; drawnClipped: CoreClippedRow[] } {
334
+ const draw = drawWalkOf(doc, order, plant);
335
+ const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, mode.loop && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
336
+ if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
337
+ // The drawn rows (issue #964): the oracle's `clipped` rows where it is posed, and a concave or inverse clip cut through the core's own decomposition.
338
+ if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
339
+ return { rows: geometry.attachments, clips: geometry.clips, drawnClipped: geometry.drawnClipped };
340
+ }
341
+
342
+ /**
343
+ * The drawn attachments of a scan pose (`ScanDrawn`): `rawDrawn`'s rows, each
344
+ * its slot, its name and its world vertices through the same `numberOf`, and
345
+ * none of what `rawDrawn` reads past them for a renderer — the UVs and
346
+ * triangles it copies, the record it looks up for the colour and the sequence
347
+ * frame, the hull, the clip rows' numbers. No vertex is computed from any of
348
+ * those. The one refusal `rawDrawn` makes past the rows, a linked mesh whose
349
+ * source carries no mesh, is not reachable here: `poseGeometry` resolved the
350
+ * same source by the same lookup (`sourceOfDoc`) and throws before it returns.
351
+ *
352
+ * 🔁 **No copy of the vertices** (issue #1179, the second part): `rawDrawn`
353
+ * maps each row through `numberOf` into a new array; under `keep` that map is
354
+ * the identity on every number and turns only a `null` into NaN. The scan
355
+ * hands the row's own array on wherever it holds no `null` (`allNumbers`) and
356
+ * maps it as before where it does, so A10 reads the same values either way.
357
+ * What makes the row's array safe to keep past the step — A10 collects an
358
+ * animation's 120 frames before it scans one — is that it is the pose's own:
359
+ * `poseGeometry` (`./vertices.ts`) builds every row's vertices with
360
+ * `.map(round)` over the corners or the skinned vertices it just computed, a
361
+ * new array per attachment per pose, held by nothing but the row. The core
362
+ * suite's `CO45` plants a poser that hands every pose the same array and reads
363
+ * the retained frames red.
364
+ */
365
+ function scanDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode, scanPlant: ScanPlant = {}): ScanDrawn[] {
366
+ const { rows } = drawnGeometry(doc, shown, world, order, plant, mode);
367
+ const drawn: ScanDrawn[] = [];
368
+ let k = 0;
369
+ for (const s of shown) {
370
+ const g = s.geometry;
371
+ if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') continue;
372
+ const row = rows[k++];
373
+ if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
374
+ const vertices = row[3];
375
+ const kept = mode.keep && allNumbers(vertices) ? vertices : vertices.map((v) => numberOf(v, `vertex of slot "${s.slot}"`, mode));
376
+ drawn.push({ slot: s.slot, attachment: row[1], vertices: scanPlant.vertices === undefined ? kept : scanPlant.vertices(kept, s.slot) });
377
+ }
378
+ return drawn;
379
+ }
380
+
381
+ /** Whether a row's vertices hold no `null` — what `numberOf` would map — so the row's own array can be handed on as numbers. */
382
+ function allNumbers(values: Array<number | null>): values is number[] {
383
+ for (const v of values) if (v === null) return false;
384
+ return true;
385
+ }
386
+
387
+ /**
388
+ * The scan's one plant (the core suite's `CO45`): what a drawn row's vertices
389
+ * are handed on as, given the array the scan would hand on. A poser that
390
+ * reuses one buffer per slot across steps is the plant `CO45` passes.
391
+ * Nothing but a control passes one.
392
+ */
393
+ export interface ScanPlant {
394
+ vertices?: (vertices: number[], slot: string) => number[];
395
+ }
396
+
397
+ /** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
398
+ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode = RAW_MODE): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
399
+ const geometry = drawnGeometry(doc, shown, world, order, plant, mode);
400
+ // Under a concave or inverse clip the oracle's `clipped` block is absent (the runtime's own triangle list); what the core draws is `drawnClipped`,
401
+ // its own decomposition, and the render samples each drawn triangle at its source triangle's affine UV, so the pixels are the decomposition's
402
+ // coverage alone (issue #964, `Mesh.source` in src/render.ts).
403
+ const rows = geometry.rows;
404
+ const drawn: RawDrawn[] = [];
405
+ let k = 0;
406
+ for (const s of shown) {
407
+ const g = s.geometry;
408
+ if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') continue;
409
+ const row = rows[k++];
410
+ if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
411
+ const record = doc.skins.find((x) => x.name === s.skin)?.attachments[s.slot]?.[s.placeholder];
412
+ const colour = record?.color === undefined ? ([1, 1, 1, 1] as [number, number, number, number]) : readColour(record.color);
413
+ let uvs: number[];
414
+ let triangles: number[];
415
+ let hull: number | null = null;
416
+ if (g.kind === 'region') {
417
+ uvs = [...REGION_UVS];
418
+ triangles = [...REGION_TRIANGLES];
419
+ } else {
420
+ const source = g.kind === 'mesh' ? g : doc.skins.find((x) => x.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
421
+ if (source === undefined || source.kind !== 'mesh') throw new CoreInputError(`slot "${s.slot}": the linked mesh's source carries no mesh`);
422
+ uvs = [...source.uvs];
423
+ triangles = [...source.triangles];
424
+ hull = source.hull ?? null;
425
+ }
426
+ const sequenceIndex = s.frame ?? record?.sequenceSetup ?? 0;
427
+ drawn.push({ slot: s.slot, attachment: row[1], kind: row[2], vertices: row[3].map((v) => numberOf(v, `vertex of slot "${s.slot}"`, mode)), uvs, triangles, hull, sequenceIndex, colour });
428
+ }
429
+ const clips = geometry.clips.map((c): RawClip => [c[0], c[1], c[2], c[3].map((v) => numberOf(v, `clip vertex of slot "${c[0]}"`, mode))]);
430
+ const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], c[3].map((v) => numberOf(v, `clipped vertex of slot "${c[0]}"`, mode)), c[4].map((v) => numberOf(v, `clipped uv of slot "${c[0]}"`, mode)), c[5]]);
431
+ return { drawn, clips, clipped };
432
+ }
433
+
434
+ /**
435
+ * The setup pose as the renderer poses a skeleton with no animation — the
436
+ * setup pose, every physics state reset — in full doubles. Refused by name
437
+ * where the core leaves a block of it out.
438
+ */
439
+ export function poseRawSetup(doc: CompiledDocument, plant: CorePlant = {}): RawPose {
440
+ return setupPoseIn(doc, plant, RAW_MODE);
441
+ }
442
+
443
+ /** `poseRawSetup` in `mode` — under `keep`, a value that is not finite stays in the pose (`./walk.ts`). */
444
+ export function setupPoseIn(doc: CompiledDocument, plant: CorePlant, mode: WalkMode): RawPose {
445
+ const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
446
+ const drawn = rawDrawn(doc, shown, world, drawOrder, plant, mode);
447
+ return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, drawOrder) };
448
+ }
449
+
450
+ /** `setupPoseIn` as the scan reads it (`ScanPose`): the same setup pose, the same refusals in the same order, assembled into the scan's pose. */
451
+ export function scanSetupIn(doc: CompiledDocument, plant: CorePlant, mode: WalkMode, scanPlant: ScanPlant = {}): ScanPose {
452
+ const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
453
+ const drawn = scanDrawn(doc, shown, world, drawOrder, plant, mode, scanPlant);
454
+ return { animationTime: 0, bones: scanBones(doc, world), slots, drawn };
455
+ }
456
+
457
+ /** The setup pose `setupPoseIn` and `scanSetupIn` assemble, refused by name where the core leaves a block of it out. */
458
+ function setupPosed(doc: CompiledDocument, plant: CorePlant, mode: WalkMode): { world: Map<string, CoreWorld>; shown: ShownGeometry[]; slots: CoreSlotRow[]; drawOrder: string[] } {
459
+ const posed = poseSetup(doc, { ...plant, ...roundPlant(mode) }, freshStepContext(plant.physicsStep));
460
+ // `setup.clipped` is the oracle's block, left out under a concave or inverse clip whose triangle list is the runtime's own; what the core draws there is `rawDrawn`'s to refuse or pose (issue #964).
461
+ const absent = posed.absent.filter(([block]) => block !== 'setup.clipped');
462
+ if (absent.length > 0) throw new CoreInputError(`the raw setup pose leaves ${absent.map(([b, why]) => `${b} out (${why})`).join('; ')}`);
463
+ const { setup, world, shown } = posed;
464
+ if (world === null || shown === null || setup.slots === null || setup.drawOrder === null) throw new CoreInputError('the raw setup pose was not posed');
465
+ return { world, shown, slots: setup.slots, drawOrder: setup.drawOrder };
466
+ }
467
+
468
+ /**
469
+ * One animation walked at the caller's steps (the header's *One step*): the
470
+ * reset pose at 0, then one pose per step of `steps` — `steps.length + 1`
471
+ * poses in all. `src/render.ts`'s walk is `count` steps of `1/fps`;
472
+ * `src/deformmeasure.ts`'s is one step of `time`. A step must be a finite
473
+ * time at or above 0; the animation must be the document's; a construct the
474
+ * core would leave out refuses the call by name. `reset` is where the
475
+ * physics is reset (`RawReset`): `animation` for render's walk, `setup` for
476
+ * deformmeasure's one jump.
477
+ */
478
+ export function poseRawAnimation(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant = {}, reset: RawReset = 'animation'): RawPose[] {
479
+ return walkIn(doc, animation, steps, plant, reset, RAW_MODE);
480
+ }
481
+
482
+ /**
483
+ * `poseRawAnimation`, each pose handed to `visit` as it is posed rather than
484
+ * collected (issue #1180): the same walk, the same poses in the same order,
485
+ * and the caller holds as many of them as it keeps. `src/render_core.ts`
486
+ * draws each and lets it go, so a render holds one pose and not an
487
+ * animation's series — on the production rig the core poser's render peaked
488
+ * highest on, the largest animation's series was 153 MiB of retained heap.
489
+ * A refusal is thrown from the pose it is met at, after `visit` has seen the
490
+ * poses before it.
491
+ */
492
+ export function poseRawAnimationEach(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, visit: (pose: RawPose, index: number) => void): void {
493
+ walkEach(doc, animation, steps, plant, reset, RAW_MODE, RAW_SHAPE, visit);
494
+ }
495
+
496
+ /**
497
+ * The animation time a looping track applies its animation at (`./walk.ts`,
498
+ * *The time*): the track time wrapped by the duration, and 0 over a duration
499
+ * of 0.
500
+ */
501
+ export function loopedTime(trackTime: number, duration: number): number {
502
+ return duration === 0 ? 0 : trackTime % duration;
503
+ }
504
+
505
+ /**
506
+ * `poseRawAnimation` in `mode`: the raw entry's walk, or the looping walk
507
+ * `./walk.ts` documents — the animation time the track time wrapped by the
508
+ * duration, every value kept whatever it is. Each difference is a branch on
509
+ * `mode` here, so the raw entry's walk is the one it was.
510
+ */
511
+ export function walkIn(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode): RawPose[] {
512
+ const poses: RawPose[] = [];
513
+ walkEach(doc, animation, steps, plant, reset, mode, RAW_SHAPE, (pose) => poses.push(pose));
514
+ return poses;
515
+ }
516
+
517
+ /**
518
+ * `walkIn` as the scan reads it (`ScanPose`, issue #1179, the second part):
519
+ * the same walk — every pose posed by the same operations in the same order,
520
+ * every refusal at the same place — each pose assembled into what A10 reads.
521
+ * `./walk.ts`'s `scanLoopingWalk` is its one caller.
522
+ */
523
+ export function scanWalkIn(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode, scanPlant: ScanPlant = {}): ScanPose[] {
524
+ const poses: ScanPose[] = [];
525
+ walkEach(doc, animation, steps, plant, reset, mode, scanPlant.vertices === undefined ? SCAN_SHAPE : scanShape(scanPlant), (pose) => poses.push(pose));
526
+ return poses;
527
+ }
528
+
529
+ /** What one step of a walk posed, before a pose is assembled from it (`PoseShape`). */
530
+ interface WalkStep {
531
+ trackTime: number;
532
+ animationTime: number;
533
+ world: Map<string, CoreWorld>;
534
+ slots: CoreSlotRow[];
535
+ drawOrder: string[];
536
+ shown: ShownGeometry[];
537
+ /** The events the step fired, computed when called — after the drawn rows, as the raw entry has always ordered its refusals. */
538
+ events: () => RawEvent[];
539
+ }
540
+
541
+ /**
542
+ * What a walk assembles each pose into (issue #1179, the second part): the raw
543
+ * entry's whole pose (`RAW_SHAPE`), or the scan's (`SCAN_SHAPE`). The walk
544
+ * (`walkEach`) is one body: every number a pose carries is computed there and
545
+ * in the two assemblers below, by the same calls, whatever the shape. A shape
546
+ * chooses only what is assembled from what was posed — the scan drops the
547
+ * readings, rows and copies nothing it reads is computed from — and keeps the
548
+ * order of every call that can refuse.
549
+ */
550
+ interface PoseShape<P> {
551
+ /** Whether a step forms the oracle's bone rows beside its world (`posedBoneWorld`), which no walk pose carries; `false` poses the world alone (`posedBoneWorldAlone`). */
552
+ rows: boolean;
553
+ /** The reset pose at the setup (`reset: 'setup'`). */
554
+ setup: (doc: CompiledDocument, world: Map<string, CoreWorld>, slots: CoreSlotRow[], drawOrder: string[], shown: ShownGeometry[], plant: CorePlant, mode: WalkMode) => P;
555
+ /** One step's pose. */
556
+ step: (doc: CompiledDocument, posed: WalkStep, plant: CorePlant, mode: WalkMode) => P;
557
+ }
558
+
559
+ /** The raw entry's pose, assembled as it always was: at the reset the bones, then the drawn rows; at a step the drawn rows, the events, then the bones. */
560
+ const RAW_SHAPE: PoseShape<RawPose> = {
561
+ rows: true,
562
+ setup: (doc, world, slots, drawOrder, shown, plant, mode) => ({ trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...rawDrawn(doc, shown, world, drawOrder, plant, mode), events: [], shown: drawOrderOf(shown, drawOrder) }),
563
+ step: (doc, p, plant, mode) => {
564
+ const drawn = rawDrawn(doc, p.shown, p.world, p.drawOrder, plant, mode);
565
+ const events = p.events();
566
+ return { trackTime: p.trackTime, animationTime: p.animationTime, bones: rawBones(doc, p.world), slots: p.slots, drawOrder: p.drawOrder, ...drawn, events, shown: p.shown };
567
+ },
568
+ };
569
+
570
+ /** The scan's pose (`ScanPose`), in the raw shape's order: the scan's walk is a looping one, which fires no event. */
571
+ function scanShape(scanPlant: ScanPlant): PoseShape<ScanPose> {
572
+ return {
573
+ rows: false,
574
+ setup: (doc, world, slots, drawOrder, shown, plant, mode) => {
575
+ const bones = scanBones(doc, world);
576
+ return { animationTime: 0, bones, slots, drawn: scanDrawn(doc, shown, world, drawOrder, plant, mode, scanPlant) };
577
+ },
578
+ step: (doc, p, plant, mode) => {
579
+ const drawn = scanDrawn(doc, p.shown, p.world, p.drawOrder, plant, mode, scanPlant);
580
+ return { animationTime: p.animationTime, bones: scanBones(doc, p.world), slots: p.slots, drawn };
581
+ },
582
+ };
583
+ }
584
+ const SCAN_SHAPE: PoseShape<ScanPose> = scanShape({});
585
+
586
+ /** `walkIn`'s walk, each pose assembled in `shape` and handed to `visit` in order as it is posed (`poseRawAnimationEach`). */
587
+ function walkEach<P>(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode, shape: PoseShape<P>, visit: (pose: P, index: number) => void): void {
588
+ const anim = doc.animations.find((a) => a.name === animation);
589
+ if (anim === undefined) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(', ')}]`);
590
+ const bad = steps.findIndex((s) => !Number.isFinite(s) || s < 0);
591
+ if (bad >= 0) throw new CoreInputError(`step ${bad} is ${steps[bad]}, not a finite time at or above 0`);
592
+ const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
593
+ if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
594
+ const raw: TimelinePlant = { ...plant, ...roundPlant(mode) };
595
+ const resolve = plant.shown ?? shownAttachment;
596
+ const duration = anim.timelines.duration;
597
+ const ctx = freshStepContext(plant.physicsStep);
598
+ if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
599
+ let visited = 0;
600
+ let trackTime = 0;
601
+ let last = -1;
602
+ if (reset === 'setup') {
603
+ // The reset taken at the setup pose, before the animation is applied (`src/deformmeasure.ts`'s `poseAt`): pose 0 is the setup pose.
604
+ const setup = poseSetup(doc, { ...plant, ...roundPlant(mode) }, ctx);
605
+ const absent = setup.absent.filter(([block]) => block !== 'setup.clipped');
606
+ if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join('; ')}`);
607
+ if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError('the raw walk\'s reset pose was not posed');
608
+ ctx.phase = 'update';
609
+ visit(shape.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, mode), visited++);
610
+ }
611
+ for (let i = reset === 'setup' ? 1 : 0; i <= steps.length; i++) {
612
+ const dt = i === 0 ? 0 : steps[i - 1];
613
+ trackTime += dt;
614
+ const t = mode.loop ? (mode.time ?? loopedTime)(trackTime, duration, trackTime - dt) : trackTime < duration ? trackTime : duration;
615
+ const before = ctx.time;
616
+ ctx.time += mode.clock === undefined ? dt : mode.clock(dt, t - (i === 0 ? 0 : Math.max(last, 0)));
617
+ const sliders: SliderApplication[] = [];
618
+ const world = shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
619
+ if (i === 0) ctx.phase = 'update';
620
+ const placeholders = new Map<string, string | null>();
621
+ const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
622
+ if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join('; ')}`);
623
+ const evalOrder = plant.drawOrder ?? drawOrderAt;
624
+ let order = evalOrder(doc.slots.length, anim.timelines.drawOrder, t) ?? doc.slots.map((_s, k) => k);
625
+ for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
626
+ const drawOrder = order.map((k) => doc.slots[k].name);
627
+ const states = attachmentStates(doc, resolve, placeholders, { timelines: anim.timelines, t }, sliders, plant);
628
+ if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
629
+ const rank = new Map(drawOrder.map((n, r) => [n, r]));
630
+ const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
631
+ // A looping walk fires no event here: what fires across the wrap was not measured, and the walk's one reader (A10) reads none. Nor does it cut its
632
+ // drawn attachments through a clip (`NO_CLIPPED_ROWS`), so its `clipped` rows are empty.
633
+ const from = last;
634
+ const events = (): RawEvent[] => (mode.loop ? [] : (plant.events ?? eventsFired)(anim.timelines.events, from, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]));
635
+ const pose = shape.step(doc, { trackTime, animationTime: t, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
636
+ last = t;
637
+ visit(pose, visited++);
638
+ }
639
+ }
640
+
641
+ // --- #907 the setup-pose bounding box: begin ---
642
+ /**
643
+ * The setup-pose bounding box (issue #907) — `setupBounds` below: the axis-aligned box around every
644
+ * region and mesh a skeleton draws at its setup pose, as spine-core's
645
+ * `Skeleton.getBounds(offset, size, temp)` returns it on a fresh skeleton —
646
+ * no skin set, `updateWorldTransform(Physics.none)` — which is what the Spine
647
+ * format says the header's `x`, `y`, `width` and `height` are.
648
+ *
649
+ * Every rule below was measured by running spine-core 4.3.13 through the
650
+ * tree's own loader, not read off its source:
651
+ *
652
+ * - **The view is "no skin set"** (`underNoSkin`, issue #1051): a skeleton
653
+ * `build` writes is loaded and bounded before anyone calls `setSkin`, so
654
+ * the slots resolve through the default skin alone and no skin's bones or
655
+ * constraint lists apply.
656
+ * - **Constraints applied.** The box is of the pose `updateWorldTransform`
657
+ * leaves, every ik, transform, path and slider constraint applied in the
658
+ * document's order — the pose `poseSetup` computes. The editor's own
659
+ * header is of that pose too: on `examples/spineboy/export/spineboy-pro.json`
660
+ * (seven ik and seven transform constraints) `getBounds` with the
661
+ * constraints applied is within 0.0035 of the header the editor wrote, and
662
+ * with them stripped it is 0.31 off in `y` and `height`.
663
+ * - **Physics.none.** A physics constraint applies nothing at the setup pose
664
+ * (the oracle's setup reading). Under `Physics.reset` spine-core's box was
665
+ * the same, to the bit, on all nineteen tree rows (two declare a physics constraint).
666
+ * - **What is counted.** A region's four corners and a mesh's (a linked
667
+ * mesh's) world vertices — the rows of the oracle's `setup.attachments`
668
+ * block, in full doubles. A clipping polygon, a bounding box, a path and a
669
+ * point are not counted (measured: each placed 5000 units out moved
670
+ * `getBounds` by nothing), and the box is not clipped: `getBounds` called
671
+ * without its optional clipper.
672
+ * - **A slot on an inactive bone is not counted** — measured: a skin-required
673
+ * bone 1000 units out, which only a named skin activates, carrying a region
674
+ * left `getBounds` at the other region's 20 units with no skin set, and 1020
675
+ * with the bone not skin-required. A bone that is not skin-required under an
676
+ * inactive parent is active and unposed, and its slot is counted at the
677
+ * vertices that pose gives; the core poses the same vertices (the oracle's
678
+ * `setup.attachments`, held at tolerance 0), and a probe of that shape read
679
+ * the same box both ways.
680
+ * - **The box.** `x`, `y` are the least `x` and `y` over every counted
681
+ * vertex and `width`, `height` the greatest less the least — the
682
+ * bottom-left corner in Spine's y-up world, not the top-left. In full
683
+ * doubles; `build` writes each on the model's 1e-6 grid at float32
684
+ * (`headerBoxNumber` in `src/compile.ts`, whose comment says why).
685
+ * - **Nothing drawn is no box.** `getBounds` over no vertex returns an offset
686
+ * of `+Infinity` and a size of `-Infinity`, which is not a box; this
687
+ * returns `null` for it, and the emitter writes no box rather than a number
688
+ * nobody measured.
689
+ *
690
+ * Pure, like the rest of `src/core/`: it links nothing from the runtime, and
691
+ * `tools/core_gate.ts` holds it to `getBounds` at tolerance 0 over the corpus
692
+ * (its `BOUNDS` rows).
693
+ */
694
+
695
+ /** The setup-pose box: its bottom-left corner and its extent, in Spine world units, in full doubles. */
696
+ export interface CoreBounds {
697
+ x: number;
698
+ y: number;
699
+ width: number;
700
+ height: number;
701
+ }
702
+
703
+ /**
704
+ * The setup-pose bounding box of a model document (the header's rules), or
705
+ * `null` where nothing is drawn. Refused by name — `CoreInputError` — where
706
+ * the core leaves the setup attachments out, since a box over the rest would
707
+ * be a box of a different pose.
708
+ */
709
+ export function setupBounds(doc: CompiledDocument, plant: CorePlant = {}): CoreBounds | null {
710
+ const view = underNoSkin(doc);
711
+ const posed = poseSetup(view, { ...plant, round: rawNumber });
712
+ const rows = posed.setup.attachments;
713
+ if (rows === null) {
714
+ const why = posed.absent.find(([block]) => block === 'setup.attachments')?.[1] ?? 'the setup attachments were not posed';
715
+ throw new CoreInputError(`the setup-pose bounding box cannot be computed: ${why}`);
716
+ }
717
+ const active = activeBones(view);
718
+ const boneOf = new Map(view.slots.map((s) => [s.name, s.bone]));
719
+ let minX = Infinity;
720
+ let minY = Infinity;
721
+ let maxX = -Infinity;
722
+ let maxY = -Infinity;
723
+ for (const [slot, attachment, , vertices] of rows) {
724
+ const bone = boneOf.get(slot);
725
+ if (bone === undefined || !active.has(bone)) continue;
726
+ for (let i = 0; i + 1 < vertices.length; i += 2) {
727
+ const x = vertices[i];
728
+ const y = vertices[i + 1];
729
+ if (x === null || y === null) throw new CoreInputError(`the setup-pose bounding box cannot be computed: slot "${slot}" attachment "${attachment}" vertex ${i / 2} is not finite`);
730
+ minX = Math.min(minX, x);
731
+ minY = Math.min(minY, y);
732
+ maxX = Math.max(maxX, x);
733
+ maxY = Math.max(maxY, y);
734
+ }
735
+ }
736
+ if (minX === Infinity) return null;
737
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
738
+ }
739
+ // --- #907 the setup-pose bounding box: end ---