@aura3d/editor-runtime 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/Command.d.ts +11 -0
- package/dist/Command.d.ts.map +1 -0
- package/dist/Command.js +2 -0
- package/dist/Command.js.map +1 -0
- package/dist/CommandHistory.d.ts +20 -0
- package/dist/CommandHistory.d.ts.map +1 -0
- package/dist/CommandHistory.js +124 -0
- package/dist/CommandHistory.js.map +1 -0
- package/dist/DiagnosticsOverlayModel.d.ts +30 -0
- package/dist/DiagnosticsOverlayModel.d.ts.map +1 -0
- package/dist/DiagnosticsOverlayModel.js +57 -0
- package/dist/DiagnosticsOverlayModel.js.map +1 -0
- package/dist/EditorPluginHost.d.ts +46 -0
- package/dist/EditorPluginHost.d.ts.map +1 -0
- package/dist/EditorPluginHost.js +57 -0
- package/dist/EditorPluginHost.js.map +1 -0
- package/dist/EditorRuntime.d.ts +95 -0
- package/dist/EditorRuntime.d.ts.map +1 -0
- package/dist/EditorRuntime.js +277 -0
- package/dist/EditorRuntime.js.map +1 -0
- package/dist/EditorStateModel.d.ts +93 -0
- package/dist/EditorStateModel.d.ts.map +1 -0
- package/dist/EditorStateModel.js +251 -0
- package/dist/EditorStateModel.js.map +1 -0
- package/dist/Gizmo.d.ts +43 -0
- package/dist/Gizmo.d.ts.map +1 -0
- package/dist/Gizmo.js +60 -0
- package/dist/Gizmo.js.map +1 -0
- package/dist/HierarchyModel.d.ts +21 -0
- package/dist/HierarchyModel.d.ts.map +1 -0
- package/dist/HierarchyModel.js +32 -0
- package/dist/HierarchyModel.js.map +1 -0
- package/dist/InspectorModel.d.ts +16 -0
- package/dist/InspectorModel.d.ts.map +1 -0
- package/dist/InspectorModel.js +53 -0
- package/dist/InspectorModel.js.map +1 -0
- package/dist/LocalizationAccessibilityFixtures.d.ts +70 -0
- package/dist/LocalizationAccessibilityFixtures.d.ts.map +1 -0
- package/dist/LocalizationAccessibilityFixtures.js +275 -0
- package/dist/LocalizationAccessibilityFixtures.js.map +1 -0
- package/dist/MaterialVariantWorkflow.d.ts +19 -0
- package/dist/MaterialVariantWorkflow.d.ts.map +1 -0
- package/dist/MaterialVariantWorkflow.js +82 -0
- package/dist/MaterialVariantWorkflow.js.map +1 -0
- package/dist/PickingService.d.ts +58 -0
- package/dist/PickingService.d.ts.map +1 -0
- package/dist/PickingService.js +119 -0
- package/dist/PickingService.js.map +1 -0
- package/dist/PlayModeBridge.d.ts +14 -0
- package/dist/PlayModeBridge.d.ts.map +1 -0
- package/dist/PlayModeBridge.js +23 -0
- package/dist/PlayModeBridge.js.map +1 -0
- package/dist/PrefabRegistry.d.ts +41 -0
- package/dist/PrefabRegistry.d.ts.map +1 -0
- package/dist/PrefabRegistry.js +119 -0
- package/dist/PrefabRegistry.js.map +1 -0
- package/dist/ProjectSerializer.d.ts +82 -0
- package/dist/ProjectSerializer.d.ts.map +1 -0
- package/dist/ProjectSerializer.js +131 -0
- package/dist/ProjectSerializer.js.map +1 -0
- package/dist/RotateGizmo.d.ts +5 -0
- package/dist/RotateGizmo.d.ts.map +1 -0
- package/dist/RotateGizmo.js +37 -0
- package/dist/RotateGizmo.js.map +1 -0
- package/dist/ScaleGizmo.d.ts +5 -0
- package/dist/ScaleGizmo.d.ts.map +1 -0
- package/dist/ScaleGizmo.js +42 -0
- package/dist/ScaleGizmo.js.map +1 -0
- package/dist/Selection.d.ts +20 -0
- package/dist/Selection.d.ts.map +1 -0
- package/dist/Selection.js +46 -0
- package/dist/Selection.js.map +1 -0
- package/dist/ShaderGraphModel.d.ts +69 -0
- package/dist/ShaderGraphModel.d.ts.map +1 -0
- package/dist/ShaderGraphModel.js +172 -0
- package/dist/ShaderGraphModel.js.map +1 -0
- package/dist/StaticExportRuntime.d.ts +13 -0
- package/dist/StaticExportRuntime.d.ts.map +1 -0
- package/dist/StaticExportRuntime.js +307 -0
- package/dist/StaticExportRuntime.js.map +1 -0
- package/dist/TimelineModel.d.ts +175 -0
- package/dist/TimelineModel.d.ts.map +1 -0
- package/dist/TimelineModel.js +335 -0
- package/dist/TimelineModel.js.map +1 -0
- package/dist/TimelineRuntimeBridge.d.ts +98 -0
- package/dist/TimelineRuntimeBridge.d.ts.map +1 -0
- package/dist/TimelineRuntimeBridge.js +186 -0
- package/dist/TimelineRuntimeBridge.js.map +1 -0
- package/dist/TranslateGizmo.d.ts +5 -0
- package/dist/TranslateGizmo.d.ts.map +1 -0
- package/dist/TranslateGizmo.js +41 -0
- package/dist/TranslateGizmo.js.map +1 -0
- package/dist/commands/CreateNodeCommand.d.ts +14 -0
- package/dist/commands/CreateNodeCommand.d.ts.map +1 -0
- package/dist/commands/CreateNodeCommand.js +16 -0
- package/dist/commands/CreateNodeCommand.js.map +1 -0
- package/dist/commands/DeleteNodeCommand.d.ts +13 -0
- package/dist/commands/DeleteNodeCommand.d.ts.map +1 -0
- package/dist/commands/DeleteNodeCommand.js +55 -0
- package/dist/commands/DeleteNodeCommand.js.map +1 -0
- package/dist/commands/ReparentNodeCommand.d.ts +24 -0
- package/dist/commands/ReparentNodeCommand.d.ts.map +1 -0
- package/dist/commands/ReparentNodeCommand.js +39 -0
- package/dist/commands/ReparentNodeCommand.js.map +1 -0
- package/dist/commands/SetPropertyCommand.d.ts +14 -0
- package/dist/commands/SetPropertyCommand.d.ts.map +1 -0
- package/dist/commands/SetPropertyCommand.js +43 -0
- package/dist/commands/SetPropertyCommand.js.map +1 -0
- package/dist/commands/TransformCommand.d.ts +42 -0
- package/dist/commands/TransformCommand.d.ts.map +1 -0
- package/dist/commands/TransformCommand.js +72 -0
- package/dist/commands/TransformCommand.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/package.json +17 -0
- package/src/Command.ts +11 -0
- package/src/CommandHistory.ts +136 -0
- package/src/DiagnosticsOverlayModel.ts +85 -0
- package/src/EditorPluginHost.ts +106 -0
- package/src/EditorRuntime.ts +349 -0
- package/src/EditorStateModel.ts +336 -0
- package/src/Gizmo.ts +101 -0
- package/src/HierarchyModel.ts +57 -0
- package/src/InspectorModel.ts +71 -0
- package/src/LocalizationAccessibilityFixtures.ts +368 -0
- package/src/MaterialVariantWorkflow.ts +101 -0
- package/src/PickingService.ts +174 -0
- package/src/PlayModeBridge.ts +27 -0
- package/src/PrefabRegistry.ts +164 -0
- package/src/ProjectSerializer.ts +200 -0
- package/src/RotateGizmo.ts +35 -0
- package/src/ScaleGizmo.ts +40 -0
- package/src/Selection.ts +63 -0
- package/src/ShaderGraphModel.ts +254 -0
- package/src/StaticExportRuntime.ts +322 -0
- package/src/TimelineModel.ts +466 -0
- package/src/TimelineRuntimeBridge.ts +280 -0
- package/src/TranslateGizmo.ts +39 -0
- package/src/commands/CreateNodeCommand.ts +23 -0
- package/src/commands/DeleteNodeCommand.ts +74 -0
- package/src/commands/ReparentNodeCommand.ts +55 -0
- package/src/commands/SetPropertyCommand.ts +47 -0
- package/src/commands/TransformCommand.ts +107 -0
- package/src/index.ts +85 -0
- package/tests/editor-runtime.test.ts +597 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
export type TimelineLoopMode = "none" | "loop" | "pingpong";
|
|
2
|
+
export type TimelineClipBlendMode = "replace" | "additive" | "multiply" | "mix";
|
|
3
|
+
export type TimelineEasingName = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
4
|
+
|
|
5
|
+
export interface TimelineClipConfig {
|
|
6
|
+
readonly id?: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly startTime: number;
|
|
9
|
+
readonly duration: number;
|
|
10
|
+
readonly assetId?: string;
|
|
11
|
+
readonly clipName?: string;
|
|
12
|
+
readonly easeInDuration?: number;
|
|
13
|
+
readonly easeOutDuration?: number;
|
|
14
|
+
readonly easeIn?: TimelineEasingName;
|
|
15
|
+
readonly easeOut?: TimelineEasingName;
|
|
16
|
+
readonly speedMultiplier?: number;
|
|
17
|
+
readonly blendMode?: TimelineClipBlendMode;
|
|
18
|
+
readonly weight?: number;
|
|
19
|
+
readonly clipInOffset?: number;
|
|
20
|
+
readonly properties?: Record<string, string | number | boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TimelineTrackConfig {
|
|
24
|
+
readonly id?: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly type: "animation" | "selection" | "signal" | "audio" | "camera" | "generic";
|
|
27
|
+
readonly muted?: boolean;
|
|
28
|
+
readonly locked?: boolean;
|
|
29
|
+
readonly weight?: number;
|
|
30
|
+
readonly clips?: readonly TimelineClipConfig[];
|
|
31
|
+
readonly properties?: Record<string, string | number | boolean>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TimelineModelConfig {
|
|
35
|
+
readonly id?: string;
|
|
36
|
+
readonly name?: string;
|
|
37
|
+
readonly duration?: number;
|
|
38
|
+
readonly loopMode?: TimelineLoopMode;
|
|
39
|
+
readonly speed?: number;
|
|
40
|
+
readonly frameRate?: number;
|
|
41
|
+
readonly tracks?: readonly TimelineTrackConfig[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TimelineActiveClipSnapshot {
|
|
45
|
+
readonly trackId: string;
|
|
46
|
+
readonly trackName: string;
|
|
47
|
+
readonly trackType: TimelineTrackConfig["type"];
|
|
48
|
+
readonly clipId: string;
|
|
49
|
+
readonly clipName: string;
|
|
50
|
+
readonly assetId?: string;
|
|
51
|
+
readonly assetClipName?: string;
|
|
52
|
+
readonly runtimeTargetId?: string;
|
|
53
|
+
readonly localTime: number;
|
|
54
|
+
readonly assetTime: number;
|
|
55
|
+
readonly normalizedTime: number;
|
|
56
|
+
readonly blendWeight: number;
|
|
57
|
+
readonly blendMode: TimelineClipBlendMode;
|
|
58
|
+
readonly properties: Record<string, string | number | boolean>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TimelineSignalEventSnapshot {
|
|
62
|
+
readonly trackId: string;
|
|
63
|
+
readonly trackName: string;
|
|
64
|
+
readonly clipId: string;
|
|
65
|
+
readonly clipName: string;
|
|
66
|
+
readonly event: string;
|
|
67
|
+
readonly time: number;
|
|
68
|
+
readonly localTime: number;
|
|
69
|
+
readonly properties: Record<string, string | number | boolean>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TimelineTrackSnapshot {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly type: TimelineTrackConfig["type"];
|
|
76
|
+
readonly muted: boolean;
|
|
77
|
+
readonly locked: boolean;
|
|
78
|
+
readonly weight: number;
|
|
79
|
+
readonly clipCount: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface TimelineSnapshot {
|
|
83
|
+
readonly id: string;
|
|
84
|
+
readonly name: string;
|
|
85
|
+
readonly playback: "playing" | "paused";
|
|
86
|
+
readonly time: number;
|
|
87
|
+
readonly normalizedTime: number;
|
|
88
|
+
readonly duration: number;
|
|
89
|
+
readonly loopMode: TimelineLoopMode;
|
|
90
|
+
readonly speed: number;
|
|
91
|
+
readonly frameRate: number;
|
|
92
|
+
readonly direction: 1 | -1;
|
|
93
|
+
readonly trackCount: number;
|
|
94
|
+
readonly clipCount: number;
|
|
95
|
+
readonly activeClipCount: number;
|
|
96
|
+
readonly activeClips: readonly TimelineActiveClipSnapshot[];
|
|
97
|
+
readonly tracks: readonly TimelineTrackSnapshot[];
|
|
98
|
+
readonly signalEvents: readonly string[];
|
|
99
|
+
readonly signalEventDetails: readonly TimelineSignalEventSnapshot[];
|
|
100
|
+
readonly evidence: {
|
|
101
|
+
readonly oldCodebasePort: true;
|
|
102
|
+
readonly boundedTimelineAuthoring: true;
|
|
103
|
+
readonly clipEasing: boolean;
|
|
104
|
+
readonly clipBlending: boolean;
|
|
105
|
+
readonly muteLockState: boolean;
|
|
106
|
+
readonly loopPlayback: boolean;
|
|
107
|
+
readonly signalMarkers: boolean;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const easing = {
|
|
112
|
+
linear: (value: number): number => value,
|
|
113
|
+
"ease-in": (value: number): number => value * value,
|
|
114
|
+
"ease-out": (value: number): number => value * (2 - value),
|
|
115
|
+
"ease-in-out": (value: number): number => value < 0.5 ? 2 * value * value : -1 + (4 - 2 * value) * value
|
|
116
|
+
} satisfies Record<TimelineEasingName, (value: number) => number>;
|
|
117
|
+
|
|
118
|
+
export class TimelineClip {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
name: string;
|
|
121
|
+
startTime: number;
|
|
122
|
+
duration: number;
|
|
123
|
+
assetId?: string;
|
|
124
|
+
clipName?: string;
|
|
125
|
+
easeInDuration: number;
|
|
126
|
+
easeOutDuration: number;
|
|
127
|
+
easeIn: TimelineEasingName;
|
|
128
|
+
easeOut: TimelineEasingName;
|
|
129
|
+
speedMultiplier: number;
|
|
130
|
+
blendMode: TimelineClipBlendMode;
|
|
131
|
+
weight: number;
|
|
132
|
+
clipInOffset: number;
|
|
133
|
+
readonly properties: Record<string, string | number | boolean>;
|
|
134
|
+
enabled = true;
|
|
135
|
+
|
|
136
|
+
constructor(config: TimelineClipConfig) {
|
|
137
|
+
if (!Number.isFinite(config.startTime) || config.startTime < 0) throw new RangeError("Timeline clip startTime must be a finite non-negative number.");
|
|
138
|
+
if (!Number.isFinite(config.duration) || config.duration <= 0) throw new RangeError("Timeline clip duration must be a finite positive number.");
|
|
139
|
+
this.id = config.id ?? stableId("clip", config.name);
|
|
140
|
+
this.name = config.name;
|
|
141
|
+
this.startTime = config.startTime;
|
|
142
|
+
this.duration = config.duration;
|
|
143
|
+
this.assetId = config.assetId;
|
|
144
|
+
this.clipName = config.clipName;
|
|
145
|
+
this.easeInDuration = Math.max(0, config.easeInDuration ?? 0);
|
|
146
|
+
this.easeOutDuration = Math.max(0, config.easeOutDuration ?? 0);
|
|
147
|
+
this.easeIn = config.easeIn ?? "linear";
|
|
148
|
+
this.easeOut = config.easeOut ?? "linear";
|
|
149
|
+
this.speedMultiplier = Math.max(0, config.speedMultiplier ?? 1);
|
|
150
|
+
this.blendMode = config.blendMode ?? "replace";
|
|
151
|
+
this.weight = clamp(config.weight ?? 1, 0, 1);
|
|
152
|
+
this.clipInOffset = Math.max(0, config.clipInOffset ?? 0);
|
|
153
|
+
this.properties = { ...(config.properties ?? {}) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get endTime(): number {
|
|
157
|
+
return this.startTime + this.duration;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
contains(time: number): boolean {
|
|
161
|
+
return this.enabled && time >= this.startTime && time < this.endTime;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
localTime(time: number): number {
|
|
165
|
+
if (!this.contains(time)) return -1;
|
|
166
|
+
return (time - this.startTime) * this.speedMultiplier;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
assetTime(time: number): number {
|
|
170
|
+
const localTime = this.localTime(time);
|
|
171
|
+
return localTime < 0 ? -1 : this.clipInOffset + localTime;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
normalizedTime(time: number): number {
|
|
175
|
+
const localTime = this.localTime(time);
|
|
176
|
+
return localTime < 0 ? -1 : clamp(localTime / this.duration, 0, 1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
blendWeight(time: number): number {
|
|
180
|
+
if (!this.contains(time)) return 0;
|
|
181
|
+
const localTime = time - this.startTime;
|
|
182
|
+
let result = this.weight;
|
|
183
|
+
if (this.easeInDuration > 0 && localTime < this.easeInDuration) {
|
|
184
|
+
result *= easing[this.easeIn](clamp(localTime / this.easeInDuration, 0, 1));
|
|
185
|
+
}
|
|
186
|
+
const timeFromEnd = this.duration - localTime;
|
|
187
|
+
if (this.easeOutDuration > 0 && timeFromEnd < this.easeOutDuration) {
|
|
188
|
+
result *= easing[this.easeOut](clamp(timeFromEnd / this.easeOutDuration, 0, 1));
|
|
189
|
+
}
|
|
190
|
+
return Number(result.toFixed(4));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
toConfig(): TimelineClipConfig {
|
|
194
|
+
return {
|
|
195
|
+
id: this.id,
|
|
196
|
+
name: this.name,
|
|
197
|
+
startTime: this.startTime,
|
|
198
|
+
duration: this.duration,
|
|
199
|
+
assetId: this.assetId,
|
|
200
|
+
clipName: this.clipName,
|
|
201
|
+
easeInDuration: this.easeInDuration,
|
|
202
|
+
easeOutDuration: this.easeOutDuration,
|
|
203
|
+
easeIn: this.easeIn,
|
|
204
|
+
easeOut: this.easeOut,
|
|
205
|
+
speedMultiplier: this.speedMultiplier,
|
|
206
|
+
blendMode: this.blendMode,
|
|
207
|
+
weight: this.weight,
|
|
208
|
+
clipInOffset: this.clipInOffset,
|
|
209
|
+
properties: { ...this.properties }
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export class TimelineTrack {
|
|
215
|
+
readonly id: string;
|
|
216
|
+
name: string;
|
|
217
|
+
readonly type: TimelineTrackConfig["type"];
|
|
218
|
+
muted: boolean;
|
|
219
|
+
locked: boolean;
|
|
220
|
+
weight: number;
|
|
221
|
+
readonly properties: Record<string, string | number | boolean>;
|
|
222
|
+
private readonly clipsInternal: TimelineClip[] = [];
|
|
223
|
+
|
|
224
|
+
constructor(config: TimelineTrackConfig) {
|
|
225
|
+
this.id = config.id ?? stableId("track", config.name);
|
|
226
|
+
this.name = config.name;
|
|
227
|
+
this.type = config.type;
|
|
228
|
+
this.muted = config.muted ?? false;
|
|
229
|
+
this.locked = config.locked ?? false;
|
|
230
|
+
this.weight = clamp(config.weight ?? 1, 0, 1);
|
|
231
|
+
this.properties = { ...(config.properties ?? {}) };
|
|
232
|
+
for (const clip of config.clips ?? []) {
|
|
233
|
+
this.addClip(new TimelineClip(clip), { ignoreLock: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
get clips(): readonly TimelineClip[] {
|
|
238
|
+
return this.clipsInternal;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
addClip(clip: TimelineClip, options: { readonly ignoreLock?: boolean } = {}): TimelineClip {
|
|
242
|
+
if (this.locked && !options.ignoreLock) throw new Error(`Cannot add clip to locked timeline track: ${this.name}`);
|
|
243
|
+
this.clipsInternal.push(clip);
|
|
244
|
+
this.clipsInternal.sort((left, right) => left.startTime - right.startTime);
|
|
245
|
+
return clip;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
activeClips(time: number): readonly TimelineClip[] {
|
|
249
|
+
if (this.muted || this.weight <= 0) return [];
|
|
250
|
+
return this.clipsInternal.filter((clip) => clip.contains(time));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
snapshot(): TimelineTrackSnapshot {
|
|
254
|
+
return {
|
|
255
|
+
id: this.id,
|
|
256
|
+
name: this.name,
|
|
257
|
+
type: this.type,
|
|
258
|
+
muted: this.muted,
|
|
259
|
+
locked: this.locked,
|
|
260
|
+
weight: this.weight,
|
|
261
|
+
clipCount: this.clipsInternal.length
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
toConfig(): TimelineTrackConfig {
|
|
266
|
+
return {
|
|
267
|
+
id: this.id,
|
|
268
|
+
name: this.name,
|
|
269
|
+
type: this.type,
|
|
270
|
+
muted: this.muted,
|
|
271
|
+
locked: this.locked,
|
|
272
|
+
weight: this.weight,
|
|
273
|
+
clips: this.clipsInternal.map((clip) => clip.toConfig()),
|
|
274
|
+
properties: { ...this.properties }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export class TimelineModel {
|
|
280
|
+
readonly id: string;
|
|
281
|
+
name: string;
|
|
282
|
+
duration: number;
|
|
283
|
+
loopMode: TimelineLoopMode;
|
|
284
|
+
speed: number;
|
|
285
|
+
frameRate: number;
|
|
286
|
+
private readonly tracksInternal: TimelineTrack[] = [];
|
|
287
|
+
private playing = false;
|
|
288
|
+
private timeInternal = 0;
|
|
289
|
+
private directionInternal: 1 | -1 = 1;
|
|
290
|
+
private readonly emittedSignals = new Set<string>();
|
|
291
|
+
|
|
292
|
+
constructor(config: TimelineModelConfig = {}) {
|
|
293
|
+
this.id = config.id ?? "editor-timeline";
|
|
294
|
+
this.name = config.name ?? "Editor Timeline";
|
|
295
|
+
this.duration = Math.max(0.01, config.duration ?? 1);
|
|
296
|
+
this.loopMode = config.loopMode ?? "loop";
|
|
297
|
+
this.speed = Math.max(0, config.speed ?? 1);
|
|
298
|
+
this.frameRate = Math.max(0, config.frameRate ?? 0);
|
|
299
|
+
for (const track of config.tracks ?? []) {
|
|
300
|
+
this.addTrack(new TimelineTrack(track));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
get tracks(): readonly TimelineTrack[] {
|
|
305
|
+
return this.tracksInternal;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
get currentTime(): number {
|
|
309
|
+
return this.timeInternal;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
get isPlaying(): boolean {
|
|
313
|
+
return this.playing;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
addTrack(track: TimelineTrack): TimelineTrack {
|
|
317
|
+
this.tracksInternal.push(track);
|
|
318
|
+
return track;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
clearTracks(): void {
|
|
322
|
+
this.tracksInternal.length = 0;
|
|
323
|
+
this.emittedSignals.clear();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
play(): void {
|
|
327
|
+
this.playing = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
pause(): void {
|
|
331
|
+
this.playing = false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
stop(): void {
|
|
335
|
+
this.playing = false;
|
|
336
|
+
this.seek(0);
|
|
337
|
+
this.directionInternal = 1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
seek(time: number): void {
|
|
341
|
+
this.timeInternal = this.wrapTime(time);
|
|
342
|
+
this.emittedSignals.clear();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
setNormalizedTime(value: number): void {
|
|
346
|
+
this.seek(clamp(value, 0, 1) * this.duration);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
tick(deltaSeconds: number): void {
|
|
350
|
+
if (!this.playing || deltaSeconds <= 0 || this.speed <= 0) return;
|
|
351
|
+
this.seek(this.timeInternal + deltaSeconds * this.speed * this.directionInternal);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
snapshot(): TimelineSnapshot {
|
|
355
|
+
const activeClips = this.tracksInternal.flatMap((track) => track.activeClips(this.timeInternal).map((clip) => ({
|
|
356
|
+
trackId: track.id,
|
|
357
|
+
trackName: track.name,
|
|
358
|
+
trackType: track.type,
|
|
359
|
+
clipId: clip.id,
|
|
360
|
+
clipName: clip.name,
|
|
361
|
+
assetId: clip.assetId,
|
|
362
|
+
assetClipName: clip.clipName ?? clip.name,
|
|
363
|
+
runtimeTargetId: runtimeTargetIdFromProperties(clip.properties),
|
|
364
|
+
localTime: Number(clip.localTime(this.timeInternal).toFixed(4)),
|
|
365
|
+
assetTime: Number(clip.assetTime(this.timeInternal).toFixed(4)),
|
|
366
|
+
normalizedTime: Number(clip.normalizedTime(this.timeInternal).toFixed(4)),
|
|
367
|
+
blendWeight: Number((clip.blendWeight(this.timeInternal) * track.weight).toFixed(4)),
|
|
368
|
+
blendMode: clip.blendMode,
|
|
369
|
+
properties: { ...clip.properties }
|
|
370
|
+
})));
|
|
371
|
+
const signalEventDetails = this.tracksInternal.flatMap((track) => track.type === "signal"
|
|
372
|
+
? track.activeClips(this.timeInternal).map((clip) => ({
|
|
373
|
+
trackId: track.id,
|
|
374
|
+
trackName: track.name,
|
|
375
|
+
clipId: clip.id,
|
|
376
|
+
clipName: clip.name,
|
|
377
|
+
event: typeof clip.properties.event === "string" ? clip.properties.event : clip.clipName ?? clip.id,
|
|
378
|
+
time: Number(this.timeInternal.toFixed(4)),
|
|
379
|
+
localTime: Number(clip.localTime(this.timeInternal).toFixed(4)),
|
|
380
|
+
properties: { ...clip.properties }
|
|
381
|
+
}))
|
|
382
|
+
: []);
|
|
383
|
+
const signalEvents = signalEventDetails.map((event) => event.event);
|
|
384
|
+
for (const signal of signalEvents) this.emittedSignals.add(signal);
|
|
385
|
+
const clips = this.tracksInternal.flatMap((track) => track.clips);
|
|
386
|
+
return {
|
|
387
|
+
id: this.id,
|
|
388
|
+
name: this.name,
|
|
389
|
+
playback: this.playing ? "playing" : "paused",
|
|
390
|
+
time: Number(this.timeInternal.toFixed(4)),
|
|
391
|
+
normalizedTime: Number((this.timeInternal / this.duration).toFixed(4)),
|
|
392
|
+
duration: this.duration,
|
|
393
|
+
loopMode: this.loopMode,
|
|
394
|
+
speed: this.speed,
|
|
395
|
+
frameRate: this.frameRate,
|
|
396
|
+
direction: this.directionInternal,
|
|
397
|
+
trackCount: this.tracksInternal.length,
|
|
398
|
+
clipCount: clips.length,
|
|
399
|
+
activeClipCount: activeClips.length,
|
|
400
|
+
activeClips,
|
|
401
|
+
tracks: this.tracksInternal.map((track) => track.snapshot()),
|
|
402
|
+
signalEvents: [...this.emittedSignals].sort(),
|
|
403
|
+
signalEventDetails,
|
|
404
|
+
evidence: {
|
|
405
|
+
oldCodebasePort: true,
|
|
406
|
+
boundedTimelineAuthoring: true,
|
|
407
|
+
clipEasing: clips.some((clip) => clip.easeIn !== "linear" || clip.easeOut !== "linear" || clip.easeInDuration > 0 || clip.easeOutDuration > 0),
|
|
408
|
+
clipBlending: clips.some((clip) => clip.blendMode !== "replace" || clip.weight < 1),
|
|
409
|
+
muteLockState: this.tracksInternal.some((track) => track.muted || track.locked),
|
|
410
|
+
loopPlayback: this.loopMode !== "none",
|
|
411
|
+
signalMarkers: this.tracksInternal.some((track) => track.type === "signal" && track.clips.length > 0)
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
toConfig(): TimelineModelConfig {
|
|
417
|
+
return {
|
|
418
|
+
id: this.id,
|
|
419
|
+
name: this.name,
|
|
420
|
+
duration: this.duration,
|
|
421
|
+
loopMode: this.loopMode,
|
|
422
|
+
speed: this.speed,
|
|
423
|
+
frameRate: this.frameRate,
|
|
424
|
+
tracks: this.tracksInternal.map((track) => track.toConfig())
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private wrapTime(value: number): number {
|
|
429
|
+
if (!Number.isFinite(value)) return this.timeInternal;
|
|
430
|
+
if (value < 0) {
|
|
431
|
+
if (this.loopMode === "loop") return modulo(value, this.duration);
|
|
432
|
+
if (this.loopMode === "pingpong") {
|
|
433
|
+
this.directionInternal = 1;
|
|
434
|
+
return Math.abs(value);
|
|
435
|
+
}
|
|
436
|
+
this.playing = false;
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
if (value <= this.duration) return value;
|
|
440
|
+
if (this.loopMode === "loop") return modulo(value, this.duration);
|
|
441
|
+
if (this.loopMode === "pingpong") {
|
|
442
|
+
this.directionInternal = -1;
|
|
443
|
+
return Math.max(0, this.duration - (value - this.duration));
|
|
444
|
+
}
|
|
445
|
+
this.playing = false;
|
|
446
|
+
return this.duration;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function stableId(prefix: string, name: string): string {
|
|
451
|
+
return `${prefix}-${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "item"}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function clamp(value: number, min: number, max: number): number {
|
|
455
|
+
return Math.max(min, Math.min(max, value));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function modulo(value: number, divisor: number): number {
|
|
459
|
+
return ((value % divisor) + divisor) % divisor;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function runtimeTargetIdFromProperties(properties: Record<string, string | number | boolean>): string | undefined {
|
|
463
|
+
if (typeof properties.runtimeNodeId === "string" && properties.runtimeNodeId.trim().length > 0) return properties.runtimeNodeId;
|
|
464
|
+
if (typeof properties.targetId === "string" && properties.targetId.trim().length > 0) return properties.targetId;
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|