@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.
Files changed (149) hide show
  1. package/README.md +20 -0
  2. package/dist/Command.d.ts +11 -0
  3. package/dist/Command.d.ts.map +1 -0
  4. package/dist/Command.js +2 -0
  5. package/dist/Command.js.map +1 -0
  6. package/dist/CommandHistory.d.ts +20 -0
  7. package/dist/CommandHistory.d.ts.map +1 -0
  8. package/dist/CommandHistory.js +124 -0
  9. package/dist/CommandHistory.js.map +1 -0
  10. package/dist/DiagnosticsOverlayModel.d.ts +30 -0
  11. package/dist/DiagnosticsOverlayModel.d.ts.map +1 -0
  12. package/dist/DiagnosticsOverlayModel.js +57 -0
  13. package/dist/DiagnosticsOverlayModel.js.map +1 -0
  14. package/dist/EditorPluginHost.d.ts +46 -0
  15. package/dist/EditorPluginHost.d.ts.map +1 -0
  16. package/dist/EditorPluginHost.js +57 -0
  17. package/dist/EditorPluginHost.js.map +1 -0
  18. package/dist/EditorRuntime.d.ts +95 -0
  19. package/dist/EditorRuntime.d.ts.map +1 -0
  20. package/dist/EditorRuntime.js +277 -0
  21. package/dist/EditorRuntime.js.map +1 -0
  22. package/dist/EditorStateModel.d.ts +93 -0
  23. package/dist/EditorStateModel.d.ts.map +1 -0
  24. package/dist/EditorStateModel.js +251 -0
  25. package/dist/EditorStateModel.js.map +1 -0
  26. package/dist/Gizmo.d.ts +43 -0
  27. package/dist/Gizmo.d.ts.map +1 -0
  28. package/dist/Gizmo.js +60 -0
  29. package/dist/Gizmo.js.map +1 -0
  30. package/dist/HierarchyModel.d.ts +21 -0
  31. package/dist/HierarchyModel.d.ts.map +1 -0
  32. package/dist/HierarchyModel.js +32 -0
  33. package/dist/HierarchyModel.js.map +1 -0
  34. package/dist/InspectorModel.d.ts +16 -0
  35. package/dist/InspectorModel.d.ts.map +1 -0
  36. package/dist/InspectorModel.js +53 -0
  37. package/dist/InspectorModel.js.map +1 -0
  38. package/dist/LocalizationAccessibilityFixtures.d.ts +70 -0
  39. package/dist/LocalizationAccessibilityFixtures.d.ts.map +1 -0
  40. package/dist/LocalizationAccessibilityFixtures.js +275 -0
  41. package/dist/LocalizationAccessibilityFixtures.js.map +1 -0
  42. package/dist/MaterialVariantWorkflow.d.ts +19 -0
  43. package/dist/MaterialVariantWorkflow.d.ts.map +1 -0
  44. package/dist/MaterialVariantWorkflow.js +82 -0
  45. package/dist/MaterialVariantWorkflow.js.map +1 -0
  46. package/dist/PickingService.d.ts +58 -0
  47. package/dist/PickingService.d.ts.map +1 -0
  48. package/dist/PickingService.js +119 -0
  49. package/dist/PickingService.js.map +1 -0
  50. package/dist/PlayModeBridge.d.ts +14 -0
  51. package/dist/PlayModeBridge.d.ts.map +1 -0
  52. package/dist/PlayModeBridge.js +23 -0
  53. package/dist/PlayModeBridge.js.map +1 -0
  54. package/dist/PrefabRegistry.d.ts +41 -0
  55. package/dist/PrefabRegistry.d.ts.map +1 -0
  56. package/dist/PrefabRegistry.js +119 -0
  57. package/dist/PrefabRegistry.js.map +1 -0
  58. package/dist/ProjectSerializer.d.ts +82 -0
  59. package/dist/ProjectSerializer.d.ts.map +1 -0
  60. package/dist/ProjectSerializer.js +131 -0
  61. package/dist/ProjectSerializer.js.map +1 -0
  62. package/dist/RotateGizmo.d.ts +5 -0
  63. package/dist/RotateGizmo.d.ts.map +1 -0
  64. package/dist/RotateGizmo.js +37 -0
  65. package/dist/RotateGizmo.js.map +1 -0
  66. package/dist/ScaleGizmo.d.ts +5 -0
  67. package/dist/ScaleGizmo.d.ts.map +1 -0
  68. package/dist/ScaleGizmo.js +42 -0
  69. package/dist/ScaleGizmo.js.map +1 -0
  70. package/dist/Selection.d.ts +20 -0
  71. package/dist/Selection.d.ts.map +1 -0
  72. package/dist/Selection.js +46 -0
  73. package/dist/Selection.js.map +1 -0
  74. package/dist/ShaderGraphModel.d.ts +69 -0
  75. package/dist/ShaderGraphModel.d.ts.map +1 -0
  76. package/dist/ShaderGraphModel.js +172 -0
  77. package/dist/ShaderGraphModel.js.map +1 -0
  78. package/dist/StaticExportRuntime.d.ts +13 -0
  79. package/dist/StaticExportRuntime.d.ts.map +1 -0
  80. package/dist/StaticExportRuntime.js +307 -0
  81. package/dist/StaticExportRuntime.js.map +1 -0
  82. package/dist/TimelineModel.d.ts +175 -0
  83. package/dist/TimelineModel.d.ts.map +1 -0
  84. package/dist/TimelineModel.js +335 -0
  85. package/dist/TimelineModel.js.map +1 -0
  86. package/dist/TimelineRuntimeBridge.d.ts +98 -0
  87. package/dist/TimelineRuntimeBridge.d.ts.map +1 -0
  88. package/dist/TimelineRuntimeBridge.js +186 -0
  89. package/dist/TimelineRuntimeBridge.js.map +1 -0
  90. package/dist/TranslateGizmo.d.ts +5 -0
  91. package/dist/TranslateGizmo.d.ts.map +1 -0
  92. package/dist/TranslateGizmo.js +41 -0
  93. package/dist/TranslateGizmo.js.map +1 -0
  94. package/dist/commands/CreateNodeCommand.d.ts +14 -0
  95. package/dist/commands/CreateNodeCommand.d.ts.map +1 -0
  96. package/dist/commands/CreateNodeCommand.js +16 -0
  97. package/dist/commands/CreateNodeCommand.js.map +1 -0
  98. package/dist/commands/DeleteNodeCommand.d.ts +13 -0
  99. package/dist/commands/DeleteNodeCommand.d.ts.map +1 -0
  100. package/dist/commands/DeleteNodeCommand.js +55 -0
  101. package/dist/commands/DeleteNodeCommand.js.map +1 -0
  102. package/dist/commands/ReparentNodeCommand.d.ts +24 -0
  103. package/dist/commands/ReparentNodeCommand.d.ts.map +1 -0
  104. package/dist/commands/ReparentNodeCommand.js +39 -0
  105. package/dist/commands/ReparentNodeCommand.js.map +1 -0
  106. package/dist/commands/SetPropertyCommand.d.ts +14 -0
  107. package/dist/commands/SetPropertyCommand.d.ts.map +1 -0
  108. package/dist/commands/SetPropertyCommand.js +43 -0
  109. package/dist/commands/SetPropertyCommand.js.map +1 -0
  110. package/dist/commands/TransformCommand.d.ts +42 -0
  111. package/dist/commands/TransformCommand.d.ts.map +1 -0
  112. package/dist/commands/TransformCommand.js +72 -0
  113. package/dist/commands/TransformCommand.js.map +1 -0
  114. package/dist/index.d.ts +49 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +29 -0
  117. package/dist/index.js.map +1 -0
  118. package/package.json +17 -0
  119. package/src/Command.ts +11 -0
  120. package/src/CommandHistory.ts +136 -0
  121. package/src/DiagnosticsOverlayModel.ts +85 -0
  122. package/src/EditorPluginHost.ts +106 -0
  123. package/src/EditorRuntime.ts +349 -0
  124. package/src/EditorStateModel.ts +336 -0
  125. package/src/Gizmo.ts +101 -0
  126. package/src/HierarchyModel.ts +57 -0
  127. package/src/InspectorModel.ts +71 -0
  128. package/src/LocalizationAccessibilityFixtures.ts +368 -0
  129. package/src/MaterialVariantWorkflow.ts +101 -0
  130. package/src/PickingService.ts +174 -0
  131. package/src/PlayModeBridge.ts +27 -0
  132. package/src/PrefabRegistry.ts +164 -0
  133. package/src/ProjectSerializer.ts +200 -0
  134. package/src/RotateGizmo.ts +35 -0
  135. package/src/ScaleGizmo.ts +40 -0
  136. package/src/Selection.ts +63 -0
  137. package/src/ShaderGraphModel.ts +254 -0
  138. package/src/StaticExportRuntime.ts +322 -0
  139. package/src/TimelineModel.ts +466 -0
  140. package/src/TimelineRuntimeBridge.ts +280 -0
  141. package/src/TranslateGizmo.ts +39 -0
  142. package/src/commands/CreateNodeCommand.ts +23 -0
  143. package/src/commands/DeleteNodeCommand.ts +74 -0
  144. package/src/commands/ReparentNodeCommand.ts +55 -0
  145. package/src/commands/SetPropertyCommand.ts +47 -0
  146. package/src/commands/TransformCommand.ts +107 -0
  147. package/src/index.ts +85 -0
  148. package/tests/editor-runtime.test.ts +597 -0
  149. package/tsconfig.json +7 -0
