lecodes-sdk 0.19.1 → 0.20.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/dist/global.d.ts +31 -0
- package/dist/host.d.ts +3 -0
- package/dist/inject.js +260 -361
- package/dist/types/gl/Locomotion.d.ts +3 -1
- package/dist/types/gl/animation/Animator.d.ts +51 -183
- package/dist/types/gl/animation/Feet.d.ts +85 -0
- package/dist/types/gl/animation/Warp.d.ts +53 -0
- package/dist/types/gl/animation/core.d.ts +46 -7
- package/dist/types/inject.d.ts +7 -2
- package/dist/types/plugins/map.d.ts +174 -0
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/src/bridges.d.ts +13 -6
- package/src/compile/__tests__/compile.test.ts +12 -0
- package/src/compile/compileProject.ts +5 -0
- package/src/core/Aspect.ts +1 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Locomotion.ts +7 -5
- package/src/gl/Particles.ts +3 -3
- package/src/gl/Ragdoll.ts +270 -270
- package/src/gl/Scene.ts +1 -1
- package/src/gl/Trigger.ts +45 -45
- package/src/gl/animation/Animator.ts +137 -329
- package/src/gl/animation/Feet.ts +134 -0
- package/src/gl/animation/Loop.ts +3 -1
- package/src/gl/animation/Warp.ts +96 -0
- package/src/gl/animation/core.ts +736 -670
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/state.ts +6 -6
- package/src/host.d.ts +3 -0
- package/src/inject.ts +11 -2
- package/src/plugins/map.ts +396 -0
- package/src/scene/gizmos.ts +148 -148
package/src/gl/animation/core.ts
CHANGED
|
@@ -1,670 +1,736 @@
|
|
|
1
|
-
// The engine-facing core behind Animator / Layer / Loop / Playback. NOT exported from the SDK: the
|
|
2
|
-
// public classes are thin views over the records here. The engine (creator-anim) runs the whole
|
|
3
|
-
// per-frame loop — sources, INERTIALIZED transitions, blend spaces, root motion — and this file only
|
|
4
|
-
// binds clips, issues play / stop / blend calls and routes the engine's slot events back to playbacks.
|
|
5
|
-
//
|
|
6
|
-
// A layer shows exactly ONE source at a time: its one-shot, its loop, or nothing. A transition is not
|
|
7
|
-
// a crossfade — the engine records the offset between the pose it SHOWED and the pose the new source
|
|
8
|
-
// shows, and decays that offset away over `fade`. So the SDK keeps no fade bookkeeping and no slot
|
|
9
|
-
// pool: a slot is simply a clip bound to a layer, restartable at will (docs/animation-v2-plan.md).
|
|
10
|
-
|
|
11
|
-
import { AnimationClip } from "./AnimationClip"
|
|
12
|
-
import { Loop } from "./Loop"
|
|
13
|
-
import { Layer } from "./Layer"
|
|
14
|
-
import { Playback } from "./Playback"
|
|
15
|
-
import { Vec3 } from "../../math/vec"
|
|
16
|
-
import type { Node } from "../Node"
|
|
17
|
-
|
|
18
|
-
/** A position on a blend axis (1D) or plane (2D). */
|
|
19
|
-
export type BlendPosition = number | readonly [number, number]
|
|
20
|
-
|
|
21
|
-
/** The transition time (seconds) a play / playLoop without `fade` uses. */
|
|
22
|
-
export const DEFAULT_FADE = 0.2
|
|
23
|
-
|
|
24
|
-
export type PlayOptions = {
|
|
25
|
-
/** Transition seconds — how long the difference to the pose the layer SHOWED takes to decay
|
|
26
|
-
* (default 0.2; `0` = cut). Also the transition back at the end unless `fadeOut` overrides it. */
|
|
27
|
-
fade?: number
|
|
28
|
-
/** Transition seconds for the way in only (overrides `fade`). */
|
|
29
|
-
fadeIn?: number
|
|
30
|
-
/** One-shots: the transition BACK at the end (overrides `fade`). It starts this long before the
|
|
31
|
-
* clip ends — while it still plays — so the hand-over lands on the last pose, not after it. On a
|
|
32
|
-
* layer with no loop to return to, giving `fadeOut` releases the layer at the end instead of
|
|
33
|
-
* holding the last frame. */
|
|
34
|
-
fadeOut?: number
|
|
35
|
-
/** Playback rate for this clip (default 1). */
|
|
36
|
-
speed?: number
|
|
37
|
-
/** Rewind even if the clip is already playing (default: rewind only if it isn't). */
|
|
38
|
-
restart?: boolean
|
|
39
|
-
/** Enter the clip at a point in the GAIT CYCLE instead of at its start: `'match'` = the phase the
|
|
40
|
-
* layer shows right now (the walk's left foot is down → the turn clip starts where its left foot
|
|
41
|
-
* is down too, so nothing slides), or a number 0–1. Clips without a gait cycle ignore it. */
|
|
42
|
-
phase?: number | "match"
|
|
43
|
-
/** TURN WARP (degrees, + = left): the node turns this much in total over the clip instead of what the
|
|
44
|
-
* clip's baked heading says — its turn curve scaled, so the difference grows where the clip turns and
|
|
45
|
-
* pivots about the planted foot. A 45° start played with `turn: 60` turns 60; played with `turn: 45`
|
|
46
|
-
* it lands on 45 exactly whatever the clip's own run-off. Needs `rootMotion` + `rootRotation` and a
|
|
47
|
-
* clip with a baked heading (`lecodes assets marks` / `retarget`); a clip that turns under 10° is
|
|
48
|
-
* left alone. Ignored by hosts without it. */
|
|
49
|
-
turn?: number
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type StopOptions = { fade?: number }
|
|
53
|
-
|
|
54
|
-
export type LoopOptions = {
|
|
55
|
-
/** Transition seconds from whatever the layer shows — its previous loop, or a one-shot still
|
|
56
|
-
* playing (default 0.2; `0` = cut). */
|
|
57
|
-
fade?: number
|
|
58
|
-
/** Playback rate of the loop's clips (default 1). */
|
|
59
|
-
speed?: number
|
|
60
|
-
/** Where in the CYCLE to start, 0–1, or `'match'` = where the layer is now (what a gait loop taking
|
|
61
|
-
* over from a start / turn clip wants). The members share one clock, so this is every member's phase. */
|
|
62
|
-
phase?: number | "match"
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
export type
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
/** the
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
*
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
/** The
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
export
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ----
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.
|
|
298
|
-
if (this.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ----
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return rec
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (!
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (s.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
this.settleLayer(L)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
L.loop =
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
if (!
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
_creator.
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
return
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
this.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
this.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1
|
+
// The engine-facing core behind Animator / Layer / Loop / Playback. NOT exported from the SDK: the
|
|
2
|
+
// public classes are thin views over the records here. The engine (creator-anim) runs the whole
|
|
3
|
+
// per-frame loop — sources, INERTIALIZED transitions, blend spaces, root motion — and this file only
|
|
4
|
+
// binds clips, issues play / stop / blend calls and routes the engine's slot events back to playbacks.
|
|
5
|
+
//
|
|
6
|
+
// A layer shows exactly ONE source at a time: its one-shot, its loop, or nothing. A transition is not
|
|
7
|
+
// a crossfade — the engine records the offset between the pose it SHOWED and the pose the new source
|
|
8
|
+
// shows, and decays that offset away over `fade`. So the SDK keeps no fade bookkeeping and no slot
|
|
9
|
+
// pool: a slot is simply a clip bound to a layer, restartable at will (docs/animation-v2-plan.md).
|
|
10
|
+
|
|
11
|
+
import { AnimationClip } from "./AnimationClip"
|
|
12
|
+
import { Loop } from "./Loop"
|
|
13
|
+
import { Layer } from "./Layer"
|
|
14
|
+
import { Playback } from "./Playback"
|
|
15
|
+
import { Vec3 } from "../../math/vec"
|
|
16
|
+
import type { Node } from "../Node"
|
|
17
|
+
|
|
18
|
+
/** A position on a blend axis (1D) or plane (2D). */
|
|
19
|
+
export type BlendPosition = number | readonly [number, number]
|
|
20
|
+
|
|
21
|
+
/** The transition time (seconds) a play / playLoop without `fade` uses. */
|
|
22
|
+
export const DEFAULT_FADE = 0.2
|
|
23
|
+
|
|
24
|
+
export type PlayOptions = {
|
|
25
|
+
/** Transition seconds — how long the difference to the pose the layer SHOWED takes to decay
|
|
26
|
+
* (default 0.2; `0` = cut). Also the transition back at the end unless `fadeOut` overrides it. */
|
|
27
|
+
fade?: number
|
|
28
|
+
/** Transition seconds for the way in only (overrides `fade`). */
|
|
29
|
+
fadeIn?: number
|
|
30
|
+
/** One-shots: the transition BACK at the end (overrides `fade`). It starts this long before the
|
|
31
|
+
* clip ends — while it still plays — so the hand-over lands on the last pose, not after it. On a
|
|
32
|
+
* layer with no loop to return to, giving `fadeOut` releases the layer at the end instead of
|
|
33
|
+
* holding the last frame. */
|
|
34
|
+
fadeOut?: number
|
|
35
|
+
/** Playback rate for this clip (default 1). */
|
|
36
|
+
speed?: number
|
|
37
|
+
/** Rewind even if the clip is already playing (default: rewind only if it isn't). */
|
|
38
|
+
restart?: boolean
|
|
39
|
+
/** Enter the clip at a point in the GAIT CYCLE instead of at its start: `'match'` = the phase the
|
|
40
|
+
* layer shows right now (the walk's left foot is down → the turn clip starts where its left foot
|
|
41
|
+
* is down too, so nothing slides), or a number 0–1. Clips without a gait cycle ignore it. */
|
|
42
|
+
phase?: number | "match"
|
|
43
|
+
/** TURN WARP (degrees, + = left): the node turns this much in total over the clip instead of what the
|
|
44
|
+
* clip's baked heading says — its turn curve scaled, so the difference grows where the clip turns and
|
|
45
|
+
* pivots about the planted foot. A 45° start played with `turn: 60` turns 60; played with `turn: 45`
|
|
46
|
+
* it lands on 45 exactly whatever the clip's own run-off. Needs `rootMotion` + `rootRotation` and a
|
|
47
|
+
* clip with a baked heading (`lecodes assets marks` / `retarget`); a clip that turns under 10° is
|
|
48
|
+
* left alone. Ignored by hosts without it. */
|
|
49
|
+
turn?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type StopOptions = { fade?: number }
|
|
53
|
+
|
|
54
|
+
export type LoopOptions = {
|
|
55
|
+
/** Transition seconds from whatever the layer shows — its previous loop, or a one-shot still
|
|
56
|
+
* playing (default 0.2; `0` = cut). */
|
|
57
|
+
fade?: number
|
|
58
|
+
/** Playback rate of the loop's clips (default 1). */
|
|
59
|
+
speed?: number
|
|
60
|
+
/** Where in the CYCLE to start, 0–1, or `'match'` = where the layer is now (what a gait loop taking
|
|
61
|
+
* over from a start / turn clip wants). The members share one clock, so this is every member's phase. */
|
|
62
|
+
phase?: number | "match"
|
|
63
|
+
}
|
|
64
|
+
/** A blend member with its CYCLE given explicitly: `at` = its position, `offset` = its gait phase at t = 0
|
|
65
|
+
* (0 = a left-foot-down, 0.5 = a right-foot-down, 0–1), `cycles` = how many gait cycles the clip holds
|
|
66
|
+
* (a two-stride loop: 2). Both are measured offline from the clip's foot marks. Members given as a
|
|
67
|
+
* bare position run in normalized time and have no cycle (nothing phase-matches to them). */
|
|
68
|
+
export type BlendMember = { at: BlendPosition, offset?: number, cycles?: number }
|
|
69
|
+
/** What a layer loops: one clip (name / index), or a blend space `{ name: position | { at, offset, cycles } }`. */
|
|
70
|
+
export type LoopDef = string | number | Record<string, BlendPosition | BlendMember>
|
|
71
|
+
|
|
72
|
+
/** One clip the layer shows this frame — `layer.active` / `anim.active` (a debug overlay's list). */
|
|
73
|
+
export type ActiveClip = {
|
|
74
|
+
name: string
|
|
75
|
+
clip: AnimationClip
|
|
76
|
+
/** contribution to the pose, 0–1: the shown one-shot 1, a loop member its blend share */
|
|
77
|
+
weight: number
|
|
78
|
+
/** clock (s) */
|
|
79
|
+
time: number
|
|
80
|
+
/** a member of the layer's loop (else the layer's one-shot) */
|
|
81
|
+
loop: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type LayerOptions = {
|
|
85
|
+
/** Bone name(s): the layer drives only these subtrees (`'Spine1'` = upper body). Default: the whole rig. */
|
|
86
|
+
mask?: string | string[]
|
|
87
|
+
/** Blend each clip's DELTA from its own first frame on top of the layers below (lean, breathe, recoil)
|
|
88
|
+
* instead of overriding them. */
|
|
89
|
+
additive?: boolean
|
|
90
|
+
/** Layer contribution 0–1 (default 1). */
|
|
91
|
+
weight?: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ClipEventHandler = (clip: string, layer: Layer) => void
|
|
95
|
+
/** A foot planted: which one, and where in the world it landed. */
|
|
96
|
+
export type StepHandler = (side: "left" | "right", position: Vec3) => void
|
|
97
|
+
|
|
98
|
+
/** A loop's GAIT CYCLE as two numbers — what a blend member's `{ offset, cycles }` wants: φ(t) = offset +
|
|
99
|
+
* cycles · t / duration, 0 = a left-foot-down. `residual` = the worst foot-down's distance from that line
|
|
100
|
+
* (cycles; a clean loop sits under 0.03), `steps` = the foot-downs it was fitted through. */
|
|
101
|
+
export type ClipCycle = { offset: number, cycles: number, residual: number, steps: number }
|
|
102
|
+
/** A loop's cycle found by POSE against a reference loop (`clipInfo(clip).alignTo(ref)`): the `{ offset,
|
|
103
|
+
* cycles }` under which it shows the reference's pose at the reference's phase. `score` = the fit at that
|
|
104
|
+
* alignment (metres, the joint distance + velocity measure), `margin` = how much worse the runner-up
|
|
105
|
+
* alignment ≥ 0.2 cycle away is — near 0 means ambiguous (a mirror pair, an in-place idle). */
|
|
106
|
+
export type ClipAlign = { offset: number, cycles: number, score: number, margin: number }
|
|
107
|
+
|
|
108
|
+
/** What the engine measured on a clip once it was bound to this skeleton (`anim.clipInfo('Walk')`).
|
|
109
|
+
* The root curves are of the clip's ROOT bone — its travel is what root motion would move. */
|
|
110
|
+
export type ClipInfo = {
|
|
111
|
+
/** total horizontal root travel, metres */
|
|
112
|
+
travel: number
|
|
113
|
+
/** mean root speed over the clip, m/s */
|
|
114
|
+
speed: number
|
|
115
|
+
/** total root yaw, radians (+ = left) */
|
|
116
|
+
turn: number
|
|
117
|
+
/** the root barely moves — an in-place clip */
|
|
118
|
+
inPlace: boolean
|
|
119
|
+
/** The gait cycle fitted through the foot contacts (a foot already down at t = 0 is not a step) —
|
|
120
|
+
* the numbers a blend member takes as `{ offset, cycles }`. Undefined: no steps. */
|
|
121
|
+
cycle(): ClipCycle | undefined
|
|
122
|
+
/** This loop's cycle found by POSE against `reference` (its cycle given, else `reference`'s `cycle()`,
|
|
123
|
+
* else offset 0 / one cycle — the answer is then relative to the reference's own time). Reads no marks:
|
|
124
|
+
* a markless loop (swimming, breathing, an upper-body sway) gets a cycle, and a marked one gets a
|
|
125
|
+
* second opinion — a mirrored anchor shows as a 0.5 disagreement with `cycle()`. Undefined: nothing to compare. */
|
|
126
|
+
alignTo(reference: string | number, cycle?: { offset: number, cycles: number }): ClipAlign | undefined
|
|
127
|
+
/** the clip has a gait cycle (feet planting in turn) — `phaseAt` is meaningful */
|
|
128
|
+
hasPhase: boolean
|
|
129
|
+
/** When each foot TOUCHES the ground, seconds — the phase anchor. `still` is the window inside it
|
|
130
|
+
* where the foot is genuinely STATIONARY (a touching foot still rolls for its first beats, and a
|
|
131
|
+
* stop take's final plant touches long before the body has braked); absent = never still.
|
|
132
|
+
* `at` = where the foot is at the touch's start ([x, y, z], model space). */
|
|
133
|
+
contacts: { side: "left" | "right", from: number, to: number, at: [number, number, number], still?: { from: number, to: number } }[]
|
|
134
|
+
/** the HANDS on the ground (a cartwheel, a roll, a vault) — from the clip's marks in the GLB; empty when none */
|
|
135
|
+
hands: { side: "left" | "right", from: number, to: number, at: [number, number, number], still?: { from: number, to: number } }[]
|
|
136
|
+
/** A CLIMBING clip's tread levels (a stair or slope take): the model-space heights its feet plant
|
|
137
|
+
* at, sorted ascending, with the riser (median level spacing — the step height the clip was
|
|
138
|
+
* authored for). Absent on a flat clip. */
|
|
139
|
+
treads?: { riser: number, levels: number[] }
|
|
140
|
+
/** the gait phase at `time`: 0 at a left-foot-down, 0.5 at a right-foot-down; -1 without a cycle */
|
|
141
|
+
phaseAt(time: number): number
|
|
142
|
+
/** cumulative root travel (m) at `time` */
|
|
143
|
+
travelAt(time: number): number
|
|
144
|
+
/** cumulative root yaw (rad, + = left) at `time` */
|
|
145
|
+
turnAt(time: number): number
|
|
146
|
+
/** the root's own speed at `time`, m/s — how fast the clip is moving the body right there */
|
|
147
|
+
speedAt(time: number): number
|
|
148
|
+
/** the unit travel direction at `time` ([x, z], model space, held through stills) — integrate
|
|
149
|
+
* direction × d(travel) to reconstruct the root's 2D path (a treadmill display, a turn's arc) */
|
|
150
|
+
directionAt(time: number): [number, number]
|
|
151
|
+
/** the first time the clip has turned `yaw` radians (+ = left): where to enter a turn a body has
|
|
152
|
+
* already begun, so the clip continues the move instead of restarting it */
|
|
153
|
+
timeAtTurn(yaw: number): number
|
|
154
|
+
/** How far this clip's pose is from `target`'s, in metres — the joint distance plus the velocity
|
|
155
|
+
* difference over a tenth of a second, the planted foot weighted most. That difference is exactly
|
|
156
|
+
* what a hand-over hands the transition to decay away, so it says what switching to `target` would
|
|
157
|
+
* cost at `time`. The two are lined up by cycle PHASE (a clip that continues the walk) or by TIME
|
|
158
|
+
* (`align: 'time'` — two clips that both begin from standing share no cycle). */
|
|
159
|
+
fitTo(target: string | number, time: number, options?: { align?: "phase" | "time" }): number | undefined
|
|
160
|
+
/** The earliest moment (seconds) at which handing over to `target` costs no more than `within`
|
|
161
|
+
* metres — a start's last steps ARE the walk, so this is where it stops being worth playing.
|
|
162
|
+
* `atContact` (default true) snaps to the next foot-down: a switch under a planted foot is the one
|
|
163
|
+
* the eye forgives. -1 = the clip never gets that close. */
|
|
164
|
+
exitTo(target: string | number, options?: { within?: number, atContact?: boolean, align?: "phase" | "time" }): number
|
|
165
|
+
/** The knee's bend plane at `time`, predicted from the skeleton's calibrated hinge axis carried by
|
|
166
|
+
* this clip's own thigh rotation — continuous even where the leg is straight (where a plane
|
|
167
|
+
* derived from positions is pure noise). `pole` = unit direction from the hip→ankle line toward
|
|
168
|
+
* the knee (a two-bone solver's bend direction), `normal` = the plane's normal; both in the model
|
|
169
|
+
* pose's frame. Undefined = no leg chain, or no knee motion bound to calibrate from. */
|
|
170
|
+
kneePoleAt(side: "left" | "right", time: number): { pole: [number, number, number], normal: [number, number, number] } | undefined
|
|
171
|
+
/** The clip's baked physics at `time` (unit body mass, model space): `com` = the body's center of
|
|
172
|
+
* mass (biomechanical segment fractions on the classified bones), `velocity` = its velocity
|
|
173
|
+
* (= linear momentum per kg), `angular` = the angular momentum about the COM (m²/s per kg,
|
|
174
|
+
* low-passed ~0.5 s — the REGULATED component, e.g. a turn's sustained yaw; the per-stride
|
|
175
|
+
* pitch/roll exchange between the limbs and the trunk is filtered out),
|
|
176
|
+
* `support` = per-foot load [left, right] — contact-gated, split by where the COM stands between
|
|
177
|
+
* the feet, scaled by the vertical force proxy (above 1 on a landing impact, 0 in flight).
|
|
178
|
+
* A model, not a measurement — meant for ranking transitions and display, both sides of any
|
|
179
|
+
* comparison sharing the same model. Undefined = nothing classified on this skeleton. */
|
|
180
|
+
physicsAt(time: number): { com: [number, number, number], velocity: [number, number, number], angular: [number, number, number], support: [number, number] } | undefined
|
|
181
|
+
/** The clip's MATCHING FEATURE ROW at `time` — a fresh Float32Array(37) in the clip's heading
|
|
182
|
+
* frame at that time (x lateral, + left; y up; z forward): 0–5 feet positions relative to the
|
|
183
|
+
* pelvis' ground point, 6–11 feet velocities, 12–14 pelvis velocity, 15 pelvis height, 16–18
|
|
184
|
+
* COM velocity, 19–20 support L/R, 21–22 contact phase L/R, 23 yaw angular momentum, 24–31 the
|
|
185
|
+
* clip's own path 0.3/0.6/1.0/1.5 s ahead as (lateral, forward) pairs, 32–35 facing change at
|
|
186
|
+
* those horizons (rad, + = left), 36 cyclic flag. What a trajectory-and-state matcher compares.
|
|
187
|
+
* Undefined = no leg chain. */
|
|
188
|
+
featuresAt(time: number): Float32Array | undefined
|
|
189
|
+
/** The take is a LOOP by measurement: its seam is continuous (feet back where they started
|
|
190
|
+
* relative to the root, no turn). Starts, stops and turns are not. */
|
|
191
|
+
cyclic: boolean
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The calibrated knee hinge-axis report (`anim.feet.kneeAxis('left')`): the axis in the thigh's local
|
|
195
|
+
* frame, and the calibration's own confidence — the angular spread of its measurements (radians;
|
|
196
|
+
* a real knee comes in at a few degrees) over `samples` consecutive-frame deltas of the pack.
|
|
197
|
+
* `plant` = the calibrated PLANT HEIGHTS (model space): where the ankle / toe actually sit under
|
|
198
|
+
* full weight, the median of the pack's still-window lows — absent when nothing plants. */
|
|
199
|
+
export type KneeAxisReport = { axis: [number, number, number], spreadMean: number, spreadMax: number, samples: number, plant?: { ankleY: number, toeY: number } }
|
|
200
|
+
|
|
201
|
+
// ---- records -------------------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/** A clip bound to a layer — one per (clip, layer), reused by every play. `playback` = the most recent
|
|
204
|
+
* one-shot on it (`playing` while it still owns the layer; kept after that so its handle can still be
|
|
205
|
+
* read — a restart re-enters the same slot with a new record). */
|
|
206
|
+
export type SlotRec = { name: string, clip: AnimationClip, slot: number, layer: LayerRec, inBlend: boolean, playback?: PlaybackRec }
|
|
207
|
+
export type PlaybackRec = { slot: SlotRec, playing: boolean, done: Promise<boolean>, resolve: (natural: boolean) => void }
|
|
208
|
+
export type LayerRec = { index: number, view: Layer, slots: SlotRec[], loop?: LoopRec, weight: number, mask: string, additive: boolean }
|
|
209
|
+
export type LoopRec = { layer: LayerRec, members: { name: string, slot: SlotRec }[], value: BlendPosition, view: Loop, dead: boolean }
|
|
210
|
+
|
|
211
|
+
const handlers = new Map<number, (slot: number, type: number) => void>()
|
|
212
|
+
const stepHandlers = new Map<number, (side: number, x: number, y: number, z: number) => void>()
|
|
213
|
+
let eventsInstalled = false
|
|
214
|
+
const ensureEvents = (): void => {
|
|
215
|
+
if (eventsInstalled) return
|
|
216
|
+
eventsInstalled = true
|
|
217
|
+
_creator.setOnAnimatorEvent((animatorId, slot, type) => handlers.get(animatorId)?.(slot, type))
|
|
218
|
+
_creator.setOnAnimatorStep((animatorId, side, x, y, z) => stepHandlers.get(animatorId)?.(side, x, y, z))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const settled = (slot: SlotRec): PlaybackRec => {
|
|
222
|
+
let resolve = (_n: boolean): void => {}
|
|
223
|
+
const done = new Promise<boolean>((r) => { resolve = r })
|
|
224
|
+
resolve(false)
|
|
225
|
+
return { slot, playing: false, done, resolve }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const curveInfo = new Float32Array(4)
|
|
229
|
+
const kneeOut = new Float32Array(8)
|
|
230
|
+
const physOut = new Float32Array(11)
|
|
231
|
+
|
|
232
|
+
export class Core {
|
|
233
|
+
id = 0
|
|
234
|
+
/** Added clips by name (`addClip`) — the lookup index; an added name overrides an embedded one. */
|
|
235
|
+
readonly table = new Map<string, AnimationClip>()
|
|
236
|
+
readonly layers: LayerRec[] = []
|
|
237
|
+
speed = 1
|
|
238
|
+
private _embedded?: AnimationClip[]
|
|
239
|
+
private readonly _added: { name: string, clip: AnimationClip }[] = []
|
|
240
|
+
private _list?: AnimationClip[]
|
|
241
|
+
private readonly _listeners = new Map<string, Set<ClipEventHandler>>()
|
|
242
|
+
|
|
243
|
+
readonly node: Node
|
|
244
|
+
constructor(node: Node) { this.node = node; this.layers.push(this.newLayer()) }
|
|
245
|
+
|
|
246
|
+
// ---- clips ----
|
|
247
|
+
embedded(): AnimationClip[] {
|
|
248
|
+
if (!this._embedded) {
|
|
249
|
+
this._embedded = AnimationClip._ofSet(_creator.getGlbClipSet(this.node.id))
|
|
250
|
+
const seen = new Set<string>()
|
|
251
|
+
for (const c of this._embedded) {
|
|
252
|
+
if (seen.has(c.name)) console.warn(`Animator: ${this.node.name || "the node"} has several clips named '${c.name}' — by name you get the first; reach the others by index (anim.clip(n))`)
|
|
253
|
+
seen.add(c.name)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return this._embedded
|
|
257
|
+
}
|
|
258
|
+
/** The ordered clip list: the GLB's clips in file order (an added clip of the same name takes the
|
|
259
|
+
* embedded one's place), then the remaining added clips in insertion order. Cached until addClip. */
|
|
260
|
+
clips(): AnimationClip[] {
|
|
261
|
+
if (this._list) return this._list
|
|
262
|
+
const out: AnimationClip[] = []
|
|
263
|
+
const placed = new Set<string>()
|
|
264
|
+
for (const c of this.embedded()) {
|
|
265
|
+
const override = this.table.get(c.name)
|
|
266
|
+
if (override && !placed.has(c.name)) { out.push(override); placed.add(c.name) }
|
|
267
|
+
else out.push(c)
|
|
268
|
+
}
|
|
269
|
+
for (const a of this._added) if (!placed.has(a.name)) { out.push(a.clip); placed.add(a.name) }
|
|
270
|
+
return this._list = out
|
|
271
|
+
}
|
|
272
|
+
addClip(name: string, clip: AnimationClip): void {
|
|
273
|
+
const i = this._added.findIndex((a) => a.name === name)
|
|
274
|
+
if (i >= 0) this._added[i] = { name, clip }
|
|
275
|
+
else this._added.push({ name, clip })
|
|
276
|
+
this.table.set(name, clip)
|
|
277
|
+
this._list = undefined
|
|
278
|
+
}
|
|
279
|
+
/** A clip by name (added first, then embedded), or by index into `clips()` (none = the first). */
|
|
280
|
+
resolveClip(ref: string | number | undefined): [string, AnimationClip] | undefined {
|
|
281
|
+
if (ref === undefined || typeof ref === "number") { const c = this.clips()[ref ?? 0]; return c ? [ c.name, c ] : undefined }
|
|
282
|
+
const c = this.table.get(ref) ?? this.embedded().find((e) => e.name === ref)
|
|
283
|
+
return c ? [ ref, c ] : undefined
|
|
284
|
+
}
|
|
285
|
+
private clipNames(): string { return this.clips().map((c) => c.name).join(", ") || "(empty)" }
|
|
286
|
+
|
|
287
|
+
// ---- lifecycle ----
|
|
288
|
+
/** Create the native animator on demand. False = nothing to animate (warned). */
|
|
289
|
+
ensure(): boolean {
|
|
290
|
+
if (this.id) return true
|
|
291
|
+
if (this.embedded().length === 0 && this.table.size === 0) { console.warn(`Animator: no clips on ${this.node.name || "the node"} — nothing to play`); return false }
|
|
292
|
+
ensureEvents()
|
|
293
|
+
this.id = _creator.animatorCreate(this.node.id)
|
|
294
|
+
if (!this.id) { console.warn("Animator: node has no transform hierarchy"); return false }
|
|
295
|
+
handlers.set(this.id, (slot, type) => this.onEvent(slot, type))
|
|
296
|
+
stepHandlers.set(this.id, (side, x, y, z) => this.onStep(side, x, y, z))
|
|
297
|
+
if (this._feet) _creator.animatorSetFeet(this.id, this._feet[0], this._feet[1])
|
|
298
|
+
if (this._warp) _creator.animatorSetWarpParams(this.id, this._warp)
|
|
299
|
+
if (this._feetParams && _creator.animatorSetFeetParams) _creator.animatorSetFeetParams(this.id, this._feetParams)
|
|
300
|
+
if (this.speed !== 1) _creator.animatorSetGlobal(this.id, this.speed, false)
|
|
301
|
+
for (const L of this.layers) if (L.index > 0) this.pushLayer(L)
|
|
302
|
+
return true
|
|
303
|
+
}
|
|
304
|
+
destroy(): void {
|
|
305
|
+
for (const L of this.layers) this.settleLayer(L)
|
|
306
|
+
if (this.id) { _creator.animatorDestroy(this.id); handlers.delete(this.id); stepHandlers.delete(this.id); this.id = 0 }
|
|
307
|
+
for (const L of this.layers) { L.slots = []; if (L.loop) { L.loop.dead = true; L.loop = undefined } }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---- layers ----
|
|
311
|
+
newLayer(options?: LayerOptions): LayerRec {
|
|
312
|
+
const rec: LayerRec = { index: this.layers.length, view: undefined as unknown as Layer, slots: [], weight: options?.weight ?? 1, mask: "", additive: options?.additive ?? false }
|
|
313
|
+
if (options?.mask !== undefined) rec.mask = Array.isArray(options.mask) ? options.mask.join("\n") : options.mask
|
|
314
|
+
rec.view = new Layer(rec, this)
|
|
315
|
+
return rec
|
|
316
|
+
}
|
|
317
|
+
addLayer(options: LayerOptions): Layer {
|
|
318
|
+
if (this.layers.length >= 255) { console.warn("Animator: too many layers"); return this.layers[this.layers.length - 1].view }
|
|
319
|
+
const rec = this.newLayer(options)
|
|
320
|
+
this.layers.push(rec)
|
|
321
|
+
if (this.id) this.pushLayer(rec)
|
|
322
|
+
return rec.view
|
|
323
|
+
}
|
|
324
|
+
/** Arm the layer's next transition with a wind-up: see `Layer.anticipate`. */
|
|
325
|
+
anticipate(L: LayerRec, amount: number): void { if (this.id) _creator.animatorSetLayerAnticipation?.(this.id, L.index, amount) }
|
|
326
|
+
|
|
327
|
+
pushLayer(L: LayerRec): void {
|
|
328
|
+
if (!this.id) return
|
|
329
|
+
for (const m of L.mask.split("\n")) if (m && !this.node.bone(m)) console.warn(`Animator: layer mask bone '${m}' not found under ${this.node.name || "the node"}`)
|
|
330
|
+
_creator.animatorSetLayer(this.id, L.index, L.weight, L.additive, L.mask)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---- slots ----
|
|
334
|
+
/** The layer's slot for the clip — bound on first use, reused after that. */
|
|
335
|
+
slotFor(L: LayerRec, name: string, clip: AnimationClip): SlotRec | undefined {
|
|
336
|
+
const have = L.slots.find((s) => s.name === name && s.clip === clip)
|
|
337
|
+
if (have) return have
|
|
338
|
+
const slot = _creator.animatorBind(this.id, clip._set, clip._index, L.index)
|
|
339
|
+
if (slot < 0) { console.warn(`Animator: could not bind clip '${name}'`); return undefined }
|
|
340
|
+
const bound = _creator.animatorBoundTracks(this.id, slot)
|
|
341
|
+
if (clip.trackCount > 0 && bound === 0) console.warn(`Animator: '${name}' — no track matched a node name under ${this.node.name || "the node"} (different rig?)`)
|
|
342
|
+
else if (bound < clip.trackCount) console.warn(`Animator: '${name}' — ${clip.trackCount - bound}/${clip.trackCount} tracks unbound (bone names not found)`)
|
|
343
|
+
const rec: SlotRec = { name, clip, slot, layer: L, inBlend: false }
|
|
344
|
+
L.slots.push(rec)
|
|
345
|
+
return rec
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The base layer's native slot for a clip, bound on first ask (-1 = no such clip / no animator). */
|
|
349
|
+
slotIndex(ref: string | number): number {
|
|
350
|
+
const found = this.resolveClip(ref)
|
|
351
|
+
if (!found || !this.ensure()) return -1
|
|
352
|
+
return this.slotFor(this.layers[0], found[0], found[1])?.slot ?? -1
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---- play / stop ----
|
|
356
|
+
play(L: LayerRec, ref: string | number | undefined, o: PlayOptions): Playback {
|
|
357
|
+
const found = this.resolveClip(ref)
|
|
358
|
+
if (!found) {
|
|
359
|
+
console.warn(`Animator: no clip ${ref === undefined ? "to play" : JSON.stringify(ref)} on ${this.node.name || "the node"} — table: ${this.clipNames()}`)
|
|
360
|
+
return new Playback(settled({ name: "", clip: AnimationClip._none(), slot: -1, layer: L, inBlend: false }), this)
|
|
361
|
+
}
|
|
362
|
+
const [name, clip] = found
|
|
363
|
+
if (!this.ensure()) return new Playback(settled({ name, clip, slot: -1, layer: L, inBlend: false }), this)
|
|
364
|
+
const s = this.slotFor(L, name, clip)
|
|
365
|
+
if (!s) return new Playback(settled({ name, clip, slot: -1, layer: L, inBlend: false }), this)
|
|
366
|
+
if (s.inBlend) { console.warn(`Animator: '${name}' is part of this layer's loop — use playLoop() to change what it rests on`); return new Playback(settled(s), this) }
|
|
367
|
+
const fadeIn = o.fadeIn ?? o.fade ?? DEFAULT_FADE
|
|
368
|
+
const fadeOut = o.fadeOut ?? o.fade ?? DEFAULT_FADE
|
|
369
|
+
const phase = this.entryPhase(L, o.phase) // read BEFORE the play: what the layer shows now
|
|
370
|
+
this.settleLayer(L) // the layer had one source: whatever it was, this play takes over from it
|
|
371
|
+
let resolve = (_n: boolean): void => {}
|
|
372
|
+
const done = new Promise<boolean>((r) => { resolve = r })
|
|
373
|
+
const rec: PlaybackRec = { slot: s, playing: true, done, resolve }
|
|
374
|
+
s.playback = rec
|
|
375
|
+
_creator.animatorPlay(this.id, s.slot, false, o.speed ?? 1, fadeIn, fadeOut, o.restart ?? false)
|
|
376
|
+
if (o.turn !== undefined) _creator.animatorSlotSetTurn?.(this.id, s.slot, o.turn * Math.PI / 180, true)
|
|
377
|
+
if (phase !== undefined) _creator.animatorSeekPhase(this.id, s.slot, phase)
|
|
378
|
+
return new Playback(rec, this)
|
|
379
|
+
}
|
|
380
|
+
/** A one-shot on the layer hasn't handed over yet. */
|
|
381
|
+
busy(L: LayerRec): boolean { return L.slots.some((s) => s.playback?.playing === true) }
|
|
382
|
+
settle(p: PlaybackRec, natural: boolean): void {
|
|
383
|
+
p.playing = false
|
|
384
|
+
p.resolve(natural)
|
|
385
|
+
}
|
|
386
|
+
/** Resolve every pending playback on the layer as cut short. */
|
|
387
|
+
private settleLayer(L: LayerRec): void { for (const s of L.slots) if (s.playback?.playing) this.settle(s.playback, false) }
|
|
388
|
+
stopSlot(s: SlotRec, fade: number): void {
|
|
389
|
+
if (s.playback?.playing) this.settle(s.playback, false)
|
|
390
|
+
if (this.id && s.slot >= 0) _creator.animatorStop(this.id, s.layer.index, s.slot, fade)
|
|
391
|
+
}
|
|
392
|
+
stopLayer(L: LayerRec, fade: number): void {
|
|
393
|
+
this.settleLayer(L)
|
|
394
|
+
if (L.loop) { L.loop.dead = true; L.loop = undefined }
|
|
395
|
+
if (this.id) _creator.animatorStop(this.id, L.index, -1, fade)
|
|
396
|
+
}
|
|
397
|
+
stopAll(fade: number): void { for (const L of this.layers) this.stopLayer(L, fade) }
|
|
398
|
+
|
|
399
|
+
// ---- loop ----
|
|
400
|
+
loop(L: LayerRec, def: LoopDef, o: LoopOptions): Loop | undefined {
|
|
401
|
+
if (!this.ensure()) return undefined
|
|
402
|
+
const fade = o.fade ?? DEFAULT_FADE
|
|
403
|
+
const phase = this.entryPhase(L, o.phase) // read BEFORE the members replace the layer's source
|
|
404
|
+
if (L.loop) { L.loop.dead = true; for (const m of L.loop.members) m.slot.inBlend = false; L.loop = undefined }
|
|
405
|
+
this.settleLayer(L) // a one-shot on the layer is cut short by the new loop
|
|
406
|
+
const space: Record<string, BlendPosition | BlendMember> = typeof def === "object" ? def : {}
|
|
407
|
+
if (typeof def !== "object") {
|
|
408
|
+
const found = this.resolveClip(def)
|
|
409
|
+
if (!found) { console.warn(`Animator: no clip ${JSON.stringify(def)} to loop — table: ${this.clipNames()}`); return undefined }
|
|
410
|
+
space[found[0]] = 0
|
|
411
|
+
}
|
|
412
|
+
const rec: LoopRec = { layer: L, members: [], value: 0, view: undefined as unknown as Loop, dead: false }
|
|
413
|
+
rec.view = new Loop(rec, this)
|
|
414
|
+
const slots: number[] = [], positions: number[] = [], phases: number[] = []
|
|
415
|
+
const posOf = (m: BlendPosition | BlendMember): BlendPosition => typeof m === "object" && !Array.isArray(m) ? (m as BlendMember).at : m as BlendPosition
|
|
416
|
+
const is2D = Object.values(space).some((m) => typeof posOf(m) !== "number")
|
|
417
|
+
for (const [name, m] of Object.entries(space)) {
|
|
418
|
+
const found = this.resolveClip(name)
|
|
419
|
+
if (!found) { console.warn(`Animator: loop clip '${name}' is not in the clip table`); continue }
|
|
420
|
+
const s = this.slotFor(L, name, found[1])
|
|
421
|
+
if (!s) continue
|
|
422
|
+
s.inBlend = true
|
|
423
|
+
rec.members.push({ name, slot: s })
|
|
424
|
+
slots.push(s.slot)
|
|
425
|
+
const at = posOf(m)
|
|
426
|
+
if (is2D) positions.push(typeof at === "number" ? at : at[0], typeof at === "number" ? 0 : at[1])
|
|
427
|
+
else positions.push(at as number)
|
|
428
|
+
// (offset, cycles) — cycles 0 = normalized time, no cycle
|
|
429
|
+
const cyc = typeof m === "object" && !Array.isArray(m) ? (m as BlendMember) : undefined
|
|
430
|
+
phases.push(cyc?.cycles && cyc.cycles > 0 ? (cyc.offset ?? 0) : 0, cyc?.cycles && cyc.cycles > 0 ? cyc.cycles : 0)
|
|
431
|
+
}
|
|
432
|
+
L.loop = rec
|
|
433
|
+
_creator.animatorSetBlend(this.id, L.index, is2D ? 2 : 1, new Uint16Array(slots), new Float32Array(positions), fade, o.speed ?? 1, new Float32Array(phases))
|
|
434
|
+
if (phase !== undefined) this.setLoopPhase(rec, phase)
|
|
435
|
+
return rec.view
|
|
436
|
+
}
|
|
437
|
+
/** The phase the layer shows, 0–1 through the gait cycle (-1 = nothing / no cycle). */
|
|
438
|
+
layerPhase(L: LayerRec): number { return this.id ? _creator.animatorLayerPhase(this.id, L.index) : -1 }
|
|
439
|
+
/** The entry phase of a play / playLoop: a number as given, `'match'` = what the layer shows now
|
|
440
|
+
* (undefined when it shows no cycle — then the clip starts at its own beginning). */
|
|
441
|
+
private entryPhase(L: LayerRec, want: number | "match" | undefined): number | undefined {
|
|
442
|
+
if (want === undefined) return undefined
|
|
443
|
+
if (typeof want === "number") return want
|
|
444
|
+
if (!this.id) return undefined
|
|
445
|
+
const p = this.layerPhase(L)
|
|
446
|
+
return p < 0 ? undefined : p
|
|
447
|
+
}
|
|
448
|
+
/** The loop's phase through its cycle, 0–1 (the members share one clock). */
|
|
449
|
+
loopPhase(b: LoopRec): number {
|
|
450
|
+
if (b.dead || b.layer.loop !== b) return 0
|
|
451
|
+
const p = this.layerPhase(b.layer)
|
|
452
|
+
return p < 0 ? 0 : p
|
|
453
|
+
}
|
|
454
|
+
setLoopPhase(b: LoopRec, phase: number): void {
|
|
455
|
+
const m = b.members[0]
|
|
456
|
+
if (!m || b.dead || !this.id) return
|
|
457
|
+
_creator.animatorSeekPhase(this.id, m.slot.slot, phase) // seeking one member moves the group
|
|
458
|
+
}
|
|
459
|
+
setLoopValue(b: LoopRec, v: BlendPosition): void {
|
|
460
|
+
if (b.dead) { console.warn("Animator: this loop was replaced — use the object the latest playLoop() returned"); return }
|
|
461
|
+
b.value = v
|
|
462
|
+
if (!this.id) return
|
|
463
|
+
const [x, y] = typeof v === "number" ? [ v, 0 ] : [ v[0], v[1] ]
|
|
464
|
+
_creator.animatorSetBlendValue(this.id, b.layer.index, x, y)
|
|
465
|
+
}
|
|
466
|
+
/** What the layer shows this frame: its source — the one-shot, or the loop's members with their
|
|
467
|
+
* blend shares — in binding order (stable rows). */
|
|
468
|
+
active(L: LayerRec): ActiveClip[] {
|
|
469
|
+
const out: ActiveClip[] = []
|
|
470
|
+
for (const s of L.slots) {
|
|
471
|
+
const weight = this.slotWeight(s)
|
|
472
|
+
if (weight <= 0) continue
|
|
473
|
+
out.push({ name: s.name, clip: s.clip, weight, time: this.slotTime(s), loop: s.inBlend })
|
|
474
|
+
}
|
|
475
|
+
return out
|
|
476
|
+
}
|
|
477
|
+
slotWeight(s: SlotRec): number { return this.id && s.slot >= 0 ? _creator.animatorGetSlotWeight(this.id, s.slot) : 0 }
|
|
478
|
+
/** The slot's gait phase now, wrapped into 0–1 (-1 without a cycle). */
|
|
479
|
+
slotPhase(s: SlotRec): number {
|
|
480
|
+
if (!this.id || s.slot < 0) return -1
|
|
481
|
+
const p = _creator.animatorSlotCurveAt(this.id, s.slot, 0, -1)
|
|
482
|
+
return p < 0 ? -1 : p - Math.floor(p)
|
|
483
|
+
}
|
|
484
|
+
/** A baked curve of the slot at its current time: 1 travel (m) / 2 yaw (rad) / 3 speed (m/s). */
|
|
485
|
+
slotCurveAt(s: SlotRec, which: 1 | 2 | 3): number { return this.id && s.slot >= 0 ? _creator.animatorSlotCurveAt(this.id, s.slot, which, -1) : 0 }
|
|
486
|
+
/** The clip's total root yaw, radians (+ = left). */
|
|
487
|
+
slotTurn(s: SlotRec): number { return this.id && s.slot >= 0 ? _creator.animatorSlotTurn(this.id, s.slot) : 0 }
|
|
488
|
+
slotTime(s: SlotRec): number { return this.id && s.slot >= 0 ? _creator.animatorGetSlotTime(this.id, s.slot) : 0 }
|
|
489
|
+
seek(s: SlotRec, time: number): void { if (this.id && s.slot >= 0) _creator.animatorSeek(this.id, s.slot, time) }
|
|
490
|
+
|
|
491
|
+
/** Scrub: set a bound clip's playhead directly, seconds — an inspector's tool. */
|
|
492
|
+
seekTime(ref: string | number, time: number): void {
|
|
493
|
+
const found = this.resolveClip(ref)
|
|
494
|
+
if (!found || !this.ensure()) return
|
|
495
|
+
const s = this.slotFor(this.layers[0], found[0], found[1])
|
|
496
|
+
if (s) this.seek(s, time)
|
|
497
|
+
}
|
|
498
|
+
/** Re-aim a playing clip's TURN WARP (see `PlayOptions.turn`): the clip's baked heading is scaled so that the
|
|
499
|
+
* whole clip turns `deg` — set mid-play, the scale applies to what is still to come, so a controller that
|
|
500
|
+
* wants a different heading while a turn plays asks for (the clip's total × what it still needs / what
|
|
501
|
+
* the clip still turns). Undefined `deg` switches the warp off. */
|
|
502
|
+
setTurnOf(ref: string | number, deg: number | undefined): void {
|
|
503
|
+
const found = this.resolveClip(ref)
|
|
504
|
+
if (!found || !this.ensure()) return
|
|
505
|
+
const s = this.slotFor(this.layers[0], found[0], found[1])
|
|
506
|
+
if (s) _creator.animatorSlotSetTurn?.(this.id, s.slot, (deg ?? 0) * Math.PI / 180, deg !== undefined)
|
|
507
|
+
}
|
|
508
|
+
/** A bound clip's current playhead, seconds (-1 = not bound). */
|
|
509
|
+
timeOf(ref: string | number): number {
|
|
510
|
+
const found = this.resolveClip(ref)
|
|
511
|
+
if (!found || !this.ensure()) return -1
|
|
512
|
+
const s = this.slotFor(this.layers[0], found[0], found[1])
|
|
513
|
+
return s ? this.slotTime(s) : -1
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ---- contacts, phase, root curves ----
|
|
517
|
+
// The linear gait cycle through a clip's foot-downs — the bake's anchor rule (the first step: left → 0,
|
|
518
|
+
// right → 0.5; +0.5 per change of foot, +1 for the same foot again) fitted with the cycle count fixed at
|
|
519
|
+
// steps / 2 (every foot lands once per cycle): offset = the mean residual, wrapped. A span starting at
|
|
520
|
+
// t = 0 is a foot already down, not a step — unless the foot is airborne at the clip's end (the seam of a
|
|
521
|
+
// loop is its landing) while the clip does not start with both feet down.
|
|
522
|
+
/** What the engine measured on the clip once it was bound to this skeleton — binds it if needed. */
|
|
523
|
+
clipInfo(ref: string | number): ClipInfo | undefined {
|
|
524
|
+
const found = this.resolveClip(ref)
|
|
525
|
+
if (!found || !this.ensure()) return undefined
|
|
526
|
+
const s = this.slotFor(this.layers[0], found[0], found[1])
|
|
527
|
+
if (!s) return undefined
|
|
528
|
+
const id = this.id, slot = s.slot
|
|
529
|
+
_creator.animatorSlotCurveInfo(id, slot, curveInfo)
|
|
530
|
+
const spans = _creator.animatorSlotContacts(id, slot, new Float32Array(0))
|
|
531
|
+
const buf = new Float32Array(spans * 8) // stride 8: side, from, to, at xyz, stillFrom, stillTo
|
|
532
|
+
if (spans > 0) _creator.animatorSlotContacts(id, slot, buf)
|
|
533
|
+
const contacts: ClipInfo["contacts"] = [], hands: ClipInfo["hands"] = []
|
|
534
|
+
for (let i = 0; i < spans; i++) {
|
|
535
|
+
const still = buf[i * 8 + 6]! >= 0 ? { from: buf[i * 8 + 6]!, to: buf[i * 8 + 7]! } : undefined
|
|
536
|
+
// side 0 / 1 = the feet, 2 / 3 = the hands
|
|
537
|
+
const code = Math.round(buf[i * 8]!)
|
|
538
|
+
;(code >= 2 ? hands : contacts).push({ side: code % 2 === 0 ? "left" : "right", from: buf[i * 8 + 1]!, to: buf[i * 8 + 2]!,
|
|
539
|
+
at: [ buf[i * 8 + 3]!, buf[i * 8 + 4]!, buf[i * 8 + 5]! ], still })
|
|
540
|
+
}
|
|
541
|
+
const nTreads = _creator.animatorSlotTreads(id, slot, new Float32Array(0))
|
|
542
|
+
let treads: ClipInfo["treads"]
|
|
543
|
+
if (nTreads > 0) {
|
|
544
|
+
const tBuf = new Float32Array(nTreads + 1) // [0] = riser, then the levels
|
|
545
|
+
_creator.animatorSlotTreads(id, slot, tBuf)
|
|
546
|
+
treads = { riser: tBuf[0]!, levels: Array.from(tBuf.subarray(1)) }
|
|
547
|
+
}
|
|
548
|
+
const featuresAt = (time: number): Float32Array | undefined => {
|
|
549
|
+
const row = new Float32Array(37)
|
|
550
|
+
return _creator.animatorSlotFeatures(id, slot, time, row) ? row : undefined
|
|
551
|
+
}
|
|
552
|
+
const first = featuresAt(0)
|
|
553
|
+
return {
|
|
554
|
+
travel: curveInfo[1], speed: curveInfo[2], turn: _creator.animatorSlotTurn(id, slot),
|
|
555
|
+
inPlace: curveInfo[3] > 0.5, hasPhase: _creator.animatorSlotCurveAt(id, slot, 0, 0) >= 0, contacts, hands, treads,
|
|
556
|
+
featuresAt, cyclic: !!first && first[36]! > 0.5,
|
|
557
|
+
fitTo: (target: string | number, time: number, options: { align?: "phase" | "time" } = {}) => {
|
|
558
|
+
const c = this.fitCurve(s, target, options.align === "time" ? 1 : 0)
|
|
559
|
+
if (!c) return undefined
|
|
560
|
+
const f = Math.max(0, Math.min(c.values.length - 1, time / c.dt))
|
|
561
|
+
const k = Math.floor(f), u = f - k
|
|
562
|
+
return k + 1 >= c.values.length ? c.values[c.values.length - 1] : c.values[k] + (c.values[k + 1] - c.values[k]) * u
|
|
563
|
+
},
|
|
564
|
+
exitTo: (target: string | number, options: { within?: number, atContact?: boolean, align?: "phase" | "time" } = {}) =>
|
|
565
|
+
this.fitExit(s, target, options.within ?? 0.05, options.atContact ?? true, options.align === "time" ? 1 : 0),
|
|
566
|
+
cycle: () => fitCycle(contacts, found[1].duration),
|
|
567
|
+
alignTo: (reference: string | number, cycle?: { offset: number, cycles: number }) => {
|
|
568
|
+
const r = this.slotOf(reference)
|
|
569
|
+
if (!r) return undefined
|
|
570
|
+
const rc = cycle ?? this.clipInfo(reference)?.cycle() ?? { offset: 0, cycles: 1 }
|
|
571
|
+
const out = new Float32Array(4)
|
|
572
|
+
if (!_creator.animatorSlotAlign(id, r.slot, slot, rc.offset, rc.cycles, out)) return undefined
|
|
573
|
+
return { offset: out[0]!, cycles: out[1]!, score: out[2]!, margin: out[3]! }
|
|
574
|
+
},
|
|
575
|
+
phaseAt: (time: number) => _creator.animatorSlotCurveAt(id, slot, 0, time),
|
|
576
|
+
travelAt: (time: number) => _creator.animatorSlotCurveAt(id, slot, 1, time),
|
|
577
|
+
turnAt: (time: number) => _creator.animatorSlotCurveAt(id, slot, 2, time),
|
|
578
|
+
speedAt: (time: number) => _creator.animatorSlotCurveAt(id, slot, 3, time),
|
|
579
|
+
directionAt: (time: number) => [ _creator.animatorSlotCurveAt(id, slot, 4, time), _creator.animatorSlotCurveAt(id, slot, 5, time) ] as [number, number],
|
|
580
|
+
timeAtTurn: (yaw: number) => _creator.animatorSlotTimeAtTurn(id, slot, yaw),
|
|
581
|
+
kneePoleAt: (side: "left" | "right", time: number) => {
|
|
582
|
+
const ok = _creator.animatorSlotKneePole(id, slot, side === "left" ? 0 : 1, time, kneeOut)
|
|
583
|
+
if (!ok) return undefined
|
|
584
|
+
return { pole: [ kneeOut[0]!, kneeOut[1]!, kneeOut[2]! ] as [number, number, number], normal: [ kneeOut[3]!, kneeOut[4]!, kneeOut[5]! ] as [number, number, number] }
|
|
585
|
+
},
|
|
586
|
+
physicsAt: (time: number) => {
|
|
587
|
+
const ok = _creator.animatorSlotPhysics(id, slot, time, physOut)
|
|
588
|
+
if (!ok) return undefined
|
|
589
|
+
return {
|
|
590
|
+
com: [ physOut[0]!, physOut[1]!, physOut[2]! ] as [number, number, number],
|
|
591
|
+
velocity: [ physOut[3]!, physOut[4]!, physOut[5]! ] as [number, number, number],
|
|
592
|
+
angular: [ physOut[6]!, physOut[7]!, physOut[8]! ] as [number, number, number],
|
|
593
|
+
support: [ physOut[9]!, physOut[10]! ] as [number, number],
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/** The skeleton's calibrated knee hinge axis for a side, with its confidence report — measured once
|
|
599
|
+
* over every bound clip's knee rotation track. Undefined = no leg chain / no knee motion bound. */
|
|
600
|
+
kneeAxis(side: "left" | "right"): KneeAxisReport | undefined {
|
|
601
|
+
if (!this.ensure()) return undefined
|
|
602
|
+
const ok = _creator.animatorKneeAxis(this.id, side === "left" ? 0 : 1, kneeOut)
|
|
603
|
+
if (!ok) return undefined
|
|
604
|
+
const r: KneeAxisReport = { axis: [ kneeOut[0]!, kneeOut[1]!, kneeOut[2]! ], spreadMean: kneeOut[3]!, spreadMax: kneeOut[4]!, samples: kneeOut[5]! }
|
|
605
|
+
if (kneeOut[6]! > -1e8) r.plant = { ankleY: kneeOut[6]!, toeY: kneeOut[7]! }
|
|
606
|
+
return r
|
|
607
|
+
}
|
|
608
|
+
/** STEP WARP v2 (the warp rewrite's knobs): `stride` scales each foot's travel-direction offset
|
|
609
|
+
* from its hip — the step shortens or lengthens, uniformly through stance and swing; `lift` is
|
|
610
|
+
* METRES added to each foot's height, gated to the swing by the contact marks (0 = neutral,
|
|
611
|
+
* negative = a shuffle — no probing), and half of what it adds raises the pelvis so the body
|
|
612
|
+
* steps higher with the foot; `pitch` (degrees, + = toes up) rotates each foot about its
|
|
613
|
+
* own lateral axis — a slope's foot rotation; `slope` (degrees, + = ascending) is the INVISIBLE
|
|
614
|
+
* STAIRCASE: foot heights follow the incline (leading foot higher) and the feet auto-pitch by
|
|
615
|
+
* the same angle — pair it with raising the character by tan(slope) × the stride-scaled clip
|
|
616
|
+
* travel (`clipInfo(clip).travelAt`), which holds every planted foot's world height constant on
|
|
617
|
+
* its own tread. Solved in the calibrated knee hinge plane with a SOFT reach (a leg at its limit
|
|
618
|
+
* keeps a residual knee bend instead of popping against the clamp), and whenever a leg would
|
|
619
|
+
* overreach — a descent, a long stride — the pelvis lowers by exactly the excess (weighted by
|
|
620
|
+
* the contact marks, spring-followed; zero when nothing overreaches, so an ascent or a shorter
|
|
621
|
+
* stride is untouched). Omit / null = stage off. */
|
|
622
|
+
setStepWarp(options?: { stride?: number, lift?: number, pitch?: number, slope?: number } | null): void {
|
|
623
|
+
if (!this.ensure()) return
|
|
624
|
+
_creator.animatorSetStepWarp(this.id, !!options, options?.stride ?? 1, options?.lift ?? 0, options?.pitch ?? 0, options?.slope ?? 0)
|
|
625
|
+
}
|
|
626
|
+
/** A slot for a clip on the base layer, bound on demand — what the pair-wise measures address. */
|
|
627
|
+
private slotOf(ref: string | number): SlotRec | undefined {
|
|
628
|
+
const found = this.resolveClip(ref)
|
|
629
|
+
if (!found || !this.ensure()) return undefined
|
|
630
|
+
return this.slotFor(this.layers[0], found[0], found[1])
|
|
631
|
+
}
|
|
632
|
+
/** The baked distance curve between two clips, cached per pair (the engine caches the bake itself;
|
|
633
|
+
* this keeps the samples on the JS side so a controller can read them per frame without copying). */
|
|
634
|
+
private readonly _fit = new Map<string, { dt: number, values: Float32Array }>()
|
|
635
|
+
private fitCurve(src: SlotRec, target: string | number, align: 0 | 1): { dt: number, values: Float32Array } | undefined {
|
|
636
|
+
const t = this.slotOf(target)
|
|
637
|
+
if (!t) return undefined
|
|
638
|
+
const key = `${src.slot}>${t.slot}:${align}`
|
|
639
|
+
const have = this._fit.get(key)
|
|
640
|
+
if (have) return have
|
|
641
|
+
const n = _creator.animatorSlotFit(this.id, src.slot, t.slot, align, new Float32Array(0))
|
|
642
|
+
if (n === 0) return undefined
|
|
643
|
+
const values = new Float32Array(n)
|
|
644
|
+
_creator.animatorSlotFit(this.id, src.slot, t.slot, align, values)
|
|
645
|
+
_creator.animatorSlotCurveInfo(this.id, src.slot, curveInfo)
|
|
646
|
+
const curve = { dt: curveInfo[0] || 1 / 60, values }
|
|
647
|
+
this._fit.set(key, curve)
|
|
648
|
+
return curve
|
|
649
|
+
}
|
|
650
|
+
/** The earliest moment handing `src` over to `target` costs no more than `within` metres. */
|
|
651
|
+
private fitExit(src: SlotRec, target: string | number, within: number, atContact: boolean, align: 0 | 1): number {
|
|
652
|
+
const t = this.slotOf(target)
|
|
653
|
+
return t ? _creator.animatorSlotExit(this.id, src.slot, t.slot, within, atContact, align) : -1
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** The contact bones, '
|
|
657
|
+
'-joined per side ("" = classify by name). */
|
|
658
|
+
private _feet?: [string, string]
|
|
659
|
+
setFeet(left: string, right: string): void {
|
|
660
|
+
this._feet = [ left, right ]
|
|
661
|
+
if (this.id) _creator.animatorSetFeet(this.id, left, right)
|
|
662
|
+
}
|
|
663
|
+
/** Stride / orientation warping, as the engine's fixed-order parameter array. */
|
|
664
|
+
private _warp?: Float32Array
|
|
665
|
+
setWarp(params: Float32Array): void {
|
|
666
|
+
this._warp = params
|
|
667
|
+
if (this.id) _creator.animatorSetWarpParams(this.id, params)
|
|
668
|
+
}
|
|
669
|
+
/** The feet stage (lock / ground IK), as the engine's fixed-order parameter array. */
|
|
670
|
+
private _feetParams?: Float32Array
|
|
671
|
+
setFeetParams(params: Float32Array): void {
|
|
672
|
+
this._feetParams = params
|
|
673
|
+
if (this.id && _creator.animatorSetFeetParams) _creator.animatorSetFeetParams(this.id, params)
|
|
674
|
+
}
|
|
675
|
+
/** One foot's lock state after this frame's evaluation into `out` (8 floats); false = no such foot
|
|
676
|
+
* or no feet stage on this host. */
|
|
677
|
+
footState(side: 0 | 1, out: Float32Array): boolean {
|
|
678
|
+
return this.id !== 0 && _creator.animatorFootState !== undefined && _creator.animatorFootState(this.id, side, out)
|
|
679
|
+
}
|
|
680
|
+
private readonly _steps = new Set<StepHandler>()
|
|
681
|
+
onStepHandler(cb: StepHandler): void { this._steps.add(cb) }
|
|
682
|
+
offStepHandler(cb: StepHandler): void { this._steps.delete(cb) }
|
|
683
|
+
private onStep(side: number, x: number, y: number, z: number): void {
|
|
684
|
+
if (this._steps.size === 0) return
|
|
685
|
+
const at = new Vec3(x, y, z)
|
|
686
|
+
for (const cb of [ ...this._steps ]) cb(side === 0 ? "left" : "right", at)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ---- props ----
|
|
690
|
+
setSpeed(v: number): void { this.speed = v; if (this.id) _creator.animatorSetGlobal(this.id, v, false) }
|
|
691
|
+
setRootMotion(on: boolean, rotation = false): void { if (this.ensure()) _creator.animatorSetRootMotion(this.id, on ? "*" : "", (on ? 2 : 0) | (on && rotation ? 4 : 0)) }
|
|
692
|
+
|
|
693
|
+
// ---- events ----
|
|
694
|
+
on(event: string, cb: ClipEventHandler): void { (this._listeners.get(event) ?? this._listeners.set(event, new Set()).get(event)!).add(cb) }
|
|
695
|
+
off(event: string, cb: ClipEventHandler): void { this._listeners.get(event)?.delete(cb) }
|
|
696
|
+
/** Native slot event: 0 completed / 1 loop / 2 settled (no longer a source) / 3 hand-over / 4+i clip event i. */
|
|
697
|
+
private onEvent(slotIndex: number, type: number): void {
|
|
698
|
+
let s: SlotRec | undefined
|
|
699
|
+
for (const L of this.layers) { s = L.slots.find((x) => x.slot === slotIndex); if (s) break }
|
|
700
|
+
if (!s) return
|
|
701
|
+
if (type >= 4) {
|
|
702
|
+
const name = s.clip._events[type - 4]?.name
|
|
703
|
+
const cbs = name ? this._listeners.get(name) : undefined
|
|
704
|
+
if (cbs) for (const cb of [ ...cbs ]) cb(s.name, s.layer.view)
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
const p = s.playback
|
|
708
|
+
if (!p?.playing) return
|
|
709
|
+
if (type === 3) this.settle(p, true) // hand-over: the clip is over; what the app starts now takes over from it
|
|
710
|
+
else if (type === 2) this.settle(p, false) // dropped by the engine without the SDK asking (backstop)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function fitCycle(contacts: ClipInfo["contacts"], duration: number): ClipCycle | undefined {
|
|
715
|
+
if (!(duration > 0)) return undefined
|
|
716
|
+
const sides = [ "left", "right" ] as const
|
|
717
|
+
const bothDown = sides.every((side) => contacts.some((c) => c.side === side && c.from <= 0.02))
|
|
718
|
+
const steps: { t: number, side: 0 | 1 }[] = []
|
|
719
|
+
for (let side = 0; side < 2; side++) {
|
|
720
|
+
const spans = contacts.filter((c) => c.side === sides[side]).sort((a, b) => a.from - b.from)
|
|
721
|
+
const airborneAtEnd = spans.length > 0 && spans[spans.length - 1]!.to < duration * 0.85
|
|
722
|
+
for (const sp of spans) if (sp.from > 0.02 || (airborneAtEnd && !bothDown)) steps.push({ t: sp.from, side: side as 0 | 1 })
|
|
723
|
+
}
|
|
724
|
+
steps.sort((a, b) => a.t - b.t)
|
|
725
|
+
if (steps.length === 0) return undefined
|
|
726
|
+
const phi: number[] = []
|
|
727
|
+
for (let i = 0; i < steps.length; i++) phi.push(i === 0 ? (steps[0]!.side === 0 ? 0 : 0.5) : phi[i - 1]! + (steps[i]!.side !== steps[i - 1]!.side ? 0.5 : 1))
|
|
728
|
+
const cycles = steps.length / 2
|
|
729
|
+
let offset = 0
|
|
730
|
+
for (let i = 0; i < steps.length; i++) offset += phi[i]! - cycles * steps[i]!.t / duration
|
|
731
|
+
offset /= steps.length
|
|
732
|
+
offset -= Math.floor(offset)
|
|
733
|
+
let residual = 0
|
|
734
|
+
for (let i = 0; i < steps.length; i++) { let d = phi[i]! - (offset + cycles * steps[i]!.t / duration); d -= Math.round(d); residual = Math.max(residual, Math.abs(d)) }
|
|
735
|
+
return { offset, cycles, residual, steps: steps.length }
|
|
736
|
+
}
|