rig-c 2.34.0 → 2.35.0
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/package.json +1 -1
- package/src/core/entry.ts +5 -2
- package/src/core/events.ts +13 -5
- package/src/core/raw.ts +117 -40
- package/src/core/track.ts +193 -0
- package/src/core/walk.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.35.0",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/core/entry.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* `CUR123` holds the closure, by name; the install smoke bundles it for a
|
|
16
16
|
* browser and holds the bundle to zero `node:` strings.
|
|
17
17
|
*
|
|
18
|
-
* ⚠️ A symbol is listed here only because a dependant was seen using it
|
|
19
|
-
*
|
|
18
|
+
* ⚠️ A symbol is listed here only because a dependant was seen using it,
|
|
19
|
+
* or asked for it in writing before it could be (`openTrack`, issue #1276).
|
|
20
|
+
* The list is `scripts/install_smoke.ts`'s `OBSERVED_SYMBOLS` rows for
|
|
20
21
|
* `./core`, and a symbol added here and not there is not promised.
|
|
21
22
|
*/
|
|
22
23
|
export { readModel, underSkin, underNoSkin, CoreInputError, CORE_DOCUMENT_SPEC, CORE_DOCUMENT_SPECS, CORE_BLEND_MODES, activeBones } from './index.ts';
|
|
@@ -24,6 +25,8 @@ export type { CompiledDocument, CoreSlotRow, CoreBlendMode } from './index.ts';
|
|
|
24
25
|
export { poseRawSetup, poseRawAnimation, poseRawAnimationEach, loopedTime, setupBounds } from './raw.ts';
|
|
25
26
|
export type { RawPose, RawBone, RawDrawn, RawClip, RawClipped, RawEvent, RawReset } from './raw.ts';
|
|
26
27
|
export { poseWalkSetup, poseLoopingWalk } from './walk.ts';
|
|
28
|
+
export { openTrack } from './track.ts';
|
|
29
|
+
export type { LiveTrack, TrackOptions } from './track.ts';
|
|
27
30
|
export type { WalkPose } from './walk.ts';
|
|
28
31
|
export { documentPageLookup, drawnRegions, regionPageUvs, meshPageUvs, readUvSequences } from './uvs.ts';
|
|
29
32
|
export type { DrawnRegion, UvPage, UvRegion, UvSource } from './uvs.ts';
|
package/src/core/events.ts
CHANGED
|
@@ -126,10 +126,18 @@ export function readEventKeys(value: unknown, label: string, defs: ReadonlyMap<s
|
|
|
126
126
|
return out;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/** What lists the events fired over `(last, t]
|
|
130
|
-
export type EventsFired = (keys: readonly CoreEventKey[], last: number, t: number, round?: (v: number) => number | null) => CoreEventRow[];
|
|
129
|
+
/** What lists the events fired over `(last, t]` — or across the wrap when `wrapped` — : `eventsFired` unless a plant passes another. */
|
|
130
|
+
export type EventsFired = (keys: readonly CoreEventKey[], last: number, t: number, round?: (v: number) => number | null, wrapped?: boolean) => CoreEventRow[];
|
|
131
131
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The rows of the keys fired over `(last, t]`, in key order — the header's
|
|
134
|
+
* rules, rounded as the oracle rounds. `wrapped` is a looping track's step
|
|
135
|
+
* whose animation time went down (`t < last`, the wrap): the keys after
|
|
136
|
+
* `last`, in key order, then the keys at or before `t`, in key order — the
|
|
137
|
+
* header's *Across the wrap*. Nothing but a looping track passes it.
|
|
138
|
+
*/
|
|
139
|
+
export function eventsFired(keys: readonly CoreEventKey[], last: number, t: number, round: (v: number) => number | null = gridRound, wrapped = false): CoreEventRow[] {
|
|
140
|
+
const row = (k: CoreEventKey): CoreEventRow => [k.name, round(k.time), k.int, round(k.float), k.string];
|
|
141
|
+
if (wrapped) return [...keys.filter((k) => k.time > last), ...keys.filter((k) => k.time <= t)].map(row);
|
|
142
|
+
return keys.filter((k) => k.time > last && k.time <= t).map(row);
|
|
135
143
|
}
|
package/src/core/raw.ts
CHANGED
|
@@ -261,6 +261,13 @@ export interface WalkMode {
|
|
|
261
261
|
clock?: (dt: number, moved: number) => number;
|
|
262
262
|
/** A plant only (`CO26`): `false` fires no `reset` key across the wrap — the reading the walk was measured against and rejected. */
|
|
263
263
|
wrapResets?: boolean;
|
|
264
|
+
/**
|
|
265
|
+
* The live track's looping mode (`./track.ts`, issue #1276): a looping walk
|
|
266
|
+
* that fires its events across the wrap and cuts its clipped rows, as the
|
|
267
|
+
* raw entry does. Absent on A10's looping walk (`./walk.ts`), which does
|
|
268
|
+
* neither — its one reader reads no event and no clipped row.
|
|
269
|
+
*/
|
|
270
|
+
live?: boolean;
|
|
264
271
|
}
|
|
265
272
|
|
|
266
273
|
/** The raw entry's mode: a track that holds at its duration, and a non-finite value refused. */
|
|
@@ -332,7 +339,7 @@ function scanBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>)
|
|
|
332
339
|
*/
|
|
333
340
|
function drawnGeometry(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode): { rows: CoreAttachmentRow[]; clips: CoreClipRow[]; drawnClipped: CoreClippedRow[] } {
|
|
334
341
|
const draw = drawWalkOf(doc, order, plant);
|
|
335
|
-
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, mode.loop && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
|
|
342
|
+
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, mode.loop && mode.live !== true && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
|
|
336
343
|
if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
|
|
337
344
|
// The drawn rows (issue #964): the oracle's `clipped` rows where it is posed, and a concave or inverse clip cut through the core's own decomposition.
|
|
338
345
|
if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
|
|
@@ -583,22 +590,53 @@ function scanShape(scanPlant: ScanPlant): PoseShape<ScanPose> {
|
|
|
583
590
|
}
|
|
584
591
|
const SCAN_SHAPE: PoseShape<ScanPose> = scanShape({});
|
|
585
592
|
|
|
586
|
-
/**
|
|
587
|
-
|
|
593
|
+
/**
|
|
594
|
+
* A walk between two of its steps (issue #1276): everything `walkEach` carries
|
|
595
|
+
* from one step to the next, held in one object, so the walk can be stepped one
|
|
596
|
+
* `dt` at a time (`openTrack` in `./track.ts`) as well as over a list. That is
|
|
597
|
+
* the physics context (`ctx`: the skeleton's clock, the phase, each physics
|
|
598
|
+
* constraint's state, the animation time a `reset` key is crossed from, whether
|
|
599
|
+
* the track loops), the running track time, the animation time of the step
|
|
600
|
+
* before (−1 before the first) and what the walk was opened with. Nothing else
|
|
601
|
+
* survives a step: every pose is posed from the setup pose at its time.
|
|
602
|
+
*/
|
|
603
|
+
export interface TrackState<P> {
|
|
604
|
+
readonly doc: CompiledDocument;
|
|
605
|
+
readonly anim: CompiledDocument['animations'][number];
|
|
606
|
+
/** The runtime's duration (`CoreAnimationTimelines.duration`). */
|
|
607
|
+
readonly duration: number;
|
|
608
|
+
readonly mode: WalkMode;
|
|
609
|
+
readonly plant: TimelinePlant;
|
|
610
|
+
/** `plant` with the mode's rounding (`roundPlant`). */
|
|
611
|
+
readonly raw: TimelinePlant;
|
|
612
|
+
readonly resolve: NonNullable<TimelinePlant['shown']>;
|
|
613
|
+
readonly shape: PoseShape<P>;
|
|
614
|
+
readonly ctx: ReturnType<typeof freshStepContext>;
|
|
615
|
+
/** The running sum of the steps taken. */
|
|
616
|
+
trackTime: number;
|
|
617
|
+
/** The animation time of the step before, −1 before the first. */
|
|
618
|
+
last: number;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** The document's animation named `animation`, or the walk refused naming the document's animations. */
|
|
622
|
+
function animationOf(doc: CompiledDocument, animation: string): CompiledDocument['animations'][number] {
|
|
588
623
|
const anim = doc.animations.find((a) => a.name === animation);
|
|
589
624
|
if (anim === undefined) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(', ')}]`);
|
|
590
|
-
|
|
591
|
-
|
|
625
|
+
return anim;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* A walk opened (issue #1276): the refusals a walk makes before its first pose,
|
|
630
|
+
* then pose 0 — the reset — and the state the walk steps on from. Under
|
|
631
|
+
* `reset: 'setup'` pose 0 is the setup pose, reset; under `animation` it is the
|
|
632
|
+
* animation applied at 0 and reset there, a step of 0 (`stepWalk`'s `first`).
|
|
633
|
+
*/
|
|
634
|
+
function openWalk<P>(doc: CompiledDocument, anim: CompiledDocument['animations'][number], plant: TimelinePlant, reset: RawReset, mode: WalkMode, shape: PoseShape<P>): { track: TrackState<P>; first: P } {
|
|
592
635
|
const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
|
|
593
636
|
if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
|
|
594
|
-
const raw: TimelinePlant = { ...plant, ...roundPlant(mode) };
|
|
595
|
-
const resolve = plant.shown ?? shownAttachment;
|
|
596
|
-
const duration = anim.timelines.duration;
|
|
597
637
|
const ctx = freshStepContext(plant.physicsStep);
|
|
598
638
|
if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
|
|
599
|
-
|
|
600
|
-
let trackTime = 0;
|
|
601
|
-
let last = -1;
|
|
639
|
+
const track: TrackState<P> = { doc, anim, duration: anim.timelines.duration, mode, plant, raw: { ...plant, ...roundPlant(mode) }, resolve: plant.shown ?? shownAttachment, shape, ctx, trackTime: 0, last: -1 };
|
|
602
640
|
if (reset === 'setup') {
|
|
603
641
|
// The reset taken at the setup pose, before the animation is applied (`src/deformmeasure.ts`'s `poseAt`): pose 0 is the setup pose.
|
|
604
642
|
const setup = poseSetup(doc, { ...plant, ...roundPlant(mode) }, ctx);
|
|
@@ -606,36 +644,75 @@ function walkEach<P>(doc: CompiledDocument, animation: string, steps: readonly n
|
|
|
606
644
|
if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join('; ')}`);
|
|
607
645
|
if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError('the raw walk\'s reset pose was not posed');
|
|
608
646
|
ctx.phase = 'update';
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
for (let i = reset === 'setup' ? 1 : 0; i <= steps.length; i++) {
|
|
612
|
-
const dt = i === 0 ? 0 : steps[i - 1];
|
|
613
|
-
trackTime += dt;
|
|
614
|
-
const t = mode.loop ? (mode.time ?? loopedTime)(trackTime, duration, trackTime - dt) : trackTime < duration ? trackTime : duration;
|
|
615
|
-
const before = ctx.time;
|
|
616
|
-
ctx.time += mode.clock === undefined ? dt : mode.clock(dt, t - (i === 0 ? 0 : Math.max(last, 0)));
|
|
617
|
-
const sliders: SliderApplication[] = [];
|
|
618
|
-
const world = shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
|
|
619
|
-
if (i === 0) ctx.phase = 'update';
|
|
620
|
-
const placeholders = new Map<string, string | null>();
|
|
621
|
-
const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
|
|
622
|
-
if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join('; ')}`);
|
|
623
|
-
const evalOrder = plant.drawOrder ?? drawOrderAt;
|
|
624
|
-
let order = evalOrder(doc.slots.length, anim.timelines.drawOrder, t) ?? doc.slots.map((_s, k) => k);
|
|
625
|
-
for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
626
|
-
const drawOrder = order.map((k) => doc.slots[k].name);
|
|
627
|
-
const states = attachmentStates(doc, resolve, placeholders, { timelines: anim.timelines, t }, sliders, plant);
|
|
628
|
-
if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
|
|
629
|
-
const rank = new Map(drawOrder.map((n, r) => [n, r]));
|
|
630
|
-
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
631
|
-
// A looping walk fires no event here: what fires across the wrap was not measured, and the walk's one reader (A10) reads none. Nor does it cut its
|
|
632
|
-
// drawn attachments through a clip (`NO_CLIPPED_ROWS`), so its `clipped` rows are empty.
|
|
633
|
-
const from = last;
|
|
634
|
-
const events = (): RawEvent[] => (mode.loop ? [] : (plant.events ?? eventsFired)(anim.timelines.events, from, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]));
|
|
635
|
-
const pose = shape.step(doc, { trackTime, animationTime: t, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
|
|
636
|
-
last = t;
|
|
637
|
-
visit(pose, visited++);
|
|
647
|
+
return { track, first: shape.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, mode) };
|
|
638
648
|
}
|
|
649
|
+
return { track, first: stepWalk(track, 0, true) };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* One step of a walk (the header's *One step*): the track time moved by `dt`,
|
|
654
|
+
* the animation applied at the mode's time from the setup pose, the physics
|
|
655
|
+
* clock moved by the step, and the pose assembled in the track's shape.
|
|
656
|
+
* `first` is the reset pose under `reset: 'animation'` — a step of 0 that
|
|
657
|
+
* resets the physics there — and nothing else passes it.
|
|
658
|
+
*/
|
|
659
|
+
function stepWalk<P>(track: TrackState<P>, dt: number, first = false): P {
|
|
660
|
+
const { doc, anim, duration, mode, plant, raw, resolve, shape, ctx } = track;
|
|
661
|
+
track.trackTime += dt;
|
|
662
|
+
const trackTime = track.trackTime;
|
|
663
|
+
const last = track.last;
|
|
664
|
+
const t = mode.loop ? (mode.time ?? loopedTime)(trackTime, duration, trackTime - dt) : trackTime < duration ? trackTime : duration;
|
|
665
|
+
const before = ctx.time;
|
|
666
|
+
ctx.time += mode.clock === undefined ? dt : mode.clock(dt, t - (first ? 0 : Math.max(last, 0)));
|
|
667
|
+
const sliders: SliderApplication[] = [];
|
|
668
|
+
const world = shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
|
|
669
|
+
if (first) ctx.phase = 'update';
|
|
670
|
+
const placeholders = new Map<string, string | null>();
|
|
671
|
+
const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
|
|
672
|
+
if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join('; ')}`);
|
|
673
|
+
const evalOrder = plant.drawOrder ?? drawOrderAt;
|
|
674
|
+
let order = evalOrder(doc.slots.length, anim.timelines.drawOrder, t) ?? doc.slots.map((_s, k) => k);
|
|
675
|
+
for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
676
|
+
const drawOrder = order.map((k) => doc.slots[k].name);
|
|
677
|
+
const states = attachmentStates(doc, resolve, placeholders, { timelines: anim.timelines, t }, sliders, plant);
|
|
678
|
+
if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
|
|
679
|
+
const rank = new Map(drawOrder.map((n, r) => [n, r]));
|
|
680
|
+
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
681
|
+
// A10's looping walk fires no event here — its one reader reads none — nor cuts its drawn attachments through a clip (`NO_CLIPPED_ROWS`), so its
|
|
682
|
+
// `clipped` rows are empty. The live track's looping mode (`live`) fires its events across the wrap by the rule `./track.ts` states as measured.
|
|
683
|
+
const fire = plant.events ?? eventsFired;
|
|
684
|
+
const keys = anim.timelines.events;
|
|
685
|
+
const events = (): RawEvent[] => (mode.loop && mode.live !== true ? [] : (mode.loop ? fire(keys, last, t, rawNumber, t < last) : fire(keys, last, t, rawNumber)).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]));
|
|
686
|
+
const pose = shape.step(doc, { trackTime, animationTime: t, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
|
|
687
|
+
track.last = t;
|
|
688
|
+
return pose;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** `walkIn`'s walk, each pose assembled in `shape` and handed to `visit` in order as it is posed (`poseRawAnimationEach`): the walk opened, then stepped over `steps`. */
|
|
692
|
+
function walkEach<P>(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode, shape: PoseShape<P>, visit: (pose: P, index: number) => void): void {
|
|
693
|
+
const anim = animationOf(doc, animation);
|
|
694
|
+
const bad = steps.findIndex((s) => !Number.isFinite(s) || s < 0);
|
|
695
|
+
if (bad >= 0) throw new CoreInputError(`step ${bad} is ${steps[bad]}, not a finite time at or above 0`);
|
|
696
|
+
const { track, first } = openWalk(doc, anim, plant, reset, mode, shape);
|
|
697
|
+
let visited = 0;
|
|
698
|
+
visit(first, visited++);
|
|
699
|
+
for (const dt of steps) visit(stepWalk(track, dt), visited++);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* The raw entry's walk opened for stepping one `dt` at a time (issue #1276,
|
|
704
|
+
* `openTrack` in `./track.ts`): the same refusals in the same order as
|
|
705
|
+
* `walkIn`'s past the step list, pose 0 as `walkIn` poses it, and the state
|
|
706
|
+
* `stepRawTrack` steps. Every pose is assembled as the raw entry's
|
|
707
|
+
* (`RAW_SHAPE`).
|
|
708
|
+
*/
|
|
709
|
+
export function openRawTrack(doc: CompiledDocument, animation: string, plant: TimelinePlant, reset: RawReset, mode: WalkMode): { track: TrackState<RawPose>; first: RawPose } {
|
|
710
|
+
return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** One step of an opened raw track (`openRawTrack`): `walkIn`'s step, the step itself checked by the caller. */
|
|
714
|
+
export function stepRawTrack(track: TrackState<RawPose>, dt: number): RawPose {
|
|
715
|
+
return stepWalk(track, dt);
|
|
639
716
|
}
|
|
640
717
|
|
|
641
718
|
// --- #907 the setup-pose bounding box: begin ---
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core's live track (issue #1276): one animation opened on a track and
|
|
3
|
+
* stepped one `dt` at a time — a player's frame loop, whose steps are not known
|
|
4
|
+
* in advance — every pose the raw entry's (`RawPose` in `./raw.ts`), the doubles
|
|
5
|
+
* the core computed.
|
|
6
|
+
*
|
|
7
|
+
* It is not new math. Both batch walks — the raw entry's `poseRawAnimation`
|
|
8
|
+
* and A10's looping walk (`./walk.ts`) — take their steps up front and pose
|
|
9
|
+
* every step from the setup pose, carrying from one step to the next only the
|
|
10
|
+
* physics context, the running track time and the animation time of the step
|
|
11
|
+
* before. That carry is one object (`TrackState` in `./raw.ts`), and the batch
|
|
12
|
+
* walk is "open, then step over the list"; this track opens the same walk and
|
|
13
|
+
* takes its steps as they come. Measured before and after that split, over the
|
|
14
|
+
* nineteen tree rows' 54 animations: 956 batch walks — the raw entry at 12, 24
|
|
15
|
+
* and 60 fps from each reset, held three times past the duration, its two
|
|
16
|
+
* one-step jumps under each reset, `poseRawAnimationEach`, A10's walk and its
|
|
17
|
+
* scan on A10's schedule and at 60 fps over three cycles — 157,771 poses, every
|
|
18
|
+
* one the same bits (the PR of issue #1276 carries the table).
|
|
19
|
+
*
|
|
20
|
+
* ## The walk
|
|
21
|
+
*
|
|
22
|
+
* - **Held** (`loop: false`): the raw entry's walk — the track time the
|
|
23
|
+
* running sum of the steps, the animation time held at the duration, the
|
|
24
|
+
* physics clock moved by the step, events over `(previous animation time,
|
|
25
|
+
* animation time]` (`./raw.ts`, *One step*). `CO46` holds it to
|
|
26
|
+
* `poseRawAnimation` pose for pose, every field, bit for bit.
|
|
27
|
+
* - **Looped** (`loop: true`): A10's looping walk's time, clock and `reset`
|
|
28
|
+
* keys across the wrap (`./walk.ts`, *The walk*) — and, which that walk does
|
|
29
|
+
* not carry because its one reader reads neither, the events it fires across
|
|
30
|
+
* the wrap and the clipped rows it cuts. `CO46` holds it to
|
|
31
|
+
* `poseLoopingWalk` narrowed to what that walk carries, bit for bit.
|
|
32
|
+
* - **The reset is the caller's**, as on the raw entry (`RawReset`): pose 0
|
|
33
|
+
* (`first`) is posed when the track is opened, the batch walk's pose 0
|
|
34
|
+
* under the same reset, and every `step` is one pose after it.
|
|
35
|
+
* - **Neither keeps a value that is not finite** (`keep: false`, the raw
|
|
36
|
+
* entry's argument: a renderer handed one draws a different picture in
|
|
37
|
+
* silence). What the raw entry refuses is a drawn vertex, a clip or clipped
|
|
38
|
+
* vertex, a clipped UV and an event's numbers; a bone's matrix and a slot's
|
|
39
|
+
* colour channel it passes on (NaN, `null`), and so does this track. Of
|
|
40
|
+
* 108 forged animations of the tree rows (a leaf and its parent scaled past
|
|
41
|
+
* the largest double), 98 were refused at the first non-finite vertex and 4
|
|
42
|
+
* walked holding a non-finite bone no drawn vertex reads; `CO49` holds both
|
|
43
|
+
* halves, the second on a slotless bone chain.
|
|
44
|
+
*
|
|
45
|
+
* ## Across the wrap — what a looping track fires, measured
|
|
46
|
+
*
|
|
47
|
+
* Read off spine-core 4.3.13 — `AnimationState` with `setAnimation(0, name,
|
|
48
|
+
* true)`, pose 0 applied at 0 and reset there or reset at the setup pose
|
|
49
|
+
* with nothing applied, then per step `update(dt)`, `apply`,
|
|
50
|
+
* `Skeleton.update(dt)`, `updateWorldTransform(Physics.update)` — by a
|
|
51
|
+
* listener collecting `event` and `complete` in the order queued, on the
|
|
52
|
+
* tree rows' six animations keying events and on 300 seeded probes whose keys
|
|
53
|
+
* sit at 0, 1e-3 after it, in the middle, one float32 ulp and 1e-3 before the
|
|
54
|
+
* duration, on it, and one ulp and 1e-3 past the bone's last key (where the
|
|
55
|
+
* key sets the duration); over step lists that cross the wrap once and
|
|
56
|
+
* several times, land exactly on the duration, and carry the track past it by
|
|
57
|
+
* more than a cycle: 4,284 walks, 229,656 steps, 13,436 across the wrap, 8,846
|
|
58
|
+
* longer than a cycle, 52,880 events and 24,587 completes, each in the
|
|
59
|
+
* listener's order on every step. `CO47` holds the rule on its own population.
|
|
60
|
+
*
|
|
61
|
+
* - **A step whose animation time rose fires `(previous time, time]`**, as the
|
|
62
|
+
* raw entry's — so a step that carries the track past the duration once or
|
|
63
|
+
* more while its animation time still rises fires only that interval: the
|
|
64
|
+
* keys it passed on the way are not fired.
|
|
65
|
+
* - **A step whose animation time went down — the wrap — fires the keys after
|
|
66
|
+
* the previous time, in key order, then the keys at or before the new time,
|
|
67
|
+
* in key order** (`eventsFired`'s `wrapped`): the second interval opens at
|
|
68
|
+
* −1, so a key at 0 fires on every wrap. Read in key order instead, 7,680 of
|
|
69
|
+
* the steps read off; with the second interval opening at 0, 6,066; with
|
|
70
|
+
* nothing fired across the wrap, 12,956.
|
|
71
|
+
* - **A key at exactly the duration fires on the wrap step and on no other**,
|
|
72
|
+
* before the keys from 0: 11,978 such firings, every one on a wrap step. A
|
|
73
|
+
* key one float32 ulp before the duration is an ordinary key — it fired on a
|
|
74
|
+
* rising step 545 times, where the animation time reached it before the wrap.
|
|
75
|
+
* - **An equal time is not a wrap** (`loopedTime`: a track time of exactly the
|
|
76
|
+
* duration applies at 0). A track stepped by exactly the duration applies at
|
|
77
|
+
* 0 on every step, so it fires its keys at 0 on its first step and nothing
|
|
78
|
+
* after; stepped by half the duration it wraps on every second step.
|
|
79
|
+
* - **`complete` is not carried** — a listener is the player's. Measured, it is
|
|
80
|
+
* queued once on a step whose track time's whole cycles rose past 0 —
|
|
81
|
+
* `⌊T / d⌋ > 0` and `⌊T / d⌋ > ⌊T_before / d⌋`, on every step at a duration
|
|
82
|
+
* of 0 — however many cycles the step crossed, after the keys after the
|
|
83
|
+
* previous time and before the keys from 0. A player derives it from
|
|
84
|
+
* `trackTime` and `duration`.
|
|
85
|
+
*
|
|
86
|
+
* ## The clipped rows
|
|
87
|
+
*
|
|
88
|
+
* The looping mode cuts every drawn attachment under a clip as the raw entry
|
|
89
|
+
* does — the oracle's rows, and the core's own decomposition under a concave
|
|
90
|
+
* or inverse clip (`clipThrough`) — where A10's walk cuts none. That is a
|
|
91
|
+
* mode and not a measurement: the cut is per pose and does not read whether
|
|
92
|
+
* the track loops. `CO48` holds it: on the clipping probe and the tree row
|
|
93
|
+
* that clips, every looping pose, past the wrap included, equals the raw
|
|
94
|
+
* entry's one step to its animation time in every field but the track time
|
|
95
|
+
* and the events (4,009 of 4,009 on the tree row, 690 of them cutting 12,420
|
|
96
|
+
* rows).
|
|
97
|
+
*
|
|
98
|
+
* ## The oracle chain
|
|
99
|
+
*
|
|
100
|
+
* Nothing here is held to spine-core directly but the events: the live track
|
|
101
|
+
* is held to the batch walk (`CO46`), the held batch walk to spine-core's
|
|
102
|
+
* render and deform-measure recipes (`CR03`), the looped one to A10's recipe
|
|
103
|
+
* (`CO25`), and both to `tools/pose_oracle.ts`'s stepped dump, whose schedule
|
|
104
|
+
* is `stepSchedule` in `./constraints_physics.ts`. A second dump would be the
|
|
105
|
+
* same numbers twice.
|
|
106
|
+
*
|
|
107
|
+
* ⛔ **A track refused part-way stays refused.** A step is checked before
|
|
108
|
+
* anything moves; a refusal past that — a construct the core leaves out, a
|
|
109
|
+
* non-finite vertex — comes after the track time and the physics have moved,
|
|
110
|
+
* so every later `step` refuses naming the step it was refused at, and the
|
|
111
|
+
* caller opens the track again. Seeking is the caller's too: a physics rig has
|
|
112
|
+
* history, so a seek is "open, then step to the time".
|
|
113
|
+
*/
|
|
114
|
+
import { CoreInputError, type CompiledDocument } from './index.ts';
|
|
115
|
+
import { openRawTrack, stepRawTrack, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
|
|
116
|
+
import type { TimelinePlant } from './animation.ts';
|
|
117
|
+
|
|
118
|
+
/** How a live track is opened: whether it loops, and where its physics is reset (`RawReset`). Both are the caller's and neither has a default. */
|
|
119
|
+
export interface TrackOptions {
|
|
120
|
+
loop: boolean;
|
|
121
|
+
reset: RawReset;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A live track (`openTrack`): pose 0, then one pose per `step`. */
|
|
125
|
+
export interface LiveTrack {
|
|
126
|
+
/** Pose 0, posed when the track was opened: the batch walk's first pose under the same `reset`. */
|
|
127
|
+
readonly first: RawPose;
|
|
128
|
+
/** The next pose: the track time moved by `dt`, a finite time at or above 0. */
|
|
129
|
+
step(dt: number): RawPose;
|
|
130
|
+
/** The running sum of the steps taken, 0 before the first. */
|
|
131
|
+
readonly trackTime: number;
|
|
132
|
+
/** The time the latest pose applied the animation at (0 for pose 0). */
|
|
133
|
+
readonly animationTime: number;
|
|
134
|
+
/** The runtime's duration of the animation (`CoreAnimationTimelines.duration`). */
|
|
135
|
+
readonly duration: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The two modes: a track that holds at its duration is the raw entry's walk; a looping one fires its events across the wrap and cuts its clipped rows (`live`). Neither keeps a non-finite value. */
|
|
139
|
+
const HELD: WalkMode = { loop: false, keep: false };
|
|
140
|
+
const LOOPED: WalkMode = { loop: true, keep: false, live: true };
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* One animation opened on a live track (the header): refused by name where the
|
|
144
|
+
* batch walk refuses the same document and animation, pose 0 posed, then
|
|
145
|
+
* stepped one `dt` at a time.
|
|
146
|
+
*/
|
|
147
|
+
export function openTrack(doc: CompiledDocument, animation: string, options: TrackOptions, plant: TimelinePlant = {}): LiveTrack {
|
|
148
|
+
if (typeof options !== 'object' || options === null || typeof options.loop !== 'boolean') throw new CoreInputError(`the live track's options.loop is ${JSON.stringify((options as Partial<TrackOptions> | null)?.loop)}, not true or false`);
|
|
149
|
+
if (options.reset !== 'animation' && options.reset !== 'setup') throw new CoreInputError(`the live track's options.reset is ${JSON.stringify(options.reset)}, not "animation" or "setup"`);
|
|
150
|
+
const { track, first } = openRawTrack(doc, animation, plant, options.reset, options.loop ? LOOPED : HELD);
|
|
151
|
+
return new Track(track, first);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class Track implements LiveTrack {
|
|
155
|
+
readonly first: RawPose;
|
|
156
|
+
private readonly track: TrackState<RawPose>;
|
|
157
|
+
private steps = 0;
|
|
158
|
+
private latest: number;
|
|
159
|
+
private refused: string | null = null;
|
|
160
|
+
|
|
161
|
+
constructor(track: TrackState<RawPose>, first: RawPose) {
|
|
162
|
+
this.track = track;
|
|
163
|
+
this.first = first;
|
|
164
|
+
this.latest = first.animationTime;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get trackTime(): number {
|
|
168
|
+
return this.track.trackTime;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get animationTime(): number {
|
|
172
|
+
return this.latest;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get duration(): number {
|
|
176
|
+
return this.track.duration;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
step(dt: number): RawPose {
|
|
180
|
+
const n = this.steps + 1;
|
|
181
|
+
if (this.refused !== null) throw new CoreInputError(`the live track was refused at step ${this.refused}, after its time and physics had moved; open the track again`);
|
|
182
|
+
if (typeof dt !== 'number' || !Number.isFinite(dt) || dt < 0) throw new CoreInputError(`the live track's step ${n} is ${String(dt)}, not a finite time at or above 0`);
|
|
183
|
+
try {
|
|
184
|
+
const pose = stepRawTrack(this.track, dt);
|
|
185
|
+
this.steps = n;
|
|
186
|
+
this.latest = pose.animationTime;
|
|
187
|
+
return pose;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err instanceof CoreInputError) this.refused = `${n} (${err.message})`;
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
package/src/core/walk.ts
CHANGED
|
@@ -57,8 +57,10 @@
|
|
|
57
57
|
* refuses, this entry writes the double; on 4,333 poses holding one, every
|
|
58
58
|
* NaN and Infinity stood where spine-core's did.
|
|
59
59
|
*
|
|
60
|
-
* Events are not carried: A10 reads none
|
|
61
|
-
*
|
|
60
|
+
* Events are not carried: A10 reads none. What a looping track fires across
|
|
61
|
+
* the wrap is measured where a reader wants it — the live track
|
|
62
|
+
* (`./track.ts`, issue #1276), whose looping mode is this walk's with the
|
|
63
|
+
* events and the clipped rows put back. Nor are the clipped rows, and the walk
|
|
62
64
|
* does not cut them (issue #1179): every clip it starts is still planned, so
|
|
63
65
|
* a clip the core does not draw refuses the walk by name, as below.
|
|
64
66
|
*
|