spine-rigc 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +48 -17
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/bone_timelines.ts +56 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +34 -16
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/index.ts +83 -2
- package/src/assertions/model/linked_meshes.ts +2 -2
- package/src/assertions/model/mesh_attachments.ts +1 -1
- package/src/assertions/model/region_joins.ts +20 -12
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +18 -8
- package/src/assertions/model/skin_members.ts +5 -3
- package/src/assertions/model/skin_meshes.ts +1 -1
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/slot_colour.ts +5 -4
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +9 -8
- package/src/bonedist.ts +18 -6
- package/src/compile.ts +114 -14
- package/src/core/additive.ts +468 -0
- package/src/core/constraints.ts +12 -2
- package/src/core/constraints_physics.ts +31 -8
- package/src/core/raw.ts +77 -16
- package/src/core/walk.ts +202 -0
- package/src/deformmeasure.ts +35 -2270
- package/src/deformstructure.ts +60 -18
- package/src/deformsurvey.ts +2295 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +200 -59
- package/src/render_core.ts +5 -0
- package/src/validate.ts +791 -1558
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numbered series and the frame a slot shows (issue #1025, cut 4c-3 of step
|
|
3
|
+
* 4c of #380) — the census's F02, F03, F18 and R4 as A46 reads them: every
|
|
4
|
+
* skin entry that draws a region, as the file states it and whether the
|
|
5
|
+
* runtime loaded it; every sequence timeline, as the file states it; an
|
|
6
|
+
* animation's duration; and what a slot shows at a time.
|
|
7
|
+
*
|
|
8
|
+
* ⭐ **Attachments by address, never by object.** A46 used to key what it knew
|
|
9
|
+
* by the loaded attachment object and to ask whether the slot shows "the keyed
|
|
10
|
+
* attachment or one playing its timelines" by identity. An object is the
|
|
11
|
+
* runtime's; an address — `skin`, `slot`, `placeholder`, the triple the file
|
|
12
|
+
* files an entry under and a timeline names it by — is the file's, and on the
|
|
13
|
+
* runtime's side each loaded entry is exactly one object (one per skin, slot
|
|
14
|
+
* and placeholder, which `Skin.getAttachment` is keyed by). So `posedFrame`
|
|
15
|
+
* answers with addresses: the entry the slot shows, and the entry whose
|
|
16
|
+
* timelines it plays (itself, or a linked mesh's source when the link plays
|
|
17
|
+
* its source's timelines).
|
|
18
|
+
*
|
|
19
|
+
* ⭐ **The file's order and spelling where A46 walks and prints**: the skins in
|
|
20
|
+
* the file's order, a skin's slot keys and a slot's placeholders in it; every
|
|
21
|
+
* `sequence` block and key as the file spells it, since A46 parses them itself
|
|
22
|
+
* and prints what it found.
|
|
23
|
+
*
|
|
24
|
+
* Links nothing from the runtime: `validate()` reads it off the skeleton JSON
|
|
25
|
+
* and the loaded skeleton (`spineSequenceFacts`), the model side off the
|
|
26
|
+
* document and the core (`../model/sequences.ts`).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** A skin entry's address: `skin`, `slot`, `placeholder`, joined by NULs. */
|
|
30
|
+
export type EntryAddress = string;
|
|
31
|
+
|
|
32
|
+
/** The address of one skin entry. */
|
|
33
|
+
export function entryAddress(skin: string, slot: string, placeholder: string): EntryAddress {
|
|
34
|
+
return `${skin}\u0000${slot}\u0000${placeholder}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One skin entry that draws a region — a region, a mesh or a linked mesh. */
|
|
38
|
+
export interface SequenceSkinEntry {
|
|
39
|
+
readonly skin: string;
|
|
40
|
+
readonly slot: string;
|
|
41
|
+
readonly placeholder: string;
|
|
42
|
+
/** The entry's `name`, `path` and `sequence` as the file spells them (`undefined` where it states none). */
|
|
43
|
+
readonly name: unknown;
|
|
44
|
+
readonly path: unknown;
|
|
45
|
+
readonly sequence: unknown;
|
|
46
|
+
/** Whether the runtime loaded an attachment for the entry: its slot exists and the skin holds it. */
|
|
47
|
+
readonly loaded: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One sequence timeline — an attachment's `sequence` that the file states as a list — the address it keys, and its keys as the file spells them. */
|
|
51
|
+
export interface SequenceTimeline {
|
|
52
|
+
readonly animation: string;
|
|
53
|
+
readonly skin: string;
|
|
54
|
+
readonly slot: string;
|
|
55
|
+
readonly placeholder: string;
|
|
56
|
+
readonly keys: readonly unknown[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What a slot shows at one pose. */
|
|
60
|
+
export interface PosedSequence {
|
|
61
|
+
/** The entry the slot shows. */
|
|
62
|
+
readonly shown: EntryAddress;
|
|
63
|
+
/** The entry whose timelines it plays: itself, or the source of a linked mesh that plays its source's. */
|
|
64
|
+
readonly playsAs: EntryAddress;
|
|
65
|
+
/** The atlas region the shown entry's series resolves to at this pose, or `null` for none. */
|
|
66
|
+
readonly region: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** What A46 reads. */
|
|
70
|
+
export interface SequenceFacts {
|
|
71
|
+
/** Every skin entry of a region, mesh or linked mesh, in the file's order: skins, then each skin's slot keys, then a slot's placeholders. */
|
|
72
|
+
readonly entries: readonly SequenceSkinEntry[];
|
|
73
|
+
/** Every animation's sequence timelines, in the file's order: animations, then skins, slots and placeholders as the animation keys them. */
|
|
74
|
+
readonly timelines: readonly SequenceTimeline[];
|
|
75
|
+
/** Whether the skeleton has a slot of this name. */
|
|
76
|
+
hasSlot(slot: string): boolean;
|
|
77
|
+
/** The runtime's duration of an animation, or `null` for no animation of that name. */
|
|
78
|
+
duration(animation: string): number | null;
|
|
79
|
+
/**
|
|
80
|
+
* What the slot shows with the animation applied at `time` on a fresh,
|
|
81
|
+
* non-looping track from the setup pose, no skin set — `null` when it
|
|
82
|
+
* shows nothing.
|
|
83
|
+
*/
|
|
84
|
+
posedFrame(animation: string, slot: string, time: number): PosedSequence | null;
|
|
85
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skeleton's bones and slots as the file lists them (issue #1025, cut 4c-4
|
|
3
|
+
* of #380) — the census's F01 and F02, the halves of them that A12, A25 and
|
|
4
|
+
* A26 read: each bone's name and its parent's, and each slot's name and
|
|
5
|
+
* whether it declares a dark colour, in the file's order.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ **The order is a value.** The slots array IS the draw order, and A26
|
|
8
|
+
* holds it to the rig's table index for index; A12 prints one line per dark
|
|
9
|
+
* slot in that order. The emitter writes both arrays in the model's own order
|
|
10
|
+
* and never re-sorts them (`emitBones`, `emitSlots` in `src/emit_spine.ts`), so
|
|
11
|
+
* the document's lists are the file's.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **Read as the bodies always read the raw JSON.** A slot is any object in
|
|
14
|
+
* the array, its name `String(name)` (a slot with no string name reads
|
|
15
|
+
* `"undefined"`, as it always printed) and `dark` whether the key is present at
|
|
16
|
+
* all; a bone is an object whose name is a string, its parent the string the
|
|
17
|
+
* file states or `null`. `validate()` reads them off the skeleton JSON
|
|
18
|
+
* (`rawSkeletonRoster`), which it parses before the round trip, so a file the
|
|
19
|
+
* loader refuses is still read; the model side reads them off the document
|
|
20
|
+
* (`../model/skeleton_roster.ts`).
|
|
21
|
+
*
|
|
22
|
+
* Links nothing from the runtime.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A bone as the rules over parentage read it. */
|
|
26
|
+
export interface RosterBone {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
/** The parent's name, or `null` for a root (or a parent the file does not spell as a string). */
|
|
29
|
+
readonly parent: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A slot as the rules over the draw order and the dark colour read it. */
|
|
33
|
+
export interface RosterSlot {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/** Whether the slot declares a dark colour — the key's presence, which is all A12 asks. */
|
|
36
|
+
readonly dark: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What A12, A25 and A26 read of the skeleton's arrays. */
|
|
40
|
+
export interface SkeletonRosterFacts {
|
|
41
|
+
/** The bones, in the file's order. */
|
|
42
|
+
readonly bones: readonly RosterBone[];
|
|
43
|
+
/** The slots, in the file's order — the draw order. */
|
|
44
|
+
readonly slots: readonly RosterSlot[];
|
|
45
|
+
}
|
|
@@ -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,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two-colour tint (issue #1025, cut 4c-3 of step 4c of #380) — the
|
|
3
|
+
* census's F02, F18, F22 and R4 as A43 reads them: the dark colour each slot
|
|
4
|
+
* states and the one the runtime loaded for it, the slot timelines the file
|
|
5
|
+
* keys, and a slot's light and dark colour posed at a time.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ **The file's spelling where A43 prints it, the runtime's reading where it
|
|
8
|
+
* compares.** A43 parses the stated hex itself — a check that read the
|
|
9
|
+
* required value out of the parser it checks would agree with it whatever it
|
|
10
|
+
* did — so the stated `dark` and every key are handed over as the file spells
|
|
11
|
+
* them; the loaded and the posed colours are the values the runtime holds.
|
|
12
|
+
*
|
|
13
|
+
* The slot timelines are `SlotColourFacts`' (`./slot_colour.ts`): one walk of
|
|
14
|
+
* the file's animations, in its order and its spelling, which A45 reads too.
|
|
15
|
+
*
|
|
16
|
+
* Links nothing from the runtime: `validate()` reads it off the skeleton JSON
|
|
17
|
+
* and the loaded skeleton (`spineTwoColourFacts`), the model side off the
|
|
18
|
+
* document and the core (`../model/two_colour.ts`).
|
|
19
|
+
*/
|
|
20
|
+
import type { SlotTimelines } from './slot_colour.ts';
|
|
21
|
+
|
|
22
|
+
/** A dark colour as loaded or posed: three channels, NaN where the reading was no number. */
|
|
23
|
+
export interface DarkColour {
|
|
24
|
+
readonly r: number;
|
|
25
|
+
readonly g: number;
|
|
26
|
+
readonly b: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A slot's two colours, posed. */
|
|
30
|
+
export interface PosedTint {
|
|
31
|
+
readonly light: { readonly r: number; readonly g: number; readonly b: number; readonly a: number };
|
|
32
|
+
/** `null` where the posed slot holds no dark colour. */
|
|
33
|
+
readonly dark: DarkColour | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What A43 reads. */
|
|
37
|
+
export interface TwoColourFacts {
|
|
38
|
+
/** Every slot whose entry states `dark` as a string, in the file's slot order, name and value as the file spells them. */
|
|
39
|
+
readonly slotDarks: ReadonlyArray<{ readonly slot: string; readonly dark: string }>;
|
|
40
|
+
/** The setup dark colour the loaded skeleton holds for the slot, or `null` for none (or no such slot). */
|
|
41
|
+
loadedDark(slot: string): DarkColour | null;
|
|
42
|
+
/** Every (animation, slot) pair whose timelines the file keys, in the file's order and spelling. */
|
|
43
|
+
readonly slotTimelines: readonly SlotTimelines[];
|
|
44
|
+
/** Whether the skeleton holds an animation of this name. */
|
|
45
|
+
hasAnimation(name: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The slot's light and dark colour with the animation applied at `time` on
|
|
48
|
+
* a fresh, non-looping track from the setup pose, no skin set — or
|
|
49
|
+
* `undefined` for a slot the skeleton does not have.
|
|
50
|
+
*/
|
|
51
|
+
posedTint(animation: string, slot: string, time: number): PosedTint | undefined;
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deepest inward advance the animation data asks for, in axis pixels —
|
|
3
|
+
* shared by A29 and A30 (issue #1025, cut 4c-4 of #380), moved here from
|
|
4
|
+
* `src/validate.ts` unchanged but for what it reads: the bone timelines are a
|
|
5
|
+
* fact (`./facts/bone_timelines.ts`) rather than the raw JSON.
|
|
6
|
+
*
|
|
7
|
+
* Shared by A29 and A30 because they bound the SAME quantity against two
|
|
8
|
+
* different measured facts. Two things spend the same clearance and so are added:
|
|
9
|
+
*
|
|
10
|
+
* * the stroke — a translateX on a bone in the axis subtree. A24 guarantees
|
|
11
|
+
* there is no hidden screen-space component to miss.
|
|
12
|
+
* * the mass bone's own inward keys. It typically hangs outside the axis
|
|
13
|
+
* subtree, so its keys are screen-space by design and get PROJECTED onto the
|
|
14
|
+
* axis rather than read as axis coordinates. Ignoring them would let a rig
|
|
15
|
+
* pass while a recoil key closed the last few pixels.
|
|
16
|
+
*
|
|
17
|
+
* Links nothing from the runtime.
|
|
18
|
+
*/
|
|
19
|
+
import type { RigInfo } from '../types.ts';
|
|
20
|
+
import type { BoneTimelineFacts } from './facts/bone_timelines.ts';
|
|
21
|
+
import { isObj, type Json } from './values.ts';
|
|
22
|
+
|
|
23
|
+
export function deepestInwardAdvance({ boneTimelines }: BoneTimelineFacts, rig: RigInfo): { total: number; describe: () => string } {
|
|
24
|
+
const subtree = new Set(rig.axisSubtree);
|
|
25
|
+
let strokeMax = 0;
|
|
26
|
+
let strokeWhere = '';
|
|
27
|
+
let massMax = 0;
|
|
28
|
+
let massWhere = '';
|
|
29
|
+
for (const { animation: animName, bone: boneName, timelines } of boneTimelines) {
|
|
30
|
+
if (!isObj(timelines)) continue;
|
|
31
|
+
const keys = (timelines as Json).translate;
|
|
32
|
+
if (!Array.isArray(keys)) continue;
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
if (!isObj(key)) continue;
|
|
35
|
+
if (subtree.has(boneName)) {
|
|
36
|
+
// +x is inward along the axis; a retracted key is negative and spends
|
|
37
|
+
// no clearance, so only the inward extreme matters.
|
|
38
|
+
const x = Number(key.x ?? 0);
|
|
39
|
+
if (Number.isFinite(x) && x > strokeMax) {
|
|
40
|
+
strokeMax = x;
|
|
41
|
+
strokeWhere = `${animName}.${boneName} t=${String(key.time ?? 0)}`;
|
|
42
|
+
}
|
|
43
|
+
} else if (boneName === rig.massBone && rig.inwardUnit) {
|
|
44
|
+
const inward = Number(key.x ?? 0) * rig.inwardUnit[0] + Number(key.y ?? 0) * rig.inwardUnit[1];
|
|
45
|
+
if (Number.isFinite(inward) && inward > massMax) {
|
|
46
|
+
massMax = inward;
|
|
47
|
+
massWhere = `${animName}.${boneName} t=${String(key.time ?? 0)}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
total: strokeMax + massMax,
|
|
54
|
+
describe: () =>
|
|
55
|
+
`deepest inward advance is ${(strokeMax + massMax).toFixed(3)}px (stroke ${strokeMax.toFixed(3)} at ${strokeWhere}` +
|
|
56
|
+
`${massMax > 0 ? ` + mass ${massMax.toFixed(3)} at ${massWhere}` : ''})`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `AnimationDurationFacts` (issue #1025, cut 4c-3
|
|
3
|
+
* of step 4c of #380): the document's animations in the file's order, each
|
|
4
|
+
* with the runtime's duration and every timeline's own duration.
|
|
5
|
+
*
|
|
6
|
+
* Every number is the core's reading, through functions rigc already runs:
|
|
7
|
+
*
|
|
8
|
+
* - **The roster's order** is the file's, `fileAnimationOrder` (`src/compile.ts`,
|
|
9
|
+
* issue #1034): a `rigc-compiled/3` document's stated `editorOrder`, else the
|
|
10
|
+
* emitter's own rule over the document's names — integer-like names first,
|
|
11
|
+
* as the file keys them.
|
|
12
|
+
* - **An animation's duration** is `CoreAnimationTimelines.duration`, the last
|
|
13
|
+
* key time of every timeline as float32 — the value `tools/pose_oracle.ts
|
|
14
|
+
* compare` holds equal to spine-core's on its `animations` roster.
|
|
15
|
+
* - **A timeline's own duration** is its last key's time as the core read it
|
|
16
|
+
* (float32): each bone and slot timeline, each constraint timeline in the
|
|
17
|
+
* order the runtime builds them (`modelConstraintTimelineSources`), each
|
|
18
|
+
* attachment's deform and sequence timeline, the draw order and the events.
|
|
19
|
+
* How many timelines that makes is the count the runtime builds
|
|
20
|
+
* (`timelineCount`, which the selftest's `VF10` holds equal to the runtime's
|
|
21
|
+
* on every slider animation); a walk that lists another count is this
|
|
22
|
+
* module's defect and throws by name rather than handing A09 a list of the
|
|
23
|
+
* wrong length.
|
|
24
|
+
*
|
|
25
|
+
* Links nothing from the runtime.
|
|
26
|
+
*/
|
|
27
|
+
import { fileAnimationOrder } from '../../compile.ts';
|
|
28
|
+
import type { AnimationDuration, AnimationDurationFacts } from '../facts/animation_durations.ts';
|
|
29
|
+
import { modelConstraintTimelineSources, timelineCount } from './constraints.ts';
|
|
30
|
+
import type { ReadDocument } from './parse.ts';
|
|
31
|
+
|
|
32
|
+
/** The last of `times`, which the core read in key order. */
|
|
33
|
+
const lastOf = (times: ReadonlyArray<{ time: number }>): number[] => (times.length === 0 ? [] : [times[times.length - 1].time]);
|
|
34
|
+
|
|
35
|
+
export function modelAnimationDurations(read: ReadDocument): AnimationDurationFacts {
|
|
36
|
+
const constraintTimelines = modelConstraintTimelineSources(read);
|
|
37
|
+
const animations: AnimationDuration[] = [];
|
|
38
|
+
for (const name of fileAnimationOrder(read.doc)) {
|
|
39
|
+
const anim = read.doc.animations.find((a) => a.name === name);
|
|
40
|
+
if (anim === undefined) continue;
|
|
41
|
+
const t = anim.timelines;
|
|
42
|
+
const durations: number[] = [];
|
|
43
|
+
for (const target of [...t.bones, ...t.slots]) for (const tl of target.timelines) durations.push(...lastOf(tl.keys));
|
|
44
|
+
for (const source of constraintTimelines) if (source.animation === name) durations.push(...lastOf(source.keys));
|
|
45
|
+
for (const a of t.attachments) {
|
|
46
|
+
if (a.deform !== null) durations.push(...lastOf(a.deform));
|
|
47
|
+
if (a.sequence !== null) durations.push(...lastOf(a.sequence));
|
|
48
|
+
}
|
|
49
|
+
durations.push(...lastOf(t.drawOrder), ...lastOf(t.events));
|
|
50
|
+
const built = timelineCount(t);
|
|
51
|
+
if (durations.length !== built) {
|
|
52
|
+
throw new Error(`internal: animation "${name}": the durations walk lists ${durations.length} timeline(s) and the core reads ${built}`);
|
|
53
|
+
}
|
|
54
|
+
animations.push({ name, duration: t.duration, timelineDurations: durations });
|
|
55
|
+
}
|
|
56
|
+
return { animations };
|
|
57
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `BoneTimelineFacts` (issue #1025, cut 4c-4 of
|
|
3
|
+
* #380): every animation's bone timelines as the Spine file states them.
|
|
4
|
+
*
|
|
5
|
+
* The same reading A45's slot timelines are given (`fileSlotTimelines` in
|
|
6
|
+
* `./slot_colour.ts`), over the `bones` group: the animations put in the
|
|
7
|
+
* file's order by `editorAnimationOrder` (`src/compile.ts`, the function the
|
|
8
|
+
* emitter is handed), each animation's bones and their timelines laid out as
|
|
9
|
+
* the file lays them (`animations.<name>.bones.<bone>.<timeline> = keys`, in
|
|
10
|
+
* the document's order, which is the order `emitAnimation` writes the model's
|
|
11
|
+
* map in), and the whole handed to the emitter's own `withoutParserDefaults`
|
|
12
|
+
* and `inEditorKeyOrder` (`src/keyorder.ts`) — so a key field the emitter
|
|
13
|
+
* leaves out at the parser's default (`time: 0`, a translate key's `y: 0`) is
|
|
14
|
+
* left out here too, and a body printing a key's `time` prints what it prints
|
|
15
|
+
* over the file. Nothing here restates either rule.
|
|
16
|
+
*
|
|
17
|
+
* Links nothing from the runtime.
|
|
18
|
+
*/
|
|
19
|
+
import { editorAnimationOrder } from '../../compile.ts';
|
|
20
|
+
import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
|
|
21
|
+
import type { BoneTimelineFacts, BoneTimelines } from '../facts/bone_timelines.ts';
|
|
22
|
+
import { isObj, type Json } from '../values.ts';
|
|
23
|
+
import type { ReadDocument } from './parse.ts';
|
|
24
|
+
|
|
25
|
+
const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
|
|
26
|
+
|
|
27
|
+
/** Every animation's bone timelines as the Spine file states them: `animations.<name>.bones.<bone>.<timeline> = keys`. */
|
|
28
|
+
export function fileBoneTimelines(read: ReadDocument): BoneTimelines[] {
|
|
29
|
+
const byName = new Map<string, Json>();
|
|
30
|
+
for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
|
|
31
|
+
const animations: Json = {};
|
|
32
|
+
for (const name of editorAnimationOrder([...byName.keys()])) {
|
|
33
|
+
const bones: Json = {};
|
|
34
|
+
for (const bone of list((byName.get(name) as Json).bones)) {
|
|
35
|
+
if (typeof bone.name !== 'string') continue;
|
|
36
|
+
const timelines: Json = {};
|
|
37
|
+
for (const timeline of list(bone.timelines)) {
|
|
38
|
+
if (typeof timeline.name === 'string') timelines[timeline.name] = list(timeline.keys).map((key) => ({ ...key }));
|
|
39
|
+
}
|
|
40
|
+
bones[bone.name] = timelines;
|
|
41
|
+
}
|
|
42
|
+
// An animation keying no bone writes no `bones` group — `emitAnimation`'s `if (animation.bones.size)`.
|
|
43
|
+
animations[name] = Object.keys(bones).length > 0 ? { bones } : {};
|
|
44
|
+
}
|
|
45
|
+
const file = inEditorKeyOrder(withoutParserDefaults({ animations }));
|
|
46
|
+
const out: BoneTimelines[] = [];
|
|
47
|
+
for (const [animation, anim] of Object.entries(file.animations)) {
|
|
48
|
+
if (!isObj(anim) || !isObj(anim.bones)) continue;
|
|
49
|
+
for (const [bone, timelines] of Object.entries(anim.bones)) out.push({ animation, bone, timelines });
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function modelBoneTimelines(read: ReadDocument): BoneTimelineFacts {
|
|
55
|
+
return { boneTimelines: fileBoneTimelines(read) };
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|