spine-rigc 1.9.1 → 1.9.2

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.
package/src/core/raw.ts CHANGED
@@ -100,6 +100,12 @@
100
100
  * `./clipping.ts`, issue #964), held to spine-core by the render's pixels.
101
101
  * - `events` — the oracle's event rows as doubles.
102
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
+ *
103
109
  * ⛔ **Nothing is posed that the core would leave out.** Where the oracle's
104
110
  * core dump names a block absent — a constraint the core cannot pose
105
111
  * exactly, skins disagreeing over a placeholder under the merged view, a
@@ -189,8 +195,6 @@ export interface RawPose {
189
195
  // --- #968 render: end ---
190
196
  }
191
197
 
192
- const RAW_PLANT: CorePlant = { round: rawNumber };
193
-
194
198
  // --- #968 render: begin ---
195
199
  /** The shown records in `order`, the pose's draw order (a pose's `shown`). */
196
200
  function drawOrderOf(shown: readonly ShownGeometry[], order: readonly string[]): ShownGeometry[] {
@@ -205,6 +209,37 @@ function finite(v: number | null, what: string): number {
205
209
  return v;
206
210
  }
207
211
 
212
+ /**
213
+ * How a walk treats the two things the raw entry and the looping walk
214
+ * (`./walk.ts`, issue #1025) read differently: whether the track loops, and
215
+ * whether a value that is not finite is kept in the pose or refuses the call.
216
+ * The raw entry is `{ loop: false, keep: false }`, and that is its contract.
217
+ */
218
+ export interface WalkMode {
219
+ loop: boolean;
220
+ keep: boolean;
221
+ /** 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. */
222
+ time?: (trackTime: number, duration: number, before: number) => number;
223
+ /** 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. */
224
+ clock?: (dt: number, moved: number) => number;
225
+ /** A plant only (`CO26`): `false` fires no `reset` key across the wrap — the reading the walk was measured against and rejected. */
226
+ wrapResets?: boolean;
227
+ }
228
+
229
+ /** The raw entry's mode: a track that holds at its duration, and a non-finite value refused. */
230
+ const RAW_MODE: WalkMode = { loop: false, keep: false };
231
+
232
+ /** What a kept walk writes a number as: the double itself, finite or not — so a NaN and an Infinity stay what they are. */
233
+ const keptNumber = (v: number): number => v;
234
+
235
+ /** The plant a walk in `mode` poses with: the raw entry's unrounded double, or under `keep` the number whatever it is. */
236
+ const roundPlant = (mode: WalkMode): CorePlant => ({ round: mode.keep ? keptNumber : rawNumber });
237
+
238
+ /** A double a kept walk computed, as it is; a raw walk's, refused when it is not finite (`finite`). */
239
+ function numberOf(v: number | null, what: string, mode: WalkMode): number {
240
+ return mode.keep ? (v === null ? Number.NaN : v) : finite(v, what);
241
+ }
242
+
208
243
  /** The bones of a pose from the world transforms (the header's `bones`). */
209
244
  function rawBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>): RawBone[] {
210
245
  const active = activeBones(doc);
@@ -230,8 +265,8 @@ function rawBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>):
230
265
  }
231
266
 
232
267
  /** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
233
- function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
234
- const geometry = poseGeometry(shown, world, sourceOfDoc(doc), rawNumber, { region: plant.region, vertices: plant.vertices }, drawWalkOf(doc, order, plant));
268
+ 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[] } {
269
+ const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, drawWalkOf(doc, order, plant));
235
270
  if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
236
271
  // 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.
237
272
  if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
@@ -262,10 +297,10 @@ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world:
262
297
  hull = source.hull ?? null;
263
298
  }
264
299
  const sequenceIndex = s.frame ?? record?.sequenceSetup ?? 0;
265
- drawn.push({ slot: s.slot, attachment: row[1], kind: row[2], vertices: row[3].map((v) => finite(v, `vertex of slot "${s.slot}"`)), uvs, triangles, hull, sequenceIndex, colour });
300
+ 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 });
266
301
  }
267
- const clips = geometry.clips.map((c): RawClip => [c[0], c[1], c[2], c[3].map((v) => finite(v, `clip vertex of slot "${c[0]}"`))]);
268
- const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], c[3].map((v) => finite(v, `clipped vertex of slot "${c[0]}"`)), c[4].map((v) => finite(v, `clipped uv of slot "${c[0]}"`)), c[5]]);
302
+ 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))]);
303
+ 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]]);
269
304
  return { drawn, clips, clipped };
270
305
  }
271
306
 
@@ -275,13 +310,18 @@ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world:
275
310
  * where the core leaves a block of it out.
276
311
  */
