@zephyr3d/editor 0.3.5 → 0.3.7

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.
Files changed (48) hide show
  1. package/dist/assets/{index-DlnbO59X.js → index-ySiLtSxz.js} +2 -2
  2. package/dist/assistant/zephyr-types-index.json +14215 -7892
  3. package/dist/index.html +1 -1
  4. package/dist/modules/zephyr3d_backend-webgl.js.map +1 -1
  5. package/dist/modules/zephyr3d_backend-webgpu.js.map +1 -1
  6. package/dist/modules/zephyr3d_base.js +152 -152
  7. package/dist/modules/zephyr3d_base.js.map +1 -1
  8. package/dist/modules/zephyr3d_imgui.js.map +1 -1
  9. package/dist/modules/zephyr3d_loaders.js +6 -6
  10. package/dist/modules/zephyr3d_loaders.js.map +1 -1
  11. package/dist/modules/zephyr3d_scene.js +2538 -348
  12. package/dist/modules/zephyr3d_scene.js.map +1 -1
  13. package/dist/vendor/zephyr3d/backend-webgl/dist/index.js.map +1 -1
  14. package/dist/vendor/zephyr3d/backend-webgpu/dist/index.js.map +1 -1
  15. package/dist/vendor/zephyr3d/base/dist/vfs/common.js +152 -152
  16. package/dist/vendor/zephyr3d/base/dist/vfs/common.js.map +1 -1
  17. package/dist/vendor/zephyr3d/imgui/dist/index.d.ts +7 -0
  18. package/dist/vendor/zephyr3d/imgui/dist/index.js.map +1 -1
  19. package/dist/vendor/zephyr3d/loaders/dist/gltf/gltf_importer.js +6 -6
  20. package/dist/vendor/zephyr3d/loaders/dist/gltf/gltf_importer.js.map +1 -1
  21. package/dist/vendor/zephyr3d/scene/dist/animation/animation.js +87 -0
  22. package/dist/vendor/zephyr3d/scene/dist/animation/animation.js.map +1 -1
  23. package/dist/vendor/zephyr3d/scene/dist/animation/animationcontroller.js +503 -0
  24. package/dist/vendor/zephyr3d/scene/dist/animation/animationcontroller.js.map +1 -0
  25. package/dist/vendor/zephyr3d/scene/dist/animation/animationset.js +782 -156
  26. package/dist/vendor/zephyr3d/scene/dist/animation/animationset.js.map +1 -1
  27. package/dist/vendor/zephyr3d/scene/dist/animation/animationtimeline.js +974 -0
  28. package/dist/vendor/zephyr3d/scene/dist/animation/animationtimeline.js.map +1 -0
  29. package/dist/vendor/zephyr3d/scene/dist/animation/joint_dynamics/convex_collider.js +320 -0
  30. package/dist/vendor/zephyr3d/scene/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
  31. package/dist/vendor/zephyr3d/scene/dist/app/scriptregistry.js +130 -125
  32. package/dist/vendor/zephyr3d/scene/dist/app/scriptregistry.js.map +1 -1
  33. package/dist/vendor/zephyr3d/scene/dist/asset/model.js +64 -63
  34. package/dist/vendor/zephyr3d/scene/dist/asset/model.js.map +1 -1
  35. package/dist/vendor/zephyr3d/scene/dist/index.d.ts +1270 -9
  36. package/dist/vendor/zephyr3d/scene/dist/index.js +3 -1
  37. package/dist/vendor/zephyr3d/scene/dist/index.js.map +1 -1
  38. package/dist/vendor/zephyr3d/scene/dist/material/mixins/lit.js +7 -3
  39. package/dist/vendor/zephyr3d/scene/dist/material/mixins/lit.js.map +1 -1
  40. package/dist/vendor/zephyr3d/scene/dist/utility/serialization/manager.js +1 -0
  41. package/dist/vendor/zephyr3d/scene/dist/utility/serialization/manager.js.map +1 -1
  42. package/dist/vendor/zephyr3d/scene/dist/utility/serialization/scene/animation.js +4 -3
  43. package/dist/vendor/zephyr3d/scene/dist/utility/serialization/scene/animation.js.map +1 -1
  44. package/dist/vendor/zephyr3d/scene/dist/utility/serialization/scene/camera.js +1 -1
  45. package/dist/vendor/zephyr3d/scene/dist/utility/serialization/scene/camera.js.map +1 -1
  46. package/dist/vendor/zephyr3d/scene/dist/utility/serialization/scene/node.js +3 -2
  47. package/dist/vendor/zephyr3d/scene/dist/utility/serialization/scene/node.js.map +1 -1
  48. package/package.json +8 -8
