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,62 @@
1
+ /**
2
+ * The model side's supply of `SkinMemberFacts` (issue #1025, step 4c of
3
+ * #380): the bones, the constraints and each skin's lists, read off the
4
+ * document and joined the way the runtime's loader joins them.
5
+ *
6
+ * - **Bones**: the document's `bones` in order, each with its `skinRequired`
7
+ * as stated (absent is not required) and its parent entry.
8
+ * - **Constraints**: the document's `constraints` in order — update order, the
9
+ * order the emitter writes them in — each skin-required exactly when it
10
+ * states `skin: true`, the field the emitter writes through unchanged.
11
+ * - **Skins**: in the emitter's order (`editorSkinOrder`, `src/compile.ts`); a
12
+ * skin's bones as it lists them, each the FIRST bone of that name; its
13
+ * constraints kind by kind in the emitter's key order
14
+ * (`RIG_SKIN_CONSTRAINT_KEYS`, which `emitSkins` walks), each the first
15
+ * constraint of that kind and name — the object the loader finds, and the
16
+ * same object `constraints` holds, so a body asking whether one is listed
17
+ * asks by identity. `readModel` refuses a skin listing a constraint of a
18
+ * kind and name the document lacks; a bone name it lacks is this module's to
19
+ * refuse, by name.
20
+ *
21
+ * Links nothing from the runtime.
22
+ */
23
+ import { editorSkinOrder } from '../../compile.ts';
24
+ import { RIG_SKIN_CONSTRAINT_KEYS } from '../../rig.ts';
25
+ import type { MemberBone, MemberConstraint, MemberSkin, SkinMemberFacts } from '../facts/skin_members.ts';
26
+ import { isObj } from '../values.ts';
27
+ import type { ReadDocument } from './parse.ts';
28
+
29
+ export function modelSkinMembers(read: ReadDocument): SkinMemberFacts {
30
+ const doc = read.doc;
31
+ const bones: MemberBone[] = [];
32
+ const boneByName = new Map<string, MemberBone>();
33
+ for (const bone of doc.bones) {
34
+ const parent = bone.parent === undefined ? null : boneByName.get(bone.parent);
35
+ if (parent === undefined) throw new Error(`internal: bone "${bone.name}" names parent "${bone.parent}", which the reader accepted and no earlier bone is`);
36
+ const entry: MemberBone = { name: bone.name, skinRequired: bone.skinRequired === true, parent };
37
+ bones.push(entry);
38
+ if (!boneByName.has(bone.name)) boneByName.set(bone.name, entry);
39
+ }
40
+ const constraints: Array<MemberConstraint & { kind: string }> = [];
41
+ const stated = Array.isArray(read.json.constraints) ? read.json.constraints.filter(isObj) : [];
42
+ for (const constraint of stated) {
43
+ constraints.push({ name: String(constraint.name), kind: String(constraint.kind), skinRequired: constraint.skin === true });
44
+ }
45
+ const skins: MemberSkin[] = editorSkinOrder(doc.skins).map((skin) => {
46
+ const listedBones = skin.bones.map((name) => {
47
+ const found = boneByName.get(name);
48
+ if (found === undefined) throw new Error(`skin "${skin.name}" lists bone "${name}", which is not a bone of the document`);
49
+ return found;
50
+ });
51
+ const listedConstraints: MemberConstraint[] = [];
52
+ for (const kind of RIG_SKIN_CONSTRAINT_KEYS) {
53
+ for (const name of skin.constraints[kind]) {
54
+ const found = constraints.find((c) => c.kind === kind && c.name === name);
55
+ if (found === undefined) throw new Error(`internal: skin "${skin.name}" lists ${kind} constraint "${name}", which the reader accepted and the document does not hold`);
56
+ listedConstraints.push(found);
57
+ }
58
+ }
59
+ return { name: skin.name, bones: listedBones, constraints: listedConstraints };
60
+ });
61
+ return { skins, bones, constraints };
62
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The model side's supply of `SkinMeshFacts` (issue #1025, step 4c of #380):
3
+ * every mesh and linked-mesh record of every skin, read off the document in the
4
+ * file's walk order.
5
+ *
6
+ * - **The order** is `fileOrderedEntries`'s (`./skin_entries.ts`): the skins by
7
+ * the emitter's `editorSkinOrder`, the slots in the document's slot order, a
8
+ * slot's records in its table's order — the walk the selftest measures equal
9
+ * to spine-core's `getAttachments()`.
10
+ * - **A linked mesh's geometry is its source's**, read through the core's own
11
+ * joins rather than a copy of them: the vertices through `sourceOfDoc` and
12
+ * the UVs and triangles through the draw walk's `meshOf` (`drawWalkOf`), both
13
+ * in `src/core/index.ts`; and its `width` and `height` are the source
14
+ * record's too, the two numbers `setSourceMesh` overwrites the link's own
15
+ * with. `readModel` refuses a link whose source does not resolve, so a miss
16
+ * here is this module's defect.
17
+ * - **The bones a mesh's weights name** are its bindings' bone names, in the
18
+ * bindings' order — the document states a weighted vertex by bone NAME, and
19
+ * the Spine file's index run is the emitter's encoding of exactly that list.
20
+ * - **`worldVerticesLength`** is two per vertex: `2 · bindings.length` for a
21
+ * weighted mesh, `xy.length` for a plain one — the derivation
22
+ * `src/deformstructure.ts` already makes for the deform survey.
23
+ * - **`width` and `height`** are the record's, off the document's JSON: the
24
+ * core's geometry reader keeps a mesh's vertices, UVs and triangles and not
25
+ * its size, which no pose reads.
26
+ *
27
+ * Links nothing from the runtime.
28
+ */
29
+ import { drawWalkOf, shownRow, sourceOfDoc } from '../../core/index.ts';
30
+ import type { ModelVertices } from '../../model.ts';
31
+ import type { MeshEntry, SkinMeshFacts } from '../facts/skin_meshes.ts';
32
+ import { isObj } from '../values.ts';
33
+ import type { ReadDocument } from './parse.ts';
34
+ import { fileOrderedEntries } from './skin_entries.ts';
35
+
36
+ /** A record's own `width` and `height` as the document states them, found by skin (the first of that name, as a link resolves its skin), slot and placeholder. */
37
+ function statedSize(read: ReadDocument, skin: string, slot: string, placeholder: string): { width: number; height: number } {
38
+ const skins = Array.isArray(read.json.skins) ? read.json.skins : [];
39
+ const found = skins.find((k: unknown) => isObj(k) && k.name === skin);
40
+ const table = isObj(found) && isObj(found.attachments) ? found.attachments[slot] : undefined;
41
+ const record = isObj(table) ? table[placeholder] : undefined;
42
+ if (!isObj(record) || typeof record.width !== 'number' || typeof record.height !== 'number') {
43
+ throw new Error(`internal: skin "${skin}" slot "${slot}" placeholder "${placeholder}" is a mesh record the document states no width and height for`);
44
+ }
45
+ return { width: record.width, height: record.height };
46
+ }
47
+
48
+ export function modelSkinMeshes(read: ReadDocument): SkinMeshFacts {
49
+ const doc = read.doc;
50
+ const sourceVertices = sourceOfDoc(doc);
51
+ const walk = drawWalkOf(doc, []);
52
+ const slotBone = new Map(doc.slots.map((slot) => [slot.name, slot.bone] as const));
53
+ const meshes: MeshEntry[] = [];
54
+ for (const entry of fileOrderedEntries(read)) {
55
+ if (entry.record.kind !== 'mesh' && entry.record.kind !== 'linkedmesh') continue;
56
+ const g = entry.record.geometry;
57
+ let vertices: ModelVertices;
58
+ let uvs: number[];
59
+ let triangles: number[];
60
+ let size: { width: number; height: number };
61
+ if (g?.kind === 'mesh') {
62
+ ({ vertices, uvs, triangles } = g);
63
+ size = statedSize(read, entry.skin, entry.slot, entry.placeholder);
64
+ } else if (g?.kind === 'linkedmesh') {
65
+ const source = sourceVertices(g.skin, g.slot, g.source);
66
+ const drawn = walk.meshOf(g.skin, g.slot, g.source);
67
+ if (typeof source === 'string' || drawn === undefined) throw new Error(`internal: skin "${entry.skin}" slot "${entry.slot}" placeholder "${entry.placeholder}": ${typeof source === 'string' ? source : 'its source draws no mesh'}, and the reader accepted it`);
68
+ vertices = source;
69
+ ({ uvs, triangles } = drawn);
70
+ size = statedSize(read, g.skin, g.slot, g.source);
71
+ } else {
72
+ // `readModel` reads every mesh and link record's geometry or refuses the document, so a record without one is this module's defect.
73
+ throw new Error(`internal: skin "${entry.skin}" slot "${entry.slot}" placeholder "${entry.placeholder}" is a ${entry.record.kind} record the reader returned without its geometry`);
74
+ }
75
+ const bone = slotBone.get(entry.slot);
76
+ if (bone === undefined) throw new Error(`internal: skin "${entry.skin}" names slot "${entry.slot}", which the reader accepted and the document does not hold`);
77
+ meshes.push({
78
+ name: shownRow(entry).name,
79
+ slot: entry.slot,
80
+ slotBone: bone,
81
+ triangles,
82
+ width: size.width,
83
+ height: size.height,
84
+ regionUVs: uvs,
85
+ worldVerticesLength: vertices.weighted ? 2 * vertices.bindings.length : vertices.xy.length,
86
+ weightBones: vertices.weighted ? vertices.bindings.flatMap((vertex) => vertex.map((binding) => binding.bone)) : [],
87
+ });
88
+ }
89
+ return { meshes };
90
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The model side's supply of `SlotColourFacts` (issue #1025, step 4c of #380).
3
+ *
4
+ * - **The slot timelines, in the file's order and spelling.** The document
5
+ * holds each animation's slot timelines in the order they are applied, with
6
+ * every key as the compiler built it; the Spine file keys the animations in
7
+ * the editor's order and passes every key through two passes. So the
8
+ * animations are put in order by `editorAnimationOrder` (`src/compile.ts`,
9
+ * the function the emitter is handed), and the timelines are laid out as the
10
+ * file lays them and handed to the emitter's own `withoutParserDefaults` and
11
+ * `inEditorKeyOrder` (`src/keyorder.ts`) — so a key field the emitter would
12
+ * leave out at the parser's default is left out here too, and a body
13
+ * printing `JSON.stringify(key.value)` prints what it prints over the file.
14
+ * Nothing here restates either rule.
15
+ * - **Whether an animation exists**: the document's own roster.
16
+ * - **A slot's posed colour**: the core's raw entry (`poseRawAnimation`,
17
+ * `src/core/raw.ts`) taking one step of the time on a fresh non-looping
18
+ * track, under the view the core poses with no skin set (`noSkinView`,
19
+ * `src/render_core.ts`) — the reading of what `validate()` does through
20
+ * spine-core (a fresh skeleton, no skin, setup pose, the track stepped to the
21
+ * time and applied). Its slot rows are the ones `CR03` holds bit for bit to
22
+ * spine-core on every frame of the nineteen recipes; that the two readings
23
+ * print the same A45 line is the selftest's supplier check, on every call
24
+ * that poses one (`VF02`). A channel the pose read as no number is NaN, as a
25
+ * non-finite channel reads on the runtime's side.
26
+ *
27
+ * Links nothing from the runtime.
28
+ */
29
+ import { editorAnimationOrder } from '../../compile.ts';
30
+ import type { CompiledDocument } from '../../core/index.ts';
31
+ import { poseRawAnimation } from '../../core/raw.ts';
32
+ import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
33
+ import { noSkinView } from '../../render_core.ts';
34
+ import type { SlotColourFacts, SlotTimelines } from '../facts/slot_colour.ts';
35
+ import { isObj, type Json } from '../values.ts';
36
+ import type { ReadDocument } from './parse.ts';
37
+
38
+ const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
39
+
40
+ /** Every animation's slot timelines as the Spine file states them: `animations.<name>.slots.<slot>.<timeline> = keys`. */
41
+ export function fileSlotTimelines(read: ReadDocument): SlotTimelines[] {
42
+ const byName = new Map<string, Json>();
43
+ for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
44
+ const animations: Json = {};
45
+ for (const name of editorAnimationOrder([...byName.keys()])) {
46
+ const slots: Json = {};
47
+ for (const slot of list((byName.get(name) as Json).slots)) {
48
+ if (typeof slot.name !== 'string') continue;
49
+ const timelines: Json = {};
50
+ for (const timeline of list(slot.timelines)) {
51
+ if (typeof timeline.name === 'string') timelines[timeline.name] = list(timeline.keys).map((key) => ({ ...key }));
52
+ }
53
+ slots[slot.name] = timelines;
54
+ }
55
+ // An animation keying no slot writes no `slots` group — `emitAnimation`'s `if (animation.slots.size)`.
56
+ animations[name] = Object.keys(slots).length > 0 ? { slots } : {};
57
+ }
58
+ const file = inEditorKeyOrder(withoutParserDefaults({ animations }));
59
+ const out: SlotTimelines[] = [];
60
+ for (const [animation, anim] of Object.entries(file.animations)) {
61
+ if (!isObj(anim) || !isObj(anim.slots)) continue;
62
+ for (const [slot, timelines] of Object.entries(anim.slots)) if (isObj(timelines)) out.push({ animation, slot, timelines });
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export function modelSlotColour(read: ReadDocument): SlotColourFacts {
68
+ let view: CompiledDocument | null = null;
69
+ return {
70
+ slotTimelines: fileSlotTimelines(read),
71
+ hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
72
+ posedSlot: (animation, slot, time) => {
73
+ view ??= noSkinView(read.doc);
74
+ const row = poseRawAnimation(view, animation, [time], {}, 'animation')[1].slots.find((r) => r[0] === slot);
75
+ if (row === undefined) return undefined;
76
+ const channel = (v: number | null): number => (v === null ? Number.NaN : v);
77
+ return { color: { r: channel(row[2]), g: channel(row[3]), b: channel(row[4]), a: channel(row[5]) } };
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The model side's supply of `StageFacts` (issue #1025, step 4c of #380): the
3
+ * stage a `rigc-compiled/3` document states (`stated.stage`, issue #1026), or,
4
+ * for a `/2` or `/1` document, which does not, the stage its caller gives.
5
+ * A `/3` document given one anyway is refused (`refuseGivenBeside`).
6
+ *
7
+ * ⚠️ A caller that gives no stage at all is refused by name rather than read as
8
+ * "the skeleton declares none": those are two different statements, and only
9
+ * the second is one the caller made. A stage stated as `null` IS that second
10
+ * statement, and reads as both extents absent, the way the runtime loads a
11
+ * header with neither key.
12
+ *
13
+ * Links nothing from the runtime.
14
+ */
15
+ import type { StageFacts } from '../facts/stage.ts';
16
+ import { refuseGivenBeside, type ModelGiven } from './given.ts';
17
+ import type { ReadDocument } from './parse.ts';
18
+
19
+ export function modelStage(read: ReadDocument, given: ModelGiven | undefined): StageFacts {
20
+ refuseGivenBeside(read, given);
21
+ // The document's own statement (issue #1026): `null` is a skeleton that declares none, read as both extents absent.
22
+ if (read.doc.stated !== null) {
23
+ const stage = read.doc.stated.stage;
24
+ return { width: stage?.width, height: stage?.height };
25
+ }
26
+ if (given === undefined) throw new Error(`the caller gave no stage; a ${read.doc.spec} document does not state one, so the model side cannot read it`);
27
+ return { width: given.stage?.width, height: given.stage?.height };
28
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The model side's supply of `PolygonFacts` (issue #1025, step 4c of #380):
3
+ * the document's bounding boxes, clipping attachments and paths, and the
4
+ * clip ends it states.
5
+ *
6
+ * - **The polygons** in the loaded walk's order, `fileOrderedEntries`
7
+ * (`./skin_entries.ts`). A polygon's `worldVerticesLength` is twice the
8
+ * `vertexCount` the record states — the number the emitter copies into the
9
+ * file, which the parser doubles into the length it reads the vertices to
10
+ * (`readVertices`); the selftest compares it with spine-core's on every
11
+ * call. A path's `closed` and `lengths` are the core's reading of the record
12
+ * (`readGeometry`: `closed` absent reads the parser's `false`).
13
+ * - **The clip ends** in the order the file spells them: the skins by the
14
+ * emitter's `editorSkinOrder`, each skin's slot keys by its
15
+ * `editorSlotKeyOrder` (both `src/compile.ts`, the functions `emitSkins` is
16
+ * handed), each slot's records in table order — every clipping record that
17
+ * states an `end`, with the `end` as it states it.
18
+ *
19
+ * `encoding` is empty on every polygon: the document states the weighted
20
+ * form outright, so a run's decode has no subject here.
21
+ *
22
+ * Links nothing from the runtime.
23
+ */
24
+ import { editorSkinOrder, editorSlotKeyOrder } from '../../compile.ts';
25
+ import { shownRow } from '../../core/index.ts';
26
+ import type { ClipEnd, PolygonEntry, PolygonFacts } from '../facts/vertex_polygons.ts';
27
+ import { isObj, type Json } from '../values.ts';
28
+ import { fileOrderedEntries } from './skin_entries.ts';
29
+ import type { ReadDocument } from './parse.ts';
30
+
31
+ /** The document's own record of one skin entry, as the JSON spells it. */
32
+ export function documentRecord(read: ReadDocument, skin: string, slot: string, placeholder: string): Json {
33
+ const skins = Array.isArray(read.json.skins) ? read.json.skins.filter(isObj) : [];
34
+ const table = skins.find((s) => s.name === skin)?.attachments;
35
+ const record = isObj(table) && isObj(table[slot]) ? (table[slot] as Json)[placeholder] : undefined;
36
+ if (!isObj(record)) throw new Error(`internal: skin "${skin}" slot "${slot}" placeholder "${placeholder}" is a record the reader returned and the document does not spell`);
37
+ return record;
38
+ }
39
+
40
+ /** Every record of the document in the order the Spine file spells it: skins, then each skin's slot keys, then the slot's table. */
41
+ export function fileSpelledEntries(read: ReadDocument): Array<{ skin: string; slot: string; placeholder: string; record: Json }> {
42
+ const out: Array<{ skin: string; slot: string; placeholder: string; record: Json }> = [];
43
+ for (const skin of editorSkinOrder(read.doc.skins)) {
44
+ for (const [slot, table] of Object.entries(editorSlotKeyOrder(skin.attachments))) {
45
+ for (const placeholder of Object.keys(table)) out.push({ skin: skin.name, slot, placeholder, record: documentRecord(read, skin.name, slot, placeholder) });
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+
51
+ const WHAT: Readonly<Record<string, string>> = { boundingbox: 'bounding box', clipping: 'clipping attachment', path: 'path' };
52
+
53
+ export function modelPolygonFacts(read: ReadDocument): PolygonFacts {
54
+ const polygons: PolygonEntry[] = [];
55
+ for (const entry of fileOrderedEntries(read)) {
56
+ const g = entry.record.geometry;
57
+ if (g === undefined || (g.kind !== 'boundingbox' && g.kind !== 'clipping' && g.kind !== 'path')) continue;
58
+ const stated = documentRecord(read, entry.skin, entry.slot, entry.placeholder).vertexCount;
59
+ polygons.push({
60
+ what: `${WHAT[g.kind]} "${shownRow(entry).name}"`,
61
+ worldVerticesLength: typeof stated === 'number' ? stated * 2 : Number.NaN,
62
+ path: g.kind === 'path' ? { closed: g.closed, lengths: g.lengths } : null,
63
+ encoding: [],
64
+ });
65
+ }
66
+ const clipEnds: ClipEnd[] = [];
67
+ for (const { slot, placeholder, record } of fileSpelledEntries(read)) {
68
+ if (record.kind === 'clipping' && record.end !== undefined) clipEnds.push({ placeholder, slot, end: record.end });
69
+ }
70
+ return { polygons, clipEnds, slots: read.doc.slots.map((s) => s.name) };
71
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * What a SKIP says, by what the assertion was denied (issues #568, #580, #608).
3
+ *
4
+ * Moved here from `src/validate.ts` by issue #1025 (step 4c of #380), words
5
+ * unchanged: an assertion body now lives in a module that links nothing from
6
+ * the runtime (`./bodies/`), and the bodies print these sentences on both
7
+ * sides — over what spine-core loaded and over the model document. A sentence
8
+ * kept in the file that links spine-core could not be read by the second.
9
+ * `src/validate.ts` re-exports every one, so nothing that imported them from
10
+ * there moves.
11
+ */
12
+
13
+ /**
14
+ * What a SKIP says when `A00_ROUNDTRIP_PARSE` handed an assertion nothing to
15
+ * look at.
16
+ *
17
+ * Two of them rather than one, because what an assertion was denied is the whole
18
+ * content of its SKIP: `A20` wanted the loaded skeleton and `A06` wanted the
19
+ * loaded atlas, and a reader who is told which can tell a rule that is waiting
20
+ * on the parse from one that is waiting on the art. Both point at A00 instead of
21
+ * restating its detail, which the FAIL row prints in full.
22
+ *
23
+ * Exported so a control compares against them rather than quoting them — the
24
+ * same reason `ASSERTION_NAMES` is exported.
25
+ */
26
+ export const SKIP_NO_SKELETON = 'the round trip did not produce a skeleton to measure (A00 owns that failure)';
27
+ export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure (A00 owns that failure)';
28
+ /**
29
+ * The third of them, and it is NOT waiting on A00 (issue #608): the atlas parsed
30
+ * and it has no pages, which is what a compile that measured no art writes. The
31
+ * four rules whose only subject is a page — A06, A17, A19, A27 — have nothing to
32
+ * look at, and an empty loop walking out of `check()` is reported as a PASS.
33
+ */
34
+ /*
35
+ * **An empty subject list: SKIP or PASS, and which one is derived (issue #580).**
36
+ *
37
+ * `check()` records a pass when a body runs to its end without a `fail()` or a
38
+ * `skip()`, so a body whose main construct is a loop over a list that is EMPTY
39
+ * passes having measured nothing — the vacuous green one ring out from the bare
40
+ * `return` guards issue #568 converted. Two different things can be true of such
41
+ * a loop, and the criterion decides between them by reading the rule's own name
42
+ * and its `fail()` sentences rather than by preference:
43
+ *
44
+ * * A name of the form ⟨subject⟩_⟨property⟩ — `REGION_WIDTH_HEIGHT_FINITE`,
45
+ * `MESH_TRIANGLES_AND_ENCODING`, `PHYSICS_CONSTRAINT_EFFECTIVE`,
46
+ * `ATLAS_PAGE_SIZE_MATCHES_PNG` — quantifies over the subject it names, and
47
+ * its `fail()` names a member of that subject, the value found and the value
48
+ * required. With no member, nothing was measured: it **SKIPs**, and the
49
+ * reason names the subject that was absent.
50
+ * * A name of the form NO_⟨construct⟩ — `NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS`,
51
+ * `NO_BONE_TRANSFORM_KEY`, `NO_CLIPPING_ATTACHMENTS`, `NO_DARK_COLOR`,
52
+ * `NO_FULL_FRAME_MESH` — quantifies over occurrences of something a correct
53
+ * artifact has NONE of, and its `fail()` reports that the construct is
54
+ * present rather than measuring a value on it. Zero occurrences IS the
55
+ * measurement, so it **PASSes**: the loop is a search of the artifact, and
56
+ * the artifact is what it measured.
57
+ * * A name carrying both halves takes each at its word.
58
+ * `A15_IDLE_NO_MESH_BONE_KEYS` is the pattern and already reads this way: no
59
+ * `idle` animation, or an `idle` with no bone timeline, is the named subject
60
+ * absent and SKIPs; no mesh-driving bone among the bones `idle` does key is
61
+ * the construct absent and passes. `A10_NO_NAN_AFTER_STEPPING` was the same
62
+ * shape until issue #902, and is the next bullet's now: #882 gave it a
63
+ * setup-pose clause whose subject is the bones, beside the stepping clause
64
+ * whose subject is the animations.
65
+ * * An assertion with more than one clause SKIPs only when EVERY clause had
66
+ * nothing to measure, which is the shape `A09`, `A33` and `A38` already
67
+ * carry (`polygons.length === 0 && endsChecked === 0`), and `A10` since #902.
68
+ * `A13_MESH_BUDGET` is why the clause is stated: a declared slot budget is
69
+ * measured against a count of mesh slots, and zero is a count, so that half
70
+ * passes on a rig with no mesh — while a rig that declares only a TRIANGLE
71
+ * budget has nothing left to measure and skips.
72
+ *
73
+ * ⚠️ What the criterion is not allowed to become is a table of assertions with
74
+ * their verdicts written beside them. Every row above is decided by reading the
75
+ * name and the sentences the assertion already prints, so a rule added tomorrow
76
+ * is decided by the same reading and nobody re-opens this question.
77
+ */
78
+
79
+ /**
80
+ * What a SKIP says when the artifact carries no member of the subject a rule
81
+ * quantifies over (issue #580), by subject.
82
+ *
83
+ * One constant per SUBJECT rather than one per assertion, for the reason the two
84
+ * above are two: what the rule was denied is the whole content of its SKIP, and
85
+ * three rules denied the same thing should say so in the same words. Exported so
86
+ * a control compares against them rather than quoting them.
87
+ */
88
+ export const SKIP_NO_REGION_ATTACHMENT = 'the skeleton carries no region attachment';
89
+ export const SKIP_NO_MESH_ATTACHMENT = 'the skeleton carries no mesh attachment';
90
+ export const SKIP_NO_ANIMATION = 'the skeleton carries no animation';
91
+ /**
92
+ * A10's (issue #902): its setup clause reads the bones and its stepping clause
93
+ * the animations, so it skips only when the skeleton carries neither — and the
94
+ * sentence is `SKIP_NO_ANIMATION`'s with the second subject added, so the two
95
+ * cannot drift into different words for the same absence.
96
+ */
97
+ export const SKIP_NO_POSE = `${SKIP_NO_ANIMATION} and no bone, so there is no setup pose to read and nothing to step`;
98
+ export const SKIP_NO_TIMELINE = 'no animation here carries a timeline';
99
+ export const SKIP_NO_PHYSICS_CONSTRAINT = 'the skeleton declares no physics constraint';
100
+ export const SKIP_NO_ATLAS_PAGE = 'the atlas declares no page';
101
+ export const SKIP_NO_ATLAS_REGION = 'the atlas declares no region';
102
+ export const SKIP_NO_ATTACHMENT_REGION_JOIN =
103
+ 'no attachment names a region and the atlas declares none, so there is no attachment-to-region join to hold';
104
+ export const SKIP_NO_TWO_COLOR_TINT =
105
+ 'no slot declares a "dark" colour and no animation keys an "rgba2" or "rgb2" timeline, so there is no two-colour tint to read back';
106
+ export const SKIP_NO_SEPARABLE_COLOR =
107
+ 'no animation keys an "rgb" or "alpha" timeline, so there is no separable slot colour to read back';
108
+ export const SKIP_NO_LINKED_MESH = 'no attachment in this skeleton takes its geometry from another one';
109
+ export const SKIP_NO_SEQUENCE =
110
+ 'no attachment carries a "sequence" block and no animation keys a "sequence" timeline, so there is no numbered series to read back';
111
+ /**
112
+ * A09's, which predates this list and joins it rather than being rewritten: it
113
+ * is the same fact about the same subject, and a control that compares against
114
+ * eight constants and quotes the ninth is a control with a hand-kept exception
115
+ * in it.
116
+ */
117
+ export const SKIP_NO_DECLARED_DURATION =
118
+ 'the motion spec declares no animations and the skeleton has none — a static rig has no duration to compare';
119
+
120
+ /**
121
+ * The model side's `SKIP_NO_SKELETON` (issue #1025): the reader refused the
122
+ * document, or the document names a region its pages lack, so a body that
123
+ * reads the model has nothing to read. It names the two model-side parse
124
+ * rules rather than A00, because on this side there is no round trip — the
125
+ * reader and the region rule are what stand in its place (`./model/parse.ts`).
126
+ */
127
+ export const SKIP_NO_MODEL = 'the model side read no document to measure (A00_MODEL_READ or A00_MODEL_REGIONS_ON_PAGES owns that failure)';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The region names one skin entry will make a loader look up (issue #1025,
3
+ * step 4c of #380) — moved here from `src/validate.ts` unchanged, because A08's
4
+ * body now runs on both sides and the model side links nothing from the
5
+ * runtime. `src/validate.ts` re-exports both names, so nothing that imported
6
+ * them from there moves.
7
+ */
8
+ import { frameRegionName } from '../core/uvs.ts';
9
+ import { isObj } from './values.ts';
10
+
11
+ /**
12
+ * The atlas region names one raw skin entry will make the loader look up — or
13
+ * `null` when the file states a sequence this walk cannot predict.
14
+ *
15
+ * ⚠️ Measured against the loader rather than reasoned out, because a join that
16
+ * merely looks right is the thing A08 exists to refuse. `readSequence`
17
+ * (`dist/SkeletonJson.js:641-649`) turns an absent or null `sequence` into
18
+ * `new Sequence(1, false)` — one lookup, at the bare path — and a present one
19
+ * into `new Sequence(count ?? 0, true)`, so a sequence map with no `count`
20
+ * looks up nothing at all. Each frame's name is the core's `frameRegionName`
21
+ * (`src/core/uvs.ts`): `start + i`, left-padded with zeros to `digits`, after
22
+ * the path (issue #1015). `PS127` records what the loader asks for and compares,
23
+ * and the core suite holds the frame names to `Sequence.getPath` over 7,680
24
+ * cases.
25
+ *
26
+ * Nothing in `examples/` carries a `sequence` (measured: 0 occurrences across
27
+ * all twelve editor exports), so without this the assertion would have read a
28
+ * sequence's base path as a region name and refused correct foreign data —
29
+ * `A21_MESH_RIM_PINNED`'s old `|| 'ring'` default, one file over.
30
+ */
31
+ export function attachmentRegionLookups(sequence: unknown, path: string): string[] | null {
32
+ if (sequence === undefined || sequence === null) return [path];
33
+ if (!isObj(sequence)) return null;
34
+ const whole = (value: unknown, fallback: number): number | null => {
35
+ if (value === undefined) return fallback;
36
+ return typeof value === 'number' && Number.isInteger(value) ? value : null;
37
+ };
38
+ const count = whole(sequence.count, 0);
39
+ const start = whole(sequence.start, 1);
40
+ const digits = whole(sequence.digits, 0);
41
+ if (count === null || start === null || digits === null || count < 0) return null;
42
+ const series = { count, start, digits, setup: 0 };
43
+ const lookups: string[] = [];
44
+ for (let i = 0; i < count; i++) lookups.push(frameRegionName(path, series, i));
45
+ return lookups;
46
+ }
47
+
48
+ /** One skin entry's join onto the atlas, as the loader will perform it. */
49
+ export interface AttachmentRegionJoin {
50
+ skin: string;
51
+ slot: string;
52
+ placeholder: string;
53
+ /** The attachment's own name — the entry's `name` when it states one, else the placeholder. */
54
+ name: string;
55
+ /**
56
+ * Every atlas region name the loader will ask this atlas for, in the order it
57
+ * asks. Empty for a `sequence` with no `count`, and `null` for a sequence map
58
+ * this walk will not guess at.
59
+ */
60
+ lookups: string[] | null;
61
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * How an assertion body reads a value — the helpers the bodies under
3
+ * `./bodies/` share with the ones still in `src/validate.ts` (issue #1025,
4
+ * step 4c of #380), moved here unchanged so both read a value one way. Links
5
+ * nothing from the runtime.
6
+ */
7
+
8
+ /** A JSON object as `JSON.parse` returns it. */
9
+ export type Json = Record<string, unknown>;
10
+
11
+ /** A JSON object, and not an array or `null`. */
12
+ export function isObj(v: unknown): v is Json {
13
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
14
+ }
15
+
16
+ /**
17
+ * The time to pose a key at so that the runtime is AT it: the later of the
18
+ * file's number and that number as spine-core stores it (issue #771).
19
+ *
20
+ * 🚨 Every timeline keeps its key times in a `Float32Array`
21
+ * (`Utils.newFloatArray`), and a key time the float cannot hold exactly is
22
+ * stored at the nearest one — for `0.2`, `0.20000000298…`, which is LATER than
23
+ * the double `0.2`. Stepped to the file's own number, a timeline is then just
24
+ * BEFORE its key: before a first key it writes the setup value (`time <
25
+ * frames[0]`), and past a stepped key it still holds the one before
26
+ * (`frames[i] > time`). That is a pose one float step from the key, not the
27
+ * key's — measured on the selftest's own `ingest_probe`, whose `alpha` key at
28
+ * 0.2 posed the setup 1.0 and was refused as `the key states value 0.4`.
29
+ *
30
+ * ⚠️ The later of the two rather than `Math.fround` alone: where the float
31
+ * rounds DOWN, the file's number is already past the stored key, and a runtime
32
+ * built without typed arrays stores the double itself — in both, the file's
33
+ * number is the one at or after the key. What a key time rounds to is the
34
+ * runtime's storage and not the file's statement, so a rule judging what a KEY
35
+ * states poses at the key; the rounding itself is nothing an author can repair.
36
+ */
37
+ export function atStoredKey(time: number): number {
38
+ return Math.max(time, Math.fround(time));
39
+ }