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/cli.ts +39 -17
- package/docs/AUTHORING.md +1 -1
- package/package.json +1 -1
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/index.ts +21 -2
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/bonedist.ts +18 -6
- package/src/core/additive.ts +468 -0
- package/src/core/constraints.ts +12 -2
- package/src/core/constraints_physics.ts +31 -8
- package/src/core/raw.ts +77 -16
- package/src/core/walk.ts +202 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +151 -86
- package/src/render_core.ts +5 -0
- package/src/validate.ts +432 -457
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) =>
|
|
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) =>
|
|
268
|
-
const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], c[3].map((v) =>
|
|
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
|
-
|
|
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, ...
|
|
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, ...
|
|
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
|
-
|
|
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
|
}
|
package/src/core/walk.ts
ADDED
|
@@ -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
|
+
}
|
package/src/nonfinite.ts
ADDED
|
@@ -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
|
+
}
|