@@ -0,0 +1,974 @@
1
+ import { Observable } from '@zephyr3d/base';
2
+
3
+ /**
4
+ * Serializable animation timeline definition.
5
+ * @public
6
+ */ class AnimationTimeline {
7
+ /**
8
+ * Root step sequence executed by runners created from this timeline.
9
+ */ steps;
10
+ /**
11
+ * Event responses declared on this timeline.
12
+ */ responses;
13
+ /**
14
+ * Create a timeline from a definition object or a root step array.
15
+ *
16
+ * @param definition - Timeline definition, or a shorthand array used as the root steps.
17
+ */ constructor(definition){
18
+ if (Array.isArray(definition)) {
19
+ this.steps = definition;
20
+ this.responses = [];
21
+ } else {
22
+ this.steps = definition.steps;
23
+ this.responses = definition.responses ?? [];
24
+ }
25
+ }
26
+ /**
27
+ * Create a runtime runner for this timeline.
28
+ *
29
+ * @param animationSet - Animation set used to create and update playbacks.
30
+ * @returns A new stopped runner bound to this timeline and animation set.
31
+ */ createRunner(animationSet) {
32
+ return new AnimationTimelineRunner(animationSet, this);
33
+ }
34
+ }
35
+ /**
36
+ * Runtime interpreter for an AnimationTimeline.
37
+ *
38
+ * The interpreter is a synchronous frame-stack state machine advanced by {@link AnimationTimelineRunner.tick}, which is
39
+ * driven by `AnimationSet.update(dt)` on the same logical clock as the animations. There is no
40
+ * `async`/`await` in the control flow, so the runtime state can be serialized and replayed.
41
+ * @public
42
+ */ class AnimationTimelineRunner extends Observable {
43
+ /**
44
+ * Animation set used to start, stop, and query animation playbacks.
45
+ */ animationSet;
46
+ /**
47
+ * Timeline definition interpreted by this runner.
48
+ */ timeline;
49
+ _stack;
50
+ _concurrent;
51
+ _queued;
52
+ _pendingEvents;
53
+ _stopped;
54
+ _ticking;
55
+ _lastCompletedPlaybackId;
56
+ _onTick;
57
+ /** Tracks playbacks created by this runner so `stop()` can tear them down. */ _ownedPlaybacks;
58
+ /** Local playback refs that survive after the sequence frame that declared them drains. */ _playbackRefs;
59
+ /** Ids of playbacks owned by concurrent (keep-active) branches; preserved across main-flow stops. */ _concurrentPlaybackIds;
60
+ /** Marker/frame crossings observed since the last tick, keyed by playback id. */ _crossedMarkers;
61
+ _crossedFrames;
62
+ /**
63
+ * Create a stopped runner for a timeline.
64
+ *
65
+ * @param animationSet - Animation set that owns clips and playbacks referenced by the timeline.
66
+ * @param timeline - Timeline definition to interpret.
67
+ */ constructor(animationSet, timeline){
68
+ super();
69
+ this.animationSet = animationSet;
70
+ this.timeline = timeline;
71
+ this._stack = [];
72
+ this._concurrent = [];
73
+ this._queued = [];
74
+ this._pendingEvents = [];
75
+ this._stopped = true;
76
+ this._ticking = false;
77
+ this._lastCompletedPlaybackId = null;
78
+ this._onTick = (deltaInSeconds)=>this.tick(deltaInSeconds);
79
+ this._ownedPlaybacks = new Map();
80
+ this._playbackRefs = new Map();
81
+ this._concurrentPlaybackIds = new Set();
82
+ this._crossedMarkers = new Map();
83
+ this._crossedFrames = new Map();
84
+ }
85
+ /**
86
+ * Playback currently referenced by the active main-flow scope.
87
+ *
88
+ * @returns The current playback, or null when the main flow has no active playback reference.
89
+ */ get currentPlayback() {
90
+ const scope = this.activeScope();
91
+ if (!scope?.currentPlaybackId) {
92
+ return null;
93
+ }
94
+ return this._ownedPlaybacks.get(scope.currentPlaybackId) ?? null;
95
+ }
96
+ /**
97
+ * Whether this runner is stopped.
98
+ *
99
+ * @returns True when the runner is stopped; otherwise false.
100
+ */ get stopped() {
101
+ return this._stopped;
102
+ }
103
+ /**
104
+ * Playback id for the most recent playback that completed naturally.
105
+ *
106
+ * @returns The playback id, or null when no owned playback has completed.
107
+ */ get lastCompletedPlaybackId() {
108
+ return this._lastCompletedPlaybackId;
109
+ }
110
+ /**
111
+ * Start or restart the runner from the beginning of the timeline.
112
+ *
113
+ * @returns This runner for chaining.
114
+ */ start() {
115
+ this._stopped = false;
116
+ this._stack = [
117
+ this.makeSeqFrame(this.timeline.steps)
118
+ ];
119
+ this._concurrent = [];
120
+ this._queued.length = 0;
121
+ this._pendingEvents = [];
122
+ this._lastCompletedPlaybackId = null;
123
+ this._playbackRefs.clear();
124
+ this.animationSet._registerTimelineTicker(this._onTick);
125
+ // Drain any leading non-blocking steps immediately so e.g. an initial `play` starts now.
126
+ this.flush();
127
+ return this;
128
+ }
129
+ /**
130
+ * Stop the runner and all playbacks it owns.
131
+ *
132
+ * @param options - Optional stop behavior applied to owned playbacks.
133
+ * @returns This runner for chaining.
134
+ */ stop(options) {
135
+ const wasStopped = this._stopped;
136
+ this._stopped = true;
137
+ this._stack = [];
138
+ this._concurrent = [];
139
+ this._queued.length = 0;
140
+ this._pendingEvents = [];
141
+ this._crossedMarkers.clear();
142
+ this._crossedFrames.clear();
143
+ this._lastCompletedPlaybackId = null;
144
+ const stopOptions = options ?? {
145
+ reason: 'interrupted'
146
+ };
147
+ // Always tear down owned playbacks, even if the control flow already drained: a state whose
148
+ // script finished may still have a looping clip playing that a transition must stop.
149
+ this._ownedPlaybacks.forEach((playback)=>{
150
+ this.detachPlayback(playback);
151
+ playback.stop(stopOptions);
152
+ });
153
+ this._ownedPlaybacks.clear();
154
+ this._playbackRefs.clear();
155
+ this._concurrentPlaybackIds.clear();
156
+ this.animationSet._unregisterTimelineTicker(this._onTick);
157
+ if (!wasStopped) {
158
+ this.dispatchEvent('stop', this);
159
+ }
160
+ return this;
161
+ }
162
+ /**
163
+ * Append a batch of steps to run after the main stack drains.
164
+ *
165
+ * If the runner has already completed, enqueueing steps revives it and registers it for ticking.
166
+ *
167
+ * @param steps - Steps to run as the next queued batch.
168
+ * @returns void
169
+ */ enqueue(steps) {
170
+ if (steps.length === 0) {
171
+ return;
172
+ }
173
+ this._queued.push(steps);
174
+ // Revive a runner that already drained so queued steps are not silently dropped.
175
+ if (this._stopped) {
176
+ this._stopped = false;
177
+ this._stack = [];
178
+ this.animationSet._registerTimelineTicker(this._onTick);
179
+ }
180
+ this.flush();
181
+ }
182
+ /**
183
+ * Run `steps` concurrently with the current control flow (a true parallel branch). Unlike
184
+ * {@link AnimationTimelineRunner.enqueue}, these do not wait for the main stack to drain.
185
+ *
186
+ * @param steps - Steps to run immediately in an independent concurrent branch.
187
+ * @returns void
188
+ * @public
189
+ */ runConcurrent(steps) {
190
+ if (steps.length === 0) {
191
+ return;
192
+ }
193
+ this._concurrent.push(this.makeSeqFrame(steps, 'concurrent'));
194
+ if (this._stopped) {
195
+ this._stopped = false;
196
+ this.animationSet._registerTimelineTicker(this._onTick);
197
+ } else {
198
+ this.ensureTicking();
199
+ }
200
+ this.flush();
201
+ }
202
+ /**
203
+ * Dispatch an event to this runner.
204
+ *
205
+ * Waiting `waitEvent` frames consume matching events first. If no waiter consumes the event,
206
+ * the timeline response table is evaluated.
207
+ *
208
+ * @param event - Event name to dispatch.
209
+ * @param payload - Optional payload returned in the result.
210
+ * @returns The resolved handling result for the event.
211
+ */ dispatch(event, payload) {
212
+ // A waiting `waitEvent` frame consumes the event; flush advances past it synchronously.
213
+ if (this.hasWaiterFor(event)) {
214
+ this._pendingEvents.push(event);
215
+ this.flush();
216
+ return {
217
+ handled: true,
218
+ policy: 'consume',
219
+ event,
220
+ payload
221
+ };
222
+ }
223
+ const response = this.timeline.responses.find((item)=>item.event === event);
224
+ if (!response || response.target.ignore) {
225
+ return {
226
+ handled: false,
227
+ policy: response ? 'ignore' : 'none',
228
+ event,
229
+ payload
230
+ };
231
+ }
232
+ if (response.target.consume) {
233
+ return {
234
+ handled: true,
235
+ policy: 'consume',
236
+ event,
237
+ payload
238
+ };
239
+ }
240
+ if (response.target.targetState !== undefined) {
241
+ // Only the controller can switch states; bubble up so it can act on its own table.
242
+ return {
243
+ handled: false,
244
+ policy: 'transition',
245
+ event,
246
+ payload
247
+ };
248
+ }
249
+ const steps = response.target.steps;
250
+ if (!steps?.length) {
251
+ return {
252
+ handled: false,
253
+ policy: 'none',
254
+ event,
255
+ payload
256
+ };
257
+ }
258
+ if (response.enqueue) {
259
+ this.enqueue(steps);
260
+ return {
261
+ handled: true,
262
+ policy: 'enqueue',
263
+ event,
264
+ payload
265
+ };
266
+ }
267
+ const disposition = response.onActive ?? 'stop';
268
+ if (disposition === 'keep') {
269
+ // Run the new steps concurrently with the existing control flow (true parallel branch).
270
+ this.runConcurrent(steps);
271
+ } else {
272
+ const stopOptions = typeof disposition === 'object' ? {
273
+ ...disposition,
274
+ reason: 'interrupted'
275
+ } : undefined;
276
+ this.stopMainFlow(stopOptions);
277
+ this._stopped = false;
278
+ this._stack = [
279
+ this.makeSeqFrame(steps)
280
+ ];
281
+ this.ensureTicking();
282
+ this.flush();
283
+ }
284
+ return {
285
+ handled: true,
286
+ policy: 'steps',
287
+ event,
288
+ payload
289
+ };
290
+ }
291
+ /**
292
+ * Run pending non-blocking work synchronously (a zero-delta tick), without advancing any
293
+ * time-based waits. Lets `start()`/`dispatch()` take effect immediately while keeping all
294
+ * runtime state in the serializable frame stack.
295
+ *
296
+ * @returns This runner for chaining.
297
+ * @public
298
+ */ flush() {
299
+ // Guard against re-entrancy: an `emit` listener firing during a tick may dispatch again.
300
+ if (!this._ticking) {
301
+ this.tick(0);
302
+ }
303
+ return this;
304
+ }
305
+ /**
306
+ * Advance the timeline by `deltaInSeconds`. Called by `AnimationSet.update`.
307
+ *
308
+ * @param deltaInSeconds - Elapsed time in seconds for this tick.
309
+ * @returns void
310
+ * @public
311
+ */ tick(deltaInSeconds) {
312
+ if (this._stopped && this._queued.length === 0) {
313
+ return;
314
+ }
315
+ this._ticking = true;
316
+ // Advance the main control-flow stack.
317
+ if (this._stack.length === 0 && this._queued.length > 0) {
318
+ const next = this._queued.shift();
319
+ this._stack = [
320
+ this.makeSeqFrame(next)
321
+ ];
322
+ }
323
+ if (this._stack.length > 0) {
324
+ this.tickFrames(this._stack, deltaInSeconds);
325
+ }
326
+ // Advance any concurrent (keep-active) branches; drop the ones that finished.
327
+ if (this._concurrent.length > 0) {
328
+ this._concurrent = this._concurrent.filter((frame)=>{
329
+ const stack = [
330
+ frame
331
+ ];
332
+ this.tickFrames(stack, deltaInSeconds);
333
+ return stack.length > 0;
334
+ });
335
+ }
336
+ // Consumed this tick.
337
+ this._pendingEvents = [];
338
+ this._crossedMarkers.clear();
339
+ this._crossedFrames.clear();
340
+ this._ticking = false;
341
+ // Pull queued batches once the stack drains.
342
+ while(this._stack.length === 0 && this._queued.length > 0){
343
+ const next = this._queued.shift();
344
+ this._stack = [
345
+ this.makeSeqFrame(next)
346
+ ];
347
+ this.tickFrames(this._stack, 0);
348
+ }
349
+ if (!this._stopped && this._stack.length === 0 && this._concurrent.length === 0 && this._queued.length === 0) {
350
+ this._stopped = true;
351
+ this.animationSet._unregisterTimelineTicker(this._onTick);
352
+ this.dispatchEvent('complete', this);
353
+ }
354
+ }
355
+ /**
356
+ * Export the runtime state as plain data.
357
+ *
358
+ * @returns A serializable snapshot of the runner state.
359
+ * @public
360
+ */ serialize() {
361
+ return {
362
+ stack: this._stack.map((frame)=>this.cloneFrame(frame)),
363
+ concurrent: this._concurrent.map((frame)=>this.cloneFrame(frame)),
364
+ queued: this._queued.map((steps)=>steps.slice()),
365
+ concurrentPlaybackIds: [
366
+ ...this._concurrentPlaybackIds
367
+ ],
368
+ playbackRefs: Object.fromEntries(this._playbackRefs),
369
+ stopped: this._stopped
370
+ };
371
+ }
372
+ /**
373
+ * Restore runtime state previously produced by {@link AnimationTimelineRunner.serialize}.
374
+ *
375
+ * Re-create the relevant active playbacks on the AnimationSet before calling this so that
376
+ * playback-bound frames (play-wait, waitMarker, waitFrame) can re-attach by id.
377
+ *
378
+ * @param animationSet - Animation set containing any live playbacks referenced by the state.
379
+ * @param timeline - Timeline definition to bind to the restored runner.
380
+ * @param state - Serialized state previously returned by {@link AnimationTimelineRunner.serialize}.
381
+ * @returns A runner restored to the supplied runtime state.
382
+ * @public
383
+ */ static deserialize(animationSet, timeline, state) {
384
+ const runner = new AnimationTimelineRunner(animationSet, timeline);
385
+ runner._stack = state.stack.map((frame)=>runner.cloneFrame(frame));
386
+ runner._concurrent = state.concurrent.map((frame)=>runner.cloneFrame(frame));
387
+ state.queued.forEach((steps)=>runner._queued.push(steps.slice()));
388
+ state.concurrentPlaybackIds?.forEach((id)=>runner._concurrentPlaybackIds.add(id));
389
+ Object.entries(state.playbackRefs ?? {}).forEach(([ref, id])=>runner._playbackRefs.set(ref, id));
390
+ runner._stopped = state.stopped;
391
+ // Re-attach to any live playbacks referenced by the restored frames.
392
+ runner.reattachPlaybacks(runner._stack);
393
+ runner._concurrent.forEach((frame)=>runner.reattachPlaybacks([
394
+ frame
395
+ ]));
396
+ // Re-attach drained concurrent playbacks tracked only by id, so they survive future stops.
397
+ runner._concurrentPlaybackIds.forEach((id)=>{
398
+ if (!runner._ownedPlaybacks.has(id)) {
399
+ const playback = runner.findLivePlayback(id);
400
+ if (playback) {
401
+ runner.attachPlayback(playback);
402
+ }
403
+ }
404
+ });
405
+ runner._playbackRefs.forEach((id)=>{
406
+ if (!runner._ownedPlaybacks.has(id)) {
407
+ const playback = runner.findLivePlayback(id);
408
+ if (playback) {
409
+ runner.attachPlayback(playback);
410
+ }
411
+ }
412
+ });
413
+ if (!runner._stopped) {
414
+ animationSet._registerTimelineTicker(runner._onTick);
415
+ }
416
+ return runner;
417
+ }
418
+ // --- internals -------------------------------------------------------------
419
+ ensureTicking() {
420
+ if (!this._ticking) {
421
+ this.animationSet._registerTimelineTicker(this._onTick);
422
+ }
423
+ }
424
+ makeSeqFrame(steps, owner = 'main') {
425
+ return {
426
+ kind: 'seq',
427
+ steps,
428
+ index: 0,
429
+ scope: {
430
+ currentPlaybackId: null,
431
+ refs: {},
432
+ owner
433
+ },
434
+ child: null
435
+ };
436
+ }
437
+ /** The scope of the innermost active sequence on the main stack (for currentPlayback). */ activeScope() {
438
+ for(let i = this._stack.length - 1; i >= 0; i--){
439
+ const scope = this.deepestScope(this._stack[i]);
440
+ if (scope) {
441
+ return scope;
442
+ }
443
+ }
444
+ return null;
445
+ }
446
+ deepestScope(frame) {
447
+ if (frame.kind === 'seq') {
448
+ return frame.child ? this.deepestScope(frame.child) ?? frame.scope : frame.scope;
449
+ }
450
+ return null;
451
+ }
452
+ /**
453
+ * Advance a frame stack in place. The top frame runs until it blocks or completes; completed
454
+ * frames pop and the parent sequence advances. Returns when the stack blocks or empties.
455
+ */ tickFrames(stack, deltaInSeconds) {
456
+ this.tickFramesWithLeftover(stack, deltaInSeconds);
457
+ }
458
+ /**
459
+ * Same as {@link AnimationTimelineRunner.tickFrames} but returns the time left unconsumed when the stack drains
460
+ * completely (so a parent sequence can hand it to its next step).
461
+ */ tickFramesWithLeftover(stack, deltaInSeconds) {
462
+ let guard = 0;
463
+ while(stack.length > 0){
464
+ if (++guard > 10000) {
465
+ break;
466
+ }
467
+ const frame = stack[stack.length - 1];
468
+ const result = this.tickFrame(frame, stack, deltaInSeconds);
469
+ if (result.status === 'block') {
470
+ return 0;
471
+ }
472
+ if (result.status === 'pop') {
473
+ stack.pop();
474
+ // Carry any unused time (e.g. a `wait` that overshot its duration this tick) to the next
475
+ // step so a sequence of waits/actions tracks logical time regardless of frame rate.
476
+ deltaInSeconds = result.leftover ?? 0;
477
+ continue;
478
+ }
479
+ // 'advanced': the frame pushed a child or moved its cursor; loop again with delta 0 so a
480
+ // single tick can drain consecutive non-blocking steps.
481
+ deltaInSeconds = 0;
482
+ }
483
+ return deltaInSeconds;
484
+ }
485
+ tickFrame(frame, stack, deltaInSeconds) {
486
+ switch(frame.kind){
487
+ case 'seq':
488
+ return this.tickSeq(frame, stack, deltaInSeconds);
489
+ case 'parallel':
490
+ return this.tickParallel(frame, deltaInSeconds);
491
+ case 'wait':
492
+ frame.remaining -= deltaInSeconds;
493
+ // On completion, hand back the overshoot (negative remaining) as leftover time.
494
+ return frame.remaining <= 0 ? {
495
+ status: 'pop',
496
+ leftover: -frame.remaining
497
+ } : {
498
+ status: 'block'
499
+ };
500
+ case 'waitEvent':
501
+ return this._pendingEvents.includes(frame.event) ? {
502
+ status: 'pop'
503
+ } : {
504
+ status: 'block'
505
+ };
506
+ case 'waitMarker':
507
+ return this.tickWaitMarker(frame);
508
+ case 'waitFrame':
509
+ return this.tickWaitFrame(frame);
510
+ case 'playWait':
511
+ return this.tickPlayWait(frame);
512
+ }
513
+ }
514
+ tickSeq(frame, _stack, deltaInSeconds) {
515
+ // Drain steps in a loop so unconsumed time (e.g. a `wait` that overshot) flows into the next
516
+ // step within the same tick instead of being dropped.
517
+ let guard = 0;
518
+ for(;;){
519
+ if (++guard > 10000) {
520
+ return {
521
+ status: 'block'
522
+ };
523
+ }
524
+ if (frame.child) {
525
+ const childStack = [
526
+ frame.child
527
+ ];
528
+ const leftover = this.tickFramesWithLeftover(childStack, deltaInSeconds);
529
+ if (childStack.length > 0) {
530
+ return {
531
+ status: 'block'
532
+ };
533
+ }
534
+ frame.child = null;
535
+ frame.index++;
536
+ deltaInSeconds = leftover;
537
+ }
538
+ if (frame.index >= frame.steps.length) {
539
+ return {
540
+ status: 'pop',
541
+ leftover: deltaInSeconds
542
+ };
543
+ }
544
+ const step = frame.steps[frame.index];
545
+ const blocking = this.beginStep(step, frame);
546
+ if (blocking) {
547
+ // Tick the freshly-pushed child immediately with the carried time on the next loop turn.
548
+ frame.child = blocking;
549
+ continue;
550
+ }
551
+ frame.index++;
552
+ // A non-blocking step consumes no time; keep the remaining delta for the following step.
553
+ }
554
+ }
555
+ tickParallel(frame, deltaInSeconds) {
556
+ frame.branches = frame.branches.filter((branch)=>{
557
+ const branchStack = [
558
+ branch
559
+ ];
560
+ this.tickFrames(branchStack, deltaInSeconds);
561
+ return branchStack.length > 0;
562
+ });
563
+ // Parallel branches may finish at different times; we do not attempt to reconcile a single
564
+ // leftover across them, so the join simply consumes the whole tick.
565
+ return frame.branches.length > 0 ? {
566
+ status: 'block'
567
+ } : {
568
+ status: 'pop'
569
+ };
570
+ }
571
+ /**
572
+ * Execute a non-blocking step immediately and return null, or return a frame to block on.
573
+ */ beginStep(step, scopeFrame) {
574
+ const scope = scopeFrame.scope;
575
+ switch(step.type){
576
+ case 'sequence':
577
+ {
578
+ const child = this.makeSeqFrame(step.steps);
579
+ // Inherit the parent scope so refs/currentPlayback carry into the nested sequence.
580
+ child.scope = scope;
581
+ return child;
582
+ }
583
+ case 'parallel':
584
+ {
585
+ // Each branch gets an isolated scope so currentPlayback/refs don't race (#7), but inherits
586
+ // the parent's owner so concurrent branches stay concurrent through nested parallels.
587
+ const branches = step.steps.map((child)=>{
588
+ const branchScope = {
589
+ currentPlaybackId: scope.currentPlaybackId,
590
+ refs: {
591
+ ...scope.refs
592
+ },
593
+ owner: scope.owner
594
+ };
595
+ const seq = this.makeSeqFrame([
596
+ child
597
+ ]);
598
+ seq.scope = branchScope;
599
+ return seq;
600
+ });
601
+ return {
602
+ kind: 'parallel',
603
+ branches
604
+ };
605
+ }
606
+ case 'play':
607
+ {
608
+ const playback = this.animationSet.play(step.clip, this.resolvePlayOptions(step.options, scope));
609
+ if (!playback) {
610
+ return null;
611
+ }
612
+ this.attachPlayback(playback);
613
+ if (scope.owner === 'concurrent') {
614
+ // Remember concurrent ownership independently of the frames: a non-blocking keep-active
615
+ // play drains out of `_concurrent` immediately, but its playback must still survive a
616
+ // later main-flow replacement (#2).
617
+ this._concurrentPlaybackIds.add(playback.id);
618
+ }
619
+ scope.currentPlaybackId = playback.id;
620
+ if (step.id) {
621
+ scope.refs[step.id] = playback.id;
622
+ this._playbackRefs.set(step.id, playback.id);
623
+ }
624
+ scope.refs[playback.id] = playback.id;
625
+ this._playbackRefs.set(playback.id, playback.id);
626
+ if (step.wait === 'complete') {
627
+ return {
628
+ kind: 'playWait',
629
+ playbackId: playback.id
630
+ };
631
+ }
632
+ return null;
633
+ }
634
+ case 'stop':
635
+ {
636
+ const playback = this.resolvePlayback(step.target, scope);
637
+ if (playback) {
638
+ playback.stop(step.options);
639
+ } else if (!step.target) {
640
+ this._ownedPlaybacks.forEach((item)=>item.stop(step.options));
641
+ }
642
+ return null;
643
+ }
644
+ case 'wait':
645
+ return step.seconds > 0 ? {
646
+ kind: 'wait',
647
+ remaining: step.seconds
648
+ } : null;
649
+ case 'waitEvent':
650
+ return {
651
+ kind: 'waitEvent',
652
+ event: step.event
653
+ };
654
+ case 'waitMarker':
655
+ {
656
+ const playback = this.resolvePlayback(step.target, scope) ?? this.scopeCurrentPlayback(scope);
657
+ if (!playback) {
658
+ return null;
659
+ }
660
+ return {
661
+ kind: 'waitMarker',
662
+ marker: step.marker,
663
+ playbackId: playback.id,
664
+ satisfied: false
665
+ };
666
+ }
667
+ case 'waitFrame':
668
+ {
669
+ const playback = this.resolvePlayback(step.target, scope) ?? this.scopeCurrentPlayback(scope);
670
+ if (!playback) {
671
+ return null;
672
+ }
673
+ return {
674
+ kind: 'waitFrame',
675
+ frame: step.frame,
676
+ playbackId: playback.id,
677
+ satisfied: false
678
+ };
679
+ }
680
+ case 'emit':
681
+ this.dispatchEvent('emit', step.event, step.payload);
682
+ return null;
683
+ }
684
+ }
685
+ resolvePlayOptions(options, scope) {
686
+ const target = options?.sync?.target;
687
+ if (!target) {
688
+ return options;
689
+ }
690
+ const mapped = this.resolvePlaybackRef(target, scope);
691
+ if (!mapped) {
692
+ return options;
693
+ }
694
+ return {
695
+ ...options,
696
+ sync: {
697
+ ...options.sync,
698
+ target: mapped
699
+ }
700
+ };
701
+ }
702
+ resolvePlaybackRef(target, scope) {
703
+ return scope.refs[target] ?? this._playbackRefs.get(target) ?? null;
704
+ }
705
+ tickWaitMarker(frame) {
706
+ if (frame.satisfied || !frame.playbackId) {
707
+ return {
708
+ status: 'pop'
709
+ };
710
+ }
711
+ const playback = this._ownedPlaybacks.get(frame.playbackId);
712
+ if (!playback || playback.state === 'stopped' || playback.state === 'completed') {
713
+ return {
714
+ status: 'pop'
715
+ };
716
+ }
717
+ const crossed = this._crossedMarkers.get(frame.playbackId);
718
+ if (crossed && crossed.has(frame.marker)) {
719
+ return {
720
+ status: 'pop'
721
+ };
722
+ }
723
+ return {
724
+ status: 'block'
725
+ };
726
+ }
727
+ tickWaitFrame(frame) {
728
+ if (frame.satisfied || frame.playbackId === null) {
729
+ return {
730
+ status: 'pop'
731
+ };
732
+ }
733
+ const playback = this._ownedPlaybacks.get(frame.playbackId);
734
+ if (!playback || playback.state === 'stopped' || playback.state === 'completed') {
735
+ return {
736
+ status: 'pop'
737
+ };
738
+ }
739
+ const crossed = this._crossedFrames.get(frame.playbackId);
740
+ if (crossed && crossed.has(frame.frame)) {
741
+ return {
742
+ status: 'pop'
743
+ };
744
+ }
745
+ return {
746
+ status: 'block'
747
+ };
748
+ }
749
+ tickPlayWait(frame) {
750
+ const playback = this._ownedPlaybacks.get(frame.playbackId);
751
+ if (!playback || playback.state === 'stopped' || playback.state === 'completed') {
752
+ return {
753
+ status: 'pop'
754
+ };
755
+ }
756
+ return {
757
+ status: 'block'
758
+ };
759
+ }
760
+ attachPlayback(playback) {
761
+ this._ownedPlaybacks.set(playback.id, playback);
762
+ playback.on('marker', this.onPlaybackMarker);
763
+ playback.on('frame', this.onPlaybackFrame);
764
+ playback.on('stop', this.onPlaybackEnd);
765
+ playback.on('complete', this.onPlaybackComplete);
766
+ }
767
+ detachPlayback(playback) {
768
+ playback.off('marker', this.onPlaybackMarker);
769
+ playback.off('frame', this.onPlaybackFrame);
770
+ playback.off('stop', this.onPlaybackEnd);
771
+ playback.off('complete', this.onPlaybackComplete);
772
+ }
773
+ forgetPlayback(playback) {
774
+ this.detachPlayback(playback);
775
+ this._ownedPlaybacks.delete(playback.id);
776
+ this._concurrentPlaybackIds.delete(playback.id);
777
+ this.forgetPlaybackRefs(playback.id);
778
+ }
779
+ /**
780
+ * Cleanup when an owned playback ends (externally or naturally). Only touches runner-local
781
+ * bookkeeping, so it is safe to run inside `AnimationSet.update`'s playback loop. Frames blocked
782
+ * on this playback observe its absence on the next tick and unblock.
783
+ */ onPlaybackEnd = (event)=>{
784
+ const playback = this._ownedPlaybacks.get(event.playback.id);
785
+ if (playback) {
786
+ this.forgetPlayback(playback);
787
+ }
788
+ };
789
+ onPlaybackComplete = (event)=>{
790
+ this._lastCompletedPlaybackId = event.playback.id;
791
+ const playback = this._ownedPlaybacks.get(event.playback.id);
792
+ if (playback) {
793
+ this.forgetPlayback(playback);
794
+ }
795
+ };
796
+ onPlaybackMarker = (event)=>{
797
+ const id = event.playback.id;
798
+ let set = this._crossedMarkers.get(id);
799
+ if (!set) {
800
+ set = new Set();
801
+ this._crossedMarkers.set(id, set);
802
+ }
803
+ if (event.marker.id !== undefined) {
804
+ set.add(event.marker.id);
805
+ }
806
+ set.add(event.marker.name);
807
+ };
808
+ onPlaybackFrame = (event)=>{
809
+ const id = event.playback.id;
810
+ let set = this._crossedFrames.get(id);
811
+ if (!set) {
812
+ set = new Set();
813
+ this._crossedFrames.set(id, set);
814
+ }
815
+ set.add(event.frame);
816
+ };
817
+ resolvePlayback(target, scope) {
818
+ if (!target) {
819
+ return null;
820
+ }
821
+ const mapped = this.resolvePlaybackRef(target, scope);
822
+ if (mapped) {
823
+ const owned = this._ownedPlaybacks.get(mapped);
824
+ if (owned) {
825
+ return owned;
826
+ }
827
+ }
828
+ return this._ownedPlaybacks.get(target) ?? this.animationSet.getPlayback(target);
829
+ }
830
+ forgetPlaybackRefs(playbackId) {
831
+ [
832
+ ...this._playbackRefs.entries()
833
+ ].forEach(([ref, id])=>{
834
+ if (id === playbackId) {
835
+ this._playbackRefs.delete(ref);
836
+ }
837
+ });
838
+ }
839
+ scopeCurrentPlayback(scope) {
840
+ return scope.currentPlaybackId ? this._ownedPlaybacks.get(scope.currentPlaybackId) ?? null : null;
841
+ }
842
+ hasWaiterFor(event) {
843
+ const inFrame = (frame)=>{
844
+ switch(frame.kind){
845
+ case 'waitEvent':
846
+ return frame.event === event;
847
+ case 'seq':
848
+ return frame.child ? inFrame(frame.child) : false;
849
+ case 'parallel':
850
+ return frame.branches.some(inFrame);
851
+ default:
852
+ return false;
853
+ }
854
+ };
855
+ return this._stack.some(inFrame) || this._concurrent.some(inFrame);
856
+ }
857
+ /** Stop only the main control flow (used when a response replaces it). */ stopMainFlow(options) {
858
+ const stopOptions = options ?? {
859
+ reason: 'interrupted'
860
+ };
861
+ // Stop every playback owned by the main flow, not just those still referenced by `_stack`:
862
+ // a sequence that already drained may have left a looping clip playing (e.g. `play idle` with
863
+ // `repeat: 0`), and replacing the main flow must stop it. Playbacks owned by concurrent
864
+ // (keep-active) branches are independent parallel tracks and are preserved — tracked by id so
865
+ // they survive even after their branch frames drained.
866
+ this._ownedPlaybacks.forEach((playback, id)=>{
867
+ if (this._concurrentPlaybackIds.has(id)) {
868
+ return;
869
+ }
870
+ this.detachPlayback(playback);
871
+ playback.stop(stopOptions);
872
+ this._ownedPlaybacks.delete(id);
873
+ this.forgetPlaybackRefs(id);
874
+ });
875
+ this._stack = [];
876
+ }
877
+ collectScopePlaybackIds(frames) {
878
+ const ids = new Set();
879
+ const visit = (frame)=>{
880
+ if (frame.kind === 'seq') {
881
+ Object.values(frame.scope.refs).forEach((id)=>ids.add(id));
882
+ if (frame.scope.currentPlaybackId) {
883
+ ids.add(frame.scope.currentPlaybackId);
884
+ }
885
+ if (frame.child) {
886
+ visit(frame.child);
887
+ }
888
+ } else if (frame.kind === 'parallel') {
889
+ frame.branches.forEach(visit);
890
+ }
891
+ };
892
+ frames.forEach(visit);
893
+ return ids;
894
+ }
895
+ cloneFrame(frame) {
896
+ switch(frame.kind){
897
+ case 'seq':
898
+ return {
899
+ kind: 'seq',
900
+ steps: frame.steps,
901
+ index: frame.index,
902
+ scope: {
903
+ currentPlaybackId: frame.scope.currentPlaybackId,
904
+ refs: {
905
+ ...frame.scope.refs
906
+ },
907
+ owner: frame.scope.owner
908
+ },
909
+ child: frame.child ? this.cloneFrame(frame.child) : null
910
+ };
911
+ case 'parallel':
912
+ return {
913
+ kind: 'parallel',
914
+ branches: frame.branches.map((b)=>this.cloneFrame(b))
915
+ };
916
+ case 'wait':
917
+ return {
918
+ kind: 'wait',
919
+ remaining: frame.remaining
920
+ };
921
+ case 'waitEvent':
922
+ return {
923
+ kind: 'waitEvent',
924
+ event: frame.event
925
+ };
926
+ case 'waitMarker':
927
+ return {
928
+ ...frame
929
+ };
930
+ case 'waitFrame':
931
+ return {
932
+ ...frame
933
+ };
934
+ case 'playWait':
935
+ return {
936
+ ...frame
937
+ };
938
+ }
939
+ }
940
+ reattachPlaybacks(frames) {
941
+ const ids = this.collectAllPlaybackIds(frames);
942
+ ids.forEach((id)=>{
943
+ if (this._ownedPlaybacks.has(id)) {
944
+ return;
945
+ }
946
+ const playback = this.findLivePlayback(id);
947
+ if (playback) {
948
+ this.attachPlayback(playback);
949
+ }
950
+ });
951
+ }
952
+ collectAllPlaybackIds(frames) {
953
+ const ids = this.collectScopePlaybackIds(frames);
954
+ const visit = (frame)=>{
955
+ if (frame.kind === 'playWait') {
956
+ ids.add(frame.playbackId);
957
+ } else if ((frame.kind === 'waitMarker' || frame.kind === 'waitFrame') && frame.playbackId) {
958
+ ids.add(frame.playbackId);
959
+ } else if (frame.kind === 'seq' && frame.child) {
960
+ visit(frame.child);
961
+ } else if (frame.kind === 'parallel') {
962
+ frame.branches.forEach(visit);
963
+ }
964
+ };
965
+ frames.forEach(visit);
966
+ return ids;
967
+ }
968
+ findLivePlayback(id) {
969
+ return this.animationSet.getPlaybacks().find((playback)=>playback.id === id) ?? null;
970
+ }
971
+ }
972
+
973
+ export { AnimationTimeline, AnimationTimelineRunner };
974
+ //# sourceMappingURL=animationtimeline.js.map