lecodes-cli 0.17.2 → 0.18.1
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/dist/index.js +2376 -755
- package/package.json +4 -4
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +52 -8
- package/runtime/sdk/compile/assetMacro.ts +116 -15
- package/runtime/sdk/compile/bundler.ts +39 -4
- package/runtime/sdk/compile/compileProject.ts +16 -1
- package/runtime/sdk/compile/header.ts +6 -1
- package/runtime/sdk/compile/index.ts +31 -0
- package/runtime/sdk/compile/liteMaterial.ts +247 -0
- package/runtime/sdk/compile/sceneEditor.ts +11 -1
- package/runtime/sdk/compile/shaderSchema.ts +202 -0
- package/runtime/sdk/compile/shaderTargets.ts +81 -0
- package/runtime/sdk/core/Aspect.ts +363 -95
- package/runtime/sdk/core/compWrite.ts +42 -0
- package/runtime/sdk/core/fields.ts +1 -1
- package/runtime/sdk/core/time.ts +81 -0
- package/runtime/sdk/g2/Camera2D.ts +8 -1
- package/runtime/sdk/g2/CharacterController2D.ts +253 -53
- package/runtime/sdk/g2/Node2D.ts +80 -10
- package/runtime/sdk/g2/OneWay2D.ts +66 -0
- package/runtime/sdk/g2/Physics2D.ts +240 -30
- package/runtime/sdk/g2/Scene2D.ts +33 -1
- package/runtime/sdk/g2/Shape2D.ts +218 -22
- package/runtime/sdk/g2/Trigger2D.ts +42 -12
- package/runtime/sdk/g2/groups2d.ts +106 -0
- package/runtime/sdk/g2/loop.ts +15 -4
- package/runtime/sdk/gl/Camera.ts +41 -0
- package/runtime/sdk/gl/CameraPlace.ts +52 -0
- package/runtime/sdk/gl/CharacterController.ts +184 -55
- package/runtime/sdk/gl/Gearbox.ts +212 -0
- package/runtime/sdk/gl/Geometry.ts +70 -9
- package/runtime/sdk/gl/IK.ts +193 -174
- package/runtime/sdk/gl/Light.ts +64 -2
- package/runtime/sdk/gl/Lightmap.ts +179 -0
- package/runtime/sdk/gl/Material.ts +36 -0
- package/runtime/sdk/gl/Mesh.ts +6 -23
- package/runtime/sdk/gl/Model.ts +23 -8
- package/runtime/sdk/gl/Node.ts +350 -285
- package/runtime/sdk/gl/Physics.ts +222 -126
- package/runtime/sdk/gl/Scene.ts +175 -8
- package/runtime/sdk/gl/Shape.ts +255 -12
- package/runtime/sdk/gl/Trigger.ts +1 -6
- package/runtime/sdk/gl/Vehicle.ts +473 -0
- package/runtime/sdk/gl/Wheel.ts +240 -0
- package/runtime/sdk/gl/{AnimationClip.ts → animation/AnimationClip.ts} +37 -7
- package/runtime/sdk/gl/animation/Animator.ts +87 -0
- package/runtime/sdk/gl/animation/Layer.ts +29 -0
- package/runtime/sdk/gl/animation/Loop.ts +25 -0
- package/runtime/sdk/gl/animation/Playback.ts +43 -0
- package/runtime/sdk/gl/animation/core.ts +294 -0
- package/runtime/sdk/gl/scenarios.ts +291 -349
- package/runtime/sdk/inject.ts +186 -162
- package/runtime/sdk/runtime/app.ts +13 -0
- package/runtime/sdk/runtime/input.ts +169 -6
- package/runtime/sdk/scene/defineScene.ts +1227 -1016
- package/runtime/sdk/scene/gizmos.ts +148 -0
- package/runtime/sdk/scene/material.ts +188 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/gl/Animator.ts +0 -642
- package/runtime/sdk/gl/ModelAnimation.ts +0 -95
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// The engine-facing core behind Animator / Layer / Blend / Playback (docs/animator-plan.md). NOT exported
|
|
2
|
+
// from the SDK: the public classes are thin views over the records here. The engine runs the whole
|
|
3
|
+
// per-frame loop (fades, blend spaces, one-shot fade-back, root motion); this file binds clips,
|
|
4
|
+
// issues play/stop/blend calls and routes the engine's slot events back to playbacks.
|
|
5
|
+
|
|
6
|
+
import { AnimationClip } from "./AnimationClip"
|
|
7
|
+
import { Loop } from "./Loop"
|
|
8
|
+
import { Layer } from "./Layer"
|
|
9
|
+
import { Playback } from "./Playback"
|
|
10
|
+
import type { Node } from "../Node"
|
|
11
|
+
|
|
12
|
+
/** A position on a blend axis (1D) or plane (2D). */
|
|
13
|
+
export type BlendPosition = number | readonly [number, number]
|
|
14
|
+
|
|
15
|
+
export type PlayOptions = {
|
|
16
|
+
/** Crossfade seconds for BOTH ends (default 0 = cut). */
|
|
17
|
+
fade?: number
|
|
18
|
+
/** Fade-in seconds (overrides `fade`). */
|
|
19
|
+
fadeIn?: number
|
|
20
|
+
/** One-shots: fade-back seconds at the END (overrides `fade`). The return fade starts this long
|
|
21
|
+
* BEFORE the clip ends, while it still plays — no freeze-frame on the last pose. On a layer with
|
|
22
|
+
* no blend to return to, giving `fadeOut` releases the layer at the end instead of holding the
|
|
23
|
+
* last frame. */
|
|
24
|
+
fadeOut?: number
|
|
25
|
+
/** Playback rate for this clip (default 1). */
|
|
26
|
+
speed?: number
|
|
27
|
+
/** Rewind even if the clip is already playing (default: rewind only if it isn't). */
|
|
28
|
+
restart?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type StopOptions = { fade?: number }
|
|
32
|
+
|
|
33
|
+
export type LoopOptions = {
|
|
34
|
+
/** Crossfade seconds from whatever the layer shows — the previous loop, or a one-shot still playing
|
|
35
|
+
* (default 0 = cut). */
|
|
36
|
+
fade?: number
|
|
37
|
+
/** Playback rate of the loop's clips (default 1). */
|
|
38
|
+
speed?: number
|
|
39
|
+
}
|
|
40
|
+
/** What a layer loops: one clip (name / index), or a blend space `{ name: position }`. */
|
|
41
|
+
export type LoopDef = string | number | Record<string, BlendPosition>
|
|
42
|
+
|
|
43
|
+
export type LayerOptions = {
|
|
44
|
+
/** Bone name(s): the layer drives only these subtrees (`'Spine1'` = upper body). Default: the whole rig. */
|
|
45
|
+
mask?: string | string[]
|
|
46
|
+
/** Blend each clip's DELTA from its own first frame on top of the layers below (lean, breathe, recoil)
|
|
47
|
+
* instead of overriding them. */
|
|
48
|
+
additive?: boolean
|
|
49
|
+
/** Layer contribution 0–1 (default 1). */
|
|
50
|
+
weight?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ClipEventHandler = (clip: string, layer: Layer) => void
|
|
54
|
+
|
|
55
|
+
// ---- records -------------------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export type SlotRec = { name: string, clip: AnimationClip, slot: number, layer: LayerRec, inBlend: boolean, playback?: PlaybackRec }
|
|
58
|
+
export type PlaybackRec = { slot: SlotRec, playing: boolean, done: Promise<boolean>, resolve: (natural: boolean) => void }
|
|
59
|
+
export type LayerRec = { index: number, view: Layer, slots: SlotRec[], byName: Map<string, SlotRec>, loop?: LoopRec, weight: number, mask: string, additive: boolean }
|
|
60
|
+
export type LoopRec = { layer: LayerRec, members: { name: string, slot: SlotRec }[], value: BlendPosition, view: Loop, dead: boolean }
|
|
61
|
+
|
|
62
|
+
const handlers = new Map<number, (slot: number, type: number) => void>()
|
|
63
|
+
let eventsInstalled = false
|
|
64
|
+
const ensureEvents = (): void => {
|
|
65
|
+
if (eventsInstalled) return
|
|
66
|
+
eventsInstalled = true
|
|
67
|
+
_creator.setOnAnimatorEvent((animatorId, slot, type) => handlers.get(animatorId)?.(slot, type))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const settled = (slot: SlotRec): PlaybackRec => {
|
|
71
|
+
let resolve = (_n: boolean): void => {}
|
|
72
|
+
const done = new Promise<boolean>((r) => { resolve = r })
|
|
73
|
+
resolve(false)
|
|
74
|
+
return { slot, playing: false, done, resolve }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class Core {
|
|
78
|
+
id = 0
|
|
79
|
+
/** Added clips by name (`addClip`) — the lookup index; an added name overrides an embedded one. */
|
|
80
|
+
readonly table = new Map<string, AnimationClip>()
|
|
81
|
+
readonly layers: LayerRec[] = []
|
|
82
|
+
speed = 1
|
|
83
|
+
private _embedded?: AnimationClip[]
|
|
84
|
+
private readonly _added: { name: string, clip: AnimationClip }[] = []
|
|
85
|
+
private _list?: AnimationClip[]
|
|
86
|
+
private readonly _listeners = new Map<string, Set<ClipEventHandler>>()
|
|
87
|
+
|
|
88
|
+
readonly node: Node
|
|
89
|
+
constructor(node: Node) { this.node = node; this.layers.push(this.newLayer()) }
|
|
90
|
+
|
|
91
|
+
// ---- clips ----
|
|
92
|
+
embedded(): AnimationClip[] {
|
|
93
|
+
if (!this._embedded) {
|
|
94
|
+
this._embedded = AnimationClip._ofSet(_creator.getGlbClipSet(this.node.id))
|
|
95
|
+
const seen = new Set<string>()
|
|
96
|
+
for (const c of this._embedded) {
|
|
97
|
+
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))`)
|
|
98
|
+
seen.add(c.name)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return this._embedded
|
|
102
|
+
}
|
|
103
|
+
/** The ordered clip list: the GLB's clips in file order (an added clip of the same name takes the
|
|
104
|
+
* embedded one's place), then the remaining added clips in insertion order. Cached until addClip. */
|
|
105
|
+
clips(): AnimationClip[] {
|
|
106
|
+
if (this._list) return this._list
|
|
107
|
+
const out: AnimationClip[] = []
|
|
108
|
+
const placed = new Set<string>()
|
|
109
|
+
for (const c of this.embedded()) {
|
|
110
|
+
const override = this.table.get(c.name)
|
|
111
|
+
if (override && !placed.has(c.name)) { out.push(override); placed.add(c.name) }
|
|
112
|
+
else out.push(c)
|
|
113
|
+
}
|
|
114
|
+
for (const a of this._added) if (!placed.has(a.name)) { out.push(a.clip); placed.add(a.name) }
|
|
115
|
+
return this._list = out
|
|
116
|
+
}
|
|
117
|
+
addClip(name: string, clip: AnimationClip): void {
|
|
118
|
+
const i = this._added.findIndex((a) => a.name === name)
|
|
119
|
+
if (i >= 0) this._added[i] = { name, clip }
|
|
120
|
+
else this._added.push({ name, clip })
|
|
121
|
+
this.table.set(name, clip)
|
|
122
|
+
this._list = undefined
|
|
123
|
+
}
|
|
124
|
+
/** A clip by name (added first, then embedded), or by index into `clips()` (none = the first). */
|
|
125
|
+
resolveClip(ref: string | number | undefined): [string, AnimationClip] | undefined {
|
|
126
|
+
if (ref === undefined || typeof ref === "number") { const c = this.clips()[ref ?? 0]; return c ? [ c.name, c ] : undefined }
|
|
127
|
+
const c = this.table.get(ref) ?? this.embedded().find((e) => e.name === ref)
|
|
128
|
+
return c ? [ ref, c ] : undefined
|
|
129
|
+
}
|
|
130
|
+
private clipNames(): string { return this.clips().map((c) => c.name).join(", ") || "(empty)" }
|
|
131
|
+
|
|
132
|
+
// ---- lifecycle ----
|
|
133
|
+
/** Create the native animator on demand. False = nothing to animate (warned). */
|
|
134
|
+
ensure(): boolean {
|
|
135
|
+
if (this.id) return true
|
|
136
|
+
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 }
|
|
137
|
+
ensureEvents()
|
|
138
|
+
this.id = _creator.animatorCreate(this.node.id)
|
|
139
|
+
if (!this.id) { console.warn("Animator: node has no transform hierarchy"); return false }
|
|
140
|
+
handlers.set(this.id, (slot, type) => this.onEvent(slot, type))
|
|
141
|
+
if (this.speed !== 1) _creator.animatorSetGlobal(this.id, this.speed, false)
|
|
142
|
+
for (const L of this.layers) if (L.index > 0) this.pushLayer(L)
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
145
|
+
destroy(): void {
|
|
146
|
+
for (const L of this.layers) for (const s of L.slots) if (s.playback) this.settle(s.playback, false)
|
|
147
|
+
if (this.id) { _creator.animatorDestroy(this.id); handlers.delete(this.id); this.id = 0 }
|
|
148
|
+
for (const L of this.layers) { L.slots = []; L.byName.clear(); if (L.loop) { L.loop.dead = true; L.loop = undefined } }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---- layers ----
|
|
152
|
+
newLayer(options?: LayerOptions): LayerRec {
|
|
153
|
+
const rec: LayerRec = { index: this.layers.length, view: undefined as unknown as Layer, slots: [], byName: new Map(), weight: options?.weight ?? 1, mask: "", additive: options?.additive ?? false }
|
|
154
|
+
if (options?.mask !== undefined) rec.mask = Array.isArray(options.mask) ? options.mask.join("\n") : options.mask
|
|
155
|
+
rec.view = new Layer(rec, this)
|
|
156
|
+
return rec
|
|
157
|
+
}
|
|
158
|
+
addLayer(options: LayerOptions): Layer {
|
|
159
|
+
if (this.layers.length >= 255) { console.warn("Animator: too many layers"); return this.layers[this.layers.length - 1].view }
|
|
160
|
+
const rec = this.newLayer(options)
|
|
161
|
+
this.layers.push(rec)
|
|
162
|
+
if (this.id) this.pushLayer(rec)
|
|
163
|
+
return rec.view
|
|
164
|
+
}
|
|
165
|
+
pushLayer(L: LayerRec): void {
|
|
166
|
+
if (!this.id) return
|
|
167
|
+
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"}`)
|
|
168
|
+
_creator.animatorSetLayer(this.id, L.index, L.weight, L.additive, L.mask)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---- slots ----
|
|
172
|
+
/** Bind a clip on a layer (once per name). */
|
|
173
|
+
slotFor(L: LayerRec, name: string, clip: AnimationClip): SlotRec | undefined {
|
|
174
|
+
const have = L.byName.get(name)
|
|
175
|
+
if (have && have.clip === clip) return have
|
|
176
|
+
const slot = _creator.animatorBind(this.id, clip._set, clip._index, L.index)
|
|
177
|
+
if (slot < 0) { console.warn(`Animator: could not bind clip '${name}'`); return undefined }
|
|
178
|
+
const bound = _creator.animatorBoundTracks(this.id, slot)
|
|
179
|
+
if (clip.trackCount > 0 && bound === 0) console.warn(`Animator: '${name}' — no track matched a node name under ${this.node.name || "the node"} (different rig?)`)
|
|
180
|
+
else if (bound < clip.trackCount) console.warn(`Animator: '${name}' — ${clip.trackCount - bound}/${clip.trackCount} tracks unbound (bone names not found)`)
|
|
181
|
+
const rec: SlotRec = { name, clip, slot, layer: L, inBlend: false }
|
|
182
|
+
L.slots.push(rec)
|
|
183
|
+
L.byName.set(name, rec)
|
|
184
|
+
return rec
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---- play / stop ----
|
|
188
|
+
play(L: LayerRec, ref: string | number | undefined, o: PlayOptions): Playback {
|
|
189
|
+
const found = this.resolveClip(ref)
|
|
190
|
+
if (!found) {
|
|
191
|
+
console.warn(`Animator: no clip ${ref === undefined ? "to play" : JSON.stringify(ref)} on ${this.node.name || "the node"} — table: ${this.clipNames()}`)
|
|
192
|
+
return new Playback(settled({ name: "", clip: AnimationClip._none(), slot: -1, layer: L, inBlend: false }), this)
|
|
193
|
+
}
|
|
194
|
+
const [name, clip] = found
|
|
195
|
+
if (!this.ensure()) return new Playback(settled({ name, clip, slot: -1, layer: L, inBlend: false }), this)
|
|
196
|
+
const s = this.slotFor(L, name, clip)
|
|
197
|
+
if (!s) return new Playback(settled({ name, clip, slot: -1, layer: L, inBlend: false }), this)
|
|
198
|
+
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) }
|
|
199
|
+
const fadeIn = o.fadeIn ?? o.fade ?? 0
|
|
200
|
+
const fadeOut = o.fadeOut ?? o.fade ?? -1
|
|
201
|
+
// the other one-shots on this layer are cut short (their promises resolve false)
|
|
202
|
+
for (const other of L.slots) if (other.playback) this.settle(other.playback, false)
|
|
203
|
+
let resolve = (_n: boolean): void => {}
|
|
204
|
+
const done = new Promise<boolean>((r) => { resolve = r })
|
|
205
|
+
const rec: PlaybackRec = { slot: s, playing: true, done, resolve }
|
|
206
|
+
s.playback = rec
|
|
207
|
+
_creator.animatorPlay(this.id, s.slot, false, o.speed ?? 1, fadeIn, fadeOut, o.restart ?? false)
|
|
208
|
+
return new Playback(rec, this)
|
|
209
|
+
}
|
|
210
|
+
/** A one-shot on the layer hasn't handed over yet. */
|
|
211
|
+
busy(L: LayerRec): boolean { return L.slots.some((s) => s.playback?.playing) }
|
|
212
|
+
settle(p: PlaybackRec, natural: boolean): void {
|
|
213
|
+
p.playing = false
|
|
214
|
+
if (p.slot.playback === p) p.slot.playback = undefined
|
|
215
|
+
p.resolve(natural)
|
|
216
|
+
}
|
|
217
|
+
stopSlot(s: SlotRec, fade: number): void {
|
|
218
|
+
if (s.playback) this.settle(s.playback, false)
|
|
219
|
+
if (this.id && s.slot >= 0) _creator.animatorStop(this.id, s.layer.index, s.slot, fade)
|
|
220
|
+
}
|
|
221
|
+
stopLayer(L: LayerRec, fade: number): void {
|
|
222
|
+
for (const s of L.slots) if (s.playback) this.settle(s.playback, false)
|
|
223
|
+
if (L.loop) { L.loop.dead = true; L.loop = undefined }
|
|
224
|
+
if (this.id) _creator.animatorStop(this.id, L.index, -1, fade)
|
|
225
|
+
}
|
|
226
|
+
stopAll(fade: number): void { for (const L of this.layers) this.stopLayer(L, fade) }
|
|
227
|
+
|
|
228
|
+
// ---- loop ----
|
|
229
|
+
loop(L: LayerRec, def: LoopDef, o: LoopOptions): Loop | undefined {
|
|
230
|
+
if (!this.ensure()) return undefined
|
|
231
|
+
const fade = o.fade ?? 0
|
|
232
|
+
if (L.loop) { L.loop.dead = true; for (const m of L.loop.members) m.slot.inBlend = false; L.loop = undefined }
|
|
233
|
+
for (const s of L.slots) if (s.playback) this.settle(s.playback, false) // one-shots on the layer are cut short
|
|
234
|
+
const space: Record<string, BlendPosition> = typeof def === "object" ? def : {}
|
|
235
|
+
if (typeof def !== "object") {
|
|
236
|
+
const found = this.resolveClip(def)
|
|
237
|
+
if (!found) { console.warn(`Animator: no clip ${JSON.stringify(def)} to loop — table: ${this.clipNames()}`); return undefined }
|
|
238
|
+
space[found[0]] = 0
|
|
239
|
+
}
|
|
240
|
+
const rec: LoopRec = { layer: L, members: [], value: 0, view: undefined as unknown as Loop, dead: false }
|
|
241
|
+
rec.view = new Loop(rec, this)
|
|
242
|
+
const slots: number[] = [], positions: number[] = []
|
|
243
|
+
const is2D = Object.values(space).some((p) => typeof p !== "number")
|
|
244
|
+
for (const [name, at] of Object.entries(space)) {
|
|
245
|
+
const found = this.resolveClip(name)
|
|
246
|
+
if (!found) { console.warn(`Animator: loop clip '${name}' is not in the clip table`); continue }
|
|
247
|
+
const s = this.slotFor(L, name, found[1])
|
|
248
|
+
if (!s) continue
|
|
249
|
+
if (s.playback) this.settle(s.playback, false)
|
|
250
|
+
s.inBlend = true
|
|
251
|
+
rec.members.push({ name, slot: s })
|
|
252
|
+
slots.push(s.slot)
|
|
253
|
+
if (is2D) positions.push(typeof at === "number" ? at : at[0], typeof at === "number" ? 0 : at[1])
|
|
254
|
+
else positions.push(at as number)
|
|
255
|
+
}
|
|
256
|
+
L.loop = rec
|
|
257
|
+
_creator.animatorSetBlend(this.id, L.index, is2D ? 2 : 1, new Uint16Array(slots), new Float32Array(positions), fade, o.speed ?? 1)
|
|
258
|
+
return rec.view
|
|
259
|
+
}
|
|
260
|
+
setLoopValue(b: LoopRec, v: BlendPosition): void {
|
|
261
|
+
if (b.dead) { console.warn("Animator: this loop was replaced — use the object the latest playLoop() returned"); return }
|
|
262
|
+
b.value = v
|
|
263
|
+
if (!this.id) return
|
|
264
|
+
const [x, y] = typeof v === "number" ? [ v, 0 ] : [ v[0], v[1] ]
|
|
265
|
+
_creator.animatorSetBlendValue(this.id, b.layer.index, x, y)
|
|
266
|
+
}
|
|
267
|
+
slotWeight(s: SlotRec): number { return this.id && s.slot >= 0 ? _creator.animatorGetSlotWeight(this.id, s.slot) : 0 }
|
|
268
|
+
slotTime(s: SlotRec): number { return this.id && s.slot >= 0 ? _creator.animatorGetSlotTime(this.id, s.slot) : 0 }
|
|
269
|
+
seek(s: SlotRec, time: number): void { if (this.id && s.slot >= 0) _creator.animatorSeek(this.id, s.slot, time) }
|
|
270
|
+
|
|
271
|
+
// ---- props ----
|
|
272
|
+
setSpeed(v: number): void { this.speed = v; if (this.id) _creator.animatorSetGlobal(this.id, v, false) }
|
|
273
|
+
setRootMotion(on: boolean): void { if (this.ensure()) _creator.animatorSetRootMotion(this.id, on ? "*" : "", on ? 2 : 0) }
|
|
274
|
+
|
|
275
|
+
// ---- events ----
|
|
276
|
+
on(event: string, cb: ClipEventHandler): void { (this._listeners.get(event) ?? this._listeners.set(event, new Set()).get(event)!).add(cb) }
|
|
277
|
+
off(event: string, cb: ClipEventHandler): void { this._listeners.get(event)?.delete(cb) }
|
|
278
|
+
/** Native slot event: 0 completed / 1 loop / 2 settled (faded to 0) / 3 hand-over / 4+i clip event i. */
|
|
279
|
+
private onEvent(slotIndex: number, type: number): void {
|
|
280
|
+
let s: SlotRec | undefined
|
|
281
|
+
for (const L of this.layers) { s = L.slots.find((x) => x.slot === slotIndex); if (s) break }
|
|
282
|
+
if (!s) return
|
|
283
|
+
if (type >= 4) {
|
|
284
|
+
const name = s.clip._events[type - 4]?.name
|
|
285
|
+
const cbs = name ? this._listeners.get(name) : undefined
|
|
286
|
+
if (cbs) for (const cb of [ ...cbs ]) cb(s.name, s.layer.view)
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
const p = s.playback
|
|
290
|
+
if (!p) return
|
|
291
|
+
if (type === 3) this.settle(p, true) // hand-over: the clip is over; the app's next call redirects the fade
|
|
292
|
+
else if (type === 2) this.settle(p, false) // faded out after a stop / replacement before handing over
|
|
293
|
+
}
|
|
294
|
+
}
|