@aura3d/editor-runtime 1.1.0 → 1.3.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.
@@ -2,12 +2,12 @@ import type { Command } from "./Command";
2
2
  import { CommandHistory } from "./CommandHistory";
3
3
  import type { EditorProjectAssetDocument } from "./ProjectSerializer";
4
4
 
5
- export type CartoonAssetCategory = "character" | "set" | "prop" | "camera" | "light" | "audio" | "material" | "unknown";
5
+ export type AnimationAssetCategory = "character" | "set" | "prop" | "camera" | "light" | "audio" | "material" | "unknown";
6
6
 
7
- export interface CartoonEditorAssetReference extends EditorProjectAssetDocument {
7
+ export interface AnimationEditorAssetReference extends EditorProjectAssetDocument {
8
8
  readonly kind?: "aura-asset-ref" | "editor-asset-ref";
9
- readonly category?: CartoonAssetCategory;
10
- readonly profile?: "cartoon-character" | "cartoon-set" | "cartoon-prop" | "cartoon-audio" | string;
9
+ readonly category?: AnimationAssetCategory;
10
+ readonly profile?: "animation-character" | "animation-set" | "animation-prop" | "animation-audio" | string;
11
11
  readonly style?: string;
12
12
  readonly rigType?: "humanoid" | "creature" | "prop" | "none";
13
13
  readonly lipSyncReady?: boolean;
@@ -21,14 +21,14 @@ export interface AssetDropPlacement {
21
21
  }
22
22
 
23
23
  export interface AssetDropResult<TNode> {
24
- readonly asset: CartoonEditorAssetReference;
24
+ readonly asset: AnimationEditorAssetReference;
25
25
  readonly node: TNode;
26
26
  readonly placement: AssetDropPlacement;
27
27
  }
28
28
 
29
29
  export interface AssetDropZoneOptions<TNode> {
30
30
  readonly history?: CommandHistory;
31
- readonly createNode: (asset: CartoonEditorAssetReference, placement: AssetDropPlacement) => TNode;
31
+ readonly createNode: (asset: AnimationEditorAssetReference, placement: AssetDropPlacement) => TNode;
32
32
  readonly addNode: (node: TNode, placement: AssetDropPlacement) => void;
33
33
  readonly removeNode: (node: TNode) => void;
34
34
  readonly onDrop?: (result: AssetDropResult<TNode>) => void;
@@ -41,7 +41,7 @@ export class AssetDropZone<TNode = unknown> {
41
41
  this.history = options.history ?? new CommandHistory();
42
42
  }
43
43
 
44
- async dropAsset(asset: CartoonEditorAssetReference, placement: AssetDropPlacement = {}): Promise<AssetDropResult<TNode>> {
44
+ async dropAsset(asset: AnimationEditorAssetReference, placement: AssetDropPlacement = {}): Promise<AssetDropResult<TNode>> {
45
45
  validateAssetReference(asset);
46
46
  validatePlacement(placement);
47
47
  const node = this.options.createNode(asset, placement);
@@ -82,7 +82,7 @@ class DropAssetCommand<TNode> implements Command {
82
82
 
83
83
  constructor(
84
84
  private readonly options: Pick<AssetDropZoneOptions<TNode>, "addNode" | "removeNode">,
85
- private readonly asset: CartoonEditorAssetReference,
85
+ private readonly asset: AnimationEditorAssetReference,
86
86
  private readonly node: TNode,
87
87
  private readonly placement: AssetDropPlacement
88
88
  ) {
@@ -98,21 +98,21 @@ class DropAssetCommand<TNode> implements Command {
98
98
  }
99
99
  }
100
100
 
101
- export function serializeAssetForDrag(asset: CartoonEditorAssetReference): string {
101
+ export function serializeAssetForDrag(asset: AnimationEditorAssetReference): string {
102
102
  validateAssetReference(asset);
103
103
  return JSON.stringify(asset);
104
104
  }
105
105
 
106
- export function readAssetFromDataTransfer(dataTransfer: DataTransfer | null): CartoonEditorAssetReference | undefined {
106
+ export function readAssetFromDataTransfer(dataTransfer: DataTransfer | null): AnimationEditorAssetReference | undefined {
107
107
  if (!dataTransfer) return undefined;
108
108
  const encoded = dataTransfer.getData("application/x-aura3d-asset") || dataTransfer.getData("text/plain");
109
109
  if (!encoded.trim()) return undefined;
110
- const parsed = JSON.parse(encoded) as CartoonEditorAssetReference;
110
+ const parsed = JSON.parse(encoded) as AnimationEditorAssetReference;
111
111
  validateAssetReference(parsed);
112
112
  return parsed;
113
113
  }
114
114
 
115
- function validateAssetReference(asset: CartoonEditorAssetReference): void {
115
+ function validateAssetReference(asset: AnimationEditorAssetReference): void {
116
116
  if (!asset.id?.trim()) throw new Error("Dropped Aura3D asset id is required.");
117
117
  if (!asset.name?.trim()) throw new Error(`Dropped Aura3D asset name is required: ${asset.id}`);
118
118
  if ((asset.uri && /^https?:\/\//i.test(asset.uri) || asset.source && /^https?:\/\//i.test(asset.source)) && asset.kind !== "aura-asset-ref") {
@@ -121,12 +121,12 @@ function validateAssetReference(asset: CartoonEditorAssetReference): void {
121
121
  if (asset.source !== undefined && !asset.source.trim()) throw new Error(`Dropped Aura3D asset source cannot be empty: ${asset.id}`);
122
122
  if (asset.license !== undefined && !asset.license.trim()) throw new Error(`Dropped Aura3D asset license cannot be empty: ${asset.id}`);
123
123
  if (asset.category === "character") {
124
- if (asset.rigType === "none") throw new Error(`Cartoon character asset "${asset.id}" must be rigged.`);
125
- if ((asset.clips?.length ?? 0) === 0) throw new Error(`Cartoon character asset "${asset.id}" requires animation clip metadata.`);
126
- if (!asset.lipSyncReady) throw new Error(`Cartoon character asset "${asset.id}" requires lip-sync readiness metadata.`);
124
+ if (asset.rigType === "none") throw new Error(`Animation character asset "${asset.id}" must be rigged.`);
125
+ if ((asset.clips?.length ?? 0) === 0) throw new Error(`Animation character asset "${asset.id}" requires animation clip metadata.`);
126
+ if (!asset.lipSyncReady) throw new Error(`Animation character asset "${asset.id}" requires lip-sync readiness metadata.`);
127
127
  }
128
128
  if ((asset.category === "set" || asset.category === "audio") && !asset.license) {
129
- throw new Error(`Cartoon ${asset.category} asset "${asset.id}" requires license metadata.`);
129
+ throw new Error(`Animation ${asset.category} asset "${asset.id}" requires license metadata.`);
130
130
  }
131
131
  }
132
132
 
@@ -0,0 +1,169 @@
1
+ // Editor authoring controller for animation event tracks (T2.2). Lets a timeline UI add / move /
2
+ // delete typed markers on named lanes (hitbox active-frames, footsteps, VFX, generic markers) and
3
+ // serialize to the canonical `animation-event-tracks/v1` shape that `@aura3d/animation`'s
4
+ // `deserializeAnimationEventTracks` / `sampleClipEvents` consume — so authored tracks drive gameplay
5
+ // (hitbox windows) and playback (footstep/VFX triggers) deterministically.
6
+ //
7
+ // Type-only imports from `@aura3d/animation` keep this controller free of any runtime coupling to
8
+ // that package (the types are erased at build time); the serialized output is verified to round-trip
9
+ // through the animation package's deserializer in tests.
10
+
11
+ import type {
12
+ AnimationClipEvent,
13
+ AnimationClipEventType,
14
+ AnimationEventTrack,
15
+ SerializedAnimationEventTracks
16
+ } from "@aura3d/animation";
17
+
18
+ export interface EventMarkerView {
19
+ readonly id: string;
20
+ readonly time: number;
21
+ readonly duration?: number;
22
+ readonly type: AnimationClipEventType;
23
+ readonly selected: boolean;
24
+ }
25
+
26
+ export interface EventTrackLane {
27
+ readonly name: string;
28
+ readonly type: AnimationClipEventType;
29
+ readonly markers: readonly EventMarkerView[];
30
+ }
31
+
32
+ export interface EventTrackEditorOptions {
33
+ readonly clipId: string;
34
+ readonly duration: number;
35
+ }
36
+
37
+ export interface AddEventMarkerOptions {
38
+ readonly id?: string;
39
+ readonly type?: AnimationClipEventType;
40
+ /** Window length (seconds). >0 makes the marker an active-frame window (e.g. hitbox on/off). */
41
+ readonly duration?: number;
42
+ readonly payload?: unknown;
43
+ readonly tags?: readonly string[];
44
+ }
45
+
46
+ interface LaneState {
47
+ type: AnimationClipEventType;
48
+ markers: AnimationClipEvent[];
49
+ }
50
+
51
+ /**
52
+ * Timeline event-track authoring controller. Deterministic: marker ids come from a monotonic
53
+ * counter (no `Date.now`/`Math.random`), so undo/redo and serialization are reproducible.
54
+ */
55
+ export class EventTrackEditor {
56
+ readonly clipId: string;
57
+ duration: number;
58
+ private readonly order: string[] = [];
59
+ private readonly lanesMap = new Map<string, LaneState>();
60
+ private selectedId: string | undefined;
61
+ private idCounter = 0;
62
+
63
+ constructor(options: EventTrackEditorOptions) {
64
+ if (!Number.isFinite(options.duration) || options.duration < 0) {
65
+ throw new Error("EventTrackEditor duration must be a finite, non-negative number.");
66
+ }
67
+ this.clipId = options.clipId;
68
+ this.duration = options.duration;
69
+ }
70
+
71
+ addTrack(name: string, type: AnimationClipEventType = "marker"): this {
72
+ if (!this.lanesMap.has(name)) {
73
+ this.lanesMap.set(name, { type, markers: [] });
74
+ this.order.push(name);
75
+ }
76
+ return this;
77
+ }
78
+
79
+ removeTrack(name: string): this {
80
+ if (this.lanesMap.delete(name)) this.order.splice(this.order.indexOf(name), 1);
81
+ return this;
82
+ }
83
+
84
+ /** Add a marker to a lane (auto-creates the lane). Returns the new marker id. */
85
+ addMarker(trackName: string, time: number, options: AddEventMarkerOptions = {}): string {
86
+ if (!Number.isFinite(time) || time < 0) {
87
+ throw new Error("Event marker time must be a finite, non-negative number.");
88
+ }
89
+ this.addTrack(trackName, options.type);
90
+ const lane = this.lanesMap.get(trackName)!;
91
+ const id = options.id ?? `${trackName}#${this.idCounter++}`;
92
+ lane.markers.push({
93
+ id,
94
+ name: trackName,
95
+ type: options.type ?? lane.type,
96
+ time,
97
+ ...(options.duration !== undefined ? { duration: options.duration } : {}),
98
+ ...(options.payload !== undefined ? { payload: options.payload } : {}),
99
+ ...(options.tags !== undefined ? { tags: options.tags } : {})
100
+ });
101
+ lane.markers.sort((a, b) => a.time - b.time);
102
+ return id;
103
+ }
104
+
105
+ /** Move an existing marker to a new time. */
106
+ moveMarker(trackName: string, markerId: string, time: number): this {
107
+ if (!Number.isFinite(time) || time < 0) {
108
+ throw new Error("Event marker time must be a finite, non-negative number.");
109
+ }
110
+ const lane = this.lanesMap.get(trackName);
111
+ const index = lane?.markers.findIndex((m) => m.id === markerId) ?? -1;
112
+ if (!lane || index < 0) throw new Error(`Marker ${markerId} not found on track ${trackName}.`);
113
+ lane.markers[index] = { ...lane.markers[index]!, time };
114
+ lane.markers.sort((a, b) => a.time - b.time);
115
+ return this;
116
+ }
117
+
118
+ /** Delete a marker. */
119
+ removeMarker(trackName: string, markerId: string): this {
120
+ const lane = this.lanesMap.get(trackName);
121
+ if (!lane) return this;
122
+ const index = lane.markers.findIndex((m) => m.id === markerId);
123
+ if (index >= 0) lane.markers.splice(index, 1);
124
+ if (this.selectedId === markerId) this.selectedId = undefined;
125
+ return this;
126
+ }
127
+
128
+ select(markerId: string | undefined): this {
129
+ this.selectedId = markerId;
130
+ return this;
131
+ }
132
+
133
+ markerCount(): number {
134
+ let count = 0;
135
+ for (const lane of this.lanesMap.values()) count += lane.markers.length;
136
+ return count;
137
+ }
138
+
139
+ /** View-model for the timeline UI: lanes with marker positions + selection state. */
140
+ lanes(): readonly EventTrackLane[] {
141
+ return this.order.map((name) => {
142
+ const lane = this.lanesMap.get(name)!;
143
+ return {
144
+ name,
145
+ type: lane.type,
146
+ markers: lane.markers.map((m) => ({
147
+ id: m.id!,
148
+ time: m.time,
149
+ ...(m.duration !== undefined ? { duration: m.duration } : {}),
150
+ type: m.type ?? lane.type,
151
+ selected: m.id === this.selectedId
152
+ }))
153
+ };
154
+ });
155
+ }
156
+
157
+ /** Serialize to the canonical `animation-event-tracks/v1` shape. */
158
+ serialize(): SerializedAnimationEventTracks {
159
+ const tracks: AnimationEventTrack[] = this.order.map((name) => {
160
+ const lane = this.lanesMap.get(name)!;
161
+ return { name, type: lane.type, markers: lane.markers.map((m) => ({ ...m })) };
162
+ });
163
+ return { schema: "animation-event-tracks/v1", clipId: this.clipId, duration: this.duration, tracks };
164
+ }
165
+ }
166
+
167
+ export function createEventTrackEditor(options: EventTrackEditorOptions): EventTrackEditor {
168
+ return new EventTrackEditor(options);
169
+ }
@@ -1,6 +1,6 @@
1
1
  import { HierarchyModel, type HierarchyNodeDescriptor } from "./HierarchyModel";
2
2
  import type { SelectionId } from "./Selection";
3
- import type { CartoonSceneNode } from "./CartoonSceneEditor";
3
+ import type { AnimationSceneNode } from "./AnimationSceneEditor";
4
4
 
5
5
  export interface SceneOutlinerItem extends HierarchyNodeDescriptor {
6
6
  readonly icon: string;
@@ -16,7 +16,7 @@ export class SceneOutliner {
16
16
  private readonly hierarchy = new HierarchyModel();
17
17
  private disposers: (() => void)[] = [];
18
18
 
19
- describe(root: CartoonSceneNode, selectedIds: ReadonlySet<SelectionId> = new Set()): readonly SceneOutlinerItem[] {
19
+ describe(root: AnimationSceneNode, selectedIds: ReadonlySet<SelectionId> = new Set()): readonly SceneOutlinerItem[] {
20
20
  return this.hierarchy.flatten(root, selectedIds).map((item) => {
21
21
  const node = findNode(root, String(item.id));
22
22
  const kind = node?.kind ?? "unknown";
@@ -28,7 +28,7 @@ export class SceneOutliner {
28
28
  });
29
29
  }
30
30
 
31
- render(root: CartoonSceneNode, container: HTMLElement, options: SceneOutlinerOptions = {}): readonly SceneOutlinerItem[] {
31
+ render(root: AnimationSceneNode, container: HTMLElement, options: SceneOutlinerOptions = {}): readonly SceneOutlinerItem[] {
32
32
  this.dispose();
33
33
  const items = this.describe(root, options.selectedIds);
34
34
  container.replaceChildren();
@@ -68,7 +68,7 @@ function iconForKind(kind: string): string {
68
68
  }
69
69
  }
70
70
 
71
- function findNode(root: CartoonSceneNode, id: string): CartoonSceneNode | undefined {
71
+ function findNode(root: AnimationSceneNode, id: string): AnimationSceneNode | undefined {
72
72
  if (root.id === id) return root;
73
73
  for (const child of root.children) {
74
74
  const found = findNode(child, id);
package/src/index.ts CHANGED
@@ -1,17 +1,19 @@
1
1
  export type { Command, CommandContext } from "./Command";
2
2
  export { CommandHistory, CommandTransactionError } from "./CommandHistory";
3
3
  export { AssetDropZone, readAssetFromDataTransfer, serializeAssetForDrag } from "./AssetDropZone";
4
- export type { AssetDropPlacement, AssetDropResult, AssetDropZoneOptions, CartoonAssetCategory, CartoonEditorAssetReference } from "./AssetDropZone";
4
+ export type { AssetDropPlacement, AssetDropResult, AssetDropZoneOptions, AnimationAssetCategory, AnimationEditorAssetReference } from "./AssetDropZone";
5
5
  export { CameraPathEditor, createCameraPathEditor, readCameraPathKeyframes } from "./CameraPathEditor";
6
6
  export type { EditorCameraPathEvidence, EditorCameraPathKeyframe, EditorCameraPathSample, EditorCameraVector3 } from "./CameraPathEditor";
7
- export { CartoonSceneEditor, createCartoonSceneNode } from "./CartoonSceneEditor";
8
- export type { CartoonSceneEditorOptions, CartoonSceneEditorSnapshot, CartoonSceneEpisodeSnapshot, CartoonSceneEpisodeState, CartoonSceneNode, CartoonSceneTransform } from "./CartoonSceneEditor";
7
+ export { AnimationSceneEditor, createAnimationSceneNode } from "./AnimationSceneEditor";
8
+ export type { AnimationSceneEditorOptions, AnimationSceneEditorSnapshot, AnimationSceneEpisodeSnapshot, AnimationSceneEpisodeState, AnimationSceneNode, AnimationSceneTransform } from "./AnimationSceneEditor";
9
9
  export { CurveEditor } from "./CurveEditor";
10
10
  export type { TimelineCurveEvidence, TimelineCurvePoint, TimelineCurveSample } from "./CurveEditor";
11
11
  export { DiagnosticsOverlayModel } from "./DiagnosticsOverlayModel";
12
12
  export type { EditorDiagnosticsInput, EditorDiagnosticsResource, EditorDiagnosticsSnapshot } from "./DiagnosticsOverlayModel";
13
13
  export { EditorRuntime } from "./EditorRuntime";
14
14
  export type { EditorMode, EditorRuntimeSnapshot } from "./EditorRuntime";
15
+ export { EventTrackEditor, createEventTrackEditor } from "./EventTrackEditor";
16
+ export type { AddEventMarkerOptions, EventMarkerView, EventTrackEditorOptions, EventTrackLane } from "./EventTrackEditor";
15
17
  export { EpisodeReviewPanel, createEpisodeReviewPanel } from "./EpisodeReviewPanel";
16
18
  export type { EpisodeReviewManualVisemeEdit, EpisodeReviewNote, EpisodeReviewPanelSnapshot, EpisodeReviewPanelState, EpisodeReviewRejectedFrame, EpisodeReviewStatus, EpisodeReviewWaveformLane } from "./EpisodeReviewPanel";
17
19
  export { EditorStateModel, createMemoryEditorStateStorage } from "./EditorStateModel";
@@ -4,7 +4,7 @@ import { Ray, Vector3 } from "@aura3d/math";
4
4
  import { Scene } from "@aura3d/scene";
5
5
  import {
6
6
  CommandHistory,
7
- CartoonSceneEditor,
7
+ AnimationSceneEditor,
8
8
  CameraPathEditor,
9
9
  CurveEditor,
10
10
  CreateNodeCommand,
@@ -692,8 +692,8 @@ test("KeyframeEditor and CurveEditor author deterministic bezier keyframes on ti
692
692
  assert.equal(curve.evidence(clip, "transform.position.x").evidence.deterministicSampling, true);
693
693
  });
694
694
 
695
- test("CartoonSceneEditor, AssetDropZone, SceneOutliner, and PropertyPanel compose and edit cartoon scene nodes", async () => {
696
- const editor = new CartoonSceneEditor();
695
+ test("AnimationSceneEditor, AssetDropZone, SceneOutliner, and PropertyPanel compose and edit animation scene nodes", async () => {
696
+ const editor = new AnimationSceneEditor();
697
697
  const reviewPanel = new EpisodeReviewPanel({
698
698
  packageId: "moon-garden-001",
699
699
  packageHash: "sha256-review",
@@ -757,7 +757,7 @@ test("CartoonSceneEditor, AssetDropZone, SceneOutliner, and PropertyPanel compos
757
757
  assert.equal(node.transform.position.x, 1);
758
758
 
759
759
  const serialized = editor.serializeScene();
760
- const restored = new CartoonSceneEditor();
760
+ const restored = new AnimationSceneEditor();
761
761
  restored.loadScene(serialized);
762
762
  assert.equal(restored.snapshot().nodeCount, 2);
763
763
 
@@ -772,7 +772,7 @@ test("CartoonSceneEditor, AssetDropZone, SceneOutliner, and PropertyPanel compos
772
772
  }), /typed Aura3D asset reference/);
773
773
  });
774
774
 
775
- test("Timeline track type helpers create cartoon timeline lanes compatible with route playback", () => {
775
+ test("Timeline track type helpers create animation timeline lanes compatible with route playback", () => {
776
776
  const dialogue = createTimelineTrackConfig("dialogue", "Dialogue");
777
777
  const captions = createTimelineTrackConfig("caption", "Captions");
778
778
  const shots = createTimelineTrackConfig("shot", "Shots");