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.
Files changed (62) hide show
  1. package/cli.ts +48 -17
  2. package/docs/AUTHORING.md +2 -2
  3. package/package.json +1 -1
  4. package/src/assertions/bodies/a09.ts +82 -0
  5. package/src/assertions/bodies/a10.ts +116 -0
  6. package/src/assertions/bodies/a12.ts +30 -0
  7. package/src/assertions/bodies/a24.ts +68 -0
  8. package/src/assertions/bodies/a25.ts +39 -0
  9. package/src/assertions/bodies/a26.ts +61 -0
  10. package/src/assertions/bodies/a29.ts +34 -0
  11. package/src/assertions/bodies/a30.ts +50 -0
  12. package/src/assertions/bodies/a32.ts +44 -0
  13. package/src/assertions/bodies/a34.ts +133 -0
  14. package/src/assertions/bodies/a39.ts +303 -0
  15. package/src/assertions/bodies/a40.ts +128 -0
  16. package/src/assertions/bodies/a43.ts +181 -0
  17. package/src/assertions/bodies/a46.ts +224 -0
  18. package/src/assertions/facts/animation_durations.ts +37 -0
  19. package/src/assertions/facts/bone_timelines.ts +37 -0
  20. package/src/assertions/facts/constraint_targets.ts +56 -0
  21. package/src/assertions/facts/deform_survey.ts +27 -0
  22. package/src/assertions/facts/event_keys.ts +55 -0
  23. package/src/assertions/facts/sequences.ts +85 -0
  24. package/src/assertions/facts/skeleton_roster.ts +45 -0
  25. package/src/assertions/facts/slider_composition.ts +78 -0
  26. package/src/assertions/facts/stepped_poses.ts +74 -0
  27. package/src/assertions/facts/two_colour.ts +52 -0
  28. package/src/assertions/inward_advance.ts +58 -0
  29. package/src/assertions/model/animation_durations.ts +57 -0
  30. package/src/assertions/model/bone_timelines.ts +56 -0
  31. package/src/assertions/model/constraint_targets.ts +82 -0
  32. package/src/assertions/model/constraints.ts +34 -16
  33. package/src/assertions/model/deform_survey.ts +24 -0
  34. package/src/assertions/model/event_keys.ts +45 -0
  35. package/src/assertions/model/index.ts +83 -2
  36. package/src/assertions/model/linked_meshes.ts +2 -2
  37. package/src/assertions/model/mesh_attachments.ts +1 -1
  38. package/src/assertions/model/region_joins.ts +20 -12
  39. package/src/assertions/model/sequences.ts +157 -0
  40. package/src/assertions/model/skeleton_roster.ts +23 -0
  41. package/src/assertions/model/skin_entries.ts +18 -8
  42. package/src/assertions/model/skin_members.ts +5 -3
  43. package/src/assertions/model/skin_meshes.ts +1 -1
  44. package/src/assertions/model/slider_composition.ts +191 -0
  45. package/src/assertions/model/slot_colour.ts +5 -4
  46. package/src/assertions/model/stepped_poses.ts +89 -0
  47. package/src/assertions/model/two_colour.ts +61 -0
  48. package/src/assertions/model/vertex_polygons.ts +9 -8
  49. package/src/bonedist.ts +18 -6
  50. package/src/compile.ts +114 -14
  51. package/src/core/additive.ts +468 -0
  52. package/src/core/constraints.ts +12 -2
  53. package/src/core/constraints_physics.ts +31 -8
  54. package/src/core/raw.ts +77 -16
  55. package/src/core/walk.ts +202 -0
  56. package/src/deformmeasure.ts +35 -2270
  57. package/src/deformstructure.ts +60 -18
  58. package/src/deformsurvey.ts +2295 -0
  59. package/src/nonfinite.ts +54 -0
  60. package/src/render.ts +200 -59
  61. package/src/render_core.ts +5 -0
  62. 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
+ }