@aura3d/editor-runtime 1.3.3 → 1.4.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.
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,262 +0,0 @@
1
- import { TimelineEditorController, type TimelineEditorSnapshot } from "./TimelineEditorController";
2
- import { TimelineModel, type TimelineClipConfig, type TimelineModelConfig, type TimelineTrackConfig } from "./TimelineModel";
3
- import { createTimelineTrackConfig, type TimelineEditorTrackKind } from "./TimelineTrackTypes";
4
-
5
- export type NonlinearBinAssetKind = "animation" | "audio" | "camera" | "shot" | "sequence" | "caption" | "viseme";
6
-
7
- export interface NonlinearBinAsset {
8
- readonly id: string;
9
- readonly name: string;
10
- readonly kind: NonlinearBinAssetKind;
11
- readonly assetId?: string | undefined;
12
- readonly clipName?: string | undefined;
13
- readonly duration?: number | undefined;
14
- readonly metadata?: Readonly<Record<string, string | number | boolean>> | undefined;
15
- }
16
-
17
- export interface NonlinearSequenceConfig {
18
- readonly id: string;
19
- readonly name: string;
20
- readonly timeline?: TimelineModelConfig | undefined;
21
- }
22
-
23
- export interface NonlinearSequenceSnapshot {
24
- readonly id: string;
25
- readonly name: string;
26
- readonly trackCount: number;
27
- readonly clipCount: number;
28
- readonly duration: number;
29
- }
30
-
31
- export interface NonlinearAnimationEditorSnapshot {
32
- readonly kind: "nonlinear-animation-editor";
33
- readonly activeSequenceId: string;
34
- readonly sequenceCount: number;
35
- readonly binAssetCount: number;
36
- readonly nestedSequenceClipCount: number;
37
- readonly totalTrackCount: number;
38
- readonly totalClipCount: number;
39
- readonly sequences: readonly NonlinearSequenceSnapshot[];
40
- readonly activeTimeline: TimelineEditorSnapshot;
41
- readonly evidence: {
42
- readonly nonlinearSequences: boolean;
43
- readonly trimSplitMoveDuplicate: boolean;
44
- readonly nestedSequences: boolean;
45
- readonly multiTrackTimeline: boolean;
46
- readonly serialization: true;
47
- };
48
- }
49
-
50
- export interface NonlinearAnimationEditorState {
51
- readonly activeSequenceId: string;
52
- readonly binAssets: readonly NonlinearBinAsset[];
53
- readonly sequences: readonly NonlinearSequenceConfig[];
54
- }
55
-
56
- interface NonlinearSequenceRuntime {
57
- readonly id: string;
58
- readonly name: string;
59
- readonly controller: TimelineEditorController;
60
- }
61
-
62
- export class NonlinearAnimationEditor {
63
- private readonly binAssets = new Map<string, NonlinearBinAsset>();
64
- private readonly sequences = new Map<string, NonlinearSequenceRuntime>();
65
- private activeSequenceId: string;
66
-
67
- constructor(state: NonlinearAnimationEditorState) {
68
- if (state.sequences.length === 0) throw new Error("Nonlinear animation editor requires at least one sequence.");
69
- for (const asset of state.binAssets) this.addBinAsset(asset);
70
- for (const sequence of state.sequences) this.createSequence(sequence);
71
- this.activeSequenceId = nonEmpty(state.activeSequenceId, "Active sequence id");
72
- this.requireSequence(this.activeSequenceId);
73
- }
74
-
75
- addBinAsset(asset: NonlinearBinAsset): NonlinearBinAsset {
76
- const clean = sanitizeBinAsset(asset);
77
- this.binAssets.set(clean.id, clean);
78
- return clean;
79
- }
80
-
81
- createSequence(config: NonlinearSequenceConfig): NonlinearSequenceSnapshot {
82
- const id = nonEmpty(config.id, "Sequence id");
83
- const timeline = new TimelineModel({
84
- id,
85
- name: config.name,
86
- duration: 10,
87
- frameRate: 24,
88
- loopMode: "none",
89
- ...config.timeline
90
- });
91
- this.sequences.set(id, {
92
- id,
93
- name: nonEmpty(config.name, "Sequence name"),
94
- controller: new TimelineEditorController({ timeline })
95
- });
96
- return this.sequenceSnapshot(this.requireSequence(id));
97
- }
98
-
99
- selectSequence(sequenceId: string): NonlinearAnimationEditorSnapshot {
100
- this.activeSequenceId = this.requireSequence(sequenceId).id;
101
- return this.snapshot();
102
- }
103
-
104
- async addTrack(kind: TimelineEditorTrackKind, name?: string, options: Omit<TimelineTrackConfig, "name" | "type"> = {}): Promise<string> {
105
- const track = await this.activeSequence().controller.addTrack(kind, name, options);
106
- return track.id;
107
- }
108
-
109
- async insertAssetClip(trackId: string, assetId: string, options: Omit<TimelineClipConfig, "name" | "startTime" | "duration" | "assetId" | "clipName"> & {
110
- readonly id?: string | undefined;
111
- readonly name?: string | undefined;
112
- readonly startTime: number;
113
- readonly duration?: number | undefined;
114
- }): Promise<string> {
115
- const asset = this.requireBinAsset(assetId);
116
- const clip = await this.activeSequence().controller.addClip(trackId, {
117
- ...options,
118
- id: options.id ?? `${asset.id}-${Math.round(options.startTime * 1000)}`,
119
- name: options.name ?? asset.name,
120
- startTime: options.startTime,
121
- duration: options.duration ?? asset.duration ?? 1,
122
- assetId: asset.assetId ?? asset.id,
123
- clipName: asset.clipName ?? asset.name,
124
- properties: {
125
- ...(options.properties ?? {}),
126
- binAssetId: asset.id,
127
- binAssetKind: asset.kind
128
- }
129
- });
130
- return clip.id;
131
- }
132
-
133
- async insertNestedSequence(trackId: string, sequenceId: string, options: Omit<TimelineClipConfig, "name" | "startTime" | "duration"> & {
134
- readonly id?: string | undefined;
135
- readonly startTime: number;
136
- readonly duration?: number | undefined;
137
- }): Promise<string> {
138
- const nested = this.requireSequence(sequenceId);
139
- const clip = await this.activeSequence().controller.addClip(trackId, {
140
- ...options,
141
- id: options.id ?? `${sequenceId}-nested-${Math.round(options.startTime * 1000)}`,
142
- name: nested.name,
143
- startTime: options.startTime,
144
- duration: options.duration ?? nested.controller.timeline.duration,
145
- properties: {
146
- ...(options.properties ?? {}),
147
- nestedSequenceId: nested.id
148
- }
149
- });
150
- return clip.id;
151
- }
152
-
153
- async moveClip(clipId: string, startTime: number): Promise<void> {
154
- await this.activeSequence().controller.moveClip(clipId, startTime);
155
- }
156
-
157
- async trimClip(clipId: string, duration: number, startTime?: number): Promise<void> {
158
- await this.activeSequence().controller.resizeClip(clipId, duration, startTime);
159
- }
160
-
161
- async splitClip(clipId: string, splitTime: number): Promise<readonly string[]> {
162
- const clips = await this.activeSequence().controller.splitClip(clipId, splitTime);
163
- return clips.map((clip) => clip.id);
164
- }
165
-
166
- async duplicateClip(clipId: string, timeOffset?: number): Promise<string> {
167
- const clip = await this.activeSequence().controller.duplicateClip(clipId, timeOffset);
168
- return clip.id;
169
- }
170
-
171
- serialize(): NonlinearAnimationEditorState {
172
- return {
173
- activeSequenceId: this.activeSequenceId,
174
- binAssets: [...this.binAssets.values()],
175
- sequences: [...this.sequences.values()].map((sequence) => ({
176
- id: sequence.id,
177
- name: sequence.name,
178
- timeline: sequence.controller.serializeTimeline()
179
- }))
180
- };
181
- }
182
-
183
- snapshot(): NonlinearAnimationEditorSnapshot {
184
- const sequences = [...this.sequences.values()];
185
- const active = this.activeSequence();
186
- const totalTrackCount = sequences.reduce((count, sequence) => count + sequence.controller.timeline.tracks.length, 0);
187
- const totalClipCount = sequences.reduce((count, sequence) => count + sequence.controller.timeline.tracks.reduce((clipCount, track) => clipCount + track.clips.length, 0), 0);
188
- const nestedSequenceClipCount = sequences.reduce((count, sequence) =>
189
- count + sequence.controller.timeline.tracks.reduce((trackCount, track) =>
190
- trackCount + track.clips.filter((clip) => typeof clip.properties.nestedSequenceId === "string").length, 0), 0);
191
- return {
192
- kind: "nonlinear-animation-editor",
193
- activeSequenceId: active.id,
194
- sequenceCount: sequences.length,
195
- binAssetCount: this.binAssets.size,
196
- nestedSequenceClipCount,
197
- totalTrackCount,
198
- totalClipCount,
199
- sequences: sequences.map((sequence) => this.sequenceSnapshot(sequence)),
200
- activeTimeline: active.controller.snapshot(),
201
- evidence: {
202
- nonlinearSequences: sequences.length > 1,
203
- trimSplitMoveDuplicate: active.controller.history.canUndo || active.controller.history.canRedo,
204
- nestedSequences: nestedSequenceClipCount > 0,
205
- multiTrackTimeline: totalTrackCount > 1,
206
- serialization: true
207
- }
208
- };
209
- }
210
-
211
- private activeSequence(): NonlinearSequenceRuntime {
212
- return this.requireSequence(this.activeSequenceId);
213
- }
214
-
215
- private requireSequence(id: string): NonlinearSequenceRuntime {
216
- const sequence = this.sequences.get(id);
217
- if (!sequence) throw new Error(`Unknown nonlinear sequence: ${id}`);
218
- return sequence;
219
- }
220
-
221
- private requireBinAsset(id: string): NonlinearBinAsset {
222
- const asset = this.binAssets.get(id);
223
- if (!asset) throw new Error(`Unknown nonlinear bin asset: ${id}`);
224
- return asset;
225
- }
226
-
227
- private sequenceSnapshot(sequence: NonlinearSequenceRuntime): NonlinearSequenceSnapshot {
228
- const timeline = sequence.controller.timeline;
229
- return {
230
- id: sequence.id,
231
- name: sequence.name,
232
- trackCount: timeline.tracks.length,
233
- clipCount: timeline.tracks.reduce((count, track) => count + track.clips.length, 0),
234
- duration: timeline.duration
235
- };
236
- }
237
- }
238
-
239
- export function createNonlinearAnimationEditor(state: NonlinearAnimationEditorState): NonlinearAnimationEditor {
240
- return new NonlinearAnimationEditor(state);
241
- }
242
-
243
- function sanitizeBinAsset(asset: NonlinearBinAsset): NonlinearBinAsset {
244
- if (asset.duration !== undefined && (!Number.isFinite(asset.duration) || asset.duration <= 0)) {
245
- throw new Error(`Nonlinear bin asset "${asset.id}" duration must be positive when provided.`);
246
- }
247
- return {
248
- id: nonEmpty(asset.id, "Bin asset id"),
249
- name: nonEmpty(asset.name, "Bin asset name"),
250
- kind: asset.kind,
251
- assetId: asset.assetId,
252
- clipName: asset.clipName,
253
- duration: asset.duration,
254
- metadata: asset.metadata
255
- };
256
- }
257
-
258
- function nonEmpty(value: string, label: string): string {
259
- const trimmed = value.trim();
260
- if (!trimmed) throw new Error(`${label} must be a non-empty string.`);
261
- return trimmed;
262
- }
@@ -1,174 +0,0 @@
1
- import type { Ray } from "@aura3d/math";
2
- import type { SceneNode } from "@aura3d/scene";
3
-
4
- type Tuple3 = readonly [number, number, number];
5
-
6
- export interface EditorPickTarget {
7
- readonly id: string;
8
- readonly node: SceneNode;
9
- readonly bounds: {
10
- readonly min: Tuple3;
11
- readonly max: Tuple3;
12
- };
13
- }
14
-
15
- export interface EditorPickHit {
16
- readonly target: EditorPickTarget;
17
- readonly distance: number;
18
- }
19
-
20
- export interface EditorPickingColorId {
21
- readonly targetId: string;
22
- readonly encodedId: number;
23
- readonly color: readonly [number, number, number, number];
24
- }
25
-
26
- export interface EditorPickingEvidenceSnapshot {
27
- readonly source: "origin-master-gpu-picking-adapted";
28
- readonly registeredTargetCount: number;
29
- readonly width: number;
30
- readonly height: number;
31
- readonly needsUpdate: boolean;
32
- readonly idCapacity: number;
33
- readonly sampleColorId: EditorPickingColorId | null;
34
- readonly decodedSampleTargetId: string | null;
35
- readonly colorIds: readonly EditorPickingColorId[];
36
- readonly evidence: {
37
- readonly colorIdEncoding: boolean;
38
- readonly colorIdDecoding: boolean;
39
- readonly framebufferResizeBoundary: boolean;
40
- readonly raycastFallback: boolean;
41
- };
42
- readonly blockedClaims: readonly string[];
43
- }
44
-
45
- export class PickingService {
46
- private readonly targets = new Map<string, EditorPickTarget>();
47
- private width = 1024;
48
- private height = 768;
49
- private needsUpdate = true;
50
-
51
- setTargets(targets: readonly EditorPickTarget[]): void {
52
- this.targets.clear();
53
- for (const target of targets) {
54
- this.targets.set(target.id, target);
55
- }
56
- this.needsUpdate = true;
57
- }
58
-
59
- addTarget(target: EditorPickTarget): void {
60
- this.targets.set(target.id, target);
61
- this.needsUpdate = true;
62
- }
63
-
64
- removeTarget(id: string): void {
65
- this.targets.delete(id);
66
- this.needsUpdate = true;
67
- }
68
-
69
- pick(ray: Ray): EditorPickHit | undefined {
70
- const hit = [...this.targets.values()]
71
- .map((target) => ({ target, distance: intersectBounds(ray, target.bounds) }))
72
- .filter((hit): hit is EditorPickHit => hit.distance !== undefined)
73
- .sort((a, b) => a.distance - b.distance)[0];
74
- this.needsUpdate = false;
75
- return hit;
76
- }
77
-
78
- resizePickingBuffer(width: number, height: number): void {
79
- if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
80
- throw new RangeError("Picking buffer dimensions must be positive integers.");
81
- }
82
- this.width = width;
83
- this.height = height;
84
- this.needsUpdate = true;
85
- }
86
-
87
- invalidatePickingBuffer(): void {
88
- this.needsUpdate = true;
89
- }
90
-
91
- colorIdForTarget(targetId: string): EditorPickingColorId | undefined {
92
- const ids = this.colorIds();
93
- return ids.find((entry) => entry.targetId === targetId);
94
- }
95
-
96
- targetIdFromColor(color: readonly [number, number, number] | readonly [number, number, number, number]): string | undefined {
97
- const encodedId = PickingService.colorToId(color[0] ?? 0, color[1] ?? 0, color[2] ?? 0);
98
- const entry = this.colorIds().find((candidate) => candidate.encodedId === encodedId);
99
- return entry?.targetId;
100
- }
101
-
102
- snapshot(): EditorPickingEvidenceSnapshot {
103
- const colorIds = this.colorIds();
104
- const sampleColorId = colorIds[0] ?? null;
105
- const decodedSampleTargetId = sampleColorId ? this.targetIdFromColor(sampleColorId.color) ?? null : null;
106
- return {
107
- source: "origin-master-gpu-picking-adapted",
108
- registeredTargetCount: this.targets.size,
109
- width: this.width,
110
- height: this.height,
111
- needsUpdate: this.needsUpdate,
112
- idCapacity: 0xffffff,
113
- sampleColorId,
114
- decodedSampleTargetId,
115
- colorIds,
116
- evidence: {
117
- colorIdEncoding: colorIds.every((entry) => entry.color[3] === 255 && entry.encodedId >= 1),
118
- colorIdDecoding: sampleColorId !== null && decodedSampleTargetId === sampleColorId.targetId,
119
- framebufferResizeBoundary: this.width > 0 && this.height > 0,
120
- raycastFallback: true
121
- },
122
- blockedClaims: [
123
- "production GPU framebuffer picking pass",
124
- "depth-buffer world-position reconstruction",
125
- "GPU picking performance parity with Unity Scene View",
126
- "GPU picking performance parity with Unreal Editor viewport"
127
- ]
128
- };
129
- }
130
-
131
- static idToColor(id: number): readonly [number, number, number, number] {
132
- if (!Number.isInteger(id) || id < 1 || id > 0xffffff) {
133
- throw new RangeError("Picking color id must be an integer in the 1..16777215 range.");
134
- }
135
- return [id & 0xff, (id >> 8) & 0xff, (id >> 16) & 0xff, 255];
136
- }
137
-
138
- static colorToId(red: number, green: number, blue: number): number {
139
- return (red & 0xff) | ((green & 0xff) << 8) | ((blue & 0xff) << 16);
140
- }
141
-
142
- private colorIds(): readonly EditorPickingColorId[] {
143
- return [...this.targets.keys()].map((targetId, index) => {
144
- const encodedId = index + 1;
145
- return { targetId, encodedId, color: PickingService.idToColor(encodedId) };
146
- });
147
- }
148
- }
149
-
150
- function intersectBounds(ray: Ray, bounds: EditorPickTarget["bounds"]): number | undefined {
151
- let tmin = Number.NEGATIVE_INFINITY;
152
- let tmax = Number.POSITIVE_INFINITY;
153
-
154
- for (const axis of [0, 1, 2] as const) {
155
- const origin = axis === 0 ? ray.origin.x : axis === 1 ? ray.origin.y : ray.origin.z;
156
- const direction = axis === 0 ? ray.direction.x : axis === 1 ? ray.direction.y : ray.direction.z;
157
- const min = bounds.min[axis];
158
- const max = bounds.max[axis];
159
- if (Math.abs(direction) < 1e-8) {
160
- if (origin < min || origin > max) return undefined;
161
- continue;
162
- }
163
- const invD = 1 / direction;
164
- let near = (min - origin) * invD;
165
- let far = (max - origin) * invD;
166
- if (near > far) [near, far] = [far, near];
167
- tmin = Math.max(tmin, near);
168
- tmax = Math.min(tmax, far);
169
- if (tmin > tmax) return undefined;
170
- }
171
-
172
- const distance = tmin >= 0 ? tmin : tmax;
173
- return distance >= 0 ? distance : undefined;
174
- }
@@ -1,27 +0,0 @@
1
- export interface SnapshotAdapter<T> {
2
- capture(): T;
3
- restore(snapshot: T): void;
4
- }
5
-
6
- export class PlayModeBridge<TSnapshot> {
7
- private snapshot?: TSnapshot;
8
-
9
- constructor(private readonly adapter: SnapshotAdapter<TSnapshot>) {}
10
-
11
- enter(): void {
12
- if (this.snapshot !== undefined) {
13
- throw new Error("Play mode is already active");
14
- }
15
- this.snapshot = this.adapter.capture();
16
- }
17
-
18
- exit({ restore = true } = {}): void {
19
- if (this.snapshot === undefined) {
20
- return;
21
- }
22
- if (restore) {
23
- this.adapter.restore(this.snapshot);
24
- }
25
- this.snapshot = undefined;
26
- }
27
- }
@@ -1,164 +0,0 @@
1
- export type EditorPrefabSchemaVersion = "aura3d-prefab";
2
-
3
- export interface EditorPrefabNodeBase {
4
- readonly id: string;
5
- readonly name: string;
6
- readonly parentId: string | null;
7
- }
8
-
9
- export interface EditorPrefab<TNode extends EditorPrefabNodeBase = EditorPrefabNodeBase> {
10
- readonly schemaVersion: EditorPrefabSchemaVersion;
11
- readonly id: string;
12
- readonly name: string;
13
- readonly rootNodeId: string;
14
- readonly sourceNodeId?: string;
15
- readonly createdAt: string;
16
- readonly nodes: readonly TNode[];
17
- }
18
-
19
- export interface CreatePrefabOptions<TNode extends EditorPrefabNodeBase> {
20
- readonly id: string;
21
- readonly name: string;
22
- readonly rootNodeId: string;
23
- readonly sourceNodeId?: string;
24
- readonly createdAt?: string;
25
- readonly nodes: readonly TNode[];
26
- }
27
-
28
- export interface InstantiatePrefabOptions {
29
- readonly idPrefix: string;
30
- readonly rootParentId?: string | null;
31
- readonly nameSuffix?: string;
32
- }
33
-
34
- export class PrefabRegistry<TNode extends EditorPrefabNodeBase = EditorPrefabNodeBase> {
35
- private readonly prefabsById = new Map<string, EditorPrefab<TNode>>();
36
-
37
- create(options: CreatePrefabOptions<TNode>): EditorPrefab<TNode> {
38
- const prefab: EditorPrefab<TNode> = {
39
- schemaVersion: "aura3d-prefab",
40
- id: normalizeId(options.id, "prefab id"),
41
- name: normalizeName(options.name, "prefab name"),
42
- rootNodeId: normalizeId(options.rootNodeId, "prefab rootNodeId"),
43
- ...(options.sourceNodeId ? { sourceNodeId: normalizeId(options.sourceNodeId, "prefab sourceNodeId") } : {}),
44
- createdAt: options.createdAt ?? new Date().toISOString(),
45
- nodes: options.nodes.map((node) => cloneNode(node))
46
- };
47
- validatePrefab(prefab);
48
- return prefab;
49
- }
50
-
51
- register(prefab: EditorPrefab<TNode>): EditorPrefab<TNode> {
52
- validatePrefab(prefab);
53
- if (this.prefabsById.has(prefab.id)) {
54
- throw new Error(`Prefab already registered: ${prefab.id}`);
55
- }
56
- const cloned = clonePrefab(prefab);
57
- this.prefabsById.set(cloned.id, cloned);
58
- return cloned;
59
- }
60
-
61
- upsert(prefab: EditorPrefab<TNode>): EditorPrefab<TNode> {
62
- validatePrefab(prefab);
63
- const cloned = clonePrefab(prefab);
64
- this.prefabsById.set(cloned.id, cloned);
65
- return cloned;
66
- }
67
-
68
- get(id: string): EditorPrefab<TNode> | undefined {
69
- const prefab = this.prefabsById.get(id);
70
- return prefab ? clonePrefab(prefab) : undefined;
71
- }
72
-
73
- list(): readonly EditorPrefab<TNode>[] {
74
- return [...this.prefabsById.values()].map((prefab) => clonePrefab(prefab));
75
- }
76
-
77
- remove(id: string): boolean {
78
- return this.prefabsById.delete(id);
79
- }
80
-
81
- clear(): void {
82
- this.prefabsById.clear();
83
- }
84
-
85
- instantiate(prefabOrId: EditorPrefab<TNode> | string, options: InstantiatePrefabOptions): readonly TNode[] {
86
- const prefab = typeof prefabOrId === "string" ? this.get(prefabOrId) : clonePrefab(prefabOrId);
87
- if (!prefab) {
88
- throw new Error(`Unknown prefab: ${prefabOrId}`);
89
- }
90
- validatePrefab(prefab);
91
- const idPrefix = normalizeId(options.idPrefix, "prefab instance idPrefix");
92
- const idMap = new Map(prefab.nodes.map((node) => [node.id, `${idPrefix}-${node.id}`]));
93
- const rootParentId = options.rootParentId ?? null;
94
- const nameSuffix = options.nameSuffix ?? " Instance";
95
- return prefab.nodes.map((node) => {
96
- const cloned = cloneNode(node);
97
- const remappedParent = node.id === prefab.rootNodeId
98
- ? rootParentId
99
- : node.parentId ? idMap.get(node.parentId) ?? null : null;
100
- return {
101
- ...cloned,
102
- id: idMap.get(node.id)!,
103
- name: `${node.name}${nameSuffix}`,
104
- parentId: remappedParent
105
- };
106
- });
107
- }
108
- }
109
-
110
- export function validatePrefab<TNode extends EditorPrefabNodeBase>(prefab: EditorPrefab<TNode>): void {
111
- if (prefab.schemaVersion !== "aura3d-prefab") {
112
- throw new Error(`Unsupported prefab schemaVersion: ${String(prefab.schemaVersion)}`);
113
- }
114
- normalizeId(prefab.id, "prefab id");
115
- normalizeName(prefab.name, "prefab name");
116
- normalizeId(prefab.rootNodeId, "prefab rootNodeId");
117
- if (prefab.nodes.length === 0) {
118
- throw new Error("Prefab must contain at least one node.");
119
- }
120
- const ids = new Set<string>();
121
- for (const node of prefab.nodes) {
122
- normalizeId(node.id, "prefab node id");
123
- normalizeName(node.name, "prefab node name");
124
- if (ids.has(node.id)) {
125
- throw new Error(`Prefab node id is duplicated: ${node.id}`);
126
- }
127
- ids.add(node.id);
128
- }
129
- if (!ids.has(prefab.rootNodeId)) {
130
- throw new Error(`Prefab rootNodeId ${prefab.rootNodeId} is not present in nodes.`);
131
- }
132
- for (const node of prefab.nodes) {
133
- if (node.parentId !== null && !ids.has(node.parentId)) {
134
- throw new Error(`Prefab node ${node.id} references external parent ${node.parentId}`);
135
- }
136
- }
137
- }
138
-
139
- function clonePrefab<TNode extends EditorPrefabNodeBase>(prefab: EditorPrefab<TNode>): EditorPrefab<TNode> {
140
- return {
141
- ...prefab,
142
- nodes: prefab.nodes.map((node) => cloneNode(node))
143
- };
144
- }
145
-
146
- function cloneNode<TNode extends EditorPrefabNodeBase>(node: TNode): TNode {
147
- return JSON.parse(JSON.stringify(node)) as TNode;
148
- }
149
-
150
- function normalizeId(value: string, label: string): string {
151
- const normalized = value.trim();
152
- if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(normalized)) {
153
- throw new Error(`${label} must be a non-empty stable id.`);
154
- }
155
- return normalized;
156
- }
157
-
158
- function normalizeName(value: string, label: string): string {
159
- const normalized = value.trim();
160
- if (normalized.length === 0) {
161
- throw new Error(`${label} cannot be empty.`);
162
- }
163
- return normalized;
164
- }