@fps-games/editor 0.1.1-beta.9 → 0.1.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/local-editor-harness.d.ts +48 -4
- package/dist/local-editor-harness.d.ts.map +1 -1
- package/dist/local-editor-harness.js +428 -88
- package/dist/local-editor-harness.js.map +1 -1
- package/dist/local-editor-scene-render-scheduler.d.ts +26 -0
- package/dist/local-editor-scene-render-scheduler.d.ts.map +1 -0
- package/dist/local-editor-scene-render-scheduler.js +139 -0
- package/dist/local-editor-scene-render-scheduler.js.map +1 -0
- package/dist/local-editor-viewport-render-coordinator.d.ts +17 -0
- package/dist/local-editor-viewport-render-coordinator.d.ts.map +1 -0
- package/dist/local-editor-viewport-render-coordinator.js +102 -0
- package/dist/local-editor-viewport-render-coordinator.js.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +2 -0
- package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +9 -3
- package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -1
- 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 +2 -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 +1 -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 +1 -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.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +167 -27
- package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/main-camera-preview.d.ts +25 -0
- package/node_modules/@fps-games/editor-babylon/dist/main-camera-preview.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/main-camera-preview.js +162 -0
- package/node_modules/@fps-games/editor-babylon/dist/main-camera-preview.js.map +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +28 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/projection.js +169 -25
- 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 +2 -22
- package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.js +1 -121
- package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.js.map +1 -1
- 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 +68 -30
- 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 +1 -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/local-editor-ui-hierarchy-actions.d.ts +2 -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 +139 -34
- 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 +2 -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 +2 -0
- 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-panels.d.ts +1 -0
- 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 +96 -14
- 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-tooltip.js +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +56 -2
- 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 +1 -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 +3 -0
- 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 +4 -2
- 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 +253 -39
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/package.json +2 -2
- package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts +6 -6
- package/node_modules/@fps-games/editor-core/dist/viewport-tools.js +3 -3
- package/node_modules/@fps-games/editor-core/dist/viewport-tools.js.map +1 -1
- package/node_modules/@fps-games/editor-core/package.json +2 -2
- package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
- package/node_modules/@fps-games/editor-protocol/package.json +1 -1
- package/package.json +6 -6
|
@@ -1,6 +1,8 @@
|
|
|
1
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,
|
|
3
|
+
import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonMainCameraPreviewController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonSceneViewMeasurementController, createBabylonSceneViewSpatialOverlayController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
|
|
4
|
+
import { createLocalEditorSceneRenderScheduler, } from './local-editor-scene-render-scheduler';
|
|
5
|
+
import { createLocalEditorViewportRenderCoordinator, } from './local-editor-viewport-render-coordinator';
|
|
4
6
|
export function createLocalEditorHarness(options) {
|
|
5
7
|
const root = options.root ?? document.body;
|
|
6
8
|
const state = {
|
|
@@ -17,8 +19,8 @@ export function createLocalEditorHarness(options) {
|
|
|
17
19
|
gridVisible: true,
|
|
18
20
|
projection: null,
|
|
19
21
|
gizmo: null,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
mainCameraPreview: null,
|
|
23
|
+
mainCameraPreviewEnabled: false,
|
|
22
24
|
sceneViewInput: null,
|
|
23
25
|
sceneViewCamera: null,
|
|
24
26
|
sceneViewMeasurement: null,
|
|
@@ -34,6 +36,9 @@ export function createLocalEditorHarness(options) {
|
|
|
34
36
|
viewportSpatialOverlay: createEmptyEditorViewportSpatialOverlayState(),
|
|
35
37
|
duplicateDrag: null,
|
|
36
38
|
armedPlacement: null,
|
|
39
|
+
sceneRenderScheduler: null,
|
|
40
|
+
viewportRenderCoordinator: null,
|
|
41
|
+
sceneFrameStats: null,
|
|
37
42
|
resizeHandler: null,
|
|
38
43
|
status: 'Game running',
|
|
39
44
|
statusTone: 'default',
|
|
@@ -47,6 +52,9 @@ export function createLocalEditorHarness(options) {
|
|
|
47
52
|
theme: options.theme,
|
|
48
53
|
localTestActions: options.localTestActions,
|
|
49
54
|
inspector: options.inspector,
|
|
55
|
+
hierarchy: {
|
|
56
|
+
contextActions: createBrowserHierarchyContextActions(state, options),
|
|
57
|
+
},
|
|
50
58
|
callbacks: {
|
|
51
59
|
onEnterEditor: () => {
|
|
52
60
|
void runExclusive(state, harness.render, () => harness.enterEditor());
|
|
@@ -138,16 +146,19 @@ export function createLocalEditorHarness(options) {
|
|
|
138
146
|
state.transformConstraint = normalizeTransformConstraint(tool, state.transformConstraint);
|
|
139
147
|
state.gizmo?.setTool(tool);
|
|
140
148
|
state.gizmo?.setConstraint(state.transformConstraint);
|
|
149
|
+
requestEditorSceneFrame(state, 'transform-tool-change');
|
|
141
150
|
harness.render();
|
|
142
151
|
},
|
|
143
152
|
onTransformSpaceChange: (space) => {
|
|
144
153
|
state.transformSpace = space;
|
|
145
154
|
state.gizmo?.setSpace(space);
|
|
155
|
+
requestEditorSceneFrame(state, 'transform-space-change');
|
|
146
156
|
harness.render();
|
|
147
157
|
},
|
|
148
158
|
onTransformConstraintChange: (constraint) => {
|
|
149
159
|
state.transformConstraint = normalizeTransformConstraint(state.transformTool, constraint);
|
|
150
160
|
state.gizmo?.setConstraint(state.transformConstraint);
|
|
161
|
+
requestEditorSceneFrame(state, 'transform-constraint-change');
|
|
151
162
|
harness.render();
|
|
152
163
|
},
|
|
153
164
|
onTransformSnapEnabledChange: (enabled) => {
|
|
@@ -159,6 +170,7 @@ export function createLocalEditorHarness(options) {
|
|
|
159
170
|
});
|
|
160
171
|
state.gizmo?.setOperationSettings(state.transformOperationSettings);
|
|
161
172
|
state.status = enabled ? 'Transform snap enabled' : 'Transform snap disabled';
|
|
173
|
+
requestEditorSceneFrame(state, 'transform-operation-settings-change');
|
|
162
174
|
harness.render();
|
|
163
175
|
},
|
|
164
176
|
onTransformSnapStepChange: (input) => {
|
|
@@ -175,6 +187,7 @@ export function createLocalEditorHarness(options) {
|
|
|
175
187
|
state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, { snap });
|
|
176
188
|
state.gizmo?.setOperationSettings(state.transformOperationSettings);
|
|
177
189
|
state.status = `Transform snap ${input.kind} step ${value}`;
|
|
190
|
+
requestEditorSceneFrame(state, 'transform-operation-settings-change');
|
|
178
191
|
harness.render();
|
|
179
192
|
},
|
|
180
193
|
onPlacementModeChange: (mode) => {
|
|
@@ -185,6 +198,7 @@ export function createLocalEditorHarness(options) {
|
|
|
185
198
|
if (state.transformOperationSettings.placementMode === 'off')
|
|
186
199
|
clearArmedPlacement(state);
|
|
187
200
|
state.status = `Placement mode: ${state.transformOperationSettings.placementMode}`;
|
|
201
|
+
requestEditorSceneFrame(state, 'placement-mode-change');
|
|
188
202
|
harness.render();
|
|
189
203
|
},
|
|
190
204
|
onTransformAction: (action) => {
|
|
@@ -211,8 +225,8 @@ export function createLocalEditorHarness(options) {
|
|
|
211
225
|
if (clearViewportMeasurement(state))
|
|
212
226
|
harness.render();
|
|
213
227
|
},
|
|
214
|
-
|
|
215
|
-
if (
|
|
228
|
+
onMainCameraPreviewToggle: (enabled) => {
|
|
229
|
+
if (setMainCameraPreviewEnabled(state, options, enabled))
|
|
216
230
|
harness.render();
|
|
217
231
|
},
|
|
218
232
|
onGridVisibleChange: (visible) => {
|
|
@@ -229,17 +243,23 @@ export function createLocalEditorHarness(options) {
|
|
|
229
243
|
},
|
|
230
244
|
onCancelActiveOperation: () => {
|
|
231
245
|
cancelActiveOperation(state);
|
|
246
|
+
requestEditorSceneFrame(state, 'cancel-active-operation');
|
|
232
247
|
harness.render();
|
|
233
248
|
},
|
|
234
249
|
},
|
|
235
250
|
});
|
|
251
|
+
const renderUi = () => {
|
|
252
|
+
syncViewportCameraState(state);
|
|
253
|
+
syncViewportMeasurementState(state);
|
|
254
|
+
syncViewportSpatialOverlay(state);
|
|
255
|
+
ui.update(createUiState(state, options));
|
|
256
|
+
};
|
|
236
257
|
harness = {
|
|
237
258
|
render() {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
ui.update(createUiState(state, options));
|
|
259
|
+
renderUi();
|
|
260
|
+
},
|
|
261
|
+
notifyViewportRevealed(reason = 'viewport-revealed') {
|
|
262
|
+
state.viewportRenderCoordinator?.requestRevealFrame(reason);
|
|
243
263
|
},
|
|
244
264
|
setTheme(theme) {
|
|
245
265
|
ui.setTheme(theme);
|
|
@@ -307,10 +327,12 @@ export function createLocalEditorHarness(options) {
|
|
|
307
327
|
reduceDocument: options.documentAdapter.reduceDocument,
|
|
308
328
|
});
|
|
309
329
|
await options.worldAdapter.disposeGameWorld();
|
|
310
|
-
await createEditorWorld(state, options, harness.render);
|
|
330
|
+
await createEditorWorld(state, options, harness.render, stats => ui.updateSceneFrameStats?.(stats));
|
|
311
331
|
state.mode = 'editor';
|
|
312
332
|
state.summary = loadedSource?.summary ?? summarizeDocument(options, preparedDocument, source);
|
|
313
333
|
state.status = `GameWorld disposed; EditorWorld active; assets=${assets.length}`;
|
|
334
|
+
harness.render();
|
|
335
|
+
harness.notifyViewportRevealed('editor-enter');
|
|
314
336
|
},
|
|
315
337
|
async saveScene() {
|
|
316
338
|
cancelActiveOperation(state);
|
|
@@ -485,11 +507,11 @@ function cloneViewportMeasurement(measurement) {
|
|
|
485
507
|
};
|
|
486
508
|
}
|
|
487
509
|
function setViewportViewPreset(state, preset) {
|
|
488
|
-
if (state.
|
|
489
|
-
state.status = 'Viewport view unavailable during
|
|
510
|
+
if (state.mainCameraPreviewEnabled) {
|
|
511
|
+
state.status = 'Viewport view unavailable during Main Camera preview';
|
|
490
512
|
state.statusTone = 'warning';
|
|
491
513
|
state.statusToneStatus = state.status;
|
|
492
|
-
state.statusDetails = 'Disable
|
|
514
|
+
state.statusDetails = 'Disable Main Camera preview before switching editor viewport views.';
|
|
493
515
|
return true;
|
|
494
516
|
}
|
|
495
517
|
const nextPreset = normalizeViewportViewPreset(preset);
|
|
@@ -508,14 +530,15 @@ function setViewportViewPreset(state, preset) {
|
|
|
508
530
|
state.statusTone = 'default';
|
|
509
531
|
state.statusToneStatus = state.status;
|
|
510
532
|
state.statusDetails = '';
|
|
533
|
+
requestEditorSceneFrame(state, 'viewport-view-preset-change');
|
|
511
534
|
return true;
|
|
512
535
|
}
|
|
513
536
|
function setViewportProjectionMode(state, mode) {
|
|
514
|
-
if (state.
|
|
515
|
-
state.status = 'Viewport projection unavailable during
|
|
537
|
+
if (state.mainCameraPreviewEnabled) {
|
|
538
|
+
state.status = 'Viewport projection unavailable during Main Camera preview';
|
|
516
539
|
state.statusTone = 'warning';
|
|
517
540
|
state.statusToneStatus = state.status;
|
|
518
|
-
state.statusDetails = 'Disable
|
|
541
|
+
state.statusDetails = 'Disable Main Camera preview before switching editor viewport projection.';
|
|
519
542
|
return true;
|
|
520
543
|
}
|
|
521
544
|
const nextMode = normalizeViewportProjectionMode(mode);
|
|
@@ -531,6 +554,7 @@ function setViewportProjectionMode(state, mode) {
|
|
|
531
554
|
state.statusTone = 'default';
|
|
532
555
|
state.statusToneStatus = state.status;
|
|
533
556
|
state.statusDetails = '';
|
|
557
|
+
requestEditorSceneFrame(state, 'viewport-projection-mode-change');
|
|
534
558
|
return true;
|
|
535
559
|
}
|
|
536
560
|
function setViewportOverlaySettings(state, settings) {
|
|
@@ -682,7 +706,7 @@ function validateTransformActionSelection(state, action) {
|
|
|
682
706
|
? { ok: true }
|
|
683
707
|
: { ok: false, message: 'Distribute needs at least 3 selected objects' };
|
|
684
708
|
}
|
|
685
|
-
async function createEditorWorld(state, options,
|
|
709
|
+
async function createEditorWorld(state, options, renderUi, updateSceneFrameStats) {
|
|
686
710
|
disposeEditorWorld(state);
|
|
687
711
|
const canvas = options.worldAdapter.getCanvas();
|
|
688
712
|
if (!canvas)
|
|
@@ -700,13 +724,27 @@ async function createEditorWorld(state, options, render) {
|
|
|
700
724
|
useRightHandedSystem: options.world?.useRightHandedSystem,
|
|
701
725
|
enableDefaultCameraControls: false,
|
|
702
726
|
});
|
|
703
|
-
const
|
|
727
|
+
const sceneRenderScheduler = createLocalEditorSceneRenderScheduler(() => world.render(), {
|
|
728
|
+
onStatsChange(stats) {
|
|
729
|
+
state.sceneFrameStats = stats;
|
|
730
|
+
updateSceneFrameStats(stats);
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
const viewportRenderCoordinator = createLocalEditorViewportRenderCoordinator({
|
|
734
|
+
scheduler: sceneRenderScheduler,
|
|
735
|
+
getEngine: () => engine,
|
|
736
|
+
getScene: () => world.scene,
|
|
737
|
+
});
|
|
738
|
+
const grid = options.createGrid?.(babylon, world.scene, world.camera) ?? null;
|
|
704
739
|
grid?.setVisible(state.gridVisible);
|
|
705
740
|
const projection = createBabylonEditorProjection({
|
|
706
741
|
babylon,
|
|
707
742
|
scene: world.scene,
|
|
708
743
|
importModel: options.worldAdapter.importProjectionModel,
|
|
709
744
|
logger: console,
|
|
745
|
+
onProjectionReady(event) {
|
|
746
|
+
viewportRenderCoordinator.invalidateScene(`projection-${event.nodeId}-ready`);
|
|
747
|
+
},
|
|
710
748
|
});
|
|
711
749
|
const gizmo = createBabylonTransformGizmoController({
|
|
712
750
|
babylon,
|
|
@@ -716,27 +754,33 @@ async function createEditorWorld(state, options, render) {
|
|
|
716
754
|
initialSpace: state.transformSpace,
|
|
717
755
|
logger: console,
|
|
718
756
|
onDragStart(event) {
|
|
757
|
+
beginEditorSceneContinuousRender(state, 'gizmo-drag');
|
|
719
758
|
state.status = event.targetIds.length > 1
|
|
720
759
|
? `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.targetIds.length} objects`
|
|
721
760
|
: `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
|
|
722
|
-
|
|
761
|
+
renderUi();
|
|
723
762
|
},
|
|
724
763
|
onDragUpdate() {
|
|
725
|
-
|
|
764
|
+
renderUi();
|
|
726
765
|
},
|
|
727
766
|
onDragEnd(event) {
|
|
728
|
-
|
|
729
|
-
|
|
767
|
+
endEditorSceneContinuousRender(state, 'gizmo-drag');
|
|
768
|
+
const changed = commitGizmoTransform(state, options, event);
|
|
769
|
+
if (!changed)
|
|
770
|
+
requestEditorSceneFrame(state, 'gizmo-drag-end');
|
|
771
|
+
renderUi();
|
|
730
772
|
},
|
|
731
773
|
onDragCancel(event) {
|
|
774
|
+
endEditorSceneContinuousRender(state, 'gizmo-drag');
|
|
732
775
|
if (event.duplicate && cancelDuplicateDrag(state, options)) {
|
|
733
|
-
|
|
776
|
+
renderUi();
|
|
734
777
|
return;
|
|
735
778
|
}
|
|
779
|
+
requestEditorSceneFrame(state, 'gizmo-drag-cancel');
|
|
736
780
|
state.status = event.targetIds.length > 1
|
|
737
781
|
? `Canceled ${event.tool} ${event.targetIds.length} objects`
|
|
738
782
|
: `Canceled ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
|
|
739
|
-
|
|
783
|
+
renderUi();
|
|
740
784
|
},
|
|
741
785
|
onDuplicateDragStart(input) {
|
|
742
786
|
return beginDuplicateDrag(state, options, input);
|
|
@@ -755,15 +799,15 @@ async function createEditorWorld(state, options, render) {
|
|
|
755
799
|
isOperationBlocked: () => state.gizmo?.getState().dragPhase === 'dragging',
|
|
756
800
|
onSelectionCommand(command) {
|
|
757
801
|
if (dispatchSelectionCommand(state, options, command))
|
|
758
|
-
|
|
802
|
+
renderUi();
|
|
759
803
|
},
|
|
760
804
|
onFocusIntent(nodeId) {
|
|
761
805
|
if (focusProjectionNode(state, nodeId))
|
|
762
|
-
|
|
806
|
+
renderUi();
|
|
763
807
|
},
|
|
764
808
|
onBoxSelectionChange(box) {
|
|
765
809
|
state.boxSelection = box;
|
|
766
|
-
|
|
810
|
+
renderUi();
|
|
767
811
|
},
|
|
768
812
|
});
|
|
769
813
|
const sceneViewMeasurement = createBabylonSceneViewMeasurementController({
|
|
@@ -779,9 +823,10 @@ async function createEditorWorld(state, options, render) {
|
|
|
779
823
|
isPlacementCandidate: () => isPlacementArmed(state),
|
|
780
824
|
isMeasurementCandidate: () => state.viewportTools.activeUtilityTool === 'measure-distance',
|
|
781
825
|
onPointerIntentStart(event) {
|
|
826
|
+
beginContinuousRenderForPointerIntent(state, event.state.intent);
|
|
782
827
|
if (event.state.intent === 'measurement') {
|
|
783
828
|
if (handleViewportMeasurementStart(state, event.originalEvent))
|
|
784
|
-
|
|
829
|
+
renderUi();
|
|
785
830
|
return;
|
|
786
831
|
}
|
|
787
832
|
if (event.state.intent === 'gizmo-drag') {
|
|
@@ -790,88 +835,109 @@ async function createEditorWorld(state, options, render) {
|
|
|
790
835
|
}
|
|
791
836
|
if (event.state.intent === 'placement') {
|
|
792
837
|
if (previewArmedPlacement(state, event.originalEvent))
|
|
793
|
-
|
|
838
|
+
renderUi();
|
|
794
839
|
return;
|
|
795
840
|
}
|
|
796
841
|
if (event.state.intent === 'view-plane-move') {
|
|
797
842
|
if (gizmo.beginViewPlaneMove(event.originalEvent))
|
|
798
|
-
|
|
843
|
+
renderUi();
|
|
799
844
|
return;
|
|
800
845
|
}
|
|
801
846
|
if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
|
|
802
847
|
selectionController.beginPointerSelection(event.originalEvent);
|
|
848
|
+
requestEditorSceneFrame(state, `pointer-${event.state.intent}-start`);
|
|
803
849
|
}
|
|
804
850
|
},
|
|
805
851
|
onPointerIntentMove(event) {
|
|
806
852
|
if (event.state.intent === 'measurement') {
|
|
807
853
|
if (handleViewportMeasurementMove(state, event.originalEvent))
|
|
808
|
-
|
|
854
|
+
renderUi();
|
|
809
855
|
return;
|
|
810
856
|
}
|
|
811
857
|
if (event.state.intent === 'placement') {
|
|
812
858
|
if (previewArmedPlacement(state, event.originalEvent))
|
|
813
|
-
|
|
859
|
+
renderUi();
|
|
814
860
|
return;
|
|
815
861
|
}
|
|
816
862
|
if (event.state.intent === 'view-plane-move') {
|
|
817
863
|
if (gizmo.updateViewPlaneMove(event.originalEvent))
|
|
818
|
-
|
|
864
|
+
renderUi();
|
|
819
865
|
return;
|
|
820
866
|
}
|
|
821
|
-
if (!state.
|
|
822
|
-
|
|
867
|
+
if (!state.mainCameraPreviewEnabled && state.sceneViewCamera?.handlePointerIntentMove(event)) {
|
|
868
|
+
renderUi();
|
|
823
869
|
return;
|
|
824
870
|
}
|
|
825
871
|
if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
|
|
826
872
|
selectionController.updatePointerSelection(event.originalEvent, event.state.intent);
|
|
827
873
|
}
|
|
874
|
+
requestEditorSceneFrame(state, `pointer-${event.state.intent}`);
|
|
828
875
|
},
|
|
829
876
|
onPointerIntentEnd(event) {
|
|
877
|
+
endContinuousRenderForPointerIntent(state, event.state.intent);
|
|
830
878
|
if (event.state.intent === 'measurement') {
|
|
831
879
|
if (handleViewportMeasurementEnd(state))
|
|
832
|
-
|
|
880
|
+
renderUi();
|
|
833
881
|
return;
|
|
834
882
|
}
|
|
835
883
|
if (event.state.intent === 'placement') {
|
|
836
884
|
if (commitArmedPlacement(state, options, event.originalEvent))
|
|
837
|
-
|
|
885
|
+
renderUi();
|
|
838
886
|
return;
|
|
839
887
|
}
|
|
840
888
|
if (event.state.intent === 'view-plane-move') {
|
|
841
|
-
if (gizmo.endViewPlaneMove(event.originalEvent))
|
|
842
|
-
|
|
889
|
+
if (gizmo.endViewPlaneMove(event.originalEvent)) {
|
|
890
|
+
requestEditorSceneFrame(state, 'view-plane-move-end');
|
|
891
|
+
renderUi();
|
|
892
|
+
}
|
|
843
893
|
return;
|
|
844
894
|
}
|
|
845
895
|
if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
|
|
846
896
|
selectionController.endPointerSelection(event.originalEvent, event.state.intent);
|
|
847
897
|
}
|
|
898
|
+
requestEditorSceneFrame(state, `pointer-${event.state.intent}-end`);
|
|
848
899
|
},
|
|
849
900
|
onPointerIntentCancel(event) {
|
|
901
|
+
endContinuousRenderForPointerIntent(state, event.state.intent);
|
|
850
902
|
if (event.state.intent === 'measurement') {
|
|
851
|
-
|
|
903
|
+
renderUi();
|
|
852
904
|
return;
|
|
853
905
|
}
|
|
854
906
|
if (event.state.intent === 'placement') {
|
|
855
907
|
state.gizmo?.setPlacementMarker(null);
|
|
856
|
-
|
|
908
|
+
requestEditorSceneFrame(state, 'placement-marker-clear');
|
|
909
|
+
renderUi();
|
|
857
910
|
return;
|
|
858
911
|
}
|
|
859
912
|
if (event.state.intent === 'view-plane-move') {
|
|
860
913
|
gizmo.cancelDrag();
|
|
861
|
-
|
|
914
|
+
requestEditorSceneFrame(state, 'view-plane-move-cancel');
|
|
915
|
+
renderUi();
|
|
862
916
|
return;
|
|
863
917
|
}
|
|
864
918
|
if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
|
|
865
919
|
selectionController.cancelBoxSelection();
|
|
866
|
-
|
|
920
|
+
renderUi();
|
|
867
921
|
}
|
|
922
|
+
requestEditorSceneFrame(state, `pointer-${event.state.intent}-cancel`);
|
|
868
923
|
},
|
|
869
924
|
onDoubleClick(event) {
|
|
870
925
|
selectionController.handleDoubleClick(event);
|
|
871
926
|
},
|
|
872
927
|
onWheel(event) {
|
|
873
|
-
if (!state.
|
|
874
|
-
|
|
928
|
+
if (!state.mainCameraPreviewEnabled && state.sceneViewCamera?.handleWheel(event)) {
|
|
929
|
+
requestEditorSceneFrame(state, 'camera-wheel');
|
|
930
|
+
renderUi();
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
onMovementKeysChange(event) {
|
|
934
|
+
if (event.pressedMovementKeys.length > 0) {
|
|
935
|
+
beginEditorSceneContinuousRender(state, 'camera-flythrough-keys');
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
endEditorSceneContinuousRender(state, 'camera-flythrough-keys');
|
|
939
|
+
}
|
|
940
|
+
renderUi();
|
|
875
941
|
},
|
|
876
942
|
});
|
|
877
943
|
const sceneViewCamera = createBabylonSceneViewCameraController({
|
|
@@ -880,7 +946,7 @@ async function createEditorWorld(state, options, render) {
|
|
|
880
946
|
camera: world.camera,
|
|
881
947
|
input: sceneViewInput,
|
|
882
948
|
});
|
|
883
|
-
const
|
|
949
|
+
const mainCameraPreview = createBabylonMainCameraPreviewController({
|
|
884
950
|
babylon,
|
|
885
951
|
scene: world.scene,
|
|
886
952
|
editorCamera: world.camera,
|
|
@@ -896,31 +962,42 @@ async function createEditorWorld(state, options, render) {
|
|
|
896
962
|
projection.syncSelection(selection);
|
|
897
963
|
gizmo.setSelection(selection);
|
|
898
964
|
}
|
|
899
|
-
const resize = () =>
|
|
965
|
+
const resize = () => {
|
|
966
|
+
engine.resize?.();
|
|
967
|
+
viewportRenderCoordinator.requestFrame('resize');
|
|
968
|
+
};
|
|
900
969
|
window.addEventListener('resize', resize);
|
|
901
|
-
engine.runRenderLoop?.(() => world.render());
|
|
902
970
|
state.babylon = babylon;
|
|
903
971
|
state.engine = engine;
|
|
904
972
|
state.world = world;
|
|
905
973
|
state.grid = grid;
|
|
906
974
|
state.projection = projection;
|
|
907
975
|
state.gizmo = gizmo;
|
|
908
|
-
state.
|
|
976
|
+
state.mainCameraPreview = mainCameraPreview;
|
|
909
977
|
state.sceneViewInput = sceneViewInput;
|
|
910
978
|
state.sceneViewCamera = sceneViewCamera;
|
|
911
979
|
state.sceneViewMeasurement = sceneViewMeasurement;
|
|
912
980
|
state.sceneViewSpatialOverlay = sceneViewSpatialOverlay;
|
|
913
981
|
state.selectionController = selectionController;
|
|
982
|
+
state.sceneRenderScheduler = sceneRenderScheduler;
|
|
983
|
+
state.viewportRenderCoordinator = viewportRenderCoordinator;
|
|
984
|
+
state.sceneFrameStats = sceneRenderScheduler.getStats();
|
|
914
985
|
state.resizeHandler = resize;
|
|
986
|
+
await sceneRenderScheduler.waitForNextFrame('editor-world-created');
|
|
915
987
|
}
|
|
916
988
|
function disposeEditorWorld(state) {
|
|
989
|
+
state.viewportRenderCoordinator?.dispose();
|
|
990
|
+
state.viewportRenderCoordinator = null;
|
|
991
|
+
state.sceneRenderScheduler?.dispose();
|
|
992
|
+
state.sceneRenderScheduler = null;
|
|
993
|
+
state.sceneFrameStats = null;
|
|
917
994
|
if (state.resizeHandler) {
|
|
918
995
|
window.removeEventListener('resize', state.resizeHandler);
|
|
919
996
|
state.resizeHandler = null;
|
|
920
997
|
}
|
|
921
|
-
state.
|
|
922
|
-
state.
|
|
923
|
-
state.
|
|
998
|
+
state.mainCameraPreview?.dispose();
|
|
999
|
+
state.mainCameraPreview = null;
|
|
1000
|
+
state.mainCameraPreviewEnabled = false;
|
|
924
1001
|
state.sceneViewCamera?.dispose();
|
|
925
1002
|
state.sceneViewCamera = null;
|
|
926
1003
|
state.sceneViewMeasurement?.dispose();
|
|
@@ -944,18 +1021,55 @@ function disposeEditorWorld(state) {
|
|
|
944
1021
|
state.projection = null;
|
|
945
1022
|
state.grid?.dispose();
|
|
946
1023
|
state.grid = null;
|
|
947
|
-
state.engine?.stopRenderLoop?.();
|
|
948
1024
|
state.world?.dispose();
|
|
949
1025
|
state.engine?.dispose?.();
|
|
950
1026
|
state.babylon = null;
|
|
951
1027
|
state.world = null;
|
|
952
1028
|
state.engine = null;
|
|
953
1029
|
}
|
|
954
|
-
|
|
1030
|
+
function requestEditorSceneFrame(state, reason) {
|
|
1031
|
+
state.viewportRenderCoordinator?.requestFrame(reason);
|
|
1032
|
+
}
|
|
1033
|
+
function beginEditorSceneContinuousRender(state, reason) {
|
|
1034
|
+
state.viewportRenderCoordinator?.beginContinuous(reason);
|
|
1035
|
+
}
|
|
1036
|
+
function endEditorSceneContinuousRender(state, reason) {
|
|
1037
|
+
state.viewportRenderCoordinator?.endContinuous(reason);
|
|
1038
|
+
}
|
|
1039
|
+
function beginContinuousRenderForPointerIntent(state, intent) {
|
|
1040
|
+
const reason = continuousRenderReasonForPointerIntent(intent);
|
|
1041
|
+
if (reason)
|
|
1042
|
+
beginEditorSceneContinuousRender(state, reason);
|
|
1043
|
+
}
|
|
1044
|
+
function endContinuousRenderForPointerIntent(state, intent) {
|
|
1045
|
+
const reason = continuousRenderReasonForPointerIntent(intent);
|
|
1046
|
+
if (reason)
|
|
1047
|
+
endEditorSceneContinuousRender(state, reason);
|
|
1048
|
+
if (intent === 'gizmo-drag')
|
|
1049
|
+
endEditorSceneContinuousRender(state, 'gizmo-drag');
|
|
1050
|
+
if (intent === 'flythrough')
|
|
1051
|
+
endEditorSceneContinuousRender(state, 'camera-flythrough-keys');
|
|
1052
|
+
}
|
|
1053
|
+
function continuousRenderReasonForPointerIntent(intent) {
|
|
1054
|
+
switch (intent) {
|
|
1055
|
+
case 'gizmo-drag':
|
|
1056
|
+
case 'view-plane-move':
|
|
1057
|
+
case 'placement':
|
|
1058
|
+
case 'measurement':
|
|
1059
|
+
case 'orbit':
|
|
1060
|
+
case 'pan':
|
|
1061
|
+
case 'dolly':
|
|
1062
|
+
case 'flythrough':
|
|
1063
|
+
return `pointer-${intent}`;
|
|
1064
|
+
default:
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function runExclusive(state, renderUi, action) {
|
|
955
1069
|
if (state.busy)
|
|
956
1070
|
return;
|
|
957
1071
|
state.busy = true;
|
|
958
|
-
|
|
1072
|
+
renderUi();
|
|
959
1073
|
try {
|
|
960
1074
|
await action();
|
|
961
1075
|
}
|
|
@@ -968,7 +1082,7 @@ async function runExclusive(state, render, action) {
|
|
|
968
1082
|
}
|
|
969
1083
|
finally {
|
|
970
1084
|
state.busy = false;
|
|
971
|
-
|
|
1085
|
+
renderUi();
|
|
972
1086
|
}
|
|
973
1087
|
}
|
|
974
1088
|
function selectItem(state, options, input) {
|
|
@@ -988,9 +1102,92 @@ function selectItem(state, options, input) {
|
|
|
988
1102
|
return false;
|
|
989
1103
|
return dispatchSelectionCommand(state, options, command);
|
|
990
1104
|
}
|
|
1105
|
+
function createBrowserHierarchyContextActions(state, options) {
|
|
1106
|
+
return (options.hierarchy?.contextActions ?? []).map((registration) => ({
|
|
1107
|
+
id: registration.id,
|
|
1108
|
+
label: registration.label,
|
|
1109
|
+
shortcut: registration.shortcut,
|
|
1110
|
+
danger: registration.danger,
|
|
1111
|
+
placement: registration.placement,
|
|
1112
|
+
separatorBefore: registration.separatorBefore,
|
|
1113
|
+
visible: (browserContext) => {
|
|
1114
|
+
const context = createHarnessHierarchyContextActionContext(state, options, browserContext);
|
|
1115
|
+
if (!context)
|
|
1116
|
+
return false;
|
|
1117
|
+
return registration.visible?.(context) ?? true;
|
|
1118
|
+
},
|
|
1119
|
+
disabled: (browserContext) => {
|
|
1120
|
+
const context = createHarnessHierarchyContextActionContext(state, options, browserContext);
|
|
1121
|
+
if (!context)
|
|
1122
|
+
return 'No editable document is loaded.';
|
|
1123
|
+
return registration.disabled?.(context) ?? false;
|
|
1124
|
+
},
|
|
1125
|
+
}));
|
|
1126
|
+
}
|
|
1127
|
+
function createHarnessHierarchyContextActionContext(state, options, browserContext, payload) {
|
|
1128
|
+
const document = state.session?.getState().workingDocument ?? null;
|
|
1129
|
+
if (!document)
|
|
1130
|
+
return null;
|
|
1131
|
+
const activeId = browserContext.activeId;
|
|
1132
|
+
return {
|
|
1133
|
+
document,
|
|
1134
|
+
contextNodeId: browserContext.contextNodeId,
|
|
1135
|
+
targetIds: browserContext.targetIds,
|
|
1136
|
+
activeId,
|
|
1137
|
+
hierarchyItem: browserContext.node,
|
|
1138
|
+
projectionNode: browserContext.contextNodeId
|
|
1139
|
+
? options.documentAdapter.getProjectionNode(document, browserContext.contextNodeId)
|
|
1140
|
+
: null,
|
|
1141
|
+
hostServices: options.hostServices ?? null,
|
|
1142
|
+
payload,
|
|
1143
|
+
browserContext,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function createBrowserContextForCustomAction(state, options, action) {
|
|
1147
|
+
const document = state.session?.getState().workingDocument ?? null;
|
|
1148
|
+
if (!document)
|
|
1149
|
+
return null;
|
|
1150
|
+
const uiState = createUiState(state, options);
|
|
1151
|
+
const node = action.contextNodeId
|
|
1152
|
+
? uiState.hierarchy.find(item => item.id === action.contextNodeId) ?? null
|
|
1153
|
+
: null;
|
|
1154
|
+
return {
|
|
1155
|
+
state: uiState,
|
|
1156
|
+
menuKind: node ? 'node' : 'blank',
|
|
1157
|
+
node,
|
|
1158
|
+
contextNodeId: action.contextNodeId,
|
|
1159
|
+
targetIds: action.targetIds,
|
|
1160
|
+
activeId: action.activeId,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function runHierarchyCustomContextAction(state, options, action) {
|
|
1164
|
+
const registration = options.hierarchy?.contextActions?.find(candidate => candidate.id === action.id);
|
|
1165
|
+
if (!registration)
|
|
1166
|
+
return false;
|
|
1167
|
+
const browserContext = createBrowserContextForCustomAction(state, options, action);
|
|
1168
|
+
const context = browserContext
|
|
1169
|
+
? createHarnessHierarchyContextActionContext(state, options, browserContext, action.payload)
|
|
1170
|
+
: null;
|
|
1171
|
+
if (!context)
|
|
1172
|
+
return false;
|
|
1173
|
+
try {
|
|
1174
|
+
return registration.run(context) === true;
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1178
|
+
state.status = `${registration.label} failed`;
|
|
1179
|
+
state.statusTone = 'error';
|
|
1180
|
+
state.statusToneStatus = state.status;
|
|
1181
|
+
state.statusDetails = message;
|
|
1182
|
+
console.error('[LocalEditorHarness] hierarchy context action failed', error);
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
991
1186
|
function handleContextAction(state, options, action) {
|
|
992
1187
|
if (action.region !== 'hierarchy')
|
|
993
1188
|
return false;
|
|
1189
|
+
if (action.action === 'custom')
|
|
1190
|
+
return runHierarchyCustomContextAction(state, options, action);
|
|
994
1191
|
if (action.action === 'focus') {
|
|
995
1192
|
const activeId = action.activeId ?? action.targetIds[action.targetIds.length - 1] ?? null;
|
|
996
1193
|
const selectionChanged = activeId && !state.session?.getState().selection.selectedIds.includes(activeId)
|
|
@@ -1399,6 +1596,13 @@ function syncSelectionToProjection(state, selection) {
|
|
|
1399
1596
|
state.projection?.syncSelection(selection);
|
|
1400
1597
|
state.gizmo?.setSelection(selection);
|
|
1401
1598
|
state.gizmo?.refreshSelection();
|
|
1599
|
+
requestEditorSceneFrame(state, 'projection-selection');
|
|
1600
|
+
}
|
|
1601
|
+
function invalidateEditorScene(state, reason, options) {
|
|
1602
|
+
if (options && state.mainCameraPreviewEnabled) {
|
|
1603
|
+
syncMainCameraPreview(state, options);
|
|
1604
|
+
}
|
|
1605
|
+
state.viewportRenderCoordinator?.invalidateScene(reason);
|
|
1402
1606
|
}
|
|
1403
1607
|
function executeTransformAction(state, options, action) {
|
|
1404
1608
|
if (state.mode !== 'editor' || !state.session)
|
|
@@ -1501,11 +1705,12 @@ function armAssetPlacement(state, options, assetId) {
|
|
|
1501
1705
|
if (state.mode !== 'editor')
|
|
1502
1706
|
return false;
|
|
1503
1707
|
cancelActiveOperation(state);
|
|
1504
|
-
const asset = state
|
|
1708
|
+
const asset = findAssetByResolvedId(state, options, assetId);
|
|
1505
1709
|
if (!asset)
|
|
1506
1710
|
return false;
|
|
1507
1711
|
state.armedPlacement = { assetId, asset };
|
|
1508
1712
|
state.gizmo?.setPlacementMarker(null);
|
|
1713
|
+
requestEditorSceneFrame(state, 'placement-arm-change');
|
|
1509
1714
|
state.status = `Placement armed: ${formatAssetLabel(asset, assetId)} (${state.transformOperationSettings.placementMode})`;
|
|
1510
1715
|
return true;
|
|
1511
1716
|
}
|
|
@@ -1535,6 +1740,7 @@ function commitArmedPlacement(state, options, event) {
|
|
|
1535
1740
|
if (!armed || !hit) {
|
|
1536
1741
|
state.gizmo?.setPlacementMarker(null);
|
|
1537
1742
|
state.status = 'Placement rejected: no hit';
|
|
1743
|
+
requestEditorSceneFrame(state, 'placement-marker-clear');
|
|
1538
1744
|
return true;
|
|
1539
1745
|
}
|
|
1540
1746
|
const patch = options.documentAdapter.createPlacedAssetPatch?.({
|
|
@@ -1584,8 +1790,11 @@ function pickArmedPlacementHit(state, event) {
|
|
|
1584
1790
|
: state.gizmo?.pickPlacementHit(event.clientX, event.clientY, mode) ?? null;
|
|
1585
1791
|
}
|
|
1586
1792
|
function clearArmedPlacement(state) {
|
|
1793
|
+
const hadArmedPlacement = !!state.armedPlacement;
|
|
1587
1794
|
state.armedPlacement = null;
|
|
1588
1795
|
state.gizmo?.setPlacementMarker(null);
|
|
1796
|
+
if (hadArmedPlacement)
|
|
1797
|
+
requestEditorSceneFrame(state, 'placement-clear');
|
|
1589
1798
|
}
|
|
1590
1799
|
function addAssetToDocument(state, options, assetId, createOptions = {}) {
|
|
1591
1800
|
if (state.mode !== 'editor') {
|
|
@@ -1599,7 +1808,7 @@ function addAssetToDocument(state, options, assetId, createOptions = {}) {
|
|
|
1599
1808
|
state.status = 'Asset creation rejected: document is not loaded';
|
|
1600
1809
|
return { ok: false, assetId, changed: false, status: state.status, error: 'document_not_loaded' };
|
|
1601
1810
|
}
|
|
1602
|
-
const asset = state
|
|
1811
|
+
const asset = findAssetByResolvedId(state, options, assetId);
|
|
1603
1812
|
if (!asset) {
|
|
1604
1813
|
state.status = `Asset creation rejected: ${assetId} not found`;
|
|
1605
1814
|
return { ok: false, assetId, changed: false, status: state.status, error: 'asset_not_found' };
|
|
@@ -1636,6 +1845,7 @@ function addAssetToDocument(state, options, assetId, createOptions = {}) {
|
|
|
1636
1845
|
state.projection?.projectNode(projectedNode);
|
|
1637
1846
|
if (selectionResult)
|
|
1638
1847
|
syncSelectionToProjection(state, selectionResult.selection);
|
|
1848
|
+
invalidateEditorScene(state, 'projection-project-node', options);
|
|
1639
1849
|
}
|
|
1640
1850
|
}
|
|
1641
1851
|
return {
|
|
@@ -1976,6 +2186,8 @@ function focusProjectionNode(state, nodeId) {
|
|
|
1976
2186
|
state.status = focused
|
|
1977
2187
|
? `Focused ${nodeId} · ${formatEditorStatusTime(Date.now())}`
|
|
1978
2188
|
: `Focus failed: camera could not frame ${nodeId}`;
|
|
2189
|
+
if (focused)
|
|
2190
|
+
requestEditorSceneFrame(state, 'viewport-focus-selection');
|
|
1979
2191
|
return true;
|
|
1980
2192
|
}
|
|
1981
2193
|
function formatEditorStatusTime(timestamp) {
|
|
@@ -2021,6 +2233,7 @@ function rebuildProjectionFromDocument(state, options, document, selection) {
|
|
|
2021
2233
|
state.projection?.rebuild(options.documentAdapter.getProjectionNodes(document));
|
|
2022
2234
|
const sanitized = sanitizeSelection(state, options, document, selection);
|
|
2023
2235
|
syncSelectionToProjection(state, sanitized ?? selection);
|
|
2236
|
+
invalidateEditorScene(state, 'projection-rebuild', options);
|
|
2024
2237
|
}
|
|
2025
2238
|
function syncProjectionForDispatchResult(state, options, result, changedId) {
|
|
2026
2239
|
if (result.documentChanged && changedId && result.workingDocument) {
|
|
@@ -2036,6 +2249,9 @@ function syncProjectionForDispatchResult(state, options, result, changedId) {
|
|
|
2036
2249
|
const sanitized = result.workingDocument ? sanitizeSelection(state, options, result.workingDocument, selection) : null;
|
|
2037
2250
|
syncSelectionToProjection(state, sanitized ?? selection);
|
|
2038
2251
|
}
|
|
2252
|
+
if (result.documentChanged || result.selectionChanged) {
|
|
2253
|
+
invalidateEditorScene(state, 'projection-dispatch-result', options);
|
|
2254
|
+
}
|
|
2039
2255
|
}
|
|
2040
2256
|
function syncProjectionForChangedIds(state, options, document, changedIds) {
|
|
2041
2257
|
for (const changedId of changedIds) {
|
|
@@ -2045,6 +2261,7 @@ function syncProjectionForChangedIds(state, options, document, changedIds) {
|
|
|
2045
2261
|
}
|
|
2046
2262
|
const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
|
|
2047
2263
|
syncSelectionToProjection(state, selection);
|
|
2264
|
+
invalidateEditorScene(state, 'projection-sync-changed-ids', options);
|
|
2048
2265
|
}
|
|
2049
2266
|
function reprojectProjectionForChangedIds(state, options, document, changedIds) {
|
|
2050
2267
|
for (const changedId of changedIds) {
|
|
@@ -2055,24 +2272,28 @@ function reprojectProjectionForChangedIds(state, options, document, changedIds)
|
|
|
2055
2272
|
const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
|
|
2056
2273
|
syncSelectionToProjection(state, selection);
|
|
2057
2274
|
state.gizmo?.refreshSelection();
|
|
2275
|
+
invalidateEditorScene(state, 'projection-reproject-changed-ids', options);
|
|
2058
2276
|
}
|
|
2059
|
-
function
|
|
2277
|
+
function setMainCameraPreviewEnabled(state, options, enabled) {
|
|
2060
2278
|
if (!enabled) {
|
|
2061
|
-
state.
|
|
2062
|
-
state.
|
|
2063
|
-
state.status = '
|
|
2279
|
+
state.mainCameraPreviewEnabled = false;
|
|
2280
|
+
state.mainCameraPreview?.setActive(false);
|
|
2281
|
+
state.status = 'Main Camera preview disabled';
|
|
2282
|
+
requestEditorSceneFrame(state, 'main-camera-preview-toggle');
|
|
2064
2283
|
return true;
|
|
2065
2284
|
}
|
|
2066
|
-
state.
|
|
2067
|
-
if (!
|
|
2068
|
-
state.
|
|
2069
|
-
state.status = '
|
|
2285
|
+
state.mainCameraPreviewEnabled = true;
|
|
2286
|
+
if (!syncMainCameraPreview(state, options)) {
|
|
2287
|
+
state.mainCameraPreviewEnabled = false;
|
|
2288
|
+
state.status = 'Main Camera preview unavailable';
|
|
2070
2289
|
state.statusTone = 'warning';
|
|
2071
2290
|
state.statusToneStatus = state.status;
|
|
2072
2291
|
state.statusDetails = 'The current document did not provide a Main Camera preview rig.';
|
|
2292
|
+
requestEditorSceneFrame(state, 'main-camera-preview-toggle');
|
|
2073
2293
|
return true;
|
|
2074
2294
|
}
|
|
2075
|
-
state.status = '
|
|
2295
|
+
state.status = 'Main Camera preview enabled';
|
|
2296
|
+
requestEditorSceneFrame(state, 'main-camera-preview-toggle');
|
|
2076
2297
|
return true;
|
|
2077
2298
|
}
|
|
2078
2299
|
function setGridVisible(state, visible) {
|
|
@@ -2085,18 +2306,19 @@ function setGridVisible(state, visible) {
|
|
|
2085
2306
|
state.statusTone = 'default';
|
|
2086
2307
|
state.statusToneStatus = state.status;
|
|
2087
2308
|
state.statusDetails = '';
|
|
2309
|
+
requestEditorSceneFrame(state, 'grid-visible-change');
|
|
2088
2310
|
return true;
|
|
2089
2311
|
}
|
|
2090
|
-
function
|
|
2091
|
-
const controller = state.
|
|
2312
|
+
function syncMainCameraPreview(state, options) {
|
|
2313
|
+
const controller = state.mainCameraPreview;
|
|
2092
2314
|
if (!controller)
|
|
2093
2315
|
return false;
|
|
2094
|
-
if (!state.
|
|
2316
|
+
if (!state.mainCameraPreviewEnabled) {
|
|
2095
2317
|
controller.setActive(false);
|
|
2096
2318
|
return false;
|
|
2097
2319
|
}
|
|
2098
2320
|
const document = state.session?.getState().workingDocument ?? null;
|
|
2099
|
-
const rig = document ? options.documentAdapter.
|
|
2321
|
+
const rig = document ? options.documentAdapter.getMainCameraPreviewRig?.(document) ?? null : null;
|
|
2100
2322
|
if (!rig) {
|
|
2101
2323
|
controller.setActive(false);
|
|
2102
2324
|
return false;
|
|
@@ -2115,6 +2337,11 @@ function syncViewportCameraState(state) {
|
|
|
2115
2337
|
};
|
|
2116
2338
|
}
|
|
2117
2339
|
function syncViewportSpatialOverlay(state) {
|
|
2340
|
+
const settings = state.viewportTools.overlay;
|
|
2341
|
+
if (!settings.bounds && !settings.dimensions && !settings.edgeLengths && !settings.anchor) {
|
|
2342
|
+
state.viewportSpatialOverlay = createEmptyEditorViewportSpatialOverlayState();
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2118
2345
|
const overlay = state.sceneViewSpatialOverlay;
|
|
2119
2346
|
const selection = state.session?.getState().selection;
|
|
2120
2347
|
const activeId = selection?.activeId ?? null;
|
|
@@ -2128,7 +2355,7 @@ function syncViewportSpatialOverlay(state) {
|
|
|
2128
2355
|
nodeId: activeId,
|
|
2129
2356
|
bounds,
|
|
2130
2357
|
anchor,
|
|
2131
|
-
settings
|
|
2358
|
+
settings,
|
|
2132
2359
|
});
|
|
2133
2360
|
}
|
|
2134
2361
|
function syncViewportMeasurementState(state) {
|
|
@@ -2138,9 +2365,9 @@ function syncViewportMeasurementState(state) {
|
|
|
2138
2365
|
if (measurement)
|
|
2139
2366
|
state.viewportMeasurement = measurement;
|
|
2140
2367
|
}
|
|
2141
|
-
function
|
|
2368
|
+
function hasMainCameraPreviewRig(state, options) {
|
|
2142
2369
|
const document = state.session?.getState().workingDocument ?? null;
|
|
2143
|
-
return !!document && !!options.documentAdapter.
|
|
2370
|
+
return !!document && !!options.documentAdapter.getMainCameraPreviewRig?.(document);
|
|
2144
2371
|
}
|
|
2145
2372
|
function createUiState(state, options) {
|
|
2146
2373
|
const sessionState = state.session?.getState();
|
|
@@ -2165,6 +2392,8 @@ function createUiState(state, options) {
|
|
|
2165
2392
|
const inspectorMultiObject = document && inspectorMultiObjectBase
|
|
2166
2393
|
? withRuntimeInspectorSections(state, options, document, inspectorMultiObjectBase)
|
|
2167
2394
|
: inspectorMultiObjectBase;
|
|
2395
|
+
const assets = dedupeLocalEditorBrowserAssetItems(state.assets
|
|
2396
|
+
.map(asset => toBrowserAssetItem(options, asset)));
|
|
2168
2397
|
return {
|
|
2169
2398
|
mode: state.mode,
|
|
2170
2399
|
busy: state.busy,
|
|
@@ -2173,10 +2402,8 @@ function createUiState(state, options) {
|
|
|
2173
2402
|
statusDetails: state.statusToneStatus === state.status ? state.statusDetails : '',
|
|
2174
2403
|
summary: state.summary,
|
|
2175
2404
|
assetFilter: state.assetFilter,
|
|
2176
|
-
assets
|
|
2177
|
-
|
|
2178
|
-
.map(asset => toBrowserAssetItem(options, asset)),
|
|
2179
|
-
assetCountLabel: `${state.assets.length} assets`,
|
|
2405
|
+
assets,
|
|
2406
|
+
assetCountLabel: `${assets.length} assets`,
|
|
2180
2407
|
hierarchy: document ? options.documentAdapter.getHierarchyItems(document) : [],
|
|
2181
2408
|
selectedIds,
|
|
2182
2409
|
activeId,
|
|
@@ -2209,9 +2436,12 @@ function createUiState(state, options) {
|
|
|
2209
2436
|
viewportTools: cloneEditorViewportToolState(state.viewportTools),
|
|
2210
2437
|
viewportMeasurement: cloneViewportMeasurement(state.viewportMeasurement),
|
|
2211
2438
|
viewportSpatialOverlay: cloneEditorViewportSpatialOverlayState(state.viewportSpatialOverlay),
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2439
|
+
sceneFrameStats: state.sceneFrameStats
|
|
2440
|
+
? { ...state.sceneFrameStats, activeReasons: [...state.sceneFrameStats.activeReasons] }
|
|
2441
|
+
: null,
|
|
2442
|
+
mainCameraPreview: {
|
|
2443
|
+
enabled: state.mainCameraPreviewEnabled,
|
|
2444
|
+
available: hasMainCameraPreviewRig(state, options),
|
|
2215
2445
|
},
|
|
2216
2446
|
grid: {
|
|
2217
2447
|
visible: state.gridVisible,
|
|
@@ -2242,6 +2472,8 @@ function createSceneViewCoordinateAxesState(state) {
|
|
|
2242
2472
|
if (!right || !up || !forward)
|
|
2243
2473
|
return null;
|
|
2244
2474
|
return {
|
|
2475
|
+
projectionMode: readSceneViewFreeCameraProjectionMode(state, camera),
|
|
2476
|
+
projectionToggleDisabled: state.mainCameraPreviewEnabled,
|
|
2245
2477
|
axes: [
|
|
2246
2478
|
createSceneViewCoordinateAxis('x', 'X', '#ff4b70', { x: 1, y: 0, z: 0 }, right, up, forward),
|
|
2247
2479
|
createSceneViewCoordinateAxis('y', 'Y', '#75ff42', { x: 0, y: 1, z: 0 }, right, up, forward),
|
|
@@ -2249,6 +2481,14 @@ function createSceneViewCoordinateAxesState(state) {
|
|
|
2249
2481
|
],
|
|
2250
2482
|
};
|
|
2251
2483
|
}
|
|
2484
|
+
function readSceneViewFreeCameraProjectionMode(state, camera) {
|
|
2485
|
+
return state.sceneViewCamera?.getState().projectionMode
|
|
2486
|
+
?? readSceneViewCameraProjectionMode(state, state.world?.camera ?? camera);
|
|
2487
|
+
}
|
|
2488
|
+
function readSceneViewCameraProjectionMode(state, camera) {
|
|
2489
|
+
const orthoMode = state.babylon?.Camera?.ORTHOGRAPHIC_CAMERA ?? 1;
|
|
2490
|
+
return camera?.mode === orthoMode ? 'orthographic' : 'perspective';
|
|
2491
|
+
}
|
|
2252
2492
|
function createSceneViewCoordinateAxis(id, label, color, worldAxis, cameraRight, cameraUp, cameraForward) {
|
|
2253
2493
|
const screenX = dotSceneViewAxisVec3(worldAxis, cameraRight);
|
|
2254
2494
|
const screenY = -dotSceneViewAxisVec3(worldAxis, cameraUp);
|
|
@@ -2382,9 +2622,10 @@ function createDefaultRuntimeInspectorSections(context) {
|
|
|
2382
2622
|
const properties = [];
|
|
2383
2623
|
if (projectionNode) {
|
|
2384
2624
|
properties.push(createRuntimeInspectorProperty('runtime.projection.nodeId', 'Projected ID', projectionNode.id, properties.length));
|
|
2385
|
-
const
|
|
2386
|
-
|
|
2387
|
-
|
|
2625
|
+
const projectionAsset = projectionNode.asset;
|
|
2626
|
+
const assetId = projectionAsset?.assetId ?? projectionAsset?.id ?? '';
|
|
2627
|
+
if (assetId)
|
|
2628
|
+
properties.push(createRuntimeInspectorProperty('runtime.projection.assetId', 'Asset ID', assetId, properties.length));
|
|
2388
2629
|
}
|
|
2389
2630
|
const runtimeClass = readRuntimeClassName(root);
|
|
2390
2631
|
if (runtimeClass)
|
|
@@ -2501,15 +2742,114 @@ function isNodeSelectableInDocument(options, document, id) {
|
|
|
2501
2742
|
}
|
|
2502
2743
|
function resolveAssetId(options, asset) {
|
|
2503
2744
|
return options.worldAdapter.resolveAssetId?.(asset)
|
|
2745
|
+
?? asset.assetId
|
|
2504
2746
|
?? asset.id;
|
|
2505
2747
|
}
|
|
2748
|
+
function findAssetByResolvedId(state, options, assetId) {
|
|
2749
|
+
let selected = null;
|
|
2750
|
+
for (const asset of state.assets) {
|
|
2751
|
+
if (resolveAssetId(options, asset) !== assetId)
|
|
2752
|
+
continue;
|
|
2753
|
+
const item = toBrowserAssetItem(options, asset);
|
|
2754
|
+
if (!selected || shouldReplaceBrowserAssetItem(selected.item, item)) {
|
|
2755
|
+
selected = { asset, item };
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return selected?.asset;
|
|
2759
|
+
}
|
|
2506
2760
|
function toBrowserAssetItem(options, asset) {
|
|
2507
2761
|
return options.worldAdapter.toBrowserAssetItem?.(asset)
|
|
2508
2762
|
?? {
|
|
2509
|
-
id: asset.id,
|
|
2510
|
-
label: asset.label,
|
|
2763
|
+
id: asset.assetId ?? asset.id,
|
|
2764
|
+
label: asset.displayName ?? asset.label,
|
|
2765
|
+
guid: asset.guid,
|
|
2766
|
+
assetId: asset.assetId,
|
|
2767
|
+
kind: asset.kind,
|
|
2768
|
+
external: asset.external,
|
|
2769
|
+
origin: asset.origin,
|
|
2770
|
+
dedupeKey: asset.dedupeKey,
|
|
2771
|
+
placeable: asset.placeable,
|
|
2511
2772
|
meta: asset.meta,
|
|
2512
|
-
disabled: asset.placeable === false,
|
|
2773
|
+
disabled: asset.disabled ?? asset.placeable === false,
|
|
2513
2774
|
};
|
|
2514
2775
|
}
|
|
2776
|
+
export function dedupeLocalEditorBrowserAssetItems(items) {
|
|
2777
|
+
const byKey = new Map();
|
|
2778
|
+
for (const item of items) {
|
|
2779
|
+
const key = getBrowserAssetCanonicalKey(item);
|
|
2780
|
+
const existing = byKey.get(key);
|
|
2781
|
+
if (!existing || shouldReplaceBrowserAssetItem(existing, item)) {
|
|
2782
|
+
byKey.set(key, item);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
const projectDuplicateKeys = new Set();
|
|
2786
|
+
for (const item of byKey.values()) {
|
|
2787
|
+
if (!item.guid || item.origin !== 'project')
|
|
2788
|
+
continue;
|
|
2789
|
+
for (const duplicateKey of getBrowserAssetDuplicateSuppressionKeys(item)) {
|
|
2790
|
+
projectDuplicateKeys.add(duplicateKey);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return [...byKey.values()].filter((item) => {
|
|
2794
|
+
if (item.guid)
|
|
2795
|
+
return true;
|
|
2796
|
+
return !getBrowserAssetDuplicateSuppressionKeys(item)
|
|
2797
|
+
.some(duplicateKey => projectDuplicateKeys.has(duplicateKey));
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
function getBrowserAssetCanonicalKey(item) {
|
|
2801
|
+
const guid = normalizeBrowserAssetString(item.guid);
|
|
2802
|
+
if (guid)
|
|
2803
|
+
return `guid:${guid}`;
|
|
2804
|
+
const platformAssetId = normalizeBrowserAssetString(item.external?.platformAssetId);
|
|
2805
|
+
if (platformAssetId)
|
|
2806
|
+
return `external:${platformAssetId}`;
|
|
2807
|
+
const dedupeKey = normalizeBrowserAssetString(item.dedupeKey);
|
|
2808
|
+
if (dedupeKey)
|
|
2809
|
+
return `dedupe:${dedupeKey}`;
|
|
2810
|
+
const assetId = normalizeBrowserAssetString(item.assetId);
|
|
2811
|
+
if (assetId)
|
|
2812
|
+
return `asset:${assetId}`;
|
|
2813
|
+
return `id:${normalizeBrowserAssetString(item.id) ?? item.id}`;
|
|
2814
|
+
}
|
|
2815
|
+
function getBrowserAssetDuplicateSuppressionKeys(item) {
|
|
2816
|
+
const keys = [];
|
|
2817
|
+
pushBrowserAssetDuplicateKey(keys, 'external', item.external?.platformAssetId);
|
|
2818
|
+
pushBrowserAssetDuplicateKey(keys, 'dedupe', item.dedupeKey);
|
|
2819
|
+
pushBrowserAssetDuplicateKey(keys, 'asset', item.assetId);
|
|
2820
|
+
pushBrowserAssetDuplicateKey(keys, 'id', item.id);
|
|
2821
|
+
return keys;
|
|
2822
|
+
}
|
|
2823
|
+
function pushBrowserAssetDuplicateKey(keys, prefix, value) {
|
|
2824
|
+
const normalized = normalizeBrowserAssetString(value);
|
|
2825
|
+
if (normalized)
|
|
2826
|
+
keys.push(`${prefix}:${normalized}`);
|
|
2827
|
+
}
|
|
2828
|
+
function shouldReplaceBrowserAssetItem(current, candidate) {
|
|
2829
|
+
const currentScore = getBrowserAssetPreferenceScore(current);
|
|
2830
|
+
const candidateScore = getBrowserAssetPreferenceScore(candidate);
|
|
2831
|
+
if (candidateScore !== currentScore)
|
|
2832
|
+
return candidateScore > currentScore;
|
|
2833
|
+
return getBrowserAssetDetailScore(candidate) > getBrowserAssetDetailScore(current);
|
|
2834
|
+
}
|
|
2835
|
+
function getBrowserAssetPreferenceScore(item) {
|
|
2836
|
+
return (item.origin === 'project' ? 1000 : 0)
|
|
2837
|
+
+ (item.guid ? 100 : 0)
|
|
2838
|
+
+ (item.assetId ? 40 : 0)
|
|
2839
|
+
+ (item.placeable === false ? 0 : 10)
|
|
2840
|
+
+ (item.disabled ? 0 : 5);
|
|
2841
|
+
}
|
|
2842
|
+
function getBrowserAssetDetailScore(item) {
|
|
2843
|
+
return [
|
|
2844
|
+
item.label,
|
|
2845
|
+
item.meta,
|
|
2846
|
+
item.kind,
|
|
2847
|
+
item.external?.platformAssetId,
|
|
2848
|
+
item.external?.assetPath,
|
|
2849
|
+
item.external?.assetUrl,
|
|
2850
|
+
].filter(value => typeof value === 'string' && value.trim()).length;
|
|
2851
|
+
}
|
|
2852
|
+
function normalizeBrowserAssetString(value) {
|
|
2853
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
2854
|
+
}
|
|
2515
2855
|
//# sourceMappingURL=local-editor-harness.js.map
|