spine-rigc 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +48 -17
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/bone_timelines.ts +56 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +34 -16
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/index.ts +83 -2
- package/src/assertions/model/linked_meshes.ts +2 -2
- package/src/assertions/model/mesh_attachments.ts +1 -1
- package/src/assertions/model/region_joins.ts +20 -12
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +18 -8
- package/src/assertions/model/skin_members.ts +5 -3
- package/src/assertions/model/skin_meshes.ts +1 -1
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/slot_colour.ts +5 -4
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +9 -8
- package/src/bonedist.ts +18 -6
- package/src/compile.ts +114 -14
- package/src/core/additive.ts +468 -0
- package/src/core/constraints.ts +12 -2
- package/src/core/constraints_physics.ts +31 -8
- package/src/core/raw.ts +77 -16
- package/src/core/walk.ts +202 -0
- package/src/deformmeasure.ts +35 -2270
- package/src/deformstructure.ts +60 -18
- package/src/deformsurvey.ts +2295 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +200 -59
- package/src/render_core.ts +5 -0
- package/src/validate.ts +791 -1558
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* - **Constraints**: the document's `constraints` in order — update order, the
|
|
9
9
|
* order the emitter writes them in — each skin-required exactly when it
|
|
10
10
|
* states `skin: true`, the field the emitter writes through unchanged.
|
|
11
|
-
* - **Skins**: in the
|
|
11
|
+
* - **Skins**: in the file's order (`fileSkinOrder`, `src/compile.ts`: a
|
|
12
|
+
* `rigc-compiled/3` document's `editorOrder`, else the emitter's
|
|
13
|
+
* `editorSkinOrder`, issue #1034); a
|
|
12
14
|
* skin's bones as it lists them, each the FIRST bone of that name; its
|
|
13
15
|
* constraints kind by kind in the emitter's key order
|
|
14
16
|
* (`RIG_SKIN_CONSTRAINT_KEYS`, which `emitSkins` walks), each the first
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
*
|
|
21
23
|
* Links nothing from the runtime.
|
|
22
24
|
*/
|
|
23
|
-
import {
|
|
25
|
+
import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
|
|
24
26
|
import { RIG_SKIN_CONSTRAINT_KEYS } from '../../rig.ts';
|
|
25
27
|
import type { MemberBone, MemberConstraint, MemberSkin, SkinMemberFacts } from '../facts/skin_members.ts';
|
|
26
28
|
import { isObj } from '../values.ts';
|
|
@@ -42,7 +44,7 @@ export function modelSkinMembers(read: ReadDocument): SkinMemberFacts {
|
|
|
42
44
|
for (const constraint of stated) {
|
|
43
45
|
constraints.push({ name: String(constraint.name), kind: String(constraint.kind), skinRequired: constraint.skin === true });
|
|
44
46
|
}
|
|
45
|
-
const skins: MemberSkin[] =
|
|
47
|
+
const skins: MemberSkin[] = skinsInFileOrder(doc.skins, fileSkinOrder(doc)).map(({ skin }) => {
|
|
46
48
|
const listedBones = skin.bones.map((name) => {
|
|
47
49
|
const found = boneByName.get(name);
|
|
48
50
|
if (found === undefined) throw new Error(`skin "${skin.name}" lists bone "${name}", which is not a bone of the document`);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* file's walk order.
|
|
5
5
|
*
|
|
6
6
|
* - **The order** is `fileOrderedEntries`'s (`./skin_entries.ts`): the skins by
|
|
7
|
-
* the
|
|
7
|
+
* the file's order (`fileSkinOrder`, issue #1034), the slots in the document's slot order, a
|
|
8
8
|
* slot's records in its table's order — the walk the selftest measures equal
|
|
9
9
|
* to spine-core's `getAttachments()`.
|
|
10
10
|
* - **A linked mesh's geometry is its source's**, read through the core's own
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `SliderCompositionFacts` (issue #1025, cut 4c-5
|
|
3
|
+
* of step 4c of #380): the document's sliders in its `constraints` order, the
|
|
4
|
+
* timelines of each animation they apply in the order the runtime builds
|
|
5
|
+
* them, and what each does when applied additively, as the core computes it.
|
|
6
|
+
*
|
|
7
|
+
* - **A slider's fields** are the core's record (`readSliderRecord`): `mix`,
|
|
8
|
+
* `additive` and `skin` read at the parser's value where the document
|
|
9
|
+
* leaves them out. The skins naming it are the document's skins in the
|
|
10
|
+
* file's order (`fileSkinOrder`), each whose `slider` list names it.
|
|
11
|
+
* - **The timelines, in the runtime's order** (`../facts/slider_composition.ts`'
|
|
12
|
+
* ⭐): the slot and bone timelines as the file states them (`fileSlotTimelines`,
|
|
13
|
+
* `fileBoneTimelines` — the emitter's own passes over the document's
|
|
14
|
+
* order), the constraint timelines in the order the runtime builds them
|
|
15
|
+
* (`modelConstraintTimelineSources`), the attachments' deform and sequence
|
|
16
|
+
* timelines at their three levels in the order the file keys them
|
|
17
|
+
* (`keyedOrder` over the document's order, deform before sequence —
|
|
18
|
+
* `emitAttachmentTimelines`' order), then the draw order and the events.
|
|
19
|
+
* - **The words.** Each timeline's class name and properties are the core's
|
|
20
|
+
* table (`ADDITIVE_APPLY`, `src/core/additive.ts`), the one value here that
|
|
21
|
+
* is not the document's, measured off the loaded timelines; an id's index is
|
|
22
|
+
* the target's position in the document's bones, slots or constraints —
|
|
23
|
+
* which the emitter writes in that order — `-1` for a physics timeline
|
|
24
|
+
* naming none; a deform's attachment is named as the runtime names it, the
|
|
25
|
+
* record's `name`, else its placeholder.
|
|
26
|
+
* - **The behaviour** is the core's probe over the document under no skin set
|
|
27
|
+
* (`noSkinView`, which refuses by name a document whose skins it was not
|
|
28
|
+
* measured over) and under each skin (`underSkin`): `additiveCells`, and the
|
|
29
|
+
* class `additiveBehaviour` reads off them. The core suite's `CO27` holds
|
|
30
|
+
* both to the runtime's probe; the selftest asks this supplier every
|
|
31
|
+
* question the body asks the runtime's (`VF14`).
|
|
32
|
+
*
|
|
33
|
+
* Links nothing from the runtime.
|
|
34
|
+
*/
|
|
35
|
+
import { fileSkinOrder, keyedOrder } from '../../compile.ts';
|
|
36
|
+
import { ADDITIVE_APPLY, additiveBehaviour, additiveCells, additiveSpelling, type AdditiveTimeline, type AdditiveView } from '../../core/additive.ts';
|
|
37
|
+
import { EVERY_GLOBAL_PHYSICS, type PhysicsTimelineKind } from '../../core/constraints_physics.ts';
|
|
38
|
+
import { underSkin, type CompiledDocument } from '../../core/index.ts';
|
|
39
|
+
import type { BoneTimelineKind, SlotTimelineKind } from '../../core/animation.ts';
|
|
40
|
+
import { noSkinView } from '../../render_core.ts';
|
|
41
|
+
import { entryAddress } from '../facts/sequences.ts';
|
|
42
|
+
import type { SliderCompositionFacts, SliderFact, SliderTimelineFact } from '../facts/slider_composition.ts';
|
|
43
|
+
import { modelConstraintTimelineSources } from './constraints.ts';
|
|
44
|
+
import { fileBoneTimelines } from './bone_timelines.ts';
|
|
45
|
+
import { fileSlotTimelines } from './slot_colour.ts';
|
|
46
|
+
import type { ReadDocument } from './parse.ts';
|
|
47
|
+
import { isObj } from '../values.ts';
|
|
48
|
+
|
|
49
|
+
/** One timeline of an animation: the facts' reading of it and the core's. */
|
|
50
|
+
export interface ModelSliderTimeline {
|
|
51
|
+
fact: SliderTimelineFact;
|
|
52
|
+
core: AdditiveTimeline;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Names in the order each first appears, put in the order a JSON object keyed by them lists them (`keyedOrder`). */
|
|
56
|
+
const keyedFirsts = (names: readonly string[]): string[] => keyedOrder(names.filter((name, i) => names.indexOf(name) === i));
|
|
57
|
+
|
|
58
|
+
/** The words of one timeline: the table's class and properties, each id addressed by `index` (and `attachment`), and the sentence's naming. */
|
|
59
|
+
function factOf(doc: CompiledDocument, core: AdditiveTimeline): SliderTimelineFact {
|
|
60
|
+
const row = ADDITIVE_APPLY[additiveSpelling(core)];
|
|
61
|
+
if (row === undefined) throw new Error(`internal: "${additiveSpelling(core)}" is no row of the additive table`);
|
|
62
|
+
const property = (p: string, index: number | null, attachment: string | null, names: string) => ({
|
|
63
|
+
id: [p, ...(index === null ? [] : [String(index)]), ...(attachment === null ? [] : [attachment])].join('|'),
|
|
64
|
+
property: p,
|
|
65
|
+
names,
|
|
66
|
+
});
|
|
67
|
+
const slotIndex = (name: string): number => doc.slots.findIndex((s) => s.name === name);
|
|
68
|
+
let properties: SliderTimelineFact['properties'];
|
|
69
|
+
switch (core.group) {
|
|
70
|
+
case 'bone': {
|
|
71
|
+
const index = doc.bones.findIndex((b) => b.name === core.bone);
|
|
72
|
+
properties = row.properties.map((p) => property(p, index, null, `bone "${core.bone}" ${p}`));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'slot':
|
|
76
|
+
properties = row.properties.map((p) => property(p, slotIndex(core.slot), null, `slot "${core.slot}" ${p}`));
|
|
77
|
+
break;
|
|
78
|
+
case 'attachment': {
|
|
79
|
+
const record = doc.skins.find((k) => k.name === core.skin)?.attachments[core.slot]?.[core.attachment];
|
|
80
|
+
const shown = record?.name ?? core.attachment;
|
|
81
|
+
const address = entryAddress(core.skin, core.slot, core.attachment);
|
|
82
|
+
properties = row.properties.map((p) => property(p, slotIndex(core.slot), address, core.word === 'deform' ? `slot "${core.slot}" deform of "${shown}"` : `slot "${core.slot}" ${p}`));
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case 'drawOrder':
|
|
86
|
+
case 'events':
|
|
87
|
+
properties = row.properties.map((p) => property(p, null, null, `the skeleton's ${p}`));
|
|
88
|
+
break;
|
|
89
|
+
default: {
|
|
90
|
+
const index = core.group === 'physics' && core.constraint === EVERY_GLOBAL_PHYSICS ? -1 : doc.constraints.findIndex((c) => c.kind === core.group && c.name === core.constraint);
|
|
91
|
+
properties = row.properties.map((p) => property(p, row.address === 'none' ? null : index, null, index >= 0 ? `constraint "${core.constraint}" ${p}` : `the skeleton's ${p}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { runtimeClass: row.runtimeClass, properties };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Every timeline of animation `name`, in the order the runtime builds them (the header). */
|
|
98
|
+
export function modelAnimationTimelines(read: ReadDocument, name: string): ModelSliderTimeline[] {
|
|
99
|
+
const { doc } = read;
|
|
100
|
+
const anim = doc.animations.find((a) => a.name === name);
|
|
101
|
+
if (anim === undefined) return [];
|
|
102
|
+
const t = anim.timelines;
|
|
103
|
+
const out: AdditiveTimeline[] = [];
|
|
104
|
+
for (const { slot, timelines } of fileSlotTimelines(read).filter((s) => s.animation === name)) {
|
|
105
|
+
const target = t.slots.find((s) => s.name === slot);
|
|
106
|
+
for (const word of Object.keys(timelines)) {
|
|
107
|
+
const keys = target?.timelines.find((x) => x.kind === word)?.keys;
|
|
108
|
+
if (keys !== undefined) out.push({ group: 'slot', word: word as SlotTimelineKind, slot, keys });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const { bone, timelines } of fileBoneTimelines(read).filter((b) => b.animation === name)) {
|
|
112
|
+
const target = t.bones.find((b) => b.name === bone);
|
|
113
|
+
for (const word of isObj(timelines) ? Object.keys(timelines) : []) {
|
|
114
|
+
const keys = target?.timelines.find((x) => x.kind === word)?.keys;
|
|
115
|
+
if (keys !== undefined) out.push({ group: 'bone', word: word as BoneTimelineKind, bone, keys });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const source of modelConstraintTimelineSources(read).filter((s) => s.animation === name)) {
|
|
119
|
+
switch (source.kind) {
|
|
120
|
+
case 'ik': {
|
|
121
|
+
const keys = anim.constraints.ik.find((x) => x.name === source.name)?.keys;
|
|
122
|
+
if (keys !== undefined) out.push({ group: 'ik', word: 'ik', constraint: source.name, keys });
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'transform':
|
|
126
|
+
out.push({ group: 'transform', word: 'transform', constraint: source.name, keys: source.keys });
|
|
127
|
+
break;
|
|
128
|
+
case 'path':
|
|
129
|
+
out.push({ group: 'path', word: source.word as 'position' | 'spacing' | 'mix', constraint: source.name, keys: source.keys });
|
|
130
|
+
break;
|
|
131
|
+
case 'physics':
|
|
132
|
+
out.push({ group: 'physics', word: source.word as PhysicsTimelineKind, constraint: source.name, keys: source.keys });
|
|
133
|
+
break;
|
|
134
|
+
case 'slider':
|
|
135
|
+
out.push({ group: 'slider', word: source.word as 'time' | 'mix', constraint: source.name, keys: source.keys });
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const attachments = t.attachments;
|
|
140
|
+
for (const skin of keyedFirsts(attachments.map((a) => a.skin))) {
|
|
141
|
+
const inSkin = attachments.filter((a) => a.skin === skin);
|
|
142
|
+
for (const slot of keyedFirsts(inSkin.map((a) => a.slot))) {
|
|
143
|
+
const inSlot = inSkin.filter((a) => a.slot === slot);
|
|
144
|
+
for (const attachment of keyedFirsts(inSlot.map((a) => a.attachment))) {
|
|
145
|
+
for (const a of inSlot.filter((x) => x.attachment === attachment)) {
|
|
146
|
+
if (a.deform !== null) out.push({ group: 'attachment', word: 'deform', skin, slot, attachment, keys: a.deform });
|
|
147
|
+
if (a.sequence !== null) out.push({ group: 'attachment', word: 'sequence', skin, slot, attachment, keys: a.sequence });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (t.drawOrder.length > 0) out.push({ group: 'drawOrder', word: 'drawOrder', keys: t.drawOrder });
|
|
153
|
+
if (t.events.length > 0) out.push({ group: 'events', word: 'events', times: t.events.map((k) => k.time) });
|
|
154
|
+
return out.map((core) => ({ fact: factOf(doc, core), core }));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The views the probe poses under: no skin set, then every skin in the file's order. */
|
|
158
|
+
export function additiveViews(doc: CompiledDocument): AdditiveView[] {
|
|
159
|
+
return [{ name: '(none)', doc: noSkinView(doc) }, ...fileSkinOrder(doc).map((skin) => ({ name: skin.name, doc: underSkin(doc, skin.name) }))];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function modelSliderComposition(read: ReadDocument): SliderCompositionFacts {
|
|
163
|
+
const { doc } = read;
|
|
164
|
+
const skinOrder = fileSkinOrder(doc).map((skin) => skin.name);
|
|
165
|
+
const walked = new Map<string, ModelSliderTimeline[]>();
|
|
166
|
+
const timelinesOf = (name: string): ModelSliderTimeline[] => {
|
|
167
|
+
const held = walked.get(name);
|
|
168
|
+
if (held !== undefined) return held;
|
|
169
|
+
const fresh = modelAnimationTimelines(read, name);
|
|
170
|
+
walked.set(name, fresh);
|
|
171
|
+
return fresh;
|
|
172
|
+
};
|
|
173
|
+
const sliders: SliderFact[] = [];
|
|
174
|
+
doc.constraints.forEach((c, index) => {
|
|
175
|
+
const r = c.record;
|
|
176
|
+
if (r?.kind !== 'slider') return;
|
|
177
|
+
const animation = doc.animations.some((a) => a.name === r.animation) ? { name: r.animation, timelines: timelinesOf(r.animation).map((x) => x.fact) } : null;
|
|
178
|
+
const skins = skinOrder.filter((name) => doc.skins.some((k) => k.name === name && k.constraints.slider.includes(r.name)));
|
|
179
|
+
sliders.push({ name: r.name, index, mix: r.mix, additive: r.additive, skinRequired: r.skin, skins, animation });
|
|
180
|
+
});
|
|
181
|
+
let views: AdditiveView[] | null = null;
|
|
182
|
+
return {
|
|
183
|
+
sliders,
|
|
184
|
+
behaviour: (animation, at) => {
|
|
185
|
+
const timeline = timelinesOf(animation)[at];
|
|
186
|
+
if (timeline === undefined) throw new Error(`internal: animation "${animation}" has no timeline ${at}`);
|
|
187
|
+
views ??= additiveViews(doc);
|
|
188
|
+
return additiveBehaviour(additiveCells(views, timeline.core));
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* holds each animation's slot timelines in the order they are applied, with
|
|
6
6
|
* every key as the compiler built it; the Spine file keys the animations in
|
|
7
7
|
* the editor's order and passes every key through two passes. So the
|
|
8
|
-
* animations are put in order by `
|
|
9
|
-
*
|
|
8
|
+
* animations are put in the file's order by `fileAnimationOrder`
|
|
9
|
+
* (`src/compile.ts`: a `rigc-compiled/3` document's `editorOrder`, else the
|
|
10
|
+
* emitter's own rule over its names, issue #1034), and the timelines are laid out as the
|
|
10
11
|
* file lays them and handed to the emitter's own `withoutParserDefaults` and
|
|
11
12
|
* `inEditorKeyOrder` (`src/keyorder.ts`) — so a key field the emitter would
|
|
12
13
|
* leave out at the parser's default is left out here too, and a body
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
*
|
|
27
28
|
* Links nothing from the runtime.
|
|
28
29
|
*/
|
|
29
|
-
import {
|
|
30
|
+
import { fileAnimationOrder } from '../../compile.ts';
|
|
30
31
|
import type { CompiledDocument } from '../../core/index.ts';
|
|
31
32
|
import { poseRawAnimation } from '../../core/raw.ts';
|
|
32
33
|
import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
|
|
@@ -42,7 +43,7 @@ export function fileSlotTimelines(read: ReadDocument): SlotTimelines[] {
|
|
|
42
43
|
const byName = new Map<string, Json>();
|
|
43
44
|
for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
|
|
44
45
|
const animations: Json = {};
|
|
45
|
-
for (const name of
|
|
46
|
+
for (const name of fileAnimationOrder(read.doc)) {
|
|
46
47
|
const slots: Json = {};
|
|
47
48
|
for (const slot of list((byName.get(name) as Json).slots)) {
|
|
48
49
|
if (typeof slot.name !== 'string') continue;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `SteppedPoseFacts` (issue #1025, cut 4c-5a of
|
|
3
|
+
* step 4c of #380): every pose A10 reads, posed by rigc's core.
|
|
4
|
+
*
|
|
5
|
+
* - **The roster**: the document's bones, and its animations in the file's
|
|
6
|
+
* order (`fileAnimationOrder`, `src/compile.ts`), each with
|
|
7
|
+
* `CoreAnimationTimelines.duration` — the last key of every timeline as
|
|
8
|
+
* float32, the value `tools/pose_oracle.ts compare` holds equal to
|
|
9
|
+
* spine-core's on its `animations` roster.
|
|
10
|
+
* - **The setup pose and each animation's walk**: the core's looping walk
|
|
11
|
+
* (`src/core/walk.ts` — `poseWalkSetup`, `poseLoopingWalk`), which reads
|
|
12
|
+
* A10's recipe as measured and returns a value that is not finite in place
|
|
13
|
+
* rather than refusing it. It is held equal to spine-core's walk at
|
|
14
|
+
* tolerance 0 by `tools/core_gate.ts`'s walk block and the selftest's
|
|
15
|
+
* `CO25`, and the poses A10 asks for here are compared with spine-core's on
|
|
16
|
+
* every call with a model in hand (`VF13`).
|
|
17
|
+
* - **A bone's mode**: the mode the core's bone timelines pose at the time
|
|
18
|
+
* (`posedBones`, a fresh non-looping track: the time held at the duration),
|
|
19
|
+
* folded as the runtime's lookup folds it (`foldInheritMode`). `readModel`
|
|
20
|
+
* refuses a setup `inheritMode` and an `inherit` key that fold to no mode by
|
|
21
|
+
* name, so on this side every bone of every pose holds one and the answer is
|
|
22
|
+
* `null` — derived from the pose rather than assumed, so a reader that
|
|
23
|
+
* started admitting such a spelling would show up here as a bone with none.
|
|
24
|
+
* - **Under no skin set**, as `validate()` poses (`noSkinView`), which refuses
|
|
25
|
+
* by name a document whose default skin it cannot read that way.
|
|
26
|
+
*
|
|
27
|
+
* Links nothing from the runtime.
|
|
28
|
+
*/
|
|
29
|
+
import { fileAnimationOrder } from '../../compile.ts';
|
|
30
|
+
import { foldInheritMode, type CompiledDocument } from '../../core/index.ts';
|
|
31
|
+
import { posedBones } from '../../core/animation.ts';
|
|
32
|
+
import { poseLoopingWalk, poseWalkSetup, type WalkPose } from '../../core/walk.ts';
|
|
33
|
+
import { noSkinView } from '../../render_core.ts';
|
|
34
|
+
import type { ModelBone } from '../../model.ts';
|
|
35
|
+
import type { SteppedFrame, SteppedPoseFacts } from '../facts/stepped_poses.ts';
|
|
36
|
+
import type { ReadDocument } from './parse.ts';
|
|
37
|
+
|
|
38
|
+
/** A bone's posed mode as the facts spell it: `null` for a mode, else the value held. */
|
|
39
|
+
const modeOf = (bone: ModelBone): string | null => (foldInheritMode(bone.inheritMode ?? 'normal') === null ? String(bone.inheritMode) : null);
|
|
40
|
+
|
|
41
|
+
/** A channel the walk computed; a slot row holds `null` only where a rounding wrote one, which the walk does not. */
|
|
42
|
+
const channel = (v: number | null): number => (v === null ? Number.NaN : v);
|
|
43
|
+
|
|
44
|
+
/** One walk pose as A10 reads it, each bone's mode taken from `posed` (the bones the timelines posed at the pose's time). */
|
|
45
|
+
function frameOf(pose: WalkPose, posed: readonly ModelBone[]): SteppedFrame {
|
|
46
|
+
const modes = new Map(posed.map((b) => [b.name, modeOf(b)]));
|
|
47
|
+
return {
|
|
48
|
+
bones: pose.bones.map((b) => ({ name: b.name, a: b.a, b: b.b, c: b.c, d: b.d, worldX: b.worldX, worldY: b.worldY, inherit: modes.get(b.name) ?? null })),
|
|
49
|
+
drawn: pose.drawn.map((d) => ({ slot: d.slot, attachment: d.attachment, vertices: d.vertices })),
|
|
50
|
+
slots: pose.slots.map((row) => ({
|
|
51
|
+
name: row[0],
|
|
52
|
+
colour: [channel(row[2]), channel(row[3]), channel(row[4]), channel(row[5])] as const,
|
|
53
|
+
dark: row[6] === null ? null : ([channel(row[6][0]), channel(row[6][1]), channel(row[6][2])] as const),
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function modelSteppedPoses(read: ReadDocument): SteppedPoseFacts {
|
|
59
|
+
let view: CompiledDocument | null = null;
|
|
60
|
+
const viewOf = (): CompiledDocument => (view ??= noSkinView(read.doc));
|
|
61
|
+
const animationOf = (name: string) => {
|
|
62
|
+
const anim = viewOf().animations.find((a) => a.name === name);
|
|
63
|
+
if (anim === undefined) throw new Error(`internal: the model side was asked to pose animation "${name}", which the document does not hold`);
|
|
64
|
+
return anim;
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
boneCount: read.doc.bones.length,
|
|
68
|
+
steppedAnimations: fileAnimationOrder(read.doc).flatMap((name) => {
|
|
69
|
+
const anim = read.doc.animations.find((a) => a.name === name);
|
|
70
|
+
return anim === undefined ? [] : [{ name, duration: anim.timelines.duration }];
|
|
71
|
+
}),
|
|
72
|
+
hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
|
|
73
|
+
hasBone: (name) => read.doc.bones.some((b) => b.name === name),
|
|
74
|
+
posedInherit: (animation, bone, time) => {
|
|
75
|
+
const anim = animationOf(animation);
|
|
76
|
+
const t = time < anim.timelines.duration ? time : anim.timelines.duration;
|
|
77
|
+
const posed = posedBones(viewOf(), anim.timelines, t).find((b) => b.name === bone);
|
|
78
|
+
return posed === undefined ? undefined : modeOf(posed);
|
|
79
|
+
},
|
|
80
|
+
// The document's stated mode — asked only of a bone posing none, which the reader leaves this side no document to hold.
|
|
81
|
+
statedInherit: (name) => `${JSON.stringify(read.doc.bones.find((b) => b.name === name)?.inheritMode)}`,
|
|
82
|
+
setup: () => frameOf(poseWalkSetup(viewOf()), viewOf().bones),
|
|
83
|
+
walk: (animation, step, frames) => {
|
|
84
|
+
const anim = animationOf(animation);
|
|
85
|
+
const poses = poseLoopingWalk(viewOf(), animation, new Array<number>(frames).fill(step));
|
|
86
|
+
return poses.slice(1).map((pose) => frameOf(pose, posedBones(viewOf(), anim.timelines, pose.animationTime)));
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `TwoColourFacts` (issue #1025, cut 4c-3 of step
|
|
3
|
+
* 4c of #380).
|
|
4
|
+
*
|
|
5
|
+
* - **The stated dark colours**: the document's slots in their order — the
|
|
6
|
+
* emitted slot array's — each `dark` as the document holds it, which the
|
|
7
|
+
* emitter writes into the slot unchanged (`emitSlot`). `readModel` refuses
|
|
8
|
+
* a `dark` that is not six or eight hex digits, so a spelling the runtime
|
|
9
|
+
* would load as NaN or drop has no document to arrive in.
|
|
10
|
+
* - **The loaded dark colour**: the core's reading of that hex (`readColour`,
|
|
11
|
+
* its first three channels) — the reading the setup pose's slot rows carry,
|
|
12
|
+
* which `tools/pose_oracle.ts compare` holds equal to spine-core's on its
|
|
13
|
+
* `setup.slots` block, on the grid and under `--raw`.
|
|
14
|
+
* - **The slot timelines**: `fileSlotTimelines` (`./slot_colour.ts`), the walk
|
|
15
|
+
* A45 reads — the file's order and spelling, through the emitter's own
|
|
16
|
+
* passes.
|
|
17
|
+
* - **A slot's posed colours**: the core's raw entry (`poseRawAnimation`,
|
|
18
|
+
* `src/core/raw.ts`) taking one step of the time on a fresh track, under the
|
|
19
|
+
* view the core poses with no skin set (`noSkinView`), as A45's posed colour
|
|
20
|
+
* is taken — the slot row's light channels and its dark colour, NaN where the
|
|
21
|
+
* row reads no number. No gate samples the times A43 asks for (a key's own
|
|
22
|
+
* time), so the selftest compares these values with spine-core's at every
|
|
23
|
+
* time the body asks, on every call with a model in hand (`VF12`).
|
|
24
|
+
*
|
|
25
|
+
* Links nothing from the runtime.
|
|
26
|
+
*/
|
|
27
|
+
import { readColour, type CompiledDocument } from '../../core/index.ts';
|
|
28
|
+
import { poseRawAnimation } from '../../core/raw.ts';
|
|
29
|
+
import { noSkinView } from '../../render_core.ts';
|
|
30
|
+
import type { DarkColour, TwoColourFacts } from '../facts/two_colour.ts';
|
|
31
|
+
import type { ReadDocument } from './parse.ts';
|
|
32
|
+
import { fileSlotTimelines } from './slot_colour.ts';
|
|
33
|
+
|
|
34
|
+
/** A channel the pose read as no number, as a non-finite channel reads on the runtime's side. */
|
|
35
|
+
const channel = (v: number | null): number => (v === null ? Number.NaN : v);
|
|
36
|
+
|
|
37
|
+
export function modelTwoColour(read: ReadDocument): TwoColourFacts {
|
|
38
|
+
let view: CompiledDocument | null = null;
|
|
39
|
+
const slotDarks = read.doc.slots.flatMap((slot) => (typeof slot.dark === 'string' ? [{ slot: slot.name, dark: slot.dark }] : []));
|
|
40
|
+
return {
|
|
41
|
+
slotDarks,
|
|
42
|
+
loadedDark: (name): DarkColour | null => {
|
|
43
|
+
const dark = read.doc.slots.find((s) => s.name === name)?.dark;
|
|
44
|
+
if (dark === undefined) return null;
|
|
45
|
+
const [r, g, b] = readColour(dark);
|
|
46
|
+
return { r, g, b };
|
|
47
|
+
},
|
|
48
|
+
slotTimelines: fileSlotTimelines(read),
|
|
49
|
+
hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
|
|
50
|
+
posedTint: (animation, slot, time) => {
|
|
51
|
+
view ??= noSkinView(read.doc);
|
|
52
|
+
const row = poseRawAnimation(view, animation, [time], {}, 'animation')[1].slots.find((r) => r[0] === slot);
|
|
53
|
+
if (row === undefined) return undefined;
|
|
54
|
+
const dark = row[6];
|
|
55
|
+
return {
|
|
56
|
+
light: { r: channel(row[2]), g: channel(row[3]), b: channel(row[4]), a: channel(row[5]) },
|
|
57
|
+
dark: dark === null ? null : { r: channel(dark[0]), g: channel(dark[1]), b: channel(dark[2]) },
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* (`readVertices`); the selftest compares it with spine-core's on every
|
|
11
11
|
* call. A path's `closed` and `lengths` are the core's reading of the record
|
|
12
12
|
* (`readGeometry`: `closed` absent reads the parser's `false`).
|
|
13
|
-
* - **The clip ends** in the order the file spells them: the skins
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
13
|
+
* - **The clip ends** in the order the file spells them: the skins and each
|
|
14
|
+
* skin's slot keys in the order `fileSkinOrder` gives (`src/compile.ts`,
|
|
15
|
+
* issue #1034: a `rigc-compiled/3` document's `editorOrder`, else the
|
|
16
|
+
* emitter's `editorSkinOrder` and `editorSlotKeyOrder`, the functions
|
|
17
|
+
* `emitSkins` is handed), each slot's records in table order — every clipping record that
|
|
17
18
|
* states an `end`, with the `end` as it states it.
|
|
18
19
|
*
|
|
19
20
|
* `encoding` is empty on every polygon: the document states the weighted
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
*
|
|
22
23
|
* Links nothing from the runtime.
|
|
23
24
|
*/
|
|
24
|
-
import {
|
|
25
|
+
import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
|
|
25
26
|
import { shownRow } from '../../core/index.ts';
|
|
26
27
|
import type { ClipEnd, PolygonEntry, PolygonFacts } from '../facts/vertex_polygons.ts';
|
|
27
28
|
import { isObj, type Json } from '../values.ts';
|
|
@@ -40,9 +41,9 @@ export function documentRecord(read: ReadDocument, skin: string, slot: string, p
|
|
|
40
41
|
/** 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
42
|
export function fileSpelledEntries(read: ReadDocument): Array<{ skin: string; slot: string; placeholder: string; record: Json }> {
|
|
42
43
|
const out: Array<{ skin: string; slot: string; placeholder: string; record: Json }> = [];
|
|
43
|
-
for (const skin of
|
|
44
|
-
for (const
|
|
45
|
-
for (const placeholder of Object.keys(
|
|
44
|
+
for (const { skin, slots } of skinsInFileOrder(read.doc.skins, fileSkinOrder(read.doc))) {
|
|
45
|
+
for (const slot of slots) {
|
|
46
|
+
for (const placeholder of Object.keys(skin.attachments[slot] ?? {})) out.push({ skin: skin.name, slot, placeholder, record: documentRecord(read, skin.name, slot, placeholder) });
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
return out;
|
package/src/bonedist.ts
CHANGED
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
*/
|
|
78
78
|
import { readFileSync } from 'node:fs';
|
|
79
79
|
import {
|
|
80
|
-
|
|
80
|
+
loadPosedSkeleton,
|
|
81
81
|
PROTOCOL_FPS,
|
|
82
82
|
sampleAnimation,
|
|
83
83
|
sampleSetupPose,
|
|
@@ -184,7 +184,7 @@ export interface SkeletonSize {
|
|
|
184
184
|
degenerate: boolean;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function skeletonSize(posable: Posable): SkeletonSize {
|
|
187
|
+
function skeletonSize(posable: Pick<Posable, 'data'>): SkeletonSize {
|
|
188
188
|
const setup = sampleSetupPose(posable.data, { bones: true }).at(0);
|
|
189
189
|
const bones = setup?.bones ?? [];
|
|
190
190
|
const rootData = posable.data.bones.find((b) => b.parent === null) ?? posable.data.bones.at(0);
|
|
@@ -338,10 +338,17 @@ export interface BoneDistReport {
|
|
|
338
338
|
export interface BoneDistOptions {
|
|
339
339
|
candidateSkeleton: string;
|
|
340
340
|
candidateAtlas: string;
|
|
341
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Where the candidate's atlas pages are. Not read since issue #1042: no
|
|
343
|
+
* figure here samples a pixel, so no page is opened, and a pair whose pages
|
|
344
|
+
* are elsewhere measures the same. Kept optional so a caller that passes it
|
|
345
|
+
* still compiles.
|
|
346
|
+
*/
|
|
347
|
+
candidateAtlasDir?: string;
|
|
342
348
|
referenceSkeleton: string;
|
|
343
349
|
referenceAtlas: string;
|
|
344
|
-
|
|
350
|
+
/** The reference's, as `candidateAtlasDir` — not read. */
|
|
351
|
+
referenceAtlasDir?: string;
|
|
345
352
|
/** A correspondence file path, or `identity`. */
|
|
346
353
|
bones: string;
|
|
347
354
|
fps?: number;
|
|
@@ -398,8 +405,13 @@ function extremesOf(values: number[]): Extremes {
|
|
|
398
405
|
export function boneDistance(options: BoneDistOptions): BoneDistReport {
|
|
399
406
|
const fps = options.fps ?? PROTOCOL_FPS;
|
|
400
407
|
if (!Number.isFinite(fps) || fps <= 0) throw new BoneDistError('fps must be a positive number');
|
|
401
|
-
|
|
402
|
-
|
|
408
|
+
// Each side through spine-core, refused by name where its skeleton does not load against its atlas (issue #1042),
|
|
409
|
+
// and without its page images: nothing here samples a pixel (`loadPosedSkeleton`).
|
|
410
|
+
const posedFor = (side: 'candidate' | 'reference', skeleton: string, atlas: string): Pick<Posable, 'data'> => ({
|
|
411
|
+
data: loadPosedSkeleton(skeleton, atlas, `bonedist's ${side}: every bone's world transform is read off spine-core's pose of it`),
|
|
412
|
+
});
|
|
413
|
+
const candidate = posedFor('candidate', options.candidateSkeleton, options.candidateAtlas);
|
|
414
|
+
const reference = posedFor('reference', options.referenceSkeleton, options.referenceAtlas);
|
|
403
415
|
|
|
404
416
|
const candidateBoneNames = candidate.data.bones.map((b) => b.name);
|
|
405
417
|
const referenceBoneNames = new Set(reference.data.bones.map((b) => b.name));
|