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
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
* runtime's), and the timelines it carries are the bone and slot timelines
|
|
20
20
|
* plus the count of every later group the reader counts.
|
|
21
21
|
* - **The timelines**: the animations in the file's order
|
|
22
|
-
* (`
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* (`fileAnimationOrder`, issue #1034: a `rigc-compiled/3` document's
|
|
23
|
+
* `editorOrder`, else the emitter's own rule over its names); in each the
|
|
24
|
+
* groups in the runtime's order (`../facts/constraints.ts`' ⭐), each
|
|
25
|
+
* group's entries in the order the file keys them — the document's, an
|
|
26
|
+
* integer-like constraint name first (`keyedOrder`) — and each entry's
|
|
27
|
+
* timelines in the document's order, which the emitter writes unchanged. A key's time and value are the core's (float32), and a
|
|
26
28
|
* Bézier segment's samples `bezierPolyline` over the numbers the document
|
|
27
29
|
* states — the runtime's curve, as the core reproduces it.
|
|
28
30
|
* - **The reach of a physics timeline naming none** is asked of the core's
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
*
|
|
40
42
|
* Links nothing from the runtime.
|
|
41
43
|
*/
|
|
42
|
-
import {
|
|
44
|
+
import { fileAnimationOrder, keyedOrder, type OrderedDocument } from '../../compile.ts';
|
|
43
45
|
import { bezierPolyline, type CoreAnimationTimelines, type CoreKey } from '../../core/animation.ts';
|
|
44
46
|
import { TRANSFORM_MIXES, TRANSFORM_PROPERTIES, type CoreConstraintRecord } from '../../core/constraints.ts';
|
|
45
47
|
import { EVERY_GLOBAL_PHYSICS, posedPhysics, type CorePhysicsRecord, type PhysicsTimelineKind } from '../../core/constraints_physics.ts';
|
|
@@ -144,39 +146,55 @@ export interface ModelConstraintTimelineSource {
|
|
|
144
146
|
keys: readonly CoreKey[];
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
/** How the animations are put in the file's order: `fileAnimationOrder`. A parameter only so the selftest can show what another order would print (`VF11`). */
|
|
150
|
+
export type AnimationFileOrder = (doc: OrderedDocument) => readonly string[];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `entries` in the order the Spine file keys them by name (issue #1034): each
|
|
154
|
+
* constraint group of an animation is an object the emitter fills in the
|
|
155
|
+
* model's order, so the names are grouped as they first appear and put
|
|
156
|
+
* through `keyedOrder` — an integer-like name first — and every entry of one
|
|
157
|
+
* name keeps its place among them.
|
|
158
|
+
*/
|
|
159
|
+
function inKeyedOrder<T extends { name: string }>(entries: readonly T[]): T[] {
|
|
160
|
+
const names = entries.map((e) => e.name);
|
|
161
|
+
return keyedOrder(names.filter((n, i) => names.indexOf(n) === i)).flatMap((name) => entries.filter((e) => e.name === name));
|
|
162
|
+
}
|
|
163
|
+
|
|
147
164
|
/** Every constraint timeline of the document, in the order the runtime builds them (the header) — the walk `modelConstraintFacts` reads, and the selftest's measurement of each derivation. */
|
|
148
|
-
export function modelConstraintTimelineSources(read: ReadDocument): ModelConstraintTimelineSource[] {
|
|
165
|
+
export function modelConstraintTimelineSources(read: ReadDocument, animations: AnimationFileOrder = fileAnimationOrder): ModelConstraintTimelineSource[] {
|
|
149
166
|
const { doc } = read;
|
|
150
167
|
const jsonAnimations = Array.isArray(read.json.animations) ? read.json.animations.filter(isObj) : [];
|
|
151
168
|
const out: ModelConstraintTimelineSource[] = [];
|
|
152
|
-
for (const name of
|
|
169
|
+
for (const name of animations(doc)) {
|
|
153
170
|
const anim = doc.animations.find((a) => a.name === name);
|
|
154
171
|
if (anim === undefined) continue;
|
|
155
172
|
const c = anim.constraints;
|
|
156
|
-
for (const tl of c.ik) out.push({ animation: name, kind: 'ik', word: 'ik', name: tl.name, keys: tl.keys });
|
|
157
|
-
for (const tl of c.transform) out.push({ animation: name, kind: 'transform', word: 'transform', name: tl.name, keys: tl.keys });
|
|
158
|
-
for (const entry of c.path) {
|
|
173
|
+
for (const tl of inKeyedOrder(c.ik)) out.push({ animation: name, kind: 'ik', word: 'ik', name: tl.name, keys: tl.keys });
|
|
174
|
+
for (const tl of inKeyedOrder(c.transform)) out.push({ animation: name, kind: 'transform', word: 'transform', name: tl.name, keys: tl.keys });
|
|
175
|
+
for (const entry of inKeyedOrder(c.path)) {
|
|
159
176
|
for (const word of Object.keys(entry)) {
|
|
160
177
|
if (word === 'position' || word === 'spacing' || word === 'mix') out.push({ animation: name, kind: 'path', word, name: entry.name, keys: entry[word] ?? [] });
|
|
161
178
|
}
|
|
162
179
|
}
|
|
163
|
-
for (const tl of c.physicsKeyed ?? []) out.push({ animation: name, kind: 'physics', word: tl.kind, name: tl.name, keys: tl.keys });
|
|
180
|
+
for (const tl of inKeyedOrder(c.physicsKeyed ?? [])) out.push({ animation: name, kind: 'physics', word: tl.kind, name: tl.name, keys: tl.keys });
|
|
164
181
|
// A slider entry's timelines in the order the document lists them, which `readSliderTimelines` keeps by name only.
|
|
165
182
|
const jsonSliders = ((): Json[] => {
|
|
166
183
|
const json = jsonAnimations.find((a) => a.name === name)?.constraints;
|
|
167
184
|
return isObj(json) && Array.isArray(json.slider) ? json.slider.filter(isObj) : [];
|
|
168
185
|
})();
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
// Paired with the document's own entry by index before the walk is put in the file's order.
|
|
187
|
+
const sliders = c.slider.map((entry, i) => ({ name: entry.name, entry, listed: Array.isArray(jsonSliders[i]?.timelines) ? (jsonSliders[i].timelines as unknown[]).filter(isObj).map((t) => t.name) : [] }));
|
|
188
|
+
for (const { entry, listed } of inKeyedOrder(sliders)) {
|
|
171
189
|
for (const word of listed) {
|
|
172
190
|
if (word === 'time' || word === 'mix') out.push({ animation: name, kind: 'slider', word, name: entry.name, keys: entry[word] ?? [] });
|
|
173
191
|
}
|
|
174
|
-
}
|
|
192
|
+
}
|
|
175
193
|
}
|
|
176
194
|
return out;
|
|
177
195
|
}
|
|
178
196
|
|
|
179
|
-
export function modelConstraintFacts(read: ReadDocument): ConstraintFacts {
|
|
197
|
+
export function modelConstraintFacts(read: ReadDocument, animations: AnimationFileOrder = fileAnimationOrder): ConstraintFacts {
|
|
180
198
|
const { doc } = read;
|
|
181
199
|
const constraints: ConstraintEntry[] = doc.constraints.map((c) => {
|
|
182
200
|
if (c.record === undefined) throw new Error(`internal: ${c.kind} constraint "${c.name}" is one the reader returned without its record`);
|
|
@@ -184,7 +202,7 @@ export function modelConstraintFacts(read: ReadDocument): ConstraintFacts {
|
|
|
184
202
|
});
|
|
185
203
|
const indexOf = (kind: ConstraintKind, name: string): number => doc.constraints.findIndex((c) => c.kind === kind && c.name === name);
|
|
186
204
|
const physicsRecords = doc.constraints.flatMap((c) => (c.record?.kind === 'physics' ? [c.record] : []));
|
|
187
|
-
const timelines: ConstraintTimeline[] = modelConstraintTimelineSources(read).map(({ animation, kind, word, name, keys }) => {
|
|
205
|
+
const timelines: ConstraintTimeline[] = modelConstraintTimelineSources(read, animations).map(({ animation, kind, word, name, keys }) => {
|
|
188
206
|
const constraint = name === EVERY_GLOBAL_PHYSICS && kind === 'physics' ? -1 : indexOf(kind, name);
|
|
189
207
|
const reach = constraint >= 0 ? [constraint] : [...unnamedReach(physicsRecords, word as PhysicsTimelineKind)].map((n) => indexOf('physics', n)).sort((a, b) => a - b);
|
|
190
208
|
const rule = kind === 'physics' ? physicsRuleFor(word) : undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `DeformSurveyFacts` (issue #1025, cut 4c-3 of
|
|
3
|
+
* step 4c of #380): the survey off the model document alone —
|
|
4
|
+
* `surveyOfModel` (`src/deformsurvey.ts`), the document's structure read by
|
|
5
|
+
* `modelStructure` (`src/deformstructure.ts`) and every pose taken by the
|
|
6
|
+
* core's poser. It is the survey `explain` takes of a build that carries a
|
|
7
|
+
* model document (`surveyOfBuild`'s model path) and the one
|
|
8
|
+
* `tools/survey_hashes.ts` holds byte for byte to spine-core's on every corpus
|
|
9
|
+
* row; called, not restated.
|
|
10
|
+
*
|
|
11
|
+
* A pose the core refuses — a construct it leaves out, a value it computes as
|
|
12
|
+
* no number — is a `CoreInputError` naming why, and it reaches A39 as the
|
|
13
|
+
* refusal: the line the harness prints for a body that threw, the core's
|
|
14
|
+
* sentence in it. A survey that is not the runtime's is never handed on.
|
|
15
|
+
*
|
|
16
|
+
* Links nothing from the runtime.
|
|
17
|
+
*/
|
|
18
|
+
import { surveyOfModel } from '../../deformsurvey.ts';
|
|
19
|
+
import type { DeformSurveyFacts } from '../facts/deform_survey.ts';
|
|
20
|
+
import type { ReadDocument } from './parse.ts';
|
|
21
|
+
|
|
22
|
+
export function modelDeformSurvey(read: ReadDocument): DeformSurveyFacts {
|
|
23
|
+
return { survey: (exempt) => surveyOfModel(read.doc, exempt) };
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `EventKeyFacts` (issue #1025, cut 4c-4 of #380):
|
|
3
|
+
* every event key of the document, with the declaration it fires.
|
|
4
|
+
*
|
|
5
|
+
* - **The order is the file's.** The animations by `editorAnimationOrder`
|
|
6
|
+
* (`src/compile.ts`, the function the emitter is handed), and an animation's
|
|
7
|
+
* keys in the document's order, which is the order `emitAnimation` writes
|
|
8
|
+
* the model's list in; an animation with no event key writes no `events`
|
|
9
|
+
* group, so it carries no event timeline here either.
|
|
10
|
+
* - **The moved clause's inputs.** Which of `volume` and `balance` the key
|
|
11
|
+
* states — the document's key carries the fields the file's does, and the
|
|
12
|
+
* parser-default pass leaves an event key's audio fields alone (its row is
|
|
13
|
+
* `time` only) — and whether the declaration names an audio path, the
|
|
14
|
+
* `audio` the document's `events` section states for that name.
|
|
15
|
+
* - **No `kept` finding, ever.** The four clauses that stay with the round
|
|
16
|
+
* trip are states `readModel` refuses (`readEventKeys`), so on a document
|
|
17
|
+
* this side has read none of them can hold.
|
|
18
|
+
*
|
|
19
|
+
* Links nothing from the runtime.
|
|
20
|
+
*/
|
|
21
|
+
import { editorAnimationOrder } from '../../compile.ts';
|
|
22
|
+
import type { EventKeyEntry, EventKeyFacts } from '../facts/event_keys.ts';
|
|
23
|
+
import { isObj, type Json } from '../values.ts';
|
|
24
|
+
import type { ReadDocument } from './parse.ts';
|
|
25
|
+
|
|
26
|
+
const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
|
|
27
|
+
|
|
28
|
+
export function modelEventKeys(read: ReadDocument): EventKeyFacts {
|
|
29
|
+
const audio = new Map<string, boolean>();
|
|
30
|
+
for (const def of list(read.json.events)) if (typeof def.name === 'string') audio.set(def.name, typeof def.audio === 'string');
|
|
31
|
+
const byName = new Map<string, Json>();
|
|
32
|
+
for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
|
|
33
|
+
const keys: EventKeyEntry[] = [];
|
|
34
|
+
let timelines = 0;
|
|
35
|
+
for (const animation of editorAnimationOrder([...byName.keys()])) {
|
|
36
|
+
const events = list((byName.get(animation) as Json).events);
|
|
37
|
+
if (events.length === 0) continue;
|
|
38
|
+
timelines++;
|
|
39
|
+
events.forEach((key, index) => {
|
|
40
|
+
const name = String(key.name);
|
|
41
|
+
keys.push({ animation, index, kept: [], stopped: false, name, sets: { volume: key.volume !== undefined, balance: key.balance !== undefined }, audio: audio.get(name) ?? false });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { parsed: true, animations: true, timelines, keys };
|
|
45
|
+
}
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
* the body inside its own `check`; this entry supplies them from the document
|
|
10
10
|
* (`./parse.ts`, `./skin_entries.ts`, `./atlas_pages.ts`, `./slot_colour.ts`;
|
|
11
11
|
* cut 4c-2's `./mesh_attachments.ts`, `./vertex_polygons.ts`,
|
|
12
|
-
* `./linked_meshes.ts`, `./constraints.ts
|
|
12
|
+
* `./linked_meshes.ts`, `./constraints.ts`; cut 4c-4's
|
|
13
|
+
* `./skeleton_roster.ts`, `./bone_timelines.ts`, `./event_keys.ts`; cut
|
|
14
|
+
* 4c-3's `./deform_survey.ts`, `./animation_durations.ts`, `./two_colour.ts`,
|
|
15
|
+
* `./sequences.ts`; cut 4c-5's `./slider_composition.ts`,
|
|
16
|
+
* `./constraint_targets.ts`; cut 4c-5a's `./stepped_poses.ts`)
|
|
13
17
|
* and calls the same body inside the same harness (`../harness.ts`). The
|
|
14
18
|
* selftest holds the two to the same lines on every call it makes with a
|
|
15
19
|
* model in hand, and `tools/verdict_gate.ts` on every recipe.
|
|
@@ -88,6 +92,40 @@ import type { MeshFacts } from '../facts/mesh_attachments.ts';
|
|
|
88
92
|
import type { PolygonFacts } from '../facts/vertex_polygons.ts';
|
|
89
93
|
import type { LinkFacts } from '../facts/linked_meshes.ts';
|
|
90
94
|
import type { ConstraintFacts } from '../facts/constraints.ts';
|
|
95
|
+
import { a12NoDarkColor } from '../bodies/a12.ts';
|
|
96
|
+
import { a24AxisSpaceStroke } from '../bodies/a24.ts';
|
|
97
|
+
import { a25DetachedBoneParentage } from '../bodies/a25.ts';
|
|
98
|
+
import { a26SlotDrawOrder } from '../bodies/a26.ts';
|
|
99
|
+
import { a29StrokeWithinContactDepth } from '../bodies/a29.ts';
|
|
100
|
+
import { a30StrokeWithinCapContainment } from '../bodies/a30.ts';
|
|
101
|
+
import { a32EventKeysResolve } from '../bodies/a32.ts';
|
|
102
|
+
import { modelSkeletonRoster } from './skeleton_roster.ts';
|
|
103
|
+
import { modelBoneTimelines } from './bone_timelines.ts';
|
|
104
|
+
import { modelEventKeys } from './event_keys.ts';
|
|
105
|
+
import type { SkeletonRosterFacts } from '../facts/skeleton_roster.ts';
|
|
106
|
+
import type { BoneTimelineFacts } from '../facts/bone_timelines.ts';
|
|
107
|
+
import type { EventKeyFacts } from '../facts/event_keys.ts';
|
|
108
|
+
import { a39DeformKeepsTriangleWinding } from '../bodies/a39.ts';
|
|
109
|
+
import { a09AnimationDurationMatchesSpec } from '../bodies/a09.ts';
|
|
110
|
+
import { a43TwoColorTintLoadsAndPosesAsWritten } from '../bodies/a43.ts';
|
|
111
|
+
import { a46SequenceAttachmentsShowTheFrameTheFileStates } from '../bodies/a46.ts';
|
|
112
|
+
import { modelDeformSurvey } from './deform_survey.ts';
|
|
113
|
+
import { modelAnimationDurations } from './animation_durations.ts';
|
|
114
|
+
import { modelTwoColour } from './two_colour.ts';
|
|
115
|
+
import { modelSequences } from './sequences.ts';
|
|
116
|
+
import type { DeformSurveyFacts } from '../facts/deform_survey.ts';
|
|
117
|
+
import type { AnimationDurationFacts } from '../facts/animation_durations.ts';
|
|
118
|
+
import type { TwoColourFacts } from '../facts/two_colour.ts';
|
|
119
|
+
import type { SequenceFacts } from '../facts/sequences.ts';
|
|
120
|
+
import { a40SlidersComposeOnASharedTarget } from '../bodies/a40.ts';
|
|
121
|
+
import { modelSliderComposition } from './slider_composition.ts';
|
|
122
|
+
import type { SliderCompositionFacts } from '../facts/slider_composition.ts';
|
|
123
|
+
import { a34ConstraintTimelineTargets } from '../bodies/a34.ts';
|
|
124
|
+
import { modelConstraintTargets } from './constraint_targets.ts';
|
|
125
|
+
import type { ConstraintTargetFacts } from '../facts/constraint_targets.ts';
|
|
126
|
+
import { a10NoNanAfterStepping } from '../bodies/a10.ts';
|
|
127
|
+
import { modelSteppedPoses } from './stepped_poses.ts';
|
|
128
|
+
import type { SteppedPoseFacts } from '../facts/stepped_poses.ts';
|
|
91
129
|
|
|
92
130
|
/** What the model side is given. */
|
|
93
131
|
export interface ModelValidateInput {
|
|
@@ -96,7 +134,7 @@ export interface ModelValidateInput {
|
|
|
96
134
|
/** The directory the document's page names resolve against — the build's `--out`. */
|
|
97
135
|
atlasDir: string;
|
|
98
136
|
profile: AssertionProfile;
|
|
99
|
-
/** The rig info the build carries, as `validate()` is handed it — A13, A15, A19 (cut 4c-1) and A20, A21, A28, A41, A47, A48 (cut 4c-2) read it; optional because `ValidateInput.rig` is: absent for a bare directory, on both sides. */
|
|
137
|
+
/** The rig info the build carries, as `validate()` is handed it — A13, A15, A19 (cut 4c-1) and A20, A21, A28, A41, A47, A48 (cut 4c-2) and A24, A25, A26, A29, A30 (cut 4c-4) read it; optional because `ValidateInput.rig` is: absent for a bare directory, on both sides. */
|
|
100
138
|
rig?: RigInfo;
|
|
101
139
|
/**
|
|
102
140
|
* The stage and the pages' `pma`, for a `rigc-compiled/2` or `/1` document,
|
|
@@ -106,6 +144,8 @@ export interface ModelValidateInput {
|
|
|
106
144
|
* `given` beside it is refused by name.
|
|
107
145
|
*/
|
|
108
146
|
given?: ModelGiven;
|
|
147
|
+
/** The motion spec's declared durations, as `validate()` is handed them — A09 reads them (cut 4c-3); absent where the caller has no motion spec, on both sides. */
|
|
148
|
+
declaredDurations?: Record<string, number>;
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
/** The model side's report: `validate()`'s shape, over the moved assertions and the two parse rules. */
|
|
@@ -134,6 +174,16 @@ export interface ModelSupply {
|
|
|
134
174
|
polygons: (read: ReadDocument) => PolygonFacts;
|
|
135
175
|
links: (read: ReadDocument) => LinkFacts;
|
|
136
176
|
constraints: (read: ReadDocument) => ConstraintFacts;
|
|
177
|
+
skeletonRoster: (read: ReadDocument) => SkeletonRosterFacts;
|
|
178
|
+
boneTimelines: (read: ReadDocument) => BoneTimelineFacts;
|
|
179
|
+
eventKeys: (read: ReadDocument) => EventKeyFacts;
|
|
180
|
+
deformSurvey: (read: ReadDocument) => DeformSurveyFacts;
|
|
181
|
+
animationDurations: (read: ReadDocument) => AnimationDurationFacts;
|
|
182
|
+
twoColour: (read: ReadDocument) => TwoColourFacts;
|
|
183
|
+
sequences: (read: ReadDocument) => SequenceFacts;
|
|
184
|
+
sliderComposition: (read: ReadDocument) => SliderCompositionFacts;
|
|
185
|
+
constraintTargets: (read: ReadDocument) => ConstraintTargetFacts;
|
|
186
|
+
steppedPoses: (read: ReadDocument) => SteppedPoseFacts;
|
|
137
187
|
}
|
|
138
188
|
|
|
139
189
|
/** The suppliers the model side runs on. */
|
|
@@ -151,6 +201,16 @@ export const MODEL_SUPPLY: ModelSupply = {
|
|
|
151
201
|
polygons: modelPolygonFacts,
|
|
152
202
|
links: modelLinkFacts,
|
|
153
203
|
constraints: modelConstraintFacts,
|
|
204
|
+
skeletonRoster: modelSkeletonRoster,
|
|
205
|
+
boneTimelines: modelBoneTimelines,
|
|
206
|
+
eventKeys: modelEventKeys,
|
|
207
|
+
deformSurvey: modelDeformSurvey,
|
|
208
|
+
animationDurations: modelAnimationDurations,
|
|
209
|
+
twoColour: modelTwoColour,
|
|
210
|
+
sequences: modelSequences,
|
|
211
|
+
sliderComposition: modelSliderComposition,
|
|
212
|
+
constraintTargets: modelConstraintTargets,
|
|
213
|
+
steppedPoses: modelSteppedPoses,
|
|
154
214
|
};
|
|
155
215
|
|
|
156
216
|
/**
|
|
@@ -169,6 +229,13 @@ export interface MovedAssertion {
|
|
|
169
229
|
* model side's place for a rule `validate()` runs BEFORE its round trip
|
|
170
230
|
* (A08, issue #589), so that a region miss is named by the rule's own
|
|
171
231
|
* sentence on both sides while the parse beside it refuses the file.
|
|
232
|
+
*
|
|
233
|
+
* Since cut 4c-4 also every rule `validate()` runs over the skeleton JSON
|
|
234
|
+
* whatever its round trip did (A12, A24, A25, A26, A29, A30, and A32, which
|
|
235
|
+
* runs before it; since cut 4c-5 A34): on the runtime's side their verdict does not wait on the
|
|
236
|
+
* load, so on this side it does not wait on the region rule, and where both
|
|
237
|
+
* parses refuse a build the two sides' lines of these rules are still
|
|
238
|
+
* compared.
|
|
172
239
|
*/
|
|
173
240
|
beforeTheParse?: boolean;
|
|
174
241
|
}
|
|
@@ -207,6 +274,20 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
|
|
|
207
274
|
{ code: 'A19_OVERLAY_PNGS_HAVE_ALPHA', run: (v, read, input, supply) => a19OverlayPngsHaveAlpha(v, supply.atlasRegions(read, input), supply.stage(read, input), supply.skinEntries(read), input), unread: SKIP_NO_ATLAS },
|
|
208
275
|
{ code: 'A27_REGION_NAME_MATCHES_PAGE_FILENAME', run: (v, read, input, supply) => a27RegionNameMatchesPageFilename(v, supply.atlasRegions(read, input)), unread: SKIP_NO_ATLAS },
|
|
209
276
|
{ code: 'A28_RIBBON_ROWS_SHARE_WEIGHTS', run: (v, read, input, supply) => a28RibbonRowsShareWeights(v, supply.meshes(read), input.rig), unread: SKIP_NO_MODEL },
|
|
277
|
+
{ code: 'A32_EVENT_KEYS_RESOLVE', run: (v, read, _input, supply) => a32EventKeysResolve(v, supply.eventKeys(read)), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
278
|
+
{ code: 'A12_NO_DARK_COLOR', run: (v, read, _input, supply) => a12NoDarkColor(v, supply.skeletonRoster(read), supply.slotColour(read).slotTimelines), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
279
|
+
{ code: 'A24_AXIS_SPACE_STROKE', run: (v, read, input, supply) => a24AxisSpaceStroke(v, supply.boneTimelines(read), input), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
280
|
+
{ code: 'A25_DETACHED_BONE_PARENTAGE', run: (v, read, input, supply) => a25DetachedBoneParentage(v, supply.skeletonRoster(read), input), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
281
|
+
{ code: 'A26_SLOT_DRAW_ORDER', run: (v, read, input, supply) => a26SlotDrawOrder(v, supply.skeletonRoster(read), input), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
282
|
+
{ code: 'A29_STROKE_WITHIN_CONTACT_DEPTH', run: (v, read, input, supply) => a29StrokeWithinContactDepth(v, supply.boneTimelines(read), input), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
283
|
+
{ code: 'A30_STROKE_WITHIN_CAP_CONTAINMENT', run: (v, read, input, supply) => a30StrokeWithinCapContainment(v, supply.boneTimelines(read), input), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
284
|
+
{ code: 'A39_DEFORM_KEEPS_TRIANGLE_WINDING', run: (v, read, input, supply) => a39DeformKeepsTriangleWinding(v, supply.deformSurvey(read), input.rig), unread: SKIP_NO_MODEL },
|
|
285
|
+
{ code: 'A09_ANIMATION_DURATION_MATCHES_SPEC', run: (v, read, input, supply) => a09AnimationDurationMatchesSpec(v, supply.animationDurations(read), input.declaredDurations), unread: SKIP_NO_MODEL },
|
|
286
|
+
{ code: 'A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN', run: (v, read, _input, supply) => a43TwoColorTintLoadsAndPosesAsWritten(v, supply.twoColour(read)), unread: SKIP_NO_MODEL },
|
|
287
|
+
{ code: 'A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES', run: (v, read, _input, supply) => a46SequenceAttachmentsShowTheFrameTheFileStates(v, supply.sequences(read)), unread: SKIP_NO_MODEL },
|
|
288
|
+
{ code: 'A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET', run: (v, read, _input, supply) => a40SlidersComposeOnASharedTarget(v, supply.sliderComposition(read), supply.constraints(read)), unread: SKIP_NO_MODEL },
|
|
289
|
+
{ code: 'A34_CONSTRAINT_TIMELINE_TARGETS', run: (v, read, _input, supply) => a34ConstraintTimelineTargets(v, supply.constraintTargets(read)), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
290
|
+
{ code: 'A10_NO_NAN_AFTER_STEPPING', run: (v, read, _input, supply) => a10NoNanAfterStepping(v, supply.boneTimelines(read), supply.steppedPoses(read)), unread: SKIP_NO_MODEL },
|
|
210
291
|
];
|
|
211
292
|
|
|
212
293
|
/** The codes the model side prints: its two parse rules, then the moved assertions. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model side's supply of `LinkFacts` (issue #1025, step 4c of #380): the
|
|
3
3
|
* document's `linkedmesh` records, in the order the Spine file spells them —
|
|
4
|
-
* the skins
|
|
5
|
-
*
|
|
4
|
+
* the skins and each skin's slot keys in the file's order (`fileSkinOrder`,
|
|
5
|
+
* issue #1034), each slot's records in table order
|
|
6
6
|
* (`fileSpelledEntries`, `./vertex_polygons.ts`).
|
|
7
7
|
*
|
|
8
8
|
* `encoding` is empty on every link: a link record has no geometry field, so
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Every value is the document's, through a function rigc already runs:
|
|
7
7
|
*
|
|
8
8
|
* - **The order** is `fileOrderedEntries` (`./skin_entries.ts`), the skins
|
|
9
|
-
* put in the file's order
|
|
9
|
+
* put in the file's order (`fileSkinOrder`, issue #1034).
|
|
10
10
|
* - **A binding's bone** is the index the emitter writes for its name,
|
|
11
11
|
* `boneIndexOf` over the document's bones (`src/emit_spine.ts`), and its
|
|
12
12
|
* weight the float32 the runtime stores a vertex array in (`Math.fround`,
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* no pages, and the registry skips A08 on it by name rather than reading that
|
|
10
10
|
* as an atlas that declares no region (`./index.ts`).
|
|
11
11
|
* - **The lookups** walk the document's skins as the file lays them: the skins
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* and each skin's slot keys in the order `fileSkinOrder` gives
|
|
13
|
+
* (`src/compile.ts`, issue #1034) — a `rigc-compiled/3` document's
|
|
14
|
+
* `editorOrder`, else the emitter's `editorSkinOrder` and
|
|
15
|
+
* `editorSlotKeyOrder`, the functions `emitSkins` is handed — NOT the draw order `./skin_entries.ts` walks, because A08 walks the
|
|
15
16
|
* file's keys and the loaded skins do not — and a slot's records in its
|
|
16
17
|
* table's order. A record of a kind that resolves a region (`CORE_REGION_KINDS`:
|
|
17
18
|
* region, mesh, linked mesh) joins under its `name`, else its placeholder,
|
|
@@ -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 { CORE_REGION_KINDS } from '../../core/index.ts';
|
|
26
27
|
import type { SkinTableEntry } from '../../model.ts';
|
|
27
28
|
import type { RegionJoinFacts } from '../facts/region_joins.ts';
|
|
@@ -29,19 +30,26 @@ import { attachmentRegionLookups, type AttachmentRegionJoin } from '../region_lo
|
|
|
29
30
|
import { isObj, type Json } from '../values.ts';
|
|
30
31
|
import type { ReadDocument } from './parse.ts';
|
|
31
32
|
|
|
32
|
-
/** The
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
/** The skins and each skin's slot keys in an order other than the file's — a parameter only so the selftest can show what that order would print. */
|
|
34
|
+
export type SlotKeyWalk = <T extends Json & { name: string }>(skins: readonly T[]) => Array<{ skin: T; slots: readonly string[] }>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The joins, with the skins and their slot keys in the file's order — the order
|
|
38
|
+
* a `rigc-compiled/3` document states, else the emitter's rule
|
|
39
|
+
* (`fileSkinOrder`, issue #1034) — unless the selftest hands `order`, the
|
|
40
|
+
* skins and each skin's slot keys in another order, to show what it would
|
|
41
|
+
* print (`VF09`, `VF11`).
|
|
42
|
+
*/
|
|
43
|
+
export function modelRegionJoinsWith(read: ReadDocument, order?: SlotKeyWalk): RegionJoinFacts {
|
|
38
44
|
const pages = read.doc.pages;
|
|
39
45
|
const regionNames = pages === null ? null : pages.flatMap((page) => page.regions.map((region) => region.name));
|
|
40
46
|
const stated = (Array.isArray(read.json.skins) ? read.json.skins.filter(isObj) : []).filter((skin): skin is Json & { name: string } => typeof skin.name === 'string');
|
|
41
47
|
const joins: AttachmentRegionJoin[] = [];
|
|
42
|
-
|
|
48
|
+
const walk = order === undefined ? skinsInFileOrder(stated, fileSkinOrder(read.doc)) : order(stated);
|
|
49
|
+
for (const { skin, slots } of walk) {
|
|
43
50
|
if (!isObj(skin.attachments)) continue;
|
|
44
|
-
for (const
|
|
51
|
+
for (const slot of slots) {
|
|
52
|
+
const table = skin.attachments[slot];
|
|
45
53
|
if (!isObj(table)) continue;
|
|
46
54
|
for (const [placeholder, record] of Object.entries(table)) {
|
|
47
55
|
if (!isObj(record) || !CORE_REGION_KINDS.has(record.kind as SkinTableEntry['kind'])) continue;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `SequenceFacts` (issue #1025, cut 4c-3 of step
|
|
3
|
+
* 4c of #380).
|
|
4
|
+
*
|
|
5
|
+
* - **The skin entries**, in the file's order: the skins and each skin's slot
|
|
6
|
+
* keys as `fileSkinOrder` gives them (`src/compile.ts`, issue #1034 — a
|
|
7
|
+
* `rigc-compiled/3` document's stated `editorOrder`, else the emitter's
|
|
8
|
+
* rule; the walk `./region_joins.ts` makes for A08, which walks the same
|
|
9
|
+
* keys), and a slot's records in its table's order. A record of a kind that draws a
|
|
10
|
+
* region (`CORE_REGION_KINDS`) is an entry; its `name` and `path` are the
|
|
11
|
+
* record's own, which the emitter writes unchanged, and its `sequence` is
|
|
12
|
+
* spelled as the file spells it — written by the emitter's own
|
|
13
|
+
* `emitSequenceBlock` (the document's block also carries each frame's atlas
|
|
14
|
+
* rectangle, which the file does not) and passed through
|
|
15
|
+
* `withoutParserDefaults`, which leaves out `start: 1` and `setup: 0`. A
|
|
16
|
+
* document the reader and the region rule accepted loads every record, so
|
|
17
|
+
* every entry is `loaded`.
|
|
18
|
+
* - **The sequence timelines**: the animations in `fileAnimationOrder`'s order,
|
|
19
|
+
* and each animation's `attachments` group at its three levels (skin, slot,
|
|
20
|
+
* attachment) in the order the file keys them — `keyedOrder` over the
|
|
21
|
+
* document's order, which is the model's, as `src/deformstructure.ts` walks
|
|
22
|
+
* the same group (issue #1034) — passed through
|
|
23
|
+
* `withoutParserDefaults` and `inEditorKeyOrder` (`src/keyorder.ts`), so a
|
|
24
|
+
* key's `delay` the parser would read off the key before it is left out
|
|
25
|
+
* here exactly where the file leaves it out, and A46 prints what it prints
|
|
26
|
+
* over the file.
|
|
27
|
+
* - **A slot at a pose**: the core's raw entry (`poseRawAnimation`,
|
|
28
|
+
* `src/core/raw.ts`) taking one step of the time on a fresh track, under the
|
|
29
|
+
* view the core poses with no skin set (`noSkinView`) — A45's posed colour
|
|
30
|
+
* and A43's posed tint are taken the same way. The entry the slot shows is
|
|
31
|
+
* the pose's shown record; the entry whose timelines it plays is the core's
|
|
32
|
+
* own rule for a linked mesh that plays its source's (`timelineIdentity`,
|
|
33
|
+
* whose condition is the record's `timelines`); and the region its series resolves to is `drawnRegions`
|
|
34
|
+
* (`src/core/uvs.ts`), which refuses by name the one case whose reading was
|
|
35
|
+
* not measured — a linked mesh with a series of its own, stepped over its
|
|
36
|
+
* source's frame count. No gate samples the times A46 asks for (mid-frame,
|
|
37
|
+
* and each `hold` key's own time), so the selftest compares these values
|
|
38
|
+
* with spine-core's at every time the body asks, on every call with a model
|
|
39
|
+
* in hand (`VF12`).
|
|
40
|
+
*
|
|
41
|
+
* Links nothing from the runtime.
|
|
42
|
+
*/
|
|
43
|
+
import { fileAnimationOrder, fileSkinOrder, keyedOrder, skinsInFileOrder } from '../../compile.ts';
|
|
44
|
+
import { CORE_REGION_KINDS, CoreInputError, type CompiledDocument } from '../../core/index.ts';
|
|
45
|
+
import { poseRawAnimation } from '../../core/raw.ts';
|
|
46
|
+
import { documentPageLookup, drawnRegions, readUvSequences } from '../../core/uvs.ts';
|
|
47
|
+
import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
|
|
48
|
+
import { emitSequenceBlock } from '../../emit_spine.ts';
|
|
49
|
+
import type { ModelSequence, SkinTableEntry } from '../../model.ts';
|
|
50
|
+
import { noSkinView } from '../../render_core.ts';
|
|
51
|
+
import { entryAddress, type SequenceFacts, type SequenceSkinEntry, type SequenceTimeline } from '../facts/sequences.ts';
|
|
52
|
+
import { isObj, type Json } from '../values.ts';
|
|
53
|
+
import type { ReadDocument } from './parse.ts';
|
|
54
|
+
|
|
55
|
+
const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
|
|
56
|
+
|
|
57
|
+
/** The skin entries that draw a region, in the file's order, each `sequence` spelled as the file spells it. */
|
|
58
|
+
function fileEntries(read: ReadDocument): SequenceSkinEntry[] {
|
|
59
|
+
const stated = list(read.json.skins).filter((skin): skin is Json & { name: string } => typeof skin.name === 'string');
|
|
60
|
+
const picked: Array<{ skin: string; slot: string; placeholder: string; record: Json }> = [];
|
|
61
|
+
const shell: Json[] = [];
|
|
62
|
+
for (const { skin, slots } of skinsInFileOrder(stated, fileSkinOrder(read.doc))) {
|
|
63
|
+
if (!isObj(skin.attachments)) continue;
|
|
64
|
+
const attachments: Json = {};
|
|
65
|
+
for (const slot of slots) {
|
|
66
|
+
const table = skin.attachments[slot];
|
|
67
|
+
if (!isObj(table)) continue;
|
|
68
|
+
const placeholders: Json = {};
|
|
69
|
+
for (const [placeholder, record] of Object.entries(table)) {
|
|
70
|
+
if (!isObj(record) || !CORE_REGION_KINDS.has(record.kind as SkinTableEntry['kind'])) continue;
|
|
71
|
+
picked.push({ skin: skin.name, slot, placeholder, record });
|
|
72
|
+
// Only what the omission pass reads to spell the series: the attachment's type, then the block as the emitter writes it
|
|
73
|
+
// (`emitSequenceBlock`: the four fields the file carries, never the document's per-frame `atlas`, in a fresh object — the pass edits in place).
|
|
74
|
+
placeholders[placeholder] = { ...(record.kind === 'region' ? {} : { type: record.kind }), ...(isObj(record.sequence) ? { sequence: emitSequenceBlock(record.sequence as unknown as ModelSequence) } : {}) };
|
|
75
|
+
}
|
|
76
|
+
attachments[slot] = placeholders;
|
|
77
|
+
}
|
|
78
|
+
shell.push({ name: skin.name, attachments });
|
|
79
|
+
}
|
|
80
|
+
withoutParserDefaults({ skins: shell });
|
|
81
|
+
return picked.map(({ skin, slot, placeholder, record }) => {
|
|
82
|
+
const spelled = (((shell.find((s) => s.name === skin) as Json).attachments as Json)[slot] as Json)[placeholder] as Json;
|
|
83
|
+
return { skin, slot, placeholder, name: record.name, path: record.path, sequence: spelled.sequence, loaded: true };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Names in the order each first appears, put in the order a JSON object keyed by them lists them (`keyedOrder`). */
|
|
88
|
+
const keyedFirsts = (names: readonly string[]): string[] => keyedOrder(names.filter((name, i) => names.indexOf(name) === i));
|
|
89
|
+
|
|
90
|
+
/** Every animation's sequence timelines in the file's order and spelling: `animations.<name>.attachments.<skin>.<slot>.<placeholder>.sequence = keys`. */
|
|
91
|
+
function fileSequenceTimelines(read: ReadDocument): SequenceTimeline[] {
|
|
92
|
+
const byName = new Map<string, Json>();
|
|
93
|
+
for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
|
|
94
|
+
type Level = { name: string; json: Json };
|
|
95
|
+
const named = (v: unknown): Level[] => list(v).flatMap((json) => (typeof json.name === 'string' ? [{ name: json.name, json }] : []));
|
|
96
|
+
/** One level of the group in the order the file keys it: `keyedOrder` over the document's order, each name's entries together. */
|
|
97
|
+
const inFileOrder = (levels: Level[]): Array<[string, Json[]]> => keyedFirsts(levels.map((l) => l.name)).map((name) => [name, levels.filter((l) => l.name === name).map((l) => l.json)]);
|
|
98
|
+
const order = fileAnimationOrder(read.doc);
|
|
99
|
+
const walk: Array<{ animation: string; skin: string; slot: string; placeholder: string }> = [];
|
|
100
|
+
const animations: Json = {};
|
|
101
|
+
for (const name of order) {
|
|
102
|
+
const anim = byName.get(name);
|
|
103
|
+
if (anim === undefined) continue;
|
|
104
|
+
const bySkin: Json = {};
|
|
105
|
+
for (const [skin, skinEntries] of inFileOrder(named(anim.attachments))) {
|
|
106
|
+
const bySlot: Json = {};
|
|
107
|
+
for (const [slot, slotEntries] of inFileOrder(skinEntries.flatMap((e) => named(e.slots)))) {
|
|
108
|
+
const byAttachment: Json = {};
|
|
109
|
+
for (const [placeholder, attachments] of inFileOrder(slotEntries.flatMap((e) => named(e.attachments)))) {
|
|
110
|
+
const timelines: Json = {};
|
|
111
|
+
// `deform` then `sequence`, each only where the document holds it — `emitAttachmentTimelines`' order.
|
|
112
|
+
for (const attachment of attachments) {
|
|
113
|
+
for (const word of ['deform', 'sequence'] as const) if (Array.isArray(attachment[word])) timelines[word] = list(attachment[word]).map((key) => ({ ...key }));
|
|
114
|
+
}
|
|
115
|
+
byAttachment[placeholder] = timelines;
|
|
116
|
+
if (Array.isArray(timelines.sequence)) walk.push({ animation: name, skin, slot, placeholder });
|
|
117
|
+
}
|
|
118
|
+
bySlot[slot] = byAttachment;
|
|
119
|
+
}
|
|
120
|
+
bySkin[skin] = bySlot;
|
|
121
|
+
}
|
|
122
|
+
animations[name] = Object.keys(bySkin).length > 0 ? { attachments: bySkin } : {};
|
|
123
|
+
}
|
|
124
|
+
// The emitter's own passes over the keys (`withoutParserDefaults`, `inEditorKeyOrder`), then the walk read back in the order built above.
|
|
125
|
+
const file = inEditorKeyOrder(withoutParserDefaults({ animations }));
|
|
126
|
+
return walk.map(({ animation, skin, slot, placeholder }) => {
|
|
127
|
+
const timelines = ((((file.animations[animation] as Json).attachments as Json)[skin] as Json)[slot] as Json)[placeholder] as Json;
|
|
128
|
+
return { animation, skin, slot, placeholder, keys: timelines.sequence as unknown[] };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function modelSequences(read: ReadDocument): SequenceFacts {
|
|
133
|
+
let view: CompiledDocument | null = null;
|
|
134
|
+
return {
|
|
135
|
+
entries: fileEntries(read),
|
|
136
|
+
timelines: fileSequenceTimelines(read),
|
|
137
|
+
hasSlot: (slot) => read.doc.slots.some((s) => s.name === slot),
|
|
138
|
+
duration: (animation) => read.doc.animations.find((a) => a.name === animation)?.timelines.duration ?? null,
|
|
139
|
+
posedFrame: (animation, slot, time) => {
|
|
140
|
+
view ??= noSkinView(read.doc);
|
|
141
|
+
const pose = poseRawAnimation(view, animation, [time], {}, 'animation')[1];
|
|
142
|
+
const shown = pose.shown.find((s) => s.slot === slot);
|
|
143
|
+
if (shown === undefined) return null;
|
|
144
|
+
const at = entryAddress(shown.skin, slot, shown.placeholder);
|
|
145
|
+
const g = shown.geometry;
|
|
146
|
+
const record = view.skins.find((k) => k.name === shown.skin)?.attachments[slot]?.[shown.placeholder];
|
|
147
|
+
// A linked mesh that plays its source's timelines plays them as its source (`timelineIdentity` in `src/core/deform.ts`, the
|
|
148
|
+
// runtime's `timelineAttachment`); every other entry plays its own.
|
|
149
|
+
const playsAs = g.kind === 'linkedmesh' && record?.timelines === true ? entryAddress(g.skin, g.slot, g.source) : at;
|
|
150
|
+
if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') return { shown: at, playsAs, region: null };
|
|
151
|
+
if (read.doc.pages === null) throw new CoreInputError('the document states no pages, so no region a series resolves to can be named');
|
|
152
|
+
const resolved = drawnRegions(view, pose.shown, [slot], { sequences: readUvSequences(read.json), lookup: documentPageLookup(read.doc.pages) });
|
|
153
|
+
if (resolved.drawn === null) throw new CoreInputError(`the core does not name the region slot "${slot}" shows at t=${time}: ${resolved.why}`);
|
|
154
|
+
return { shown: at, playsAs, region: resolved.drawn[0]?.region ?? null };
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `SkeletonRosterFacts` (issue #1025, cut 4c-4 of
|
|
3
|
+
* #380): the document's bones and slots, in the document's order.
|
|
4
|
+
*
|
|
5
|
+
* The emitter writes the file's `bones` and `slots` arrays in the model's
|
|
6
|
+
* order and never re-sorts them (`emitBones`, `emitSlots` in
|
|
7
|
+
* `src/emit_spine.ts`; a slot's order is the draw order a draw-order key's
|
|
8
|
+
* offsets count in), and the document writes each list in that same order, so
|
|
9
|
+
* the document's lists are the file's. A bone's parent is the name the
|
|
10
|
+
* document states, `null` for a root; a slot declares a dark colour where the
|
|
11
|
+
* document states one, which is where `emitSlots` writes the key.
|
|
12
|
+
*
|
|
13
|
+
* Links nothing from the runtime.
|
|
14
|
+
*/
|
|
15
|
+
import type { SkeletonRosterFacts } from '../facts/skeleton_roster.ts';
|
|
16
|
+
import type { ReadDocument } from './parse.ts';
|
|
17
|
+
|
|
18
|
+
export function modelSkeletonRoster(read: ReadDocument): SkeletonRosterFacts {
|
|
19
|
+
return {
|
|
20
|
+
bones: read.doc.bones.map((bone) => ({ name: bone.name, parent: bone.parent ?? null })),
|
|
21
|
+
slots: read.doc.slots.map((slot) => ({ name: slot.name, dark: slot.dark !== undefined })),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* document in its own order lists the same entries in another order on a rig
|
|
9
9
|
* whose skins the comparator moves — the census measured that on 3 of 52
|
|
10
10
|
* multi-skin compiles. The skins are therefore put in the file's order by
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* `fileSkinOrder` from `src/compile.ts` (issue #1034): the order a
|
|
12
|
+
* `rigc-compiled/3` document states, else `editorSkinOrder`, the function the
|
|
13
|
+
* emitter is handed (`emitSkins`) — never a copy of its rule. Within a skin the entries go
|
|
13
14
|
* slot by slot in the skeleton's slot order — the document's `slots`, which is
|
|
14
15
|
* the emitted array's order — and a slot's entries in its table's order, which
|
|
15
16
|
* the emitter writes unchanged; the selftest measures this walk equal to
|
|
@@ -22,18 +23,27 @@
|
|
|
22
23
|
*
|
|
23
24
|
* Links nothing from the runtime.
|
|
24
25
|
*/
|
|
25
|
-
import {
|
|
26
|
+
import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
|
|
26
27
|
import { shownRow } from '../../core/index.ts';
|
|
27
28
|
import type { RegionEntry, SkinEntryFacts } from '../facts/skin_entries.ts';
|
|
28
29
|
import type { ReadDocument } from './parse.ts';
|
|
29
30
|
|
|
30
|
-
/** How the skins are put in
|
|
31
|
+
/** How the skins are put in an order other than the file's. A parameter only so the selftest can show what the document's own order would print (`VF05`). */
|
|
31
32
|
export type SkinOrder = <T extends { name: string }>(skins: readonly T[]) => T[];
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The document's skins in the file's order (issue #1034): `fileSkinOrder` —
|
|
36
|
+
* a `rigc-compiled/3` document's `editorOrder`, the one source on `/3`, else
|
|
37
|
+
* the emitter's own rule (`editorSkinOrder`) over the document's skins.
|
|
38
|
+
*/
|
|
39
|
+
export function skinsInTheFilesOrder<T extends { name: string }>(read: ReadDocument, skins: readonly T[]): T[] {
|
|
40
|
+
return skinsInFileOrder(skins, fileSkinOrder(read.doc)).map((walk) => walk.skin);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The skins' entries in the file's walk order, each with its skin, slot and placeholder; `order`, when given, replaces the file's skin order (`VF05`). */
|
|
44
|
+
export function fileOrderedEntries(read: ReadDocument, order?: SkinOrder): Array<{ skin: string; slot: string; placeholder: string; record: ReadDocument['doc']['skins'][number]['attachments'][string][string] }> {
|
|
35
45
|
const out: ReturnType<typeof fileOrderedEntries> = [];
|
|
36
|
-
for (const skin of order(read.doc.skins)) {
|
|
46
|
+
for (const skin of order === undefined ? skinsInTheFilesOrder(read, read.doc.skins) : order(read.doc.skins)) {
|
|
37
47
|
for (const { name: slot } of read.doc.slots) {
|
|
38
48
|
const table = skin.attachments[slot];
|
|
39
49
|
if (table === undefined) continue;
|
|
@@ -43,7 +53,7 @@ export function fileOrderedEntries(read: ReadDocument, order: SkinOrder = editor
|
|
|
43
53
|
return out;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
export function modelSkinEntries(read: ReadDocument, order
|
|
56
|
+
export function modelSkinEntries(read: ReadDocument, order?: SkinOrder): SkinEntryFacts {
|
|
47
57
|
const regionAttachments: RegionEntry[] = [];
|
|
48
58
|
let clippingCount = 0;
|
|
49
59
|
for (const entry of fileOrderedEntries(read, order)) {
|