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.
Files changed (79) hide show
  1. package/cli.ts +88 -21
  2. package/docs/AUTHORING.md +18 -12
  3. package/package.json +1 -1
  4. package/src/assertions/bodies/a03.ts +27 -0
  5. package/src/assertions/bodies/a04.ts +40 -0
  6. package/src/assertions/bodies/a06.ts +195 -0
  7. package/src/assertions/bodies/a08.ts +76 -0
  8. package/src/assertions/bodies/a11.ts +15 -0
  9. package/src/assertions/bodies/a13.ts +51 -0
  10. package/src/assertions/bodies/a14.ts +35 -0
  11. package/src/assertions/bodies/a15.ts +97 -0
  12. package/src/assertions/bodies/a17.ts +26 -0
  13. package/src/assertions/bodies/a19.ts +295 -0
  14. package/src/assertions/bodies/a20.ts +122 -0
  15. package/src/assertions/bodies/a21.ts +190 -0
  16. package/src/assertions/bodies/a22.ts +39 -0
  17. package/src/assertions/bodies/a23.ts +255 -0
  18. package/src/assertions/bodies/a27.ts +33 -0
  19. package/src/assertions/bodies/a28.ts +70 -0
  20. package/src/assertions/bodies/a33.ts +110 -0
  21. package/src/assertions/bodies/a36.ts +81 -0
  22. package/src/assertions/bodies/a37.ts +77 -0
  23. package/src/assertions/bodies/a38.ts +73 -0
  24. package/src/assertions/bodies/a41.ts +83 -0
  25. package/src/assertions/bodies/a42.ts +97 -0
  26. package/src/assertions/bodies/a44.ts +23 -0
  27. package/src/assertions/bodies/a45.ts +172 -0
  28. package/src/assertions/bodies/a47.ts +59 -0
  29. package/src/assertions/bodies/a48.ts +83 -0
  30. package/src/assertions/constraint_words.ts +177 -0
  31. package/src/assertions/facts/animated_bones.ts +28 -0
  32. package/src/assertions/facts/atlas_pages.ts +19 -0
  33. package/src/assertions/facts/atlas_regions.ts +52 -0
  34. package/src/assertions/facts/constraints.ts +131 -0
  35. package/src/assertions/facts/linked_meshes.ts +38 -0
  36. package/src/assertions/facts/mesh_attachments.ts +71 -0
  37. package/src/assertions/facts/region_joins.ts +34 -0
  38. package/src/assertions/facts/skin_entries.ts +37 -0
  39. package/src/assertions/facts/skin_members.ts +53 -0
  40. package/src/assertions/facts/skin_meshes.ts +49 -0
  41. package/src/assertions/facts/slot_colour.ts +43 -0
  42. package/src/assertions/facts/stage.ts +24 -0
  43. package/src/assertions/facts/vertex_polygons.ts +53 -0
  44. package/src/assertions/harness.ts +109 -0
  45. package/src/assertions/kinds.ts +102 -0
  46. package/src/assertions/mesh_kinds.ts +56 -0
  47. package/src/assertions/model/animated_bones.ts +30 -0
  48. package/src/assertions/model/atlas_pages.ts +15 -0
  49. package/src/assertions/model/atlas_regions.ts +76 -0
  50. package/src/assertions/model/constraints.ts +207 -0
  51. package/src/assertions/model/given.ts +45 -0
  52. package/src/assertions/model/index.ts +247 -0
  53. package/src/assertions/model/linked_meshes.ts +24 -0
  54. package/src/assertions/model/mesh_attachments.ts +90 -0
  55. package/src/assertions/model/parse.ts +138 -0
  56. package/src/assertions/model/region_joins.ts +59 -0
  57. package/src/assertions/model/skin_entries.ts +59 -0
  58. package/src/assertions/model/skin_members.ts +62 -0
  59. package/src/assertions/model/skin_meshes.ts +90 -0
  60. package/src/assertions/model/slot_colour.ts +80 -0
  61. package/src/assertions/model/stage.ts +28 -0
  62. package/src/assertions/model/vertex_polygons.ts +71 -0
  63. package/src/assertions/reasons.ts +127 -0
  64. package/src/assertions/region_lookups.ts +61 -0
  65. package/src/assertions/values.ts +39 -0
  66. package/src/check.ts +36 -9
  67. package/src/compile.ts +204 -57
  68. package/src/core/index.ts +161 -29
  69. package/src/core/world.ts +21 -20
  70. package/src/deformmeasure.ts +377 -154
  71. package/src/deformreport.ts +3 -0
  72. package/src/deformstructure.ts +344 -0
  73. package/src/emit_spine.ts +21 -41
  74. package/src/keyorder.ts +40 -0
  75. package/src/model.ts +188 -22
  76. package/src/render.ts +243 -54
  77. package/src/render_core.ts +101 -13
  78. package/src/transform.ts +79 -30
  79. 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
+ }