@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type { Command, CommandContext } from "./Command";
|
|
2
|
+
export { CommandHistory, CommandTransactionError } from "./CommandHistory";
|
|
3
|
+
export { DiagnosticsOverlayModel } from "./DiagnosticsOverlayModel";
|
|
4
|
+
export type { EditorDiagnosticsInput, EditorDiagnosticsResource, EditorDiagnosticsSnapshot } from "./DiagnosticsOverlayModel";
|
|
5
|
+
export { EditorRuntime } from "./EditorRuntime";
|
|
6
|
+
export type { EditorMode, EditorRuntimeSnapshot } from "./EditorRuntime";
|
|
7
|
+
export { EditorStateModel, createMemoryEditorStateStorage } from "./EditorStateModel";
|
|
8
|
+
export type { EditorGridSnapSettings, EditorStateChange, EditorStateSnapshot, EditorStateStorage, EditorViewportSettings } from "./EditorStateModel";
|
|
9
|
+
export { EditorPluginHost } from "./EditorPluginHost";
|
|
10
|
+
export type {
|
|
11
|
+
EditorImporterContribution,
|
|
12
|
+
EditorPanelContribution,
|
|
13
|
+
EditorPlugin,
|
|
14
|
+
EditorPluginSnapshot,
|
|
15
|
+
EditorScriptingNodeContribution,
|
|
16
|
+
EditorToolContribution
|
|
17
|
+
} from "./EditorPluginHost";
|
|
18
|
+
export { Gizmo } from "./Gizmo";
|
|
19
|
+
export { DEFAULT_GIZMO_SETTINGS, normalizeGizmoSettings } from "./Gizmo";
|
|
20
|
+
export type { GizmoAxis, GizmoDrag, GizmoHandle, GizmoHit, GizmoPivotMode, GizmoPlaneAxis, GizmoSettings, GizmoSpaceMode } from "./Gizmo";
|
|
21
|
+
export { HierarchyModel } from "./HierarchyModel";
|
|
22
|
+
export type { HierarchyLikeNode, HierarchyNodeDescriptor } from "./HierarchyModel";
|
|
23
|
+
export { InspectorModel } from "./InspectorModel";
|
|
24
|
+
export type { InspectorEditableValue, InspectorProperty } from "./InspectorModel";
|
|
25
|
+
export { sampleLocalizationAccessibilityFixture } from "./LocalizationAccessibilityFixtures";
|
|
26
|
+
export type {
|
|
27
|
+
EditorAccessibilityElementSample,
|
|
28
|
+
EditorAccessibilityRole,
|
|
29
|
+
EditorLocalizationAccessibilityFixture,
|
|
30
|
+
EditorLocalizedStringSample,
|
|
31
|
+
EditorLocaleDescriptor,
|
|
32
|
+
EditorLocaleDirection,
|
|
33
|
+
EditorPluralCategory
|
|
34
|
+
} from "./LocalizationAccessibilityFixtures";
|
|
35
|
+
export { MaterialVariantWorkflow } from "./MaterialVariantWorkflow";
|
|
36
|
+
export type { MaterialVariantRenderOptions, MaterialVariantState } from "./MaterialVariantWorkflow";
|
|
37
|
+
export { PickingService } from "./PickingService";
|
|
38
|
+
export type { EditorPickHit, EditorPickTarget, EditorPickingColorId, EditorPickingEvidenceSnapshot } from "./PickingService";
|
|
39
|
+
export { PlayModeBridge } from "./PlayModeBridge";
|
|
40
|
+
export type { SnapshotAdapter } from "./PlayModeBridge";
|
|
41
|
+
export { PrefabRegistry, validatePrefab } from "./PrefabRegistry";
|
|
42
|
+
export type { CreatePrefabOptions, EditorPrefab, EditorPrefabNodeBase, EditorPrefabSchemaVersion, InstantiatePrefabOptions } from "./PrefabRegistry";
|
|
43
|
+
export { RotateGizmo } from "./RotateGizmo";
|
|
44
|
+
export { ScaleGizmo } from "./ScaleGizmo";
|
|
45
|
+
export { Selection } from "./Selection";
|
|
46
|
+
export type { SelectionChange, SelectionId, SelectionListener } from "./Selection";
|
|
47
|
+
export { createOldBranchShaderGraphFixture } from "./ShaderGraphModel";
|
|
48
|
+
export type { ShaderGraphDiagnostic, ShaderGraphEdge, ShaderGraphFixture, ShaderGraphNode, ShaderGraphPort, ShaderGraphValueType } from "./ShaderGraphModel";
|
|
49
|
+
export { createStaticExportHtml, createStaticExportRuntime } from "./StaticExportRuntime";
|
|
50
|
+
export type { StaticExportHtmlOptions, StaticExportRuntimeOptions } from "./StaticExportRuntime";
|
|
51
|
+
export * from "./ProjectSerializer.js";
|
|
52
|
+
export {
|
|
53
|
+
TimelineRuntimeBridge,
|
|
54
|
+
createTimelineRuntimeBridge
|
|
55
|
+
} from "./TimelineRuntimeBridge";
|
|
56
|
+
export type {
|
|
57
|
+
TimelineRuntimeAnimationApplication,
|
|
58
|
+
TimelineRuntimeBindingConfig,
|
|
59
|
+
TimelineRuntimeBridgeConfig,
|
|
60
|
+
TimelineRuntimeBridgeSnapshot,
|
|
61
|
+
TimelineRuntimeSignalDispatch,
|
|
62
|
+
TimelineRuntimeTarget,
|
|
63
|
+
TimelineRuntimeTargetSnapshot
|
|
64
|
+
} from "./TimelineRuntimeBridge";
|
|
65
|
+
export { TranslateGizmo } from "./TranslateGizmo";
|
|
66
|
+
export { TimelineClip, TimelineModel, TimelineTrack } from "./TimelineModel";
|
|
67
|
+
export type {
|
|
68
|
+
TimelineActiveClipSnapshot,
|
|
69
|
+
TimelineClipBlendMode,
|
|
70
|
+
TimelineClipConfig,
|
|
71
|
+
TimelineEasingName,
|
|
72
|
+
TimelineLoopMode,
|
|
73
|
+
TimelineModelConfig,
|
|
74
|
+
TimelineSignalEventSnapshot,
|
|
75
|
+
TimelineSnapshot,
|
|
76
|
+
TimelineTrackConfig,
|
|
77
|
+
TimelineTrackSnapshot
|
|
78
|
+
} from "./TimelineModel";
|
|
79
|
+
export { CreateNodeCommand } from "./commands/CreateNodeCommand";
|
|
80
|
+
export type { NodeContainer } from "./commands/CreateNodeCommand";
|
|
81
|
+
export { DeleteNodeCommand } from "./commands/DeleteNodeCommand";
|
|
82
|
+
export { ReparentNodeCommand } from "./commands/ReparentNodeCommand";
|
|
83
|
+
export { SetPropertyCommand } from "./commands/SetPropertyCommand";
|
|
84
|
+
export { TransformCommand } from "./commands/TransformCommand";
|
|
85
|
+
export type { SceneTransformTargetLike, TransformLike, TransformTarget } from "./commands/TransformCommand";
|
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Ray, Vector3 } from "@aura3d/math";
|
|
4
|
+
import { Scene } from "@aura3d/scene";
|
|
5
|
+
import {
|
|
6
|
+
CommandHistory,
|
|
7
|
+
CreateNodeCommand,
|
|
8
|
+
DeleteNodeCommand,
|
|
9
|
+
EditorRuntime,
|
|
10
|
+
InspectorModel,
|
|
11
|
+
PickingService,
|
|
12
|
+
Selection,
|
|
13
|
+
SetPropertyCommand,
|
|
14
|
+
TimelineClip,
|
|
15
|
+
TimelineModel,
|
|
16
|
+
TimelineTrack,
|
|
17
|
+
TransformCommand,
|
|
18
|
+
TranslateGizmo,
|
|
19
|
+
collectEditorProjectEvidence,
|
|
20
|
+
createTimelineRuntimeBridge,
|
|
21
|
+
parseEditorProject,
|
|
22
|
+
serializeEditorProject,
|
|
23
|
+
type Command,
|
|
24
|
+
type EditorProjectDocument,
|
|
25
|
+
type TimelineRuntimeAnimationApplication,
|
|
26
|
+
type TimelineRuntimeSignalDispatch
|
|
27
|
+
} from "../src/index";
|
|
28
|
+
|
|
29
|
+
test("CommandHistory executes undo and redo deterministically", async () => {
|
|
30
|
+
const target = { position: { x: 0, y: 0, z: 0 } };
|
|
31
|
+
const history = new CommandHistory();
|
|
32
|
+
|
|
33
|
+
await history.execute(new TransformCommand(target, { position: { x: 1, y: 2, z: 3 } }));
|
|
34
|
+
assert.deepEqual(target.position, { x: 1, y: 2, z: 3 });
|
|
35
|
+
await history.undo();
|
|
36
|
+
assert.deepEqual(target.position, { x: 0, y: 0, z: 0 });
|
|
37
|
+
await history.redo();
|
|
38
|
+
assert.deepEqual(target.position, { x: 1, y: 2, z: 3 });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("Selection emits changes and can prune deleted ids", () => {
|
|
42
|
+
const selection = new Selection();
|
|
43
|
+
const changes: unknown[] = [];
|
|
44
|
+
selection.subscribe((change) => changes.push(change));
|
|
45
|
+
selection.set(["a", "b"]);
|
|
46
|
+
selection.prune((id) => id !== "a");
|
|
47
|
+
assert.deepEqual(selection.current(), ["b"]);
|
|
48
|
+
assert.equal(changes.length, 2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("Create, delete, and set-property commands mutate through public command contract", async () => {
|
|
52
|
+
const nodes: string[] = [];
|
|
53
|
+
const container = {
|
|
54
|
+
add: (node: string) => nodes.push(node),
|
|
55
|
+
remove: (node: string) => {
|
|
56
|
+
const index = nodes.indexOf(node);
|
|
57
|
+
if (index >= 0) nodes.splice(index, 1);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const history = new CommandHistory();
|
|
61
|
+
await history.execute(new CreateNodeCommand(container, "node-a"));
|
|
62
|
+
await history.execute(new DeleteNodeCommand(container, "node-a"));
|
|
63
|
+
assert.deepEqual(nodes, []);
|
|
64
|
+
await history.undo();
|
|
65
|
+
assert.deepEqual(nodes, ["node-a"]);
|
|
66
|
+
|
|
67
|
+
const model = { transform: { visible: true } };
|
|
68
|
+
await history.execute(new SetPropertyCommand(model, ["transform", "visible"], false));
|
|
69
|
+
assert.equal(model.transform.visible, false);
|
|
70
|
+
await history.undo();
|
|
71
|
+
assert.equal(model.transform.visible, true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("CommandHistory transactions roll back partial execution", async () => {
|
|
75
|
+
const history = new CommandHistory();
|
|
76
|
+
const calls: string[] = [];
|
|
77
|
+
const first: Command = {
|
|
78
|
+
name: "first",
|
|
79
|
+
execute() {
|
|
80
|
+
calls.push("first:execute");
|
|
81
|
+
},
|
|
82
|
+
undo() {
|
|
83
|
+
calls.push("first:undo");
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const second: Command = {
|
|
87
|
+
name: "second",
|
|
88
|
+
execute() {
|
|
89
|
+
calls.push("second:execute");
|
|
90
|
+
throw new Error("second failed");
|
|
91
|
+
},
|
|
92
|
+
undo() {
|
|
93
|
+
calls.push("second:undo");
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await assert.rejects(() => history.executeTransaction([first, second]), /second failed/);
|
|
98
|
+
assert.deepEqual(calls, ["first:execute", "second:execute", "first:undo"]);
|
|
99
|
+
assert.equal(history.undoDepth, 0);
|
|
100
|
+
assert.equal(history.redoDepth, 0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("CommandHistory transactions undo and redo as one history entry", async () => {
|
|
104
|
+
const history = new CommandHistory();
|
|
105
|
+
const values: string[] = [];
|
|
106
|
+
const createPushCommand = (value: string): Command => ({
|
|
107
|
+
name: `push ${value}`,
|
|
108
|
+
execute() {
|
|
109
|
+
values.push(value);
|
|
110
|
+
},
|
|
111
|
+
undo() {
|
|
112
|
+
const index = values.lastIndexOf(value);
|
|
113
|
+
if (index >= 0) {
|
|
114
|
+
values.splice(index, 1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await history.executeTransaction([createPushCommand("a"), createPushCommand("b")]);
|
|
120
|
+
assert.deepEqual(values, ["a", "b"]);
|
|
121
|
+
assert.equal(history.undoDepth, 1);
|
|
122
|
+
|
|
123
|
+
await history.undo();
|
|
124
|
+
assert.deepEqual(values, []);
|
|
125
|
+
assert.equal(history.redoDepth, 1);
|
|
126
|
+
|
|
127
|
+
await history.redo();
|
|
128
|
+
assert.deepEqual(values, ["a", "b"]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("CommandHistory preserves history entries when undo or redo fail", async () => {
|
|
132
|
+
const undoHistory = new CommandHistory();
|
|
133
|
+
const failingUndo: Command = {
|
|
134
|
+
name: "failing undo",
|
|
135
|
+
execute() {},
|
|
136
|
+
undo() {
|
|
137
|
+
throw new Error("undo failed");
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
await undoHistory.execute(failingUndo);
|
|
141
|
+
await assert.rejects(() => undoHistory.undo(), /undo failed/);
|
|
142
|
+
assert.equal(undoHistory.undoDepth, 1);
|
|
143
|
+
assert.equal(undoHistory.redoDepth, 0);
|
|
144
|
+
|
|
145
|
+
const redoHistory = new CommandHistory();
|
|
146
|
+
let failRedo = false;
|
|
147
|
+
const failingRedo: Command = {
|
|
148
|
+
name: "failing redo",
|
|
149
|
+
execute() {
|
|
150
|
+
if (failRedo) {
|
|
151
|
+
throw new Error("redo failed");
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
undo() {}
|
|
155
|
+
};
|
|
156
|
+
await redoHistory.execute(failingRedo);
|
|
157
|
+
await redoHistory.undo();
|
|
158
|
+
failRedo = true;
|
|
159
|
+
await assert.rejects(() => redoHistory.redo(), /redo failed/);
|
|
160
|
+
assert.equal(redoHistory.undoDepth, 0);
|
|
161
|
+
assert.equal(redoHistory.redoDepth, 1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("DeleteNodeCommand restores scene parent and sibling order", async () => {
|
|
165
|
+
const scene = new Scene();
|
|
166
|
+
const parent = scene.createNode("parent");
|
|
167
|
+
const child = scene.createNode("child");
|
|
168
|
+
const sibling = scene.createNode("sibling");
|
|
169
|
+
scene.root.addChild(parent);
|
|
170
|
+
parent.addChild(child);
|
|
171
|
+
parent.addChild(sibling);
|
|
172
|
+
const container = {
|
|
173
|
+
add: (node: typeof child) => parent.addChild(node),
|
|
174
|
+
remove: (node: typeof child) => parent.removeChild(node)
|
|
175
|
+
};
|
|
176
|
+
const history = new CommandHistory();
|
|
177
|
+
|
|
178
|
+
await history.execute(new DeleteNodeCommand(container, child));
|
|
179
|
+
assert.equal(child.parent, null);
|
|
180
|
+
assert.deepEqual(parent.children.map((node) => node.name), ["sibling"]);
|
|
181
|
+
|
|
182
|
+
await history.undo();
|
|
183
|
+
assert.equal(child.parent, parent);
|
|
184
|
+
assert.deepEqual(parent.children.map((node) => node.name), ["child", "sibling"]);
|
|
185
|
+
|
|
186
|
+
await history.redo();
|
|
187
|
+
assert.equal(child.parent, null);
|
|
188
|
+
assert.deepEqual(parent.children.map((node) => node.name), ["sibling"]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("DeleteNodeCommand falls back to generic containers for unparented scene-shaped nodes", async () => {
|
|
192
|
+
const scene = new Scene();
|
|
193
|
+
const node = scene.createNode("loose");
|
|
194
|
+
const nodes = [node];
|
|
195
|
+
const container = {
|
|
196
|
+
add: (value: typeof node) => nodes.push(value),
|
|
197
|
+
remove: (value: typeof node) => {
|
|
198
|
+
const index = nodes.indexOf(value);
|
|
199
|
+
if (index >= 0) {
|
|
200
|
+
nodes.splice(index, 1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const history = new CommandHistory();
|
|
205
|
+
|
|
206
|
+
await history.execute(new DeleteNodeCommand(container, node));
|
|
207
|
+
assert.deepEqual(nodes, []);
|
|
208
|
+
|
|
209
|
+
await history.undo();
|
|
210
|
+
assert.deepEqual(nodes, [node]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("PickingService and TranslateGizmo use command history for scene node edits", async () => {
|
|
214
|
+
const scene = new Scene();
|
|
215
|
+
const node = scene.createNode("editable");
|
|
216
|
+
scene.root.addChild(node);
|
|
217
|
+
const picking = new PickingService();
|
|
218
|
+
picking.addTarget({ id: "editable", node, bounds: { min: [-1, -1, -1], max: [1, 1, 1] } });
|
|
219
|
+
picking.resizePickingBuffer(640, 360);
|
|
220
|
+
|
|
221
|
+
const evidence = picking.snapshot();
|
|
222
|
+
assert.equal(evidence.source, "origin-master-gpu-picking-adapted");
|
|
223
|
+
assert.equal(evidence.registeredTargetCount, 1);
|
|
224
|
+
assert.equal(evidence.width, 640);
|
|
225
|
+
assert.equal(evidence.height, 360);
|
|
226
|
+
assert.equal(evidence.sampleColorId?.targetId, "editable");
|
|
227
|
+
assert.deepEqual(evidence.sampleColorId?.color, [1, 0, 0, 255]);
|
|
228
|
+
assert.equal(evidence.decodedSampleTargetId, "editable");
|
|
229
|
+
assert.equal(picking.targetIdFromColor([1, 0, 0, 255]), "editable");
|
|
230
|
+
assert.equal(evidence.evidence.colorIdEncoding, true);
|
|
231
|
+
assert.equal(evidence.evidence.colorIdDecoding, true);
|
|
232
|
+
assert.equal(evidence.evidence.raycastFallback, true);
|
|
233
|
+
assert.ok(evidence.blockedClaims.includes("production GPU framebuffer picking pass"));
|
|
234
|
+
|
|
235
|
+
const hit = picking.pick(new Ray(new Vector3(0, 0, 5), new Vector3(0, 0, -1)));
|
|
236
|
+
assert.equal(hit?.target.node, node);
|
|
237
|
+
assert.equal(picking.snapshot().needsUpdate, false);
|
|
238
|
+
picking.invalidatePickingBuffer();
|
|
239
|
+
assert.equal(picking.snapshot().needsUpdate, true);
|
|
240
|
+
assert.throws(() => picking.resizePickingBuffer(0, 360), /positive integers/);
|
|
241
|
+
assert.throws(() => PickingService.idToColor(0), /1..16777215/);
|
|
242
|
+
|
|
243
|
+
const history = new CommandHistory();
|
|
244
|
+
const gizmo = new TranslateGizmo(history);
|
|
245
|
+
gizmo.setTarget(node);
|
|
246
|
+
await gizmo.drag({ axis: "x", delta: 2 });
|
|
247
|
+
assert.equal(node.transform.position[0], 2);
|
|
248
|
+
await history.undo();
|
|
249
|
+
assert.equal(node.transform.position[0], 0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("InspectorModel and EditorRuntime apply typed inspector edits through undo history", async () => {
|
|
253
|
+
const target = {
|
|
254
|
+
name: "cube",
|
|
255
|
+
visible: true,
|
|
256
|
+
transform: {
|
|
257
|
+
x: 1,
|
|
258
|
+
y: 2
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const inspector = new InspectorModel();
|
|
262
|
+
const history = new CommandHistory();
|
|
263
|
+
|
|
264
|
+
await history.execute(inspector.createSetPropertyCommand(target, ["transform", "x"], 4));
|
|
265
|
+
assert.equal(target.transform.x, 4);
|
|
266
|
+
await history.undo();
|
|
267
|
+
assert.equal(target.transform.x, 1);
|
|
268
|
+
assert.throws(() => inspector.createSetPropertyCommand(target, ["transform"], 0), /not editable/);
|
|
269
|
+
assert.throws(() => inspector.createSetPropertyCommand(target, ["visible"], "false"), /expected boolean/);
|
|
270
|
+
assert.throws(() => inspector.createSetPropertyCommand(target, ["missing"], true), /does not exist/);
|
|
271
|
+
|
|
272
|
+
const runtime = new EditorRuntime();
|
|
273
|
+
await runtime.editInspectedProperty(target, ["name"], "hero");
|
|
274
|
+
assert.equal(target.name, "hero");
|
|
275
|
+
await runtime.undo();
|
|
276
|
+
assert.equal(target.name, "cube");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("EditorRuntime exposes public selection, picking, transform, and diagnostics operations", async () => {
|
|
280
|
+
const scene = new Scene();
|
|
281
|
+
const node = scene.createNode("runtime-owned");
|
|
282
|
+
scene.root.addChild(node);
|
|
283
|
+
const runtime = new EditorRuntime();
|
|
284
|
+
|
|
285
|
+
runtime.select([node.id]);
|
|
286
|
+
assert.deepEqual(runtime.currentSelection(), [node.id]);
|
|
287
|
+
|
|
288
|
+
runtime.setPickTargets([{ id: node.id, node, bounds: { min: [-1, -1, -1], max: [1, 1, 1] } }]);
|
|
289
|
+
const hit = runtime.pick(new Ray(new Vector3(0, 0, 5), new Vector3(0, 0, -1)));
|
|
290
|
+
assert.equal(hit?.target.id, node.id);
|
|
291
|
+
assert.equal(runtime.snapshot().picking.decodedSampleTargetId, node.id);
|
|
292
|
+
|
|
293
|
+
await runtime.translateTarget(node, { axis: "x", delta: 1.5 });
|
|
294
|
+
assert.equal(node.transform.position[0], 2);
|
|
295
|
+
assert.equal(runtime.snapshot().undoDepth, 1);
|
|
296
|
+
|
|
297
|
+
const diagnostics = runtime.updateDiagnostics({
|
|
298
|
+
frameTimeMs: 2,
|
|
299
|
+
drawCalls: 1,
|
|
300
|
+
nodeCount: 1,
|
|
301
|
+
assetCount: 0,
|
|
302
|
+
physicsBodies: 0,
|
|
303
|
+
resources: [{ id: "shader-runtime", label: "Runtime shader", kind: "shader", status: "warning" }]
|
|
304
|
+
});
|
|
305
|
+
assert.equal(diagnostics.warnings, 1);
|
|
306
|
+
assert.equal(runtime.diagnosticsSnapshot().shaderWarnings[0]?.id, "shader-runtime");
|
|
307
|
+
|
|
308
|
+
runtime.clearSelection();
|
|
309
|
+
assert.deepEqual(runtime.currentSelection(), []);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("DiagnosticsOverlayModel validates profiler resource and shader diagnostics", () => {
|
|
313
|
+
const runtime = new EditorRuntime();
|
|
314
|
+
const snapshot = runtime.updateDiagnostics({
|
|
315
|
+
frameTimeMs: 4.5,
|
|
316
|
+
drawCalls: 2,
|
|
317
|
+
triangleCount: 12,
|
|
318
|
+
nodeCount: 3,
|
|
319
|
+
assetCount: 1,
|
|
320
|
+
physicsBodies: 1,
|
|
321
|
+
resources: [
|
|
322
|
+
{ id: "shader-main", label: "Main shader", kind: "shader", status: "warning", detail: "Missing normal map" },
|
|
323
|
+
{ id: "asset-model", label: "model.glb", kind: "asset", status: "ok" }
|
|
324
|
+
]
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
assert.equal(snapshot.warnings, 1);
|
|
328
|
+
assert.equal(snapshot.errors, 0);
|
|
329
|
+
assert.equal(snapshot.shaderWarnings[0]?.id, "shader-main");
|
|
330
|
+
assert.equal(runtime.snapshot().diagnostics.drawCalls, 2);
|
|
331
|
+
|
|
332
|
+
assert.throws(() => runtime.updateDiagnostics({
|
|
333
|
+
frameTimeMs: -1,
|
|
334
|
+
drawCalls: 0,
|
|
335
|
+
nodeCount: 0,
|
|
336
|
+
assetCount: 0,
|
|
337
|
+
physicsBodies: 0
|
|
338
|
+
}), /frame time/);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("TimelineModel ports bounded track, clip, easing, loop, lock, mute, and signal evidence", () => {
|
|
342
|
+
const timeline = new TimelineModel({
|
|
343
|
+
id: "timeline-port",
|
|
344
|
+
duration: 2,
|
|
345
|
+
loopMode: "loop",
|
|
346
|
+
speed: 1.5,
|
|
347
|
+
frameRate: 60,
|
|
348
|
+
tracks: [
|
|
349
|
+
{
|
|
350
|
+
id: "animation-track",
|
|
351
|
+
name: "Animation",
|
|
352
|
+
type: "animation",
|
|
353
|
+
clips: [
|
|
354
|
+
{
|
|
355
|
+
id: "run",
|
|
356
|
+
name: "Run",
|
|
357
|
+
startTime: 0,
|
|
358
|
+
duration: 1,
|
|
359
|
+
easeInDuration: 0.5,
|
|
360
|
+
easeIn: "ease-in-out",
|
|
361
|
+
blendMode: "mix",
|
|
362
|
+
weight: 0.8,
|
|
363
|
+
clipInOffset: 0.2
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: "signal-track",
|
|
369
|
+
name: "Signals",
|
|
370
|
+
type: "signal",
|
|
371
|
+
locked: true,
|
|
372
|
+
clips: [{ id: "footstep", name: "Footstep", clipName: "footstep", startTime: 0.25, duration: 0.1 }]
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: "muted-guide",
|
|
376
|
+
name: "Muted Guide",
|
|
377
|
+
type: "audio",
|
|
378
|
+
muted: true,
|
|
379
|
+
clips: [{ id: "beat", name: "Beat", startTime: 0, duration: 2 }]
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
timeline.seek(0.25);
|
|
385
|
+
const snapshot = timeline.snapshot();
|
|
386
|
+
assert.equal(snapshot.activeClipCount, 2);
|
|
387
|
+
assert.equal(snapshot.activeClips.find((clip) => clip.clipId === "run")?.blendWeight, 0.4);
|
|
388
|
+
assert.equal(snapshot.activeClips.find((clip) => clip.clipId === "run")?.assetTime, 0.45);
|
|
389
|
+
assert.deepEqual(snapshot.signalEvents, ["footstep"]);
|
|
390
|
+
assert.equal(snapshot.evidence.oldCodebasePort, true);
|
|
391
|
+
assert.equal(snapshot.evidence.clipEasing, true);
|
|
392
|
+
assert.equal(snapshot.evidence.clipBlending, true);
|
|
393
|
+
assert.equal(snapshot.evidence.muteLockState, true);
|
|
394
|
+
assert.equal(snapshot.evidence.loopPlayback, true);
|
|
395
|
+
assert.equal(snapshot.evidence.signalMarkers, true);
|
|
396
|
+
|
|
397
|
+
timeline.play();
|
|
398
|
+
timeline.tick(2);
|
|
399
|
+
assert.equal(timeline.snapshot().time, 1.25);
|
|
400
|
+
|
|
401
|
+
const lockedTrack = timeline.tracks.find((track) => track.id === "signal-track");
|
|
402
|
+
assert.throws(() => lockedTrack?.addClip(new TimelineClip({ name: "Late Signal", startTime: 1, duration: 0.1 })), /locked timeline track/);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("TimelineRuntimeBridge applies editor-authored clips and signals to runtime targets", () => {
|
|
406
|
+
const timeline = new TimelineModel({
|
|
407
|
+
id: "combat-timeline",
|
|
408
|
+
duration: 1.5,
|
|
409
|
+
loopMode: "none",
|
|
410
|
+
tracks: [
|
|
411
|
+
{
|
|
412
|
+
id: "fighter-animation",
|
|
413
|
+
name: "Fighter Animation",
|
|
414
|
+
type: "animation",
|
|
415
|
+
weight: 0.5,
|
|
416
|
+
clips: [
|
|
417
|
+
{
|
|
418
|
+
id: "light-punch",
|
|
419
|
+
name: "Light Punch",
|
|
420
|
+
startTime: 0,
|
|
421
|
+
duration: 0.8,
|
|
422
|
+
assetId: "fighter-glb",
|
|
423
|
+
clipName: "LightPunch",
|
|
424
|
+
blendMode: "mix",
|
|
425
|
+
weight: 0.8,
|
|
426
|
+
clipInOffset: 0.1,
|
|
427
|
+
properties: {
|
|
428
|
+
runtimeNodeId: "player",
|
|
429
|
+
authoringLane: "upper-body"
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
]
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: "combat-events",
|
|
436
|
+
name: "Combat Events",
|
|
437
|
+
type: "signal",
|
|
438
|
+
clips: [
|
|
439
|
+
{
|
|
440
|
+
id: "hitbox-open",
|
|
441
|
+
name: "Hitbox Open",
|
|
442
|
+
startTime: 0.25,
|
|
443
|
+
duration: 0.05,
|
|
444
|
+
properties: {
|
|
445
|
+
event: "hitbox.open",
|
|
446
|
+
targetId: "player",
|
|
447
|
+
hitbox: "right-fist"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
});
|
|
454
|
+
const applications: TimelineRuntimeAnimationApplication[] = [];
|
|
455
|
+
const signals: TimelineRuntimeSignalDispatch[] = [];
|
|
456
|
+
const bridge = createTimelineRuntimeBridge({
|
|
457
|
+
timeline,
|
|
458
|
+
bindings: [
|
|
459
|
+
{
|
|
460
|
+
trackId: "fighter-animation",
|
|
461
|
+
targetId: "player",
|
|
462
|
+
assetId: "fighter-glb",
|
|
463
|
+
clipNameMap: { LightPunch: "LightPunch" }
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
targets: [
|
|
467
|
+
{
|
|
468
|
+
id: "player",
|
|
469
|
+
applyTimelineAnimation(application) {
|
|
470
|
+
applications.push(application);
|
|
471
|
+
},
|
|
472
|
+
applyTimelineSignal(signal) {
|
|
473
|
+
signals.push(signal);
|
|
474
|
+
},
|
|
475
|
+
snapshot() {
|
|
476
|
+
return { applications: applications.length, signals: signals.length };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
]
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const first = bridge.applyAt(0.25);
|
|
483
|
+
assert.equal(applications.length, 1);
|
|
484
|
+
assert.equal(applications[0]?.targetId, "player");
|
|
485
|
+
assert.equal(applications[0]?.clipName, "LightPunch");
|
|
486
|
+
assert.equal(applications[0]?.assetTime, 0.35);
|
|
487
|
+
assert.equal(applications[0]?.blendWeight, 0.4);
|
|
488
|
+
assert.equal(signals.length, 1);
|
|
489
|
+
assert.equal(signals[0]?.event, "hitbox.open");
|
|
490
|
+
assert.equal(first.kind, "aura-editor-timeline-runtime-bridge");
|
|
491
|
+
assert.equal(first.evidence.timelineToRuntimeBridge, true);
|
|
492
|
+
assert.equal(first.evidence.animationClipBinding, true);
|
|
493
|
+
assert.equal(first.evidence.signalDispatch, true);
|
|
494
|
+
const targetSnapshot = first.targets[0]?.snapshot as { applications: number } | undefined;
|
|
495
|
+
assert.equal(targetSnapshot?.applications, 1);
|
|
496
|
+
|
|
497
|
+
bridge.applyAt(0.26);
|
|
498
|
+
assert.equal(signals.length, 1);
|
|
499
|
+
bridge.applyAt(0.25, { replaySignals: true });
|
|
500
|
+
assert.equal(signals.length, 2);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("ProjectSerializer round-trips editor timelines, runtime bindings, asset provenance, and visual graph hooks", () => {
|
|
504
|
+
const timeline = new TimelineModel({
|
|
505
|
+
id: "episode-timeline",
|
|
506
|
+
name: "Episode Timeline",
|
|
507
|
+
duration: 2,
|
|
508
|
+
tracks: [
|
|
509
|
+
{
|
|
510
|
+
id: "character-animation",
|
|
511
|
+
name: "Character Animation",
|
|
512
|
+
type: "animation",
|
|
513
|
+
clips: [{ id: "idle", name: "Idle", startTime: 0, duration: 2, assetId: "toon-glb", clipName: "Idle" }]
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: "markers",
|
|
517
|
+
name: "Markers",
|
|
518
|
+
type: "signal",
|
|
519
|
+
clips: [{ id: "mouth-aa", name: "Mouth AA", startTime: 0.4, duration: 0.05, properties: { event: "viseme", targetId: "toon", viseme: "AA" } }]
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
});
|
|
523
|
+
const project: EditorProjectDocument = {
|
|
524
|
+
schema: "a3d-editor-project",
|
|
525
|
+
version: 105,
|
|
526
|
+
name: "1.0.5 Editor Workflow",
|
|
527
|
+
nodes: [{ id: "toon", name: "Typed Toon", runtimeNodeId: "toon" }],
|
|
528
|
+
assets: [
|
|
529
|
+
{
|
|
530
|
+
id: "toon-glb",
|
|
531
|
+
name: "toon.glb",
|
|
532
|
+
type: "glb",
|
|
533
|
+
uri: "assets/toon.glb",
|
|
534
|
+
source: "Aura3D typed asset catalog fixture",
|
|
535
|
+
license: "CC0",
|
|
536
|
+
clips: ["Idle"],
|
|
537
|
+
morphTargets: ["AA"]
|
|
538
|
+
}
|
|
539
|
+
],
|
|
540
|
+
timelines: [
|
|
541
|
+
{
|
|
542
|
+
...timeline.toConfig(),
|
|
543
|
+
bindings: [{ trackId: "character-animation", targetId: "toon", assetId: "toon-glb" }],
|
|
544
|
+
evidence: {
|
|
545
|
+
authoredInEditor: true,
|
|
546
|
+
runtimeReplay: true,
|
|
547
|
+
animationEvents: true
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
],
|
|
551
|
+
visualGraphs: [
|
|
552
|
+
{
|
|
553
|
+
id: "graph-viseme",
|
|
554
|
+
name: "Viseme Graph",
|
|
555
|
+
nodes: [{ id: "on-viseme", type: "event" }],
|
|
556
|
+
edges: [],
|
|
557
|
+
runtimeBindings: [{ nodeId: "on-viseme", targetId: "toon", event: "viseme" }]
|
|
558
|
+
}
|
|
559
|
+
],
|
|
560
|
+
editor: {
|
|
561
|
+
selectedNodeId: "toon",
|
|
562
|
+
activeTool: "timeline",
|
|
563
|
+
activeTimelineId: "episode-timeline",
|
|
564
|
+
playMode: "edit"
|
|
565
|
+
},
|
|
566
|
+
evidence: {
|
|
567
|
+
serializedBy: "editor-runtime",
|
|
568
|
+
roundTripReady: true,
|
|
569
|
+
browserWorkflowReady: false
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const serialized = serializeEditorProject(project);
|
|
574
|
+
assert.match(serialized, /"timelines"/);
|
|
575
|
+
assert.match(serialized, /"visualGraphs"/);
|
|
576
|
+
const parsed = parseEditorProject(serialized);
|
|
577
|
+
assert.equal(parsed.timelines?.[0]?.bindings?.[0]?.targetId, "toon");
|
|
578
|
+
assert.equal(parsed.timelines?.[0]?.tracks?.[1]?.clips?.[0]?.properties?.event, "viseme");
|
|
579
|
+
|
|
580
|
+
const evidence = collectEditorProjectEvidence(parsed);
|
|
581
|
+
assert.equal(evidence.kind, "aura-editor-project-evidence");
|
|
582
|
+
assert.equal(evidence.timelineCount, 1);
|
|
583
|
+
assert.equal(evidence.visualGraphCount, 1);
|
|
584
|
+
assert.equal(evidence.timelineBindingCount, 1);
|
|
585
|
+
assert.equal(evidence.signalMarkerCount, 1);
|
|
586
|
+
assert.equal(evidence.typedAssetEvidenceCount, 1);
|
|
587
|
+
assert.equal(evidence.roundTripReady, true);
|
|
588
|
+
assert.equal(evidence.evidence.timelineSerialization, true);
|
|
589
|
+
assert.equal(evidence.evidence.visualGraphSerialization, true);
|
|
590
|
+
assert.equal(evidence.evidence.runtimeReplayBindings, true);
|
|
591
|
+
assert.equal(evidence.evidence.sourceLicenseAssetEvidence, true);
|
|
592
|
+
|
|
593
|
+
assert.throws(() => serializeEditorProject({
|
|
594
|
+
...project,
|
|
595
|
+
timelines: [{ ...timeline.toConfig(), bindings: [{ trackId: "character-animation", targetId: "" }] }]
|
|
596
|
+
}), /targetId is required/);
|
|
597
|
+
});
|