spine-rigc 1.9.0 → 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 +48 -17
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/bone_timelines.ts +56 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +34 -16
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/index.ts +83 -2
- package/src/assertions/model/linked_meshes.ts +2 -2
- package/src/assertions/model/mesh_attachments.ts +1 -1
- package/src/assertions/model/region_joins.ts +20 -12
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +18 -8
- package/src/assertions/model/skin_members.ts +5 -3
- package/src/assertions/model/skin_meshes.ts +1 -1
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/slot_colour.ts +5 -4
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +9 -8
- package/src/bonedist.ts +18 -6
- package/src/compile.ts +114 -14
- 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/deformmeasure.ts +35 -2270
- package/src/deformstructure.ts +60 -18
- package/src/deformsurvey.ts +2295 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +200 -59
- package/src/render_core.ts +5 -0
- package/src/validate.ts +791 -1558
|
@@ -431,6 +431,16 @@ export interface PhysicsTimelinePlant {
|
|
|
431
431
|
search?: (keys: readonly CoreKey[], t: number) => number;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
/**
|
|
435
|
+
* The records a physics timeline that names no constraint (`*`) writes: every
|
|
436
|
+
* active one whose `…Global` flag for `kind` is on, and for `reset` every
|
|
437
|
+
* active one (the header's *The timelines*), in the order given. `posedPhysics`
|
|
438
|
+
* asks it, and so does the additive probe (`./additive.ts`, issue #1025).
|
|
439
|
+
*/
|
|
440
|
+
export function unnamedPhysicsTargets<R extends CorePhysicsRecord>(records: readonly R[], kind: PhysicsTimelineKind, active: (r: CorePhysicsRecord) => boolean): R[] {
|
|
441
|
+
return records.filter((r) => active(r) && (kind === 'reset' || r.global[kind]));
|
|
442
|
+
}
|
|
443
|
+
|
|
434
444
|
/**
|
|
435
445
|
* Every physics record posed by the animation's physics timelines at `t`, in
|
|
436
446
|
* the timelines' order (the header's *The timelines*), and the names of the
|
|
@@ -438,18 +448,16 @@ export interface PhysicsTimelinePlant {
|
|
|
438
448
|
* `last` to `t` (`resetCrossed`). `active`
|
|
439
449
|
* says whether a constraint is applied at all (its skin and its bone).
|
|
440
450
|
*/
|
|
441
|
-
export function posedPhysics(records: readonly CorePhysicsRecord[], timelines: readonly CorePhysicsTimeline[], t: number, last: number, active: (r: CorePhysicsRecord) => boolean, plant: PhysicsTimelinePlant = {}): { records: Map<string, CorePhysicsRecord>; reset: Set<string> } {
|
|
451
|
+
export function posedPhysics(records: readonly CorePhysicsRecord[], timelines: readonly CorePhysicsTimeline[], t: number, last: number, active: (r: CorePhysicsRecord) => boolean, plant: PhysicsTimelinePlant = {}, wrapped = false): { records: Map<string, CorePhysicsRecord>; reset: Set<string> } {
|
|
442
452
|
const search = plant.search ?? keyIndexAt;
|
|
443
453
|
const channel = plant.channel ?? channelAt;
|
|
444
454
|
const setupOf = new Map(records.map((r) => [r.name, r]));
|
|
445
455
|
const posed = new Map(records.map((r) => [r.name, { ...r }]));
|
|
446
456
|
const reset = new Set<string>();
|
|
447
457
|
for (const tl of timelines) {
|
|
448
|
-
const targets = tl.name === EVERY_GLOBAL_PHYSICS
|
|
449
|
-
? [...posed.values()].filter((r) => active(r) && (tl.kind === 'reset' || r.global[tl.kind]))
|
|
450
|
-
: [posed.get(tl.name) as CorePhysicsRecord].filter((r) => active(r));
|
|
458
|
+
const targets = tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets([...posed.values()], tl.kind, active) : [posed.get(tl.name) as CorePhysicsRecord].filter((r) => active(r));
|
|
451
459
|
if (tl.kind === 'reset') {
|
|
452
|
-
if (resetCrossed(tl.keys, last, t)) for (const r of targets) reset.add(r.name);
|
|
460
|
+
if (resetCrossed(tl.keys, last, t, wrapped)) for (const r of targets) reset.add(r.name);
|
|
453
461
|
continue;
|
|
454
462
|
}
|
|
455
463
|
const i = search(tl.keys, t);
|
|
@@ -473,9 +481,18 @@ export function posedPhysics(records: readonly CorePhysicsRecord[], timelines: r
|
|
|
473
481
|
* issue #960): exactly when a key lies in `(last, t]` — so a key is crossed
|
|
474
482
|
* once, by the step that reaches it, and `last` is −1 before the walk's
|
|
475
483
|
* first apply, where a key at 0 is crossed.
|
|
484
|
+
*
|
|
485
|
+
* `wrapped` is a looping track's step whose animation time went DOWN (issue
|
|
486
|
+
* #1025, `./walk.ts`): the key crossed is then one after `last` or one at or
|
|
487
|
+
* before `t` — `(last, ∞)` and `(−1, t]` — measured against spine-core's
|
|
488
|
+
* `PhysicsConstraint.reset` calls on A10's walk. A step that wraps the track
|
|
489
|
+
* one or more times while its animation time still rises is not `wrapped`:
|
|
490
|
+
* the runtime fired only `(last, t]` there (a key at 0.25 of a 0.3 s
|
|
491
|
+
* animation stepped by 0.7 s, crossed by the track twice between two applies
|
|
492
|
+
* at 0.1 and 0.2, fired on neither).
|
|
476
493
|
*/
|
|
477
|
-
export function resetCrossed(keys: readonly CoreKey[], last: number, t: number): boolean {
|
|
478
|
-
return keys.some((k) => last < k.time && k.time <= t);
|
|
494
|
+
export function resetCrossed(keys: readonly CoreKey[], last: number, t: number, wrapped = false): boolean {
|
|
495
|
+
return wrapped ? keys.some((k) => k.time > last || k.time <= t) : keys.some((k) => last < k.time && k.time <= t);
|
|
479
496
|
}
|
|
480
497
|
|
|
481
498
|
// ---------------------------------------------------------------------------
|
|
@@ -546,6 +563,12 @@ export interface PhysicsStepContext {
|
|
|
546
563
|
* to the step's time (`resetCrossed`).
|
|
547
564
|
*/
|
|
548
565
|
last: number;
|
|
566
|
+
/**
|
|
567
|
+
* The walk's track loops (`./walk.ts`, issue #1025): a step whose animation
|
|
568
|
+
* time went down from `last` wrapped, and crosses the keys `resetCrossed`
|
|
569
|
+
* names for that. Absent on every walk but the looping one.
|
|
570
|
+
*/
|
|
571
|
+
loop?: boolean;
|
|
549
572
|
}
|
|
550
573
|
|
|
551
574
|
/** A constraint's state in `ctx`, created fresh on its first update. */
|
|
@@ -788,7 +811,7 @@ export function physicsRows(records: readonly CorePhysicsRecord[], round: (v: nu
|
|
|
788
811
|
*/
|
|
789
812
|
export function stepPhysicsRecords(records: readonly CoreConstraintRecord[], keyed: readonly CorePhysicsTimeline[], t: number, active: ReadonlySet<string>, ctx: PhysicsStepContext, before: number): CoreConstraintRecord[] {
|
|
790
813
|
const physics = records.filter((r): r is CorePhysicsRecord => r.kind === 'physics');
|
|
791
|
-
const posed = posedPhysics(physics, keyed, t, ctx.last, (r) => physicsActive(r, active));
|
|
814
|
+
const posed = posedPhysics(physics, keyed, t, ctx.last, (r) => physicsActive(r, active), {}, ctx.loop === true && t < ctx.last);
|
|
792
815
|
ctx.last = t;
|
|
793
816
|
for (const name of posed.reset) resetPhysicsState(physicsState(ctx, name), before);
|
|
794
817
|
return records.map((r) => (r.kind === 'physics' ? (posed.records.get(r.name) as CorePhysicsRecord) : r));
|
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
|
+
}
|