spine-rigc 1.5.3 → 1.6.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/README.md +2 -1
- package/cli.ts +131 -403
- package/docs/AUTHORING.md +63 -14
- package/docs/INGEST.md +1 -0
- package/docs/MOTION.md +1 -0
- package/package.json +1 -1
- package/src/atlas.ts +25 -0
- package/src/check.ts +113 -24
- package/src/checkpics.ts +2 -0
- package/src/compile.ts +641 -506
- package/src/core/animation.ts +982 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1721 -0
- package/src/core/constraints_path.ts +942 -0
- package/src/core/constraints_physics.ts +830 -0
- package/src/core/constraints_slider.ts +505 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1103 -0
- package/src/core/raw.ts +347 -0
- package/src/core/skins.ts +104 -0
- package/src/core/uvs.ts +448 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/world.ts +233 -0
- package/src/deformmeasure.ts +337 -73
- package/src/deformreport.ts +370 -0
- package/src/depth.ts +101 -2
- package/src/emit_spine.ts +860 -0
- package/src/mesh.ts +24 -15
- package/src/model.ts +958 -0
- package/src/render.ts +649 -143
- package/src/render_core.ts +582 -0
- package/src/rig.ts +34 -6
- package/src/transform.ts +26 -4
- package/src/types.ts +18 -0
- package/src/validate.ts +30 -2
package/src/core/raw.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core's raw entry (issue #966, step 3b of issue #380): one pose, or one
|
|
3
|
+
* animation walked at the caller's own steps, returned as the doubles the
|
|
4
|
+
* core computed — no grid — with everything a renderer or a deform measurer
|
|
5
|
+
* reads past the oracle's rows: each bone's world rotation and scale
|
|
6
|
+
* readings, each drawn attachment's local UVs, triangles, hull, sequence
|
|
7
|
+
* frame and colour, the clip polygons, the triangles drawn under a clip, and
|
|
8
|
+
* the events fired.
|
|
9
|
+
*
|
|
10
|
+
* The oracle's document (`tools/pose_oracle.ts`) samples an animation on a
|
|
11
|
+
* phase grid, re-applying it from the setup pose at every sample; the two
|
|
12
|
+
* consumers step instead. `src/render.ts` resets a track at 0 and then takes
|
|
13
|
+
* exactly one step of `1/fps` per frame; `src/deformmeasure.ts` resets and
|
|
14
|
+
* takes one step of `time`. So the entry takes a list of steps rather than a
|
|
15
|
+
* list of times, and this header states what one step is, as measured.
|
|
16
|
+
*
|
|
17
|
+
* ## One step — the walk, measured
|
|
18
|
+
*
|
|
19
|
+
* Measured against spine-core 4.3.13 running `src/render.ts`'s recipe
|
|
20
|
+
* (`AnimationState` with one non-looping track: frame 0 `apply`,
|
|
21
|
+
* `Skeleton.update(0)`, `updateWorldTransform(Physics.reset)`; every later
|
|
22
|
+
* frame `AnimationState.update(dt)`, `apply`, `Skeleton.update(dt)`,
|
|
23
|
+
* `updateWorldTransform(Physics.update)`), read off `TrackEntry.trackTime`,
|
|
24
|
+
* `TrackEntry.getAnimationTime()` and every bone's `appliedPose` in full
|
|
25
|
+
* doubles (the PR of issue #966 carries the table):
|
|
26
|
+
*
|
|
27
|
+
* - **The track time is the running sum of the steps**, `T += dt` in double,
|
|
28
|
+
* not `i·dt`, and the animation is applied at the sum. Over every
|
|
29
|
+
* animation of the nineteen tree rows `TrackEntry.trackTime` equalled the
|
|
30
|
+
* running sum on 2,612 of 2,612 frames at 12 fps (12,850 of 12,850 at 60,
|
|
31
|
+
* 5,173 of 5,173 at 24) and `i·dt` on only 662 (1,168; 810): at 12 fps
|
|
32
|
+
* frame 6 reads `0.49999999999999994`, not `0.5`, and frame 10
|
|
33
|
+
* `0.8333333333333334`, not `0.8333333333333333`. `src/render.ts` files a
|
|
34
|
+
* frame under `i·dt`, which is its label and not the time posed.
|
|
35
|
+
* - **A non-looping track holds at its duration**: the animation time is
|
|
36
|
+
* `min(T, duration)`, where `duration` is the runtime's (the last key of
|
|
37
|
+
* every timeline, as float32 — `CoreAnimationTimelines.duration`). With
|
|
38
|
+
* `round(duration·fps)` frames the last one overshoots on 20 animation
|
|
39
|
+
* frames of the 2,612 at 12 fps (19 at 60, 25 at 24), and
|
|
40
|
+
* `getAnimationTime()` read the duration on every one of them.
|
|
41
|
+
* - **The physics clock moves by the step itself**, `time += dt` (the
|
|
42
|
+
* skeleton's `update`), and a physics constraint integrates the time it
|
|
43
|
+
* moved (`./constraints_physics.ts`). Pose 0 is the reset, every physics
|
|
44
|
+
* state reset there (where, below).
|
|
45
|
+
* - **Events fire over `(previous animation time, animation time]`**, the
|
|
46
|
+
* first step's interval opening at −1 — so held past the duration, a step
|
|
47
|
+
* fires nothing. Measured against `AnimationState`'s event listener on
|
|
48
|
+
* every frame above.
|
|
49
|
+
* - **Where the reset is taken is the caller's** (`RawReset`): render's
|
|
50
|
+
* frame 0 applies the animation at 0 and resets there (`animation`);
|
|
51
|
+
* deformmeasure's `poseAt` resets at the setup pose and then applies the
|
|
52
|
+
* animation in its one step (`setup`). On a physics rig the two differ by
|
|
53
|
+
* whole units, not bits: of the 162 one-jump poses (three per animation),
|
|
54
|
+
* 12 read off under `animation` — six on `7-anticipation`'s physics rig (a
|
|
55
|
+
* cape bone 1.7° off at 0.617 s) and six on `gallery/look`'s — and none
|
|
56
|
+
* under `setup`.
|
|
57
|
+
*
|
|
58
|
+
* Every step poses the animation from the setup pose at its animation time
|
|
59
|
+
* (the setup blend at alpha 1), as the oracle's stepped dump does, applied
|
|
60
|
+
* from the animation time of the step before (−1 before the first): a
|
|
61
|
+
* physics `reset` key fires on the step that crosses it, once. Until issue
|
|
62
|
+
* #960 this walk fired such a key at every step from it on (the oracle's
|
|
63
|
+
* reading then), and against the recipe above a probe with a key at 0.5
|
|
64
|
+
* read 32 of 61 frames bit-exact at 60 fps, the rest off by up to 53 units;
|
|
65
|
+
* with the key crossed once, 61 of 61 at 60 fps and 13 of 13 at 12. None of
|
|
66
|
+
* the nineteen tree rows keys a `reset`, which is why the raw gate never
|
|
67
|
+
* saw it. The one difference from `--physics step` is where the steps fall,
|
|
68
|
+
* which is the caller's.
|
|
69
|
+
*
|
|
70
|
+
* ## What a pose carries
|
|
71
|
+
*
|
|
72
|
+
* - `bones` — the world matrix and origin, and the four readings the
|
|
73
|
+
* runtime's getters give: `rotationX = atan2(c, a)·DEG`, `rotationY =
|
|
74
|
+
* atan2(d, b)·DEG`, `scaleX = √(a² + c²)`, `scaleY = √(b² + d²)`, with
|
|
75
|
+
* `DEG = 180 / π` at the runtime's π (`RUNTIME_DEG` in `./world.ts`) —
|
|
76
|
+
* measured bit-exact against `BonePose.getWorldRotationX/Y` and
|
|
77
|
+
* `getWorldScaleX/Y` on every bone of 2,793 poses of the nineteen tree
|
|
78
|
+
* rows (the setup, every 12 fps frame, three jumps per animation).
|
|
79
|
+
* - `slots` — the oracle's slot row as doubles (`posedSlots`).
|
|
80
|
+
* - `drawn` — every region and mesh a slot shows, in draw order: its world
|
|
81
|
+
* vertices (the oracle's `attachments` row as doubles), its LOCAL UVs as the
|
|
82
|
+
* runtime holds them (a region's unit corners `0 1, 0 0, 1 0, 1 1` in the
|
|
83
|
+
* corner order; a mesh's `uvs` — a linked mesh's source's — as the
|
|
84
|
+
* document spells them: `MeshAttachment.regionUVs` read the doubles, not
|
|
85
|
+
* their float32, on every mesh of the nineteen rows — `6-arcs`'
|
|
86
|
+
* `2.554152e-7` where `Math.fround` gives `2.5541518766658555e-7`), its
|
|
87
|
+
* triangles (a region's `0 1 2 2
|
|
88
|
+
* 3 0`), its hull (a mesh's; `null` for a region), its sequence frame (the
|
|
89
|
+
* frame a sequence timeline set, else the series' setup frame, else 0 —
|
|
90
|
+
* the runtime gives every region and mesh a series, one frame long when
|
|
91
|
+
* the record states none, and `Sequence.resolveIndex` read 0 on every such
|
|
92
|
+
* attachment of the nineteen rows) and its own colour (the record's `color`, white
|
|
93
|
+
* unstated) — so the caller forms the tint, slot colour times attachment
|
|
94
|
+
* colour, itself. Where a region sits on a page is not the model's
|
|
95
|
+
* (`ModelAtlasRect`'s 🔸); the page UVs are issue #967's.
|
|
96
|
+
* - `clips`, `clipped` — the oracle's rows as doubles; `clipped` is what the
|
|
97
|
+
* core DRAWS, which is the oracle's block wherever the core poses it and,
|
|
98
|
+
* under a clip that is not strictly convex or is inverse (a block the core
|
|
99
|
+
* leaves out), the core's own convex decomposition (`clipThrough` in
|
|
100
|
+
* `./clipping.ts`, issue #964), held to spine-core by the render's pixels.
|
|
101
|
+
* - `events` — the oracle's event rows as doubles.
|
|
102
|
+
*
|
|
103
|
+
* ⛔ **Nothing is posed that the core would leave out.** Where the oracle's
|
|
104
|
+
* core dump names a block absent — a constraint the core cannot pose
|
|
105
|
+
* exactly, skins disagreeing over a placeholder under the merged view, a
|
|
106
|
+
* region with no atlas rectangle — the raw entry refuses the whole call,
|
|
107
|
+
* naming why (`CoreInputError`): a renderer handed a pose with a block
|
|
108
|
+
* missing would draw a different picture in silence.
|
|
109
|
+
*/
|
|
110
|
+
import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber, readColour, shownAttachment, sourceOfDoc, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
|
|
111
|
+
import { posedBoneWorld, posedSlots, type TimelinePlant } from './animation.ts';
|
|
112
|
+
import { constraintsAbsentWhy, pathAnimationsWhy } from './constraints.ts';
|
|
113
|
+
import { freshStepContext, steppedPreviousPassWhy } from './constraints_physics.ts';
|
|
114
|
+
import { attachmentStates } from './deform.ts';
|
|
115
|
+
import { drawOrderAt } from './draw_order.ts';
|
|
116
|
+
import { eventsFired } from './events.ts';
|
|
117
|
+
import { REGION_TRIANGLES, REGION_UVS } from './clipping.ts';
|
|
118
|
+
import { poseGeometry, type ShownGeometry } from './vertices.ts';
|
|
119
|
+
import { RUNTIME_DEG, type CoreWorld } from './world.ts';
|
|
120
|
+
import type { SliderApplication } from './constraints_slider.ts';
|
|
121
|
+
|
|
122
|
+
/** One bone, posed: the world transform and the runtime getters' four readings (the header's `bones`). */
|
|
123
|
+
export interface RawBone {
|
|
124
|
+
name: string;
|
|
125
|
+
parent: string | null;
|
|
126
|
+
active: boolean;
|
|
127
|
+
a: number;
|
|
128
|
+
b: number;
|
|
129
|
+
c: number;
|
|
130
|
+
d: number;
|
|
131
|
+
worldX: number;
|
|
132
|
+
worldY: number;
|
|
133
|
+
rotationX: number;
|
|
134
|
+
rotationY: number;
|
|
135
|
+
scaleX: number;
|
|
136
|
+
scaleY: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One region or mesh drawn, in draw order (the header's `drawn`). */
|
|
140
|
+
export interface RawDrawn {
|
|
141
|
+
slot: string;
|
|
142
|
+
attachment: string;
|
|
143
|
+
kind: 'region' | 'mesh';
|
|
144
|
+
vertices: number[];
|
|
145
|
+
uvs: number[];
|
|
146
|
+
triangles: number[];
|
|
147
|
+
hull: number | null;
|
|
148
|
+
sequenceIndex: number;
|
|
149
|
+
colour: [number, number, number, number];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One clip polygon: `[slot, attachment, end, polygon]` as doubles. */
|
|
153
|
+
export type RawClip = [string, string, string | null, number[]];
|
|
154
|
+
/** One attachment drawn under a clip: `[slot, attachment, clipped, vertices, uvs, triangles]` as the clipper returns them. */
|
|
155
|
+
export type RawClipped = [string, string, 0 | 1, number[], number[], number[]];
|
|
156
|
+
/** One event fired: `[name, time, int, float, string]`. */
|
|
157
|
+
export type RawEvent = [string, number, number, number, string | null];
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Where a walk's physics is reset (the header's *One step*): `animation` —
|
|
161
|
+
* the animation applied at 0 and the reset taken there (`src/render.ts`'s
|
|
162
|
+
* frame 0); `setup` — the reset taken at the setup pose before the animation
|
|
163
|
+
* is applied (`src/deformmeasure.ts`'s `poseAt`), pose 0 then the setup pose.
|
|
164
|
+
*/
|
|
165
|
+
export type RawReset = 'animation' | 'setup';
|
|
166
|
+
|
|
167
|
+
/** One pose — the setup's, or one step of an animation's walk. */
|
|
168
|
+
export interface RawPose {
|
|
169
|
+
/** The track time (the running sum of the steps), 0 for the setup pose. */
|
|
170
|
+
trackTime: number;
|
|
171
|
+
/** The time the animation was applied at: the track time held at the duration. */
|
|
172
|
+
animationTime: number;
|
|
173
|
+
bones: RawBone[];
|
|
174
|
+
slots: CoreSlotRow[];
|
|
175
|
+
drawOrder: string[];
|
|
176
|
+
drawn: RawDrawn[];
|
|
177
|
+
clips: RawClip[];
|
|
178
|
+
clipped: RawClipped[];
|
|
179
|
+
events: RawEvent[];
|
|
180
|
+
// --- #968 render: begin ---
|
|
181
|
+
/**
|
|
182
|
+
* What each slot shows, in draw order — the records `drawn` was posed from
|
|
183
|
+
* (skin, placeholder, series frame, deform). `src/render_core.ts` resolves
|
|
184
|
+
* each drawn attachment's atlas region and page UVs through it, with
|
|
185
|
+
* `./uvs.ts`'s `drawnRegions` (issue #967's gated rule), rather than walking
|
|
186
|
+
* which record a slot shows a second time.
|
|
187
|
+
*/
|
|
188
|
+
shown: ShownGeometry[];
|
|
189
|
+
// --- #968 render: end ---
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const RAW_PLANT: CorePlant = { round: rawNumber };
|
|
193
|
+
|
|
194
|
+
// --- #968 render: begin ---
|
|
195
|
+
/** The shown records in `order`, the pose's draw order (a pose's `shown`). */
|
|
196
|
+
function drawOrderOf(shown: readonly ShownGeometry[], order: readonly string[]): ShownGeometry[] {
|
|
197
|
+
const rank = new Map(order.map((n, r) => [n, r]));
|
|
198
|
+
return [...shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
199
|
+
}
|
|
200
|
+
// --- #968 render: end ---
|
|
201
|
+
|
|
202
|
+
/** A double the entry computed: never `null`, since a non-finite pose is refused rather than passed on. */
|
|
203
|
+
function finite(v: number | null, what: string): number {
|
|
204
|
+
if (v === null) throw new CoreInputError(`the raw pose computed a non-finite ${what}`);
|
|
205
|
+
return v;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The bones of a pose from the world transforms (the header's `bones`). */
|
|
209
|
+
function rawBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>): RawBone[] {
|
|
210
|
+
const active = activeBones(doc);
|
|
211
|
+
return doc.bones.map((b): RawBone => {
|
|
212
|
+
const w = world.get(b.name);
|
|
213
|
+
if (w === undefined) throw new CoreInputError(`bone "${b.name}" has no world transform`);
|
|
214
|
+
return {
|
|
215
|
+
name: b.name,
|
|
216
|
+
parent: b.parent ?? null,
|
|
217
|
+
active: active.has(b.name),
|
|
218
|
+
a: w.a,
|
|
219
|
+
b: w.b,
|
|
220
|
+
c: w.c,
|
|
221
|
+
d: w.d,
|
|
222
|
+
worldX: w.worldX,
|
|
223
|
+
worldY: w.worldY,
|
|
224
|
+
rotationX: Math.atan2(w.c, w.a) * RUNTIME_DEG,
|
|
225
|
+
rotationY: Math.atan2(w.d, w.b) * RUNTIME_DEG,
|
|
226
|
+
scaleX: Math.sqrt(w.a * w.a + w.c * w.c),
|
|
227
|
+
scaleY: Math.sqrt(w.b * w.b + w.d * w.d),
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
|
|
233
|
+
function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
|
|
234
|
+
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), rawNumber, { region: plant.region, vertices: plant.vertices }, drawWalkOf(doc, order, plant));
|
|
235
|
+
if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
|
|
236
|
+
// The drawn rows (issue #964): the oracle's `clipped` rows where it is posed, and a concave or inverse clip cut through the core's own decomposition.
|
|
237
|
+
if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
|
|
238
|
+
// Under a concave or inverse clip the oracle's `clipped` block is absent (the runtime's own triangle list); what the core draws is `drawnClipped`,
|
|
239
|
+
// its own decomposition, and the render samples each drawn triangle at its source triangle's affine UV, so the pixels are the decomposition's
|
|
240
|
+
// coverage alone (issue #964, `Mesh.source` in src/render.ts).
|
|
241
|
+
const rows = geometry.attachments;
|
|
242
|
+
const drawn: RawDrawn[] = [];
|
|
243
|
+
let k = 0;
|
|
244
|
+
for (const s of shown) {
|
|
245
|
+
const g = s.geometry;
|
|
246
|
+
if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') continue;
|
|
247
|
+
const row = rows[k++];
|
|
248
|
+
if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
|
|
249
|
+
const record = doc.skins.find((x) => x.name === s.skin)?.attachments[s.slot]?.[s.placeholder];
|
|
250
|
+
const colour = record?.color === undefined ? ([1, 1, 1, 1] as [number, number, number, number]) : readColour(record.color);
|
|
251
|
+
let uvs: number[];
|
|
252
|
+
let triangles: number[];
|
|
253
|
+
let hull: number | null = null;
|
|
254
|
+
if (g.kind === 'region') {
|
|
255
|
+
uvs = [...REGION_UVS];
|
|
256
|
+
triangles = [...REGION_TRIANGLES];
|
|
257
|
+
} else {
|
|
258
|
+
const source = g.kind === 'mesh' ? g : doc.skins.find((x) => x.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
|
|
259
|
+
if (source === undefined || source.kind !== 'mesh') throw new CoreInputError(`slot "${s.slot}": the linked mesh's source carries no mesh`);
|
|
260
|
+
uvs = [...source.uvs];
|
|
261
|
+
triangles = [...source.triangles];
|
|
262
|
+
hull = source.hull ?? null;
|
|
263
|
+
}
|
|
264
|
+
const sequenceIndex = s.frame ?? record?.sequenceSetup ?? 0;
|
|
265
|
+
drawn.push({ slot: s.slot, attachment: row[1], kind: row[2], vertices: row[3].map((v) => finite(v, `vertex of slot "${s.slot}"`)), uvs, triangles, hull, sequenceIndex, colour });
|
|
266
|
+
}
|
|
267
|
+
const clips = geometry.clips.map((c): RawClip => [c[0], c[1], c[2], c[3].map((v) => finite(v, `clip vertex of slot "${c[0]}"`))]);
|
|
268
|
+
const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], c[3].map((v) => finite(v, `clipped vertex of slot "${c[0]}"`)), c[4].map((v) => finite(v, `clipped uv of slot "${c[0]}"`)), c[5]]);
|
|
269
|
+
return { drawn, clips, clipped };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The setup pose as the renderer poses a skeleton with no animation — the
|
|
274
|
+
* setup pose, every physics state reset — in full doubles. Refused by name
|
|
275
|
+
* where the core leaves a block of it out.
|
|
276
|
+
*/
|
|
277
|
+
export function poseRawSetup(doc: CompiledDocument, plant: CorePlant = {}): RawPose {
|
|
278
|
+
const posed = poseSetup(doc, { ...plant, ...RAW_PLANT }, freshStepContext(plant.physicsStep));
|
|
279
|
+
// `setup.clipped` is the oracle's block, left out under a concave or inverse clip whose triangle list is the runtime's own; what the core draws there is `rawDrawn`'s to refuse or pose (issue #964).
|
|
280
|
+
const absent = posed.absent.filter(([block]) => block !== 'setup.clipped');
|
|
281
|
+
if (absent.length > 0) throw new CoreInputError(`the raw setup pose leaves ${absent.map(([b, why]) => `${b} out (${why})`).join('; ')}`);
|
|
282
|
+
const { setup, world, shown } = posed;
|
|
283
|
+
if (world === null || shown === null || setup.slots === null || setup.drawOrder === null) throw new CoreInputError('the raw setup pose was not posed');
|
|
284
|
+
const drawn = rawDrawn(doc, shown, world, setup.drawOrder, plant);
|
|
285
|
+
return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots: setup.slots, drawOrder: setup.drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, setup.drawOrder) };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* One animation walked at the caller's steps (the header's *One step*): the
|
|
290
|
+
* reset pose at 0, then one pose per step of `steps` — `steps.length + 1`
|
|
291
|
+
* poses in all. `src/render.ts`'s walk is `count` steps of `1/fps`;
|
|
292
|
+
* `src/deformmeasure.ts`'s is one step of `time`. A step must be a finite
|
|
293
|
+
* time at or above 0; the animation must be the document's; a construct the
|
|
294
|
+
* core would leave out refuses the call by name. `reset` is where the
|
|
295
|
+
* physics is reset (`RawReset`): `animation` for render's walk, `setup` for
|
|
296
|
+
* deformmeasure's one jump.
|
|
297
|
+
*/
|
|
298
|
+
export function poseRawAnimation(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant = {}, reset: RawReset = 'animation'): RawPose[] {
|
|
299
|
+
const anim = doc.animations.find((a) => a.name === animation);
|
|
300
|
+
if (anim === undefined) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(', ')}]`);
|
|
301
|
+
const bad = steps.findIndex((s) => !Number.isFinite(s) || s < 0);
|
|
302
|
+
if (bad >= 0) throw new CoreInputError(`step ${bad} is ${steps[bad]}, not a finite time at or above 0`);
|
|
303
|
+
const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
|
|
304
|
+
if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
|
|
305
|
+
const raw: TimelinePlant = { ...plant, ...RAW_PLANT };
|
|
306
|
+
const resolve = plant.shown ?? shownAttachment;
|
|
307
|
+
const duration = anim.timelines.duration;
|
|
308
|
+
const ctx = freshStepContext(plant.physicsStep);
|
|
309
|
+
const poses: RawPose[] = [];
|
|
310
|
+
let trackTime = 0;
|
|
311
|
+
let last = -1;
|
|
312
|
+
if (reset === 'setup') {
|
|
313
|
+
// The reset taken at the setup pose, before the animation is applied (`src/deformmeasure.ts`'s `poseAt`): pose 0 is the setup pose.
|
|
314
|
+
const setup = poseSetup(doc, { ...plant, ...RAW_PLANT }, ctx);
|
|
315
|
+
const absent = setup.absent.filter(([block]) => block !== 'setup.clipped');
|
|
316
|
+
if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join('; ')}`);
|
|
317
|
+
if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError('the raw walk\'s reset pose was not posed');
|
|
318
|
+
ctx.phase = 'update';
|
|
319
|
+
poses.push({ trackTime: 0, animationTime: 0, bones: rawBones(doc, setup.world), slots: setup.setup.slots, drawOrder: setup.setup.drawOrder, ...rawDrawn(doc, setup.shown, setup.world, setup.setup.drawOrder, plant), events: [], shown: drawOrderOf(setup.shown, setup.setup.drawOrder) });
|
|
320
|
+
}
|
|
321
|
+
for (let i = reset === 'setup' ? 1 : 0; i <= steps.length; i++) {
|
|
322
|
+
const dt = i === 0 ? 0 : steps[i - 1];
|
|
323
|
+
trackTime += dt;
|
|
324
|
+
const t = trackTime < duration ? trackTime : duration;
|
|
325
|
+
const before = ctx.time;
|
|
326
|
+
ctx.time += dt;
|
|
327
|
+
const sliders: SliderApplication[] = [];
|
|
328
|
+
const posed = posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
|
|
329
|
+
if (i === 0) ctx.phase = 'update';
|
|
330
|
+
const placeholders = new Map<string, string | null>();
|
|
331
|
+
const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
|
|
332
|
+
if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join('; ')}`);
|
|
333
|
+
const evalOrder = plant.drawOrder ?? drawOrderAt;
|
|
334
|
+
let order = evalOrder(doc.slots.length, anim.timelines.drawOrder, t) ?? doc.slots.map((_s, k) => k);
|
|
335
|
+
for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
336
|
+
const drawOrder = order.map((k) => doc.slots[k].name);
|
|
337
|
+
const states = attachmentStates(doc, resolve, placeholders, { timelines: anim.timelines, t }, sliders, plant);
|
|
338
|
+
if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
|
|
339
|
+
const rank = new Map(drawOrder.map((n, r) => [n, r]));
|
|
340
|
+
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
341
|
+
const drawn = rawDrawn(doc, shown, posed.world, drawOrder, plant);
|
|
342
|
+
const events = (plant.events ?? eventsFired)(anim.timelines.events, last, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]);
|
|
343
|
+
last = t;
|
|
344
|
+
poses.push({ trackTime, animationTime: t, bones: rawBones(doc, posed.world), slots: slots.rows, drawOrder, ...drawn, events, shown });
|
|
345
|
+
}
|
|
346
|
+
return poses;
|
|
347
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skin a document is posed under (issue #932): every skin merged — the
|
|
3
|
+
* oracle's `--skin all` — or one skin by name, `--skin <name>`. Each rule
|
|
4
|
+
* below was measured by posing hand-written skeletons through
|
|
5
|
+
* `tools/pose_oracle.ts dump --skin <name>` (spine-core 4.3.13) and reading
|
|
6
|
+
* the rows it printed; the core suite's `CN` controls hold the same
|
|
7
|
+
* skeletons against the core at tolerance 0.
|
|
8
|
+
*
|
|
9
|
+
* - **What a slot shows under a named skin**: the named skin's record for
|
|
10
|
+
* the placeholder, else the default skin's, else nothing. Three skins
|
|
11
|
+
* `default`, `s1`, `s2` over five slots — a placeholder the default and
|
|
12
|
+
* `s1` and `s2` fill, one the default alone fills, one `s1` alone, one no
|
|
13
|
+
* skin, one `s1` and the default — dumped under `default`, `s1` and `s2`
|
|
14
|
+
* in two file orders (`default, s1, s2` and `s2, s1, default`): `s1`
|
|
15
|
+
* showed `s1`'s record where it filled the placeholder and the default's
|
|
16
|
+
* where only the default did; `s2` showed `s2`'s, then the default's, and
|
|
17
|
+
* nothing for the placeholder only `s1` fills; a placeholder no skin fills
|
|
18
|
+
* showed nothing under every skin; the two file orders read the same row
|
|
19
|
+
* under every named skin. An attachment key switching the slot at a
|
|
20
|
+
* sample resolves the same way. So a named skin has no order question:
|
|
21
|
+
* nothing a placeholder shows depends on the file's skin order, which the
|
|
22
|
+
* model does not hold (the slots' ⚠️ in `./index.ts`).
|
|
23
|
+
* - **The bones a named skin activates**: a skin-required bone is active
|
|
24
|
+
* exactly when the NAMED skin names it or a bone below it — the default
|
|
25
|
+
* skin's `bones` list does not count. Measured on the same probe: a
|
|
26
|
+
* skin-required bone only the default names read active under `default`
|
|
27
|
+
* and inactive under `s1` and `s2`; one `s1` names, and its skin-required
|
|
28
|
+
* parent, active under `s1` only; a bone that is not skin-required under
|
|
29
|
+
* an inactive one stayed active under every skin.
|
|
30
|
+
* - **The constraints a named skin applies**: a skin-required constraint of
|
|
31
|
+
* any kind is applied exactly when the named skin's list for its kind names
|
|
32
|
+
* it — the default skin's list does not count either (`./constraints.ts`'s
|
|
33
|
+
* header, the nine readings per kind).
|
|
34
|
+
*
|
|
35
|
+
* - **A slot on an inactive bone is not animated** (the commander's private
|
|
36
|
+
* finding on this issue: a 19-skin rig read DIFF on 18 of its 19 skin
|
|
37
|
+
* runs). A slot whose bone the applied skin leaves inactive keeps its
|
|
38
|
+
* setup colour, dark colour and attachment under every slot timeline —
|
|
39
|
+
* `rgba`, `rgb`, `alpha`, `rgba2`, `rgb2` and `attachment` — whether an
|
|
40
|
+
* animation plays it or a slider applies it, and a deform timeline on its
|
|
41
|
+
* attachment does not apply either (a mesh weighted to an ACTIVE bone, so
|
|
42
|
+
* the deform would show: moved by 5 under `all` and `s1`, unmoved under
|
|
43
|
+
* `default`, which leaves the slot's bone inactive). Measured on
|
|
44
|
+
* hand-written skeletons under `--skin all`, `s1` (naming the bone) and
|
|
45
|
+
* `default` (not): each timeline moved the slot under the first two and
|
|
46
|
+
* not under the third, and the setup row read the setup under all three
|
|
47
|
+
* (a slider's key included). A slot on an ACTIVE bone that shows nothing
|
|
48
|
+
* (no skin holds its placeholder) IS animated: every colour key applied,
|
|
49
|
+
* and an attachment key switched it. A draw-order key moves a slot on an
|
|
50
|
+
* inactive bone like any other. A sequence timeline is held by the same
|
|
51
|
+
* gate; its effect on such a slot is not observable in the dump — a
|
|
52
|
+
* region's corners on an inactive bone are all zeros whatever the frame.
|
|
53
|
+
* So the predicate is the slot bone's activity (`slotTimelinesApply`),
|
|
54
|
+
* not what the slot shows; under `all` every bone some skin names is
|
|
55
|
+
* active, so a one-skin rig is unchanged.
|
|
56
|
+
*
|
|
57
|
+
* Under `all` every skin is applied at once: `bones` and the constraint
|
|
58
|
+
* lists of every skin count, and a placeholder several skins fill shows the
|
|
59
|
+
* LAST of them in the Spine file's order — the case `./index.ts` leaves out
|
|
60
|
+
* by name, and the one the per-skin dumps exist to judge.
|
|
61
|
+
*/
|
|
62
|
+
import type { ModelSlot } from '../model.ts';
|
|
63
|
+
import type { CompiledDocument, CoreSkin } from './index.ts';
|
|
64
|
+
|
|
65
|
+
/** The skin option that merges every skin — the oracle's `--skin all`. */
|
|
66
|
+
export const CORE_ALL_SKINS = 'all';
|
|
67
|
+
|
|
68
|
+
/** The skin a document names its default skin with — Spine's `SkeletonData.defaultSkin` is the skin of this name. */
|
|
69
|
+
export const CORE_DEFAULT_SKIN = 'default';
|
|
70
|
+
|
|
71
|
+
/** The skins whose `bones` and constraint lists are applied: every skin under `all`, the named skin alone otherwise (the header's measurement). */
|
|
72
|
+
export function appliedSkins(doc: CompiledDocument): CoreSkin[] {
|
|
73
|
+
return doc.skin === CORE_ALL_SKINS ? doc.skins : doc.skins.filter((k) => k.name === doc.skin);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Under a named skin, the skins a placeholder is looked up in, in precedence order: the named skin, then the default skin (the header's measurement). */
|
|
77
|
+
export function lookupSkins(doc: CompiledDocument): CoreSkin[] {
|
|
78
|
+
if (doc.skin === CORE_ALL_SKINS) return doc.skins;
|
|
79
|
+
const named = doc.skins.find((k) => k.name === doc.skin);
|
|
80
|
+
const fallback = doc.skin === CORE_DEFAULT_SKIN ? undefined : doc.skins.find((k) => k.name === CORE_DEFAULT_SKIN);
|
|
81
|
+
return [named, fallback].filter((k): k is CoreSkin => k !== undefined);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The skins that could show a slot's placeholder: under `all` every skin
|
|
86
|
+
* filling it — several is the file-order question — and under a named skin
|
|
87
|
+
* the one it resolves to, or none.
|
|
88
|
+
*/
|
|
89
|
+
export function fillingSkins(doc: CompiledDocument, slot: string, placeholder: string): string[] {
|
|
90
|
+
if (doc.skin === CORE_ALL_SKINS) return doc.skins.filter((k) => k.attachments[slot]?.[placeholder] !== undefined).map((k) => k.name);
|
|
91
|
+
const first = lookupSkins(doc).find((k) => k.attachments[slot]?.[placeholder] !== undefined);
|
|
92
|
+
return first === undefined ? [] : [first.name];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether a slot's timelines — its colour and attachment timelines, and the deform and sequence timelines on what it shows — apply: `slotTimelinesApply` unless a plant passes another. */
|
|
96
|
+
export type SlotTimelineGate = (doc: CompiledDocument, slot: ModelSlot, active: ReadonlySet<string>) => boolean;
|
|
97
|
+
|
|
98
|
+
/** A slot's timelines apply exactly when its bone is active under the skin posed (the header's measurement). */
|
|
99
|
+
export const slotTimelinesApply: SlotTimelineGate = (_doc, slot, active) => active.has(slot.bone);
|
|
100
|
+
|
|
101
|
+
/** Whether an applied skin's list for `kind` names the constraint — what applies a skin-required one (`./constraints.ts`'s header). */
|
|
102
|
+
export function listedByAppliedSkin(doc: CompiledDocument, kind: keyof CoreSkin['constraints'], name: string): boolean {
|
|
103
|
+
return appliedSkins(doc).some((k) => k.constraints[kind].includes(name));
|
|
104
|
+
}
|