@fps-games/editor 0.1.2-beta.1 → 0.1.3-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.
Files changed (110) hide show
  1. package/dist/local-editor-harness.d.ts +44 -8
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +436 -170
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/dist/local-editor-scene-render-scheduler.d.ts +8 -1
  6. package/dist/local-editor-scene-render-scheduler.d.ts.map +1 -1
  7. package/dist/local-editor-scene-render-scheduler.js +23 -2
  8. package/dist/local-editor-scene-render-scheduler.js.map +1 -1
  9. package/dist/local-editor-scene-view-interaction-runtime.d.ts +22 -0
  10. package/dist/local-editor-scene-view-interaction-runtime.d.ts.map +1 -0
  11. package/dist/local-editor-scene-view-interaction-runtime.js +80 -0
  12. package/dist/local-editor-scene-view-interaction-runtime.js.map +1 -0
  13. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +2 -2
  14. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -1
  15. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +31 -15
  16. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -1
  17. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +10 -0
  18. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  19. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +31 -9
  20. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  21. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +1 -0
  22. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  23. package/node_modules/@fps-games/editor-babylon/dist/index.js +1 -0
  24. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  25. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +24 -0
  26. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -1
  27. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +241 -48
  28. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -1
  29. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts +3 -1
  30. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts.map +1 -1
  31. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js +166 -0
  32. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js.map +1 -1
  33. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +12 -1
  34. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  35. package/node_modules/@fps-games/editor-babylon/dist/projection.js +231 -32
  36. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  37. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts +4 -0
  38. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts.map +1 -1
  39. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js +37 -23
  40. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js.map +1 -1
  41. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts +1 -0
  42. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts.map +1 -1
  43. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js +100 -8
  44. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js.map +1 -1
  45. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts +22 -0
  46. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts.map +1 -0
  47. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js +71 -0
  48. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js.map +1 -0
  49. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts +6 -1
  50. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -1
  51. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +107 -79
  52. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -1
  53. package/node_modules/@fps-games/editor-babylon/dist/transform-solver.d.ts +30 -0
  54. package/node_modules/@fps-games/editor-babylon/dist/transform-solver.d.ts.map +1 -0
  55. package/node_modules/@fps-games/editor-babylon/dist/transform-solver.js +120 -0
  56. package/node_modules/@fps-games/editor-babylon/dist/transform-solver.js.map +1 -0
  57. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +3 -1
  58. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  59. package/node_modules/@fps-games/editor-babylon/package.json +5 -4
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +28 -0
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +103 -1
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts +3 -1
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -1
  68. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +49 -4
  69. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -1
  70. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.d.ts +4 -1
  71. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.d.ts.map +1 -1
  72. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.js +12 -3
  73. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.js.map +1 -1
  74. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts +3 -1
  75. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts.map +1 -1
  76. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js +7 -0
  77. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js.map +1 -1
  78. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -3
  79. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  80. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +645 -20
  81. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  82. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +1 -1
  83. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
  84. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  85. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +7 -6
  86. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  87. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +150 -3
  88. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  89. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +6 -0
  90. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  91. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +1 -0
  92. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  93. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +3 -4
  94. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  95. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +1067 -87
  96. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  97. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  98. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts +7 -0
  99. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts.map +1 -1
  100. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts +22 -0
  101. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts.map +1 -1
  102. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js.map +1 -1
  103. package/node_modules/@fps-games/editor-core/package.json +2 -2
  104. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  105. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +61 -0
  106. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
  107. package/node_modules/@fps-games/editor-protocol/dist/index.js +157 -0
  108. package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
  109. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  110. package/package.json +8 -6
@@ -1,8 +1,9 @@
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, createBabylonMainCameraPreviewController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonSceneViewMeasurementController, createBabylonSceneViewSpatialOverlayController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
3
+ import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonSceneCameraPreviewController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonSceneViewMeasurementController, createBabylonSceneViewSpatialOverlayController, createBabylonEditorShadowPreviewController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
4
4
  import { createLocalEditorSceneRenderScheduler, } from './local-editor-scene-render-scheduler';
5
5
  import { createLocalEditorViewportRenderCoordinator, } from './local-editor-viewport-render-coordinator';