277
312
  export function poseRawSetup(doc: CompiledDocument, plant: CorePlant = {}): RawPose {
278
- const posed = poseSetup(doc, { ...plant, ...RAW_PLANT }, freshStepContext(plant.physicsStep));
313
+ return setupPoseIn(doc, plant, RAW_MODE);
314
+ }
315
+
316
+ /** `poseRawSetup` in `mode` — under `keep`, a value that is not finite stays in the pose (`./walk.ts`). */
317
+ export function setupPoseIn(doc: CompiledDocument, plant: CorePlant, mode: WalkMode): RawPose {
318
+ const posed = poseSetup(doc, { ...plant, ...roundPlant(mode) }, freshStepContext(plant.physicsStep));
279
319
  // `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).
280
320
  const absent = posed.absent.filter(([block]) => block !== 'setup.clipped');
281
321
  if (absent.length > 0) throw new CoreInputError(`the raw setup pose leaves ${absent.map(([b, why]) => `${b} out (${why})`).join('; ')}`);
282
322
  const { setup, world, shown } = posed;
283
323
  if (world === null || shown === null || setup.slots === null || setup.drawOrder === null) throw new CoreInputError('the raw setup pose was not posed');
284
- const drawn = rawDrawn(doc, shown, world, setup.drawOrder, plant);
324
+ const drawn = rawDrawn(doc, shown, world, setup.drawOrder, plant, mode);
285
325
  return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots: setup.slots, drawOrder: setup.drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, setup.drawOrder) };
286
326
  }
287
327
 
@@ -296,34 +336,54 @@ export function poseRawSetup(doc: CompiledDocument, plant: CorePlant = {}): RawP
296
336
  * deformmeasure's one jump.
297
337
  */
298
338
  export function poseRawAnimation(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant = {}, reset: RawReset = 'animation'): RawPose[] {
339
+ return walkIn(doc, animation, steps, plant, reset, RAW_MODE);
340
+ }
341
+
342
+ /**
343
+ * The animation time a looping track applies its animation at (`./walk.ts`,
344
+ * *The time*): the track time wrapped by the duration, and 0 over a duration
345
+ * of 0.
346
+ */
347
+ export function loopedTime(trackTime: number, duration: number): number {
348
+ return duration === 0 ? 0 : trackTime % duration;
349
+ }
350
+
351
+ /**
352
+ * `poseRawAnimation` in `mode`: the raw entry's walk, or the looping walk
353
+ * `./walk.ts` documents — the animation time the track time wrapped by the
354
+ * duration, every value kept whatever it is. Each difference is a branch on
355
+ * `mode` here, so the raw entry's walk is the one it was.
356
+ */
357
+ export function walkIn(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode): RawPose[] {
299
358
  const anim = doc.animations.find((a) => a.name === animation);
300
359
  if (anim === undefined) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(', ')}]`);
301
360
  const bad = steps.findIndex((s) => !Number.isFinite(s) || s < 0);
302
361
  if (bad >= 0) throw new CoreInputError(`step ${bad} is ${steps[bad]}, not a finite time at or above 0`);
303
362
  const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
304
363
  if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
305
- const raw: TimelinePlant = { ...plant, ...RAW_PLANT };
364
+ const raw: TimelinePlant = { ...plant, ...roundPlant(mode) };
306
365
  const resolve = plant.shown ?? shownAttachment;
307
366
  const duration = anim.timelines.duration;
308
367
  const ctx = freshStepContext(plant.physicsStep);
368
+ if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
309
369
  const poses: RawPose[] = [];
310
370
  let trackTime = 0;
311
371
  let last = -1;
312
372
  if (reset === 'setup') {
313
373
  // The reset taken at the setup pose, before the animation is applied (`src/deformmeasure.ts`'s `poseAt`): pose 0 is the setup pose.
314
- const setup = poseSetup(doc, { ...plant, ...RAW_PLANT }, ctx);
374
+ const setup = poseSetup(doc, { ...plant, ...roundPlant(mode) }, ctx);
315
375
  const absent = setup.absent.filter(([block]) => block !== 'setup.clipped');
316
376
  if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join('; ')}`);