@@ -0,0 +1,164 @@
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
+ }
@@ -0,0 +1,200 @@
1
+ import { TimelineModel, type TimelineModelConfig } from "./TimelineModel";
2
+ import type { TimelineRuntimeBindingConfig } from "./TimelineRuntimeBridge";
3
+
4
+ export interface EditorProjectAssetDocument {
5
+ readonly id: string;
6
+ readonly name: string;
7
+ readonly uri?: string;
8
+ readonly type?: "gltf" | "glb" | "image" | "audio" | "material" | "unknown";
9
+ readonly source?: string;
10
+ readonly license?: string;
11
+ readonly clips?: readonly string[];
12
+ readonly morphTargets?: readonly string[];
13
+ readonly diagnostics?: readonly string[];
14
+ }
15
+
16
+ export interface EditorProjectTimelineDocument extends TimelineModelConfig {
17
+ readonly bindings?: readonly TimelineRuntimeBindingConfig[];
18
+ readonly evidence?: {
19
+ readonly authoredInEditor?: boolean;
20
+ readonly runtimeReplay?: boolean;
21
+ readonly animationEvents?: boolean;
22
+ };
23
+ }
24
+
25
+ export interface EditorProjectVisualGraphDocument {
26
+ readonly id: string;
27
+ readonly name: string;
28
+ readonly nodes: readonly unknown[];
29
+ readonly edges: readonly unknown[];
30
+ readonly runtimeBindings?: readonly {
31
+ readonly nodeId: string;
32
+ readonly targetId: string;
33
+ readonly event?: string;
34
+ }[];
35
+ }
36
+
37
+ export interface EditorProjectStateDocument {
38
+ readonly selectedNodeId?: string | number | null;
39
+ readonly activeTool?: string;
40
+ readonly activeTimelineId?: string | null;
41
+ readonly playMode?: "edit" | "play" | "paused";
42
+ }
43
+
44
+ export interface EditorProjectDocument {
45
+ readonly schema: "a3d-editor-project";
46
+ readonly version?: number;
47
+ readonly name: string;
48
+ readonly nodes: readonly unknown[];
49
+ readonly assets?: readonly (string | EditorProjectAssetDocument)[];
50
+ readonly timelines?: readonly EditorProjectTimelineDocument[];
51
+ readonly visualGraphs?: readonly EditorProjectVisualGraphDocument[];
52
+ readonly editor?: EditorProjectStateDocument;
53
+ readonly evidence?: {
54
+ readonly serializedBy?: "editor-runtime" | "editor-app" | string;
55
+ readonly roundTripReady?: boolean;
56
+ readonly browserWorkflowReady?: boolean;
57
+ };
58
+ }
59
+
60
+ export interface EditorProjectEvidence {
61
+ readonly kind: "aura-editor-project-evidence";
62
+ readonly schema: EditorProjectDocument["schema"];
63
+ readonly version: number | null;
64
+ readonly name: string;
65
+ readonly nodeCount: number;
66
+ readonly assetCount: number;
67
+ readonly timelineCount: number;
68
+ readonly visualGraphCount: number;
69
+ readonly timelineBindingCount: number;
70
+ readonly signalMarkerCount: number;
71
+ readonly typedAssetEvidenceCount: number;
72
+ readonly hasAnimationTimeline: boolean;
73
+ readonly hasRuntimeReplayBindings: boolean;
74
+ readonly hasVisualGraphBindings: boolean;
75
+ readonly roundTripReady: boolean;
76
+ readonly evidence: {
77
+ readonly projectSerialization: true;
78
+ readonly timelineSerialization: boolean;
79
+ readonly visualGraphSerialization: boolean;
80
+ readonly runtimeReplayBindings: boolean;
81
+ readonly sourceLicenseAssetEvidence: boolean;
82
+ };
83
+ }
84
+
85
+ export function serializeEditorProject(project: EditorProjectDocument): string {
86
+ const normalized = validateEditorProject(project);
87
+ return `${JSON.stringify(normalized, null, 2)}\n`;
88
+ }
89
+
90
+ export function parseEditorProject(text: string): EditorProjectDocument {
91
+ const parsed = JSON.parse(text) as EditorProjectDocument;
92
+ return validateEditorProject(parsed);
93
+ }
94
+
95
+ export function validateEditorProject(project: EditorProjectDocument): EditorProjectDocument {
96
+ if (!isRecord(project)) throw new Error("Editor project document must be an object.");
97
+ if (project.schema !== "a3d-editor-project") throw new Error("Unsupported editor project schema.");
98
+ if (typeof project.name !== "string" || !project.name.trim()) throw new Error("Editor project name is required.");
99
+ if (!Array.isArray(project.nodes)) throw new Error("Editor project nodes must be an array.");
100
+ validateAssets(project.assets ?? []);
101
+ validateTimelines(project.timelines ?? []);
102
+ validateVisualGraphs(project.visualGraphs ?? []);
103
+ return project;
104
+ }
105
+
106
+ export function collectEditorProjectEvidence(project: EditorProjectDocument): EditorProjectEvidence {
107
+ const parsed = validateEditorProject(project);
108
+ const timelines = parsed.timelines ?? [];
109
+ const visualGraphs = parsed.visualGraphs ?? [];
110
+ const timelineBindingCount = timelines.reduce((total, timeline) => total + (timeline.bindings?.length ?? 0), 0);
111
+ const signalMarkerCount = timelines.reduce((total, timeline) => {
112
+ return total + (timeline.tracks ?? []).reduce((trackTotal, track) => {
113
+ if (track.type !== "signal") return trackTotal;
114
+ return trackTotal + (track.clips?.length ?? 0);
115
+ }, 0);
116
+ }, 0);
117
+ const typedAssetEvidenceCount = (parsed.assets ?? []).filter((asset) => {
118
+ return typeof asset !== "string" && Boolean(asset.id && asset.name && asset.source && asset.license);
119
+ }).length;
120
+ const assetCount = parsed.assets?.length ?? 0;
121
+ const hasAnimationTimeline = timelines.some((timeline) => (timeline.tracks ?? []).some((track) => track.type === "animation"));
122
+ const hasVisualGraphBindings = visualGraphs.some((graph) => (graph.runtimeBindings?.length ?? 0) > 0);
123
+ const hasRuntimeReplayBindings = timelineBindingCount > 0;
124
+ return {
125
+ kind: "aura-editor-project-evidence",
126
+ schema: parsed.schema,
127
+ version: parsed.version ?? null,
128
+ name: parsed.name,
129
+ nodeCount: parsed.nodes.length,
130
+ assetCount,
131
+ timelineCount: timelines.length,
132
+ visualGraphCount: visualGraphs.length,
133
+ timelineBindingCount,
134
+ signalMarkerCount,
135
+ typedAssetEvidenceCount,
136
+ hasAnimationTimeline,
137
+ hasRuntimeReplayBindings,
138
+ hasVisualGraphBindings,
139
+ roundTripReady: parsed.nodes.length > 0 && timelines.length > 0 && hasRuntimeReplayBindings,
140
+ evidence: {
141
+ projectSerialization: true,
142
+ timelineSerialization: timelines.length > 0,
143
+ visualGraphSerialization: visualGraphs.length > 0,
144
+ runtimeReplayBindings: hasRuntimeReplayBindings || hasVisualGraphBindings,
145
+ sourceLicenseAssetEvidence: assetCount > 0 && typedAssetEvidenceCount === assetCount
146
+ }
147
+ };
148
+ }
149
+
150
+ function validateAssets(assets: readonly (string | EditorProjectAssetDocument)[]): void {
151
+ if (!Array.isArray(assets)) throw new Error("Editor project assets must be an array.");
152
+ for (const asset of assets) {
153
+ if (typeof asset === "string") {
154
+ if (!asset.trim()) throw new Error("Editor project asset ids must be non-empty.");
155
+ continue;
156
+ }
157
+ if (!asset.id.trim()) throw new Error("Editor project asset id is required.");
158
+ if (!asset.name.trim()) throw new Error(`Editor project asset name is required: ${asset.id}`);
159
+ assertStringArray(asset.clips, `asset clips for ${asset.id}`);
160
+ assertStringArray(asset.morphTargets, `asset morphTargets for ${asset.id}`);
161
+ assertStringArray(asset.diagnostics, `asset diagnostics for ${asset.id}`);
162
+ }
163
+ }
164
+
165
+ function validateTimelines(timelines: readonly EditorProjectTimelineDocument[]): void {
166
+ if (!Array.isArray(timelines)) throw new Error("Editor project timelines must be an array.");
167
+ for (const timeline of timelines) {
168
+ new TimelineModel(timeline);
169
+ for (const binding of timeline.bindings ?? []) {
170
+ if (!binding.targetId.trim()) throw new Error(`Timeline binding targetId is required for timeline ${timeline.id ?? timeline.name ?? "unnamed"}.`);
171
+ if (binding.trackId !== undefined && !binding.trackId.trim()) throw new Error("Timeline binding trackId cannot be empty.");
172
+ if (binding.trackName !== undefined && !binding.trackName.trim()) throw new Error("Timeline binding trackName cannot be empty.");
173
+ }
174
+ }
175
+ }
176
+
177
+ function validateVisualGraphs(graphs: readonly EditorProjectVisualGraphDocument[]): void {
178
+ if (!Array.isArray(graphs)) throw new Error("Editor project visualGraphs must be an array.");
179
+ for (const graph of graphs) {
180
+ if (!graph.id.trim()) throw new Error("Editor project visual graph id is required.");
181
+ if (!graph.name.trim()) throw new Error(`Editor project visual graph name is required: ${graph.id}`);
182
+ if (!Array.isArray(graph.nodes)) throw new Error(`Editor project visual graph nodes must be an array: ${graph.id}`);
183
+ if (!Array.isArray(graph.edges)) throw new Error(`Editor project visual graph edges must be an array: ${graph.id}`);
184
+ for (const binding of graph.runtimeBindings ?? []) {
185
+ if (!binding.nodeId.trim()) throw new Error(`Visual graph runtime binding nodeId is required: ${graph.id}`);
186
+ if (!binding.targetId.trim()) throw new Error(`Visual graph runtime binding targetId is required: ${graph.id}`);
187
+ }
188
+ }
189
+ }
190
+
191
+ function assertStringArray(values: readonly string[] | undefined, label: string): void {
192
+ if (values === undefined) return;
193
+ if (!Array.isArray(values) || values.some((value) => !value.trim())) {
194
+ throw new Error(`Editor project ${label} must be non-empty strings.`);
195
+ }
196
+ }
197
+
198
+ function isRecord(value: unknown): value is Record<string, unknown> {
199
+ return typeof value === "object" && value !== null;
200
+ }
@@ -0,0 +1,35 @@
1
+ import { Gizmo, type GizmoDrag } from "./Gizmo";
2
+ import { TransformCommand, type TransformLike } from "./commands/TransformCommand";
3
+
4
+ export class RotateGizmo 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
+ next.rotation ??= { x: 0, y: 0, z: 0, w: 1 };
10
+ const delta = this.snapRotationDelta(input.delta);
11
+ if (input.axis === "x") next.rotation.x += delta;
12
+ if (input.axis === "y") next.rotation.y += delta;
13
+ if (input.axis === "z" || input.axis === "uniform") next.rotation.z += delta;
14
+ await this.history.execute(new TransformCommand(this.target, next));
15
+ }
16
+ }
17
+
18
+ function readTransform(target: NonNullable<RotateGizmo["target"]>): TransformLike {
19
+ if ("transform" in target) {
20
+ return {
21
+ position: { x: target.transform.position[0], y: target.transform.position[1], z: target.transform.position[2] },
22
+ rotation: { x: target.transform.rotation[0], y: target.transform.rotation[1], z: target.transform.rotation[2], w: target.transform.rotation[3] },
23
+ scale: { x: target.transform.scale[0], y: target.transform.scale[1], z: target.transform.scale[2] }
24
+ };
25
+ }
26
+ return clone(target);
27
+ }
28
+
29
+ function clone(transform: TransformLike): TransformLike {
30
+ return {
31
+ position: { ...transform.position },
32
+ rotation: transform.rotation ? { ...transform.rotation } : undefined,
33
+ scale: transform.scale ? { ...transform.scale } : undefined
34
+ };
35
+ }
@@ -0,0 +1,40 @@
1
+ import { Gizmo, type GizmoDrag } from "./Gizmo";
2
+ import { TransformCommand, type TransformLike } from "./commands/TransformCommand";
3
+
4
+ export class ScaleGizmo 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
+ next.scale ??= { x: 1, y: 1, z: 1 };
10
+ const amount = Math.max(0.001, this.snapScaleFactor(1 + input.delta));
11
+ if (input.axis === "x") next.scale.x *= amount;
12
+ if (input.axis === "y") next.scale.y *= amount;
13
+ if (input.axis === "z") next.scale.z *= amount;
14
+ if (input.axis === "uniform") {
15
+ next.scale.x *= amount;
16
+ next.scale.y *= amount;
17
+ next.scale.z *= amount;
18
+ }
19
+ await this.history.execute(new TransformCommand(this.target, next));
20
+ }
21
+ }
22
+
23
+ function readTransform(target: NonNullable<ScaleGizmo["target"]>): TransformLike {
24
+ if ("transform" in target) {
25
+ return {
26
+ position: { x: target.transform.position[0], y: target.transform.position[1], z: target.transform.position[2] },
27
+ rotation: { x: target.transform.rotation[0], y: target.transform.rotation[1], z: target.transform.rotation[2], w: target.transform.rotation[3] },
28
+ scale: { x: target.transform.scale[0], y: target.transform.scale[1], z: target.transform.scale[2] }
29
+ };
30
+ }
31
+ return clone(target);
32
+ }
33
+
34
+ function clone(transform: TransformLike): TransformLike {
35
+ return {
36
+ position: { ...transform.position },
37
+ rotation: transform.rotation ? { ...transform.rotation } : undefined,
38
+ scale: transform.scale ? { ...transform.scale } : undefined
39
+ };
40
+ }
@@ -0,0 +1,63 @@
1
+ export type SelectionId = string | number;
2
+
3
+ export interface SelectionChange {
4
+ readonly previous: readonly SelectionId[];
5
+ readonly current: readonly SelectionId[];
6
+ }
7
+
8
+ export type SelectionListener = (change: SelectionChange) => void;
9
+
10
+ export class Selection {
11
+ private readonly ids = new Set<SelectionId>();
12
+ private readonly listeners = new Set<SelectionListener>();
13
+
14
+ current(): readonly SelectionId[] {
15
+ return [...this.ids];
16
+ }
17
+
18
+ has(id: SelectionId): boolean {
19
+ return this.ids.has(id);
20
+ }
21
+
22
+ set(ids: readonly SelectionId[]): void {
23
+ this.replace(new Set(ids));
24
+ }
25
+
26
+ add(id: SelectionId): void {
27
+ this.replace(new Set([...this.ids, id]));
28
+ }
29
+
30
+ remove(id: SelectionId): void {
31
+ const next = new Set(this.ids);
32
+ next.delete(id);
33
+ this.replace(next);
34
+ }
35
+
36
+ clear(): void {
37
+ this.replace(new Set());
38
+ }
39
+
40
+ subscribe(listener: SelectionListener): () => void {
41
+ this.listeners.add(listener);
42
+ return () => this.listeners.delete(listener);
43
+ }
44
+
45
+ prune(alive: (id: SelectionId) => boolean): void {
46
+ this.replace(new Set([...this.ids].filter(alive)));
47
+ }
48
+
49
+ private replace(next: Set<SelectionId>): void {
50
+ const previous = this.current();
51
+ this.ids.clear();
52
+ for (const id of next) {
53
+ this.ids.add(id);
54
+ }
55
+ const current = this.current();
56
+ if (previous.length !== current.length || previous.some((id, index) => id !== current[index])) {
57
+ const change = { previous, current };
58
+ for (const listener of [...this.listeners]) {
59
+ listener(change);
60
+ }
61
+ }
62
+ }
63
+ }