spine-rigc 1.5.3 → 1.6.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.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Construct 4 of the core, its remainder (issue #955, step 2f of issue #380):
3
+ * the draw order — at the setup pose, and a draw-order timeline's at a
4
+ * sample time and under a slider.
5
+ *
6
+ * Every rule below was measured by posing hand-written skeletons through
7
+ * `tools/pose_oracle.ts dump` (spine-core 4.3.13) and reading the slot names
8
+ * the dump lists (`drawOrder`); nothing here was written from the runtime's
9
+ * source. The core suite's `CD` controls hold the same skeletons against the
10
+ * core.
11
+ *
12
+ * - **The setup order is the slot order** (`ModelSlot`: the model's slot
13
+ * array is the draw order), on every probe and on 19 of 19 recipes.
14
+ * - **A key's moves are a permutation of the SETUP order**, whatever the
15
+ * order before it: each moved slot lands at its setup index plus its
16
+ * offset, and the slots no key moves fill the remaining places in setup
17
+ * order. The moves are applied as a set — the model holds them in the order
18
+ * the spec states them, and the order they are listed in changes nothing.
19
+ * - **Before the first key the setup order; a key with no moves restores
20
+ * it.** Measured on 3,600 samples of 120 random timelines (three to eight
21
+ * slots, one to four keys, one to three moves each, empty keys among them):
22
+ * 0 misses.
23
+ * - **Key times are float32**, as every key's (`./animation.ts`).
24
+ * - **A slider applies its animation's draw-order key after the sample's, at
25
+ * any mix but 0, and writes nothing before its first key**: a slider at
26
+ * 0.5 over a key at 0.3 moving the first of three slots two places read
27
+ * `s1, s2, s0` at setup at mix 1, 0.5, 0.01 and −1, and the setup order at
28
+ * mix 0 and at a time before the key.
29
+ *
30
+ * 🚫 **Two moves landing on one place are refused by `readModel`, by name.**
31
+ * The writer refuses a slot moved twice and a destination outside the slots
32
+ * (`compileDrawOrder` in `src/compile.ts`), not two slots sent to one
33
+ * destination; the runtime's fill then leaves a slot out and lists another
34
+ * twice (the compiler's note on the parser), which is a parser's accident
35
+ * and not an order to reproduce.
36
+ */
37
+ import type { ModelSlot } from '../model.ts';
38
+ import { keyIndexAt } from './animation.ts';
39
+
40
+ /** One draw-order key, as read: its time (float32) and its moves as slot indices, empty for the setup order. */
41
+ export interface CoreDrawOrderKey {
42
+ time: number;
43
+ moves: Array<{ slot: number; offset: number }>;
44
+ }
45
+
46
+ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
47
+
48
+ /**
49
+ * An animation record's `drawOrder` keys, read: times strictly increasing,
50
+ * every move naming a slot of the document once per key, a whole offset
51
+ * landing inside the slots, no two moves landing on one place.
52
+ */
53
+ export function readDrawOrderKeys(value: unknown, label: string, slots: readonly ModelSlot[], problems: string[]): CoreDrawOrderKey[] {
54
+ const where = `${label}.drawOrder`;
55
+ if (!Array.isArray(value)) {
56
+ problems.push(`${where} is not a list`);
57
+ return [];
58
+ }
59
+ const indexOf = new Map(slots.map((s, i) => [s.name, i]));
60
+ const out: CoreDrawOrderKey[] = [];
61
+ let last = -Infinity;
62
+ value.forEach((k, i) => {
63
+ const at = `${where}[${i}]`;
64
+ if (!isRecord(k)) {
65
+ problems.push(`${at} is not an object`);
66
+ return;
67
+ }
68
+ for (const key of Object.keys(k)) if (key !== 'time' && key !== 'offsets') problems.push(`${at}: field "${key}" is not one a draw-order key carries; it carries [time, offsets]`);
69
+ const t = k.time;
70
+ if (typeof t !== 'number' || !Number.isFinite(t) || t < 0) problems.push(`${at}: time is ${JSON.stringify(t)}, not a finite time at or after 0`);
71
+ else if (t <= last) problems.push(`${at}: time ${t} is not after the key before it — the writer refuses key times that do not strictly increase`);
72
+ if (typeof t === 'number' && Number.isFinite(t)) last = Math.max(last, t);
73
+ const moves: CoreDrawOrderKey['moves'] = [];
74
+ if (k.offsets !== undefined) {
75
+ if (!Array.isArray(k.offsets)) problems.push(`${at}: offsets is not a list`);
76
+ else {
77
+ const landed = new Map<number, string>();
78
+ k.offsets.forEach((o: unknown, j: number) => {
79
+ const oat = `${at}.offsets[${j}]`;
80
+ if (!isRecord(o) || typeof o.slot !== 'string') {
81
+ problems.push(`${oat} names no slot`);
82
+ return;
83
+ }
84
+ const index = indexOf.get(o.slot);
85
+ if (index === undefined) {
86
+ problems.push(`${oat}: "${o.slot}" is not a slot of this document`);
87
+ return;
88
+ }
89
+ if (typeof o.offset !== 'number' || !Number.isInteger(o.offset)) {
90
+ problems.push(`${oat}: slot "${o.slot}" offset is ${JSON.stringify(o.offset)}, not a whole number`);
91
+ return;
92
+ }
93
+ if (moves.some((m) => m.slot === index)) problems.push(`${oat}: slot "${o.slot}" is moved twice in one key`);
94
+ const to = index + o.offset;
95
+ if (to < 0 || to >= slots.length) problems.push(`${oat}: slot "${o.slot}" is at ${index} and offset ${o.offset} lands at ${to}, outside the ${slots.length} slots`);
96
+ else if (landed.has(to)) problems.push(`${oat}: slot "${o.slot}" lands at ${to}, where slot "${landed.get(to)}" lands too — two moves on one place leave a slot out of the runtime's order`);
97
+ else landed.set(to, o.slot);
98
+ moves.push({ slot: index, offset: o.offset });
99
+ });
100
+ }
101
+ }
102
+ out.push({ time: Math.fround(typeof t === 'number' && Number.isFinite(t) ? t : 0), moves });
103
+ });
104
+ return out;
105
+ }
106
+
107
+ /** The slot indices in the order a key's moves put them — the header's permutation of the setup order. */
108
+ export function orderOf(count: number, moves: CoreDrawOrderKey['moves']): number[] {
109
+ const order = new Array<number>(count).fill(-1);
110
+ for (const m of moves) order[m.slot + m.offset] = m.slot;
111
+ const moved = new Set(moves.map((m) => m.slot));
112
+ const rest = Array.from({ length: count }, (_v, i) => i).filter((i) => !moved.has(i));
113
+ let next = 0;
114
+ for (let i = 0; i < count; i++) if (order[i] === -1) order[i] = rest[next++];
115
+ return order;
116
+ }
117
+
118
+ /** What evaluates a draw-order timeline: `drawOrderAt` unless a plant passes another. */
119
+ export type DrawOrderEvaluator = (count: number, keys: readonly CoreDrawOrderKey[], t: number) => number[] | null;
120
+
121
+ /** The order a timeline sets at `t` as slot indices, or `null` before its first key. */
122
+ export function drawOrderAt(count: number, keys: readonly CoreDrawOrderKey[], t: number): number[] | null {
123
+ const i = keyIndexAt(keys, t);
124
+ return i < 0 ? null : orderOf(count, keys[i].moves);
125
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Construct 4 of the core, its remainder (issue #955, step 2f of issue #380):
3
+ * the events an animation's event timeline fires between two samples — the
4
+ * oracle's per-sample `events` block, `[name, time, int, float, string]`.
5
+ *
6
+ * Every rule below was measured by posing hand-written skeletons through
7
+ * `tools/pose_oracle.ts dump` (spine-core 4.3.13) and reading the block back;
8
+ * nothing here was written from the runtime's source. The core suite's `CD`
9
+ * controls hold the same skeletons against the core.
10
+ *
11
+ * - **A sample lists the keys whose time is in `(previous sample's t, t]`**,
12
+ * the first sample's interval opening at −1: an event keyed at 0.25 fired
13
+ * at the sample at 0.25 and not at the one after; one keyed at 0 fired at
14
+ * the first sample; on an animation of duration 0 every sample sits at 0,
15
+ * and the keys at 0 fired at the first sample alone.
16
+ * - **Key times are float32**: an event keyed at 0.1 (float32
17
+ * 0.10000000149) did not fire at a sample at 0.1 but at the next.
18
+ * - **In key order**: two events keyed at one time fired in the order the
19
+ * keys are listed.
20
+ * - **The row's time is the key's (float32), and each of `int`, `float`,
21
+ * `string` is the key's where it states one, else the event
22
+ * definition's, else the parser's `0`, `0`, `""`** — an event declared with
23
+ * no string read `""`, not `null` (so `tools/pose_oracle.ts`' header, which
24
+ * said `null`, was wrong about the runtime and says so now). `volume` and
25
+ * `balance` are the audio's, and the block does not carry them.
26
+ * - **`float` is the double the document states**, the definition's and the
27
+ * key's alike: `0.1000065`, which rounds to `0.100007` as a double and to
28
+ * `0.100006` through `Math.fround`, read `0.100007` from each.
29
+ * - A slider fires no event into the block: the oracle lists the events of
30
+ * the sample's own animation.
31
+ */
32
+ import { gridRound } from './index.ts';
33
+
34
+ /** An event definition, as the model states it (`ModelEvent`): the payload a key falls back to. */
35
+ export interface CoreEventDef {
36
+ name: string;
37
+ int: number;
38
+ float: number;
39
+ string: string;
40
+ }
41
+
42
+ /** One event key, as read: its time (float32) and the payload it fires with, each field resolved. */
43
+ export interface CoreEventKey {
44
+ time: number;
45
+ name: string;
46
+ int: number;
47
+ float: number;
48
+ string: string;
49
+ }
50
+
51
+ /** One row of a sample's `events` block: `[name, time, int, float, string]`, the oracle's row. */
52
+ export type CoreEventRow = [string, number | null, number, number | null, string | null];
53
+
54
+ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
55
+ const EVENT_DEF_FIELDS = ['name', 'int', 'float', 'string', 'audio', 'volume', 'balance'] as const;
56
+ const EVENT_KEY_FIELDS = ['time', 'name', 'int', 'float', 'string', 'volume', 'balance'] as const;
57
+
58
+ function payload(raw: Record<string, unknown>, at: string, problems: string[]): { int?: number; float?: number; string?: string } {
59
+ const out: { int?: number; float?: number; string?: string } = {};
60
+ if (raw.int !== undefined) {
61
+ if (typeof raw.int !== 'number' || !Number.isInteger(raw.int)) problems.push(`${at}: int is ${JSON.stringify(raw.int)}, not a whole number`);
62
+ else out.int = raw.int;
63
+ }
64
+ if (raw.float !== undefined) {
65
+ if (typeof raw.float !== 'number' || !Number.isFinite(raw.float)) problems.push(`${at}: float is ${JSON.stringify(raw.float)}, not a finite number`);
66
+ else out.float = raw.float;
67
+ }
68
+ if (raw.string !== undefined) {
69
+ if (typeof raw.string !== 'string') problems.push(`${at}: string is ${JSON.stringify(raw.string)}, not a string`);
70
+ else out.string = raw.string;
71
+ }
72
+ for (const key of ['volume', 'balance'] as const) if (raw[key] !== undefined && (typeof raw[key] !== 'number' || !Number.isFinite(raw[key]))) problems.push(`${at}: ${key} is ${JSON.stringify(raw[key])}, not a finite number`);
73
+ return out;
74
+ }
75
+
76
+ /** The document's `events` section, read: a list of named definitions, each name once, each field of its type. */
77
+ export function readEventDefs(value: unknown, problems: string[]): Map<string, CoreEventDef> {
78
+ const out = new Map<string, CoreEventDef>();
79
+ if (!Array.isArray(value)) {
80
+ problems.push('events is not a list');
81
+ return out;
82
+ }
83
+ value.forEach((raw, i) => {
84
+ const at = `events[${i}]`;
85
+ if (!isRecord(raw) || typeof raw.name !== 'string' || raw.name === '') {
86
+ problems.push(`${at} names no event`);
87
+ return;
88
+ }
89
+ for (const key of Object.keys(raw)) if (!(EVENT_DEF_FIELDS as readonly string[]).includes(key)) problems.push(`${at} "${raw.name}": field "${key}" is not one this reader knows; it reads [${EVENT_DEF_FIELDS.join(', ')}]`);
90
+ if (raw.audio !== undefined && typeof raw.audio !== 'string') problems.push(`${at} "${raw.name}": audio is ${JSON.stringify(raw.audio)}, not a string`);
91
+ if (out.has(raw.name)) problems.push(`${at}: event "${raw.name}" is declared twice`);
92
+ const p = payload(raw, `${at} "${raw.name}"`, problems);
93
+ out.set(raw.name, { name: raw.name, int: p.int ?? 0, float: p.float ?? 0, string: p.string ?? '' });
94
+ });
95
+ return out;
96
+ }
97
+
98
+ /** An animation record's `events` keys, read: each names a declared event, times do not go backwards (equal times are two firings), each field of its type. */
99
+ export function readEventKeys(value: unknown, label: string, defs: ReadonlyMap<string, CoreEventDef>, problems: string[]): CoreEventKey[] {
100
+ const where = `${label}.events`;
101
+ if (!Array.isArray(value)) {
102
+ problems.push(`${where} is not a list`);
103
+ return [];
104
+ }
105
+ const out: CoreEventKey[] = [];
106
+ let last = -Infinity;
107
+ value.forEach((raw, i) => {
108
+ const at = `${where}[${i}]`;
109
+ if (!isRecord(raw)) {
110
+ problems.push(`${at} is not an object`);
111
+ return;
112
+ }
113
+ for (const key of Object.keys(raw)) if (!(EVENT_KEY_FIELDS as readonly string[]).includes(key)) problems.push(`${at}: field "${key}" is not one an event key carries; it carries [${EVENT_KEY_FIELDS.join(', ')}]`);
114
+ const t = raw.time;
115
+ if (typeof t !== 'number' || !Number.isFinite(t) || t < 0) problems.push(`${at}: time is ${JSON.stringify(t)}, not a finite time at or after 0`);
116
+ else if (t < last) problems.push(`${at}: time ${t} is before the key before it — the writer refuses event times that go backwards`);
117
+ if (typeof t === 'number' && Number.isFinite(t)) last = Math.max(last, t);
118
+ const def = typeof raw.name === 'string' ? defs.get(raw.name) : undefined;
119
+ if (def === undefined) {
120
+ problems.push(`${at}: event ${JSON.stringify(raw.name)} is not declared in the document's events`);
121
+ return;
122
+ }
123
+ const p = payload(raw, at, problems);
124
+ out.push({ time: Math.fround(typeof t === 'number' && Number.isFinite(t) ? t : 0), name: def.name, int: p.int ?? def.int, float: p.float ?? def.float, string: p.string ?? def.string });
125
+ });
126
+ return out;
127
+ }
128
+
129
+ /** What lists the events fired over `(last, t]`: `eventsFired` unless a plant passes another. */
130
+ export type EventsFired = (keys: readonly CoreEventKey[], last: number, t: number, round?: (v: number) => number | null) => CoreEventRow[];
131
+
132
+ /** The rows of the keys fired over `(last, t]`, in key order — the header's rules, rounded as the oracle rounds. */
133
+ export function eventsFired(keys: readonly CoreEventKey[], last: number, t: number, round: (v: number) => number | null = gridRound): CoreEventRow[] {
134
+ return keys.filter((k) => k.time > last && k.time <= t).map((k): CoreEventRow => [k.name, round(k.time), k.int, round(k.float), k.string]);
135
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * The core's hooks for the deform survey (issue #969, step 3e of issue #380):
3
+ * the poses `src/deformmeasure.ts` takes, and what it reads off them, in full
4
+ * doubles — the raw entry's arithmetic (`./raw.ts`) reached at the survey's
5
+ * own seams rather than at a renderer's.
6
+ *
7
+ * The survey poses a skeleton two ways and reads four things off it. Each is
8
+ * an entry here, and each was measured against spine-core 4.3.13 running the
9
+ * survey's own recipe on hand-written documents and on every built row of the
10
+ * tree, at tolerance 0 — every double equal, every float32 equal to the bit
11
+ * (the selftest's `DM` controls; the PR of issue #969 carries the counts). The
12
+ * runtime's source was not read.
13
+ *
14
+ * - **A jump** (`poseJump`) — `deformmeasure.ts`'s `poseAt`: a fresh skeleton,
15
+ * the setup pose, `update(0)`, `Physics.reset`; then ONE step of `time`
16
+ * (`AnimationState.update(time)`, `apply`, `Skeleton.update(time)`,
17
+ * `Physics.update`). It is the raw walk's `setup` reset with one step
18
+ * (`poseRawAnimation(doc, animation, [time], {}, 'setup')`, measured there),
19
+ * returning what the walk computes and does not hand out: the world
20
+ * transforms, the slot rows and every slot's shown record with the deform
21
+ * array its timelines set.
22
+ * - **A dial** (`poseDial`) — the survey's slider probe: the setup pose with ONE
23
+ * local field of one bone overridden (`bone.pose[field] = u` after
24
+ * `setupPose`) or one bone-less slider's time overridden
25
+ * (`slider.pose.time = u`), `update(0)`, `Physics.reset`. The override is
26
+ * the pose's, not the setup's: a slider's non-additive blend still reads the
27
+ * bone's SETUP value (`CoreSliderRecord.setup`), so the view carries the
28
+ * overridden bone and the slider records keep the ones they were read with.
29
+ * Two readings come back:
30
+ * - `read` — the dial's property as `FromProperty.value(skeleton,
31
+ * bone.appliedPose, local, zeros)` returns it after the whole update (and,
32
+ * under `local`, after `BonePose.validateLocalTransform`): `sourceValue` in
33
+ * `./constraints.ts` with no offset, over the solver's LAST state — every
34
+ * bone's local values as the constraints left them (`applyConstraints`'
35
+ * `settled`), its world as they left it;
36
+ * - `applied` — `SliderPose.time` off the applied pose: the time
37
+ * `sliderTime` computed when the slider ran, before its animation's loop
38
+ * wrap (`SliderApplication.time`); a slider that did not run (mix 0, or
39
+ * not active under the skin) keeps its pose's time, the record's `time`
40
+ * (the override, on a bone-less dial).
41
+ * - **The mesh's world vertices with the slot's deform as posed, cleared, or
42
+ * replaced** (`meshWorld`) — `VertexAttachment.computeWorldVertices(skeleton,
43
+ * slot, …)` over the slot's `appliedPose.deform`: an empty array reads the
44
+ * attachment's own vertices (float32, `worldVertices`' default coords); a
45
+ * non-empty one IS the vertex array — positions unweighted, offsets added to
46
+ * each binding's float32 coordinate weighted (`deformedVertices`) — read as
47
+ * the doubles it holds (`EXACT_COORDS`). The array is the slot's, not the
48
+ * attachment's: the runtime reads whatever the slot carries, so the posed
49
+ * array is the one the shown record's timelines set (`ShownGeometry.deform`)
50
+ * whichever record the vertices are asked of — including another mesh's
51
+ * array, when the slot shows another mesh that a timeline deforms. That
52
+ * array is read as the runtime reads it whatever its length: its first
53
+ * `deformLength` numbers, and NaN past its end (an unweighted mesh reads the
54
+ * array AS its positions; a weighted one adds it binding by binding) —
55
+ * measured on a slot switched to a three-vertex mesh while a four-vertex
56
+ * one's key is asked of it, and on the reverse (`DM02`).
57
+ * - **What a slot shows and at what alpha** (`slotDraw`): the slot row's shown
58
+ * name and light alpha, the shown record's timeline identity
59
+ * (`timelineIdentity` in `./deform.ts` — the runtime's
60
+ * `Attachment.timelineAttachment` compared by record) and its own colour's
61
+ * alpha on a mesh or linked mesh (1 on anything else, as the survey reads
62
+ * `shown instanceof MeshAttachment ? shown.color.a : 1`).
63
+ * - **Float32 rows** (`float32Rows`): the survey holds world vertices in a
64
+ * `Float32Array` (`computeWorldVertices` into one), so the core's doubles are
65
+ * stored through the same conversion — a store, `Math.fround` per value.
66
+ *
67
+ * ⛔ Nothing is posed that the core would leave out: a construct the raw entry
68
+ * refuses (`./raw.ts`), a block the setup pose leaves absent that the survey
69
+ * reads (bones, slots, the shown records), a record the survey names that the
70
+ * view does not hold — each is a `CoreInputError` naming it, and the survey's
71
+ * seam then says which poser it used and why.
72
+ *
73
+ * ## Purity
74
+ *
75
+ * As the rest of the core: nothing from the Spine runtime package, nothing
76
+ * from `src/transform.ts`, no clock, no randomness, no I/O.
77
+ */
78
+ import type { ModelBone } from '../model.ts';
79
+ import { activeBones, constraintRecords, CoreInputError, poseSetup, rawNumber, readColour, shownAttachment, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
80
+ import { posedBoneWorld, posedSlots } from './animation.ts';
81
+ import { applyConstraints, constraintsAbsentWhy, pathAnimationsWhy, sourceValue, type SolverState, type TransformProperty } from './constraints.ts';
82
+ import { freshStepContext, steppedPreviousPassWhy } from './constraints_physics.ts';
83
+ import type { CoreSliderRecord, SliderApplication } from './constraints_slider.ts';
84
+ import { attachmentStates, deformedVertices, deformLength, timelineIdentity } from './deform.ts';
85
+ import { EXACT_COORDS, worldVertices, type ShownGeometry } from './vertices.ts';
86
+ import { worldTransforms, type CoreWorld } from './world.ts';
87
+
88
+ const RAW_PLANT: CorePlant = { round: rawNumber };
89
+
90
+ /** The six local fields a dial can drive, as a `ModelBone` names them. */
91
+ export const DIAL_BONE_FIELDS = ['rotation', 'x', 'y', 'scaleX', 'scaleY', 'shearY'] as const;
92
+ export type DialBoneField = (typeof DIAL_BONE_FIELDS)[number];
93
+
94
+ /** One pose the survey reads: the view it was posed under, the world transforms, the slot rows and the shown records. */
95
+ export interface CoreSurveyPose {
96
+ doc: CompiledDocument;
97
+ world: ReadonlyMap<string, CoreWorld>;
98
+ slots: readonly CoreSlotRow[];
99
+ shown: readonly ShownGeometry[];
100
+ }
101
+
102
+ /** A dial's override: one bone's local field, or one bone-less slider's time. */
103
+ export type DialOverride = { bone: string; field: DialBoneField; value: number } | { slider: string; time: number };
104
+
105
+ /** A dial pose, and its two readings (the header's *A dial*). */
106
+ export interface CoreDialPose extends CoreSurveyPose {
107
+ /** The slider's property read off its bone, or `null` on a bone-less slider. */
108
+ read: number | null;
109
+ applied: number;
110
+ }
111
+
112
+ /** The slider record `name` of the view, refused by name when it has none. */
113
+ export function sliderRecordOf(doc: CompiledDocument, name: string): CoreSliderRecord {
114
+ const c = doc.constraints.find((x) => x.kind === 'slider' && x.name === name);
115
+ if (c?.record?.kind !== 'slider') throw new CoreInputError(`the model document carries no slider "${name}" (it declares [${doc.constraints.filter((x) => x.kind === 'slider').map((x) => x.name).join(', ')}])`);
116
+ return c.record;
117
+ }
118
+
119
+ /**
120
+ * The jump (the header's first entry): the setup pose reset, then one step of
121
+ * `time`, as `poseRawAnimation(doc, animation, [time], {}, 'setup')` walks it.
122
+ */
123
+ export function poseJump(doc: CompiledDocument, animation: string, time: number): CoreSurveyPose {
124
+ const anim = doc.animations.find((a) => a.name === animation);
125
+ if (anim === undefined) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(', ')}]`);
126
+ if (!Number.isFinite(time) || time < 0) throw new CoreInputError(`the jump's time is ${time}, not a finite time at or above 0`);
127
+ const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
128
+ if (why !== null) throw new CoreInputError(`the jump leaves the bones out: ${why}`);
129
+ const ctx = freshStepContext();
130
+ const reset = poseSetup(doc, RAW_PLANT, ctx);
131
+ const resetWhy = reset.absent.filter(([b]) => b === 'setup.bones');
132
+ if (resetWhy.length > 0 || reset.world === null) throw new CoreInputError(`the jump's reset pose leaves ${resetWhy.map(([b, w]) => `${b} out (${w})`).join('; ') || 'the bones out'}`);
133
+ ctx.phase = 'update';
134
+ const t = time < anim.timelines.duration ? time : anim.timelines.duration;
135
+ const before = ctx.time;
136
+ ctx.time += time;
137
+ const sliders: SliderApplication[] = [];
138
+ const posed = posedBoneWorld(doc, anim.timelines, t, RAW_PLANT, anim.constraints, sliders, { ctx, before });
139
+ const placeholders = new Map<string, string | null>();
140
+ const slots = posedSlots(doc, anim.timelines, t, RAW_PLANT, sliders, placeholders);
141
+ if (slots.conflicts.length > 0) throw new CoreInputError(`the jump leaves the slots out: ${slots.conflicts.join('; ')}`);
142
+ const states = attachmentStates(doc, shownAttachment, placeholders, { timelines: anim.timelines, t }, sliders, {});
143
+ if (states.why.length > 0) throw new CoreInputError(`the jump leaves the attachments out: ${states.why.join('; ')}`);
144
+ return { doc, world: posed.world, slots: slots.rows, shown: states.shown };
145
+ }
146
+
147
+ /** The view with the override written into the pose — a bone's local field, or a slider record's time. */
148
+ function overridden(doc: CompiledDocument, override: DialOverride): CompiledDocument {
149
+ if ('bone' in override) {
150
+ if (!doc.bones.some((b) => b.name === override.bone)) throw new CoreInputError(`the dial's bone "${override.bone}" is not a bone of the model document`);
151
+ return { ...doc, bones: doc.bones.map((b): ModelBone => (b.name === override.bone ? { ...b, [override.field]: override.value } : b)) };
152
+ }
153
+ sliderRecordOf(doc, override.slider);
154
+ return {
155
+ ...doc,
156
+ constraints: doc.constraints.map((c) => (c.kind === 'slider' && c.name === override.slider && c.record?.kind === 'slider' ? { ...c, record: { ...c.record, time: override.time } } : c)),
157
+ };
158
+ }
159
+
160
+ const ZERO_OFFSETS: Record<TransformProperty, number> = { rotate: 0, x: 0, y: 0, scaleX: 0, scaleY: 0, shearY: 0 };
161
+
162
+ /**
163
+ * The dial (the header's second entry): the setup pose with `override`
164
+ * written into it, every physics state reset, and `slider`'s two readings.
165
+ */
166
+ export function poseDial(doc: CompiledDocument, override: DialOverride, slider: string): CoreDialPose {
167
+ const view = overridden(doc, override);
168
+ const record = sliderRecordOf(view, slider);
169
+ const posed = poseSetup(view, RAW_PLANT, freshStepContext());
170
+ const missing = posed.absent.filter(([b]) => b === 'setup.bones' || b === 'setup.slots');
171
+ if (missing.length > 0 || posed.world === null || posed.setup.slots === null) throw new CoreInputError(`the dial pose leaves ${missing.map(([b, w]) => `${b} out (${w})`).join('; ') || 'the bones out'}`);
172
+ if (posed.shown === null) throw new CoreInputError('the dial pose leaves the shown records out (a timeline keys a placeholder several skins fill)');
173
+ // The solver's last state, for the reading: the same pass `poseSetup` ran, with its `settled` sink.
174
+ const active = activeBones(view);
175
+ const applied: SliderApplication[] = [];
176
+ const sink: { state: SolverState | null } = { state: null };
177
+ const again = applyConstraints(view.bones, worldTransforms(view.bones, active), active, constraintRecords(view), null, applied, freshStepContext(), (state) => {
178
+ sink.state = state;
179
+ });
180
+ const settled = sink.state;
181
+ if (settled === null) throw new CoreInputError('the dial pose\'s solver left no state to read');
182
+ for (const [name, w] of posed.world) {
183
+ const v = again.get(name);
184
+ if (v === undefined || v.a !== w.a || v.b !== w.b || v.c !== w.c || v.d !== w.d || v.worldX !== w.worldX || v.worldY !== w.worldY) {
185
+ throw new CoreInputError(`the dial pose's two passes disagree on bone "${name}" — the reading would not be the pose's`);
186
+ }
187
+ }
188
+ const read = record.bone === null ? null : sourceValue(settled, { source: record.bone, localSource: record.local, offsets: ZERO_OFFSETS }, record.property);
189
+ const ran = applied.find((a) => a.name === slider);
190
+ return { doc: view, world: posed.world, slots: posed.setup.slots, shown: posed.shown, read, applied: ran === undefined ? record.time : ran.time };
191
+ }
192
+
193
+ /** `skin/slot/placeholder` — the identity a deform timeline keys a record under (`timelineIdentity`). */
194
+ export function recordIdentity(skin: string, slot: string, placeholder: string): string {
195
+ return `${skin}/${slot}/${placeholder}`;
196
+ }
197
+
198
+ /**
199
+ * The world vertices of the mesh record `skin/slot/placeholder` drawn on
200
+ * `slot` (the header's third entry): with the slot's deform as posed
201
+ * (`posed`), cleared (`cleared`), or replaced by `deform`. Doubles; the
202
+ * survey stores them through `float32Rows`.
203
+ */
204
+ export function meshWorld(pose: CoreSurveyPose, slot: string, record: { skin: string; slot: string; placeholder: string }, deform: 'posed' | 'cleared' | ArrayLike<number>): number[] {
205
+ const where = recordIdentity(record.skin, record.slot, record.placeholder);
206
+ const g = pose.doc.skins.find((k) => k.name === record.skin)?.attachments[record.slot]?.[record.placeholder]?.geometry;
207
+ if (g?.kind !== 'mesh') throw new CoreInputError(`${where} is not a mesh record of the model document`);
208
+ const slotRecord = pose.doc.slots.find((s) => s.name === slot);
209
+ if (slotRecord === undefined) throw new CoreInputError(`slot "${slot}" is not a slot of the model document`);
210
+ const bone = pose.world.get(slotRecord.bone);
211
+ if (bone === undefined) throw new CoreInputError(`slot "${slot}"'s bone "${slotRecord.bone}" has no world transform`);
212
+ const array: readonly number[] =
213
+ deform === 'cleared' ? [] : deform === 'posed' ? (pose.shown.find((s) => s.slot === slot)?.deform ?? []) : Array.from(deform);
214
+ if (array.length === 0) return worldVertices(g.vertices, bone, pose.world);
215
+ // The slot's array read as the runtime reads it whatever its length: its first `deformLength` numbers, and NaN past its end (the header's third entry).
216
+ const length = deformLength(g.vertices);
217
+ const fitted = array.length === length ? array : Array.from({ length }, (_v, i) => (i < array.length ? array[i] : Number.NaN));
218
+ return worldVertices(deformedVertices(g.vertices, fitted), bone, pose.world, EXACT_COORDS);
219
+ }
220
+
221
+ /** What one slot shows and at what alpha (the header's fourth entry). */
222
+ export interface CoreSlotDraw {
223
+ /** The shown attachment's name, or `null` for none. */
224
+ shown: string | null;
225
+ /** The shown record's timeline identity (`timelineIdentity`), or `null` when none can key it. */
226
+ identity: string | null;
227
+ /** The slot's light alpha as posed. */
228
+ slotAlpha: number;
229
+ /** The shown record's own colour alpha on a mesh or linked mesh; 1 otherwise. */
230
+ attachmentAlpha: number;
231
+ }
232
+
233
+ export function slotDraw(pose: CoreSurveyPose, slot: string): CoreSlotDraw {
234
+ const row = pose.slots.find((r) => r[0] === slot);
235
+ if (row === undefined) throw new CoreInputError(`slot "${slot}" has no row in the pose`);
236
+ const slotAlpha = row[5];
237
+ if (slotAlpha === null) throw new CoreInputError(`slot "${slot}"'s alpha is not finite`);
238
+ const shown = pose.shown.find((s) => s.slot === slot);
239
+ const record = shown === undefined ? undefined : pose.doc.skins.find((k) => k.name === shown.skin)?.attachments[slot]?.[shown.placeholder];
240
+ if (shown === undefined || record === undefined) return { shown: row[1], identity: null, slotAlpha, attachmentAlpha: 1 };
241
+ const identity = timelineIdentity(slot, { skin: shown.skin, placeholder: shown.placeholder, record });
242
+ const mesh = record.kind === 'mesh' || record.kind === 'linkedmesh';
243
+ return { shown: row[1], identity, slotAlpha, attachmentAlpha: mesh && record.color !== undefined ? readColour(record.color)[3] : 1 };
244
+ }
245
+
246
+ /** Doubles stored as the survey stores world vertices: one `Float32Array`, each value through the store's `Math.fround`. */
247
+ export function float32Rows(values: readonly number[]): Float32Array {
248
+ return Float32Array.from(values);
249
+ }