317
377
  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');
318
378
  ctx.phase = 'update';
319
- poses.push({ trackTime: 0, animationTime: 0, bones: rawBones(doc, setup.world), slots: setup.setup.slots, drawOrder: setup.setup.drawOrder, ...rawDrawn(doc, setup.shown, setup.world, setup.setup.drawOrder, plant), events: [], shown: drawOrderOf(setup.shown, setup.setup.drawOrder) });
379
+ poses.push({ trackTime: 0, animationTime: 0, bones: rawBones(doc, setup.world), slots: setup.setup.slots, drawOrder: setup.setup.drawOrder, ...rawDrawn(doc, setup.shown, setup.world, setup.setup.drawOrder, plant, mode), events: [], shown: drawOrderOf(setup.shown, setup.setup.drawOrder) });
320
380
  }
321
381
  for (let i = reset === 'setup' ? 1 : 0; i <= steps.length; i++) {
322
382
  const dt = i === 0 ? 0 : steps[i - 1];
323
383
  trackTime += dt;
324
- const t = trackTime < duration ? trackTime : duration;
384
+ const t = mode.loop ? (mode.time ?? loopedTime)(trackTime, duration, trackTime - dt) : trackTime < duration ? trackTime : duration;
325
385
  const before = ctx.time;
326
- ctx.time += dt;
386
+ ctx.time += mode.clock === undefined ? dt : mode.clock(dt, t - (i === 0 ? 0 : Math.max(last, 0)));
327
387
  const sliders: SliderApplication[] = [];
328
388
  const posed = posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
329
389
  if (i === 0) ctx.phase = 'update';
@@ -338,8 +398,9 @@ export function poseRawAnimation(doc: CompiledDocument, animation: string, steps
338
398
  if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
339
399
  const rank = new Map(drawOrder.map((n, r) => [n, r]));
340
400
  const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
341
- const drawn = rawDrawn(doc, shown, posed.world, drawOrder, plant);
342
- const events = (plant.events ?? eventsFired)(anim.timelines.events, last, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]);
401
+ const drawn = rawDrawn(doc, shown, posed.world, drawOrder, plant, mode);
402
+ // A looping walk fires no event here: what fires across the wrap was not measured, and the walk's one reader (A10) reads none.
403
+ const events = mode.loop ? [] : (plant.events ?? eventsFired)(anim.timelines.events, last, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]);
343
404
  last = t;
344
405
  poses.push({ trackTime, animationTime: t, bones: rawBones(doc, posed.world), slots: slots.rows, drawOrder, ...drawn, events, shown });
345
406
  }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The core's looping walk (issue #1025, cut 4c-5a of step 4c of #380): one
