spine-rigc 1.7.0 → 1.9.0
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 +88 -21
- package/docs/AUTHORING.md +18 -12
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a06.ts +195 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a19.ts +295 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +255 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a41.ts +83 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a47.ts +59 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/constraint_words.ts +177 -0
- package/src/assertions/facts/animated_bones.ts +28 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/constraints.ts +131 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +71 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/skin_meshes.ts +49 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +24 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +30 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/constraints.ts +207 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +247 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +90 -0
- package/src/assertions/model/parse.ts +138 -0
- package/src/assertions/model/region_joins.ts +59 -0
- package/src/assertions/model/skin_entries.ts +59 -0
- package/src/assertions/model/skin_members.ts +62 -0
- package/src/assertions/model/skin_meshes.ts +90 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/vertex_polygons.ts +71 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/values.ts +39 -0
- package/src/check.ts +36 -9
- package/src/compile.ts +204 -57
- package/src/core/index.ts +161 -29
- package/src/core/world.ts +21 -20
- package/src/deformmeasure.ts +377 -154
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +344 -0
- package/src/emit_spine.ts +21 -41
- package/src/keyorder.ts +40 -0
- package/src/model.ts +188 -22
- package/src/render.ts +243 -54
- package/src/render_core.ts +101 -13
- package/src/transform.ts +79 -30
- package/src/validate.ts +742 -2781
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The constraints and the timelines that key them, as the bodies that ask
|
|
3
|
+
* "does this constraint do anything" read them (issue #1025, step 4c of
|
|
4
|
+
* #380) — the census's F12 to F17 and F19 with R1 and R2, for A23, A36, A37,
|
|
5
|
+
* A41, A42, A47 and A48.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ **The order is the runtime's update order**, which is the file's
|
|
8
|
+
* `constraints` array and the document's: a body that prints one line per
|
|
9
|
+
* constraint prints them in this order, and `A42` compares two positions in
|
|
10
|
+
* it. A timeline's `constraint` is a position in the same list.
|
|
11
|
+
*
|
|
12
|
+
* ⭐ **The timelines are in the order the runtime builds them**: the
|
|
13
|
+
* animations in the file's order, and within one the groups `ik`,
|
|
14
|
+
* `transform`, `path`, `physics`, `slider` — whatever order the file keys
|
|
15
|
+
* them in [measured: an animation keying `slider`, `physics`, `transform`,
|
|
16
|
+
* `ik` in that key order loads ik, transform, physics, slider] — each group's
|
|
17
|
+
* constraints and their timelines in the file's order. A body printing one
|
|
18
|
+
* line per key prints them in this order.
|
|
19
|
+
*
|
|
20
|
+
* 🔸 **Values are the ones the runtime holds after its parse, and each
|
|
21
|
+
* derivation is a function rigc already runs**, called by the model side: a
|
|
22
|
+
* field the record leaves out reads the parser's value through the core's
|
|
23
|
+
* record readers (`readModel`); `massInverse` and `step` are the core
|
|
24
|
+
* physics record's `1 / mass` and `1 / fps`; a key's time and value are the
|
|
25
|
+
* float32 the core's timeline reader stores; a Bézier segment's samples are
|
|
26
|
+
* `bezierPolyline` (`src/core/animation.ts`), the runtime's curve the core
|
|
27
|
+
* reproduces; the constraints a physics timeline naming none reaches are
|
|
28
|
+
* read off the core's own `posedPhysics`, asked rather than restated.
|
|
29
|
+
*
|
|
30
|
+
* Links nothing from the runtime.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The five constraint kinds, in the motion spec's words. */
|
|
34
|
+
export type ConstraintKind = 'ik' | 'transform' | 'path' | 'physics' | 'slider';
|
|
35
|
+
|
|
36
|
+
/** The five components a physics constraint drives, by name. */
|
|
37
|
+
export interface PhysicsComponents {
|
|
38
|
+
readonly x: number;
|
|
39
|
+
readonly y: number;
|
|
40
|
+
readonly rotate: number;
|
|
41
|
+
readonly scaleX: number;
|
|
42
|
+
readonly shearX: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The four bounded values of a physics constraint's setup pose — the fields `PHYSICS_POSE_RULES` judges. */
|
|
46
|
+
export interface PhysicsSetup {
|
|
47
|
+
readonly mix: number;
|
|
48
|
+
readonly massInverse: number;
|
|
49
|
+
readonly strength: number;
|
|
50
|
+
readonly damping: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One constraint, in update order. Exactly one of the kind fields is set, the one its `kind` names. */
|
|
54
|
+
export interface ConstraintEntry {
|
|
55
|
+
readonly kind: ConstraintKind;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
/** The runtime class the constraint updates as (`IkConstraint`, `Slider`, …) — the word A42's sentence names `update` on. */
|
|
58
|
+
readonly runtimeClass: string;
|
|
59
|
+
readonly physics?: { readonly bone: string; readonly components: PhysicsComponents; readonly setup: PhysicsSetup; readonly step: number };
|
|
60
|
+
readonly path?: { readonly bones: readonly string[]; readonly slot: string; readonly setup: { readonly mixRotate: number; readonly mixX: number; readonly mixY: number } };
|
|
61
|
+
readonly slider?: {
|
|
62
|
+
/** The animation it applies — its name, how many timelines it carries and its duration — or `null` when it applies none. */
|
|
63
|
+
readonly animation: { readonly name: string; readonly timelines: number; readonly duration: number } | null;
|
|
64
|
+
/** The dial bone's name, or `null` for the bone-less form. */
|
|
65
|
+
readonly bone: string | null;
|
|
66
|
+
readonly loop: boolean;
|
|
67
|
+
readonly scale: number;
|
|
68
|
+
readonly mix: number;
|
|
69
|
+
};
|
|
70
|
+
readonly ik?: { readonly bones: readonly string[]; readonly target: string; readonly mix: number };
|
|
71
|
+
/**
|
|
72
|
+
* A transform constraint's six mix channels in frame order — `mixRotate`,
|
|
73
|
+
* `mixX`, `mixY`, `mixScaleX`, `mixScaleY`, `mixShearY` — each the field's
|
|
74
|
+
* name and its setup value where the constraint declares that property as
|
|
75
|
+
* a `to`, and `null` where it does not: a mix the constraint never reads.
|
|
76
|
+
*/
|
|
77
|
+
readonly transform?: { readonly bones: readonly string[]; readonly mixes: ReadonlyArray<{ readonly field: string; readonly setup: number } | null> };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One frame of a constraint timeline: its time and its first channel, as the runtime stores them (float32). */
|
|
81
|
+
export interface ConstraintFrame {
|
|
82
|
+
readonly time: number;
|
|
83
|
+
readonly value: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One constraint timeline of one animation. */
|
|
87
|
+
export interface ConstraintTimeline {
|
|
88
|
+
readonly animation: string;
|
|
89
|
+
readonly kind: ConstraintKind;
|
|
90
|
+
/**
|
|
91
|
+
* What it keys, in the motion spec's word: `ik` or `transform` for those
|
|
92
|
+
* two; `position`, `spacing`, `mix`; `inertia` … `mix` or `reset`; `time`
|
|
93
|
+
* or `mix`.
|
|
94
|
+
*/
|
|
95
|
+
readonly word: string;
|
|
96
|
+
/** The constraint it names, as a position in `constraints` — or −1 for a physics timeline naming none. */
|
|
97
|
+
readonly constraint: number;
|
|
98
|
+
/** The constraints it writes into, as positions in `constraints`. */
|
|
99
|
+
readonly reach: readonly number[];
|
|
100
|
+
/** Its frames; empty for `reset`. */
|
|
101
|
+
readonly frames: readonly ConstraintFrame[];
|
|
102
|
+
/**
|
|
103
|
+
* Every value one channel poses while it plays: each key's own and every
|
|
104
|
+
* sample of each Bézier segment between two keys, in frame order. Channel
|
|
105
|
+
* 0 is the first value a frame holds (an ik frame's `mix`, a transform
|
|
106
|
+
* frame's `mixRotate`, a path `mix` frame's `mixRotate`, the one value of
|
|
107
|
+
* the others).
|
|
108
|
+
*/
|
|
109
|
+
channelValues(channel: number): readonly number[];
|
|
110
|
+
/**
|
|
111
|
+
* On a physics timeline whose value `PHYSICS_POSE_RULES` bounds, the number
|
|
112
|
+
* a keyed `value` becomes in the pose field the integrator reads — a `mass`
|
|
113
|
+
* key lands as its reciprocal (`massInverse`), the rest as they are: the
|
|
114
|
+
* census's R1. `validate()` asks the runtime's own `set`; the model side the
|
|
115
|
+
* rule's `toPose`, the compiler's reading, which a selftest control holds to
|
|
116
|
+
* the runtime's. Absent on every other timeline.
|
|
117
|
+
*/
|
|
118
|
+
readonly posed?: (value: number) => number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** What A23, A36, A37, A41, A42, A47 and A48 read. */
|
|
122
|
+
export interface ConstraintFacts {
|
|
123
|
+
/** How many animations the skeleton declares. */
|
|
124
|
+
readonly animations: number;
|
|
125
|
+
/** Every constraint, in update order. */
|
|
126
|
+
readonly constraints: readonly ConstraintEntry[];
|
|
127
|
+
/** Every constraint timeline of every animation, in the order the runtime builds them. */
|
|
128
|
+
readonly timelines: readonly ConstraintTimeline[];
|
|
129
|
+
/** The slots some skin gives a path attachment, each once. */
|
|
130
|
+
readonly pathSlots: readonly string[];
|
|
131
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The linked meshes the file declares, as `A44` reads them (issue #1025, step
|
|
3
|
+
* 4c of #380): the census's F10, the subject half.
|
|
4
|
+
*
|
|
5
|
+
* ⭐ **The subject is the file's links, not the loaded ones** (A44's own ⚠️):
|
|
6
|
+
* a link whose region is missing loads as nothing and is in no skin, so the
|
|
7
|
+
* walk is over what the file declares — the skins in the file's order, each
|
|
8
|
+
* skin's slot keys as the file keys them, each slot's entries in table order.
|
|
9
|
+
* The model side walks the document's `linkedmesh` records in that order, by
|
|
10
|
+
* calling the emitter's own orders (`editorSkinOrder`, `editorSlotKeyOrder`).
|
|
11
|
+
*
|
|
12
|
+
* 🔒 **Everything A44 refuses is the encoding's.** A linked mesh's record
|
|
13
|
+
* holds no geometry field at all — `uvs`, `triangles`, `vertices`, `hull` and
|
|
14
|
+
* `edges` written on a link exist only in the Spine text, which is the
|
|
15
|
+
* card's census row for A44 ("a link record has no geometry field"). So the
|
|
16
|
+
* clause that refuses them stays in `src/validate.ts`, which hands its finding
|
|
17
|
+
* about each link here; the model side, which cannot spell such a link, hands
|
|
18
|
+
* none. What moved is the roster — which links there are, so a rig with none
|
|
19
|
+
* SKIPs on both sides and a rig with some is measured on both.
|
|
20
|
+
*
|
|
21
|
+
* Links nothing from the runtime.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** One link the file declares. */
|
|
25
|
+
export interface LinkEntry {
|
|
26
|
+
readonly skin: string;
|
|
27
|
+
readonly slot: string;
|
|
28
|
+
readonly placeholder: string;
|
|
29
|
+
/** The placeholder of the mesh it draws. */
|
|
30
|
+
readonly source: string;
|
|
31
|
+
/** The kept clause's finding about this link — the geometry keys it states — in order; never on the model side. */
|
|
32
|
+
readonly encoding: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What A44 reads. */
|
|
36
|
+
export interface LinkFacts {
|
|
37
|
+
readonly links: readonly LinkEntry[];
|
|
38
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mesh attachments, as the bodies that read their geometry and their
|
|
3
|
+
* weights see them (issue #1025, step 4c of #380) — the census's F07 (vertex
|
|
4
|
+
* data), F08 (mesh geometry) and F10 (the linked-mesh join), for A04, A20,
|
|
5
|
+
* A21, A28 and the mesh half of A23.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ **The order is the file's**, as `./skin_entries.ts` states it: every
|
|
8
|
+
* skin in the file's order, and within a skin the entries slot by slot in the
|
|
9
|
+
* skeleton's slot order — the walk spine-core's `getAttachments()` makes and
|
|
10
|
+
* the model side reproduces by calling the emitter's own order.
|
|
11
|
+
*
|
|
12
|
+
* 🔸 **What a vertex's binding names is the bone's INDEX in the roster**,
|
|
13
|
+
* because two bodies print it (`A20`'s weight-0 clause and `A28`'s row
|
|
14
|
+
* shapes) and the index is what the file holds. The model document names a
|
|
15
|
+
* binding's bone by name; its supplier turns the name into the index the
|
|
16
|
+
* emitter writes (`boneIndexOf` in `src/emit_spine.ts`), called, and a weight
|
|
17
|
+
* into the float32 the runtime stores it as (`Math.fround`).
|
|
18
|
+
*
|
|
19
|
+
* 🔒 **`encoding` is the round trip's, and only the round trip's.** A clause
|
|
20
|
+
* whose subject is the Spine encoding — the flat vertex run's own coherence,
|
|
21
|
+
* a binding's index into the bone array — has no model-side subject: the
|
|
22
|
+
* document names bones and states the weighted form outright, and the run is
|
|
23
|
+
* written by the emitter (the card's point 2). Those clauses stay in
|
|
24
|
+
* `src/validate.ts`, which hands their findings to the body here, at the
|
|
25
|
+
* place in the walk the body used to print them, so the moved body and the
|
|
26
|
+
* kept clause print the lines the one body printed, in the same order. The
|
|
27
|
+
* model side supplies none, because it has no encoding to be wrong.
|
|
28
|
+
*
|
|
29
|
+
* Links nothing from the runtime: the shapes are plain values.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** One influence of a weighted vertex: the bone's index in the roster and the weight the runtime holds. */
|
|
33
|
+
export interface MeshBinding {
|
|
34
|
+
readonly bone: number;
|
|
35
|
+
readonly weight: number;
|
|
36
|
+
/** The kept clause's finding about this binding's index (`A20`'s range rule), where it has one; never on the model side. */
|
|
37
|
+
readonly encoding?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One mesh attachment of one skin, a linked mesh included (it loads as a mesh drawing its source's geometry). */
|
|
41
|
+
export interface MeshEntry {
|
|
42
|
+
/** The attachment's own name: the entry's `name`, else its placeholder. */
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly skin: string;
|
|
45
|
+
/** The slot it is filed under, and that slot's bone. */
|
|
46
|
+
readonly slot: string;
|
|
47
|
+
readonly slotBone: string;
|
|
48
|
+
readonly placeholder: string;
|
|
49
|
+
/** The file's link, when this entry takes its geometry from another mesh, or `null`. */
|
|
50
|
+
readonly link: { readonly source: string } | null;
|
|
51
|
+
/** The triangles it draws, as indices into its vertices — a link's are its source's. */
|
|
52
|
+
readonly triangles: readonly number[];
|
|
53
|
+
/** `worldVerticesLength`: two numbers per vertex. */
|
|
54
|
+
readonly worldVerticesLength: number;
|
|
55
|
+
/** Its `uvs`, as the runtime reads them (doubles). */
|
|
56
|
+
readonly regionUVs: readonly number[];
|
|
57
|
+
/** `hullLength`: two numbers per hull vertex. */
|
|
58
|
+
readonly hullLength: number;
|
|
59
|
+
/** Per vertex its influences, in order — or `null` for an unweighted mesh. */
|
|
60
|
+
readonly weights: ReadonlyArray<readonly MeshBinding[]> | null;
|
|
61
|
+
/** The kept clauses' findings about this mesh's vertex run (`A04`'s encoding rules), in order; never on the model side. */
|
|
62
|
+
readonly encoding: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** What A04, A20, A21, A28 and A23 read of the meshes. */
|
|
66
|
+
export interface MeshFacts {
|
|
67
|
+
/** The bone roster's names, by index. */
|
|
68
|
+
readonly bones: readonly string[];
|
|
69
|
+
/** Every mesh attachment of every skin, in the file's walk order. */
|
|
70
|
+
readonly meshes: readonly MeshEntry[];
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The join from an attachment to an atlas region, before anything loads it
|
|
3
|
+
* (issue #1025, step 4c of #380) — the census's F21 names plus the skin
|
|
4
|
+
* entries' region paths, as A08 reads them: every region name the atlas
|
|
5
|
+
* declares, and every lookup a skin entry will make.
|
|
6
|
+
*
|
|
7
|
+
* 🚨 **Both halves are read before the round trip, on purpose** (issue #589):
|
|
8
|
+
* A08 exists to name a miss the loader would refuse without naming the
|
|
9
|
+
* placeholder or the skin, so it cannot read what the loader produced. On the
|
|
10
|
+
* model side the same holds one step over — A08 runs behind the reader alone,
|
|
11
|
+
* not behind the region rule that stands in A00's place, so a miss is named by
|
|
12
|
+
* A08's sentence on both sides and the parse beside it refuses the same file.
|
|
13
|
+
*
|
|
14
|
+
* ⭐ **The order is the file's.** A08 prints one line per lookup that misses,
|
|
15
|
+
* in the order the skins, a skin's slot keys and a slot's placeholders stand in
|
|
16
|
+
* the file — the emitter's `editorSkinOrder` and `editorSlotKeyOrder`, called
|
|
17
|
+
* on the model side — and then one per padded region name, in the atlas's
|
|
18
|
+
* order.
|
|
19
|
+
*
|
|
20
|
+
* Links nothing from the runtime: `validate()` reads the region names off a
|
|
21
|
+
* `TextureAtlas` it builds for this alone and the lookups off the skeleton
|
|
22
|
+
* JSON; the model side reads both off the document (`../model/region_joins.ts`).
|
|
23
|
+
*/
|
|
24
|
+
import type { AttachmentRegionJoin } from '../region_lookups.ts';
|
|
25
|
+
|
|
26
|
+
export type { AttachmentRegionJoin };
|
|
27
|
+
|
|
28
|
+
/** What A08 reads. Each half is `null` when its file did not parse, and the body says which. */
|
|
29
|
+
export interface RegionJoinFacts {
|
|
30
|
+
/** Every region the atlas declares, by its name as spelled (untrimmed), in the atlas's order. */
|
|
31
|
+
readonly regionNames: readonly string[] | null;
|
|
32
|
+
/** Every skin entry that resolves through a region, with the names it will look up, in the file's order. */
|
|
33
|
+
readonly joins: readonly AttachmentRegionJoin[] | null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skin entries, as the bodies that walk them read them (issue #1025, step
|
|
3
|
+
* 4c of #380) — the census's F03, F05 and F06: every skin's attachments, of
|
|
4
|
+
* which kind each is, and a region's name and size.
|
|
5
|
+
*
|
|
6
|
+
* ⭐ **The order is a fact, and it is the file's.** A body that prints one
|
|
7
|
+
* line per offending entry prints them in the order this list holds them, so a
|
|
8
|
+
* supplier that walked another order would print the same findings in another
|
|
9
|
+
* order — a different report about the same rig. The order is the one
|
|
10
|
+
* spine-core's loaded skins list their entries in: the skins in the file's
|
|
11
|
+
* order (`default` first, the rest in the editor's order the emitter writes —
|
|
12
|
+
* `editorSkinOrder` in `src/compile.ts`), and within a skin the entries slot by
|
|
13
|
+
* slot in the skeleton's slot order, each slot's in the order its table states
|
|
14
|
+
* them. The model side produces it by calling the emitter's own order, never
|
|
15
|
+
* by restating it (`../model/skin_entries.ts`); the selftest measures the two
|
|
16
|
+
* walks equal on every multi-skin build it makes.
|
|
17
|
+
*
|
|
18
|
+
* Links nothing from the runtime: a spine-core `RegionAttachment` satisfies
|
|
19
|
+
* the entry shape structurally, which is how `validate()` supplies it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** One region attachment: the name it loaded under (its `name`, else its placeholder), the region path it resolves through (its `path`, else that name) and its size. */
|
|
23
|
+
export interface RegionEntry {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly width: number;
|
|
26
|
+
readonly height: number;
|
|
27
|
+
/** Read by A19 (issue #1025, cut 4c-1), which exempts a base plate by the region it draws; the runtime types it as possibly unset, and reads it with the name as fallback. */
|
|
28
|
+
readonly path?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What the skins hold, for A03, A11 and A19. */
|
|
32
|
+
export interface SkinEntryFacts {
|
|
33
|
+
/** Every region attachment of every skin, in the file's walk order (the header's ⭐). */
|
|
34
|
+
readonly regionAttachments: readonly RegionEntry[];
|
|
35
|
+
/** How many clipping attachments the skins hold, every skin counted. */
|
|
36
|
+
readonly clippingCount: number;
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a skin activates, and what asks to be activated (issue #1025, step 4c
|
|
3
|
+
* of #380) — the census's F01, F04 and F12 as A38 reads them: every bone with
|
|
4
|
+
* its parent and its `skinRequired` flag, every constraint in update order with
|
|
5
|
+
* its flag, and each skin's own `bones` and constraint lists.
|
|
6
|
+
*
|
|
7
|
+
* 🔑 **A constraint is an object, not a name.** A skin lists constraint
|
|
8
|
+
* OBJECTS, and two constraints of one name under two kinds are two objects a
|
|
9
|
+
* skin may list separately (issue #692) — so a skin's `constraints` hold the
|
|
10
|
+
* very entries `constraints` holds, and a body asks whether one is listed by
|
|
11
|
+
* identity. A bone's `parent` is the bone entry itself, so the ancestor chain is
|
|
12
|
+
* walked by reference too.
|
|
13
|
+
*
|
|
14
|
+
* ⭐ **The order is the file's.** A38 prints one line per offending member:
|
|
15
|
+
* skins in the file's order (the emitter's, `editorSkinOrder`), a skin's bones
|
|
16
|
+
* as it lists them and its constraints kind by kind in the order the file keys
|
|
17
|
+
* them (`ik`, `transform`, `path`, `physics`, `slider` — `RIG_SKIN_CONSTRAINT_KEYS`,
|
|
18
|
+
* the emitter's), then the bones in the skeleton's order and the constraints in
|
|
19
|
+
* update order.
|
|
20
|
+
*
|
|
21
|
+
* Links nothing from the runtime: spine-core's loaded `SkeletonData` satisfies
|
|
22
|
+
* the shape structurally, which is how `validate()` supplies it, and the model
|
|
23
|
+
* side builds it from the document (`../model/skin_members.ts`).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** One bone: its name, whether it is skin-required, and its parent entry. */
|
|
27
|
+
export interface MemberBone {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly skinRequired: boolean;
|
|
30
|
+
readonly parent: MemberBone | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One constraint: its name and whether it is skin-required. */
|
|
34
|
+
export interface MemberConstraint {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly skinRequired: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One skin: its name and the bones and constraint entries it lists. */
|
|
40
|
+
export interface MemberSkin {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly bones: readonly MemberBone[];
|
|
43
|
+
readonly constraints: readonly MemberConstraint[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What A38 reads. */
|
|
47
|
+
export interface SkinMemberFacts {
|
|
48
|
+
readonly skins: readonly MemberSkin[];
|
|
49
|
+
/** Every bone, in the skeleton's order. */
|
|
50
|
+
readonly bones: readonly MemberBone[];
|
|
51
|
+
/** Every constraint, in update order. */
|
|
52
|
+
readonly constraints: readonly MemberConstraint[];
|
|
53
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skins' mesh entries, as the budget and canvas rules read them (issue
|
|
3
|
+
* #1025, step 4c of #380) — the census's F03, F05 and F08 for A13, A14, A15 and
|
|
4
|
+
* A22: every mesh a skin holds, linked meshes included, with where it sits
|
|
5
|
+
* (its slot and that slot's bone), how big it is (its size, its triangle list,
|
|
6
|
+
* its authored UVs and how many coordinates its vertices make) and which bones
|
|
7
|
+
* its weights name.
|
|
8
|
+
*
|
|
9
|
+
* ⭐ **A linked mesh is the geometry it draws.** The runtime loads a link as a
|
|
10
|
+
* mesh whose triangles, UVs, vertices, weights, width and height are its
|
|
11
|
+
* source's (`setSourceMesh` overwrites the link's own `width` and `height`),
|
|
12
|
+
* so an entry for a link carries its source's — the model side reads them
|
|
13
|
+
* through the core's own join, `sourceOfDoc`, never a copy of the rule.
|
|
14
|
+
*
|
|
15
|
+
* ⭐ **The order is the file's**, as `./skin_entries.ts` states it: skins in
|
|
16
|
+
* the emitter's order, a skin's entries slot by slot in the skeleton's slot
|
|
17
|
+
* order, each slot's in its table's order. A13 and A22 print one line per
|
|
18
|
+
* offending mesh in this order.
|
|
19
|
+
*
|
|
20
|
+
* Links nothing from the runtime: `validate()` reads each entry off
|
|
21
|
+
* spine-core's loaded `MeshAttachment`, and the model side off the document
|
|
22
|
+
* (`../model/skin_meshes.ts`).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** One mesh entry of one skin. */
|
|
26
|
+
export interface MeshEntry {
|
|
27
|
+
/** The name it loaded under: its `name`, else its placeholder. */
|
|
28
|
+
readonly name: string;
|
|
29
|
+
/** The slot whose table holds it. */
|
|
30
|
+
readonly slot: string;
|
|
31
|
+
/** That slot's bone. */
|
|
32
|
+
readonly slotBone: string;
|
|
33
|
+
/** Its triangle list, three vertex indices per triangle. */
|
|
34
|
+
readonly triangles: readonly number[];
|
|
35
|
+
readonly width: number;
|
|
36
|
+
readonly height: number;
|
|
37
|
+
/** The UVs the file authored, two per vertex — the runtime's `regionUVs`, not the page-space `uvs` a renderer computes. */
|
|
38
|
+
readonly regionUVs: ArrayLike<number>;
|
|
39
|
+
/** Two per vertex, whatever the vertex encoding — the count the runtime recomputes. */
|
|
40
|
+
readonly worldVerticesLength: number;
|
|
41
|
+
/** Every bone its weights name, in the weight run's order (repeats kept); empty for an unweighted mesh. */
|
|
42
|
+
readonly weightBones: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What A13, A14, A15 and A22 read of the skins. */
|
|
46
|
+
export interface SkinMeshFacts {
|
|
47
|
+
/** Every mesh entry of every skin, in the file's walk order. */
|
|
48
|
+
readonly meshes: readonly MeshEntry[];
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot colour timelines and the colour a slot poses at (issue #1025, step 4c
|
|
3
|
+
* of #380) — the census's F18 and R4: the animations and the slot timelines
|
|
4
|
+
* they key, and one slot's light colour posed by the runtime's rule.
|
|
5
|
+
*
|
|
6
|
+
* ⭐ **The file's order, and the file's spelling.** A45's first clause names
|
|
7
|
+
* the timeline "which the file states last" as the one that wins, so the order
|
|
8
|
+
* of a slot's timelines is a value and not a presentation; and its findings
|
|
9
|
+
* print a key's `time`, `color` and `value` as the file states them. So the
|
|
10
|
+
* list holds, for every animation in the order the file keys them and every
|
|
11
|
+
* slot in the order the animation keys them, that slot's timelines in the
|
|
12
|
+
* file's order, each key as the file spells it. `validate()` reads it off the
|
|
13
|
+
* skeleton JSON; the model side produces it from the document by calling the
|
|
14
|
+
* emitter's own order and its own parser-default and key-order passes
|
|
15
|
+
* (`../model/slot_colour.ts`), so a key the emitter would write without a field
|
|
16
|
+
* reads without it here too.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** One animation's timelines on one slot: timeline name -> keys, as the file states them. */
|
|
20
|
+
export interface SlotTimelines {
|
|
21
|
+
readonly animation: string;
|
|
22
|
+
readonly slot: string;
|
|
23
|
+
readonly timelines: Readonly<Record<string, unknown>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A slot's light colour as posed: four channels, NaN where the pose read no number. */
|
|
27
|
+
export interface PosedSlotColour {
|
|
28
|
+
readonly color: { readonly r: number; readonly g: number; readonly b: number; readonly a: number };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What A45 reads. */
|
|
32
|
+
export interface SlotColourFacts {
|
|
33
|
+
/** Every (animation, slot) pair whose timelines the file keys, in the header's order. */
|
|
34
|
+
readonly slotTimelines: readonly SlotTimelines[];
|
|
35
|
+
/** Whether the skeleton holds an animation of this name. */
|
|
36
|
+
hasAnimation(name: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The slot's colour with the animation applied at `time` on a fresh,
|
|
39
|
+
* non-looping track from the setup pose, no skin set — or `undefined` for a
|
|
40
|
+
* slot the skeleton does not have.
|
|
41
|
+
*/
|
|
42
|
+
posedSlot(animation: string, slot: string, time: number): PosedSlotColour | undefined;
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stage the skeleton states (issue #1025, step 4c of #380) — the census's
|
|
3
|
+
* F11: the setup-pose box's width and height, which A14 measures a mesh against
|
|
4
|
+
* and A19 measures an attachment against.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ **`undefined` is a value here, and it is the runtime's.** A skeleton that
|
|
7
|
+
* declares no stage loads with `width` and `height` undefined (measured through
|
|
8
|
+
* spine-core 4.3.13: a header with neither key reads both as `undefined`, not
|
|
9
|
+
* 0), and the two rules read that absence differently on purpose — A14 skips,
|
|
10
|
+
* A19 names the base plate it cannot decide. So the fact carries the absence
|
|
11
|
+
* rather than a number standing in for it, and each body reads it as it always
|
|
12
|
+
* did.
|
|
13
|
+
*
|
|
14
|
+
* Not in the model document: the stage is the emitter's header
|
|
15
|
+
* (`SkeletonHeader.stage`), and it enters the document with issue #1026. Until
|
|
16
|
+
* then the model side is given it by its caller (`../model/stage.ts`).
|
|
17
|
+
* Links nothing from the runtime.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The stage's two extents, each `undefined` where the skeleton states none. */
|
|
21
|
+
export interface StageFacts {
|
|
22
|
+
readonly width: number | undefined;
|
|
23
|
+
readonly height: number | undefined;
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The polygons — bounding boxes, clipping attachments and paths — and the
|
|
3
|
+
* slot a clip ends at, as `A33` reads them (issue #1025, step 4c of #380):
|
|
4
|
+
* the census's F07 and F09.
|
|
5
|
+
*
|
|
6
|
+
* ⭐ **Two orders, each the one its walk always had.** The polygons are in
|
|
7
|
+
* the loaded skins' order (`./skin_entries.ts`'s ⭐), which is the order A33
|
|
8
|
+
* walked spine-core's `getAttachments()`. The clip ends are in the order the
|
|
9
|
+
* FILE spells them — the skins in the file's order, each skin's slot keys as
|
|
10
|
+
* the file keys them, each slot's entries in table order — because A33 read
|
|
11
|
+
* them off the skeleton JSON: a `null` end and an `end` never written are the
|
|
12
|
+
* same loaded object. The model side produces both by calling the emitter's
|
|
13
|
+
* own orders (`editorSkinOrder`, `editorSlotKeyOrder`).
|
|
14
|
+
*
|
|
15
|
+
* 🔒 **`encoding` is the round trip's**, as in `./mesh_attachments.ts`: the
|
|
16
|
+
* weighted run's decode — a vertex claiming no bone, a binding indexing past
|
|
17
|
+
* the roster, a run decoding to another vertex count, a weight array of the
|
|
18
|
+
* wrong length — and an unweighted array of the wrong length have no subject
|
|
19
|
+
* in a document that states the weighted form outright and names bones.
|
|
20
|
+
* `src/validate.ts` keeps those clauses and hands their findings here, in the
|
|
21
|
+
* order the body printed them; the model side supplies none.
|
|
22
|
+
*
|
|
23
|
+
* Links nothing from the runtime.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** One polygon: how A33 names it, the vertex count it loaded, and a path's own fields. */
|
|
27
|
+
export interface PolygonEntry {
|
|
28
|
+
/** `bounding box "x"`, `clipping attachment "x"` or `path "x"` — the attachment's own name. */
|
|
29
|
+
readonly what: string;
|
|
30
|
+
/** `worldVerticesLength`: two numbers per vertex. */
|
|
31
|
+
readonly worldVerticesLength: number;
|
|
32
|
+
/** A path's `closed` and cumulative curve `lengths`, as the runtime holds them; `null` for the other two kinds. */
|
|
33
|
+
readonly path: { readonly closed: boolean; readonly lengths: readonly number[] } | null;
|
|
34
|
+
/** The kept clauses' findings about this polygon's vertex run, in order; never on the model side. */
|
|
35
|
+
readonly encoding: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One clipping attachment that states an `end`, as the file states it. */
|
|
39
|
+
export interface ClipEnd {
|
|
40
|
+
readonly placeholder: string;
|
|
41
|
+
readonly slot: string;
|
|
42
|
+
readonly end: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What A33 reads. */
|
|
46
|
+
export interface PolygonFacts {
|
|
47
|
+
/** Every bounding box, clipping attachment and path of every skin, in the loaded walk's order. */
|
|
48
|
+
readonly polygons: readonly PolygonEntry[];
|
|
49
|
+
/** Every clipping attachment that states an `end`, in the file's order. */
|
|
50
|
+
readonly clipEnds: readonly ClipEnd[];
|
|
51
|
+
/** The skeleton's slot names. */
|
|
52
|
+
readonly slots: readonly string[];
|
|
53
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness an assertion body runs in, and the report it fills (issue
|
|
3
|
+
* #1025, step 4c of #380).
|
|
4
|
+
*
|
|
5
|
+
* One harness for both sides. `validate()` runs a body over the facts it reads
|
|
6
|
+
* off what spine-core loaded; the model side (`./model/index.ts`) runs the same
|
|
7
|
+
* body over the facts it reads off the model document and the core. What makes
|
|
8
|
+
* a line the same line on both sides is that the body is one function and the
|
|
9
|
+
* harness deciding PASS, SKIP, FAIL and PROF around it is one function too — a
|
|
10
|
+
* second copy of `check` would be a second place a verdict could be decided
|
|
11
|
+
* differently.
|
|
12
|
+
*
|
|
13
|
+
* Moved here from `validate()`, the rule unchanged: an assertion is recorded
|
|
14
|
+
* as passed only when its body neither failed nor skipped, a body that throws
|
|
15
|
+
* is a FAIL whose detail is `threw: <message>`, and a rule the profile does not
|
|
16
|
+
* carry is not run at all and is recorded as out of profile.
|
|
17
|
+
*
|
|
18
|
+
* This module links nothing from the runtime, directly or through what it
|
|
19
|
+
* imports — the condition for the model side to use it, held by the selftest.
|
|
20
|
+
*/
|
|
21
|
+
import { kindRunsUnder, type AssertionKind, type AssertionProfile } from './kinds.ts';
|
|
22
|
+
|
|
23
|
+
/** One failure: the assertion's code and the sentence naming the object, the value found and the value required. */
|
|
24
|
+
export interface Failure {
|
|
25
|
+
assertion: string;
|
|
26
|
+
detail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What a body is handed: how to fail, how to declare it had nothing to measure, and the stats line it may add to. */
|
|
30
|
+
export interface Verdicts {
|
|
31
|
+
fail: (assertion: string, detail: string) => void;
|
|
32
|
+
skip: (assertion: string, reason: string) => void;
|
|
33
|
+
stats: Record<string, number | string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The four lists and the stats a run of bodies leaves — the shape `validate()` returns, less the profile. */
|
|
37
|
+
export interface VerdictLists {
|
|
38
|
+
failures: Failure[];
|
|
39
|
+
/** Assertions that ran and passed, in order. */
|
|
40
|
+
passed: string[];
|
|
41
|
+
/** Assertions that had nothing to run against, with the reason. */
|
|
42
|
+
skipped: Array<{ assertion: string; reason: string }>;
|
|
43
|
+
/** Assertions the profile does not apply, with their kind. */
|
|
44
|
+
profileSkipped: Array<{ assertion: string; kind: 'renderer' | 'archetype' }>;
|
|
45
|
+
stats: Record<string, number | string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A harness: the lists it fills, the two calls a body makes, and `check`, which runs a body. */
|
|
49
|
+
export interface VerdictHarness extends VerdictLists {
|
|
50
|
+
fail: Verdicts['fail'];
|
|
51
|
+
skip: Verdicts['skip'];
|
|
52
|
+
/**
|
|
53
|
+
* Run one assertion; record it as passed only if it neither failed nor skipped,
|
|
54
|
+
* and do not run it at all when the profile does not carry that kind of rule.
|
|
55
|
+
*
|
|
56
|
+
* The unknown-assertion throw is deliberate: an assertion with no entry in
|
|
57
|
+
* the kind table would otherwise pick a profile by accident, and picking
|
|
58
|
+
* wrong means either a rule that never runs or a rule that fires on
|
|
59
|
+
* everybody's data.
|
|
60
|
+
*
|
|
61
|
+
* The body's return value is handed back — `undefined` when the assertion did
|
|
62
|
+
* not run or threw. `validate()`'s A00 uses it so that the loaded atlas and
|
|
63
|
+
* skeleton can be `const`: assigning them from inside this callback leaves
|
|
64
|
+
* the type checker unable to see that they were ever set, and every later read
|
|
65
|
+
* of `atlas.pages` or `data.bones` becomes a property of `never`.
|
|
66
|
+
*/
|
|
67
|
+
check: <T>(assertion: string, body: () => T) => T | undefined;
|
|
68
|
+
/** `fail`, `skip` and `stats`, as one value to hand a body. */
|
|
69
|
+
verdicts: Verdicts;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A fresh harness for one run under `profile`. `kinds` is the kind table the
|
|
74
|
+
* run's codes are looked up in, and `who` names the run in the throw for a code
|
|
75
|
+
* the table lacks — `validate` for the gate, so its message is the one it was.
|
|
76
|
+
*/
|
|
77
|
+
export function verdictHarness(profile: AssertionProfile, kinds: Readonly<Record<string, AssertionKind>>, who: string): VerdictHarness {
|
|
78
|
+
const failures: Failure[] = [];
|
|
79
|
+
const passed: string[] = [];
|
|
80
|
+
const skipped: VerdictLists['skipped'] = [];
|
|
81
|
+
const profileSkipped: VerdictLists['profileSkipped'] = [];
|
|
82
|
+
const stats: Record<string, number | string> = {};
|
|
83
|
+
const fail = (assertion: string, detail: string): void => {
|
|
84
|
+
failures.push({ assertion, detail });
|
|
85
|
+
};
|
|
86
|
+
/** Declare that an assertion had nothing to check, and why. */
|
|
87
|
+
const skip = (assertion: string, reason: string): void => {
|
|
88
|
+
skipped.push({ assertion, reason });
|
|
89
|
+
};
|
|
90
|
+
const check = <T>(assertion: string, body: () => T): T | undefined => {
|
|
91
|
+
const kind = kinds[assertion];
|
|
92
|
+
if (!kind) throw new Error(`${who}: assertion "${assertion}" has no ASSERTION_KIND entry`);
|
|
93
|
+
if (!kindRunsUnder(kind, profile)) {
|
|
94
|
+
profileSkipped.push({ assertion, kind: kind === 'renderer' ? 'renderer' : 'archetype' });
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const before = failures.length;
|
|
98
|
+
const skippedBefore = skipped.length;
|
|
99
|
+
let result: T | undefined;
|
|
100
|
+
try {
|
|
101
|
+
result = body();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
fail(assertion, `threw: ${(err as Error).message}`);
|
|
104
|
+
}
|
|
105
|
+
if (failures.length === before && skipped.length === skippedBefore) passed.push(assertion);
|
|
106
|
+
return result;
|
|
107
|
+
};
|
|
108
|
+
return { failures, passed, skipped, profileSkipped, stats, fail, skip, check, verdicts: { fail, skip, stats } };
|
|
109
|
+
}
|