spine-rigc 1.9.1 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The sliders and the timelines their animations key, as A40 reads them
3
+ * (issue #1025, cut 4c-5 of step 4c of #380) — the census's F01–F04, F12,
4
+ * F15, F18, F19 and R3.
5
+ *
6
+ * ⭐ **The order is the runtime's.** The sliders in the `constraints` array's
7
+ * order, which is the update order and the order A40 calls "earlier" and
8
+ * "later" in; each animation's timelines in the order the runtime builds
9
+ * them — the slot timelines, the bone timelines, then ik, transform, path,
10
+ * physics, slider, the attachments' deform and sequence, the draw order and
11
+ * the events, each group's targets in the order the file keys them [measured:
12
+ * an animation keying `events`, `drawOrder`, `attachments`, `slider`,
13
+ * `physics`, `path`, `transform`, `ik`, `slots`, `bones` in that key order
14
+ * loads them slots, bones, ik, transform, path, physics, slider, deform,
15
+ * sequence, draw order, events]; each timeline's properties in the order it
16
+ * registers them. A40 groups the timelines by property in that order and
17
+ * prints one line per property shared, so the order is a fact both suppliers
18
+ * state, not a sort either applies.
19
+ *
20
+ * 🔑 **A property is named by an id both suppliers spell the same way**: the
21
+ * property's name, then the index of the bone, slot or constraint it
22
+ * addresses in the skeleton's order (`-1` for a physics timeline naming no
23
+ * constraint), then — for a deform or sequence timeline — the attachment it
24
+ * is keyed on as `skin/slot/placeholder`. The runtime spells the last as an
25
+ * object's serial number, which is the runtime's and not the rig's, so its
26
+ * supplier maps each attachment object to the address it is filed under; a
27
+ * physics `reset` and the draw-order and event timelines carry no index at
28
+ * all (measured: a named `reset` reads `physicsConstraintReset` alone).
29
+ *
30
+ * 🔸 **What applying a timeline additively does is a question, not a value.**
31
+ * It is posed — twice, with `add`, from two start states, under every skin —
32
+ * so a body asks for it (`behaviour`), and only for the timelines it needs:
33
+ * the validator answers with the runtime's probe, the model side with the
34
+ * core's (`src/core/additive.ts`), and the selftest puts every question a
35
+ * body asks to both.
36
+ *
37
+ * Links nothing from the runtime.
38
+ */
39
+
40
+ /** What applying a timeline twice with `add` does (`src/core/additive.ts`' header). */
41
+ export type AddBehaviour = 'accumulates' | 'overwrites' | 'inert';
42
+
43
+ /** One property a timeline registers: the id it is shared by, the property's name, and the target and property as A40's sentence names them. */
44
+ export interface SliderTimelineProperty {
45
+ readonly id: string;
46
+ readonly property: string;
47
+ readonly names: string;
48
+ }
49
+
50
+ /** One timeline of a slider's animation. */
51
+ export interface SliderTimelineFact {
52
+ /** The runtime class it loads as — the word A40's sentence names `apply` on. */
53
+ readonly runtimeClass: string;
54
+ readonly properties: readonly SliderTimelineProperty[];
55
+ }
56
+
57
+ /** One slider constraint. */
58
+ export interface SliderFact {
59
+ readonly name: string;
60
+ /** Its position in the skeleton's `constraints` array. */
61
+ readonly index: number;
62
+ /** Its setup `mix`. */
63
+ readonly mix: number;
64
+ readonly additive: boolean;
65
+ readonly skinRequired: boolean;
66
+ /** The skins whose `slider` list names it, in the skeleton's skin order. */
67
+ readonly skins: readonly string[];
68
+ /** The animation it applies, with its timelines in the runtime's order — or `null` where it applies none. */
69
+ readonly animation: { readonly name: string; readonly timelines: readonly SliderTimelineFact[] } | null;
70
+ }
71
+
72
+ /** What A40 reads beside the constraint facts. */
73
+ export interface SliderCompositionFacts {
74
+ /** Every slider, in the `constraints` array's order. */
75
+ readonly sliders: readonly SliderFact[];
76
+ /** What the `timeline`-th timeline of `animation` does when it is applied twice with `add` — posed. */
77
+ behaviour(animation: string, timeline: number): AddBehaviour;
78
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The stepped poses (issue #1025, cut 4c-5a of step 4c of #380) — the census's
3
+ * F01, F18, R4 (the posed `inherit`) and R5 as A10 reads them: the skeleton's
4
+ * setup pose with every physics state reset, every animation walked on a
5
+ * LOOPING track from that pose in equal steps, and a bone's inheritance mode
6
+ * posed at one time on a fresh, non-looping track.
7
+ *
8
+ * ⭐ **A pose is handed over whole, non-finite values in place.** A10's
9
+ * subject is the first number of a pose that is not finite, so a supplier
10
+ * that refused such a pose, or wrote it as something else, would answer a
11
+ * different question. The runtime's supplier reads what spine-core posed; the
12
+ * model side's is the core's looping walk (`src/core/walk.ts`), which returns
13
+ * the value rather than refusing it.
14
+ *
15
+ * 🔸 **The inheritance mode is `null` where the pose holds a mode**, and the
16
+ * value it holds, as `String` spells it, where it holds none — the one bone
17
+ * reading that can be wrong with every number finite (#733): the runtime's
18
+ * lookup leaves a spelling it does not resolve as no mode at all.
19
+ *
20
+ * The bone timelines A10 walks for its `inherit` keys are `BoneTimelineFacts`
21
+ * (`./bone_timelines.ts`), the family A24, A29 and A30 read.
22
+ *
23
+ * Links nothing from the runtime: `validate()` supplies it from spine-core
24
+ * (`spineSteppedPoses`), the model side from the document and the core
25
+ * (`../model/stepped_poses.ts`).
26
+ */
27
+ import type { PosedVertices, WorldTransform } from '../../nonfinite.ts';
28
+
29
+ /** One bone of a pose: its world transform as computed, and its inheritance mode — `null` for a mode, else the value held. */
30
+ export interface SteppedBone extends WorldTransform {
31
+ readonly inherit: string | null;
32
+ }
33
+
34
+ /** One slot of a pose: its light colour and its dark colour (`null` for a slot holding none), as computed. */
35
+ export interface SteppedSlot {
36
+ readonly name: string;
37
+ readonly colour: readonly [number, number, number, number];
38
+ readonly dark: readonly [number, number, number] | null;
39
+ }
40
+
41
+ /** One pose: every bone in skeleton order, every region and mesh shown in draw order, every slot in setup order. */
42
+ export interface SteppedFrame {
43
+ readonly bones: readonly SteppedBone[];
44
+ readonly drawn: readonly PosedVertices[];
45
+ readonly slots: readonly SteppedSlot[];
46
+ }
47
+
48
+ /** What A10 reads. */
49
+ export interface SteppedPoseFacts {
50
+ /** The skeleton's bone count. */
51
+ readonly boneCount: number;
52
+ /** The animations, in the file's order, each with the runtime's duration (the last key of every timeline, as float32). */
53
+ readonly steppedAnimations: ReadonlyArray<{ readonly name: string; readonly duration: number }>;
54
+ /** Whether the skeleton holds an animation of this name. */
55
+ hasAnimation(name: string): boolean;
56
+ /** Whether the skeleton holds a bone of this name. */
57
+ hasBone(name: string): boolean;
58
+ /**
59
+ * The bone's inheritance mode with the animation applied at `time` on a
60
+ * fresh, non-looping track from the setup pose, every physics state reset
61
+ * before it — `null` where the pose holds a mode, else that value spelled by
62
+ * `String`, and `undefined` for a bone the skeleton does not have.
63
+ */
64
+ posedInherit(animation: string, bone: string, time: number): string | null | undefined;
65
+ /** How the file spells a bone's setup `inherit`: `JSON.stringify` of the stated value (`"undefined"` for none). */
66
+ statedInherit(bone: string): string;
67
+ /** The setup pose, every physics state reset, before any animation is set. */
68
+ setup(): SteppedFrame;
69
+ /**
70
+ * The animation set on a LOOPING track over that setup pose, then `frames`
71
+ * steps of `step` each — the poses after steps 1 to `frames`, in order.
72
+ */
73
+ walk(animation: string, step: number, frames: number): SteppedFrame[];
74
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The model side's supply of `ConstraintTargetFacts` (issue #1025, cut 4c-5
3
+ * of step 4c of #380): the document's constraints and every animation's
4
+ * constraint groups, laid out as the Spine file lays them.
5
+ *
6
+ * - **The constraints**: the document's, in its order — which the emitter
7
+ * writes the `constraints` array in — each its name and its kind, the word
8
+ * the file spells its `type` with.
9
+ * - **The animations**: in the file's order (`fileAnimationOrder`, issue
10
+ * #1034); in each the five groups in A34's fixed order, a group only where
11
+ * the document holds an entry for it (`emitAnimation` writes a group only
12
+ * when the model holds something for it); each group's entries in the order
13
+ * the file keys them — an object the emitter fills in the document's order,
14
+ * so its names land as `keyedOrder` lands them, built here as that object;
15
+ * the timeline naming no physics constraint under the empty name the
16
+ * emitter spells it with, where the document spells it `*`
17
+ * (`EVERY_GLOBAL_PHYSICS`); each entry's timelines in the document's order,
18
+ * which the emitter writes unchanged (a name-keyed map no key-order pass
19
+ * touches, `src/keyorder.ts`). A document the reader accepted states every
20
+ * key array as a non-empty list, a named group's entry as named timelines,
21
+ * and only constraints of the kind its group names, so the clauses that
22
+ * refuse those shapes have nothing to fire on here.
23
+ * - **The reach** of a physics timeline naming none: the core's own reading,
24
+ * `unnamedReach` (`./constraints.ts`, which poses the timeline through
25
+ * `posedPhysics` and reads which records took the key), over the document's
26
+ * physics records in its order. The reader refuses a physics timeline name
27
+ * it does not know, so every name asked of it is one the core poses.
28
+ *
29
+ * Links nothing from the runtime.
30
+ */
31
+ import { fileAnimationOrder } from '../../compile.ts';
32
+ import { EVERY_GLOBAL_PHYSICS, PHYSICS_TIMELINE_KINDS, type CorePhysicsRecord } from '../../core/constraints_physics.ts';
33
+ import type { ConstraintTargetFacts, TargetAnimation, TargetEntry, TargetKeyArray } from '../facts/constraint_targets.ts';
34
+ import { isObj, type Json } from '../values.ts';
35
+ import { unnamedReach } from './constraints.ts';
36
+ import type { ReadDocument } from './parse.ts';
37
+
38
+ const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
39
+ const keyArray = (timeline: string, keys: unknown): TargetKeyArray => ({ timeline, keys: Array.isArray(keys) ? { count: keys.length } : { spelled: `${JSON.stringify(keys)}` } });
40
+
41
+ /** One group's entries as the file keys them: an object filled in the document's order, read back in its key order. */
42
+ function fileEntries(group: 'ik' | 'transform' | 'path' | 'physics' | 'slider', entries: readonly Json[]): TargetEntry[] {
43
+ const keyed: Record<string, TargetEntry> = {};
44
+ for (const entry of entries) {
45
+ if (typeof entry.name !== 'string') continue;
46
+ const name = group === 'physics' && entry.name === EVERY_GLOBAL_PHYSICS ? '' : entry.name;
47
+ keyed[name] =
48
+ group === 'ik' || group === 'transform'
49
+ ? { name, bare: null, keyArrays: [keyArray('', entry.keys)] }
50
+ : { name, bare: null, keyArrays: list(entry.timelines).flatMap((t) => (typeof t.name === 'string' ? [keyArray(t.name, t.keys)] : [])) };
51
+ }
52
+ return Object.values(keyed);
53
+ }
54
+
55
+ export function modelConstraintTargets(read: ReadDocument): ConstraintTargetFacts {
56
+ const { doc } = read;
57
+ const byName = new Map<string, Json>();
58
+ for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
59
+ const animations: TargetAnimation[] = [];
60
+ for (const name of fileAnimationOrder(doc)) {
61
+ const anim = byName.get(name);
62
+ if (anim === undefined) continue;
63
+ const groups: TargetAnimation['groups'][number][] = [];
64
+ const constraints = isObj(anim.constraints) ? anim.constraints : {};
65
+ for (const group of ['ik', 'transform', 'path', 'physics', 'slider'] as const) {
66
+ const entries = list(constraints[group]);
67
+ if (entries.length > 0) groups.push({ group, entries: fileEntries(group, entries) });
68
+ }
69
+ animations.push({ name, groups });
70
+ }
71
+ const records = doc.constraints.flatMap((c): CorePhysicsRecord[] => (c.record?.kind === 'physics' ? [c.record] : []));
72
+ return {
73
+ constraints: doc.constraints.map((c) => ({ name: c.name, type: c.kind, spelled: c.name })),
74
+ groupsByAnimation: animations,
75
+ reach: (timeline) => {
76
+ const kind = PHYSICS_TIMELINE_KINDS.find((k) => k === timeline);
77
+ if (kind === undefined) return null;
78
+ const reached = unnamedReach(records, kind);
79
+ return { resets: kind === 'reset', reached: records.filter((r) => reached.has(r.name)).map((r) => r.name) };
80
+ },
81
+ };
82
+ }
@@ -12,7 +12,8 @@
12
12
  * `./linked_meshes.ts`, `./constraints.ts`; cut 4c-4's
13
13
  * `./skeleton_roster.ts`, `./bone_timelines.ts`, `./event_keys.ts`; cut
14
14
  * 4c-3's `./deform_survey.ts`, `./animation_durations.ts`, `./two_colour.ts`,
15
- * `./sequences.ts`)
15
+ * `./sequences.ts`; cut 4c-5's `./slider_composition.ts`,
16
+ * `./constraint_targets.ts`; cut 4c-5a's `./stepped_poses.ts`)
16
17
  * and calls the same body inside the same harness (`../harness.ts`). The
17
18
  * selftest holds the two to the same lines on every call it makes with a
18
19
  * model in hand, and `tools/verdict_gate.ts` on every recipe.
@@ -116,6 +117,15 @@ import type { DeformSurveyFacts } from '../facts/deform_survey.ts';
116
117
  import type { AnimationDurationFacts } from '../facts/animation_durations.ts';
117
118
  import type { TwoColourFacts } from '../facts/two_colour.ts';
118
119
  import type { SequenceFacts } from '../facts/sequences.ts';
120
+ import { a40SlidersComposeOnASharedTarget } from '../bodies/a40.ts';
121
+ import { modelSliderComposition } from './slider_composition.ts';
122
+ import type { SliderCompositionFacts } from '../facts/slider_composition.ts';
123
+ import { a34ConstraintTimelineTargets } from '../bodies/a34.ts';
124
+ import { modelConstraintTargets } from './constraint_targets.ts';
125
+ import type { ConstraintTargetFacts } from '../facts/constraint_targets.ts';
126
+ import { a10NoNanAfterStepping } from '../bodies/a10.ts';
127
+ import { modelSteppedPoses } from './stepped_poses.ts';
128
+ import type { SteppedPoseFacts } from '../facts/stepped_poses.ts';
119
129
 
120
130
  /** What the model side is given. */
121
131
  export interface ModelValidateInput {
@@ -171,6 +181,9 @@ export interface ModelSupply {
171
181
  animationDurations: (read: ReadDocument) => AnimationDurationFacts;
172
182
  twoColour: (read: ReadDocument) => TwoColourFacts;
173
183
  sequences: (read: ReadDocument) => SequenceFacts;
184
+ sliderComposition: (read: ReadDocument) => SliderCompositionFacts;
185
+ constraintTargets: (read: ReadDocument) => ConstraintTargetFacts;
186
+ steppedPoses: (read: ReadDocument) => SteppedPoseFacts;
174
187
  }
175
188
 
176
189
  /** The suppliers the model side runs on. */
@@ -195,6 +208,9 @@ export const MODEL_SUPPLY: ModelSupply = {
195
208
  animationDurations: modelAnimationDurations,
196
209
  twoColour: modelTwoColour,
197
210
  sequences: modelSequences,
211
+ sliderComposition: modelSliderComposition,
212
+ constraintTargets: modelConstraintTargets,
213
+ steppedPoses: modelSteppedPoses,
198
214
  };
199
215
 
200
216
  /**
@@ -216,7 +232,7 @@ export interface MovedAssertion {
216
232
  *
217
233
  * Since cut 4c-4 also every rule `validate()` runs over the skeleton JSON
218
234
  * whatever its round trip did (A12, A24, A25, A26, A29, A30, and A32, which
219
- * runs before it): on the runtime's side their verdict does not wait on the
235
+ * runs before it; since cut 4c-5 A34): on the runtime's side their verdict does not wait on the
220
236
  * load, so on this side it does not wait on the region rule, and where both
221
237
  * parses refuse a build the two sides' lines of these rules are still
222
238
  * compared.
@@ -269,6 +285,9 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
269
285
  { code: 'A09_ANIMATION_DURATION_MATCHES_SPEC', run: (v, read, input, supply) => a09AnimationDurationMatchesSpec(v, supply.animationDurations(read), input.declaredDurations), unread: SKIP_NO_MODEL },
270
286
  { code: 'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN', run: (v, read, _input, supply) => a43TwoColorTintLoadsAndPosesAsWritten(v, supply.twoColour(read)), unread: SKIP_NO_MODEL },
271
287
  { code: 'A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES', run: (v, read, _input, supply) => a46SequenceAttachmentsShowTheFrameTheFileStates(v, supply.sequences(read)), unread: SKIP_NO_MODEL },
288
+ { code: 'A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET', run: (v, read, _input, supply) => a40SlidersComposeOnASharedTarget(v, supply.sliderComposition(read), supply.constraints(read)), unread: SKIP_NO_MODEL },
289
+ { code: 'A34_CONSTRAINT_TIMELINE_TARGETS', run: (v, read, _input, supply) => a34ConstraintTimelineTargets(v, supply.constraintTargets(read)), unread: SKIP_NO_MODEL, beforeTheParse: true },
290
+ { code: 'A10_NO_NAN_AFTER_STEPPING', run: (v, read, _input, supply) => a10NoNanAfterStepping(v, supply.boneTimelines(read), supply.steppedPoses(read)), unread: SKIP_NO_MODEL },
272
291
  ];
273
292
 
274
293
  /** The codes the model side prints: its two parse rules, then the moved assertions. */
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The model side's supply of `SliderCompositionFacts` (issue #1025, cut 4c-5
3
+ * of step 4c of #380): the document's sliders in its `constraints` order, the
4
+ * timelines of each animation they apply in the order the runtime builds
5
+ * them, and what each does when applied additively, as the core computes it.
6
+ *
7
+ * - **A slider's fields** are the core's record (`readSliderRecord`): `mix`,
8
+ * `additive` and `skin` read at the parser's value where the document
9
+ * leaves them out. The skins naming it are the document's skins in the
10
+ * file's order (`fileSkinOrder`), each whose `slider` list names it.
11
+ * - **The timelines, in the runtime's order** (`../facts/slider_composition.ts`'
12
+ * ⭐): the slot and bone timelines as the file states them (`fileSlotTimelines`,
13
+ * `fileBoneTimelines` — the emitter's own passes over the document's
14
+ * order), the constraint timelines in the order the runtime builds them
15
+ * (`modelConstraintTimelineSources`), the attachments' deform and sequence
16
+ * timelines at their three levels in the order the file keys them
17
+ * (`keyedOrder` over the document's order, deform before sequence —
18
+ * `emitAttachmentTimelines`' order), then the draw order and the events.
19
+ * - **The words.** Each timeline's class name and properties are the core's
20
+ * table (`ADDITIVE_APPLY`, `src/core/additive.ts`), the one value here that
21
+ * is not the document's, measured off the loaded timelines; an id's index is
22
+ * the target's position in the document's bones, slots or constraints —
23
+ * which the emitter writes in that order — `-1` for a physics timeline
24
+ * naming none; a deform's attachment is named as the runtime names it, the
25
+ * record's `name`, else its placeholder.
26
+ * - **The behaviour** is the core's probe over the document under no skin set
27
+ * (`noSkinView`, which refuses by name a document whose skins it was not
28
+ * measured over) and under each skin (`underSkin`): `additiveCells`, and the
29
+ * class `additiveBehaviour` reads off them. The core suite's `CO27` holds
30
+ * both to the runtime's probe; the selftest asks this supplier every
31
+ * question the body asks the runtime's (`VF14`).
32
+ *
33
+ * Links nothing from the runtime.
34
+ */
35
+ import { fileSkinOrder, keyedOrder } from '../../compile.ts';
36
+ import { ADDITIVE_APPLY, additiveBehaviour, additiveCells, additiveSpelling, type AdditiveTimeline, type AdditiveView } from '../../core/additive.ts';
37
+ import { EVERY_GLOBAL_PHYSICS, type PhysicsTimelineKind } from '../../core/constraints_physics.ts';
38
+ import { underSkin, type CompiledDocument } from '../../core/index.ts';
39
+ import type { BoneTimelineKind, SlotTimelineKind } from '../../core/animation.ts';
40
+ import { noSkinView } from '../../render_core.ts';
41
+ import { entryAddress } from '../facts/sequences.ts';
42
+ import type { SliderCompositionFacts, SliderFact, SliderTimelineFact } from '../facts/slider_composition.ts';
43
+ import { modelConstraintTimelineSources } from './constraints.ts';
44
+ import { fileBoneTimelines } from './bone_timelines.ts';
45
+ import { fileSlotTimelines } from './slot_colour.ts';
46
+ import type { ReadDocument } from './parse.ts';
47
+ import { isObj } from '../values.ts';
48
+
49
+ /** One timeline of an animation: the facts' reading of it and the core's. */
50
+ export interface ModelSliderTimeline {
51
+ fact: SliderTimelineFact;
52
+ core: AdditiveTimeline;
53
+ }
54
+
55
+ /** Names in the order each first appears, put in the order a JSON object keyed by them lists them (`keyedOrder`). */
56
+ const keyedFirsts = (names: readonly string[]): string[] => keyedOrder(names.filter((name, i) => names.indexOf(name) === i));
57
+
58
+ /** The words of one timeline: the table's class and properties, each id addressed by `index` (and `attachment`), and the sentence's naming. */
59
+ function factOf(doc: CompiledDocument, core: AdditiveTimeline): SliderTimelineFact {
60
+ const row = ADDITIVE_APPLY[additiveSpelling(core)];
61
+ if (row === undefined) throw new Error(`internal: "${additiveSpelling(core)}" is no row of the additive table`);
62
+ const property = (p: string, index: number | null, attachment: string | null, names: string) => ({
63
+ id: [p, ...(index === null ? [] : [String(index)]), ...(attachment === null ? [] : [attachment])].join('|'),
64
+ property: p,
65
+ names,
66
+ });
67
+ const slotIndex = (name: string): number => doc.slots.findIndex((s) => s.name === name);
68
+ let properties: SliderTimelineFact['properties'];
69
+ switch (core.group) {
70
+ case 'bone': {
71
+ const index = doc.bones.findIndex((b) => b.name === core.bone);
72
+ properties = row.properties.map((p) => property(p, index, null, `bone "${core.bone}" ${p}`));
73
+ break;
74
+ }
75
+ case 'slot':
76
+ properties = row.properties.map((p) => property(p, slotIndex(core.slot), null, `slot "${core.slot}" ${p}`));
77
+ break;
78
+ case 'attachment': {
79
+ const record = doc.skins.find((k) => k.name === core.skin)?.attachments[core.slot]?.[core.attachment];
80
+ const shown = record?.name ?? core.attachment;
81
+ const address = entryAddress(core.skin, core.slot, core.attachment);
82
+ properties = row.properties.map((p) => property(p, slotIndex(core.slot), address, core.word === 'deform' ? `slot "${core.slot}" deform of "${shown}"` : `slot "${core.slot}" ${p}`));
83
+ break;
84
+ }
85
+ case 'drawOrder':
86
+ case 'events':
87
+ properties = row.properties.map((p) => property(p, null, null, `the skeleton's ${p}`));
88
+ break;
89
+ default: {
90
+ const index = core.group === 'physics' && core.constraint === EVERY_GLOBAL_PHYSICS ? -1 : doc.constraints.findIndex((c) => c.kind === core.group && c.name === core.constraint);
91
+ properties = row.properties.map((p) => property(p, row.address === 'none' ? null : index, null, index >= 0 ? `constraint "${core.constraint}" ${p}` : `the skeleton's ${p}`));
92
+ }
93
+ }
94
+ return { runtimeClass: row.runtimeClass, properties };
95
+ }
96
+
97
+ /** Every timeline of animation `name`, in the order the runtime builds them (the header). */
98
+ export function modelAnimationTimelines(read: ReadDocument, name: string): ModelSliderTimeline[] {
99
+ const { doc } = read;
100
+ const anim = doc.animations.find((a) => a.name === name);
101
+ if (anim === undefined) return [];
102
+ const t = anim.timelines;
103
+ const out: AdditiveTimeline[] = [];
104
+ for (const { slot, timelines } of fileSlotTimelines(read).filter((s) => s.animation === name)) {
105
+ const target = t.slots.find((s) => s.name === slot);
106
+ for (const word of Object.keys(timelines)) {
107
+ const keys = target?.timelines.find((x) => x.kind === word)?.keys;
108
+ if (keys !== undefined) out.push({ group: 'slot', word: word as SlotTimelineKind, slot, keys });
109
+ }
110
+ }
111
+ for (const { bone, timelines } of fileBoneTimelines(read).filter((b) => b.animation === name)) {
112
+ const target = t.bones.find((b) => b.name === bone);
113
+ for (const word of isObj(timelines) ? Object.keys(timelines) : []) {
114
+ const keys = target?.timelines.find((x) => x.kind === word)?.keys;
115
+ if (keys !== undefined) out.push({ group: 'bone', word: word as BoneTimelineKind, bone, keys });
116
+ }
117
+ }
118
+ for (const source of modelConstraintTimelineSources(read).filter((s) => s.animation === name)) {
119
+ switch (source.kind) {
120
+ case 'ik': {
121
+ const keys = anim.constraints.ik.find((x) => x.name === source.name)?.keys;
122
+ if (keys !== undefined) out.push({ group: 'ik', word: 'ik', constraint: source.name, keys });
123
+ break;
124
+ }
125
+ case 'transform':
126
+ out.push({ group: 'transform', word: 'transform', constraint: source.name, keys: source.keys });
127
+ break;
128
+ case 'path':
129
+ out.push({ group: 'path', word: source.word as 'position' | 'spacing' | 'mix', constraint: source.name, keys: source.keys });
130
+ break;
131
+ case 'physics':
132
+ out.push({ group: 'physics', word: source.word as PhysicsTimelineKind, constraint: source.name, keys: source.keys });
133
+ break;
134
+ case 'slider':
135
+ out.push({ group: 'slider', word: source.word as 'time' | 'mix', constraint: source.name, keys: source.keys });
136
+ break;
137
+ }
138
+ }
139
+ const attachments = t.attachments;
140
+ for (const skin of keyedFirsts(attachments.map((a) => a.skin))) {
141
+ const inSkin = attachments.filter((a) => a.skin === skin);
142
+ for (const slot of keyedFirsts(inSkin.map((a) => a.slot))) {
143
+ const inSlot = inSkin.filter((a) => a.slot === slot);
144
+ for (const attachment of keyedFirsts(inSlot.map((a) => a.attachment))) {
145
+ for (const a of inSlot.filter((x) => x.attachment === attachment)) {
146
+ if (a.deform !== null) out.push({ group: 'attachment', word: 'deform', skin, slot, attachment, keys: a.deform });
147
+ if (a.sequence !== null) out.push({ group: 'attachment', word: 'sequence', skin, slot, attachment, keys: a.sequence });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ if (t.drawOrder.length > 0) out.push({ group: 'drawOrder', word: 'drawOrder', keys: t.drawOrder });
153
+ if (t.events.length > 0) out.push({ group: 'events', word: 'events', times: t.events.map((k) => k.time) });
154
+ return out.map((core) => ({ fact: factOf(doc, core), core }));
155
+ }
156
+
157
+ /** The views the probe poses under: no skin set, then every skin in the file's order. */
158
+ export function additiveViews(doc: CompiledDocument): AdditiveView[] {
159
+ return [{ name: '(none)', doc: noSkinView(doc) }, ...fileSkinOrder(doc).map((skin) => ({ name: skin.name, doc: underSkin(doc, skin.name) }))];
160
+ }
161
+
162
+ export function modelSliderComposition(read: ReadDocument): SliderCompositionFacts {
163
+ const { doc } = read;
164
+ const skinOrder = fileSkinOrder(doc).map((skin) => skin.name);
165
+ const walked = new Map<string, ModelSliderTimeline[]>();
166
+ const timelinesOf = (name: string): ModelSliderTimeline[] => {
167
+ const held = walked.get(name);
168
+ if (held !== undefined) return held;
169
+ const fresh = modelAnimationTimelines(read, name);
170
+ walked.set(name, fresh);
171
+ return fresh;
172
+ };
173
+ const sliders: SliderFact[] = [];
174
+ doc.constraints.forEach((c, index) => {
175
+ const r = c.record;
176
+ if (r?.kind !== 'slider') return;
177
+ const animation = doc.animations.some((a) => a.name === r.animation) ? { name: r.animation, timelines: timelinesOf(r.animation).map((x) => x.fact) } : null;
178
+ const skins = skinOrder.filter((name) => doc.skins.some((k) => k.name === name && k.constraints.slider.includes(r.name)));
179
+ sliders.push({ name: r.name, index, mix: r.mix, additive: r.additive, skinRequired: r.skin, skins, animation });
180
+ });
181
+ let views: AdditiveView[] | null = null;
182
+ return {
183
+ sliders,
184
+ behaviour: (animation, at) => {
185
+ const timeline = timelinesOf(animation)[at];
186
+ if (timeline === undefined) throw new Error(`internal: animation "${animation}" has no timeline ${at}`);
187
+ views ??= additiveViews(doc);
188
+ return additiveBehaviour(additiveCells(views, timeline.core));
189
+ },
190
+ };
191
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The model side's supply of `SteppedPoseFacts` (issue #1025, cut 4c-5a of
3
+ * step 4c of #380): every pose A10 reads, posed by rigc's core.
4
+ *
5
+ * - **The roster**: the document's bones, and its animations in the file's
6
+ * order (`fileAnimationOrder`, `src/compile.ts`), each with
7
+ * `CoreAnimationTimelines.duration` — the last key of every timeline as
8
+ * float32, the value `tools/pose_oracle.ts compare` holds equal to
9
+ * spine-core's on its `animations` roster.
10
+ * - **The setup pose and each animation's walk**: the core's looping walk
11
+ * (`src/core/walk.ts` — `poseWalkSetup`, `poseLoopingWalk`), which reads
12
+ * A10's recipe as measured and returns a value that is not finite in place
13
+ * rather than refusing it. It is held equal to spine-core's walk at
14
+ * tolerance 0 by `tools/core_gate.ts`'s walk block and the selftest's
15
+ * `CO25`, and the poses A10 asks for here are compared with spine-core's on
16
+ * every call with a model in hand (`VF13`).
17
+ * - **A bone's mode**: the mode the core's bone timelines pose at the time
18
+ * (`posedBones`, a fresh non-looping track: the time held at the duration),
19
+ * folded as the runtime's lookup folds it (`foldInheritMode`). `readModel`
20
+ * refuses a setup `inheritMode` and an `inherit` key that fold to no mode by
21
+ * name, so on this side every bone of every pose holds one and the answer is
22
+ * `null` — derived from the pose rather than assumed, so a reader that
23
+ * started admitting such a spelling would show up here as a bone with none.
24
+ * - **Under no skin set**, as `validate()` poses (`noSkinView`), which refuses
25
+ * by name a document whose default skin it cannot read that way.
26
+ *
27
+ * Links nothing from the runtime.
28
+ */
29
+ import { fileAnimationOrder } from '../../compile.ts';
30
+ import { foldInheritMode, type CompiledDocument } from '../../core/index.ts';
31
+ import { posedBones } from '../../core/animation.ts';
32
+ import { poseLoopingWalk, poseWalkSetup, type WalkPose } from '../../core/walk.ts';
33
+ import { noSkinView } from '../../render_core.ts';
34
+ import type { ModelBone } from '../../model.ts';
35
+ import type { SteppedFrame, SteppedPoseFacts } from '../facts/stepped_poses.ts';
36
+ import type { ReadDocument } from './parse.ts';
37
+
38
+ /** A bone's posed mode as the facts spell it: `null` for a mode, else the value held. */
39
+ const modeOf = (bone: ModelBone): string | null => (foldInheritMode(bone.inheritMode ?? 'normal') === null ? String(bone.inheritMode) : null);
40
+
41
+ /** A channel the walk computed; a slot row holds `null` only where a rounding wrote one, which the walk does not. */
42
+ const channel = (v: number | null): number => (v === null ? Number.NaN : v);
43
+
44
+ /** One walk pose as A10 reads it, each bone's mode taken from `posed` (the bones the timelines posed at the pose's time). */
45
+ function frameOf(pose: WalkPose, posed: readonly ModelBone[]): SteppedFrame {
46
+ const modes = new Map(posed.map((b) => [b.name, modeOf(b)]));
47
+ return {
48
+ bones: pose.bones.map((b) => ({ name: b.name, a: b.a, b: b.b, c: b.c, d: b.d, worldX: b.worldX, worldY: b.worldY, inherit: modes.get(b.name) ?? null })),
49
+ drawn: pose.drawn.map((d) => ({ slot: d.slot, attachment: d.attachment, vertices: d.vertices })),
50
+ slots: pose.slots.map((row) => ({
51
+ name: row[0],
52
+ colour: [channel(row[2]), channel(row[3]), channel(row[4]), channel(row[5])] as const,
53
+ dark: row[6] === null ? null : ([channel(row[6][0]), channel(row[6][1]), channel(row[6][2])] as const),
54
+ })),
55
+ };
56
+ }
57
+
58
+ export function modelSteppedPoses(read: ReadDocument): SteppedPoseFacts {
59
+ let view: CompiledDocument | null = null;
60
+ const viewOf = (): CompiledDocument => (view ??= noSkinView(read.doc));
61
+ const animationOf = (name: string) => {
62
+ const anim = viewOf().animations.find((a) => a.name === name);
63
+ if (anim === undefined) throw new Error(`internal: the model side was asked to pose animation "${name}", which the document does not hold`);
64
+ return anim;
65
+ };
66
+ return {
67
+ boneCount: read.doc.bones.length,
68
+ steppedAnimations: fileAnimationOrder(read.doc).flatMap((name) => {
69
+ const anim = read.doc.animations.find((a) => a.name === name);
70
+ return anim === undefined ? [] : [{ name, duration: anim.timelines.duration }];
71
+ }),
72
+ hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
73
+ hasBone: (name) => read.doc.bones.some((b) => b.name === name),
74
+ posedInherit: (animation, bone, time) => {
75
+ const anim = animationOf(animation);
76
+ const t = time < anim.timelines.duration ? time : anim.timelines.duration;
77
+ const posed = posedBones(viewOf(), anim.timelines, t).find((b) => b.name === bone);
78
+ return posed === undefined ? undefined : modeOf(posed);
79
+ },
80
+ // The document's stated mode — asked only of a bone posing none, which the reader leaves this side no document to hold.
81
+ statedInherit: (name) => `${JSON.stringify(read.doc.bones.find((b) => b.name === name)?.inheritMode)}`,
82
+ setup: () => frameOf(poseWalkSetup(viewOf()), viewOf().bones),
83
+ walk: (animation, step, frames) => {
84
+ const anim = animationOf(animation);
85
+ const poses = poseLoopingWalk(viewOf(), animation, new Array<number>(frames).fill(step));
86
+ return poses.slice(1).map((pose) => frameOf(pose, posedBones(viewOf(), anim.timelines, pose.animationTime)));
87
+ },
88
+ };
89
+ }
package/src/bonedist.ts CHANGED
@@ -77,7 +77,7 @@
77
77
  */
78
78
  import { readFileSync } from 'node:fs';
79
79
  import {
80
- loadPosable,
80
+ loadPosedSkeleton,
81
81
  PROTOCOL_FPS,
82
82
  sampleAnimation,
83
83
  sampleSetupPose,
@@ -184,7 +184,7 @@ export interface SkeletonSize {
184
184
  degenerate: boolean;
185
185
  }
186
186
 
187
- function skeletonSize(posable: Posable): SkeletonSize {
187
+ function skeletonSize(posable: Pick<Posable, 'data'>): SkeletonSize {
188
188
  const setup = sampleSetupPose(posable.data, { bones: true }).at(0);
189
189
  const bones = setup?.bones ?? [];
190
190
  const rootData = posable.data.bones.find((b) => b.parent === null) ?? posable.data.bones.at(0);
@@ -338,10 +338,17 @@ export interface BoneDistReport {
338
338
  export interface BoneDistOptions {
339
339
  candidateSkeleton: string;
340
340
  candidateAtlas: string;
341
- candidateAtlasDir: string;
341
+ /**
342
+ * Where the candidate's atlas pages are. Not read since issue #1042: no
343
+ * figure here samples a pixel, so no page is opened, and a pair whose pages
344
+ * are elsewhere measures the same. Kept optional so a caller that passes it
345
+ * still compiles.
346
+ */
347
+ candidateAtlasDir?: string;
342
348
  referenceSkeleton: string;
343
349
  referenceAtlas: string;
344
- referenceAtlasDir: string;
350
+ /** The reference's, as `candidateAtlasDir` — not read. */
351
+ referenceAtlasDir?: string;
345
352
  /** A correspondence file path, or `identity`. */
346
353
  bones: string;
347
354
  fps?: number;
@@ -398,8 +405,13 @@ function extremesOf(values: number[]): Extremes {
398
405
  export function boneDistance(options: BoneDistOptions): BoneDistReport {
399
406
  const fps = options.fps ?? PROTOCOL_FPS;
400
407
  if (!Number.isFinite(fps) || fps <= 0) throw new BoneDistError('fps must be a positive number');
401
- const candidate = loadPosable(options.candidateSkeleton, options.candidateAtlas, options.candidateAtlasDir);
402
- const reference = loadPosable(options.referenceSkeleton, options.referenceAtlas, options.referenceAtlasDir);
408
+ // Each side through spine-core, refused by name where its skeleton does not load against its atlas (issue #1042),
409
+ // and without its page images: nothing here samples a pixel (`loadPosedSkeleton`).
410
+ const posedFor = (side: 'candidate' | 'reference', skeleton: string, atlas: string): Pick<Posable, 'data'> => ({
411
+ data: loadPosedSkeleton(skeleton, atlas, `bonedist's ${side}: every bone's world transform is read off spine-core's pose of it`),
412
+ });
413
+ const candidate = posedFor('candidate', options.candidateSkeleton, options.candidateAtlas);
414
+ const reference = posedFor('reference', options.referenceSkeleton, options.referenceAtlas);
403
415
 
404
416
  const candidateBoneNames = candidate.data.bones.map((b) => b.name);
405
417
  const referenceBoneNames = new Set(reference.data.bones.map((b) => b.name));