6
+ import { createLocalEditorSceneViewInteractionRuntime, } from './local-editor-scene-view-interaction-runtime';
6
7
  export function createLocalEditorHarness(options) {
7
8
  const root = options.root ?? document.body;
8
9
  const state = {
@@ -12,20 +13,28 @@ export function createLocalEditorHarness(options) {
12
13
  source: null,
13
14
  assets: [],
14
15
  assetFilter: '',
16
+ selectedAssetId: null,
15
17
  babylon: null,
16
18
  engine: null,
17
19
  world: null,
18
20
  grid: null,
19
21
  gridVisible: true,
20
22
  projection: null,
23
+ shadowPreview: null,
21
24
  gizmo: null,
22
- mainCameraPreview: null,
23
- mainCameraPreviewEnabled: false,
25
+ sceneCameraPreview: null,
26
+ sceneCameraPreviewEnabled: false,
24
27
  sceneViewInput: null,
25
28
  sceneViewCamera: null,
29
+ sceneViewInteraction: null,
26
30
  sceneViewMeasurement: null,
27
31
  sceneViewSpatialOverlay: null,
28
32
  selectionController: null,
33
+ sceneRenderScheduler: null,
34
+ viewportRenderCoordinator: null,
35
+ sceneFrameStats: null,
36
+ worldAppearanceKey: '',
37
+ worldRenderingKey: '',
29
38
  boxSelection: null,
30
39
  transformTool: 'select',
31
40
  transformSpace: 'world',
@@ -36,9 +45,6 @@ export function createLocalEditorHarness(options) {
36
45
  viewportSpatialOverlay: createEmptyEditorViewportSpatialOverlayState(),
37
46
  duplicateDrag: null,
38
47
  armedPlacement: null,
39
- sceneRenderScheduler: null,
40
- viewportRenderCoordinator: null,
41
- sceneFrameStats: null,
42
48
  resizeHandler: null,
43
49
  status: 'Game running',
44
50
  statusTone: 'default',
@@ -51,6 +57,9 @@ export function createLocalEditorHarness(options) {
51
57
  root,
52
58
  theme: options.theme,
53
59
  localTestActions: options.localTestActions,
60
+ input: {
61
+ isShortcutReserved: (event) => state.sceneViewInput?.ownsKeyboardEvent(event) ?? false,
62
+ },
54
63
  inspector: options.inspector,
55
64
  hierarchy: {
56
65
  contextActions: createBrowserHierarchyContextActions(state, options),
@@ -80,6 +89,14 @@ export function createLocalEditorHarness(options) {
80
89
  if (createAssetFromBrowserIntent(state, options, assetId))
81
90
  harness.render();
82
91
  },
92
+ onSelectAsset: (assetId) => {
93
+ if (selectBrowserAsset(state, assetId))
94
+ harness.render();
95
+ },
96
+ onAssetAction: (input) => {
97
+ if (handleBrowserAssetAction(state, options, input))
98
+ harness.render();
99
+ },
83
100
  onSelectHierarchyItem: (input) => {
84
101
  if (selectItem(state, options, input))
85
102
  harness.render();
@@ -141,25 +158,43 @@ export function createLocalEditorHarness(options) {
141
158
  if (patched || state.status !== previousStatus)
142
159
  harness.render();
143
160
  },
161
+ onRenderingPropertyChange: options.documentAdapter.onRenderingPropertyChange
162
+ ? (input) => {
163
+ void applyRenderingPropertyChange(state, options, input)
164
+ .then((changed) => {
165
+ if (changed)
166
+ harness.render();
167
+ });
168
+ }
169
+ : undefined,
170
+ onRenderingAction: options.documentAdapter.onRenderingAction
171
+ ? (input) => {
172
+ void applyRenderingAction(state, options, input)
173
+ .then((changed) => {
174
+ if (changed)
175
+ harness.render();
176
+ });
177
+ }
178
+ : undefined,
144
179
  onTransformToolChange: (tool) => {
145
180
  state.transformTool = tool;
146
181
  state.transformConstraint = normalizeTransformConstraint(tool, state.transformConstraint);
147
182
  state.gizmo?.setTool(tool);
148
183
  state.gizmo?.setConstraint(state.transformConstraint);
149
- requestEditorSceneFrame(state, 'transform-tool-change');
150
184
  harness.render();
185
+ requestEditorSceneFrame(state, 'transform-tool-change');
151
186
  },
152
187
  onTransformSpaceChange: (space) => {
153
188
  state.transformSpace = space;
154
189
  state.gizmo?.setSpace(space);
155
- requestEditorSceneFrame(state, 'transform-space-change');
156
190
  harness.render();
191
+ requestEditorSceneFrame(state, 'transform-space-change');
157
192
  },
158
193
  onTransformConstraintChange: (constraint) => {
159
194
  state.transformConstraint = normalizeTransformConstraint(state.transformTool, constraint);
160
195
  state.gizmo?.setConstraint(state.transformConstraint);
161
- requestEditorSceneFrame(state, 'transform-constraint-change');
162
196
  harness.render();
197
+ requestEditorSceneFrame(state, 'transform-constraint-change');
163
198
  },
164
199
  onTransformSnapEnabledChange: (enabled) => {
165
200
  state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
@@ -170,8 +205,8 @@ export function createLocalEditorHarness(options) {
170
205
  });
171
206
  state.gizmo?.setOperationSettings(state.transformOperationSettings);
172
207
  state.status = enabled ? 'Transform snap enabled' : 'Transform snap disabled';
173
- requestEditorSceneFrame(state, 'transform-operation-settings-change');
174
208
  harness.render();
209
+ requestEditorSceneFrame(state, 'transform-operation-settings-change');
175
210
  },
176
211
  onTransformSnapStepChange: (input) => {
177
212
  const value = normalizePositiveStep(input.value);
@@ -187,8 +222,8 @@ export function createLocalEditorHarness(options) {
187
222
  state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, { snap });
188
223
  state.gizmo?.setOperationSettings(state.transformOperationSettings);
189
224
  state.status = `Transform snap ${input.kind} step ${value}`;
190
- requestEditorSceneFrame(state, 'transform-operation-settings-change');
191
225
  harness.render();
226
+ requestEditorSceneFrame(state, 'transform-operation-settings-change');
192
227
  },
193
228
  onPlacementModeChange: (mode) => {
194
229
  state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
@@ -198,8 +233,8 @@ export function createLocalEditorHarness(options) {
198
233
  if (state.transformOperationSettings.placementMode === 'off')
199
234
  clearArmedPlacement(state);
200
235
  state.status = `Placement mode: ${state.transformOperationSettings.placementMode}`;
201
- requestEditorSceneFrame(state, 'placement-mode-change');
202
236
  harness.render();
237
+ requestEditorSceneFrame(state, 'placement-mode-change');
203
238
  },
204
239
  onTransformAction: (action) => {
205
240
  if (executeTransformAction(state, options, action))
@@ -225,8 +260,8 @@ export function createLocalEditorHarness(options) {
225
260
  if (clearViewportMeasurement(state))
226
261
  harness.render();
227
262
  },
228
- onMainCameraPreviewToggle: (enabled) => {
229
- if (setMainCameraPreviewEnabled(state, options, enabled))
263
+ onSceneCameraPreviewToggle: (enabled) => {
264
+ if (setSceneCameraPreviewEnabled(state, options, enabled))
230
265
  harness.render();
231
266
  },
232
267
  onGridVisibleChange: (visible) => {
@@ -243,20 +278,17 @@ export function createLocalEditorHarness(options) {
243
278
  },
244
279
  onCancelActiveOperation: () => {
245
280
  cancelActiveOperation(state);
246
- requestEditorSceneFrame(state, 'cancel-active-operation');
247
281
  harness.render();
248
282
  },
249
283
  },
250
284
  });
251
- const renderUi = () => {
252
- syncViewportCameraState(state);
253
- syncViewportMeasurementState(state);
254
- syncViewportSpatialOverlay(state);
255
- ui.update(createUiState(state, options));
256
- };
257
285
  harness = {
258
286
  render() {
259
- renderUi();
287
+ syncSceneCameraPreview(state, options);
288
+ syncViewportCameraState(state);
289
+ syncViewportMeasurementState(state);
290
+ syncViewportSpatialOverlay(state);
291
+ ui.update(createUiState(state, options));
260
292
  },
261
293
  notifyViewportRevealed(reason = 'viewport-revealed') {
262
294
  state.viewportRenderCoordinator?.requestRevealFrame(reason);
@@ -507,11 +539,11 @@ function cloneViewportMeasurement(measurement) {
507
539
  };
508
540
  }
509
541
  function setViewportViewPreset(state, preset) {
510
- if (state.mainCameraPreviewEnabled) {
511
- state.status = 'Viewport view unavailable during Main Camera preview';
542
+ if (state.sceneCameraPreviewEnabled) {
543
+ state.status = 'Viewport view unavailable during Scene Camera preview';
512
544
  state.statusTone = 'warning';
513
545
  state.statusToneStatus = state.status;
514
- state.statusDetails = 'Disable Main Camera preview before switching editor viewport views.';
546
+ state.statusDetails = 'Disable Scene Camera preview before switching editor viewport views.';
515
547
  return true;
516
548
  }
517
549
  const nextPreset = normalizeViewportViewPreset(preset);
@@ -530,15 +562,15 @@ function setViewportViewPreset(state, preset) {
530
562
  state.statusTone = 'default';
531
563
  state.statusToneStatus = state.status;
532
564
  state.statusDetails = '';
533
- requestEditorSceneFrame(state, 'viewport-view-preset-change');
565
+ requestEditorSceneFrame(state, 'viewport-view-preset');
534
566
  return true;
535
567
  }
536
568
  function setViewportProjectionMode(state, mode) {
537
- if (state.mainCameraPreviewEnabled) {
538
- state.status = 'Viewport projection unavailable during Main Camera preview';
569
+ if (state.sceneCameraPreviewEnabled) {
570
+ state.status = 'Viewport projection unavailable during Scene Camera preview';
539
571
  state.statusTone = 'warning';
540
572
  state.statusToneStatus = state.status;
541
- state.statusDetails = 'Disable Main Camera preview before switching editor viewport projection.';
573
+ state.statusDetails = 'Disable Scene Camera preview before switching editor viewport projection.';
542
574
  return true;
543
575
  }
544
576
  const nextMode = normalizeViewportProjectionMode(mode);
@@ -554,7 +586,7 @@ function setViewportProjectionMode(state, mode) {
554
586
  state.statusTone = 'default';
555
587
  state.statusToneStatus = state.status;
556
588
  state.statusDetails = '';
557
- requestEditorSceneFrame(state, 'viewport-projection-mode-change');
589
+ requestEditorSceneFrame(state, 'viewport-projection-mode');
558
590
  return true;
559
591
  }
560
592
  function setViewportOverlaySettings(state, settings) {
@@ -603,6 +635,7 @@ function setViewportUtilityTool(state, tool) {
603
635
  state.statusTone = 'default';
604
636
  state.statusToneStatus = state.status;
605
637
  state.statusDetails = '';
638
+ requestEditorSceneFrame(state, 'viewport-utility-tool');
606
639
  return true;
607
640
  }
608
641
  function clearViewportMeasurement(state) {
@@ -615,6 +648,7 @@ function clearViewportMeasurement(state) {
615
648
  state.statusTone = 'default';
616
649
  state.statusToneStatus = state.status;
617
650
  state.statusDetails = '';
651
+ requestEditorSceneFrame(state, 'viewport-measurement-clear');
618
652
  return true;
619
653
  }
620
654
  export function applyLocalEditorHarnessViewportMeasurementPointerStart(state, event) {
@@ -708,23 +742,35 @@ function validateTransformActionSelection(state, action) {
708
742
  }
709
743
  async function createEditorWorld(state, options, renderUi, updateSceneFrameStats) {
710
744
  disposeEditorWorld(state);
745
+ const render = (reason = 'editor-world-update') => {
746
+ renderUi();
747
+ requestEditorSceneFrame(state, reason);
748
+ };
711
749
  const canvas = options.worldAdapter.getCanvas();
712
750
  if (!canvas)
713
751
  throw new Error('Editor canvas not found');
714
752
  const babylon = await options.worldAdapter.loadBabylon();
715
753
  const engine = options.worldAdapter.createEngine(babylon, canvas);
754
+ const document = state.session?.getState().workingDocument;
755
+ const initialAppearance = resolveLocalEditorWorldAppearance(options, document);
756
+ const initialAppearanceKey = serializeLocalEditorWorldAppearance(initialAppearance);
757
+ const initialRendering = resolveLocalEditorWorldRendering(options, document);
758
+ const initialRenderingKey = serializeLocalEditorWorldRendering(initialRendering);
716
759
  const world = createBabylonEditorWorld({
717
760
  engine,
718
761
  canvas,
719
762
  babylon,
720
763
  cameraTarget: options.world?.cameraTarget,
721
764
  cameraRadius: options.world?.cameraRadius,
722
- clearColor: options.world?.clearColor,
723
- sky: options.world?.sky,
765
+ clearColor: initialAppearance.clearColor,
766
+ sky: initialAppearance.sky,
724
767
  useRightHandedSystem: options.world?.useRightHandedSystem,
725
768
  enableDefaultCameraControls: false,
726
769
  });
727
- const sceneRenderScheduler = createLocalEditorSceneRenderScheduler(() => world.render(), {
770
+ const sceneRenderScheduler = createLocalEditorSceneRenderScheduler((frame) => {
771
+ state.sceneViewInteraction?.updateFrame(frame);
772
+ world.render();
773
+ }, {
728
774
  onStatsChange(stats) {
729
775
  state.sceneFrameStats = stats;
730
776
  updateSceneFrameStats(stats);
@@ -735,7 +781,17 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
735
781
  getEngine: () => engine,
736
782
  getScene: () => world.scene,
737
783
  });
738
- const grid = options.createGrid?.(babylon, world.scene, world.camera) ?? null;
784
+ const sceneViewInteraction = createLocalEditorSceneViewInteractionRuntime({
785
+ coordinator: viewportRenderCoordinator,
786
+ getCamera: () => state.sceneViewCamera,
787
+ });
788
+ state.sceneViewInteraction = sceneViewInteraction;
789
+ const grid = options.createGrid?.(babylon, world.scene, world.camera, {
790
+ getCamera: () => world.scene?.activeCamera ?? world.camera ?? null,
791
+ getEditorCamera: () => world.camera ?? null,
792
+ }) ?? null;
793
+ if (initialAppearance.grid)
794
+ grid?.setColors(initialAppearance.grid);
739
795
  grid?.setVisible(state.gridVisible);
740
796
  const projection = createBabylonEditorProjection({
741
797
  babylon,
@@ -743,6 +799,7 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
743
799
  importModel: options.worldAdapter.importProjectionModel,
744
800
  logger: console,
745
801
  onProjectionReady(event) {
802
+ syncCurrentSelectionToSceneArtifacts(state);
746
803
  viewportRenderCoordinator.invalidateScene(`projection-${event.nodeId}-ready`);
747
804
  },
748
805
  });
@@ -754,32 +811,41 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
754
811
  initialSpace: state.transformSpace,
755
812
  logger: console,
756
813
  onDragStart(event) {
757
- beginEditorSceneContinuousRender(state, 'gizmo-drag');
814
+ sceneViewInteraction.beginGizmoDrag();
758
815
  state.status = event.targetIds.length > 1
759
816
  ? `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.targetIds.length} objects`
760
817
  : `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
761
- renderUi();
818
+ render();
762
819
  },
763
820
  onDragUpdate() {
764
- renderUi();
821
+ render();
765
822
  },
766
823
  onDragEnd(event) {
767
- endEditorSceneContinuousRender(state, 'gizmo-drag');
824
+ sceneViewInteraction.endGizmoDrag();
768
825
  const changed = commitGizmoTransform(state, options, event);
769
826
  if (!changed)
770
827
  requestEditorSceneFrame(state, 'gizmo-drag-end');
771
828
  renderUi();
772
829
  },
773
830
  onDragCancel(event) {
774
- endEditorSceneContinuousRender(state, 'gizmo-drag');
831
+ sceneViewInteraction.endGizmoDrag();
775
832
  if (event.duplicate && cancelDuplicateDrag(state, options)) {
776
- renderUi();
833
+ render();
777
834
  return;
778
835
  }
779
- requestEditorSceneFrame(state, 'gizmo-drag-cancel');
780
836
  state.status = event.targetIds.length > 1
781
837
  ? `Canceled ${event.tool} ${event.targetIds.length} objects`
782
838
  : `Canceled ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
839
+ render();
840
+ },
841
+ onDragBlocked(event) {
842
+ sceneViewInteraction.endGizmoDrag();
843
+ if (event.duplicate)
844
+ cancelDuplicateDrag(state, options);
845
+ requestEditorSceneFrame(state, 'gizmo-drag-blocked');
846
+ state.status = formatBlockedGizmoTransformStatus(event);
847
+ state.statusTone = 'warning';
848
+ state.statusToneStatus = state.status;
783
849
  renderUi();
784
850
  },
785
851
  onDuplicateDragStart(input) {
@@ -799,15 +865,15 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
799
865
  isOperationBlocked: () => state.gizmo?.getState().dragPhase === 'dragging',
800
866
  onSelectionCommand(command) {
801
867
  if (dispatchSelectionCommand(state, options, command))
802
- renderUi();
868
+ render();
803
869
  },
804
870
  onFocusIntent(nodeId) {
805
871
  if (focusProjectionNode(state, nodeId))
806
- renderUi();
872
+ render();
807
873
  },
808
874
  onBoxSelectionChange(box) {
809
875
  state.boxSelection = box;
810
- renderUi();
876
+ render();
811
877
  },
812
878
  });
813
879
  const sceneViewMeasurement = createBabylonSceneViewMeasurementController({
@@ -823,10 +889,10 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
823
889
  isPlacementCandidate: () => isPlacementArmed(state),
824
890
  isMeasurementCandidate: () => state.viewportTools.activeUtilityTool === 'measure-distance',
825
891
  onPointerIntentStart(event) {
826
- beginContinuousRenderForPointerIntent(state, event.state.intent);
892
+ sceneViewInteraction.beginPointerIntent(event.state.intent);
827
893
  if (event.state.intent === 'measurement') {
828
894
  if (handleViewportMeasurementStart(state, event.originalEvent))
829
- renderUi();
895
+ render();
830
896
  return;
831
897
  }
832
898
  if (event.state.intent === 'gizmo-drag') {
@@ -835,108 +901,96 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
835
901
  }
836
902
  if (event.state.intent === 'placement') {
837
903
  if (previewArmedPlacement(state, event.originalEvent))
838
- renderUi();
904
+ render();
839
905
  return;
840
906
  }
841
907
  if (event.state.intent === 'view-plane-move') {
842
908
  if (gizmo.beginViewPlaneMove(event.originalEvent))
843
- renderUi();
909
+ render();
844
910
  return;
845
911
  }
846
912
  if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
847
913
  selectionController.beginPointerSelection(event.originalEvent);
848
- requestEditorSceneFrame(state, `pointer-${event.state.intent}-start`);
914
+ requestEditorSceneFrame(state, 'viewport-pointer-selection-start');
849
915
  }
850
916
  },
851
917
  onPointerIntentMove(event) {
852
918
  if (event.state.intent === 'measurement') {
853
919
  if (handleViewportMeasurementMove(state, event.originalEvent))
854
- renderUi();
920
+ render();
855
921
  return;
856
922
  }
857
923
  if (event.state.intent === 'placement') {
858
924
  if (previewArmedPlacement(state, event.originalEvent))
859
- renderUi();
925
+ render();
860
926
  return;
861
927
  }
862
928
  if (event.state.intent === 'view-plane-move') {
863
929
  if (gizmo.updateViewPlaneMove(event.originalEvent))
864
- renderUi();
930
+ render();
865
931
  return;
866
932
  }
867
- if (!state.mainCameraPreviewEnabled && state.sceneViewCamera?.handlePointerIntentMove(event)) {
868
- renderUi();
933
+ if (!state.sceneCameraPreviewEnabled && state.sceneViewCamera?.handlePointerIntentMove(event)) {
934
+ render();
869
935
  return;
870
936
  }
871
937
  if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
872
938
  selectionController.updatePointerSelection(event.originalEvent, event.state.intent);
939
+ requestEditorSceneFrame(state, 'viewport-pointer-selection-update');
873
940
  }
874
- requestEditorSceneFrame(state, `pointer-${event.state.intent}`);
875
941
  },
876
942
  onPointerIntentEnd(event) {
877
- endContinuousRenderForPointerIntent(state, event.state.intent);
943
+ sceneViewInteraction.endPointerIntent(event.state.intent);
878
944
  if (event.state.intent === 'measurement') {
879
945
  if (handleViewportMeasurementEnd(state))
880
- renderUi();
946
+ render();
881
947
  return;
882
948
  }
883
949
  if (event.state.intent === 'placement') {
884
950
  if (commitArmedPlacement(state, options, event.originalEvent))
885
- renderUi();
951
+ render();
886
952
  return;
887
953
  }
888
954
  if (event.state.intent === 'view-plane-move') {
889
- if (gizmo.endViewPlaneMove(event.originalEvent)) {
890
- requestEditorSceneFrame(state, 'view-plane-move-end');
891
- renderUi();
892
- }
955
+ if (gizmo.endViewPlaneMove(event.originalEvent))
956
+ render();
893
957
  return;
894
958
  }
895
959
  if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
896
960
  selectionController.endPointerSelection(event.originalEvent, event.state.intent);
961
+ requestEditorSceneFrame(state, 'viewport-pointer-selection-end');
897
962
  }
898
- requestEditorSceneFrame(state, `pointer-${event.state.intent}-end`);
899
963
  },
900
964
  onPointerIntentCancel(event) {
901
- endContinuousRenderForPointerIntent(state, event.state.intent);
965
+ sceneViewInteraction.cancelPointerIntent(event.state.intent);
902
966
  if (event.state.intent === 'measurement') {
903
- renderUi();
967
+ render();
904
968
  return;
905
969
  }
906
970
  if (event.state.intent === 'placement') {
907
971
  state.gizmo?.setPlacementMarker(null);
908
- requestEditorSceneFrame(state, 'placement-marker-clear');
909
- renderUi();
972
+ render();
910
973
  return;
911
974
  }
912
975
  if (event.state.intent === 'view-plane-move') {
913
976
  gizmo.cancelDrag();
914
- requestEditorSceneFrame(state, 'view-plane-move-cancel');
915
- renderUi();
977
+ render();
916
978
  return;
917
979
  }
918
980
  if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
919
981
  selectionController.cancelBoxSelection();
920
- renderUi();
982
+ render();
921
983
  }
922
- requestEditorSceneFrame(state, `pointer-${event.state.intent}-cancel`);
923
984
  },
924
985
  onDoubleClick(event) {
925
986
  selectionController.handleDoubleClick(event);
926
987
  },
927
988
  onWheel(event) {
928
- if (!state.mainCameraPreviewEnabled && state.sceneViewCamera?.handleWheel(event)) {
929
- requestEditorSceneFrame(state, 'camera-wheel');
930
- renderUi();
931
- }
989
+ if (!state.sceneCameraPreviewEnabled && state.sceneViewCamera?.handleWheel(event))
990
+ render();
932
991
  },
933
992
  onMovementKeysChange(event) {
934
- if (event.pressedMovementKeys.length > 0) {
935
- beginEditorSceneContinuousRender(state, 'camera-flythrough-keys');
936
- }
937
- else {
938
- endEditorSceneContinuousRender(state, 'camera-flythrough-keys');
939
- }
993
+ sceneViewInteraction.updateMovementKeys(event.pressedMovementKeys);
940
994
  renderUi();
941
995
  },
942
996
  });
@@ -946,7 +1000,7 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
946
1000
  camera: world.camera,
947
1001
  input: sceneViewInput,
948
1002
  });
949
- const mainCameraPreview = createBabylonMainCameraPreviewController({
1003
+ const sceneCameraPreview = createBabylonSceneCameraPreviewController({
950
1004
  babylon,
951
1005
  scene: world.scene,
952
1006
  editorCamera: world.camera,
@@ -955,13 +1009,17 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
955
1009
  babylon,
956
1010
  scene: world.scene,
957
1011
  });
958
- const document = state.session?.getState().workingDocument;
959
1012
  if (document) {
960
1013
  projection.projectNodes(options.documentAdapter.getProjectionNodes(document));
961
1014
  const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
962
1015
  projection.syncSelection(selection);
963
1016
  gizmo.setSelection(selection);
964
1017
  }
1018
+ const shadowPreview = createBabylonEditorShadowPreviewController({
1019
+ scene: world.scene,
1020
+ projection,
1021
+ options: initialRendering.shadowPreview,
1022
+ });
965
1023
  const resize = () => {
966
1024
  engine.resize?.();
967
1025
  viewportRenderCoordinator.requestFrame('resize');
@@ -972,8 +1030,9 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
972
1030
  state.world = world;
973
1031
  state.grid = grid;
974
1032
  state.projection = projection;
1033
+ state.shadowPreview = shadowPreview;
975
1034
  state.gizmo = gizmo;
976
- state.mainCameraPreview = mainCameraPreview;
1035
+ state.sceneCameraPreview = sceneCameraPreview;
977
1036
  state.sceneViewInput = sceneViewInput;
978
1037
  state.sceneViewCamera = sceneViewCamera;
979
1038
  state.sceneViewMeasurement = sceneViewMeasurement;
@@ -982,22 +1041,21 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
982
1041
  state.sceneRenderScheduler = sceneRenderScheduler;
983
1042
  state.viewportRenderCoordinator = viewportRenderCoordinator;
984
1043
  state.sceneFrameStats = sceneRenderScheduler.getStats();
1044
+ state.worldAppearanceKey = initialAppearanceKey;
1045
+ state.worldRenderingKey = initialRenderingKey;
985
1046
  state.resizeHandler = resize;
986
- await sceneRenderScheduler.waitForNextFrame('editor-world-created');
1047
+ viewportRenderCoordinator.requestFrame('editor-world-created');
987
1048
  }
988
1049
  function disposeEditorWorld(state) {
989
- state.viewportRenderCoordinator?.dispose();
990
- state.viewportRenderCoordinator = null;
991
- state.sceneRenderScheduler?.dispose();
992
- state.sceneRenderScheduler = null;
993
- state.sceneFrameStats = null;
1050
+ state.sceneViewInteraction?.dispose();
1051
+ state.sceneViewInteraction = null;
994
1052
  if (state.resizeHandler) {
995
1053
  window.removeEventListener('resize', state.resizeHandler);
996
1054
  state.resizeHandler = null;
997
1055
  }
998
- state.mainCameraPreview?.dispose();
999
- state.mainCameraPreview = null;
1000
- state.mainCameraPreviewEnabled = false;
1056
+ state.sceneCameraPreview?.dispose();
1057
+ state.sceneCameraPreview = null;
1058
+ state.sceneCameraPreviewEnabled = false;
1001
1059
  state.sceneViewCamera?.dispose();
1002
1060
  state.sceneViewCamera = null;
1003
1061
  state.sceneViewMeasurement?.dispose();
@@ -1008,6 +1066,11 @@ function disposeEditorWorld(state) {
1008
1066
  state.sceneViewInput = null;
1009
1067
  state.selectionController?.dispose();
1010
1068
  state.selectionController = null;
1069
+ state.viewportRenderCoordinator?.dispose();
1070
+ state.viewportRenderCoordinator = null;
1071
+ state.sceneRenderScheduler?.dispose();
1072
+ state.sceneRenderScheduler = null;
1073
+ state.sceneFrameStats = null;
1011
1074
  state.boxSelection = null;
1012
1075
  state.viewportSpatialOverlay = createEmptyEditorViewportSpatialOverlayState();
1013
1076
  state.viewportMeasurement = createEmptyViewportMeasurement();
@@ -1017,10 +1080,13 @@ function disposeEditorWorld(state) {
1017
1080
  };
1018
1081
  state.gizmo?.dispose();
1019
1082
  state.gizmo = null;
1083
+ state.shadowPreview?.dispose();
1084
+ state.shadowPreview = null;
1020
1085
  state.projection?.dispose();
1021
1086
  state.projection = null;
1022
1087
  state.grid?.dispose();
1023
1088
  state.grid = null;
1089
+ state.engine?.stopRenderLoop?.();
1024
1090
  state.world?.dispose();
1025
1091
  state.engine?.dispose?.();
1026
1092
  state.babylon = null;
@@ -1030,46 +1096,11 @@ function disposeEditorWorld(state) {
1030
1096
  function requestEditorSceneFrame(state, reason) {
1031
1097
  state.viewportRenderCoordinator?.requestFrame(reason);
1032
1098
  }
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) {
1099
+ async function runExclusive(state, render, action) {
1069
1100
  if (state.busy)
1070
1101
  return;
1071
1102
  state.busy = true;
1072
- renderUi();
1103
+ render();
1073
1104
  try {
1074
1105
  await action();
1075
1106
  }
@@ -1082,7 +1113,7 @@ async function runExclusive(state, renderUi, action) {
1082
1113
  }
1083
1114
  finally {
1084
1115
  state.busy = false;
1085
- renderUi();
1116
+ render();
1086
1117
  }
1087
1118
  }
1088
1119
  function selectItem(state, options, input) {
@@ -1598,12 +1629,73 @@ function syncSelectionToProjection(state, selection) {
1598
1629
  state.gizmo?.refreshSelection();
1599
1630
  requestEditorSceneFrame(state, 'projection-selection');
1600
1631
  }
1632
+ function syncCurrentSelectionToSceneArtifacts(state) {
1633
+ syncSelectionToProjection(state, state.session?.getState().selection ?? { selectedIds: [], activeId: null });
1634
+ }
1601
1635
  function invalidateEditorScene(state, reason, options) {
1602
- if (options && state.mainCameraPreviewEnabled) {
1603
- syncMainCameraPreview(state, options);
1636
+ if (options && state.sceneCameraPreviewEnabled) {
1637
+ syncSceneCameraPreview(state, options);
1604
1638
  }
1605
1639
  state.viewportRenderCoordinator?.invalidateScene(reason);
1606
1640
  }
1641
+ function syncEditorWorldAppearanceFromDocument(state, options, document, reason) {
1642
+ const appearance = resolveLocalEditorWorldAppearance(options, document);
1643
+ const key = serializeLocalEditorWorldAppearance(appearance);
1644
+ if (key === state.worldAppearanceKey)
1645
+ return;
1646
+ state.worldAppearanceKey = key;
1647
+ state.world?.setAppearance({
1648
+ clearColor: appearance.clearColor,
1649
+ sky: appearance.sky,
1650
+ });
1651
+ state.grid?.setColors(appearance.grid ?? {});
1652
+ requestEditorSceneFrame(state, reason);
1653
+ }
1654
+ function syncEditorWorldRenderingFromDocument(state, options, document, reason, refreshProjection = false) {
1655
+ const rendering = resolveLocalEditorWorldRendering(options, document);
1656
+ const key = serializeLocalEditorWorldRendering(rendering);
1657
+ if (key === state.worldRenderingKey) {
1658
+ if (refreshProjection)
1659
+ state.shadowPreview?.refresh();
1660
+ return;
1661
+ }
1662
+ state.worldRenderingKey = key;
1663
+ state.shadowPreview?.setOptions(rendering.shadowPreview);
1664
+ requestEditorSceneFrame(state, reason);
1665
+ }
1666
+ function resolveLocalEditorWorldAppearance(options, document) {
1667
+ const documentAppearance = document
1668
+ ? options.documentAdapter.getWorldAppearance?.(document) ?? null
1669
+ : null;
1670
+ return {
1671
+ clearColor: documentAppearance?.clearColor ?? options.world?.clearColor,
1672
+ sky: hasEditorWorldAppearanceSky(documentAppearance) ? documentAppearance.sky : options.world?.sky,
1673
+ grid: documentAppearance?.grid ?? options.world?.grid,
1674
+ };
1675
+ }
1676
+ function resolveLocalEditorWorldRendering(options, document) {
1677
+ const documentRendering = document
1678
+ ? options.documentAdapter.getWorldRendering?.(document) ?? null
1679
+ : null;
1680
+ return {
1681
+ shadowPreview: documentRendering?.shadowPreview ?? null,
1682
+ };
1683
+ }
1684
+ function hasEditorWorldAppearanceSky(appearance) {
1685
+ return !!appearance && Object.prototype.hasOwnProperty.call(appearance, 'sky');
1686
+ }
1687
+ function serializeLocalEditorWorldAppearance(appearance) {
1688
+ return JSON.stringify({
1689
+ clearColor: appearance.clearColor ?? null,
1690
+ sky: hasEditorWorldAppearanceSky(appearance) ? appearance.sky : null,
1691
+ grid: appearance.grid ?? null,
1692
+ });
1693
+ }
1694
+ function serializeLocalEditorWorldRendering(rendering) {
1695
+ return JSON.stringify({
1696
+ shadowPreview: rendering.shadowPreview ?? null,
1697
+ });
1698
+ }
1607
1699
  function executeTransformAction(state, options, action) {
1608
1700
  if (state.mode !== 'editor' || !state.session)
1609
1701
  return false;
@@ -1701,6 +1793,66 @@ function createAssetFromBrowserIntent(state, options, assetId) {
1701
1793
  }
1702
1794
  return armAssetPlacement(state, options, assetId);
1703
1795
  }
1796
+ function selectBrowserAsset(state, assetId) {
1797
+ if (state.selectedAssetId === assetId)
1798
+ return false;
1799
+ state.selectedAssetId = assetId;
1800
+ state.status = `Selected asset ${assetId}`;
1801
+ return true;
1802
+ }
1803
+ function handleBrowserAssetAction(state, options, input) {
1804
+ state.selectedAssetId = input.browserAssetId;
1805
+ if (input.actionId === 'asset.add-to-scene') {
1806
+ return addAssetToDocument(state, options, input.assetId).ok;
1807
+ }
1808
+ if (input.actionId === 'asset.place') {
1809
+ if (state.transformOperationSettings.placementMode === 'off') {
1810
+ state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
1811
+ placementMode: 'ground',
1812
+ });
1813
+ state.gizmo?.setOperationSettings(state.transformOperationSettings);
1814
+ }
1815
+ return armAssetPlacement(state, options, input.assetId);
1816
+ }
1817
+ const patched = patchBrowserAssetAction(state, options, input);
1818
+ if (patched)
1819
+ return true;
1820
+ state.status = `Asset action unavailable: ${input.actionId}`;
1821
+ state.statusTone = 'warning';
1822
+ state.statusToneStatus = state.status;
1823
+ return true;
1824
+ }
1825
+ function patchBrowserAssetAction(state, options, input) {
1826
+ const session = state.session;
1827
+ const document = session?.getState().workingDocument ?? null;
1828
+ if (!session || !document || !options.documentAdapter.createAssetActionPatch)
1829
+ return false;
1830
+ const asset = findAssetByResolvedId(state, options, input.assetId);
1831
+ const patch = options.documentAdapter.createAssetActionPatch({
1832
+ ...input,
1833
+ document,
1834
+ asset,
1835
+ activeId: session.getState().selection.activeId,
1836
+ selectedIds: [...session.getState().selection.selectedIds],
1837
+ });
1838
+ if (!patch)
1839
+ return false;
1840
+ const result = session.dispatch({
1841
+ type: 'document.patch',
1842
+ label: patch.label ?? `Asset action ${input.actionId}`,
1843
+ patch: patch.patch,
1844
+ });
1845
+ state.summary = summarizeDocument(options, result.workingDocument, session.getSource());
1846
+ state.status = patch.label ?? `Asset action ${input.actionId}`;
1847
+ state.statusTone = 'success';
1848
+ state.statusToneStatus = state.status;
1849
+ state.statusDetails = '';
1850
+ if (patch.reprojectIds?.length)
1851
+ reprojectProjectionForChangedIds(state, options, result.workingDocument, patch.reprojectIds);
1852
+ else if (patch.changedIds)
1853
+ syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds);
1854
+ return true;
1855
+ }
1704
1856
  function armAssetPlacement(state, options, assetId) {
1705
1857
  if (state.mode !== 'editor')
1706
1858
  return false;
@@ -1710,7 +1862,6 @@ function armAssetPlacement(state, options, assetId) {
1710
1862
  return false;
1711
1863
  state.armedPlacement = { assetId, asset };
1712
1864
  state.gizmo?.setPlacementMarker(null);
1713
- requestEditorSceneFrame(state, 'placement-arm-change');
1714
1865
  state.status = `Placement armed: ${formatAssetLabel(asset, assetId)} (${state.transformOperationSettings.placementMode})`;
1715
1866
  return true;
1716
1867
  }
@@ -1740,7 +1891,6 @@ function commitArmedPlacement(state, options, event) {
1740
1891
  if (!armed || !hit) {
1741
1892
  state.gizmo?.setPlacementMarker(null);
1742
1893
  state.status = 'Placement rejected: no hit';
1743
- requestEditorSceneFrame(state, 'placement-marker-clear');
1744
1894
  return true;
1745
1895
  }
1746
1896
  const patch = options.documentAdapter.createPlacedAssetPatch?.({
@@ -1790,11 +1940,8 @@ function pickArmedPlacementHit(state, event) {
1790
1940
  : state.gizmo?.pickPlacementHit(event.clientX, event.clientY, mode) ?? null;
1791
1941
  }
1792
1942
  function clearArmedPlacement(state) {
1793
- const hadArmedPlacement = !!state.armedPlacement;
1794
1943
  state.armedPlacement = null;
1795
1944
  state.gizmo?.setPlacementMarker(null);
1796
- if (hadArmedPlacement)
1797
- requestEditorSceneFrame(state, 'placement-clear');
1798
1945
  }
1799
1946
  function addAssetToDocument(state, options, assetId, createOptions = {}) {
1800
1947
  if (state.mode !== 'editor') {
@@ -1845,7 +1992,7 @@ function addAssetToDocument(state, options, assetId, createOptions = {}) {
1845
1992
  state.projection?.projectNode(projectedNode);
1846
1993
  if (selectionResult)
1847
1994
  syncSelectionToProjection(state, selectionResult.selection);
1848
- invalidateEditorScene(state, 'projection-project-node', options);
1995
+ invalidateEditorScene(state, 'projection-project-node');
1849
1996
  }
1850
1997
  }
1851
1998
  return {
@@ -1932,6 +2079,93 @@ function patchSerializedProperty(state, options, input) {
1932
2079
  state.status = patch.label ?? `Patched ${payload.path}`;
1933
2080
  return true;
1934
2081
  }
2082
+ async function applyRenderingPropertyChange(state, options, input) {
2083
+ if (state.mode !== 'editor')
2084
+ return false;
2085
+ if (!state.session || !options.documentAdapter.onRenderingPropertyChange)
2086
+ return false;
2087
+ const document = state.session.getState().workingDocument;
2088
+ try {
2089
+ const result = await options.documentAdapter.onRenderingPropertyChange({
2090
+ document,
2091
+ sectionId: input.sectionId,
2092
+ systemId: input.systemId,
2093
+ path: input.path,
2094
+ value: input.value,
2095
+ control: input.control,
2096
+ valueType: input.valueType,
2097
+ commitMode: input.commitMode,
2098
+ source: input.source,
2099
+ });
2100
+ const resultObject = typeof result === 'object' && result != null ? result : null;
2101
+ const changed = result === true || resultObject?.changed === true || resultObject?.refreshWorldRendering === true;
2102
+ if (resultObject?.status) {
2103
+ state.status = resultObject.status;
2104
+ state.statusTone = resultObject.statusTone ?? 'default';
2105
+ state.statusToneStatus = state.status;
2106
+ state.statusDetails = resultObject.statusDetails ?? '';
2107
+ }
2108
+ else if (changed) {
2109
+ state.status = `Updated rendering: ${input.path}`;
2110
+ state.statusTone = 'success';
2111
+ state.statusToneStatus = state.status;
2112
+ state.statusDetails = '';
2113
+ }
2114
+ if (changed) {
2115
+ syncEditorWorldRenderingFromDocument(state, options, document, 'rendering-panel-change', true);
2116
+ requestEditorSceneFrame(state, 'rendering-panel-change');
2117
+ }
2118
+ return changed || !!resultObject?.status;
2119
+ }
2120
+ catch (error) {
2121
+ state.status = error instanceof Error ? error.message : String(error);
2122
+ state.statusTone = 'error';
2123
+ state.statusToneStatus = state.status;
2124
+ state.statusDetails = state.status;
2125
+ console.error('[LocalEditorHarness] rendering property change failed', error);
2126
+ return true;
2127
+ }
2128
+ }
2129
+ async function applyRenderingAction(state, options, input) {
2130
+ if (state.mode !== 'editor')
2131
+ return false;
2132
+ if (!state.session || !options.documentAdapter.onRenderingAction)
2133
+ return false;
2134
+ const document = state.session.getState().workingDocument;
2135
+ try {
2136
+ const result = await options.documentAdapter.onRenderingAction({
2137
+ document,
2138
+ actionId: input.actionId,
2139
+ });
2140
+ const resultObject = typeof result === 'object' && result != null ? result : null;
2141
+ const changed = result === true || resultObject?.changed === true || resultObject?.refreshWorldRendering === true;
2142
+ if (resultObject?.status) {
2143
+ state.status = resultObject.status;
2144
+ state.statusTone = resultObject.statusTone ?? 'default';
2145
+ state.statusToneStatus = state.status;
2146
+ state.statusDetails = resultObject.statusDetails ?? '';
2147
+ }
2148
+ else if (changed) {
2149
+ state.status = `Rendering action: ${input.actionId}`;
2150
+ state.statusTone = 'success';
2151
+ state.statusToneStatus = state.status;
2152
+ state.statusDetails = '';
2153
+ }
2154
+ if (changed) {
2155
+ syncEditorWorldRenderingFromDocument(state, options, document, 'rendering-panel-action', true);
2156
+ requestEditorSceneFrame(state, 'rendering-panel-action');
2157
+ }
2158
+ return changed || !!resultObject?.status;
2159
+ }
2160
+ catch (error) {
2161
+ state.status = error instanceof Error ? error.message : String(error);
2162
+ state.statusTone = 'error';
2163
+ state.statusToneStatus = state.status;
2164
+ state.statusDetails = state.status;
2165
+ console.error('[LocalEditorHarness] rendering action failed', error);
2166
+ return true;
2167
+ }
2168
+ }
1935
2169
  function createInspectorEditTransaction(state, options, document, input, targetIds) {
1936
2170
  const property = findInspectorPropertyForEdit(state, options, document, input, targetIds);
1937
2171
  if (!property) {
@@ -2138,6 +2372,22 @@ function restoreBatchTransformPreview(state, targets) {
2138
2372
  transforms[target.id] = target.before;
2139
2373
  state.projection?.setNodeTransformsPreview(transforms);
2140
2374
  }
2375
+ function formatBlockedGizmoTransformStatus(event) {
2376
+ const target = event.targetIds.length > 1
2377
+ ? `${event.targetIds.length} objects`
2378
+ : event.nodeId ?? event.activeId ?? 'selection';
2379
+ const failed = event.failedTargetId && event.targetIds.length > 1
2380
+ ? ` at ${event.failedTargetId}`
2381
+ : '';
2382
+ return `Blocked ${event.tool} ${target}${failed}: ${formatTransformOperationBlockReason(event.reason)}`;
2383
+ }
2384
+ function formatTransformOperationBlockReason(reason) {
2385
+ if (reason === 'non-trs-representable')
2386
+ return 'result cannot be represented as position/rotation/scale';
2387
+ if (reason === 'non-invertible-parent')
2388
+ return 'parent transform cannot be inverted';
2389
+ return 'unsupported transform';
2390
+ }
2141
2391
  function cancelActiveGizmoDrag(state) {
2142
2392
  state.gizmo?.cancelDrag();
2143
2393
  }
@@ -2233,7 +2483,9 @@ function rebuildProjectionFromDocument(state, options, document, selection) {
2233
2483
  state.projection?.rebuild(options.documentAdapter.getProjectionNodes(document));
2234
2484
  const sanitized = sanitizeSelection(state, options, document, selection);
2235
2485
  syncSelectionToProjection(state, sanitized ?? selection);
2236
- invalidateEditorScene(state, 'projection-rebuild', options);
2486
+ syncEditorWorldAppearanceFromDocument(state, options, document, 'world-appearance-rebuild');
2487
+ syncEditorWorldRenderingFromDocument(state, options, document, 'world-rendering-rebuild', true);
2488
+ invalidateEditorScene(state, 'projection-rebuild');
2237
2489
  }
2238
2490
  function syncProjectionForDispatchResult(state, options, result, changedId) {
2239
2491
  if (result.documentChanged && changedId && result.workingDocument) {
@@ -2249,8 +2501,12 @@ function syncProjectionForDispatchResult(state, options, result, changedId) {
2249
2501
  const sanitized = result.workingDocument ? sanitizeSelection(state, options, result.workingDocument, selection) : null;
2250
2502
  syncSelectionToProjection(state, sanitized ?? selection);
2251
2503
  }
2504
+ if (result.documentChanged && result.workingDocument) {
2505
+ syncEditorWorldAppearanceFromDocument(state, options, result.workingDocument, 'world-appearance-dispatch-result');
2506
+ syncEditorWorldRenderingFromDocument(state, options, result.workingDocument, 'world-rendering-dispatch-result', true);
2507
+ }
2252
2508
  if (result.documentChanged || result.selectionChanged) {
2253
- invalidateEditorScene(state, 'projection-dispatch-result', options);
2509
+ invalidateEditorScene(state, 'projection-dispatch-result');
2254
2510
  }
2255
2511
  }
2256
2512
  function syncProjectionForChangedIds(state, options, document, changedIds) {
@@ -2261,7 +2517,9 @@ function syncProjectionForChangedIds(state, options, document, changedIds) {
2261
2517
  }
2262
2518
  const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
2263
2519
  syncSelectionToProjection(state, selection);
2264
- invalidateEditorScene(state, 'projection-sync-changed-ids', options);
2520
+ syncEditorWorldAppearanceFromDocument(state, options, document, 'world-appearance-sync-changed-ids');
2521
+ syncEditorWorldRenderingFromDocument(state, options, document, 'world-rendering-sync-changed-ids', true);
2522
+ invalidateEditorScene(state, 'projection-sync-changed-ids');
2265
2523
  }
2266
2524
  function reprojectProjectionForChangedIds(state, options, document, changedIds) {
2267
2525
  for (const changedId of changedIds) {
@@ -2269,31 +2527,30 @@ function reprojectProjectionForChangedIds(state, options, document, changedIds)
2269
2527
  if (projectedNode)
2270
2528
  state.projection?.projectNode(projectedNode);
2271
2529
  }
2272
- const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
2273
- syncSelectionToProjection(state, selection);
2274
- state.gizmo?.refreshSelection();
2530
+ syncCurrentSelectionToSceneArtifacts(state);
2531
+ syncEditorWorldAppearanceFromDocument(state, options, document, 'world-appearance-reproject-changed-ids');
2532
+ syncEditorWorldRenderingFromDocument(state, options, document, 'world-rendering-reproject-changed-ids', true);
2275
2533
  invalidateEditorScene(state, 'projection-reproject-changed-ids', options);
2276
2534
  }
2277
- function setMainCameraPreviewEnabled(state, options, enabled) {
2535
+ function setSceneCameraPreviewEnabled(state, options, enabled) {
2278
2536
  if (!enabled) {
2279
- state.mainCameraPreviewEnabled = false;
2280
- state.mainCameraPreview?.setActive(false);
2281
- state.status = 'Main Camera preview disabled';
2282
- requestEditorSceneFrame(state, 'main-camera-preview-toggle');
2537
+ state.sceneCameraPreviewEnabled = false;
2538
+ state.sceneCameraPreview?.setActive(false);
2539
+ state.status = 'Scene Camera preview disabled';
2540
+ requestEditorSceneFrame(state, 'scene-camera-preview-toggle');
2283
2541
  return true;
2284
2542
  }
2285
- state.mainCameraPreviewEnabled = true;
2286
- if (!syncMainCameraPreview(state, options)) {
2287
- state.mainCameraPreviewEnabled = false;
2288
- state.status = 'Main Camera preview unavailable';
2543
+ state.sceneCameraPreviewEnabled = true;
2544
+ if (!syncSceneCameraPreview(state, options)) {
2545
+ state.sceneCameraPreviewEnabled = false;
2546
+ state.status = 'Scene Camera preview unavailable';
2289
2547
  state.statusTone = 'warning';
2290
2548
  state.statusToneStatus = state.status;
2291
2549
  state.statusDetails = 'The current document did not provide a Main Camera preview rig.';
2292
- requestEditorSceneFrame(state, 'main-camera-preview-toggle');
2293
2550
  return true;
2294
2551
  }
2295
- state.status = 'Main Camera preview enabled';
2296
- requestEditorSceneFrame(state, 'main-camera-preview-toggle');
2552
+ state.status = 'Scene Camera preview enabled';
2553
+ requestEditorSceneFrame(state, 'scene-camera-preview-toggle');
2297
2554
  return true;
2298
2555
  }
2299
2556
  function setGridVisible(state, visible) {
@@ -2309,16 +2566,16 @@ function setGridVisible(state, visible) {
2309
2566
  requestEditorSceneFrame(state, 'grid-visible-change');
2310
2567
  return true;
2311
2568
  }
2312
- function syncMainCameraPreview(state, options) {
2313
- const controller = state.mainCameraPreview;
2569
+ function syncSceneCameraPreview(state, options) {
2570
+ const controller = state.sceneCameraPreview;
2314
2571
  if (!controller)
2315
2572
  return false;
2316
- if (!state.mainCameraPreviewEnabled) {
2573
+ if (!state.sceneCameraPreviewEnabled) {
2317
2574
  controller.setActive(false);
2318
2575
  return false;
2319
2576
  }
2320
2577
  const document = state.session?.getState().workingDocument ?? null;
2321
- const rig = document ? options.documentAdapter.getMainCameraPreviewRig?.(document) ?? null : null;
2578
+ const rig = document ? options.documentAdapter.getSceneCameraPreviewRig?.(document) ?? null : null;
2322
2579
  if (!rig) {
2323
2580
  controller.setActive(false);
2324
2581
  return false;
@@ -2365,9 +2622,9 @@ function syncViewportMeasurementState(state) {
2365
2622
  if (measurement)
2366
2623
  state.viewportMeasurement = measurement;
2367
2624
  }
2368
- function hasMainCameraPreviewRig(state, options) {
2625
+ function hasSceneCameraPreviewRig(state, options) {
2369
2626
  const document = state.session?.getState().workingDocument ?? null;
2370
- return !!document && !!options.documentAdapter.getMainCameraPreviewRig?.(document);
2627
+ return !!document && !!options.documentAdapter.getSceneCameraPreviewRig?.(document);
2371
2628
  }
2372
2629
  function createUiState(state, options) {
2373
2630
  const sessionState = state.session?.getState();
@@ -2392,8 +2649,13 @@ function createUiState(state, options) {
2392
2649
  const inspectorMultiObject = document && inspectorMultiObjectBase
2393
2650
  ? withRuntimeInspectorSections(state, options, document, inspectorMultiObjectBase)
2394
2651
  : inspectorMultiObjectBase;
2395
- const assets = dedupeLocalEditorBrowserAssetItems(state.assets
2396
- .map(asset => toBrowserAssetItem(options, asset)));
2652
+ const documentAssetItems = document
2653
+ ? options.documentAdapter.getBrowserAssetItems?.(document) ?? []
2654
+ : [];
2655
+ const assets = dedupeLocalEditorBrowserAssetItems([
2656
+ ...state.assets.map(asset => toBrowserAssetItem(options, asset)),
2657
+ ...documentAssetItems,
2658
+ ]);
2397
2659
  return {
2398
2660
  mode: state.mode,
2399
2661
  busy: state.busy,
@@ -2403,6 +2665,7 @@ function createUiState(state, options) {
2403
2665
  summary: state.summary,
2404
2666
  assetFilter: state.assetFilter,
2405
2667
  assets,
2668
+ selectedAssetId: state.selectedAssetId,
2406
2669
  assetCountLabel: `${assets.length} assets`,
2407
2670
  hierarchy: document ? options.documentAdapter.getHierarchyItems(document) : [],
2408
2671
  selectedIds,
@@ -2415,6 +2678,7 @@ function createUiState(state, options) {
2415
2678
  serializedMultiObject,
2416
2679
  inspectorObject,
2417
2680
  inspectorMultiObject,
2681
+ renderingPanel: document ? options.documentAdapter.getRenderingPanelState?.(document) ?? null : null,
2418
2682
  boxSelection: state.boxSelection,
2419
2683
  coordinateAxes: options.world?.coordinateAxes === false
2420
2684
  ? null
@@ -2439,9 +2703,9 @@ function createUiState(state, options) {
2439
2703
  sceneFrameStats: state.sceneFrameStats
2440
2704
  ? { ...state.sceneFrameStats, activeReasons: [...state.sceneFrameStats.activeReasons] }
2441
2705
  : null,
2442
- mainCameraPreview: {
2443
- enabled: state.mainCameraPreviewEnabled,
2444
- available: hasMainCameraPreviewRig(state, options),
2706
+ sceneCameraPreview: {
2707
+ enabled: state.sceneCameraPreviewEnabled,
2708
+ available: hasSceneCameraPreviewRig(state, options),
2445
2709
  },
2446
2710
  grid: {
2447
2711
  visible: state.gridVisible,
@@ -2473,7 +2737,7 @@ function createSceneViewCoordinateAxesState(state) {
2473
2737
  return null;
2474
2738
  return {
2475
2739
  projectionMode: readSceneViewFreeCameraProjectionMode(state, camera),
2476
- projectionToggleDisabled: state.mainCameraPreviewEnabled,
2740
+ projectionToggleDisabled: state.sceneCameraPreviewEnabled,
2477
2741
  axes: [
2478
2742
  createSceneViewCoordinateAxis('x', 'X', '#ff4b70', { x: 1, y: 0, z: 0 }, right, up, forward),
2479
2743
  createSceneViewCoordinateAxis('y', 'Y', '#75ff42', { x: 0, y: 1, z: 0 }, right, up, forward),
@@ -2755,7 +3019,7 @@ function findAssetByResolvedId(state, options, assetId) {
2755
3019
  selected = { asset, item };
2756
3020
  }
2757
3021
  }
2758
- return selected?.asset;
3022
+ return selected?.asset ?? null;
2759
3023
  }
2760
3024
  function toBrowserAssetItem(options, asset) {
2761
3025
  return options.worldAdapter.toBrowserAssetItem?.(asset)
@@ -2769,6 +3033,8 @@ function toBrowserAssetItem(options, asset) {
2769
3033
  origin: asset.origin,
2770
3034
  dedupeKey: asset.dedupeKey,
2771
3035
  placeable: asset.placeable,
3036
+ preview: asset.preview,
3037
+ material: asset.material,
2772
3038
  meta: asset.meta,
2773
3039
  disabled: asset.disabled ?? asset.placeable === false,
2774
3040
  };