@aura3d/editor-runtime 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/package.json +36 -5
  2. package/src/AnimationSceneEditor.ts +0 -368
  3. package/src/AssetDropZone.ts +0 -144
  4. package/src/CameraPathEditor.ts +0 -223
  5. package/src/Command.ts +0 -11
  6. package/src/CommandHistory.ts +0 -136
  7. package/src/CurveEditor.ts +0 -152
  8. package/src/DiagnosticsOverlayModel.ts +0 -85
  9. package/src/EditorPluginHost.ts +0 -106
  10. package/src/EditorRuntime.ts +0 -349
  11. package/src/EditorStateModel.ts +0 -336
  12. package/src/EpisodeReviewPanel.ts +0 -236
  13. package/src/EventTrackEditor.ts +0 -169
  14. package/src/Gizmo.ts +0 -101
  15. package/src/HierarchyModel.ts +0 -57
  16. package/src/InspectorModel.ts +0 -71
  17. package/src/KeyframeEditor.ts +0 -214
  18. package/src/LocalizationAccessibilityFixtures.ts +0 -368
  19. package/src/MaterialVariantWorkflow.ts +0 -101
  20. package/src/MultiUserReviewWorkflow.ts +0 -229
  21. package/src/NonlinearAnimationEditor.ts +0 -262
  22. package/src/PickingService.ts +0 -174
  23. package/src/PlayModeBridge.ts +0 -27
  24. package/src/PrefabRegistry.ts +0 -164
  25. package/src/ProjectSerializer.ts +0 -200
  26. package/src/PropertyPanel.ts +0 -82
  27. package/src/RenderQueuePanel.ts +0 -106
  28. package/src/RotateGizmo.ts +0 -35
  29. package/src/ScaleGizmo.ts +0 -40
  30. package/src/SceneOutliner.ts +0 -78
  31. package/src/Selection.ts +0 -63
  32. package/src/ShaderGraphModel.ts +0 -254
  33. package/src/StaticExportRuntime.ts +0 -322
  34. package/src/TimelineEditorController.ts +0 -525
  35. package/src/TimelineModel.ts +0 -466
  36. package/src/TimelineRuntimeBridge.ts +0 -280
  37. package/src/TimelineTrackTypes.ts +0 -89
  38. package/src/TimelineUI.ts +0 -153
  39. package/src/TranslateGizmo.ts +0 -39
  40. package/src/VisualReviewDashboard.ts +0 -87
  41. package/src/commands/CreateNodeCommand.ts +0 -23
  42. package/src/commands/DeleteNodeCommand.ts +0 -74
  43. package/src/commands/ReparentNodeCommand.ts +0 -55
  44. package/src/commands/SetPropertyCommand.ts +0 -47
  45. package/src/commands/TransformCommand.ts +0 -107
  46. package/src/index.ts +0 -140
  47. package/tests/editor-runtime.test.ts +0 -1087
  48. package/tsconfig.json +0 -7