3
+ * animation on a LOOPING track, reset at the setup pose and then stepped at
4
+ * the caller's steps, every pose returned as the doubles the core computed —
5
+ * a value that is not finite included, in place, rather than refused.
6
+ *
7
+ * This is the walk `A10_NO_NAN_AFTER_STEPPING` takes, and the reason it is
8
+ * its own entry rather than the raw entry (`./raw.ts`) under another name is
9
+ * the two things the raw entry's contract excludes on purpose: its track
10
+ * holds at the duration, and it refuses a non-finite value by name
11
+ * (`CoreInputError`), because a renderer handed one would draw a different
12
+ * picture in silence. A10's subject is exactly that value, so here it is the
13
+ * answer, not a refusal. Everything else is the raw entry's walk, called
14
+ * rather than restated (`walkIn` in `./raw.ts`, which branches on the two).
15
+ *
16
+ * ## The walk, measured
17
+ *
18
+ * Written from spine-core 4.3.13 running A10's own recipe — `Skeleton`,
19
+ * `AnimationState` with `setAnimation(0, name, true)`; `setupPose()`,
20
+ * `update(0)`, `updateWorldTransform(Physics.reset)`; then per step
21
+ * `AnimationState.update(dt)`, `apply`, `Skeleton.update(dt)`,
22
+ * `updateWorldTransform(Physics.update)` — and read off `TrackEntry.trackTime`,
23
+ * `TrackEntry.getAnimationTime()`, every bone's `appliedPose`, every shown
24
+ * region's and mesh's world vertices, every slot's colours and the calls the
25
+ * runtime makes to `PhysicsConstraint.reset`. The PR of issue #1025's cut
26
+ * 4c-5a carries the tables; each rule names the count that held it.
27
+ *
28
+ * - **Pose 0 is the setup pose, reset** — the animation is set and not yet
29
+ * applied, so the reset is taken at the setup pose (`RawReset` `setup`).
30
+ * - **The track time is the running sum of the steps**, as on the raw
31
+ * entry's walk: `TrackEntry.trackTime` equalled it on every step of the
32
+ * nineteen tree rows' walks (6,480 of 6,480).
33
+ * - **The time** — the animation is applied at the track time wrapped by the
34
+ * duration (`loopedTime` in `./raw.ts`): `trackTime % duration` in double,
35
+ * so a track time of exactly the duration is 0, the first pose again, and
36
+ * over a duration of 0 the time is 0 whatever the track time (an animation
37
+ * keying nothing, or keying only at 0) — `getAnimationTime()` read so on
38
+ * 6,480 of 6,480 steps, 1,087 of them at or past the duration and 4 exactly
39
+ * on it; before the zero-duration reading it read 6,000.
40
+ * - **The physics clock moves by the step itself** (`Skeleton.update(dt)`),
41
+ * whatever the wrap does to the animation time: moved by the animation
42
+ * time's own change instead, 32 of 82 seeded and forged walks read off.
43
+ * - **A `reset` key across the wrap.** A key is crossed when it lies in
44
+ * `(previous animation time, animation time]`, as on the raw entry; on a
45
+ * step whose animation time went DOWN — the wrap — it is crossed when it
46
+ * lies after the previous time or at or before the new one, `(last, ∞)`
47
+ * and `(−1, t]` (`resetCrossed` in `./constraints_physics.ts`). A step
48
+ * that passes the duration once or more while its animation time still
49
+ * rises is not a wrap there: a key at 0.25 s of a 0.3 s animation stepped
50
+ * by 0.7 s fired on neither of the two steps that carried the track past
51
+ * it, and an equal time is not a wrap either (a 0.25 s animation stepped by
52
+ * 0.25 s fired its key at 0 once). With the wrap firing nothing, 957 of
53
+ * 14,460 poses of a seeded population read off.
54
+ * - **A value that is not finite** is the IEEE result of the same
55
+ * arithmetic, kept: a world matrix term, a world position, a vertex, a slot
56
+ * colour channel. Where the raw entry writes such a value `null` and
57
+ * refuses, this entry writes the double; on 4,333 poses holding one, every
58
+ * NaN and Infinity stood where spine-core's did.
59
+ *
60
+ * Events are not carried: A10 reads none, and what a looping track fires
61
+ * across the wrap was not measured.
62
+ *
63
+ * ⛔ **What is still refused** is the raw entry's other refusal: a construct
64
+ * the core would leave out (a block the oracle's core dump names absent) is a
65
+ * `CoreInputError` naming why, as there — a pose with a block missing is not
66
+ * a pose whose values can be judged finite.
67
+ *
68
+ * ⛔ **And one construct this walk refuses that the raw entry does not**
69
+ * (`sliderPhysicsWhy`): a slider whose animation keys a physics timeline.
70
+ * Issue #1049. Of the selftest's builds of that class (a dial, or two
71
+ * sliders, keying a physics constraint's `wind` or `gravity`), five walked off spine-core from the third
72
+ * step on — `tip` at worldX 14.87 against 12.09 on the first, at 0.025 s —
73
+ * while others of the same class walked exact. The raw entry's own non-looping walk reads the same
74
+ * gap (and with the slider made non-additive, 14.78 against 12.09), so it is
75
+ * the stepped walk's reading of a slider's physics keys, not the loop's.
76
+ * Which of the class it reaches is not measured, so the walk refuses the
77
+ * class by name rather than return a pose it cannot vouch for. On the same
78
+ * five inputs, `./additive.ts` and spine-core's probe agree that each slider's
79
+ * `wind` or `gravity` timeline accumulates when applied twice with `add` — so
80
+ * the gap is downstream of that rule, in the stepped bones, not in it.
81
+ */
82
+ import { setupPoseIn, walkIn, type RawBone, type RawDrawn, type WalkMode } from './raw.ts';
83
+ import { activeBones, CoreInputError, constraintRecords, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
84
+ import { historyTaint } from './constraints.ts';
85
+ import type { TimelinePlant } from './animation.ts';
86
+
87
+ /** One pose of the looping walk: the raw entry's pose less what this walk does not carry (events, the clip rows, the shown records). */
88
+ export interface WalkPose {
89
+ /** The running sum of the steps, 0 for pose 0. */
90
+ trackTime: number;
91
+ /** The time the animation was applied at: the track time wrapped by the duration (`loopedTime`). */
92
+ animationTime: number;
93
+ /** Every bone in document order, its world matrix and origin as computed, finite or not. */
94
+ bones: RawBone[];
95
+ /** Every slot row (`CoreSlotRow`) with its colour channels as computed, finite or not. */
96
+ slots: CoreSlotRow[];
97
+ /** The slot names in the posed draw order. */
98
+ drawOrder: string[];
99
+ /** Every region and mesh a slot shows, in draw order, its world vertices as computed, finite or not. */
100
+ drawn: RawDrawn[];
101
+ }
102
+
103
+ /**
104
+ * Why the looping walk refuses this document, or `null` (the header's last
105
+ * ⛔, issue #1049): every slider whose animation keys a physics timeline,
106
+ * named with it.
107
+ */
108
+ export function sliderPhysicsWhy(doc: CompiledDocument): string | null {
109
+ const keyed = constraintRecords(doc).flatMap((r) => {
110
+ if (r.kind !== 'slider') return [];
111
+ const anim = doc.animations.find((a) => a.name === r.animation);
112
+ return anim !== undefined && anim.constraints.physics > 0 ? [`slider "${r.name}" applies animation "${r.animation}", which keys physics timelines`] : [];
113
+ });
114
+ return keyed.length === 0 ? null : `${keyed.join('; ')} — under the stepped walk spine-core's bones leave the core's from the third step, on the raw entry's walk as on this one, and that reading is not measured (issue #1049)`;
115
+ }
116
+
117
+ /**
118
+ * The bones and slots whose numbers the walk does not vouch for, because the
119
+ * runtime's own value there is HISTORY (issue #979): a bone the view leaves
120
+ * unposed — inactive, or below an inactive bone — that a constraint writing
121
+ * into an inactive bone reaches (`historyTaint` in `./constraints.ts`, the
122
+ * walk the core's leak refusal reads), each with that writer; and every slot
123
+ * on such a bone. spine-core never updates an inactive bone, so what a
124
+ * constraint wrote into it on the step before is what the constraint reads
125
+ * on the next one; the core poses every step from the setup pose. Measured on
126
+ * a public probe (a transform constraint writing four skin-required bones no
127
+ * skin activates): at mix 1 the two differ only in the sign of zero of the
128
+ * matrix cells (`atan2` of a zero matrix is 0° or 180° by those signs), and at
129
+ * a translate mix of 0.5 by value (worldX 30.000000348 in spine-core against
130
+ * 20.000000232 in the core at step 1), on the bones and on an active child of
131
+ * one. Where such a value reaches a posed bone the core already refuses the
132
+ * document (`unposedLeakWhy`); here nothing posed reads it, so a walk
133
+ * comparison counts these numbers apart by name and compares them with
134
+ * nothing, as `tools/pose_oracle.ts unposed` does under the step.
135
+ */
136
+ export function walkHistory(view: CompiledDocument): { bones: Map<string, string>; slots: Map<string, string> } {
137
+ const active = activeBones(view);
138
+ const unposed = new Set<string>();
139
+ for (const b of view.bones) if (!active.has(b.name) || (b.parent !== undefined && unposed.has(b.parent))) unposed.add(b.name);
140
+ const bones = new Map<string, string>();
141
+ for (const [bone, w] of historyTaint(view).tainted) if (unposed.has(bone)) bones.set(bone, `${w.kind}/${w.name} on inactive ${w.inactive}`);
142
+ return { bones, slots: new Map(view.slots.filter((s) => bones.has(s.bone)).map((s) => [s.name, s.bone])) };
143
+ }
144
+
145
+ /** Refuse, by name, a document the looping walk cannot vouch for (`sliderPhysicsWhy`). */
146
+ function refuseUnmeasured(doc: CompiledDocument): void {
147
+ const why = sliderPhysicsWhy(doc);
148
+ if (why !== null) throw new CoreInputError(`the looping walk leaves the bones out: ${why}`);
149
+ }
150
+
151
+ /** The looping walk's mode: the track loops, and a non-finite value stays in the pose. */
152
+ const LOOPING: WalkMode = { loop: true, keep: true };
153
+
154
+ /**
155
+ * The looping walk's plants (the core suite's `CO26`), each the reading the
156
+ * walk was measured against and rejected, in a copy: the time a step applies
157
+ * at, what the physics clock moves by, and what a number is written as.
158
+ * Nothing but a control passes one.
159
+ */
160
+ export interface WalkPlant {
161
+ time?: WalkMode['time'];
162
+ clock?: WalkMode['clock'];
163
+ wrapResets?: WalkMode['wrapResets'];
164
+ /** Every number of a pose rewritten — a swallowed non-finite value is `(v) => (Number.isFinite(v) ? v : 0)`. */
165
+ number?: (v: number) => number;
166
+ }
167
+
168
+ /** A raw pose narrowed to what the looping walk carries, every number through `number`. */
169
+ function narrow(p: { trackTime: number; animationTime: number; bones: RawBone[]; slots: CoreSlotRow[]; drawOrder: string[]; drawn: RawDrawn[] }, number?: (v: number) => number): WalkPose {
170
+ if (number === undefined) return { trackTime: p.trackTime, animationTime: p.animationTime, bones: p.bones, slots: p.slots, drawOrder: p.drawOrder, drawn: p.drawn };
171
+ const n = (v: number | null): number | null => (v === null ? null : number(v));
172
+ return {
173
+ trackTime: p.trackTime,
174
+ animationTime: p.animationTime,
175
+ bones: p.bones.map((b) => ({ ...b, a: number(b.a), b: number(b.b), c: number(b.c), d: number(b.d), worldX: number(b.worldX), worldY: number(b.worldY) })),
176
+ slots: p.slots.map((r): CoreSlotRow => [r[0], r[1], n(r[2]), n(r[3]), n(r[4]), n(r[5]), r[6] === null ? null : [n(r[6][0]), n(r[6][1]), n(r[6][2])], r[7], r[8]]),
177
+ drawOrder: p.drawOrder,
178
+ drawn: p.drawn.map((d) => ({ ...d, vertices: d.vertices.map(number) })),
179
+ };
180
+ }
181
+
182
+ /**
183
+ * The setup pose, every physics state reset, every value kept whatever it is —
184
+ * what A10 reads before any animation is set.
185
+ */
186
+ export function poseWalkSetup(doc: CompiledDocument, plant: CorePlant = {}, walkPlant: WalkPlant = {}): WalkPose {
187
+ refuseUnmeasured(doc);
188
+ return narrow(setupPoseIn(doc, plant, LOOPING), walkPlant.number);
189
+ }
190
+
191
+ /**
192
+ * One animation on a looping track (the header's *The walk*): pose 0 the setup
193
+ * pose reset, then one pose per step of `steps` — `steps.length + 1` poses in
194
+ * all. A step must be a finite time at or above 0 and the animation the
195
+ * document's, as on the raw entry; a construct the core would leave out
196
+ * refuses the call by name; a value that is not finite does not.
197
+ */
198
+ export function poseLoopingWalk(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant = {}, walkPlant: WalkPlant = {}): WalkPose[] {
199
+ refuseUnmeasured(doc);
200
+ const mode: WalkMode = { ...LOOPING, ...(walkPlant.time === undefined ? {} : { time: walkPlant.time }), ...(walkPlant.clock === undefined ? {} : { clock: walkPlant.clock }), ...(walkPlant.wrapResets === undefined ? {} : { wrapResets: walkPlant.wrapResets }) };
201
+ return walkIn(doc, animation, steps, plant, 'setup', mode).map((p) => narrow(p, walkPlant.number));
202
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The one sentence for a pose number that is not finite (issue #882, #873) —
3
+ * moved here from `src/render.ts` by issue #1025 (cut 4c-5a of step 4c of
4
+ * #380), unchanged, because it now has a reader that may not link the
5
+ * runtime: `A10_NO_NAN_AFTER_STEPPING`'s body (`./assertions/bodies/a10.ts`),
6
+ * which reads it over the poses spine-core gives and over the poses rigc's
7
+ * core gives. `render` refuses on the same sentence, so the gate and the
8
+ * renderer still hold one definition of "not finite" — the six terms of a
9
+ * bone's world transform, then the vertices computed from them.
10
+ *
11
+ * Pure: no import at all.
12
+ */
13
+
14
+ /** The bone fields a world transform is made of — what `firstNonFinite` reads off a bone. */
15
+ export interface WorldTransform {
16
+ readonly name: string;
17
+ readonly a: number;
18
+ readonly b: number;
19
+ readonly c: number;
20
+ readonly d: number;
21
+ readonly worldX: number;
22
+ readonly worldY: number;
23
+ }
24
+
25
+ /** The fields of an attachment entry `firstNonFinite` reads — a frame's posed attachment or a rest entry. */
26
+ export interface PosedVertices {
27
+ readonly slot: string;
28
+ readonly attachment: string;
29
+ readonly vertices: readonly number[];
30
+ }
31
+
32
+ /**
33
+ * The sentence for the first number at `where` that is not finite — bones before
34
+ * vertices, since a bone that overflowed is the cause and its vertices the
35
+ * symptom — or `null` when every number there is finite.
36
+ */
37
+ export function firstNonFinite(where: string, entries: readonly PosedVertices[], bones: readonly WorldTransform[]): string | null {
38
+ for (const bone of bones) {
39
+ for (const field of ['a', 'b', 'c', 'd', 'worldX', 'worldY'] as const) {
40
+ if (!Number.isFinite(bone[field])) {
41
+ return `${where}: bone ${JSON.stringify(bone.name)} has ${field} ${String(bone[field])}; a world transform is finite`;
42
+ }
43
+ }
44
+ }
45
+ for (const entry of entries) {
46
+ const bad = entry.vertices.findIndex((value) => !Number.isFinite(value));
47
+ if (bad === -1) continue;
48
+ return (
49
+ `${where}: slot ${JSON.stringify(entry.slot)} attachment ${JSON.stringify(entry.attachment)} vertex ` +
50
+ `${Math.floor(bad / 2)} has ${bad % 2 === 0 ? 'x' : 'y'} ${String(entry.vertices[bad])}; a posed vertex is finite`
51
+ );
52
+ }
53
+ return null;
54
+ }