@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,280 @@
|
|
|
1
|
+
import { TimelineModel } from "./TimelineModel";
|
|
2
|
+
import type {
|
|
3
|
+
TimelineActiveClipSnapshot,
|
|
4
|
+
TimelineClipBlendMode,
|
|
5
|
+
TimelineSignalEventSnapshot,
|
|
6
|
+
TimelineSnapshot
|
|
7
|
+
} from "./TimelineModel";
|
|
8
|
+
|
|
9
|
+
export interface TimelineRuntimeBindingConfig {
|
|
10
|
+
readonly trackId?: string;
|
|
11
|
+
readonly trackName?: string;
|
|
12
|
+
readonly targetId: string;
|
|
13
|
+
readonly assetId?: string;
|
|
14
|
+
readonly clipNameMap?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TimelineRuntimeAnimationApplication {
|
|
18
|
+
readonly targetId: string;
|
|
19
|
+
readonly trackId: string;
|
|
20
|
+
readonly trackName: string;
|
|
21
|
+
readonly clipId: string;
|
|
22
|
+
readonly clipName: string;
|
|
23
|
+
readonly assetId?: string;
|
|
24
|
+
readonly localTime: number;
|
|
25
|
+
readonly assetTime: number;
|
|
26
|
+
readonly normalizedTime: number;
|
|
27
|
+
readonly blendWeight: number;
|
|
28
|
+
readonly blendMode: TimelineClipBlendMode;
|
|
29
|
+
readonly playback: TimelineSnapshot["playback"];
|
|
30
|
+
readonly properties: Record<string, string | number | boolean>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TimelineRuntimeSignalDispatch {
|
|
34
|
+
readonly targetId?: string;
|
|
35
|
+
readonly trackId: string;
|
|
36
|
+
readonly trackName: string;
|
|
37
|
+
readonly clipId: string;
|
|
38
|
+
readonly clipName: string;
|
|
39
|
+
readonly event: string;
|
|
40
|
+
readonly time: number;
|
|
41
|
+
readonly localTime: number;
|
|
42
|
+
readonly properties: Record<string, string | number | boolean>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TimelineRuntimeTarget {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
applyTimelineAnimation?(application: TimelineRuntimeAnimationApplication): void;
|
|
48
|
+
applyTimelineSignal?(dispatch: TimelineRuntimeSignalDispatch): void;
|
|
49
|
+
snapshot?(): unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TimelineRuntimeBridgeConfig {
|
|
53
|
+
readonly timeline: TimelineModel;
|
|
54
|
+
readonly targets: readonly TimelineRuntimeTarget[];
|
|
55
|
+
readonly bindings?: readonly TimelineRuntimeBindingConfig[];
|
|
56
|
+
readonly strictBindings?: boolean;
|
|
57
|
+
readonly dispatchSignals?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TimelineRuntimeTargetSnapshot {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly appliedAnimationCount: number;
|
|
63
|
+
readonly dispatchedSignalCount: number;
|
|
64
|
+
readonly snapshot?: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TimelineRuntimeBridgeSnapshot {
|
|
68
|
+
readonly kind: "aura-editor-timeline-runtime-bridge";
|
|
69
|
+
readonly timeline: TimelineSnapshot;
|
|
70
|
+
readonly appliedAnimationCount: number;
|
|
71
|
+
readonly dispatchedSignalCount: number;
|
|
72
|
+
readonly warnings: readonly string[];
|
|
73
|
+
readonly targets: readonly TimelineRuntimeTargetSnapshot[];
|
|
74
|
+
readonly lastApplications: readonly TimelineRuntimeAnimationApplication[];
|
|
75
|
+
readonly lastSignals: readonly TimelineRuntimeSignalDispatch[];
|
|
76
|
+
readonly evidence: {
|
|
77
|
+
readonly timelineToRuntimeBridge: true;
|
|
78
|
+
readonly deterministicApplyAt: true;
|
|
79
|
+
readonly animationClipBinding: boolean;
|
|
80
|
+
readonly signalDispatch: boolean;
|
|
81
|
+
readonly runtimeTargetSnapshots: boolean;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface TargetCounters {
|
|
86
|
+
appliedAnimationCount: number;
|
|
87
|
+
dispatchedSignalCount: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class TimelineRuntimeBridge {
|
|
91
|
+
private readonly timeline: TimelineModel;
|
|
92
|
+
private readonly targets = new Map<string, TimelineRuntimeTarget>();
|
|
93
|
+
private readonly counters = new Map<string, TargetCounters>();
|
|
94
|
+
private readonly bindings: readonly TimelineRuntimeBindingConfig[];
|
|
95
|
+
private readonly strictBindings: boolean;
|
|
96
|
+
private readonly dispatchSignals: boolean;
|
|
97
|
+
private readonly seenSignalKeys = new Set<string>();
|
|
98
|
+
private lastTimelineTime = 0;
|
|
99
|
+
private lastSnapshot: TimelineRuntimeBridgeSnapshot;
|
|
100
|
+
|
|
101
|
+
constructor(config: TimelineRuntimeBridgeConfig) {
|
|
102
|
+
this.timeline = config.timeline;
|
|
103
|
+
this.bindings = config.bindings ?? [];
|
|
104
|
+
this.strictBindings = config.strictBindings ?? false;
|
|
105
|
+
this.dispatchSignals = config.dispatchSignals ?? true;
|
|
106
|
+
for (const target of config.targets) {
|
|
107
|
+
const id = target.id.trim();
|
|
108
|
+
if (!id) throw new Error("Timeline runtime target id is required.");
|
|
109
|
+
if (this.targets.has(id)) throw new Error(`Duplicate timeline runtime target id: ${id}`);
|
|
110
|
+
this.targets.set(id, target);
|
|
111
|
+
this.counters.set(id, { appliedAnimationCount: 0, dispatchedSignalCount: 0 });
|
|
112
|
+
}
|
|
113
|
+
this.lastSnapshot = this.createBridgeSnapshot(this.timeline.snapshot(), [], [], []);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
step(deltaSeconds: number): TimelineRuntimeBridgeSnapshot {
|
|
117
|
+
this.timeline.tick(deltaSeconds);
|
|
118
|
+
return this.applyCurrent();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
applyAt(time: number, options: { readonly replaySignals?: boolean } = {}): TimelineRuntimeBridgeSnapshot {
|
|
122
|
+
this.timeline.seek(time);
|
|
123
|
+
if (options.replaySignals) this.resetSignalCursor();
|
|
124
|
+
return this.applyCurrent();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
applyCurrent(): TimelineRuntimeBridgeSnapshot {
|
|
128
|
+
const timelineSnapshot = this.timeline.snapshot();
|
|
129
|
+
if (timelineSnapshot.time < this.lastTimelineTime) {
|
|
130
|
+
this.resetSignalCursor();
|
|
131
|
+
}
|
|
132
|
+
this.lastTimelineTime = timelineSnapshot.time;
|
|
133
|
+
|
|
134
|
+
const warnings: string[] = [];
|
|
135
|
+
const applications = this.applyAnimations(timelineSnapshot, warnings);
|
|
136
|
+
const signals = this.dispatchSignals ? this.applySignals(timelineSnapshot, warnings) : [];
|
|
137
|
+
this.lastSnapshot = this.createBridgeSnapshot(timelineSnapshot, applications, signals, warnings);
|
|
138
|
+
return this.lastSnapshot;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
resetSignalCursor(): void {
|
|
142
|
+
this.seenSignalKeys.clear();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
snapshot(): TimelineRuntimeBridgeSnapshot {
|
|
146
|
+
return this.lastSnapshot;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private applyAnimations(snapshot: TimelineSnapshot, warnings: string[]): TimelineRuntimeAnimationApplication[] {
|
|
150
|
+
const applications: TimelineRuntimeAnimationApplication[] = [];
|
|
151
|
+
for (const clip of snapshot.activeClips) {
|
|
152
|
+
if (clip.trackType !== "animation") continue;
|
|
153
|
+
const binding = this.resolveBinding(clip);
|
|
154
|
+
const targetId = clip.runtimeTargetId ?? binding?.targetId;
|
|
155
|
+
if (!targetId) {
|
|
156
|
+
this.warnOrThrow(`No runtime target binding for animation track ${clip.trackId}.`, warnings);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const target = this.targets.get(targetId);
|
|
160
|
+
if (!target) {
|
|
161
|
+
this.warnOrThrow(`Runtime target is not registered for timeline binding: ${targetId}`, warnings);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const clipName = binding?.clipNameMap?.[clip.assetClipName ?? clip.clipName] ?? binding?.clipNameMap?.[clip.clipName] ?? clip.assetClipName ?? clip.clipName;
|
|
165
|
+
const application: TimelineRuntimeAnimationApplication = {
|
|
166
|
+
targetId,
|
|
167
|
+
trackId: clip.trackId,
|
|
168
|
+
trackName: clip.trackName,
|
|
169
|
+
clipId: clip.clipId,
|
|
170
|
+
clipName,
|
|
171
|
+
assetId: clip.assetId ?? binding?.assetId,
|
|
172
|
+
localTime: clip.localTime,
|
|
173
|
+
assetTime: clip.assetTime,
|
|
174
|
+
normalizedTime: clip.normalizedTime,
|
|
175
|
+
blendWeight: clip.blendWeight,
|
|
176
|
+
blendMode: clip.blendMode,
|
|
177
|
+
playback: snapshot.playback,
|
|
178
|
+
properties: { ...clip.properties }
|
|
179
|
+
};
|
|
180
|
+
target.applyTimelineAnimation?.(application);
|
|
181
|
+
this.counters.get(targetId)!.appliedAnimationCount += 1;
|
|
182
|
+
applications.push(application);
|
|
183
|
+
}
|
|
184
|
+
return applications;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private applySignals(snapshot: TimelineSnapshot, warnings: string[]): TimelineRuntimeSignalDispatch[] {
|
|
188
|
+
const signals: TimelineRuntimeSignalDispatch[] = [];
|
|
189
|
+
for (const event of snapshot.signalEventDetails) {
|
|
190
|
+
const key = `${event.trackId}:${event.clipId}:${event.event}`;
|
|
191
|
+
if (this.seenSignalKeys.has(key)) continue;
|
|
192
|
+
this.seenSignalKeys.add(key);
|
|
193
|
+
const targetId = this.resolveSignalTargetId(event);
|
|
194
|
+
const dispatch: TimelineRuntimeSignalDispatch = {
|
|
195
|
+
targetId,
|
|
196
|
+
trackId: event.trackId,
|
|
197
|
+
trackName: event.trackName,
|
|
198
|
+
clipId: event.clipId,
|
|
199
|
+
clipName: event.clipName,
|
|
200
|
+
event: event.event,
|
|
201
|
+
time: event.time,
|
|
202
|
+
localTime: event.localTime,
|
|
203
|
+
properties: { ...event.properties }
|
|
204
|
+
};
|
|
205
|
+
if (targetId) {
|
|
206
|
+
const target = this.targets.get(targetId);
|
|
207
|
+
if (!target) {
|
|
208
|
+
this.warnOrThrow(`Runtime target is not registered for timeline signal: ${targetId}`, warnings);
|
|
209
|
+
signals.push(dispatch);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
target.applyTimelineSignal?.(dispatch);
|
|
213
|
+
this.counters.get(targetId)!.dispatchedSignalCount += 1;
|
|
214
|
+
}
|
|
215
|
+
signals.push(dispatch);
|
|
216
|
+
}
|
|
217
|
+
return signals;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private resolveBinding(clip: TimelineActiveClipSnapshot): TimelineRuntimeBindingConfig | undefined {
|
|
221
|
+
return this.bindings.find((binding) => {
|
|
222
|
+
if (binding.trackId && binding.trackId === clip.trackId) return true;
|
|
223
|
+
if (binding.trackName && binding.trackName === clip.trackName) return true;
|
|
224
|
+
return false;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private resolveSignalTargetId(event: TimelineSignalEventSnapshot): string | undefined {
|
|
229
|
+
if (typeof event.properties.runtimeNodeId === "string" && event.properties.runtimeNodeId.trim().length > 0) return event.properties.runtimeNodeId;
|
|
230
|
+
if (typeof event.properties.targetId === "string" && event.properties.targetId.trim().length > 0) return event.properties.targetId;
|
|
231
|
+
const binding = this.bindings.find((candidate) => {
|
|
232
|
+
if (candidate.trackId && candidate.trackId === event.trackId) return true;
|
|
233
|
+
if (candidate.trackName && candidate.trackName === event.trackName) return true;
|
|
234
|
+
return false;
|
|
235
|
+
});
|
|
236
|
+
return binding?.targetId;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private createBridgeSnapshot(
|
|
240
|
+
timeline: TimelineSnapshot,
|
|
241
|
+
applications: readonly TimelineRuntimeAnimationApplication[],
|
|
242
|
+
signals: readonly TimelineRuntimeSignalDispatch[],
|
|
243
|
+
warnings: readonly string[]
|
|
244
|
+
): TimelineRuntimeBridgeSnapshot {
|
|
245
|
+
return {
|
|
246
|
+
kind: "aura-editor-timeline-runtime-bridge",
|
|
247
|
+
timeline,
|
|
248
|
+
appliedAnimationCount: [...this.counters.values()].reduce((total, counter) => total + counter.appliedAnimationCount, 0),
|
|
249
|
+
dispatchedSignalCount: [...this.counters.values()].reduce((total, counter) => total + counter.dispatchedSignalCount, 0),
|
|
250
|
+
warnings,
|
|
251
|
+
targets: [...this.targets.values()].map((target) => {
|
|
252
|
+
const counter = this.counters.get(target.id)!;
|
|
253
|
+
return {
|
|
254
|
+
id: target.id,
|
|
255
|
+
appliedAnimationCount: counter.appliedAnimationCount,
|
|
256
|
+
dispatchedSignalCount: counter.dispatchedSignalCount,
|
|
257
|
+
snapshot: target.snapshot?.()
|
|
258
|
+
};
|
|
259
|
+
}),
|
|
260
|
+
lastApplications: applications,
|
|
261
|
+
lastSignals: signals,
|
|
262
|
+
evidence: {
|
|
263
|
+
timelineToRuntimeBridge: true,
|
|
264
|
+
deterministicApplyAt: true,
|
|
265
|
+
animationClipBinding: applications.length > 0,
|
|
266
|
+
signalDispatch: signals.length > 0,
|
|
267
|
+
runtimeTargetSnapshots: [...this.targets.values()].some((target) => typeof target.snapshot === "function")
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private warnOrThrow(message: string, warnings: string[]): void {
|
|
273
|
+
if (this.strictBindings) throw new Error(message);
|
|
274
|
+
warnings.push(message);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function createTimelineRuntimeBridge(config: TimelineRuntimeBridgeConfig): TimelineRuntimeBridge {
|
|
279
|
+
return new TimelineRuntimeBridge(config);
|
|
280
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Gizmo, type GizmoDrag } from "./Gizmo";
|
|
2
|
+
import { TransformCommand, type TransformLike } from "./commands/TransformCommand";
|
|
3
|
+
|
|
4
|
+
export class TranslateGizmo extends Gizmo {
|
|
5
|
+
async drag(input: GizmoDrag): Promise<void> {
|
|
6
|
+
if (!this.target) return;
|
|
7
|
+
const current = readTransform(this.target);
|
|
8
|
+
const next = clone(current);
|
|
9
|
+
const delta = this.snapPositionDelta(input.delta);
|
|
10
|
+
if (input.axis === "x" || input.axis === "xy" || input.axis === "xz") next.position.x += delta;
|
|
11
|
+
if (input.axis === "y" || input.axis === "xy" || input.axis === "yz") next.position.y += delta;
|
|
12
|
+
if (input.axis === "z" || input.axis === "xz" || input.axis === "yz") next.position.z += delta;
|
|
13
|
+
if (input.axis === "uniform") {
|
|
14
|
+
next.position.x += delta;
|
|
15
|
+
next.position.y += delta;
|
|
16
|
+
next.position.z += delta;
|
|
17
|
+
}
|
|
18
|
+
await this.history.execute(new TransformCommand(this.target, next));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readTransform(target: NonNullable<TranslateGizmo["target"]>): TransformLike {
|
|
23
|
+
if ("transform" in target) {
|
|
24
|
+
return {
|
|
25
|
+
position: { x: target.transform.position[0], y: target.transform.position[1], z: target.transform.position[2] },
|
|
26
|
+
rotation: { x: target.transform.rotation[0], y: target.transform.rotation[1], z: target.transform.rotation[2], w: target.transform.rotation[3] },
|
|
27
|
+
scale: { x: target.transform.scale[0], y: target.transform.scale[1], z: target.transform.scale[2] }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return clone(target);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clone(transform: TransformLike): TransformLike {
|
|
34
|
+
return {
|
|
35
|
+
position: { ...transform.position },
|
|
36
|
+
rotation: transform.rotation ? { ...transform.rotation } : undefined,
|
|
37
|
+
scale: transform.scale ? { ...transform.scale } : undefined
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Command } from "../Command";
|
|
2
|
+
|
|
3
|
+
export interface NodeContainer<TNode> {
|
|
4
|
+
add(node: TNode): void;
|
|
5
|
+
remove(node: TNode): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class CreateNodeCommand<TNode> implements Command {
|
|
9
|
+
readonly name = "Create Node";
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly container: NodeContainer<TNode>,
|
|
13
|
+
private readonly node: TNode
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
execute(): void {
|
|
17
|
+
this.container.add(this.node);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
undo(): void {
|
|
21
|
+
this.container.remove(this.node);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Command } from "../Command";
|
|
2
|
+
import type { NodeContainer } from "./CreateNodeCommand";
|
|
3
|
+
|
|
4
|
+
interface SceneNodeLike {
|
|
5
|
+
readonly children: SceneNodeLike[];
|
|
6
|
+
parent: SceneNodeLike | null;
|
|
7
|
+
addChild(child: SceneNodeLike): unknown;
|
|
8
|
+
removeChild(child: SceneNodeLike): boolean;
|
|
9
|
+
removeFromParent(): boolean;
|
|
10
|
+
markWorldDirty?(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class DeleteNodeCommand<TNode> implements Command {
|
|
14
|
+
readonly name = "Delete Node";
|
|
15
|
+
private sceneParent: SceneNodeLike | null = null;
|
|
16
|
+
private sceneIndex = -1;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly container: NodeContainer<TNode>,
|
|
20
|
+
private readonly node: TNode
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
execute(): void {
|
|
24
|
+
const sceneNode = asSceneNode(this.node);
|
|
25
|
+
if (sceneNode) {
|
|
26
|
+
this.sceneParent = sceneNode.parent;
|
|
27
|
+
this.sceneIndex = this.sceneParent?.children.indexOf(sceneNode) ?? -1;
|
|
28
|
+
if (this.sceneParent && sceneNode.removeFromParent()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.sceneParent = null;
|
|
32
|
+
this.sceneIndex = -1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.container.remove(this.node);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
undo(): void {
|
|
39
|
+
const sceneNode = asSceneNode(this.node);
|
|
40
|
+
if (sceneNode && this.sceneParent) {
|
|
41
|
+
this.sceneParent.addChild(sceneNode);
|
|
42
|
+
if (this.sceneIndex >= 0) {
|
|
43
|
+
const currentIndex = this.sceneParent.children.indexOf(sceneNode);
|
|
44
|
+
if (currentIndex >= 0 && currentIndex !== this.sceneIndex) {
|
|
45
|
+
this.sceneParent.children.splice(currentIndex, 1);
|
|
46
|
+
this.sceneParent.children.splice(Math.min(this.sceneIndex, this.sceneParent.children.length), 0, sceneNode);
|
|
47
|
+
sceneNode.markWorldDirty?.();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.container.add(this.node);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function asSceneNode(value: unknown): SceneNodeLike | undefined {
|
|
58
|
+
if (!value || typeof value !== "object") {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const candidate = value as Partial<SceneNodeLike>;
|
|
63
|
+
if (
|
|
64
|
+
Array.isArray(candidate.children) &&
|
|
65
|
+
"parent" in candidate &&
|
|
66
|
+
typeof candidate.addChild === "function" &&
|
|
67
|
+
typeof candidate.removeChild === "function" &&
|
|
68
|
+
typeof candidate.removeFromParent === "function"
|
|
69
|
+
) {
|
|
70
|
+
return candidate as SceneNodeLike;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Command } from "../Command";
|
|
2
|
+
|
|
3
|
+
interface ParentLike<TNode> {
|
|
4
|
+
readonly children: TNode[];
|
|
5
|
+
addChild(child: TNode): unknown;
|
|
6
|
+
removeChild(child: TNode): boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ChildLike<TNode> {
|
|
10
|
+
parent: ParentLike<TNode> | null;
|
|
11
|
+
markWorldDirty?(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ReparentNodeCommand<TNode extends ChildLike<TNode>> implements Command {
|
|
15
|
+
readonly name = "Reparent Node";
|
|
16
|
+
private readonly oldParent: ParentLike<TNode> | null;
|
|
17
|
+
private readonly oldIndex: number;
|
|
18
|
+
private readonly newIndex: number;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly node: TNode,
|
|
22
|
+
private readonly newParent: ParentLike<TNode>,
|
|
23
|
+
newIndex?: number
|
|
24
|
+
) {
|
|
25
|
+
this.oldParent = node.parent;
|
|
26
|
+
this.oldIndex = this.oldParent?.children.indexOf(node) ?? -1;
|
|
27
|
+
this.newIndex = newIndex ?? newParent.children.length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
execute(): void {
|
|
31
|
+
this.move(this.newParent, this.newIndex);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
undo(): void {
|
|
35
|
+
if (!this.oldParent) {
|
|
36
|
+
this.node.parent?.removeChild(this.node);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.move(this.oldParent, this.oldIndex);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private move(parent: ParentLike<TNode>, index: number): void {
|
|
43
|
+
if (this.node.parent !== parent) {
|
|
44
|
+
this.node.parent?.removeChild(this.node);
|
|
45
|
+
parent.addChild(this.node);
|
|
46
|
+
}
|
|
47
|
+
const currentIndex = parent.children.indexOf(this.node);
|
|
48
|
+
const targetIndex = Math.max(0, Math.min(index, parent.children.length - 1));
|
|
49
|
+
if (currentIndex >= 0 && currentIndex !== targetIndex) {
|
|
50
|
+
parent.children.splice(currentIndex, 1);
|
|
51
|
+
parent.children.splice(targetIndex, 0, this.node);
|
|
52
|
+
this.node.markWorldDirty?.();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Command } from "../Command";
|
|
2
|
+
|
|
3
|
+
export class SetPropertyCommand<T extends object> implements Command {
|
|
4
|
+
readonly name = "Set Property";
|
|
5
|
+
private readonly before: unknown;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly target: T,
|
|
9
|
+
private readonly path: readonly string[],
|
|
10
|
+
private readonly after: unknown
|
|
11
|
+
) {
|
|
12
|
+
if (path.length === 0) {
|
|
13
|
+
throw new Error("Property path cannot be empty");
|
|
14
|
+
}
|
|
15
|
+
this.before = this.read();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
execute(): void {
|
|
19
|
+
this.write(this.after);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
undo(): void {
|
|
23
|
+
this.write(this.before);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private read(): unknown {
|
|
27
|
+
let cursor: unknown = this.target;
|
|
28
|
+
for (const segment of this.path) {
|
|
29
|
+
if (cursor === null || typeof cursor !== "object" || !(segment in cursor)) {
|
|
30
|
+
throw new Error(`Invalid property path: ${this.path.join(".")}`);
|
|
31
|
+
}
|
|
32
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
33
|
+
}
|
|
34
|
+
return cursor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private write(value: unknown): void {
|
|
38
|
+
let cursor: unknown = this.target;
|
|
39
|
+
for (const segment of this.path.slice(0, -1)) {
|
|
40
|
+
if (cursor === null || typeof cursor !== "object" || !(segment in cursor)) {
|
|
41
|
+
throw new Error(`Invalid property path: ${this.path.join(".")}`);
|
|
42
|
+
}
|
|
43
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
44
|
+
}
|
|
45
|
+
(cursor as Record<string, unknown>)[this.path[this.path.length - 1]] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Command } from "../Command";
|
|
2
|
+
|
|
3
|
+
export interface TransformLike {
|
|
4
|
+
position: { x: number; y: number; z: number };
|
|
5
|
+
rotation?: { x: number; y: number; z: number; w?: number };
|
|
6
|
+
scale?: { x: number; y: number; z: number };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SceneTransformTargetLike {
|
|
10
|
+
readonly transform: {
|
|
11
|
+
readonly position: [number, number, number];
|
|
12
|
+
readonly rotation: [number, number, number, number];
|
|
13
|
+
readonly scale: [number, number, number];
|
|
14
|
+
setPosition(x: number, y: number, z: number): unknown;
|
|
15
|
+
setRotation(x: number, y: number, z: number, w: number): unknown;
|
|
16
|
+
setScale(x: number, y: number, z: number): unknown;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type TransformTarget = TransformLike | SceneTransformTargetLike;
|
|
21
|
+
|
|
22
|
+
function cloneTransform(transform: TransformLike): TransformLike {
|
|
23
|
+
return {
|
|
24
|
+
position: { ...transform.position },
|
|
25
|
+
rotation: transform.rotation ? { ...transform.rotation } : undefined,
|
|
26
|
+
scale: transform.scale ? { ...transform.scale } : undefined
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readTransform(target: TransformTarget): TransformLike {
|
|
31
|
+
if ("transform" in target) {
|
|
32
|
+
return {
|
|
33
|
+
position: {
|
|
34
|
+
x: target.transform.position[0],
|
|
35
|
+
y: target.transform.position[1],
|
|
36
|
+
z: target.transform.position[2]
|
|
37
|
+
},
|
|
38
|
+
rotation: {
|
|
39
|
+
x: target.transform.rotation[0],
|
|
40
|
+
y: target.transform.rotation[1],
|
|
41
|
+
z: target.transform.rotation[2],
|
|
42
|
+
w: target.transform.rotation[3]
|
|
43
|
+
},
|
|
44
|
+
scale: {
|
|
45
|
+
x: target.transform.scale[0],
|
|
46
|
+
y: target.transform.scale[1],
|
|
47
|
+
z: target.transform.scale[2]
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return cloneTransform(target);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function applyTransform(target: TransformTarget, transform: TransformLike): void {
|
|
55
|
+
if ("transform" in target) {
|
|
56
|
+
target.transform.setPosition(transform.position.x, transform.position.y, transform.position.z);
|
|
57
|
+
if (transform.rotation) {
|
|
58
|
+
target.transform.setRotation(
|
|
59
|
+
transform.rotation.x,
|
|
60
|
+
transform.rotation.y,
|
|
61
|
+
transform.rotation.z,
|
|
62
|
+
transform.rotation.w ?? 1
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (transform.scale) {
|
|
66
|
+
target.transform.setScale(transform.scale.x, transform.scale.y, transform.scale.z);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
target.position = { ...transform.position };
|
|
71
|
+
target.rotation = transform.rotation ? { ...transform.rotation } : undefined;
|
|
72
|
+
target.scale = transform.scale ? { ...transform.scale } : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class TransformCommand implements Command {
|
|
76
|
+
readonly name = "Transform";
|
|
77
|
+
private readonly before: TransformLike;
|
|
78
|
+
private readonly after: TransformLike;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly target: TransformTarget,
|
|
82
|
+
next: TransformLike,
|
|
83
|
+
before?: TransformLike
|
|
84
|
+
) {
|
|
85
|
+
this.before = cloneTransform(before ?? readTransform(target));
|
|
86
|
+
this.after = cloneTransform(next);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
execute(): void {
|
|
90
|
+
applyTransform(this.target, this.after);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
undo(): void {
|
|
94
|
+
applyTransform(this.target, this.before);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
canMerge(next: Command): boolean {
|
|
98
|
+
return next instanceof TransformCommand && next.target === this.target;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
merge(next: Command): Command {
|
|
102
|
+
if (!(next instanceof TransformCommand) || next.target !== this.target) {
|
|
103
|
+
throw new Error("TransformCommand can only merge commands for the same target.");
|
|
104
|
+
}
|
|
105
|
+
return new TransformCommand(this.target, next.after, this.before);
|
|
106
|
+
}
|
|
107
|
+
}
|