@@ -1,1087 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { Ray, Vector3 } from "@aura3d/math";
4
- import { Scene } from "@aura3d/scene";
5
- import {
6
- CommandHistory,
7
- AnimationSceneEditor,
8
- CameraPathEditor,
9
- CurveEditor,
10
- CreateNodeCommand,
11
- DeleteNodeCommand,
12
- EditorRuntime,
13
- EpisodeReviewPanel,
14
- InspectorModel,
15
- KeyframeEditor,
16
- MultiUserReviewWorkflow,
17
- NonlinearAnimationEditor,
18
- PickingService,
19
- PropertyPanel,
20
- RenderQueuePanel,
21
- SceneOutliner,
22
- Selection,
23
- SetPropertyCommand,
24
- TimelineClip,
25
- TimelineEditorController,
26
- TimelineModel,
27
- TimelineTrack,
28
- TransformCommand,
29
- TranslateGizmo,
30
- VisualReviewDashboard,
31
- collectEditorProjectEvidence,
32
- createTimelineRuntimeBridge,
33
- createTimelineTrackConfig,
34
- parseEditorProject,
35
- readTimelineKeyframes,
36
- serializeEditorProject,
37
- type Command,
38
- type EditorProjectDocument,
39
- type TimelineRuntimeAnimationApplication,
40
- type TimelineRuntimeSignalDispatch
41
- } from "../src/index";
42
- import { createAudioWaveformReviewData, type AudioWaveformData } from "../../audio/src/index";
43
- import {
44
- createAuraVoiceVisemeTrack,
45
- createVisemeTimelineTrack,
46
- sampleVisemeTimelineTrack
47
- } from "../../engine/src/agent-api/index";
48
-
49
- test("CommandHistory executes undo and redo deterministically", async () => {
50
- const target = { position: { x: 0, y: 0, z: 0 } };
51
- const history = new CommandHistory();
52
-
53
- await history.execute(new TransformCommand(target, { position: { x: 1, y: 2, z: 3 } }));
54
- assert.deepEqual(target.position, { x: 1, y: 2, z: 3 });
55
- await history.undo();
56
- assert.deepEqual(target.position, { x: 0, y: 0, z: 0 });
57
- await history.redo();
58
- assert.deepEqual(target.position, { x: 1, y: 2, z: 3 });
59
- });
60
-
61
- test("Selection emits changes and can prune deleted ids", () => {
62
- const selection = new Selection();
63
- const changes: unknown[] = [];
64
- selection.subscribe((change) => changes.push(change));
65
- selection.set(["a", "b"]);
66
- selection.prune((id) => id !== "a");
67
- assert.deepEqual(selection.current(), ["b"]);
68
- assert.equal(changes.length, 2);
69
- });
70
-
71
- test("Create, delete, and set-property commands mutate through public command contract", async () => {
72
- const nodes: string[] = [];
73
- const container = {
74
- add: (node: string) => nodes.push(node),
75
- remove: (node: string) => {
76
- const index = nodes.indexOf(node);
77
- if (index >= 0) nodes.splice(index, 1);
78
- }
79
- };
80
- const history = new CommandHistory();
81
- await history.execute(new CreateNodeCommand(container, "node-a"));
82
- await history.execute(new DeleteNodeCommand(container, "node-a"));
83
- assert.deepEqual(nodes, []);
84
- await history.undo();
85
- assert.deepEqual(nodes, ["node-a"]);
86
-
87
- const model = { transform: { visible: true } };
88
- await history.execute(new SetPropertyCommand(model, ["transform", "visible"], false));
89
- assert.equal(model.transform.visible, false);
90
- await history.undo();
91
- assert.equal(model.transform.visible, true);
92
- });
93
-
94
- test("CommandHistory transactions roll back partial execution", async () => {
95
- const history = new CommandHistory();
96
- const calls: string[] = [];
97
- const first: Command = {
98
- name: "first",
99
- execute() {
100
- calls.push("first:execute");
101
- },
102
- undo() {
103
- calls.push("first:undo");
104
- }
105
- };
106
- const second: Command = {
107
- name: "second",
108
- execute() {
109
- calls.push("second:execute");
110
- throw new Error("second failed");
111
- },
112
- undo() {
113
- calls.push("second:undo");
114
- }
115
- };
116
-
117
- await assert.rejects(() => history.executeTransaction([first, second]), /second failed/);
118
- assert.deepEqual(calls, ["first:execute", "second:execute", "first:undo"]);
119
- assert.equal(history.undoDepth, 0);
120
- assert.equal(history.redoDepth, 0);
121
- });
122
-
123
- test("CommandHistory transactions undo and redo as one history entry", async () => {
124
- const history = new CommandHistory();
125
- const values: string[] = [];
126
- const createPushCommand = (value: string): Command => ({
127
- name: `push ${value}`,
128
- execute() {
129
- values.push(value);
130
- },
131
- undo() {
132
- const index = values.lastIndexOf(value);
133
- if (index >= 0) {
134
- values.splice(index, 1);
135
- }
136
- }
137
- });
138
-
139
- await history.executeTransaction([createPushCommand("a"), createPushCommand("b")]);
140
- assert.deepEqual(values, ["a", "b"]);
141
- assert.equal(history.undoDepth, 1);
142
-
143
- await history.undo();
144
- assert.deepEqual(values, []);
145
- assert.equal(history.redoDepth, 1);
146
-
147
- await history.redo();
148
- assert.deepEqual(values, ["a", "b"]);
149
- });
150
-
151
- test("CommandHistory preserves history entries when undo or redo fail", async () => {
152
- const undoHistory = new CommandHistory();
153
- const failingUndo: Command = {
154
- name: "failing undo",
155
- execute() {},
156
- undo() {
157
- throw new Error("undo failed");
158
- }
159
- };
160
- await undoHistory.execute(failingUndo);
161
- await assert.rejects(() => undoHistory.undo(), /undo failed/);
162
- assert.equal(undoHistory.undoDepth, 1);
163
- assert.equal(undoHistory.redoDepth, 0);
164
-
165
- const redoHistory = new CommandHistory();
166
- let failRedo = false;
167
- const failingRedo: Command = {
168
- name: "failing redo",
169
- execute() {
170
- if (failRedo) {
171
- throw new Error("redo failed");
172
- }
173
- },
174
- undo() {}
175
- };
176
- await redoHistory.execute(failingRedo);
177
- await redoHistory.undo();
178
- failRedo = true;
179
- await assert.rejects(() => redoHistory.redo(), /redo failed/);
180
- assert.equal(redoHistory.undoDepth, 0);
181
- assert.equal(redoHistory.redoDepth, 1);
182
- });
183
-
184
- test("DeleteNodeCommand restores scene parent and sibling order", async () => {
185
- const scene = new Scene();
186
- const parent = scene.createNode("parent");
187
- const child = scene.createNode("child");
188
- const sibling = scene.createNode("sibling");
189
- scene.root.addChild(parent);
190
- parent.addChild(child);
191
- parent.addChild(sibling);
192
- const container = {
193
- add: (node: typeof child) => parent.addChild(node),
194
- remove: (node: typeof child) => parent.removeChild(node)
195
- };
196
- const history = new CommandHistory();
197
-
198
- await history.execute(new DeleteNodeCommand(container, child));
199
- assert.equal(child.parent, null);
200
- assert.deepEqual(parent.children.map((node) => node.name), ["sibling"]);
201
-
202
- await history.undo();
203
- assert.equal(child.parent, parent);
204
- assert.deepEqual(parent.children.map((node) => node.name), ["child", "sibling"]);
205
-
206
- await history.redo();
207
- assert.equal(child.parent, null);
208
- assert.deepEqual(parent.children.map((node) => node.name), ["sibling"]);
209
- });
210
-
211
- test("DeleteNodeCommand falls back to generic containers for unparented scene-shaped nodes", async () => {
212
- const scene = new Scene();
213
- const node = scene.createNode("loose");
214
- const nodes = [node];
215
- const container = {
216
- add: (value: typeof node) => nodes.push(value),
217
- remove: (value: typeof node) => {
218
- const index = nodes.indexOf(value);
219
- if (index >= 0) {
220
- nodes.splice(index, 1);
221
- }
222
- }
223
- };
224
- const history = new CommandHistory();
225
-
226
- await history.execute(new DeleteNodeCommand(container, node));
227
- assert.deepEqual(nodes, []);
228
-
229
- await history.undo();
230
- assert.deepEqual(nodes, [node]);
231
- });
232
-
233
- test("PickingService and TranslateGizmo use command history for scene node edits", async () => {
234
- const scene = new Scene();
235
- const node = scene.createNode("editable");
236
- scene.root.addChild(node);
237
- const picking = new PickingService();
238
- picking.addTarget({ id: "editable", node, bounds: { min: [-1, -1, -1], max: [1, 1, 1] } });
239
- picking.resizePickingBuffer(640, 360);
240
-
241
- const evidence = picking.snapshot();
242
- assert.equal(evidence.source, "origin-master-gpu-picking-adapted");
243
- assert.equal(evidence.registeredTargetCount, 1);
244
- assert.equal(evidence.width, 640);
245
- assert.equal(evidence.height, 360);
246
- assert.equal(evidence.sampleColorId?.targetId, "editable");
247
- assert.deepEqual(evidence.sampleColorId?.color, [1, 0, 0, 255]);
248
- assert.equal(evidence.decodedSampleTargetId, "editable");
249
- assert.equal(picking.targetIdFromColor([1, 0, 0, 255]), "editable");
250
- assert.equal(evidence.evidence.colorIdEncoding, true);
251
- assert.equal(evidence.evidence.colorIdDecoding, true);
252
- assert.equal(evidence.evidence.raycastFallback, true);
253
- assert.ok(evidence.blockedClaims.includes("production GPU framebuffer picking pass"));
254
-
255
- const hit = picking.pick(new Ray(new Vector3(0, 0, 5), new Vector3(0, 0, -1)));
256
- assert.equal(hit?.target.node, node);
257
- assert.equal(picking.snapshot().needsUpdate, false);
258
- picking.invalidatePickingBuffer();
259
- assert.equal(picking.snapshot().needsUpdate, true);
260
- assert.throws(() => picking.resizePickingBuffer(0, 360), /positive integers/);
261
- assert.throws(() => PickingService.idToColor(0), /1..16777215/);
262
-
263
- const history = new CommandHistory();
264
- const gizmo = new TranslateGizmo(history);
265
- gizmo.setTarget(node);
266
- await gizmo.drag({ axis: "x", delta: 2 });
267
- assert.equal(node.transform.position[0], 2);
268
- await history.undo();
269
- assert.equal(node.transform.position[0], 0);
270
- });
271
-
272
- test("InspectorModel and EditorRuntime apply typed inspector edits through undo history", async () => {
273
- const target = {
274
- name: "cube",
275
- visible: true,
276
- transform: {
277
- x: 1,
278
- y: 2
279
- }
280
- };
281
- const inspector = new InspectorModel();
282
- const history = new CommandHistory();
283
-
284
- await history.execute(inspector.createSetPropertyCommand(target, ["transform", "x"], 4));
285
- assert.equal(target.transform.x, 4);
286
- await history.undo();
287
- assert.equal(target.transform.x, 1);
288
- assert.throws(() => inspector.createSetPropertyCommand(target, ["transform"], 0), /not editable/);
289
- assert.throws(() => inspector.createSetPropertyCommand(target, ["visible"], "false"), /expected boolean/);
290
- assert.throws(() => inspector.createSetPropertyCommand(target, ["missing"], true), /does not exist/);
291
-
292
- const runtime = new EditorRuntime();
293
- await runtime.editInspectedProperty(target, ["name"], "hero");
294
- assert.equal(target.name, "hero");
295
- await runtime.undo();
296
- assert.equal(target.name, "cube");
297
- });
298
-
299
- test("EditorRuntime exposes public selection, picking, transform, and diagnostics operations", async () => {
300
- const scene = new Scene();
301
- const node = scene.createNode("runtime-owned");
302
- scene.root.addChild(node);
303
- const runtime = new EditorRuntime();
304
-
305
- runtime.select([node.id]);
306
- assert.deepEqual(runtime.currentSelection(), [node.id]);
307
-
308
- runtime.setPickTargets([{ id: node.id, node, bounds: { min: [-1, -1, -1], max: [1, 1, 1] } }]);
309
- const hit = runtime.pick(new Ray(new Vector3(0, 0, 5), new Vector3(0, 0, -1)));
310
- assert.equal(hit?.target.id, node.id);
311
- assert.equal(runtime.snapshot().picking.decodedSampleTargetId, node.id);
312
-
313
- await runtime.translateTarget(node, { axis: "x", delta: 1.5 });
314
- assert.equal(node.transform.position[0], 2);
315
- assert.equal(runtime.snapshot().undoDepth, 1);
316
-
317
- const diagnostics = runtime.updateDiagnostics({
318
- frameTimeMs: 2,
319
- drawCalls: 1,
320
- nodeCount: 1,
321
- assetCount: 0,
322
- physicsBodies: 0,
323
- resources: [{ id: "shader-runtime", label: "Runtime shader", kind: "shader", status: "warning" }]
324
- });
325
- assert.equal(diagnostics.warnings, 1);
326
- assert.equal(runtime.diagnosticsSnapshot().shaderWarnings[0]?.id, "shader-runtime");
327
-
328
- runtime.clearSelection();
329
- assert.deepEqual(runtime.currentSelection(), []);
330
- });
331
-
332
- test("DiagnosticsOverlayModel validates profiler resource and shader diagnostics", () => {
333
- const runtime = new EditorRuntime();
334
- const snapshot = runtime.updateDiagnostics({
335
- frameTimeMs: 4.5,
336
- drawCalls: 2,
337
- triangleCount: 12,
338
- nodeCount: 3,
339
- assetCount: 1,
340
- physicsBodies: 1,
341
- resources: [
342
- { id: "shader-main", label: "Main shader", kind: "shader", status: "warning", detail: "Missing normal map" },
343
- { id: "asset-model", label: "model.glb", kind: "asset", status: "ok" }
344
- ]
345
- });
346
-
347
- assert.equal(snapshot.warnings, 1);
348
- assert.equal(snapshot.errors, 0);
349
- assert.equal(snapshot.shaderWarnings[0]?.id, "shader-main");
350
- assert.equal(runtime.snapshot().diagnostics.drawCalls, 2);
351
-
352
- assert.throws(() => runtime.updateDiagnostics({
353
- frameTimeMs: -1,
354
- drawCalls: 0,
355
- nodeCount: 0,
356
- assetCount: 0,
357
- physicsBodies: 0
358
- }), /frame time/);
359
- });
360
-
361
- test("TimelineModel ports bounded track, clip, easing, loop, lock, mute, and signal evidence", () => {
362
- const timeline = new TimelineModel({
363
- id: "timeline-port",
364
- duration: 2,
365
- loopMode: "loop",
366
- speed: 1.5,
367
- frameRate: 60,
368
- tracks: [
369
- {
370
- id: "animation-track",
371
- name: "Animation",
372
- type: "animation",
373
- clips: [
374
- {
375
- id: "run",
376
- name: "Run",
377
- startTime: 0,
378
- duration: 1,
379
- easeInDuration: 0.5,
380
- easeIn: "ease-in-out",
381
- blendMode: "mix",
382
- weight: 0.8,
383
- clipInOffset: 0.2
384
- }
385
- ]
386
- },
387
- {
388
- id: "signal-track",
389
- name: "Signals",
390
- type: "signal",
391
- locked: true,
392
- clips: [{ id: "footstep", name: "Footstep", clipName: "footstep", startTime: 0.25, duration: 0.1 }]
393
- },
394
- {
395
- id: "muted-guide",
396
- name: "Muted Guide",
397
- type: "audio",
398
- muted: true,
399
- clips: [{ id: "beat", name: "Beat", startTime: 0, duration: 2 }]
400
- }
401
- ]
402
- });
403
-
404
- timeline.seek(0.25);
405
- const snapshot = timeline.snapshot();
406
- assert.equal(snapshot.activeClipCount, 2);
407
- assert.equal(snapshot.activeClips.find((clip) => clip.clipId === "run")?.blendWeight, 0.4);
408
- assert.equal(snapshot.activeClips.find((clip) => clip.clipId === "run")?.assetTime, 0.45);
409
- assert.deepEqual(snapshot.signalEvents, ["footstep"]);
410
- assert.equal(snapshot.evidence.oldCodebasePort, true);
411
- assert.equal(snapshot.evidence.clipEasing, true);
412
- assert.equal(snapshot.evidence.clipBlending, true);
413
- assert.equal(snapshot.evidence.muteLockState, true);
414
- assert.equal(snapshot.evidence.loopPlayback, true);
415
- assert.equal(snapshot.evidence.signalMarkers, true);
416
-
417
- timeline.play();
418
- timeline.tick(2);
419
- assert.equal(timeline.snapshot().time, 1.25);
420
-
421
- const lockedTrack = timeline.tracks.find((track) => track.id === "signal-track");
422
- assert.throws(() => lockedTrack?.addClip(new TimelineClip({ name: "Late Signal", startTime: 1, duration: 0.1 })), /locked timeline track/);
423
- });
424
-
425
- test("TimelineRuntimeBridge applies editor-authored clips and signals to runtime targets", () => {
426
- const timeline = new TimelineModel({
427
- id: "combat-timeline",
428
- duration: 1.5,
429
- loopMode: "none",
430
- tracks: [
431
- {
432
- id: "fighter-animation",
433
- name: "Fighter Animation",
434
- type: "animation",
435
- weight: 0.5,
436
- clips: [
437
- {
438
- id: "light-punch",
439
- name: "Light Punch",
440
- startTime: 0,
441
- duration: 0.8,
442
- assetId: "fighter-glb",
443
- clipName: "LightPunch",
444
- blendMode: "mix",
445
- weight: 0.8,
446
- clipInOffset: 0.1,
447
- properties: {
448
- runtimeNodeId: "player",
449
- authoringLane: "upper-body"
450
- }
451
- }
452
- ]
453
- },
454
- {
455
- id: "combat-events",
456
- name: "Combat Events",
457
- type: "signal",
458
- clips: [
459
- {
460
- id: "hitbox-open",
461
- name: "Hitbox Open",
462
- startTime: 0.25,
463
- duration: 0.05,
464
- properties: {
465
- event: "hitbox.open",
466
- targetId: "player",
467
- hitbox: "right-fist"
468
- }
469
- }
470
- ]
471
- }
472
- ]
473
- });
474
- const applications: TimelineRuntimeAnimationApplication[] = [];
475
- const signals: TimelineRuntimeSignalDispatch[] = [];
476
- const bridge = createTimelineRuntimeBridge({
477
- timeline,
478
- bindings: [
479
- {
480
- trackId: "fighter-animation",
481
- targetId: "player",
482
- assetId: "fighter-glb",
483
- clipNameMap: { LightPunch: "LightPunch" }
484
- }
485
- ],
486
- targets: [
487
- {
488
- id: "player",
489
- applyTimelineAnimation(application) {
490
- applications.push(application);
491
- },
492
- applyTimelineSignal(signal) {
493
- signals.push(signal);
494
- },
495
- snapshot() {
496
- return { applications: applications.length, signals: signals.length };
497
- }
498
- }
499
- ]
500
- });
501
-
502
- const first = bridge.applyAt(0.25);
503
- assert.equal(applications.length, 1);
504
- assert.equal(applications[0]?.targetId, "player");
505
- assert.equal(applications[0]?.clipName, "LightPunch");
506
- assert.equal(applications[0]?.assetTime, 0.35);
507
- assert.equal(applications[0]?.blendWeight, 0.4);
508
- assert.equal(signals.length, 1);
509
- assert.equal(signals[0]?.event, "hitbox.open");
510
- assert.equal(first.kind, "aura-editor-timeline-runtime-bridge");
511
- assert.equal(first.evidence.timelineToRuntimeBridge, true);
512
- assert.equal(first.evidence.animationClipBinding, true);
513
- assert.equal(first.evidence.signalDispatch, true);
514
- const targetSnapshot = first.targets[0]?.snapshot as { applications: number } | undefined;
515
- assert.equal(targetSnapshot?.applications, 1);
516
-
517
- bridge.applyAt(0.26);
518
- assert.equal(signals.length, 1);
519
- bridge.applyAt(0.25, { replaySignals: true });
520
- assert.equal(signals.length, 2);
521
- });
522
-
523
- test("ProjectSerializer round-trips editor timelines, runtime bindings, asset provenance, and visual graph hooks", () => {
524
- const timeline = new TimelineModel({
525
- id: "episode-timeline",
526
- name: "Episode Timeline",
527
- duration: 2,
528
- tracks: [
529
- {
530
- id: "character-animation",
531
- name: "Character Animation",
532
- type: "animation",
533
- clips: [{ id: "idle", name: "Idle", startTime: 0, duration: 2, assetId: "toon-glb", clipName: "Idle" }]
534
- },
535
- {
536
- id: "markers",
537
- name: "Markers",
538
- type: "signal",
539
- clips: [{ id: "mouth-aa", name: "Mouth AA", startTime: 0.4, duration: 0.05, properties: { event: "viseme", targetId: "toon", viseme: "AA" } }]
540
- }
541
- ]
542
- });
543
- const project: EditorProjectDocument = {
544
- schema: "a3d-editor-project",
545
- version: 105,
546
- name: "1.0.5 Editor Workflow",
547
- nodes: [{ id: "toon", name: "Typed Toon", runtimeNodeId: "toon" }],
548
- assets: [
549
- {
550
- id: "toon-glb",
551
- name: "toon.glb",
552
- type: "glb",
553
- uri: "assets/toon.glb",
554
- source: "Aura3D typed asset catalog fixture",
555
- license: "CC0",
556
- clips: ["Idle"],
557
- morphTargets: ["AA"]
558
- }
559
- ],
560
- timelines: [
561
- {
562
- ...timeline.toConfig(),
563
- bindings: [{ trackId: "character-animation", targetId: "toon", assetId: "toon-glb" }],
564
- evidence: {
565
- authoredInEditor: true,
566
- runtimeReplay: true,
567
- animationEvents: true
568
- }
569
- }
570
- ],
571
- visualGraphs: [
572
- {
573
- id: "graph-viseme",
574
- name: "Viseme Graph",
575
- nodes: [{ id: "on-viseme", type: "event" }],
576
- edges: [],
577
- runtimeBindings: [{ nodeId: "on-viseme", targetId: "toon", event: "viseme" }]
578
- }
579
- ],
580
- editor: {
581
- selectedNodeId: "toon",
582
- activeTool: "timeline",
583
- activeTimelineId: "episode-timeline",
584
- playMode: "edit"
585
- },
586
- evidence: {
587
- serializedBy: "editor-runtime",
588
- roundTripReady: true,
589
- browserWorkflowReady: false
590
- }
591
- };
592
-
593
- const serialized = serializeEditorProject(project);
594
- assert.match(serialized, /"timelines"/);
595
- assert.match(serialized, /"visualGraphs"/);
596
- const parsed = parseEditorProject(serialized);
597
- assert.equal(parsed.timelines?.[0]?.bindings?.[0]?.targetId, "toon");
598
- assert.equal(parsed.timelines?.[0]?.tracks?.[1]?.clips?.[0]?.properties?.event, "viseme");
599
-
600
- const evidence = collectEditorProjectEvidence(parsed);
601
- assert.equal(evidence.kind, "aura-editor-project-evidence");
602
- assert.equal(evidence.timelineCount, 1);
603
- assert.equal(evidence.visualGraphCount, 1);
604
- assert.equal(evidence.timelineBindingCount, 1);
605
- assert.equal(evidence.signalMarkerCount, 1);
606
- assert.equal(evidence.typedAssetEvidenceCount, 1);
607
- assert.equal(evidence.roundTripReady, true);
608
- assert.equal(evidence.evidence.timelineSerialization, true);
609
- assert.equal(evidence.evidence.visualGraphSerialization, true);
610
- assert.equal(evidence.evidence.runtimeReplayBindings, true);
611
- assert.equal(evidence.evidence.sourceLicenseAssetEvidence, true);
612
-
613
- assert.throws(() => serializeEditorProject({
614
- ...project,
615
- timelines: [{ ...timeline.toConfig(), bindings: [{ trackId: "character-animation", targetId: "" }] }]
616
- }), /targetId is required/);
617
- });
618
-
619
- test("TimelineEditorController edits clips with snapping, selection, undo, redo, copy, paste, and serialization", async () => {
620
- const controller = new TimelineEditorController({
621
- timeline: new TimelineModel({ id: "episode", duration: 6, frameRate: 24, loopMode: "none" }),
622
- snapInterval: 0.5
623
- });
624
- const animationTrack = await controller.addTrack("animation", "Character");
625
- assert.equal(animationTrack.type, "animation");
626
- assert.equal(animationTrack.properties.auraTrackKind, "animation");
627
-
628
- const clip = await controller.addClip(animationTrack.id, {
629
- id: "wave",
630
- name: "Wave",
631
- startTime: 0.26,
632
- duration: 1.25,
633
- clipName: "Wave",
634
- properties: { targetId: "toon" }
635
- });
636
- assert.equal(clip.startTime, 0.5);
637
- assert.equal(controller.snapshot().evidence.clipEditing, true);
638
- assert.equal(controller.snapshot().evidence.keyframeReadyTracks, true);
639
-
640
- await controller.moveClip("wave", 1.26);
641
- assert.equal(clip.startTime, 1.5);
642
- await controller.resizeClip("wave", 2);
643
- assert.equal(clip.duration, 2);
644
- controller.selectClip("wave");
645
- controller.copySelection();
646
- const pasted = await controller.pasteClips(animationTrack.id, 4);
647
- assert.equal(pasted.length, 1);
648
- assert.equal(pasted[0].startTime, 4);
649
- assert.deepEqual(controller.snapshot().selectedIds, [pasted[0].id]);
650
-
651
- await controller.undo();
652
- assert.equal(animationTrack.clips.length, 1);
653
- await controller.redo();
654
- assert.equal(animationTrack.clips.length, 2);
655
-
656
- const split = await controller.splitClip("wave", 2);
657
- assert.deepEqual(split.map((item) => item.id), ["wave-a", "wave-b"]);
658
- assert.equal(animationTrack.clips.length, 3);
659
- controller.scrubTo(2.49);
660
- assert.equal(controller.timeline.currentTime, 2.5);
661
- controller.handleKeyboardShortcut("ArrowLeft");
662
- assert.equal(Number(controller.timeline.currentTime.toFixed(4)), 2.4583);
663
-
664
- const serialized = controller.serializeTimeline();
665
- assert.equal(serialized.tracks?.[0]?.properties?.auraTrackKind, "animation");
666
- assert.equal(serialized.tracks?.[0]?.clips?.length, 3);
667
- });
668
-
669
- test("KeyframeEditor and CurveEditor author deterministic bezier keyframes on timeline clips", async () => {
670
- const clip = new TimelineClip({ id: "camera-move", name: "Camera Move", startTime: 0, duration: 2 });
671
- const keyframes = new KeyframeEditor();
672
- await keyframes.addKeyframe(clip, { id: "x0", propertyPath: "transform.position.x", time: 0, value: 0, interpolation: "bezier", outHandle: { time: 0.3, value: 4 } });
673
- await keyframes.addKeyframe(clip, { id: "x1", propertyPath: "transform.position.x", time: 2, value: 10, interpolation: "linear", inHandle: { time: -0.3, value: -2 } });
674
- assert.equal(readTimelineKeyframes(clip).length, 2);
675
-
676
- const curve = new CurveEditor(keyframes.commandHistory);
677
- assert.equal(curve.sample(clip, "transform.position.x", 0).value, 0);
678
- assert.equal(curve.sample(clip, "transform.position.x", 2).value, 10);
679
- assert.equal(typeof curve.sample(clip, "transform.position.x", 1).value, "number");
680
-
681
- await curve.setBezierHandles(clip, "x1", { inHandle: { time: -0.25, value: -1 }, outHandle: { time: 0.25, value: 1 } });
682
- assert.equal(readTimelineKeyframes(clip).find((keyframe) => keyframe.id === "x1")?.interpolation, "bezier");
683
- const moved = await keyframes.moveKeyframes(clip, ["x0", "x1"], 0.5);
684
- assert.deepEqual(moved.map((keyframe) => keyframe.time), [0.5, 2.5]);
685
- const copy = keyframes.copyKeyframes(clip, ["x0"]);
686
- const pasted = await keyframes.pasteKeyframes(clip, copy, { timeOffset: 1, idPrefix: "copy" });
687
- assert.equal(pasted[0].id, "copy-x0-0");
688
- assert.equal(readTimelineKeyframes(clip).length, 3);
689
-
690
- await keyframes.commandHistory.undo();
691
- assert.equal(readTimelineKeyframes(clip).length, 2);
692
- assert.equal(curve.evidence(clip, "transform.position.x").evidence.deterministicSampling, true);
693
- });
694
-
695
- test("AnimationSceneEditor, AssetDropZone, SceneOutliner, and PropertyPanel compose and edit animation scene nodes", async () => {
696
- const editor = new AnimationSceneEditor();
697
- const reviewPanel = new EpisodeReviewPanel({
698
- packageId: "moon-garden-001",
699
- packageHash: "sha256-review",
700
- status: "needs-review",
701
- notes: [],
702
- rejectedFrames: []
703
- });
704
- const renderQueue = new RenderQueuePanel([
705
- { id: "episode-webm", label: "Episode WebM", status: "running", progress: 0.25, currentFrame: 45, totalFrames: 180 }
706
- ]);
707
- editor.setEpisodeState({
708
- shots: ["intro", "dialogue"],
709
- assets: ["typed-toon"],
710
- captions: ["caption-1"],
711
- visemes: ["miko-aa"],
712
- renderState: renderQueue.snapshot(),
713
- reviewState: reviewPanel.snapshot()
714
- });
715
- const node = await editor.placeAsset(
716
- {
717
- kind: "aura-asset-ref",
718
- id: "typed-toon",
719
- name: "Typed Toon",
720
- type: "glb",
721
- source: "typed-catalog",
722
- license: "CC0",
723
- category: "character",
724
- clips: ["Idle", "Wave"],
725
- lipSyncReady: true
726
- },
727
- { position: { x: 1, y: 0, z: 2 } }
728
- );
729
-
730
- assert.equal(node.kind, "character");
731
- assert.equal(node.transform.position.x, 1);
732
- assert.deepEqual(editor.runtime.currentSelection(), [node.id]);
733
- assert.equal(editor.snapshot().placedAssetCount, 1);
734
- assert.equal(editor.snapshot().episode.shotCount, 2);
735
- assert.equal(editor.snapshot().episode.hasRenderState, true);
736
- assert.equal(editor.snapshot().episode.hasReviewState, true);
737
- assert.equal(editor.snapshot().evidence.episodeState, true);
738
-
739
- await editor.setTransform(node.id, { scale: { x: 2, y: 2, z: 2 } });
740
- assert.equal(node.transform.scale.x, 2);
741
- await editor.runtime.history.undo();
742
- assert.equal(node.transform.scale.x, 1);
743
- await editor.runtime.history.redo();
744
- assert.equal(node.transform.scale.x, 2);
745
-
746
- const outliner = new SceneOutliner();
747
- const outlinerItems = outliner.describe(editor.root, new Set(editor.runtime.currentSelection()));
748
- assert.equal(outlinerItems.find((item) => item.id === node.id)?.icon, "[CHAR]");
749
- assert.equal(outlinerItems.find((item) => item.id === node.id)?.selected, true);
750
-
751
- const panel = new PropertyPanel({ history: editor.runtime.history });
752
- const fields = panel.describe(node);
753
- assert.equal(fields.some((field) => field.label === "transform.position.x" && field.editable), true);
754
- await panel.edit(node, ["transform", "position", "x"], 3);
755
- assert.equal(node.transform.position.x, 3);
756
- await editor.runtime.history.undo();
757
- assert.equal(node.transform.position.x, 1);
758
-
759
- const serialized = editor.serializeScene();
760
- const restored = new AnimationSceneEditor();
761
- restored.loadScene(serialized);
762
- assert.equal(restored.snapshot().nodeCount, 2);
763
-
764
- await assert.rejects(() => editor.placeAsset({
765
- id: "raw-url-character",
766
- name: "Raw URL Character",
767
- type: "glb",
768
- uri: "https://example.test/raw-character.glb",
769
- category: "character",
770
- clips: ["Idle"],
771
- lipSyncReady: true
772
- }), /typed Aura3D asset reference/);
773
- });
774
-
775
- test("Timeline track type helpers create animation timeline lanes compatible with route playback", () => {
776
- const dialogue = createTimelineTrackConfig("dialogue", "Dialogue");
777
- const captions = createTimelineTrackConfig("caption", "Captions");
778
- const shots = createTimelineTrackConfig("shot", "Shots");
779
- const routeCalls: string[] = [];
780
- const timeline = new TimelineModel({
781
- duration: 3,
782
- tracks: [
783
- { ...shots, clips: [{ id: "shot-open", name: "Open", startTime: 0.5, duration: 1, properties: { shotId: "open" } }] },
784
- { ...dialogue, clips: [{ id: "line", name: "Line", startTime: 0, duration: 1, properties: { speaker: "Narrator" } }] },
785
- { ...captions, clips: [{ id: "caption", name: "Caption", startTime: 0, duration: 1, properties: { text: "Hello" } }] }
786
- ]
787
- });
788
- const controller = new TimelineEditorController({
789
- timeline,
790
- routeBinding: {
791
- play: () => routeCalls.push("play"),
792
- pause: () => routeCalls.push("pause"),
793
- scrub: (time) => routeCalls.push(`scrub:${time}`),
794
- jumpToShot: (shotId, time) => routeCalls.push(`shot:${shotId}:${time}`)
795
- }
796
- });
797
-
798
- assert.equal(timeline.tracks[1].type, "audio");
799
- assert.equal(timeline.tracks[1].properties.auraTrackKind, "dialogue");
800
- assert.equal(timeline.tracks[2].type, "generic");
801
- assert.equal(timeline.snapshot().trackCount, 3);
802
- assert.equal(controller.jumpToShot("open"), 0.5);
803
- controller.togglePlayback();
804
- controller.togglePlayback();
805
- assert.deepEqual(routeCalls, ["scrub:0.5", "shot:open:0.5", "play", "pause"]);
806
- });
807
-
808
- test("EpisodeReviewPanel and RenderQueuePanel serialize approval and render progress", () => {
809
- const review = new EpisodeReviewPanel({
810
- packageId: "moon-garden-001",
811
- status: "needs-review",
812
- notes: [],
813
- rejectedFrames: []
814
- });
815
- review.addNote({ id: "note-1", author: "director", text: "Caption clears action.", time: 2 });
816
- const approved = review.approve("reviewer", "2026-06-06T00:00:00.000Z");
817
- const rejected = new EpisodeReviewPanel({
818
- packageId: "moon-garden-002",
819
- status: "needs-review",
820
- notes: [],
821
- rejectedFrames: []
822
- });
823
- rejected.rejectFrame({ id: "frame-12", time: 1.2, reason: "mouth shape is static" });
824
- const queue = new RenderQueuePanel([
825
- { id: "png-sequence", label: "PNG Sequence", status: "queued", progress: 0 },
826
- { id: "episode-webm", label: "Episode WebM", status: "running", progress: 0.5, currentFrame: 90, totalFrames: 180 }
827
- ]);
828
- const done = queue.updateProgress("episode-webm", 1, {
829
- status: "done",
830
- currentFrame: 180,
831
- totalFrames: 180,
832
- outputPath: "dist/episodes/moon-garden-001/episode.webm"
833
- });
834
-
835
- assert.equal(approved.approvalRecorded, true);
836
- assert.equal(approved.noteCount, 1);
837
- assert.equal(rejected.snapshot().rejectedFrameCount, 1);
838
- assert.throws(() => rejected.approve("reviewer"), /rejected frames/);
839
- assert.equal(done.doneCount, 1);
840
- assert.equal(done.runningCount, 0);
841
- assert.deepEqual(done.outputPaths, ["dist/episodes/moon-garden-001/episode.webm"]);
842
- });
843
-
844
- test("EpisodeReviewPanel supports waveform review lanes, manual viseme edits, and visual review dashboard evidence", () => {
845
- const waveform: AudioWaveformData = {
846
- duration: 1,
847
- sampleRate: 4,
848
- channels: 1,
849
- peakCount: 4,
850
- samplesPerPeak: 1,
851
- normalized: true,
852
- peaks: [
853
- { min: -0.2, max: 0.25, rms: 0.18 },
854
- { min: -0.7, max: 0.8, rms: 0.56 },
855
- { min: -1, max: 0.9, rms: 0.72 },
856
- { min: -0.1, max: 0.2, rms: 0.12 }
857
- ]
858
- };
859
- const waveformReview = createAudioWaveformReviewData([
860
- { id: "miko-dialogue", label: "Miko dialogue", startTime: 2, waveform }
861
- ], { width: 240, height: 48 });
862
- const sourceVisemes = createAuraVoiceVisemeTrack({
863
- episodeId: "moon-garden",
864
- language: "en-US",
865
- frameRate: 24,
866
- cues: [{
867
- id: "source-aa",
868
- characterId: "miko",
869
- startTime: 2,
870
- endTime: 2.25,
871
- visemeId: "aa",
872
- mouthOpenness: 0.4,
873
- weight: 0.7
874
- }]
875
- });
876
- const editedVisemes = createVisemeTimelineTrack({
877
- episodeId: "moon-garden",
878
- language: "en-US",
879
- frameRate: 24,
880
- sourceTrack: sourceVisemes,
881
- manualEdits: [{
882
- id: "manual-oh",
883
- reason: "director adjusted close-up mouth shape",
884
- cue: {
885
- id: "manual-oh-cue",
886
- characterId: "miko",
887
- startTime: 2.08,
888
- endTime: 2.32,
889
- visemeId: "oh",
890
- mouthOpenness: 0.82,
891
- weight: 1
892
- }
893
- }]
894
- });
895
- assert.equal(sampleVisemeTimelineTrack(editedVisemes, 2.12, "miko").visemeId, "oh");
896
-
897
- const review = new EpisodeReviewPanel({
898
- packageId: "moon-garden-review",
899
- status: "needs-review",
900
- notes: [],
901
- rejectedFrames: []
902
- });
903
- review.setWaveformLanes(waveformReview.stems.map((stem) => ({
904
- id: stem.id,
905
- label: stem.label,
906
- startTime: stem.startTime,
907
- duration: stem.duration,
908
- peakCount: stem.peakCount,
909
- pathPointCount: stem.path.length
910
- })));
911
- review.applyManualVisemeEdit({
912
- id: editedVisemes.manualEdits[0].id,
913
- characterId: editedVisemes.manualEdits[0].cue.characterId,
914
- visemeId: editedVisemes.manualEdits[0].cue.visemeId,
915
- startTime: editedVisemes.manualEdits[0].cue.startTime,
916
- endTime: editedVisemes.manualEdits[0].cue.endTime,
917
- reason: editedVisemes.manualEdits[0].reason
918
- });
919
- review.addNote({ id: "note-mouth", author: "director", text: "Hold OH through the close-up.", time: 2.1 });
920
- review.rejectFrame({ id: "frame-51", time: 2.125, reason: "mouth shape drifts before manual edit" });
921
-
922
- const snapshot = review.snapshot();
923
- assert.equal(snapshot.waveformLaneCount, 1);
924
- assert.equal(snapshot.manualVisemeEditCount, 1);
925
- assert.equal(snapshot.reviewUiEvidence.waveformReview, true);
926
- assert.equal(snapshot.reviewUiEvidence.manualVisemeEdits, true);
927
- assert.equal(snapshot.reviewUiEvidence.reviewerNotes, true);
928
- assert.equal(snapshot.reviewUiEvidence.visualFrameReview, true);
929
-
930
- const dashboard = new VisualReviewDashboard([snapshot]).snapshot();
931
- assert.equal(dashboard.kind, "visual-review-dashboard");
932
- assert.deepEqual(dashboard.approvalBlockedPackageIds, ["moon-garden-review"]);
933
- assert.equal(dashboard.failedFrames[0].reason, "mouth shape drifts before manual edit");
934
- assert.equal(dashboard.reviewerNotes[0].text, "Hold OH through the close-up.");
935
- assert.equal(dashboard.waveformLanes[0].pathPointCount, 4);
936
- assert.equal(dashboard.manualVisemeEdits[0].visemeId, "oh");
937
- assert.equal(dashboard.evidence.listsFailedFrames, true);
938
- assert.equal(dashboard.evidence.listsReviewerNotes, true);
939
- assert.equal(dashboard.evidence.waveformReview, true);
940
- assert.equal(dashboard.evidence.manualVisemeEdits, true);
941
- assert.equal(dashboard.evidence.approvalBlocking, true);
942
- });
943
-
944
- test("CameraPathEditor edits shot camera moves through timeline keyframes and route playback evidence", async () => {
945
- const routeCalls: string[] = [];
946
- const shots = createTimelineTrackConfig("shot", "Shots");
947
- const camera = createTimelineTrackConfig("camera", "Camera");
948
- const timeline = new TimelineModel({
949
- duration: 5,
950
- frameRate: 24,
951
- tracks: [
952
- { ...shots, clips: [{ id: "shot-open", name: "Open", startTime: 1, duration: 2, properties: { shotId: "open" } }] },
953
- { ...camera, clips: [{ id: "camera-open", name: "Open Camera", startTime: 1, duration: 2, properties: { shotId: "open" } }] }
954
- ]
955
- });
956
- const controller = new TimelineEditorController({
957
- timeline,
958
- routeBinding: {
959
- scrub: (time) => routeCalls.push(`scrub:${time}`),
960
- jumpToShot: (shotId, time) => routeCalls.push(`shot:${shotId}:${time}`)
961
- }
962
- });
963
- const cameraClip = timeline.tracks[1].clips[0];
964
- const cameraEditor = new CameraPathEditor();
965
-
966
- await cameraEditor.setCameraKeyframe(cameraClip, {
967
- id: "open-start",
968
- time: 1,
969
- position: [0, 1.6, 4],
970
- target: [0, 1.2, 0],
971
- fov: 42,
972
- focusDistance: 4,
973
- shake: 0,
974
- interpolation: "linear"
975
- });
976
- await cameraEditor.setCameraKeyframe(cameraClip, {
977
- id: "open-end",
978
- time: 3,
979
- position: [1, 1.8, 3],
980
- target: [0.2, 1.4, 0],
981
- fov: 32,
982
- focusDistance: 3,
983
- shake: 0.08,
984
- interpolation: "linear"
985
- });
986
- await cameraEditor.setCameraKeyframe(cameraClip, {
987
- id: "open-end",
988
- time: 3,
989
- position: [1.5, 1.8, 2.8],
990
- target: [0.25, 1.4, 0],
991
- fov: 30,
992
- focusDistance: 2.8,
993
- shake: 0.05,
994
- interpolation: "linear"
995
- });
996
-
997
- const keyframes = cameraEditor.readCameraPathKeyframes(cameraClip);
998
- assert.equal(keyframes.length, 2);
999
- assert.deepEqual(keyframes[1].position, [1.5, 1.8, 2.8]);
1000
- assert.equal(readTimelineKeyframes(cameraClip).length, 18);
1001
-
1002
- const sample = cameraEditor.sample(cameraClip, 2);
1003
- assert.deepEqual(sample.position, [0.75, 1.7, 3.4]);
1004
- assert.equal(sample.fov, 36);
1005
-
1006
- const evidence = cameraEditor.evidence(cameraClip);
1007
- assert.equal(evidence.cameraKeyframeCount, 2);
1008
- assert.equal(evidence.editablePropertyCount, 9);
1009
- assert.equal(evidence.evidence.shotCameraMoveEditing, true);
1010
- assert.equal(evidence.evidence.cameraPositionCurves, true);
1011
- assert.equal(evidence.evidence.cameraTargetCurves, true);
1012
- assert.equal(evidence.evidence.fovCurve, true);
1013
-
1014
- assert.equal(controller.jumpToShot("open"), 1);
1015
- assert.deepEqual(routeCalls, ["scrub:1", "shot:open:1"]);
1016
- assert.equal(controller.serializeTimeline().tracks?.[1]?.clips?.[0]?.properties?.cameraPathEdited, "true");
1017
- });
1018
-
1019
- test("MultiUserReviewWorkflow blocks publish until assigned notes resolve and reviewer quorum approves", () => {
1020
- const workflow = new MultiUserReviewWorkflow({
1021
- packageId: "moon-garden-001",
1022
- requiredReviewerCount: 2,
1023
- participants: [
1024
- { id: "director", name: "Director", role: "owner" },
1025
- { id: "animation", name: "Animation Reviewer", role: "reviewer" },
1026
- { id: "audio", name: "Audio Reviewer", role: "reviewer" }
1027
- ],
1028
- threads: [],
1029
- decisions: []
1030
- });
1031
-
1032
- const open = workflow.addThread({
1033
- id: "thread-mouth-shape",
1034
- authorId: "director",
1035
- text: "Miko mouth shape misses the long vowel.",
1036
- time: 1.25,
1037
- frame: 30,
1038
- assignedTo: "animation",
1039
- resolved: false
1040
- });
1041
- assert.equal(open.canPublish, false);
1042
- assert.equal(open.unresolvedThreadCount, 1);
1043
- assert.equal(open.evidence.assignmentWorkflow, true);
1044
-
1045
- workflow.recordDecision({ reviewerId: "animation", status: "approved", at: "2026-06-06T00:00:00.000Z" });
1046
- assert.equal(workflow.snapshot().canPublish, false);
1047
- workflow.resolveThread("thread-mouth-shape");
1048
- const approved = workflow.recordDecision({ reviewerId: "audio", status: "approved", at: "2026-06-06T00:01:00.000Z" });
1049
-
1050
- assert.equal(approved.status, "approved");
1051
- assert.equal(approved.canPublish, true);
1052
- assert.equal(approved.evidence.multiUserReview, true);
1053
- assert.equal(approved.evidence.reviewerQuorum, true);
1054
- });
1055
-
1056
- test("NonlinearAnimationEditor edits bins, sequences, nested timelines, and clip operations", async () => {
1057
- const editor = new NonlinearAnimationEditor({
1058
- activeSequenceId: "main",
1059
- binAssets: [
1060
- { id: "miko-wave", name: "Miko Wave", kind: "animation", assetId: "assets.miko", clipName: "Wave", duration: 1.5 },
1061
- { id: "line-voice", name: "Line Voice", kind: "audio", assetId: "assets.mikoDialogueStem", duration: 2 }
1062
- ],
1063
- sequences: [
1064
- { id: "main", name: "Main Sequence", timeline: { duration: 8, frameRate: 24, loopMode: "none" } },
1065
- { id: "reaction", name: "Reaction Insert", timeline: { duration: 2, frameRate: 24, loopMode: "none" } }
1066
- ]
1067
- });
1068
- const animationTrack = await editor.addTrack("animation", "Character Animation");
1069
- await editor.addTrack("dialogue", "Dialogue");
1070
- const clipId = await editor.insertAssetClip(animationTrack, "miko-wave", { id: "wave-1", startTime: 0 });
1071
-
1072
- await editor.trimClip(clipId, 1.25);
1073
- const splitIds = await editor.splitClip(clipId, 0.5);
1074
- assert.deepEqual(splitIds, ["wave-1-a", "wave-1-b"]);
1075
- const duplicateId = await editor.duplicateClip("wave-1-b", 1);
1076
- await editor.moveClip(duplicateId, 3);
1077
- await editor.insertNestedSequence(animationTrack, "reaction", { id: "reaction-nest", startTime: 5 });
1078
-
1079
- const snapshot = editor.snapshot();
1080
- assert.equal(snapshot.kind, "nonlinear-animation-editor");
1081
- assert.equal(snapshot.sequenceCount, 2);
1082
- assert.equal(snapshot.nestedSequenceClipCount, 1);
1083
- assert.equal(snapshot.evidence.nonlinearSequences, true);
1084
- assert.equal(snapshot.evidence.trimSplitMoveDuplicate, true);
1085
- assert.equal(snapshot.evidence.multiTrackTimeline, true);
1086
- assert.equal(editor.serialize().sequences.find((sequence) => sequence.id === "main")?.timeline?.tracks?.length, 2);
1087
- });