spine-rigc 2.53.0 → 2.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/AUTHORING.md +266 -18
- package/package.json +1 -1
- package/src/assertions/bodies/a23.ts +254 -5
- package/src/check.ts +12 -0
- package/src/cli/core_commands.ts +168 -8
- package/src/cli/shared.ts +20 -1
- package/src/core/raw.ts +29 -6
- package/src/environment.ts +193 -0
- package/src/passthrough.ts +575 -0
- package/src/render.ts +15 -1
- package/src/render_core.ts +10 -6
- package/src/render_shared.ts +37 -2
- package/src/timelines.ts +130 -23
- package/src/types.ts +4 -3
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a cloth slot passes through a leg slot — the reading behind
|
|
3
|
+
* `rigc render --pass-through <cloth>:<leg>[,…]` (issue #1413).
|
|
4
|
+
*
|
|
5
|
+
* ⭐ **A reading, not a gate**, as `--folds` is. A skirt drawn over a thigh can
|
|
6
|
+
* swing its side straight through the thigh on a build that passes every
|
|
7
|
+
* assertion and every check, and until this it was found by a person looking at
|
|
8
|
+
* the right frame. A real skirt cannot do that while it is in front of the leg:
|
|
9
|
+
* the leg is inside it and leaves through the hem. So this names the frames where
|
|
10
|
+
* the cloth's drawn side lies inside the leg's drawn outline, and decides nothing.
|
|
11
|
+
*
|
|
12
|
+
* ## What "passes through" means here
|
|
13
|
+
*
|
|
14
|
+
* At a frame where both slots draw and the cloth slot draws **over** the leg slot
|
|
15
|
+
* (the frame's own draw order, so a draw-order key is honoured — a leg drawn over
|
|
16
|
+
* the cloth is not read at that frame):
|
|
17
|
+
*
|
|
18
|
+
* 1. **The cloth's rows are its setup pose's.** The cloth is drawn onto a grid in
|
|
19
|
+
* its own setup-pose geometry (the rest table's vertices: the setup pose's
|
|
20
|
+
* bones, no deform), at the frame's page UVs and tint. A cell is drawn when the
|
|
21
|
+
* renderer's own traversal (`rasterisePiece`) paints its centre at half
|
|
22
|
+
* opacity or more — the half-opacity line is where an antialiased edge sits.
|
|
23
|
+
* The cell is one texel of the cloth's drawing as the setup pose lays it out
|
|
24
|
+
* (`texelSize`), so the grid is the drawing's own resolution, not a number
|
|
25
|
+
* chosen here. Reading the rows in the setup pose's frame rather than the
|
|
26
|
+
* world's is what keeps a cloth swung or bent by the pose readable: its side
|
|
27
|
+
* is still its side. Read in world rows, a waist or hem the pose tilts becomes
|
|
28
|
+
* a "side" on the rows it spans, and a leg under it reads as passing through —
|
|
29
|
+
* measured on the suite's probe, where a 2.5° swing read a run of 16 off the
|
|
30
|
+
* tilted waist (`PX11`).
|
|
31
|
+
* 2. **The hem's corner bounds the rows.** On each side, the corner is the
|
|
32
|
+
* outermost drawn cell in the lower half of the cloth's drawn rows, the lowest
|
|
33
|
+
* on a tie. Rows from the corner up to the cloth's top are read; rows below it
|
|
34
|
+
* are the hem's own curve, under which a leg is meant to show.
|
|
35
|
+
* 3. **The run.** On a read row, from the centre of the outermost drawn cell as
|
|
36
|
+
* the frame poses it, step outward in **world units** — along the setup pose's
|
|
37
|
+
* outward direction carried to the frame by the affine map of the cloth
|
|
38
|
+
* triangle that holds that centre, normalised — one step at a time, and count
|
|
39
|
+
* the steps whose world point the leg's drawing covers at half opacity, until
|
|
40
|
+
* the first it does not. A step is one texel of the **coarser** of the two
|
|
41
|
+
* drawings at the setup pose, so the run is in the outlines' own resolution
|
|
42
|
+
* and in the units the picture shows. That count is how far the leg's outline
|
|
43
|
+
* passes the cloth's outline on that row, in steps.
|
|
44
|
+
* 4. **A frame passes through** when some read row's run is more than one step
|
|
45
|
+
* (`PASS_THROUGH_RUN` = 2): one step is the resolution of the outlines themselves.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ The run is in world steps, not in the cloth's setup-pose cells, for a
|
|
48
|
+
* reason that was measured (issue #1413). Stepped in setup cells, a cloth
|
|
49
|
+
* squashed at a frame reads a longer run than the picture holds: the line
|
|
50
|
+
* printed a depth in units over a box narrower than that depth. The rows are the
|
|
51
|
+
* setup pose's because they say *which* part of the cloth is its side; the run
|
|
52
|
+
* is the world's because it says *how far* the leg shows.
|
|
53
|
+
*
|
|
54
|
+
* Its known blind spots, stated rather than discovered: a cloth whose side widens
|
|
55
|
+
* all the way to its waist has its corner at mid-height and the rows below it
|
|
56
|
+
* unread; a leg the cloth's side never meets on a read row (the leg wholly beside
|
|
57
|
+
* the cloth, entering above its top) has no run to count; and the rows are the
|
|
58
|
+
* setup pose's, so a cloth authored lying on its side reads its side as a hem.
|
|
59
|
+
*
|
|
60
|
+
* Pure: no clock, no randomness, no runtime. The pieces, the attachments and the
|
|
61
|
+
* rest table are handed in by whichever poser drew the frames.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import type { Plate } from '../tools/plate.ts';
|
|
65
|
+
import { type AttachmentPose, type AttachmentRest, type Piece, projector, rasterisePiece, type Viewport } from './render_shared.ts';
|
|
66
|
+
import { hypot2 } from './numeric.ts';
|
|
67
|
+
|
|
68
|
+
/** The reading's file name inside an animation's frame directory, and its format tag. */
|
|
69
|
+
export const PASS_THROUGH_FILE = 'pass-through.json';
|
|
70
|
+
export const PASS_THROUGH_SPEC = 'rigc-pass-through/1';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* How many steps a run must reach for a frame to pass through: more than one.
|
|
74
|
+
* One step is the outlines' own resolution — the cloth's outermost drawn cell and
|
|
75
|
+
* the leg's first covered step can sit one step apart on two edges that only
|
|
76
|
+
* touch — so a run of one is the outlines meeting, and two is the leg showing.
|
|
77
|
+
*/
|
|
78
|
+
export const PASS_THROUGH_RUN = 2;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The opacity, of 255, a cell's centre must be painted at to count as drawn: the
|
|
82
|
+
* half-opacity line, where an antialiased edge is. The renderer paints anything
|
|
83
|
+
* above 0.5 of 255, which would put the outline on the faintest fringe of the
|
|
84
|
+
* art; `rasterisePiece` reports the painted alpha rounded, and this reads it.
|
|
85
|
+
*/
|
|
86
|
+
export const DRAWN_ALPHA = 128;
|
|
87
|
+
|
|
88
|
+
/** One pair as the flag names it: the cloth that should hold the leg, and the leg. */
|
|
89
|
+
export interface PassPair {
|
|
90
|
+
cloth: string;
|
|
91
|
+
leg: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A malformed `--pass-through` value, or a pair the poses cannot be read for — said by name. */
|
|
95
|
+
export class PassThroughError extends Error {}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `--pass-through`'s value — `<cloth>:<leg>[,<cloth>:<leg>…]` — read against
|
|
99
|
+
* the skeleton's slots, or refused naming the entry: an empty entry, one without
|
|
100
|
+
* exactly one `:`, an empty name, a slot the skeleton does not have (with the
|
|
101
|
+
* slots it does), and a pair named twice. A cloth paired with itself is read, not
|
|
102
|
+
* refused: a slot never draws over itself, so it reads 0 frames and says why.
|
|
103
|
+
*
|
|
104
|
+
* The order is the declaration: the first name is the cloth, which must draw over
|
|
105
|
+
* the second for a frame to be read. `a:b` and `b:a` are two readings, never one
|
|
106
|
+
* read both ways.
|
|
107
|
+
*/
|
|
108
|
+
export function readPassPairs(raw: string, slots: readonly string[]): PassPair[] {
|
|
109
|
+
const form = '<cloth slot>:<leg slot>[,<cloth slot>:<leg slot>…]';
|
|
110
|
+
const pairs: PassPair[] = [];
|
|
111
|
+
const entries = raw.split(',');
|
|
112
|
+
for (let i = 0; i < entries.length; i++) {
|
|
113
|
+
const entry = entries[i].trim();
|
|
114
|
+
if (entry === '') throw new PassThroughError(`--pass-through ${JSON.stringify(raw)}: entry ${i} is empty — it takes ${form}`);
|
|
115
|
+
const parts = entry.split(':');
|
|
116
|
+
if (parts.length !== 2) {
|
|
117
|
+
throw new PassThroughError(`--pass-through: entry ${i} ${JSON.stringify(entry)} has ${parts.length - 1} ":" — it takes ${form}, one ":" per pair`);
|
|
118
|
+
}
|
|
119
|
+
const [cloth, leg] = parts.map((p) => p.trim());
|
|
120
|
+
for (const [role, name] of [['cloth', cloth], ['leg', leg]] as const) {
|
|
121
|
+
if (name === '') throw new PassThroughError(`--pass-through: entry ${i} ${JSON.stringify(entry)} names no ${role} slot — it takes ${form}`);
|
|
122
|
+
if (!slots.includes(name)) {
|
|
123
|
+
throw new PassThroughError(`--pass-through: entry ${i} names ${role} slot ${JSON.stringify(name)}, which this skeleton does not have; its slots are [${slots.join(', ')}]`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const again = pairs.findIndex((p) => p.cloth === cloth && p.leg === leg);
|
|
127
|
+
if (again >= 0) throw new PassThroughError(`--pass-through: entry ${i} names "${cloth}:${leg}" again — entry ${again} already reads it`);
|
|
128
|
+
pairs.push({ cloth, leg });
|
|
129
|
+
}
|
|
130
|
+
return pairs;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One sampled frame: its index, its time, the pieces it draws in draw order, and every slot's whole attachment. */
|
|
134
|
+
export interface PassFrame {
|
|
135
|
+
index: number;
|
|
136
|
+
time: number;
|
|
137
|
+
pieces: readonly Piece[];
|
|
138
|
+
attachments: readonly AttachmentPose[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A world box `[minX, minY, maxX, maxY]`, world units, y up. */
|
|
142
|
+
export type PassBox = [number, number, number, number];
|
|
143
|
+
|
|
144
|
+
/** The side of the cloth a run is read on — its setup pose's left (−x) or right (+x). */
|
|
145
|
+
export type PassSide = 'left' | 'right';
|
|
146
|
+
|
|
147
|
+
/** One read frame's figures. */
|
|
148
|
+
export interface PassDepth {
|
|
149
|
+
index: number;
|
|
150
|
+
time: number;
|
|
151
|
+
/** The longest run on the frame, in steps. */
|
|
152
|
+
cells: number;
|
|
153
|
+
/** `cells` in world units: `cells × cell`. */
|
|
154
|
+
depth: number;
|
|
155
|
+
/** The step: one texel of the coarser of the two drawings at the setup pose, world units. */
|
|
156
|
+
cell: number;
|
|
157
|
+
/** The side the longest run is on; the left on a tie. */
|
|
158
|
+
side: PassSide;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A frame on which the cloth passes through the leg. */
|
|
162
|
+
export interface PassFrameReading extends PassDepth {
|
|
163
|
+
/** The cloth's attachment at the frame. */
|
|
164
|
+
attachment: string;
|
|
165
|
+
/** Read rows whose run reaches `PASS_THROUGH_RUN`, over both sides. */
|
|
166
|
+
rows: number;
|
|
167
|
+
/** The world box of every run of a row whose run reaches `PASS_THROUGH_RUN`: from the cloth's outline to the run's far end. */
|
|
168
|
+
box: PassBox;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** One pair's reading over one frame set. */
|
|
172
|
+
export interface PairReading {
|
|
173
|
+
cloth: string;
|
|
174
|
+
leg: string;
|
|
175
|
+
/** Frames on which both slots draw. */
|
|
176
|
+
drawn: number;
|
|
177
|
+
/** Of those, frames on which the cloth draws over the leg — the frames read. */
|
|
178
|
+
over: number;
|
|
179
|
+
/** Every frame that passes through, in frame order; empty when none does. */
|
|
180
|
+
frames: PassFrameReading[];
|
|
181
|
+
/** The read frame with the longest run — the earliest on a tie — or `null` when no frame is read. */
|
|
182
|
+
deepest: PassDepth | null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `pass-through.json`: one frame set's reading, every pair the flag names, in the flag's order. */
|
|
186
|
+
export interface PassThroughFile {
|
|
187
|
+
spec: typeof PASS_THROUGH_SPEC;
|
|
188
|
+
/** The animation, or `null` for a skeleton with none (its one frame is the setup pose). */
|
|
189
|
+
animation: string | null;
|
|
190
|
+
skin?: string;
|
|
191
|
+
fps: number;
|
|
192
|
+
/** What the numbers mean, stated in the file so a reader does not need this module. */
|
|
193
|
+
reading: { read: string; drawn: string; rows: string; run: string; passes: string; box: string };
|
|
194
|
+
pairs: PairReading[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A planted defect, for the suite's mutants only (`PT` controls in
|
|
199
|
+
* `selftest.ts`). Production passes none.
|
|
200
|
+
*/
|
|
201
|
+
export interface PassPlant {
|
|
202
|
+
/** Read a frame whichever slot draws over the other. */
|
|
203
|
+
ignoreDrawOrder?: boolean;
|
|
204
|
+
/** The run a frame needs to pass through, in steps, rather than `PASS_THROUGH_RUN`. */
|
|
205
|
+
run?: number;
|
|
206
|
+
/** Read the cloth's rows in the frame's world rather than in its setup pose. */
|
|
207
|
+
rowsInWorld?: boolean;
|
|
208
|
+
/** Step the run in the cloth's setup-pose cells carried to the frame, rather than in world units. */
|
|
209
|
+
cellSteps?: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Twice the signed area of triangle (a, b, c) of `xy`. */
|
|
213
|
+
function cross(xy: ArrayLike<number>, a: number, b: number, c: number): number {
|
|
214
|
+
return (xy[2 * b] - xy[2 * a]) * (xy[2 * c + 1] - xy[2 * a + 1]) - (xy[2 * c] - xy[2 * a]) * (xy[2 * b + 1] - xy[2 * a + 1]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A piece's triangles: a mesh's own, a region's the runtime's `0 1 2 2 3 0`. */
|
|
218
|
+
function trianglesOf(piece: Piece): ArrayLike<number> {
|
|
219
|
+
return piece.kind === 'mesh' ? piece.triangles : [0, 1, 2, 2, 3, 0];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* One texel of a drawing as `vertices` lay it out, in world units: the square
|
|
224
|
+
* root of the drawing's world area over its texel area, summed over its
|
|
225
|
+
* triangles — the page UVs times the page's size. `null` when either is 0.
|
|
226
|
+
*/
|
|
227
|
+
export function texelSize(piece: Piece, vertices: ArrayLike<number>, page: Plate): number | null {
|
|
228
|
+
const triangles = trianglesOf(piece);
|
|
229
|
+
let world = 0;
|
|
230
|
+
let texels = 0;
|
|
231
|
+
for (let t = 0; t + 2 < triangles.length; t += 3) {
|
|
232
|
+
const [a, b, c] = [triangles[t], triangles[t + 1], triangles[t + 2]];
|
|
233
|
+
world += Math.abs(cross(vertices, a, b, c));
|
|
234
|
+
texels += Math.abs(cross(piece.uvs, a, b, c)) * page.width * page.height;
|
|
235
|
+
}
|
|
236
|
+
if (!(world > 0) || !(texels > 0)) return null;
|
|
237
|
+
return Math.sqrt(world / texels);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The cells `piece` draws at half opacity or more over `viewport`, row major, 1 where drawn. */
|
|
241
|
+
function drawnCells(piece: Piece, page: Plate, viewport: Viewport): Uint8Array {
|
|
242
|
+
const cells = new Uint8Array(viewport.width * viewport.height);
|
|
243
|
+
rasterisePiece(page, piece, projector(viewport), viewport, (px, py, _r, _g, _b, a) => {
|
|
244
|
+
if (a >= DRAWN_ALPHA) cells[py * viewport.width + px] = 1;
|
|
245
|
+
});
|
|
246
|
+
return cells;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Whether `piece` draws the world point (x, y) at half opacity or more — the
|
|
251
|
+
* renderer's own traversal over a one-cell grid centred on the point, so the
|
|
252
|
+
* test is the frame's pixel test and nothing re-derived beside it.
|
|
253
|
+
*/
|
|
254
|
+
function drawsAt(piece: Piece, page: Plate, x: number, y: number, cell: number): boolean {
|
|
255
|
+
const viewport: Viewport = { minX: x - cell / 2, minY: y - cell / 2, maxX: x + cell / 2, maxY: y + cell / 2, scale: 1 / cell, width: 1, height: 1 };
|
|
256
|
+
let drawn = false;
|
|
257
|
+
rasterisePiece(page, piece, projector(viewport), viewport, (_px, _py, _r, _g, _b, a) => {
|
|
258
|
+
if (a >= DRAWN_ALPHA) drawn = true;
|
|
259
|
+
});
|
|
260
|
+
return drawn;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The world box of `xy`. */
|
|
264
|
+
function boxOf(xy: ArrayLike<number>): PassBox {
|
|
265
|
+
const box: PassBox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
266
|
+
for (let i = 0; i + 1 < xy.length; i += 2) {
|
|
267
|
+
box[0] = Math.min(box[0], xy[i]);
|
|
268
|
+
box[1] = Math.min(box[1], xy[i + 1]);
|
|
269
|
+
box[2] = Math.max(box[2], xy[i]);
|
|
270
|
+
box[3] = Math.max(box[3], xy[i + 1]);
|
|
271
|
+
}
|
|
272
|
+
return box;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The affine map of the cloth triangle holding the setup-pose point (x, y):
|
|
277
|
+
* setup pose → frame, extended past the triangle's edges by the same map. The
|
|
278
|
+
* triangle is the first, by index, whose barycentric weights are all at least
|
|
279
|
+
* −1e-9 — a point on a shared edge is in both, and the lower index is chosen so
|
|
280
|
+
* one build reads one way. `null` when no triangle holds it, or the one that does
|
|
281
|
+
* has collapsed at the setup pose or at the frame.
|
|
282
|
+
*/
|
|
283
|
+
function mapAt(
|
|
284
|
+
triangles: ArrayLike<number>,
|
|
285
|
+
rest: ArrayLike<number>,
|
|
286
|
+
posed: ArrayLike<number>,
|
|
287
|
+
x: number,
|
|
288
|
+
y: number,
|
|
289
|
+
): ((px: number, py: number) => [number, number]) | null {
|
|
290
|
+
for (let t = 0; t + 2 < triangles.length; t += 3) {
|
|
291
|
+
const [a, b, c] = [triangles[t], triangles[t + 1], triangles[t + 2]];
|
|
292
|
+
const area = cross(rest, a, b, c);
|
|
293
|
+
if (area === 0) continue;
|
|
294
|
+
const weights = (px: number, py: number): [number, number, number] => {
|
|
295
|
+
const wa = ((rest[2 * b] - px) * (rest[2 * c + 1] - py) - (rest[2 * c] - px) * (rest[2 * b + 1] - py)) / area;
|
|
296
|
+
const wb = ((rest[2 * c] - px) * (rest[2 * a + 1] - py) - (rest[2 * a] - px) * (rest[2 * c + 1] - py)) / area;
|
|
297
|
+
return [wa, wb, 1 - wa - wb];
|
|
298
|
+
};
|
|
299
|
+
const [wa, wb, wc] = weights(x, y);
|
|
300
|
+
if (wa < -1e-9 || wb < -1e-9 || wc < -1e-9) continue;
|
|
301
|
+
if (cross(posed, a, b, c) === 0) return null;
|
|
302
|
+
return (px, py) => {
|
|
303
|
+
const [ka, kb, kc] = weights(px, py);
|
|
304
|
+
return [ka * posed[2 * a] + kb * posed[2 * b] + kc * posed[2 * c], ka * posed[2 * a + 1] + kb * posed[2 * b + 1] + kc * posed[2 * c + 1]];
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** What one read frame found: its longest run, its side, and the runs that reach the line. */
|
|
311
|
+
interface FrameRuns {
|
|
312
|
+
cells: number;
|
|
313
|
+
side: PassSide;
|
|
314
|
+
rows: number;
|
|
315
|
+
box: PassBox | null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* One frame's runs: `cloth` drawn over `leg`, `rest` the cloth attachment's
|
|
320
|
+
* setup-pose vertices, `cell` its texel at the setup pose. See the module comment.
|
|
321
|
+
*/
|
|
322
|
+
function runsAt(
|
|
323
|
+
cloth: Piece,
|
|
324
|
+
clothPage: Plate,
|
|
325
|
+
rest: ArrayLike<number>,
|
|
326
|
+
cell: number,
|
|
327
|
+
stepSize: number,
|
|
328
|
+
leg: Piece,
|
|
329
|
+
legPage: Plate,
|
|
330
|
+
need: number,
|
|
331
|
+
plant: PassPlant,
|
|
332
|
+
): FrameRuns {
|
|
333
|
+
const [x0, y0, x1, y1] = boxOf(rest);
|
|
334
|
+
// On multiples of the cell from the world origin, one cell of margin all round, so one build grids one way.
|
|
335
|
+
const minX = (Math.floor(x0 / cell) - 1) * cell;
|
|
336
|
+
const maxY = (Math.ceil(y1 / cell) + 1) * cell;
|
|
337
|
+
const width = Math.ceil((x1 - minX) / cell) + 1;
|
|
338
|
+
const height = Math.ceil((maxY - y0) / cell) + 1;
|
|
339
|
+
const viewport: Viewport = { minX, minY: maxY - height * cell, maxX: minX + width * cell, maxY, scale: 1 / cell, width, height };
|
|
340
|
+
const drawn = drawnCells({ ...cloth, world: Array.from(rest) }, clothPage, viewport);
|
|
341
|
+
const extent: Array<[number, number] | null> = [];
|
|
342
|
+
let top = -1;
|
|
343
|
+
let bottom = -1;
|
|
344
|
+
for (let py = 0; py < height; py++) {
|
|
345
|
+
let lo = -1;
|
|
346
|
+
let hi = -1;
|
|
347
|
+
for (let px = 0; px < width; px++) {
|
|
348
|
+
if (drawn[py * width + px] === 0) continue;
|
|
349
|
+
if (lo < 0) lo = px;
|
|
350
|
+
hi = px;
|
|
351
|
+
}
|
|
352
|
+
extent.push(lo < 0 ? null : [lo, hi]);
|
|
353
|
+
if (lo >= 0) {
|
|
354
|
+
if (top < 0) top = py;
|
|
355
|
+
bottom = py;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const found: FrameRuns = { cells: 0, side: 'left', rows: 0, box: null };
|
|
359
|
+
if (top < 0) return found;
|
|
360
|
+
const triangles = trianglesOf(cloth);
|
|
361
|
+
const legBox = boxOf(leg.world);
|
|
362
|
+
const reach = hypot2(legBox[2] - legBox[0], legBox[3] - legBox[1]);
|
|
363
|
+
let box: PassBox | null = null;
|
|
364
|
+
const take = (p: [number, number]): void => {
|
|
365
|
+
box ??= [Infinity, Infinity, -Infinity, -Infinity];
|
|
366
|
+
box[0] = Math.min(box[0], p[0]);
|
|
367
|
+
box[1] = Math.min(box[1], p[1]);
|
|
368
|
+
box[2] = Math.max(box[2], p[0]);
|
|
369
|
+
box[3] = Math.max(box[3], p[1]);
|
|
370
|
+
};
|
|
371
|
+
const runs: Record<PassSide, Array<{ cells: number; ends: [[number, number], [number, number]] }>> = { left: [], right: [] };
|
|
372
|
+
for (const side of ['left', 'right'] as const) {
|
|
373
|
+
const out = side === 'left' ? -1 : 1;
|
|
374
|
+
// The corner: the outermost drawn cell in the lower half of the drawn rows, the lowest on a tie.
|
|
375
|
+
const half = bottom - 0.5 * (bottom - top);
|
|
376
|
+
let corner = -1;
|
|
377
|
+
for (let py = bottom; py >= top && py >= half; py--) {
|
|
378
|
+
const row = extent[py];
|
|
379
|
+
if (row === null) continue;
|
|
380
|
+
const edge = side === 'left' ? row[0] : row[1];
|
|
381
|
+
const best = corner < 0 ? null : (extent[corner] as [number, number]);
|
|
382
|
+
if (best === null || (side === 'left' ? edge < best[0] : edge > best[1])) corner = py;
|
|
383
|
+
}
|
|
384
|
+
for (let py = corner; py >= top; py--) {
|
|
385
|
+
const row = extent[py];
|
|
386
|
+
if (row === null) continue;
|
|
387
|
+
const cx = minX + ((side === 'left' ? row[0] : row[1]) + 0.5) * cell;
|
|
388
|
+
const cy = maxY - (py + 0.5) * cell;
|
|
389
|
+
const map = mapAt(triangles, rest, cloth.world, cx, cy);
|
|
390
|
+
if (map === null) continue;
|
|
391
|
+
const [ox, oy] = map(cx, cy);
|
|
392
|
+
const [sx, sy] = map(cx + out * cell, cy);
|
|
393
|
+
const length = hypot2(sx - ox, sy - oy);
|
|
394
|
+
if (!(length > 0)) continue;
|
|
395
|
+
// Step k's world point: `stepSize` world units at a time along the outward direction, normalised — or, planted,
|
|
396
|
+
// one setup-pose cell at a time through the triangle's map, which a squashed cloth shortens.
|
|
397
|
+
const at = (k: number): [number, number] =>
|
|
398
|
+
plant.cellSteps ? map(cx + out * k * cell, cy) : [ox + ((sx - ox) / length) * k * stepSize, oy + ((sy - oy) / length) * k * stepSize];
|
|
399
|
+
const stride = plant.cellSteps ? length : stepSize;
|
|
400
|
+
const limit = Math.ceil(reach / stride) + 1;
|
|
401
|
+
let k = 0;
|
|
402
|
+
while (k < limit) {
|
|
403
|
+
const [wx, wy] = at(k + 1);
|
|
404
|
+
if (!drawsAt(leg, legPage, wx, wy, stride)) break;
|
|
405
|
+
k++;
|
|
406
|
+
}
|
|
407
|
+
runs[side].push({ cells: k, ends: [map(cx + out * 0.5 * cell, cy), at(k + 0.5)] });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const side of ['left', 'right'] as const) {
|
|
411
|
+
for (const run of runs[side]) {
|
|
412
|
+
if (run.cells > found.cells) {
|
|
413
|
+
found.cells = run.cells;
|
|
414
|
+
found.side = side;
|
|
415
|
+
}
|
|
416
|
+
if (run.cells >= need) {
|
|
417
|
+
found.rows++;
|
|
418
|
+
take(run.ends[0]);
|
|
419
|
+
take(run.ends[1]);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
found.box = box;
|
|
424
|
+
return found;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const restKey = (slot: string, attachment: string): string => `${slot}\u0000${attachment}`;
|
|
428
|
+
|
|
429
|
+
/** Whether two vertex lists are the same numbers — a piece drawn from its attachment whole, not cut by a clip. */
|
|
430
|
+
function sameVertices(a: ArrayLike<number>, b: ArrayLike<number>): boolean {
|
|
431
|
+
if (a.length !== b.length) return false;
|
|
432
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Every pair's reading over one frame set: `rest` the rest table, `frames` the
|
|
438
|
+
* frames as the sampler returned them (pieces in draw order, attachments whole),
|
|
439
|
+
* `pages` the atlas pages the pieces sample. Refused by name: a cloth a clip cuts
|
|
440
|
+
* at a frame, whose outline is no longer its attachment's, and a cloth whose
|
|
441
|
+
* drawing has no area.
|
|
442
|
+
*/
|
|
443
|
+
export function passThroughOf(
|
|
444
|
+
pairs: readonly PassPair[],
|
|
445
|
+
rest: readonly AttachmentRest[],
|
|
446
|
+
frames: readonly PassFrame[],
|
|
447
|
+
pages: ReadonlyMap<string, Plate>,
|
|
448
|
+
plant: PassPlant = {},
|
|
449
|
+
): PairReading[] {
|
|
450
|
+
const restOf = new Map(rest.map((r) => [restKey(r.slot, r.attachment), r]));
|
|
451
|
+
const need = plant.run ?? PASS_THROUGH_RUN;
|
|
452
|
+
const pageOf = (piece: Piece): Plate => {
|
|
453
|
+
const page = pages.get(piece.page);
|
|
454
|
+
if (page === undefined) throw new PassThroughError(`slot "${piece.slot}" samples atlas page "${piece.page}", which is not among [${[...pages.keys()].join(', ')}]`);
|
|
455
|
+
return page;
|
|
456
|
+
};
|
|
457
|
+
return pairs.map(({ cloth, leg }) => {
|
|
458
|
+
const reading: PairReading = { cloth, leg, drawn: 0, over: 0, frames: [], deepest: null };
|
|
459
|
+
for (const frame of frames) {
|
|
460
|
+
const ci = frame.pieces.findIndex((p) => p.slot === cloth);
|
|
461
|
+
const li = frame.pieces.findIndex((p) => p.slot === leg);
|
|
462
|
+
if (ci < 0 || li < 0) continue;
|
|
463
|
+
reading.drawn++;
|
|
464
|
+
if (!plant.ignoreDrawOrder && ci <= li) continue;
|
|
465
|
+
reading.over++;
|
|
466
|
+
const piece = frame.pieces[ci];
|
|
467
|
+
const pose = frame.attachments.find((a) => a.slot === cloth);
|
|
468
|
+
if (pose === undefined) throw new PassThroughError(`frame ${frame.index}: slot "${cloth}" draws, and the frame records no attachment for it`);
|
|
469
|
+
if (!sameVertices(piece.world, pose.vertices)) {
|
|
470
|
+
throw new PassThroughError(
|
|
471
|
+
`frame ${frame.index}: a clip cuts cloth slot "${cloth}" (attachment "${pose.attachment}"), so its drawn outline is no longer its attachment's and its rows cannot be read in its setup pose`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const restEntry = restOf.get(restKey(cloth, pose.attachment));
|
|
475
|
+
if (restEntry === undefined) throw new PassThroughError(`frame ${frame.index}: the rest table has no entry for slot "${cloth}" attachment "${pose.attachment}"`);
|
|
476
|
+
const restVertices = plant.rowsInWorld ? piece.world : restEntry.vertices;
|
|
477
|
+
const clothPage = pageOf(piece);
|
|
478
|
+
const cell = texelSize(piece, restVertices, clothPage);
|
|
479
|
+
if (cell === null) throw new PassThroughError(`frame ${frame.index}: cloth slot "${cloth}" (attachment "${pose.attachment}") draws no area at its setup pose, so it has no texel to grid its rows by`);
|
|
480
|
+
// The leg's texel at its setup pose, off its own rest entry — so a leg a clip cuts is refused as the cloth is.
|
|
481
|
+
const legPiece = frame.pieces[li];
|
|
482
|
+
const legPose = frame.attachments.find((a) => a.slot === leg);
|
|
483
|
+
if (legPose === undefined) throw new PassThroughError(`frame ${frame.index}: slot "${leg}" draws, and the frame records no attachment for it`);
|
|
484
|
+
if (!sameVertices(legPiece.world, legPose.vertices)) {
|
|
485
|
+
throw new PassThroughError(`frame ${frame.index}: a clip cuts leg slot "${leg}" (attachment "${legPose.attachment}"), so its drawing's texel cannot be read off its setup pose`);
|
|
486
|
+
}
|
|
487
|
+
const legRest = restOf.get(restKey(leg, legPose.attachment));
|
|
488
|
+
if (legRest === undefined) throw new PassThroughError(`frame ${frame.index}: the rest table has no entry for slot "${leg}" attachment "${legPose.attachment}"`);
|
|
489
|
+
const legPage = pageOf(legPiece);
|
|
490
|
+
const legCell = texelSize(legPiece, legRest.vertices, legPage);
|
|
491
|
+
if (legCell === null) throw new PassThroughError(`frame ${frame.index}: leg slot "${leg}" (attachment "${legPose.attachment}") draws no area at its setup pose, so it has no texel to step by`);
|
|
492
|
+
const step = Math.max(cell, legCell);
|
|
493
|
+
const runs = runsAt(piece, clothPage, restVertices, cell, step, legPiece, legPage, need, plant);
|
|
494
|
+
const depth: PassDepth = { index: frame.index, time: frame.time, cells: runs.cells, depth: runs.cells * step, cell: step, side: runs.side };
|
|
495
|
+
if (reading.deepest === null || depth.cells > reading.deepest.cells) reading.deepest = depth;
|
|
496
|
+
if (runs.cells >= need && runs.box !== null) reading.frames.push({ ...depth, attachment: pose.attachment, rows: runs.rows, box: runs.box });
|
|
497
|
+
}
|
|
498
|
+
return reading;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** What `pass-through.json` states its numbers mean — the one wording the file and the docs share. */
|
|
503
|
+
export const PASS_THROUGH_READING: PassThroughFile['reading'] = {
|
|
504
|
+
read: "a frame is read when both slots draw and the cloth slot draws over the leg slot in that frame's draw order; a frame where the leg draws over the cloth is not read",
|
|
505
|
+
drawn: `a point is drawn when the renderer paints it at ${DRAWN_ALPHA} of 255 or more (half opacity) — the slot's art through its page, tint included`,
|
|
506
|
+
rows:
|
|
507
|
+
"the cloth's rows are cells of one texel of its drawing at its setup pose (the rest table's vertices), on multiples of that texel from the world origin; on each side its corner is the outermost drawn cell in the lower half of its drawn rows, the lowest on a tie, and the rows from the corner up to its top are read",
|
|
508
|
+
run: "on a read row, the number of steps outward from the outermost drawn cell's centre as the frame poses it — in world units, along the setup pose's outward direction carried to the frame by the cloth triangle holding that centre — whose world point the leg draws, up to the first it does not: how far the leg's outline passes the cloth's. A step (cell) is one texel of the coarser of the two drawings at the setup pose, in world units; depth is the run times the step",
|
|
509
|
+
passes: `a frame passes through when some read row's run is ${PASS_THROUGH_RUN} steps or more — more than the one step an outline is resolved to`,
|
|
510
|
+
box: "a passing frame's box is [minX, minY, maxX, maxY] in world units (y up) over both ends of every run of that frame that reaches the line — the cloth's outline and the run's far end",
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
/** The file for one frame set. */
|
|
514
|
+
export function passThroughFileOf(animation: string | null, fps: number, skin: string | undefined, pairs: PairReading[]): PassThroughFile {
|
|
515
|
+
return { spec: PASS_THROUGH_SPEC, animation, ...(skin === undefined ? {} : { skin }), fps, reading: PASS_THROUGH_READING, pairs };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** The file's text: two-space JSON and a newline, so one build and one request are one byte sequence. */
|
|
519
|
+
export function passThroughText(file: PassThroughFile): string {
|
|
520
|
+
return `${JSON.stringify(file, null, 2)}\n`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Ascending indices as runs: `[3, 4, 5, 9]` → `3-5, 9`. */
|
|
524
|
+
function indexRuns(indices: readonly number[]): string {
|
|
525
|
+
const out: string[] = [];
|
|
526
|
+
for (let i = 0; i < indices.length; ) {
|
|
527
|
+
let j = i;
|
|
528
|
+
while (j + 1 < indices.length && indices[j + 1] === indices[j] + 1) j++;
|
|
529
|
+
out.push(j === i ? String(indices[i]) : `${indices[i]}-${indices[j]}`);
|
|
530
|
+
i = j + 1;
|
|
531
|
+
}
|
|
532
|
+
return out.join(', ');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** `x` rounded to hundredths away from the box's inside, so the printed box holds what the exact one does. */
|
|
536
|
+
const outward = (x: number, up: boolean): string => ((up ? Math.ceil(x * 100) : Math.floor(x * 100)) / 100).toFixed(2);
|
|
537
|
+
|
|
538
|
+
/** A box as the lines print it, rounded outward to hundredths: `x [x0, x1] y [y0, y1] (w × h)`. */
|
|
539
|
+
function boxText([x0, y0, x1, y1]: PassBox): string {
|
|
540
|
+
return `x [${outward(x0, false)}, ${outward(x1, true)}] y [${outward(y0, false)}, ${outward(y1, true)}] (${(x1 - x0).toFixed(2)} × ${(y1 - y0).toFixed(2)})`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** A depth as the lines print it: `D units (C cells of T)`. */
|
|
544
|
+
function depthText(d: PassDepth): string {
|
|
545
|
+
return `${d.depth.toFixed(2)} units (${d.cells} step(s) of ${d.cell.toFixed(4)}) on its ${d.side} side`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The summary line for one pair over one set — printed for every pair, so a pair
|
|
550
|
+
* that reads 0 says so and says whether it was read at all.
|
|
551
|
+
*/
|
|
552
|
+
export function passSummaryLine(animation: string | null, fps: number, pair: PairReading): string {
|
|
553
|
+
const where = animation === null ? 'the setup pose' : JSON.stringify(animation);
|
|
554
|
+
const head = `${where}: cloth "${pair.cloth}" over leg "${pair.leg}"`;
|
|
555
|
+
if (pair.drawn === 0) return `${head}: the two slots draw together on no frame — nothing read`;
|
|
556
|
+
if (pair.over === 0) {
|
|
557
|
+
const why = pair.cloth === pair.leg ? 'a slot does not draw over itself' : `"${pair.leg}" draws over "${pair.cloth}" wherever both draw`;
|
|
558
|
+
return `${head}: the cloth draws over the leg on 0 of ${pair.drawn} frame(s) both draw (${why}) — nothing read`;
|
|
559
|
+
}
|
|
560
|
+
const frames = pair.frames.map((f) => f.index);
|
|
561
|
+
const deepest = pair.deepest as PassDepth;
|
|
562
|
+
return (
|
|
563
|
+
`${head} passes through on ${frames.length} of ${pair.over} frame(s) read${frames.length === 0 ? '' : ` [${indexRuns(frames)}]`} at ${fps} fps; ` +
|
|
564
|
+
`deepest frame ${deepest.index} (t=${deepest.time.toFixed(4)}s): ${depthText(deepest)}`
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** The line for one frame that passes through: the frame, the pair, the depth and the world box. */
|
|
569
|
+
export function passFrameLine(animation: string | null, pair: PairReading, frame: PassFrameReading): string {
|
|
570
|
+
const where = animation === null ? 'the setup pose' : JSON.stringify(animation);
|
|
571
|
+
return (
|
|
572
|
+
`${where} frame ${frame.index} (t=${frame.time.toFixed(4)}s): cloth "${pair.cloth}" over leg "${pair.leg}" by ${depthText(frame)}, ` +
|
|
573
|
+
`${frame.rows} row(s), in world box ${boxText(frame.box)}`
|
|
574
|
+
);
|
|
575
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -130,6 +130,7 @@ import { dirname, join } from 'node:path';
|
|
|
130
130
|
import { Plate, readPlate } from '../tools/plate.ts';
|
|
131
131
|
import { clipSourceOf, corePoser, inactiveBoneSnapshot, physicsClockStep, subsetOver, unposedBones } from './render_core.ts';
|
|
132
132
|
import { type PosedVertices, type WorldTransform } from './nonfinite.ts';
|
|
133
|
+
import type { EnvironmentChange } from './environment.ts';
|
|
133
134
|
import {
|
|
134
135
|
atlasScales,
|
|
135
136
|
choosePosers,
|
|
@@ -500,7 +501,7 @@ export function spinePoser(data: SkeletonData): Poser {
|
|
|
500
501
|
slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
|
|
501
502
|
subset: (opts, skin) => slotSubsetOf(data, opts, skin),
|
|
502
503
|
setup: (skin) => spinePosed(setupPosed(data, skin)),
|
|
503
|
-
animation: (name, skin, fps, count, visit) => {
|
|
504
|
+
animation: (name, skin, fps, count, visit, environment) => {
|
|
504
505
|
const skeleton = skeletonUnderSkin(data, skin);
|
|
505
506
|
const state = new AnimationState(new AnimationStateData(data));
|
|
506
507
|
// Not looping: the last frame sits at the animation's duration, and a looping
|
|
@@ -515,10 +516,12 @@ export function spinePoser(data: SkeletonData): Poser {
|
|
|
515
516
|
if (i > 0) {
|
|
516
517
|
state.update(step);
|
|
517
518
|
state.apply(skeleton);
|
|
519
|
+
setSkeletonEnvironment(skeleton, environment?.(i));
|
|
518
520
|
skeleton.update(clock);
|
|
519
521
|
skeleton.updateWorldTransform(Physics.update);
|
|
520
522
|
} else {
|
|
521
523
|
state.apply(skeleton);
|
|
524
|
+
setSkeletonEnvironment(skeleton, environment?.(i));
|
|
522
525
|
skeleton.update(0);
|
|
523
526
|
skeleton.updateWorldTransform(Physics.reset);
|
|
524
527
|
}
|
|
@@ -529,6 +532,17 @@ export function spinePoser(data: SkeletonData): Poser {
|
|
|
529
532
|
};
|
|
530
533
|
}
|
|
531
534
|
|
|
535
|
+
/**
|
|
536
|
+
* `render --environment`'s change for one frame (issue #1416), set on spine-core's own `Skeleton` before the update that
|
|
537
|
+
* poses the frame: `wind` is `windX, windY` and `gravity` is `gravityX, gravityY`, both in the runtime's convention, x
|
|
538
|
+
* right and y down — the live track's `setEnvironment` mapping, held to these four fields by the core suite's `CO56`. A
|
|
539
|
+
* half left out keeps the one standing, as the core merges it.
|
|
540
|
+
*/
|
|
541
|
+
function setSkeletonEnvironment(skeleton: Skeleton, change: EnvironmentChange | undefined): void {
|
|
542
|
+
if (change?.wind !== undefined) [skeleton.windX, skeleton.windY] = change.wind;
|
|
543
|
+
if (change?.gravity !== undefined) [skeleton.gravityX, skeleton.gravityY] = change.gravity;
|
|
544
|
+
}
|
|
545
|
+
|
|
532
546
|
/** A spine-core skeleton as it stands posed, read through the seam's `Posed`. */
|
|
533
547
|
function spinePosed(skeleton: Skeleton): Posed {
|
|
534
548
|
return {
|
package/src/render_core.ts
CHANGED
|
@@ -100,7 +100,7 @@ import { atlasRegionLookup, parseAtlasText } from './atlas.ts';
|
|
|
100
100
|
import { pagesOfAtlas, spineFileSha256, type ModelPage } from './model.ts';
|
|
101
101
|
import { clipThrough, type ClipShape, type ShapeClipper } from './core/clipping.ts';
|
|
102
102
|
import { activeBones, CoreInputError, readModel, sourceOfDoc, underNoSkin, underSkin, type CompiledDocument, type CoreSlotRow } from './core/index.ts';
|
|
103
|
-
import { PHYSICS_CLOCK_LEAD, physicsClockStep, poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
|
|
103
|
+
import { PHYSICS_CLOCK_LEAD, physicsClockStep, poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose, type StepEnvironment } from './core/raw.ts';
|
|
104
104
|
import type { TimelinePlant } from './core/animation.ts';
|
|
105
105
|
import { CORE_DEFAULT_SKIN, lookupSkins } from './core/skins.ts';
|
|
106
106
|
import { documentPageLookup, drawnRegions, meshPageUvs, readUvSequences, regionPageUvs, type DrawnRegion, type UvRegion, type UvSource } from './core/uvs.ts';
|
|
@@ -363,10 +363,14 @@ export function keptPieceArrays(): PieceArrays {
|
|
|
363
363
|
* one pose is held at a time rather than the animation's whole series. A plant
|
|
364
364
|
* passes another (`RC44`).
|
|
365
365
|
*/
|
|
366
|
-
export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void) => void;
|
|
366
|
+
export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void, environment?: StepEnvironment) => void;
|
|
367
367
|
|
|
368
|
-
/**
|
|
369
|
-
|
|
368
|
+
/**
|
|
369
|
+
* The walk `corePoser` steps with unless a plant passes another: one pose posed, drawn and released before the next, the
|
|
370
|
+
* physics clock moved by `physicsClockStep` (issue #1380), and `render --environment`'s change for each pose set before it
|
|
371
|
+
* is posed (issue #1416, `StepEnvironment`).
|
|
372
|
+
*/
|
|
373
|
+
export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit, environment) => poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit, physicsClockStep, environment);
|
|
370
374
|
|
|
371
375
|
/**
|
|
372
376
|
* The render's physics clock (issue #1380): defined beside the walk that takes it (`./core/raw.ts`), where the mesh
|
|
@@ -750,7 +754,7 @@ export function corePoser(
|
|
|
750
754
|
const input = inputOf(skin);
|
|
751
755
|
return corePosed(input, poseRawSetup(input.doc, { through }), through);
|
|
752
756
|
},
|
|
753
|
-
animation: (name, skin, fps, count, visit) => {
|
|
757
|
+
animation: (name, skin, fps, count, visit, environment) => {
|
|
754
758
|
const input = inputOf(skin);
|
|
755
759
|
const step = 1 / fps;
|
|
756
760
|
// Each pose is drawn as it is posed and released before the next (issue #1180), so the walk holds one pose and not
|
|
@@ -765,7 +769,7 @@ export function corePoser(
|
|
|
765
769
|
} catch (thrown) {
|
|
766
770
|
drawFailed.push(thrown);
|
|
767
771
|
}
|
|
768
|
-
});
|
|
772
|
+
}, environment);
|
|
769
773
|
if (drawFailed.length > 0) throw drawFailed[0];
|
|
770
774
|
},
|
|
771
775
|
rest: (skin, shown) => {
|