spine-rigc 1.9.1 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +39 -17
- package/docs/AUTHORING.md +1 -1
- package/package.json +1 -1
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/index.ts +21 -2
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/bonedist.ts +18 -6
- package/src/core/additive.ts +468 -0
- package/src/core/constraints.ts +12 -2
- package/src/core/constraints_physics.ts +31 -8
- package/src/core/raw.ts +77 -16
- package/src/core/walk.ts +202 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +151 -86
- package/src/render_core.ts +5 -0
- package/src/validate.ts +432 -457
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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));
|