@fps-games/editor 0.1.1-beta.1 → 0.1.1-beta.10
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/local-editor-harness.d.ts +113 -6
- package/dist/local-editor-harness.d.ts.map +1 -1
- package/dist/local-editor-harness.js +1253 -55
- package/dist/local-editor-harness.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +33 -0
- package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +231 -0
- package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +3 -0
- package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +8 -0
- package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +5 -0
- package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/index.js +5 -0
- package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +40 -0
- package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +237 -0
- package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js +15 -50
- package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +27 -0
- package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection.js +368 -14
- package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.d.ts +25 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.js +176 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.js.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts +18 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js +142 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts +2 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js +6 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.d.ts +20 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.js +187 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.js.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.d.ts +20 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.js +188 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.js.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts +22 -2
- package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +469 -90
- package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +20 -0
- package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/package.json +4 -4
- package/node_modules/@fps-games/editor-browser/dist/index.d.ts +2 -0
- package/node_modules/@fps-games/editor-browser/dist/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/index.js +1 -0
- package/node_modules/@fps-games/editor-browser/dist/index.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.d.ts +3 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js +103 -48
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +17 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +327 -33
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts +3 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +143 -17
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js +0 -2
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +27 -20
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts +10 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js +253 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js.map +1 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +403 -110
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts +13 -3
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js +107 -14
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +20 -3
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +127 -16
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +13 -4
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts +4 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js +114 -9
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts +11 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js +148 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js.map +1 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +141 -3
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +35 -0
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +376 -2
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +11 -6
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +1617 -110
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/package.json +4 -1
- package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts +4 -1
- package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/editor-session.js +13 -3
- package/node_modules/@fps-games/editor-core/dist/editor-session.js.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/index.d.ts +3 -0
- package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/index.js +3 -0
- package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/inspector.d.ts +6 -1
- package/node_modules/@fps-games/editor-core/dist/inspector.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/inspector.js +6 -1
- package/node_modules/@fps-games/editor-core/dist/inspector.js.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts +7 -0
- package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/scene-graph.js.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts +1 -1
- package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts +12 -0
- package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-core/dist/selection-strategy.js +52 -0
- package/node_modules/@fps-games/editor-core/dist/selection-strategy.js.map +1 -0
- package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts +78 -2
- package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js +81 -1
- package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/transform-operations.d.ts +32 -0
- package/node_modules/@fps-games/editor-core/dist/transform-operations.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-core/dist/transform-operations.js +125 -0
- package/node_modules/@fps-games/editor-core/dist/transform-operations.js.map +1 -0
- package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts +94 -0
- package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-core/dist/viewport-tools.js +58 -0
- package/node_modules/@fps-games/editor-core/dist/viewport-tools.js.map +1 -0
- package/node_modules/@fps-games/editor-core/package.json +2 -2
- package/node_modules/@fps-games/editor-forge-play/dist/index.d.ts +1 -1
- package/node_modules/@fps-games/editor-forge-play/dist/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-forge-play/dist/index.js +5 -1
- package/node_modules/@fps-games/editor-forge-play/dist/index.js.map +1 -1
- package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
- package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +4 -0
- package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-protocol/dist/index.js +4 -0
- package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
- package/node_modules/@fps-games/editor-protocol/package.json +1 -1
- package/package.json +6 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createEditorSession, createInspectorRegistry, createInspectorEditPayload, mergeInspectorSections, serializedMultiObjectToInspectorObject, serializedObjectToInspectorObject, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphGroupSelection, validateSceneGraphMove, validateSceneGraphRename, } from '@fps-games/editor-core';
|
|
1
|
+
import { createEditorSession, createInspectorRegistry, DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, DEFAULT_EDITOR_VIEWPORT_TOOL_STATE, createEmptyEditorViewportSpatialOverlayState, cloneEditorViewportToolState, cloneEditorViewportSpatialOverlayState, createInspectorEditPayload, computeEditorTransformActionTargets, mergeInspectorSections, normalizeEditorTransformConstraint, resolveEditorSelectionCommand, serializedMultiObjectToInspectorObject, serializedObjectToInspectorObject, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphGroupSelection, validateSceneGraphMove, validateSceneGraphRename, } from '@fps-games/editor-core';
|
|
2
2
|
import { createLocalEditorBrowserUi, } from '@fps-games/editor-browser';
|
|
3
|
-
import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
|
|
3
|
+
import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonSceneCameraPreviewController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonSceneViewMeasurementController, createBabylonSceneViewSpatialOverlayController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
|
|
4
4
|
export function createLocalEditorHarness(options) {
|
|
5
5
|
const root = options.root ?? document.body;
|
|
6
6
|
const state = {
|
|
@@ -13,15 +13,27 @@ export function createLocalEditorHarness(options) {
|
|
|
13
13
|
babylon: null,
|
|
14
14
|
engine: null,
|
|
15
15
|
world: null,
|
|
16
|
+
grid: null,
|
|
17
|
+
gridVisible: true,
|
|
16
18
|
projection: null,
|
|
17
19
|
gizmo: null,
|
|
20
|
+
sceneCameraPreview: null,
|
|
21
|
+
sceneCameraPreviewEnabled: false,
|
|
18
22
|
sceneViewInput: null,
|
|
19
23
|
sceneViewCamera: null,
|
|
24
|
+
sceneViewMeasurement: null,
|
|
25
|
+
sceneViewSpatialOverlay: null,
|
|
20
26
|
selectionController: null,
|
|
21
27
|
boxSelection: null,
|
|
22
28
|
transformTool: 'select',
|
|
23
29
|
transformSpace: 'world',
|
|
24
30
|
transformConstraint: 'axis',
|
|
31
|
+
transformOperationSettings: cloneTransformOperationSettings(DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS),
|
|
32
|
+
viewportTools: cloneEditorViewportToolState(DEFAULT_EDITOR_VIEWPORT_TOOL_STATE),
|
|
33
|
+
viewportMeasurement: createEmptyViewportMeasurement(),
|
|
34
|
+
viewportSpatialOverlay: createEmptyEditorViewportSpatialOverlayState(),
|
|
35
|
+
duplicateDrag: null,
|
|
36
|
+
armedPlacement: null,
|
|
25
37
|
resizeHandler: null,
|
|
26
38
|
status: 'Game running',
|
|
27
39
|
statusTone: 'default',
|
|
@@ -32,7 +44,12 @@ export function createLocalEditorHarness(options) {
|
|
|
32
44
|
let harness;
|
|
33
45
|
const ui = createLocalEditorBrowserUi({
|
|
34
46
|
root,
|
|
47
|
+
theme: options.theme,
|
|
48
|
+
localTestActions: options.localTestActions,
|
|
35
49
|
inspector: options.inspector,
|
|
50
|
+
hierarchy: {
|
|
51
|
+
contextActions: createBrowserHierarchyContextActions(state, options),
|
|
52
|
+
},
|
|
36
53
|
callbacks: {
|
|
37
54
|
onEnterEditor: () => {
|
|
38
55
|
void runExclusive(state, harness.render, () => harness.enterEditor());
|
|
@@ -55,13 +72,17 @@ export function createLocalEditorHarness(options) {
|
|
|
55
72
|
harness.render();
|
|
56
73
|
},
|
|
57
74
|
onCreateFromAsset: (assetId) => {
|
|
58
|
-
if (
|
|
75
|
+
if (createAssetFromBrowserIntent(state, options, assetId))
|
|
59
76
|
harness.render();
|
|
60
77
|
},
|
|
61
78
|
onSelectHierarchyItem: (input) => {
|
|
62
79
|
if (selectItem(state, options, input))
|
|
63
80
|
harness.render();
|
|
64
81
|
},
|
|
82
|
+
onSelectionCommand: (command) => {
|
|
83
|
+
if (dispatchSelectionCommand(state, options, command))
|
|
84
|
+
harness.render();
|
|
85
|
+
},
|
|
65
86
|
onSceneGraphRename: (intent) => {
|
|
66
87
|
if (renameSceneGraphNode(state, options, intent))
|
|
67
88
|
harness.render();
|
|
@@ -70,10 +91,25 @@ export function createLocalEditorHarness(options) {
|
|
|
70
91
|
if (createSceneGraphGroup(state, options, intent))
|
|
71
92
|
harness.render();
|
|
72
93
|
},
|
|
94
|
+
onSceneGraphCreatePrimitive: options.documentAdapter.createSceneGraphCreatePrimitivePatch
|
|
95
|
+
? (intent) => {
|
|
96
|
+
if (createSceneGraphPrimitive(state, options, intent))
|
|
97
|
+
harness.render();
|
|
98
|
+
}
|
|
99
|
+
: undefined,
|
|
73
100
|
onSceneGraphDelete: (intent) => {
|
|
74
101
|
if (deleteSceneGraphNodes(state, options, intent))
|
|
75
102
|
harness.render();
|
|
76
103
|
},
|
|
104
|
+
onSceneGraphDuplicate: options.documentAdapter.createDuplicateSelectionPatch
|
|
105
|
+
? (intent) => {
|
|
106
|
+
if (duplicateSceneGraphNodes(state, options, {
|
|
107
|
+
targetIds: intent.targetIds,
|
|
108
|
+
activeId: intent.activeId ?? null,
|
|
109
|
+
}))
|
|
110
|
+
harness.render();
|
|
111
|
+
}
|
|
112
|
+
: undefined,
|
|
77
113
|
onSceneGraphDrop: (intent) => {
|
|
78
114
|
if (dropSceneGraphNode(state, options, intent))
|
|
79
115
|
harness.render();
|
|
@@ -102,7 +138,9 @@ export function createLocalEditorHarness(options) {
|
|
|
102
138
|
},
|
|
103
139
|
onTransformToolChange: (tool) => {
|
|
104
140
|
state.transformTool = tool;
|
|
141
|
+
state.transformConstraint = normalizeTransformConstraint(tool, state.transformConstraint);
|
|
105
142
|
state.gizmo?.setTool(tool);
|
|
143
|
+
state.gizmo?.setConstraint(state.transformConstraint);
|
|
106
144
|
harness.render();
|
|
107
145
|
},
|
|
108
146
|
onTransformSpaceChange: (space) => {
|
|
@@ -111,14 +149,87 @@ export function createLocalEditorHarness(options) {
|
|
|
111
149
|
harness.render();
|
|
112
150
|
},
|
|
113
151
|
onTransformConstraintChange: (constraint) => {
|
|
114
|
-
state.transformConstraint = constraint;
|
|
115
|
-
state.gizmo?.setConstraint(
|
|
152
|
+
state.transformConstraint = normalizeTransformConstraint(state.transformTool, constraint);
|
|
153
|
+
state.gizmo?.setConstraint(state.transformConstraint);
|
|
154
|
+
harness.render();
|
|
155
|
+
},
|
|
156
|
+
onTransformSnapEnabledChange: (enabled) => {
|
|
157
|
+
state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
|
|
158
|
+
snap: {
|
|
159
|
+
...state.transformOperationSettings.snap,
|
|
160
|
+
enabled,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
state.gizmo?.setOperationSettings(state.transformOperationSettings);
|
|
164
|
+
state.status = enabled ? 'Transform snap enabled' : 'Transform snap disabled';
|
|
116
165
|
harness.render();
|
|
117
166
|
},
|
|
167
|
+
onTransformSnapStepChange: (input) => {
|
|
168
|
+
const value = normalizePositiveStep(input.value);
|
|
169
|
+
if (value == null)
|
|
170
|
+
return;
|
|
171
|
+
const snap = { ...state.transformOperationSettings.snap };
|
|
172
|
+
if (input.kind === 'move')
|
|
173
|
+
snap.moveStep = value;
|
|
174
|
+
else if (input.kind === 'rotate')
|
|
175
|
+
snap.rotateStepDegrees = value;
|
|
176
|
+
else
|
|
177
|
+
snap.scaleStep = value;
|
|
178
|
+
state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, { snap });
|
|
179
|
+
state.gizmo?.setOperationSettings(state.transformOperationSettings);
|
|
180
|
+
state.status = `Transform snap ${input.kind} step ${value}`;
|
|
181
|
+
harness.render();
|
|
182
|
+
},
|
|
183
|
+
onPlacementModeChange: (mode) => {
|
|
184
|
+
state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
|
|
185
|
+
placementMode: normalizePlacementMode(mode),
|
|
186
|
+
});
|
|
187
|
+
state.gizmo?.setOperationSettings(state.transformOperationSettings);
|
|
188
|
+
if (state.transformOperationSettings.placementMode === 'off')
|
|
189
|
+
clearArmedPlacement(state);
|
|
190
|
+
state.status = `Placement mode: ${state.transformOperationSettings.placementMode}`;
|
|
191
|
+
harness.render();
|
|
192
|
+
},
|
|
193
|
+
onTransformAction: (action) => {
|
|
194
|
+
if (executeTransformAction(state, options, action))
|
|
195
|
+
harness.render();
|
|
196
|
+
},
|
|
197
|
+
onViewportViewPresetChange: (preset) => {
|
|
198
|
+
if (setViewportViewPreset(state, preset))
|
|
199
|
+
harness.render();
|
|
200
|
+
},
|
|
201
|
+
onViewportProjectionModeChange: (mode) => {
|
|
202
|
+
if (setViewportProjectionMode(state, mode))
|
|
203
|
+
harness.render();
|
|
204
|
+
},
|
|
205
|
+
onViewportOverlaySettingsChange: (settings) => {
|
|
206
|
+
if (setViewportOverlaySettings(state, settings))
|
|
207
|
+
harness.render();
|
|
208
|
+
},
|
|
209
|
+
onViewportUtilityToolChange: (tool) => {
|
|
210
|
+
if (setViewportUtilityTool(state, tool))
|
|
211
|
+
harness.render();
|
|
212
|
+
},
|
|
213
|
+
onViewportMeasurementClear: () => {
|
|
214
|
+
if (clearViewportMeasurement(state))
|
|
215
|
+
harness.render();
|
|
216
|
+
},
|
|
217
|
+
onSceneCameraPreviewToggle: (enabled) => {
|
|
218
|
+
if (setSceneCameraPreviewEnabled(state, options, enabled))
|
|
219
|
+
harness.render();
|
|
220
|
+
},
|
|
221
|
+
onGridVisibleChange: (visible) => {
|
|
222
|
+
if (setGridVisible(state, visible))
|
|
223
|
+
harness.render();
|
|
224
|
+
},
|
|
118
225
|
onFocusSelection: () => {
|
|
119
226
|
if (focusSelectedProjection(state))
|
|
120
227
|
harness.render();
|
|
121
228
|
},
|
|
229
|
+
onCancelEditorIntent: () => {
|
|
230
|
+
if (cancelEditorIntent(state, options))
|
|
231
|
+
harness.render();
|
|
232
|
+
},
|
|
122
233
|
onCancelActiveOperation: () => {
|
|
123
234
|
cancelActiveOperation(state);
|
|
124
235
|
harness.render();
|
|
@@ -127,14 +238,59 @@ export function createLocalEditorHarness(options) {
|
|
|
127
238
|
});
|
|
128
239
|
harness = {
|
|
129
240
|
render() {
|
|
241
|
+
syncSceneCameraPreview(state, options);
|
|
242
|
+
syncViewportCameraState(state);
|
|
243
|
+
syncViewportMeasurementState(state);
|
|
244
|
+
syncViewportSpatialOverlay(state);
|
|
130
245
|
ui.update(createUiState(state, options));
|
|
131
246
|
},
|
|
247
|
+
setTheme(theme) {
|
|
248
|
+
ui.setTheme(theme);
|
|
249
|
+
},
|
|
250
|
+
getTheme() {
|
|
251
|
+
return ui.getTheme();
|
|
252
|
+
},
|
|
132
253
|
getHostServices() {
|
|
133
254
|
return options.hostServices ?? null;
|
|
134
255
|
},
|
|
135
256
|
getWorkingDocument() {
|
|
136
257
|
return state.session?.getState().workingDocument ?? null;
|
|
137
258
|
},
|
|
259
|
+
async reloadAssets() {
|
|
260
|
+
try {
|
|
261
|
+
const assets = await options.persistenceAdapter.loadAssets();
|
|
262
|
+
state.assets = assets;
|
|
263
|
+
clearArmedPlacement(state);
|
|
264
|
+
state.status = `Assets reloaded; assets=${assets.length}`;
|
|
265
|
+
state.statusTone = 'success';
|
|
266
|
+
state.statusToneStatus = state.status;
|
|
267
|
+
state.statusDetails = '';
|
|
268
|
+
harness.render();
|
|
269
|
+
return {
|
|
270
|
+
ok: true,
|
|
271
|
+
assetCount: assets.length,
|
|
272
|
+
status: state.status,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
state.status = 'Asset reload failed';
|
|
277
|
+
state.statusTone = 'error';
|
|
278
|
+
state.statusToneStatus = state.status;
|
|
279
|
+
state.statusDetails = error instanceof Error ? error.message : String(error);
|
|
280
|
+
harness.render();
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
assetCount: state.assets.length,
|
|
284
|
+
status: state.status,
|
|
285
|
+
error: state.statusDetails,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
createAssetFromAssetId(assetId, createOptions) {
|
|
290
|
+
const result = addAssetToDocument(state, options, assetId, createOptions);
|
|
291
|
+
harness.render();
|
|
292
|
+
return result;
|
|
293
|
+
},
|
|
138
294
|
async enterEditor() {
|
|
139
295
|
const loadedSource = options.persistenceAdapter.loadAuthoringSource
|
|
140
296
|
? await options.persistenceAdapter.loadAuthoringSource()
|
|
@@ -268,6 +424,267 @@ export function mergeLocalEditorHarnessInspectorComponentSections(input) {
|
|
|
268
424
|
}),
|
|
269
425
|
};
|
|
270
426
|
}
|
|
427
|
+
function normalizeTransformConstraint(tool, constraint) {
|
|
428
|
+
return normalizeEditorTransformConstraint(tool, constraint) ?? 'axis';
|
|
429
|
+
}
|
|
430
|
+
function cloneTransformOperationSettings(settings) {
|
|
431
|
+
return {
|
|
432
|
+
snap: {
|
|
433
|
+
enabled: settings.snap.enabled,
|
|
434
|
+
moveStep: settings.snap.moveStep,
|
|
435
|
+
rotateStepDegrees: settings.snap.rotateStepDegrees,
|
|
436
|
+
scaleStep: settings.snap.scaleStep,
|
|
437
|
+
},
|
|
438
|
+
placementMode: settings.placementMode,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function updateTransformOperationSettings(current, patch) {
|
|
442
|
+
return {
|
|
443
|
+
...current,
|
|
444
|
+
...patch,
|
|
445
|
+
snap: patch.snap
|
|
446
|
+
? { ...current.snap, ...patch.snap }
|
|
447
|
+
: { ...current.snap },
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function normalizePositiveStep(value) {
|
|
451
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
452
|
+
return null;
|
|
453
|
+
return Number(value.toFixed(4));
|
|
454
|
+
}
|
|
455
|
+
function normalizePlacementMode(mode) {
|
|
456
|
+
return mode === 'ground' || mode === 'surface' ? mode : 'off';
|
|
457
|
+
}
|
|
458
|
+
function normalizeViewportViewPreset(preset) {
|
|
459
|
+
return preset === 'top' || preset === 'front' || preset === 'right' ? preset : 'perspective';
|
|
460
|
+
}
|
|
461
|
+
function normalizeViewportProjectionMode(mode) {
|
|
462
|
+
return mode === 'orthographic' ? 'orthographic' : 'perspective';
|
|
463
|
+
}
|
|
464
|
+
function createEmptyViewportMeasurement() {
|
|
465
|
+
return {
|
|
466
|
+
active: false,
|
|
467
|
+
start: null,
|
|
468
|
+
end: null,
|
|
469
|
+
preview: null,
|
|
470
|
+
distance: null,
|
|
471
|
+
screenStart: null,
|
|
472
|
+
screenEnd: null,
|
|
473
|
+
screenPreview: null,
|
|
474
|
+
label: null,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function cloneViewportMeasurement(measurement) {
|
|
478
|
+
return {
|
|
479
|
+
active: measurement.active,
|
|
480
|
+
start: measurement.start ? { ...measurement.start } : null,
|
|
481
|
+
end: measurement.end ? { ...measurement.end } : null,
|
|
482
|
+
preview: measurement.preview ? { ...measurement.preview } : null,
|
|
483
|
+
distance: measurement.distance,
|
|
484
|
+
screenStart: measurement.screenStart ? { ...measurement.screenStart } : null,
|
|
485
|
+
screenEnd: measurement.screenEnd ? { ...measurement.screenEnd } : null,
|
|
486
|
+
screenPreview: measurement.screenPreview ? { ...measurement.screenPreview } : null,
|
|
487
|
+
label: measurement.label ? { ...measurement.label } : null,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function setViewportViewPreset(state, preset) {
|
|
491
|
+
if (state.sceneCameraPreviewEnabled) {
|
|
492
|
+
state.status = 'Viewport view unavailable during Scene Camera preview';
|
|
493
|
+
state.statusTone = 'warning';
|
|
494
|
+
state.statusToneStatus = state.status;
|
|
495
|
+
state.statusDetails = 'Disable Scene Camera preview before switching editor viewport views.';
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
const nextPreset = normalizeViewportViewPreset(preset);
|
|
499
|
+
const target = resolveViewportPresetTarget(state);
|
|
500
|
+
const radius = resolveViewportPresetRadius(state);
|
|
501
|
+
const changed = state.sceneViewCamera?.setViewPreset(nextPreset, { target, radius }) ?? false;
|
|
502
|
+
if (!changed)
|
|
503
|
+
return false;
|
|
504
|
+
const cameraState = state.sceneViewCamera?.getState();
|
|
505
|
+
state.viewportTools = {
|
|
506
|
+
...state.viewportTools,
|
|
507
|
+
viewPreset: cameraState?.viewPreset ?? nextPreset,
|
|
508
|
+
projectionMode: cameraState?.projectionMode ?? (nextPreset === 'perspective' ? 'perspective' : 'orthographic'),
|
|
509
|
+
};
|
|
510
|
+
state.status = `Viewport view: ${state.viewportTools.viewPreset}`;
|
|
511
|
+
state.statusTone = 'default';
|
|
512
|
+
state.statusToneStatus = state.status;
|
|
513
|
+
state.statusDetails = '';
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
function setViewportProjectionMode(state, mode) {
|
|
517
|
+
if (state.sceneCameraPreviewEnabled) {
|
|
518
|
+
state.status = 'Viewport projection unavailable during Scene Camera preview';
|
|
519
|
+
state.statusTone = 'warning';
|
|
520
|
+
state.statusToneStatus = state.status;
|
|
521
|
+
state.statusDetails = 'Disable Scene Camera preview before switching editor viewport projection.';
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
const nextMode = normalizeViewportProjectionMode(mode);
|
|
525
|
+
const changed = state.sceneViewCamera?.setProjectionMode(nextMode) ?? false;
|
|
526
|
+
if (!changed)
|
|
527
|
+
return false;
|
|
528
|
+
const cameraState = state.sceneViewCamera?.getState();
|
|
529
|
+
state.viewportTools = {
|
|
530
|
+
...state.viewportTools,
|
|
531
|
+
projectionMode: cameraState?.projectionMode ?? nextMode,
|
|
532
|
+
};
|
|
533
|
+
state.status = `Viewport projection: ${state.viewportTools.projectionMode}`;
|
|
534
|
+
state.statusTone = 'default';
|
|
535
|
+
state.statusToneStatus = state.status;
|
|
536
|
+
state.statusDetails = '';
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
function setViewportOverlaySettings(state, settings) {
|
|
540
|
+
state.viewportTools = {
|
|
541
|
+
...state.viewportTools,
|
|
542
|
+
overlay: {
|
|
543
|
+
...state.viewportTools.overlay,
|
|
544
|
+
bounds: settings.bounds ?? state.viewportTools.overlay.bounds,
|
|
545
|
+
dimensions: settings.dimensions ?? state.viewportTools.overlay.dimensions,
|
|
546
|
+
edgeLengths: settings.edgeLengths ?? state.viewportTools.overlay.edgeLengths,
|
|
547
|
+
anchor: settings.anchor ?? state.viewportTools.overlay.anchor,
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
state.status = 'Viewport overlay settings updated';
|
|
551
|
+
state.statusTone = 'default';
|
|
552
|
+
state.statusToneStatus = state.status;
|
|
553
|
+
state.statusDetails = '';
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
function setViewportUtilityTool(state, tool) {
|
|
557
|
+
const nextTool = tool === 'measure-distance' ? 'measure-distance' : 'none';
|
|
558
|
+
state.viewportTools = {
|
|
559
|
+
...state.viewportTools,
|
|
560
|
+
activeUtilityTool: nextTool,
|
|
561
|
+
};
|
|
562
|
+
if (nextTool === 'measure-distance') {
|
|
563
|
+
state.viewportMeasurement = {
|
|
564
|
+
...cloneViewportMeasurement(state.viewportMeasurement),
|
|
565
|
+
active: !!state.viewportMeasurement.start && !state.viewportMeasurement.end,
|
|
566
|
+
};
|
|
567
|
+
state.status = 'Measure distance: pick first XZ ground point';
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
const unfinishedMeasurement = state.viewportMeasurement.active
|
|
571
|
+
|| (!!state.viewportMeasurement.start && !state.viewportMeasurement.end);
|
|
572
|
+
state.viewportMeasurement = unfinishedMeasurement
|
|
573
|
+
? state.sceneViewMeasurement?.clear() ?? createEmptyViewportMeasurement()
|
|
574
|
+
: {
|
|
575
|
+
...cloneViewportMeasurement(state.viewportMeasurement),
|
|
576
|
+
active: false,
|
|
577
|
+
preview: null,
|
|
578
|
+
screenPreview: null,
|
|
579
|
+
};
|
|
580
|
+
state.status = 'Viewport utility: none';
|
|
581
|
+
}
|
|
582
|
+
state.statusTone = 'default';
|
|
583
|
+
state.statusToneStatus = state.status;
|
|
584
|
+
state.statusDetails = '';
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
function clearViewportMeasurement(state) {
|
|
588
|
+
state.viewportMeasurement = state.sceneViewMeasurement?.clear() ?? createEmptyViewportMeasurement();
|
|
589
|
+
state.viewportTools = {
|
|
590
|
+
...state.viewportTools,
|
|
591
|
+
activeUtilityTool: 'none',
|
|
592
|
+
};
|
|
593
|
+
state.status = 'Measurement cleared';
|
|
594
|
+
state.statusTone = 'default';
|
|
595
|
+
state.statusToneStatus = state.status;
|
|
596
|
+
state.statusDetails = '';
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
export function applyLocalEditorHarnessViewportMeasurementPointerStart(state, event) {
|
|
600
|
+
if (state.viewportTools.activeUtilityTool !== 'measure-distance')
|
|
601
|
+
return false;
|
|
602
|
+
if (!state.viewportMeasurement.start || state.viewportMeasurement.end) {
|
|
603
|
+
const next = state.sceneViewMeasurement?.beginAt(event.clientX, event.clientY) ?? null;
|
|
604
|
+
if (!next)
|
|
605
|
+
return false;
|
|
606
|
+
state.viewportMeasurement = next;
|
|
607
|
+
state.status = 'Measure distance: pick second XZ ground point';
|
|
608
|
+
state.statusTone = 'default';
|
|
609
|
+
state.statusToneStatus = state.status;
|
|
610
|
+
state.statusDetails = '';
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
const next = state.sceneViewMeasurement?.completeAt(event.clientX, event.clientY) ?? null;
|
|
614
|
+
if (!next)
|
|
615
|
+
return false;
|
|
616
|
+
state.viewportMeasurement = next;
|
|
617
|
+
state.viewportTools = {
|
|
618
|
+
...state.viewportTools,
|
|
619
|
+
activeUtilityTool: 'none',
|
|
620
|
+
};
|
|
621
|
+
state.status = `Measured distance: ${formatViewportMeasurementDistance(next.distance)} units`;
|
|
622
|
+
state.statusTone = 'default';
|
|
623
|
+
state.statusToneStatus = state.status;
|
|
624
|
+
state.statusDetails = next.start && next.end
|
|
625
|
+
? `A (${formatViewportMeasurementDistance(next.start.x)}, ${formatViewportMeasurementDistance(next.start.z)}) · B (${formatViewportMeasurementDistance(next.end.x)}, ${formatViewportMeasurementDistance(next.end.z)})`
|
|
626
|
+
: '';
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
export function applyLocalEditorHarnessViewportMeasurementPointerMove(state, event) {
|
|
630
|
+
if (state.viewportTools.activeUtilityTool !== 'measure-distance')
|
|
631
|
+
return false;
|
|
632
|
+
const next = state.sceneViewMeasurement?.previewAt(event.clientX, event.clientY) ?? null;
|
|
633
|
+
if (!next)
|
|
634
|
+
return false;
|
|
635
|
+
state.viewportMeasurement = next;
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
export function applyLocalEditorHarnessViewportMeasurementPointerEnd(_state) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
function handleViewportMeasurementStart(state, event) {
|
|
642
|
+
return applyLocalEditorHarnessViewportMeasurementPointerStart(state, event);
|
|
643
|
+
}
|
|
644
|
+
function handleViewportMeasurementMove(state, event) {
|
|
645
|
+
return applyLocalEditorHarnessViewportMeasurementPointerMove(state, event);
|
|
646
|
+
}
|
|
647
|
+
function handleViewportMeasurementEnd(state) {
|
|
648
|
+
return applyLocalEditorHarnessViewportMeasurementPointerEnd(state);
|
|
649
|
+
}
|
|
650
|
+
function formatViewportMeasurementDistance(value) {
|
|
651
|
+
if (!Number.isFinite(value))
|
|
652
|
+
return '0';
|
|
653
|
+
const rounded = Math.abs(value) < 0.005 ? 0 : value;
|
|
654
|
+
return rounded.toFixed(2).replace(/\.?0+$/, '');
|
|
655
|
+
}
|
|
656
|
+
function resolveViewportPresetTarget(state) {
|
|
657
|
+
const selection = state.session?.getState().selection;
|
|
658
|
+
const activeId = selection?.activeId ?? null;
|
|
659
|
+
if (!activeId || selection?.selectedIds.length !== 1)
|
|
660
|
+
return null;
|
|
661
|
+
const bounds = state.projection?.getSelectionBounds([activeId]) ?? null;
|
|
662
|
+
return bounds?.center ?? null;
|
|
663
|
+
}
|
|
664
|
+
function resolveViewportPresetRadius(state) {
|
|
665
|
+
const selection = state.session?.getState().selection;
|
|
666
|
+
const activeId = selection?.activeId ?? null;
|
|
667
|
+
if (!activeId || selection?.selectedIds.length !== 1)
|
|
668
|
+
return undefined;
|
|
669
|
+
const bounds = state.projection?.getSelectionBounds([activeId]) ?? null;
|
|
670
|
+
if (!bounds)
|
|
671
|
+
return undefined;
|
|
672
|
+
const size = bounds.size;
|
|
673
|
+
const radius = Math.hypot(size.x, size.y, size.z);
|
|
674
|
+
return Number.isFinite(radius) && radius > 0 ? Math.max(radius * 1.5, 2) : undefined;
|
|
675
|
+
}
|
|
676
|
+
function validateTransformActionSelection(state, action) {
|
|
677
|
+
const selection = state.session?.getState().selection;
|
|
678
|
+
const selectedCount = selection?.selectedIds.length ?? 0;
|
|
679
|
+
if (action.startsWith('align-')) {
|
|
680
|
+
return selectedCount >= 2 && selection?.activeId
|
|
681
|
+
? { ok: true }
|
|
682
|
+
: { ok: false, message: 'Align needs at least 2 selected objects and an active object' };
|
|
683
|
+
}
|
|
684
|
+
return selectedCount >= 3
|
|
685
|
+
? { ok: true }
|
|
686
|
+
: { ok: false, message: 'Distribute needs at least 3 selected objects' };
|
|
687
|
+
}
|
|
271
688
|
async function createEditorWorld(state, options, render) {
|
|
272
689
|
disposeEditorWorld(state);
|
|
273
690
|
const canvas = options.worldAdapter.getCanvas();
|
|
@@ -282,10 +699,12 @@ async function createEditorWorld(state, options, render) {
|
|
|
282
699
|
cameraTarget: options.world?.cameraTarget,
|
|
283
700
|
cameraRadius: options.world?.cameraRadius,
|
|
284
701
|
clearColor: options.world?.clearColor,
|
|
702
|
+
sky: options.world?.sky,
|
|
285
703
|
useRightHandedSystem: options.world?.useRightHandedSystem,
|
|
286
704
|
enableDefaultCameraControls: false,
|
|
287
705
|
});
|
|
288
|
-
options.createGrid?.(babylon, world.scene);
|
|
706
|
+
const grid = options.createGrid?.(babylon, world.scene) ?? null;
|
|
707
|
+
grid?.setVisible(state.gridVisible);
|
|
289
708
|
const projection = createBabylonEditorProjection({
|
|
290
709
|
babylon,
|
|
291
710
|
scene: world.scene,
|
|
@@ -301,8 +720,8 @@ async function createEditorWorld(state, options, render) {
|
|
|
301
720
|
logger: console,
|
|
302
721
|
onDragStart(event) {
|
|
303
722
|
state.status = event.targetIds.length > 1
|
|
304
|
-
? `Dragging ${event.tool} ${event.targetIds.length} objects`
|
|
305
|
-
: `Dragging ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
|
|
723
|
+
? `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.targetIds.length} objects`
|
|
724
|
+
: `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
|
|
306
725
|
render();
|
|
307
726
|
},
|
|
308
727
|
onDragUpdate() {
|
|
@@ -313,12 +732,20 @@ async function createEditorWorld(state, options, render) {
|
|
|
313
732
|
render();
|
|
314
733
|
},
|
|
315
734
|
onDragCancel(event) {
|
|
735
|
+
if (event.duplicate && cancelDuplicateDrag(state, options)) {
|
|
736
|
+
render();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
316
739
|
state.status = event.targetIds.length > 1
|
|
317
740
|
? `Canceled ${event.tool} ${event.targetIds.length} objects`
|
|
318
741
|
: `Canceled ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
|
|
319
742
|
render();
|
|
320
743
|
},
|
|
744
|
+
onDuplicateDragStart(input) {
|
|
745
|
+
return beginDuplicateDrag(state, options, input);
|
|
746
|
+
},
|
|
321
747
|
});
|
|
748
|
+
gizmo.setOperationSettings(state.transformOperationSettings);
|
|
322
749
|
gizmo.setConstraint(state.transformConstraint);
|
|
323
750
|
const selectionController = createBabylonProjectionSelectionController({
|
|
324
751
|
scene: world.scene,
|
|
@@ -342,13 +769,33 @@ async function createEditorWorld(state, options, render) {
|
|
|
342
769
|
render();
|
|
343
770
|
},
|
|
344
771
|
});
|
|
772
|
+
const sceneViewMeasurement = createBabylonSceneViewMeasurementController({
|
|
773
|
+
babylon,
|
|
774
|
+
scene: world.scene,
|
|
775
|
+
});
|
|
345
776
|
const sceneViewInput = createBabylonSceneViewInputController({
|
|
346
777
|
canvas,
|
|
347
778
|
isEnabled: () => state.mode === 'editor',
|
|
348
779
|
isGizmoDragCandidate: (event) => gizmo.isGizmoDragCandidate(event),
|
|
349
780
|
isBoxSelectCandidate: (event) => selectionController.isBoxSelectionCandidate(event),
|
|
350
781
|
isViewPlaneMoveCandidate: (event) => gizmo.isViewPlaneMoveCandidate(event),
|
|
782
|
+
isPlacementCandidate: () => isPlacementArmed(state),
|
|
783
|
+
isMeasurementCandidate: () => state.viewportTools.activeUtilityTool === 'measure-distance',
|
|
351
784
|
onPointerIntentStart(event) {
|
|
785
|
+
if (event.state.intent === 'measurement') {
|
|
786
|
+
if (handleViewportMeasurementStart(state, event.originalEvent))
|
|
787
|
+
render();
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (event.state.intent === 'gizmo-drag') {
|
|
791
|
+
gizmo.preparePointerDrag(event.originalEvent);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (event.state.intent === 'placement') {
|
|
795
|
+
if (previewArmedPlacement(state, event.originalEvent))
|
|
796
|
+
render();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
352
799
|
if (event.state.intent === 'view-plane-move') {
|
|
353
800
|
if (gizmo.beginViewPlaneMove(event.originalEvent))
|
|
354
801
|
render();
|
|
@@ -359,12 +806,22 @@ async function createEditorWorld(state, options, render) {
|
|
|
359
806
|
}
|
|
360
807
|
},
|
|
361
808
|
onPointerIntentMove(event) {
|
|
809
|
+
if (event.state.intent === 'measurement') {
|
|
810
|
+
if (handleViewportMeasurementMove(state, event.originalEvent))
|
|
811
|
+
render();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (event.state.intent === 'placement') {
|
|
815
|
+
if (previewArmedPlacement(state, event.originalEvent))
|
|
816
|
+
render();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
362
819
|
if (event.state.intent === 'view-plane-move') {
|
|
363
820
|
if (gizmo.updateViewPlaneMove(event.originalEvent))
|
|
364
821
|
render();
|
|
365
822
|
return;
|
|
366
823
|
}
|
|
367
|
-
if (state.sceneViewCamera?.handlePointerIntentMove(event)) {
|
|
824
|
+
if (!state.sceneCameraPreviewEnabled && state.sceneViewCamera?.handlePointerIntentMove(event)) {
|
|
368
825
|
render();
|
|
369
826
|
return;
|
|
370
827
|
}
|
|
@@ -373,6 +830,16 @@ async function createEditorWorld(state, options, render) {
|
|
|
373
830
|
}
|
|
374
831
|
},
|
|
375
832
|
onPointerIntentEnd(event) {
|
|
833
|
+
if (event.state.intent === 'measurement') {
|
|
834
|
+
if (handleViewportMeasurementEnd(state))
|
|
835
|
+
render();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (event.state.intent === 'placement') {
|
|
839
|
+
if (commitArmedPlacement(state, options, event.originalEvent))
|
|
840
|
+
render();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
376
843
|
if (event.state.intent === 'view-plane-move') {
|
|
377
844
|
if (gizmo.endViewPlaneMove(event.originalEvent))
|
|
378
845
|
render();
|
|
@@ -383,6 +850,15 @@ async function createEditorWorld(state, options, render) {
|
|
|
383
850
|
}
|
|
384
851
|
},
|
|
385
852
|
onPointerIntentCancel(event) {
|
|
853
|
+
if (event.state.intent === 'measurement') {
|
|
854
|
+
render();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (event.state.intent === 'placement') {
|
|
858
|
+
state.gizmo?.setPlacementMarker(null);
|
|
859
|
+
render();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
386
862
|
if (event.state.intent === 'view-plane-move') {
|
|
387
863
|
gizmo.cancelDrag();
|
|
388
864
|
render();
|
|
@@ -397,7 +873,7 @@ async function createEditorWorld(state, options, render) {
|
|
|
397
873
|
selectionController.handleDoubleClick(event);
|
|
398
874
|
},
|
|
399
875
|
onWheel(event) {
|
|
400
|
-
if (state.sceneViewCamera?.handleWheel(event))
|
|
876
|
+
if (!state.sceneCameraPreviewEnabled && state.sceneViewCamera?.handleWheel(event))
|
|
401
877
|
render();
|
|
402
878
|
},
|
|
403
879
|
});
|
|
@@ -407,6 +883,15 @@ async function createEditorWorld(state, options, render) {
|
|
|
407
883
|
camera: world.camera,
|
|
408
884
|
input: sceneViewInput,
|
|
409
885
|
});
|
|
886
|
+
const sceneCameraPreview = createBabylonSceneCameraPreviewController({
|
|
887
|
+
babylon,
|
|
888
|
+
scene: world.scene,
|
|
889
|
+
editorCamera: world.camera,
|
|
890
|
+
});
|
|
891
|
+
const sceneViewSpatialOverlay = createBabylonSceneViewSpatialOverlayController({
|
|
892
|
+
babylon,
|
|
893
|
+
scene: world.scene,
|
|
894
|
+
});
|
|
410
895
|
const document = state.session?.getState().workingDocument;
|
|
411
896
|
if (document) {
|
|
412
897
|
projection.projectNodes(options.documentAdapter.getProjectionNodes(document));
|
|
@@ -420,10 +905,14 @@ async function createEditorWorld(state, options, render) {
|
|
|
420
905
|
state.babylon = babylon;
|
|
421
906
|
state.engine = engine;
|
|
422
907
|
state.world = world;
|
|
908
|
+
state.grid = grid;
|
|
423
909
|
state.projection = projection;
|
|
424
910
|
state.gizmo = gizmo;
|
|
911
|
+
state.sceneCameraPreview = sceneCameraPreview;
|
|
425
912
|
state.sceneViewInput = sceneViewInput;
|
|
426
913
|
state.sceneViewCamera = sceneViewCamera;
|
|
914
|
+
state.sceneViewMeasurement = sceneViewMeasurement;
|
|
915
|
+
state.sceneViewSpatialOverlay = sceneViewSpatialOverlay;
|
|
427
916
|
state.selectionController = selectionController;
|
|
428
917
|
state.resizeHandler = resize;
|
|
429
918
|
}
|
|
@@ -432,17 +921,32 @@ function disposeEditorWorld(state) {
|
|
|
432
921
|
window.removeEventListener('resize', state.resizeHandler);
|
|
433
922
|
state.resizeHandler = null;
|
|
434
923
|
}
|
|
924
|
+
state.sceneCameraPreview?.dispose();
|
|
925
|
+
state.sceneCameraPreview = null;
|
|
926
|
+
state.sceneCameraPreviewEnabled = false;
|
|
435
927
|
state.sceneViewCamera?.dispose();
|
|
436
928
|
state.sceneViewCamera = null;
|
|
929
|
+
state.sceneViewMeasurement?.dispose();
|
|
930
|
+
state.sceneViewMeasurement = null;
|
|
931
|
+
state.sceneViewSpatialOverlay?.dispose();
|
|
932
|
+
state.sceneViewSpatialOverlay = null;
|
|
437
933
|
state.sceneViewInput?.dispose();
|
|
438
934
|
state.sceneViewInput = null;
|
|
439
935
|
state.selectionController?.dispose();
|
|
440
936
|
state.selectionController = null;
|
|
441
937
|
state.boxSelection = null;
|
|
938
|
+
state.viewportSpatialOverlay = createEmptyEditorViewportSpatialOverlayState();
|
|
939
|
+
state.viewportMeasurement = createEmptyViewportMeasurement();
|
|
940
|
+
state.viewportTools = {
|
|
941
|
+
...state.viewportTools,
|
|
942
|
+
activeUtilityTool: 'none',
|
|
943
|
+
};
|
|
442
944
|
state.gizmo?.dispose();
|
|
443
945
|
state.gizmo = null;
|
|
444
946
|
state.projection?.dispose();
|
|
445
947
|
state.projection = null;
|
|
948
|
+
state.grid?.dispose();
|
|
949
|
+
state.grid = null;
|
|
446
950
|
state.engine?.stopRenderLoop?.();
|
|
447
951
|
state.world?.dispose();
|
|
448
952
|
state.engine?.dispose?.();
|
|
@@ -475,31 +979,104 @@ function selectItem(state, options, input) {
|
|
|
475
979
|
return false;
|
|
476
980
|
if (!isDocumentNodeSelectable(state, options, input.id))
|
|
477
981
|
return false;
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
selectedIds: [input.id],
|
|
489
|
-
activeId: input.id,
|
|
490
|
-
label: 'Add Selection',
|
|
491
|
-
}
|
|
492
|
-
: {
|
|
493
|
-
type: 'selection.replace',
|
|
494
|
-
selectedIds: [input.id],
|
|
495
|
-
activeId: input.id,
|
|
496
|
-
label: 'Select Item',
|
|
497
|
-
};
|
|
982
|
+
const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
|
|
983
|
+
const command = resolveEditorSelectionCommand({
|
|
984
|
+
selection,
|
|
985
|
+
targetIds: [input.id],
|
|
986
|
+
activeId: input.id,
|
|
987
|
+
gesture: 'click',
|
|
988
|
+
modifier: input.toggle ? 'toggle' : input.additive ? 'additive' : 'replace',
|
|
989
|
+
});
|
|
990
|
+
if (!command)
|
|
991
|
+
return false;
|
|
498
992
|
return dispatchSelectionCommand(state, options, command);
|
|
499
993
|
}
|
|
994
|
+
function createBrowserHierarchyContextActions(state, options) {
|
|
995
|
+
return (options.hierarchy?.contextActions ?? []).map((registration) => ({
|
|
996
|
+
id: registration.id,
|
|
997
|
+
label: registration.label,
|
|
998
|
+
shortcut: registration.shortcut,
|
|
999
|
+
danger: registration.danger,
|
|
1000
|
+
placement: registration.placement,
|
|
1001
|
+
separatorBefore: registration.separatorBefore,
|
|
1002
|
+
visible: (browserContext) => {
|
|
1003
|
+
const context = createHarnessHierarchyContextActionContext(state, options, browserContext);
|
|
1004
|
+
if (!context)
|
|
1005
|
+
return false;
|
|
1006
|
+
return registration.visible?.(context) ?? true;
|
|
1007
|
+
},
|
|
1008
|
+
disabled: (browserContext) => {
|
|
1009
|
+
const context = createHarnessHierarchyContextActionContext(state, options, browserContext);
|
|
1010
|
+
if (!context)
|
|
1011
|
+
return 'No editable document is loaded.';
|
|
1012
|
+
return registration.disabled?.(context) ?? false;
|
|
1013
|
+
},
|
|
1014
|
+
}));
|
|
1015
|
+
}
|
|
1016
|
+
function createHarnessHierarchyContextActionContext(state, options, browserContext, payload) {
|
|
1017
|
+
const document = state.session?.getState().workingDocument ?? null;
|
|
1018
|
+
if (!document)
|
|
1019
|
+
return null;
|
|
1020
|
+
const activeId = browserContext.activeId;
|
|
1021
|
+
return {
|
|
1022
|
+
document,
|
|
1023
|
+
contextNodeId: browserContext.contextNodeId,
|
|
1024
|
+
targetIds: browserContext.targetIds,
|
|
1025
|
+
activeId,
|
|
1026
|
+
hierarchyItem: browserContext.node,
|
|
1027
|
+
projectionNode: browserContext.contextNodeId
|
|
1028
|
+
? options.documentAdapter.getProjectionNode(document, browserContext.contextNodeId)
|
|
1029
|
+
: null,
|
|
1030
|
+
hostServices: options.hostServices ?? null,
|
|
1031
|
+
payload,
|
|
1032
|
+
browserContext,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function createBrowserContextForCustomAction(state, options, action) {
|
|
1036
|
+
const document = state.session?.getState().workingDocument ?? null;
|
|
1037
|
+
if (!document)
|
|
1038
|
+
return null;
|
|
1039
|
+
const uiState = createUiState(state, options);
|
|
1040
|
+
const node = action.contextNodeId
|
|
1041
|
+
? uiState.hierarchy.find(item => item.id === action.contextNodeId) ?? null
|
|
1042
|
+
: null;
|
|
1043
|
+
return {
|
|
1044
|
+
state: uiState,
|
|
1045
|
+
menuKind: node ? 'node' : 'blank',
|
|
1046
|
+
node,
|
|
1047
|
+
contextNodeId: action.contextNodeId,
|
|
1048
|
+
targetIds: action.targetIds,
|
|
1049
|
+
activeId: action.activeId,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function runHierarchyCustomContextAction(state, options, action) {
|
|
1053
|
+
const registration = options.hierarchy?.contextActions?.find(candidate => candidate.id === action.id);
|
|
1054
|
+
if (!registration)
|
|
1055
|
+
return false;
|
|
1056
|
+
const browserContext = createBrowserContextForCustomAction(state, options, action);
|
|
1057
|
+
const context = browserContext
|
|
1058
|
+
? createHarnessHierarchyContextActionContext(state, options, browserContext, action.payload)
|
|
1059
|
+
: null;
|
|
1060
|
+
if (!context)
|
|
1061
|
+
return false;
|
|
1062
|
+
try {
|
|
1063
|
+
return registration.run(context) === true;
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1067
|
+
state.status = `${registration.label} failed`;
|
|
1068
|
+
state.statusTone = 'error';
|
|
1069
|
+
state.statusToneStatus = state.status;
|
|
1070
|
+
state.statusDetails = message;
|
|
1071
|
+
console.error('[LocalEditorHarness] hierarchy context action failed', error);
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
500
1075
|
function handleContextAction(state, options, action) {
|
|
501
1076
|
if (action.region !== 'hierarchy')
|
|
502
1077
|
return false;
|
|
1078
|
+
if (action.action === 'custom')
|
|
1079
|
+
return runHierarchyCustomContextAction(state, options, action);
|
|
503
1080
|
if (action.action === 'focus') {
|
|
504
1081
|
const activeId = action.activeId ?? action.targetIds[action.targetIds.length - 1] ?? null;
|
|
505
1082
|
const selectionChanged = activeId && !state.session?.getState().selection.selectedIds.includes(activeId)
|
|
@@ -518,7 +1095,15 @@ function handleContextAction(state, options, action) {
|
|
|
518
1095
|
return createSceneGraphGroup(state, options, {
|
|
519
1096
|
parentId: action.parentId ?? null,
|
|
520
1097
|
activeId: action.activeId ?? null,
|
|
521
|
-
name: '
|
|
1098
|
+
name: 'Empty',
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
if (action.action === 'create-primitive') {
|
|
1102
|
+
return createSceneGraphPrimitive(state, options, {
|
|
1103
|
+
parentId: action.parentId ?? null,
|
|
1104
|
+
activeId: action.activeId ?? null,
|
|
1105
|
+
shape: action.shape,
|
|
1106
|
+
name: action.name,
|
|
522
1107
|
});
|
|
523
1108
|
}
|
|
524
1109
|
if (action.action === 'delete') {
|
|
@@ -527,6 +1112,18 @@ function handleContextAction(state, options, action) {
|
|
|
527
1112
|
activeId: action.activeId ?? null,
|
|
528
1113
|
});
|
|
529
1114
|
}
|
|
1115
|
+
if (action.action === 'duplicate') {
|
|
1116
|
+
return duplicateSceneGraphNodes(state, options, {
|
|
1117
|
+
targetIds: action.targetIds,
|
|
1118
|
+
activeId: action.activeId ?? null,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
if (action.action === 'paste') {
|
|
1122
|
+
return duplicateSceneGraphNodes(state, options, {
|
|
1123
|
+
targetIds: action.sourceIds,
|
|
1124
|
+
activeId: action.activeId ?? null,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
530
1127
|
return false;
|
|
531
1128
|
}
|
|
532
1129
|
function dispatchSelectionCommand(state, options, command) {
|
|
@@ -579,17 +1176,52 @@ function createSceneGraphGroup(state, options, intent) {
|
|
|
579
1176
|
cancelActiveOperation(state);
|
|
580
1177
|
const patch = options.documentAdapter.createSceneGraphCreateGroupPatch?.(document, intent);
|
|
581
1178
|
if (!patch) {
|
|
582
|
-
state.status = 'Create
|
|
1179
|
+
state.status = 'Create empty rejected';
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
const result = state.session.dispatch({
|
|
1183
|
+
type: 'document.patch',
|
|
1184
|
+
label: patch.label ?? 'Create Empty',
|
|
1185
|
+
patch: patch.patch,
|
|
1186
|
+
targetId: patch.createdId ?? undefined,
|
|
1187
|
+
});
|
|
1188
|
+
if (!result.documentChanged) {
|
|
1189
|
+
state.status = 'Create empty unchanged';
|
|
1190
|
+
return true;
|
|
1191
|
+
}
|
|
1192
|
+
const createdId = patch.createdId ?? null;
|
|
1193
|
+
let selection = result.selection;
|
|
1194
|
+
if (createdId && isNodeSelectableInDocument(options, result.workingDocument, createdId)) {
|
|
1195
|
+
selection = state.session.dispatch({
|
|
1196
|
+
type: 'selection.replace',
|
|
1197
|
+
selectedIds: [createdId],
|
|
1198
|
+
activeId: createdId,
|
|
1199
|
+
label: 'Select Created Empty',
|
|
1200
|
+
}).selection;
|
|
1201
|
+
}
|
|
1202
|
+
rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
|
|
1203
|
+
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
1204
|
+
state.status = patch.label ?? (createdId ? `Created empty ${createdId}` : 'Created empty');
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
function createSceneGraphPrimitive(state, options, intent) {
|
|
1208
|
+
const document = state.session?.getState().workingDocument;
|
|
1209
|
+
if (state.mode !== 'editor' || !state.session || !document)
|
|
1210
|
+
return false;
|
|
1211
|
+
cancelActiveOperation(state);
|
|
1212
|
+
const patch = options.documentAdapter.createSceneGraphCreatePrimitivePatch?.(document, intent);
|
|
1213
|
+
if (!patch) {
|
|
1214
|
+
state.status = `Create ${intent.shape} rejected`;
|
|
583
1215
|
return true;
|
|
584
1216
|
}
|
|
585
1217
|
const result = state.session.dispatch({
|
|
586
1218
|
type: 'document.patch',
|
|
587
|
-
label: patch.label ??
|
|
1219
|
+
label: patch.label ?? `Create ${intent.shape}`,
|
|
588
1220
|
patch: patch.patch,
|
|
589
1221
|
targetId: patch.createdId ?? undefined,
|
|
590
1222
|
});
|
|
591
1223
|
if (!result.documentChanged) {
|
|
592
|
-
state.status =
|
|
1224
|
+
state.status = `Create ${intent.shape} unchanged`;
|
|
593
1225
|
return true;
|
|
594
1226
|
}
|
|
595
1227
|
const createdId = patch.createdId ?? null;
|
|
@@ -599,12 +1231,12 @@ function createSceneGraphGroup(state, options, intent) {
|
|
|
599
1231
|
type: 'selection.replace',
|
|
600
1232
|
selectedIds: [createdId],
|
|
601
1233
|
activeId: createdId,
|
|
602
|
-
label: 'Select Created
|
|
1234
|
+
label: 'Select Created Primitive',
|
|
603
1235
|
}).selection;
|
|
604
1236
|
}
|
|
605
1237
|
rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
|
|
606
1238
|
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
607
|
-
state.status = patch.label ?? (createdId ? `Created
|
|
1239
|
+
state.status = patch.label ?? (createdId ? `Created ${intent.shape} ${createdId}` : `Created ${intent.shape}`);
|
|
608
1240
|
return true;
|
|
609
1241
|
}
|
|
610
1242
|
function deleteSceneGraphNodes(state, options, intent) {
|
|
@@ -650,6 +1282,57 @@ function deleteSceneGraphNodes(state, options, intent) {
|
|
|
650
1282
|
state.status = patch.label ?? `Deleted ${patch.deletedIds?.length ?? intent.ids.length} node(s)`;
|
|
651
1283
|
return true;
|
|
652
1284
|
}
|
|
1285
|
+
function duplicateSceneGraphNodes(state, options, intent) {
|
|
1286
|
+
const document = state.session?.getState().workingDocument;
|
|
1287
|
+
if (state.mode !== 'editor' || !state.session || !document)
|
|
1288
|
+
return false;
|
|
1289
|
+
cancelActiveOperation(state);
|
|
1290
|
+
const targetIds = Array.from(new Set(intent.targetIds.filter(Boolean)));
|
|
1291
|
+
if (targetIds.length === 0) {
|
|
1292
|
+
state.status = 'Duplicate rejected: empty selection';
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
const patch = options.documentAdapter.createDuplicateSelectionPatch?.({
|
|
1296
|
+
document,
|
|
1297
|
+
targetIds,
|
|
1298
|
+
activeId: intent.activeId && targetIds.includes(intent.activeId) ? intent.activeId : targetIds[targetIds.length - 1] ?? null,
|
|
1299
|
+
transforms: {},
|
|
1300
|
+
});
|
|
1301
|
+
if (!patch || patch.createdIds.length === 0) {
|
|
1302
|
+
state.status = 'Duplicate rejected';
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
const result = state.session.dispatch({
|
|
1306
|
+
type: 'document.patch',
|
|
1307
|
+
label: patch.label ?? `Duplicate ${targetIds.length} object(s)`,
|
|
1308
|
+
patch: patch.patch,
|
|
1309
|
+
targetId: patch.activeId ?? patch.createdIds[patch.createdIds.length - 1] ?? undefined,
|
|
1310
|
+
});
|
|
1311
|
+
if (!result.documentChanged) {
|
|
1312
|
+
state.status = 'Duplicate unchanged';
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
const createdIds = patch.createdIds.filter(id => isNodeSelectableInDocument(options, result.workingDocument, id));
|
|
1316
|
+
const activeId = patch.activeId && createdIds.includes(patch.activeId)
|
|
1317
|
+
? patch.activeId
|
|
1318
|
+
: createdIds[createdIds.length - 1] ?? null;
|
|
1319
|
+
const selection = createdIds.length > 0
|
|
1320
|
+
? state.session.dispatch({
|
|
1321
|
+
type: 'selection.replace',
|
|
1322
|
+
selectedIds: createdIds,
|
|
1323
|
+
activeId,
|
|
1324
|
+
label: 'Select Duplicated Nodes',
|
|
1325
|
+
}).selection
|
|
1326
|
+
: sanitizeSelection(state, options, result.workingDocument, result.selection) ?? result.selection;
|
|
1327
|
+
rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
|
|
1328
|
+
if (patch.reprojectIds?.length)
|
|
1329
|
+
reprojectProjectionForChangedIds(state, options, result.workingDocument, patch.reprojectIds);
|
|
1330
|
+
else
|
|
1331
|
+
syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds ?? createdIds);
|
|
1332
|
+
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
1333
|
+
state.status = patch.label ?? `Duplicated ${createdIds.length} object(s)`;
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
653
1336
|
function dropSceneGraphNode(state, options, intent) {
|
|
654
1337
|
const document = state.session?.getState().workingDocument;
|
|
655
1338
|
if (state.mode !== 'editor' || !state.session || !document)
|
|
@@ -743,27 +1426,27 @@ function groupSceneGraphSelection(state, options, intent) {
|
|
|
743
1426
|
const hierarchy = options.documentAdapter.getHierarchyItems(document);
|
|
744
1427
|
const coreValidation = validateSceneGraphGroupSelection(hierarchy, intent);
|
|
745
1428
|
if (!coreValidation.ok) {
|
|
746
|
-
state.status = `
|
|
1429
|
+
state.status = `Parent selection rejected: ${coreValidation.reason ?? 'invalid scene graph parent selection'}`;
|
|
747
1430
|
return true;
|
|
748
1431
|
}
|
|
749
1432
|
const projectValidation = options.documentAdapter.validateSceneGraphGroupSelection?.(document, intent);
|
|
750
1433
|
if (projectValidation && !projectValidation.ok) {
|
|
751
|
-
state.status = `
|
|
1434
|
+
state.status = `Parent selection rejected: ${projectValidation.reason ?? 'project validation failed'}`;
|
|
752
1435
|
return true;
|
|
753
1436
|
}
|
|
754
1437
|
const patch = options.documentAdapter.createSceneGraphGroupSelectionPatch?.(document, intent);
|
|
755
1438
|
if (!patch) {
|
|
756
|
-
state.status = '
|
|
1439
|
+
state.status = 'Parent selection rejected';
|
|
757
1440
|
return true;
|
|
758
1441
|
}
|
|
759
1442
|
const result = state.session.dispatch({
|
|
760
1443
|
type: 'document.patch',
|
|
761
|
-
label: patch.label ?? '
|
|
1444
|
+
label: patch.label ?? 'Parent Selection',
|
|
762
1445
|
patch: patch.patch,
|
|
763
1446
|
targetId: patch.createdId,
|
|
764
1447
|
});
|
|
765
1448
|
if (!result.documentChanged) {
|
|
766
|
-
state.status = '
|
|
1449
|
+
state.status = 'Parent selection unchanged';
|
|
767
1450
|
return true;
|
|
768
1451
|
}
|
|
769
1452
|
let selection = result.selection;
|
|
@@ -772,7 +1455,7 @@ function groupSceneGraphSelection(state, options, intent) {
|
|
|
772
1455
|
type: 'selection.replace',
|
|
773
1456
|
selectedIds: [patch.createdId],
|
|
774
1457
|
activeId: patch.createdId,
|
|
775
|
-
label: 'Select Created
|
|
1458
|
+
label: 'Select Created Parent',
|
|
776
1459
|
}).selection;
|
|
777
1460
|
}
|
|
778
1461
|
else {
|
|
@@ -780,7 +1463,7 @@ function groupSceneGraphSelection(state, options, intent) {
|
|
|
780
1463
|
}
|
|
781
1464
|
rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
|
|
782
1465
|
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
783
|
-
state.status = patch.label ?? `
|
|
1466
|
+
state.status = patch.label ?? `Parented ${patch.changedIds?.length ?? intent.ids.length} node(s)`;
|
|
784
1467
|
return true;
|
|
785
1468
|
}
|
|
786
1469
|
function sanitizeSelection(state, options, document, selection) {
|
|
@@ -803,18 +1486,216 @@ function syncSelectionToProjection(state, selection) {
|
|
|
803
1486
|
state.gizmo?.setSelection(selection);
|
|
804
1487
|
state.gizmo?.refreshSelection();
|
|
805
1488
|
}
|
|
806
|
-
function
|
|
807
|
-
if (state.mode !== 'editor')
|
|
1489
|
+
function executeTransformAction(state, options, action) {
|
|
1490
|
+
if (state.mode !== 'editor' || !state.session)
|
|
808
1491
|
return false;
|
|
1492
|
+
const document = state.session.getState().workingDocument;
|
|
1493
|
+
const selection = state.session.getState().selection;
|
|
809
1494
|
cancelActiveOperation(state);
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
1495
|
+
const validation = validateTransformActionSelection(state, action);
|
|
1496
|
+
if (!validation.ok) {
|
|
1497
|
+
state.status = validation.message;
|
|
1498
|
+
return true;
|
|
1499
|
+
}
|
|
1500
|
+
const beforeTransforms = state.projection?.readNodeTransforms(selection.selectedIds) ?? {};
|
|
1501
|
+
const transformTargets = selection.selectedIds
|
|
1502
|
+
.map((id) => {
|
|
1503
|
+
const transform = beforeTransforms[id];
|
|
1504
|
+
return transform ? { id, transform } : null;
|
|
1505
|
+
})
|
|
1506
|
+
.filter((target) => !!target);
|
|
1507
|
+
const targets = computeEditorTransformActionTargets({
|
|
1508
|
+
action,
|
|
1509
|
+
activeId: selection.activeId,
|
|
1510
|
+
targets: transformTargets,
|
|
1511
|
+
});
|
|
1512
|
+
if (targets.length === 0) {
|
|
1513
|
+
state.status = `Transform action rejected: ${action}`;
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
const changedTargets = targets.filter(target => !editorTransformSnapshotsEqual(target.before, target.after));
|
|
1517
|
+
if (changedTargets.length === 0) {
|
|
1518
|
+
state.status = `Transform action unchanged: ${action}`;
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
const patch = options.documentAdapter.createTransformBatchPatch?.({
|
|
1522
|
+
document,
|
|
1523
|
+
targetIds: changedTargets.map(target => target.id),
|
|
1524
|
+
activeId: selection.activeId,
|
|
1525
|
+
tool: 'move',
|
|
1526
|
+
space: 'world',
|
|
1527
|
+
constraint: action === 'align-all' ? 'free' : 'axis',
|
|
1528
|
+
pivot: createTransformActionPivot(targets),
|
|
1529
|
+
targets: changedTargets,
|
|
1530
|
+
});
|
|
1531
|
+
if (!patch) {
|
|
1532
|
+
state.status = `Transform action ignored: ${action}`;
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
const result = state.session.dispatch({
|
|
1536
|
+
type: 'document.patch',
|
|
1537
|
+
label: patch.label ?? formatTransformActionStatus(action, changedTargets.length),
|
|
1538
|
+
patch: patch.patch,
|
|
1539
|
+
targetId: selection.activeId ?? changedTargets[0]?.id,
|
|
1540
|
+
});
|
|
1541
|
+
if (!result.documentChanged) {
|
|
1542
|
+
state.status = `Transform action unchanged: ${action}`;
|
|
1543
|
+
return true;
|
|
1544
|
+
}
|
|
1545
|
+
const changedIds = patch.changedIds ?? changedTargets.map(target => target.id);
|
|
1546
|
+
syncProjectionForChangedIds(state, options, result.workingDocument, changedIds);
|
|
1547
|
+
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
1548
|
+
state.status = patch.label ?? formatTransformActionStatus(action, changedTargets.length);
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
function createTransformActionPivot(targets) {
|
|
1552
|
+
const center = averageTransformTargetPositions(targets);
|
|
1553
|
+
return {
|
|
1554
|
+
mode: 'selection-center',
|
|
1555
|
+
position: center,
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function averageTransformTargetPositions(targets) {
|
|
1559
|
+
if (targets.length === 0)
|
|
1560
|
+
return { x: 0, y: 0, z: 0 };
|
|
1561
|
+
return {
|
|
1562
|
+
x: targets.reduce((sum, target) => sum + target.before.position.x, 0) / targets.length,
|
|
1563
|
+
y: targets.reduce((sum, target) => sum + target.before.position.y, 0) / targets.length,
|
|
1564
|
+
z: targets.reduce((sum, target) => sum + target.before.position.z, 0) / targets.length,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
function editorTransformSnapshotsEqual(left, right) {
|
|
1568
|
+
return editorVec3Equal(left.position, right.position)
|
|
1569
|
+
&& editorVec3Equal(left.rotation, right.rotation)
|
|
1570
|
+
&& editorVec3Equal(left.scale, right.scale);
|
|
1571
|
+
}
|
|
1572
|
+
function editorVec3Equal(left, right) {
|
|
1573
|
+
return Math.abs(left.x - right.x) < 0.000000001
|
|
1574
|
+
&& Math.abs(left.y - right.y) < 0.000000001
|
|
1575
|
+
&& Math.abs(left.z - right.z) < 0.000000001;
|
|
1576
|
+
}
|
|
1577
|
+
function formatTransformActionStatus(action, count) {
|
|
1578
|
+
return `${action} ${count} object${count === 1 ? '' : 's'}`;
|
|
1579
|
+
}
|
|
1580
|
+
function createAssetFromBrowserIntent(state, options, assetId) {
|
|
1581
|
+
if (state.transformOperationSettings.placementMode === 'off') {
|
|
1582
|
+
return addAssetToDocument(state, options, assetId).ok;
|
|
1583
|
+
}
|
|
1584
|
+
return armAssetPlacement(state, options, assetId);
|
|
1585
|
+
}
|
|
1586
|
+
function armAssetPlacement(state, options, assetId) {
|
|
1587
|
+
if (state.mode !== 'editor')
|
|
813
1588
|
return false;
|
|
1589
|
+
cancelActiveOperation(state);
|
|
814
1590
|
const asset = state.assets.find(candidate => resolveAssetId(options, candidate) === assetId);
|
|
815
1591
|
if (!asset)
|
|
816
1592
|
return false;
|
|
817
|
-
|
|
1593
|
+
state.armedPlacement = { assetId, asset };
|
|
1594
|
+
state.gizmo?.setPlacementMarker(null);
|
|
1595
|
+
state.status = `Placement armed: ${formatAssetLabel(asset, assetId)} (${state.transformOperationSettings.placementMode})`;
|
|
1596
|
+
return true;
|
|
1597
|
+
}
|
|
1598
|
+
function isPlacementArmed(state) {
|
|
1599
|
+
return state.mode === 'editor'
|
|
1600
|
+
&& !!state.armedPlacement
|
|
1601
|
+
&& state.transformOperationSettings.placementMode !== 'off';
|
|
1602
|
+
}
|
|
1603
|
+
function previewArmedPlacement(state, event) {
|
|
1604
|
+
if (!isPlacementArmed(state))
|
|
1605
|
+
return false;
|
|
1606
|
+
const hit = pickArmedPlacementHit(state, event);
|
|
1607
|
+
state.gizmo?.setPlacementMarker(hit);
|
|
1608
|
+
const mode = state.transformOperationSettings.placementMode;
|
|
1609
|
+
state.status = hit
|
|
1610
|
+
? `Placement ${mode}: ${formatVec3(hit.position)}`
|
|
1611
|
+
: `Placement ${mode}: no hit`;
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
function commitArmedPlacement(state, options, event) {
|
|
1615
|
+
if (!isPlacementArmed(state) || !state.session)
|
|
1616
|
+
return false;
|
|
1617
|
+
const armed = state.armedPlacement;
|
|
1618
|
+
const session = state.session;
|
|
1619
|
+
const beforeDocument = session.getState().workingDocument;
|
|
1620
|
+
const hit = pickArmedPlacementHit(state, event);
|
|
1621
|
+
if (!armed || !hit) {
|
|
1622
|
+
state.gizmo?.setPlacementMarker(null);
|
|
1623
|
+
state.status = 'Placement rejected: no hit';
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
const patch = options.documentAdapter.createPlacedAssetPatch?.({
|
|
1627
|
+
document: beforeDocument,
|
|
1628
|
+
asset: armed.asset,
|
|
1629
|
+
hit,
|
|
1630
|
+
});
|
|
1631
|
+
if (!patch) {
|
|
1632
|
+
state.status = 'Placement rejected: document adapter does not support placed assets';
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
const result = session.dispatch({
|
|
1636
|
+
type: 'document.patch',
|
|
1637
|
+
label: patch.label ?? `Place ${armed.assetId}`,
|
|
1638
|
+
patch: patch.patch,
|
|
1639
|
+
targetId: patch.createdId ?? undefined,
|
|
1640
|
+
});
|
|
1641
|
+
if (!result.documentChanged) {
|
|
1642
|
+
state.status = 'Placement unchanged';
|
|
1643
|
+
return true;
|
|
1644
|
+
}
|
|
1645
|
+
const createdId = patch.createdId
|
|
1646
|
+
?? options.documentAdapter.findCreatedId?.(beforeDocument, result.workingDocument)
|
|
1647
|
+
?? null;
|
|
1648
|
+
let selection = result.selection;
|
|
1649
|
+
if (createdId && isNodeSelectableInDocument(options, result.workingDocument, createdId)) {
|
|
1650
|
+
selection = session.dispatch({
|
|
1651
|
+
type: 'selection.replace',
|
|
1652
|
+
selectedIds: [createdId],
|
|
1653
|
+
activeId: createdId,
|
|
1654
|
+
label: 'Select Placed Item',
|
|
1655
|
+
}).selection;
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
selection = sanitizeSelection(state, options, result.workingDocument, selection) ?? selection;
|
|
1659
|
+
}
|
|
1660
|
+
clearArmedPlacement(state);
|
|
1661
|
+
rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
|
|
1662
|
+
state.summary = summarizeDocument(options, result.workingDocument, session.getSource());
|
|
1663
|
+
state.status = patch.label ?? `Placed ${formatAssetLabel(armed.asset, armed.assetId)} at ${formatVec3(hit.position)}`;
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1666
|
+
function pickArmedPlacementHit(state, event) {
|
|
1667
|
+
const mode = state.transformOperationSettings.placementMode;
|
|
1668
|
+
return mode === 'off'
|
|
1669
|
+
? null
|
|
1670
|
+
: state.gizmo?.pickPlacementHit(event.clientX, event.clientY, mode) ?? null;
|
|
1671
|
+
}
|
|
1672
|
+
function clearArmedPlacement(state) {
|
|
1673
|
+
state.armedPlacement = null;
|
|
1674
|
+
state.gizmo?.setPlacementMarker(null);
|
|
1675
|
+
}
|
|
1676
|
+
function addAssetToDocument(state, options, assetId, createOptions = {}) {
|
|
1677
|
+
if (state.mode !== 'editor') {
|
|
1678
|
+
state.status = 'Asset creation rejected: editor is not active';
|
|
1679
|
+
return { ok: false, assetId, changed: false, status: state.status, error: 'editor_not_active' };
|
|
1680
|
+
}
|
|
1681
|
+
cancelActiveOperation(state);
|
|
1682
|
+
const session = state.session;
|
|
1683
|
+
const beforeDocument = session?.getState().workingDocument;
|
|
1684
|
+
if (!session || !beforeDocument) {
|
|
1685
|
+
state.status = 'Asset creation rejected: document is not loaded';
|
|
1686
|
+
return { ok: false, assetId, changed: false, status: state.status, error: 'document_not_loaded' };
|
|
1687
|
+
}
|
|
1688
|
+
const asset = state.assets.find(candidate => resolveAssetId(options, candidate) === assetId);
|
|
1689
|
+
if (!asset) {
|
|
1690
|
+
state.status = `Asset creation rejected: ${assetId} not found`;
|
|
1691
|
+
return { ok: false, assetId, changed: false, status: state.status, error: 'asset_not_found' };
|
|
1692
|
+
}
|
|
1693
|
+
const patch = options.documentAdapter.createPatchFromAsset(asset, {
|
|
1694
|
+
document: beforeDocument,
|
|
1695
|
+
asset,
|
|
1696
|
+
assetId,
|
|
1697
|
+
placement: createOptions.placement,
|
|
1698
|
+
});
|
|
818
1699
|
const result = session.dispatch({
|
|
819
1700
|
type: 'document.patch',
|
|
820
1701
|
label: patch.label ?? 'Create Object From Asset',
|
|
@@ -832,6 +1713,9 @@ function addAssetToDocument(state, options, assetId) {
|
|
|
832
1713
|
}
|
|
833
1714
|
state.summary = summarizeDocument(options, result.workingDocument, session.getSource());
|
|
834
1715
|
state.status = `Added ${assetId}`;
|
|
1716
|
+
state.statusTone = 'success';
|
|
1717
|
+
state.statusToneStatus = state.status;
|
|
1718
|
+
state.statusDetails = '';
|
|
835
1719
|
if (createdId) {
|
|
836
1720
|
const projectedNode = options.documentAdapter.getProjectionNode(result.workingDocument, createdId);
|
|
837
1721
|
if (projectedNode) {
|
|
@@ -840,7 +1724,13 @@ function addAssetToDocument(state, options, assetId) {
|
|
|
840
1724
|
syncSelectionToProjection(state, selectionResult.selection);
|
|
841
1725
|
}
|
|
842
1726
|
}
|
|
843
|
-
return
|
|
1727
|
+
return {
|
|
1728
|
+
ok: true,
|
|
1729
|
+
assetId,
|
|
1730
|
+
changed: result.documentChanged,
|
|
1731
|
+
status: state.status,
|
|
1732
|
+
createdId: createdId ?? null,
|
|
1733
|
+
};
|
|
844
1734
|
}
|
|
845
1735
|
function patchSerializedProperty(state, options, input) {
|
|
846
1736
|
if (state.mode !== 'editor')
|
|
@@ -971,6 +1861,86 @@ function findInspectorPropertyByPath(inspector, path) {
|
|
|
971
1861
|
}
|
|
972
1862
|
return null;
|
|
973
1863
|
}
|
|
1864
|
+
function beginDuplicateDrag(state, options, input) {
|
|
1865
|
+
if (state.mode !== 'editor' || !state.session)
|
|
1866
|
+
return null;
|
|
1867
|
+
if (state.duplicateDrag)
|
|
1868
|
+
return null;
|
|
1869
|
+
const document = state.session.getState().workingDocument;
|
|
1870
|
+
const patch = options.documentAdapter.createDuplicateSelectionPatch?.({
|
|
1871
|
+
document,
|
|
1872
|
+
targetIds: input.targetIds,
|
|
1873
|
+
activeId: input.activeId,
|
|
1874
|
+
transforms: input.beforeTransforms,
|
|
1875
|
+
});
|
|
1876
|
+
if (!patch || patch.createdIds.length === 0) {
|
|
1877
|
+
state.status = 'Duplicate drag rejected';
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
const originalSelection = state.session.getSelection();
|
|
1881
|
+
const result = state.session.dispatch({
|
|
1882
|
+
type: 'document.patch',
|
|
1883
|
+
label: patch.label ?? `Duplicate ${input.targetIds.length} object(s)`,
|
|
1884
|
+
patch: patch.patch,
|
|
1885
|
+
targetId: patch.activeId ?? patch.createdIds[patch.createdIds.length - 1] ?? undefined,
|
|
1886
|
+
});
|
|
1887
|
+
if (!result.documentChanged) {
|
|
1888
|
+
state.status = 'Duplicate drag unchanged';
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
const createdIds = patch.createdIds.filter(id => isNodeSelectableInDocument(options, result.workingDocument, id));
|
|
1892
|
+
if (createdIds.length === 0) {
|
|
1893
|
+
const undone = state.session.undo();
|
|
1894
|
+
if (undone)
|
|
1895
|
+
rebuildProjectionFromDocument(state, options, undone.workingDocument, originalSelection);
|
|
1896
|
+
state.status = 'Duplicate drag rejected: duplicated selection is not selectable';
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
const activeId = patch.activeId && createdIds.includes(patch.activeId)
|
|
1900
|
+
? patch.activeId
|
|
1901
|
+
: createdIds[createdIds.length - 1] ?? null;
|
|
1902
|
+
const selectionResult = state.session.dispatch({
|
|
1903
|
+
type: 'selection.replace',
|
|
1904
|
+
selectedIds: createdIds,
|
|
1905
|
+
activeId,
|
|
1906
|
+
label: 'Select Duplicate Drag Targets',
|
|
1907
|
+
});
|
|
1908
|
+
state.duplicateDrag = {
|
|
1909
|
+
originalSelection,
|
|
1910
|
+
createdIds,
|
|
1911
|
+
activeId,
|
|
1912
|
+
};
|
|
1913
|
+
rebuildProjectionFromDocument(state, options, result.workingDocument, selectionResult.selection);
|
|
1914
|
+
if (patch.reprojectIds?.length)
|
|
1915
|
+
reprojectProjectionForChangedIds(state, options, result.workingDocument, patch.reprojectIds);
|
|
1916
|
+
else
|
|
1917
|
+
syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds ?? createdIds);
|
|
1918
|
+
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
1919
|
+
state.status = patch.label ?? `Duplicated ${createdIds.length} object(s)`;
|
|
1920
|
+
return {
|
|
1921
|
+
targetIds: createdIds,
|
|
1922
|
+
activeId,
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function cancelDuplicateDrag(state, options) {
|
|
1926
|
+
const duplicate = state.duplicateDrag;
|
|
1927
|
+
if (!duplicate || !state.session)
|
|
1928
|
+
return false;
|
|
1929
|
+
state.duplicateDrag = null;
|
|
1930
|
+
const undone = state.session.undo();
|
|
1931
|
+
if (!undone)
|
|
1932
|
+
return false;
|
|
1933
|
+
const selectionResult = state.session.dispatch({
|
|
1934
|
+
type: 'selection.replace',
|
|
1935
|
+
selectedIds: duplicate.originalSelection.selectedIds,
|
|
1936
|
+
activeId: duplicate.originalSelection.activeId,
|
|
1937
|
+
label: 'Restore Duplicate Drag Selection',
|
|
1938
|
+
});
|
|
1939
|
+
rebuildProjectionFromDocument(state, options, undone.workingDocument, selectionResult.selection);
|
|
1940
|
+
state.summary = summarizeDocument(options, undone.workingDocument, state.session.getSource());
|
|
1941
|
+
state.status = `Canceled duplicate drag ${duplicate.createdIds.length} object(s)`;
|
|
1942
|
+
return true;
|
|
1943
|
+
}
|
|
974
1944
|
function commitGizmoTransform(state, options, event) {
|
|
975
1945
|
if (state.mode !== 'editor' || !state.session)
|
|
976
1946
|
return false;
|
|
@@ -981,8 +1951,10 @@ function commitGizmoTransform(state, options, event) {
|
|
|
981
1951
|
document,
|
|
982
1952
|
});
|
|
983
1953
|
if (!patch) {
|
|
984
|
-
|
|
985
|
-
|
|
1954
|
+
if (!cancelDuplicateDrag(state, options)) {
|
|
1955
|
+
restoreBatchTransformPreview(state, event.targets);
|
|
1956
|
+
state.status = `Ignored ${event.tool} ${event.targetIds.length} objects`;
|
|
1957
|
+
}
|
|
986
1958
|
return false;
|
|
987
1959
|
}
|
|
988
1960
|
const result = state.session.dispatch({
|
|
@@ -990,11 +1962,14 @@ function commitGizmoTransform(state, options, event) {
|
|
|
990
1962
|
label: patch.label ?? `${event.tool} ${event.targetIds.length} objects`,
|
|
991
1963
|
patch: patch.patch,
|
|
992
1964
|
targetId: event.activeId ?? undefined,
|
|
1965
|
+
}, {
|
|
1966
|
+
mergeWithPrevious: event.targetIds.some(id => state.duplicateDrag?.createdIds.includes(id)) === true,
|
|
993
1967
|
});
|
|
994
1968
|
syncProjectionForDispatchResult(state, options, result);
|
|
995
1969
|
syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds ?? event.targetIds);
|
|
996
1970
|
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
997
1971
|
state.status = patch.label ?? `${event.tool} ${event.targetIds.length} objects`;
|
|
1972
|
+
state.duplicateDrag = null;
|
|
998
1973
|
return result.documentChanged;
|
|
999
1974
|
}
|
|
1000
1975
|
const patch = options.documentAdapter.createTransformPatch?.({
|
|
@@ -1002,12 +1977,15 @@ function commitGizmoTransform(state, options, event) {
|
|
|
1002
1977
|
targetId: event.nodeId,
|
|
1003
1978
|
tool: event.tool,
|
|
1004
1979
|
space: event.space,
|
|
1980
|
+
constraint: event.constraint,
|
|
1005
1981
|
before: event.before,
|
|
1006
1982
|
after: event.after,
|
|
1007
1983
|
});
|
|
1008
1984
|
if (!patch) {
|
|
1009
|
-
state
|
|
1010
|
-
|
|
1985
|
+
if (!cancelDuplicateDrag(state, options)) {
|
|
1986
|
+
state.projection?.setNodeTransformPreview(event.nodeId, event.before);
|
|
1987
|
+
state.status = `Ignored ${event.tool} ${event.nodeId}`;
|
|
1988
|
+
}
|
|
1011
1989
|
return false;
|
|
1012
1990
|
}
|
|
1013
1991
|
const result = state.session.dispatch({
|
|
@@ -1015,6 +1993,8 @@ function commitGizmoTransform(state, options, event) {
|
|
|
1015
1993
|
label: patch.label ?? `${event.tool} ${event.nodeId}`,
|
|
1016
1994
|
patch: patch.patch,
|
|
1017
1995
|
targetId: event.nodeId,
|
|
1996
|
+
}, {
|
|
1997
|
+
mergeWithPrevious: state.duplicateDrag?.createdIds.includes(event.nodeId) === true,
|
|
1018
1998
|
});
|
|
1019
1999
|
if (patch.changedIds)
|
|
1020
2000
|
syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds);
|
|
@@ -1022,6 +2002,7 @@ function commitGizmoTransform(state, options, event) {
|
|
|
1022
2002
|
syncProjectionForDispatchResult(state, options, result, patch.changedId ?? event.nodeId);
|
|
1023
2003
|
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
1024
2004
|
state.status = patch.label ?? `${event.tool} ${event.nodeId}`;
|
|
2005
|
+
state.duplicateDrag = null;
|
|
1025
2006
|
return result.documentChanged;
|
|
1026
2007
|
}
|
|
1027
2008
|
function isTransformBatchCommit(event) {
|
|
@@ -1040,6 +2021,22 @@ function cancelActiveOperation(state) {
|
|
|
1040
2021
|
state.sceneViewInput?.cancelActiveIntent();
|
|
1041
2022
|
state.selectionController?.cancelBoxSelection();
|
|
1042
2023
|
cancelActiveGizmoDrag(state);
|
|
2024
|
+
clearArmedPlacement(state);
|
|
2025
|
+
}
|
|
2026
|
+
function cancelEditorIntent(state, options) {
|
|
2027
|
+
if (state.mode !== 'editor')
|
|
2028
|
+
return false;
|
|
2029
|
+
if (state.viewportTools.activeUtilityTool === 'measure-distance'
|
|
2030
|
+
|| state.viewportMeasurement.start
|
|
2031
|
+
|| state.viewportMeasurement.end) {
|
|
2032
|
+
return clearViewportMeasurement(state);
|
|
2033
|
+
}
|
|
2034
|
+
const selection = state.session?.getSelection();
|
|
2035
|
+
if (selection && selection.selectedIds.length > 0) {
|
|
2036
|
+
return dispatchSelectionCommand(state, options, { type: 'selection.clear', label: 'Clear Selection' });
|
|
2037
|
+
}
|
|
2038
|
+
cancelActiveOperation(state);
|
|
2039
|
+
return true;
|
|
1043
2040
|
}
|
|
1044
2041
|
function focusSelectedProjection(state) {
|
|
1045
2042
|
if (state.mode !== 'editor')
|
|
@@ -1072,6 +2069,17 @@ function formatEditorStatusTime(timestamp) {
|
|
|
1072
2069
|
const pad = (value) => String(value).padStart(2, '0');
|
|
1073
2070
|
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1074
2071
|
}
|
|
2072
|
+
function formatVec3(value) {
|
|
2073
|
+
return `${formatPlacementNumber(value.x)}, ${formatPlacementNumber(value.y)}, ${formatPlacementNumber(value.z)}`;
|
|
2074
|
+
}
|
|
2075
|
+
function formatPlacementNumber(value) {
|
|
2076
|
+
return Number.isFinite(value) ? Number(value.toFixed(3)).toString() : String(value);
|
|
2077
|
+
}
|
|
2078
|
+
function formatAssetLabel(asset, fallback) {
|
|
2079
|
+
const record = isObjectRecord(asset) ? asset : null;
|
|
2080
|
+
const label = record?.label ?? record?.displayName ?? record?.name;
|
|
2081
|
+
return typeof label === 'string' && label.trim() ? label : fallback;
|
|
2082
|
+
}
|
|
1075
2083
|
function undoSessionChange(state, options) {
|
|
1076
2084
|
cancelActiveOperation(state);
|
|
1077
2085
|
const result = state.session?.undo();
|
|
@@ -1083,7 +2091,7 @@ function undoSessionChange(state, options) {
|
|
|
1083
2091
|
return true;
|
|
1084
2092
|
}
|
|
1085
2093
|
function redoSessionChange(state, options) {
|
|
1086
|
-
|
|
2094
|
+
cancelActiveOperation(state);
|
|
1087
2095
|
const result = state.session?.redo();
|
|
1088
2096
|
if (!result)
|
|
1089
2097
|
return false;
|
|
@@ -1134,6 +2142,92 @@ function reprojectProjectionForChangedIds(state, options, document, changedIds)
|
|
|
1134
2142
|
syncSelectionToProjection(state, selection);
|
|
1135
2143
|
state.gizmo?.refreshSelection();
|
|
1136
2144
|
}
|
|
2145
|
+
function setSceneCameraPreviewEnabled(state, options, enabled) {
|
|
2146
|
+
if (!enabled) {
|
|
2147
|
+
state.sceneCameraPreviewEnabled = false;
|
|
2148
|
+
state.sceneCameraPreview?.setActive(false);
|
|
2149
|
+
state.status = 'Scene Camera preview disabled';
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
state.sceneCameraPreviewEnabled = true;
|
|
2153
|
+
if (!syncSceneCameraPreview(state, options)) {
|
|
2154
|
+
state.sceneCameraPreviewEnabled = false;
|
|
2155
|
+
state.status = 'Scene Camera preview unavailable';
|
|
2156
|
+
state.statusTone = 'warning';
|
|
2157
|
+
state.statusToneStatus = state.status;
|
|
2158
|
+
state.statusDetails = 'The current document did not provide a Main Camera preview rig.';
|
|
2159
|
+
return true;
|
|
2160
|
+
}
|
|
2161
|
+
state.status = 'Scene Camera preview enabled';
|
|
2162
|
+
return true;
|
|
2163
|
+
}
|
|
2164
|
+
function setGridVisible(state, visible) {
|
|
2165
|
+
const nextVisible = visible === true;
|
|
2166
|
+
if (state.gridVisible === nextVisible)
|
|
2167
|
+
return false;
|
|
2168
|
+
state.gridVisible = nextVisible;
|
|
2169
|
+
state.grid?.setVisible(nextVisible);
|
|
2170
|
+
state.status = nextVisible ? 'Grid visible' : 'Grid hidden';
|
|
2171
|
+
state.statusTone = 'default';
|
|
2172
|
+
state.statusToneStatus = state.status;
|
|
2173
|
+
state.statusDetails = '';
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
function syncSceneCameraPreview(state, options) {
|
|
2177
|
+
const controller = state.sceneCameraPreview;
|
|
2178
|
+
if (!controller)
|
|
2179
|
+
return false;
|
|
2180
|
+
if (!state.sceneCameraPreviewEnabled) {
|
|
2181
|
+
controller.setActive(false);
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
const document = state.session?.getState().workingDocument ?? null;
|
|
2185
|
+
const rig = document ? options.documentAdapter.getSceneCameraPreviewRig?.(document) ?? null : null;
|
|
2186
|
+
if (!rig) {
|
|
2187
|
+
controller.setActive(false);
|
|
2188
|
+
return false;
|
|
2189
|
+
}
|
|
2190
|
+
controller.setActive(true, rig);
|
|
2191
|
+
return controller.isActive();
|
|
2192
|
+
}
|
|
2193
|
+
function syncViewportCameraState(state) {
|
|
2194
|
+
const cameraState = state.sceneViewCamera?.getState();
|
|
2195
|
+
if (!cameraState)
|
|
2196
|
+
return;
|
|
2197
|
+
state.viewportTools = {
|
|
2198
|
+
...state.viewportTools,
|
|
2199
|
+
viewPreset: cameraState.viewPreset,
|
|
2200
|
+
projectionMode: cameraState.projectionMode,
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
function syncViewportSpatialOverlay(state) {
|
|
2204
|
+
const overlay = state.sceneViewSpatialOverlay;
|
|
2205
|
+
const selection = state.session?.getState().selection;
|
|
2206
|
+
const activeId = selection?.activeId ?? null;
|
|
2207
|
+
if (state.mode !== 'editor' || !overlay || !activeId || selection?.selectedIds.length !== 1) {
|
|
2208
|
+
state.viewportSpatialOverlay = createEmptyEditorViewportSpatialOverlayState();
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
const bounds = state.projection?.getSelectionBounds([activeId]) ?? null;
|
|
2212
|
+
const anchor = state.projection?.readNodeTransform(activeId)?.position ?? bounds?.center ?? null;
|
|
2213
|
+
state.viewportSpatialOverlay = overlay.compute({
|
|
2214
|
+
nodeId: activeId,
|
|
2215
|
+
bounds,
|
|
2216
|
+
anchor,
|
|
2217
|
+
settings: state.viewportTools.overlay,
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
function syncViewportMeasurementState(state) {
|
|
2221
|
+
if (state.mode !== 'editor')
|
|
2222
|
+
return;
|
|
2223
|
+
const measurement = state.sceneViewMeasurement?.getState();
|
|
2224
|
+
if (measurement)
|
|
2225
|
+
state.viewportMeasurement = measurement;
|
|
2226
|
+
}
|
|
2227
|
+
function hasSceneCameraPreviewRig(state, options) {
|
|
2228
|
+
const document = state.session?.getState().workingDocument ?? null;
|
|
2229
|
+
return !!document && !!options.documentAdapter.getSceneCameraPreviewRig?.(document);
|
|
2230
|
+
}
|
|
1137
2231
|
function createUiState(state, options) {
|
|
1138
2232
|
const sessionState = state.session?.getState();
|
|
1139
2233
|
const document = sessionState?.workingDocument ?? null;
|
|
@@ -1181,6 +2275,9 @@ function createUiState(state, options) {
|
|
|
1181
2275
|
inspectorObject,
|
|
1182
2276
|
inspectorMultiObject,
|
|
1183
2277
|
boxSelection: state.boxSelection,
|
|
2278
|
+
coordinateAxes: options.world?.coordinateAxes === false
|
|
2279
|
+
? null
|
|
2280
|
+
: createSceneViewCoordinateAxesState(state),
|
|
1184
2281
|
transformTool: {
|
|
1185
2282
|
activeTool: state.gizmo?.getState().tool ?? state.transformTool,
|
|
1186
2283
|
activeSpace: state.gizmo?.getState().space ?? state.transformSpace,
|
|
@@ -1188,6 +2285,24 @@ function createUiState(state, options) {
|
|
|
1188
2285
|
dragPhase: state.gizmo?.getState().dragPhase ?? 'idle',
|
|
1189
2286
|
draggingNodeId: state.gizmo?.getState().draggingNodeId ?? null,
|
|
1190
2287
|
},
|
|
2288
|
+
transformOperations: {
|
|
2289
|
+
settings: cloneTransformOperationSettings(state.transformOperationSettings),
|
|
2290
|
+
selectedCount: selectedIds.length,
|
|
2291
|
+
activeId,
|
|
2292
|
+
canAlign: selectedIds.length >= 2 && activeId != null,
|
|
2293
|
+
canDistribute: selectedIds.length >= 3,
|
|
2294
|
+
},
|
|
2295
|
+
viewportTools: cloneEditorViewportToolState(state.viewportTools),
|
|
2296
|
+
viewportMeasurement: cloneViewportMeasurement(state.viewportMeasurement),
|
|
2297
|
+
viewportSpatialOverlay: cloneEditorViewportSpatialOverlayState(state.viewportSpatialOverlay),
|
|
2298
|
+
sceneCameraPreview: {
|
|
2299
|
+
enabled: state.sceneCameraPreviewEnabled,
|
|
2300
|
+
available: hasSceneCameraPreviewRig(state, options),
|
|
2301
|
+
},
|
|
2302
|
+
grid: {
|
|
2303
|
+
visible: state.gridVisible,
|
|
2304
|
+
available: !!state.grid,
|
|
2305
|
+
},
|
|
1191
2306
|
session: sessionState
|
|
1192
2307
|
? {
|
|
1193
2308
|
source: sessionState.source,
|
|
@@ -1199,6 +2314,89 @@ function createUiState(state, options) {
|
|
|
1199
2314
|
: null,
|
|
1200
2315
|
};
|
|
1201
2316
|
}
|
|
2317
|
+
function createSceneViewCoordinateAxesState(state) {
|
|
2318
|
+
if (state.mode !== 'editor')
|
|
2319
|
+
return null;
|
|
2320
|
+
const scene = state.world?.scene ?? null;
|
|
2321
|
+
const camera = scene?.activeCamera ?? state.world?.camera ?? null;
|
|
2322
|
+
const Vector3 = state.babylon?.Vector3;
|
|
2323
|
+
if (!scene || !camera || !Vector3)
|
|
2324
|
+
return null;
|
|
2325
|
+
const right = readSceneViewCameraDirection(camera, Vector3, { x: 1, y: 0, z: 0 });
|
|
2326
|
+
const up = readSceneViewCameraDirection(camera, Vector3, { x: 0, y: 1, z: 0 });
|
|
2327
|
+
const forward = readSceneViewCameraForward(scene, camera, Vector3);
|
|
2328
|
+
if (!right || !up || !forward)
|
|
2329
|
+
return null;
|
|
2330
|
+
return {
|
|
2331
|
+
axes: [
|
|
2332
|
+
createSceneViewCoordinateAxis('x', 'X', '#ff4b70', { x: 1, y: 0, z: 0 }, right, up, forward),
|
|
2333
|
+
createSceneViewCoordinateAxis('y', 'Y', '#75ff42', { x: 0, y: 1, z: 0 }, right, up, forward),
|
|
2334
|
+
createSceneViewCoordinateAxis('z', 'Z', '#4f86ff', { x: 0, y: 0, z: 1 }, right, up, forward),
|
|
2335
|
+
],
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
function createSceneViewCoordinateAxis(id, label, color, worldAxis, cameraRight, cameraUp, cameraForward) {
|
|
2339
|
+
const screenX = dotSceneViewAxisVec3(worldAxis, cameraRight);
|
|
2340
|
+
const screenY = -dotSceneViewAxisVec3(worldAxis, cameraUp);
|
|
2341
|
+
const projectedLength = Math.hypot(screenX, screenY);
|
|
2342
|
+
const fallback = getSceneViewAxisFallbackDirection(id);
|
|
2343
|
+
const direction = projectedLength > 0.0001
|
|
2344
|
+
? { x: screenX / projectedLength, y: screenY / projectedLength }
|
|
2345
|
+
: fallback;
|
|
2346
|
+
return {
|
|
2347
|
+
id,
|
|
2348
|
+
label,
|
|
2349
|
+
color,
|
|
2350
|
+
x: direction.x,
|
|
2351
|
+
y: direction.y,
|
|
2352
|
+
depth: dotSceneViewAxisVec3(worldAxis, cameraForward),
|
|
2353
|
+
scale: clampNumber(projectedLength, 0.36, 1),
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
function readSceneViewCameraDirection(camera, Vector3, local) {
|
|
2357
|
+
if (!camera?.getDirection)
|
|
2358
|
+
return null;
|
|
2359
|
+
try {
|
|
2360
|
+
return normalizeSceneViewAxisVec3(camera.getDirection(new Vector3(local.x, local.y, local.z)));
|
|
2361
|
+
}
|
|
2362
|
+
catch {
|
|
2363
|
+
return null;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
function readSceneViewCameraForward(scene, camera, Vector3) {
|
|
2367
|
+
const rayDirection = normalizeSceneViewAxisVec3(camera?.getForwardRay?.()?.direction);
|
|
2368
|
+
if (rayDirection)
|
|
2369
|
+
return rayDirection;
|
|
2370
|
+
return readSceneViewCameraDirection(camera, Vector3, {
|
|
2371
|
+
x: 0,
|
|
2372
|
+
y: 0,
|
|
2373
|
+
z: scene?.useRightHandedSystem ? -1 : 1,
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
function normalizeSceneViewAxisVec3(value) {
|
|
2377
|
+
const x = Number(value?.x) || 0;
|
|
2378
|
+
const y = Number(value?.y) || 0;
|
|
2379
|
+
const z = Number(value?.z) || 0;
|
|
2380
|
+
const length = Math.hypot(x, y, z);
|
|
2381
|
+
if (!Number.isFinite(length) || length <= 0.000001)
|
|
2382
|
+
return null;
|
|
2383
|
+
return { x: x / length, y: y / length, z: z / length };
|
|
2384
|
+
}
|
|
2385
|
+
function dotSceneViewAxisVec3(left, right) {
|
|
2386
|
+
return left.x * right.x + left.y * right.y + left.z * right.z;
|
|
2387
|
+
}
|
|
2388
|
+
function getSceneViewAxisFallbackDirection(id) {
|
|
2389
|
+
if (id === 'x')
|
|
2390
|
+
return { x: 1, y: 0 };
|
|
2391
|
+
if (id === 'y')
|
|
2392
|
+
return { x: 0, y: -1 };
|
|
2393
|
+
return { x: -1, y: 0 };
|
|
2394
|
+
}
|
|
2395
|
+
function clampNumber(value, min, max) {
|
|
2396
|
+
if (!Number.isFinite(value))
|
|
2397
|
+
return min;
|
|
2398
|
+
return Math.min(max, Math.max(min, value));
|
|
2399
|
+
}
|
|
1202
2400
|
function withRuntimeInspectorSections(state, options, document, inspectorObject) {
|
|
1203
2401
|
const baseInspectorObject = withInspectorComponentSections(state, options, document, inspectorObject);
|
|
1204
2402
|
const activeId = inspectorObject.activeId;
|