@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,336 @@
|
|
|
1
|
+
import { normalizeGizmoSettings, type GizmoPivotMode, type GizmoSettings, type GizmoSpaceMode } from "./Gizmo";
|
|
2
|
+
|
|
3
|
+
export interface EditorStateStorage {
|
|
4
|
+
getItem(key: string): string | null;
|
|
5
|
+
setItem(key: string, value: string): void;
|
|
6
|
+
removeItem?(key: string): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface EditorViewportSettings {
|
|
10
|
+
readonly showGrid: boolean;
|
|
11
|
+
readonly gridSize: number;
|
|
12
|
+
readonly gridSubdivisions: number;
|
|
13
|
+
readonly showAxisGizmo: boolean;
|
|
14
|
+
readonly showStats: boolean;
|
|
15
|
+
readonly showWireframe: boolean;
|
|
16
|
+
readonly showBounds: boolean;
|
|
17
|
+
readonly showLights: boolean;
|
|
18
|
+
readonly showCameras: boolean;
|
|
19
|
+
readonly fov: number;
|
|
20
|
+
readonly near: number;
|
|
21
|
+
readonly far: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EditorGridSnapSettings {
|
|
25
|
+
readonly snapToGrid: boolean;
|
|
26
|
+
readonly positionSnap: number;
|
|
27
|
+
readonly rotationSnapEnabled: boolean;
|
|
28
|
+
readonly rotationSnapDegrees: number;
|
|
29
|
+
readonly scaleSnapEnabled: boolean;
|
|
30
|
+
readonly scaleSnap: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EditorStateSnapshot {
|
|
34
|
+
readonly activeTool: string;
|
|
35
|
+
readonly transformSpace: GizmoSpaceMode;
|
|
36
|
+
readonly pivotMode: GizmoPivotMode;
|
|
37
|
+
readonly viewport: EditorViewportSettings;
|
|
38
|
+
readonly gridSnap: EditorGridSnapSettings;
|
|
39
|
+
readonly persisted: boolean;
|
|
40
|
+
readonly storageKey: string;
|
|
41
|
+
readonly changeCount: number;
|
|
42
|
+
readonly evidence: {
|
|
43
|
+
readonly oldCodebasePort: true;
|
|
44
|
+
readonly persistentEditorState: boolean;
|
|
45
|
+
readonly viewportSettings: boolean;
|
|
46
|
+
readonly gridSnapSettings: boolean;
|
|
47
|
+
readonly transformSpacePivotMode: boolean;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface EditorStateChange {
|
|
52
|
+
readonly property: string;
|
|
53
|
+
readonly oldValue: unknown;
|
|
54
|
+
readonly newValue: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface EditorStateModelOptions {
|
|
58
|
+
readonly storage?: EditorStateStorage;
|
|
59
|
+
readonly storageKey?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type Listener = (change: EditorStateChange) => void;
|
|
63
|
+
|
|
64
|
+
const defaultViewportSettings: EditorViewportSettings = {
|
|
65
|
+
showGrid: true,
|
|
66
|
+
gridSize: 10,
|
|
67
|
+
gridSubdivisions: 10,
|
|
68
|
+
showAxisGizmo: true,
|
|
69
|
+
showStats: true,
|
|
70
|
+
showWireframe: false,
|
|
71
|
+
showBounds: false,
|
|
72
|
+
showLights: true,
|
|
73
|
+
showCameras: true,
|
|
74
|
+
fov: 60,
|
|
75
|
+
near: 0.1,
|
|
76
|
+
far: 1000
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const defaultGridSnapSettings: EditorGridSnapSettings = {
|
|
80
|
+
snapToGrid: true,
|
|
81
|
+
positionSnap: 1,
|
|
82
|
+
rotationSnapEnabled: false,
|
|
83
|
+
rotationSnapDegrees: 15,
|
|
84
|
+
scaleSnapEnabled: false,
|
|
85
|
+
scaleSnap: 0.1
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export class EditorStateModel {
|
|
89
|
+
private activeToolRef = "select";
|
|
90
|
+
private transformSpaceRef: GizmoSpaceMode = "world";
|
|
91
|
+
private pivotModeRef: GizmoPivotMode = "center";
|
|
92
|
+
private viewportRef: EditorViewportSettings = defaultViewportSettings;
|
|
93
|
+
private gridSnapRef: EditorGridSnapSettings = defaultGridSnapSettings;
|
|
94
|
+
private readonly listeners = new Set<Listener>();
|
|
95
|
+
private readonly storageKey: string;
|
|
96
|
+
private readonly storage: EditorStateStorage | undefined;
|
|
97
|
+
private persisted = false;
|
|
98
|
+
private changes = 0;
|
|
99
|
+
|
|
100
|
+
constructor(options: EditorStateModelOptions = {}) {
|
|
101
|
+
this.storage = options.storage;
|
|
102
|
+
this.storageKey = options.storageKey ?? "aura3d:editor-state:externalParity";
|
|
103
|
+
this.load();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get activeTool(): string {
|
|
107
|
+
return this.activeToolRef;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get transformSpace(): GizmoSpaceMode {
|
|
111
|
+
return this.transformSpaceRef;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get pivotMode(): GizmoPivotMode {
|
|
115
|
+
return this.pivotModeRef;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setActiveTool(tool: string): void {
|
|
119
|
+
const normalized = tool.trim();
|
|
120
|
+
if (normalized.length === 0) throw new Error("Editor state active tool cannot be empty.");
|
|
121
|
+
this.setValue("activeTool", this.activeToolRef, normalized, (value) => {
|
|
122
|
+
this.activeToolRef = value;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setTransformSpace(space: GizmoSpaceMode): void {
|
|
127
|
+
this.setValue("transformSpace", this.transformSpaceRef, space, (value) => {
|
|
128
|
+
this.transformSpaceRef = value;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setPivotMode(mode: GizmoPivotMode): void {
|
|
133
|
+
this.setValue("pivotMode", this.pivotModeRef, mode, (value) => {
|
|
134
|
+
this.pivotModeRef = value;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
configureViewport(settings: Partial<EditorViewportSettings>): EditorViewportSettings {
|
|
139
|
+
const next = normalizeViewportSettings({ ...this.viewportRef, ...settings });
|
|
140
|
+
this.setValue("viewport", this.viewportRef, next, (value) => {
|
|
141
|
+
this.viewportRef = value;
|
|
142
|
+
});
|
|
143
|
+
return this.viewportRef;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
configureGridSnap(settings: Partial<EditorGridSnapSettings>): EditorGridSnapSettings {
|
|
147
|
+
const next = normalizeGridSnapSettings({ ...this.gridSnapRef, ...settings });
|
|
148
|
+
this.setValue("gridSnap", this.gridSnapRef, next, (value) => {
|
|
149
|
+
this.gridSnapRef = value;
|
|
150
|
+
});
|
|
151
|
+
return this.gridSnapRef;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
configureFromGizmoSettings(settings: GizmoSettings): void {
|
|
155
|
+
this.setTransformSpace(settings.spaceMode);
|
|
156
|
+
this.setPivotMode(settings.pivotMode);
|
|
157
|
+
this.configureGridSnap({
|
|
158
|
+
snapToGrid: settings.snapEnabled,
|
|
159
|
+
positionSnap: settings.positionSnap,
|
|
160
|
+
rotationSnapEnabled: settings.snapEnabled,
|
|
161
|
+
rotationSnapDegrees: settings.rotationSnapDegrees,
|
|
162
|
+
scaleSnapEnabled: settings.snapEnabled,
|
|
163
|
+
scaleSnap: settings.scaleSnap
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
gizmoSettings(): GizmoSettings {
|
|
168
|
+
return normalizeGizmoSettings({
|
|
169
|
+
snapEnabled: this.gridSnapRef.snapToGrid,
|
|
170
|
+
positionSnap: this.gridSnapRef.positionSnap,
|
|
171
|
+
rotationSnapDegrees: this.gridSnapRef.rotationSnapDegrees,
|
|
172
|
+
scaleSnap: this.gridSnapRef.scaleSnap,
|
|
173
|
+
spaceMode: this.transformSpaceRef,
|
|
174
|
+
pivotMode: this.pivotModeRef
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
snapPosition(value: number): number {
|
|
179
|
+
return this.gridSnapRef.snapToGrid ? snap(value, this.gridSnapRef.positionSnap) : value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
snapRotation(radians: number): number {
|
|
183
|
+
return this.gridSnapRef.rotationSnapEnabled ? snap(radians, this.gridSnapRef.rotationSnapDegrees * Math.PI / 180) : radians;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
snapScale(value: number): number {
|
|
187
|
+
return this.gridSnapRef.scaleSnapEnabled ? snap(value, this.gridSnapRef.scaleSnap) : value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
subscribe(listener: Listener): () => void {
|
|
191
|
+
this.listeners.add(listener);
|
|
192
|
+
return () => this.listeners.delete(listener);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
snapshot(): EditorStateSnapshot {
|
|
196
|
+
return {
|
|
197
|
+
activeTool: this.activeToolRef,
|
|
198
|
+
transformSpace: this.transformSpaceRef,
|
|
199
|
+
pivotMode: this.pivotModeRef,
|
|
200
|
+
viewport: this.viewportRef,
|
|
201
|
+
gridSnap: this.gridSnapRef,
|
|
202
|
+
persisted: this.persisted,
|
|
203
|
+
storageKey: this.storageKey,
|
|
204
|
+
changeCount: this.changes,
|
|
205
|
+
evidence: {
|
|
206
|
+
oldCodebasePort: true,
|
|
207
|
+
persistentEditorState: this.persisted,
|
|
208
|
+
viewportSettings: this.viewportRef.gridSize > 0 && this.viewportRef.fov > 0 && this.viewportRef.far > this.viewportRef.near,
|
|
209
|
+
gridSnapSettings: this.gridSnapRef.positionSnap > 0 && this.gridSnapRef.rotationSnapDegrees > 0 && this.gridSnapRef.scaleSnap > 0,
|
|
210
|
+
transformSpacePivotMode: Boolean(this.transformSpaceRef) && Boolean(this.pivotModeRef)
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
reset(): void {
|
|
216
|
+
this.activeToolRef = "select";
|
|
217
|
+
this.transformSpaceRef = "world";
|
|
218
|
+
this.pivotModeRef = "center";
|
|
219
|
+
this.viewportRef = defaultViewportSettings;
|
|
220
|
+
this.gridSnapRef = defaultGridSnapSettings;
|
|
221
|
+
this.changes += 1;
|
|
222
|
+
this.persist();
|
|
223
|
+
this.emit({ property: "reset", oldValue: null, newValue: this.snapshot() });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private setValue<T>(property: string, current: T, next: T, apply: (value: T) => void): void {
|
|
227
|
+
if (deepEqual(current, next)) return;
|
|
228
|
+
apply(next);
|
|
229
|
+
this.changes += 1;
|
|
230
|
+
this.persist();
|
|
231
|
+
this.emit({ property, oldValue: current, newValue: next });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private emit(change: EditorStateChange): void {
|
|
235
|
+
for (const listener of this.listeners) listener(change);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private persist(): void {
|
|
239
|
+
if (!this.storage) return;
|
|
240
|
+
this.storage.setItem(this.storageKey, JSON.stringify({
|
|
241
|
+
activeTool: this.activeToolRef,
|
|
242
|
+
transformSpace: this.transformSpaceRef,
|
|
243
|
+
pivotMode: this.pivotModeRef,
|
|
244
|
+
viewport: this.viewportRef,
|
|
245
|
+
gridSnap: this.gridSnapRef
|
|
246
|
+
}));
|
|
247
|
+
this.persisted = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private load(): void {
|
|
251
|
+
if (!this.storage) return;
|
|
252
|
+
const text = this.storage.getItem(this.storageKey);
|
|
253
|
+
if (!text) return;
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(text) as unknown;
|
|
256
|
+
if (!isRecord(parsed)) return;
|
|
257
|
+
this.activeToolRef = typeof parsed.activeTool === "string" && parsed.activeTool.trim().length > 0 ? parsed.activeTool : this.activeToolRef;
|
|
258
|
+
this.transformSpaceRef = parsed.transformSpace === "local" ? "local" : "world";
|
|
259
|
+
this.pivotModeRef = parsed.pivotMode === "active" || parsed.pivotMode === "individual" ? parsed.pivotMode : "center";
|
|
260
|
+
this.viewportRef = normalizeViewportSettings({ ...this.viewportRef, ...(isRecord(parsed.viewport) ? parsed.viewport : {}) });
|
|
261
|
+
this.gridSnapRef = normalizeGridSnapSettings({ ...this.gridSnapRef, ...(isRecord(parsed.gridSnap) ? parsed.gridSnap : {}) });
|
|
262
|
+
this.persisted = true;
|
|
263
|
+
} catch {
|
|
264
|
+
this.persisted = false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createMemoryEditorStateStorage(initial: Record<string, string> = {}): EditorStateStorage & { readonly values: Record<string, string> } {
|
|
270
|
+
const values = { ...initial };
|
|
271
|
+
return {
|
|
272
|
+
values,
|
|
273
|
+
getItem(key) {
|
|
274
|
+
return values[key] ?? null;
|
|
275
|
+
},
|
|
276
|
+
setItem(key, value) {
|
|
277
|
+
values[key] = value;
|
|
278
|
+
},
|
|
279
|
+
removeItem(key) {
|
|
280
|
+
delete values[key];
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalizeViewportSettings(settings: Partial<EditorViewportSettings>): EditorViewportSettings {
|
|
286
|
+
return {
|
|
287
|
+
showGrid: booleanOr(settings.showGrid, defaultViewportSettings.showGrid),
|
|
288
|
+
gridSize: positive(settings.gridSize, defaultViewportSettings.gridSize),
|
|
289
|
+
gridSubdivisions: Math.max(1, Math.round(positive(settings.gridSubdivisions, defaultViewportSettings.gridSubdivisions))),
|
|
290
|
+
showAxisGizmo: booleanOr(settings.showAxisGizmo, defaultViewportSettings.showAxisGizmo),
|
|
291
|
+
showStats: booleanOr(settings.showStats, defaultViewportSettings.showStats),
|
|
292
|
+
showWireframe: booleanOr(settings.showWireframe, defaultViewportSettings.showWireframe),
|
|
293
|
+
showBounds: booleanOr(settings.showBounds, defaultViewportSettings.showBounds),
|
|
294
|
+
showLights: booleanOr(settings.showLights, defaultViewportSettings.showLights),
|
|
295
|
+
showCameras: booleanOr(settings.showCameras, defaultViewportSettings.showCameras),
|
|
296
|
+
fov: clamp(positive(settings.fov, defaultViewportSettings.fov), 10, 140),
|
|
297
|
+
near: positive(settings.near, defaultViewportSettings.near),
|
|
298
|
+
far: Math.max(positive(settings.far, defaultViewportSettings.far), positive(settings.near, defaultViewportSettings.near) + 1)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function normalizeGridSnapSettings(settings: Partial<EditorGridSnapSettings>): EditorGridSnapSettings {
|
|
303
|
+
return {
|
|
304
|
+
snapToGrid: booleanOr(settings.snapToGrid, defaultGridSnapSettings.snapToGrid),
|
|
305
|
+
positionSnap: positive(settings.positionSnap, defaultGridSnapSettings.positionSnap),
|
|
306
|
+
rotationSnapEnabled: booleanOr(settings.rotationSnapEnabled, defaultGridSnapSettings.rotationSnapEnabled),
|
|
307
|
+
rotationSnapDegrees: positive(settings.rotationSnapDegrees, defaultGridSnapSettings.rotationSnapDegrees),
|
|
308
|
+
scaleSnapEnabled: booleanOr(settings.scaleSnapEnabled, defaultGridSnapSettings.scaleSnapEnabled),
|
|
309
|
+
scaleSnap: positive(settings.scaleSnap, defaultGridSnapSettings.scaleSnap)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function booleanOr(value: unknown, fallback: boolean): boolean {
|
|
314
|
+
return typeof value === "boolean" ? value : fallback;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function positive(value: unknown, fallback: number): number {
|
|
318
|
+
const number = Number(value);
|
|
319
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function clamp(value: number, min: number, max: number): number {
|
|
323
|
+
return Math.max(min, Math.min(max, value));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function snap(value: number, increment: number): number {
|
|
327
|
+
return Math.round(value / increment) * increment;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function deepEqual(left: unknown, right: unknown): boolean {
|
|
331
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
335
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
336
|
+
}
|
package/src/Gizmo.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Ray } from "@aura3d/math";
|
|
2
|
+
import type { CommandHistory } from "./CommandHistory";
|
|
3
|
+
import type { TransformTarget } from "./commands/TransformCommand";
|
|
4
|
+
|
|
5
|
+
export type GizmoAxis = "x" | "y" | "z" | "uniform";
|
|
6
|
+
export type GizmoPlaneAxis = "xy" | "xz" | "yz";
|
|
7
|
+
export type GizmoHandle = GizmoAxis | GizmoPlaneAxis;
|
|
8
|
+
export type GizmoSpaceMode = "world" | "local";
|
|
9
|
+
export type GizmoPivotMode = "center" | "active" | "individual";
|
|
10
|
+
|
|
11
|
+
export interface GizmoSettings {
|
|
12
|
+
readonly snapEnabled: boolean;
|
|
13
|
+
readonly positionSnap: number;
|
|
14
|
+
readonly rotationSnapDegrees: number;
|
|
15
|
+
readonly scaleSnap: number;
|
|
16
|
+
readonly spaceMode: GizmoSpaceMode;
|
|
17
|
+
readonly pivotMode: GizmoPivotMode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_GIZMO_SETTINGS: GizmoSettings = {
|
|
21
|
+
snapEnabled: false,
|
|
22
|
+
positionSnap: 1,
|
|
23
|
+
rotationSnapDegrees: 15,
|
|
24
|
+
scaleSnap: 0.1,
|
|
25
|
+
spaceMode: "world",
|
|
26
|
+
pivotMode: "center"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface GizmoHit {
|
|
30
|
+
readonly axis: GizmoHandle;
|
|
31
|
+
readonly distance: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GizmoDrag {
|
|
35
|
+
readonly axis: GizmoHandle;
|
|
36
|
+
readonly delta: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export abstract class Gizmo {
|
|
40
|
+
enabled = true;
|
|
41
|
+
target?: TransformTarget;
|
|
42
|
+
protected settings: GizmoSettings = DEFAULT_GIZMO_SETTINGS;
|
|
43
|
+
|
|
44
|
+
constructor(protected readonly history: CommandHistory, settings: Partial<GizmoSettings> = {}) {
|
|
45
|
+
this.configure(settings);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setTarget(target: TransformTarget | undefined): void {
|
|
49
|
+
this.target = target;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
configure(settings: Partial<GizmoSettings>): void {
|
|
53
|
+
this.settings = normalizeGizmoSettings({ ...this.settings, ...settings });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
snapshotSettings(): GizmoSettings {
|
|
57
|
+
return this.settings;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
hitTest(_ray: Ray): GizmoHit | undefined {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
abstract drag(input: GizmoDrag): Promise<void>;
|
|
65
|
+
|
|
66
|
+
dispose(): void {
|
|
67
|
+
this.enabled = false;
|
|
68
|
+
this.target = undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected snapPositionDelta(value: number): number {
|
|
72
|
+
return this.settings.snapEnabled ? snap(value, this.settings.positionSnap) : value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected snapRotationDelta(radians: number): number {
|
|
76
|
+
return this.settings.snapEnabled ? snap(radians, this.settings.rotationSnapDegrees * Math.PI / 180) : radians;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected snapScaleFactor(value: number): number {
|
|
80
|
+
return this.settings.snapEnabled ? snap(value, this.settings.scaleSnap) : value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function normalizeGizmoSettings(settings: Partial<GizmoSettings> = {}): GizmoSettings {
|
|
85
|
+
return {
|
|
86
|
+
snapEnabled: settings.snapEnabled ?? DEFAULT_GIZMO_SETTINGS.snapEnabled,
|
|
87
|
+
positionSnap: positive(settings.positionSnap, DEFAULT_GIZMO_SETTINGS.positionSnap),
|
|
88
|
+
rotationSnapDegrees: positive(settings.rotationSnapDegrees, DEFAULT_GIZMO_SETTINGS.rotationSnapDegrees),
|
|
89
|
+
scaleSnap: positive(settings.scaleSnap, DEFAULT_GIZMO_SETTINGS.scaleSnap),
|
|
90
|
+
spaceMode: settings.spaceMode ?? DEFAULT_GIZMO_SETTINGS.spaceMode,
|
|
91
|
+
pivotMode: settings.pivotMode ?? DEFAULT_GIZMO_SETTINGS.pivotMode
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function positive(value: number | undefined, fallback: number): number {
|
|
96
|
+
return Number.isFinite(value) && Number(value) > 0 ? Number(value) : fallback;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function snap(value: number, increment: number): number {
|
|
100
|
+
return Math.round(value / increment) * increment;
|
|
101
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SelectionId } from "./Selection";
|
|
2
|
+
|
|
3
|
+
export interface HierarchyLikeNode {
|
|
4
|
+
readonly id?: SelectionId;
|
|
5
|
+
readonly name?: string;
|
|
6
|
+
readonly visible?: boolean;
|
|
7
|
+
readonly children?: readonly HierarchyLikeNode[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HierarchyNodeDescriptor {
|
|
11
|
+
readonly id: SelectionId;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly visible: boolean;
|
|
14
|
+
readonly selected: boolean;
|
|
15
|
+
readonly depth: number;
|
|
16
|
+
readonly childCount: number;
|
|
17
|
+
readonly children: readonly HierarchyNodeDescriptor[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class HierarchyModel {
|
|
21
|
+
describe(root: HierarchyLikeNode, selectedIds: ReadonlySet<SelectionId> = new Set()): HierarchyNodeDescriptor {
|
|
22
|
+
return describeNode(root, selectedIds, 0, "root");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
flatten(root: HierarchyLikeNode, selectedIds: ReadonlySet<SelectionId> = new Set()): readonly HierarchyNodeDescriptor[] {
|
|
26
|
+
const output: HierarchyNodeDescriptor[] = [];
|
|
27
|
+
const visit = (node: HierarchyNodeDescriptor): void => {
|
|
28
|
+
output.push(node);
|
|
29
|
+
for (const child of node.children) {
|
|
30
|
+
visit(child);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
visit(this.describe(root, selectedIds));
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function describeNode(
|
|
39
|
+
node: HierarchyLikeNode,
|
|
40
|
+
selectedIds: ReadonlySet<SelectionId>,
|
|
41
|
+
depth: number,
|
|
42
|
+
fallbackId: SelectionId
|
|
43
|
+
): HierarchyNodeDescriptor {
|
|
44
|
+
const id = node.id ?? fallbackId;
|
|
45
|
+
const name = node.name ?? String(id);
|
|
46
|
+
const visible = node.visible ?? true;
|
|
47
|
+
const children = (node.children ?? []).map((child, index) => describeNode(child, selectedIds, depth + 1, `${id}:${index}`));
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
name,
|
|
51
|
+
visible,
|
|
52
|
+
selected: selectedIds.has(id),
|
|
53
|
+
depth,
|
|
54
|
+
childCount: children.length,
|
|
55
|
+
children
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { SetPropertyCommand } from "./commands/SetPropertyCommand";
|
|
2
|
+
|
|
3
|
+
export type InspectorEditableValue = number | string | boolean;
|
|
4
|
+
|
|
5
|
+
export interface InspectorProperty {
|
|
6
|
+
readonly path: readonly string[];
|
|
7
|
+
readonly type: "number" | "string" | "boolean" | "object";
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
readonly editable: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class InspectorModel {
|
|
13
|
+
describe(target: object): readonly InspectorProperty[] {
|
|
14
|
+
return this.describeObject(target);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
createSetPropertyCommand<T extends object>(
|
|
18
|
+
target: T,
|
|
19
|
+
path: readonly string[],
|
|
20
|
+
value: InspectorEditableValue
|
|
21
|
+
): SetPropertyCommand<T> {
|
|
22
|
+
const property = this.describe(target).find((descriptor) => samePath(descriptor.path, path));
|
|
23
|
+
if (!property) {
|
|
24
|
+
throw new Error(`Inspector property path does not exist: ${path.join(".")}`);
|
|
25
|
+
}
|
|
26
|
+
if (!property.editable) {
|
|
27
|
+
throw new Error(`Inspector property is not editable: ${path.join(".")}`);
|
|
28
|
+
}
|
|
29
|
+
this.assertValueMatchesProperty(property, value);
|
|
30
|
+
return new SetPropertyCommand(target, path, value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private describeObject(target: object, prefix: readonly string[] = []): InspectorProperty[] {
|
|
34
|
+
const properties: InspectorProperty[] = [];
|
|
35
|
+
for (const [key, value] of Object.entries(target)) {
|
|
36
|
+
const path = [...prefix, key];
|
|
37
|
+
const type = this.propertyType(value);
|
|
38
|
+
properties.push({
|
|
39
|
+
path,
|
|
40
|
+
type,
|
|
41
|
+
value,
|
|
42
|
+
editable: type !== "object"
|
|
43
|
+
});
|
|
44
|
+
if (type === "object" && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
45
|
+
properties.push(...this.describeObject(value as object, path));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return properties;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private propertyType(value: unknown): InspectorProperty["type"] {
|
|
52
|
+
const valueType = typeof value;
|
|
53
|
+
if (valueType === "number" || valueType === "string" || valueType === "boolean") {
|
|
54
|
+
return valueType;
|
|
55
|
+
}
|
|
56
|
+
return "object";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private assertValueMatchesProperty(property: InspectorProperty, value: InspectorEditableValue): void {
|
|
60
|
+
if (property.type === "number" && !Number.isFinite(value)) {
|
|
61
|
+
throw new Error(`Inspector property ${property.path.join(".")} expected a finite number.`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== property.type) {
|
|
64
|
+
throw new Error(`Inspector property ${property.path.join(".")} expected ${property.type}, got ${typeof value}.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function samePath(left: readonly string[], right: readonly string[]): boolean {
|
|
70
|
+
return left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
71
|
+
}
|