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,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
|
+
}
|